feat(twincat): TIME/TOD as Double ms, DATE/DT as UTC DateTime with read decode + write encode (#306)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -194,18 +194,28 @@ public sealed class TwinCATArrayCodecTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TIME / DATE / DT / TOD elements publish as their raw 32-bit count (<c>uint[]</c>) — the same
|
||||
/// CLR shape a scalar of those types publishes. Live: <c>.THROTTLE_MODE_DEFAULT</c>
|
||||
/// (<c>ARRAY [0..12] OF TIME</c>) begins <c>0, 10, 20</c> ms.
|
||||
/// TIME / TOD elements publish as Double milliseconds and DATE / DT as UTC DateTime (#306) — the same
|
||||
/// CLR shape a scalar publishes. Live: <c>.THROTTLE_MODE_DEFAULT</c> (<c>ARRAY [0..12] OF TIME</c>)
|
||||
/// begins <c>0, 10, 20</c> ms.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(TwinCATDataType.Time)]
|
||||
[InlineData(TwinCATDataType.TimeOfDay)]
|
||||
public void Decode_duration_elements_as_double_ms(TwinCATDataType type)
|
||||
=> TwinCATArrayCodec.Decode(type, [0, 0, 0, 0, 10, 0, 0, 0, 20, 0, 0, 0], new TwinCATArrayLayout(3, 4), utf8: false)
|
||||
.ShouldBe(new double[] { 0, 10, 20 });
|
||||
|
||||
/// <summary>DATE / DT elements decode to UTC DateTime from seconds since 1970.</summary>
|
||||
[Theory]
|
||||
[InlineData(TwinCATDataType.Date)]
|
||||
[InlineData(TwinCATDataType.DateTime)]
|
||||
[InlineData(TwinCATDataType.TimeOfDay)]
|
||||
public void Decode_time_and_date_elements_as_uint(TwinCATDataType type)
|
||||
=> TwinCATArrayCodec.Decode(type, [0, 0, 0, 0, 10, 0, 0, 0, 20, 0, 0, 0], new TwinCATArrayLayout(3, 4), utf8: false)
|
||||
.ShouldBe(new uint[] { 0, 10, 20 });
|
||||
public void Decode_date_elements_as_utc_datetime(TwinCATDataType type)
|
||||
=> TwinCATArrayCodec.Decode(type, [0, 0, 0, 0, 60, 0, 0, 0], new TwinCATArrayLayout(2, 4), utf8: false)
|
||||
.ShouldBe(new[]
|
||||
{
|
||||
new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
new DateTime(1970, 1, 1, 0, 1, 0, DateTimeKind.Utc),
|
||||
});
|
||||
|
||||
/// <summary>A BOOL element is one byte; any non-zero byte is TRUE (live: <c>.ARGTYPE_IS_BIT_TYPE</c>).</summary>
|
||||
[Fact]
|
||||
|
||||
@@ -93,8 +93,8 @@ public sealed class TwinCATDriverTests
|
||||
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);
|
||||
// IEC TIME/TOD surface as Double ms, DATE/DT as DateTime (#306).
|
||||
TwinCATDataType.Time.ToDriverDataType().ShouldBe(DriverDataType.Float64);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that status mapper covers known ADS error codes.</summary>
|
||||
|
||||
+8
-9
@@ -21,17 +21,16 @@ public sealed class TwinCATLowFindingsRegressionTests
|
||||
|
||||
// ---- Driver.TwinCAT-004 — TIME/DATE/DT/TOD surface unchanged but comments corrected ----
|
||||
|
||||
/// <summary>Verifies that IEC time types map to uint32 raw counters.</summary>
|
||||
/// <summary>IEC time types surface as native values (#306): TIME/TOD as Double ms, DATE/DT as DateTime.</summary>
|
||||
[Fact]
|
||||
public void Iec_time_types_map_to_uint32_raw_counter()
|
||||
public void Iec_time_types_map_to_native_types()
|
||||
{
|
||||
// Documents the contract called out in the corrected comments: TIME / DATE / DT / TOD
|
||||
// surface as their raw UDINT counter (32-bit unsigned), not as decoded DateTime/TimeSpan.
|
||||
// The next implementer who wants to decode them needs to see this mapping is intentional.
|
||||
TwinCATDataType.Time.ToDriverDataType().ShouldBe(DriverDataType.UInt32);
|
||||
TwinCATDataType.Date.ToDriverDataType().ShouldBe(DriverDataType.UInt32);
|
||||
TwinCATDataType.DateTime.ToDriverDataType().ShouldBe(DriverDataType.UInt32);
|
||||
TwinCATDataType.TimeOfDay.ToDriverDataType().ShouldBe(DriverDataType.UInt32);
|
||||
TwinCATDataType.Time.ToDriverDataType().ShouldBe(DriverDataType.Float64);
|
||||
TwinCATDataType.TimeOfDay.ToDriverDataType().ShouldBe(DriverDataType.Float64);
|
||||
TwinCATDataType.Date.ToDriverDataType().ShouldBe(DriverDataType.DateTime);
|
||||
TwinCATDataType.DateTime.ToDriverDataType().ShouldBe(DriverDataType.DateTime);
|
||||
// The raw counter stays reachable by declaring UDInt.
|
||||
TwinCATDataType.UDInt.ToDriverDataType().ShouldBe(DriverDataType.UInt32);
|
||||
}
|
||||
|
||||
// ---- Driver.TwinCAT-006 — ResolveHost sentinel when no devices are configured ----
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
||||
|
||||
/// <summary>#306 — TIME / DATE / DT / TOD decode and encode round trips on known values.</summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class TwinCATTemporalCodecTests
|
||||
{
|
||||
private static readonly DateTime Dt = new(2026, 10, 8, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public void Dt_decodes_seconds_since_1970_to_utc()
|
||||
{
|
||||
var raw = (uint)(Dt - DateTime.UnixEpoch).TotalSeconds;
|
||||
var v = (DateTime)TwinCATTemporalCodec.Decode(TwinCATDataType.DateTime, raw);
|
||||
v.ShouldBe(Dt);
|
||||
v.Kind.ShouldBe(DateTimeKind.Utc);
|
||||
TwinCATTemporalCodec.Encode(TwinCATDataType.DateTime, v).ShouldBe(raw);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Date_round_trips_and_truncates_to_day()
|
||||
{
|
||||
var day = new DateTime(2026, 10, 8, 0, 0, 0, DateTimeKind.Utc);
|
||||
var raw = TwinCATTemporalCodec.Encode(TwinCATDataType.Date, day);
|
||||
((DateTime)TwinCATTemporalCodec.Decode(TwinCATDataType.Date, raw)).ShouldBe(day);
|
||||
TwinCATTemporalCodec.Encode(TwinCATDataType.Date, day.AddHours(13)).ShouldBe(raw);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tod_round_trips_milliseconds()
|
||||
{
|
||||
const uint ms = (12 * 3600 + 30 * 60 + 15) * 1000 + 250; // TOD#12:30:15.250
|
||||
TwinCATTemporalCodec.Decode(TwinCATDataType.TimeOfDay, ms).ShouldBe(45_015_250d);
|
||||
TwinCATTemporalCodec.Encode(TwinCATDataType.TimeOfDay, 45_015_250d).ShouldBe(ms);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Time_round_trips_milliseconds_and_accepts_timespan()
|
||||
{
|
||||
const uint ms = ((1 * 60 + 2) * 60 + 3) * 1000 + 4; // T#1h2m3s4ms
|
||||
TwinCATTemporalCodec.Decode(TwinCATDataType.Time, ms).ShouldBe(3_723_004d);
|
||||
TwinCATTemporalCodec.Encode(TwinCATDataType.Time, 3_723_004d).ShouldBe(ms);
|
||||
TwinCATTemporalCodec.Encode(TwinCATDataType.Time, new TimeSpan(0, 1, 2, 3, 4)).ShouldBe(ms);
|
||||
TwinCATTemporalCodec.Encode(TwinCATDataType.Time, 5u).ShouldBe(5u); // raw integer still writes
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1d)]
|
||||
[InlineData(double.NaN)]
|
||||
[InlineData(5_000_000_000d)]
|
||||
public void Time_encode_refuses_out_of_range(double value)
|
||||
=> Should.Throw<OverflowException>(() => TwinCATTemporalCodec.Encode(TwinCATDataType.Time, value));
|
||||
|
||||
[Fact]
|
||||
public void Dt_before_1970_is_refused()
|
||||
=> Should.Throw<OverflowException>(() =>
|
||||
TwinCATTemporalCodec.Encode(TwinCATDataType.DateTime, new DateTime(1960, 1, 1, 0, 0, 0, DateTimeKind.Utc)));
|
||||
|
||||
[Fact]
|
||||
public void Wrong_shape_is_a_type_mismatch()
|
||||
{
|
||||
Should.Throw<InvalidCastException>(() => TwinCATTemporalCodec.Encode(TwinCATDataType.Time, Dt));
|
||||
Should.Throw<InvalidCastException>(() => TwinCATTemporalCodec.Encode(TwinCATDataType.DateTime, TimeSpan.FromSeconds(1)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Client_write_conversion_routes_temporal_types_through_the_codec()
|
||||
{
|
||||
AdsTwinCATClient.ConvertForWrite(TwinCATDataType.Time, 1000d).ShouldBe(1000u);
|
||||
AdsTwinCATClient.ConvertForWrite(TwinCATDataType.DateTime, Dt)
|
||||
.ShouldBe((uint)(Dt - DateTime.UnixEpoch).TotalSeconds);
|
||||
// Authored UDInt against a TIME symbol keeps the raw counter.
|
||||
AdsTwinCATClient.ConvertForWrite(TwinCATDataType.UDInt, 1000u).ShouldBe(1000u);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user