v3(b1-twincat): apply Modbus exemplar pattern to TwinCAT (multi-device)
- Rename TwinCATEquipmentTagParser -> TwinCATTagDefinitionFactory; TryParse ->
FromTagConfig(tagConfig, rawPath, out def). Drop leading-'{' heuristic + the
deviceHostAddress key; def.Name = rawPath. Keep strict-enum + Inspect; add a
Structure guard (sole tag path now) + inverse ToTagConfig.
- Options: Tags -> RawTags (RawTagEntry); keep Devices + protocol fields.
- Driver: _tagsByRawPath (Ordinal); byName-only resolver; build table from
_options.RawTags via FromTagConfig, threading WriteIdempotent + DeviceName.
Multi-device: ResolveDeviceHost matches entry.DeviceName against options.Devices
(by name, then host) -> def.DeviceHostAddress (TODO(v3 WaveC): live host from the
Device row's DeviceConfig). DiscoverAsync now emits from the table.
- Factory: retire pre-declared tags[] DTO path; bind rawTags; keep Devices.
- Cli: BuildOptions serialises typed defs -> RawTagEntry via ToTagConfig.
- Tests: TwinCATRawTags helper; migrate Tags= -> RawTags=; rename parser tests to
FromTagConfig; equipment/resolve-host/array tests delivered via RawTags.
Gate: Contracts + Driver build green; Driver.TwinCAT.Tests 192 pass, Cli.Tests 70 pass.
This commit is contained in:
@@ -27,11 +27,11 @@ public sealed class TwinCATArraySupportTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags =
|
||||
RawTags = TwinCATRawTags.Entries(
|
||||
[
|
||||
new TwinCATTagDefinition("Speeds", Host, "MAIN.Speeds", TwinCATDataType.DInt,
|
||||
Writable: true, WriteIdempotent: false, ArrayLength: 8),
|
||||
],
|
||||
]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
EnableControllerBrowse = false,
|
||||
}, "drv-1", new FakeTwinCATClientFactory());
|
||||
@@ -52,7 +52,7 @@ public sealed class TwinCATArraySupportTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags = [new TwinCATTagDefinition("Speed", Host, "MAIN.Speed", TwinCATDataType.DInt)],
|
||||
RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("Speed", Host, "MAIN.Speed", TwinCATDataType.DInt)]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "drv-1", new FakeTwinCATClientFactory());
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
@@ -104,12 +104,14 @@ public sealed class TwinCATArraySupportTests
|
||||
|
||||
// ---- (2) read path: array read returns a typed CLR array ----
|
||||
|
||||
/// <summary>An equipment array tag reads a typed CLR array (boxed as object) through the driver.</summary>
|
||||
private const string ArrayRawPath = "equip/Speeds";
|
||||
private const string ScalarRawPath = "equip/Speed";
|
||||
|
||||
/// <summary>An array raw tag reads a typed CLR array (boxed as object) through the driver.</summary>
|
||||
[Fact]
|
||||
public async Task Driver_reads_an_array_equipment_tag_as_typed_clr_array()
|
||||
public async Task Driver_reads_an_array_raw_tag_as_typed_clr_array()
|
||||
{
|
||||
var json =
|
||||
$$"""{"deviceHostAddress":"{{Host}}","symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":4}""";
|
||||
var blob = """{"symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":4}""";
|
||||
var expected = new[] { 10, 20, 30, 40 };
|
||||
var factory = new FakeTwinCATClientFactory
|
||||
{
|
||||
@@ -118,12 +120,12 @@ public sealed class TwinCATArraySupportTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags = [],
|
||||
RawTags = [new RawTagEntry(ArrayRawPath, blob, WriteIdempotent: false, DeviceName: Host)],
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "twincat-arr", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var r = await drv.ReadAsync([json], CancellationToken.None);
|
||||
var r = await drv.ReadAsync([ArrayRawPath], CancellationToken.None);
|
||||
|
||||
r[0].StatusCode.ShouldBe(TwinCATStatusMapper.Good);
|
||||
r[0].Value.ShouldBeOfType<int[]>().ShouldBe(expected);
|
||||
@@ -131,11 +133,11 @@ public sealed class TwinCATArraySupportTests
|
||||
factory.Clients[0].LastReadArrayCount.ShouldBe(4);
|
||||
}
|
||||
|
||||
/// <summary>A scalar equipment tag reads with a null array count (scalar path unchanged).</summary>
|
||||
/// <summary>A scalar raw tag reads with a null array count (scalar path unchanged).</summary>
|
||||
[Fact]
|
||||
public async Task Driver_reads_a_scalar_equipment_tag_with_null_array_count()
|
||||
public async Task Driver_reads_a_scalar_raw_tag_with_null_array_count()
|
||||
{
|
||||
var json = $$"""{"deviceHostAddress":"{{Host}}","symbolPath":"MAIN.Speed","dataType":"DInt"}""";
|
||||
var blob = """{"symbolPath":"MAIN.Speed","dataType":"DInt"}""";
|
||||
var factory = new FakeTwinCATClientFactory
|
||||
{
|
||||
Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Speed"] = 7 } },
|
||||
@@ -143,100 +145,83 @@ public sealed class TwinCATArraySupportTests
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags = [],
|
||||
RawTags = [new RawTagEntry(ScalarRawPath, blob, WriteIdempotent: false, DeviceName: Host)],
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
}, "twincat-scalar", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var r = await drv.ReadAsync([json], CancellationToken.None);
|
||||
var r = await drv.ReadAsync([ScalarRawPath], CancellationToken.None);
|
||||
|
||||
r[0].StatusCode.ShouldBe(TwinCATStatusMapper.Good);
|
||||
r[0].Value.ShouldBe(7);
|
||||
factory.Clients[0].LastReadArrayCount.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ---- (3) resolver: equipment-tag parser threads arrayLength ----
|
||||
// ---- (3) mapper: FromTagConfig threads arrayLength ----
|
||||
|
||||
/// <summary>The equipment-tag parser threads <c>arrayLength</c> into the transient definition.</summary>
|
||||
/// <summary>The tag-definition factory threads <c>arrayLength</c> into the definition.</summary>
|
||||
[Fact]
|
||||
public void Parser_threads_arrayLength_into_the_definition()
|
||||
public void FromTagConfig_threads_arrayLength_into_the_definition()
|
||||
{
|
||||
var json =
|
||||
"""{"deviceHostAddress":"ads://5.23.91.23.1.1:851","symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":12}""";
|
||||
TwinCATEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
|
||||
var blob = """{"symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":12}""";
|
||||
TwinCATTagDefinitionFactory.FromTagConfig(blob, ArrayRawPath, out var def).ShouldBeTrue();
|
||||
def!.ArrayLength.ShouldBe(12);
|
||||
}
|
||||
|
||||
/// <summary>A blob without isArray (or with isArray=false) leaves ArrayLength null.</summary>
|
||||
[Fact]
|
||||
public void Parser_leaves_ArrayLength_null_for_a_scalar_blob()
|
||||
public void FromTagConfig_leaves_ArrayLength_null_for_a_scalar_blob()
|
||||
{
|
||||
TwinCATEquipmentTagParser.TryParse(
|
||||
"""{"symbolPath":"MAIN.X","dataType":"DInt"}""", out var def).ShouldBeTrue();
|
||||
TwinCATTagDefinitionFactory.FromTagConfig(
|
||||
"""{"symbolPath":"MAIN.X","dataType":"DInt"}""", ScalarRawPath, out var def).ShouldBeTrue();
|
||||
def!.ArrayLength.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>arrayLength is ignored when isArray is absent/false (no orphan length).</summary>
|
||||
[Fact]
|
||||
public void Parser_ignores_arrayLength_when_isArray_is_false()
|
||||
public void FromTagConfig_ignores_arrayLength_when_isArray_is_false()
|
||||
{
|
||||
TwinCATEquipmentTagParser.TryParse(
|
||||
TwinCATTagDefinitionFactory.FromTagConfig(
|
||||
"""{"symbolPath":"MAIN.X","dataType":"DInt","isArray":false,"arrayLength":9}""",
|
||||
out var def).ShouldBeTrue();
|
||||
ScalarRawPath, out var def).ShouldBeTrue();
|
||||
def!.ArrayLength.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ---- (4) Driver.TwinCAT-017 — pre-declared array tag parseable via driver config JSON ----
|
||||
// ---- (4) Driver.TwinCAT-017 — array raw tag delivered via driver config JSON rawTags ----
|
||||
|
||||
/// <summary>
|
||||
/// A pre-declared tag in the driver config JSON with <c>arrayLength</c> must produce a
|
||||
/// An array raw tag in the driver config JSON <c>rawTags</c> array must bind + map to a
|
||||
/// <see cref="TwinCATTagDefinition"/> with the correct <see cref="TwinCATTagDefinition.ArrayLength"/>,
|
||||
/// which in turn drives <c>IsArray</c>/<c>ArrayDim</c> at discovery and a native array read
|
||||
/// at runtime (Driver.TwinCAT-017).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Predeclared_array_tag_arrayLength_parses_from_driver_config_json()
|
||||
public void Array_raw_tag_arrayLength_binds_from_driver_config_json()
|
||||
{
|
||||
var blob = """{"symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":8}""";
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
devices = new[] { new { hostAddress = Host } },
|
||||
tags = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
name = "Speeds",
|
||||
deviceHostAddress = Host,
|
||||
symbolPath = "MAIN.Speeds",
|
||||
dataType = "DInt",
|
||||
arrayLength = 8,
|
||||
},
|
||||
},
|
||||
rawTags = new[] { new { rawPath = "Speeds", tagConfig = blob, writeIdempotent = false, deviceName = Host } },
|
||||
});
|
||||
var parsed = TwinCATDriverFactoryExtensions.ParseOptionsForTests(json, "drv-1");
|
||||
parsed.Tags.Single().ArrayLength.ShouldBe(8);
|
||||
var entry = parsed.RawTags.Single();
|
||||
TwinCATTagDefinitionFactory.FromTagConfig(entry.TagConfig, entry.RawPath, out var def).ShouldBeTrue();
|
||||
def!.ArrayLength.ShouldBe(8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A pre-declared array tag parsed from JSON must surface as <c>IsArray=true</c> /
|
||||
/// An array raw tag delivered via the driver config JSON must surface as <c>IsArray=true</c> /
|
||||
/// <c>ArrayDim=8</c> in the discovery address space.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Predeclared_array_tag_from_json_reports_IsArray_in_discovery()
|
||||
public async Task Array_raw_tag_from_json_reports_IsArray_in_discovery()
|
||||
{
|
||||
var blob = """{"symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":8}""";
|
||||
var configJson = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
devices = new[] { new { hostAddress = Host } },
|
||||
tags = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
name = "Speeds",
|
||||
deviceHostAddress = Host,
|
||||
symbolPath = "MAIN.Speeds",
|
||||
dataType = "DInt",
|
||||
arrayLength = 8,
|
||||
},
|
||||
},
|
||||
rawTags = new[] { new { rawPath = "Speeds", tagConfig = blob, writeIdempotent = false, deviceName = Host } },
|
||||
});
|
||||
var builder = new RecordingBuilder();
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions(), "drv-1", new FakeTwinCATClientFactory());
|
||||
|
||||
Reference in New Issue
Block a user