namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Import; /// /// Converts a parsed into entries /// ready to be merged into . UDT definitions become /// lists keyed by data-type name; tags whose /// matches a known UDT get those members attached so the /// discovery code can fan out the structure. /// /// /// /// Alias tags are skipped — when is /// non-null the entry is dropped at ingest. Surfacing both the alias + its target /// creates duplicate Variables in the OPC UA address space (Kepware's L5K importer /// takes the same approach for this reason; the alias target is the single source of /// truth for storage). /// /// /// Tags with ExternalAccess := None are skipped — the controller /// actively rejects external reads/writes, so emitting them as Variables would just /// produce permanent BadCommunicationError. Read Only maps to Writable=false; /// Read/Write (or absent) maps to Writable=true. /// /// /// Unknown data-type names (not atomic + not a parsed UDT) fall through as /// with no member layout — discovery can still /// expose them as black-box variables and the operator can pin them via dotted paths. /// /// public sealed class L5kIngest { /// Default device host address applied to every imported tag. public string DefaultDeviceHostAddress { get; init; } = string.Empty; /// /// Optional prefix prepended to imported tag names — useful when ingesting multiple /// L5K exports into one driver instance to avoid name collisions. Default empty. /// public string NamePrefix { get; init; } = string.Empty; public L5kIngestResult Ingest(L5kDocument document) { ArgumentNullException.ThrowIfNull(document); if (string.IsNullOrWhiteSpace(DefaultDeviceHostAddress)) throw new InvalidOperationException( $"{nameof(L5kIngest)}.{nameof(DefaultDeviceHostAddress)} must be set before {nameof(Ingest)} is called — every imported tag needs a target device."); // Index UDT definitions by name so we can fan out structure tags inline. var udtIndex = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var dt in document.DataTypes) { var members = new List(dt.Members.Count); foreach (var m in dt.Members) { var atomic = TryMapAtomic(m.DataType); var memberType = atomic ?? AbCipDataType.Structure; var writable = !IsReadOnly(m.ExternalAccess) && !IsAccessNone(m.ExternalAccess); members.Add(new AbCipStructureMember(m.Name, memberType, writable)); } udtIndex[dt.Name] = members; } var tags = new List(); var skippedAliases = 0; var skippedNoAccess = 0; foreach (var t in document.Tags) { if (!string.IsNullOrEmpty(t.AliasFor)) { skippedAliases++; continue; } if (IsAccessNone(t.ExternalAccess)) { skippedNoAccess++; continue; } var atomic = TryMapAtomic(t.DataType); AbCipDataType dataType; IReadOnlyList? members = null; if (atomic is { } a) { dataType = a; } else { dataType = AbCipDataType.Structure; if (udtIndex.TryGetValue(t.DataType, out var udtMembers)) members = udtMembers; } var tagPath = t.ProgramScope is { Length: > 0 } ? $"Program:{t.ProgramScope}.{t.Name}" : t.Name; var name = string.IsNullOrEmpty(NamePrefix) ? t.Name : $"{NamePrefix}{t.Name}"; // Make the OPC UA tag name unique when both controller-scope + program-scope tags // share the same simple Name. if (t.ProgramScope is { Length: > 0 }) name = string.IsNullOrEmpty(NamePrefix) ? $"{t.ProgramScope}.{t.Name}" : $"{NamePrefix}{t.ProgramScope}.{t.Name}"; var writable = !IsReadOnly(t.ExternalAccess); tags.Add(new AbCipTagDefinition( Name: name, DeviceHostAddress: DefaultDeviceHostAddress, TagPath: tagPath, DataType: dataType, Writable: writable, Members: members)); } return new L5kIngestResult(tags, skippedAliases, skippedNoAccess); } private static bool IsReadOnly(string? externalAccess) => externalAccess is not null && externalAccess.Trim().Replace(" ", string.Empty).Equals("ReadOnly", StringComparison.OrdinalIgnoreCase); private static bool IsAccessNone(string? externalAccess) => externalAccess is not null && externalAccess.Trim().Equals("None", StringComparison.OrdinalIgnoreCase); /// Map a Logix atomic type name. Returns null for UDT/structure references. private static AbCipDataType? TryMapAtomic(string logixType) => logixType?.Trim().ToUpperInvariant() switch { "BOOL" or "BIT" => AbCipDataType.Bool, "SINT" => AbCipDataType.SInt, "INT" => AbCipDataType.Int, "DINT" => AbCipDataType.DInt, "LINT" => AbCipDataType.LInt, "USINT" => AbCipDataType.USInt, "UINT" => AbCipDataType.UInt, "UDINT" => AbCipDataType.UDInt, "ULINT" => AbCipDataType.ULInt, "REAL" => AbCipDataType.Real, "LREAL" => AbCipDataType.LReal, "STRING" => AbCipDataType.String, "DT" or "DATETIME" => AbCipDataType.Dt, _ => null, }; } /// Result of — produced tags + per-skip-reason counts. public sealed record L5kIngestResult( IReadOnlyList Tags, int SkippedAliasCount, int SkippedNoAccessCount);