MQTT Exercise: Receiving LED Commands
✅ Subscribe to color_topic(uuid)
✅ Run host_client
in parallel in its own terminal. The host_client
publishes board LED color
roughly every second.
✅ Verify your subscription is working by logging the information received through the topic.
✅ React to the LED commands by setting the newly received color to the board with led.set_pixel(/* received color here */)
.
intro/mqtt/exercise/solution/solution_publ_rcv.rs
contains a solution. You can run it with the following command:
cargo run --example solution_publ_rcv
Encoding and Decoding Message Payloads
The board LED commands are made of three bytes indicating red, green, and blue.
enum ColorData
contains a topiccolor_topic(uuid)
and theBoardLed
- It can convert the
data()
field of anEspMqttMessage
by usingtry_from()
. The message needs first to be coerced into a slice, usinglet message_data: &[u8] = &message.data();
#![allow(unused)] fn main() { // RGB LED command if let Ok(ColorData::BoardLed(color)) = ColorData::try_from(message_data) { /* set new color here */ } }
Publish & Subscribe
EspMqttClient
isn't only responsible for publishing but also for subscribing to topics.
#![allow(unused)] fn main() { let subscribe_topic = /* ... */; client.subscribe(subscribe_topic, QoS::AtLeastOnce) }
Handling Incoming Messages
The message_event
parameter in the handler closure is of type Result<Event<EspMqttMessage>
.
Since we're only interested in processing successfully received messages, we can make use of deep pattern matching into the closure:
#![allow(unused)] fn main() { let mut client = EspMqttClient::new( broker_url, &mqtt_config, move |message_event| match message_event { Ok(Received(msg)) => process_message(msg, &mut led), _ => warn!("Received from MQTT: {:?}", message_event), }, )?; }
In the processing function, you will handle Complete
messages.
💡 Use Rust Analyzer to generate the missing match arms or match any other type of response by logging an info!()
.
#![allow(unused)] fn main() { match message.details() { // All messages in this exercise will be of type `Complete` // The other variants of the `Details` enum are for larger message payloads Complete => { // Cow<&[u8]> can be coerced into a slice &[u8] or a Vec<u8> // You can coerce it into a slice to be sent to try_from() let message_data: &[u8] = &message.data(); if let Ok(ColorData::BoardLed(color)) = ColorData::try_from(message_data) { // Set the LED to the newly received color } } // Use Rust Analyzer to generate the missing match arms or match an incomplete message with a log message. } }
💡 Use a logger to see what you are receiving, for example, info!("{}", color);
or dbg!(color)
.
Extra Tasks
Implement MQTT with Hierarchical Topics
✅ Work on this if you have finished everything else. We don't provide a full solution for this, as this is to test how far you get on your own.
Check common/lib/mqtt-messages
:
✅ Implement the same procedure, but by using an MQTT hierarchy. Subscribe by subscribing to all "command" messages, combining cmd_topic_fragment(uuid)
with a trailing #
wildcard.
✅ Use enum Command
instead of enum ColorData
. enum Command
represents all possible commands (here: just BoardLed
).
✅ RawCommandData
stores the last part of a message topic (e.g. board_led
in a-uuid/command/board_led
). It can be converted into a Command
using try_from
.
#![allow(unused)] fn main() { // RGB LED command let raw = RawCommandData { path: command, data: message.data(), }; }
Check the host-client
:
✅ you will need to replace color
with command
. For example, with this:
#![allow(unused)] fn main() { let command = Command::BoardLed(color) }
✅ in the process_message()
function, you will need to parse the topic.
#![allow(unused)] fn main() { match message.details() { Complete => { // All messages in this exercise will be of type `Complete` // the other variants of the `Details` enum // are for larger message payloads // Cow<str> behaves a lot like other Rust strings (&str, String) let topic: Cow<str> = message.topic(token); // Determine if we're interested in this topic and // Dispatch based on its content let is_command_topic: bool = /* ... */; if is_command_topic { let raw = RawCommandData { /* ... */ }; if let Ok(Command::BoardLed(color)) = Command::try_from(raw) { // Set the LED to the newly received color } }, _ => {} } } }
💡 Since you will be iterating over a MQTT topic, you will need to split()
on a string returns an iterator. You can access a specific item from an iterator using nth()
.
💡 The solution implementing hierarchy can be run with cargo run --example solution2
, while the solution without can be run with cargo run
or cargo run --example solution1
Other Tasks
✅ Leverage serde_json
to encode/decode your message data as JSON.
✅ Send some messages with a large payload from the host client and process them on the microcontroller. Large messages will be delivered in parts instead of Details::Complete
:
#![allow(unused)] fn main() { InitialChunk(chunk_info) => { /* first chunk */}, SubsequentChunk(chunk_data) => { /* all subsequent chunks */ } }
💡 You don't need to differentiate incoming chunks based on message ID, since at most one message will be in flight at any given time.
Troubleshooting
error: expected expression, found .
When building host client: update your stable Rust installation to 1.58 or newer- MQTT messages not showing up? make sure all clients (board and workstation) use the same UUID (you can see it in the log output)