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:
+31
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user