dcc35f3481
An array tag subscribed natively used to register only its declared scalar (publishing element 0 at Good) and was then refused wholesale with BadTypeMismatch. The driver now hands the authored ArrayLength to the client, which registers the whole array as raw bytes and decodes every notification. - TwinCATTypeGuard.CheckArray plans the layout from the RESOLVED array type: element checked like a scalar notification (width / float kind / string kind); STRING(n)/WSTRING(n) elements stride n+1 / 2(n+1); multi-dim arrays flattened in PLC memory order; an authored ArrayLength shorter than the PLC array reads the leading sub-range, a longer one is refused; struct / pointer / nested-array elements, non-array symbols and VAR_IN_OUT refs are refused. - CheckNotificationSize caps a native array at 128 KiB (largest registration proven on a live TC 2.11 target); above it the tag is refused BadOutOfRange. - TwinCATArrayCodec decodes the bytes into the element-typed CLR array. - The poll read uses the same plan + codec instead of the Beckhoff typed array read, which failed live for most real arrays (exact CLR element type, rank-N for multi-dim, member arrays sliced by the under-reported TC2 symbol size). - TwinCATNotificationRefusedException carries the refusal's status; the type mismatch derives from it and the driver isolates either per tag.
138 lines
6.7 KiB
C#
138 lines
6.7 KiB
C#
using System.Collections.Concurrent;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
|
|
|
/// <summary>
|
|
/// Native ADS notifications for ARRAY tags. Before this, the notification path registered only the
|
|
/// declared scalar, so an array tag published its first element at Good; the declared-type guard
|
|
/// then refused every array tag with <c>BadTypeMismatch</c>. The driver now hands the authored
|
|
/// <c>ArrayLength</c> to the client, which registers the whole array as raw bytes and decodes it
|
|
/// (<see cref="TwinCATArrayCodec"/>) — these pin the driver's half of that contract.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class TwinCATNativeArraySubscribeTests
|
|
{
|
|
private const string Host = "ads://5.23.91.23.1.1:851";
|
|
|
|
private static (TwinCATDriver drv, FakeTwinCATClientFactory factory) NewNativeDriver(params TwinCATTagDefinition[] tags)
|
|
{
|
|
var factory = new FakeTwinCATClientFactory();
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
|
{
|
|
Devices = [new TwinCATDeviceOptions(Host)],
|
|
RawTags = TwinCATRawTags.Entries(tags),
|
|
Probe = new TwinCATProbeOptions { Enabled = false },
|
|
UseNativeNotifications = true,
|
|
}, "drv-1", factory);
|
|
return (drv, factory);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An array tag registers with its authored ArrayLength, and the element-typed array the client
|
|
/// decodes reaches OnDataChange whole, at Good — the same shape the poll read publishes.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Native_subscribe_registers_an_array_tag_with_its_authored_length_and_publishes_the_array()
|
|
{
|
|
var (drv, factory) = NewNativeDriver(
|
|
new TwinCATTagDefinition("Samples", Host, "P_FILTER.SAMPLE", TwinCATDataType.Real, ArrayLength: 20));
|
|
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
|
var events = new ConcurrentQueue<DataChangeEventArgs>();
|
|
drv.OnDataChange += (_, e) => events.Enqueue(e);
|
|
|
|
_ = await drv.SubscribeAsync(["Samples"], TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken);
|
|
|
|
var reg = factory.Clients[0].Notifications.ShouldHaveSingleItem();
|
|
reg.Type.ShouldBe(TwinCATDataType.Real);
|
|
reg.ArrayCount.ShouldBe(20);
|
|
|
|
var samples = Enumerable.Range(1, 20).Select(i => i * 1.5f).ToArray();
|
|
factory.Clients[0].FireNotification("P_FILTER.SAMPLE", samples);
|
|
var e = events.ShouldHaveSingleItem();
|
|
e.Snapshot.StatusCode.ShouldBe(TwinCATStatusMapper.Good);
|
|
e.Snapshot.Value.ShouldBeSameAs(samples);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A scalar tag, an element-indexed path (<c>Arr[3]</c>) and a bit-indexed BOOL stay scalars: no
|
|
/// array count reaches the client.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Scalar_element_indexed_and_bit_indexed_tags_register_without_an_array_count()
|
|
{
|
|
var (drv, factory) = NewNativeDriver(
|
|
new TwinCATTagDefinition("Count", Host, "MAIN.Count", TwinCATDataType.DInt),
|
|
new TwinCATTagDefinition("Fault3", Host, "MAIN.FaultCodes[3]", TwinCATDataType.UInt),
|
|
new TwinCATTagDefinition("Run0", Host, "MAIN.State.0", TwinCATDataType.Bool));
|
|
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
|
|
|
_ = await drv.SubscribeAsync(["Count", "Fault3", "Run0"], TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken);
|
|
|
|
var regs = factory.Clients[0].Notifications;
|
|
regs.Count.ShouldBe(3);
|
|
regs.ShouldAllBe(n => n.ArrayCount == null);
|
|
regs.Single(n => n.SymbolPath == "MAIN.State.0").BitIndex.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>A reconnect replays an array registration with the same array count.</summary>
|
|
[Fact]
|
|
public async Task Reconnect_replays_an_array_registration_with_its_array_count()
|
|
{
|
|
var (drv, factory) = NewNativeDriver(
|
|
new TwinCATTagDefinition("Faults", Host, "MAIN.FaultCodes", TwinCATDataType.UInt, ArrayLength: 13));
|
|
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
|
_ = await drv.SubscribeAsync(["Faults"], TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken);
|
|
|
|
factory.Clients[0].SimulateWireDrop();
|
|
_ = await drv.ReadAsync(["Faults"], TestContext.Current.CancellationToken);
|
|
|
|
factory.Clients.Count.ShouldBe(2);
|
|
factory.Clients[1].Notifications.ShouldHaveSingleItem().ArrayCount.ShouldBe(13);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A notification the client refuses for a reason other than a type mismatch (an array above the
|
|
/// native-notification size cap → <c>BadOutOfRange</c>) is isolated the same way: that one tag
|
|
/// publishes the refusal's own status and the rest of the subscription registers.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task A_refused_oversized_array_is_isolated_with_the_refusals_own_status()
|
|
{
|
|
var (drv, factory) = NewNativeDriver(
|
|
new TwinCATTagDefinition("Slaves", Host, "P_EC.SLAVEADDRESS", TwinCATDataType.UInt, ArrayLength: 70000),
|
|
new TwinCATTagDefinition("Count", Host, "MAIN.Count", TwinCATDataType.DInt));
|
|
factory.Customise = () =>
|
|
{
|
|
var c = new FakeTwinCATClient();
|
|
c.AddNotificationErrors["P_EC.SLAVEADDRESS"] = new TwinCATNotificationRefusedException(
|
|
"P_EC.SLAVEADDRESS", "140000 B exceeds the 131072 B native-notification cap",
|
|
TwinCATStatusMapper.BadOutOfRange);
|
|
return c;
|
|
};
|
|
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
|
var events = new ConcurrentQueue<DataChangeEventArgs>();
|
|
drv.OnDataChange += (_, e) => events.Enqueue(e);
|
|
|
|
_ = await drv.SubscribeAsync(["Slaves", "Count"], TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken);
|
|
|
|
factory.Clients[0].Notifications.ShouldHaveSingleItem().SymbolPath.ShouldBe("MAIN.Count");
|
|
var refused = events.ShouldHaveSingleItem();
|
|
refused.FullReference.ShouldBe("Slaves");
|
|
refused.Snapshot.StatusCode.ShouldBe(TwinCATStatusMapper.BadOutOfRange);
|
|
refused.Snapshot.Value.ShouldBeNull();
|
|
}
|
|
|
|
/// <summary>A type-mismatch refusal is a notification refusal carrying <c>BadTypeMismatch</c>.</summary>
|
|
[Fact]
|
|
public void A_type_mismatch_is_a_notification_refusal_carrying_BadTypeMismatch()
|
|
{
|
|
TwinCATNotificationRefusedException ex = new TwinCATTypeMismatchException("MAIN.X", "declared DInt over REAL");
|
|
ex.StatusCode.ShouldBe(TwinCATStatusMapper.BadTypeMismatch);
|
|
ex.SymbolPath.ShouldBe("MAIN.X");
|
|
}
|
|
}
|