feat(mqtt): Sparkplug DataSet / Template / PropertySet metric support
#515
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
SparkplugCodechandles scalar metrics.DataSet,Template(UDT instances) andPropertySetmetrics areskipped with a warning — v1-unsupported by explicit decision, recorded in the plan's Deferred / out of
scope section and design §3.5. This issue tracks closing that.
The current behaviour is the right default and should survive any fix: an unsupported metric is skipped and
warned about, never coerced into a guessed type. A
Templateflattened into a String, or aDataSetwhosefirst column is silently taken as "the value", would bind a tag to data that is not what its name says — worse
than not binding it. Whatever shape support takes, do not lose the skip-and-warn fallback for the cases it
still does not cover.
Design questions to settle first, since these are structured values and OPC UA has more than one honest
mapping:
Template— map to an OPC UA Structure/UDT, or flatten members into sibling tags (Motor/Speed,Motor/Torque)? Flattening interacts with #509: member paths contain/.DataSet— a table has no single scalar value. Array-valued variable, or per-column tags?PropertySet— arguably not a value at all; likely belongs on the node as OPC UA properties/metadatarather than as tags.
Bytes/Filemetrics beyond a base64/raw-String fallback are the same class of gap and can ride along or splitout.
Acceptance. A simulator publishing a
Templateand aDataSetmetric produces addressable, correctly-typedOPC UA nodes, live-verified; every still-unsupported kind continues to skip with a warning.
Decision (owner, 2026-07-31): flatten to sibling tags. Not yet implemented — recording the settled
design so the mapping question is closed.
Template(UDT instance)Motor/Speed,Motor/TorqueDataSetPropertySetChosen over the structured-OPC-UA-types alternative because flattened scalars are consumable by any OPC
UA client (ScadaBridge included), and the result mirrors how the
/rawtree already works. Structureswould be truer to Sparkplug's model but much harder to consume, and the address space would stop
mirroring raw.
Two things that must survive the change:
guessed type — a
Templateflattened into a String would bind a tag to data that is not what its namesays.
/, which is exactly the interaction this issue flagged with #509. That is nowhandled: #509 shipped
RawBrowseCommitMapper.ToRawSegment, which coerces the tag Name to a legalRawPath segment while carrying the original slashed string through as
metricName, plus a collisionguard that refuses two members that would sanitise onto one Name. So the flattening work inherits a
solved problem rather than reopening it.
Live gate will need the project-owned C# edge-node simulator
(
tests/.../SparkplugSimulator) extended to publish aTemplateand aDataSetmetric — it currentlyemits scalars only.
Implemented and live-gated.
master857a10b4(feature commitc74ccc55).Flattened to sibling tags, as decided:
Template/-joined, recursivelyMotor→Motor/Speed,Motor/Stator/TempDataSet{column}/{rowIndex}Recipe→Recipe/Step/0,Recipe/Target/1PropertySetengUnit,Range/LowOne place the decision needed sharpening
"Per-column tags" cannot survive N > 1 rows — a column of N rows is not a scalar — so it is per
cell, and the row index is always present, including on row 0. Emitting a bare
{column}fora single-row table and
{column}/{row}otherwise would make a tag's own name depend on how many rowsthe plant happened to publish, so a table growing from one row to two would rename every tag already
authored against it.
Recipe/Target/0costs a/0on the common single-row "current values" shapeand buys a stable address.
The case a naive implementation gets wrong
A DATA metric carries the parent's alias and no name. So member names cannot be composed until
the alias table has resolved the parent — flattening at decode time alone is not enough. Expansion
happens in
SparkplugIngestor.PublishStructuredMembers, after resolution, at the single funnel boththe DATA and birth-value paths already pass through (so neither can end up supporting structured
metrics while the other silently does not). The alias table installs a binding per member, which is
also what lets a DDATA member that omits its datatype fall back to the birth's — guessing from the
CLR type of the wire value would read a signed
Int32of −42 out of its unsigned field as4294967254.
Skip-and-warn survives, and refusal is whole
A template definition (
is_definition— shape, no values), an unnamed member, an extensionvalue, a column/type count mismatch, a short row, or a payload past
MaxDepth(5) /MaxMembers(512) refuses the entire metric rather than publishing part of it. A silently half-flattened UDT
looks exactly like a complete one in the picker, and the operator would author tags against a subtree
that quietly stops partway down.
Two consequences worth recording
and no servable datatype, so a leaf for it would offer an address that can only ever commit a tag
which never reads.
leaves the parent metric's name identical, so a gate built on raw wire names would never move — and
for a UDT that is the change that matters most.
Member paths contain
/, which #509 had already solved, exactly as this issue anticipated: theslashed name travels intact as
metricNamewhile the derived tag Name is coerced to a legalRawPath segment, with the collision guard behind it.
Live gate — 3/3
The project-owned simulator was extended to publish a nested Template (
MotorwithSpeed,Runningand a nestedStator/Temp) and a 2×2DataSet, both moving on the data tick so a fedmember is distinguishable from one published once at birth and then quiet. Legs: template across
birth and alias-only DDATA; dataset cells typed per column; and a template definition still
skipped — that last one with a positive control that must arrive first, or "Speed never arrived"
would be satisfied by any failure to connect at all.
Falsified at three points — dropping the member bindings, not expanding the parent, and refusing to
flatten templates — each turning exactly the expected tests red.
Docs:
docs/drivers/Mqtt.md§"Structured metrics".