diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/AbLegacyCommandBase.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/AbLegacyCommandBase.cs index 9f006aaf..49b4dfcd 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/AbLegacyCommandBase.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/AbLegacyCommandBase.cs @@ -1,4 +1,5 @@ using CliFx.Attributes; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies; using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common; @@ -37,7 +38,12 @@ public abstract class AbLegacyCommandBase : DriverCommandBase /// /// Build an with the device + tag list a subclass - /// supplies. Probe disabled for CLI one-shot runs. + /// supplies. The CLI authors typed s from operator flags; + /// v3 delivers tags to the driver as records, so each typed def is + /// serialised back to its TagConfig blob via + /// (RawPath = the def's Name, + /// DeviceName = the def's DeviceHostAddress — the CLI's single device). Probe disabled + /// for CLI one-shot runs. /// /// The tag definitions to include in the options. /// Configured AB Legacy driver options. @@ -47,7 +53,8 @@ public abstract class AbLegacyCommandBase : DriverCommandBase HostAddress: Gateway, PlcFamily: PlcType, DeviceName: $"cli-{PlcType}")], - Tags = tags, + RawTags = [.. tags.Select(t => new RawTagEntry( + t.Name, AbLegacyTagDefinitionFactory.ToTagConfig(t), t.WriteIdempotent, DeviceName: t.DeviceHostAddress))], Timeout = Timeout, Probe = new AbLegacyProbeOptions { Enabled = false }, }; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs index 598fb18b..c9376a09 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; @@ -14,8 +15,15 @@ public sealed class AbLegacyDriverOptions /// Gets or sets the list of PCCC devices to connect to. public IReadOnlyList Devices { get; init; } = []; - /// Gets or sets the list of PCCC tag definitions. - public IReadOnlyList Tags { get; init; } = []; + /// + /// Authored raw tags this driver serves. The deploy artifact hands each authored raw + /// Tag as a (RawPath identity + driver TagConfig blob + + /// WriteIdempotent flag + owning ); the driver maps each + /// through into its RawPath → definition + /// table, routing each to its device by . AbLegacy has no + /// discovery protocol — the driver serves exactly these. + /// + public IReadOnlyList RawTags { get; init; } = []; /// Gets or sets the probe (connectivity check) options. public AbLegacyProbeOptions Probe { get; init; } = new(); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyEquipmentTagParser.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyEquipmentTagParser.cs deleted file mode 100644 index 59d20d7a..00000000 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyEquipmentTagParser.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Text.Json; -using ZB.MOM.WW.OtOpcUa.Core.Abstractions; - -namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; - -/// Parses an equipment tag's TagConfig JSON (the shape authored by the AdminUI -/// AbLegacyTagConfigModel) into a transient whose -/// equals the reference string itself, so a value the -/// driver publishes back keys the runtime's forward router correctly. -public static class AbLegacyEquipmentTagParser -{ - /// Attempts to parse an equipment-tag reference into a transient definition. - /// The equipment tag's TagConfig JSON (also used as the def identity). - /// The transient definition when parsing succeeds. - /// when is an AbLegacy address object. - /// - /// - /// When isArray is the JSON literal but arrayLength is - /// absent, zero, or negative, the result is silently a scalar - /// ( is ). - /// A valid positive arrayLength is required to produce an array tag; isArray:true - /// alone is not sufficient. This is intentional: a stale length behind a cleared or absent - /// isArray flag must never produce an orphan array tag that mismatches its scalar OPC UA - /// node (see in-source comment, review C-2). - /// - /// - public static bool TryParse(string reference, out AbLegacyTagDefinition def) - { - def = null!; - // Authored tag names never start with '{' (AdminUI name validation), so a leading brace marks an equipment-tag TagConfig blob. - if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return false; - try - { - using var doc = JsonDocument.Parse(reference); - var root = doc.RootElement; - if (root.ValueKind != JsonValueKind.Object - || !root.TryGetProperty("address", out var addr) - || addr.ValueKind != JsonValueKind.String) - return false; - var address = addr.GetString(); - if (string.IsNullOrWhiteSpace(address)) return false; - // Strict enum read (R2-11 Phase C): a typo'd dataType rejects the tag (→ BadNodeIdUnknown) - // instead of silently defaulting to a wrong-width Good. - if (!TagConfigJson.TryReadEnumStrict(root, "dataType", AbLegacyDataType.Int, out var dataType)) return false; - var deviceHostAddress = ReadString(root, "deviceHostAddress"); - int? arrayLength = null; - if (IsArrayFlag(root)) - { - var rawLength = ReadInt(root, "arrayLength"); - if (rawLength >= 1) - arrayLength = Math.Min(rawLength, AbLegacyArray.MaxElements); - } - // "writable" defaults to true when absent (today's value) — makes read-only equipment tags - // authorable (UNDER-6). - var writable = TagConfigJson.ReadWritable(root); - def = new AbLegacyTagDefinition( - Name: reference, DeviceHostAddress: deviceHostAddress, Address: address, - DataType: dataType, Writable: writable, ArrayLength: arrayLength); - return true; - } - catch (JsonException) { return false; } - catch (FormatException) { return false; } - catch (InvalidOperationException) { return false; } - } - - /// - /// Deploy-time inspection (05/CONV-2): warns on a present-but-invalid dataType (silently - /// defaulted by the lenient runtime) and on a structurally unparseable TagConfig. Empty when clean - /// or not an equipment-tag object. Never throws. - /// - /// The equipment tag's TagConfig JSON. - /// The warnings; empty when clean. - public static IReadOnlyList Inspect(string reference) - { - var warnings = new List(); - if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return warnings; - try - { - using var doc = JsonDocument.Parse(reference); - var root = doc.RootElement; - if (root.ValueKind != JsonValueKind.Object) - { - warnings.Add("AbLegacy TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown)."); - return warnings; - } - var w = TagConfigJson.DescribeInvalidEnum(root, "dataType"); - if (w is not null) warnings.Add(w); - } - catch (JsonException) - { - warnings.Add("AbLegacy TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown)."); - } - return warnings; - } - - private static string ReadString(JsonElement o, string name) - => o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String - ? e.GetString() ?? "" : ""; - - private static int ReadInt(JsonElement o, string name) - => o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.Number - && e.TryGetInt32(out var v) ? v : 0; - - // True only when the `isArray` property is the JSON literal true. Absent or false → scalar. - private static bool IsArrayFlag(JsonElement o) - => o.TryGetProperty("isArray", out var e) && e.ValueKind == JsonValueKind.True; -} diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyTagDefinitionFactory.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyTagDefinitionFactory.cs new file mode 100644 index 00000000..011be526 --- /dev/null +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyTagDefinitionFactory.cs @@ -0,0 +1,158 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; + +/// +/// v3 pure mapper: turns an authored raw tag's TagConfig JSON (the shape produced by the +/// AdminUI AbLegacyTagConfigModel) into an . Under v3 a +/// tag's identity is its RawPath (a cluster-scoped slash path), not the address blob — so the +/// produced definition's is the RawPath the driver was +/// handed, which is exactly the wire reference the driver's RawPath → def resolver keys on. +/// The driver builds that table by mapping each the deploy artifact +/// delivers through ; is the inverse, used by +/// authoring paths (the Client CLI) that hold a typed definition and need to synthesise the +/// TagConfig blob for a . +/// +/// +/// +/// Device routing is out of band. The AB Legacy driver is multi-device, but a tag's +/// device no longer travels inside the TagConfig blob — the deviceHostAddress key +/// is DROPPED here. The deploy artifact carries the tag's device on the +/// segment instead; the driver threads that name onto the +/// definition (resolving it to a configured device) at table-build time. +/// +/// +public static class AbLegacyTagDefinitionFactory +{ + /// + /// Maps an authored TagConfig object to a typed definition keyed by . + /// The input is always an authored TagConfig JSON object (there is no name-vs-blob heuristic in v3). + /// The dataType enum is read STRICTLY — a present-but-invalid (typo'd) value rejects the whole + /// tag (returns ⇒ the driver surfaces BadNodeIdUnknown) rather than + /// silently defaulting to a wrong-width Good. is + /// NOT read from the blob — it is a platform flag the caller threads in from the tag's + /// . + /// is left empty here — device routing travels on and the driver + /// resolves it at table-build time. + /// + /// The authored equipment-tag TagConfig JSON. + /// The tag's RawPath — becomes the definition's identity (Name). + /// The mapped definition when this returns . + /// when is a valid AbLegacy address object. + /// + /// + /// When isArray is the JSON literal but arrayLength is + /// absent, zero, or negative, the result is silently a scalar + /// ( is ). + /// A valid positive arrayLength is required to produce an array tag; isArray:true + /// alone is not sufficient. This is intentional: a stale length behind a cleared or absent + /// isArray flag must never produce an orphan array tag that mismatches its scalar OPC UA + /// node (see in-source comment, review C-2). + /// + /// + public static bool FromTagConfig(string tagConfig, string rawPath, out AbLegacyTagDefinition def) + { + def = null!; + if (string.IsNullOrWhiteSpace(tagConfig)) return false; + try + { + using var doc = JsonDocument.Parse(tagConfig); + var root = doc.RootElement; + if (root.ValueKind != JsonValueKind.Object + || !root.TryGetProperty("address", out var addr) + || addr.ValueKind != JsonValueKind.String) + return false; + var address = addr.GetString(); + if (string.IsNullOrWhiteSpace(address)) return false; + // Strict enum read (R2-11 Phase C): a typo'd dataType rejects the tag (→ BadNodeIdUnknown) + // instead of silently defaulting to a wrong-width Good. + if (!TagConfigJson.TryReadEnumStrict(root, "dataType", AbLegacyDataType.Int, out var dataType)) return false; + int? arrayLength = null; + if (IsArrayFlag(root)) + { + var rawLength = ReadInt(root, "arrayLength"); + if (rawLength >= 1) + arrayLength = Math.Min(rawLength, AbLegacyArray.MaxElements); + } + // "writable" defaults to true when absent (today's value) — makes read-only equipment tags + // authorable (UNDER-6). + var writable = TagConfigJson.ReadWritable(root); + def = new AbLegacyTagDefinition( + Name: rawPath, DeviceHostAddress: "", Address: address, + DataType: dataType, Writable: writable, WriteIdempotent: false, ArrayLength: arrayLength); + return true; + } + catch (JsonException) { return false; } + catch (FormatException) { return false; } + catch (InvalidOperationException) { return false; } + } + + /// + /// Inverse of : serialises a typed definition back to the camelCase + /// TagConfig JSON the mapper reads. Used by authoring paths (the Client CLI) that construct + /// an from operator flags and need the TagConfig string + /// to hand the driver as a . The enum value is written as its name string; + /// the optional array fields are emitted only for a real array tag. WriteIdempotent and + /// deviceHostAddress are NOT emitted — they travel on the + /// ( / ), not inside + /// the blob. + /// + /// The definition to serialise. + /// The camelCase TagConfig JSON string. + public static string ToTagConfig(AbLegacyTagDefinition def) + { + ArgumentNullException.ThrowIfNull(def); + var o = new JsonObject + { + ["address"] = def.Address, + ["dataType"] = def.DataType.ToString(), + ["writable"] = def.Writable, + }; + if (def.ArrayLength is { } len) + { + o["isArray"] = true; + o["arrayLength"] = len; + } + return o.ToJsonString(); + } + + /// + /// Deploy-time inspection (05/CONV-2): warns on a present-but-invalid dataType (silently + /// defaulted by the lenient runtime) and on a structurally unparseable TagConfig. Empty when clean + /// or not an equipment-tag object. Never throws. + /// + /// The equipment tag's TagConfig JSON. + /// The warnings; empty when clean. + public static IReadOnlyList Inspect(string reference) + { + var warnings = new List(); + if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return warnings; + try + { + using var doc = JsonDocument.Parse(reference); + var root = doc.RootElement; + if (root.ValueKind != JsonValueKind.Object) + { + warnings.Add("AbLegacy TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown)."); + return warnings; + } + var w = TagConfigJson.DescribeInvalidEnum(root, "dataType"); + if (w is not null) warnings.Add(w); + } + catch (JsonException) + { + warnings.Add("AbLegacy TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown)."); + } + return warnings; + } + + private static int ReadInt(JsonElement o, string name) + => o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.Number + && e.TryGetInt32(out var v) ? v : 0; + + // True only when the `isArray` property is the JSON literal true. Absent or false → scalar. + private static bool IsArrayFlag(JsonElement o) + => o.TryGetProperty("isArray", out var e) && e.ValueKind == JsonValueKind.True; +} diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs index 5c7caf65..94add6bd 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs @@ -19,11 +19,13 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover private readonly ILogger _logger; private readonly PollGroupEngine _poll; private readonly Dictionary _devices = new(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _tagsByName = new(StringComparer.OrdinalIgnoreCase); - // Resolves a read/write/subscribe fullReference to a tag definition, bridging the two - // authoring models: an authored tag-table entry (by name) OR an equipment tag whose - // reference is its raw TagConfig JSON (parsed once via AbLegacyEquipmentTagParser, cached). + // v3 authored-tag table, keyed by RawPath (the tag identity + driver wire reference). RawPath is + // case-sensitive ordinal. Built in InitializeAsync from _options.RawTags via the pure mapper. + private readonly Dictionary _tagsByRawPath = new(StringComparer.Ordinal); + + // Resolves a read/write/subscribe RawPath reference to its tag definition via a single hit on the + // authored _tagsByRawPath table (v3: the reference is always a RawPath; a miss is a miss). private readonly EquipmentTagRefResolver _resolver; // volatile: _health is read by GetHealth() on any thread while ReadAsync / WriteAsync / @@ -60,8 +62,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover _tagFactory = tagFactory ?? new LibplctagLegacyTagFactory(); _logger = logger ?? NullLogger.Instance; _resolver = new EquipmentTagRefResolver( - r => _tagsByName.TryGetValue(r, out var t) ? t : null, - r => AbLegacyEquipmentTagParser.TryParse(r, out var d) ? d : null); + r => _tagsByRawPath.TryGetValue(r, out var t) ? t : null); _poll = new PollGroupEngine( reader: ReadAsync, onChange: (handle, tagRef, snapshot) => @@ -107,13 +108,32 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover var profile = AbLegacyPlcFamilyProfile.ForFamily(device.PlcFamily); _devices[device.HostAddress] = new DeviceState(addr, device, profile); } - foreach (var tag in _options.Tags) _tagsByName[tag.Name] = tag; + // Build the RawPath → definition table from the authored raw tags. Each entry's TagConfig + // blob is mapped by the pure factory; the entry's WriteIdempotent flag is threaded onto the + // def (it lives on the RawTagEntry, not inside the blob), and its DeviceName is resolved to + // the owning device's host so the existing DeviceHostAddress-keyed routing continues to work. + // A mapper miss is skipped (logged), never thrown — a bad tag must not fail the whole init. + foreach (var entry in _options.RawTags) + { + if (!AbLegacyTagDefinitionFactory.FromTagConfig(entry.TagConfig, entry.RawPath, out var def)) + { + _logger.LogWarning( + "AbLegacy tag config did not map to a definition; skipping. Driver={DriverInstanceId} RawPath={RawPath}", + _driverInstanceId, entry.RawPath); + continue; + } + _tagsByRawPath[entry.RawPath] = def with + { + WriteIdempotent = entry.WriteIdempotent, + DeviceHostAddress = ResolveDeviceHost(entry.DeviceName), + }; + } // Validate tag types against their device's family profile. Long (32-bit integer) // and String (ST-file) are not supported by all PCCC families; reject them early // so a misconfigured tag fails at init time with a clear message rather than // surfacing an opaque comms error at runtime. - foreach (var tag in _options.Tags) + foreach (var tag in _tagsByRawPath.Values) { if (!_devices.TryGetValue(tag.DeviceHostAddress, out var deviceForTag)) continue; var profile = deviceForTag.Profile; @@ -157,7 +177,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover state.DisposeRuntimes(); } _devices.Clear(); - _tagsByName.Clear(); + _tagsByRawPath.Clear(); _resolver.Clear(); // drop transient equipment-tag parses so a config change re-parses throw; } @@ -183,7 +203,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover state.DisposeRuntimes(); } _devices.Clear(); - _tagsByName.Clear(); + _tagsByRawPath.Clear(); _resolver.Clear(); // drop transient equipment-tag parses so a config change re-parses _health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null); } @@ -415,7 +435,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover { var label = device.DeviceName ?? device.HostAddress; var deviceFolder = root.Folder(device.HostAddress, label); - var tagsForDevice = _options.Tags.Where(t => + var tagsForDevice = _tagsByRawPath.Values.Where(t => string.Equals(t.DeviceHostAddress, device.HostAddress, StringComparison.OrdinalIgnoreCase)); foreach (var tag in tagsForDevice) { @@ -522,6 +542,42 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover new HostStatusChangedEventArgs(state.Options.HostAddress, old, newState)); } + /// + /// Resolve the a raw tag carries to the host-address key of + /// the device it should route to. The AB Legacy driver is multi-device and keys its + /// _devices table by HostAddress, so this maps the tag's owning device name to that + /// key: it matches the name against each configured by its + /// first, then by HostAddress as a fallback, + /// and returns the matched device's HostAddress. When the name matches nothing but the + /// driver has exactly one device, that sole device is used (single-device deployments never need + /// to name their tag's device). Otherwise returns empty — the tag is unroutable and its + /// read/write surfaces BadNodeIdUnknown. + /// + /// + /// TODO(v3 WaveC): the live device host address must ultimately come from the owning + /// Device row's DeviceConfig (resolved by the deploy artifact / bootstrapper) rather + /// than from a configured entry. Until Wave C threads + /// the Device row through, the best available option is to route by matching the delivered + /// against _options.Devices and reusing that entry's + /// HostAddress as the connection key. + /// + /// The owning device name delivered on the . + /// The matched device's HostAddress; empty when unroutable. + private string ResolveDeviceHost(string deviceName) + { + if (!string.IsNullOrEmpty(deviceName)) + { + foreach (var d in _options.Devices) + if (string.Equals(d.DeviceName, deviceName, StringComparison.OrdinalIgnoreCase)) + return d.HostAddress; + foreach (var d in _options.Devices) + if (string.Equals(d.HostAddress, deviceName, StringComparison.OrdinalIgnoreCase)) + return d.HostAddress; + } + // Best-available fallback: a single-device driver routes every tag to its sole device. + return _options.Devices.Count == 1 ? _options.Devices[0].HostAddress : ""; + } + // ---- IPerCallHostResolver ---- /// @@ -744,7 +800,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover state.DisposeRuntimes(); } _devices.Clear(); - _tagsByName.Clear(); + _tagsByRawPath.Clear(); _resolver.Clear(); // drop transient equipment-tag parses so a config change re-parses _health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs index bd96cc71..fdd3dd57 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs @@ -1,6 +1,7 @@ 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; @@ -66,19 +67,11 @@ public static class AbLegacyDriverFactoryExtensions fallback: AbLegacyPlcFamily.Slc500), DeviceName: d.DeviceName))] : [], - Tags = dto.Tags is { Count: > 0 } - ? [.. dto.Tags.Select(t => new AbLegacyTagDefinition( - Name: t.Name ?? throw new InvalidOperationException( - $"AB Legacy config for '{driverInstanceId}' has a tag missing Name"), - DeviceHostAddress: t.DeviceHostAddress ?? throw new InvalidOperationException( - $"AB Legacy tag '{t.Name}' in '{driverInstanceId}' missing DeviceHostAddress"), - Address: t.Address ?? throw new InvalidOperationException( - $"AB Legacy tag '{t.Name}' in '{driverInstanceId}' missing Address"), - DataType: ParseEnum(t.DataType, driverInstanceId, "DataType", - tagName: t.Name), - Writable: t.Writable ?? true, - WriteIdempotent: t.WriteIdempotent ?? false))] - : [], + // 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, @@ -145,9 +138,11 @@ public static class AbLegacyDriverFactoryExtensions public List? Devices { get; init; } /// - /// Gets or sets the list of tags to monitor. + /// 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? Tags { get; init; } + public List? RawTags { get; init; } /// /// Gets or sets the probe configuration. @@ -173,39 +168,6 @@ public static class AbLegacyDriverFactoryExtensions public string? DeviceName { get; init; } } - internal sealed class AbLegacyTagDto - { - /// - /// Gets or sets the tag name. - /// - public string? Name { get; init; } - - /// - /// Gets or sets the device host address. - /// - public string? DeviceHostAddress { get; init; } - - /// - /// Gets or sets the tag address. - /// - public string? Address { get; init; } - - /// - /// Gets or sets the data type. - /// - public string? DataType { get; init; } - - /// - /// Gets or sets whether the tag is writable. - /// - public bool? Writable { get; init; } - - /// - /// Gets or sets whether write is idempotent. - /// - public bool? WriteIdempotent { get; init; } - } - internal sealed class AbLegacyProbeDto { /// diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs index 1256540c..fadeab3a 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs @@ -93,10 +93,7 @@ public sealed class AbLegacyDriverProbe : IDriverProbe // S:0 is present on all PCCC PLCs that support the status file, so a "not found" response // still proves PCCC connectivity; a successful read is the happy path. var tagName = opts.Probe.ProbeAddress - ?? opts.Tags.FirstOrDefault( - t => string.Equals(t.DeviceHostAddress, firstDevice?.HostAddress, StringComparison.OrdinalIgnoreCase)) - ?.Address - ?? opts.Tags.FirstOrDefault()?.Address + ?? FirstProbeableTagAddress(opts, firstDevice) ?? "S:0"; var p = new AbLegacyTagCreateParams( @@ -187,4 +184,30 @@ public sealed class AbLegacyDriverProbe : IDriverProbe var parsed = AbLegacyHostAddress.TryParse(firstDevice.HostAddress); return (parsed, firstDevice); } + + /// + /// Pick a PCCC address to probe from the authored raw tags: prefer a tag owned by the device + /// being probed, else any authored tag. v3 tags carry no typed address — the RawTagEntry's + /// TagConfig blob is mapped through + /// to recover the PCCC address. Returns when no tag maps, so the caller + /// falls back to the status file (S:0). + /// + /// The deserialised driver options. + /// The device being probed (matched by DeviceName / HostAddress). + /// A probeable PCCC address, or . + private static string? FirstProbeableTagAddress(AbLegacyDriverOptions opts, AbLegacyDeviceOptions? firstDevice) + { + static string? Address(RawTagEntry e) => + AbLegacyTagDefinitionFactory.FromTagConfig(e.TagConfig, e.RawPath, out var d) ? d.Address : null; + + foreach (var e in opts.RawTags) + if ((string.Equals(e.DeviceName, firstDevice?.DeviceName, StringComparison.OrdinalIgnoreCase) + || string.Equals(e.DeviceName, firstDevice?.HostAddress, StringComparison.OrdinalIgnoreCase)) + && Address(e) is { } matched) + return matched; + foreach (var e in opts.RawTags) + if (Address(e) is { } any) + return any; + return null; + } } diff --git a/tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests/BuildOptionsTests.cs b/tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests/BuildOptionsTests.cs index e484e36b..0b60a179 100644 --- a/tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests/BuildOptionsTests.cs +++ b/tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests/BuildOptionsTests.cs @@ -94,9 +94,13 @@ public sealed class BuildOptionsTests options.Devices[0].DeviceName.ShouldBe("cli-MicroLogix"); } - /// Verifies that tag list is forwarded verbatim to the options. + /// + /// Verifies that the typed tag list is delivered as v3 records: RawPath = + /// the def's Name, DeviceName = the def's DeviceHostAddress, and the TagConfig blob round-trips the + /// def's address / dataType / writable through the real mapper. + /// [Fact] - public void BuildOptions_forwards_tag_list_verbatim() + public void BuildOptions_delivers_tags_as_rawtag_entries() { var cmd = new TestCommand { @@ -107,7 +111,18 @@ public sealed class BuildOptionsTests var options = cmd.Build(SampleTags); - options.Tags.ShouldBe(SampleTags); + options.RawTags.Count.ShouldBe(SampleTags.Count); + for (var i = 0; i < SampleTags.Count; i++) + { + var def = SampleTags[i]; + var entry = options.RawTags[i]; + entry.RawPath.ShouldBe(def.Name); + entry.DeviceName.ShouldBe(def.DeviceHostAddress); + AbLegacyTagDefinitionFactory.FromTagConfig(entry.TagConfig, entry.RawPath, out var round).ShouldBeTrue(); + round.Address.ShouldBe(def.Address); + round.DataType.ShouldBe(def.DataType); + round.Writable.ShouldBe(def.Writable); + } } /// Verifies that timeout-ms option is propagated to the driver options. @@ -139,7 +154,7 @@ public sealed class BuildOptionsTests var options = cmd.Build([]); - options.Tags.ShouldBeEmpty(); + options.RawTags.ShouldBeEmpty(); options.Devices.Count.ShouldBe(1); options.Devices[0].PlcFamily.ShouldBe(AbLegacyPlcFamily.Plc5); } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyArrayTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyArrayTests.cs index 3123f85f..b01bfbee 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyArrayTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyArrayTests.cs @@ -22,7 +22,7 @@ public sealed class AbLegacyArrayTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = tags, + RawTags = AbLegacyRawTags.Entries(tags), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "ablegacy-array", factory); return (drv, factory); @@ -169,7 +169,7 @@ public sealed class AbLegacyArrayTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("Vector", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 8)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Vector", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 8)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "ablegacy-array", new FakeAbLegacyTagFactory()); await drv.InitializeAsync("{}", CancellationToken.None); @@ -231,30 +231,30 @@ public sealed class AbLegacyArrayTests // ---- Equipment-tag resolver threads arrayLength ---- - /// The equipment-tag parser threads arrayLength into the transient definition. + /// The tag-definition factory threads arrayLength into the definition. [Fact] - public void EquipmentTagParser_threads_arrayLength() + public void TagDefinitionFactory_threads_arrayLength() { - var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":true,"arrayLength":10}"""; - AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue(); + var json = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":10}"""; + AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue(); def!.ArrayLength.ShouldBe(10); } - /// The parser caps arrayLength at the PCCC 256-word file maximum (isArray:true). + /// The factory caps arrayLength at the PCCC 256-word file maximum (isArray:true). [Fact] - public void EquipmentTagParser_caps_arrayLength_at_256() + public void TagDefinitionFactory_caps_arrayLength_at_256() { - var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":true,"arrayLength":99999}"""; - AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue(); + var json = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":99999}"""; + AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue(); def!.ArrayLength.ShouldBe(256); } - /// A scalar equipment tag (no arrayLength) leaves ArrayLength null. + /// A scalar tag (no arrayLength) leaves ArrayLength null. [Fact] - public void EquipmentTagParser_scalar_leaves_arrayLength_null() + public void TagDefinitionFactory_scalar_leaves_arrayLength_null() { - var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int"}"""; - AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue(); + var json = """{"address":"N7:0","dataType":"Int"}"""; + AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue(); def!.ArrayLength.ShouldBeNull(); } @@ -264,19 +264,19 @@ public sealed class AbLegacyArrayTests /// cleared / absent isArray must leave ArrayLength null and never produce an orphan array. /// [Fact] - public void EquipmentTagParser_arrayLength_without_isArray_is_scalar() + public void TagDefinitionFactory_arrayLength_without_isArray_is_scalar() { - var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","arrayLength":8}"""; - AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue(); + var json = """{"address":"N7:0","dataType":"Int","arrayLength":8}"""; + AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue(); def!.ArrayLength.ShouldBeNull(); } /// C-2 regression: isArray:false with a positive arrayLength is a SCALAR. [Fact] - public void EquipmentTagParser_isArray_false_with_arrayLength_is_scalar() + public void TagDefinitionFactory_isArray_false_with_arrayLength_is_scalar() { - var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":false,"arrayLength":8}"""; - AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue(); + var json = """{"address":"N7:0","dataType":"Int","isArray":false,"arrayLength":8}"""; + AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue(); def!.ArrayLength.ShouldBeNull(); } @@ -286,32 +286,33 @@ public sealed class AbLegacyArrayTests /// as a 1-element array. /// [Fact] - public void EquipmentTagParser_isArray_true_arrayLength_one_is_one_element_array() + public void TagDefinitionFactory_isArray_true_arrayLength_one_is_one_element_array() { - var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":true,"arrayLength":1}"""; - AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue(); + var json = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":1}"""; + AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue(); def!.ArrayLength.ShouldBe(1); } /// - /// End-to-end: an AbLegacy driver with NO authored tags reads an equipment-tag ref whose - /// TagConfig carries isArray:true + arrayLength — the resolver threads the - /// count and the read surfaces a typed array, capping the libplctag element count at 256. + /// End-to-end: an AbLegacy driver reads a raw tag authored as a whose + /// TagConfig carries isArray:true + arrayLength — the mapper threads the count and + /// the read surfaces a typed array, capping the libplctag element count at 256. /// [Fact] - public async Task Driver_reads_equipment_array_ref_as_typed_array() + public async Task Driver_reads_array_rawtag_as_typed_array() { - var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":true,"arrayLength":3}"""; + const string rawPath = "cell/ablegacy/dev1/Vec"; + var tagConfig = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":3}"""; var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { ArrayValue = new short[] { 11, 22, 33 } } }; var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [], + RawTags = [new RawTagEntry(rawPath, tagConfig, WriteIdempotent: false, DeviceName: "ab://10.0.0.5/1,0")], Probe = new AbLegacyProbeOptions { Enabled = false }, }, "ablegacy-eq-array", factory); await drv.InitializeAsync("{}", CancellationToken.None); - var r = await drv.ReadAsync([json], CancellationToken.None); + var r = await drv.ReadAsync([rawPath], CancellationToken.None); r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good); var arr = r[0].Value.ShouldBeOfType(); @@ -320,24 +321,25 @@ public sealed class AbLegacyArrayTests } /// - /// C-2 end-to-end: an equipment-tag ref carrying isArray:false, arrayLength:8 reads - /// a single SCALAR value (the parser drops the orphan length), so the read never decodes a + /// C-2 end-to-end: a raw tag whose TagConfig carries isArray:false, arrayLength:8 reads + /// a single SCALAR value (the mapper drops the orphan length), so the read never decodes a /// phantom 8-element array against a scalar node. ElementCount stays 1. /// [Fact] public async Task Driver_reads_isArray_false_with_arrayLength_as_scalar() { - var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int","isArray":false,"arrayLength":8}"""; + const string rawPath = "cell/ablegacy/dev1/Scl"; + var tagConfig = """{"address":"N7:0","dataType":"Int","isArray":false,"arrayLength":8}"""; var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { Value = 99 } }; var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [], + RawTags = [new RawTagEntry(rawPath, tagConfig, WriteIdempotent: false, DeviceName: "ab://10.0.0.5/1,0")], Probe = new AbLegacyProbeOptions { Enabled = false }, }, "ablegacy-eq-scalar", factory); await drv.InitializeAsync("{}", CancellationToken.None); - var r = await drv.ReadAsync([json], CancellationToken.None); + var r = await drv.ReadAsync([rawPath], CancellationToken.None); r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good); r[0].Value.ShouldBe(99); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitIndexRangeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitIndexRangeTests.cs index 3fde086d..321b1e02 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitIndexRangeTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitIndexRangeTests.cs @@ -80,7 +80,7 @@ public sealed class AbLegacyBitIndexRangeTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("LBit20", "ab://10.0.0.5/1,0", "L9:0/20", AbLegacyDataType.Bit)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("LBit20", "ab://10.0.0.5/1,0", "L9:0/20", AbLegacyDataType.Bit)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -105,7 +105,7 @@ public sealed class AbLegacyBitIndexRangeTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("Bit0", "ab://10.0.0.5/1,0", "N7:0/0", AbLegacyDataType.Bit)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Bit0", "ab://10.0.0.5/1,0", "N7:0/0", AbLegacyDataType.Bit)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs index f9941642..53824c40 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs @@ -19,7 +19,7 @@ public sealed class AbLegacyBitRmwTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("Flag3", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Flag3", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -43,7 +43,7 @@ public sealed class AbLegacyBitRmwTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -67,7 +67,7 @@ public sealed class AbLegacyBitRmwTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = tags, + RawTags = AbLegacyRawTags.Entries(tags), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -89,11 +89,11 @@ public sealed class AbLegacyBitRmwTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = + RawTags = AbLegacyRawTags.Entries( [ new AbLegacyTagDefinition("Bit0", "ab://10.0.0.5/1,0", "N7:0/0", AbLegacyDataType.Bit), new AbLegacyTagDefinition("Bit5", "ab://10.0.0.5/1,0", "N7:0/5", AbLegacyDataType.Bit), - ], + ]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -120,7 +120,7 @@ public sealed class AbLegacyBitRmwTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", bitAddr, AbLegacyDataType.Bit)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", bitAddr, AbLegacyDataType.Bit)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -143,7 +143,7 @@ public sealed class AbLegacyBitRmwTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "B3:0/3", AbLegacyDataType.Bit)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "B3:0/3", AbLegacyDataType.Bit)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -167,7 +167,7 @@ public sealed class AbLegacyBitRmwTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = tags, + RawTags = AbLegacyRawTags.Entries(tags), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -195,7 +195,7 @@ public sealed class AbLegacyBitRmwTests var opts = new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("Bit2", "ab://10.0.0.5/1,0", "N7:0/2", AbLegacyDataType.Bit)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Bit2", "ab://10.0.0.5/1,0", "N7:0/2", AbLegacyDataType.Bit)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }; @@ -241,7 +241,7 @@ public sealed class AbLegacyBitRmwTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "I:0/3", AbLegacyDataType.Bit)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("F", "ab://10.0.0.5/1,0", "I:0/3", AbLegacyDataType.Bit)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyCapabilityTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyCapabilityTests.cs index fa4cdcef..99977356 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyCapabilityTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyCapabilityTests.cs @@ -19,11 +19,11 @@ public sealed class AbLegacyCapabilityTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", DeviceName: "Press-SLC-1")], - Tags = + RawTags = AbLegacyRawTags.Entries( [ new AbLegacyTagDefinition("Speed", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int), new AbLegacyTagDefinition("Temperature", "ab://10.0.0.5/1,0", "F8:0", AbLegacyDataType.Float, Writable: false), - ], + ]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); @@ -50,7 +50,7 @@ public sealed class AbLegacyCapabilityTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -75,7 +75,7 @@ public sealed class AbLegacyCapabilityTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -190,11 +190,11 @@ public sealed class AbLegacyCapabilityTests new AbLegacyDeviceOptions("ab://10.0.0.5/1,0"), new AbLegacyDeviceOptions("ab://10.0.0.6/1,0"), ], - Tags = + RawTags = AbLegacyRawTags.Entries( [ new AbLegacyTagDefinition("A", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int), new AbLegacyTagDefinition("B", "ab://10.0.0.6/1,0", "N7:0", AbLegacyDataType.Int), - ], + ]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyDisposeAndResolveHostTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyDisposeAndResolveHostTests.cs index 1037ab08..c629e285 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyDisposeAndResolveHostTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyDisposeAndResolveHostTests.cs @@ -29,7 +29,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", AbLegacyPlcFamily.Slc500)], - Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-dispose", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -72,7 +72,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -135,7 +135,7 @@ public sealed class AbLegacyDisposeAndResolveHostTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]), }, "drv-1"); drv.ResolveHost("X").ShouldBe("ab://10.0.0.5/1,0"); } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyDriverTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyDriverTests.cs index ee0266ab..16a38802 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyDriverTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyDriverTests.cs @@ -134,7 +134,7 @@ public sealed class AbLegacyDriverTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/", AbLegacyPlcFamily.Slc500)], - Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/", "N7:0", AbLegacyDataType.Int)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/", "N7:0", AbLegacyDataType.Int)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -154,7 +154,7 @@ public sealed class AbLegacyDriverTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,2", AbLegacyPlcFamily.Slc500)], - Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,2", "N7:0", AbLegacyDataType.Int)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,2", "N7:0", AbLegacyDataType.Int)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -171,7 +171,7 @@ public sealed class AbLegacyDriverTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/", AbLegacyPlcFamily.MicroLogix)], - Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/", "L9:0", AbLegacyDataType.Long)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/", "L9:0", AbLegacyDataType.Long)]), }, "drv-1"); var ex = await Should.ThrowAsync( @@ -188,7 +188,7 @@ public sealed class AbLegacyDriverTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", AbLegacyPlcFamily.Slc500)], - Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "L9:0", AbLegacyDataType.Long)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "L9:0", AbLegacyDataType.Long)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1"); @@ -209,7 +209,7 @@ public sealed class AbLegacyDriverTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", AbLegacyPlcFamily.Plc5)], - Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "L9:0", AbLegacyDataType.Long)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "L9:0", AbLegacyDataType.Long)]), }, "drv-1"); var ex = await Should.ThrowAsync( diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEquipmentTagParserStrictnessTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEquipmentTagParserStrictnessTests.cs index 67f4dfbd..d0464854 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEquipmentTagParserStrictnessTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEquipmentTagParserStrictnessTests.cs @@ -4,30 +4,34 @@ using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; -/// R2-11 Phase C strictness surface for the AbLegacy equipment-tag parser: the runtime is now STRICT — -/// a typo'd dataType rejects the tag (TryParseBadNodeIdUnknown), -/// Inspect reports the typo, and the writable key is honoured (default true). +/// R2-11 Phase C strictness surface for the AbLegacy tag-definition factory: the runtime is now STRICT — +/// a typo'd dataType rejects the tag (FromTagConfigBadNodeIdUnknown), +/// Inspect reports the typo, and the writable key is honoured (default true). Under v3 the mapped +/// definition's Name is the RawPath the factory was handed, not the blob. public sealed class AbLegacyEquipmentTagParserStrictnessTests { + private const string RawPath = "cell/ablegacy/dev1/T1"; + [Fact] public void Typo_dataType_rejects_the_tag() { - AbLegacyEquipmentTagParser.TryParse("{\"address\":\"N7:0\",\"dataType\":\"Intt\"}", out _) + AbLegacyTagDefinitionFactory.FromTagConfig("{\"address\":\"N7:0\",\"dataType\":\"Intt\"}", RawPath, out _) .ShouldBeFalse(); } [Fact] public void Valid_dataType_still_parses() { - AbLegacyEquipmentTagParser.TryParse("{\"address\":\"N7:0\",\"dataType\":\"Int\"}", out var def) + AbLegacyTagDefinitionFactory.FromTagConfig("{\"address\":\"N7:0\",\"dataType\":\"Int\"}", RawPath, out var def) .ShouldBeTrue(); def.DataType.ShouldBe(AbLegacyDataType.Int); + def.Name.ShouldBe(RawPath); } [Fact] public void Inspect_reports_typo_dataType() { - var warnings = AbLegacyEquipmentTagParser.Inspect("{\"address\":\"N7:0\",\"dataType\":\"Intt\"}"); + var warnings = AbLegacyTagDefinitionFactory.Inspect("{\"address\":\"N7:0\",\"dataType\":\"Intt\"}"); warnings.ShouldHaveSingleItem(); warnings[0].ShouldContain("Intt"); warnings[0].ShouldContain("dataType"); @@ -36,13 +40,13 @@ public sealed class AbLegacyEquipmentTagParserStrictnessTests [Fact] public void Inspect_clean_config_has_no_warnings() { - AbLegacyEquipmentTagParser.Inspect("{\"address\":\"N7:0\",\"dataType\":\"Int\"}").ShouldBeEmpty(); + AbLegacyTagDefinitionFactory.Inspect("{\"address\":\"N7:0\",\"dataType\":\"Int\"}").ShouldBeEmpty(); } [Fact] public void Writable_false_is_honoured() { - AbLegacyEquipmentTagParser.TryParse("{\"address\":\"N7:0\",\"dataType\":\"Int\",\"writable\":false}", out var def) + AbLegacyTagDefinitionFactory.FromTagConfig("{\"address\":\"N7:0\",\"dataType\":\"Int\",\"writable\":false}", RawPath, out var def) .ShouldBeTrue(); def.Writable.ShouldBeFalse(); } @@ -50,7 +54,7 @@ public sealed class AbLegacyEquipmentTagParserStrictnessTests [Fact] public void Writable_absent_defaults_to_true() { - AbLegacyEquipmentTagParser.TryParse("{\"address\":\"N7:0\",\"dataType\":\"Int\"}", out var def) + AbLegacyTagDefinitionFactory.FromTagConfig("{\"address\":\"N7:0\",\"dataType\":\"Int\"}", RawPath, out var def) .ShouldBeTrue(); def.Writable.ShouldBeTrue(); } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEquipmentTagTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEquipmentTagTests.cs index 5433cdef..dba23a33 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEquipmentTagTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEquipmentTagTests.cs @@ -8,34 +8,38 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; [Trait("Category", "Unit")] public sealed class AbLegacyEquipmentTagTests { + private const string RawPath = "cell/ablegacy/dev1/Counter"; + [Fact] - public void Parses_equipment_tagconfig_into_a_transient_definition() + public void Maps_tagconfig_into_a_definition_keyed_by_rawpath() { + // v3: the deviceHostAddress key is dropped by the mapper (device routing travels on the + // RawTagEntry's DeviceName). The mapped definition is keyed by the RawPath, not the blob. var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int"}"""; - AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue(); - def!.Name.ShouldBe(json); + AbLegacyTagDefinitionFactory.FromTagConfig(json, RawPath, out var def).ShouldBeTrue(); + def!.Name.ShouldBe(RawPath); def.Address.ShouldBe("N7:0"); def.DataType.ShouldBe(AbLegacyDataType.Int); - def.DeviceHostAddress.ShouldBe("ab://10.0.0.5/1,0"); + def.DeviceHostAddress.ShouldBe(""); // dropped — resolved from RawTagEntry.DeviceName at table-build time } [Fact] public void Rejects_a_non_address_blob() - => AbLegacyEquipmentTagParser.TryParse("""{"FullName":"x"}""", out _).ShouldBeFalse(); + => AbLegacyTagDefinitionFactory.FromTagConfig("""{"FullName":"x"}""", RawPath, out _).ShouldBeFalse(); [Fact] public void Rejects_garbage() - => AbLegacyEquipmentTagParser.TryParse("not json", out _).ShouldBeFalse(); + => AbLegacyTagDefinitionFactory.FromTagConfig("not json", RawPath, out _).ShouldBeFalse(); [Fact] public void Rejects_address_as_a_json_number() - => AbLegacyEquipmentTagParser.TryParse( - """{"address":7,"dataType":"Int"}""", out _).ShouldBeFalse(); + => AbLegacyTagDefinitionFactory.FromTagConfig( + """{"address":7,"dataType":"Int"}""", RawPath, out _).ShouldBeFalse(); [Fact] public void Rejects_empty_address() - => AbLegacyEquipmentTagParser.TryParse( - """{"address":"","dataType":"Int"}""", out _).ShouldBeFalse(); + => AbLegacyTagDefinitionFactory.FromTagConfig( + """{"address":"","dataType":"Int"}""", RawPath, out _).ShouldBeFalse(); // -002 regression: isArray:true without a valid positive arrayLength → scalar (null ArrayLength). @@ -43,7 +47,7 @@ public sealed class AbLegacyEquipmentTagTests public void IsArray_true_with_arrayLength_zero_produces_scalar() { var json = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":0}"""; - AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue(); + AbLegacyTagDefinitionFactory.FromTagConfig(json, RawPath, out var def).ShouldBeTrue(); def!.ArrayLength.ShouldBeNull(); } @@ -51,7 +55,7 @@ public sealed class AbLegacyEquipmentTagTests public void IsArray_true_with_no_arrayLength_produces_scalar() { var json = """{"address":"N7:0","dataType":"Int","isArray":true}"""; - AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue(); + AbLegacyTagDefinitionFactory.FromTagConfig(json, RawPath, out var def).ShouldBeTrue(); def!.ArrayLength.ShouldBeNull(); } @@ -59,28 +63,30 @@ public sealed class AbLegacyEquipmentTagTests public void IsArray_true_with_negative_arrayLength_produces_scalar() { var json = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":-5}"""; - AbLegacyEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue(); + AbLegacyTagDefinitionFactory.FromTagConfig(json, RawPath, out var def).ShouldBeTrue(); def!.ArrayLength.ShouldBeNull(); } /// - /// End-to-end driver-level proof: an AbLegacy driver with NO authored tags can still read an - /// equipment-tag ref (the raw TagConfig JSON) — the resolver parses it into a transient - /// definition and the read goes to the fake runtime instead of returning BadNodeIdUnknown. + /// End-to-end driver-level proof: a raw tag authored as a (RawPath + /// identity + TagConfig blob) is resolved by the driver's RawPath → def table and read against the + /// fake runtime, returning Good instead of BadNodeIdUnknown. Under v3 the read reference is always + /// a RawPath — the driver no longer parses a raw TagConfig blob handed as the reference. /// [Fact] - public async Task Driver_resolves_an_equipment_ref_and_reads_instead_of_BadNodeIdUnknown() + public async Task Driver_resolves_a_rawpath_ref_and_reads_instead_of_BadNodeIdUnknown() { - var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","address":"N7:0","dataType":"Int"}"""; + var tagConfig = """{"address":"N7:0","dataType":"Int"}"""; var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { Value = 4242 } }; var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [], + RawTags = [new RawTagEntry(RawPath, tagConfig, WriteIdempotent: false, DeviceName: "ab://10.0.0.5/1,0")], + Probe = new AbLegacyProbeOptions { Enabled = false }, }, "ablegacy-eq", factory); await drv.InitializeAsync("{}", CancellationToken.None); - var r = await drv.ReadAsync([json], CancellationToken.None); + var r = await drv.ReadAsync([RawPath], CancellationToken.None); r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good); r[0].StatusCode.ShouldNotBe(AbLegacyStatusMapper.BadNodeIdUnknown); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEvictOnFailureTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEvictOnFailureTests.cs index fbc2850c..428c1144 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEvictOnFailureTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEvictOnFailureTests.cs @@ -25,7 +25,7 @@ public sealed class AbLegacyEvictOnFailureTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions(Device)], - Tags = tags, + RawTags = AbLegacyRawTags.Entries(tags), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); return (drv, factory); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyLoggerInjectionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyLoggerInjectionTests.cs index 2167da4d..af28965a 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyLoggerInjectionTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyLoggerInjectionTests.cs @@ -79,7 +79,7 @@ public sealed class AbLegacyLoggerInjectionTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-logged", factory, logger); await drv.InitializeAsync("{}", CancellationToken.None); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyRawTags.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyRawTags.cs new file mode 100644 index 00000000..86d2b2c6 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyRawTags.cs @@ -0,0 +1,31 @@ +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; + +/// +/// Test helper bridging the v2 authoring shape (a typed ) to the v3 +/// delivery shape (). Under v3 the driver no longer takes pre-declared +/// definitions — the deploy artifact hands it authored raw tags (RawPath + TagConfig blob + owning +/// DeviceName), and the driver rebuilds the typed definitions via +/// . These tests keep expressing their intent as typed +/// definitions; this helper serialises each back to its TagConfig blob (via +/// ) and packages it as a +/// whose RawPath is the def's Name and whose is the def's +/// DeviceHostAddress — so the round-trip through the real mapper reproduces the same definition +/// the test authored, keyed by the same name the read/write/subscribe call sites use and routed to the +/// same device (the driver matches DeviceName against a configured device's Name or HostAddress). +/// +internal static class AbLegacyRawTags +{ + /// Serialises one typed definition into its (RawPath = def.Name, DeviceName = def.DeviceHostAddress). + /// The typed definition to convert. + /// The equivalent . + public static RawTagEntry Entry(AbLegacyTagDefinition def) + => new(def.Name, AbLegacyTagDefinitionFactory.ToTagConfig(def), def.WriteIdempotent, DeviceName: def.DeviceHostAddress); + + /// Serialises typed definitions into records. + /// The typed definitions to convert (array, params, or a collection expression). + /// The equivalent raw-tag entries. + public static IReadOnlyList Entries(params AbLegacyTagDefinition[] defs) + => [.. defs.Select(Entry)]; +} diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs index 2c7e9856..45d36552 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs @@ -15,7 +15,7 @@ public sealed class AbLegacyReadWriteTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = tags, + RawTags = AbLegacyRawTags.Entries(tags), }, "drv-1", factory); return (drv, factory); } @@ -179,7 +179,7 @@ public sealed class AbLegacyReadWriteTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("Bit3", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Bit3", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)]), }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -206,7 +206,7 @@ public sealed class AbLegacyReadWriteTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("Flag", "ab://10.0.0.5/1,0", "B3:0", AbLegacyDataType.Bit)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Flag", "ab://10.0.0.5/1,0", "B3:0", AbLegacyDataType.Bit)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -245,11 +245,11 @@ public sealed class AbLegacyReadWriteTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = + RawTags = AbLegacyRawTags.Entries( [ new AbLegacyTagDefinition("A", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int), new AbLegacyTagDefinition("B", "ab://10.0.0.5/1,0", "N7:1", AbLegacyDataType.Int, Writable: false), - ], + ]), }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyResolveHostTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyResolveHostTests.cs index 52109f9d..7189db65 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyResolveHostTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyResolveHostTests.cs @@ -1,13 +1,15 @@ using Shouldly; using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; /// -/// CONV-4 — must resolve an equipment-tag reference -/// (raw TagConfig JSON) to its OWN device host so per-host breaker keys are correct for -/// equipment tags, not the first-device fallback. +/// CONV-4 — must resolve a RawPath reference to its OWN +/// device host so per-host breaker keys are correct for multi-device tags, not the first-device +/// fallback. v3: the tag's device travels on the the deploy +/// artifact delivers; the driver resolves it to that device's host at table-build time. /// [Trait("Category", "Unit")] public sealed class AbLegacyResolveHostTests @@ -15,21 +17,25 @@ public sealed class AbLegacyResolveHostTests private const string DeviceA = "ab://10.0.0.5/1,0"; private const string DeviceB = "ab://10.0.0.6/1,0"; - private static AbLegacyDriver NewDriver() => new( + private static AbLegacyDriver NewDriver(params RawTagEntry[] rawTags) => new( new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions(DeviceA), new AbLegacyDeviceOptions(DeviceB)], + RawTags = rawTags, Probe = new AbLegacyProbeOptions { Enabled = false }, }, "ablegacy-1", new FakeAbLegacyTagFactory()); - /// An equipment-tag ref naming device B resolves to device B's host, not the first device. + /// A raw tag owned by device B resolves to device B's host, not the first device. [Fact] - public void ResolveHost_EquipmentTagRef_ReturnsItsOwnDeviceHost() + public async Task ResolveHost_RawTagRef_ReturnsItsOwnDeviceHost() { - var drv = NewDriver(); - var json = $$"""{"deviceHostAddress":"{{DeviceB}}","address":"N7:0","dataType":"Int"}"""; - drv.ResolveHost(json).ShouldBe(DeviceB); + const string rawPath = "cell/ablegacy/devB/T1"; + var drv = NewDriver(new RawTagEntry( + rawPath, """{"address":"N7:0","dataType":"Int"}""", WriteIdempotent: false, DeviceName: DeviceB)); + await drv.InitializeAsync("{}", CancellationToken.None); + + drv.ResolveHost(rawPath).ShouldBe(DeviceB); } /// An unresolvable ref keeps the current first-device fallback. diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyRuntimeConcurrencyTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyRuntimeConcurrencyTests.cs index e48c81d5..9b423904 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyRuntimeConcurrencyTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyRuntimeConcurrencyTests.cs @@ -84,7 +84,7 @@ public sealed class AbLegacyRuntimeConcurrencyTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); @@ -117,7 +117,7 @@ public sealed class AbLegacyRuntimeConcurrencyTests var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], - Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)], + RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]), Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None);