v3(ablegacy): apply Modbus exemplar — RawTags delivery + RawPath mapper

- Rename AbLegacyEquipmentTagParser → AbLegacyTagDefinitionFactory; TryParse →
  FromTagConfig(tagConfig, rawPath, out def). Drop the leading-{ heuristic and the
  deviceHostAddress key; def.Name = rawPath. Add inverse ToTagConfig. Keep strict-enum
  guards + Inspect.
- Options: Tags(IReadOnlyList<AbLegacyTagDefinition>) → RawTags(IReadOnlyList<RawTagEntry>).
- Driver: _tagsByRawPath (Ordinal); byName-only resolver; build table from _options.RawTags
  via FromTagConfig, threading WriteIdempotent + resolving RawTagEntry.DeviceName to the
  owning device host (ResolveDeviceHost; TODO(v3 WaveC) for the live Device-row host).
  DiscoverAsync + family-profile validation iterate the built table. Probe picks its address
  from RawTags via the mapper.
- Factory: retire the pre-declared tag DTO path; bind List<RawTagEntry>? RawTags; keep Devices.
- Cli BuildOptions: deliver typed defs as RawTagEntry via ToTagConfig.
- Tests: AbLegacyRawTags helper; migrate Tags= → RawTags; parser tests → FromTagConfig;
  equipment-ref driver/ResolveHost tests re-authored as RawTagEntry + read by RawPath.

Driver+Contracts+Cli green; 213 driver + 36 Cli tests pass.
This commit is contained in:
Joseph Doherty
2026-07-15 20:09:46 -04:00
parent c379e246d0
commit 3878b51e97
22 changed files with 458 additions and 287 deletions
@@ -1,6 +1,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
@@ -66,19 +67,11 @@ public static class AbLegacyDriverFactoryExtensions
fallback: AbLegacyPlcFamily.Slc500),
DeviceName: d.DeviceName))]
: [],
Tags = dto.Tags is { Count: > 0 }
? [.. dto.Tags.Select(t => new AbLegacyTagDefinition(
Name: t.Name ?? throw new InvalidOperationException(
$"AB Legacy config for '{driverInstanceId}' has a tag missing Name"),
DeviceHostAddress: t.DeviceHostAddress ?? throw new InvalidOperationException(
$"AB Legacy tag '{t.Name}' in '{driverInstanceId}' missing DeviceHostAddress"),
Address: t.Address ?? throw new InvalidOperationException(
$"AB Legacy tag '{t.Name}' in '{driverInstanceId}' missing Address"),
DataType: ParseEnum<AbLegacyDataType>(t.DataType, driverInstanceId, "DataType",
tagName: t.Name),
Writable: t.Writable ?? true,
WriteIdempotent: t.WriteIdempotent ?? false))]
: [],
// v3: the deploy artifact (Wave C) populates rawTags with the cluster's authored raw AbLegacy
// tags (RawPath + TagConfig blob + WriteIdempotent + owning DeviceName). The driver maps each
// RawTagEntry's TagConfig blob through AbLegacyTagDefinitionFactory at Initialize and routes it
// to its device by DeviceName; the legacy pre-declared DTO tag path is retired.
RawTags = dto.RawTags is { Count: > 0 } ? [.. dto.RawTags] : [],
Probe = new AbLegacyProbeOptions
{
Enabled = dto.Probe?.Enabled ?? true,
@@ -145,9 +138,11 @@ public static class AbLegacyDriverFactoryExtensions
public List<AbLegacyDeviceDto>? Devices { get; init; }
/// <summary>
/// Gets or sets the list of tags to monitor.
/// Gets or sets the authored raw tags (RawPath + TagConfig blob + WriteIdempotent + owning
/// DeviceName) the deploy artifact delivers. The driver maps each through
/// <see cref="AbLegacyTagDefinitionFactory.FromTagConfig"/> at Initialize.
/// </summary>
public List<AbLegacyTagDto>? Tags { get; init; }
public List<RawTagEntry>? RawTags { get; init; }
/// <summary>
/// Gets or sets the probe configuration.
@@ -173,39 +168,6 @@ public static class AbLegacyDriverFactoryExtensions
public string? DeviceName { get; init; }
}
internal sealed class AbLegacyTagDto
{
/// <summary>
/// Gets or sets the tag name.
/// </summary>
public string? Name { get; init; }
/// <summary>
/// Gets or sets the device host address.
/// </summary>
public string? DeviceHostAddress { get; init; }
/// <summary>
/// Gets or sets the tag address.
/// </summary>
public string? Address { get; init; }
/// <summary>
/// Gets or sets the data type.
/// </summary>
public string? DataType { get; init; }
/// <summary>
/// Gets or sets whether the tag is writable.
/// </summary>
public bool? Writable { get; init; }
/// <summary>
/// Gets or sets whether write is idempotent.
/// </summary>
public bool? WriteIdempotent { get; init; }
}
internal sealed class AbLegacyProbeDto
{
/// <summary>