Merge #610: server-side array writes gated per driver capability (DriverArrayWriteSupport); ArrayWritable on the surgical whitelist + UNS Backing; shape/IndexRange refusal; ValueRank>1 out of scope

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe

# Conflicts:
#	docs/drivers/S7.md
This commit is contained in:
Joseph Doherty
2026-10-08 07:39:40 -04:00
20 changed files with 907 additions and 35 deletions
+3 -2
View File
@@ -80,8 +80,9 @@ removed later.
Editing a raw **tag's** attributes is no longer a fleet-wide event. A **non-alarm**
raw-tag delta whose only differences are the in-place-updatable attributes —
`Writable`, `IsHistorized`, `HistorianTagname`, `DataType`, `IsArray`, `ArrayLength` —
and/or its set of referencing equipment is applied **in place** on the existing Raw-realm
`Writable`, `IsHistorized`, `HistorianTagname`, `DataType`, `IsArray`, `ArrayLength`,
`ArrayWritable` (the driver's array-write capability, #610 — see
[Uns.md §Array writes](Uns.md#array-writes-610)) — and/or its set of referencing equipment is applied **in place** on the existing Raw-realm
node (`ISurgicalAddressSpaceSink.UpdateTagAttributes`), so every client subscription on
that tag and on the rest of the server survives. The same is true of **every** UNS
tag-reference change: adding, removing or re-pointing a reference used to flip the
+42 -2
View File
@@ -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.
+4 -3
View File
@@ -255,12 +255,13 @@ read is byte-granular, so a matching write would overwrite the 7 neighbouring bi
touches, and a safe bit-packed write needs read-modify-write. Mark Bool arrays `writable: false`. A `null` array element is rejected (`BadTypeMismatch`, nothing written)
rather than coerced to 0. An array write larger than one PDU is split by S7.Net into several writes and is
**not atomic** — a mid-write failure can leave the PLC array partly updated.
Note the *server* still presents every array node read-only to OPC UA clients ([Uns.md §Array
tags](../Uns.md#array-tags-1-d), #610 item 1) — the driver path is reachable today through the Client
The server exposes these as **writable OPC UA array nodes** (#610) for exactly the element types above —
a `Bool` / wide-type S7 array stays read-only at the node (`BadNotWritable`), see [Uns.md §Array
writes](../Uns.md#array-writes-610). The driver path is also reachable directly through the Client
CLI / driver-level callers and is proven by the unit suite plus the snap7 fixture test
(`S7_1500ArrayWriteTests`); the AdminUI tag editor rejects wide-type and writable-Bool arrays at author time.
**Deferrals** — server-side exposure of array writes, multi-dimensional arrays, per-element historization,
**Deferrals** — multi-dimensional arrays (out of scope by decision, see Uns.md), per-element historization,
wide-type array elements (`Int64`, `UInt64`, `Float64`, `String`, `DateTime`), and
Timer/Counter array elements.