Merge review R6 fixes: numeric array writes converted element-wise (checked) to the node's element type, refusing out-of-range/fractional/NaN; array-to-scalar refusal scoped to Modbus/S7; Primary-verdict pre-join window documented + real MemberUp subscription test; passive LocalDb pair half drain-gated on its mesh partner

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 08:43:59 -04:00
37 changed files with 764 additions and 112 deletions
+31 -12
View File
@@ -343,17 +343,34 @@ 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 — reachable only on
OpcUaClient); anything else is `BadTypeMismatch`, refused inline before dispatch.
- **The element type must match the node's DataType exactly** — an `Int32[]` to an `Int16` array,
a `UInt32[]` to an `Int32` array or a `Double[]` to a `Float` array is `BadTypeMismatch`, refused
inline (the one equivalence is a `Byte` array, which arrives as a ByteString). This is the
check the OPC UA SDK would normally make itself: it skips its own type check on any node with an
`OnWriteValue` handler, which every writable equipment node has, so the node manager makes it
instead. Without it a `UInt32[]` reached Galaxy's `int[]` encoder by CLR array covariance and was
written bit-reinterpreted (4,000,000,000 → −294,967,296) with a Good result. **Scalar** writes keep
their looser rule on purpose — any numeric to any numeric node — because consumers (ScadaBridge)
write CLR-typed values such as a `Double` to a `Float` node, and every driver's scalar encoder
converts with a range check, so an out-of-range scalar fails the device write and is reverted.
- An **array value to a scalar node** is likewise `BadTypeMismatch`, refused inline.
- **The element type must match the node's DataType, or be a numeric type that converts to it
exactly.** A matching element type (or a `Byte` array, which arrives as a ByteString) is dispatched
as written. A **numeric** array of another numeric element type is **converted, checked, element by
element** into the node's element type, and the converted array is what the node stores and the
driver receives — an `Int32[]` written to an `Int16` array reaches the driver as `short[]`. The whole
write is refused `BadTypeMismatch` inline, nothing dispatched, if **any** element is out of the
target's range, fractional for an integer node (`1.5` → `Int16` refused; `2.0` → `2`), NaN/±∞ for an
integer node, or finite but beyond `Float` range for a `Float` node. Every **non-numeric** mismatch
(`Boolean[]` → `Int32`, `Int32[]` → `String`, a `Variant[]`) is refused. This is the check the OPC UA
SDK would normally make itself: it skips its own type check on any node with an `OnWriteValue`
handler, which every writable equipment node has, so the node manager makes it instead. Without it a
`UInt32[]` reached Galaxy's `int[]` encoder by CLR array covariance and was written bit-reinterpreted
(4,000,000,000 → −294,967,296) with a Good result — that write is still refused (4,000,000,000
overflows `Int32`), while an in-range `UInt32[]` converts. The conversion is required, not a
nicety: ScadaBridge writes the CLR list it decoded from a `List` attribute, and its element types are
only Boolean/Int32/Float/Double/String/DateTime, so an exact-match rule left every `Int16`/`UInt16`/
`Byte`/`UInt32`/`Int64`/`UInt64` array node (Modbus's default `Int16` included) and a `Double` list to
a `Float` array unwritable from it. **Scalar** writes keep their looser rule — any numeric to any
numeric node, unconverted — because every driver's scalar encoder converts with a range check, so an
out-of-range scalar fails the device write and is reverted.
- An **array value to a scalar node** is refused `BadTypeMismatch` inline **only on Modbus and S7**,
whose scalar encoders cannot take one (`DriverArrayWriteSupport.RefusesArrayValueOnScalarTag`). Every
other driver keeps the pre-#610 behaviour — the write is dispatched and the driver decides. That
matters for browse-committed tags: the `/raw` commit sets the DataType but never `isArray`, so an
upstream array variable (OpcUaClient) or a Galaxy array attribute committed from the browser is a
**scalar** node into which the driver publishes an array, and writing that array back still works
(OpcUaClient forwards it to the upstream server, which arbitrates the shape; Galaxy encodes it). Set
`isArray` + `arrayLength` on such a tag to get the array-node checks above.
- 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.
@@ -362,7 +379,9 @@ What a client sees on a writable array node:
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`.
the classifier's raw-tag whitelist, and the mirrors repair through `BackingArrayWritable`. A
change to the array-on-scalar refusal (`ScalarRefusesArrayValue` — only a driver-type edit can flip
it) is deliberately NOT whitelisted and rebuilds.
> ⚠️ **Upgrade note — array tags become client-writable without a deploy.** Before #610 every
> array node was read-only regardless of its authored `AccessLevel`. On the first boot after