MQTT Sparkplug Client

SparkplugB is an optional Lua plugin for publishing node and device metrics over MQTT and receiving commands. It acts as a Sparkplug Edge of Network (EoN) node. This reference explains how to create a client, publish values and handle commands in Barracuda App Server (BAS), Mako Server and Xedge applications.

The plugin implements a subset of Sparkplug 3.0. Read Current limitations before integrating it. The implementation and protobuf schema are maintained in BAS-Resources.

For runnable node and console-monitor applications, see the Sparkplug examples. Their README covers configuration, starting the applications and packaging for Xedge.

Prerequisites

The runtime must include the native pb module and the Lua modules protoc, SparkplugB, EventEmitter and MQTT clients. The resource file .lua/sparkplug_b.proto must be available through ba.openio"vm". Availability depends on how Mako or Xedge was built and packaged. Loading SparkplugB throws if its protobuf dependencies or schema are unavailable. An MQTT broker must be accessible from the runtime.

On this page

How it works

Node topics have the form spBv1.0/groupId/messageType/nodeName; device topics append /deviceId.

Message Purpose
NBIRTH / DBIRTH Publish the node's or device's metric definitions and current values.
NDATA / DDATA Publish changed node or device values.
NCMD / DCMD Receive node or device commands.
NDEATH MQTT will message registered by the plugin for the node.
DDEATH Application notification that a device is offline.

The plugin starts with MQTT 5 and can fall back to MQTT 3.1.1. It reconnects automatically while running. The application supplies birth messages in its birth event handler; the plugin does not retain and retransmit the application's birth payload by itself.

Complete example

This example uses a local broker without authentication. Set connection options for your broker. Install the code as an application's .preload so onunload() owns cleanup.

local SP = require"SparkplugB"
local deviceId = "device-1"
local nodeMetrics = {SP.metric("temperature", "double", 21.5)}
local deviceMetrics = {SP.metric("enabled", "boolean", false)}
local client = SP.create("localhost", "example-group", "example-node")

client:on("birth", function()
   -- Supply fresh definitions and values after each successful subscription setup.
   client:publishNodeBirth{metrics=nodeMetrics}
   client:publishDeviceBirth(deviceId, {metrics=deviceMetrics})
end)

client:on("connect", function()
   trace("Sparkplug subscriptions ready")
end)

client:on("error", function(message, status)
   trace("Sparkplug error", message, status)
end)

client:on("dcmd", function(id, payload)
   -- Apply only this example's known Boolean device command.
   if id ~= deviceId then return end
   for _, metric in ipairs(payload.metrics) do
      if metric.name == "enabled" and metric.type == "boolean"
         and type(metric.value) == "boolean" then
         deviceMetrics[1].value = metric.value
         client:publishDeviceData(id, {metrics=deviceMetrics})
      end
   end
end)

function onunload()
   -- Stop owned network activity when the application is unloaded.
   client:stop()
end

Function reference

Load the module with local SP = require"SparkplugB". In the signatures below, square brackets mark optional arguments. table objects belong to this Lua implementation; they are not native userdata handles.

Incorrect API usage can throw. Network and broker outcomes are reported through events because connection and publication work runs asynchronously. Event callbacks' return values are ignored. The underlying EventEmitter catches callback exceptions and traces them; callbacks should still handle their own application failures.

SP.create(broker, groupId, nodeName [, opt])

Parameters

Return values

Throws

Throws for incorrect argument types or invalid connection options. Connection and broker failures are delivered through events, not a synchronous nil, error result.

See the MQTT 5 and MQTT 3.1.1 references for transport details. The plugin manages its own subscriptions and will; it does not expose all underlying MQTT methods.

client:stop()

Parameters

None.

Return values

Throws

Does not deliberately throw for an ordinary stop. Calling the method on an invalid object is incorrect API usage. Pending subscription acknowledgements cannot emit birth or connect after stopping.

Stopping is terminal for this client. Create a new client to start again. All five publish methods throw "client stopped" after stopping.

client:close()

Parameters

None.

Return values

Throws

The same behavior as stop(): no deliberate operational exception; an invalid receiver is incorrect usage.

client:on(event, callback)

Parameters

Return values

No values. Registering the same function for the same event does not add another copy. An unrecognized event name does not create a new plugin event.

Throws

Incorrect receiver, event or callback usage can fail during registration or invocation. Callback exceptions are caught and traced by EventEmitter. A callback result does not cancel an event or control reconnect behavior.

SP.metric(name, type, value [, alias [, ts]])

Parameters

Return values

Throws

Throws for an unknown type name or an invalid type argument. Other invalid fields or values can fail later during encoding.

SP.encode(payload)

Parameters

Return values

Throws

Throws for invalid payload structure, missing required Lua fields, unknown type names or values the protobuf encoder cannot encode. The normalized table uses wire field names; use the original payload or SP.decode(bytes) when calling SP.encode() again.

SP.decode(bytes)

Parameters

Return values

Signed Int8/Int16/Int32 values are converted according to their declared widths. Dataset cells are plain values. Template references use templateRef. Null values and empty collections are retained. See the numeric and metadata limitations below.

Throws

Throws if bytes is not a string. Does not deliberately throw for invalid received payload data.

Lua payload format

The conversion helpers use ordinary Lua tables instead of protobuf oneof field names such as int_value and boolean_value.

Metric values

Datatype Lua value
int8, int16, int/int32, int64 Signed integer value within the selected width and the runtime's representable range.
uint8, uint16, uint32, uint64 Nonnegative integer value within the selected width and the runtime's representable range.
float, double Number; precision depends on Lua and protobuf storage.
boolean Boolean, including false.
string, text, uuid String.
datetime Numeric Unix timestamp in milliseconds.
bytes, file Binary string.
dataset Dataset table.
template Template table.

The plugin performs conversion, not exhaustive validation of all Sparkplug rules. Supply valid datatype/value combinations and protocol fields. propertyset and propertysetlist describe property values, not ordinary metric values. Array datatypes are not implemented.

Datasets

A dataset value has table columns (array of column-name strings), table types (matching array of datatype-name strings), and table rows (array of rows). Each row is an array of plain scalar values, not {value=...} wrappers. Column/type counts and row widths must match. Use scalar datatypes supported by the schema. Supply integer num_of_columns when the wire payload needs that field; the encoder does not calculate it automatically.

-- Plain cell values are used both before encoding and after decoding.
local metric = SP.metric("samples", "dataset", {
   num_of_columns=2,
   columns={"temperature", "enabled"},
   types={"double", "boolean"},
   rows={{21.5, false}, {22.0, true}}
})
local normalized, bytes = SP.encode{metrics={metric}}
local payload, err = SP.decode(bytes)
assert(payload, err)
assert(payload.metrics[1].value.rows[1][2] == false)

Empty row lists and empty collections decode as Lua tables, so they can be re-encoded.

Templates

A template value contains:

Each parameter contains string name, string type, and a matching number, boolean or string value supported by the schema. false is a value; a missing value is an encoding error. Decoding restores templateRef and empty metric/parameter lists.

Properties

A property set is table properties with table keys (array of strings) and table values (matching array of property tables). Each property has string type and its matching number, boolean, string or table value. For a null property, set boolean is_null to true and omit value.

Use scalar types supported by the schema, or these nested forms:

Boolean false, explicitly null properties and empty nested collections survive encoding and decoding. The encoder requires the named array fields even when they are empty.

Publishing messages

All five methods copy the supplied payload recursively, assign the payload timestamp/sequence as described above, and queue a QoS 0 MQTT publication. They return no delivery acknowledgement. While the client is running but disconnected, messages can be queued for later sending. Publish normal application data after connect; publish definitions from the birth handler.

client:publishNodeBirth(payload)

Parameters

Return values

No values. Resets sequence to 0 and adds an Int64 bdSeq metric to the copied payload.

Throws

Throws after stopping, or for invalid payloads/values during encoding.

client:publishDeviceBirth(deviceId, payload)

Parameters

Return values

No values. Queues DBIRTH using the shared sequence counter.

Throws

Throws after stopping, or for invalid device/payload arguments or encoding values.

client:publishNodeData(payload)

Parameters

Return values

No values. Queues NDATA using the shared sequence counter.

Throws

Throws after stopping, or for invalid payloads/values during encoding.

client:publishDeviceData(deviceId, payload)

Parameters

Return values

No values. Queues DDATA using the shared sequence counter.

Throws

Throws after stopping, or for invalid device/payload arguments or encoding values.

client:publishDeviceDeath(deviceId, payload)

Parameters

Return values

No values. Queues DDEATH. The copied payload's metrics array is cleared; any metrics supplied by the caller are not sent.

Throws

Throws after stopping, or for invalid device/payload arguments or encoding values.

Node death (NDEATH)

There is no node-death publishing method. The plugin registers an NDEATH will containing bdSeq when connecting. The MQTT broker publishes that will according to the connection's will rules. Do not assume the close event means that the broker or another client has received it.

Receiving events

Use client:on(event, callback). The following table lists every event emitted by this implementation. There is no generic command event.

Event Callback arguments: name and type When it occurs
birth No arguments after subscription setup; table metric for a node-control rebirth request. After all three subscriptions succeed, before connect; also for a Node Control/Rebirth metric whose type is Boolean and value is true. The application should publish its birth messages.
connect None. After successful subscription setup and the birth event. A refused subscription suppresses both events, closes the socket and lets reconnection retry.
reboot table metric. For a Boolean Node Control/Reboot value of true. The plugin emits an event; it does not reboot the device itself.
ncmd table payload. A successfully decoded NCMD. Recognized node-control metrics are removed; remaining metrics form a dense array in incoming order. The array may be empty.
dcmd string deviceId, table payload. A successfully decoded DCMD for the device component in the topic.
state string id, string payload. A matching spBv1.0/STATE/id message. id is the third topic component. The payload is passed through as a string; JSON is not decoded here.
offline None. Loss of an established connection, or server shutdown reported by MQTT.
reconnect None. After an established connection is lost while the client is running, before retry. Initial connection retries need not emit it.
error string or integer message, integer or nil status. MQTT connection/read errors, broker or subscription refusals, unsupported/malformed payloads, or unrecognized incoming topics. The optional status carries a broker/subscription reason code when supplied.
close None. The first explicit stop()/close() call, including before a connection exists. Repeated calls do not emit it again.

False, null and non-Boolean node-control values do not emit birth or reboot. They are still removed from ncmd's ordinary metric list. Unsupported datatypes or payload conversion failures emit error instead of dispatching that command.

Current limitations

These limits mean that the plugin is not a complete Sparkplug 3.0 implementation. Check interoperability with the target broker and host application. Protobuf roundtrip tests and controlled MQTT tests do not establish full protocol conformance or hardware behavior.