Merge branch 'master' into b626-opcuaclient

This commit is contained in:
Joseph Doherty
2026-10-08 14:47:49 -04:00
72 changed files with 3775 additions and 343 deletions
+40
View File
@@ -67,6 +67,9 @@ pieces for driver-side routing (e.g. deciding a bit needs read-modify-write):
| `ST9:0` | String file (82-byte fixed-length) |
| `T4:0.ACC` / `C5:0.PRE` | Timer / counter sub-element |
| `I:0/0` / `O:1/2` / `S:1` | Input / output / status system files (no file number) |
| `I:017/07` | **PLC-5 only:** `I:`/`O:` word and bit indices are **octal** (#244); a digit 8/9 is a parse error |
| `RTC:0.HR` / `HSC:0.ACC` | **MicroLogix only:** function files RTC, HSC, DLS, MMI, PTO, PWM, STI, EII, IOS, BHI (#245) |
| `N7:[N7:0]` / `N[N7:0]:5` | Indirect (index word read from the PLC) — parsed, **not supported at runtime** (#247) |
`AbLegacyDataType` covers the corresponding PCCC types: `Bit`, `Int` (N), `Long`
(L), `Float` (F), `AnalogInt` (A), `String` (ST), and the `TimerElement` /
@@ -85,6 +88,42 @@ this is an inherent limitation of any software-level RMW. Input-image (`I`) bit
writes are forwarded to the PLC as-is; the device drives the input image from
physical inputs and may reject the write.
### Family-specific address forms (#244 / #245 / #247)
`AbLegacyAddress.TryParse(text, family)` is family-aware; the driver passes the device's `PlcFamily`
(the one-argument overload is the family-neutral grammar: decimal, no function files).
- **PLC-5 octal I/O (#244).** RSLogix 5 shows `I:`/`O:` word and bit indices in octal, so `I:017/07` is word 15,
bit 7 and `I:017/17` is bit 15. Only `I`/`O` on PLC-5 (`OctalIoAddressing` on the family profile) — SLC,
MicroLogix and Logix-PCCC stay decimal, and PLC-5 `N`/`B`/... files stay decimal. The record stores decimal and
`ToLibplctagName()` **emits decimal**, so the libplctag `name=` string and the string the native transport
re-parses carry identical numbers. This is not a double conversion: libplctag's `pccc.c`
(`parse_pccc_elem_num` / `parse_pccc_bit_num`) parses decimal numerals only (a FIXME there says octal is
unsupported), so the driver must convert. Pinned by `Plc5_io_indices_are_octal` (`I:017/17` -> `I:15/15`).
The connectivity-probe address is normalised the same way.
- ⚠️ **Behaviour change / migration.** Before this change a PLC-5 `I:`/`O:` address was passed to libplctag as
written, i.e. read as **decimal**. An address copied from RSLogix in octal (e.g. `I:017`) therefore read the
wrong word, and an authored decimal such as `I:1/12` now means bit 10 (octal 12). A previously "working"
address containing the digits 8 or 9 (`I:018`, `O:1/9`) is no longer valid on PLC-5. Re-author any PLC-5
I/O tags in RSLogix octal. Such a tag is refused (see below), never silently misread.
- **A malformed / family-invalid address** (also: a function-file letter off MicroLogix) reads and writes
`BadNodeIdUnknown` for that tag only, creates no runtime, leaves driver health alone, and logs **one**
Warning per tag. The same check (`AbLegacyAddress.Validate`) runs in the AdminUI tag editor (family-independent:
rejects only what *no* family accepts) and the deploy gate (`EquipmentTagConfigInspector`, using the device's
`DeviceConfig.PlcFamily`, absent = SLC 500, unrecognised = unknown) — so `I:018` on a PLC-5 device warns at
deploy (an error under `TagConfigValidationMode.Error`), and the editor is never stricter than the gate.
- **MicroLogix function files (#245).** `RTC`, `HSC`, `DLS`, `MMI`, `PTO`, `PWM`, `STI`, `EII`, `IOS`, `BHI`
(single fixed instance — no file number — with a `.SUBELEMENT`; no bit index) parse on MicroLogix devices only.
`AbLegacyFunctionFile.SubElementType` carries the sub-element catalogue (flags → Boolean, words → Int32; unknown
members default to Int32). ⚠️ **Wire-unverified on both transports:** there is no capture and no simulator
coverage; the libplctag path passes the name through and the native transport has no file-type code for them, so
it refuses the tag as `Unsupported` (`BadNotSupported` to the client; see Wire evidence).
- **Indirect addressing (#247).** `N7:[N7:0]` (word from another address) and `N[N7:0]:5` (file from another
address) parse, one level deep (the inner address must be direct, and carries the family's octal rule).
**Runtime resolution is not implemented** (it needs a pre-read of the index word and per-resolved-address
runtimes): reads and writes of an indirect tag answer `BadNotSupported` before any runtime is created, on both
transports.
## Configuration
`AbLegacyDriverOptions` (`Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs`)
@@ -172,6 +211,7 @@ Everything below is against the `ab_server` simulator; no real PCCC controller h
| N-file write (`N7:5`) | capture `slc500-write-n7` + native `AbLegacyReadSmokeTests` write/read round trip (SLC 500) |
| F-file read (`F8`) | one capture, `slc500-read-f8` (codec parity only; no native F write on a wire) |
| S (incl. the default probe `S:0`), B, T, C, R, L, ST, A, O, I | **none** — encoded from the PCCC file-type table |
| MicroLogix function files (RTC/HSC/...), PLC-5 octal I/O, indirect addressing | **none** — parsed only; native refuses function files and indirect names as Unsupported |
| T/C/R sub-elements (`.PRE`/`.ACC`/`.LEN`/`.POS`), 6-byte T/C/R element layout | **none** |
### Native known gaps
+4
View File
@@ -91,6 +91,10 @@ on.
| `MACRO:500` | Custom macro variable | System / user macro variable number |
| `FIXED:Identity/SeriesNumber` | Fixed-tree field | A path into the [fixed node tree](#fixed-node-tree) — read-only |
PMC letters are series-gated by `FocasCapabilityMatrix.PmcLetters`; the 16i accepts `X Y F G R D` (F/G are
the CNC-to-PMC / PMC-to-CNC handshake groups its ladders use, #265 — reads only, FOCAS stays read-only), while
`M C E A K T` remain 0i-F / 30i-family.
The first three name a location the driver asks the CNC for on demand.
`FIXED:` does not: it names a field of the per-device fixed-tree snapshot the
driver's own poll loop already maintains, so a `FIXED:` read issues no wire
+59 -20
View File
@@ -230,10 +230,37 @@ Other status codes an observation can surface:
|---|---|
| `BadTypeMismatch` (`0x80740000`) | The Agent's text isn't a value of the tag's coerced type at all |
| `BadOutOfRange` (`0x803C0000`) | The Agent reported a number the coerced type can't represent |
| `BadNotSupported` (`0x803D0000`) | A shape this build doesn't materialize — a `TIME_SERIES` vector, or a structured `DATA_SET`/`TABLE` observation (deferred to P1.5; see [Known limitations](#known-limitations)) |
| `BadDecodingError` (`0x80070000`) | A `TIME_SERIES` vector whose element count disagrees with the observation's own `sampleCount` (or the tag's fixed `ArrayDim`) — see [TIME_SERIES](#time_series-arrays-627) |
| `BadNotSupported` (`0x803D0000`) | A shape this build doesn't materialize — a structured `DATA_SET`/`TABLE` observation (decided: stays unsupported, #627), or an array tag declared as anything other than Float64 |
| `BadWaitingForInitialData` | Tag is authored but the Agent hasn't reported it yet |
| `BadNodeIdUnknown` | DataItem id is neither authored nor ever observed |
### TIME_SERIES arrays (#627)
A `SAMPLE` DataItem with `representation="TIME_SERIES"` is served as a
**variable-length `double[]`** (`ValueRank = 1`, no fixed `ArrayDimensions` —
a live Agent never declares `sampleCount` on the DataItem, see Known
limitations 4; a probe that does declare one yields a fixed `ArrayDim`).
Parsing: the observation text is split on any whitespace, each token parsed
invariant-culture. The result is **all-or-nothing** — one non-numeric /
non-finite token is `BadTypeMismatch`, a literal `UNAVAILABLE` token or an
empty body is `BadNoCommunication`, and a count that disagrees with the
observation's own `sampleCount` attribute (or a fixed `ArrayDim`) is
`BadDecodingError`. A partial vector is never served as Good. Only a
**Float64** array tag is served; any other declared element type stays
`BadNotSupported`. An authored tag needs `"isArray": true` and
`"dataType": "Float64"` in its TagConfig (the AdminUI editor shows an advisory
for any other array type; a browse-commit does not stamp `isArray`, so set it
in the Tag modal's array section).
**Sample rate is not exposed on the served node.** The observation's
`sampleRate` and `sampleCount` attributes are parsed (`MTConnectObservation`)
but the OPC UA node has no description/property slot through `DriverAttributeInfo`,
and a sibling property node would need a new address-space mechanism. Decision:
document it — the rate is a property of the Agent's DataItem (`sampleRate` in
`/probe`), read it there. **`DATA_SET` / `TABLE` stay `BadNotSupported`** (decision, #627): they are
key/value maps with no scalar or array OPC UA shape that preserves the keys.
### Subscribe — the `/sample` pump
One shared `/sample` long-poll stream per driver instance (the Agent streams
@@ -383,14 +410,30 @@ for anything beyond values-and-conditions.
`GetHostStatuses()` fresh, and the result rides `DriverHealthChanged` to
the AdminUI `/hosts` page (no SQL; the `DriverHostStatus` table was
dropped). See `docs/Telemetry.md`.
2. **`RawTagEntry.DeviceName` is accepted on the wire shape but ignored for
routing.** One driver instance owns exactly one Agent client scoped by
`MTConnectDriverOptions.DeviceName` (the top-level config key); the
per-tag `RawTagEntry.DeviceName` field the v3 raw-tag identity carries is
never read by the driver. Two `/raw` Devices authored under one MTConnect
driver instance both resolve to the same one Agent connection. Real
per-device routing (a client per device) would be a design change, not a
bug fix.
2. **One MTConnect driver instance = one Agent client = at most one `/raw`
Device (#628, decision (b)).** The driver never reads
`RawTagEntry.DeviceName`; every raw tag binds through the single Agent client
scoped by `MTConnectDriverOptions.DeviceName`. The v3 raw tree always has a
Device level (a new driver auto-creates `Device1`), and that single-Device
layout is the normal, valid shape — its `DeviceConfig` is merged up into the
driver config by `DriverDeviceConfigMerger` (this is how the endpoint lands).
With **two or more** Devices under one MTConnect driver there is no merge-up
and no routing, so which Device supplies the endpoint is ambiguous. That is
now a **deploy-time `DraftValidator` error** (`MTConnectMultipleDevices`),
and the AdminUI refuses to *create* the second Device
(`RawTreeService.CreateDeviceAsync`, same message): "MTConnect binds all tags
through one agent client per driver instance; author one MTConnect driver per
agent device." Option (a), a client per Device, was rejected as a design
change with no demand. A multi-device Agent is served by several driver
instances, each with its own `DeviceName` scope. The driver's init-time
Warning for differing `RawTagEntry.DeviceName` values stays as defence in
depth for configs that predate the rule.
⚠️ **Upgrade note:** an MTConnect driver that *already* has two or more Device
rows fails its next deploy with `MTConnectMultipleDevices` (the rule counts every
Device row, enabled or not, because nothing at runtime reads `Device.Enabled`).
Before upgrading, move each extra Device's tags under its own MTConnect driver
and delete the extra Device rows.
**The write path is the one place a device name is resolved per DataItem**,
and it does not use `RawTagEntry.DeviceName` either: it reads the owning
@@ -399,18 +442,13 @@ for anything beyond values-and-conditions.
So `MTConnectDriverOptions.DeviceName` does double duty — request scope for
reads/browse, fallback write target for writes.
**It is no longer silent (#613).** When the instance's raw tags carry two or
more distinct non-blank `RawTagEntry.DeviceName` values, driver init logs one
Warning naming them and the driver-level scope they collapse into. A single
value is not reported (it is just the `/raw` Device's own name). Real
per-device routing remains deferred.
3. **Authoring only *warns* when a declared coercion type cannot hold the
inferred one (#613).** The AdminUI tag editor (`MTConnectTagConfigModel.Warnings()`)
compares the declared type with `MTConnectDataTypeInference` over the
`/probe` metadata a browse-commit stamped (`mtCategory`/`mtType`), and shows an
advisory line — it never blocks a save (the deploy gate does not refuse it) and
is a no-op for hand-authored blobs with no stamped metadata. `representation`
is not on the model, so TIME_SERIES arrays are not covered. Beyond that,
is not on the model; TIME_SERIES arrays get their own `isArray` advisory (declare Float64). Beyond that,
nothing validates a raw tag's declared OPC UA `DataType` against the
blob's coercion type. The `/probe`-sourced inference
(`MTConnectDataTypeInference`) narrows how often an author picks a wrong
@@ -427,9 +465,11 @@ for anything beyond values-and-conditions.
declare a `sampleCount`, is unrealistic on this point and exists only to
pin the parsing rule itself.
5. **CONDITION is a plain `String`, not a native Part 9 alarm** (see above).
6. **`TIME_SERIES` arrays and structured `DATA_SET`/`TABLE` observations
surface as `BadNotSupported`**, not an approximation — see the Quality
mapping table.
6. **Structured `DATA_SET`/`TABLE` observations surface as
`BadNotSupported`**, not an approximation — see the Quality mapping table.
`TIME_SERIES` is served as `double[]` (#627, see
[TIME_SERIES arrays](#time_series-arrays-627)); the sample rate is not
surfaced on the node.
## Deferred (not in this build)
@@ -447,8 +487,7 @@ for anything beyond values-and-conditions.
- **P1.5 fast-follow** (design
[§9](../plans/2026-07-15-mtconnect-driver-design.md#9-phasing--effort)):
CONDITION → native OPC UA Part 9 alarms via `IAlarmSource` (the Galaxy
native-alarm pattern); `TIME_SERIES` SAMPLE arrays materialized as real
OPC UA arrays; EVENT controlled-vocabulary values → OPC UA enumerations.
native-alarm pattern); EVENT controlled-vocabulary values → OPC UA enumerations.
- **P2 (on demand): SHDR adapter ingest.** A `SourceMode: "Agent" | "Shdr"`
switch that opens the raw pipe-delimited SHDR TCP socket directly. Loses
auto-discovery (no device model without an Agent in front, so
+74 -8
View File
@@ -65,6 +65,30 @@ hardware. A wrong slot causes a connection refusal during the handshake. See
`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7DriverOptions.cs` for the
per-field defaults.
### TSAP / connection-type override
By default S7.Net derives both ISO-TSAPs from `CpuType` / `Rack` / `Slot` (local `01.00`; remote
`03.<rack×0x20+slot>` for the S7-300/400/1200/1500 families). Three optional keys override that
for CPUs and gateways that hand out connection resources by TSAP (S7-400H, CP343, LOGO!):
| Key | Form | Effect |
|---|---|---|
| `localTsap` | `"01.00"` (also `"0100"`, `"0x0100"`, or a 16-bit number) | replaces the local TSAP |
| `remoteTsap` | `"03.01"` (same forms) | replaces the whole remote TSAP |
| `connectionType` | `"PG"` \| `"OP"` \| `"S7Basic"` (string) | writes `1` / `2` / `3` into the **first** remote-TSAP byte, keeping `rack×0x20+slot` as the second |
`remoteTsap` and `connectionType` are mutually exclusive (a config carrying both fails to load); a
malformed TSAP fails to load rather than silently falling back to the slot-derived default. With none of
the three set the connection is built exactly as before. The override applies to the driver, the AdminUI
Test Connect probe and the AdminUI S7 driver form (Connection type / Local TSAP / Remote TSAP fields).
### Test Connect and PUT/GET
After the handshake, Test Connect reads 2 bytes from `MB0`. A CPU with PUT/GET disabled (the S7-1200/1500
default) completes the handshake but denies every read, so the probe fails with an actionable "enable
PUT/GET communication" message instead of a false green tick. A CPU that merely rejects that address (no
`MB0`) still passes — the connection is fine; any other read fault fails the probe with its message.
## Address forms
Addresses use Siemens TIA-Portal / STEP 7 Classic syntax, parsed by
@@ -102,19 +126,52 @@ extra PLC round-trip.
| `UInt64` | ULINT/LWORD | 8 bytes | yes | yes |
| `Float64` | LREAL | 8 bytes | yes | yes |
| `String` | STRING | `StringLength + 2` bytes | yes | yes |
| `DateTime` | DATE_AND_TIME | 8 bytes | yes | yes |
| `WString` | WSTRING | `4 + 2 × StringLength` bytes | yes | yes |
| `Char` | CHAR | 1 byte | yes | yes |
| `WChar` | WCHAR | 2 bytes | yes | yes |
| `DateTime` | DATE_AND_TIME (DT) | 8 bytes (BCD) | yes | yes |
| `Dtl` | DTL | 12 bytes | yes | yes |
| `Date` | DATE | 2 bytes | yes | yes |
| `Tod` | TIME_OF_DAY | 4 bytes | yes | yes |
| `Time` | TIME | 4 bytes | yes | yes |
| `S5Time` | S5TIME | 2 bytes | yes | yes |
| Timer (`T{n}`) | TIME | — | yes | **read-only** |
| Counter (`C{n}`) | COUNTER | — | yes | **read-only** |
Wide types (`Int64`, `UInt64`, `Float64`, `String`, `DateTime`) and Timer/Counter require
byte-anchored addressing — see the section below.
Wide types (`Int64`, `UInt64`, `Float64`, `String`, `DateTime`, and the string / date-time
types below) and Timer/Counter require byte-anchored addressing — see the section below.
### String, character and date/time/duration types
These add no new OPC UA data types — they map onto existing ones:
| DataType | OPC UA type | Representation | Write refuses (`BadOutOfRange`, nothing sent) |
|---|---|---|---|
| `String` | String | S7 `[maxLen][curLen][chars]`, ASCII | over `StringLength` (S7.Net behaviour) |
| `WString` | String | `[maxLen UInt16][curLen UInt16][UTF-16BE]`; `stringLength` is the declared max in characters, **0–16382** (default 254) | more UTF-16 code units than `StringLength` |
| `Char` | String (1 char) | 1 byte, Latin-1 | a character above U+00FF |
| `WChar` | String (1 char) | UTF-16BE code unit | — (a string whose length is not 1 is `BadTypeMismatch`) |
| `DateTime` | DateTime | DT, 8-byte BCD (S7.Net codec; 1990–2089) | out-of-range year |
| `Dtl` | DateTime | year UInt16 + month/day/weekday/h/m/s + UInt32 nanoseconds; 100 ns resolution | year outside 1970–2262 |
| `Date` | DateTime (midnight) | UInt16 days since 1990-01-01 (max 2169-06-06) | before 1990-01-01 or after 2169-06-06 |
| `Tod` | **Int32 ms** since midnight | UInt32 ms | outside 0–86 399 999 |
| `Time` | **Int32 ms** (signed duration) | Int32 ms | outside the Int32 range |
| `S5Time` | **Int32 ms** | 2-bit time base (10 ms / 100 ms / 1 s / 10 s) + 3 BCD digits | above 9 990 000 ms, negative, or not exactly representable (resolution is 10 ms up to 9 990 ms, then coarser) — **never rounded** |
A fractional millisecond `Double` is rounded to the nearest whole millisecond (banker's rounding), exactly
as the driver does for every other Int32 tag write; only values off the S5TIME time-base grid are refused.
A write of a `TimeSpan` is also accepted for `Tod` / `Time` / `S5Time`. A PLC buffer the codec cannot
decode — an uninitialised (all-zero) `Dtl`, a non-BCD `S5Time` digit, a `WString` whose current length
exceeds its reserved length — reads `BadOutOfRange` for that tag only, without degrading driver health
or condemning the connection. `S5Time` writes pick the finest time base that holds the value. The
`Timer` area (`T{n}`) keeps its own S5TIME-to-**seconds** (`Float64`) decode.
## Wide types & Timer/Counter
### Byte-anchored addressing for wide / structured types
Wide types (`Int64`, `UInt64`, `Float64`/LReal, `String`, `DateTime`) are
**byte-anchored**: the address must use the `B` suffix pointing at the **first
Wide types (`Int64`, `UInt64`, `Float64`/LReal, `String`, `DateTime`, `WString`, `Char`,
`WChar`, `Dtl`, `Date`, `Tod`, `Time`, `S5Time`) are **byte-anchored**: the address must use the `B` suffix pointing at the **first
byte** of the value; the driver reads the correct number of contiguous bytes
based on the DataType.
@@ -125,6 +182,8 @@ based on the DataType.
| `Float64` | same | 8 | `DB1.DBB8` → LREAL at bytes 8–15 |
| `DateTime` | same | 8 | `DB3.DBB0` → DATE_AND_TIME at bytes 0–7 |
| `String` | same | `StringLength + 2` | `DB4.DBB0`, StringLength=40 → 42 bytes |
| `WString` | same | `4 + 2 × StringLength` | `DB5.DBB0`, StringLength=10 → 24 bytes |
| `Dtl` | same | 12 | `DB6.DBB0` → DTL at bytes 0–11 |
Using a `W` or `D` suffix with a wide DataType is a config error caught at
`InitializeAsync`. For array tags (`isArray: true`), wide-type element arrays
@@ -155,11 +214,18 @@ The following are explicitly deferred and will produce a config error or
`BadNotSupported` if attempted:
- **Wide-type arrays** — array tags (`isArray: true`) with element types `Int64`,
`UInt64`, `Float64`, `String`, or `DateTime`.
- **`S7WString`** (2-byte character strings; distinct from classic `STRING`).
- **`DTL` / `DateTimeLong`** (12-byte Siemens date-and-time-long).
`UInt64`, `Float64`, `String`, `DateTime`, or any of the string / date-time types above.
- **Timer / Counter writes** — surfaced as `BadNotWritable` until write support lands.
## Negotiated PDU size
The PDU size the CPU negotiates during the connection handshake (typically 240 bytes; 480 / 960 with
extended PDU) is snapshotted at every (re)connect and exposed as `S7Driver.NegotiatedPduSize` /
`S7Driver.GetDiagnostics()["S7.NegotiatedPduSize"]` (0 before the first connect and after shutdown). It is
also written to the `S7Driver connected` / `reconnected` log lines and to the AdminUI Test Connect
message (`S7 connected (CPU S71500, PDU 240 bytes)`). `DriverHealth` has no diagnostics bag in this
codebase, so the value is not yet on the `/hosts` health snapshot — adding one is a Core change.
## Capability surface
`S7Driver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe`
+24 -1
View File
@@ -135,6 +135,29 @@ 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).
**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`
@@ -315,7 +338,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) |