feat(mqtt): read Sparkplug B array metrics as 1-D OPC UA arrays (lmxopcua#754)
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
R06:M-5 made Sparkplug *Array leaves unbindable because the ingestor refused them. The ingestor now decodes the packed bytes_value of all 13 array types (new SparkplugArrayCodec: LE fixed widths, int64-ms DateTime, uint32-count + MSB-first BooleanArray, NUL-terminated UTF-8 StringArray; golden vectors are the spec's examples, reproduced byte-for-byte by SparkplugNet 1.3.10) and publishes a typed 1-D array through the scalar coercion, all-or-nothing. Shape uses the fleet's one array model: the tag's platform isArray/arrayLength keys (ValueRank=1, ArrayDimensions=[arrayLength] or [0] = variable length). The driver reads them with TagConfigIntent's exact semantics and never rejects on them, so composer, driver, authoring model and deploy inspector agree (MqttArrayShapeParityTests). Shape mismatch (array metric on a scalar tag, or the reverse) is BadTypeMismatch - a String tag no longer gets the packed buffer as base64; a malformed body or a length differing from a declared arrayLength is BadDecodingError (MTConnect #627 verdict). Template/DataSet members of an array type take the same path. Arrays stay read-only (encoder refuses them; MQTT is not in DriverArrayWriteSupport). Browser: array leaves are bindable again and state isArray in AddressFields; the commit mapper writes "isArray": true (no arrayLength). Discovery reports IsArray/ArrayDim. Docs: Mqtt.md "Array metrics", design table, drivers rule. Tests: codec (golden + malformed), ingest through the real codec + Dispatch (the project's MiniBroker never forwards a PUBLISH, so there is no in-process broker harness), browse, discovery, commit mapper, parity corpus. Mutating the fix back out fails 18 MQTT + 9 AdminUI tests. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -351,6 +351,12 @@ public static class RawBrowseCommitMapper
|
||||
if (TryGetField(addressFields, MqttTagConfigKeys.DeviceId) is { } deviceId)
|
||||
o[MqttTagConfigKeys.DeviceId] = deviceId;
|
||||
|
||||
// A Sparkplug *Array metric binds only to an ARRAY tag (#754): the session states the bit, and it
|
||||
// is written as the JSON literal true — the only form the composer and the driver both read as an
|
||||
// array. No arrayLength: a Sparkplug array is variable-length, so the node is ArrayDimensions=[0].
|
||||
if (string.Equals(TryGetField(addressFields, MqttTagConfigKeys.IsArray), "true", StringComparison.OrdinalIgnoreCase))
|
||||
o[MqttTagConfigKeys.IsArray] = true;
|
||||
|
||||
return o.ToJsonString();
|
||||
}
|
||||
|
||||
@@ -422,12 +428,12 @@ public static class RawBrowseCommitMapper
|
||||
if (TryGetField(addressFields, MqttTagConfigKeys.Topic) is not null) return null;
|
||||
|
||||
// G2-F2: the Sparkplug browser deliberately states no address for a metric the ingestor will not
|
||||
// read (an *Array type, or a type with no driver mapping — R06:M-5), and says so on its descriptor
|
||||
// read (a type with no driver mapping — R06:M-5; *Array types are read since #754), and says so on its descriptor
|
||||
// line. That is a "not bindable in this version" refusal, not a failed lookup, so say which.
|
||||
if (descriptor is not null && descriptor.Contains(NotBindableMarker, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return $"'{browseName}' cannot be bound in this version: the Sparkplug browser reports it as "
|
||||
+ $"\"{descriptor}\". Array metrics and metrics of an unsupported type are not read yet; "
|
||||
+ $"\"{descriptor}\". Metrics of an unsupported type are not read yet; "
|
||||
+ "remove it from the selection.";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user