diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/AbCipCommandBase.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/AbCipCommandBase.cs
index 5631d413..c69d04b1 100644
--- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/AbCipCommandBase.cs
+++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/AbCipCommandBase.cs
@@ -1,4 +1,5 @@
using CliFx.Attributes;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli;
@@ -48,7 +49,10 @@ public abstract class AbCipCommandBase : DriverCommandBase
///
/// Build an with the device + tag list a subclass
/// supplies. Probe + alarm projection are disabled — CLI runs are one-shot; the
- /// probe loop would race the operator's own reads.
+ /// probe loop would race the operator's own reads. Each typed tag is serialised to the v3
+ /// shape (RawPath = tag name, TagConfig blob via
+ /// , DeviceName = the tag's device routing key)
+ /// — the same seam the deploy artifact uses.
///
/// The list of tag definitions to include in the options.
/// The constructed .
@@ -58,7 +62,11 @@ public abstract class AbCipCommandBase : DriverCommandBase
HostAddress: Gateway,
PlcFamily: Family,
DeviceName: $"cli-{Family}")],
- Tags = tags,
+ RawTags = [.. tags.Select(t => new RawTagEntry(
+ RawPath: t.Name,
+ TagConfig: AbCipTagDefinitionFactory.ToTagConfig(t),
+ WriteIdempotent: t.WriteIdempotent,
+ DeviceName: t.DeviceHostAddress))],
Timeout = Timeout,
Probe = new AbCipProbeOptions { Enabled = false },
EnableControllerBrowse = false,
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipDriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipDriverOptions.cs
index 775af04d..7c949862 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipDriverOptions.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipDriverOptions.cs
@@ -1,10 +1,12 @@
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
///
/// AB CIP / EtherNet-IP driver configuration, bound from the driver's DriverConfig
/// JSON at DriverHost.RegisterAsync. One instance supports N devices (PLCs) behind
-/// the same driver; per-device routing is keyed on
-/// via IPerCallHostResolver.
+/// the same driver; per-device routing is keyed on the tag's device routing key (the RawPath's
+/// device segment, delivered on ).
///
public sealed class AbCipDriverOptions
{
@@ -12,14 +14,23 @@ public sealed class AbCipDriverOptions
/// PLCs this driver instance talks to. Each device contributes its own
/// string as the hostName key used by resilience pipelines and the Admin UI.
///
+ ///
+ /// Wave C moves the device's live connection host into the Device row's DeviceConfig;
+ /// for Wave B the list stays bound from DriverConfig as today, and a tag routes to its device
+ /// by matching against the device's name ()
+ /// or its .
+ ///
public IReadOnlyList Devices { get; init; } = [];
///
- /// Pre-declared tag map across all devices. Pre-declared tags always emit during
- /// discovery; opt in to controller-side discovery via
+ /// Authored raw tags this driver serves. The deploy artifact hands each authored raw
+ /// Tag as a (RawPath identity + driver TagConfig blob +
+ /// WriteIdempotent flag + DeviceName routing key); the driver maps each through
+ /// into its RawPath → definition table.
+ /// Pre-declared tags always emit during discovery; opt in to controller-side discovery via
/// .
///
- public IReadOnlyList Tags { get; init; } = [];
+ public IReadOnlyList RawTags { get; init; } = [];
/// Per-device probe settings. Falls back to defaults when omitted.
public AbCipProbeOptions Probe { get; init; } = new();
@@ -111,8 +122,13 @@ public sealed record AbCipDeviceOptions(
///
/// One AB-backed OPC UA variable. Mirrors the ModbusTagDefinition shape.
///
-/// Tag name; becomes the OPC UA browse name and full reference.
-/// Which device () this tag lives on.
+/// Tag name; becomes the OPC UA browse name and full reference (the RawPath in v3).
+/// The tag's device routing key — the RawPath's device segment,
+/// threaded in from by the driver at table-build time (the mapper
+/// leaves it empty; device placement is no longer in the TagConfig blob). Resolved against the driver's
+/// devices by matching or .
+/// TODO(v3 WaveC): once the connection host moves to the Device row's DeviceConfig this is a pure logical
+/// device name; the name still reads DeviceHostAddress to avoid churn across the driver + CLI.
/// Logix symbolic path (controller or program scope).
/// Logix atomic type, or for UDT-typed tags.
/// When true and the tag's ExternalAccess permits writes, IWritable routes writes here.
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipEquipmentTagParser.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipEquipmentTagParser.cs
deleted file mode 100644
index 82e77ee4..00000000
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipEquipmentTagParser.cs
+++ /dev/null
@@ -1,123 +0,0 @@
-using System.Text.Json;
-using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
-
-namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
-
-/// Parses an equipment tag's TagConfig JSON (the shape authored by the AdminUI
-/// AbCipTagConfigModel) 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 AbCipEquipmentTagParser
-{
- /// 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 AbCip TagConfig object.
- ///
- /// is read from the optional "writable"
- /// boolean field in the TagConfig JSON; it defaults to true when the field is absent,
- /// matching the record's documented default and the behaviour of pre-declared tags. Operators
- /// who need a read-only OPC UA surface can author "writable":false in the TagConfig;
- /// the PLC's ExternalAccess attribute remains the effective write gate at the wire level.
- ///
- public static bool TryParse(string reference, out AbCipTagDefinition 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;
- // AbCip is a symbolic driver: the mandatory addressing field is the Logix tag path.
- if (root.ValueKind != JsonValueKind.Object
- || !root.TryGetProperty("tagPath", out var tagPathEl)
- || tagPathEl.ValueKind != JsonValueKind.String)
- return false;
- var tagPath = tagPathEl.GetString();
- if (string.IsNullOrWhiteSpace(tagPath)) return false;
-
- var deviceHostAddress = ReadString(root, "deviceHostAddress");
- // A "dataType":"Structure" input is accepted and produces a Structure-typed definition
- // with Members:null. The driver treats this as a black-box dotted-path read: libplctag
- // resolves the full tag path (e.g. "Motor.Speed") without enumerating UDT members.
- // The address space emits a placeholder String variable; UDT member declarations are
- // not supported in the equipment-tag flow.
- // 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", AbCipDataType.DInt, out var dataType)) return false;
- // Review I-1 — an equipment tag is an ARRAY ⟺ isArray:true AND arrayLength >= 1. A
- // 1-element array (isArray:true, arrayLength:1) is a VALID 1-element array — the
- // foundation materialises a [1] OPC UA array node — so it must read as an array, not a
- // scalar. ElementCount can't carry the signal (a scalar and a 1-element array both
- // have a count of 1), so the explicit IsArray flag does.
- var (isArray, elementCount) = ReadArrayShape(root);
- // "writable" defaults to true when absent — matches AbCipTagDefinition.Writable default.
- // Now via the shared TagConfigJson.ReadWritable (explicit-false-only), byte-identical to the
- // hand-rolled read it replaces.
- var writable = TagConfigJson.ReadWritable(root);
- def = new AbCipTagDefinition(
- Name: reference, DeviceHostAddress: deviceHostAddress, TagPath: tagPath,
- DataType: dataType, Writable: writable, ElementCount: elementCount, IsArray: isArray);
- return true;
- }
- catch (JsonException) { return false; }
- catch (FormatException) { return false; }
- catch (InvalidOperationException) { return false; }
- }
-
- ///
- /// Resolve the 1-D array shape from an isArray / arrayLength pair (the foundation
- /// contract carrier). The canonical rule: the tag is an ARRAY ⟺ isArray is truthy AND
- /// arrayLength is a number >= 1. Any other combination (isArray absent/false,
- /// or isArray:true with arrayLength missing/invalid/<1) is a SCALAR and returns
- /// (IsArray: false, ElementCount: 1). This matches every other driver (Modbus, S7,
- /// TwinCAT, AbLegacy) which also return scalar for that degenerate input.
- ///
- private static (bool IsArray, int ElementCount) ReadArrayShape(JsonElement o)
- {
- var isArray = o.TryGetProperty("isArray", out var a) && a.ValueKind == JsonValueKind.True;
- if (!isArray) return (false, 1);
- if (o.TryGetProperty("arrayLength", out var len)
- && len.ValueKind == JsonValueKind.Number
- && len.TryGetInt32(out var n)
- && n >= 1)
- return (true, n);
- // isArray:true but arrayLength missing/invalid/<1 — canonical rule: scalar (matches all other drivers).
- return (false, 1);
- }
-
- ///
- /// 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("AbCip 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("AbCip 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() ?? "" : "";
-}
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipTagDefinitionFactory.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipTagDefinitionFactory.cs
new file mode 100644
index 00000000..eaeaa4fb
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipTagDefinitionFactory.cs
@@ -0,0 +1,226 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
+
+///
+/// v3 pure mapper: turns an authored raw tag's TagConfig JSON (the shape produced by the
+/// AdminUI AbCipTagConfigModel) 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 + the driver-test AbCipRawTags helper) that hold a typed definition and need to
+/// synthesise the TagConfig blob for a .
+///
+///
+///
+/// Device placement is NOT in the blob. The pre-v3 parser read a deviceHostAddress
+/// key off the TagConfig; v3 drops it — a tag's device is the RawPath's device segment, delivered
+/// out-of-band on and threaded onto the definition's
+/// (the device routing key) by the driver at
+/// table-build time. therefore leaves that field empty.
+///
+///
+/// Array / member / safety round-trip. Today's production authoring surface
+/// (AbCipTagConfigModel) emits only tagPath / dataType / writable, so a
+/// real equipment tag maps to a scalar atomic definition. The additional isArray /
+/// arrayLength / safetyTag / members keys are read (and emitted by
+/// ) so the retired pre-declared-tag coverage — arrays and declared UDT
+/// member fan-out — survives the migration to the RawPath seam. They are additive: absent keys map
+/// to the scalar / no-member / non-safety defaults.
+///
+///
+public static class AbCipTagDefinitionFactory
+{
+ ///
+ /// 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 mandatory addressing field is the Logix tagPath; dataType 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.
+ /// and
+ /// are NOT read from the blob — the driver threads them in from the tag's .
+ ///
+ /// 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 AbCip address object.
+ ///
+ /// is read from the optional "writable" boolean;
+ /// it defaults to true when absent, matching the record's documented default. Operators who
+ /// need a read-only OPC UA surface can author "writable":false; the PLC's ExternalAccess
+ /// attribute remains the effective write gate at the wire level.
+ ///
+ public static bool FromTagConfig(string tagConfig, string rawPath, out AbCipTagDefinition def)
+ {
+ def = null!;
+ if (string.IsNullOrWhiteSpace(tagConfig)) return false;
+ try
+ {
+ using var doc = JsonDocument.Parse(tagConfig);
+ var root = doc.RootElement;
+ // AbCip is a symbolic driver: the mandatory addressing field is the Logix tag path.
+ if (root.ValueKind != JsonValueKind.Object
+ || !root.TryGetProperty("tagPath", out var tagPathEl)
+ || tagPathEl.ValueKind != JsonValueKind.String)
+ return false;
+ var tagPath = tagPathEl.GetString();
+ if (string.IsNullOrWhiteSpace(tagPath)) 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", AbCipDataType.DInt, out var dataType)) return false;
+ // Review I-1 — an equipment tag is an ARRAY ⟺ isArray:true AND arrayLength >= 1. A 1-element
+ // array is a VALID 1-element array (the foundation materialises a [1] OPC UA array node).
+ var (isArray, elementCount) = ReadArrayShape(root);
+ // "writable" defaults to true when absent — matches AbCipTagDefinition.Writable default.
+ var writable = TagConfigJson.ReadWritable(root);
+ var safetyTag = ReadBool(root, "safetyTag");
+
+ // Optional declared UDT member fan-out (round-trips the retired pre-declared coverage). A
+ // malformed member (missing name / typo'd dataType) rejects the whole tag, mirroring the
+ // strict top-level contract.
+ IReadOnlyList? members = null;
+ if (root.TryGetProperty("members", out var membersEl) && membersEl.ValueKind == JsonValueKind.Array)
+ {
+ var list = new List();
+ foreach (var m in membersEl.EnumerateArray())
+ {
+ if (m.ValueKind != JsonValueKind.Object
+ || !m.TryGetProperty("name", out var nameEl)
+ || nameEl.ValueKind != JsonValueKind.String
+ || string.IsNullOrWhiteSpace(nameEl.GetString()))
+ return false;
+ if (!TagConfigJson.TryReadEnumStrict(m, "dataType", AbCipDataType.DInt, out var memberType)) return false;
+ var (memberIsArray, memberCount) = ReadArrayShape(m);
+ list.Add(new AbCipStructureMember(
+ Name: nameEl.GetString()!,
+ DataType: memberType,
+ Writable: ReadBoolDefaultTrue(m, "writable"),
+ WriteIdempotent: ReadBool(m, "writeIdempotent"),
+ ElementCount: memberCount,
+ IsArray: memberIsArray));
+ }
+ if (list.Count > 0) members = list;
+ }
+
+ def = new AbCipTagDefinition(
+ Name: rawPath, DeviceHostAddress: "", TagPath: tagPath!,
+ DataType: dataType, Writable: writable, WriteIdempotent: false,
+ Members: members, SafetyTag: safetyTag, ElementCount: elementCount, IsArray: isArray);
+ 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, the driver-test
+ /// AbCipRawTags helper) that construct an and need the
+ /// TagConfig string to hand the driver as a . Enum values are written
+ /// as their name strings; optional fields are emitted only when non-default so the blob stays minimal.
+ /// (device routing key) and
+ /// are NOT emitted — they travel on the
+ /// , not inside the blob.
+ ///
+ /// The definition to serialise.
+ /// The camelCase TagConfig JSON string.
+ public static string ToTagConfig(AbCipTagDefinition def)
+ {
+ ArgumentNullException.ThrowIfNull(def);
+ var o = new JsonObject
+ {
+ ["tagPath"] = def.TagPath,
+ ["dataType"] = def.DataType.ToString(),
+ ["writable"] = def.Writable,
+ };
+ if (def.IsArray)
+ {
+ o["isArray"] = true;
+ o["arrayLength"] = Math.Max(1, def.ElementCount);
+ }
+ if (def.SafetyTag) o["safetyTag"] = true;
+ if (def.Members is { Count: > 0 } members)
+ {
+ var arr = new JsonArray();
+ foreach (var m in members)
+ {
+ var mo = new JsonObject
+ {
+ ["name"] = m.Name,
+ ["dataType"] = m.DataType.ToString(),
+ ["writable"] = m.Writable,
+ };
+ if (m.WriteIdempotent) mo["writeIdempotent"] = true;
+ if (m.IsArray)
+ {
+ mo["isArray"] = true;
+ mo["arrayLength"] = Math.Max(1, m.ElementCount);
+ }
+ arr.Add(mo);
+ }
+ o["members"] = arr;
+ }
+ return o.ToJsonString();
+ }
+
+ ///
+ /// Resolve the 1-D array shape from an isArray / arrayLength pair (the foundation
+ /// contract carrier). The canonical rule: the tag is an ARRAY ⟺ isArray is truthy AND
+ /// arrayLength is a number >= 1. Any other combination is a SCALAR and returns
+ /// (IsArray: false, ElementCount: 1) — matching every other driver.
+ ///
+ private static (bool IsArray, int ElementCount) ReadArrayShape(JsonElement o)
+ {
+ var isArray = o.TryGetProperty("isArray", out var a) && a.ValueKind == JsonValueKind.True;
+ if (!isArray) return (false, 1);
+ if (o.TryGetProperty("arrayLength", out var len)
+ && len.ValueKind == JsonValueKind.Number
+ && len.TryGetInt32(out var n)
+ && n >= 1)
+ return (true, n);
+ // isArray:true but arrayLength missing/invalid/<1 — canonical rule: scalar (matches all other drivers).
+ return (false, 1);
+ }
+
+ ///
+ /// 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("AbCip 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("AbCip TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown).");
+ }
+ return warnings;
+ }
+
+ private static bool ReadBool(JsonElement o, string name)
+ => o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.True;
+
+ private static bool ReadBoolDefaultTrue(JsonElement o, string name)
+ => !o.TryGetProperty(name, out var e) || e.ValueKind != JsonValueKind.False;
+}
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
index 8b2cd78f..05ad2904 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
@@ -82,12 +82,23 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
_discoveredUdtShapes[UdtShapeKey(deviceHostAddress, structName)] = shape;
private readonly PollGroupEngine _poll;
+ // Devices keyed by host address — the primary registry (GetDeviceState, discovery, probe, diagnostics).
private readonly Dictionary _devices = new(StringComparer.OrdinalIgnoreCase);
- private readonly Dictionary _tagsByName = new(StringComparer.OrdinalIgnoreCase);
+ // Tag-routing index: a tag's device routing key (DeviceName ?? HostAddress, plus the HostAddress alias)
+ // → its DeviceState. A tag resolves to its device by DeviceName-or-host so multi-device routing keys on
+ // the RawPath's device segment. TODO(v3 WaveC): collapses to a pure DeviceName index once the connection
+ // host moves into the Device row's DeviceConfig.
+ private readonly Dictionary _devicesByRoutingKey = new(StringComparer.OrdinalIgnoreCase);
+ // v3 authored RawPath → definition table (parents + fanned-out UDT members), keyed case-sensitive
+ // ordinal. Built in InitializeAsync from _options.RawTags via the pure mapper.
+ private readonly Dictionary _tagsByRawPath = new(StringComparer.Ordinal);
+ // Top-level authored definitions (parents + flat tags, NOT synthesised members), in author order —
+ // the source DiscoverAsync groups by device. Members are synthesised inside the discovery/read paths.
+ private readonly List _declaredTags = new();
- // 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 AbCipEquipmentTagParser, cached).
+ // Resolves a read/write/subscribe fullReference (always a RawPath in v3) to a tag definition by a
+ // single hit on the authored _tagsByRawPath table; a miss is a miss (the pre-v3 blob-parse fallback
+ // is retired — see EquipmentTagRefResolver).
private readonly EquipmentTagRefResolver _resolver;
private readonly ILogger _logger;
@@ -128,8 +139,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
_templateReaderFactory = templateReaderFactory ?? new LibplctagTemplateReaderFactory();
_logger = logger ?? NullLogger.Instance;
_resolver = new EquipmentTagRefResolver(
- r => _tagsByName.TryGetValue(r, out var t) ? t : null,
- r => AbCipEquipmentTagParser.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) =>
@@ -248,7 +258,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
if (!string.IsNullOrWhiteSpace(driverConfigJson))
{
var parsed = AbCipDriverFactoryExtensions.ParseOptions(_driverInstanceId, driverConfigJson);
- if (parsed.Devices.Count > 0 || parsed.Tags.Count > 0)
+ if (parsed.Devices.Count > 0 || parsed.RawTags.Count > 0)
{
_options = parsed;
_alarmProjection = new AbCipAlarmProjection(this, _options.AlarmPollInterval, _logger);
@@ -261,18 +271,39 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
?? throw new InvalidOperationException(
$"AbCip device has invalid HostAddress '{device.HostAddress}' — expected 'ab://gateway[:port]/cip-path'.");
var profile = AbCipPlcFamilyProfile.ForFamily(device.PlcFamily);
- _devices[device.HostAddress] = new DeviceState(addr, device, profile);
+ var state = new DeviceState(addr, device, profile);
+ _devices[device.HostAddress] = state;
+ // Route index: a tag reaches this device by matching its DeviceName routing key OR its
+ // host address. Devices with no explicit DeviceName are identified purely by host today.
+ _devicesByRoutingKey[device.HostAddress] = state;
+ if (!string.IsNullOrEmpty(device.DeviceName))
+ _devicesByRoutingKey[device.DeviceName] = state;
}
- foreach (var tag in _options.Tags)
+
+ // Build the RawPath → definition table from the authored raw tags. Each entry's TagConfig blob
+ // is mapped by the pure factory; the entry's DeviceName (routing key) + WriteIdempotent flag are
+ // threaded onto the def (they live on the RawTagEntry, not inside the blob). A mapper miss is
+ // skipped (logged), never thrown — a bad tag must not fail the whole driver init.
+ foreach (var entry in _options.RawTags)
{
- // Duplicate-key check: a collision means two configured tags have the same name.
+ if (!AbCipTagDefinitionFactory.FromTagConfig(entry.TagConfig, entry.RawPath, out var mapped))
+ {
+ _logger.LogWarning(
+ "AbCip tag config did not map to a definition; skipping. Driver={DriverInstanceId} RawPath={RawPath}",
+ _driverInstanceId, entry.RawPath);
+ continue;
+ }
+ var tag = mapped with { DeviceHostAddress = entry.DeviceName, WriteIdempotent = entry.WriteIdempotent };
+
+ // Duplicate-key check: a collision means two authored tags share the same RawPath.
// Fail fast at init time with a diagnostic rather than silently clobbering.
- if (_tagsByName.TryGetValue(tag.Name, out var existingTag))
+ if (_tagsByRawPath.TryGetValue(tag.Name, out var existingTag))
throw new InvalidOperationException(
$"AbCip tag name collision: '{tag.Name}' is declared more than once. " +
$"Existing entry DeviceHostAddress='{existingTag.DeviceHostAddress}', " +
$"TagPath='{existingTag.TagPath}'. Rename or remove the duplicate.");
- _tagsByName[tag.Name] = tag;
+ _tagsByRawPath[tag.Name] = tag;
+ _declaredTags.Add(tag);
if (tag.DataType == AbCipDataType.Structure && tag.Members is { Count: > 0 })
{
@@ -295,12 +326,12 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// Member fan-out duplicate check: a member-path collision means two
// configured structure tags produce the same member path, or a member
// name collides with an independently-declared tag.
- if (_tagsByName.TryGetValue(memberTag.Name, out var existingMember))
+ if (_tagsByRawPath.TryGetValue(memberTag.Name, out var existingMember))
throw new InvalidOperationException(
$"AbCip tag name collision: '{memberTag.Name}' is produced by both " +
$"'{tag.Name}.{member.Name}' (member fan-out) and an existing tag " +
$"'{existingMember.Name}'. Rename one of the configured tags to resolve.");
- _tagsByName[memberTag.Name] = memberTag;
+ _tagsByRawPath[memberTag.Name] = memberTag;
}
}
}
@@ -383,8 +414,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
state.DisposeHandles();
}
_devices.Clear();
- _tagsByName.Clear();
- _resolver.Clear(); // drop transient equipment-tag parses so a config change re-parses
+ _devicesByRoutingKey.Clear();
+ _tagsByRawPath.Clear();
+ _declaredTags.Clear();
+ _resolver.Clear(); // no-op in v3 (the resolver reads the live table by closure); kept for symmetry
_health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
}
@@ -502,19 +535,19 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- IPerCallHostResolver ----
///
- /// Resolve a read/write/subscribe reference to the device host that keys its per-host
- /// resilience (bulkhead / circuit breaker). CONV-4: routes through
- /// so an equipment-tag reference (raw TagConfig
- /// JSON) resolves to its OWN authored device rather than the first-device fallback — else a
- /// broken device B would trip device A's breaker. The resolver caches parses, so this adds
- /// no per-call cost after first use.
+ /// Resolve a read/write/subscribe reference (a RawPath in v3) to the device host that keys its
+ /// per-host resilience (bulkhead / circuit breaker). CONV-4: routes through the authored table so a
+ /// tag resolves to its OWN device's host rather than the first-device fallback — else a broken
+ /// device B would trip device A's breaker. The tag's device routing key is resolved to the device's
+ /// actual host address (the resilience key), matching by DeviceName-or-host.
///
- /// The tag reference (authored name or equipment-tag JSON).
+ /// The tag RawPath reference.
/// The device host key.
public string ResolveHost(string fullReference)
{
- if (_resolver.TryResolve(fullReference, out var def) && !string.IsNullOrEmpty(def.DeviceHostAddress))
- return def.DeviceHostAddress;
+ if (_resolver.TryResolve(fullReference, out var def)
+ && _devicesByRoutingKey.TryGetValue(def.DeviceHostAddress, out var state))
+ return state.Options.HostAddress;
return _options.Devices.FirstOrDefault()?.HostAddress ?? DriverInstanceId;
}
@@ -536,7 +569,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// grouping is itself gated behind EnableDeclarationOnlyUdtGrouping — Studio 5000 may
// reorder UDT members vs declaration order, so the fast path is opt-in only.
var plan = AbCipUdtReadPlanner.Build(
- fullReferences, _tagsByName, _options.EnableDeclarationOnlyUdtGrouping);
+ fullReferences, _tagsByRawPath, _options.EnableDeclarationOnlyUdtGrouping);
foreach (var group in plan.Groups)
await ReadGroupAsync(group, results, now, cancellationToken).ConfigureAwait(false);
@@ -564,7 +597,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
results[fb.OriginalIndex] = new DataValueSnapshot(null, AbCipStatusMapper.BadNotSupported, null, now);
return;
}
- if (!_devices.TryGetValue(def.DeviceHostAddress, out var device))
+ if (!_devicesByRoutingKey.TryGetValue(def.DeviceHostAddress, out var device))
{
results[fb.OriginalIndex] = new DataValueSnapshot(null, AbCipStatusMapper.BadNodeIdUnknown, null, now);
return;
@@ -646,7 +679,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
{
var parent = group.ParentDefinition;
- if (!_devices.TryGetValue(parent.DeviceHostAddress, out var device))
+ if (!_devicesByRoutingKey.TryGetValue(parent.DeviceHostAddress, out var device))
{
StampGroupStatus(group, results, now, AbCipStatusMapper.BadNodeIdUnknown);
return;
@@ -736,7 +769,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
results[i] = new WriteResult(AbCipStatusMapper.BadNotWritable);
continue;
}
- if (!_devices.TryGetValue(def.DeviceHostAddress, out var device))
+ if (!_devicesByRoutingKey.TryGetValue(def.DeviceHostAddress, out var device))
{
results[i] = new WriteResult(AbCipStatusMapper.BadNodeIdUnknown);
continue;
@@ -1017,9 +1050,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// Pre-declared tags — always emitted; the primary config path. UDT tags with declared
// Members fan out into a sub-folder + one Variable per member instead of a single
// Structure Variable (Structure has no useful scalar value + member-addressable paths
- // are what downstream consumers actually want).
- var preDeclared = _options.Tags.Where(t =>
- string.Equals(t.DeviceHostAddress, device.HostAddress, StringComparison.OrdinalIgnoreCase));
+ // are what downstream consumers actually want). A tag reaches this device when its
+ // device routing key matches the device's DeviceName or its host address.
+ var preDeclared = _declaredTags.Where(t => RoutesToDevice(t, device));
foreach (var tag in preDeclared)
{
if (AbCipSystemTagFilter.IsSystemTag(tag.Name)) continue;
@@ -1235,6 +1268,14 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// a legacy definition carrying only ElementCount > 1 stays an array for back-compat.
private static bool IsArrayTag(AbCipTagDefinition tag) => tag.IsArray || tag.ElementCount > 1;
+ // A tag routes to a device when its device routing key (DeviceHostAddress, threaded from the
+ // RawTagEntry.DeviceName) matches the device's DeviceName or its host address. TODO(v3 WaveC):
+ // collapses to a DeviceName-only match once the connection host lives on the Device row's DeviceConfig.
+ private static bool RoutesToDevice(AbCipTagDefinition tag, AbCipDeviceOptions device) =>
+ string.Equals(tag.DeviceHostAddress, device.HostAddress, StringComparison.OrdinalIgnoreCase)
+ || (!string.IsNullOrEmpty(device.DeviceName)
+ && string.Equals(tag.DeviceHostAddress, device.DeviceName, StringComparison.OrdinalIgnoreCase));
+
private static DriverAttributeInfo ToAttributeInfo(AbCipTagDefinition tag) => new(
FullName: tag.Name,
DriverDataType: tag.DataType.ToDriverDataType(),
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverFactoryExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverFactoryExtensions.cs
index 5761cb95..da0fd469 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverFactoryExtensions.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverFactoryExtensions.cs
@@ -1,5 +1,5 @@
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.AbCip;
@@ -68,9 +68,11 @@ public static class AbCipDriverFactoryExtensions
AllowPacking: d.AllowPacking,
ConnectionSize: d.ConnectionSize))]
: [],
- Tags = dto.Tags is { Count: > 0 }
- ? [.. dto.Tags.Select(t => BuildTag(t, driverInstanceId))]
- : [],
+ // v3: the deploy artifact (Wave C) populates RawTags with the cluster's authored raw AbCip
+ // tags (RawPath + TagConfig blob + WriteIdempotent + DeviceName). The driver maps each entry
+ // through AbCipTagDefinitionFactory.FromTagConfig at Initialize; the legacy pre-declared DTO
+ // tag path (structured Tags[] → BuildTag) is retired.
+ RawTags = dto.RawTags is { Count: > 0 } ? [.. dto.RawTags] : [],
Probe = new AbCipProbeOptions
{
Enabled = dto.Probe?.Enabled ?? true,
@@ -100,32 +102,6 @@ public static class AbCipDriverFactoryExtensions
private static TimeSpan PositiveTimeoutOrDefault(int? ms, int defaultMs) =>
TimeSpan.FromMilliseconds(ms is int v and > 0 ? v : defaultMs);
- private static AbCipTagDefinition BuildTag(AbCipTagDto t, string driverInstanceId) =>
- new(
- Name: t.Name ?? throw new InvalidOperationException(
- $"AB CIP config for '{driverInstanceId}' has a tag missing Name"),
- DeviceHostAddress: t.DeviceHostAddress ?? throw new InvalidOperationException(
- $"AB CIP tag '{t.Name}' in '{driverInstanceId}' missing DeviceHostAddress"),
- TagPath: t.TagPath ?? throw new InvalidOperationException(
- $"AB CIP tag '{t.Name}' in '{driverInstanceId}' missing TagPath"),
- DataType: ParseEnum(t.DataType, t.Name, driverInstanceId, "DataType"),
- Writable: t.Writable ?? true,
- WriteIdempotent: t.WriteIdempotent ?? false,
- Members: t.Members is { Count: > 0 }
- ? [.. t.Members.Select(m => new AbCipStructureMember(
- Name: m.Name ?? throw new InvalidOperationException(
- $"AB CIP tag '{t.Name}' in '{driverInstanceId}' has a member missing Name"),
- DataType: ParseEnum(m.DataType, t.Name, driverInstanceId,
- $"Members[{m.Name}].DataType"),
- Writable: m.Writable ?? true,
- WriteIdempotent: m.WriteIdempotent ?? false,
- ElementCount: m.ElementCount is > 0 ? m.ElementCount.Value : 1,
- IsArray: m.IsArray))]
- : null,
- SafetyTag: t.SafetyTag ?? false,
- ElementCount: t.ElementCount is > 0 ? t.ElementCount.Value : 1,
- IsArray: t.IsArray);
-
private static T ParseEnum(string? raw, string? tagName, string driverInstanceId, string field,
T? fallback = null) where T : struct, Enum
{
@@ -182,9 +158,10 @@ public static class AbCipDriverFactoryExtensions
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 + DeviceName)
+ /// the deploy artifact delivers. The driver maps each through the tag-definition factory at Initialize.
///
- public List? Tags { get; init; }
+ public List? RawTags { get; init; }
///
/// Gets or sets the probe configuration.
@@ -220,100 +197,6 @@ public static class AbCipDriverFactoryExtensions
public int? ConnectionSize { get; init; }
}
- internal sealed class AbCipTagDto
- {
- ///
- /// 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 path.
- ///
- public string? TagPath { 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; }
-
- ///
- /// Gets or sets the list of structure members.
- ///
- public List? Members { get; init; }
-
- ///
- /// Gets or sets whether this is a safety tag.
- ///
- public bool? SafetyTag { get; init; }
-
- ///
- /// Gets or sets the explicit 1-D array signal — true ⟺ the tag is an array (even a
- /// 1-element one). Mirrors AbCipTagDefinition.IsArray; optional (absent ⇒ scalar).
- ///
- [JsonPropertyName("isArray")]
- public bool IsArray { get; init; }
-
- ///
- /// Gets or sets the number of elements for a 1-D array tag; 1 (or absent) for a scalar.
- /// Mirrors AbCipTagDefinition.ElementCount; only positive values are honoured.
- ///
- [JsonPropertyName("elementCount")]
- public int? ElementCount { get; init; }
- }
-
- internal sealed class AbCipMemberDto
- {
- ///
- /// Gets or sets the member name.
- ///
- public string? Name { get; init; }
-
- ///
- /// Gets or sets the data type.
- ///
- public string? DataType { get; init; }
-
- ///
- /// Gets or sets whether the member is writable.
- ///
- public bool? Writable { get; init; }
-
- ///
- /// Gets or sets whether write is idempotent.
- ///
- public bool? WriteIdempotent { get; init; }
-
- ///
- /// Gets or sets the explicit 1-D array signal for the member — true ⟺ the member is
- /// an array (even a 1-element one). Mirrors AbCipStructureMember.IsArray; optional.
- ///
- [JsonPropertyName("isArray")]
- public bool IsArray { get; init; }
-
- ///
- /// Gets or sets the number of elements for a 1-D array member; 1 (or absent) for a
- /// scalar. Mirrors AbCipStructureMember.ElementCount; only positive values are honoured.
- ///
- [JsonPropertyName("elementCount")]
- public int? ElementCount { get; init; }
- }
-
internal sealed class AbCipProbeDto
{
///
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs
index 1621ecec..58e8a0ec 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs
@@ -89,14 +89,22 @@ public sealed class AbCipDriverProbe : IDriverProbe
// Forward Open to confirm the remote endpoint speaks CIP, not just TCP.
var profile = AbCipPlcFamilyProfile.ForFamily(firstDevice?.PlcFamily ?? AbCipPlcFamily.ControlLogix);
- // Prefer the first declared tag path as the probe tag name (it must exist on the PLC).
- // Fall back to a benign system attribute that is present on all ControlLogix/CompactLogix
- // controllers (@raw_cpu_type); still returns ErrorNotFound on Micro800 / MicroLogix, which
- // is sufficient to confirm CIP reachability.
- var tagName = opts.Tags.FirstOrDefault(
- t => string.Equals(t.DeviceHostAddress, firstDevice?.HostAddress, StringComparison.OrdinalIgnoreCase))
- ?.TagPath
- ?? opts.Tags.FirstOrDefault()?.TagPath
+ // Prefer the first authored tag path as the probe tag name (it must exist on the PLC). v3: the
+ // tags arrive as RawTagEntry blobs — map each through the pure factory to recover its TagPath +
+ // device routing key. Fall back to a benign system attribute present on all ControlLogix /
+ // CompactLogix controllers (@raw_cpu_type); still returns ErrorNotFound on Micro800 / MicroLogix,
+ // which is sufficient to confirm CIP reachability.
+ var mappedTags = opts.RawTags
+ .Select(e => AbCipTagDefinitionFactory.FromTagConfig(e.TagConfig, e.RawPath, out var d)
+ ? (Ok: true, Device: e.DeviceName, d.TagPath)
+ : (Ok: false, Device: e.DeviceName, TagPath: null!))
+ .Where(x => x.Ok)
+ .ToList();
+ var tagName = mappedTags.FirstOrDefault(
+ x => string.Equals(x.Device, firstDevice?.HostAddress, StringComparison.OrdinalIgnoreCase)
+ || (firstDevice?.DeviceName is { } dn && string.Equals(x.Device, dn, StringComparison.OrdinalIgnoreCase)))
+ .TagPath
+ ?? mappedTags.FirstOrDefault().TagPath
?? "@raw_cpu_type";
var p = new AbCipTagCreateParams(
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipUdtReadPlanner.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipUdtReadPlanner.cs
index fd729773..28bc181f 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipUdtReadPlanner.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipUdtReadPlanner.cs
@@ -18,9 +18,10 @@ public static class AbCipUdtReadPlanner
{
///
/// Split into whole-UDT groups + per-tag leftovers.
- /// is the driver's _tagsByName map — both parent
- /// UDT rows and their fanned-out member rows live there. Lookup is OrdinalIgnoreCase
- /// to match the driver's dictionary semantics. When
+ /// is the driver's _tagsByRawPath map (RawPath → def) —
+ /// both parent UDT rows and their fanned-out member rows live there. Tag lookup honours the
+ /// passed dictionary's comparer (v3: case-sensitive Ordinal RawPaths); the internal
+ /// parent-name grouping is OrdinalIgnoreCase. When
/// is false no groups are
/// formed — every reference goes to the per-tag fallback path so member decoding never
/// relies on declaration-order offsets that may not match the controller layout.
diff --git a/tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests/AbCipCommandBaseTests.cs b/tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests/AbCipCommandBaseTests.cs
index e8e6052f..bfc5a028 100644
--- a/tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests/AbCipCommandBaseTests.cs
+++ b/tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Tests/AbCipCommandBaseTests.cs
@@ -135,9 +135,10 @@ public sealed class AbCipCommandBaseTests
var options = cmd.InvokeBuildOptions(tags);
- options.Tags.Count.ShouldBe(2);
- options.Tags[0].Name.ShouldBe("t1");
- options.Tags[1].Name.ShouldBe("t2");
+ // v3: BuildOptions projects each typed tag to a RawTagEntry (RawPath = tag name).
+ options.RawTags.Count.ShouldBe(2);
+ options.RawTags[0].RawPath.ShouldBe("t1");
+ options.RawTags[1].RawPath.ShouldBe("t2");
}
///
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipAlarmProjectionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipAlarmProjectionTests.cs
index bf26f406..d7c253cc 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipAlarmProjectionTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipAlarmProjectionTests.cs
@@ -56,7 +56,7 @@ public sealed class AbCipAlarmProjectionTests
var opts = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags = [AlmdTag("HighTemp")],
+ RawTags = AbCipRawTags.From(AlmdTag("HighTemp")),
EnableAlarmProjection = false, // explicit; also the default
};
var drv = new AbCipDriver(opts, "drv-1", factory);
@@ -83,7 +83,7 @@ public sealed class AbCipAlarmProjectionTests
var opts = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags = [AlmdTag("HighTemp")],
+ RawTags = AbCipRawTags.From(AlmdTag("HighTemp")),
EnableAlarmProjection = true,
AlarmPollInterval = TimeSpan.FromMilliseconds(20),
// The ALMD projection here drives the parent-UDT runtime via offset-keyed values,
@@ -138,7 +138,7 @@ public sealed class AbCipAlarmProjectionTests
var opts = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags = [AlmdTag("HighTemp")],
+ RawTags = AbCipRawTags.From(AlmdTag("HighTemp")),
EnableAlarmProjection = true,
AlarmPollInterval = TimeSpan.FromMilliseconds(20),
EnableDeclarationOnlyUdtGrouping = true,
@@ -189,7 +189,7 @@ public sealed class AbCipAlarmProjectionTests
var opts = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags = [AlmdTag("HighTemp")],
+ RawTags = AbCipRawTags.From(AlmdTag("HighTemp")),
EnableAlarmProjection = true,
AlarmPollInterval = TimeSpan.FromMilliseconds(20),
// The ALMD projection here drives the parent-UDT runtime via offset-keyed values,
@@ -230,7 +230,7 @@ public sealed class AbCipAlarmProjectionTests
var opts = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags = [AlmdTag("HighTemp")],
+ RawTags = AbCipRawTags.From(AlmdTag("HighTemp")),
EnableAlarmProjection = true,
AlarmPollInterval = TimeSpan.FromMilliseconds(20),
// The ALMD projection here drives the parent-UDT runtime via offset-keyed values,
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayTests.cs
index 1df0b24f..e0e0c08f 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipArrayTests.cs
@@ -9,20 +9,21 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
/// Phase 4c (Task 6) — 1-D array support for AbCip. Covers: discovery flips
/// /
/// for an array atomic tag + an array UDT member; the read path returns a typed CLR array
-/// boxed as ; and the equipment-tag resolver threads
-/// arrayLength from the TagConfig into the transient definition's element count so
-/// an isArray equipment tag reads the whole array.
+/// boxed as ; and the v3 tag mapper threads arrayLength from the
+/// TagConfig into the definition's element count so an isArray tag reads the whole array.
///
[Trait("Category", "Unit")]
public sealed class AbCipArrayTests
{
+ private const string Device = "ab://10.0.0.5/1,0";
+
private static (AbCipDriver drv, FakeAbCipTagFactory factory) NewDriver(params AbCipTagDefinition[] tags)
{
var factory = new FakeAbCipTagFactory();
var opts = new AbCipDriverOptions
{
- Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = tags,
+ Devices = [new AbCipDeviceOptions(Device)],
+ RawTags = AbCipRawTags.From(tags),
};
var drv = new AbCipDriver(opts, "drv-array", factory);
return (drv, factory);
@@ -36,8 +37,8 @@ public sealed class AbCipArrayTests
{
var builder = new RecordingBuilder();
var (drv, _) = NewDriver(
- new AbCipTagDefinition("Recipe", "ab://10.0.0.5/1,0", "Recipe", AbCipDataType.DInt, ElementCount: 10),
- new AbCipTagDefinition("Single", "ab://10.0.0.5/1,0", "Single", AbCipDataType.DInt));
+ new AbCipTagDefinition("Recipe", Device, "Recipe", AbCipDataType.DInt, ElementCount: 10, IsArray: true),
+ new AbCipTagDefinition("Single", Device, "Single", AbCipDataType.DInt));
await drv.InitializeAsync("{}", CancellationToken.None);
await drv.DiscoverAsync(builder, CancellationToken.None);
@@ -57,10 +58,10 @@ public sealed class AbCipArrayTests
{
var builder = new RecordingBuilder();
var (drv, _) = NewDriver(
- new AbCipTagDefinition("Motor", "ab://10.0.0.5/1,0", "Motor", AbCipDataType.Structure,
+ new AbCipTagDefinition("Motor", Device, "Motor", AbCipDataType.Structure,
Members:
[
- new AbCipStructureMember("Setpoints", AbCipDataType.Real, ElementCount: 4),
+ new AbCipStructureMember("Setpoints", AbCipDataType.Real, ElementCount: 4, IsArray: true),
new AbCipStructureMember("Speed", AbCipDataType.DInt),
]));
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -83,7 +84,7 @@ public sealed class AbCipArrayTests
public async Task Array_DInt_read_returns_typed_int_array()
{
var (drv, factory) = NewDriver(
- new AbCipTagDefinition("Recipe", "ab://10.0.0.5/1,0", "Recipe", AbCipDataType.DInt, ElementCount: 4));
+ new AbCipTagDefinition("Recipe", Device, "Recipe", AbCipDataType.DInt, ElementCount: 4, IsArray: true));
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new ArrayFakeAbCipTag(p, new int[] { 11, 22, 33, 44 });
@@ -101,7 +102,7 @@ public sealed class AbCipArrayTests
public async Task Array_Real_read_returns_typed_float_array()
{
var (drv, factory) = NewDriver(
- new AbCipTagDefinition("Floats", "ab://10.0.0.5/1,0", "Floats", AbCipDataType.Real, ElementCount: 3));
+ new AbCipTagDefinition("Floats", Device, "Floats", AbCipDataType.Real, ElementCount: 3, IsArray: true));
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new ArrayFakeAbCipTag(p, new float[] { 1.5f, 2.5f, 3.5f });
@@ -116,7 +117,7 @@ public sealed class AbCipArrayTests
public async Task Array_Bool_read_returns_typed_bool_array()
{
var (drv, factory) = NewDriver(
- new AbCipTagDefinition("Flags", "ab://10.0.0.5/1,0", "Flags", AbCipDataType.Bool, ElementCount: 3));
+ new AbCipTagDefinition("Flags", Device, "Flags", AbCipDataType.Bool, ElementCount: 3, IsArray: true));
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new ArrayFakeAbCipTag(p, new bool[] { true, false, true });
@@ -131,7 +132,7 @@ public sealed class AbCipArrayTests
public async Task Array_String_read_returns_typed_string_array()
{
var (drv, factory) = NewDriver(
- new AbCipTagDefinition("Names", "ab://10.0.0.5/1,0", "Names", AbCipDataType.String, ElementCount: 2));
+ new AbCipTagDefinition("Names", Device, "Names", AbCipDataType.String, ElementCount: 2, IsArray: true));
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new ArrayFakeAbCipTag(p, new string[] { "a", "b" });
@@ -146,7 +147,7 @@ public sealed class AbCipArrayTests
public async Task Scalar_read_path_unchanged_for_element_count_one()
{
var (drv, factory) = NewDriver(
- new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt));
+ new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt));
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new FakeAbCipTag(p) { Value = 4200 };
@@ -158,20 +159,18 @@ public sealed class AbCipArrayTests
///
/// Driver.AbCip-016 — a DECLARED UDT member that is a 1-D array (Setpoints : REAL[4])
- /// must READ as a typed CLR array, matching the array node it discovers as. Before the fix
- /// the member fan-out in InitializeAsync dropped the member's ElementCount /
- /// IsArray, so the fanned-out runtime definition defaulted to scalar and the read
- /// returned a single element (or null) instead of the array — a declared-type-vs-runtime-value
- /// mismatch.
+ /// must READ as a typed CLR array, matching the array node it discovers as. The member fan-out
+ /// in InitializeAsync threads the member's ElementCount / IsArray into the
+ /// fanned-out runtime definition.
///
[Fact]
public async Task Declared_udt_array_member_reads_as_typed_array()
{
var (drv, factory) = NewDriver(
- new AbCipTagDefinition("Motor", "ab://10.0.0.5/1,0", "Motor", AbCipDataType.Structure,
+ new AbCipTagDefinition("Motor", Device, "Motor", AbCipDataType.Structure,
Members:
[
- new AbCipStructureMember("Setpoints", AbCipDataType.Real, ElementCount: 4),
+ new AbCipStructureMember("Setpoints", AbCipDataType.Real, ElementCount: 4, IsArray: true),
new AbCipStructureMember("Speed", AbCipDataType.DInt),
]));
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -187,24 +186,18 @@ public sealed class AbCipArrayTests
factory.Tags["Motor.Setpoints"].CreationParams.IsArray.ShouldBeTrue();
}
- // ---- Resolver: arrayLength threading ----
+ // ---- Mapper: arrayLength threading ----
- /// The equipment-tag resolver threads arrayLength into the def's ElementCount.
+ /// An authored array tag reads as a typed CLR array end-to-end.
[Fact]
- public async Task Equipment_ref_with_arrayLength_reads_as_a_typed_array()
+ public async Task Authored_array_tag_reads_as_a_typed_array()
{
- var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","tagPath":"Recipe","dataType":"DInt","isArray":true,"arrayLength":4}""";
- var factory = new FakeAbCipTagFactory();
- var opts = new AbCipDriverOptions
- {
- Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [],
- };
- var drv = new AbCipDriver(opts, "abcip-eq-array", factory);
+ var (drv, factory) = NewDriver(
+ new AbCipTagDefinition("Recipe", Device, "Recipe", AbCipDataType.DInt, ElementCount: 4, IsArray: true));
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new ArrayFakeAbCipTag(p, new int[] { 7, 8, 9, 10 });
- var snapshots = await drv.ReadAsync([json], CancellationToken.None);
+ var snapshots = await drv.ReadAsync(["Recipe"], CancellationToken.None);
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
var value = snapshots.Single().Value.ShouldBeOfType();
@@ -212,80 +205,75 @@ public sealed class AbCipArrayTests
factory.Tags["Recipe"].CreationParams.ElementCount.ShouldBe(4);
}
- /// The parser threads arrayLength into the transient definition's ElementCount and sets IsArray.
+ /// The mapper threads arrayLength into the definition's ElementCount and sets IsArray.
[Fact]
- public void Parser_threads_arrayLength_into_ElementCount()
+ public void Mapper_threads_arrayLength_into_ElementCount()
{
var json = """{"tagPath":"Recipe","dataType":"DInt","isArray":true,"arrayLength":8}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
+ AbCipTagDefinitionFactory.FromTagConfig(json, "line1/eq/tag", out var def).ShouldBeTrue();
def!.ElementCount.ShouldBe(8);
def.IsArray.ShouldBeTrue();
}
- /// A non-array equipment ref defaults ElementCount to 1 (scalar) and IsArray false.
+ /// A non-array TagConfig defaults ElementCount to 1 (scalar) and IsArray false.
[Fact]
- public void Parser_defaults_ElementCount_to_one_when_not_an_array()
+ public void Mapper_defaults_ElementCount_to_one_when_not_an_array()
{
var json = """{"tagPath":"Recipe","dataType":"DInt"}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
+ AbCipTagDefinitionFactory.FromTagConfig(json, "line1/eq/tag", out var def).ShouldBeTrue();
def!.ElementCount.ShouldBe(1);
def.IsArray.ShouldBeFalse();
}
///
/// Review finding I-2 — isArray:true with NO arrayLength is a DEGENERATE input
- /// that must parse as SCALAR (IsArray false, ElementCount 1), matching every other driver
- /// (Modbus, S7, TwinCAT, AbLegacy). The AdminUI validator blocks authoring this combination,
- /// but the parser contract must still be consistent cross-driver.
+ /// that must map as SCALAR (IsArray false, ElementCount 1), matching every other driver.
///
[Fact]
- public void Parser_isArray_true_without_arrayLength_parses_as_scalar()
+ public void Mapper_isArray_true_without_arrayLength_maps_as_scalar()
{
var json = """{"tagPath":"Recipe","dataType":"DInt","isArray":true}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
+ AbCipTagDefinitionFactory.FromTagConfig(json, "line1/eq/tag", out var def).ShouldBeTrue();
def!.IsArray.ShouldBeFalse();
def.ElementCount.ShouldBe(1);
}
///
/// Review finding I-2 — isArray:true with an invalid (zero) arrayLength also
- /// parses as SCALAR, consistent with the cross-driver rule.
+ /// maps as SCALAR, consistent with the cross-driver rule.
///
[Fact]
- public void Parser_isArray_true_with_zero_arrayLength_parses_as_scalar()
+ public void Mapper_isArray_true_with_zero_arrayLength_maps_as_scalar()
{
var json = """{"tagPath":"Recipe","dataType":"DInt","isArray":true,"arrayLength":0}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
+ AbCipTagDefinitionFactory.FromTagConfig(json, "line1/eq/tag", out var def).ShouldBeTrue();
def!.IsArray.ShouldBeFalse();
def.ElementCount.ShouldBe(1);
}
///
/// Review finding I-1 — a 1-element array (isArray:true, arrayLength:1) is a valid
- /// 1-element array, NOT a scalar: the parser sets
- /// true and 1.
+ /// 1-element array, NOT a scalar: the mapper sets IsArray true and ElementCount 1.
///
[Fact]
- public void Parser_treats_isArray_with_arrayLength_one_as_a_one_element_array()
+ public void Mapper_treats_isArray_with_arrayLength_one_as_a_one_element_array()
{
var json = """{"tagPath":"Recipe","dataType":"DInt","isArray":true,"arrayLength":1}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
+ AbCipTagDefinitionFactory.FromTagConfig(json, "line1/eq/tag", out var def).ShouldBeTrue();
def!.IsArray.ShouldBeTrue();
def.ElementCount.ShouldBe(1);
}
///
- /// Review finding I-1 — isArray:true, arrayLength:1 must DISCOVER as a [1] array node
- /// (IsArray + ArrayDim 1), matching the foundation's materialisation, not as a scalar.
+ /// Review finding I-1 — a 1-element array tag must DISCOVER as a [1] array node (IsArray +
+ /// ArrayDim 1), matching the foundation's materialisation, not as a scalar.
///
[Fact]
- public async Task Equipment_ref_isArray_arrayLength_one_discovers_as_one_element_array()
+ public async Task Authored_one_element_array_discovers_as_one_element_array()
{
- var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","tagPath":"Recipe","dataType":"DInt","isArray":true,"arrayLength":1}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
-
var builder = new RecordingBuilder();
- var (drv, _) = NewDriver(def!);
+ var (drv, _) = NewDriver(
+ new AbCipTagDefinition("Recipe", Device, "Recipe", AbCipDataType.DInt, ElementCount: 1, IsArray: true));
await drv.InitializeAsync("{}", CancellationToken.None);
await drv.DiscoverAsync(builder, CancellationToken.None);
@@ -296,25 +284,17 @@ public sealed class AbCipArrayTests
}
///
- /// Review finding I-1 — the I-1 case: an isArray:true, arrayLength:1 equipment tag
- /// reads a 1-ELEMENT typed array, NOT a scalar. On current code (gate ElementCount > 1)
- /// this reads a scalar; the explicit IsArray flag fixes it.
+ /// Review finding I-1 — a 1-element array tag reads a 1-ELEMENT typed array, NOT a scalar.
///
[Fact]
- public async Task Equipment_ref_isArray_arrayLength_one_reads_as_one_element_array()
+ public async Task Authored_one_element_array_reads_as_one_element_array()
{
- var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","tagPath":"Recipe","dataType":"DInt","isArray":true,"arrayLength":1}""";
- var factory = new FakeAbCipTagFactory();
- var opts = new AbCipDriverOptions
- {
- Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [],
- };
- var drv = new AbCipDriver(opts, "abcip-eq-array1", factory);
+ var (drv, factory) = NewDriver(
+ new AbCipTagDefinition("Recipe", Device, "Recipe", AbCipDataType.DInt, ElementCount: 1, IsArray: true));
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new ArrayFakeAbCipTag(p, new int[] { 99 });
- var snapshots = await drv.ReadAsync([json], CancellationToken.None);
+ var snapshots = await drv.ReadAsync(["Recipe"], CancellationToken.None);
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
var value = snapshots.Single().Value.ShouldBeOfType();
@@ -324,24 +304,18 @@ public sealed class AbCipArrayTests
}
///
- /// Regression — a genuinely scalar equipment ref (isArray:false) reads a boxed
- /// scalar via , never an array.
+ /// Regression — a genuinely scalar tag (isArray:false) reads a boxed scalar via
+ /// , never an array.
///
[Fact]
- public async Task Equipment_ref_isArray_false_reads_as_scalar()
+ public async Task Scalar_tag_reads_as_scalar()
{
- var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","tagPath":"Speed","dataType":"DInt","isArray":false}""";
- var factory = new FakeAbCipTagFactory();
- var opts = new AbCipDriverOptions
- {
- Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [],
- };
- var drv = new AbCipDriver(opts, "abcip-eq-scalar", factory);
+ var (drv, factory) = NewDriver(
+ new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt));
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new FakeAbCipTag(p) { Value = 4200 };
- var snapshots = await drv.ReadAsync([json], CancellationToken.None);
+ var snapshots = await drv.ReadAsync(["Speed"], CancellationToken.None);
snapshots.Single().Value.ShouldBe(4200);
snapshots.Single().Value.ShouldNotBeOfType();
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipBoolInDIntRmwTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipBoolInDIntRmwTests.cs
index ad0f73c2..21bd9ddd 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipBoolInDIntRmwTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipBoolInDIntRmwTests.cs
@@ -29,10 +29,9 @@ public sealed class AbCipBoolInDIntRmwTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags =
- [
- new AbCipTagDefinition("Flag3", "ab://10.0.0.5/1,0", "Motor.Flags.3", AbCipDataType.Bool),
- ],
+ RawTags = AbCipRawTags.From(
+ new AbCipTagDefinition("Flag3", "ab://10.0.0.5/1,0", "Motor.Flags.3", AbCipDataType.Bool))
+ ,
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -60,7 +59,7 @@ public sealed class AbCipBoolInDIntRmwTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [new AbCipTagDefinition("F", "ab://10.0.0.5/1,0", "Motor.Flags.3", AbCipDataType.Bool)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("F", "ab://10.0.0.5/1,0", "Motor.Flags.3", AbCipDataType.Bool)),
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -86,7 +85,7 @@ public sealed class AbCipBoolInDIntRmwTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = tags,
+ RawTags = AbCipRawTags.From(tags),
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -108,11 +107,10 @@ public sealed class AbCipBoolInDIntRmwTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("A", "ab://10.0.0.5/1,0", "Motor1.Flags.0", AbCipDataType.Bool),
- new AbCipTagDefinition("B", "ab://10.0.0.5/1,0", "Motor2.Flags.0", AbCipDataType.Bool),
- ],
+ new AbCipTagDefinition("B", "ab://10.0.0.5/1,0", "Motor2.Flags.0", AbCipDataType.Bool))
+ ,
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -135,11 +133,10 @@ public sealed class AbCipBoolInDIntRmwTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("Bit0", "ab://10.0.0.5/1,0", "Flags.0", AbCipDataType.Bool),
- new AbCipTagDefinition("Bit5", "ab://10.0.0.5/1,0", "Flags.5", AbCipDataType.Bool),
- ],
+ new AbCipTagDefinition("Bit5", "ab://10.0.0.5/1,0", "Flags.5", AbCipDataType.Bool))
+ ,
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipConnectBackoffTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipConnectBackoffTests.cs
index 5c5ae0fd..4e1d4900 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipConnectBackoffTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipConnectBackoffTests.cs
@@ -17,7 +17,7 @@ public sealed class AbCipConnectBackoffTests
private static AbCipDriverOptions Options(bool probeEnabled) => new()
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Motor1.Speed", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Motor1.Speed", AbCipDataType.DInt)),
Probe = new AbCipProbeOptions
{
Enabled = probeEnabled,
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverCodeReviewRegressionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverCodeReviewRegressionTests.cs
index 477a6bb6..dbc39685 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverCodeReviewRegressionTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverCodeReviewRegressionTests.cs
@@ -18,17 +18,18 @@ public sealed class AbCipDriverCodeReviewRegressionTests
// ---- Driver.AbCip-001 — ReinitializeAsync must apply a changed config JSON ----
- /// Tests that InitializeAsync applies devices and tags from the config JSON.
+ /// Tests that InitializeAsync applies devices and raw tags from the config JSON.
[Fact]
public async Task InitializeAsync_applies_devices_and_tags_from_the_config_json()
{
- // Constructed with NO devices/tags — the JSON is the only source of config.
+ // Constructed with NO devices/tags — the JSON is the only source of config. v3: tags arrive as
+ // RawTags (RawPath + TagConfig blob + DeviceName routing key).
var drv = new AbCipDriver(new AbCipDriverOptions(), "drv-1");
const string json = """
{
"Devices": [ { "HostAddress": "ab://10.0.0.9/1,0", "PlcFamily": "ControlLogix" } ],
- "Tags": [ { "Name": "Speed", "DeviceHostAddress": "ab://10.0.0.9/1,0",
- "TagPath": "Speed", "DataType": "DInt" } ]
+ "RawTags": [ { "RawPath": "line1/speed", "TagConfig": "{\"tagPath\":\"Speed\",\"dataType\":\"DInt\"}",
+ "WriteIdempotent": false, "DeviceName": "ab://10.0.0.9/1,0" } ]
}
""";
@@ -92,14 +93,12 @@ public sealed class AbCipDriverCodeReviewRegressionTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("Motor", Device, "Motor", AbCipDataType.Structure, Members:
[
new AbCipStructureMember("Speed", AbCipDataType.DInt),
new AbCipStructureMember("Torque", AbCipDataType.Real),
- ]),
- ],
+ ])),
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -186,7 +185,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags = [new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)),
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -228,7 +227,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags = [new AbCipTagDefinition("Counter", Device, "Counter", AbCipDataType.UDInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Counter", Device, "Counter", AbCipDataType.UDInt)),
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -254,14 +253,12 @@ public sealed class AbCipDriverCodeReviewRegressionTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("Motor", Device, "Motor", AbCipDataType.Structure, Members:
[
new AbCipStructureMember("Speed", AbCipDataType.DInt),
new AbCipStructureMember("Torque", AbCipDataType.Real),
- ]),
- ],
+ ])),
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -281,11 +278,9 @@ public sealed class AbCipDriverCodeReviewRegressionTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt),
- new AbCipTagDefinition("Speed", Device, "SpeedAlias", AbCipDataType.Real), // same name
- ],
+ new AbCipTagDefinition("Speed", Device, "SpeedAlias", AbCipDataType.Real)), // same name
}, "drv-1");
Should.Throw(() =>
@@ -301,14 +296,12 @@ public sealed class AbCipDriverCodeReviewRegressionTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("Motor", Device, "Motor", AbCipDataType.Structure, Members:
[
new AbCipStructureMember("Speed", AbCipDataType.DInt),
]),
- new AbCipTagDefinition("Motor.Speed", Device, "Motor.Speed", AbCipDataType.DInt), // collision
- ],
+ new AbCipTagDefinition("Motor.Speed", Device, "Motor.Speed", AbCipDataType.DInt)), // collision
}, "drv-1");
Should.Throw(() =>
@@ -336,7 +329,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags = [new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)),
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverDiscoveryTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverDiscoveryTests.cs
index 8c6e3342..8e860f9e 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverDiscoveryTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverDiscoveryTests.cs
@@ -19,11 +19,9 @@ public sealed class AbCipDriverDiscoveryTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", DeviceName: "Line1-PLC")],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Motor1.Speed", AbCipDataType.DInt),
- new AbCipTagDefinition("Temperature", "ab://10.0.0.5/1,0", "T", AbCipDataType.Real, Writable: false),
- ],
+ new AbCipTagDefinition("Temperature", "ab://10.0.0.5/1,0", "T", AbCipDataType.Real, Writable: false)),
}, "drv-1");
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -61,12 +59,10 @@ public sealed class AbCipDriverDiscoveryTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("__DEFVAL_X", "ab://10.0.0.5/1,0", "__DEFVAL_X", AbCipDataType.DInt),
new AbCipTagDefinition("Routine:SomeRoutine", "ab://10.0.0.5/1,0", "R", AbCipDataType.DInt),
- new AbCipTagDefinition("UserTag", "ab://10.0.0.5/1,0", "U", AbCipDataType.DInt),
- ],
+ new AbCipTagDefinition("UserTag", "ab://10.0.0.5/1,0", "U", AbCipDataType.DInt)),
}, "drv-1");
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -83,7 +79,7 @@ public sealed class AbCipDriverDiscoveryTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [new AbCipTagDefinition("Orphan", "ab://10.0.0.99/1,0", "O", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Orphan", "ab://10.0.0.99/1,0", "O", AbCipDataType.DInt)),
}, "drv-1");
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -418,21 +414,19 @@ public sealed class AbCipDriverDiscoveryTests
}
///
- /// A discovered member-path read goes end-to-end through the driver: with NO pre-declared
- /// tag for the member, resolves the authored TagConfig
- /// JSON blob (FullName = Parent.Member) via the equipment-tag resolver, materialises a
- /// runtime on the device, reads it, and decodes the member's atomic value — proving discovered
- /// members are readable with no extra registration. The libplctag tag name the driver builds
- /// for the member equals the dotted member path.
+ /// A member-path read goes end-to-end through the driver: an authored raw tag whose TagConfig
+ /// addresses a dotted member path (Motor1.Status.Code) is delivered as a
+ /// , resolves by its RawPath, materialises a runtime on the device, and
+ /// decodes the member's atomic value. The libplctag tag name the driver builds equals the dotted
+ /// member path (the TagConfig's tagPath), independent of the RawPath identity.
///
[Fact]
- public async Task Discovered_member_path_reads_through_driver_as_member_atomic_type()
+ public async Task Member_path_tag_reads_through_driver_as_member_atomic_type()
{
const string device = "ab://10.0.0.5/1,0";
- // The wire reference handed to ReadAsync for a discovered member is the authored TagConfig
- // JSON blob (FullName = Motor1.Status.Code, atomic type Real here). No tag is pre-declared.
- const string tagConfig =
- "{\"tagPath\":\"Motor1.Status.Code\",\"dataType\":\"Real\",\"deviceHostAddress\":\"ab://10.0.0.5/1,0\"}";
+ const string rawPath = "line1/motor1/status/code";
+ // The authored TagConfig addresses the dotted member path (atomic Real here).
+ const string tagConfig = "{\"tagPath\":\"Motor1.Status.Code\",\"dataType\":\"Real\"}";
var tagFactory = new FakeAbCipTagFactory
{
@@ -443,16 +437,17 @@ public sealed class AbCipDriverDiscoveryTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(device)],
+ RawTags = [new RawTagEntry(rawPath, tagConfig, WriteIdempotent: false, DeviceName: device)],
}, "drv-1", tagFactory: tagFactory);
await drv.InitializeAsync("{}", CancellationToken.None);
- var results = await drv.ReadAsync([tagConfig], CancellationToken.None);
+ var results = await drv.ReadAsync([rawPath], CancellationToken.None);
results.Count.ShouldBe(1);
results[0].StatusCode.ShouldBe(AbCipStatusMapper.Good);
results[0].Value.ShouldBe(12.5f);
// The driver built a runtime under the dotted member tag name — confirming the member-path
- // read addresses the member directly (no parent-UDT registration needed).
+ // read addresses the member directly.
tagFactory.Tags.ShouldContainKey("Motor1.Status.Code");
}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs
index e37b1a0f..735a9c35 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs
@@ -15,7 +15,7 @@ public sealed class AbCipDriverReadTests
var opts = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = tags,
+ RawTags = AbCipRawTags.From(tags),
};
var drv = new AbCipDriver(opts, "drv-1", factory);
return (drv, factory);
@@ -42,7 +42,7 @@ public sealed class AbCipDriverReadTests
var opts = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [new AbCipTagDefinition("Orphan", "ab://10.0.0.99/1,0", "Tag1", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Orphan", "ab://10.0.0.99/1,0", "Tag1", AbCipDataType.DInt)),
};
var drv = new AbCipDriver(opts, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWholeUdtReadTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWholeUdtReadTests.cs
index 56f48150..b5403ca9 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWholeUdtReadTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWholeUdtReadTests.cs
@@ -19,7 +19,7 @@ public sealed class AbCipDriverWholeUdtReadTests
var opts = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags = tags,
+ RawTags = AbCipRawTags.From(tags),
// Whole-UDT grouping is opt-in (Driver.AbCip-003) — these tests exercise the
// grouping fast path, so they switch it on explicitly.
EnableDeclarationOnlyUdtGrouping = true,
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWriteTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWriteTests.cs
index 9340dec3..2417399b 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWriteTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWriteTests.cs
@@ -15,7 +15,7 @@ public sealed class AbCipDriverWriteTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = tags,
+ RawTags = AbCipRawTags.From(tags),
}, "drv-1", factory);
return (drv, factory);
}
@@ -74,7 +74,7 @@ public sealed class AbCipDriverWriteTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [new AbCipTagDefinition("Flag3", "ab://10.0.0.5/1,0", "Flags.3", AbCipDataType.Bool)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Flag3", "ab://10.0.0.5/1,0", "Flags.3", AbCipDataType.Bool)),
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -115,7 +115,7 @@ public sealed class AbCipDriverWriteTests
var drv2 = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt)),
}, "drv-2", factory);
await drv2.InitializeAsync("{}", CancellationToken.None);
@@ -133,7 +133,7 @@ public sealed class AbCipDriverWriteTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [new AbCipTagDefinition("Narrow", "ab://10.0.0.5/1,0", "N", AbCipDataType.Int)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Narrow", "ab://10.0.0.5/1,0", "N", AbCipDataType.Int)),
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -167,12 +167,11 @@ public sealed class AbCipDriverWriteTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("A", "ab://10.0.0.5/1,0", "A", AbCipDataType.DInt),
new AbCipTagDefinition("B", "ab://10.0.0.5/1,0", "B", AbCipDataType.DInt, Writable: false),
- new AbCipTagDefinition("C", "ab://10.0.0.5/1,0", "C", AbCipDataType.DInt),
- ],
+ new AbCipTagDefinition("C", "ab://10.0.0.5/1,0", "C", AbCipDataType.DInt))
+ ,
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipEquipmentTagParserStrictnessTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipEquipmentTagParserStrictnessTests.cs
index 0441f976..94b6c845 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipEquipmentTagParserStrictnessTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipEquipmentTagParserStrictnessTests.cs
@@ -12,14 +12,14 @@ public sealed class AbCipEquipmentTagParserStrictnessTests
[Fact]
public void Typo_dataType_rejects_the_tag()
{
- AbCipEquipmentTagParser.TryParse("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DIntt\"}", out _)
+ AbCipTagDefinitionFactory.FromTagConfig("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DIntt\"}", "line1/eq/tag", out _)
.ShouldBeFalse();
}
[Fact]
public void Valid_dataType_still_parses()
{
- AbCipEquipmentTagParser.TryParse("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DInt\"}", out var def)
+ AbCipTagDefinitionFactory.FromTagConfig("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DInt\"}", "line1/eq/tag", out var def)
.ShouldBeTrue();
def.DataType.ShouldBe(AbCipDataType.DInt);
}
@@ -27,7 +27,7 @@ public sealed class AbCipEquipmentTagParserStrictnessTests
[Fact]
public void Inspect_reports_typo_dataType()
{
- var warnings = AbCipEquipmentTagParser.Inspect("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DIntt\"}");
+ var warnings = AbCipTagDefinitionFactory.Inspect("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DIntt\"}");
warnings.ShouldHaveSingleItem();
warnings[0].ShouldContain("DIntt");
warnings[0].ShouldContain("dataType");
@@ -36,13 +36,13 @@ public sealed class AbCipEquipmentTagParserStrictnessTests
[Fact]
public void Inspect_clean_config_has_no_warnings()
{
- AbCipEquipmentTagParser.Inspect("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DInt\"}").ShouldBeEmpty();
+ AbCipTagDefinitionFactory.Inspect("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DInt\"}").ShouldBeEmpty();
}
[Fact]
public void Writable_false_is_honoured_bit_identical()
{
- AbCipEquipmentTagParser.TryParse("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DInt\",\"writable\":false}", out var def)
+ AbCipTagDefinitionFactory.FromTagConfig("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DInt\",\"writable\":false}", "line1/eq/tag", out var def)
.ShouldBeTrue();
def.Writable.ShouldBeFalse();
}
@@ -50,7 +50,7 @@ public sealed class AbCipEquipmentTagParserStrictnessTests
[Fact]
public void Writable_absent_defaults_to_true()
{
- AbCipEquipmentTagParser.TryParse("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DInt\"}", out var def)
+ AbCipTagDefinitionFactory.FromTagConfig("{\"tagPath\":\"Motor.Speed\",\"dataType\":\"DInt\"}", "line1/eq/tag", out var def)
.ShouldBeTrue();
def.Writable.ShouldBeTrue();
}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipEquipmentTagParserTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipEquipmentTagParserTests.cs
deleted file mode 100644
index c434d1b4..00000000
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipEquipmentTagParserTests.cs
+++ /dev/null
@@ -1,142 +0,0 @@
-using Shouldly;
-using Xunit;
-using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
-
-namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
-
-///
-/// Dedicated unit tests for .
-/// Covers all distinct outcome branches: valid scalar, 1-element array, N-element array,
-/// degenerate array shapes, non-JSON input, non-object JSON, blank/missing tagPath,
-/// the writable field, and the Structure dataType path (Driver.AbCip.Contracts-004).
-///
-[Trait("Category", "Unit")]
-public class AbCipEquipmentTagParserTests
-{
- // ── Happy-path scalar ────────────────────────────────────────────────────────────────
-
- [Fact]
- public void Valid_scalar_round_trip_parses_all_fields()
- {
- var json = """{"deviceHostAddress":"ab://10.0.0.1/1,0","tagPath":"Motor.Speed","dataType":"Real"}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
- def!.Name.ShouldBe(json);
- def.TagPath.ShouldBe("Motor.Speed");
- def.DeviceHostAddress.ShouldBe("ab://10.0.0.1/1,0");
- def.DataType.ShouldBe(AbCipDataType.Real);
- def.Writable.ShouldBeTrue();
- def.IsArray.ShouldBeFalse();
- def.ElementCount.ShouldBe(1);
- }
-
- // ── Array shape ──────────────────────────────────────────────────────────────────────
-
- [Fact]
- public void One_element_array_isArray_true_arrayLength_1_is_an_array_not_a_scalar()
- {
- var json = """{"tagPath":"Tags[0]","dataType":"DInt","isArray":true,"arrayLength":1}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
- def!.IsArray.ShouldBeTrue();
- def.ElementCount.ShouldBe(1);
- }
-
- [Fact]
- public void N_element_array_isArray_true_arrayLength_N_parses_correctly()
- {
- var json = """{"tagPath":"Buf","dataType":"SInt","isArray":true,"arrayLength":8}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
- def!.IsArray.ShouldBeTrue();
- def.ElementCount.ShouldBe(8);
- }
-
- [Fact]
- public void IsArray_true_arrayLength_0_is_canonical_scalar()
- {
- // Canonical rule: isArray:true AND arrayLength < 1 → scalar.
- var json = """{"tagPath":"PT_101","isArray":true,"arrayLength":0}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
- def!.IsArray.ShouldBeFalse();
- def.ElementCount.ShouldBe(1);
- }
-
- [Fact]
- public void IsArray_true_arrayLength_absent_is_canonical_scalar()
- {
- // Canonical rule: isArray:true but arrayLength absent → scalar.
- var json = """{"tagPath":"PT_101","isArray":true}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
- def!.IsArray.ShouldBeFalse();
- def.ElementCount.ShouldBe(1);
- }
-
- // ── Rejection paths ──────────────────────────────────────────────────────────────────
-
- [Fact]
- public void Non_JSON_input_returns_false()
- => AbCipEquipmentTagParser.TryParse("not json at all", out _).ShouldBeFalse();
-
- [Fact]
- public void Non_object_JSON_array_returns_false()
- => AbCipEquipmentTagParser.TryParse("""["tagPath","foo"]""", out _).ShouldBeFalse();
-
- [Fact]
- public void Non_object_JSON_string_returns_false()
- => AbCipEquipmentTagParser.TryParse("\"Motor.Speed\"", out _).ShouldBeFalse();
-
- [Fact]
- public void Missing_tagPath_returns_false()
- => AbCipEquipmentTagParser.TryParse("""{"dataType":"DInt"}""", out _).ShouldBeFalse();
-
- [Fact]
- public void Blank_tagPath_returns_false()
- => AbCipEquipmentTagParser.TryParse("""{"tagPath":" "}""", out _).ShouldBeFalse();
-
- [Fact]
- public void TagPath_as_number_returns_false()
- => AbCipEquipmentTagParser.TryParse("""{"tagPath":42}""", out _).ShouldBeFalse();
-
- // ── Writable field (Driver.AbCip.Contracts-001) ───────────────────────────────────────
-
- [Fact]
- public void Writable_false_is_honoured()
- {
- var json = """{"tagPath":"Sensor.Val","writable":false}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
- def!.Writable.ShouldBeFalse();
- }
-
- [Fact]
- public void Writable_absent_defaults_to_true()
- {
- var json = """{"tagPath":"Sensor.Val"}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
- def!.Writable.ShouldBeTrue();
- }
-
- [Fact]
- public void Writable_true_explicit_is_honoured()
- {
- var json = """{"tagPath":"Sensor.Val","writable":true}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
- def!.Writable.ShouldBeTrue();
- }
-
- // ── Structure dataType (Driver.AbCip.Contracts-001 Structure concern) ─────────────────
-
- ///
- /// A "dataType":"Structure" equipment-tag input is accepted and produces a Structure-typed
- /// definition with Members:null. The driver treats the tag path as a black-box dotted-path
- /// read (libplctag resolves the full path); UDT member declarations are not supported in the
- /// equipment-tag flow. This test documents current behaviour so a future change to reject
- /// Structure is a conscious choice.
- ///
- [Fact]
- public void Structure_dataType_is_accepted_with_null_Members_and_returns_true()
- {
- var json = """{"tagPath":"Motor","dataType":"Structure"}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
- def!.DataType.ShouldBe(AbCipDataType.Structure);
- def.Members.ShouldBeNull();
- def.TagPath.ShouldBe("Motor");
- }
-}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipEquipmentTagTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipEquipmentTagTests.cs
index af351073..bfb8fa69 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipEquipmentTagTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipEquipmentTagTests.cs
@@ -7,14 +7,16 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
[Trait("Category", "Unit")]
public class AbCipEquipmentTagTests
{
+ private const string Raw = "line1/eq/tag";
+
[Fact]
- public void Parses_equipment_tagconfig_into_a_transient_definition()
+ public void Parses_equipment_tagconfig_into_a_definition()
{
var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","tagPath":"Motor1.Speed","dataType":"Real"}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
- def!.Name.ShouldBe(json);
+ AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
+ def!.Name.ShouldBe(Raw);
def.TagPath.ShouldBe("Motor1.Speed");
- def.DeviceHostAddress.ShouldBe("ab://10.0.0.5/1,0");
+ def.DeviceHostAddress.ShouldBe(""); // v3: mapper never reads deviceHostAddress
def.DataType.ShouldBe(AbCipDataType.Real);
def.Writable.ShouldBeTrue();
}
@@ -23,83 +25,82 @@ public class AbCipEquipmentTagTests
public void Defaults_optional_fields_when_only_the_mandatory_tagPath_is_present()
{
var json = """{"tagPath":"PT_101"}""";
- AbCipEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
- def!.Name.ShouldBe(json);
+ AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
+ def!.Name.ShouldBe(Raw);
def.TagPath.ShouldBe("PT_101");
- def.DeviceHostAddress.ShouldBe(""); // absent → empty, matching AbCipTagConfigModel default
+ def.DeviceHostAddress.ShouldBe(""); // routing key travels on the RawTagEntry, not the blob
def.DataType.ShouldBe(AbCipDataType.DInt); // AbCipTagConfigModel's default atomic type
}
[Fact]
public void Rejects_a_non_abcip_blob()
- => AbCipEquipmentTagParser.TryParse("""{"region":"x"}""", out _).ShouldBeFalse();
+ => AbCipTagDefinitionFactory.FromTagConfig("""{"region":"x"}""", Raw, out _).ShouldBeFalse();
[Fact]
public void Rejects_a_blob_with_an_empty_tagPath()
- => AbCipEquipmentTagParser.TryParse("""{"tagPath":" ","dataType":"DInt"}""", out _).ShouldBeFalse();
+ => AbCipTagDefinitionFactory.FromTagConfig("""{"tagPath":" ","dataType":"DInt"}""", Raw, out _).ShouldBeFalse();
[Fact]
public void Rejects_tagPath_given_as_a_non_string()
- => AbCipEquipmentTagParser.TryParse("""{"tagPath":42}""", out _).ShouldBeFalse();
+ => AbCipTagDefinitionFactory.FromTagConfig("""{"tagPath":42}""", Raw, out _).ShouldBeFalse();
[Fact]
public void Rejects_garbage()
- => AbCipEquipmentTagParser.TryParse("not json", out _).ShouldBeFalse();
-
- [Fact]
- public void Rejects_a_plain_authored_name_without_a_leading_brace()
- => AbCipEquipmentTagParser.TryParse("Motor1.Speed", out _).ShouldBeFalse();
+ => AbCipTagDefinitionFactory.FromTagConfig("not json", Raw, out _).ShouldBeFalse();
///
- /// End-to-end driver-level proof: an AbCip driver with NO authored tags can still read an
- /// equipment-tag ref (the raw TagConfig JSON) — the resolver parses it into a transient
- /// definition (whose DeviceHostAddress names a configured device) and the read goes to the
- /// wire instead of returning BadNodeIdUnknown.
+ /// End-to-end driver-level proof: an authored raw AbCip tag (RawPath + TagConfig blob delivered as a
+ /// RawTagEntry) resolves by its RawPath and the read goes to the wire instead of returning
+ /// BadNodeIdUnknown. This is the v3 replacement for the retired blob-parse-fallback resolver.
///
[Fact]
- public async Task Driver_resolves_an_equipment_ref_and_reads_instead_of_BadNodeIdUnknown()
+ public async Task Driver_resolves_a_raw_tag_and_reads_instead_of_BadNodeIdUnknown()
{
- var json = """{"deviceHostAddress":"ab://10.0.0.5/1,0","tagPath":"Motor1.Speed","dataType":"DInt"}""";
+ const string device = "ab://10.0.0.5/1,0";
+ const string rawPath = "line1/motor/speed";
+ var tagConfig = """{"tagPath":"Motor1.Speed","dataType":"DInt"}""";
var factory = new FakeAbCipTagFactory();
var opts = new AbCipDriverOptions
{
- Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [],
+ Devices = [new AbCipDeviceOptions(device)],
+ RawTags = [new global::ZB.MOM.WW.OtOpcUa.Core.Abstractions.RawTagEntry(rawPath, tagConfig, WriteIdempotent: false, DeviceName: device)],
};
var drv = new AbCipDriver(opts, "abcip-eq", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new FakeAbCipTag(p) { Value = 4242 };
- var snapshots = await drv.ReadAsync([json], CancellationToken.None);
+ var snapshots = await drv.ReadAsync([rawPath], CancellationToken.None);
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
snapshots.Single().StatusCode.ShouldNotBe(AbCipStatusMapper.BadNodeIdUnknown);
snapshots.Single().Value.ShouldBe(4242);
+ // The runtime is keyed by the resolved libplctag tag path, not the RawPath identity.
+ factory.Tags.ShouldContainKey("Motor1.Speed");
}
///
- /// End-to-end driver-level proof of the R2-11 Phase C flip: an equipment-tag ref carrying a
- /// TYPO'd dataType enum now REJECTS at the resolver (strict parse ⇒ TryParse false)
- /// so the read surfaces — instead of the old
- /// lenient behaviour that silently defaulted to DInt and published a wrong-width
- /// Good. Same driver + device wiring as the clean-read proof above; only the enum is
- /// mistyped.
+ /// R2-11 Phase C strictness end-to-end: a raw tag whose TagConfig carries a TYPO'd dataType
+ /// fails to map (FromTagConfig ⇒ false) so it never enters the driver's RawPath table; a read
+ /// of that RawPath surfaces instead of a
+ /// silently-defaulted wrong-width Good.
///
[Fact]
- public async Task Driver_read_of_a_typod_dataType_ref_surfaces_BadNodeIdUnknown()
+ public async Task Driver_read_of_a_typod_dataType_tag_surfaces_BadNodeIdUnknown()
{
- var typoJson = """{"deviceHostAddress":"ab://10.0.0.5/1,0","tagPath":"Motor1.Speed","dataType":"DIntt"}""";
+ const string device = "ab://10.0.0.5/1,0";
+ const string rawPath = "line1/motor/speed";
+ var typoConfig = """{"tagPath":"Motor1.Speed","dataType":"DIntt"}""";
var factory = new FakeAbCipTagFactory();
var opts = new AbCipDriverOptions
{
- Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [],
+ Devices = [new AbCipDeviceOptions(device)],
+ RawTags = [new global::ZB.MOM.WW.OtOpcUa.Core.Abstractions.RawTagEntry(rawPath, typoConfig, WriteIdempotent: false, DeviceName: device)],
};
var drv = new AbCipDriver(opts, "abcip-eq", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => new FakeAbCipTag(p) { Value = 4242 };
- var snapshots = await drv.ReadAsync([typoJson], CancellationToken.None);
+ var snapshots = await drv.ReadAsync([rawPath], CancellationToken.None);
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipFactoryArrayTagTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipFactoryArrayTagTests.cs
index bda261fc..101d1154 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipFactoryArrayTagTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipFactoryArrayTagTests.cs
@@ -5,18 +5,18 @@ using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
///
-/// Regression coverage for Driver.AbCip-018 — the driver-config factory path
-/// () dropped the array shape of a
-/// pre-declared tag. A tags[] entry with "isArray": true, "elementCount": 4
-/// deserialised into a scalar (because AbCipTagDto /
-/// AbCipMemberDto had no ElementCount / IsArray fields), so the tag read
-/// as a single scalar despite the explicit array declaration. These tests assert the array
-/// shape now survives factory deserialization for both top-level tags and UDT members, and
-/// that omitting the fields keeps the legacy scalar default (additive change — no break).
+/// ParseOptions timeout hardening + the array-shape round-trip through the v3 tag mapper. Under v3 the
+/// factory no longer builds typed tag definitions (it carries authored RawTags blobs verbatim);
+/// the array/member shape survives the TagConfig round-trip in
+/// instead. These tests assert the array shape
+/// survives mapping for both top-level tags and declared UDT members, and that omitting the fields keeps
+/// the scalar default.
///
[Trait("Category", "Unit")]
public sealed class AbCipFactoryArrayTagTests
{
+ private const string Raw = "line1/eq/tag";
+
///
/// A non-positive TimeoutMs clamps to the 2 s default rather than reaching libplctag's
/// Tag.Timeout setter (which throws for a
@@ -44,103 +44,56 @@ public sealed class AbCipFactoryArrayTagTests
opts.Timeout.ShouldBe(TimeSpan.FromMilliseconds(500));
}
- /// A driver-config tag declaring isArray/elementCount produces an array definition.
+ /// A TagConfig declaring isArray/arrayLength maps to an array definition.
[Fact]
- public void ParseOptions_threads_isArray_and_elementCount_into_tag_definition()
+ public void FromTagConfig_threads_isArray_and_arrayLength_into_tag_definition()
{
- const string json = """
- {
- "Tags": [ {
- "Name": "Setpoints",
- "DeviceHostAddress": "ab://10.0.0.5/1,0",
- "TagPath": "Setpoints",
- "DataType": "Real",
- "isArray": true,
- "elementCount": 4
- } ]
- }
- """;
+ const string tagConfig = """{"tagPath":"Setpoints","dataType":"Real","isArray":true,"arrayLength":4}""";
- var opts = AbCipDriverFactoryExtensions.ParseOptions("drv-1", json);
+ AbCipTagDefinitionFactory.FromTagConfig(tagConfig, Raw, out var tag).ShouldBeTrue();
- var tag = opts.Tags.Single();
tag.IsArray.ShouldBeTrue();
tag.ElementCount.ShouldBe(4);
}
- /// A driver-config UDT member declaring isArray/elementCount produces an array member.
+ /// A declared UDT member declaring isArray/arrayLength maps to an array member.
[Fact]
- public void ParseOptions_threads_isArray_and_elementCount_into_structure_member()
+ public void FromTagConfig_threads_isArray_and_arrayLength_into_structure_member()
{
- const string json = """
- {
- "Tags": [ {
- "Name": "Motor",
- "DeviceHostAddress": "ab://10.0.0.5/1,0",
- "TagPath": "Motor",
- "DataType": "Structure",
- "Members": [ {
- "Name": "Setpoints",
- "DataType": "Real",
- "isArray": true,
- "elementCount": 4
- } ]
- } ]
- }
+ const string tagConfig = """
+ {"tagPath":"Motor","dataType":"Structure",
+ "members":[{"name":"Setpoints","dataType":"Real","isArray":true,"arrayLength":4}]}
""";
- var opts = AbCipDriverFactoryExtensions.ParseOptions("drv-1", json);
+ AbCipTagDefinitionFactory.FromTagConfig(tagConfig, Raw, out var tag).ShouldBeTrue();
- var member = opts.Tags.Single().Members!.Single();
+ var member = tag.Members!.Single();
member.IsArray.ShouldBeTrue();
member.ElementCount.ShouldBe(4);
}
- /// A driver-config tag without array fields stays scalar (additive change — no break).
+ /// A TagConfig without array fields stays scalar.
[Fact]
- public void ParseOptions_defaults_to_scalar_when_array_fields_absent()
+ public void FromTagConfig_defaults_to_scalar_when_array_fields_absent()
{
- const string json = """
- {
- "Tags": [ {
- "Name": "Speed",
- "DeviceHostAddress": "ab://10.0.0.5/1,0",
- "TagPath": "Speed",
- "DataType": "DInt"
- } ]
- }
- """;
+ const string tagConfig = """{"tagPath":"Speed","dataType":"DInt"}""";
- var opts = AbCipDriverFactoryExtensions.ParseOptions("drv-1", json);
+ AbCipTagDefinitionFactory.FromTagConfig(tagConfig, Raw, out var tag).ShouldBeTrue();
- var tag = opts.Tags.Single();
tag.IsArray.ShouldBeFalse();
tag.ElementCount.ShouldBe(1);
}
- /// A non-positive elementCount falls back to the scalar count of 1 (positive-value guard).
+ /// A non-positive arrayLength falls back to the scalar count of 1 (canonical rule: scalar).
[Fact]
- public void ParseOptions_guards_against_non_positive_elementCount()
+ public void FromTagConfig_guards_against_non_positive_arrayLength()
{
- const string json = """
- {
- "Tags": [ {
- "Name": "Speed",
- "DeviceHostAddress": "ab://10.0.0.5/1,0",
- "TagPath": "Speed",
- "DataType": "DInt",
- "isArray": true,
- "elementCount": 0
- } ]
- }
- """;
+ const string tagConfig = """{"tagPath":"Speed","dataType":"DInt","isArray":true,"arrayLength":0}""";
- var opts = AbCipDriverFactoryExtensions.ParseOptions("drv-1", json);
+ AbCipTagDefinitionFactory.FromTagConfig(tagConfig, Raw, out var tag).ShouldBeTrue();
- var tag = opts.Tags.Single();
- // elementCount <= 0 is degenerate — count clamps to 1; the explicit IsArray flag still
- // carries (a 1-element array is valid), mirroring the equipment-tag parser's contract.
+ // arrayLength <= 0 with isArray:true is degenerate — canonical rule collapses to scalar.
tag.ElementCount.ShouldBe(1);
- tag.IsArray.ShouldBeTrue();
+ tag.IsArray.ShouldBeFalse();
}
}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipHostProbeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipHostProbeTests.cs
index 4e8366ec..bbb25483 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipHostProbeTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipHostProbeTests.cs
@@ -172,11 +172,10 @@ public sealed class AbCipHostProbeTests
new AbCipDeviceOptions("ab://10.0.0.5/1,0"),
new AbCipDeviceOptions("ab://10.0.0.6/1,0"),
],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("A", "ab://10.0.0.5/1,0", "A", AbCipDataType.DInt),
- new AbCipTagDefinition("B", "ab://10.0.0.6/1,0", "B", AbCipDataType.DInt),
- ],
+ new AbCipTagDefinition("B", "ab://10.0.0.6/1,0", "B", AbCipDataType.DInt))
+ ,
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1");
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -216,11 +215,10 @@ public sealed class AbCipHostProbeTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.7/1,0")],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("Motor1", "ab://10.0.0.7/1,0", "Motor1", AbCipDataType.Structure,
- Members: [new AbCipStructureMember("Speed", AbCipDataType.DInt)]),
- ],
+ Members: [new AbCipStructureMember("Speed", AbCipDataType.DInt)]))
+ ,
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1");
await drv.InitializeAsync("{}", CancellationToken.None);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipLoggingTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipLoggingTests.cs
index 73f2f5b8..11123518 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipLoggingTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipLoggingTests.cs
@@ -93,7 +93,7 @@ public sealed class AbCipLoggingTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags = [new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)),
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory, logger: logger);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -124,7 +124,7 @@ public sealed class AbCipLoggingTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags = [new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)),
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory, logger: logger);
await drv.InitializeAsync("{}", CancellationToken.None);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipPerDeviceConnectionOptionsTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipPerDeviceConnectionOptionsTests.cs
index c290672f..78077e41 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipPerDeviceConnectionOptionsTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipPerDeviceConnectionOptionsTests.cs
@@ -29,7 +29,7 @@ public sealed class AbCipPerDeviceConnectionOptionsTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device, AllowPacking: false)],
- Tags = [new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)),
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -49,7 +49,7 @@ public sealed class AbCipPerDeviceConnectionOptionsTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags = [new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)),
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -69,7 +69,7 @@ public sealed class AbCipPerDeviceConnectionOptionsTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(micro, AbCipPlcFamily.Micro800)],
- Tags = [new AbCipTagDefinition("X", micro, "X", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("X", micro, "X", AbCipDataType.DInt)),
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -88,7 +88,7 @@ public sealed class AbCipPerDeviceConnectionOptionsTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device, ConnectionSize: 504)],
- Tags = [new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)),
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -106,7 +106,7 @@ public sealed class AbCipPerDeviceConnectionOptionsTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Device)],
- Tags = [new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)),
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipPlcFamilyTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipPlcFamilyTests.cs
index 78b496c2..13acd500 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipPlcFamilyTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipPlcFamilyTests.cs
@@ -109,7 +109,7 @@ public sealed class AbCipPlcFamilyTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://192.168.1.20/", AbCipPlcFamily.Micro800)],
- Tags = [new AbCipTagDefinition("X", "ab://192.168.1.20/", "X", AbCipDataType.DInt)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("X", "ab://192.168.1.20/", "X", AbCipDataType.DInt)),
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -141,12 +141,11 @@ public sealed class AbCipPlcFamilyTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.GuardLogix)],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("NormalTag", "ab://10.0.0.5/1,0", "N", AbCipDataType.DInt),
new AbCipTagDefinition("SafetyTag", "ab://10.0.0.5/1,0", "S", AbCipDataType.DInt,
- Writable: true, SafetyTag: true),
- ],
+ Writable: true, SafetyTag: true))
+ ,
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1");
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -167,11 +166,10 @@ public sealed class AbCipPlcFamilyTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.GuardLogix)],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("SafetySet", "ab://10.0.0.5/1,0", "S", AbCipDataType.DInt,
- Writable: true, SafetyTag: true),
- ],
+ Writable: true, SafetyTag: true))
+ ,
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipRawTags.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipRawTags.cs
new file mode 100644
index 00000000..f663a2c4
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipRawTags.cs
@@ -0,0 +1,26 @@
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
+
+///
+/// Test helper bridging the pre-v3 pre-declared-tag authoring style (a typed
+/// ) to the v3 RawPath seam the driver now consumes
+/// (). Each typed def is serialised to its TagConfig blob via
+/// , and the def's identity (Name → RawPath),
+/// device routing key (DeviceHostAddress → ) and write-idempotence
+/// travel on the entry — exactly the shape the deploy artifact delivers. Lets the driver's read / write /
+/// discovery suites keep expressing fixtures as typed definitions while exercising the v3
+/// RawTags → FromTagConfig table-build path.
+///
+internal static class AbCipRawTags
+{
+ /// Projects typed definitions to the driver's RawTags option shape.
+ /// The typed definitions to project.
+ /// The equivalent list.
+ public static IReadOnlyList From(params AbCipTagDefinition[] defs) =>
+ [.. defs.Select(d => new RawTagEntry(
+ RawPath: d.Name,
+ TagConfig: AbCipTagDefinitionFactory.ToTagConfig(d),
+ WriteIdempotent: d.WriteIdempotent,
+ DeviceName: d.DeviceHostAddress))];
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipResolveHostTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipResolveHostTests.cs
index fd305b8f..687acbc6 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipResolveHostTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipResolveHostTests.cs
@@ -1,14 +1,15 @@
using Shouldly;
using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
///
/// CONV-4 — keys per-host resilience (bulkhead / circuit
-/// breaker). It must resolve an equipment-tag reference (raw TagConfig JSON) to its OWN device
-/// host, not fall back to the first device — otherwise a broken device B trips device A's
-/// breaker. Authored tags and unknown refs keep the current fallback.
+/// breaker). It must resolve an authored tag (by its RawPath) to its OWN device host, not fall back
+/// to the first device — otherwise a broken device B trips device A's breaker. Unknown refs keep the
+/// current first-device fallback.
///
[Trait("Category", "Unit")]
public sealed class AbCipResolveHostTests
@@ -16,21 +17,27 @@ public sealed class AbCipResolveHostTests
private const string DeviceA = "ab://10.0.0.5/1,0";
private const string DeviceB = "ab://10.0.0.6/1,0";
- private static AbCipDriver NewDriver() => new(
+ private static AbCipDriver NewDriver(params RawTagEntry[] rawTags) => new(
new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(DeviceA), new AbCipDeviceOptions(DeviceB)],
+ RawTags = rawTags,
Probe = new AbCipProbeOptions { Enabled = false },
},
"abcip-1", new FakeAbCipTagFactory());
- /// An equipment-tag ref naming device B resolves to device B's host, not the first device.
+ /// An authored tag routed to device B resolves to device B's host, not the first device.
[Fact]
- public void ResolveHost_EquipmentTagRef_ReturnsItsOwnDeviceHost()
+ public async Task ResolveHost_TagRoutedToDeviceB_ReturnsItsOwnDeviceHost()
{
- var drv = NewDriver();
- var json = $$"""{"deviceHostAddress":"{{DeviceB}}","tagPath":"Motor1.Speed","dataType":"DInt"}""";
- drv.ResolveHost(json).ShouldBe(DeviceB);
+ var drv = NewDriver(new RawTagEntry(
+ RawPath: "line1/motorB/speed",
+ TagConfig: """{"tagPath":"Motor1.Speed","dataType":"DInt"}""",
+ WriteIdempotent: false,
+ DeviceName: DeviceB));
+ await drv.InitializeAsync("{}", CancellationToken.None);
+
+ drv.ResolveHost("line1/motorB/speed").ShouldBe(DeviceB);
}
/// An unresolvable ref keeps the current first-device fallback.
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipRuntimeConcurrencyTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipRuntimeConcurrencyTests.cs
index 118ad5a9..8c9bb24f 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipRuntimeConcurrencyTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipRuntimeConcurrencyTests.cs
@@ -70,7 +70,7 @@ public sealed class AbCipRuntimeConcurrencyTests
var opts = new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = tags,
+ RawTags = AbCipRawTags.From(tags),
};
return new AbCipDriver(opts, "drv-1", factory);
}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipSubscriptionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipSubscriptionTests.cs
index 1bb8c48c..901589a1 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipSubscriptionTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipSubscriptionTests.cs
@@ -15,7 +15,7 @@ public sealed class AbCipSubscriptionTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = tags,
+ RawTags = AbCipRawTags.From(tags),
}, "drv-1", factory);
return (drv, factory);
}
@@ -161,11 +161,10 @@ public sealed class AbCipSubscriptionTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("Motor1", "ab://10.0.0.5/1,0", "Motor1", AbCipDataType.Structure,
- Members: [new AbCipStructureMember("Speed", AbCipDataType.DInt)]),
- ],
+ Members: [new AbCipStructureMember("Speed", AbCipDataType.DInt)]))
+ ,
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
factory.Customise = p => p.TagName == "Motor1.Speed"
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipTagDefinitionFactoryTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipTagDefinitionFactoryTests.cs
new file mode 100644
index 00000000..62468459
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipTagDefinitionFactoryTests.cs
@@ -0,0 +1,182 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
+
+///
+/// Dedicated unit tests for .
+/// Covers all distinct outcome branches: valid scalar, 1-element array, N-element array,
+/// degenerate array shapes, non-JSON input, non-object JSON, blank/missing tagPath,
+/// the writable field, and the Structure dataType path. Under v3 the definition's
+/// identity (Name) is the RawPath the mapper is handed, not the blob, and the mapper does
+/// NOT read deviceHostAddress (device placement travels on the RawTagEntry).
+///
+[Trait("Category", "Unit")]
+public class AbCipTagDefinitionFactoryTests
+{
+ private const string Raw = "line1/eq/tag";
+
+ // ── Happy-path scalar ────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Valid_scalar_round_trip_parses_all_fields()
+ {
+ var json = """{"deviceHostAddress":"ab://10.0.0.1/1,0","tagPath":"Motor.Speed","dataType":"Real"}""";
+ AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
+ def!.Name.ShouldBe(Raw);
+ def.TagPath.ShouldBe("Motor.Speed");
+ def.DeviceHostAddress.ShouldBe(""); // v3: mapper never reads deviceHostAddress
+ def.DataType.ShouldBe(AbCipDataType.Real);
+ def.Writable.ShouldBeTrue();
+ def.IsArray.ShouldBeFalse();
+ def.ElementCount.ShouldBe(1);
+ }
+
+ // ── Array shape ──────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void One_element_array_isArray_true_arrayLength_1_is_an_array_not_a_scalar()
+ {
+ var json = """{"tagPath":"Tags[0]","dataType":"DInt","isArray":true,"arrayLength":1}""";
+ AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
+ def!.IsArray.ShouldBeTrue();
+ def.ElementCount.ShouldBe(1);
+ }
+
+ [Fact]
+ public void N_element_array_isArray_true_arrayLength_N_parses_correctly()
+ {
+ var json = """{"tagPath":"Buf","dataType":"SInt","isArray":true,"arrayLength":8}""";
+ AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
+ def!.IsArray.ShouldBeTrue();
+ def.ElementCount.ShouldBe(8);
+ }
+
+ [Fact]
+ public void IsArray_true_arrayLength_0_is_canonical_scalar()
+ {
+ // Canonical rule: isArray:true AND arrayLength < 1 → scalar.
+ var json = """{"tagPath":"PT_101","isArray":true,"arrayLength":0}""";
+ AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
+ def!.IsArray.ShouldBeFalse();
+ def.ElementCount.ShouldBe(1);
+ }
+
+ [Fact]
+ public void IsArray_true_arrayLength_absent_is_canonical_scalar()
+ {
+ // Canonical rule: isArray:true but arrayLength absent → scalar.
+ var json = """{"tagPath":"PT_101","isArray":true}""";
+ AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
+ def!.IsArray.ShouldBeFalse();
+ def.ElementCount.ShouldBe(1);
+ }
+
+ // ── Rejection paths ──────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Non_JSON_input_returns_false()
+ => AbCipTagDefinitionFactory.FromTagConfig("not json at all", Raw, out _).ShouldBeFalse();
+
+ [Fact]
+ public void Non_object_JSON_array_returns_false()
+ => AbCipTagDefinitionFactory.FromTagConfig("""["tagPath","foo"]""", Raw, out _).ShouldBeFalse();
+
+ [Fact]
+ public void Non_object_JSON_string_returns_false()
+ => AbCipTagDefinitionFactory.FromTagConfig("\"Motor.Speed\"", Raw, out _).ShouldBeFalse();
+
+ [Fact]
+ public void Missing_tagPath_returns_false()
+ => AbCipTagDefinitionFactory.FromTagConfig("""{"dataType":"DInt"}""", Raw, out _).ShouldBeFalse();
+
+ [Fact]
+ public void Blank_tagPath_returns_false()
+ => AbCipTagDefinitionFactory.FromTagConfig("""{"tagPath":" "}""", Raw, out _).ShouldBeFalse();
+
+ [Fact]
+ public void TagPath_as_number_returns_false()
+ => AbCipTagDefinitionFactory.FromTagConfig("""{"tagPath":42}""", Raw, out _).ShouldBeFalse();
+
+ // ── Writable field ────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Writable_false_is_honoured()
+ {
+ var json = """{"tagPath":"Sensor.Val","writable":false}""";
+ AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
+ def!.Writable.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void Writable_absent_defaults_to_true()
+ {
+ var json = """{"tagPath":"Sensor.Val"}""";
+ AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
+ def!.Writable.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Writable_true_explicit_is_honoured()
+ {
+ var json = """{"tagPath":"Sensor.Val","writable":true}""";
+ AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
+ def!.Writable.ShouldBeTrue();
+ }
+
+ // ── Structure dataType ────────────────────────────────────────────────────────────────
+
+ ///
+ /// A "dataType":"Structure" equipment-tag input is accepted and produces a Structure-typed
+ /// definition with Members:null. The driver treats the tag path as a black-box dotted-path
+ /// read (libplctag resolves the full path); UDT member declarations are not authored via the
+ /// production equipment-tag flow (AbCipTagConfigModel emits no members key).
+ ///
+ [Fact]
+ public void Structure_dataType_is_accepted_with_null_Members_and_returns_true()
+ {
+ var json = """{"tagPath":"Motor","dataType":"Structure"}""";
+ AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
+ def!.DataType.ShouldBe(AbCipDataType.Structure);
+ def.Members.ShouldBeNull();
+ def.TagPath.ShouldBe("Motor");
+ }
+
+ // ── ToTagConfig / FromTagConfig round-trip (v3 RawTagEntry carrier) ────────────────────
+
+ ///
+ /// A typed definition serialised via and
+ /// re-read via reproduces every per-tag
+ /// field except the out-of-band device routing key + WriteIdempotent (which travel on the
+ /// RawTagEntry, not the blob). Covers the array + declared-member round-trip the driver-test
+ /// AbCipRawTags helper relies on.
+ ///
+ [Fact]
+ public void ToTagConfig_FromTagConfig_round_trips_arrays_and_members()
+ {
+ var def = new AbCipTagDefinition(
+ Name: "Motor1", DeviceHostAddress: "ab://10.0.0.5/1,0", TagPath: "Motor1",
+ DataType: AbCipDataType.Structure, Writable: false, SafetyTag: true,
+ Members:
+ [
+ new AbCipStructureMember("Speed", AbCipDataType.DInt),
+ new AbCipStructureMember("Buf", AbCipDataType.Real, Writable: false, ElementCount: 4, IsArray: true),
+ ]);
+
+ var blob = AbCipTagDefinitionFactory.ToTagConfig(def);
+ AbCipTagDefinitionFactory.FromTagConfig(blob, "Motor1", out var back).ShouldBeTrue();
+
+ back!.Name.ShouldBe("Motor1");
+ back.DeviceHostAddress.ShouldBe(""); // routing key not in the blob
+ back.TagPath.ShouldBe("Motor1");
+ back.DataType.ShouldBe(AbCipDataType.Structure);
+ back.Writable.ShouldBeFalse();
+ back.SafetyTag.ShouldBeTrue();
+ back.Members!.Count.ShouldBe(2);
+ back.Members[0].Name.ShouldBe("Speed");
+ back.Members[1].IsArray.ShouldBeTrue();
+ back.Members[1].ElementCount.ShouldBe(4);
+ back.Members[1].Writable.ShouldBeFalse();
+ }
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipUdtMemberTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipUdtMemberTests.cs
index fea9b9f6..50aab447 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipUdtMemberTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipUdtMemberTests.cs
@@ -16,8 +16,7 @@ public sealed class AbCipUdtMemberTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition(
Name: "Motor1",
DeviceHostAddress: "ab://10.0.0.5/1,0",
@@ -28,8 +27,8 @@ public sealed class AbCipUdtMemberTests
new AbCipStructureMember("Speed", AbCipDataType.DInt),
new AbCipStructureMember("Running", AbCipDataType.Bool, Writable: false),
new AbCipStructureMember("SetPoint", AbCipDataType.Real, WriteIdempotent: true),
- ]),
- ],
+ ]))
+ ,
}, "drv-1");
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -63,15 +62,14 @@ public sealed class AbCipUdtMemberTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("Motor1", "ab://10.0.0.5/1,0", "Motor1", AbCipDataType.Structure,
Members:
[
new AbCipStructureMember("Speed", AbCipDataType.DInt),
new AbCipStructureMember("Running", AbCipDataType.Bool),
- ]),
- ],
+ ]))
+ ,
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -91,14 +89,13 @@ public sealed class AbCipUdtMemberTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("Motor1", "ab://10.0.0.5/1,0", "Motor1", AbCipDataType.Structure,
Members:
[
new AbCipStructureMember("SetPoint", AbCipDataType.Real),
- ]),
- ],
+ ]))
+ ,
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -117,14 +114,13 @@ public sealed class AbCipUdtMemberTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("Motor1", "ab://10.0.0.5/1,0", "Motor1", AbCipDataType.Structure,
Members:
[
new AbCipStructureMember("Status", AbCipDataType.DInt, Writable: false),
- ]),
- ],
+ ]))
+ ,
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -145,7 +141,7 @@ public sealed class AbCipUdtMemberTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [new AbCipTagDefinition("OpaqueUdt", "ab://10.0.0.5/1,0", "OpaqueUdt", AbCipDataType.Structure)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("OpaqueUdt", "ab://10.0.0.5/1,0", "OpaqueUdt", AbCipDataType.Structure)),
}, "drv-1");
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -163,7 +159,7 @@ public sealed class AbCipUdtMemberTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags = [new AbCipTagDefinition("EmptyUdt", "ab://10.0.0.5/1,0", "E", AbCipDataType.Structure, Members: [])],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("EmptyUdt", "ab://10.0.0.5/1,0", "E", AbCipDataType.Structure, Members: [])),
}, "drv-1");
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -181,16 +177,15 @@ public sealed class AbCipUdtMemberTests
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
- Tags =
- [
+ RawTags = AbCipRawTags.From(
new AbCipTagDefinition("FlatA", "ab://10.0.0.5/1,0", "A", AbCipDataType.DInt),
new AbCipTagDefinition("Motor1", "ab://10.0.0.5/1,0", "Motor1", AbCipDataType.Structure,
Members:
[
new AbCipStructureMember("Speed", AbCipDataType.DInt),
]),
- new AbCipTagDefinition("FlatB", "ab://10.0.0.5/1,0", "B", AbCipDataType.Real),
- ],
+ new AbCipTagDefinition("FlatB", "ab://10.0.0.5/1,0", "B", AbCipDataType.Real))
+ ,
}, "drv-1");
await drv.InitializeAsync("{}", CancellationToken.None);