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

Hey @torchard Terry, I want to know about the Seq = -1. as per the documentation we have to increase every time the message published. so what to do with that? why it is set to -1?

also, can you make flow on multiple values in a single payload? as per docs, for multiple, it is set to a single payload. I’m able to do it but I want to know more from you about how to do it?

If you can share flow on it.

Hi @PranavT, welcome to the forums!

You’ve asked a really great question here, and one I had intentionally glossed over in the original post, which was focused more on the application than the deep theory behind it. Unfortunately I do not have a complete answer – but I do have some information that will hopefully help you out.


I’ll start by answering the harder question, “why set it to -1?”
Short answer: Because that’s what every application I’ve tested does, so that’s what I did.

Long answer: The use of the sequence field is described in the Sparkplug B specification pdf in lots of places; here’s part of the 15.1.1 section of the payload breakdown:

A sequence number must be included in the payload of every Sparkplug™ MQTT message. A NBIRTH message must always contain a sequence number of zero. All subsequent messages must contain a sequence number that is continually increasing by one in each message until a value of 255 is reached. At that point, the sequence number of the following message must be zero.

This is admittedly not very helpful, since how could I possibly know what the last sequence value was? There could be dozens or even hundreds or thousands of other clients publishing these DCMD messages!
But that’s all I could get out of the spec, so I tested with both MQTT.FX and the Node-RED decoding method from the original post to see what the device command sequence was.
It turns out that in MQTT.FX I actually see -1 as the sequence value when I trigger a value change (which is a DCMD message) from both groov EPIC changing the value OR an Ignition instance changing the value. In Node-RED I get the value 18,446,744,073,709,551,615 … which it turns out, is technically still -1 due to the roll-over on 64-bit integers. (I found a more detailed explanation about that here if you’re especially curious; That difference in the storage and reading of integers explains why one piece of software gives 18,446,744,073,709,551,615 and the other just gives -1.)

It seems that this -1 is how Sparkplug handles this issue of keeping track of the sequence value. When a subscribing client receives an incoming message with seq = -1, it just treats it as the next message in the sequence. This way any publishing clients just say what they want done, call it sequence -1, and then let the “subscribing” / receiving client handle the actual sequence count.

This screenshot is from MQTT.FX receiving a DCMD message from Ignition. You can see even Ignition does not give a direct sequence integer, it just sends seq = -1.

Ultimately this led to me testing it when sending my own custom messages, and when it worked I didn’t dig any deeper. If anyone has any insight as to why this is or how it works behind the scenes, I’d be very curious to hear about it!


Regarding your second question, pushing multiple values in a single update, it’s as simple as looking at other message values in Node-RED or MQTT.FX and copying what they do, as in the above message. I’ve not thoroughly tested this, but I believe it should be as simple as adding more entries into the metrics array like this:

msg.payload = {
    "timestamp" : timestamp,
    "metrics":
    [{
        "name":"",
        "alias":4,
        "timestamp":timestamp,
        "dataType":"Boolean",
        "value":true
    },{
        "name":"",
        "alias":1,
        "timestamp":timestamp,
        "dataType":"Int32",
        "value":3
    }],
    "seq":-1
};

@PranavT You said you’ve been able to do it as well, could you please share your method? I’m curious if they’re the same or if you have found some other approach!

Again, welcome to the forum and thanks for your great question. I wish I understood this subject more deeply, but I hope I gave you some helpful information nonetheless.

1 Like

@torchard Thank you Terry for the detailed information. Now I can understand a bit more about seq.

Now for the Flow part, I was doing exactly the same as you have posted here until when I came to know that the google proto buffer is changed. So for that part, I’ve created a new flow.

msg = {
payload:{
"timestamp": new Date().getTime(),
"metrics": [
    {
    name : "Inputs/A",
    timestamp: Date.now(),
    datatype: 11,
    booleanValue : true,
    
    
    properties : {
        "engUnit": {type:3, isNull: false, intValue:11 }
    }
}]

},
“seq” : -1
};

but here the properties field is not working. It is passing from google proto buffer but on result it is not showing the properties result.