IEC 61131-3 TIME/TOD now surface as TimeSpan (UA Duration); DATE/DT surface as DateTime (UTC). The wire form stays UDINT — AdsTwinCATClient post-processes raw values in ReadValueAsync and OnAdsNotificationEx, and accepts native CLR types in ConvertForWrite. Added Duration to DriverDataType (back-compat: existing switches default to BaseDataType for unknown enum values) and mapped it to DataTypeIds.Duration in DriverNodeManager. Closes #306
194 lines
7.5 KiB
C#
194 lines
7.5 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.LInt.ToDriverDataType().ShouldBe(DriverDataType.Int64);
|
|
TwinCATDataType.ULInt.ToDriverDataType().ShouldBe(DriverDataType.UInt64);
|
|
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 durations map to UA Duration; absolute timestamps map to UA DateTime.
|
|
TwinCATDataType.Time.ToDriverDataType().ShouldBe(DriverDataType.Duration);
|
|
TwinCATDataType.TimeOfDay.ToDriverDataType().ShouldBe(DriverDataType.Duration);
|
|
TwinCATDataType.Date.ToDriverDataType().ShouldBe(DriverDataType.DateTime);
|
|
TwinCATDataType.DateTime.ToDriverDataType().ShouldBe(DriverDataType.DateTime);
|
|
}
|
|
|
|
[Fact]
|
|
public void IecTime_post_process_converts_TIME_to_TimeSpan_ms()
|
|
{
|
|
var ts = (TimeSpan)AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.Time, 12_345u)!;
|
|
ts.ShouldBe(TimeSpan.FromMilliseconds(12_345));
|
|
}
|
|
|
|
[Fact]
|
|
public void IecTime_post_process_converts_TOD_to_TimeSpan_ms()
|
|
{
|
|
var ts = (TimeSpan)AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.TimeOfDay, 3_600_000u)!;
|
|
ts.ShouldBe(TimeSpan.FromHours(1));
|
|
}
|
|
|
|
[Fact]
|
|
public void IecTime_post_process_converts_DT_to_DateTime_utc()
|
|
{
|
|
// 1970-01-01 + 1 hour = 1970-01-01 01:00:00 UTC
|
|
var dt = (DateTime)AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.DateTime, 3600u)!;
|
|
dt.ShouldBe(new DateTime(1970, 1, 1, 1, 0, 0, DateTimeKind.Utc));
|
|
dt.Kind.ShouldBe(DateTimeKind.Utc);
|
|
}
|
|
|
|
[Fact]
|
|
public void IecTime_post_process_converts_DATE_to_midnight_utc()
|
|
{
|
|
// 86400 seconds = exactly 1970-01-02 00:00 UTC
|
|
var dt = (DateTime)AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.Date, 86_400u)!;
|
|
dt.ShouldBe(new DateTime(1970, 1, 2, 0, 0, 0, DateTimeKind.Utc));
|
|
}
|
|
|
|
[Fact]
|
|
public void IecTime_post_process_passthrough_for_non_time_types()
|
|
{
|
|
AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.DInt, 42).ShouldBe(42);
|
|
AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.LReal, 3.14).ShouldBe(3.14);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertForWrite_TIME_accepts_TimeSpan_and_returns_UDINT_ms()
|
|
{
|
|
var raw = AdsTwinCATClient.ConvertForWrite(TwinCATDataType.Time, TimeSpan.FromMilliseconds(2_500));
|
|
raw.ShouldBeOfType<uint>();
|
|
((uint)raw).ShouldBe(2_500u);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertForWrite_TOD_accepts_double_ms()
|
|
{
|
|
var raw = AdsTwinCATClient.ConvertForWrite(TwinCATDataType.TimeOfDay, 60_000.0);
|
|
((uint)raw).ShouldBe(60_000u);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertForWrite_DT_accepts_DateTime_and_returns_UDINT_seconds()
|
|
{
|
|
var raw = AdsTwinCATClient.ConvertForWrite(
|
|
TwinCATDataType.DateTime,
|
|
new DateTime(1970, 1, 1, 1, 0, 0, DateTimeKind.Utc));
|
|
((uint)raw).ShouldBe(3600u);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertForWrite_DT_round_trips_via_post_process()
|
|
{
|
|
var original = new DateTime(2024, 6, 15, 12, 30, 45, DateTimeKind.Utc);
|
|
var raw = (uint)AdsTwinCATClient.ConvertForWrite(TwinCATDataType.DateTime, original);
|
|
var roundTrip = (DateTime)AdsTwinCATClient.PostProcessIecTime(TwinCATDataType.DateTime, raw)!;
|
|
roundTrip.ShouldBe(original);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConvertForWrite_DT_rejects_pre_epoch_values()
|
|
{
|
|
Should.Throw<ArgumentOutOfRangeException>(() =>
|
|
AdsTwinCATClient.ConvertForWrite(
|
|
TwinCATDataType.DateTime,
|
|
new DateTime(1969, 12, 31, 23, 59, 59, DateTimeKind.Utc)));
|
|
}
|
|
|
|
[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);
|
|
}
|
|
}
|