v3(focas): adopt Modbus RawTagEntry exemplar (multi-device)

- Rename FocasEquipmentTagParser -> FocasTagDefinitionFactory; TryParse ->
  FromTagConfig(tagConfig, rawPath, out def). Drop leading-{ heuristic + the
  deviceHostAddress blob key; def.Name = rawPath. Add inverse ToTagConfig.
  writable: absent -> read-only, explicit true honoured (note preserved).
- FocasDriverOptions.Tags -> RawTags (IReadOnlyList<RawTagEntry>).
- FocasDriver: _tagsByRawPath (Ordinal); byName-only resolver; build table from
  RawTags via FromTagConfig; multi-device routing threads RawTagEntry.DeviceName
  -> ResolveDeviceHost match against options.Devices -> def.DeviceHostAddress
  (TODO v3 WaveC: live host from Device row DeviceConfig). Tolerant per-tag
  skip+log (mapper/address/matrix miss -> BadNodeIdUnknown); unknown device
  fails init fast.
- Factory: bind List<RawTagEntry> RawTags; retire FocasTagDto/ParseDataType.
- CLI FocasCommandBase.BuildOptions -> RawTags via ToTagConfig + DeviceName.
- Tests: FocasRawTags helper; migrate Tags= -> RawTags=; rename parser tests to
  FromTagConfig; rewrite retired blob-ref tests (equipment-tag/capability-gate/
  resolve-host) onto authored RawPath. 272 driver + 52 CLI green.
This commit is contained in:
Joseph Doherty
2026-07-15 20:19:55 -04:00
parent c379e246d0
commit 8b2c3fa04c
23 changed files with 467 additions and 350 deletions
@@ -27,11 +27,11 @@ public sealed class FocasDriverMediumFindingsTests
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
Tags =
RawTags = FocasRawTags.Entries(
[
// DeviceHostAddress has a port typo — not in Devices
// DeviceHostAddress (→ RawTagEntry.DeviceName) has a port typo — not in Devices
new FocasTagDefinition("X", "focas://10.0.0.5:9999", "R100", FocasDataType.Byte),
],
]),
Probe = new FocasProbeOptions { Enabled = false },
}, "drv-1", new FakeFocasClientFactory());
@@ -49,11 +49,11 @@ public sealed class FocasDriverMediumFindingsTests
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
Tags =
RawTags = FocasRawTags.Entries(
[
// Correct address so address validation passes
new FocasTagDefinition("TypoTag", "focas://10.0.0.99:8193", "R100", FocasDataType.Byte),
],
]),
Probe = new FocasProbeOptions { Enabled = false },
}, "drv-1", new FakeFocasClientFactory());
@@ -74,11 +74,11 @@ public sealed class FocasDriverMediumFindingsTests
new FocasDeviceOptions("focas://10.0.0.5:8193"),
new FocasDeviceOptions("focas://10.0.0.6:8193"),
],
Tags =
RawTags = FocasRawTags.Entries(
[
new FocasTagDefinition("A", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte),
new FocasTagDefinition("B", "focas://10.0.0.6:8193", "R100", FocasDataType.Byte),
],
]),
Probe = new FocasProbeOptions { Enabled = false },
}, "drv-1", new FakeFocasClientFactory());
@@ -97,12 +97,12 @@ public sealed class FocasDriverMediumFindingsTests
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
Tags =
RawTags = FocasRawTags.Entries(
[
// Writable: true is the default — must still be ViewOnly
new FocasTagDefinition("Speed", "focas://10.0.0.5:8193", "R100", FocasDataType.Int16, Writable: true),
new FocasTagDefinition("Alarm", "focas://10.0.0.5:8193", "R200", FocasDataType.Byte, Writable: false),
],
]),
Probe = new FocasProbeOptions { Enabled = false },
}, "drv-1", new FakeFocasClientFactory());
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -132,11 +132,11 @@ public sealed class FocasDriverMediumFindingsTests
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
Tags =
RawTags = FocasRawTags.Entries(
[
new FocasTagDefinition("Idempotent", "focas://10.0.0.5:8193", "R100", FocasDataType.Int16, WriteIdempotent: true),
new FocasTagDefinition("NonIdempotent", "focas://10.0.0.5:8193", "R200", FocasDataType.Int16, WriteIdempotent: false),
],
]),
Probe = new FocasProbeOptions { Enabled = false },
}, "drv-1", new FakeFocasClientFactory());
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -162,7 +162,7 @@ public sealed class FocasDriverMediumFindingsTests
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
Tags = [new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte)],
RawTags = [FocasRawTags.Entry(new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte))],
Probe = new FocasProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -191,7 +191,7 @@ public sealed class FocasDriverMediumFindingsTests
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
Tags = [new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte)],
RawTags = [FocasRawTags.Entry(new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte))],
Probe = new FocasProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -227,7 +227,7 @@ public sealed class FocasDriverMediumFindingsTests
var drv = new FocasDriver(new FocasDriverOptions
{
Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
Tags = [new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte)],
RawTags = [FocasRawTags.Entry(new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte))],
Probe = new FocasProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);