fix(address-space): numeric array writes convert (checked) to the node element type; array-to-scalar refusal only for Modbus/S7 (#610 review R6)
MAJOR-1: R4's exact element-type match refused ScadaBridge's CLR-list writes (its element types are only Boolean/Int32/Float/Double/String/ DateTime) to every Int16/UInt16/Byte/UInt32/Int64/UInt64 array node and a Double list to a Float array. EvaluateEquipmentWriteStructure now converts a numeric array of another numeric element type element-wise, CHECKED, into the node's element type and hands the converted array back through OnEquipmentTagWrite's ref value, so the node stores and the driver receives the node's own type. Any out-of-range, fractional-to-integer, NaN/Inf-to- integer or Float-overflowing element refuses the whole write BadTypeMismatch (nothing dispatched) - so the UInt32 -> Int32 bit reinterpretation stays closed. Non-numeric mismatches stay refused. The wrong 'no client relies on a looser rule' comment is corrected. MINOR-1: the array-value-to-scalar-node refusal broke writes back to browse-committed array tags (committed scalar, isArray never set) on pass-through drivers. It now applies only when the owning driver's scalar encoder cannot take an array (DriverArrayWriteSupport. RefusesArrayValueOnScalarTag: Modbus, S7), carried RawTagPlan. ScalarRefusesArrayValue / UnsReferenceVariable.BackingScalarRefusesArrayValue -> EnsureVariable(refuseArrayOnScalar) -> a per-node set in the node manager. OpcUaClient/Galaxy/others keep pre-#610 dispatch. Not on the classifier whitelist (driver-type edit rebuilds); a mirror whose flag changes is recreated, not updated in place. 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
|
||||
|
||||
@@ -116,4 +116,4 @@ The session-less alarm feed (`GatewayGalaxyAlarmFeed`) and alarm acknowledger (`
|
||||
- **Duplicate `attributeRef` (#605 / 06-S-13)**: two authored RawPaths dialling the same Galaxy attribute are both dialled, but the reverse map is first-wins, so value fan-out and alarm `ConditionId` surface under the first RawPath only. The loser is logged at Warning and recorded in the skipped-tag ledger with the winner path in the reason.
|
||||
- **Subscribe-once reads are bounded (#605 / 06-S-3)**: `ReadAsync` on the production path (SubscribeBulk, first event, Unsubscribe) carries an internal 30 s deadline in addition to the caller token; slots that never publish come back `BadTimeout`, so `CancellationToken.None` can no longer hang on a silent tag.
|
||||
- **Supervisory advise is retried after a thrown failure (#605 / 06-S-12)**: the writer marks an item handle advised before the call; a thrown advise (not just a non-OK status) now unmarks it, fails the write closed and re-advises on the next write.
|
||||
- **Array writes (#610)**: a Galaxy array tag is client-writable only for the `MxValueEncoder` array types (OPC UA `Boolean`/`Int32`/`Int64`/`Float`/`Double`/`String`/`DateTime`) **and only with a declared `arrayLength`** — with none the node is unbounded, and a short whole-array write would *resize* the attribute. The element type must match the node exactly (`BadTypeMismatch` inline otherwise), and `MxValueEncoder` itself refuses `uint[]`/`ulong[]`: CLR array covariance let them match the `int[]`/`long[]` arms and be written bit-reinterpreted. Encoder-level only — Galaxy array writes have **no live gate** yet. See [Uns.md §Array writes](../Uns.md#array-writes-610), including the upgrade note.
|
||||
- **Array writes (#610)**: a Galaxy array tag is client-writable only for the `MxValueEncoder` array types (OPC UA `Boolean`/`Int32`/`Int64`/`Float`/`Double`/`String`/`DateTime`) **and only with a declared `arrayLength`** — with none the node is unbounded, and a short whole-array write would *resize* the attribute. The element type must match the node or be a numeric type that converts to it exactly — a numeric array of another element type is converted, checked, to the node's type in the node manager (an `Int32` list to an `Int64` array arrives as `long[]`), and any unrepresentable element refuses the write `BadTypeMismatch` inline. An array written to a **scalar** Galaxy node (a browse-committed array attribute without `isArray`) is still dispatched, as before #610. Separately, `MxValueEncoder` itself refuses `uint[]`/`ulong[]`: CLR array covariance let them match the `int[]`/`long[]` arms and be written bit-reinterpreted. Encoder-level only — Galaxy array writes have **no live gate** yet. See [Uns.md §Array writes](../Uns.md#array-writes-610), including the upgrade note.
|
||||
|
||||
Reference in New Issue
Block a user