using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Hosting; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; /// /// Static factory registration helper for . Server's Program.cs /// calls once at startup; the bootstrapper then /// materialises AB Legacy DriverInstance rows from the central config DB into live /// driver instances. Mirrors GalaxyProxyDriverFactoryExtensions. /// public static class AbLegacyDriverFactoryExtensions { public const string DriverTypeName = "AbLegacy"; /// /// Register the AbLegacy factory with the driver registry. The optional /// is captured at registration time and used to /// construct an per driver instance — without it, /// the driver runs with the null logger (existing tests and standalone callers stay /// unchanged). Mirrors the Modbus driver registration pattern. /// /// The driver factory registry to register with. /// Optional logger factory for driver instances. public static void Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory = null) { ArgumentNullException.ThrowIfNull(registry); registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory)); } /// /// Creates an instance of the AB Legacy driver from configuration. /// /// The unique identifier for this driver instance. /// The driver configuration as a JSON string. /// A configured instance. internal static AbLegacyDriver CreateInstance(string driverInstanceId, string driverConfigJson) => CreateInstance(driverInstanceId, driverConfigJson, loggerFactory: null); /// /// Creates an instance of the AB Legacy driver with optional logger. /// /// The unique identifier for this driver instance. /// The driver configuration as a JSON string. /// Optional logger factory for the driver instance. /// A configured instance. internal static AbLegacyDriver CreateInstance(string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory) { ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId); ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson); var dto = JsonSerializer.Deserialize(driverConfigJson, JsonOptions) ?? throw new InvalidOperationException( $"AB Legacy driver config for '{driverInstanceId}' deserialised to null"); var options = new AbLegacyDriverOptions { Devices = dto.Devices is { Count: > 0 } ? [.. dto.Devices.Select(d => new AbLegacyDeviceOptions( HostAddress: d.HostAddress ?? throw new InvalidOperationException( $"AB Legacy config for '{driverInstanceId}' has a device missing HostAddress"), PlcFamily: ParseEnum(d.PlcFamily, driverInstanceId, "PlcFamily", fallback: AbLegacyPlcFamily.Slc500), DeviceName: d.DeviceName))] : [], // v3: the deploy artifact (Wave C) populates rawTags with the cluster's authored raw AbLegacy // tags (RawPath + TagConfig blob + WriteIdempotent + owning DeviceName). The driver maps each // RawTagEntry's TagConfig blob through AbLegacyTagDefinitionFactory at Initialize and routes it // to its device by DeviceName; the legacy pre-declared DTO tag path is retired. RawTags = dto.RawTags is { Count: > 0 } ? [.. dto.RawTags] : [], Probe = new AbLegacyProbeOptions { Enabled = dto.Probe?.Enabled ?? true, Interval = TimeSpan.FromMilliseconds(dto.Probe?.IntervalMs ?? 5_000), Timeout = PositiveTimeoutOrDefault(dto.Probe?.TimeoutMs, 2_000), ProbeAddress = dto.Probe?.ProbeAddress ?? "S:0", }, Timeout = PositiveTimeoutOrDefault(dto.TimeoutMs, 2_000), }; return new AbLegacyDriver( options, driverInstanceId, tagFactory: null, logger: loggerFactory?.CreateLogger()); } /// /// Maps an operator-supplied timeout (ms) to a positive , substituting /// for a null / zero / negative value. libplctag's managed /// Tag.Timeout setter throws for a /// non-positive value, which would otherwise fault tag creation on every read/write; clamping /// here keeps a misconfigured TimeoutMs: 0 running on the default bound instead. (Driver /// timeout hardening, sibling of the S7 R2-01 read-leg fix.) /// /// The operator-supplied timeout in milliseconds, or null. /// The default applied when is null or non-positive. /// A strictly positive timeout. private static TimeSpan PositiveTimeoutOrDefault(int? ms, int defaultMs) => TimeSpan.FromMilliseconds(ms is int v and > 0 ? v : defaultMs); 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( $"AB Legacy {(tagName is null ? "config" : $"tag '{tagName}'")} in '{driverInstanceId}' missing {field}"); } return Enum.TryParse(raw, ignoreCase: true, out var v) ? v : throw new InvalidOperationException( $"AB Legacy {(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, }; internal sealed class AbLegacyDriverConfigDto { /// /// Gets or sets the timeout in milliseconds for operations. /// public int? TimeoutMs { get; init; } /// /// Gets or sets the list of devices to connect to. /// public List? Devices { get; init; } /// /// Gets or sets the authored raw tags (RawPath + TagConfig blob + WriteIdempotent + owning /// DeviceName) the deploy artifact delivers. The driver maps each through /// at Initialize. /// public List? RawTags { get; init; } /// /// Gets or sets the probe configuration. /// public AbLegacyProbeDto? Probe { get; init; } } internal sealed class AbLegacyDeviceDto { /// /// Gets or sets the host address of the device. /// public string? HostAddress { get; init; } /// /// Gets or sets the PLC family. /// public string? PlcFamily { get; init; } /// /// Gets or sets the device name. /// public string? DeviceName { get; init; } } internal sealed class AbLegacyProbeDto { /// /// Gets or sets whether probing is enabled. /// public bool? Enabled { get; init; } /// /// Gets or sets the probe interval in milliseconds. /// public int? IntervalMs { get; init; } /// /// Gets or sets the probe timeout in milliseconds. /// public int? TimeoutMs { get; init; } /// /// Gets or sets the probe address. /// public string? ProbeAddress { get; init; } } }