using System.Text.Json; using ZB.MOM.WW.OtOpcUa.Core.Hosting; namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; /// /// Static factory registration helper for . Server's Program.cs /// calls once at startup; the bootstrapper materialises TwinCAT /// DriverInstance rows from the central config DB into live driver instances. Mirrors /// S7DriverFactoryExtensions / AbCipDriverFactoryExtensions. /// public static class TwinCATDriverFactoryExtensions { public const string DriverTypeName = "TwinCAT"; public static void Register(DriverFactoryRegistry registry) { ArgumentNullException.ThrowIfNull(registry); registry.Register(DriverTypeName, CreateInstance); } internal static TwinCATDriver CreateInstance(string driverInstanceId, string driverConfigJson) { ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId); var options = ParseOptions(driverConfigJson, driverInstanceId); return new TwinCATDriver(options, driverInstanceId); } /// /// Parse a TwinCAT driver-config JSON document into a . /// Shared by (constructor-time) and /// / /// so a config generation pushed via Reinitialize is actually applied (Driver.TwinCAT-001). /// internal static TwinCATDriverOptions ParseOptions(string driverConfigJson, string driverInstanceId) { ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson); var dto = JsonSerializer.Deserialize(driverConfigJson, JsonOptions) ?? throw new InvalidOperationException( $"TwinCAT driver config for '{driverInstanceId}' deserialised to null"); return new TwinCATDriverOptions { Devices = dto.Devices is { Count: > 0 } ? [.. dto.Devices.Select(d => new TwinCATDeviceOptions( HostAddress: d.HostAddress ?? throw new InvalidOperationException( $"TwinCAT config for '{driverInstanceId}' has a device missing HostAddress"), DeviceName: d.DeviceName))] : [], Tags = dto.Tags is { Count: > 0 } ? [.. dto.Tags.Select(t => BuildTag(t, driverInstanceId))] : [], Probe = new TwinCATProbeOptions { Enabled = dto.Probe?.Enabled ?? true, Interval = TimeSpan.FromMilliseconds(dto.Probe?.IntervalMs ?? 5_000), Timeout = TimeSpan.FromMilliseconds(dto.Probe?.TimeoutMs ?? 2_000), }, Timeout = TimeSpan.FromMilliseconds(dto.TimeoutMs ?? 2_000), UseNativeNotifications = dto.UseNativeNotifications ?? true, EnableControllerBrowse = dto.EnableControllerBrowse ?? false, NotificationMaxDelayMs = dto.NotificationMaxDelayMs ?? 0, }; } /// /// Test-visible wrapper around for the regression suite — /// keeps the public driver surface unchanged while letting tests assert that JSON /// fields like NotificationMaxDelayMs and Structure-tag rejection are /// honored end-to-end. /// public static TwinCATDriverOptions ParseOptionsForTests(string driverConfigJson, string driverInstanceId) => ParseOptions(driverConfigJson, driverInstanceId); private static TwinCATTagDefinition BuildTag(TwinCATTagDto t, string driverInstanceId) { var dataType = ParseEnum(t.DataType, t.Name, driverInstanceId, "DataType"); // Driver.TwinCAT-003: Structure-typed pre-declared tags are not supported. The driver's // atomic surface cannot read/write UDT blobs — MapToClrType falls through to typeof(int) // and ConvertForWrite throws NotSupportedException, producing garbage reads or late // runtime failures. BrowseSymbolsAsync already correctly yields DataType = null for // Structure symbols so they never appear in the discovered address space. Reject here // with a clear error so operators get a configuration-time failure, not a silent wrong value. if (dataType == TwinCATDataType.Structure) throw new InvalidOperationException( $"TwinCAT tag '{t.Name ?? ""}' in '{driverInstanceId}' specifies " + "DataType 'Structure'. The driver does not support UDT/FB-instance pre-declared tags. " + "Use EnableControllerBrowse to discover UDT members individually, or declare " + "individual atomic-typed tags for the fields you need."); return new TwinCATTagDefinition( Name: t.Name ?? throw new InvalidOperationException( $"TwinCAT config for '{driverInstanceId}' has a tag missing Name"), DeviceHostAddress: t.DeviceHostAddress ?? throw new InvalidOperationException( $"TwinCAT tag '{t.Name}' in '{driverInstanceId}' missing DeviceHostAddress"), SymbolPath: t.SymbolPath ?? throw new InvalidOperationException( $"TwinCAT tag '{t.Name}' in '{driverInstanceId}' missing SymbolPath"), DataType: dataType, Writable: t.Writable ?? true, WriteIdempotent: t.WriteIdempotent ?? false); } private static T ParseEnum(string? raw, string? tagName, string driverInstanceId, string field) where T : struct, Enum { if (string.IsNullOrWhiteSpace(raw)) throw new InvalidOperationException( $"TwinCAT tag '{tagName ?? ""}' in '{driverInstanceId}' missing {field}"); return Enum.TryParse(raw, ignoreCase: true, out var v) ? v : throw new InvalidOperationException( $"TwinCAT 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 TwinCATDriverConfigDto { public int? TimeoutMs { get; init; } public bool? UseNativeNotifications { get; init; } public bool? EnableControllerBrowse { get; init; } public int? NotificationMaxDelayMs { get; init; } public List? Devices { get; init; } public List? Tags { get; init; } public TwinCATProbeDto? Probe { get; init; } } internal sealed class TwinCATDeviceDto { public string? HostAddress { get; init; } public string? DeviceName { get; init; } } internal sealed class TwinCATTagDto { public string? Name { get; init; } public string? DeviceHostAddress { get; init; } public string? SymbolPath { get; init; } public string? DataType { get; init; } public bool? Writable { get; init; } public bool? WriteIdempotent { get; init; } } internal sealed class TwinCATProbeDto { public bool? Enabled { get; init; } public int? IntervalMs { get; init; } public int? TimeoutMs { get; init; } } }