MQTT Exercise: Receiving LED Commands

✅ Subscribe to color_topic(uuid)

✅ Run host_client in parallel in it's 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 */).

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 is not 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.
        
    }
}

Hints

  • 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 MQTT hierarchy. Subscribe 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 like this
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
            }
        
        },
        _ => {}
        }
    }
}
}

Hints!

  • 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 espflash --release --example solution2 --monitor /dev/tty.usbmodem0, while the solution without can be run with cargo espflash --release --monitor /dev/tty.usbmodem0 or cargo espflash --release --example solution1 --monitor /dev/tty.usbmodem0

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 do not 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)