MQTT 练习:接收 LED 命令

✅ 订阅 color_topic(uuid) 主题

✅ 在单独的终端里运行 host_clienthost_client 大约每秒会发布一个开发板 LED 的颜色 color

✅ 通过记录从这个主题收到的信息,来验证订阅是否有效。

✅ 对 LED 命令作出响应:用 led.set_pixel(/* 收到的颜色 */) 函数把新收到的颜色设置到板子上。

intro/mqtt/exercise/solution/solution_publ_rcv.rs 包含解答。你可以用下面的命令运行它:

cargo run --example solution_publ_rcv

编码和解码消息 payload

开发板 LED 命令包含三个字节,分别表示红、绿、蓝。

  • enum ColorData 包含一个主题 color_topic(uuid)BoardLed
  • 可以使用 try_from() 来转换 EspMqttMessagedata() 字段。首先需要用 let message_data: &[u8] = &message.data(); 将消息强制转换为 slice

#![allow(unused)]
fn main() {
// RGB LED 命令

if let Ok(ColorData::BoardLed(color)) = ColorData::try_from(message_data) { /* 在这里设置新的颜色 */ }
}

发布 & 订阅

EspMqttClient 不止负责发布消息,也用于订阅主题。


#![allow(unused)]
fn main() {
let subscribe_topic = /* ... */;
client.subscribe(subscribe_topic, QoS::AtLeastOnce)
}

处理收到的消息

处理函数闭包里的 message_event 参数的类型是 Result<Event<EspMqttMessage>。 因为我们只对接收成功的消息感兴趣,我们可以在闭包里使用模式匹配:


#![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),
        },
    )?;
}

在处理函数中,我们将会处理 Complete 消息。

💡 使用 Rust Analyzer 来生成缺失的 match 分支,或者匹配所有其他类型,输出一个 info!()


#![allow(unused)]
fn main() {
match message.details() {
    // 本练习中的消息都会是 `Complete` 类型的
    // `Details` 枚举的其他变体用于更大的消息 payload
    Complete => {

        // Cow<&[u8]> 可以被强制转换为 slice &[u8] 或 Vec<u8>
        // 你可以将它强制转换为 slice ,用 try_from() 发送
        let message_data: &[u8] = &message.data();
        if let Ok(ColorData::BoardLed(color)) = ColorData::try_from(message_data) {
            // 把 LED 配置为新收到的颜色

        }
    }
    // 使用 Rust Analyzer 来生成缺失的 match 分支,或者匹配非 complete 的消息来输出日志消息。
}
}

💡 用 logger 来查看接收到的东西,例如:info!("{}", color);dbg!(color)

额外的任务

实现具有分层主题的 MQTT

✅ 如果你已经完成了所有其他工作,可以考虑实现这个任务。我们不提供完整的解答,因为这是用于测试你自己能走多远。

检查 common/lib/mqtt-messages

✅ 使用分层主题的 MQTT 实现相同的功能。订阅所有的“命令”消息,在 cmd_topic_fragment(uuid) 后面加一个 # 通配符。

✅ 用 enum Command 代替 enum ColorDataenum Command 表示所有可能的命令(这里仅有 BoardLed)。

RawCommandData 存储了消息主题的最后一部分(例如 a-uuid/command/board_led 中的 board_led)。可以用 try_from 将其转换为 Command


#![allow(unused)]
fn main() {
// RGB LED 命令
let raw = RawCommandData {
    path: command,
    data: message.data(),
};

}

检查 host-client:

✅ 你需要将 color 替换成 command。例如:


#![allow(unused)]
fn main() {
let command = Command::BoardLed(color)
}

✅ 在 process_message() 函数中,你需要解析主题。


#![allow(unused)]
fn main() {
match message.details() {
    Complete => {
        // 本练习中的消息都会是 `Complete` 类型的
        // `Details` 枚举的其他变体用于更大的消息 payload
        // 

        // Cow<str> 的行为很像其他 Rust 字符串 (&str, String)
        let topic: Cow<str> = message.topic(token);

        // 确认我们是否对这个主题感兴趣
        // 并根据它的内容来分发
        let is_command_topic: bool = /* ... */;
        if is_command_topic {
            let raw = RawCommandData { /* ... */ };
            if let Ok(Command::BoardLed(color)) = Command::try_from(raw) {
                // 把 LED 配置为新收到的颜色
            }

        },
        _ => {}
    }
}
}

💡 由于你需要遍历 MQTT 主题,你需要对字符串执行 split() 并得到一个迭代器。你可以用 nth() 来直接访问迭代器中的特定项。 💡 可以用 cargo run --example solution2 运行实现了层次结构的解答。可以用 cargo runcargo run --example solution1 运行未实现层次结构的解答。

其他任务

✅ 利用 serde_json 将消息数据编码/解码为 JSON。

✅ 从主机客户端上发送一些带有大量 payload 的消息,并在微控制器上处理它们。大体积的消息将会分部分传递,而不是使用 Details::Complete


#![allow(unused)]
fn main() {
InitialChunk(chunk_info) => { /* 第一块 */},
SubsequentChunk(chunk_data) => { /* 所有后续块 */ }
}

💡 不需要根据消息 ID 来区分收到的块,因为在任意时刻,最多只有一条消息正在传输。

Troubleshooting

  • 构建示例客户端时出现 error: expected expression, found .:将你的 stable Rust 更新到 1.58 或更新的版本
  • 没有显示 MQTT 消息?确保所有客户端(板子和电脑)使用的是相同的 UUID(你可以在日志输出中看见它)