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 topic color_topic(uuid) and the BoardLed
  • It can convert the data() field of an EspMqttMessage by using try_from(). The message needs first to be coerced into a slice, using let 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 EspMqttEvent, which has a payload() method to access the EventPayload Since we're only interested in processing successfully received messages:

#![allow(unused)]
fn main() {
    let mut client =
        EspMqttClient::new_cb(
            &broker_url,
            &mqtt_config,
            move |message_event| match message_event.payload() {
                Received { data, details, .. } => process_message(data, details, &mut led),
                Error(e) => warn!("Received error from MQTT: {:?}", e),
                _ => info!("Received from MQTT: {:?}", message_event.payload()),
            },
        )?;
}

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() {
fn process_message(data: &[u8], details: Details, led: &mut WS2812RMT) {
    match details {
        Complete => {
            info!("{:?}", data);
            let message_data: &[u8] = data;
            if let Ok(ColorData::BoardLed(color)) = ColorData::try_from(message_data) {
                info!("{}", color);
                if let Err(e) = led.set_pixel(color) {
                    error!("Could not set board LED: {:?}", e)
                };
            }
        }
        _ => {}
    }
}
}

💡 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)
}

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)