Repository navigation
Description
Part of #388. The last step: it needs #389 (detachable links, Input / Output) and #390 (connect → ConnectorHandle, OutboundLink::on / InboundLink::on) to have landed. Closing this issue completes #388.
No spike was needed. Everything here is thin code on top of #389 and #390, and the #388 prototype already showed the compile errors this layer produces.
1. publish / subscribe in aimdb-data-contracts
Feature linkable, in aimdb-data-contracts/src/linkable.rs, next to the existing LinkableRegistrarExt.
let mqtt = builder.connect(MqttConnector::new(broker_url));
reg.input(subscribe(mqtt, "commands/temp/indoor").with_qos(1))
.output(publish(mqtt, "sensors/temp/indoor").with_qos(1).with_retain(false));publish::<T: Linkable>(conn: ConnectorHandle, address: &str)buildsOutboundLink::on(conn, address).with_link_codec(link_codecs::Default).subscribeis the same withInboundLink.- The record's schema must still be reported. Today
linked_to/linked_fromcallreg.schema(T::NAME, T::VERSION)before linking.publish/subscribeare values built before a registrar exists, so they need their own smallOutput<T>/Input<T>wrapper types whosewire()callsreg.schema(..)and thenreg.output(link)/reg.input(link).- The wrappers also give a readable error. [FEAT] Detachable link builders and Input / Output traits in core #389 notes that a raw
OutboundLinkBuilder<Temp, Detached>in an E0277 reads poorly. - Connector options must keep working on them. Either the wrappers deref/forward to the link, or the wrapper is generic over the link and the extension traits (
MqttLinkExt,Ros2LinkExt,LinkCodecBuilderExt) are implemented for it as well. Pick whichever needs less code; the extension traits must not be duplicated per connector.
- The wrappers also give a readable error. [FEAT] Detachable link builders and Input / Output traits in core #389 notes that a raw
T: Linkableis a bound onpublish/subscribethemselves, so a contract with no wire format is E0277 at the call.- Per-link codec: either
publish(..).with_link_codec(Json)overrides the default (preferred, if the wrapper forwardsLinkCodecBuilderExt), or addpublish_with(conn, address, codec). Keep this in line with the existinglinked_to_with. addressaccepts a bare address or a full URL with a matching scheme ([FEAT] AimDbBuilder::connect returns a ConnectorHandle; links through a handle need no scheme #390), sopublish(mqtt, SensorKey::TempIndoor.link_address().unwrap())works with existing#[link_address]keys.
2. Rebuild linked_to / linked_from on the same path
Decided in #388 (comment): keep the existing LinkableRegistrarExt::linked_to(url) / linked_from(url) and LinkCodecRegistrarExt::linked_to_with / linked_from_with. Re-implement them as one-line wrappers over the same code as publish / subscribe, using OutboundLink::new(url) / InboundLink::new(url) instead of a handle, so there is one implementation. Behaviour, including the schema() call, must not change. Whether to deprecate them is a separate decision, made after the demos have moved over.
3. Migrate the two MQTT demos
These are the acceptance test for the whole feature. Behaviour must not change.
-
examples/tokio-mqtt-connector-demo/src/main.rs(215 lines). Today:.with_connector(MqttConnector…)- one hand-written
.link_to(..).with_qos(1).with_retain(false).with_serializer(..) - two
reg.linked_to(..) - one
.link_from(..).with_deserializer(..) - one
reg.linked_from(..) - three nearly identical producer functions (
indoor_temp_producer…)
Contracts come from
examples/mqtt-connector-demo-common(Temperature,TemperatureCommand,#[derive(Linkable)]behind itsdata-contractsfeature). Keys come fromkeys.rswith#[link_address = "mqtt://…"]. Target:let mqtt = builder.connect(..)publish/subscribeeverywhere- the simulated thermometers as one
Inputtype that takes an owned channel (SimThermometers { indoor, outdoor, server_room }) and is generic overT: From<Reading>, moved intosim.rs
The [FEAT] reg.input / reg.output with connection handles, so a wrong wiring is a compile error #388 prototype got
main.rsdown from 215 to 84 lines, plus 78 insim.rs. -
examples/embassy-mqtt-connector-demo/src/main.rs(546 lines). Today:.with_connector(mqtt)and.with_connector(SerialServer::new(..)), the remote-access server, which has no links. Keep it onwith_connector, or useconnectand ignore the handle.- three
.link_to(..).with_serializer(|_, t| Ok(t.to_json_vec())) .link_from(..)for commands
Check whether the embassy build enables the common crate's
data-contractsfeature (it serializes by hand today).publishneedsLinkable. Target, as in the prototype:- a board module (heap, clocks, Ethernet, UART) that hands out
NucleoInputs { b1, .. } - an application module holding only records
- user button B1 as a real
PushButtoninput, built withT: From<ButtonEvent>
Must still build with
cargo build -p embassy-mqtt-connector-demo --target thumbv8m.main-none-eabihf(seemakearound line 682). The prototype reached the original firmware size. Flashing is optional; say in the PR whether it was done. -
Moving a channel into a second record must be a compile error (E0382). Show it in a
compile_faildoctest or a short note in the PR, not in the demo itself.
4. Document the conventions
In docs/aimdb-usage-guide.md (its "Using Connectors" section) and in the rustdoc of Input / publish:
- Inputs yield an event; contracts are built with
From<Event>. An input is generic over its record and bounded byT: From<Event>, so one input type serves every contract that can be built from its event (start trigger, door contact, counter pulse). A device that sends its own JSON is an input over a connection: it parses with the existingwith_deserializerand maps withT::from. - Device channels are owned, non-
Clonevalues. A board hands out its inputs as struct fields (let NucleoInputs { b1 } = board::init(..)), and an input takes one by value (PushButton::on(b1)). Using a channel for a second record is thenuse of moved value. Pins never leave the board module. This is a pattern, not a type in core. - Where
From<Event>impls live (open question 3 of [FEAT] reg.input / reg.output with connection handles, so a wrong wiring is a compile error #388). The orphan rule allowsimpl From<E> for Conly in the crate ofEor ofC. Convention: driver crates own their event types and ship their inputs with them, and contract crates implementFrom<Event>. When neither crate can, use a newtype or a.transform(). - What is checked where. Wrong pairings (
Frommissing, contract notLinkable, wrong record type) are compile errors. A second channel use is a compile error through ownership. "One input per record" is not a compile error: a second.input()/.source()/.link_from()failsbuild(), as it does today. - Update the
with_connectordocs and the Quick Start examples to showconnect+publish/subscribe, and keep one example of the rawlink_topath as the escape hatch.
Acceptance criteria
-
publish/subscribeinaimdb-data-contracts(featurelinkable): they report the schema, forward connector options, give E0277 for a type that isn'tLinkable, and accept#[link_address]keys. -
linked_to/linked_from/*_withre-implemented on the same path, with unchanged behaviour (the existing tests inlinkable.rs/link_codec.rsstill pass). - Tests:
publish/subscriberegister the same links and schema aslinked_to/linked_from; options such aswith_qosapply; acompile_faildoctest for a type that isn'tLinkable. -
tokio-mqtt-connector-demomigrated and still works against a broker. (Migrated in refactor(examples): shared MQTT demo crate on publish / subscribe (#391, stage 2) #401; verified against thedev/mosquittobench broker: all readings published, both commands received.) -
embassy-mqtt-connector-demomigrated, with a board module and aPushButtoninput on B1; builds forthumbv8m.main-none-eabihf. - A
compile_failexample of using a channel twice (E0382). - Usage guide and rustdoc cover the conventions above; CHANGELOG entry.
Testing notes
The commands and caveats from #389's implementation notes apply here too: use the Makefile's feature sets, don't use --all-features on aimdb-core (defmt fails to link on the host), and run MQTT tests with --features std. For data-contracts, test both the full set (std,simulatable,migratable,observable,linkable-json,linkable-postcard,linkable-cdr,ros2) and --no-default-features --features alloc,linkable, so publish / subscribe also build without JSON.
Activity
feat: publish / subscribe for Linkable contracts (#391) (#400)
refactor(examples): shared MQTT demo crate on publish / subscribe (#391…
feat(examples): embassy MQTT demo board module and B1 push button (#391…
docs: make the usage guide's Tokio examples compile (#391)
docs: input and contract conventions, connect + publish / subscribe (#…
docs: changelog for publish / subscribe (#391) (#404)
lxsaah commentedon Nov 2, 2026
Implemented on the #392 feature branch:
publish/subscribeinaimdb-data-contracts, andlinked_*rebuilt on the same path.mqtt-connector-demo-commonrewritten as the shared crate, and both demos moved toconnect+publish/subscribe.board.rs, and B1 as aPushButtoninput publishingButtonPresstosensors/button/b1.compile_fail,E0382doctest onInput.Decisions that differ from the text above:
publish/subscribereturn a plainOutboundLink/InboundLink. The schema travels on the link through a new core option,with_schema(name, version), reported when the link is registered. A wrapper would have needed its own impl of every connector extension trait, which this issue rules out. A type that isn't a contract gets its readable error from#[diagnostic::on_unimplemented]onLinkable.publish_with.publish(..).with_link_codec(Json)replaces the codec.publish/subscribe.mqtt-connector-demo-commonnow always derivesLinkable, and the hand-rolled no_std JSON is gone. The release firmware stays near its original size:.textwent from 557,548 to 539,948 bytes before the button record and is 567,068 bytes with it.Thermometer::on(channel)for everyT: From<Reading>) and runs on Tokio and Embassy.Still open: the tokio demo hasn't run against a real broker; it only ran without one. The embassy demo hasn't been flashed. Both are noted in #392, which closes this issue when it merges.
lxsaah commentedon Nov 2, 2026
Verified on hardware and against a real broker, the
dev/mosquittobench broker (authenticated, plain 1883):tokio-mqtt-connector-demo: all 15 readings published, and both commands received.embassy-mqtt-connector-demo, flashed on a Nucleo-H563ZI:sensors/button/b1, each exactly once and in order.Every acceptance criterion is now met. The code is on the #392 feature branch and reaches
mainwhen #392 merges.