IoT-For-Beginners 实战:Raspberry Pi 与虚拟设备通过 MQTT 订阅命令远程控制夜灯
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
导读
本文聚焦于 Microsoft「IoT-For-Beginners」课程第 4 课「Connect your device to the Internet」中的设备端环节:让运行在Raspberry Pi 或虚拟 IoT 设备上的夜灯(nightlight)项目订阅来自 MQTT 代理的commands(命令),根据服务端下发的led_on指令点亮或熄灭 LED,从而完成「光照遥测上传 → 云端判断 → 命令下发 → 执行器动作」的完整闭环。读完本文,你将掌握 Paho-MQTT 客户端订阅主题、注册on_message回调、解析 JSON 命令负载并驱动 Grove LED 的完整方法,并能对照仓库源码独立复现这一可远程控制的物联网应用。
一、场景回顾:遥测上行、命令下行
在进入命令订阅之前,需要先明确这条链路在课程中的位置。本课共分为三个阶段,前两个阶段是本文的前提:
- 连接 MQTT 代理(single-board-computer-mqtt.md):为设备安装
paho-mqtt依赖,创建id + 'nightlight_client'的客户端名称并连接公共测试代理test.mosquitto.org; - 发布遥测(single-board-computer-telemetry.md):将光敏传感器读数封装为 JSON(
{"light": <数值>}),发布到id + '/telemetry'主题; - 订阅命令(即本文):订阅
id + '/commands'主题,接收服务端回传的{"led_on": true/false}指令并驱动 LED。
整条数据流可以用课程 README 中的流程图概括:设备读取光强后发布遥测,运行在本地(或 Pi 上)的「服务器代码」订阅遥测、判断光强阈值,再向命令主题发布控制指令,设备收到指令后点亮或熄灭 LED。
💡 该课程文档中使用的是
test.mosquitto.org公共测试代理,无需注册账号即可使用,但它公开且不加密,任何数据都可能被监听,因此只适合教学测试,严禁用于任何需要保密的业务数据。
在 MQTT 的发布/订阅模型下,命令是「云端 → 设备」方向的消息。课程 README 用恒温器做了类比:云端根据多个传感器的遥测数据做出决策后,向设备发送「打开暖气」的命令,设备端再通过执行器产生实际输出。
二、定义命令主题:server_command_topic
设备端的第一步,是在遥测主题定义之后声明命令主题。打开夜灯项目(Raspberry Pi 直接编辑,虚拟设备则需确保终端处于虚拟环境中),在client_telemetry_topic定义之后添加:
server_command_topic = id + '/commands'这里的id是你此前为设备生成并填写在代码中的唯一标识(如 GUID),它同时被用于客户端名称、遥测主题和命令主题的命名,起到命名空间隔离的作用。由于test.mosquitto.org是公共代理,很多学习者共用同一 broker,只有使用唯一 ID才能保证你的主题不会与他人冲突。
server_command_topic就是设备要订阅的、用来接收 LED 控制命令的 MQTT 主题。对应仓库中的完整实现见 code-commands/virtual-device/nightlight/app.py:
id = '<ID>' client_telemetry_topic = id + '/telemetry' server_command_topic = id + '/commands' client_name = id + 'nightlight_client'三、编写命令处理函数并订阅
3.1handle_command:解析 JSON 并驱动 LED
在主循环之前、mqtt_client.loop_start()之后添加以下代码:
def handle_command(client, userdata, message): payload = json.loads(message.payload.decode()) print("Message received:", payload) if payload['led_on']: led.on() else: led.off() mqtt_client.subscribe(server_command_topic) mqtt_client.on_message = handle_command这段代码做了三件事:
- 解码负载:
message.payload.decode()将 MQTT 消息的字节负载转为字符串,json.loads(...)再将其解析为 Python 字典; - 读取指令:从字典中取出
led_on键——服务端发送的{"led_on": true}表示点亮 LED,{"led_on": false}表示熄灭 LED; - 执行动作:分别调用
led.on()与led.off()驱动 Grove LED 执行器。
随后,mqtt_client.subscribe(server_command_topic)让客户端订阅命令主题;mqtt_client.on_message = handle_command将handle_command注册为消息到达时的回调函数。
💁 关键细节:
on_message回调对所有已订阅主题都会触发。如果后续代码同时监听多个主题,需要通过回调传入的message对象获取消息实际来自哪个主题(即message.topic属性),据此分发处理逻辑。
3.2 回调执行在后台线程
需要理解loop_start()与回调之间的关系。mqtt_client.loop_start()会启动一个后台线程持续处理网络收发与消息分发,因此主线程中的while True:无限循环(发布遥测)可以与命令接收并行运行,二者互不阻塞。这正是从源码中可以确认的设计:
mqtt_client.loop_start() def handle_command(client, userdata, message): ... mqtt_client.subscribe(server_command_topic) mqtt_client.on_message = handle_command while True: light = light_sensor.light print('Light level:', light) mqtt_client.publish(client_telemetry_topic, json.dumps({'light' : light})) time.sleep(5)四、完整设备代码对照
4.1 虚拟设备版本(CounterFit)
仓库 code-commands/virtual-device/nightlight/app.py 给出了虚拟设备的完整实现:
import time from counterfit_connection import CounterFitConnection from counterfit_shims_grove.grove_light_sensor_v1_2 import GroveLightSensor from counterfit_shims_grove.grove_led import GroveLed import json import paho.mqtt.client as mqtt CounterFitConnection.init('127.0.0.1', 5000) light_sensor = GroveLightSensor(0) led = GroveLed(5) id = '<ID>' client_telemetry_topic = id + '/telemetry' server_command_topic = id + '/commands' client_name = id + 'nightlight_client' mqtt_client = mqtt.Client(client_name) mqtt_client.connect('test.mosquitto.org') mqtt_client.loop_start() def handle_command(client, userdata, message): payload = json.loads(message.payload.decode()) print("Message received:", payload) if payload['led_on']: led.on() else: led.off() mqtt_client.subscribe(server_command_topic) mqtt_client.on_message = handle_command while True: light = light_sensor.light print('Light level:', light) mqtt_client.publish(client_telemetry_topic, json.dumps({'light' : light})) time.sleep(5)虚拟设备版本与 Pi 版本的差异仅在于硬件抽象层:虚拟设备通过counterfit_connection.CounterFitConnection.init('127.0.0.1', 5000)连接本机运行的 CounterFit 模拟应用,传感器与 LED 均来自counterfit_shims_grove模拟库,并在 CounterFit 界面中创建到对应引脚(光敏传感器在引脚 0,LED 在引脚 5)。运行前务必保证 CounterFit 应用已启动,且光敏传感器与 LED 已创建在正确引脚上。
4.2 Raspberry Pi 版本(真实硬件)
仓库 code-commands/pi/nightlight/app.py 提供了真实硬件的实现,核心逻辑与虚拟设备完全一致,仅替换了 Grove 驱动库:
import time from grove.grove_light_sensor_v1_2 import GroveLightSensor from grove.grove_led import GroveLed import json import paho.mqtt.client as mqtt light_sensor = GroveLightSensor(0) led = GroveLed(5) id = '<ID>' client_telemetry_topic = id + '/telemetry' server_command_topic = id + '/commands' client_name = id + 'nightlight_client' mqtt_client = mqtt.Client(client_name) mqtt_client.connect('test.mosquitto.org') mqtt_client.loop_start() def handle_command(client, userdata, message): payload = json.loads(message.payload.decode()) print("Message received:", payload) if payload['led_on']: led.on() else: led.off() mqtt_client.subscribe(server_command_topic) mqtt_client.on_message = handle_command while True: light = light_sensor.light print('Light level:', light) mqtt_client.publish(client_telemetry_topic, json.dumps({'light' : light})) time.sleep(5)两种环境下的handle_command、主题定义、订阅与回调注册逻辑完全同构,这意味着你可以在 CounterFit 虚拟环境中完成调试后,将同一套 MQTT 逻辑直接迁移到真实 Pi 硬件上。
五、配套服务端:命令从哪里来?
为了让命令真正「发得出去」,课程还提供了一段运行在本地(或 Pi 上)的服务器代码,见 code-commands/server/app.py。它订阅遥测主题,根据光强阈值反向生成命令并发布:
import json import time import paho.mqtt.client as mqtt id = '<ID>' client_telemetry_topic = id + '/telemetry' server_command_topic = id + '/commands' client_name = id + 'nightlight_server' mqtt_client = mqtt.Client(client_name) mqtt_client.connect('test.mosquitto.org') mqtt_client.loop_start() def handle_telemetry(client, userdata, message): payload = json.loads(message.payload.decode()) print("Message received:", payload) command = { 'led_on' : payload['light'] < 300 } print("Sending message:", command) client.publish(server_command_topic, json.dumps(command)) mqtt_client.subscribe(client_telemetry_topic) mqtt_client.on_message = handle_telemetry while True: time.sleep(2)关键点:
- 阈值逻辑:
command = { 'led_on' : payload['light'] < 300 }—— 当遥测光强小于 300 时led_on为True,命令设备开灯;否则为False,命令关灯; - 对称主题:服务端用与设备端完全相同的
id + '/commands'主题发布命令,这正是两端能对上暗号的根本原因; - ⚠️ ID 一致性:服务器代码中的
<ID>必须与设备端使用的 ID 完全一致,否则服务器订阅不到设备的遥测,设备也收不到服务器发布的命令; - 命令负载同样是 JSON 文本(
json.dumps编码),与设备端json.loads(message.payload.decode())的解析一一对应。
运行服务器后,调整物理或虚拟设备的光照强度,终端会交替打印遥测与命令,LED 也随之亮灭,例如:
(.venv) ➜ nightlight-server python app.py Message received: {'light': 0} Sending message: {'led_on': True} Message received: {'light': 400} Sending message: {'led_on': False}六、运行与验证
- 先用前一课的方法运行设备端代码(
python app.py)。虚拟设备环境请确认 CounterFit 已运行、光敏传感器与 LED 已就位;Pi 环境无需虚拟环境。 - 在另一个终端运行服务器代码(同样
python app.py)。 - 遮挡或照射光敏传感器,改变检测到的光照水平。设备端终端会打印
Message received: {...},LED 会按命令点亮或熄灭。
整个闭环即告完成:遥测上行(设备 → broker)→ 服务端决策 → 命令下行(broker → 设备)→ LED 动作。
七、深入理解:主题设计与多设备扩展
课程 README 对命令主题设计给出了重要补充,值得在实现时一并考虑:
- 单主题广播:本文方案中所有设备共用
id + '/commands'这一条命令主题,适合单设备或广播场景; - 按设备拆分主题:若需向特定设备下发命令,可使用带唯一设备 ID 的多主题,例如
/commands/device1、/commands/device2,让每台设备只订阅属于自己的主题,例如id + '/commands'与/commands/device1这类设计,避免设备间命令串扰; - 通配符订阅:MQTT 主题支持层级与通配符,例如订阅
/commands/*可接收全部设备的命令(也可利用message.topic区分来源); - QoS 等级:MQTT 支持「至多一次 / 至少一次 / 恰好一次」三种服务质量,命令类消息通常比遥测更依赖可靠投递,可按需为
subscribe/publish指定 QoS; - 离线补偿:MQTT 本身不提供消息队列,设备断线重连后不会收到断开期间的消息;若命令有先后顺序要求(如机械臂先抬升再合拢夹爪),需要在应用层设计确认与重放机制。
八、小结
至此,你已经完成了「Internet 控制夜灯」项目在**单板计算机(Raspberry Pi / 虚拟设备)**上的全部设备端工作:通过server_command_topic定义命令主题,用handle_command回调解析 JSON 命令并驱动 Grove LED,再借助loop_start()后台线程实现遥测发布与命令接收的并行。整个项目的源码分别存放在:
- 虚拟设备完整实现:code-commands/virtual-device/nightlight/app.py
- Raspberry Pi 完整实现:code-commands/pi/nightlight/app.py
- 服务端决策与命令发布实现:code-commands/server/app.py
若使用 Arduino(Wio Terminal)平台,请参考同课的 wio-terminal-commands.md;完整的课程背景与 MQTT 协议讲解见 课程 README。这套「遥测 + 命令」的发布/订阅模式是绝大多数物联网云控应用的通用骨架,掌握后即可迁移到智能家居、远程监测等更多真实场景。
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考