相关知识链接:
2、MQTT协议详解
4、ESP32指南(网络入门-MQTT协议)超有用!!!!!!!
一、网关
1、网关定义
网关(Gateway)又称网间连接器、协议转换器,是一种复杂的网络连接设备,在不同的网络环境中扮演着至关重要的角色。
网关是在采用不同体系结构或协议的网络之间进行通信时,用于连接、转换和管理数据传输的设备或软件程序。它工作在网络层(OSI 模型的第三层)及以上,能够理解不同网络协议的规则和格式,实现不同网络之间的互联互通。
2、主要功能
- 协议转换 :这是网关最核心的功能。例如,在物联网中,传感器设备可能使用 LoRaWAN、ZigBee 等低功耗通信协议,而网络层通常基于 TCP/IP 协议。网关能够将 LoRaWAN 协议传输的数据转换为 TCP/IP 协议可识别的格式,反之亦然,从而让不同协议的设备可以进行通信。
- 数据处理与过滤:网关可以对传输的数据进行预处理,如数据清洗、格式转换、数据聚合等。比如,智能家居网关可以收集多个传感器(温度、湿度、光照等)的数据,将其整理后再发送到云端服务器,减少不必要的数据传输量,提高网络传输效率 。同时,网关还能根据预设规则对数据进行过滤,只允许符合特定条件的数据通过,增强网络安全性。
- 网络安全保障:网关充当网络的安全屏障,通过访问控制、身份认证、加密和解密等手段,保护内部网络免受外部非法访问和攻击。例如,企业网关可以阻止外部未经授权的设备访问企业内部网络,防止数据泄露和恶意软件入侵。
- 设备管理:在物联网场景中,网关可以对连接到它的各种设备进行管理和监控,包括设备的注册、配置、状态监测和故障诊断等。比如,智能家居网关能够实时查看各个智能设备(智能灯泡、智能门锁等)的工作状态,当设备出现故障时及时发出警报 。
- 路由选择:当有多个网络路径可供数据传输时,网关可以根据网络的负载情况、传输距离、带宽等因素,选择最优的路由路径,确保数据能够高效、稳定地传输。
3、常见应用场景
- 家庭网络:家庭网关通常集成了路由器、调制解调器等功能,它将家庭内部的 Wi-Fi 网络(如手机、电脑、智能电视等设备连接的网络)与外部的互联网连接起来,实现家庭设备的上网需求。同时,智能家居网关还能连接各种智能家居设备,实现设备之间的互联互通和远程控制。
- 工业物联网:在工业生产环境中,网关用于连接不同的工业设备(如传感器、执行器、PLC 等)和企业管理系统。它可以收集生产线上设备的运行数据,将这些数据上传到工业云平台进行分析和处理,帮助企业实现生产过程的优化、设备的预测性维护等。
- 车联网:车载网关可以连接车内不同的电子控制单元(ECU),如发动机控制模块、车身控制系统等,实现车内设备之间的数据共享和通信。同时,它还能将车辆的相关信息(如位置、速度、车况等)通过移动网络上传到云端,支持远程车辆监控、导航、紧急救援等服务。
- 智能建筑:智能建筑网关可以整合建筑内的各种系统,如照明系统、空调系统、安防系统等,实现对这些系统的集中管理和自动化控制,提高建筑的能源效率和安全性。
二、MQTT物联网网关实验
1、介绍
MQTT 物联网网关实验,是利用特定硬件(如 M5Stack CoreS3)结合 MQTT 协议,构建一个能在感知层设备(各类 传感器、执行器等)与网络层(MQTT 服务器所在的网络)之间,实现数据转发、协议转换、设备管理等功能的中间设备(网关)的实验。简单来说,网关就像一个 "翻译官" 和 "交通枢纽",让不同的物联网设备能通过 MQTT 协议顺畅地进行数据交互。
2、实验要实现的功能
- 数据采集与上传:网关能够从连接的传感器(如温湿度传感器、光照传感器等)采集环境或设备数据,然后通过 MQTT 协议将这些数据发布到 MQTT 服务器,供远程客户端(如手机 APP、网页应用等)订阅和查看。
- 指令接收与执行:网关可以订阅 MQTT 服务器上特定的主题,当远程客户端向这些主题发布控制指令时,网关能接收指令,并根据指令控制连接的执行器(如 LED 灯、继电器等),实现对设备的远程操控。
- 协议转换(可选):如果感知层设备使用的是其他通信协议(如 ZigBee、LoRa 等),网关可将这些协议的数据转换为 MQTT 协议数据,再进行上传,反之亦然,实现不同协议设备间的互联互通。
- 设备管理:对连接到网关的设备进行注册、状态监测、故障诊断等管理操作,例如监测传感器是否在线、是否正常工作等。
3、实验重难点
- 硬件连接与驱动适配
- 难点:不同的传感器、执行器模块可能有不同的接口(如 I2C、UART 等)和通信协议,需要正确连接硬件,并为这些外设编写或适配相应的驱动代码,确保硬件能被网关正常识别和控制。例如,在 M5Stack CoreS3 上连接 ENV 温湿度传感器,要确保 I2C 引脚配置正确,且能通过对应的库函数读取传感器数据。
- 重点:熟悉硬件的接口类型和通信时序,掌握相关外设驱动的编写方法,保证硬件层的数据采集和控制功能稳定。
- MQTT 连接与通信稳定性
- 难点:实现网关与 MQTT 服务器的稳定连接,包括处理网络波动、连接断开重连等情况。在网络环境不稳定时,要确保 MQTT 客户端能自动重新连接服务器,且不会丢失重要的发布或订阅消息。同时,要合理设置 MQTT 的 QoS(服务质量)等级,在消息可靠性和网络开销之间取得平衡。
- 重点:理解 MQTT 协议的连接机制、消息发布 / 订阅模式以及 QoS 机制,编写健壮的 MQTT 客户端代码,保证通信的稳定性和可靠性。
- 数据处理与并发控制
- 难点:网关需要同时处理数据采集、MQTT 通信、设备管理等多个任务,这涉及到并发或多任务处理。在资源有限的嵌入式设备(如 M5Stack CoreS3)上,如何高效地调度这些任务,避免任务阻塞,是一个难点。例如,在采集传感器数据的同时,要能及时响应 MQTT 服务器的指令,需要合理设计程序的任务结构,如使用定时器中断、多线程(若支持)或事件驱动等方式。
- 重点:掌握嵌入式系统中的多任务处理方法,设计高效的数据处理和任务调度逻辑,确保网关各功能模块能协同工作,不出现卡顿或响应不及时的情况。
- 安全性保障
- 难点:物联网网关作为连接感知层和网络层的关键设备,其安全性至关重要。需要考虑如何对 MQTT 通信进行加密(如使用 TLS/SSL)、对设备进行身份认证(如用户名 / 密码认证、客户端证书认证),防止非法设备接入和数据被窃听、篡改。
- 重点:了解物联网设备的安全威胁,掌握 MQTT 协议的安全机制,在实验中实现基本的安全防护措施,提高网关的安全性。
三、MQTT通信
1、MQTT 服务器和客户端的作用
- MQTT 服务器(MQTT Broker) :是 MQTT 通信中的核心枢纽,负责接收、存储和转发客户端发布的消息。它就像一个邮局,各个 MQTT 客户端(比如传感器设备、控制终端等)可以向它 "寄信"(发布消息到特定主题 ),也可以从它那里 "取信"(订阅特定主题获取消息 )。服务器管理着所有客户端的连接、订阅关系,确保消息能够准确地发送到订阅了相应主题的客户端。
- MQTT 客户端 :是参与 MQTT 通信的终端设备或应用程序,包括发布消息的设备(如传感器,将采集到的数据发布到 MQTT 服务器 )和订阅消息的设备(如手机 APP,订阅相关主题来获取传感器数据或发送控制指令 ) 。客户端通过与 MQTT 服务器建立连接,实现消息的发布和订阅操作。
四、M5Stack CoreS3 MQTT 物联网
1、实验概述
本实验将使用M5Stack CoreS3 作为核心,搭配温湿度传感器(ENV Unit) 和蜂鸣器,构建一个简单的 MQTT 物联网网关。该网关能够采集环境数据并发送到 MQTT 服务器,同时接收来自服务器的控制指令,实现对 蜂鸣器 的远程控制。
2、所需材料
- M5Stack CoreS3 开发板 ×1
- M5Stack ENV III Unit(温湿度传感器) ×1
- M5Stack LED Unit ×1
- USB Type-C 数据线 ×1
- 可用的 WiFi 网络
- MQTT 服务器(推荐使用公共服务器:test.mosquitto.org)
3、程序
(功能:通过发布主题控制蜂鸣器响和不响;每10s向服务器发布一次获取的环境数据,也可以通过按键发布环境数据)
主程序入口 (main)
├── 初始化阶段
│ ├── init_hardware() - 初始化传感器和蜂鸣器
│ │ ├── I2C总线初始化
│ │ ├── ENV4环境传感器初始化
│ │ └── 蜂鸣器(PWM)初始化
│ └── 网络连接
│ ├── connect_wifi() - WiFi连接
│ └── connect_mqtt() - MQTT连接和订阅
│
├── 主循环
│ ├── MQTT消息检查 (client.check_msg())
│ ├── 定时发布环境数据
│ └── 异常处理
│
└── 清理阶段
└── MQTT断开连接
python
"""
M5Stack Core S3 物联网网关程序
功能:读取环境传感器数据并通过MQTT发布,同时接收MQTT控制蜂鸣器
硬件:M5Stack Core S3 + ENV IV Unit (SHT40+QMP6988)
作者:基于micropython-lib MQTTClient修改
日期:2024-01-20
"""
import network # 网络模块,用于WiFi连接
import time # 时间模块,用于延时和时间戳
from umqtt.simple import MQTTClient # MQTT客户端库
from machine import Pin, I2C, PWM # 硬件控制模块
from unit import ENVUnit # M5Stack环境传感器单元库
import json # JSON数据格式处理
# ============================
# 配置区域
# ============================
# WiFi配置
WIFI_SSID = "填入你的wifi" # WiFi网络名称(SSID)
WIFI_PASSWORD = "填入你的wifi密码" # WiFi密码
# MQTT服务器配置
MQTT_SERVER = "test.mosquitto.org" # MQTT公共测试服务器地址
MQTT_PORT = 1883 # MQTT默认非加密端口
MQTT_CLIENT_ID = "m5stack_core_s3_gateway" # 客户端标识符(必须唯一)
MQTT_USER = "" # MQTT用户名(留空表示不需要认证)
MQTT_PASSWORD = "" # MQTT密码(留空)
# MQTT主题配置
MQTT_PUB_TOPIC = "m5stack/env/data" # 发布环境数据的主题
MQTT_SUB_TOPIC = "m5stack/buzzer/control" # 订阅蜂鸣器控制命令的主题
# ============================
# 全局变量
# ============================
buzzer_state = False # 蜂鸣器状态变量,True=开启,False=关闭
BUZZER_PIN = 9 # 蜂鸣器连接的GPIO引脚号(M5Stack Core S3)
buzzer = None # 蜂鸣器PWM对象,将在init_hardware()中初始化
# ============================
# 硬件初始化函数
# ============================
def init_hardware():
"""
初始化所有硬件设备
功能:
1. 初始化I2C总线用于连接环境传感器
2. 初始化环境传感器(ENV IV Unit)
3. 初始化蜂鸣器为PWM控制模式
4. 测试传感器读取,确认硬件正常
返回:
env4_0: 初始化好的环境传感器对象
"""
global buzzer # 声明使用全局的buzzer变量,以便在函数内修改
# 初始化I2C0总线
# scl=引脚1,sda=引脚2,频率100kHz
i2c0 = I2C(0, scl=Pin(1), sda=Pin(2), freq=100000)
# 初始化ENV IV环境传感器单元
# type=4表示ENV IV型号(SHT40温湿度传感器 + QMP6988气压传感器)
env4_0 = ENVUnit(i2c=i2c0, type=4)
# 初始化蜂鸣器为PWM控制
# 引脚9,频率4000Hz,初始占空比0(关闭)
buzzer = PWM(Pin(BUZZER_PIN), freq=4000, duty=0)
# 打印I2C总线上的设备地址(用于调试)
print("I2C设备地址:", i2c0.scan())
# 测试读取传感器数据(用于验证硬件连接)
print("温度测试:", env4_0.read_temperature())
print("气压测试:", env4_0.read_pressure())
print("湿度测试:", env4_0.read_humidity())
return env4_0 # 返回传感器对象供后续使用
# ============================
# 网络连接函数
# ============================
def connect_wifi():
"""
连接WiFi网络
功能:
1. 激活WLAN站点模式
2. 连接到指定的WiFi网络
3. 等待连接成功
4. 打印分配的IP地址
返回:
ip: 分配到的本地IP地址
"""
# 创建WLAN对象,设置为站点模式(STA_IF表示作为客户端连接路由器)
wlan = network.WLAN(network.STA_IF)
# 激活WLAN接口
wlan.active(True)
# 如果尚未连接,则尝试连接
if not wlan.isconnected():
print(f"正在连接WiFi:{WIFI_SSID}")
# 发起连接请求
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
# 等待连接成功,最多等待约10秒
timeout = 20 # 20个循环,每个0.5秒,共10秒
while not wlan.isconnected() and timeout > 0:
time.sleep(0.5)
print(".", end="") # 打印进度点
timeout -= 1
if timeout == 0:
print("\nWiFi连接超时!")
return None
# 获取并打印IP地址
ip = wlan.ifconfig()[0] # ifconfig返回[IP, 子网掩码, 网关, DNS]
print(f"\nWiFi已连接,IP地址: {ip}")
return ip
# ============================
# MQTT回调函数
# ============================
def mqtt_callback(topic, msg):
"""
MQTT消息接收回调函数
功能:
1. 解码收到的主题和消息
2. 根据主题处理不同类型的消息
3. 控制蜂鸣器的开关
参数:
topic: 收到的主题(字节类型)
msg: 收到的消息(字节类型)
"""
global buzzer_state, buzzer # 使用全局变量
# 将字节类型的主题和消息解码为字符串
topic_str = topic.decode('utf-8')
msg_str = msg.decode('utf-8')
print(f"收到MQTT消息:主题={topic_str}, 消息={msg_str}")
# 检查是否为蜂鸣器控制主题
if topic_str == MQTT_SUB_TOPIC:
if msg_str == "on":
# 开启蜂鸣器:设置PWM占空比为30(0-1023范围)
buzzer.duty(30)
buzzer_state = True
print("蜂鸣器已开启")
elif msg_str == "off":
# 关闭蜂鸣器:设置PWM占空比为0
buzzer.duty(0)
buzzer_state = False
print("蜂鸣器已关闭")
else:
print(f"未知的控制命令: {msg_str}")
# ============================
# MQTT连接函数
# ============================
def connect_mqtt():
"""
连接MQTT服务器并订阅主题
功能:
1. 创建MQTT客户端实例
2. 设置消息回调函数
3. 连接到MQTT服务器
4. 订阅感兴趣的主题
返回:
client: 连接成功的MQTT客户端对象,失败则返回None
"""
# 创建MQTT客户端实例
# 参数:客户端ID,服务器地址,端口,用户名,密码
client = MQTTClient(MQTT_CLIENT_ID, MQTT_SERVER, MQTT_PORT,
MQTT_USER, MQTT_PASSWORD)
# 设置消息回调函数
client.set_callback(mqtt_callback)
try:
# 连接到MQTT服务器
client.connect()
# 订阅蜂鸣器控制主题
client.subscribe(MQTT_SUB_TOPIC)
print(f"已成功连接到MQTT服务器:{MQTT_SERVER}:{MQTT_PORT}")
print(f"已订阅主题:{MQTT_SUB_TOPIC}")
return client
except Exception as e:
# 连接失败的处理
print(f"MQTT连接失败:{e}")
print("请检查:")
print("1. 网络连接是否正常")
print("2. MQTT服务器地址是否正确")
print("3. 防火墙设置是否允许1883端口")
return None
# ============================
# 数据发布函数
# ============================
def publish_env_data(client, sensor):
"""
读取环境传感器数据并发布到MQTT
功能:
1. 从传感器读取温度、湿度、气压
2. 将数据格式化为JSON字符串
3. 通过MQTT发布到指定主题
参数:
client: MQTT客户端对象
sensor: 环境传感器对象
返回:
bool: 发布成功返回True,失败返回False
"""
try:
# 读取传感器数据
temp = sensor.read_temperature() # 温度(摄氏度)
humi = sensor.read_humidity() # 湿度(百分比)
pres = sensor.read_pressure() # 气压(百帕)
# 构建JSON格式数据
data = {
"temperature": round(temp, 2), # 保留两位小数
"humidity": round(humi, 2),
"pressure": round(pres, 2),
"timestamp": time.time() # 当前时间戳
}
# 将字典转换为JSON字符串
data_str = json.dumps(data)
# 发布到MQTT主题
client.publish(MQTT_PUB_TOPIC, data_str)
# 打印数据到控制台(用于调试)
print(f"传感器数据 -> 温度: {temp:.2f}°C, 湿度: {humi:.2f}%, 气压: {pres:.2f}hPa")
print(f"数据已发送到主题: {MQTT_PUB_TOPIC}")
return True
except Exception as e:
# 发布失败的处理
print(f"发布数据失败:{e}")
print("可能的原因:传感器读取失败或MQTT连接断开")
return False
# ============================
# 主函数
# ============================
def main():
"""
主程序入口
程序流程:
1. 初始化硬件
2. 连接WiFi
3. 连接MQTT服务器
4. 进入主循环:
a. 检查并处理MQTT消息
b. 定期发布环境数据
c. 支持按键强制发布(需要定义buttonA)
"""
print("=" * 50)
print("M5Stack Core S3 物联网网关启动")
print("=" * 50)
# 1. 初始化硬件
print("\n[步骤1] 初始化硬件...")
env4_0 = init_hardware() # 初始化硬件,蜂鸣器已在init_hardware中通过全局变量设置
print("硬件初始化完成")
# 2. 连接WiFi
print("\n[步骤2] 连接WiFi...")
ip = connect_wifi()
if ip is None:
print("WiFi连接失败,程序退出")
return
# 3. 连接MQTT服务器
print("\n[步骤3] 连接MQTT服务器...")
client = connect_mqtt()
if not client:
print("无法连接到MQTT服务器,程序退出")
return
try:
print("\n[步骤4] 进入主循环...")
print("- 每10秒自动发布一次环境数据")
print("- 接收主题 '{}' 控制蜂鸣器".format(MQTT_SUB_TOPIC))
print("- 按Ctrl+C退出程序\n")
# 定时发布变量
last_publish_time = 0 # 上次发布时间
publish_interval = 10 # 发布间隔(秒)
# 主循环
while True:
# 检查并处理MQTT消息(非阻塞)
client.check_msg()
# 定时发布环境数据
current_time = time.time()
if current_time - last_publish_time >= publish_interval:
if publish_env_data(client, env4_0):
last_publish_time = current_time
# 注意:原代码中的buttonA.wasPressed()需要buttonA对象的定义
# 如果需要按键功能,需要先初始化buttonA,例如:
# from m5stack import buttonA
# 或使用:
# buttonA = Pin(39, Pin.IN, Pin.PULL_UP) # 示例引脚,请根据实际硬件调整
# 短暂延时,降低CPU使用率
time.sleep(0.1)
except KeyboardInterrupt:
# 用户按Ctrl+C中断程序
print("\n程序被用户中断")
except Exception as e:
# 其他异常处理
print(f"\n发生未预期错误:{e}")
import traceback
traceback.print_exc() # 打印完整的错误堆栈
finally:
# 清理资源
print("\n[清理] 断开MQTT连接...")
client.disconnect()
print("程序结束")
# ============================
# 程序入口
# ============================
if __name__ == "__main__":
"""
程序执行入口
当直接运行此脚本时,执行main()函数
如果此脚本被导入为模块,则不执行
"""
main()
4、注:下面是这个MQTTClient的代码和注释(from umqtt.simple import MQTTClient)。
python
# SPDX-FileCopyrightText: Copyright (c) 2013-2014 micropython-lib contributors
#
# SPDX-License-Identifier: MIT
import socket
import struct
class MQTTException(Exception):
"""MQTT异常类,继承自Python的Exception基类"""
pass
class MQTTClient:
"""MQTT客户端类,实现MQTT协议的基本功能"""
def __init__(
self,
client_id, # 客户端标识符,每个客户端必须唯一
server, # MQTT服务器地址/域名
port=0, # 服务器端口,默认为0会自动选择(SSL:8883, 非SSL:1883)
user=None, # 用户名(用于认证)
password=None, # 密码(用于认证)
keepalive=0, # 心跳间隔(秒),0表示不使用心跳
ssl=False, # 是否使用SSL/TLS加密连接
ssl_params={}, # SSL参数字典,传递给ssl.wrap_socket()
):
"""
初始化MQTT客户端实例
参数说明:
- client_id: 客户端ID,服务器用此标识客户端,必须唯一
- server: MQTT代理服务器地址
- port: 连接端口,默认为0时根据ssl标志自动选择
- user: 认证用户名(可选)
- password: 认证密码(可选)
- keepalive: 心跳包发送间隔,保持连接活跃
- ssl: 是否启用SSL/TLS安全连接
- ssl_params: SSL配置参数字典
"""
if port == 0:
port = 8883 if ssl else 1883 # 默认端口:SSL用8883,非SSL用1883
# 客户端属性初始化
self.client_id = client_id # 客户端标识符
self.sock = None # socket连接对象
self.server = server # 服务器地址
self.port = port # 服务器端口
self.ssl = ssl # SSL标志
self.ssl_params = ssl_params # SSL参数
self.pid = 0 # 报文标识符(用于QoS>0的消息)
self.cb = None # 消息回调函数
self.user = user # 用户名
self.pswd = password # 密码
self.keepalive = keepalive # 心跳间隔
self.lw_topic = None # 遗愿主题(Last Will Topic)
self.lw_msg = None # 遗愿消息(Last Will Message)
self.lw_qos = 0 # 遗愿消息的QoS等级
self.lw_retain = False # 遗愿消息是否保留
def _send_str(self, s):
"""
内部方法:发送字符串到socket
MQTT协议要求字符串前要有2字节的长度前缀
参数:s - 要发送的字符串
"""
self.sock.write(struct.pack("!H", len(s))) # 打包字符串长度为2字节(大端序)
self.sock.write(s) # 写入字符串数据
def _recv_len(self):
"""
内部方法:接收可变长度编码
MQTT协议使用可变长度编码:
- 每个字节低7位存储数据
- 最高位为1表示还有后续字节,为0表示结束
返回:解码后的长度值
"""
n = 0 # 存储最终长度值
sh = 0 # 移位计数器
while 1:
b = self.sock.read(1)[0] # 读取1字节
n |= (b & 0x7F) << sh # 取低7位并左移
if not b & 0x80: # 检查最高位是否为0(结束标志)
return n # 返回解码后的长度
sh += 7 # 准备处理下一个字节
def set_callback(self, f):
"""
设置消息接收回调函数
参数:f - 回调函数,格式应为 f(topic, message)
当接收到订阅的消息时自动调用
"""
self.cb = f
def set_last_will(self, topic, msg, retain=False, qos=0):
"""
设置遗愿(Last Will)消息
遗愿消息:当客户端异常断开时,服务器会发布此消息
参数:
- topic: 遗愿消息发布的主题
- msg: 遗愿消息内容
- retain: 是否保留消息(新订阅者能收到)
- qos: 服务质量等级(0,1,2)
"""
assert 0 <= qos <= 2 # QoS必须在0-2范围内
assert topic # 主题不能为空
self.lw_topic = topic # 存储遗愿主题
self.lw_msg = msg # 存储遗愿消息
self.lw_qos = qos # 存储QoS等级
self.lw_retain = retain # 存储保留标志
def connect(self, clean_session=True):
"""
连接到MQTT服务器
参数:clean_session - 是否清理会话
True: 创建新会话,不保留之前的订阅
False: 恢复之前的会话(需要服务器支持)
返回:session_present - 是否恢复了之前的会话
"""
# 创建socket连接
self.sock = socket.socket()
addr = socket.getaddrinfo(self.server, self.port)[0][-1]
self.sock.connect(addr)
# 如果需要SSL,包装socket
if self.ssl:
import ssl
self.sock = ssl.wrap_socket(self.sock, **self.ssl_params)
# 构建CONNECT报文
premsg = bytearray(b"\x10\0\0\0\0\0") # 固定报头:CONNECT命令 + 预留长度位
msg = bytearray(b"\x04MQTT\x04\x02\0\0") # 可变报头:协议名+版本+标志+心跳
# 计算剩余长度
sz = 10 + 2 + len(self.client_id) # 基础长度:协议头+客户端ID
# 设置清理会话标志
msg[6] = clean_session << 1
# 如果有用户名密码,设置标志位并增加长度
if self.user is not None:
sz += 2 + len(self.user) + 2 + len(self.pswd)
msg[6] |= 0xC0 # 设置用户名和密码标志位
# 设置心跳间隔
if self.keepalive:
assert self.keepalive < 65536 # 心跳间隔必须小于65536
msg[7] |= self.keepalive >> 8 # 高字节
msg[8] |= self.keepalive & 0x00FF # 低字节
# 如果有遗愿消息,设置标志位并增加长度
if self.lw_topic:
sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
msg[6] |= self.lw_retain << 5
# 编码剩余长度(可变长度编码)
i = 1
while sz > 0x7F:
premsg[i] = (sz & 0x7F) | 0x80 # 设置最高位表示还有后续字节
sz >>= 7 # 右移7位处理下一个字节
i += 1
premsg[i] = sz # 最后一个字节最高位为0
# 发送CONNECT报文
self.sock.write(premsg, i + 2) # 写入固定报头
self.sock.write(msg) # 写入可变报头
self._send_str(self.client_id) # 写入客户端ID
# 写入遗愿消息(如果有)
if self.lw_topic:
self._send_str(self.lw_topic)
self._send_str(self.lw_msg)
# 写入用户名密码(如果有)
if self.user is not None:
self._send_str(self.user)
self._send_str(self.pswd)
# 读取CONNACK响应
resp = self.sock.read(4)
assert resp[0] == 0x20 and resp[1] == 0x02 # 确认是CONNACK报文
# 检查返回码
if resp[3] != 0:
raise MQTTException(resp[3]) # 连接失败,抛出异常
# 返回会话是否已存在
return resp[2] & 1 # 位0表示session present
def disconnect(self):
"""断开与MQTT服务器的连接"""
self.sock.write(b"\xe0\0") # DISCONNECT报文
self.sock.close() # 关闭socket
def ping(self):
"""发送PINGREQ心跳请求包"""
self.sock.write(b"\xc0\0") # PINGREQ报文
def isconnected(self):
"""
检查是否仍然连接
尝试发送PINGREQ,通过是否成功写入判断连接状态
返回:布尔值,True表示连接正常
"""
try:
return self.sock.write(b"\xc0\0") == 2 # 尝试发送PINGREQ
except OSError: # 发生异常表示连接已断开
return False
def publish(self, topic, msg, retain=False, qos=0):
"""
发布消息到指定主题
参数:
- topic: 发布主题
- msg: 消息内容
- retain: 是否保留消息
- qos: 服务质量等级(0,1,2)
注意:QoS 2尚未完全实现
"""
# 构建PUBLISH报文固定报头
pkt = bytearray(b"\x30\0\0\0") # PUBLISH报文基础
pkt[0] |= qos << 1 | retain # 设置QoS和保留标志
# 计算剩余长度
sz = 2 + len(topic) + len(msg) # 主题长度+主题+消息
if qos > 0:
sz += 2 # QoS>0需要报文标识符
assert sz < 2097152 # 确保消息长度不超过协议限制
# 编码剩余长度
i = 1
while sz > 0x7F:
pkt[i] = (sz & 0x7F) | 0x80
sz >>= 7
i += 1
pkt[i] = sz
# 发送固定报头
self.sock.write(pkt, i + 1)
# 发送主题
self._send_str(topic)
# 发送报文标识符(QoS>0时需要)
if qos > 0:
self.pid += 1 # 递增报文标识符
pid = self.pid
struct.pack_into("!H", pkt, 0, pid) # 打包标识符
self.sock.write(pkt, 2)
# 发送消息内容
self.sock.write(msg)
# QoS 1: 等待PUBACK确认
if qos == 1:
while 1:
op = self.wait_msg() # 等待消息
if op == 0x40: # PUBACK报文
sz = self.sock.read(1)
assert sz == b"\x02" # 剩余长度应为2
rcv_pid = self.sock.read(2)
rcv_pid = rcv_pid[0] << 8 | rcv_pid[1]
if pid == rcv_pid: # 确认是期望的报文
return
# QoS 2: 尚未实现
elif qos == 2:
assert 0 # QoS 2 not supported
def subscribe(self, topic, qos=0):
"""
订阅主题
参数:
- topic: 要订阅的主题
- qos: 请求的QoS等级
注意:必须先设置回调函数(通过set_callback)
"""
assert self.cb is not None, "Subscribe callback is not set"
# 构建SUBSCRIBE报文
pkt = bytearray(b"\x82\0\0\0") # SUBSCRIBE报文基础
self.pid += 1 # 递增报文标识符
# 计算剩余长度并打包
struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid)
# 发送SUBSCRIBE报文
self.sock.write(pkt)
self._send_str(topic) # 发送主题
self.sock.write(qos.to_bytes(1, "little")) # 发送QoS
# 等待SUBACK确认
while 1:
op = self.wait_msg() # 等待消息
if op == 0x90: # SUBACK报文
resp = self.sock.read(4) # 读取SUBACK
# 验证报文标识符匹配
assert resp[1] == pkt[2] and resp[2] == pkt[3]
if resp[3] == 0x80: # 订阅失败
raise MQTTException(resp[3])
return
def wait_msg(self):
"""
等待并处理单个MQTT消息
工作流程:
1. 读取报文类型
2. 如果是PINGRESP,忽略
3. 如果是PUBLISH,传递给回调函数
4. 根据QoS发送确认
返回:报文类型(如果不是PUBLISH)
"""
res = self.sock.read(1) # 读取第一个字节(报文类型)
self.sock.setblocking(True) # 设置为阻塞模式
if res is None: # 无数据
return None
if res == b"": # 连接关闭
raise OSError(-1)
# 处理PINGRESP(心跳响应)
if res == b"\xd0": # PINGRESP报文
sz = self.sock.read(1)[0]
assert sz == 0 # PINGRESP剩余长度应为0
return None
op = res[0] # 报文类型
# 处理PUBLISH报文
if op & 0xF0 != 0x30: # 如果不是PUBLISH报文
return op # 返回报文类型
# 读取剩余长度
sz = self._recv_len()
# 读取主题长度和主题
topic_len = self.sock.read(2)
topic_len = (topic_len[0] << 8) | topic_len[1]
topic = self.sock.read(topic_len)
sz -= topic_len + 2
# 读取报文标识符(QoS>0时)
if op & 6: # QoS>0
pid = self.sock.read(2)
pid = pid[0] << 8 | pid[1]
sz -= 2
# 读取消息内容
msg = self.sock.read(sz)
# 调用回调函数处理消息
self.cb(topic, msg)
# 根据QoS发送确认
if op & 6 == 2: # QoS 1
pkt = bytearray(b"\x40\x02\0\0") # PUBACK报文
struct.pack_into("!H", pkt, 2, pid) # 打包报文标识符
self.sock.write(pkt)
elif op & 6 == 4: # QoS 2(未实现)
assert 0
return op
def check_msg(self):
"""
检查是否有待处理消息
与wait_msg()的区别:
- 非阻塞模式:如果没有数据立即返回
- 有数据时调用wait_msg()处理
返回:报文类型或None
"""
self.sock.setblocking(False) # 设置为非阻塞模式
return self.wait_msg() # 尝试读取消息
5、可通过下面这些指令来发布和订阅相关主题、数据。
a. 订阅环境数据

b. 发布蜂鸣器控制的信息,当是"on"时,打开蜂鸣器;当是"off"时,关闭蜂鸣器。

五、总结
在代码中,网关(Gateway)的角色主要体现在两个方面:
-
它从传感器(环境传感器ENV4.0)读取数据,并将这些数据通过MQTT发布到云端(或MQTT服务器),这是数据上传(上行)的过程。
-
它订阅了MQTT主题(MQTT_SUB_TOPIC)来接收控制命令(例如控制蜂鸣器),这是指令下发(下行)的过程。
因此,这个程序作为一个网关,连接了本地传感器(环境传感器、蜂鸣器)和远程的MQTT服务器(云端),实现了数据的双向传输。
但是,从严格意义上讲,这个程序更偏向于一个具有MQTT通信能力的传感器节点(或执行器节点)。通常,网关会连接多个传感器或设备,并聚合它们的数据,同时可能进行一些协议转换。在这个例子中,它只连接了一个环境传感器和一个蜂鸣器,所以可以看作是一个简单的网关。
如果要体现更复杂的网关功能,可能会包括:
-
连接多个传感器(如温度、湿度、光照等)
-
聚合多个传感器的数据并一次性发布
-
实现本地决策(如根据温度自动控制蜂鸣器)
-
支持多种协议(如同时支持MQTT和HTTP)
但是,当前代码已经具备了网关的基本特征:即从传感器采集数据并上传到云,同时从云接收控制命令并控制本地设备。
所以,在代码中,网关的体现主要在:
-
init_hardware() 初始化了传感器和蜂鸣器,这是连接本地设备。
-
connect_wifi() 和 connect_mqtt() 连接到远程服务器,这是连接云端。
-
在主循环中,定期发布传感器数据(上行)并检查来自云端的控制消息(下行)。
因此,我们可以将这个程序称为一个简单的物联网网关。

六、留言
如有错误,敬请指正。