v3(b1-abcip): RawPath read-path for AbCip (multi-device)

Apply the reviewed Modbus exemplar to the AbCip multi-device driver:
- Mapper AbCipEquipmentTagParser -> AbCipTagDefinitionFactory; TryParse -> FromTagConfig(tagConfig, rawPath, out def); drop leading-{ heuristic + the deviceHostAddress key; add inverse ToTagConfig; round-trip arrays/members/safety for coverage.
- Options: Tags (typed) -> RawTags (RawTagEntry list); Devices list kept.
- Driver: byName-only resolver over _tagsByRawPath (Ordinal); table built from RawTags via FromTagConfig, threading entry.DeviceName (device routing key) + entry.WriteIdempotent onto the def. Multi-device routing keys on DeviceName-or-host via a _devicesByRoutingKey index; discovery groups _declaredTags by RoutesToDevice.
- Factory: retire the pre-declared Tags DTO/BuildTag; bind List<RawTagEntry> RawTags.
- Probe: derive probe tag path from RawTags via the mapper.
- Cli BuildOptions projects typed tags -> RawTagEntry (ToTagConfig).
- Tests: AbCipRawTags helper (typed def -> RawTagEntry); parser tests -> FromTagConfig; blob-fallback driver tests rewritten to author via RawTags + read by RawPath. Driver.AbCip.Tests 342/342, Cli.Tests 42/42.

TODO(v3 WaveC): DeviceName becomes a pure logical name once the device connection host moves to the Device row's DeviceConfig.
This commit is contained in:
Joseph Doherty
2026-07-15 20:18:28 -04:00
parent c379e246d0
commit c0379742bc
32 changed files with 819 additions and 783 deletions
@@ -1,10 +1,12 @@
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// <summary>
/// AB CIP / EtherNet-IP driver configuration, bound from the driver's <c>DriverConfig</c>
/// JSON at <c>DriverHost.RegisterAsync</c>. One instance supports N devices (PLCs) behind
/// the same driver; per-device routing is keyed on <see cref="AbCipDeviceOptions.HostAddress"/>
/// via <c>IPerCallHostResolver</c>.
/// the same driver; per-device routing is keyed on the tag's device routing key (the RawPath's
/// device segment, delivered on <see cref="RawTagEntry.DeviceName"/>).
/// </summary>
public sealed class AbCipDriverOptions
{
@@ -12,14 +14,23 @@ public sealed class AbCipDriverOptions
/// PLCs this driver instance talks to. Each device contributes its own <see cref="AbCipHostAddress"/>
/// string as the <c>hostName</c> key used by resilience pipelines and the Admin UI.
/// </summary>
/// <remarks>
/// Wave C moves the device's live connection host into the <c>Device</c> row's <c>DeviceConfig</c>;
/// for Wave B the list stays bound from <c>DriverConfig</c> as today, and a tag routes to its device
/// by matching <see cref="RawTagEntry.DeviceName"/> against the device's name (<see cref="AbCipDeviceOptions.DeviceName"/>)
/// or its <see cref="AbCipDeviceOptions.HostAddress"/>.
/// </remarks>
public IReadOnlyList<AbCipDeviceOptions> Devices { get; init; } = [];
/// <summary>
/// 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
/// <c>Tag</c> as a <see cref="RawTagEntry"/> (RawPath identity + driver <c>TagConfig</c> blob +
/// WriteIdempotent flag + DeviceName routing key); the driver maps each through
/// <see cref="AbCipTagDefinitionFactory.FromTagConfig"/> into its RawPath → definition table.
/// Pre-declared tags always emit during discovery; opt in to controller-side discovery via
/// <see cref="EnableControllerBrowse"/>.
/// </summary>
public IReadOnlyList<AbCipTagDefinition> Tags { get; init; } = [];
public IReadOnlyList<RawTagEntry> RawTags { get; init; } = [];
/// <summary>Per-device probe settings. Falls back to defaults when omitted.</summary>
public AbCipProbeOptions Probe { get; init; } = new();
@@ -111,8 +122,13 @@ public sealed record AbCipDeviceOptions(
/// <summary>
/// One AB-backed OPC UA variable. Mirrors the <c>ModbusTagDefinition</c> shape.
/// </summary>
/// <param name="Name">Tag name; becomes the OPC UA browse name and full reference.</param>
/// <param name="DeviceHostAddress">Which device (<see cref="AbCipDeviceOptions.HostAddress"/>) this tag lives on.</param>
/// <param name="Name">Tag name; becomes the OPC UA browse name and full reference (the RawPath in v3).</param>
/// <param name="DeviceHostAddress">The tag's <b>device routing key</b> — the RawPath's device segment,
/// threaded in from <see cref="RawTagEntry.DeviceName"/> 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 <see cref="AbCipDeviceOptions.DeviceName"/> or <see cref="AbCipDeviceOptions.HostAddress"/>.
/// 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 <c>DeviceHostAddress</c> to avoid churn across the driver + CLI.</param>
/// <param name="TagPath">Logix symbolic path (controller or program scope).</param>
/// <param name="DataType">Logix atomic type, or <see cref="AbCipDataType.Structure"/> for UDT-typed tags.</param>
/// <param name="Writable">When <c>true</c> and the tag's ExternalAccess permits writes, IWritable routes writes here.</param>