-
-
Notifications
You must be signed in to change notification settings - Fork 5
Embedded Guide
Use the mqtt5-protocol crate on bare-metal embedded systems.
mqtt5-protocol is a platform-agnostic MQTT v5.0 and v3.1.1 implementation. It is the shared
foundation used by both mqtt5 (native) and mqtt5-wasm (browser), and it compiles on no_std
targets (requires alloc).
It provides:
- Full MQTT v5.0 (and v3.1.1) packet encoding/decoding
- Session building blocks — flow-control config, message queues, subscription manager, topic-alias manager, and limits
- Topic validation and topic matching
- Platform-agnostic time types with an injectable time source
It does not include:
- Networking (you provide the transport)
- Async runtime (integrate with your embedded runtime)
┌─────────────────────────────────────────────────────────────────┐
│ Your Embedded Application │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ mqtt5-protocol│ │ Your Network │ │ Your Runtime │ │
│ │ (packets) │<──>│ Driver │<──>│ (embassy/RTIC)│ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
| Target | Platform | Notes |
|---|---|---|
x86_64-*, aarch64-*
|
Desktop/Server | Full std support |
wasm32-unknown-unknown |
WebAssembly | Browser / Node.js |
thumbv7em-none-eabihf |
ARM Cortex-M4F | STM32F4, nRF52, etc. |
riscv32imc-unknown-none-elf |
RISC-V | ESP32-C3, BL602, etc. |
Other no_std targets that provide alloc (e.g. thumbv6m-none-eabi for Cortex-M0+) also work;
see the single-core note below.
[dependencies]
mqtt5-protocol = { version = "0.14", default-features = false }The only feature flag is std (default on); disabling default features gives you the no_std
build. alloc is always required.
Single-core MCUs need portable-atomic's single-core assumption. Add to .cargo/config.toml:
[target.riscv32imc-unknown-none-elf]
rustflags = ["--cfg", "portable_atomic_unsafe_assume_single_core"]
[target.thumbv6m-none-eabi]
rustflags = ["--cfg", "portable_atomic_unsafe_assume_single_core"]On no_std targets there is no OS clock, so Instant::now() and SystemTime::now() rely on an
externally supplied time source. The time module exposes free functions (there is no trait to
implement):
| Function | Purpose |
|---|---|
set_time_source(monotonic, epoch) |
Initialize both time sources at startup |
update_monotonic_time(millis) |
Update the monotonic clock (for Instant) |
update_epoch_time(millis) |
Update the wall clock (for SystemTime) |
#![no_std]
extern crate alloc;
use mqtt5_protocol::time::set_time_source;
fn init_time() {
// monotonic_millis, epoch_millis — usually 0 at boot
set_time_source(0, 0);
}Call from your timer interrupt or main loop:
use mqtt5_protocol::time::update_monotonic_time;
#[interrupt]
fn TIM2() {
static mut MILLIS: u64 = 0;
unsafe {
*MILLIS += 1;
update_monotonic_time(*MILLIS);
}
}use mqtt5_protocol::time::{Instant, Duration};
let start = Instant::now();
// ... do work ...
if start.elapsed() > Duration::from_secs(5) {
// Timeout handling
}Implementation notes: the time source uses AtomicU32 pairs (32-bit target friendly) with SeqCst
ordering, and returns 0 until initialized.
The concrete per-packet types (ConnectPacket, PublishPacket, …) implement the MqttPacket
trait, which provides encode/encode_body. A decoded wire packet is returned as the Packet
enum.
use mqtt5_protocol::packet::connect::ConnectPacket;
use mqtt5_protocol::packet::MqttPacket; // trait providing encode()
use mqtt5_protocol::types::ConnectOptions;
use mqtt5_protocol::time::Duration;
use bytes::BytesMut;
fn create_connect_packet() -> BytesMut {
let options = ConnectOptions::new("my-device-001")
.with_keep_alive(Duration::from_secs(60))
.with_clean_start(true);
let connect = ConnectPacket::new(options);
let mut buf = BytesMut::with_capacity(128);
connect.encode(&mut buf).unwrap();
buf
}Note:
with_keep_alivetakes aDuration, not a raw integer.
Packet::decode reads from anything implementing bytes::Buf and advances it:
use mqtt5_protocol::packet::Packet;
use mqtt5_protocol::ReasonCode;
use bytes::Bytes;
fn handle_packet(data: &[u8]) {
let mut bytes = Bytes::copy_from_slice(data);
match Packet::decode(&mut bytes) {
Ok(Packet::ConnAck(connack)) => {
if connack.reason_code == ReasonCode::Success {
// Connected successfully
}
}
Ok(Packet::Publish(publish)) => {
let topic = &publish.topic_name;
let payload = &publish.payload;
let _ = (topic, payload);
}
Ok(Packet::PubAck(_puback)) => {
// QoS 1 acknowledged
}
Ok(_) => {}
Err(_e) => {
// Handle decode error
}
}
}The full-packet enum is
Packet(variantsConnAck,Publish,PubAck, …).MqttPacketis the trait, not the enum.reason_codeis aReasonCode, compared against variants such asReasonCode::Success— not an integer.
mqtt5_protocol::session provides reusable pieces rather than a single "session" object:
SubscriptionManager, MessageQueue, TopicAliasManager, LimitsManager, and
FlowControlConfig.
use mqtt5_protocol::session::{SubscriptionManager, Subscription};
let mut subs = SubscriptionManager::new();
subs.add("sensors/#".to_string(), Subscription::default())?;
let matches = subs.matching_subscriptions("sensors/temp");use mqtt5_protocol::session::FlowControlConfig;
use mqtt5_protocol::time::Duration;
let flow = FlowControlConfig {
enable_backpressure: true,
backpressure_timeout: Some(Duration::from_secs(30)),
max_pending_queue_size: 1000,
in_flight_timeout: Duration::from_secs(60),
};Using with the Embassy async runtime:
#![no_std]
#![no_main]
extern crate alloc;
use embassy_executor::Spawner;
use embassy_net::tcp::TcpSocket;
use mqtt5_protocol::packet::{connect::ConnectPacket, Packet, MqttPacket};
use mqtt5_protocol::types::ConnectOptions;
use mqtt5_protocol::ReasonCode;
use bytes::{Bytes, BytesMut};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
// Initialize hardware, network stack, socket ...
let mut socket = TcpSocket::new(stack, rx_buffer, tx_buffer);
socket.connect(broker_addr).await.unwrap();
// Build and send CONNECT
let connect = ConnectPacket::new(ConnectOptions::new("embassy-device"));
let mut buf = BytesMut::with_capacity(128);
connect.encode(&mut buf).unwrap();
socket.write_all(&buf).await.unwrap();
// Read and decode the response
let mut rx_buf = [0u8; 256];
let n = socket.read(&mut rx_buf).await.unwrap();
let mut response = Bytes::copy_from_slice(&rx_buf[..n]);
if let Ok(Packet::ConnAck(connack)) = Packet::decode(&mut response) {
if connack.reason_code == ReasonCode::Success {
// Connected!
}
}
}Using with the RTIC framework — drive the time source from a periodic timer:
#![no_std]
#![no_main]
use rtic::app;
use mqtt5_protocol::time::{set_time_source, update_monotonic_time};
#[app(device = stm32f4xx_hal::pac)]
mod app {
use super::*;
#[shared]
struct Shared {}
#[local]
struct Local {
timer: Timer<TIM2>,
}
#[init]
fn init(ctx: init::Context) -> (Shared, Local) {
set_time_source(0, 0);
let mut timer = Timer::new(ctx.device.TIM2, 1.kHz(), &clocks);
timer.listen(Event::Update);
(Shared {}, Local { timer })
}
#[task(binds = TIM2, local = [timer, ticks: u64 = 0])]
fn timer_tick(ctx: timer_tick::Context) {
ctx.local.timer.clear_interrupt(Event::Update);
*ctx.local.ticks += 1;
update_monotonic_time(*ctx.local.ticks);
}
}The crate uses alloc for topic strings, payload buffers, and properties. Configure an allocator:
use embedded_alloc::Heap;
#[global_allocator]
static HEAP: Heap = Heap::empty();
fn init_heap() {
unsafe { HEAP.init(HEAP_START, 4096) };
}Packet encoding/decoding uses stack buffers — ensure adequate stack. Small control packets need a few dozen bytes; CONNECT is on the order of a few hundred; PUBLISH scales with payload size.
# ARM Cortex-M4F (hardware FPU)
cargo build --target thumbv7em-none-eabihf -p mqtt5-protocol --no-default-features
# ARM Cortex-M0+ (no FPU)
cargo build --target thumbv6m-none-eabi -p mqtt5-protocol --no-default-features
# RISC-V (ESP32-C3)
cargo build --target riscv32imc-unknown-none-elf -p mqtt5-protocol --no-default-featuresundefined symbol: __atomic_*
Add the single-core portable-atomic cfg to .cargo/config.toml:
rustflags = ["--cfg", "portable_atomic_unsafe_assume_single_core"]- Reduce buffer sizes and payloads.
- Grow the allocator heap.
- Ensure
set_time_source()is called at init. - Ensure
update_monotonic_time()is called regularly from a timer. - Verify the timer interrupt is enabled.
Getting Started
Broker Guide
Client Guide
Platform Guides
CLI Reference
Development