MQTT Sparkplug Protocol
The MQTT Datasource can be used to receive and send messages in line with the Sparkplug protocol, using the Lua Callback options.
This section addresses the following topics:
Message Encoding and Decoding
The embedded syslib.sparkplugb Lua library contains functions to decode and encode sparkplug message payloads (encoded using the Google Protobuf data format) for subscription and publishing respectively to an MQTT Sparkplug broker. For information regarding the compression/decompression of the payload, see the Lua Scripting page on Compressing Data.
In order to receive messages in the Sparkplug binary format, they can be decoded to Lua table format using the decode_payload function:
local sp = require("syslib.sparkplugb")
local payload, ok = sp:decode_payload(payload_binary) --payload_binary variable contains payload data in the binary format
-- if the call succeeds (ok == true) then payload is the Lua table containing sparkplug data. Otherwise, when ok==false payload (first result) will contain error message.
You can also call using a path to a file that contains the binary payload:
local sp = require("syslib.sparkplugb")
local payload, ok = sp:decode_payload(path_to_file, true) --in this case path_to_file variable contains the path to file on the server containing payload binary data.
-- if the call succeeds (ok == true) then payload is the Lua table containing sparkplug data. Otherwise, when ok==false payload (first result) will contain error message.
Here is an example that can be added to the On Message Receive property of an MQTT Subscriber I/O Item:
local SPB = require("syslib.sparkplugb")
local JSON = require "rapidjson"
local VAR = require "esi-variables"
-- Example function to process Sparkplug messages
return function(guid, mid, topic, payload, qos, retain)
local payload, ok = SPB:decode_payload(payload, false)
local result = nil
if ok then
if payload.metrics then
-- parse timestamp from sparkplug message
local posixTs = tonumber(payload.timestamp)
assert(posixTs, "Invalid timestamp")
local timestamp = syslib.gettime(posixTs)
-- foreach metric, get values and relative path
for _, metricObj in pairs(payload.metrics) do
local relPath = metricObj.name
local value = nil
for k,v in pairs(metricObj) do
if k:find("value") then
value = v
end
end
VAR:SET(topic .. "/" .. relPath,value, 0, timestamp)
end
end
result = JSON.encode({ok=true, result=payload})
else
result = JSON.encode({ok=false,err=payload})
end
-- return full payload as return for the IO-Item
local targetObjectPath = nil -- the subscriber IO-Item itself will be used
return targetObjectPath, result, 0, syslib.currenttime()
end
Payload Compression
The Sparkplug B payload can be compressed (and decompressed) using the deflate (ZLIB RFC 1950) or the gzip algorithm. For more information see Compressing Data.
Sparkplug Message Validation
The following functions can be used to validate that MQTT messages received from a broker are compliant with Sparkplug:
-
validate_payload(payload, message_type) - checks if an MQTT payload is valid for a specific message type
-
parse_nd_topic(mqtt_topic) - checks if an MQTT topic is valid for a specific message type other than STATE
-
parse_state_topic(mqtt_topic) - checks if an MQTT topic is a valid STATE type message
Functions
validate_payload(payload, message_type)
Checks if an MQTT paylod is valid for a specific message type.
Parameters
| Name | Type | Description |
|---|---|---|
payload |
string |
Sparkplug B payload; for message_type STATE the payload needs to be JSON format. For all other message types the payload needs to be in binary format |
message_type |
string |
Options are:
|
parse_nd_topic(mqtt_topic)
Description
Checks if MQTT topics are valid for the message types NBIRTH, NDEATH , DBIRTH, DDEATH, NDATA, DDATA, NCMD, and DCMD. If the topic is valid, the exact message_type can be identified in the returned result table. For the validation against STATE messages, see the parse_state_topic(mqtt_topic) function below.
Parameters
| Name | Type | Description |
|---|---|---|
mqtt_topic |
string |
an MQTT sparkplug B topic of the form "namespace/group_id/message_type/edge_node_id/[device_id] for Edge Node messages |
Returns
| Name | Type | Description |
|---|---|---|
ok |
boolean |
true if the parsing of the mqtt_topic succeeded; otherwise false |
result |
string/table |
If ok is false, this contains the error message;
{
"is_sparkplug_namespace":true,
"has_all_sparkplug_elements":true,
"message_type":"NBIRTH",
"group_id":"Group1",
"is_known_message_type":true,
"namespace":"spBv1.0",
"edge_node_id":"EdgeNode1"
}
{
"is_sparkplug_namespace":true,
"has_all_sparkplug_elements":true,
"message_type":"NBIRTH",
"group_id":"Group1",
"is_known_message_type":true,
"namespace":"spBv1.0",
"edge_node_id":"EdgeNode1",
"device_id":"device1"
}
|
parse_state_topic(mqtt_topic)
Description
Checks if an MQTT topic is a valid STATE type message. For the validation against all other message types, see the parse_nt_topic(mqtt_topic) function above.
Parameters
| Name | Type | Description |
|---|---|---|
mqtt_topic |
string |
an MQTT Sparkplug B STATE topic of the form "spBv1.0/STATE/host_application_id" |
Returns
| Name | Type | Description |
|---|---|---|
ok |
boolean |
true if the parsing of the mqtt_topic succeeded; otherwise false |
result |
string |
If ok is false, this contains the error message; {
"namespace":"spBv1.0",
"is_sparkplug_namespace":true,
"has_all_sparkplug_elements":true,
"is_state_topic":true,
"sparkplug_host_id":"host_application_id"
}
|
Example for Sparkplug Message Validation
local sp = require("syslib.sparkplugb")
local j = require("rapidjson")
return function(guid, mid, topic, payload, qos, retain)
local result, ok = sp:parse_nd_topic(topic)
if ok then
if result.is_sparkplug_namespace and result.edge_node_id ~= nil and result.is_known_message_type then
-- this is Edge Node message and payload should have binary protobuf format
local _, vok = sp:validate_payload(payload, result.message_type)
if vok then
local result1, ok1 = sp:decode_payload(payload)
if ok1 then
-- do something with result1
end
end
end
elseif result.is_sparkplug_namespace then
local result2, ok2 = sp:parse_state_topic(topic)
if ok2 and result2.is_sparkplug_namespace and result2.sparkplug_host_id ~= nil and result2.is_state_topic and result2.has_all_sparkplug_elements then
-- this is STATE message and payload should have JSON format
local _, vok = sp:validate_payload(payload, result.message_type)
if vok then
-- and first decode JSON to Lua table
local result1= j.decode(payload)
if result1 then
-- do something with result1
end
end
end
end
end
end