IoT-For-Beginners 第 4 课深度指南:用 MQTT 将物联网设备接入互联网——遥测、命令与夜灯远程控制实战
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
本指南以开源课程《IoT-For-Beginners》第 4 课(1-getting-started/lessons/4-connect-internet/README.md)为核心,系统讲解物联网设备接入互联网的关键机制:publish/subscribe 通信模型、MQTT 协议核心特性、遥测(telemetry)数据上行与命令(commands)下行。课程将带你动手改造上一课完成的"夜灯"项目——通过公共 MQTT broker(test.mosquitto.org)把光照传感器读数发送到服务端,再由服务端代码判断后回传指令控制 LED 开关,完整走通"设备采集 → 云端处理 → 反向控制"的闭环。学完本篇,你将掌握 MQTT 主题、QoS、retained 消息等概念,并能在 Wio Terminal(Arduino)与树莓派/虚拟设备两种平台上实现 IoT 双向通信。
从"IoT"中的 I 说起:为什么需要通信协议
IoT 中的I 代表 Internet(互联网)。云连接和云端服务支撑了 IoT 设备的大部分能力——从采集连接在设备上的传感器测量值,到发送消息控制执行器。典型架构中,IoT 设备通过标准通信协议连接到单个云端 IoT 服务,该服务再与 IoT 应用的其他部分打通:用于基于数据做智能决策的 AI 服务、用于控制或报表的 Web 应用等。
两个贯穿全课的核心方向:
- 遥测(Telemetry):从传感器采集数据并发送到云端,例如光照强度、温度、湿度。
- 命令(Commands):从云端发送到设备、指示其执行某种动作的消息。动作可以是内部操作(如重启、固件更新),也可以是通过执行器输出的物理动作(如开灯)。
通信协议总览:publish/subscribe 与 broker
IoT 设备与互联网通信有若干主流协议,其中最流行的一类基于**发布/订阅(publish/subscribe)**消息模型,借助某种broker(消息代理)完成路由:
- IoT 设备连接到 broker,发布(publish)遥测并订阅(subscribe)命令;
- 云端服务同样连接到 broker,订阅全部遥测消息,并向特定设备或设备组发布命令。
在该模型中,发送方与接收方完全解耦:设备不需要知道云服务是谁,云服务也不需要知道设备是谁,双方只与 broker 交互,通过命名主题路由消息。除了本课重点的 MQTT 之外,常见的 IoT 通信协议还包括 AMQP 与 HTTP/HTTPS。
MQTT 协议深入:轻量级消息传输的机制与特性
MQTT(Message Queueing Telemetry Transport)是目前 IoT 设备最流行的通信协议。它是一个轻量、开放标准的设备间消息协议,1999 年最初设计用于监控石油管道,15 年后由 IBM 以开放标准形式发布。
MQTT 采用"一个 broker + 多个客户端"的拓扑:所有客户端都连接到 broker,broker 负责把消息路由给相关客户端。消息按命名主题路由,而不是直接发给某个客户端——客户端可以向主题发布消息,所有订阅了该主题的客户端都会收到。
主题层级与通配符
主题可以拥有层级结构,客户端可以用通配符订阅层级中的不同级别。例如:
- 温度遥测发布到
/telemetry/temperature; - 湿度遥测发布到
/telemetry/humidity; - 云端应用订阅
/telemetry/*即可同时收到温度与湿度两类遥测消息。
QoS:服务质量三档
消息可以携带服务质量(Quality of Service,QoS),决定投递保障的强弱:
| 级别 | 语义 | 投递保障 |
|---|---|---|
| QoS 0 | 至多一次 | 消息只发送一次,客户端与 broker 不做任何确认,属于"发了就忘(fire and forget)" |
| QoS 1 | 至少一次 | 发送方反复重试,直到收到确认(acknowledged delivery) |
| QoS 2 | 恰好一次 | 发送方与接收方进行两级握手,确保消息只被收到一份(assured delivery) |
思考题:什么场景下必须用"恰好一次"级别的可靠投递,而不是"发了就忘"?
关于"队列"的澄清、retained 标志与 keep alive
尽管协议名中包含Message Queueing,但 MQTT实际上并不支持消息队列。这意味着:客户端断线后重连,将收不到断线期间发送的消息(QoS 处理流程中已经开始处理的消息除外)。
MQTT 提供以下三个关键补充机制:
- retained 标志:消息可以设置保留标志。若设置了该标志,broker 会保存该主题上最近一条带此标志的消息,并在之后订阅该主题的客户端上线时发给它。这样新订阅者总能拿到该主题的"最新消息"。
- keep alive:MQTT 支持保活机制,在消息间隔较长时检查连接是否仍然存活。
- 安全与传输:MQTT 连接可以是公开开放的,也可以用用户名/密码或证书加密保护。MQTT 走TCP/IP(与 HTTP 相同的底层网络协议,但端口不同);还可以通过websocket与浏览器中的 Web 应用通信,或在防火墙/网络规则阻断标准 MQTT 连接时使用。
动手第一步:把夜灯设备连接到 MQTT broker
现在进入实战。本节为夜灯添加互联网控制的第一步——连接到 MQTT broker。
课程不要求自己搭建 broker,而是直接使用Eclipse Mosquitto(开源 MQTT broker)运营的公共测试服务器test.mosquitto.org。该测试 broker 无需注册账号,非常适合测试 MQTT 客户端与服务端。
⚠️ 注意:该测试 broker 是公开且不安全的——任何人都可能监听你发布的内容,因此绝不能用于任何需要保密的私有数据。
整体流程如下:IoT 设备将光照水平作为遥测通过 MQTT 发布到公共 broker,你编写的服务端代码捕获该消息,检查光照水平后回发一条命令消息,指示设备开/关 LED。
一个真实的同类场景是:在拥有大量灯具的场所(如体育场),汇总多个光传感器的数据后再决定是否开灯——如果只有一个传感器被云或鸟遮挡而其他传感器检测到足够光线,就可以避免误开灯。
根据硬件平台选择对应步骤:
- Arduino - Wio Terminal
- 单板计算机 - Raspberry Pi / 虚拟 IoT 设备
仓库中的连接实现参考
以 Python 版本为例,连接代码展示在仓库的code-mqtt目录中。虚拟设备版本(code-mqtt/virtual-device/nightlight/app.py)与树莓派版本(code-mqtt/pi/nightlight/app.py)的核心连接逻辑一致:
id = '<ID>' client_name = id + 'nightlight_client' mqtt_client = mqtt.Client(client_name) mqtt_client.connect('test.mosquitto.org') mqtt_client.loop_start() print("MQTT connected!")关键点在于id必须全局唯一(可自行替换<ID>为任意标识串),它用于构造客户端名称与后续的主题名,保证多个学习者之间互不串扰。loop_start()会在后台线程启动网络处理循环,让客户端持续收发消息。Wio Terminal(Arduino)版本的完整工程位于 code-mqtt/wio-terminal 目录。
遥测(Telemetry):把传感器数据送上天
Telemetry一词源自希腊语词根,含义是"远程测量"——即从传感器收集数据并发送到云端的行为。有趣的是,最早的遥测设备之一于 1874 年在法国发明,用物理导线把勃朗峰(Mont Blanc)的实时天气与积雪深度发送到巴黎。
回到第 1 课引入的智能恒温器例子:恒温器用温度传感器收集遥测,通常内置一个温度传感器,还可以通过蓝牙低功耗(BLE)等无线协议连接多个外部温度传感器。它可能发送的遥测数据示例如下:
| 名称 | 值 | 描述 |
|---|---|---|
thermostat_temperature | 18°C | 恒温器内置温度传感器测得的温度 |
livingroom_temperature | 19°C | 名为livingroom的远程温度传感器测得的温度,用于标识所在房间 |
bedroom_temperature | 21°C | 名为bedroom的远程温度传感器测得的温度,用于标识所在房间 |
云端服务可以利用这些遥测数据,决策发送何种命令来控制供暖。
从 IoT 设备发送遥测:JSON 编码
为夜灯添加互联网控制的下一步,是把光照水平遥测发布到 MQTT broker 的遥测主题上。消息以JSON(JavaScript Object Notation)编码——一种用键值对在文本中编码数据的标准格式。
按硬件平台选择对应步骤:
- Arduino - Wio Terminal
- 单板计算机 - Raspberry Pi / 虚拟 IoT 设备
以 Python 虚拟设备版本(code-telemetry/virtual-device/nightlight/app.py)为例,遥测发布代码为:
client_telemetry_topic = id + '/telemetry' # ... while True: light = light_sensor.light telemetry = json.dumps({'light' : light}) print("Sending telemetry ", telemetry) mqtt_client.publish(client_telemetry_topic, telemetry) time.sleep(5)设备每 5 秒读取一次光传感器,用json.dumps把{'light': 读数}序列化为 JSON 字符串,发布到id + '/telemetry'主题。
Wio Terminal(Arduino)版本则借助ArduinoJson库完成同样的工作。在platformio.ini的lib_deps中添加依赖(bblanchon/ArduinoJson @ 6.17.3),在config.h中定义主题名const string CLIENT_TELEMETRY_TOPIC = ID + "/telemetry";,然后在loop中读取模拟引脚WIO_LIGHT的数值、构造 JSON 文档并发布:
int light = analogRead(WIO_LIGHT); DynamicJsonDocument doc(1024); doc["light"] = light; string telemetry; serializeJson(doc, telemetry); Serial.print("Sending telemetry "); Serial.println(telemetry.c_str()); client.publish(CLIENT_TELEMETRY_TOPIC.c_str(), telemetry.c_str());Wio Terminal 的完整工程位于 code-telemetry/wio-terminal,通过串口监视器可以看到类似Sending telemetry {"light":652}的输出。
服务端:接收遥测的 Python 程序
只发不收没有意义——光照遥测需要监听端来处理数据。这种"服务端"代码正是大型 IoT 应用中部署到云服务的代码类型,但本课先在本地电脑(或在树莓派上直接编程)运行。
安装 Python 与 VS Code
若本地尚未安装 Python 和 VS Code,需要先安装(使用虚拟 IoT 设备或在树莓派上工作时可跳过,环境已就绪):
- 安装 Python(建议最新稳定版)。
- 安装 Visual Studio Code。
- 安装 VS Code 的Pylance扩展(提供 Python 语言支持)。
你也可以使用任何惯用的 Python IDE 或编辑器,但本课操作说明基于 VS Code。
配置 Python 虚拟环境并安装 paho-mqtt
Python 的强项之一是通过pip安装第三方包。但默认安装的包全局可用,容易引发版本冲突——例如某应用依赖包版本 A,而另一个应用安装新版本后导致前者不可用。解决方案是Python 虚拟环境(venv):它本质上是专用文件夹中的一份 Python 副本,pip安装的包只进入该文件夹。
操作步骤:
mkdir nightlight-server cd nightlight-serverpython3 -m venv .venv💁 创建虚拟环境时须显式调用
python3,以防机器上还装有 Python 2 时python指向旧版本。
激活虚拟环境:
- Windows 命令提示符:
.venv\Scripts\activate.bat - Windows PowerShell:
.\.venv\Scripts\Activate.ps1 - macOS / Linux:
source ./.venv/bin/activate
激活后,默认python命令将运行创建虚拟环境所用的 Python 版本,用python --version确认(3.6 及以上均可)。最后安装 MQTT 库:
pip install paho-mqttpaho-mqtt是流行的 Python MQTT 库,且只安装在该虚拟环境内部。
编写服务端代码 app.py
在虚拟环境内创建app.py(Windows 用type nul > app.py,macOS/Linux 用touch app.py),再用code .打开当前文件夹。VS Code 会自动识别并激活虚拟环境(状态栏可见),若已有终端未激活,可用Kill the active terminal instance按钮结束旧终端,再通过Terminal -> New Terminal新建终端加载虚拟环境。
app.py的完整代码(与 code-server/server/app.py 一致):
import json import time import paho.mqtt.client as mqtt id = '<ID>' client_telemetry_topic = id + '/telemetry' 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) mqtt_client.subscribe(client_telemetry_topic) mqtt_client.on_message = handle_telemetry while True: time.sleep(2)请把<ID>替换为创建设备代码时使用的同一唯一 ID。⚠️ 该 ID 必须与设备端完全一致,否则服务端代码将订阅/发布到错误的主题,导致两端失联。
这段代码的逻辑:
- 以唯一名称创建 MQTT 客户端,连接
test.mosquitto.orgbroker; loop_start()在后台线程启动处理循环,持续监听已订阅主题的消息;- 订阅遥测主题,并把
handle_telemetry注册为消息回调; handle_telemetry用json.loads解码 JSON 负载并打印到控制台;while True死循环保持主程序存活,后台线程则持续监听消息。
运行服务端:
python app.py确保设备在运行并发送遥测后,调节物理或虚拟设备检测到的光照水平,终端会实时打印:
(.venv) ➜ nightlight-server python app.py Message received: {'light': 0} Message received: {'light': 400}注意:设备端nightlight虚拟环境中的app.py必须保持运行,nightlight-server虚拟环境中的app.py才能收到消息。
遥测应该多久发送一次?
这是遥测设计的关键权衡:采样越频繁,对变化的响应越快,但功耗、带宽、数据量、云端处理成本也越高——需要"足够频繁,但不过于频繁"。
- 恒温器:每几分钟测量一次通常绰绰有余,因为温度变化没那么快。每天只测一次,可能在大晴天正午仍按夜间温度供暖;每秒测一次,则会积累成千上万条重复的温度测量,浪费用户带宽(对流量受限用户是问题)、消耗更多电量(对电池供电的远程传感器是问题),并抬高云端计算与存储成本。
- 工厂机械监控:如果设备故障可能造成灾难性损坏和数百万美元损失,可能必须每秒多次测量——浪费带宽好过漏掉"机械需要停机检修"的遥测信号。
在这种高要求场景下,可考虑先用**边缘设备(edge device)**处理遥测,降低对互联网的依赖。
断连怎么办?
互联网连接不可靠,断网时有发生。IoT 设备应丢弃数据还是暂存到恢复连接后补发?答案依然是"看情况":
- 恒温器:一旦新的温度测量完成,旧数据即可丢弃——供暖系统不关心 20 分钟前是 20.5°C,只要现在是 19°C,决定开关供暖的是当前温度。
- 机械设备:可能希望保留所有数据,尤其是用于趋势分析时。存在能检测数据流异常的机器学习模型(观察最近一小时等时间段的数据并识别异常),常用于预测性维护——寻找"即将故障"的迹象以便提前维修更换。此时可能希望每条遥测都被发送用于异常检测,一旦设备重连就把断网期间产生的全部遥测补发。
设备设计者还应考虑:断网或信号差(如地下车库)时设备能否降级工作?智能恒温器在无法向云端发送遥测时,也应具备有限的本地供暖决策能力。这好比一辆因在地下车库无信号环境尝试 OTA 升级而被"变砖"的汽车——完全依赖云端的设备在连接丢失时会彻底失效。
对 MQTT 而言,处理连接丢失需要设备与服务端代码共同负责保障投递:例如要求所有发出的消息都在回复主题上收到确认,否则手动入队、待重连后重放。
命令(Commands):从云端反向控制设备
命令是从云端发送到设备、指示其执行动作的消息。多数时候命令通过执行器产生某种输出,但也可以是指令设备本身——例如重启,或采集额外遥测并作为命令的响应返回。
恒温器可能收到云端发来的"打开供暖"命令:云服务根据所有传感器的遥测数据做出"应开启供暖"的决策后,发送相应命令。
服务端发送命令:基于光照阈值决策
为互联网控制的夜灯添加命令环节:服务端代码根据感知到的光照水平,向 IoT 设备发送控制灯光的命令。
在服务端代码中,于client_telemetry_topic声明之后增加命令主题:
server_command_topic = id + '/commands'然后在handle_telemetry函数末尾追加:
command = { 'led_on' : payload['light'] < 300 } print("Sending message:", command) client.publish(server_command_topic, json.dumps(command))这段代码向命令主题发送一条 JSON 消息,led_on根据光照是否小于 300 设为true或false——光照小于 300 时发送true,指示设备打开 LED。完整代码见 code-commands/server/app.py。
运行后调节光照,终端输出遥测接收与命令发送的完整闭环:
(.venv) ➜ nightlight-server python app.py Message received: {'light': 0} Sending message: {'led_on': True} Message received: {'light': 400} Sending message: {'led_on': False}💁 本实现中,遥测与命令各自只用一个主题,因此多个设备的遥测会出现在同一遥测主题、命令也会出现在同一命令主题。若要向特定设备发送命令,可以用唯一设备 ID 命名多个主题,例如
/commands/device1、/commands/device2,这样设备只监听发给自己的消息。
设备端处理命令并控制 LED
服务端发送命令后,需要在 IoT 设备侧增加处理逻辑来驱动 LED。按硬件平台选择对应步骤:
- Arduino - Wio Terminal
- 单板计算机 - Raspberry Pi / 虚拟 IoT 设备
Python 版本的完整实现见 code-commands/virtual-device/nightlight/app.py(虚拟设备)与 code-commands/pi/nightlight/app.py(树莓派):
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)设备订阅id + '/commands'主题,注册handle_command回调:收到命令后解码 JSON、读取led_on字段并调用led.on()/led.off()控制 LED;主循环仍持续采集并发布光照遥测。
Wio Terminal(Arduino)版本在 code-commands/wio-terminal 中。其实现要点:
config.h中定义const string SERVER_COMMAND_TOPIC = ID + "/commands";;- 在
reconnectMQTTClient函数末尾调用client.subscribe(SERVER_COMMAND_TOPIC.c_str()),保证重连后自动恢复订阅; - 定义
clientCallback回调:消息以无符号 8 位整数数组到达,需先转为字符数组;再用 ArduinoJson 的deserializeJson解码 JSON 文档,读取led_on字段后通过digitalWrite(D0, HIGH/LOW)控制 LED; - 在
createMQTTClient中通过client.setCallback(clientCallback)注册回调。
💁 注意:
clientCallback会处理所有已订阅主题的消息。若以后订阅多个主题,可从回调的topic参数判断消息来自哪个主题。
全部代码写好后运行,调节光照水平即可观察完整效果:服务端收到遥测、决策并发送命令,设备端打印收到的命令并相应点亮/熄灭 LED。
命令与断连
云服务需要向离线设备发送命令时怎么办?同样取决于场景:
- 若最新命令覆盖旧命令,旧命令可以忽略。例如云端先发送"打开供暖"再发送"关闭供暖",那么"打开"命令可以忽略且无需补发。
- 若命令必须按顺序处理(如先上移机械臂、再闭合夹爪),则必须在连接恢复后按顺序补发。
思考题:如果需要,设备端或服务端代码如何确保命令始终按正确顺序通过 MQTT 发送和处理?
实践练习与延伸学习
挑战:延续前三课的练习——尽可能多地列出家中、学校或工作场所的 IoT 设备,判断它们基于微控制器还是单板计算机(或两者混合),并思考它们使用的传感器和执行器。对每个设备,进一步分析:它们发送什么遥测?可能接收什么消息或命令?它们安全吗?
延伸学习:
自行运行 MQTT broker(例如 Mosquitto),并从 IoT 设备与服务端代码连接它。注意:默认配置下 Mosquitto不允许匿名连接(无用户名/密码),也不接受运行主机之外的连接。可通过
mosquitto.conf配置文件开启(仅建议本地实验):listener 1883 0.0.0.0 allow_anonymous true完成课后作业:对比 MQTT 与其他通信协议,从协议机制、适用场景、可靠性保障等维度做横向分析。
小结:一条完整的 IoT 双向通信链路
本课完整走通了 IoT 双向通信的最小闭环:设备端读取光传感器 → JSON 编码后经 MQTT 发布到/telemetry主题 → 本地服务端订阅并解码遥测 → 依据阈值(光照 < 300)决策 → 向/commands主题发布命令 → 设备端回调解码命令、驱动 LED。四份渐进式代码目录(code-mqtt、code-telemetry、code-server、code-commands)恰好对应这一学习路径的每个里程碑,读者可在 1-getting-started/lessons/4-connect-internet 目录下逐一对照验证。理解 MQTT 的主题路由、QoS、retained 与 keep alive 机制,以及遥测频率、断连处理等设计权衡,是构建可靠、可扩展的 IoT 应用的基础能力。
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考