Skip to content

feat(052): runtime-neutral MQTT connector - #253

Merged
lxsaah merged 49 commits into
mainfrom
feat/platform-agnostic-mqtt-connector
Sep 14, 2026
Merged

lxsaah merged 49 commits into
mainfrom
feat/platform-agnostic-mqtt-connector

Conversation

@lxsaah

@lxsaah lxsaah commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Wave C of design 052, plus the event-driven session rewrite that came out of it.

The split is now std vs no_std, not Tokio vs Embassy: the embedded backend runs on any target whose adapter supplies a StreamDialer, so a FreeRTOS port costs one adapter crate and no change here.

Why MQTT keeps two protocol clients

Unifying on mountain-mqtt was considered and rejected: it does not support QoS 2 (Qos2NotSupported; the codec has pubrec/pubrel/pubcomp, the state machine does not use them). rumqttc cannot ride the neutral layer either — its Transport is a closed enum, so no stream can be injected.

So one MqttConnector<B> over two backends, which keeps unification a later, small change: if QoS 2 lands in the fork, deleting Native is a deletion, not a rewrite.

Backend Client QoS TLS
Native (no transport supplied) rumqttc (std) 0–2 rustls
Embedded<D> (.transport(..)) mountain-mqtt (no_std) 0–1 embedded-tls

The session is event-driven — nothing polls

The old loop woke every 10 ms to ask three sources whether they had work, which on a battery node is the only state that normally runs. Worse, the "non-blocking peek" it polled with could block indefinitely, parking the loop and with it the pings, the liveness check and every queued publish.

The stream is now split into halves driven by three futures in one select: a reader lifting bytes off the socket, a writer draining encoded packets, and the session itself on two channels and one timer. tests/session_loop.rs asserts an idle session wakes fewer than 30 times in 3 s where the poll cost ~300, and that a QoS 1 publish no longer spins waiting inline for its PUBACK.

TLS runs that same loop. mqtts:// used to need a loop of its own because the client wanted a readiness peek a TLS session cannot answer honestly — bytes on the wire may decrypt to no application data at all. Nothing peeks any more, so the bespoke Connection, the readiness probe onto the raw socket beneath the TLS session, and the RefCell wrapping the whole socket so both could reach it are gone. What replaces them is one lock per direction behind a cloneable handle.

mountain-mqtt is now the codec alonedefault-features = false, no features, on aimdb-mountain-mqtt 0.5.1 (upstream main, zero-line source delta). The driver half lives here. A Makefile cargo tree guard asserts the subtree stays codec-only and that no executor, network stack, adapter or logger reaches the embedded graph.

Breaking

Features rename. std carries rumqttc (tokio-runtime is a deprecated alias, still works); embedded carries mountain-mqtt with alloc only; embedded-tls is runtime-neutral TLS; embassy-runtime and embassy-tls are convenience bundles over those. critical-section-std-impl links a critical-section impl for std binaries.

Modules rename with no compatibility re-export: tokio_clientnative, embassy_clientembedded. A shim would have been theatre — the builders those modules held are gone too, so the old import fails either way.

The Tokio path is otherwise unchangedMqttConnector::new(&url) still. Embedded callers supply the transport:

// mqtt:// — the adapter owns the socket
MqttConnector::new(&url).transport(EmbassyNet::tcp(*stack, MQTT_RX.init(..), MQTT_TX.init(..)))

// mqtts:// — same transport; the dialer resolves the host, so TLS needs no stack
MqttConnector::new(&url).tls(EmbassyNet::tcp(..), TlsOptions::new(..))

.tls(dialer, options) replaces .tls(stack, options). The certificate-validity clock comes from RuntimeOps::unix_time(); SNTP is opt-in via TlsOptions::with_sntp for a runtime with no wall clock of its own. MqttConnectorBuilder and the Tokio*/Embassy* aliases are gone; with_client_id/with_credentials now work on either backend.

The largest MQTT packet the session can receive is 3328 bytes (was 4096). Reassembly, read scratch and the inbound slot are carved out of one BUFFER_SIZE rather than added to it, with one read chunk held in reserve so a chunk completing one packet can still carry the head of the next. Outbound packets are encoded to exactly their own size on the heap, so they gain no fixed cap.

Core and adapters

ByteStream::split, with ByteRead/ByteWrite (aimdb-core) — borrows a stream into independently pollable halves, which a single &mut stream cannot express. Both adapters implement it. The cancellation contract is written down with it: read is cancel-safe on both adapters AimDB ships, write_all is cancel-safe nowhere and must never sit in a select arm.

⚠️ Action required — EmbassyTcpDialer resolves hostnames, so the adapter's net feature now enables embassy-net/dns. embassy_net::new adds the resolver socket itself, so every application using net must grow its StackResources<N> by one; too small an N panics at stack construction. Previously a connector handing through a hostname dialed fine on Tokio and failed forever on Embassy.

aimdb-tokio-adapter gains an embedded-io feature — the embedded-io-async trio on the net streams, including a non-destructive ReadReady via poll_peek. That is what lets the embedded backend and embedded-tls run on a host, so both are covered by real tests rather than a cross-compile alone.

Both dialers also implement Delay, so a connector generic over one needs no separate clock handle.

Fixed during review

  • PUBACKs were dropped under an inbound burst. The 4-slot write queue was try_send-and-forget, so a coalesced burst lost every response past the fourth — measured at 19 of 40 acknowledged. Unrecoverable: the client state had already retired the message, and pings kept the liveness watchdog satisfied, so the node went deaf on inbound QoS 1 with no error and no reconnect. Protocol obligations now wait for a slot; only pings are still dropped, since a ping arms no response deadline. every_qos1_push_is_acknowledged is the regression test.
  • qos=2 diverged silently between backendsNative honours it, Embedded downgrades to QoS 1. The build now names each such route in a warning.
  • Two test harnesses asserted nothing: they set QoS via a ?qos= query on the link URL, which LinkAddress::parse strips, so every test ran at the default regardless. Both now use .with_qos(..).
  • The documented packet-size ceiling was a chunk too high; embedded-hal-async was a dead manifest entry; the TLS locking comment pointed at a test file that did not exist.

Not addressed

  • A malformed route option (with_config("qos", "two")) falls to the default on both backends identically. Consistent, so not a divergence — worth its own pass if route-URL typos bite.
  • An over-limit retained message reconnect-loops the connector, since retain replays on every resubscribe and clean-start does not clear it. Documented in the CHANGELOG; the fix needs a "discard N more bytes" state in the reader.

Verification

Locally: all connector tests across six binaries, the aimdb-core session suite, clippy -D warnings on the std, embedded, embedded,tracing and embassy-runtime,embassy-tls,defmt legs (thumbv7em where applicable), rustdoc -D warnings per feature leg, and cargo fmt --check. The full matrix is CI's.

🤖 Generated with Claude Code

lxsaah and others added 30 commits September 6, 2026 11:07
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Self::tls` is `embassy-tls`-gated, but `make doc` builds this crate with
`embassy-runtime` only, so the link failed the docs gate in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Implemented a new `manager` module to handle per-session broker state, event handling, and message pumping.
- Updated `MqttConnector` to support `Delay` and `StreamDialer` traits for embedded systems.
- Refactored `MqttSink` and `MqttSource` to use action and event channels directly, removing unnecessary wrappers.
- Introduced `ClientDelay` to bridge core's `Delay` with the MQTT client's requirements.
- Enhanced the `run_sessions` function to manage MQTT sessions more effectively, ensuring reconnections and resubscriptions.
- Updated tests to ensure proper session reconnection and resubscription behavior.
- Adjusted Tokio adapter to implement `Delay` for seamless integration with the connector.
…ial support

- Consolidated the MqttConnector structure to allow seamless switching between Native and Embedded backends without separate builders.
- Introduced credential handling in the Native backend, allowing credentials to be set via the MqttConnector interface.
- Updated the Embassy client to streamline the transport setup and improve TLS handling.
- Enhanced tests to verify credential transmission for both backends, ensuring consistent behavior across different configurations.
- Removed deprecated builder patterns and unnecessary complexity in the connector implementation.
- Implement SNTP codec for encoding and parsing SNTP packets, including request and reply handling.
- Introduce TLS transport for the Embassy MQTT client, supporting secure connections with certificate verification.
- Refactor the library structure to separate native and embedded implementations, enhancing modularity.
- Update the MQTT connector to support both plain and secure MQTT connections, with appropriate error handling and logging.
- Introduced a new feature `_test-tls-broker` for testing MQTT over TLS with a pinned self-signed root CA.
- Updated the Makefile to include new test commands for the MQTT connector with TLS.
- Modified `Cargo.toml` to add dependencies for embedded TLS and SNTP.
- Refactored the `EmbeddedTls` struct to use a caller-supplied transport instead of owning the network stack.
- Implemented a new `WallClock` for certificate validity checks in the absence of an RTC.
- Created a new test `tls_broker.rs` to validate the MQTT handshake over TLS.
- Updated the example to demonstrate the use of MQTT over TLS with SNTP for time synchronization.
…ges and enhanced features for std and no_std runtimes
…ated docs

`make doc` runs `cargo doc` per feature leg with `-D warnings`, so an
intra-doc link that resolves on one leg and not another fails the build.
Five such links had crept in:

- `[Embedded]` in the ungated backend table, which only exists with the
  embedded backend
- `[build]` and `[WallClock]`, both private
- `[SntpClock]`, deleted when the TLS clock became the runtime's
- `[sntp]`, now `embassy-tls`-gated while `tls.rs` is `embedded-tls`

The `doc` target only covered `std` and `embassy-runtime`, which is why
only the first two reached CI; add the `embedded`, `embedded-tls` and
`embassy-tls` legs so the rest cannot recur silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in the runtime-neutral TCP, serial and KNX connectors (#250, #251,
#252) alongside this branch's runtime-neutral MQTT work.

Conflicts resolved:

- Makefile (`doc` target): kept this branch's MQTT doc legs (`std`,
  `embassy-tls`) and took main's KNX legs, which move off the deprecated
  `tokio-runtime`/`embassy-runtime` aliases to `std`/`connector`, plus its
  new serial and TCP embedded legs.
- aimdb-embassy-adapter/CHANGELOG.md: both Unreleased entries kept - the
  `Delay for EmbassyTcpDialer` addition and main's `EmbassyUdpBinder::bind`
  `TransportError::Busy` fix are independent.
- aimdb-embassy-adapter/src/net.rs: took main's `EmbassyTcpDialer` doc
  paragraph on `Clone` sharing the socket (docs only, no code change).

Verified: make doc, make fmt-check, make clippy, the three thumbv8m
example builds, and the core/embassy-adapter/MQTT/KNX/serial/TCP test legs
of `make test` all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016YAJUWDFEi3TeBgVEYA5Bs
- Updated `StreamDialer::connect` to accept hostnames and IP literals, enabling DNS resolution in `EmbassyTcpDialer`.
- Modified `EmbassyTcpDialer` to include a DNS query mechanism for hostname resolution.
- Updated changelog to reflect breaking changes and required adjustments for users.
- Added tests to ensure hostname resolution works correctly across both embedded and native backends.
- Adjusted resource allocation in examples and other components to accommodate the new DNS feature.
- Updated Clippy commands in the Makefile to reflect changes in the MQTT connector's features and targets.
- Changed descriptions for Clippy runs to better represent the current configurations.

---

Update CHANGELOG.md for aimdb-mqtt-connector

- Documented significant changes including protocol backend updates, removal of deprecated features, and improvements in error handling.
- Added details about the new `MqttConnectorBuilder` API and adjustments in the session management.

---

Modify README.md for aimdb-mqtt-connector

- Updated example usage to reflect the new API structure for the MQTT connector.
- Adjusted import statements to align with the latest changes in the library.

---

Update lib.rs documentation for aimdb-mqtt-connector

- Revised documentation to clarify the usage of the embedded and TLS features.
- Enhanced descriptions of the API and its components.

---

Revise CHANGELOG.md for aimdb-tokio-adapter

- Clarified the addition of `Delay` for `TokioTcpDialer` and its implications for the embedded MQTT backend.

---

Revise README.md for embassy-mqtt-connector-demo

- Updated the demo example to reflect the new connector API and usage patterns.
- Clarified hardware requirements and resource links.

---

Update main.rs for embassy-mqtt-connector-demo

- Adjusted import statements to align with the latest changes in the MQTT connector library.
- Introduced a new session loop in `session_loop.rs` that operates on an event-driven model, replacing the previous polling mechanism.
- The new loop efficiently manages reading and writing operations without contention, ensuring that no partially-read packets are discarded.
- Added support for handling multiple futures concurrently using `select3`, allowing for improved responsiveness to incoming data and actions.
- Implemented a structured approach to manage MQTT packets, including connection, subscription, and publish actions, with appropriate error handling.
- Created a comprehensive test suite in `session_loop.rs` to validate the behavior of the new session loop against various scenarios, ensuring it meets the specified design criteria.
- The tests cover idle session behavior, handling of partial packets, QoS 1 publish acknowledgments, and the interaction between inbound and outbound traffic under load.
- Added a scripted broker implementation in `common/mod.rs` to simulate various broker behaviors during tests.
- Introduced `CountingDialer` to track sleep calls made by the session.
- Refactored session loop tests in `session_loop.rs` to utilize the new scripted broker.
- Created a new test file `tls_session.rs` to validate MQTT over TLS, ensuring session behavior aligns with design criteria.
- Implemented tests for idle session wake-up, handling partial packets, and ensuring pings are sent during slow PUBACKs over TLS.
…d-hal-async dependencies; enhance session handling and add ByteStream::split support
- Updated comments in `sntp.rs` to clarify the purpose of SNTP for Unix time synchronization.
- Simplified and clarified documentation in `tls.rs`, focusing on the TLS transport and its interaction with the MQTT client.
- Enhanced clarity in `native.rs` regarding the broker connection setup and the role of the `MqttConnectorImpl`.
- Improved comments in `backend_parity.rs` and `common/mod.rs` to better explain the purpose of tests and the behavior of the fake broker.
- Streamlined documentation in `embassy_broker.rs` to focus on the interaction between `embassy-net` stacks and the fake broker.
- Clarified the behavior of the session loop in `session_loop.rs` and `tokio_broker.rs`, emphasizing reconnect and subscription logic.
- Updated `net.rs` to improve the explanation of stream splitting and its implications for locking and deadlock prevention.
@lxsaah
lxsaah merged commit cfe9559 into main Sep 14, 2026
9 checks passed
@lxsaah
lxsaah deleted the feat/platform-agnostic-mqtt-connector branch September 14, 2026 20:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants