Status & Roadmap | API Reference | Implementation Details | Testing Guide
31 unit tests pass, 5 skip on qemu_riscv32 (CI), using an in-process fake broker. No external MQTT broker or Docker required. 31 pass, 5 skip on real ESP32-S3 hardware (plain build, April 12, 2026, commit 27f33f9). 35 pass, 1 skip on real ESP32-S3 hardware (TLS build, April 12, 2026, commit 27f33f9). 35 tests pass, 1 skip in the TLS build (qemu_riscv32, mbedTLS overlay). 7 integration tests pass on native_sim against an external MQTT broker via ETH_NATIVE_TAP + NAT routing (no mocking, real TCP/IP, commit 27f33f9). GitHub Actions CI runs on push and pull request.
An experimental MQTT client library for Zephyr RTOS that uses Boost.SML (State Machine Language) for state management. The library provides both C++ and C APIs and targets MQTT v3.1.1 protocol support for resource-constrained embedded systems.
- State Machine Based: Uses Boost.SML for clean, verifiable state transitions
- Embedded-Friendly: Static memory allocation, configurable buffer sizes, noexcept guarantees
- Dual API: Native C++ and C wrapper for integration with C codebases
- MQTT v3.1.1 Protocol: QoS 0/1/2, retained messages, keepalive, clean session (implementation complete)
- Multiple Connections: Support for multiple simultaneous client instances
- Event Callbacks: State change and message received callbacks for C applications
- TLS Support: Optional software TLS via mbedTLS (
CONFIG_MQTT_LIB_TLS); mTLS supported - Zero Dependencies: Only requires Zephyr MQTT library and Boost.SML header
31 pass, 5 skip on qemu_riscv32 (CI) (April 12, 2026, commit 27f33f9). 35 pass, 1 skip in TLS build on qemu_riscv32 (April 12, 2026, commit 27f33f9). 31 pass, 5 skip on real ESP32-S3 hardware (plain build, April 12, 2026, commit 27f33f9). 35 pass, 1 skip on real ESP32-S3 hardware (TLS build, April 12, 2026, commit 27f33f9). 7/7 integration tests passing on native_sim against external MQTT broker (April 12, 2026, commit 27f33f9).
| Suite | Tests (unit) | Coverage |
|---|---|---|
| sml_mqtt_basic | 6/6 | Object creation, init, connect/disconnect, C API |
| sml_mqtt_pubsub | 5/5 | QoS 0 publish, subscribe, unsubscribe, loopback pubsub |
| sml_mqtt_qos | 10/10 | QoS 1/2 handshakes, timeouts, regression tests, subscription levels, retained flag |
| sml_mqtt_concurrent | 4/4 | Receive during publish QoS 1/2; evt_publish_done/evt_receive_done isolation |
| sml_mqtt_multiple | 5/5 | Sequential clients, reconnection, mixed C/C++ API, keepalive, rapid publish |
| sml_mqtt_tls | 1 pass / 5 skip (unit build) | TLS not-supported guard; full TLS tests run in TLS build |
| sml_mqtt_integration | 7/7 | Real Mosquitto: connect, QoS 0/1/2, subscribe+receive, multi-QoS, keepalive |
The highlight is test_loopback_pubsub: subscribe to a topic, publish to
the same topic, verify the publish_received_cb fires with the correct
topic and payload - all on a single client, no external broker, inside QEMU.
The library implements the MQTT v3.1.1 protocol using a hierarchical state machine with composite submachines:
Main SM: Disconnected <-> Connecting <-> Connected
├─> publishing_sm (QoS-specific publish lifecycle)
├─> receiving_sm (QoS-specific receive lifecycle)
├─> Subscribing
└─> Unsubscribing
stateDiagram-v2
[*] --> Disconnected
Disconnected --> Connecting : connect()
Connecting --> Connected : CONNACK [success]
Connecting --> Disconnected : CONNACK [fail] / timeout
Connected --> Disconnected : disconnect
state publishing_sm {
[*] --> pub_idle
pub_idle --> qos0 : publish [QoS 0]
qos0 --> done_pub : sent
pub_idle --> qos1 : publish [QoS 1]
qos1 --> done_pub : PUBACK
pub_idle --> qos2 : publish [QoS 2]
qos2 --> releasing : PUBREC
releasing --> done_pub : PUBCOMP
qos0 --> done_pub : error
qos1 --> done_pub : error
qos2 --> done_pub : error
releasing --> done_pub : error
done_pub --> [*]
}
state receiving_sm {
[*] --> recv_idle
recv_idle --> done_recv : PUBLISH [QoS 0/1]
recv_idle --> waiting_rel : PUBLISH [QoS 2]
waiting_rel --> done_recv : PUBREL
waiting_rel --> done_recv : error
done_recv --> [*]
}
Connected --> publishing_sm : publish()
publishing_sm --> Connected : evt_publish_done
publishing_sm --> Disconnected : disconnect
Connected --> receiving_sm : PUBLISH received
receiving_sm --> Connected : evt_receive_done
receiving_sm --> Disconnected : disconnect
Connected --> Subscribing : subscribe()
Subscribing --> Connected : SUBACK / error
Subscribing --> Disconnected : disconnect
Connected --> Unsubscribing : unsubscribe()
Unsubscribing --> Connected : UNSUBACK / error
Unsubscribing --> Disconnected : disconnect
Main State Machine (mqtt_state_machine):
- Disconnected: Initial state, no connection
- Connecting: TCP connected, waiting for CONNACK
- Connected: Session active, transitions to submachines for operations
- Subscribing/Unsubscribing: Subscription management
Publishing Submachine (publishing_sm):
- idle: Ready to publish (initial state)
- qos0: QoS 0 publish in progress, waiting for send confirmation
- qos1: QoS 1 publish, waiting for PUBACK from broker
- qos2: QoS 2 publish, waiting for PUBREC from broker
- releasing: QoS 2 release phase, waiting for PUBCOMP
Each path ends at sml::X (terminate); the MQTT event handler then fires
evt_publish_done so the outer SM returns to Connected.
Receiving Submachine (receiving_sm):
- idle: Ready to receive (initial state)
- waiting_rel: QoS 2 only — waiting for PUBREL from broker (will send PUBCOMP)
QoS 0 and QoS 1 incoming messages complete immediately at sml::X without
intermediate states; only QoS 2 requires the waiting_rel intermediate state.
Key Benefits:
- Each message type has its own lifecycle in dedicated submachine
- Guards work correctly on runtime event data (QoS field)
- Symmetric architecture for publishing and receiving
- Clean separation of concerns
- Error recovery isolated within each submachine
Current Version: 0.2.0-beta (April 2026) Status: Beta - 31 unit tests passing, 5 skip on qemu_riscv32 (CI); 35 TLS tests passing, 1 skip; 7/7 integration tests passing on native_sim
See STATUS.md for:
- Development history and timeline
- Current features and limitations
- Roadmap for future versions
- Compatibility matrix
- Known issues and workarounds
Copyright (c) 2026 sml-mqtt-cli contributors SPDX-License-Identifier: MIT
To use add the repository path to your west manifest file, probably west.yml. See the zephyr documentation on project manifests for details.
projects:
- name: boost-sml
url: https://github.com/boost-ext/sml.git
revision: zephyr_module
- name: sml-mqtt-cli
url: http://your.git.server/sml-mqtt-cli
revision: mainThen run west update to pull in the code. You need to then add the following config options to prj.conf or wherever you manage your configuration.
CONFIG_CPP=y
CONFIG_STD_CPP17=y
CONFIG_REQUIRES_FULL_LIBCPP=y
# Include boost sml state machine
CONFIG_LIB_BOOST_SML=y
# Include the zephyr MQTT library
CONFIG_MQTT_LIB=y
# Include the sml mqtt client
CONFIG_LIB_SML_MQTT_CLI=y
# Configure buffer sizes (optional, these are defaults)
CONFIG_SML_MQTT_CLI_RX_BUFFER_SIZE=256
CONFIG_SML_MQTT_CLI_TX_BUFFER_SIZE=256
CONFIG_SML_MQTT_CLI_MAX_PAYLOAD_LEN=512
CONFIG_SML_MQTT_CLI_MAX_TOPIC_LEN=128
CONFIG_SML_MQTT_CLI_MAX_SUBSCRIPTIONS=8
Then you can #include <sml-mqtt-cli.hpp> to use the library.
#include <sml-mqtt-cli.hpp>
using namespace sml_mqtt_cli;
// Create and initialize client
mqtt_client client;
client.init("my_device_001");
// Connect to broker
int ret = client.connect("mqtt.example.com", 1883, false);
if (ret != 0) {
LOG_ERR("Failed to connect: %d", ret);
return ret;
}
// Wait for connection
k_sleep(K_SECONDS(2));
client.input(); // Process CONNACK
// Publish a message
const char *topic = "sensors/temperature";
const char *payload = "{\"temp\": 22.5}";
ret = client.publish(topic, (const uint8_t*)payload, strlen(payload),
MQTT_QOS_0_AT_MOST_ONCE, false);
// Subscribe to a topic
ret = client.subscribe("commands/device001", MQTT_QOS_0_AT_MOST_ONCE);
// Main loop
while (client.is_connected()) {
client.input(); // Process incoming messages
client.live(); // Send keepalive if needed
k_sleep(K_MSEC(100));
}
client.disconnect();#include <sml-mqtt-cli.hpp>
// Allocate storage for client (no heap allocation)
static uint8_t client_storage[sml_mqtt_client_get_size()] __attribute__((aligned(8)));
sml_mqtt_client_handle_t client = sml_mqtt_client_init_with_storage(client_storage, sizeof(client_storage));
if (!client) {
// Handle error
}
sml_mqtt_client_init(client, "my_device_001");
// Connect
int ret = sml_mqtt_client_connect(client, "mqtt.example.com", 1883, false);
// Publish
const char *topic = "sensors/temperature";
const char *payload = "{\"temp\": 22.5}";
ret = sml_mqtt_client_publish(client, topic, (const uint8_t*)payload,
strlen(payload), MQTT_QOS_0_AT_MOST_ONCE, false);
// Subscribe with callback
void on_message(void *user_data, const char *topic,
const uint8_t *payload, size_t len, enum mqtt_qos qos) {
printk("Received on %s: %.*s\n", topic, (int)len, payload);
}
sml_mqtt_client_set_publish_received_callback(client, on_message, NULL);
sml_mqtt_client_subscribe(client, "commands/#", MQTT_QOS_0_AT_MOST_ONCE);
// Process loop
while (sml_mqtt_client_is_connected(client)) {
sml_mqtt_client_input(client);
sml_mqtt_client_live(client);
k_sleep(K_MSEC(100));
}
// Cleanup (does not free storage)
sml_mqtt_client_disconnect(client);
sml_mqtt_client_deinit(client);
// client_storage remains allocated on stack or in static memory// Create multiple clients for different purposes
mqtt_client sensor_client;
mqtt_client command_client;
sensor_client.init("device001_sensors");
command_client.init("device001_commands");
sensor_client.connect("sensors.example.com", 1883, false);
// TLS: register cert, call init() with tls_config, then connect with use_tls=true
command_client.connect("commands.example.com", 8883, true);
// Use independently
sensor_client.publish("temp/living_room", data, len, MQTT_QOS_0_AT_MOST_ONCE, false);
command_client.subscribe("commands/device001/#", MQTT_QOS_1_AT_LEAST_ONCE);All buffer sizes and limits are configurable via Kconfig:
| Option | Default | Description |
|---|---|---|
CONFIG_SML_MQTT_CLI_MAX_TOPIC_LEN |
128 | Maximum topic string length |
CONFIG_SML_MQTT_CLI_MAX_PAYLOAD_LEN |
512 | Maximum payload size |
CONFIG_SML_MQTT_CLI_MAX_CLIENT_ID_LEN |
64 | Maximum client ID length |
CONFIG_SML_MQTT_CLI_RX_BUFFER_SIZE |
256 | MQTT RX buffer size |
CONFIG_SML_MQTT_CLI_TX_BUFFER_SIZE |
256 | MQTT TX buffer size |
CONFIG_SML_MQTT_CLI_MAX_SUBSCRIPTIONS |
8 | Max concurrent subscriptions |
CONFIG_SML_MQTT_CLI_CONNECT_TIMEOUT_MS |
5000 | Connection timeout |
CONFIG_SML_MQTT_CLI_KEEPALIVE_SEC |
60 | Keepalive interval |
Typical RAM usage per client instance (default configuration):
- Base structure: ~200 bytes
- RX buffer: 256 bytes
- TX buffer: 256 bytes
- Topic/payload buffers: 640 bytes
- Subscriptions: 8 x 132 = 1056 bytes
- Total: ~2.4 KB per client
Reduce footprint by adjusting Kconfig options based on your needs.
Comprehensive test suite included in tests/ directory. See tests/README.md for details.
# Unit tests - QEMU (no broker required)
source .venv/bin/activate
west build -p always -s sml-mqtt-cli/tests -b qemu_riscv32 \
-- -DZEPHYR_MODULES="$PWD/boost-sml;$PWD/sml-mqtt-cli"
timeout 120 west build -t run
# Integration tests - native_sim against a real Mosquitto broker
west build -p always -s sml-mqtt-cli/tests -b native_sim \
-- -DSML_MQTT_TEST_BROKER_HOST=<broker-ip>
sml-mqtt-cli/tests/scripts/run_integration_tests.sh \
--broker-host <broker-ip> --enable-routing --no-local-brokerUnit tests run entirely inside QEMU with an in-process fake broker.
No Mosquitto, no Docker, no host network access needed.
Integration tests use native_sim + ETH_NATIVE_TAP; see tests/README.md.
The library implements these state transition flows. Publishing and receiving use
Boost.SML composite submachines; the outer SM re-enters Connected via typed synthetic
events fired by the MQTT event handler: evt_publish_done after publishing and
evt_receive_done after receiving.
- Connection: Disconnected -> Connecting -> Connected
- Publishing QoS 0: Connected -> publishing_sm (idle->qos0->X) -> Connected
- Publishing QoS 1: Connected -> publishing_sm (idle->qos1->[PUBACK]->X) -> Connected
- Publishing QoS 2: Connected -> publishing_sm (idle->qos2->[PUBREC]->releasing->[PUBCOMP]->X) -> Connected
- Receiving QoS 0/1: Connected -> receiving_sm (idle->X) -> Connected
- Receiving QoS 2: Connected -> receiving_sm (idle->waiting_rel->[PUBREL]->X) -> Connected
- Subscribing: Connected -> Subscribing -> [receive SUBACK] -> Connected
- Unsubscribing: Connected -> Unsubscribing -> [receive UNSUBACK] -> Connected
- Error Recovery: Sub-SM intermediate states -> [evt_send_error]->X -> evt_publish_done or evt_receive_done -> Connected
All functions return standard errno codes:
0: Success-EINVAL: Invalid parameter-ENOTCONN: Not connected-EHOSTUNREACH: Cannot reach broker-EMSGSIZE: Message too large-ENOMEM: Out of resources (e.g., subscriptions)-ENOENT: Item not found
The state machine automatically handles protocol-level send errors:
- If PUBACK/PUBREC/PUBREL/PUBCOMP sending fails, the state machine returns to Connected state
- The error is logged with details
- Pending operations are reset
- The client can retry the operation
For QoS 1/2 messages, if acknowledgment sending fails:
- The broker may retransmit the message
- The application should handle duplicate deliveries
- Use message IDs to detect duplicates if needed
Not thread-safe: Each mqtt_client instance should be used from a single thread. For multi-threaded applications, use one client per thread or add external synchronization.
- MQTT v3.1.1 only (v5.0 not supported due to Zephyr dependency)
- Clean session only (persistent sessions not implemented)
- No automatic reconnection (application must handle)
- No offline message queueing (messages sent only when connected)
- Max message size limited by configured buffers
See the test suite in tests/src/ for comprehensive examples:
test_basic_connection.cpp: Connection handlingtest_publish_subscribe.cpp: Pub/sub operationstest_qos_levels.cpp: QoS 0/1/2 usagetest_multiple_clients.cpp: Multiple connections, reconnection
Contributions welcome! Please ensure:
- Code follows Zephyr coding style
- All functions are
noexcept - Static allocation is used (no heap)
- Tests pass for your changes
- Documentation is updated
- STATUS.md - Project status, roadmap, and compatibility matrix
- QUICKREF.md - Quick API reference
- IMPLEMENTATION.md - Design decisions and architecture
- tests/README.md - Testing guide and setup
- example_usage.cpp - Standalone example application