ControlForge Sparkplug B Protocol Guide
James M. Belcher Founder, JMB Technical Services LLC May 2026 | ControlForge v1.0.865
1. Architecture Overview
ControlForge implements Sparkplug B v3.0 — both the edge-node (publisher) and the host-application (subscriber) roles — callable directly from IEC 61131-3 Structured Text. No Ignition modules, no Java dependencies, no external gateway software. ControlForge connects to any MQTT 3.1.1 broker (Mosquitto, EMQX, HiveMQ Cloud, AWS IoT) and speaks native Sparkplug B with Google Protocol Buffer payloads, the full birth/death lifecycle, metric aliases for bandwidth-efficient NDATA, PropertySet metadata, quality flags, devices, primary-host STATE awareness, and seq-gap-driven rebirth requests.
| Role | Functions | Use Case |
|---|---|---|
| Edge Node | SPARKPLUG_NODE_CREATE / SPARKPLUG_NODE_BIRTH / SPARKPLUG_NODE_DATA | Publish PLC data to SCADA via Sparkplug-aware infrastructure |
| Metrics | SPARKPLUG_METRIC_ADD / SPARKPLUG_METRIC_SET / SPARKPLUG_METRIC_GET | Register and update named data points; NBIRTH+alias / alias-only NDATA |
| Properties | SPARKPLUG_METRIC_SET_UNIT / _SET_RANGE / _SET_DESC | PropertySet metadata (engUnit / engHigh / engLow / description) — Ignition tag browser displays it |
| Quality flags | SPARKPLUG_METRIC_SET_NULL / _MARK_HISTORICAL / _MARK_TRANSIENT | is_null suppresses value; is_historical / is_transient flag store-and-forward replays |
| Extended types | SPARKPLUG_METRIC_ADD_TEXT / _BYTES / _*_ARRAY | Text, UUID, Bytes, File, DateTime, and all 13 array types from spec §16 |
| Devices | SPARKPLUG_DEVICE_ADD / _BIRTH / _DATA / DCMD_* | Sub-devices under one edge node (DBIRTH/DDEATH/DDATA/DCMD) |
| Commands | SPARKPLUG_CMD_SUBSCRIBE / _GET / _CLEAR | Receive NCMD writes from SCADA; Node Control/Rebirth handled automatically |
| Host STATE awareness | SPARKPLUG_NODE_SET_PRIMARY_HOST / _HOST_ONLINE | Suspend NDATA when configured primary host is offline; auto-rebirth on host online |
| Host Application | SPARKPLUG_HOST_CREATE / _START / _GET_METRIC / _PUBLISH_NCMD | ControlForge as Sparkplug consumer — subscribe to remote EoNs, read metrics, push NCMD/DCMD |
All functions are controlled entirely from IEC 61131-3 Structured Text in ControlForge's browser-based IDE.
System Diagram
Sparkplug B Concepts
| Concept | Description |
|---|---|
| Group ID | Logical grouping (e.g., Plant, Building1) — first level of the topic namespace |
| Edge Node ID | Unique identifier for this ControlForge instance within the group |
| Metric | A named data point (temperature, pressure, motor state) with type, value, and timestamp |
| NBIRTH | Node Birth certificate — full metric catalog published on connect |
| NDEATH | Node Death certificate — pre-registered LWT, published by broker on disconnect |
| NDATA | Node Data — incremental update with only changed metrics |
| NCMD | Node Command — write-back from SCADA to edge node |
| Sequence Number | 0-255 monotonic counter in every message — consumers detect gaps as missed data |
Sparkplug B Topic Namespace
spBv1.0/{group_id}/{message_type}/{edge_node_id}
Examples:
spBv1.0/Plant/Line1/NBIRTH/ControlForge-Edge1 ← birth certificate
spBv1.0/Plant/Line1/NDEATH/ControlForge-Edge1 ← death certificate (LWT)
spBv1.0/Plant/Line1/NDATA/ControlForge-Edge1 ← data updates
spBv1.0/Plant/Line1/NCMD/ControlForge-Edge1 ← commands from SCADA
Why Sparkplug over raw MQTT? Raw MQTT requires every subscriber to know every topic. Sparkplug adds structure: a birth certificate defines every metric (name, type, initial value), consumers auto-discover tags, the death certificate handles ungraceful disconnects, and sequence numbers detect data loss. Ignition SCADA discovers and displays all ControlForge tags automatically — zero configuration on the Ignition side.
2. Node Lifecycle
2.1 SPARKPLUG_NODE_CREATE -- Create Edge Node
ok := SPARKPLUG_NODE_CREATE('node1', 'Plant/Line1', 'ControlForge-Edge1',
'tcp://10.0.0.144:1883', 'goplc-sparkplug-1');
| Param | Type | Required | Description |
|---|---|---|---|
name | STRING | Yes | Instance name (used in all subsequent calls) |
groupID | STRING | Yes | Sparkplug group ID (e.g., Plant/Line1) |
edgeNodeID | STRING | Yes | Unique edge node identifier |
brokerURL | STRING | Yes | MQTT broker URL: tcp://host:port or ssl://host:port |
clientID | STRING | Yes | MQTT client ID (must be unique per broker) |
Returns TRUE on success. The node is created but not yet connected -- call SPARKPLUG_NODE_CONNECT next.
For authenticated connections, use SPARKPLUG_NODE_CREATE_AUTH:
(* No authentication *)
ok := SPARKPLUG_NODE_CREATE('node1', 'Plant', 'Edge1',
'tcp://broker:1883', 'goplc-sp-1');
(* With authentication — use SPARKPLUG_NODE_CREATE_AUTH *)
ok := SPARKPLUG_NODE_CREATE_AUTH('node1', 'Plant', 'Edge1',
'tcp://broker:1883', 'goplc-sp-1',
'goplc_user', 's3cretP@ss');
(* TLS + authentication *)
ok := SPARKPLUG_NODE_CREATE_AUTH('node1', 'Plant', 'Edge1',
'ssl://broker:8883', 'goplc-sp-1',
'goplc_user', 's3cretP@ss');
Client ID uniqueness: If two nodes connect with the same client ID, the broker disconnects the first one. Use a unique ID per ControlForge instance — hostname or MAC address works well.
2.2 SPARKPLUG_NODE_CONNECT / Disconnect / IsConnected
(* Connect to broker — registers NDEATH as LWT *)
ok := SPARKPLUG_NODE_CONNECT('node1');
(* Check connection state *)
IF SPARKPLUG_NODE_IS_CONNECTED('node1') THEN
(* publish metrics *)
END_IF;
(* Graceful disconnect — triggers NDEATH from broker LWT *)
SPARKPLUG_NODE_DISCONNECT('node1');
SPARKPLUG_NODE_CONNECT establishes the MQTT connection and registers the NDEATH message as the broker's Last Will and Testament (LWT). If ControlForge crashes or loses network, the broker publishes NDEATH automatically — Ignition and other consumers see the node go offline immediately.
2.3 SPARKPLUG_NODE_DELETE / SPARKPLUG_NODE_LIST
(* Remove a node *)
SPARKPLUG_NODE_DELETE('node1');
(* List all Sparkplug nodes *)
names := SPARKPLUG_NODE_LIST();
(* Returns: 'node1,node2' — comma-separated string *)
3. Metrics
3.1 SPARKPLUG_METRIC_ADD -- Register a Metric
(* Add metrics with initial values — type is auto-detected *)
ok := SPARKPLUG_METRIC_ADD('node1', 'Temperature', 72.5); (* Float *)
ok := SPARKPLUG_METRIC_ADD('node1', 'MotorRunning', TRUE); (* Boolean *)
ok := SPARKPLUG_METRIC_ADD('node1', 'BatchCount', 0); (* Integer *)
ok := SPARKPLUG_METRIC_ADD('node1', 'RecipeName', 'Default'); (* String *)
| Param | Type | Description |
|---|---|---|
name | STRING | Node instance name |
metricName | STRING | Metric name (appears in Sparkplug birth certificate and SCADA tag browser) |
value | ANY | Initial value — type is inferred (BOOL, INT, REAL, STRING) |
Returns TRUE on success. Metrics must be added before calling SPARKPLUG_NODE_BIRTH — the birth certificate includes the complete metric catalog.
Metric naming: Use descriptive, hierarchical names. Ignition displays them as-is in the tag browser.
Line1/Motor/Speedis better thanN7_0. Sparkplug metric names support/separators — Ignition renders them as a folder tree.
3.2 SPARKPLUG_METRIC_SET -- Update a Metric Value
(* Update metric — marks it as changed for next NDATA *)
ok := SPARKPLUG_METRIC_SET('node1', 'Temperature', 73.1);
ok := SPARKPLUG_METRIC_SET('node1', 'MotorRunning', FALSE);
ok := SPARKPLUG_METRIC_SET('node1', 'BatchCount', 42);
| Param | Type | Description |
|---|---|---|
name | STRING | Node instance name |
metricName | STRING | Metric name (must have been added with SPARKPLUG_METRIC_ADD) |
value | ANY | New value |
Returns TRUE on success. This does not immediately publish — it marks the metric as changed. Call SPARKPLUG_NODE_DATA to publish all changed metrics in a single NDATA message.
3.3 SPARKPLUG_METRIC_GET -- Read Current Value
temp := SPARKPLUG_METRIC_GET('node1', 'Temperature');
(* Returns: 73.1 *)
running := SPARKPLUG_METRIC_GET('node1', 'MotorRunning');
(* Returns: TRUE *)
Returns the current local value of the metric. This reads from ControlForge's in-memory metric store, not from the broker.
Type-Specific Getters
For type safety, use the typed variants instead of the generic SPARKPLUG_METRIC_GET:
temp := SPARKPLUG_METRIC_GET_REAL('node1', 'Temperature'); (* Returns: 73.1 as REAL *)
count := SPARKPLUG_METRIC_GET_INT('node1', 'BatchCount'); (* Returns: 42 as INT *)
running := SPARKPLUG_METRIC_GET_BOOL('node1', 'MotorRunning'); (* Returns: TRUE as BOOL *)
recipe := SPARKPLUG_METRIC_GET_STR('node1', 'RecipeName'); (* Returns: "Batch-A" as STRING *)
The generic SPARKPLUG_METRIC_GET returns ANY and relies on the runtime to infer the type. The typed variants guarantee the return type and return a default value (0.0, 0, FALSE, or empty string) if the metric is not found.
4. Publishing
4.1 SPARKPLUG_NODE_BIRTH -- Send NBIRTH
ok := SPARKPLUG_NODE_BIRTH('node1');
Publishes the Node Birth Certificate to spBv1.0/{groupID}/NBIRTH/{edgeNodeID}. The birth message contains:
- All registered metrics with their current values and Sparkplug data types
- Sequence number reset to 0
- Timestamp (milliseconds since epoch)
The birth certificate is a retained message — new subscribers (like Ignition connecting later) receive the full metric catalog immediately.
When to send NBIRTH: Call
SPARKPLUG_NODE_BIRTHonce after connecting and adding all metrics. If Ignition sends a rebirth request via NCMD, call it again to republish the full metric catalog.
4.2 SPARKPLUG_NODE_DEATH -- Send NDEATH
ok := SPARKPLUG_NODE_DEATH('node1');
Publishes the Node Death Certificate to spBv1.0/{groupID}/NDEATH/{edgeNodeID}. This signals an intentional, graceful shutdown. The broker also publishes NDEATH automatically (via LWT) if the connection drops unexpectedly.
Graceful vs. ungraceful:
SPARKPLUG_NODE_DISCONNECTtriggers the broker's LWT (NDEATH).SPARKPLUG_NODE_DEATHsends it explicitly before disconnecting. Both result in the same NDEATH message reaching consumers — the distinction matters only for timing.
4.3 SPARKPLUG_NODE_DATA -- Send NDATA (Changed Metrics Only)
ok := SPARKPLUG_NODE_DATA('node1');
Publishes an NDATA message containing only metrics that changed since the last NDATA or NBIRTH. This is the primary data publishing function — call it on every scan cycle or at your desired publish rate.
(* Typical scan cycle pattern *)
SPARKPLUG_METRIC_SET('node1', 'Temperature', current_temp);
SPARKPLUG_METRIC_SET('node1', 'Pressure', current_pressure);
SPARKPLUG_METRIC_SET('node1', 'MotorRunning', motor_fb);
(* Publish only changed values *)
SPARKPLUG_NODE_DATA('node1');
If no metrics have changed, SPARKPLUG_NODE_DATA returns TRUE without publishing — no empty messages are sent. The sequence number only increments when a message is actually published.
4.4 SPARKPLUG_GET_SEQ -- Current Sequence Number
seq := SPARKPLUG_GET_SEQ('node1');
(* Returns: 42 — current sequence number (0-255, wraps) *)
Returns the current Sparkplug sequence number. Consumers use this to detect missed messages — a gap in the sequence means data was lost and a rebirth should be requested.
5. Commands (NCMD)
5.1 SPARKPLUG_CMD_SUBSCRIBE -- Listen for SCADA Commands
ok := SPARKPLUG_CMD_SUBSCRIBE('node1');
Subscribes to the NCMD topic: spBv1.0/{groupID}/NCMD/{edgeNodeID}. SCADA systems (Ignition, etc.) publish NCMD messages to write values back to the edge node — setpoints, mode changes, rebirth requests.
5.2 SPARKPLUG_CMD_HAS / CmdGet / CmdClear
(* Check if a command arrived for a specific metric *)
IF SPARKPLUG_CMD_HAS('node1', 'Setpoint') THEN
(* Read the commanded value *)
new_sp := SPARKPLUG_CMD_GET('node1', 'Setpoint');
(* Apply it *)
target_temp := new_sp;
(* Clear the command flag *)
SPARKPLUG_CMD_CLEAR('node1', 'Setpoint');
END_IF;
(* Handle rebirth request from Ignition *)
IF SPARKPLUG_CMD_HAS('node1', 'Node Control/Rebirth') THEN
SPARKPLUG_CMD_CLEAR('node1', 'Node Control/Rebirth');
SPARKPLUG_NODE_BIRTH('node1'); (* republish full metric catalog *)
END_IF;
| Function | Params | Returns | Description |
|---|---|---|---|
SPARKPLUG_CMD_HAS | (name, metricName) | BOOL | Check if a command arrived |
SPARKPLUG_CMD_GET | (name, metricName) | ANY | Read the commanded value |
SPARKPLUG_CMD_CLEAR | (name, metricName) | BOOL | Clear the command flag |
Ignition rebirth: When Ignition connects to a broker and finds an existing edge node, it sends a rebirth request via NCMD with metric name
Node Control/Rebirth. Your ST program must handle this by callingSPARKPLUG_NODE_BIRTHto republish the full metric catalog.
6. Complete Example: Production Line to Ignition
This example connects a ControlForge edge node to Ignition SCADA via Sparkplug B, publishing production data and accepting setpoint commands:
PROGRAM POU_Sparkplug_Production
VAR
state : INT := 0;
ok : BOOL;
(* Process values — updated from other programs or I/O *)
line_speed : REAL := 0.0;
motor_temp : REAL := 0.0;
conveyor_running : BOOL := FALSE;
batch_count : DINT := 0;
reject_count : DINT := 0;
(* Setpoint from SCADA *)
speed_setpoint : REAL := 100.0;
new_sp : REAL;
publish_counter : INT := 0;
END_VAR
CASE state OF
0: (* Create Sparkplug edge node *)
ok := SPARKPLUG_NODE_CREATE('prod', 'Factory/Line1', 'ControlForge-Line1',
'tcp://10.0.0.144:1883', 'goplc-line1-sp');
IF ok THEN state := 1; END_IF;
1: (* Connect to broker *)
ok := SPARKPLUG_NODE_CONNECT('prod');
IF ok THEN state := 2; END_IF;
2: (* Register all metrics *)
SPARKPLUG_METRIC_ADD('prod', 'Line/Speed', line_speed);
SPARKPLUG_METRIC_ADD('prod', 'Line/SpeedSetpoint', speed_setpoint);
SPARKPLUG_METRIC_ADD('prod', 'Motor/Temperature', motor_temp);
SPARKPLUG_METRIC_ADD('prod', 'Conveyor/Running', conveyor_running);
SPARKPLUG_METRIC_ADD('prod', 'Production/BatchCount', batch_count);
SPARKPLUG_METRIC_ADD('prod', 'Production/RejectCount', reject_count);
state := 3;
3: (* Publish birth certificate — Ignition auto-discovers all tags *)
ok := SPARKPLUG_NODE_BIRTH('prod');
IF ok THEN state := 4; END_IF;
4: (* Subscribe to commands from Ignition *)
ok := SPARKPLUG_CMD_SUBSCRIBE('prod');
IF ok THEN state := 10; END_IF;
10: (* Running — update metrics and publish *)
(* Update metric values from process *)
SPARKPLUG_METRIC_SET('prod', 'Line/Speed', line_speed);
SPARKPLUG_METRIC_SET('prod', 'Motor/Temperature', motor_temp);
SPARKPLUG_METRIC_SET('prod', 'Conveyor/Running', conveyor_running);
SPARKPLUG_METRIC_SET('prod', 'Production/BatchCount', batch_count);
SPARKPLUG_METRIC_SET('prod', 'Production/RejectCount', reject_count);
(* Publish changed metrics every 10 scans (~1 second at 100ms scan) *)
publish_counter := publish_counter + 1;
IF publish_counter >= 10 THEN
SPARKPLUG_NODE_DATA('prod');
publish_counter := 0;
END_IF;
(* Handle setpoint commands from Ignition *)
IF SPARKPLUG_CMD_HAS('prod', 'Line/SpeedSetpoint') THEN
new_sp := SPARKPLUG_CMD_GET('prod', 'Line/SpeedSetpoint');
speed_setpoint := new_sp;
SPARKPLUG_METRIC_SET('prod', 'Line/SpeedSetpoint', speed_setpoint);
SPARKPLUG_CMD_CLEAR('prod', 'Line/SpeedSetpoint');
END_IF;
(* Handle rebirth request *)
IF SPARKPLUG_CMD_HAS('prod', 'Node Control/Rebirth') THEN
SPARKPLUG_CMD_CLEAR('prod', 'Node Control/Rebirth');
SPARKPLUG_NODE_BIRTH('prod');
END_IF;
END_CASE;
END_PROGRAM
7. Ignition SCADA Integration
Connecting Ignition to ControlForge via Sparkplug
-
Install Cirrus Link MQTT Transmission/Engine modules in Ignition (or use the built-in Sparkplug support in Ignition 8.1+).
-
Configure MQTT Engine to connect to the same broker ControlForge uses:
- Server URL:
tcp://10.0.0.144:1883 - Group ID filter:
Factory(or leave blank for all)
- Server URL:
-
Start ControlForge with the Sparkplug program above. Ignition auto-discovers the edge node and creates tags under:
[MQTT Engine]Factory/Line1/ControlForge-Line1/Line/Speed[MQTT Engine]Factory/Line1/ControlForge-Line1/Motor/Temperature[MQTT Engine]Factory/Line1/ControlForge-Line1/Conveyor/Running... -
Bind Ignition tags to Vision/Perspective screens. Writes from Ignition flow back as NCMD messages, which ControlForge receives via
SPARKPLUG_CMD_HAS/SPARKPLUG_CMD_GET.
Tag Quality and Stale Detection
Ignition tracks tag quality based on Sparkplug lifecycle:
| Sparkplug Event | Ignition Tag Quality |
|---|---|
| NBIRTH received | Good |
| NDATA received | Good (updated) |
| NDEATH received | Bad (stale) |
| Sequence gap detected | Bad (stale) — Ignition requests rebirth |
| No NDATA for timeout period | Uncertain (stale) |
Metric Naming Best Practices for Ignition
(* Good — creates folder hierarchy in Ignition tag browser *)
SPARKPLUG_METRIC_ADD('prod', 'Line1/Motor/Speed', 0.0);
SPARKPLUG_METRIC_ADD('prod', 'Line1/Motor/Temperature', 0.0);
SPARKPLUG_METRIC_ADD('prod', 'Line1/Motor/Running', FALSE);
SPARKPLUG_METRIC_ADD('prod', 'Line1/Conveyor/Speed', 0.0);
(* Bad — flat namespace, hard to navigate in Ignition *)
SPARKPLUG_METRIC_ADD('prod', 'line1_motor_speed', 0.0);
SPARKPLUG_METRIC_ADD('prod', 'line1_motor_temp', 0.0);
8. Sparkplug B Message Lifecycle
Startup Sequence
1. SPARKPLUG_NODE_CREATE() → Register node (no network)
2. SPARKPLUG_METRIC_ADD() ×N → Register all metrics (aliases auto-assigned)
3. SPARKPLUG_METRIC_SET_UNIT(...) → (optional) Add property metadata
4. SPARKPLUG_DEVICE_ADD() / DEVICE_METRIC_ADD() → (optional) Add sub-devices
5. SPARKPLUG_NODE_SET_PRIMARY_HOST() → (optional) Track a host's STATE
6. SPARKPLUG_NODE_CONNECT() → MQTT CONNECT (LWT carries current bdSeq)
7. SPARKPLUG_NODE_BIRTH() → Publish NBIRTH (name+alias, seq=0)
8. SPARKPLUG_DEVICE_BIRTH() ×N → Publish DBIRTH for each device
9. SPARKPLUG_CMD_SUBSCRIBE() → Subscribe to NCMD (and DCMD per device)
10. loop: SPARKPLUG_NODE_DATA() / DEVICE_DATA() → NDATA/DDATA, alias-only
Shutdown Sequence
1. SPARKPLUG_NODE_DISCONNECT() → Publish NDEATH (matching bdSeq), close MQTT
2. SPARKPLUG_NODE_DELETE() → Free resources
Ungraceful Disconnect & Recovery
1. Network drops / process crashes
2. Broker detects TCP timeout (keepalive, default 30s)
3. Broker publishes NDEATH from the LWT (carries the session's bdSeq)
4. SCADA marks all tags Bad/Stale
5. ControlForge's ConnectionLostHandler fires:
• bdSeq is bumped for the next session
• paho client is rebuilt with a fresh LWT (new bdSeq)
• Reconnect attempted with exponential backoff (1s → 30s cap)
6. On reconnect: OnConnect handler auto-publishes NBIRTH + all DBIRTHs
(any prior NCMD subscription is re-applied too — clean sessions forget them)
Host-driven Rebirth (NCMD)
If a SCADA host sends Node Control/Rebirth = true on the NCMD topic, ControlForge
intercepts it (does not surface to ST) and republishes NBIRTH followed by
DBIRTH for every birthed device. This complies with Sparkplug 3.0 §5.4.
Primary-Host STATE Awareness
SPARKPLUG_NODE_SET_PRIMARY_HOST(name, host_id) subscribes the node to
spBv1.0/STATE/<host_id>. While the host is offline, NDATA/DDATA publishing
is suspended (no point talking into the void). When the host comes back
online, the node automatically republishes NBIRTH + DBIRTHs so the host
rebuilds its view. Both Sparkplug 3.0 JSON STATE ({"online":bool,...})
and legacy 2.x plain-text (ONLINE/OFFLINE) payloads are parsed.
Protobuf Payload Structure
Every Sparkplug message body is a Google Protocol Buffer (org.eclipse.tahu.protobuf.Payload):
Payload {
timestamp: uint64 // ms since epoch — field 1
seq: uint64 // 0–255, wraps — field 3
metrics: [
{
name: string // field 1 (NBIRTH only when alias is used)
alias: uint64 // field 2 — stable per session
timestamp: uint64 // field 3
datatype: uint32 // field 4 (1–18 per Sparkplug 3.0 §6.4.6 + 22–34 for arrays)
is_historical: bool // field 5
is_transient: bool // field 6
is_null: bool // field 7 (suppresses value field below)
properties: PropertySet // field 9 (NBIRTH only — engUnit / engHigh / engLow / description / ...)
value: oneof // int_value(10) / long_value(11) / float_value(12) /
// double_value(13) / boolean_value(14) / string_value(15) /
// bytes_value(16, for Bytes/File/arrays) / dataset_value(17)
},
...
]
}
ControlForge handles all Protobuf encoding/decoding automatically. You work with
native ST types (INT, REAL, BOOL, STRING) plus the typed _ADD_* builtins
for binary/arrays, and ControlForge maps them to the correct Sparkplug data types.
9. Troubleshooting
Common Issues
| Symptom | Cause | Fix |
|---|---|---|
| Ignition shows no tags | NBIRTH not published | Ensure SPARKPLUG_NODE_BIRTH is called after all metrics are added |
| Tags show Bad quality | NDEATH received | Check ControlForge connection to broker; verify keepalive |
| Stale data in Ignition | NDATA not publishing | Verify SPARKPLUG_NODE_DATA is called periodically |
| Ignition requests rebirth repeatedly | Sequence gap | Ensure no duplicate client IDs; check for network drops |
| Commands not arriving | NCMD not subscribed | Call SPARKPLUG_CMD_SUBSCRIBE after connecting |
| Metrics missing from birth | Added after NBIRTH | Add all metrics before calling SPARKPLUG_NODE_BIRTH |
| Broker rejects connection | Duplicate client ID | Use unique clientID per ControlForge instance |
| TLS handshake failure | Certificate mismatch | Verify broker CA cert and hostname match ssl:// URL |
Appendix A: Function Quick Reference
Edge Node — lifecycle
| Function | Params | Returns | Description |
|---|---|---|---|
SPARKPLUG_NODE_CREATE | (name, groupID, edgeNodeID, brokerURL, clientID) | BOOL | Register node (empty clientID auto-generates) |
SPARKPLUG_NODE_CREATE_AUTH | (name, groupID, edgeNodeID, brokerURL, clientID, user, pass) | BOOL | As above, with broker credentials |
SPARKPLUG_NODE_CONNECT | (name) | BOOL | Connect; LWT carries current bdSeq |
SPARKPLUG_NODE_DISCONNECT | (name) | BOOL | Publish NDEATH + close; bumps bdSeq for next session |
SPARKPLUG_NODE_IS_CONNECTED | (name) | BOOL | True if currently connected |
SPARKPLUG_NODE_DELETE | (name) | BOOL | Stop reconnect loop + close + free |
SPARKPLUG_NODE_LIST | () | STRING | Comma-separated node names |
SPARKPLUG_NODE_BIRTH | (name) | BOOL | NBIRTH (full metric catalog, name+alias) |
SPARKPLUG_NODE_DEATH | (name) | BOOL | NDEATH explicitly (matches session's bdSeq) |
SPARKPLUG_NODE_DATA | (name) | BOOL | NDATA (changed metrics, alias-only); suspended while primary host offline |
SPARKPLUG_GET_SEQ | (name) | INT | Current Sparkplug seq counter (0–255) |
Edge Node — metrics
| Function | Params | Returns | Description |
|---|---|---|---|
SPARKPLUG_METRIC_ADD | (name, metric, value) | BOOL | Register with inferred type + auto alias |
SPARKPLUG_METRIC_SET | (name, metric, value) | BOOL | Update + mark changed |
SPARKPLUG_METRIC_GET / _GET_REAL / _INT / _BOOL / _STR | (name, metric) | typed | Read current local value |
SPARKPLUG_METRIC_GET_ALIAS | (name, metric) | INT | Alias assigned in NBIRTH (0 if no such metric) |
SPARKPLUG_METRIC_ADD_TEXT / _UUID / _BYTES / _FILE / _DATETIME | (name, metric, value) | BOOL | Typed adds for extended Sparkplug types |
SPARKPLUG_METRIC_ADD_INT_ARRAY / _LONG_ / _REAL_ / _DOUBLE_ / _BOOL_ / _STR_ARRAY | (name, metric, csv_str) | BOOL | Add array metric — CSV input, packed LE per spec §16 |
Edge Node — properties + flags
| Function | Params | Returns | Description |
|---|---|---|---|
SPARKPLUG_METRIC_SET_UNIT | (name, metric, unit_str) | BOOL | Sets engUnit property (e.g. "°C") |
SPARKPLUG_METRIC_SET_RANGE | (name, metric, low_real, high_real) | BOOL | Sets engLow + engHigh |
SPARKPLUG_METRIC_SET_DESC | (name, metric, desc_str) | BOOL | Sets description |
SPARKPLUG_METRIC_SET_DOC | (name, metric, doc_str) | BOOL | Sets documentation |
SPARKPLUG_METRIC_SET_PROP_STR / _REAL / _INT / _BOOL | (name, metric, prop_name, value) | BOOL | Generic typed property setter |
SPARKPLUG_METRIC_SET_NULL | (name, metric, is_null) | BOOL | Toggle is_null (suppresses value on wire) |
SPARKPLUG_METRIC_MARK_HISTORICAL | (name, metric, hist) | BOOL | Mark as historical (store-and-forward replay) |
SPARKPLUG_METRIC_MARK_TRANSIENT | (name, metric, trans) | BOOL | Mark as transient (host should not historize) |
Edge Node — NCMD
| Function | Params | Returns | Description |
|---|---|---|---|
SPARKPLUG_CMD_SUBSCRIBE | (name) | BOOL | Subscribe to NCMD topic (auto-re-applied on reconnect) |
SPARKPLUG_CMD_HAS | (name, metric) | BOOL | True if command arrived for that metric |
SPARKPLUG_CMD_GET / _GET_REAL / _INT / _BOOL / _STR | (name, metric) | typed | Read commanded value |
SPARKPLUG_CMD_CLEAR | (name, metric) | BOOL | Clear the "has cmd" flag |
Internal NCMD metrics — Node Control/Rebirth, Node Control/Next Server, Node Control/Reboot — are intercepted and handled automatically; they do not appear to ST.
Devices
| Function | Params | Returns | Description |
|---|---|---|---|
SPARKPLUG_DEVICE_ADD | (node, device) | BOOL | Register a sub-device |
SPARKPLUG_DEVICE_DELETE | (node, device) | BOOL | Publish DDEATH + remove |
SPARKPLUG_DEVICE_LIST | (node) | STRING | Comma-separated device IDs |
SPARKPLUG_DEVICE_METRIC_ADD / _SET / _GET[_*] | (node, device, metric, ...) | typed | Per-device metric ops (shared alias namespace) |
SPARKPLUG_DEVICE_BIRTH / _DEATH / _DATA | (node, device) | BOOL | DBIRTH/DDEATH/DDATA — seq from parent's counter |
SPARKPLUG_DCMD_SUBSCRIBE / _HAS / _GET[_*] / _CLEAR | (node, device, metric, ...) | typed | DCMD inbound on .../<device> |
Primary-Host STATE awareness
| Function | Params | Returns | Description |
|---|---|---|---|
SPARKPLUG_NODE_SET_PRIMARY_HOST | (node, host_id) | BOOL | Track spBv1.0/STATE/<host_id>; empty disables |
SPARKPLUG_NODE_HOST_ONLINE | (node) | BOOL | Current host online status (true while STATE unknown) |
Host Application role
| Function | Params | Returns | Description |
|---|---|---|---|
SPARKPLUG_HOST_CREATE | (name, host_id, broker, client_id) | BOOL | Register a host-app identity |
SPARKPLUG_HOST_CREATE_AUTH | (name, host_id, broker, client_id, user, pass) | BOOL | As above with auth |
SPARKPLUG_HOST_START | (name) | BOOL | Connect, publish retained ONLINE STATE, subscribe to spBv1.0/+/... |
SPARKPLUG_HOST_STOP | (name) | BOOL | Publish OFFLINE STATE + disconnect |
SPARKPLUG_HOST_DELETE | (name) | BOOL | Tear down |
SPARKPLUG_HOST_LIST | () | STRING | Comma-separated host names |
SPARKPLUG_HOST_NODE_LIST | (name) | STRING | group/edge of every remote node discovered |
SPARKPLUG_HOST_NODE_IS_ONLINE | (name, group, edge) | BOOL | Remote node online state |
SPARKPLUG_HOST_DEVICE_IS_ONLINE | (name, group, edge, device) | BOOL | Remote device online state |
SPARKPLUG_HOST_GET_METRIC / _REAL / _INT / _BOOL / _STR | (name, "group/edge[/device]/metric") | typed | Read a remote metric |
SPARKPLUG_HOST_PUBLISH_NCMD | (name, group, edge, metric, value) | BOOL | Issue NCMD write to a remote edge node |
SPARKPLUG_HOST_PUBLISH_DCMD | (name, group, edge, device, metric, value) | BOOL | Issue DCMD write to a remote device |
SPARKPLUG_HOST_REQUEST_REBIRTH | (name, group, edge) | BOOL | Sugar for Node Control/Rebirth=true NCMD |
The host automatically issues Node Control/Rebirth when it detects an
NDATA/DDATA sequence gap or an NDATA/DDATA arriving without a prior NBIRTH.
ControlForge v1.0.865 | Sparkplug B v3.0 | Eclipse Tahu Protobuf + MQTT 3.1.1
© 2026 JMB Technical Services LLC. All rights reserved. Back to All Guides