From 11eceb7460eb1e85116817a7a34981cf9282b292 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Thu, 5 Feb 2026 15:27:10 -0600 Subject: [PATCH 1/5] Add Sparkplug B example with two communicating MQTT clients New example demonstrating the Sparkplug B industrial IoT protocol: - Edge Node client publishes sensor data and responds to commands - Host Application client subscribes to data and sends commands - Implements Sparkplug topic namespace (spBv1.0/{group}/{type}/{node}[/{device}]) - Demonstrates NBIRTH, NDEATH, DDATA, and DCMD message types - Includes simplified payload encoding (not full protobuf) - Supports both single-threaded and multi-threaded builds Co-Authored-By: Claude Opus 4.5 --- CMakeLists.txt | 1 + examples/include.am | 19 +- examples/sparkplug/sparkplug.c | 1022 ++++++++++++++++++++++++++++++++ examples/sparkplug/sparkplug.h | 542 +++++++++++++++++ 4 files changed, 1581 insertions(+), 3 deletions(-) create mode 100644 examples/sparkplug/sparkplug.c create mode 100644 examples/sparkplug/sparkplug.h diff --git a/CMakeLists.txt b/CMakeLists.txt index fe13be5d..6418bb75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -284,6 +284,7 @@ if (WOLFMQTT_EXAMPLES) add_mqtt_example(fwclient firmware/fwclient.c) add_mqtt_example(mqtt-pub pub-sub/mqtt-pub.c) add_mqtt_example(mqtt-sub pub-sub/mqtt-sub.c) + add_mqtt_example(sparkplug sparkplug/sparkplug.c) endif() #################################################### diff --git a/examples/include.am b/examples/include.am index b4d6fcda..cd90c9cb 100644 --- a/examples/include.am +++ b/examples/include.am @@ -12,7 +12,8 @@ noinst_PROGRAMS += examples/mqttclient/mqttclient \ examples/nbclient/nbclient \ examples/multithread/multithread \ examples/pub-sub/mqtt-pub \ - examples/pub-sub/mqtt-sub + examples/pub-sub/mqtt-sub \ + examples/sparkplug/sparkplug if BUILD_SN noinst_PROGRAMS += examples/sn-client/sn-client \ examples/sn-client/sn-client_qos-1 \ @@ -32,7 +33,8 @@ noinst_HEADERS += examples/mqttclient/mqttclient.h \ examples/mqttport.h \ examples/nbclient/nbclient.h \ examples/multithread/multithread.h \ - examples/pub-sub/mqtt-pub-sub.h + examples/pub-sub/mqtt-pub-sub.h \ + examples/sparkplug/sparkplug.h if BUILD_SN noinst_HEADERS += examples/sn-client/sn-client.h endif @@ -151,6 +153,15 @@ examples_pub_sub_mqtt_sub_LDADD = src/libwolfmqtt.la examples_pub_sub_mqtt_sub_DEPENDENCIES = src/libwolfmqtt.la examples_pub_sub_mqtt_sub_CPPFLAGS = -I$(top_srcdir)/examples $(AM_CPPFLAGS) + +# Sparkplug B Example (two-client industrial IoT) +examples_sparkplug_sparkplug_SOURCES = examples/sparkplug/sparkplug.c \ + examples/mqttnet.c \ + examples/mqttexample.c +examples_sparkplug_sparkplug_LDADD = src/libwolfmqtt.la +examples_sparkplug_sparkplug_DEPENDENCIES = src/libwolfmqtt.la +examples_sparkplug_sparkplug_CPPFLAGS = -I$(top_srcdir)/examples $(AM_CPPFLAGS) + # WebSocket example if BUILD_WEBSOCKET noinst_HEADERS += examples/websocket/net_libwebsockets.h @@ -185,6 +196,7 @@ dist_example_DATA+= examples/sn-client/sn-multithread.c endif dist_example_DATA+= examples/pub-sub/mqtt-pub.c dist_example_DATA+= examples/pub-sub/mqtt-sub.c +dist_example_DATA+= examples/sparkplug/sparkplug.c if BUILD_WEBSOCKET dist_example_DATA+= examples/websocket/websocket_client.c dist_example_DATA+= examples/websocket/net_libwebsockets.c @@ -199,7 +211,8 @@ DISTCLEANFILES+= examples/mqttclient/.libs/mqttclient \ examples/nbclient/.libs/nbclient \ examples/multithread/.libs/multithread \ examples/pub-sub/mqtt-pub \ - examples/pub-sub/mqtt-sub + examples/pub-sub/mqtt-sub \ + examples/sparkplug/.libs/sparkplug if BUILD_SN DISTCLEANFILES+= examples/sn-client/.libs/sn-client \ examples/sn-client/.libs/sn-client_qos-1 \ diff --git a/examples/sparkplug/sparkplug.c b/examples/sparkplug/sparkplug.c new file mode 100644 index 00000000..03d31fb8 --- /dev/null +++ b/examples/sparkplug/sparkplug.c @@ -0,0 +1,1022 @@ +/* sparkplug.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfMQTT. + * + * wolfMQTT is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfMQTT is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* Sparkplug B Example + * + * This example demonstrates Sparkplug B industrial IoT protocol using wolfMQTT. + * Two MQTT clients communicate: + * - Edge Node: Publishes sensor data, responds to commands + * - Host Application: Subscribes to data, sends commands + * + * To run: + * ./examples/sparkplug/sparkplug + * + * The example will: + * 1. Connect both clients to the MQTT broker + * 2. Edge Node publishes NBIRTH (Node Birth Certificate) + * 3. Host subscribes to Sparkplug namespace + * 4. Edge Node publishes DDATA (Device Data) with sensor values + * 5. Host sends DCMD (Device Command) to toggle LED + * 6. Edge Node receives command and updates state + * 7. Both clients disconnect cleanly with NDEATH + */ + +#include "wolfmqtt/mqtt_client.h" +#include "examples/mqttnet.h" +#include "examples/mqttexample.h" +#include "examples/sparkplug/sparkplug.h" + +/* Enable for verbose debug output */ +/* #define SPARKPLUG_DEBUG */ + +/* Configuration */ +#define SPARKPLUG_QOS MQTT_QOS_1 +#define NUM_DATA_PUBLISHES 5 +#define DATA_PUBLISH_INTERVAL 1 /* seconds */ +#define MAX_BUFFER_SIZE 1024 + +/* Threading support */ +#ifdef WOLFMQTT_MULTITHREAD + #include "wolfmqtt/mqtt_types.h" + + #ifdef USE_WINDOWS_API + typedef HANDLE THREAD_T; + typedef DWORD THREAD_RET_T; + #define THREAD_RET_SUCCESS 0 + #define THREAD_CREATE(h, f, c) ((*h = CreateThread(NULL, 0, f, c, 0, NULL)) == NULL) + #define THREAD_JOIN(h) WaitForSingleObject(h, INFINITE) + #define THREAD_EXIT(c) ExitThread(c) + #define SLEEP(ms) Sleep(ms) + #else + #include + #include + typedef pthread_t THREAD_T; + typedef void* THREAD_RET_T; + #define THREAD_RET_SUCCESS NULL + #define THREAD_CREATE(h, f, c) pthread_create(h, NULL, f, c) + #define THREAD_JOIN(h) pthread_join(h, NULL) + #define THREAD_EXIT(c) pthread_exit(c) + #define SLEEP(ms) usleep((ms) * 1000) + #endif + + static wm_Sem gSparkplugLock; + static int gStopFlag = 0; + static int gHostReady = 0; + static int gEdgeReady = 0; + static int gCmdReceived = 0; + + #define SPARKPLUG_LOCK() wm_SemLock(&gSparkplugLock) + #define SPARKPLUG_UNLOCK() wm_SemUnlock(&gSparkplugLock) +#else + #define SPARKPLUG_LOCK() + #define SPARKPLUG_UNLOCK() +#endif + +/* Simulated sensor data */ +static struct { + float temperature; /* Temperature in Celsius */ + float humidity; /* Humidity in percent */ + int led_state; /* LED on/off */ + uint32_t counter; /* Message counter */ +} gSensorData = { 22.5f, 45.0f, 0, 0 }; + +/* Forward declarations */ +static int edge_node_run(SparkplugCtx* spCtx); +#ifdef WOLFMQTT_MULTITHREAD +static int host_app_run(SparkplugCtx* spCtx); +#endif +static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, + byte msg_new, byte msg_done); + +/* Helper to print payload (used when SPARKPLUG_DEBUG is defined) */ +#ifdef __GNUC__ +__attribute__((unused)) +#endif +static void print_payload(const char* prefix, const SparkplugPayload* payload) +{ + int i; + PRINTF("%s: seq=%llu, timestamp=%llu, metrics=%d", + prefix, (unsigned long long)payload->seq, + (unsigned long long)payload->timestamp, + payload->metric_count); + for (i = 0; i < payload->metric_count; i++) { + const SparkplugMetric* m = &payload->metrics[i]; + PRINTF(" [%d] %s (alias=%llu, type=%d):", + i, m->name, (unsigned long long)m->alias, m->datatype); + switch (m->datatype) { + case SP_DTYPE_FLOAT: + { + union { uint32_t u; float f; } conv; + conv.u = m->value.uint32_val; + PRINTF(" value = %.2f", conv.f); + } + break; + case SP_DTYPE_BOOLEAN: + PRINTF(" value = %s", m->value.uint8_val ? "true" : "false"); + break; + case SP_DTYPE_UINT32: + PRINTF(" value = %u", (unsigned int)m->value.uint32_val); + break; + case SP_DTYPE_STRING: + PRINTF(" value = \"%s\"", m->value.str_val); + break; + case SP_DTYPE_INT8: + case SP_DTYPE_INT16: + case SP_DTYPE_INT32: + case SP_DTYPE_INT64: + case SP_DTYPE_UINT8: + case SP_DTYPE_UINT16: + case SP_DTYPE_UINT64: + case SP_DTYPE_DOUBLE: + case SP_DTYPE_BYTES: + PRINTF(" value = (raw)"); + break; + } + } +} + +/* Initialize Sparkplug context */ +static int sparkplug_init(SparkplugCtx* spCtx, const char* client_id, + int is_host) +{ + MQTTCtx* mqttCtx = &spCtx->mqttCtx; + + XMEMSET(spCtx, 0, sizeof(*spCtx)); + + /* Initialize base MQTT context */ + mqtt_init_ctx(mqttCtx); + + /* Set Sparkplug-specific fields */ + spCtx->group_id = SPARKPLUG_GROUP_ID; + spCtx->edge_node_id = SPARKPLUG_EDGE_NODE_ID; + spCtx->device_id = SPARKPLUG_DEVICE_ID; + spCtx->is_host = is_host; + spCtx->bdSeq = 0; + spCtx->seq = 0; + + /* Override MQTT context settings */ + mqttCtx->client_id = client_id; + mqttCtx->qos = SPARKPLUG_QOS; + mqttCtx->retain = 0; + mqttCtx->clean_session = 1; + mqttCtx->keep_alive_sec = 60; + mqttCtx->cmd_timeout_ms = 30000; + + return MQTT_CODE_SUCCESS; +} + +/* Connect to MQTT broker */ +static int sparkplug_connect(SparkplugCtx* spCtx) +{ + int rc; + MQTTCtx* mqttCtx = &spCtx->mqttCtx; + + PRINTF("Sparkplug: Connecting %s to broker %s:%d...", + mqttCtx->client_id, mqttCtx->host, mqttCtx->port); + + /* Initialize network */ + rc = MqttClientNet_Init(&mqttCtx->net, mqttCtx); + if (rc != MQTT_CODE_SUCCESS) { + PRINTF("Sparkplug: Network init failed: %s (%d)", + MqttClient_ReturnCodeToString(rc), rc); + return rc; + } + + /* Allocate buffers */ + mqttCtx->tx_buf = (byte*)WOLFMQTT_MALLOC(MAX_BUFFER_SIZE); + mqttCtx->rx_buf = (byte*)WOLFMQTT_MALLOC(MAX_BUFFER_SIZE); + if (mqttCtx->tx_buf == NULL || mqttCtx->rx_buf == NULL) { + return MQTT_CODE_ERROR_MEMORY; + } + + /* Initialize MQTT client */ + rc = MqttClient_Init(&mqttCtx->client, &mqttCtx->net, + mqtt_message_cb, + mqttCtx->tx_buf, MAX_BUFFER_SIZE, + mqttCtx->rx_buf, MAX_BUFFER_SIZE, + mqttCtx->cmd_timeout_ms); + if (rc != MQTT_CODE_SUCCESS) { + PRINTF("Sparkplug: MQTT init failed: %s (%d)", + MqttClient_ReturnCodeToString(rc), rc); + return rc; + } + + /* Set context pointer for callbacks */ + mqttCtx->client.ctx = spCtx; + + /* Connect socket (loop for non-blocking) */ + do { + rc = MqttClient_NetConnect(&mqttCtx->client, mqttCtx->host, + mqttCtx->port, DEFAULT_CON_TIMEOUT_MS, + mqttCtx->use_tls, mqtt_tls_cb); + } while (rc == MQTT_CODE_CONTINUE); + + if (rc != MQTT_CODE_SUCCESS) { + PRINTF("Sparkplug: Socket connect failed: %s (%d)", + MqttClient_ReturnCodeToString(rc), rc); + return rc; + } + + /* Build MQTT CONNECT packet */ + XMEMSET(&mqttCtx->connect, 0, sizeof(mqttCtx->connect)); + mqttCtx->connect.keep_alive_sec = mqttCtx->keep_alive_sec; + mqttCtx->connect.clean_session = mqttCtx->clean_session; + mqttCtx->connect.client_id = mqttCtx->client_id; + + /* Set Last Will and Testament (NDEATH) for Edge Node */ + if (!spCtx->is_host) { + static byte lwt_payload[128]; + static MqttMessage lwt_msg; + SparkplugPayload lwt_pl; + int lwt_len; + + /* Build NDEATH payload */ + XMEMSET(&lwt_pl, 0, sizeof(lwt_pl)); + lwt_pl.timestamp = SparkplugTimestamp_Get(); + lwt_pl.seq = 0; + lwt_pl.metric_count = 1; + lwt_pl.metrics[0].name = "bdSeq"; + lwt_pl.metrics[0].datatype = SP_DTYPE_UINT64; + lwt_pl.metrics[0].value.uint64_val = spCtx->bdSeq; + lwt_pl.metrics[0].timestamp = lwt_pl.timestamp; + + lwt_len = SparkplugPayload_Encode(&lwt_pl, lwt_payload, sizeof(lwt_payload)); + if (lwt_len > 0) { + /* Build NDEATH topic */ + SparkplugTopic_Build(spCtx->topic_buf, sizeof(spCtx->topic_buf), + spCtx->group_id, SP_MSG_NDEATH, + spCtx->edge_node_id, NULL); + + XMEMSET(&lwt_msg, 0, sizeof(lwt_msg)); + lwt_msg.qos = MQTT_QOS_1; + lwt_msg.retain = 0; + lwt_msg.topic_name = spCtx->topic_buf; + lwt_msg.buffer = lwt_payload; + lwt_msg.total_len = lwt_len; + + mqttCtx->connect.lwt_msg = &lwt_msg; + mqttCtx->connect.enable_lwt = 1; + + PRINTF("Sparkplug: LWT configured on topic: %s", spCtx->topic_buf); + } + } + + /* Send CONNECT (loop for non-blocking and stdin wake) */ + do { + rc = MqttClient_Connect(&mqttCtx->client, &mqttCtx->connect); + } while (rc == MQTT_CODE_CONTINUE || rc == MQTT_CODE_STDIN_WAKE); + + if (rc != MQTT_CODE_SUCCESS) { + PRINTF("Sparkplug: MQTT connect failed: %s (%d)", + MqttClient_ReturnCodeToString(rc), rc); + return rc; + } + + PRINTF("Sparkplug: Connected! (client_id=%s)", mqttCtx->client_id); + return MQTT_CODE_SUCCESS; +} + +/* Publish a Sparkplug message */ +static int sparkplug_publish(SparkplugCtx* spCtx, SparkplugMsgType msg_type, + const char* device_id, SparkplugPayload* payload) +{ + int rc; + MQTTCtx* mqttCtx = &spCtx->mqttCtx; + byte payload_buf[MAX_BUFFER_SIZE]; + int payload_len; + + /* Build topic */ + SparkplugTopic_Build(spCtx->topic_buf, sizeof(spCtx->topic_buf), + spCtx->group_id, msg_type, + spCtx->edge_node_id, device_id); + + /* Set sequence number */ + payload->seq = spCtx->seq; + if (msg_type != SP_MSG_NBIRTH && msg_type != SP_MSG_DBIRTH) { + spCtx->seq = (spCtx->seq + 1) % 256; /* 0-255 per spec */ + } + + /* Encode payload */ + payload_len = SparkplugPayload_Encode(payload, payload_buf, sizeof(payload_buf)); + if (payload_len <= 0) { + PRINTF("Sparkplug: Payload encode failed"); + return MQTT_CODE_ERROR_BAD_ARG; + } + + /* Setup publish */ + XMEMSET(&mqttCtx->publish, 0, sizeof(mqttCtx->publish)); + mqttCtx->publish.retain = 0; + mqttCtx->publish.qos = mqttCtx->qos; + mqttCtx->publish.duplicate = 0; + mqttCtx->publish.topic_name = spCtx->topic_buf; + mqttCtx->publish.packet_id = mqtt_get_packetid(); + mqttCtx->publish.buffer = payload_buf; + mqttCtx->publish.total_len = payload_len; + +#ifdef SPARKPLUG_DEBUG + PRINTF("Sparkplug: Publishing to %s (%d bytes)", spCtx->topic_buf, payload_len); + print_payload(" Payload", payload); +#else + PRINTF("Sparkplug: Published %s to %s", + SparkplugMsgType_ToString(msg_type), spCtx->topic_buf); +#endif + + /* Publish (loop for non-blocking) */ + do { + rc = MqttClient_Publish(&mqttCtx->client, &mqttCtx->publish); + } while (rc == MQTT_CODE_CONTINUE || rc == MQTT_CODE_PUB_CONTINUE); + + if (rc != MQTT_CODE_SUCCESS) { + PRINTF("Sparkplug: Publish failed: %s (%d)", + MqttClient_ReturnCodeToString(rc), rc); + } + + return rc; +} + +/* Subscribe to Sparkplug topics */ +static int sparkplug_subscribe(SparkplugCtx* spCtx, const char* topic_filter) +{ + int rc; + MQTTCtx* mqttCtx = &spCtx->mqttCtx; + static MqttTopic topics[1]; + + XMEMSET(&mqttCtx->subscribe, 0, sizeof(mqttCtx->subscribe)); + mqttCtx->subscribe.packet_id = mqtt_get_packetid(); + mqttCtx->subscribe.topic_count = 1; + topics[0].topic_filter = topic_filter; + topics[0].qos = mqttCtx->qos; + mqttCtx->subscribe.topics = topics; + + PRINTF("Sparkplug: Subscribing to %s", topic_filter); + + /* Subscribe (loop for non-blocking) */ + do { + rc = MqttClient_Subscribe(&mqttCtx->client, &mqttCtx->subscribe); + } while (rc == MQTT_CODE_CONTINUE); + + if (rc != MQTT_CODE_SUCCESS) { + PRINTF("Sparkplug: Subscribe failed: %s (%d)", + MqttClient_ReturnCodeToString(rc), rc); + } + else { + PRINTF("Sparkplug: Subscribed (granted QoS=%d)", + topics[0].return_code); + } + + return rc; +} + +/* Disconnect from broker */ +static int sparkplug_disconnect(SparkplugCtx* spCtx) +{ + int rc; + MQTTCtx* mqttCtx = &spCtx->mqttCtx; + + PRINTF("Sparkplug: Disconnecting %s...", mqttCtx->client_id); + + /* MQTT Disconnect (loop for non-blocking) */ + do { + rc = MqttClient_Disconnect(&mqttCtx->client); + } while (rc == MQTT_CODE_CONTINUE); + + if (rc != MQTT_CODE_SUCCESS) { + PRINTF("Sparkplug: Disconnect failed: %s (%d)", + MqttClient_ReturnCodeToString(rc), rc); + } + + /* Network disconnect */ + MqttClient_NetDisconnect(&mqttCtx->client); + + /* Cleanup */ + MqttClientNet_DeInit(&mqttCtx->net); + + if (mqttCtx->tx_buf) { + WOLFMQTT_FREE(mqttCtx->tx_buf); + mqttCtx->tx_buf = NULL; + } + if (mqttCtx->rx_buf) { + WOLFMQTT_FREE(mqttCtx->rx_buf); + mqttCtx->rx_buf = NULL; + } + + PRINTF("Sparkplug: Disconnected %s", mqttCtx->client_id); + return rc; +} + +/* Message callback */ +static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, + byte msg_new, byte msg_done) +{ + SparkplugCtx* spCtx = (SparkplugCtx*)client->ctx; + SparkplugPayload payload; + SparkplugMsgType msg_type; + char group_id[64], node_id[64], device_id[64]; + int rc; + + if (msg_new) { + /* Parse topic */ + char topic_str[SPARKPLUG_TOPIC_MAX_LEN]; + int topic_len = msg->topic_name_len; + if (topic_len >= (int)sizeof(topic_str)) { + topic_len = sizeof(topic_str) - 1; + } + XMEMCPY(topic_str, msg->topic_name, topic_len); + topic_str[topic_len] = '\0'; + + rc = SparkplugTopic_Parse(topic_str, group_id, sizeof(group_id), + &msg_type, node_id, sizeof(node_id), + device_id, sizeof(device_id)); + + if (rc == MQTT_CODE_SUCCESS) { + PRINTF("Sparkplug [%s]: Received %s from %s/%s%s%s", + spCtx->mqttCtx.client_id, + SparkplugMsgType_ToString(msg_type), + group_id, node_id, + device_id[0] ? "/" : "", device_id); + + /* Decode payload if complete */ + if (msg_done && msg->buffer_len > 0) { + rc = SparkplugPayload_Decode(msg->buffer, msg->buffer_len, &payload); + if (rc > 0) { +#ifdef SPARKPLUG_DEBUG + print_payload(" Received", &payload); +#endif + + /* Handle message based on type and role */ + if (spCtx->is_host) { + /* Host Application message handling */ + switch (msg_type) { + case SP_MSG_NBIRTH: + PRINTF(" -> Edge Node came online (bdSeq=%llu)", + (unsigned long long)payload.metrics[0].value.uint64_val); + break; + case SP_MSG_NDEATH: + PRINTF(" -> Edge Node went offline"); + break; + case SP_MSG_DDATA: + { + int i; + PRINTF(" -> Device data received:"); + for (i = 0; i < payload.metric_count; i++) { + const SparkplugMetric* m = &payload.metrics[i]; + if (m->datatype == SP_DTYPE_FLOAT) { + union { uint32_t u; float f; } conv; + conv.u = m->value.uint32_val; + PRINTF(" %s = %.2f", m->name, conv.f); + } + else if (m->datatype == SP_DTYPE_BOOLEAN) { + PRINTF(" %s = %s", m->name, + m->value.uint8_val ? "ON" : "OFF"); + } + else if (m->datatype == SP_DTYPE_UINT32) { + PRINTF(" %s = %u", m->name, + (unsigned int)m->value.uint32_val); + } + } + } + break; + case SP_MSG_DBIRTH: + case SP_MSG_DDEATH: + case SP_MSG_NDATA: + case SP_MSG_NCMD: + case SP_MSG_DCMD: + case SP_MSG_STATE: + case SP_MSG_COUNT: + /* Not handled by host in this example */ + break; + } + } + else { + /* Edge Node message handling */ + switch (msg_type) { + case SP_MSG_DCMD: + { + int i; + PRINTF(" -> Command received:"); + for (i = 0; i < payload.metric_count; i++) { + const SparkplugMetric* m = &payload.metrics[i]; + if (XSTRCMP(m->name, "LED") == 0 && + m->datatype == SP_DTYPE_BOOLEAN) { + gSensorData.led_state = m->value.uint8_val; + PRINTF(" LED set to %s", + gSensorData.led_state ? "ON" : "OFF"); +#ifdef WOLFMQTT_MULTITHREAD + SPARKPLUG_LOCK(); + gCmdReceived = 1; + SPARKPLUG_UNLOCK(); +#endif + } + } + } + break; + case SP_MSG_NBIRTH: + case SP_MSG_NDEATH: + case SP_MSG_DBIRTH: + case SP_MSG_DDEATH: + case SP_MSG_NDATA: + case SP_MSG_DDATA: + case SP_MSG_NCMD: + case SP_MSG_STATE: + case SP_MSG_COUNT: + /* Not handled by edge node in this example */ + break; + } + } + } + } + } + else { + PRINTF("Sparkplug [%s]: Received non-Sparkplug message on topic: %s", + spCtx->mqttCtx.client_id, topic_str); + } + } + + (void)msg_done; + return MQTT_CODE_SUCCESS; +} + +/* Publish Node Birth Certificate */ +static int edge_publish_nbirth(SparkplugCtx* spCtx) +{ + SparkplugPayload payload; + union { float f; uint32_t u; } conv; + + XMEMSET(&payload, 0, sizeof(payload)); + payload.timestamp = SparkplugTimestamp_Get(); + payload.metric_count = 4; + + /* bdSeq metric (required) */ + payload.metrics[0].name = "bdSeq"; + payload.metrics[0].datatype = SP_DTYPE_UINT64; + payload.metrics[0].value.uint64_val = spCtx->bdSeq; + payload.metrics[0].timestamp = payload.timestamp; + + /* Define available metrics */ + payload.metrics[1].name = "Temperature"; + payload.metrics[1].alias = 1; + payload.metrics[1].datatype = SP_DTYPE_FLOAT; + conv.f = gSensorData.temperature; + payload.metrics[1].value.uint32_val = conv.u; + payload.metrics[1].timestamp = payload.timestamp; + + payload.metrics[2].name = "Humidity"; + payload.metrics[2].alias = 2; + payload.metrics[2].datatype = SP_DTYPE_FLOAT; + conv.f = gSensorData.humidity; + payload.metrics[2].value.uint32_val = conv.u; + payload.metrics[2].timestamp = payload.timestamp; + + payload.metrics[3].name = "LED"; + payload.metrics[3].alias = 3; + payload.metrics[3].datatype = SP_DTYPE_BOOLEAN; + payload.metrics[3].value.uint8_val = (uint8_t)gSensorData.led_state; + payload.metrics[3].timestamp = payload.timestamp; + + spCtx->seq = 0; /* Reset sequence on birth */ + return sparkplug_publish(spCtx, SP_MSG_NBIRTH, NULL, &payload); +} + +/* Publish Device Data */ +static int edge_publish_ddata(SparkplugCtx* spCtx) +{ + SparkplugPayload payload; + union { float f; uint32_t u; } conv; + + /* Simulate sensor changes */ + gSensorData.temperature += ((float)(rand() % 100) - 50) / 100.0f; + gSensorData.humidity += ((float)(rand() % 100) - 50) / 100.0f; + gSensorData.counter++; + + if (gSensorData.humidity < 0) gSensorData.humidity = 0; + if (gSensorData.humidity > 100) gSensorData.humidity = 100; + + XMEMSET(&payload, 0, sizeof(payload)); + payload.timestamp = SparkplugTimestamp_Get(); + payload.metric_count = 4; + + payload.metrics[0].name = "Temperature"; + payload.metrics[0].alias = 1; + payload.metrics[0].datatype = SP_DTYPE_FLOAT; + conv.f = gSensorData.temperature; + payload.metrics[0].value.uint32_val = conv.u; + payload.metrics[0].timestamp = payload.timestamp; + + payload.metrics[1].name = "Humidity"; + payload.metrics[1].alias = 2; + payload.metrics[1].datatype = SP_DTYPE_FLOAT; + conv.f = gSensorData.humidity; + payload.metrics[1].value.uint32_val = conv.u; + payload.metrics[1].timestamp = payload.timestamp; + + payload.metrics[2].name = "LED"; + payload.metrics[2].alias = 3; + payload.metrics[2].datatype = SP_DTYPE_BOOLEAN; + payload.metrics[2].value.uint8_val = (uint8_t)gSensorData.led_state; + payload.metrics[2].timestamp = payload.timestamp; + + payload.metrics[3].name = "Counter"; + payload.metrics[3].alias = 4; + payload.metrics[3].datatype = SP_DTYPE_UINT32; + payload.metrics[3].value.uint32_val = gSensorData.counter; + payload.metrics[3].timestamp = payload.timestamp; + + return sparkplug_publish(spCtx, SP_MSG_DDATA, spCtx->device_id, &payload); +} + +#ifdef WOLFMQTT_MULTITHREAD +/* Host sends command to Edge Node */ +static int host_send_command(SparkplugCtx* spCtx, int led_state) +{ + SparkplugPayload payload; + int rc; + MQTTCtx* mqttCtx = &spCtx->mqttCtx; + byte payload_buf[MAX_BUFFER_SIZE]; + int payload_len; + char topic[SPARKPLUG_TOPIC_MAX_LEN]; + + XMEMSET(&payload, 0, sizeof(payload)); + payload.timestamp = SparkplugTimestamp_Get(); + payload.seq = 0; /* Commands don't use sequence */ + payload.metric_count = 1; + + payload.metrics[0].name = "LED"; + payload.metrics[0].alias = 3; + payload.metrics[0].datatype = SP_DTYPE_BOOLEAN; + payload.metrics[0].value.uint8_val = (uint8_t)led_state; + payload.metrics[0].timestamp = payload.timestamp; + + /* Build DCMD topic (target the edge node's device) */ + SparkplugTopic_Build(topic, sizeof(topic), + spCtx->group_id, SP_MSG_DCMD, + spCtx->edge_node_id, spCtx->device_id); + + /* Encode payload */ + payload_len = SparkplugPayload_Encode(&payload, payload_buf, sizeof(payload_buf)); + if (payload_len <= 0) { + return MQTT_CODE_ERROR_BAD_ARG; + } + + /* Setup publish */ + XMEMSET(&mqttCtx->publish, 0, sizeof(mqttCtx->publish)); + mqttCtx->publish.retain = 0; + mqttCtx->publish.qos = mqttCtx->qos; + mqttCtx->publish.topic_name = topic; + mqttCtx->publish.packet_id = mqtt_get_packetid(); + mqttCtx->publish.buffer = payload_buf; + mqttCtx->publish.total_len = payload_len; + + PRINTF("Sparkplug [Host]: Sending DCMD to %s (LED=%s)", + topic, led_state ? "ON" : "OFF"); + + /* Publish command (loop for non-blocking) */ + do { + rc = MqttClient_Publish(&mqttCtx->client, &mqttCtx->publish); + } while (rc == MQTT_CODE_CONTINUE || rc == MQTT_CODE_PUB_CONTINUE); + + if (rc != MQTT_CODE_SUCCESS) { + PRINTF("Sparkplug: Command publish failed: %s (%d)", + MqttClient_ReturnCodeToString(rc), rc); + } + + return rc; +} +#endif /* WOLFMQTT_MULTITHREAD */ + +/* Edge Node main loop */ +static int edge_node_run(SparkplugCtx* spCtx) +{ + int rc; + int i; +#ifdef WOLFMQTT_MULTITHREAD + int stop; +#endif + + /* Connect to broker */ + rc = sparkplug_connect(spCtx); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + + /* Subscribe to commands */ + { + char cmd_topic[SPARKPLUG_TOPIC_MAX_LEN]; + XSNPRINTF(cmd_topic, sizeof(cmd_topic), "%s/%s/DCMD/%s/#", + SPARKPLUG_NAMESPACE, spCtx->group_id, spCtx->edge_node_id); + rc = sparkplug_subscribe(spCtx, cmd_topic); + if (rc != MQTT_CODE_SUCCESS) { + sparkplug_disconnect(spCtx); + return rc; + } + } + + /* Publish NBIRTH */ + rc = edge_publish_nbirth(spCtx); + if (rc != MQTT_CODE_SUCCESS) { + sparkplug_disconnect(spCtx); + return rc; + } + +#ifdef WOLFMQTT_MULTITHREAD + /* Signal that edge node is ready */ + SPARKPLUG_LOCK(); + gEdgeReady = 1; + SPARKPLUG_UNLOCK(); + + /* Wait for host to be ready */ + do { + SLEEP(100); + SPARKPLUG_LOCK(); + stop = gHostReady; + SPARKPLUG_UNLOCK(); + } while (!stop); +#endif + + /* Publish sensor data periodically */ + for (i = 0; i < NUM_DATA_PUBLISHES; i++) { +#ifdef WOLFMQTT_MULTITHREAD + SPARKPLUG_LOCK(); + stop = gStopFlag; + SPARKPLUG_UNLOCK(); + if (stop) break; +#endif + + /* Publish device data */ + rc = edge_publish_ddata(spCtx); + if (rc != MQTT_CODE_SUCCESS) { + break; + } + + /* Wait and check for incoming commands */ + rc = MqttClient_WaitMessage(&spCtx->mqttCtx.client, + DATA_PUBLISH_INTERVAL * 1000); + if (rc == MQTT_CODE_ERROR_TIMEOUT) { + rc = MQTT_CODE_SUCCESS; /* Timeout is OK */ + } + else if (rc != MQTT_CODE_SUCCESS) { + PRINTF("Sparkplug [Edge]: WaitMessage error: %s (%d)", + MqttClient_ReturnCodeToString(rc), rc); + break; + } + } + + /* Disconnect */ + sparkplug_disconnect(spCtx); + return rc; +} + +#ifdef WOLFMQTT_MULTITHREAD +/* Host Application main loop */ +static int host_app_run(SparkplugCtx* spCtx) +{ + int rc; + int msg_count = 0; + int cmd_sent = 0; +#ifdef WOLFMQTT_MULTITHREAD + int stop; +#endif + + /* Connect to broker */ + rc = sparkplug_connect(spCtx); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + + /* Subscribe to all Sparkplug messages in our group */ + { + char sub_topic[SPARKPLUG_TOPIC_MAX_LEN]; + XSNPRINTF(sub_topic, sizeof(sub_topic), "%s/%s/#", + SPARKPLUG_NAMESPACE, spCtx->group_id); + rc = sparkplug_subscribe(spCtx, sub_topic); + if (rc != MQTT_CODE_SUCCESS) { + sparkplug_disconnect(spCtx); + return rc; + } + } + +#ifdef WOLFMQTT_MULTITHREAD + /* Signal that host is ready */ + SPARKPLUG_LOCK(); + gHostReady = 1; + SPARKPLUG_UNLOCK(); + + /* Wait for edge node to be ready */ + do { + SLEEP(100); + SPARKPLUG_LOCK(); + stop = gEdgeReady; + SPARKPLUG_UNLOCK(); + } while (!stop); +#endif + + /* Wait for messages from Edge Node */ + while (msg_count < (NUM_DATA_PUBLISHES + 2)) { /* +2 for NBIRTH and extra */ +#ifdef WOLFMQTT_MULTITHREAD + SPARKPLUG_LOCK(); + stop = gStopFlag; + SPARKPLUG_UNLOCK(); + if (stop) break; +#endif + + rc = MqttClient_WaitMessage(&spCtx->mqttCtx.client, 2000); + if (rc == MQTT_CODE_ERROR_TIMEOUT) { + rc = MQTT_CODE_SUCCESS; + msg_count++; /* Count timeouts to eventually exit */ + } + else if (rc == MQTT_CODE_SUCCESS) { + msg_count++; + + /* Send command after receiving some data */ + if (!cmd_sent && msg_count >= 2) { + PRINTF("Sparkplug [Host]: Sending command to toggle LED ON"); + host_send_command(spCtx, 1); /* Turn LED ON */ + cmd_sent = 1; + } + } + else { + PRINTF("Sparkplug [Host]: WaitMessage error: %s (%d)", + MqttClient_ReturnCodeToString(rc), rc); + break; + } + } + +#ifdef WOLFMQTT_MULTITHREAD + /* Signal stop */ + SPARKPLUG_LOCK(); + gStopFlag = 1; + SPARKPLUG_UNLOCK(); +#endif + + /* Disconnect */ + sparkplug_disconnect(spCtx); + return rc; +} +#endif /* WOLFMQTT_MULTITHREAD */ + +#ifdef WOLFMQTT_MULTITHREAD +/* Thread functions */ +static THREAD_RET_T edge_node_thread(void* arg) +{ + SparkplugCtx* spCtx = (SparkplugCtx*)arg; + edge_node_run(spCtx); + THREAD_EXIT(THREAD_RET_SUCCESS); + return THREAD_RET_SUCCESS; +} + +static THREAD_RET_T host_app_thread(void* arg) +{ + SparkplugCtx* spCtx = (SparkplugCtx*)arg; + host_app_run(spCtx); + THREAD_EXIT(THREAD_RET_SUCCESS); + return THREAD_RET_SUCCESS; +} +#endif + +/* Main test function */ +int sparkplug_test(MQTTCtx *mqttCtx) +{ + int rc = 0; + SparkplugCtx edgeCtx, hostCtx; + + PRINTF("Sparkplug B Example"); + PRINTF("==================="); + PRINTF("This example demonstrates two MQTT clients communicating"); + PRINTF("using the Sparkplug B industrial IoT protocol.\n"); + +#ifdef WOLFMQTT_MULTITHREAD + { + THREAD_T edge_thread, host_thread; + + /* Initialize synchronization */ + rc = wm_SemInit(&gSparkplugLock); + if (rc != 0) { + PRINTF("Failed to initialize semaphore"); + return rc; + } + + gStopFlag = 0; + gHostReady = 0; + gEdgeReady = 0; + gCmdReceived = 0; + + /* Initialize contexts */ + rc = sparkplug_init(&edgeCtx, "WolfMQTT_Sparkplug_Edge", 0); + if (rc != MQTT_CODE_SUCCESS) { + wm_SemFree(&gSparkplugLock); + return rc; + } + + rc = sparkplug_init(&hostCtx, "WolfMQTT_Sparkplug_Host", 1); + if (rc != MQTT_CODE_SUCCESS) { + wm_SemFree(&gSparkplugLock); + return rc; + } + + /* Copy host/port from provided context */ + if (mqttCtx != NULL) { + edgeCtx.mqttCtx.host = mqttCtx->host; + edgeCtx.mqttCtx.port = mqttCtx->port; + edgeCtx.mqttCtx.use_tls = mqttCtx->use_tls; + hostCtx.mqttCtx.host = mqttCtx->host; + hostCtx.mqttCtx.port = mqttCtx->port; + hostCtx.mqttCtx.use_tls = mqttCtx->use_tls; + } + + PRINTF("Starting Edge Node and Host Application threads...\n"); + + /* Start threads */ + if (THREAD_CREATE(&edge_thread, edge_node_thread, &edgeCtx) != 0) { + PRINTF("Failed to create edge node thread"); + wm_SemFree(&gSparkplugLock); + return -1; + } + + if (THREAD_CREATE(&host_thread, host_app_thread, &hostCtx) != 0) { + PRINTF("Failed to create host app thread"); + SPARKPLUG_LOCK(); + gStopFlag = 1; + SPARKPLUG_UNLOCK(); + THREAD_JOIN(edge_thread); + wm_SemFree(&gSparkplugLock); + return -1; + } + + /* Wait for threads to complete */ + THREAD_JOIN(edge_thread); + THREAD_JOIN(host_thread); + + wm_SemFree(&gSparkplugLock); + + PRINTF("\nSparkplug example completed!"); + } +#else + /* Single-threaded: run Edge Node first, then Host */ + PRINTF("Note: Running in single-threaded mode."); + PRINTF("For full two-client demo, enable WOLFMQTT_MULTITHREAD.\n"); + + rc = sparkplug_init(&edgeCtx, "WolfMQTT_Sparkplug_Edge", 0); + if (rc != MQTT_CODE_SUCCESS) { + return rc; + } + + /* Copy host/port from provided context */ + if (mqttCtx != NULL) { + edgeCtx.mqttCtx.host = mqttCtx->host; + edgeCtx.mqttCtx.port = mqttCtx->port; + edgeCtx.mqttCtx.use_tls = mqttCtx->use_tls; + } + + PRINTF("Running Edge Node...\n"); + rc = edge_node_run(&edgeCtx); + + PRINTF("\nSparkplug example completed (single-threaded mode)!"); + (void)hostCtx; +#endif + + (void)mqttCtx; + return rc; +} + +#if defined(NO_MAIN_DRIVER) +int sparkplug_main(int argc, char** argv) +#else +int main(int argc, char** argv) +#endif +{ + int rc; + MQTTCtx mqttCtx; + + /* Initialize context with defaults */ + mqtt_init_ctx(&mqttCtx); + + /* Parse command line arguments */ + mqttCtx.app_name = "sparkplug"; + rc = mqtt_parse_args(&mqttCtx, argc, argv); + if (rc != 0) { + return rc; + } + + rc = sparkplug_test(&mqttCtx); + + mqtt_free_ctx(&mqttCtx); + + return (rc == 0) ? 0 : EXIT_FAILURE; +} diff --git a/examples/sparkplug/sparkplug.h b/examples/sparkplug/sparkplug.h new file mode 100644 index 00000000..f354bb72 --- /dev/null +++ b/examples/sparkplug/sparkplug.h @@ -0,0 +1,542 @@ +/* sparkplug.h + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfMQTT. + * + * wolfMQTT is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfMQTT is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* Sparkplug B Example + * + * This example demonstrates the Sparkplug B specification using wolfMQTT. + * It creates two MQTT clients: + * 1. Edge Node - Publishes sensor data and responds to commands + * 2. Host Application - Subscribes to data and sends commands + * + * Sparkplug Topic Namespace: + * spBv1.0/{group_id}/{message_type}/{edge_node_id}[/{device_id}] + * + * Message Types: + * NBIRTH - Node Birth Certificate + * NDEATH - Node Death Certificate + * DBIRTH - Device Birth Certificate + * DDEATH - Device Death Certificate + * NDATA - Node Data + * DDATA - Device Data + * NCMD - Node Command + * DCMD - Device Command + * STATE - SCADA Host Application State + * + * Note: This example uses a simplified payload format instead of full + * Protocol Buffers to keep dependencies minimal. In production, use + * the official Sparkplug B protobuf definitions. + */ + +#ifndef WOLFMQTT_SPARKPLUG_H +#define WOLFMQTT_SPARKPLUG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "examples/mqttexample.h" + +/* Standard integer types for Sparkplug protocol */ +#include +#include /* for rand() */ + +/* Define word64 if not already defined (wolfMQTT only defines byte/word16/word32) */ +#ifndef word64 + typedef uint64_t word64; +#endif + +/* Time support for timestamps */ +#ifdef USE_WINDOWS_API + /* Windows defines FILETIME */ +#else + #include +#endif + +/* XSTRCMP and XSSCANF may not be defined by mqtt_types.h */ +#ifndef XSTRCMP + #define XSTRCMP(s1, s2) strcmp((s1), (s2)) +#endif +#ifndef XSSCANF + #define XSSCANF sscanf +#endif + +/* Sparkplug configuration */ +#define SPARKPLUG_NAMESPACE "spBv1.0" +#define SPARKPLUG_GROUP_ID "WolfMQTT" +#define SPARKPLUG_EDGE_NODE_ID "EdgeNode1" +#define SPARKPLUG_DEVICE_ID "Device1" +#define SPARKPLUG_HOST_ID "HostApp1" + +/* Topic buffer size */ +#define SPARKPLUG_TOPIC_MAX_LEN 256 + +/* Sparkplug message types */ +typedef enum SparkplugMsgType { + SP_MSG_NBIRTH = 0, /* Node Birth Certificate */ + SP_MSG_NDEATH, /* Node Death Certificate */ + SP_MSG_DBIRTH, /* Device Birth Certificate */ + SP_MSG_DDEATH, /* Device Death Certificate */ + SP_MSG_NDATA, /* Node Data */ + SP_MSG_DDATA, /* Device Data */ + SP_MSG_NCMD, /* Node Command */ + SP_MSG_DCMD, /* Device Command */ + SP_MSG_STATE, /* SCADA Host State */ + SP_MSG_COUNT +} SparkplugMsgType; + +/* Sparkplug metric data types */ +typedef enum SparkplugDataType { + SP_DTYPE_INT8 = 1, + SP_DTYPE_INT16, + SP_DTYPE_INT32, + SP_DTYPE_INT64, + SP_DTYPE_UINT8, + SP_DTYPE_UINT16, + SP_DTYPE_UINT32, + SP_DTYPE_UINT64, + SP_DTYPE_FLOAT, + SP_DTYPE_DOUBLE, + SP_DTYPE_BOOLEAN, + SP_DTYPE_STRING, + SP_DTYPE_BYTES +} SparkplugDataType; + +/* Metric value union */ +typedef union SparkplugValue { + int8_t int8_val; + int16_t int16_val; + int32_t int32_val; + int64_t int64_val; + uint8_t uint8_val; + uint16_t uint16_val; + uint32_t uint32_val; + uint64_t uint64_val; + float float_val; + double double_val; + int bool_val; + char* str_val; + struct { + byte* data; + word32 len; + } bytes_val; +} SparkplugValue; + +/* Sparkplug metric */ +typedef struct SparkplugMetric { + const char* name; /* Metric name */ + word64 alias; /* Metric alias (optional) */ + word64 timestamp; /* Metric timestamp (ms since epoch) */ + SparkplugDataType datatype; /* Data type */ + SparkplugValue value; /* Metric value */ + int is_null; /* True if value is null */ +} SparkplugMetric; + +/* Sparkplug payload (simplified) */ +#define SPARKPLUG_MAX_METRICS 16 +typedef struct SparkplugPayload { + word64 timestamp; /* Payload timestamp */ + word64 seq; /* Sequence number (0-255) */ + SparkplugMetric metrics[SPARKPLUG_MAX_METRICS]; + int metric_count; +} SparkplugPayload; + +/* Sparkplug client context extension */ +typedef struct SparkplugCtx { + MQTTCtx mqttCtx; /* Base MQTT context */ + const char* group_id; /* Group ID */ + const char* edge_node_id; /* Edge Node ID */ + const char* device_id; /* Device ID (NULL for node-level) */ + word64 bdSeq; /* Birth/Death sequence number */ + word64 seq; /* Message sequence number */ + int is_host; /* True if this is a host application */ + char topic_buf[SPARKPLUG_TOPIC_MAX_LEN]; +} SparkplugCtx; + +/* Message type string names */ +static const char* sparkplug_msg_type_str[] = { + "NBIRTH", "NDEATH", "DBIRTH", "DDEATH", + "NDATA", "DDATA", "NCMD", "DCMD", "STATE" +}; + +/* Get message type string */ +static INLINE const char* SparkplugMsgType_ToString(SparkplugMsgType type) { + if (type < SP_MSG_COUNT) { + return sparkplug_msg_type_str[type]; + } + return "UNKNOWN"; +} + +/* Build Sparkplug topic string */ +static INLINE int SparkplugTopic_Build(char* buf, int buf_len, + const char* group_id, SparkplugMsgType msg_type, + const char* edge_node_id, const char* device_id) +{ + int len; + const char* type_str = SparkplugMsgType_ToString(msg_type); + + if (device_id != NULL) { + len = XSNPRINTF(buf, buf_len, "%s/%s/%s/%s/%s", + SPARKPLUG_NAMESPACE, group_id, type_str, edge_node_id, device_id); + } + else { + len = XSNPRINTF(buf, buf_len, "%s/%s/%s/%s", + SPARKPLUG_NAMESPACE, group_id, type_str, edge_node_id); + } + + return len; +} + +/* Parse Sparkplug topic to extract components */ +static INLINE int SparkplugTopic_Parse(const char* topic, + char* group_id, int group_len, + SparkplugMsgType* msg_type, + char* edge_node_id, int node_len, + char* device_id, int device_len) +{ + char namespace_buf[16]; + char type_buf[16]; + int i, matched; + + /* Initialize outputs */ + if (group_id) group_id[0] = '\0'; + if (edge_node_id) edge_node_id[0] = '\0'; + if (device_id) device_id[0] = '\0'; + + /* Parse topic: spBv1.0/group/type/node[/device] */ + matched = XSSCANF(topic, "%15[^/]/%63[^/]/%15[^/]/%63[^/]/%63s", + namespace_buf, group_id, type_buf, edge_node_id, device_id); + + if (matched < 4) { + return MQTT_CODE_ERROR_BAD_ARG; + } + + /* Verify namespace */ + if (XSTRCMP(namespace_buf, SPARKPLUG_NAMESPACE) != 0) { + return MQTT_CODE_ERROR_BAD_ARG; + } + + /* Parse message type */ + if (msg_type) { + *msg_type = SP_MSG_COUNT; /* Invalid default */ + for (i = 0; i < SP_MSG_COUNT; i++) { + if (XSTRCMP(type_buf, sparkplug_msg_type_str[i]) == 0) { + *msg_type = (SparkplugMsgType)i; + break; + } + } + } + + (void)group_len; + (void)node_len; + (void)device_len; + + return MQTT_CODE_SUCCESS; +} + +/* Encode a simple payload (simplified format, not protobuf) */ +/* Format: [timestamp:8][seq:8][count:4][metrics...] */ +/* Metric: [name_len:2][name][alias:8][ts:8][type:1][value] */ +static INLINE int SparkplugPayload_Encode(const SparkplugPayload* payload, + byte* buf, int buf_len) +{ + int i, pos = 0; + word16 name_len; + + if (buf_len < 20) { + return MQTT_CODE_ERROR_BAD_ARG; + } + + /* Timestamp (8 bytes, big-endian) */ + buf[pos++] = (byte)(payload->timestamp >> 56); + buf[pos++] = (byte)(payload->timestamp >> 48); + buf[pos++] = (byte)(payload->timestamp >> 40); + buf[pos++] = (byte)(payload->timestamp >> 32); + buf[pos++] = (byte)(payload->timestamp >> 24); + buf[pos++] = (byte)(payload->timestamp >> 16); + buf[pos++] = (byte)(payload->timestamp >> 8); + buf[pos++] = (byte)(payload->timestamp); + + /* Sequence (8 bytes) */ + buf[pos++] = (byte)(payload->seq >> 56); + buf[pos++] = (byte)(payload->seq >> 48); + buf[pos++] = (byte)(payload->seq >> 40); + buf[pos++] = (byte)(payload->seq >> 32); + buf[pos++] = (byte)(payload->seq >> 24); + buf[pos++] = (byte)(payload->seq >> 16); + buf[pos++] = (byte)(payload->seq >> 8); + buf[pos++] = (byte)(payload->seq); + + /* Metric count (4 bytes) */ + buf[pos++] = (byte)(payload->metric_count >> 24); + buf[pos++] = (byte)(payload->metric_count >> 16); + buf[pos++] = (byte)(payload->metric_count >> 8); + buf[pos++] = (byte)(payload->metric_count); + + /* Encode each metric */ + for (i = 0; i < payload->metric_count; i++) { + const SparkplugMetric* m = &payload->metrics[i]; + + /* Name length and name */ + name_len = (word16)XSTRLEN(m->name); + if (pos + 2 + name_len + 17 > buf_len) { + return MQTT_CODE_ERROR_OUT_OF_BUFFER; + } + buf[pos++] = (byte)(name_len >> 8); + buf[pos++] = (byte)(name_len); + XMEMCPY(&buf[pos], m->name, name_len); + pos += name_len; + + /* Alias (8 bytes) */ + buf[pos++] = (byte)(m->alias >> 56); + buf[pos++] = (byte)(m->alias >> 48); + buf[pos++] = (byte)(m->alias >> 40); + buf[pos++] = (byte)(m->alias >> 32); + buf[pos++] = (byte)(m->alias >> 24); + buf[pos++] = (byte)(m->alias >> 16); + buf[pos++] = (byte)(m->alias >> 8); + buf[pos++] = (byte)(m->alias); + + /* Timestamp (8 bytes) */ + buf[pos++] = (byte)(m->timestamp >> 56); + buf[pos++] = (byte)(m->timestamp >> 48); + buf[pos++] = (byte)(m->timestamp >> 40); + buf[pos++] = (byte)(m->timestamp >> 32); + buf[pos++] = (byte)(m->timestamp >> 24); + buf[pos++] = (byte)(m->timestamp >> 16); + buf[pos++] = (byte)(m->timestamp >> 8); + buf[pos++] = (byte)(m->timestamp); + + /* Data type (1 byte) */ + buf[pos++] = (byte)m->datatype; + + /* Value based on type */ + switch (m->datatype) { + case SP_DTYPE_BOOLEAN: + case SP_DTYPE_INT8: + case SP_DTYPE_UINT8: + if (pos + 1 > buf_len) return MQTT_CODE_ERROR_OUT_OF_BUFFER; + buf[pos++] = (byte)m->value.uint8_val; + break; + case SP_DTYPE_INT16: + case SP_DTYPE_UINT16: + if (pos + 2 > buf_len) return MQTT_CODE_ERROR_OUT_OF_BUFFER; + buf[pos++] = (byte)(m->value.uint16_val >> 8); + buf[pos++] = (byte)(m->value.uint16_val); + break; + case SP_DTYPE_INT32: + case SP_DTYPE_UINT32: + case SP_DTYPE_FLOAT: + if (pos + 4 > buf_len) return MQTT_CODE_ERROR_OUT_OF_BUFFER; + buf[pos++] = (byte)(m->value.uint32_val >> 24); + buf[pos++] = (byte)(m->value.uint32_val >> 16); + buf[pos++] = (byte)(m->value.uint32_val >> 8); + buf[pos++] = (byte)(m->value.uint32_val); + break; + case SP_DTYPE_INT64: + case SP_DTYPE_UINT64: + case SP_DTYPE_DOUBLE: + if (pos + 8 > buf_len) return MQTT_CODE_ERROR_OUT_OF_BUFFER; + buf[pos++] = (byte)(m->value.uint64_val >> 56); + buf[pos++] = (byte)(m->value.uint64_val >> 48); + buf[pos++] = (byte)(m->value.uint64_val >> 40); + buf[pos++] = (byte)(m->value.uint64_val >> 32); + buf[pos++] = (byte)(m->value.uint64_val >> 24); + buf[pos++] = (byte)(m->value.uint64_val >> 16); + buf[pos++] = (byte)(m->value.uint64_val >> 8); + buf[pos++] = (byte)(m->value.uint64_val); + break; + case SP_DTYPE_STRING: + { + word16 str_len = (word16)XSTRLEN(m->value.str_val); + if (pos + 2 + str_len > buf_len) { + return MQTT_CODE_ERROR_OUT_OF_BUFFER; + } + buf[pos++] = (byte)(str_len >> 8); + buf[pos++] = (byte)(str_len); + XMEMCPY(&buf[pos], m->value.str_val, str_len); + pos += str_len; + } + break; + case SP_DTYPE_BYTES: + /* Bytes encoding not implemented in this example */ + break; + } + } + + return pos; +} + +/* Decode a simple payload */ +static INLINE int SparkplugPayload_Decode(const byte* buf, int buf_len, + SparkplugPayload* payload) +{ + int i, pos = 0; + word16 name_len; + static char name_bufs[SPARKPLUG_MAX_METRICS][64]; + static char str_bufs[SPARKPLUG_MAX_METRICS][128]; + + if (buf_len < 20) { + return MQTT_CODE_ERROR_BAD_ARG; + } + + XMEMSET(payload, 0, sizeof(*payload)); + + /* Timestamp */ + payload->timestamp = ((word64)buf[pos] << 56) | ((word64)buf[pos+1] << 48) | + ((word64)buf[pos+2] << 40) | ((word64)buf[pos+3] << 32) | + ((word64)buf[pos+4] << 24) | ((word64)buf[pos+5] << 16) | + ((word64)buf[pos+6] << 8) | (word64)buf[pos+7]; + pos += 8; + + /* Sequence */ + payload->seq = ((word64)buf[pos] << 56) | ((word64)buf[pos+1] << 48) | + ((word64)buf[pos+2] << 40) | ((word64)buf[pos+3] << 32) | + ((word64)buf[pos+4] << 24) | ((word64)buf[pos+5] << 16) | + ((word64)buf[pos+6] << 8) | (word64)buf[pos+7]; + pos += 8; + + /* Metric count */ + payload->metric_count = ((int)buf[pos] << 24) | ((int)buf[pos+1] << 16) | + ((int)buf[pos+2] << 8) | (int)buf[pos+3]; + pos += 4; + + if (payload->metric_count > SPARKPLUG_MAX_METRICS) { + payload->metric_count = SPARKPLUG_MAX_METRICS; + } + + /* Decode each metric */ + for (i = 0; i < payload->metric_count && pos < buf_len; i++) { + SparkplugMetric* m = &payload->metrics[i]; + + /* Name length and name */ + if (pos + 2 > buf_len) break; + name_len = ((word16)buf[pos] << 8) | buf[pos+1]; + pos += 2; + if (pos + name_len > buf_len || name_len >= sizeof(name_bufs[0])) break; + XMEMCPY(name_bufs[i], &buf[pos], name_len); + name_bufs[i][name_len] = '\0'; + m->name = name_bufs[i]; + pos += name_len; + + /* Alias */ + if (pos + 8 > buf_len) break; + m->alias = ((word64)buf[pos] << 56) | ((word64)buf[pos+1] << 48) | + ((word64)buf[pos+2] << 40) | ((word64)buf[pos+3] << 32) | + ((word64)buf[pos+4] << 24) | ((word64)buf[pos+5] << 16) | + ((word64)buf[pos+6] << 8) | (word64)buf[pos+7]; + pos += 8; + + /* Timestamp */ + if (pos + 8 > buf_len) break; + m->timestamp = ((word64)buf[pos] << 56) | ((word64)buf[pos+1] << 48) | + ((word64)buf[pos+2] << 40) | ((word64)buf[pos+3] << 32) | + ((word64)buf[pos+4] << 24) | ((word64)buf[pos+5] << 16) | + ((word64)buf[pos+6] << 8) | (word64)buf[pos+7]; + pos += 8; + + /* Data type */ + if (pos + 1 > buf_len) break; + m->datatype = (SparkplugDataType)buf[pos++]; + + /* Value based on type */ + switch (m->datatype) { + case SP_DTYPE_BOOLEAN: + case SP_DTYPE_INT8: + case SP_DTYPE_UINT8: + if (pos + 1 > buf_len) break; + m->value.uint8_val = buf[pos++]; + break; + case SP_DTYPE_INT16: + case SP_DTYPE_UINT16: + if (pos + 2 > buf_len) break; + m->value.uint16_val = ((word16)buf[pos] << 8) | buf[pos+1]; + pos += 2; + break; + case SP_DTYPE_INT32: + case SP_DTYPE_UINT32: + case SP_DTYPE_FLOAT: + if (pos + 4 > buf_len) break; + m->value.uint32_val = ((word32)buf[pos] << 24) | ((word32)buf[pos+1] << 16) | + ((word32)buf[pos+2] << 8) | buf[pos+3]; + pos += 4; + break; + case SP_DTYPE_INT64: + case SP_DTYPE_UINT64: + case SP_DTYPE_DOUBLE: + if (pos + 8 > buf_len) break; + m->value.uint64_val = ((word64)buf[pos] << 56) | ((word64)buf[pos+1] << 48) | + ((word64)buf[pos+2] << 40) | ((word64)buf[pos+3] << 32) | + ((word64)buf[pos+4] << 24) | ((word64)buf[pos+5] << 16) | + ((word64)buf[pos+6] << 8) | (word64)buf[pos+7]; + pos += 8; + break; + case SP_DTYPE_STRING: + { + word16 str_len; + if (pos + 2 > buf_len) break; + str_len = ((word16)buf[pos] << 8) | buf[pos+1]; + pos += 2; + if (pos + str_len > buf_len || str_len >= sizeof(str_bufs[0])) break; + XMEMCPY(str_bufs[i], &buf[pos], str_len); + str_bufs[i][str_len] = '\0'; + m->value.str_val = str_bufs[i]; + pos += str_len; + } + break; + case SP_DTYPE_BYTES: + /* Unsupported type */ + break; + } + } + + return pos; +} + +/* Get current timestamp in milliseconds */ +static INLINE word64 SparkplugTimestamp_Get(void) +{ +#if defined(_WIN32) + FILETIME ft; + ULARGE_INTEGER uli; + GetSystemTimeAsFileTime(&ft); + uli.LowPart = ft.dwLowDateTime; + uli.HighPart = ft.dwHighDateTime; + /* Convert from 100-nanosecond intervals since 1601 to ms since 1970 */ + return (uli.QuadPart - 116444736000000000ULL) / 10000; +#else + struct timeval tv; + gettimeofday(&tv, NULL); + return (word64)tv.tv_sec * 1000 + (word64)tv.tv_usec / 1000; +#endif +} + +/* Exposed functions */ +int sparkplug_test(MQTTCtx *mqttCtx); + +#if defined(NO_MAIN_DRIVER) +int sparkplug_main(int argc, char** argv); +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* WOLFMQTT_SPARKPLUG_H */ From 7cb54cb81db07c2e65b0d237ab1738dbfc1c2c3b Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Thu, 5 Feb 2026 15:28:14 -0600 Subject: [PATCH 2/5] Add Sparkplug example documentation README covers: - Protocol overview and architecture - Message types and simulated metrics - Build instructions (Autotools and CMake) - Command-line options and example output - Configuration and payload format notes Co-Authored-By: Claude Opus 4.5 --- examples/sparkplug/README.md | 195 +++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 examples/sparkplug/README.md diff --git a/examples/sparkplug/README.md b/examples/sparkplug/README.md new file mode 100644 index 00000000..4e3b8b7e --- /dev/null +++ b/examples/sparkplug/README.md @@ -0,0 +1,195 @@ +# Sparkplug B Example + +This example demonstrates the [Sparkplug B](https://sparkplug.eclipse.org/) industrial IoT protocol specification using wolfMQTT. It creates two MQTT clients that communicate using the Sparkplug topic namespace and message types. + +## Overview + +Sparkplug B is an open-source specification designed for industrial IoT and SCADA systems. It defines: + +- **Topic Namespace**: `spBv1.0/{group_id}/{message_type}/{edge_node_id}[/{device_id}]` +- **Message Types**: Birth/Death certificates, Data, and Commands +- **Payload Format**: Google Protocol Buffers (this example uses a simplified encoding) + +## Architecture + +The example implements two clients: + +### Edge Node (Publisher) +- Represents an industrial device or gateway +- Publishes **NBIRTH** (Node Birth Certificate) on startup +- Publishes **DDATA** (Device Data) with sensor metrics periodically +- Subscribes to **DCMD** (Device Command) topics to receive commands +- Configures **NDEATH** (Node Death Certificate) as Last Will and Testament + +### Host Application (Subscriber) +- Represents a SCADA or supervisory system +- Subscribes to all Sparkplug messages in the group (`spBv1.0/{group}/#`) +- Receives and processes birth certificates and device data +- Sends **DCMD** (Device Command) messages to control devices + +## Message Types + +| Type | Description | +|------|-------------| +| NBIRTH | Node Birth Certificate - Edge node announces itself | +| NDEATH | Node Death Certificate - Edge node goes offline (LWT) | +| DBIRTH | Device Birth Certificate - Device announces itself | +| DDEATH | Device Death Certificate - Device goes offline | +| NDATA | Node Data - Metrics from the edge node | +| DDATA | Device Data - Metrics from a device | +| NCMD | Node Command - Command to the edge node | +| DCMD | Device Command - Command to a specific device | +| STATE | Host Application state | + +## Simulated Metrics + +The Edge Node simulates the following sensor data: + +| Metric | Type | Description | +|--------|------|-------------| +| Temperature | Float | Simulated temperature in Celsius | +| Humidity | Float | Simulated humidity percentage | +| LED | Boolean | LED on/off state (controllable via command) | +| Counter | UInt32 | Message counter | + +## Building + +### Autotools + +```bash +# Single-threaded (Edge Node only) +./configure --disable-tls +make + +# Multi-threaded (both clients communicate) +./configure --enable-mt --disable-tls +make +``` + +### CMake + +```bash +mkdir build && cd build + +# Single-threaded +cmake -DWOLFMQTT_TLS=no .. +make sparkplug + +# Multi-threaded +cmake -DWOLFMQTT_TLS=no -DWOLFMQTT_MT=yes .. +make sparkplug +``` + +## Running + +```bash +# Using default broker (test.mosquitto.org) +./examples/sparkplug/sparkplug + +# Specify broker +./examples/sparkplug/sparkplug -h -p + +# With TLS (if built with TLS support) +./examples/sparkplug/sparkplug -h -p 8883 -t +``` + +### Command-Line Options + +| Option | Description | +|--------|-------------| +| `-h ` | MQTT broker hostname (default: test.mosquitto.org) | +| `-p ` | MQTT broker port (default: 1883) | +| `-t` | Enable TLS | +| `-c ` | TLS CA certificate file | +| `-q ` | QoS level (0, 1, or 2) | +| `-C` | Clean session | + +## Example Output + +``` +Sparkplug B Example +=================== +This example demonstrates two MQTT clients communicating +using the Sparkplug B industrial IoT protocol. + +Starting Edge Node and Host Application threads... + +Sparkplug: Connecting WolfMQTT_Sparkplug_Edge to broker test.mosquitto.org:1883... +Sparkplug: Connected! (client_id=WolfMQTT_Sparkplug_Edge) +Sparkplug: Subscribing to spBv1.0/WolfMQTT/DCMD/EdgeNode1/# +Sparkplug: Subscribed (granted QoS=1) +Sparkplug: Published NBIRTH to spBv1.0/WolfMQTT/NBIRTH/EdgeNode1 + +Sparkplug: Connecting WolfMQTT_Sparkplug_Host to broker test.mosquitto.org:1883... +Sparkplug: Connected! (client_id=WolfMQTT_Sparkplug_Host) +Sparkplug: Subscribing to spBv1.0/WolfMQTT/# +Sparkplug: Subscribed (granted QoS=1) + +Sparkplug [WolfMQTT_Sparkplug_Host]: Received NBIRTH from WolfMQTT/EdgeNode1 + -> Edge Node came online (bdSeq=0) + +Sparkplug: Published DDATA to spBv1.0/WolfMQTT/DDATA/EdgeNode1/Device1 +Sparkplug [WolfMQTT_Sparkplug_Host]: Received DDATA from WolfMQTT/EdgeNode1/Device1 + -> Device data received: + Temperature = 22.83 + Humidity = 45.36 + LED = OFF + Counter = 1 + +Sparkplug [Host]: Sending command to toggle LED ON +Sparkplug [Host]: Sending DCMD to spBv1.0/WolfMQTT/DCMD/EdgeNode1/Device1 (LED=ON) + +Sparkplug [WolfMQTT_Sparkplug_Edge]: Received DCMD from WolfMQTT/EdgeNode1/Device1 + -> Command received: + LED set to ON + +Sparkplug: Published DDATA to spBv1.0/WolfMQTT/DDATA/EdgeNode1/Device1 +Sparkplug [WolfMQTT_Sparkplug_Host]: Received DDATA from WolfMQTT/EdgeNode1/Device1 + -> Device data received: + Temperature = 23.10 + Humidity = 45.01 + LED = ON + Counter = 2 + +Sparkplug: Disconnecting WolfMQTT_Sparkplug_Host... +Sparkplug: Disconnected WolfMQTT_Sparkplug_Host +Sparkplug: Disconnecting WolfMQTT_Sparkplug_Edge... +Sparkplug: Disconnected WolfMQTT_Sparkplug_Edge + +Sparkplug example completed! +``` + +## Configuration + +The following constants can be modified in `sparkplug.h`: + +```c +#define SPARKPLUG_NAMESPACE "spBv1.0" +#define SPARKPLUG_GROUP_ID "WolfMQTT" +#define SPARKPLUG_EDGE_NODE_ID "EdgeNode1" +#define SPARKPLUG_DEVICE_ID "Device1" +#define SPARKPLUG_HOST_ID "HostApp1" +``` + +## Payload Format + +This example uses a simplified binary payload format for demonstration purposes. Production Sparkplug implementations should use the official [Sparkplug B Protocol Buffer definitions](https://github.com/eclipse/tahu/tree/master/sparkplug_b). + +The simplified format encodes: +- Timestamp (8 bytes) +- Sequence number (8 bytes) +- Metric count (4 bytes) +- For each metric: name, alias, timestamp, datatype, and value + +## Notes + +- **Multi-threading Required**: For full two-client communication, build with `--enable-mt` (Autotools) or `-DWOLFMQTT_MT=yes` (CMake). In single-threaded mode, only the Edge Node runs. +- **Birth/Death Sequence**: The `bdSeq` metric in NBIRTH and NDEATH allows hosts to correlate birth and death messages. +- **Sequence Numbers**: Data messages include a sequence number (0-255) for ordering and gap detection. +- **Last Will and Testament**: The Edge Node configures NDEATH as its LWT so the broker publishes it if the client disconnects unexpectedly. + +## See Also + +- [Sparkplug Specification](https://sparkplug.eclipse.org/specification/version/3.0/documents/sparkplug-specification-3.0.0.pdf) +- [Eclipse Tahu](https://github.com/eclipse/tahu) - Reference Sparkplug implementations +- [wolfMQTT Documentation](https://www.wolfssl.com/documentation/wolfMQTT-Manual.pdf) From fffbff785639fb113970f9ff4a5bd1b05e9aa504 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Thu, 5 Feb 2026 15:33:45 -0600 Subject: [PATCH 3/5] Add Sparkplug example section to main README Link to examples/sparkplug/README.md for detailed documentation. Co-Authored-By: Claude Opus 4.5 --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 0658659a..650cf569 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,16 @@ The multi-threading feature can also be used with the non-blocking socket (--ena If you are having issues with thread synchronization on Linux consider using not the conditional signal (`WOLFMQTT_NO_COND_SIGNAL`). +### Sparkplug Example +This example demonstrates the [Sparkplug B](https://sparkplug.eclipse.org/) industrial IoT protocol specification. Sparkplug defines a standard MQTT topic namespace and payload format for SCADA and industrial automation systems. The example creates two MQTT clients that communicate using Sparkplug: + +* **Edge Node**: Publishes birth certificates (NBIRTH), device data (DDATA), and responds to commands (DCMD) +* **Host Application**: Subscribes to Sparkplug topics, receives telemetry data, and sends device commands + +The example simulates sensor metrics (temperature, humidity, LED state) and demonstrates command/control where the Host toggles the Edge Node's LED. For full two-client communication, build with `--enable-mt`. The example is located in `/examples/sparkplug/`. + +More details in [examples/sparkplug/README.md](examples/sparkplug/README.md) + ### Atomic publish and subscribe examples In the `examples/pub-sub` folder, there are two simple client examples: * mqtt-pub - publishes to a topic From 80a288447a3418669c3799ef988579c29d83fdef Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Thu, 5 Feb 2026 15:40:20 -0600 Subject: [PATCH 4/5] Add sparkplug binary to .gitignore Co-Authored-By: Claude Opus 4.5 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8628c648..d650bc2e 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,7 @@ examples/multithread/multithread examples/wiot/wiot examples/pub-sub/mqtt-pub examples/pub-sub/mqtt-sub +examples/sparkplug/sparkplug examples/websocket/websocket_client # eclipse From 7313a012f021d1860c6ab9ab022a43d91812952e Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Fri, 6 Feb 2026 09:04:31 -0600 Subject: [PATCH 5/5] Fix word64 typedef redefinition when building with wolfSSL Use WOLF_CRYPT_TYPES_H include guard to skip the word64 typedef when wolfSSL has already defined it, matching the pattern used in wolfmqtt/mqtt_types.h. Fixes build error on macOS where wolfSSL defines word64 as unsigned long vs uint64_t (unsigned long long). Co-Authored-By: Claude Opus 4.6 --- examples/sparkplug/sparkplug.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/sparkplug/sparkplug.h b/examples/sparkplug/sparkplug.h index f354bb72..eb677707 100644 --- a/examples/sparkplug/sparkplug.h +++ b/examples/sparkplug/sparkplug.h @@ -58,8 +58,9 @@ extern "C" { #include #include /* for rand() */ -/* Define word64 if not already defined (wolfMQTT only defines byte/word16/word32) */ -#ifndef word64 +/* word64 is defined by wolfSSL's wolfcrypt/types.h. Define it here only + * when not building with wolfSSL. */ +#ifndef WOLF_CRYPT_TYPES_H typedef uint64_t word64; #endif