fix(twincat): TOD write bound, historian note, driver-level refused-write test (#306)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -152,6 +152,12 @@ found against a real TwinCAT 2 PLC:
|
||||
the decoded type — re-browse/redeploy to pick up the new node type. A tag
|
||||
authored `UDInt` against a TIME/DATE symbol is unchanged: it still reads and
|
||||
writes the raw 32-bit counter (the guard tolerates the same-width divergence).
|
||||
**Historian:** a historized tag authored `Date`/`DateTime` now publishes
|
||||
`DriverDataType.DateTime`, which continuous historization skips as non-numeric
|
||||
(it was historized as `UInt32` before) — such tags stop recording. Author them
|
||||
`UDInt` to keep historizing the raw counter. `Time`/`TimeOfDay` are now
|
||||
`Float64` and still historize. A `TimeOfDay` write of 86 400 000 ms or more
|
||||
is refused `BadOutOfRange`.
|
||||
- **Sizes come from the resolved data type, not the symbol.** On a TwinCAT 2
|
||||
target the Beckhoff library reports a dotted *member* symbol's `ByteSize` as if
|
||||
the PLC's byte count were a bit count (`.OPC_MACHINEDATA.PROCESSSTATUS`
|
||||
|
||||
@@ -35,7 +35,7 @@ internal static class TwinCATTemporalCodec
|
||||
switch (type)
|
||||
{
|
||||
case TwinCATDataType.Time or TwinCATDataType.TimeOfDay:
|
||||
return value switch
|
||||
var ms = value switch
|
||||
{
|
||||
TimeSpan ts => ToUInt(ts.TotalMilliseconds),
|
||||
DateTime => throw new InvalidCastException($"{type} takes a duration in milliseconds, not a DateTime"),
|
||||
@@ -43,6 +43,10 @@ internal static class TwinCATTemporalCodec
|
||||
float f => ToUInt(f),
|
||||
_ => Convert.ToUInt32(value),
|
||||
};
|
||||
// TOD is a time of day: a full day or more is not representable.
|
||||
if (type == TwinCATDataType.TimeOfDay && ms >= 86_400_000u)
|
||||
throw new OverflowException($"{ms} ms is not a time of day (< 86 400 000)");
|
||||
return ms;
|
||||
case TwinCATDataType.Date or TwinCATDataType.DateTime:
|
||||
if (value is DateTime dt)
|
||||
{
|
||||
|
||||
@@ -191,6 +191,35 @@ public sealed class TwinCATReadWriteTests
|
||||
results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.BadNotWritable);
|
||||
}
|
||||
|
||||
/// <summary>A refused temporal write (#306) surfaces a status through WriteAsync, never an exception.</summary>
|
||||
[Theory]
|
||||
[InlineData(TwinCATDataType.TimeOfDay, 86_400_000d, TwinCATStatusMapper.BadOutOfRange)]
|
||||
[InlineData(TwinCATDataType.Time, -1d, TwinCATStatusMapper.BadOutOfRange)]
|
||||
[InlineData(TwinCATDataType.DateTime, "TimeSpan", TwinCATStatusMapper.BadTypeMismatch)]
|
||||
public async Task Refused_temporal_write_surfaces_status_not_exception(
|
||||
TwinCATDataType type, object value, uint expected)
|
||||
{
|
||||
var (drv, factory) = NewDriver(new TwinCATTagDefinition("T", "ads://5.23.91.23.1.1:851", "MAIN.T", type));
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
factory.Customise = () => new ConvertingFake();
|
||||
if (value is "TimeSpan") value = TimeSpan.FromSeconds(1);
|
||||
|
||||
var results = await drv.WriteAsync([new WriteRequest("T", value)], CancellationToken.None);
|
||||
|
||||
results.Single().StatusCode.ShouldBe(expected);
|
||||
}
|
||||
|
||||
// Runs the production write conversion the fake otherwise skips.
|
||||
private sealed class ConvertingFake : FakeTwinCATClient
|
||||
{
|
||||
public override Task<uint> WriteValueAsync(
|
||||
string symbolPath, TwinCATDataType type, int? bitIndex, object? value, CancellationToken ct)
|
||||
{
|
||||
AdsTwinCATClient.ConvertForWrite(type, value);
|
||||
return base.WriteValueAsync(symbolPath, type, bitIndex, value, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write exceptions surface as BadCommunicationError status.</summary>
|
||||
[Fact]
|
||||
public async Task Write_exception_surfaces_BadCommunicationError()
|
||||
|
||||
@@ -53,6 +53,12 @@ public sealed class TwinCATTemporalCodecTests
|
||||
public void Time_encode_refuses_out_of_range(double value)
|
||||
=> Should.Throw<OverflowException>(() => TwinCATTemporalCodec.Encode(TwinCATDataType.Time, value));
|
||||
|
||||
[Theory]
|
||||
[InlineData(86_400_000d)]
|
||||
[InlineData(-1d)]
|
||||
public void Tod_encode_refuses_a_full_day_or_more(double value)
|
||||
=> Should.Throw<OverflowException>(() => TwinCATTemporalCodec.Encode(TwinCATDataType.TimeOfDay, value));
|
||||
|
||||
[Fact]
|
||||
public void Dt_before_1970_is_refused()
|
||||
=> Should.Throw<OverflowException>(() =>
|
||||
|
||||
Reference in New Issue
Block a user