using System.Text.Json; using System.Text.Json.Serialization; using ZB.MOM.WW.OtOpcUa.Core.Hosting; namespace ZB.MOM.WW.OtOpcUa.Driver.S7; /// /// Static factory registration helper for . Server's Program.cs /// calls once at startup; the bootstrapper (task #248) then /// materialises S7 DriverInstance rows from the central config DB into live driver /// instances. Mirrors GalaxyProxyDriverFactoryExtensions. /// public static class S7DriverFactoryExtensions { public const string DriverTypeName = "S7"; /// Registers the S7 driver factory with the registry. /// The driver factory registry. public static void Register(DriverFactoryRegistry registry) { ArgumentNullException.ThrowIfNull(registry); registry.Register(DriverTypeName, CreateInstance); } /// Creates a new S7 driver instance from configuration. /// The unique identifier for the driver instance. /// The JSON configuration for the driver. /// A newly created S7 driver instance. internal static S7Driver CreateInstance(string driverInstanceId, string driverConfigJson) { ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId); ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson); return new S7Driver(ParseOptions(driverInstanceId, driverConfigJson), driverInstanceId); } /// /// Parse a driver-config JSON document into a strongly-typed . /// Shared by the factory (instance creation) and by /// / so a config change delivered through the /// IDriver contract is actually applied — see code-review finding Driver.S7-011. /// /// The unique identifier for the driver instance. /// The JSON configuration for the driver. /// Parsed S7 driver options. internal static S7DriverOptions ParseOptions(string driverInstanceId, string driverConfigJson) { ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId); ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson); var dto = JsonSerializer.Deserialize(driverConfigJson, JsonOptions) ?? throw new InvalidOperationException( $"S7 driver config for '{driverInstanceId}' deserialised to null"); if (string.IsNullOrWhiteSpace(dto.Host)) throw new InvalidOperationException( $"S7 driver config for '{driverInstanceId}' missing required Host"); return new S7DriverOptions { Host = dto.Host!, Port = dto.Port ?? 102, CpuType = ParseEnum(dto.CpuType, driverInstanceId, "CpuType", fallback: S7CpuType.S71500), Rack = dto.Rack ?? 0, Slot = dto.Slot ?? 0, Timeout = TimeSpan.FromMilliseconds(dto.TimeoutMs ?? 5_000), Tags = dto.Tags is { Count: > 0 } ? [.. dto.Tags.Select(t => BuildTag(t, driverInstanceId))] : [], Probe = new S7ProbeOptions { Enabled = dto.Probe?.Enabled ?? true, Interval = TimeSpan.FromMilliseconds(dto.Probe?.IntervalMs ?? 5_000), Timeout = TimeSpan.FromMilliseconds(dto.Probe?.TimeoutMs ?? 2_000), // Driver.S7-012: ProbeAddress removed — probe uses ReadStatusAsync, not a tag read. }, }; } private static S7TagDefinition BuildTag(S7TagDto t, string driverInstanceId) => new( Name: t.Name ?? throw new InvalidOperationException( $"S7 config for '{driverInstanceId}' has a tag missing Name"), Address: t.Address ?? throw new InvalidOperationException( $"S7 tag '{t.Name}' in '{driverInstanceId}' missing Address"), DataType: ParseEnum(t.DataType, driverInstanceId, "DataType", tagName: t.Name), Writable: t.Writable ?? true, StringLength: t.StringLength ?? 254, WriteIdempotent: t.WriteIdempotent ?? false); private static T ParseEnum(string? raw, string driverInstanceId, string field, string? tagName = null, T? fallback = null) where T : struct, Enum { if (string.IsNullOrWhiteSpace(raw)) { if (fallback.HasValue) return fallback.Value; throw new InvalidOperationException( $"S7 tag '{tagName ?? ""}' in '{driverInstanceId}' missing {field}"); } return Enum.TryParse(raw, ignoreCase: true, out var v) ? v : throw new InvalidOperationException( $"S7 {(tagName is null ? "config" : $"tag '{tagName}'")} has unknown {field} '{raw}'. " + $"Expected one of {string.Join(", ", Enum.GetNames())}"); } private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, }; /// Data transfer object for S7 driver configuration. internal sealed class S7DriverConfigDto { /// Gets the PLC host address. public string? Host { get; init; } /// Gets the PLC port. public int? Port { get; init; } /// Gets the CPU type name. public string? CpuType { get; init; } /// Gets the rack number. public short? Rack { get; init; } /// Gets the slot number. public short? Slot { get; init; } /// Gets the connection timeout in milliseconds. public int? TimeoutMs { get; init; } /// Gets the list of tag definitions. public List? Tags { get; init; } /// Gets the probe configuration. public S7ProbeDto? Probe { get; init; } } /// Data transfer object for S7 tag definition. internal sealed class S7TagDto { /// Gets the tag name. public string? Name { get; init; } /// Gets the S7 address (e.g., DB1.DBD0). public string? Address { get; init; } /// Gets the data type name. public string? DataType { get; init; } /// Gets a value indicating whether the tag is writable. public bool? Writable { get; init; } /// Gets the string length for string types. public int? StringLength { get; init; } /// Gets a value indicating whether write is idempotent. public bool? WriteIdempotent { get; init; } } /// Data transfer object for S7 probe configuration. internal sealed class S7ProbeDto { /// Gets a value indicating whether probing is enabled. public bool? Enabled { get; init; } /// Gets the probe interval in milliseconds. public int? IntervalMs { get; init; } /// Gets the probe timeout in milliseconds. public int? TimeoutMs { get; init; } // Driver.S7-012: ProbeAddress removed from the configurable surface — the probe uses // ReadStatusAsync (CPU status), not a tag-address read. Config documents that previously // set probeAddress are safely ignored (unknown JSON fields are tolerated by the deserialiser). } }