Skip to content

Cross-platform IoT (Arduino C++)

One of the primary structural advantages of SyMqtt is its lightweight text layout. Because it avoids complex envelope structures like JSON or XML at the transport layer, resource-constrained edge devices (such as Arduino, ESP8266, or ESP32 microcontrollers) can parse requests and compile responses with minimal CPU overhead and zero memory leaks.

To participate in SyMqtt’s synchronous Request-Response pattern, an embedded device simply needs to follow two rules when a message arrives:

  1. Extract the incoming MessageId (everything before the first comma).
  2. Publish the response back containing that exact same MessageId, followed by a SuccessCode and the operational Details.

Arduino C++ - Minimal in-place implementation example

Section titled “Arduino C++ - Minimal in-place implementation example”

The following standalone sketch demonstrates how to handle incoming SyMqtt channel requests using the popular PubSubClient library on an ESP32 or Arduino network shield.

#include <WiFi.h>
#include <PubSubClient.h>
// Network configuration
const char* ssid = "Your_WiFi_SSID";
const char* password = "Your_WiFi_Password";
const char* mqtt_server = "mybroker.net";
// SyMqtt Channel configuration paths
const char* subscribe_topic = "smartcontrol/factory/floor1/conveyor/cmd/conveyor1";
const char* response_topic = "smartcontrol/factory/floor1/conveyor/res/conveyor1";
// Maximum predictable size for any outbound SyMqtt response message string
const size_t MQTT_MAX_RESPONSE_SIZE = 128;
WiFiClient espClient;
PubSubClient client(espClient);
// Callback function executed whenever an MQTT payload arrives
void callback(char* topic, byte* payload, unsigned int length) {
// 1. Convert raw byte payload into a safe null-terminated C-string in-place
char incomingBuffer[length + 1];
memcpy(incomingBuffer, payload, length);
incomingBuffer[length] = '\0';
// 2. Locate the envelope delimiter (the first comma) using pointer arithmetic
char* firstComma = strchr(incomingBuffer, ',');
if (firstComma == NULL) {
// Invalid SyMqtt wire structure; discard the message safely
return;
}
// 3. Extract tokens in-place by splitting the string natively (zero-allocation)
*firstComma = '\0'; // Mutates the first comma to a null-terminator, splitting the string
char* messageId = incomingBuffer; // This pointer now points strictly to the MessageId
char* commandContents = firstComma + 1; // This pointer points to the remaining payload
// 4. Process the business logic safely
int successCode = -1;
const char* responseDetails = "";
if (strcmp(commandContents, "START_CONVEYOR") == 0) {
// Simulate successful hardware execution
successCode = 0;
responseDetails = "CONVEYOR_RUNNING_NORMAL";
}
else if (strcmp(commandContents, "STOP_CONVEYOR") == 0) {
successCode = 0;
responseDetails = "CONVEYOR_STOPPED_SAFETY";
}
else {
// Command not recognized by the firmware execution tree
successCode = 404;
responseDetails = "UNKNOWN_HARDWARE_COMMAND";
}
// 5. Construct the SyMqtt response message string safely into a fixed static buffer
char responsePayload[MQTT_MAX_RESPONSE_SIZE];
snprintf(responsePayload, sizeof(responsePayload), "%s,%d,%s", messageId, successCode, responseDetails);
// 6. Return the response back to the .NET waiting thread via the response leg
client.publish(response_topic, responsePayload);
}
void setup_wifi() {
delay(10);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); }
}
void reconnect() {
while (!client.connected()) {
if (client.connect("ESP32_Conveyor_Client")) {
client.subscribe(subscribe_topic);
} else {
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
}

Arduino C++ - Production-ready refactored architecture example

Section titled “Arduino C++ - Production-ready refactored architecture example”

While the inline pointer mutation in the first example is highly efficient for single-command devices, it quickly becomes difficult to maintain and scale as your firmware evolves to handle dozens of different hardware operations.

To transition this pattern into production-grade firmware, we can refactor the logic by separating concerns. By defining dedicated data structures for incoming and outgoing payloads (InputMessage and OutputMessage) and moving the low-level string manipulation into specialized helper functions (ParseMessage and PackMessage), we achieve an architecture that is highly readable and easy to extend.

Crucially, this structured approach still completely avoids the dangerous String class. It operates strictly with stack allocation and fixed-size buffers, preserving SyMqtt’s core promises: minimal footprint, zero memory fragmentation, and zero risk of runtime leaks on long-running edge devices.

#include <WiFi.h>
#include <PubSubClient.h>
// Network configuration
const char* ssid = "Your_WiFi_SSID";
const char* password = "Your_WiFi_Password";
const char* mqtt_server = "mybroker.net";
// SyMqtt Channel configuration paths
const char* subscribe_topic = "smartcontrol/factory/floor1/conveyor/cmd/conveyor1";
const char* response_topic = "smartcontrol/factory/floor1/conveyor/res/conveyor1";
// Maximum predictable size for any outbound SyMqtt response message string
const size_t MQTT_MAX_RESPONSE_SIZE = 128;
WiFiClient espClient;
PubSubClient client(espClient);
// --- SyMqtt Types & Helpers ---
// Zero-allocation structures holding pointers directly into the network buffer
struct InputMessage {
const char* MessageId;
const char* Contents;
};
struct OutputMessage {
const char* MessageId;
int SuccessCode;
const char* Details;
};
// Parses the raw buffer in-place by replacing the first comma with a null-terminator
bool ParseMessage(char* incomingBuffer, InputMessage& outMsg) {
char* firstComma = strchr(incomingBuffer, ',');
if (firstComma == NULL) {
return false; // Invalid SyMqtt wire structure
}
*firstComma = '\0'; // Mutate comma to null-terminator to split strings natively
outMsg.MessageId = incomingBuffer;
outMsg.Contents = firstComma + 1;
return true;
}
// Safely compiles the fields into a standard SyMqtt response message string format
void PackMessage(char* outputBuffer, size_t bufferSize, const OutputMessage& msg) {
snprintf(outputBuffer, bufferSize, "%s,%d,%s", msg.MessageId, msg.SuccessCode, msg.Details);
}
// --- MQTT Infrastructure ---
void callback(char* topic, byte* payload, unsigned int length) {
// 1. Create a safe null-terminated stack buffer for the incoming raw data
char incomingBuffer[length + 1];
memcpy(incomingBuffer, payload, length);
incomingBuffer[length] = '\0';
// 2. Extract into the InputMessage structure
InputMessage inMsg;
if (!ParseMessage(incomingBuffer, inMsg)) {
return; // Discard invalid payload safely
}
// 3. Prepare the OutputMessage container linked to the received MessageId
OutputMessage outMsg;
outMsg.MessageId = inMsg.MessageId;
outMsg.SuccessCode = -1;
outMsg.Details = "";
// 4. Clean execution branch based on structured fields
if (strcmp(inMsg.Contents, "START_CONVEYOR") == 0) {
outMsg.SuccessCode = 0;
outMsg.Details = "CONVEYOR_RUNNING_NORMAL";
}
else if (strcmp(inMsg.Contents, "STOP_CONVEYOR") == 0) {
outMsg.SuccessCode = 0;
outMsg.Details = "CONVEYOR_STOPPED_SAFETY";
}
else {
outMsg.SuccessCode = 404;
outMsg.Details = "UNKNOWN_HARDWARE_COMMAND";
}
// 5. Serialize and transmit the SyMqtt response message string safely using a fixed-size buffer
char responsePayload[MQTT_MAX_RESPONSE_SIZE];
PackMessage(responsePayload, sizeof(responsePayload), outMsg);
client.publish(response_topic, responsePayload);
}
void setup_wifi() {
delay(10);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
}
void reconnect() {
while (!client.connected()) {
if (client.connect("ESP32_Conveyor_Client")) {
client.subscribe(subscribe_topic);
} else {
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
}

When writing custom embedded firmware (whether in C++, MicroPython, or Rust) to interface with a .NET application running SyMqtt, verify that your device logic honors the following constraints:

  • Strict token preservation: The MessageId string parsed from the request payload must be captured completely and mirrored back into the SyMqtt response message string without any character mutations, capitalization changes, or whitespace stripping.
  • Double-comma response design: Ensure your response layout formats exactly two delimiters separating the three target atoms ([Id],[Code],[Details]). If your details string payload contains internal commas, that is entirely fine—the .NET SerialMessage engine only treats the first two commas as structural envelopes, passing the remainder cleanly into the MessageDetails variable.
  • Timeout alignment: Because the .NET thread suspends while invoking .FetchAsync() with a specified time budget (e.g., 3000ms), your device firmware must process the incoming payload and publish its response back within that designated timeout window to prevent the .NET service from throwing a communication timeout exception. If a physical hardware operation inherently requires more time to execute, ensure that the .FetchAsync timeout value on the .NET side is configured with an appropriately larger threshold to accommodate the latency.