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:
Joseph Doherty
2026-10-08 14:24:12 -04:00
parent d898262c4a
commit 90e3fe1cf7
10 changed files with 226 additions and 32 deletions
+18 -1
View File
@@ -135,6 +135,23 @@ found against a real TwinCAT 2 PLC:
registers, and the probe-tick retry re-checks it after a PLC program change.
Reads go through the Beckhoff symbol cache, which converts numeric values by
the PLC's own type, so only the string-kind rule applies there.
- **IEC temporal types publish native values (#306, behaviour change).** `TIME` and
`TOD` (TIME_OF_DAY) publish **`Double` milliseconds** (`DriverDataType.Float64`);
`DATE` and `DT` (DATE_AND_TIME) publish a **UTC `DateTime`** decoded from the
UDINT seconds-since-1970 counter (`DriverDataType.DateTime`). They used to
publish the raw counter as `UInt32`. Reads, notifications, array elements and
browse/discovery all agree (`TwinCATTemporalCodec`). No new `DriverDataType`
member was added (`Duration` would touch every switch in Core, the node
manager, historian mapper and editors); a duration is a Double ms, which is
what OPC UA `Duration` is on the wire. `LTIME`/`LDT` are not in the driver's
type surface and are still unsupported. **Writes** accept a `Double` ms /
`TimeSpan` (TIME, TOD) or a `DateTime` (DATE — truncated to a whole day — and
DT); negative, NaN, pre-1970 or > UDINT values are refused `BadOutOfRange`, and
a duration written to a date (or vice versa) `BadTypeMismatch`.
**Existing tags:** a tag authored with `dataType: "Time"` (etc.) now surfaces
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).
- **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`
@@ -315,7 +332,7 @@ works:
| `.ARRFAULTCODE` | `ARRAY [0..12] OF UINT` | UInt × 13 | `UInt16[13]` (element 3 = 32, matching `.ARRFAULTCODE[3]` scalar) |
| `.ARRUSPOWER` | `ARRAY [1..3, 1..2] OF REAL` / `[1..2, 1..2]` | Real × 6 / 4 | `Single[6]` / `Single[4]` — changing (~1060 W on Z33733); **23/23** notifications identical to an immediate poll |
| `P_INPUTVALUESFILTER.FBUSPWR1G1.SAMPLE` | `ARRAY [1..20] OF REAL` (member, symbol 10 B) | Real × 20, and × 5 (sub-range) | `Single[20]` changing, **14/14** identical; `Single[5]` = its first 5 |
| `.THROTTLE_MODE_DEFAULT` | `ARRAY [0..12] OF TIME` | Time × 13 | `UInt32[13]` `[0, 10, 20, 40, …]` ms |
| `.THROTTLE_MODE_DEFAULT` | `ARRAY [0..12] OF TIME` | Time × 13 | `Double[13]` `[0, 10, 20, 40, …]` ms (was `UInt32[13]` before #306; declare `UDInt` for the raw counter) |
| `.ARGTYPE_IS_BIT_TYPE` | `ARRAY [0..18] OF BOOL` | Bool × 19 | `Boolean[19]` |
| `.FORMAT_DEFAULT_PRECISION` / `.DWORD_DECADE_DIVIDER` / `.DIXTIMEALARM` | `INT` / `DWORD` / `UDINT` arrays | Int / UDInt | `Int16[12]` / `UInt32[10]` / `UInt32[101]` |
| `.IMQN_TABLE` / `.AMSNETID` / `.ARRMODULENO` | `LREAL` / `USINT` / `BYTE` arrays | LReal / USInt / SInt | `Double[32]` / `Byte[6]` / `SByte[13]` (changing on Z33856) |