using System.Text.Json; using System.Text.Json.Serialization; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; 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 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. /// /// 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), // v3: the deploy artifact (Wave C) populates rawTags with the cluster's authored raw S7 // tags. The driver maps each RawTagEntry's TagConfig blob through S7TagDefinitionFactory // at Initialize; the legacy pre-declared DTO tag path (Name/Address → BuildTag) is retired. RawTags = dto.RawTags is { Count: > 0 } ? [.. dto.RawTags] : [], Probe = new S7ProbeOptions { Enabled = dto.Probe?.Enabled ?? true, Interval = TimeSpan.FromMilliseconds(dto.Probe?.IntervalMs ?? 5_000), Timeout = TimeSpan.FromMilliseconds(dto.Probe?.TimeoutMs ?? 2_000), // ProbeAddress removed — probe uses ReadStatusAsync, not a tag read. }, }; } 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 authored raw tags (RawPath + TagConfig blob + WriteIdempotent) the deploy /// artifact delivers. The driver maps each through at Initialize. public List? RawTags { get; init; } /// Gets the probe configuration. public S7ProbeDto? Probe { 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; } // 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). } }