MQTT物联网网关实验

相关知识链接:

1、MQTT - mosquitto安装、启动、使用

2、MQTT协议详解

3、windows mosquitto.exe闪退

4、ESP32指南(网络入门-MQTT协议)超有用!!!!!!!

5、一文彻底搞懂OSI七层模型和TCP/IP四层模型

6、MicroPython介绍(内含MicroPython库的链接)

7、M5Stack 控件官方文档

一、网关

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)的角色主要体现在两个方面:

  1. 它从传感器(环境传感器ENV4.0)读取数据,并将这些数据通过MQTT发布到云端(或MQTT服务器),这是数据上传(上行)的过程。

  2. 它订阅了MQTT主题(MQTT_SUB_TOPIC)来接收控制命令(例如控制蜂鸣器),这是指令下发(下行)的过程。

因此,这个程序作为一个网关,连接了本地传感器(环境传感器、蜂鸣器)和远程的MQTT服务器(云端),实现了数据的双向传输。

但是,从严格意义上讲,这个程序更偏向于一个具有MQTT通信能力的传感器节点(或执行器节点)。通常,网关会连接多个传感器或设备,并聚合它们的数据,同时可能进行一些协议转换。在这个例子中,它只连接了一个环境传感器和一个蜂鸣器,所以可以看作是一个简单的网关。

如果要体现更复杂的网关功能,可能会包括:

  • 连接多个传感器(如温度、湿度、光照等)

  • 聚合多个传感器的数据并一次性发布

  • 实现本地决策(如根据温度自动控制蜂鸣器)

  • 支持多种协议(如同时支持MQTT和HTTP)

但是,当前代码已经具备了网关的基本特征:即从传感器采集数据并上传到云,同时从云接收控制命令并控制本地设备。

所以,在代码中,网关的体现主要在:

  1. init_hardware() 初始化了传感器和蜂鸣器,这是连接本地设备。

  2. connect_wifi() 和 connect_mqtt() 连接到远程服务器,这是连接云端。

  3. 在主循环中,定期发布传感器数据(上行)并检查来自云端的控制消息(下行)。

因此,我们可以将这个程序称为一个简单的物联网网关。

六、留言

如有错误,敬请指正。

相关推荐
黑客思维者6 小时前
XGW-9000系列高端新能源电站边缘网关软件架构设计
人工智能·物联网·iot·新能源·软件架构·边缘网关·计算机硬件
wotaifuzao7 小时前
Nordic-nRF54L 系列架构全景:从蓝牙 6.0 到超低功耗设计详解
单片机·物联网·硬件架构·蓝牙·nordic
数智顾问9 小时前
(102页PPT)数字化转型,从战略到执行(附下载方式)
大数据·人工智能·物联网
wotaifuzao9 小时前
硬核拆解:从 RFID 到 NFC,一文读懂智能卡底层技术 (突出“硬核”和“全覆盖”)
物联网·信息与通信·rfid·nfc·感应卡
Stanford_sun9 小时前
基于Zigbee的无线火灾报警系统(云平台版)
网络·嵌入式硬件·物联网·zigbee
Wnq1007210 小时前
当无人机 “飞” 入生活,安全隐患如何破解?
嵌入式硬件·物联网·网络安全·信息与通信·信号处理
TDengine (老段)11 小时前
TDengine 新性能基准测试工具 taosgen
大数据·数据库·物联网·测试工具·时序数据库·tdengine·涛思数据
黑客思维者11 小时前
Python modbus-tk在配电物联网边缘网关的应用
开发语言·python·物联网
小李做物联网11 小时前
【单片机毕业设计】143.1基于单片机stm32塔吊控制反馈物联网嵌入式项目程序开发系统
stm32·单片机·嵌入式硬件·物联网