Sparkplug B Bridge Plugin
The Sparkplug B Bridge publishes Atlas values to an MQTT broker as a proper Sparkplug session, so they arrive in a Sparkplug UNS with birth certificates, aliases and death handling.
Overview#
The Sparkplug B Bridge subscribes to Atlas value-hub slices and publishes them to an MQTT broker as a Sparkplug B session: birth certificates when it connects, aliased data messages on change, and a death certificate if it drops. Use it when the consumer on the other side speaks Sparkplug - Ignition MQTT Engine, Cirrus Link tooling, or the Sparkplug support in HiveMQ and EMQX - rather than plain MQTT.
This is not the MQTT plugin with a different payload. The MQTT Bridge publishes free-form JSON on topics you choose, one message per row, and holds no state. Sparkplug is a stateful protocol: the topic namespace is fixed by the specification, payloads are protobuf, metrics are declared once and then referenced by number, and the broker holds a death certificate on your behalf. Pick this plugin when you need that contract, and the MQTT plugin when you do not.
Templates written before Timebase 2.0 must be updated. The tokens were previously written in curly braces with flat names -
{instanceUri},{branch}and so on. Those are no longer recognised, and a destination still carrying one is reported as a configuration error at load and refuses to resolve, rather than being passed through as text. Rewrite them as[instance.uri],[property.uri],[type.uri],[subscription.branch],[subscription.domain]and[subscription.name].
Configuration#
A Sparkplug egress instance is one entry in the host's Bridges dictionary.


{
"Name": "line1-uns",
"Type": "SparkplugB",
"Enabled": true,
"Host": "localhost",
"Port": 1883,
"ClientId": "timebase-bridge",
"Username": "",
"Password": "",
"UseTls": false,
"KeepAlive": "00:00:15",
"GroupId": "Timebase",
"EdgeNodeId": "bridge",
"RetainBirths": false,
"Qos": "AtLeastOnce",
"Subscriptions": {
"fillers-live": {
"Branch": "main",
"Domain": "default",
"Mode": "Type",
"TypeUri": "urn:demo:Filler",
"EmitFullRow": true,
"DeviceId": "[instance.uri]"
}
}
}
Connection and identity options#
| Field | Required | Default | Notes |
|---|---|---|---|
Type |
Yes | - | Must be SparkplugB |
Host |
Yes | - | Broker hostname or IP |
Port |
No | 1883 |
1883 plain, 8883 TLS |
ClientId, Username, Password |
No | - | Leave blank for an anonymous broker |
UseTls |
No | false |
Connect over TLS |
KeepAlive |
No | 00:00:15 |
Also bounds how quickly the broker publishes this node's death certificate after an ungraceful drop |
GroupId |
Yes | - | Sparkplug Group ID, the top of the topic namespace. Fixed for the instance's lifetime |
EdgeNodeId |
Yes | - | Sparkplug Edge Node ID. Fixed for the instance's lifetime |
RetainBirths |
No | false |
Retain the birth messages so a late consumer sees the current model. Data messages are never retained |
Qos |
No | AtLeastOnce |
QoS for published messages. The death certificate is always QoS 1 |
Subscriptions |
Yes | - | At least one named subscription |
Subscription options#
| Field | Required | Default | Notes |
|---|---|---|---|
Branch, Domain |
Yes | - | The Atlas world to subscribe to |
Mode |
Yes | - | Instance, InstanceProperty, Type, TypeProperty or TypeDescription |
TypeUri, InstanceUri, PropertyUri |
Mode-dependent | - | As required by the mode. TypeDescription needs TypeUri only. See Subscription Modes |
EmitFullRow |
No | true |
Leave this on. A birth certificate has to declare the complete metric set, so a delta-only stream would birth a partial device |
EnumValues |
No | Key |
What an enumeration-typed property's value is written as: Key (the key Atlas stores, the default), Name (the member's name) or Label (its label, falling back to the name). In Name or Label mode the column arrives as a string. See Enumeration values. In Name or Label mode the metric is a String metric. |
DeviceId |
No | blank | Sparkplug Device ID. Blank means [instance.@uri] for a Values subscription and [type.@name] for a Type description subscription. Tokens: [instance.uri], [property.uri], [type.uri], [subscription.branch], [subscription.domain], [subscription.name], plus [instance.label], [type.label], [property.label], [instance.<property>] and [instance.<compound>.<leaf>], which are read from the Atlas model. See Tokens from the Atlas model |
with Mode set to Type or TypeProperty, keep an instance token ([instance.uri], [instance.label] or an instance property) in DeviceId. A fixed value collapses every instance onto a single Sparkplug Device, and they overwrite each other's metrics.
every substituted value in the Device ID is made legal for the Sparkplug topic: /, +, #, % and control characters are percent-encoded, so the label Pump 1/A+B# becomes Pump 1%2FA%2BB%23. The text you typed in the template is never changed. See Values are made legal for the sink for the full table. A Device ID built from a value that contains / changes on upgrade, because that value is now encoded.
a Device ID is an identity on the broker, so prefer model values that do not change. A property value that changes at runtime moves the device to a different Device ID from the next row on. When a token in DeviceId has no value yet, the row is held instead of dropped and published in its original order once the value is known. Past Queue:HeldRowsPerInstance (default 1000) the oldest held row is dropped and one warning per hold is logged. See Rows wait for their destination.
How Atlas maps onto Sparkplug#
| Timebase | Sparkplug |
|---|---|
| One plugin instance | One Edge Node (GroupId + EdgeNodeId) |
| One Atlas instance it publishes | One Device under that node |
| One property or column | One metric |
A compound property becomes a metric folder, so flowRate.Data.Value is published as flowRate/Data/Value.
To run several Edge Nodes, configure several Bridges entries. One instance is deliberately one node.
Topic namespace#
Fixed by the specification, not something you configure:
spBv1.0/{GroupId}/NBIRTH/{EdgeNodeId} published
spBv1.0/{GroupId}/NDEATH/{EdgeNodeId} published, as the MQTT Will
spBv1.0/{GroupId}/DBIRTH/{EdgeNodeId}/{DeviceId} published
spBv1.0/{GroupId}/DDATA/{EdgeNodeId}/{DeviceId} published
spBv1.0/{GroupId}/NCMD/{EdgeNodeId} subscribed
Session behaviour#
- On connect, the plugin opens a new session, registers the death certificate as its MQTT Will, and publishes a node birth carrying the session number and a rebirth control metric.
- The first row for a device publishes a device birth declaring every metric with its name, a numeric alias, its datatype and its value.
- Later rows publish data by alias only, reporting by exception. An unchanged value is omitted; a genuine null is sent as null rather than skipped.
- Sequence numbers roll 0 to 255 across every message from the node, so a consumer can detect a gap. A separate session number pairs each birth with its matching death.
- An ungraceful drop leaves the broker to publish the death certificate. A graceful stop suppresses it, which is the standard Sparkplug behaviour and means a planned shutdown does not look like a failure.
- Re-subscribing re-arms every device's birth from a fresh value-hub snapshot without starting a new session.
Rebirth on command#
The node subscribes to its command topic. When a primary application sets Node Control/Rebirth, the plugin re-issues its node birth and then a device birth for every device.
Every metric that has been born is cached with its name, alias, datatype and last value, so a rebirth re-declares the full model immediately rather than waiting for Atlas to push values again. The rebirth is handled on the same gate as normal publishing, so a command arriving mid-stream interleaves without breaking the sequence numbering.
Only Node Control/Rebirth is honoured. Other node commands are ignored, and device-level commands are out of scope: this is a read-only egress and never writes back into Atlas.
Type mapping#
| Atlas column | Sparkplug datatype |
|---|---|
| Double | Double |
| Float | Float |
| Long | Int64 |
| Int | Int32 |
| Bool | Boolean |
| String | String |
| DateTime | DateTime, as Unix milliseconds |
Non-finite doubles and floats - NaN, Infinity - are published as null, because Sparkplug has no representation for them.
Type description subscriptions#
A subscription with Mode set to TypeDescription publishes a type's compiled description instead of values. It needs a TypeUri, and nothing else from the URI row. For when Bridge sends a description, and the document it contains, see Type Description Subscriptions.
- One device row per description. The device carries a single metric, a String named
TypeDescription, whose value is the whole JSON document described in the overview. A consumer reads the string and parses it. - The device ID resolves without an instance. A blank
DeviceIdmeans[type.@name]for this kind (and[instance.@uri]for a Values subscription), so each described type becomes its own device.[type.uri],[type.name]and the[subscription.*]tokens work as usual. An[instance.*]token inDeviceIdis a validation error when the configuration loads, because a description has no instance. - The first description is the device's birth, and each later one is a data message. The first description is published as the device's
DBIRTHcarrying theTypeDescriptionmetric, and each later description is aDDATAcarrying the new document. - A description is published at the start and on a shape change. A deleted type publishes a final one with
"deleted": trueand an emptyschema. EmitFullRowandIncludeWatermarksare ignored, and backfill does not apply.
"type-descriptions": {
"Branch": "main",
"Domain": "default",
"Mode": "TypeDescription",
"TypeUri": "urn:demo:Filler"
}
Configuration warnings#
The plugin checks its configuration at load and on every reload, and logs a warning without refusing to start, for:
- a blank or wildcarded
GroupIdorEdgeNodeId - a
/,+or#in aDeviceIdoutside a token, which would break the topic - a
TypeorTypePropertysubscription whoseDeviceIdhas no[instance.*]token ([instance.label]or an instance property counts) - a mode missing the URI it requires
- an
[instance.*]token in theDeviceIdof aTypeDescriptionsubscription
These are warnings rather than errors so a single bad subscription does not take down the rest of the host. Check the log after a configuration change.
Verifying it works#
Point the instance at a broker and watch the Sparkplug namespace:
mosquitto_sub -h localhost -t 'spBv1.0/#' -v
You should see a node birth, then a device birth per Atlas instance, then data messages as values change. An Ignition MQTT Engine or any Tahu-based decoder will reconstruct the values with their types, timestamps and null semantics intact.
Limitations#
- One Edge Node per instance. Configure multiple instances to publish several nodes.
- Egress only. Nothing written on the Sparkplug side reaches Atlas.
- The Sparkplug hierarchy is explicit.
GroupId,EdgeNodeIdandDeviceIdare configuration. Deriving them from an ISA-95 equipment model in Atlas is not available.
Related articles#
- MQTT Bridge Plugin - the free-form JSON alternative, and the shared subscription modes
- Exporting Data with Bridge - host configuration, connecting to a secured Atlas, and backpressure