Sparkplug Data
Every message the node publishes or receives sits under the Sparkplug B namespace, built from your Group ID, the message type, and your Node ID:
spBv1.0/{group_id}/{message_type}/{node_id}[/{device_id}]For GROUP_ID=Plant1 and NODE_ID=Line1, a device named press_002 publishes on spBv1.0/Plant1/DDATA/Line1/press_002. The device segment is the DeviceHub device name, or the Digital Twin instance's Sparkplug device ID; node-level messages have no device segment.
Message Types
Type | Direction | Topic | QoS | Retained | Purpose |
|---|---|---|---|---|---|
NBIRTH | Node to broker | spBv1.0/{group}/NBIRTH/{node} | 0 | No | Opens a session and declares node metrics |
DBIRTH | Node to broker | …/DBIRTH/{node}/{device} | 0 | No | Declares one device's metrics, with names, aliases, and datatypes |
DDATA | Node to broker | …/DDATA/{node}/{device} | 0 | No | Device metric values, referenced by alias |
NDATA | Node to broker | …/NDATA/{node} | 0 | No | Node-level metric values |
DDEATH | Node to broker | …/DDEATH/{node}/{device} | 0 | No | One device is no longer publishing |
NDEATH | Node or broker | …/NDEATH/{node} | 1 | No | The session ended |
NCMD | Host to node | …/NCMD/{node} | 1 subscription | — | Command for the node |
DCMD | Host to node | …/DCMD/{node}/{device} | 1 subscription | — | Command for one device |
STATE | Host to broker | spBv1.0/STATE/{host_id} | 1 subscription | Set by the host | Host application liveness |
NDEATH is registered as the MQTT Will Message when the node connects, at QoS 1 and not retained, so the broker publishes it if the node disappears without warning — that is what makes an unplugged device visible to host applications. On a clean shutdown the node publishes NDEATH itself, so the message arrives once either way.
Birth Order
Host applications reject data for a node they have not seen born, so ordering is strict:
flowchart LR
NB[NBIRTH] --> DB[DBIRTH per device] --> DD[DDATA]The node holds its publish lock until the broker acknowledges each birth message, so no data message can overtake the birth set. Nothing is published at all while the birth set is being rebuilt.
Sequence Numbers
Two counters appear in payloads, and host applications check both.
seq orders messages within one session. NBIRTH carries 0, each following data or birth message increments by one, and the value wraps to 0 after 255. NDEATH carries no seq at all. The node consumes a sequence number only for a message it actually sends — a tag update that produces no publishable metric does not advance the counter, which is what keeps host applications from reporting a gap.
bdSeq identifies the session itself. The NBIRTH that opens a session and the NDEATH that closes it carry the same value, and the next session uses the next value, wrapping after 255. It is stored at <CONFIG_PATH>/seqNum/<group>-<node>_bdSeq.txt, which must survive restarts — see Deployment. A rebirth requested over NCMD reuses the current bdSeq, because the MQTT session has not changed.
Aliases
Birth messages declare every metric with a name, a numeric alias, and a datatype. Data messages then carry only the alias and the value, omitting names and datatypes entirely; a null value is sent as the alias with the null flag set.
Aliases come from stable identities — the device and tag IDs for DeviceHub, the device and metric key for Digital Twins — so a metric keeps its alias across a rebirth, and a host application can keep resolving in-flight data after a topology change. They are assigned per process, so a restarted node reassigns them and host applications must re-read them from each new NBIRTH rather than caching them across sessions.
DeviceHub Devices and Tags
Each DeviceHub device becomes one Sparkplug device, and each of its tags becomes one metric on that device. The node reads device and tag metadata from the Litmus Edge API to build birth certificates, then publishes values as they arrive.
The Sparkplug device ID is the DeviceHub device name, trimmed of surrounding whitespace; a device with no name falls back to its Litmus Edge device ID. That name is the last segment of the device's topics, so renaming a device in DeviceHub changes its topic and host applications see a new device.
Metric group in DBIRTH | Contents |
|---|---|
Tags/<tag name> | One metric per tag, with the tag's datatype, an alias, and a typed initial value |
Device Properties/… | Device metadata: ID, name, description, driver ID, name, description, and version, device type ID and name, creation timestamp, and tag count |
Device Control/Rebirth | Boolean command metric a host application can write to request this device's birth certificate again |
Tag metrics keep the tag name as configured in DeviceHub, so a tag named Bearing Temp publishes as Tags/Bearing Temp. Tag metadata entries defined in DeviceHub travel with the metric as properties, named metadata_<key> with non-alphanumeric characters in the key replaced by _.
Datatypes
Litmus Edge tag types map to Sparkplug datatypes as follows. Matching is case-insensitive.
Litmus Edge value type | Sparkplug datatype |
|---|---|
bit, bit(int), bin, bool, int(bit) | Boolean |
uint8, byte, octal, left_byte, right_byte | UInt8 |
uint16, word | UInt16 |
uint32, dword, usint, uint | UInt32 |
uint64, lword, ulint, udint, ulong | UInt64 |
int8, sint, int16, short, float, real, float32, flt32, fl | Float |
int, int32, int64, lint, long, dint, double, float64, lreal, number, decimal | Double |
string, char, date, time, json, hex, raw, blob, counter, timer, and other text-like types | String |
A tag whose Count property is greater than 1 publishes as the matching array type instead: BooleanArray, UInt32Array, UInt64Array, FloatArray, DoubleArray, or StringArray.
A tag with a value type the node does not recognize is left out of the birth certificate, so it never appears in the host application. A tag whose name contains no alphanumeric characters, or is not valid UTF-8, is also skipped, with an error logged naming the tag.
Values and Lifecycle
Tag updates arrive from the Litmus Edge NATS broker and publish as DDATA on the device's topic, carrying the alias declared at birth and the value. Each update publishes as it arrives; the node adds no batching delay of its own. The metric timestamp is the event time supplied by Litmus Edge, so history in your host application reflects when the value was read rather than when it was published.
The node also subscribes to Litmus Edge lifecycle events:
Event | Reaction |
|---|---|
device.enabled, device.connected | Publish DBIRTH for that device |
device.removed, device.disabled, device.disconnected, device.start.failed | Publish DDEATH for that device |
device.created, device.updated, tags.created, tags.updated, tags.removed | Refresh metadata and republish the birth set if the advertised metric set changed |
Paired events are de-duplicated, so a device that reports both disabled and disconnected produces one DDEATH. Litmus Edge sometimes emits an event before the API reflects the change, so the node re-checks shortly afterwards, comparing the new topology against the old one and publishing nothing when nothing changed.
Host applications do not reliably retire a metric that simply disappears from a later birth certificate. So when a tag set shrinks, the node publishes DDEATH for the affected device first, then a fresh NBIRTH and DBIRTH with the current tags, and no data for that device in between. The result is a brief gap rather than a stale tag that keeps reporting good quality.
Digital Twins
Each enabled Digital Twin instance becomes one Sparkplug device, and each of its dynamic attributes becomes one metric. Unlike a DeviceHub tag, a twin attribute has no inherent datatype, so the node decides every metric's datatype from the twin's model before any value flows — and never changes it because of a runtime sample.
flowchart LR
MODEL[Model, hierarchy, schema, source tags] --> DBIRTH[DBIRTH contract]
RUNTIME[Runtime payloads] --> DDATA[DDATA values only]
DBIRTH --> HOST[Host application]
DDATA --> HOSTThe birth certificate declares the metric names, aliases, and datatypes; runtime payloads then supply values for metrics that already exist. A field that appears in a payload but not in the contract is ignored, which keeps a host application's tag list stable.
An instance is published only when it is enabled and has a topic. One whose contract compiles to no metrics at all is skipped, and counted by le_sparkplug_digital_twin_quarantine_total.
Metric group in DBIRTH | Contents |
|---|---|
Attributes/… | One metric per dynamic attribute, at its path in the model hierarchy |
Device Properties/Digital Twin/… | Model and instance diagnostics |
Device Control/Rebirth | Boolean command metric to request this device's birth certificate again |
A twin's Sparkplug device ID is namespaced separately from DeviceHub device names, so a twin and a device can share a display name without colliding.
How Each Metric Gets Its Datatype
Four sources are tried in order. The first that yields a scalar type wins, and the winner is recorded on the metric as the typeSource property.
Order | Source | typeSource | Notes |
|---|---|---|---|
1 | The DeviceHub tag the attribute subscribes to | devicehub | The intended path. The tag's value type decides the Sparkplug datatype |
2 | The attribute's own dataType field | declared | Author intent beats a sample-derived schema. array becomes StringArray |
3 | The instance's exported JSON Schema | schema | Sample-derived. Numbers and integers both become Double |
4 | Fallback | untyped | The metric is registered as String |
untyped is the honest "not stated anywhere" result rather than a guess: values serialize as text, nothing is dropped, and no datatype is inferred from a name or a unit.
Metric Properties
Birth metrics carry flat, scalar properties so an operator can tell where a metric came from without server-side lookups. Reading declaredDataType=json together with typeSource=untyped, for instance, names exactly which field to fix in the model.
Property | Meaning |
|---|---|
attributeId, attributeName | The model attribute behind the metric |
engUnit | The attribute's unit, when the model declares one |
typeSource | Which of the four sources above set the datatype |
declaredDataType | The raw dataType token the author wrote |
topicExpression | The attribute's subscription expression |
runtimeValuePath | The field inside the runtime payload holding the value, when it is not value |
contentType | json for metrics whose runtime payload is an object |
transformationName, transformationType | The configured transformation, when there is one |
sourceDeviceId, sourceDeviceName, sourceTagName, sourceValueType, and related | The DeviceHub tag behind a devicehub-typed metric |
Runtime Values
Situation | Result |
|---|---|
Field present and convertible | Published as the contract's datatype |
Field absent | Skipped, other metrics still publish |
Field explicitly null | Published as null for that metric |
Value cannot convert to the birth datatype | That one metric is dropped, the rest of the batch publishes |
No metric in the batch converts | Nothing is published, and no sequence number is consumed |
Compatible shapes are coerced rather than rejected: "true", 0, and 1 all satisfy a Boolean, and "12.5" satisfies a Double. A value that cannot convert is dropped rather than sent, because a mismatched datatype makes a host application reject the whole payload. Drops are counted by le_sparkplug_digital_twin_attribute_value_mismatch_total, labelled with what was expected and what arrived.
Metric timestamps use the value's own timestamp when the payload carries one, then the payload's top-level timestamp, then publish time.
Changes to a Model
The node polls Litmus Edge for twin changes every 10 seconds, stretching to 60 seconds while nothing changes and returning to 10 seconds as soon as something does. Instance lifecycle events trigger a refresh immediately.
When a contract changes — an attribute added, removed, retyped, or moved — the node publishes DDEATH for that device and then a fresh birth certificate, so the host application rebuilds its tag list instead of holding a stale one. A change in values alone publishes data only.
Commands: NCMD and DCMD
The node subscribes for commands on every connect, at QoS 1: spBv1.0/{group}/NCMD/{node} for the node, and spBv1.0/{group}/DCMD/{node}/# for any of its devices. It accepts three command metrics, each taking effect when its boolean value is true.
Command metric | Sent on | Effect |
|---|---|---|
Node Control/Rebirth | NCMD | Refreshes metadata and republishes the full birth set: NBIRTH followed by every DBIRTH |
Node Control/Next Server | NCMD | Ends the MQTT session and connects to the next broker in the list |
Device Control/Rebirth | DCMD | Republishes that one device's DBIRTH |
Every NBIRTH declares the two node commands, and every DBIRTH declares the device command, so a host application discovers them from the birth set. A command metric may be addressed by name or by the alias declared at birth.
Rebirth is how a host application recovers when it has lost track of the node's metrics, after its own restart or an alias it cannot resolve. The node refreshes DeviceHub and Digital Twin metadata first, then publishes the birth set as one unit, with data publishing closed for that window so nothing arrives referencing aliases the host has not read yet. The session's bdSeq does not change, because the MQTT session has not changed. Overlapping requests are suppressed rather than queued: while a birth set is publishing, further requests are counted by le_sparkplug_rebirth_suppressed_total{reason="active"} and dropped, so a host application requesting rebirths in a tight loop cannot stall the node.
Next Server moves the node to the next broker in MQTT_SERVERS: it publishes NDEATH, disconnects, and connects to the next entry, starting a new session with the next bdSeq. With a single broker configured, it reconnects to that same broker.
Any other command metric is ignored and counted by le_sparkplug_ncmd_total{type="unknown"}, with the metric name and value logged at debug level. A malformed metric — no name and no alias — is logged as an error and skipped. Because commands are counted as they arrive, those counters show whether the node received a command a host application believes it sent.
Signed 8-bit and 16-bit integers publish as `Float`, and `int32`, `int64`, and `dint` publish as `Double`. If your host application expects integer datatypes for those tags, expect floating-point metrics instead.A wave of `untyped` metrics on a new instance means its attributes carry no type signal. Fix it in the model editor: bind the attribute to a DeviceHub tag, or set its `dataType` to a real scalar. `le_sparkplug_digital_twin_attribute_fallback_total` counts these per attribute.Writing values back to devices is not supported. `DCMD` handles `Device Control/Rebirth` only — there is no path from a Sparkplug command to a DeviceHub tag write.