Sending MQTT Sparkplug B control messages from Node-RED

Sparkplug B is an awesome way to go about using MQTT communication, especially in industrial automation applications – but it does come with some extra complexity besides just reading and writing message payloads.
You may have already seen my forum post on decoding Sparkplug B messages using Node-RED (if not, you can check that out here: Decode Sparkplug Encoded MQTT Messages with Node-RED).
In this post I’ll be going over how to create and send control payloads. I’ll go over how to build them, how to encode them, and how to use them to both toggle digital outputs and set analog point values.


The first steps are similar to the decoding post: you’ll need to download and install the Node-RED package node-red-contrib-protobuf in order to encode the outgoing control message, and you will need to download the official protobuf format definition file directly from the Eclipse Tahu GitHub here: sparkplug_b.proto.
Just be sure to download the file with the correct *.proto file extension and not as a *.txt file.

The next part of the setup is to point the encoder node to the protocol definition file. With groov EPIC I recommend uploading the proto file to the unsecured file area using the file system in groov Manage, but if you have shell access you can put it anywhere you want as long as you know the file path.
For the unsecured file area you should use the path /home/dev/unsecured/sparkplug_b.proto and since we are going to be encoding the message payload, the type should be “Payload”:

image
The “Name” field is not critical, but choosing an accurate name like “sparkplug_b” can help keep track of what the node does.

Once this node is correctly configured, the next two things are to figure out the payload format and the topic to publish on. You can find much, much more detail from the official Sparkplug B specification document, but I’ll cover the basics here.


The topic namespace is defined by the following structure:
namespace/group_id/message_type/edge_node_id/[device_id]
For our purposes the namespace and message type will be consistent, but the group, edge node, and device ID’s will all be determined by the device you’re using and how it’s configured:
spBv1.0/(group)/DCMD/(edge_node)/[device_id]
It’s very important that the message type is “DCMD” which represents “Device Command”, also know that “device_id” is an optional field, so that will depend on your settings.

In order to figure out the exact topic namespace, as well as some other details, I highly recommend using both Ignition (the free trial time is more than enough) as well as the decoding method for Node-RED through the link above and reading the Sparkplug payloads to listen in to some initial control messages. That way you can pick up the exact topic and message format by toggling or changing an output like I did here:

image
In order to trigger this DCMD message you just need to change the output through the Ignition Designer, which you can see here:

image

Of course, for this to be available you will need to make sure your point is set to have read/write access enabled through either groov Manage or PAC Control (you can find full setup details in this demo video).


Once the topic is determined, you need to figure out the message payload content. It will follow this general format, but the exact details can be found in the message payload output using the method described above. Here is a debug output of a decoded DCMD message for reference:
image

The “alias” field determines the value that you are writing to, which is set with the device birth or “DBIRTH” initial message when you initialize your connection or make another control message. Once you know the alias you should also find the datatype, but you can also find it in section 15.2 (page 51 at the time of writing this post) of the Sparkplug B spec document.
In this case float has the datatype numeration value of 9, and boolean digital toggles have the datatype value of 11. Once you set that and the “floatValue” or “booleanValue” respectively, you should be good to go as long as you replace “writeValue” with either a contant value or a variable that you define in the function node. I personally chose to define writeValue to be an incoming message payload for simplicity (flow import is included at the end of this forum post).

msg.payload = {
"metrics":[{
    "alias":22,
    "datatype":9,
    "isNull":false,
    "floatValue":writeValue
    }],
"seq":-1
};

Once the broker, topic, and payload are all correctly set up, you just need to wire the output into an MQTT out node that is included with Node-RED by default as a part of the core nodes.
Here are both digital and analog write examples:
image

You can import the flow here, just change the specific aliases, datatypes, and write values to your own needs. You can use any source to inject the message besides the “inject” node, just make sure the JSON is correctly formatted before it goes into the Sparkplug encoder node.

Here’s the import JSON for the flows:

[{"id":"e4cfdef8.62409","type":"encode","z":"6dc3f358.26a64c","name":"sparkplug_b","protofile":"6780b84f.200d88","protoType":"Payload","x":610,"y":2620,"wires":[["cbbc13e0.96ac9"]]},{"id":"cbbc13e0.96ac9","type":"mqtt out","z":"6dc3f358.26a64c","name":"","topic":"","qos":"","retain":"","broker":"ec400768.e29c68","x":770,"y":2620,"wires":[]},{"id":"859404f.4cdc6f8","type":"function","z":"6dc3f358.26a64c","name":"digital","func":"time = msg.timestamp;\nwriteValue = msg.payload;\n\nmsg.payload = {\n    \"metrics\":[{\n        \"alias\":18,\n        \"datatype\":11,\n        \"isNull\":false,\n        \"booleanValue\":writeValue\n    }],\n    \"seq\":-1\n};\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":430,"y":2620,"wires":[["7a032374.521f2c","e4cfdef8.62409"]]},{"id":"7a032374.521f2c","type":"debug","z":"6dc3f358.26a64c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":610,"y":2580,"wires":[]},{"id":"7ce4f61e.007328","type":"inject","z":"6dc3f358.26a64c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"},{"p":"timestamp","v":"","vt":"date"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"spBv1.0/DEVELOPER/DCMD/epic-dev/epic-dev","payload":"false","payloadType":"bool","x":270,"y":2640,"wires":[["859404f.4cdc6f8"]]},{"id":"b577b567.6b4f08","type":"inject","z":"6dc3f358.26a64c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"},{"p":"timestamp","v":"","vt":"date"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"spBv1.0/DEVELOPER/DCMD/epic-dev/epic-dev","payload":"true","payloadType":"bool","x":270,"y":2600,"wires":[["859404f.4cdc6f8"]]},{"id":"39da79e1.58e276","type":"function","z":"6dc3f358.26a64c","name":"analog","func":"writeValue = msg.payload;\n\nmsg.payload = {\n    \"metrics\":[{\n        \"alias\":22,\n        \"datatype\":9,\n        \"isNull\":false,\n        \"floatValue\":writeValue\n    }],\n    \"seq\":-1\n};\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":430,"y":2740,"wires":[["10b44f39.22d771","50182af3.9bf904"]]},{"id":"10b44f39.22d771","type":"debug","z":"6dc3f358.26a64c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":610,"y":2700,"wires":[]},{"id":"50182af3.9bf904","type":"encode","z":"6dc3f358.26a64c","name":"sparkplug_b","protofile":"6780b84f.200d88","protoType":"Payload","x":610,"y":2740,"wires":[["29a02be4.eee6b4"]]},{"id":"8bb42f96.a52","type":"inject","z":"6dc3f358.26a64c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"},{"p":"timestamp","v":"","vt":"date"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"spBv1.0/DEVELOPER/DCMD/epic-dev/epic-dev","payload":"0","payloadType":"num","x":270,"y":2780,"wires":[["39da79e1.58e276"]]},{"id":"29a02be4.eee6b4","type":"mqtt out","z":"6dc3f358.26a64c","name":"","topic":"","qos":"","retain":"","broker":"ec400768.e29c68","x":770,"y":2740,"wires":[]},{"id":"5d57e5ff.2a092c","type":"inject","z":"6dc3f358.26a64c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"},{"p":"timestamp","v":"","vt":"date"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"spBv1.0/DEVELOPER/DCMD/epic-dev/epic-dev","payload":"1","payloadType":"num","x":270,"y":2740,"wires":[["39da79e1.58e276"]]},{"id":"2a7b66bd.eeaa2a","type":"inject","z":"6dc3f358.26a64c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"},{"p":"timestamp","v":"","vt":"date"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"spBv1.0/DEVELOPER/DCMD/epic-dev/epic-dev","payload":"3.5","payloadType":"num","x":270,"y":2700,"wires":[["39da79e1.58e276"]]},{"id":"6780b84f.200d88","type":"protobuf-file","z":"","protopath":"/home/dev/unsecured/sparkplug_b.proto","watchFile":false},{"id":"ec400768.e29c68","type":"mqtt-broker","z":"","name":"epic-dev","broker":"epic-dev","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]

If you have any questions, or end up using this in any of your projects or applications please drop a message in the thread below.

And as always, happy coding!

3 Likes

This is such an incredibly important aspect to making all this work that I want to just expand on it for a moment.

The SPB spec states that the alias can freely change. So you can NOT write your code with a fixed alias number and always expect it to be tied to that I/O point over time.
The whole point of the alias is to shrink the message payload by not including the point (tag) name in each published message to the broker.
As Terry points out, you only see the point name and alias in one message, the DBIRTH message.

Again, as Terry points out, you only get the DBIRTH message when you make the first connection or when ever you reconnect with the broker after a disconnect. So your alias can change quite a bit if you are on a flaky network connection for example.

What does all this really mean then? It means that you the programmer need to keep very careful track of DBIRTH messages. You might build a table, or even have a database that links the tag name with the alias number and that way you can be sure you are controlling the correct I/O point when you publish on the topic. Keep in mind that if you have more than one device publishing SPB messages, you may well have the same alias numbers floating around in the system. Obviously they will be on different topics, but they could very well have the same alias number. All the more reason for you to keep very careful track of those DBIRTH messages.

Of course Ignition does a great job of doing all this for you.
Your tag really does become a single source of truth and as a human, you can just focus on your job and automate/collect data from the real world tag name and never know (or care) about what an alias is.

Long story short, before you go down the rabbit hole of building your own SPB end-to-end system, hopefully this post and Terry’s will give you reason to pause and deeply consider if its worth your time rolling your own custom system.

3 Likes