feat(address-space): server-side array writes gated per driver capability (#610)
Array nodes were forced read-only for every driver. A new single authority, Core.Abstractions DriverArrayWriteSupport, says which (driver, element type) pairs encode a whole-array write: Modbus (Coils / numeric HoldingRegisters), S7 (Byte/Int16/UInt16/Int32/UInt32/Float32), OpcUaClient (pass-through) and Galaxy (Boolean/Int32/Int64/Float/Double/String/DateTime). Everything else stays read-only, so the server never advertises a write the driver refuses. - Composer derives RawTagPlan.ArrayWritable + UnsReferenceVariable .BackingArrayWritable at compose time (both seams, byte-parity); the applier's one writability rule is Writable && (!IsArray || ArrayWritable) for materialise and surgical paths, raw and UNS alike. - ArrayWritable is on the classifier's surgical raw-tag whitelist and mirrored on the UNS record (UnsReferenceBackingParityTests updated). - Node manager refuses a wrong-length / non-array value on an array node (BadTypeMismatch) and any IndexRange write (BadWriteNotSupported) inline, before dispatch; WriteOperate/ACL gates and the revert are unchanged. - Per-driver parity tests pin the table against the real Modbus write path, S7 EncodeArrayBlock and Galaxy MxValueEncoder (UInt32/UInt64 excluded: array covariance reinterprets their bits). - Over-the-wire proof in OpcUaServer.IntegrationTests (ArrayWriteWireTests). - ValueRank > 1 documented as out of scope by decision (docs/Uns.md). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+42
-2
@@ -312,10 +312,50 @@ Combined with historization (values are arrays — history of the whole array sn
|
||||
| **AB Legacy** | PCCC multi-element file read via libplctag (cap 256 elements) | Unit-proven (sim fixture down) |
|
||||
| **TwinCAT** | Raw-bytes read of the declared `SymbolPath` at the resolved array type's layout + per-element decode; native ADS notifications carry the whole array (same decode). Multi-dim PLC arrays publish flattened; a shorter `arrayLength` reads the leading sub-range | Live-verified 2026-10-07 on two TwinCAT 2.11 PLCs (see [TwinCAT.md §Array tags](drivers/TwinCAT.md#array-tags)) |
|
||||
|
||||
### Array writes (#610)
|
||||
|
||||
An authored-`ReadWrite` array tag is **writable only when its driver can write that array** —
|
||||
the server never advertises a write the driver will refuse. The capability is one table,
|
||||
`DriverArrayWriteSupport` (`Core.Abstractions`), consulted by the composer at compose time and
|
||||
carried on the plan records (`RawTagPlan.ArrayWritable`, mirrored onto every UNS reference as
|
||||
`UnsReferenceVariable.BackingArrayWritable`); the node is writable iff
|
||||
`Writable && (!IsArray || ArrayWritable)`, on the raw node and every UNS mirror alike.
|
||||
|
||||
| Driver | Array writes | Element types |
|
||||
|---|---|---|
|
||||
| **Modbus** | Yes | `region: Coils` (FC15, any element); `region: HoldingRegisters` with `Int16`/`UInt16`/`Bcd16`/`Int32`/`UInt32`/`Bcd32`/`Float32`/`Int64`/`UInt64`/`Float64` (FC16). Not BitInRegister / String / Bool-in-register; input regions are read-only |
|
||||
| **S7** | Yes | `Byte`/`Int16`/`UInt16`/`Int32`/`UInt32`/`Float32` (one contiguous block). Not `Bool` (would clobber neighbouring bits) nor the wide / String / DateTime types |
|
||||
| **OpcUaClient** | Yes | All — forwarded upstream unchanged; the upstream server decides |
|
||||
| **Galaxy** | Yes | OPC UA `Boolean`/`Int32`/`Int64`/`Float`/`Double`/`String`/`DateTime` (the `MxValueEncoder` array variants) |
|
||||
| AB CIP, AB Legacy, TwinCAT, FOCAS, MQTT, MTConnect, Sql, Calculation | **No** — scalar-only write encoders; array nodes stay read-only and a client write is `BadNotWritable` | — |
|
||||
|
||||
Each "Yes" row is pinned against the driver's real encoder by a parity test in that driver's
|
||||
test project (`*ArrayWriteSupportParityTests`), so a driver gaining or losing an array encoder
|
||||
fails a test until the table follows.
|
||||
|
||||
What a client sees on a writable array node:
|
||||
|
||||
- **Whole-array writes only.** The value must be a 1-D array of exactly `arrayLength` elements
|
||||
(an unbounded `arrayLength`/`ArrayDimensions = [0]` takes any length); anything else is
|
||||
`BadTypeMismatch`, refused inline before dispatch. A partial **IndexRange** write is
|
||||
`BadWriteNotSupported` — every array-capable driver writes the whole block, and the server
|
||||
does not hold the device array authoritatively enough to read-modify-write it.
|
||||
- The same realm-qualified `WriteOperate` / ACL gate as a scalar applies to both NodeIds, and a
|
||||
device-refused array write is reverted on the NodeId it was written through (the scalar
|
||||
write-outcome self-correction, unchanged).
|
||||
- Flipping a tag's array-write capability (a driver-type or element-type edit on an existing
|
||||
array tag) is a **surgical** in-place writability update, not a rebuild: `ArrayWritable` is on
|
||||
the classifier's raw-tag whitelist, and the mirrors repair through `BackingArrayWritable`.
|
||||
|
||||
### Out of scope (named deferrals)
|
||||
|
||||
- **Array writes** (inbound client→device write of an array value) — tagged for a follow-up phase. The S7 driver's array write path exists (`Byte`/`Int16`/`UInt16`/`Int32`/`UInt32`/`Float32`, not `Bool`; see [S7.md](drivers/S7.md)) but the applier still forces array nodes read-only (#610 item 1).
|
||||
- **Multi-dimensional arrays** (`ValueRank > 1`) — not supported; all arrays are 1-D.
|
||||
- **Multi-dimensional arrays** (`ValueRank > 1`) — **out of scope by decision** (#610 item 3),
|
||||
not merely deferred. Every driver's array read decodes a flat 1-D block (TwinCAT already
|
||||
publishes multi-dim PLC arrays flattened), and the node manager materialises every array
|
||||
`ValueRank = OneDimension` with a single `ArrayDimensions` entry. Supporting rank > 1 would
|
||||
need per-rank dimensions in TagConfig, the plan records, the classifier whitelist, the node
|
||||
manager and every driver's decode/encode, and no consumer has asked for it. Author a
|
||||
multi-dimensional PLC array as a flat 1-D tag (row-major), or as several 1-D tags.
|
||||
- **Array historization** — a historized array tag materialises with the correct `Historizing` flag, but the historian backend treats the value as an opaque blob; per-element history is out of scope. (Continuous historization is numeric-analog only — array / non-numeric values are not recorded.)
|
||||
|
||||
See the individual driver docs under `docs/drivers/` for per-driver implementation details.
|
||||
|
||||
Reference in New Issue
Block a user