Driver.TwinCAT-001 — InitializeAsync/ReinitializeAsync ignored driverConfigJson. Extracted the DTO-to-options parse into a shared TwinCATDriverFactoryExtensions.ParseOptions; InitializeAsync now re-parses driverConfigJson into a mutable _options field, so a config generation pushed via ReinitializeAsync (added/removed devices, tags, probe settings) is actually applied at runtime. Driver.TwinCAT-002 — LInt/ULInt narrowed to Int32. ToDriverDataType now maps LInt to Int64, ULInt to UInt64, UDInt to UInt32, UInt/USInt to UInt16, Int/SInt to Int16, and the IEC TIME/DATE/DT/TOD types to UInt32 (their raw UDINT counter). Removed the stale "Int64 gap" comment — no truncation or sign flips at the OPC UA encode layer. Driver.TwinCAT-007 — EnsureConnectedAsync was not thread-safe. Connect/reconnect is now serialized per device by a SemaphoreSlim (DeviceState.ConnectGate) with a double-checked connect, mirroring the S7 driver. Concurrent read/write/probe callers can no longer leak a client or race a create-vs-dispose. Driver.TwinCAT-008 — native ADS notification callbacks ran driver logic on the AMS router thread. AdsTwinCATClient now enqueues AdsNotificationEx callbacks onto a bounded Channel drained by a dedicated managed task; the router-thread callback only does a non-blocking TryWrite, so a slow consumer cannot stall ADS notification delivery process-wide. Driver.TwinCAT-013 — TwinCATDriver did not implement IRediscoverable. The driver now implements IRediscoverable; AdsTwinCATClient detects ADS 0x0702 (symbol-version-changed) on read/write paths and raises OnSymbolVersionChanged, which the driver forwards as OnRediscoveryNeeded so Core rebuilds the address space after a PLC program re-download. Adds TwinCATHighFindingsRegressionTests covering all five fixes; updates the data-type mapping assertion in TwinCATDriverTests. TwinCAT driver builds clean; 119 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
109 lines
4.2 KiB
C#
109 lines
4.2 KiB
C#
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;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class TwinCATDriverTests
|
|
{
|
|
[Fact]
|
|
public void DriverType_is_TwinCAT()
|
|
{
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions(), "drv-1");
|
|
drv.DriverType.ShouldBe("TwinCAT");
|
|
drv.DriverInstanceId.ShouldBe("drv-1");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InitializeAsync_parses_device_addresses()
|
|
{
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
|
{
|
|
Devices =
|
|
[
|
|
new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851"),
|
|
new TwinCATDeviceOptions("ads://10.0.0.1.1.1:852", DeviceName: "Machine2"),
|
|
],
|
|
}, "drv-1");
|
|
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
drv.DeviceCount.ShouldBe(2);
|
|
drv.GetDeviceState("ads://5.23.91.23.1.1:851")!.ParsedAddress.Port.ShouldBe(851);
|
|
drv.GetDeviceState("ads://10.0.0.1.1.1:852")!.Options.DeviceName.ShouldBe("Machine2");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InitializeAsync_malformed_address_faults()
|
|
{
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
|
{
|
|
Devices = [new TwinCATDeviceOptions("not-an-address")],
|
|
}, "drv-1");
|
|
|
|
await Should.ThrowAsync<InvalidOperationException>(
|
|
() => drv.InitializeAsync("{}", CancellationToken.None));
|
|
drv.GetHealth().State.ShouldBe(DriverState.Faulted);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ShutdownAsync_clears_devices()
|
|
{
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
|
{
|
|
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
|
Probe = new TwinCATProbeOptions { Enabled = false },
|
|
}, "drv-1");
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
await drv.ShutdownAsync(CancellationToken.None);
|
|
drv.DeviceCount.ShouldBe(0);
|
|
drv.GetHealth().State.ShouldBe(DriverState.Unknown);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReinitializeAsync_cycles_devices()
|
|
{
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
|
{
|
|
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
|
Probe = new TwinCATProbeOptions { Enabled = false },
|
|
}, "drv-1");
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
await drv.ReinitializeAsync("{}", CancellationToken.None);
|
|
|
|
drv.DeviceCount.ShouldBe(1);
|
|
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
|
}
|
|
|
|
[Fact]
|
|
public void DataType_mapping_covers_atomic_iec_types()
|
|
{
|
|
TwinCATDataType.Bool.ToDriverDataType().ShouldBe(DriverDataType.Boolean);
|
|
TwinCATDataType.DInt.ToDriverDataType().ShouldBe(DriverDataType.Int32);
|
|
TwinCATDataType.Real.ToDriverDataType().ShouldBe(DriverDataType.Float32);
|
|
TwinCATDataType.LReal.ToDriverDataType().ShouldBe(DriverDataType.Float64);
|
|
TwinCATDataType.String.ToDriverDataType().ShouldBe(DriverDataType.String);
|
|
TwinCATDataType.WString.ToDriverDataType().ShouldBe(DriverDataType.String);
|
|
// IEC TIME/DATE/DT/TOD surface as their raw UDINT counter (Driver.TwinCAT-002).
|
|
TwinCATDataType.Time.ToDriverDataType().ShouldBe(DriverDataType.UInt32);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0u, TwinCATStatusMapper.Good)]
|
|
[InlineData(1798u, TwinCATStatusMapper.BadNodeIdUnknown)] // symbol not found
|
|
[InlineData(1808u, TwinCATStatusMapper.BadNotWritable)] // access denied
|
|
[InlineData(1861u, TwinCATStatusMapper.BadTimeout)] // sync timeout
|
|
[InlineData(1793u, TwinCATStatusMapper.BadOutOfRange)] // invalid index group
|
|
[InlineData(1794u, TwinCATStatusMapper.BadOutOfRange)] // invalid index offset
|
|
[InlineData(1792u, TwinCATStatusMapper.BadNotSupported)] // service not supported
|
|
[InlineData(7u, TwinCATStatusMapper.BadCommunicationError)] // port unreachable
|
|
[InlineData(99999u, TwinCATStatusMapper.BadCommunicationError)] // unknown → generic comm fail
|
|
public void StatusMapper_covers_known_ads_error_codes(uint adsError, uint expected)
|
|
{
|
|
TwinCATStatusMapper.MapAdsError(adsError).ShouldBe(expected);
|
|
}
|
|
}
|