拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于MQTT的智能照明系统:从设备到服务端的完整实现

简介这份资源是基于物联网技术的智能照明系统完整项目文件面向物联网、嵌入式或智能家居方向的学习者及开发者解决传统照明无法远程控制与智能管理的问题。资源包含 Arduino 主程序.ino、MQTT 与 Wi-Fi 配置头文件.h/ini、安卓安装包.apk、项目报告.pdf以及 README 说明.md共 8 个文件压缩包大小 6.8MB可帮助用户快速搭建从设备端到移动端的远程照明控制链路。系统支持手机 App 远程开关灯、亮度与颜色调节、定时调度并结合温度湿度传感器自动调整照明同时兼容 LED 等常见灯具配有安全加密设计。项目报告与 README 能提供配置思路和运行说明已有 71 人学习下载适合作为课程设计、毕设或物联网入门实践参考。1. 为什么说基于物联网的智能照明系统核心不在灯而在协议网上这种标题的源码包常见的构成是设备端固件、服务端程序、Web 控制台和一张数据库脚本但「源码」两个字往往只代表服务端和前端设备端要么给你一个可以直接烧录的固件要么只留说明让你自己配。如果你把全部精力放在继电器怎么接、灯怎么亮那这套系统做完也只是个遥控开关不算真正的物联网应用。真正把「智能照明系统」和普通遥控灯区分开的是设备状态的同步网页下发开灯命令后五秒内界面必须能显示灯已开意外断电重启后状态不能回到默认值网络抖动时同一条命令不能被设备重复执行成闪烁。这些问题的解法不在灯上而在消息协议和状态模型里。我聊的就是这条链路的完整落地方案设备侧怎么选型、用 MQTT 怎么规划主题、broker 怎么配、服务端怎么把控制变成业务闭环以及最后怎么验证和排错。如果你拿这个题目做课程设计或物联网毕业设计这套架子可以直接改如果你是想把开源代码跑起来再二次开发读完你也知道该去源码里找哪几个关键文件。智能照明系统的技术栈并不复杂但每一步都有它的边界和坑。2. 智能照明系统的设备侧选型与网关接入ESP32、继电器与MQTT客户端2.1 智能照明系统为什么建议保留网关层而非让设备裸连最简单的方案确实是让 ESP32 直接连到某个公共 MQTT broker一个 topic 一把梭网页照着发就行。但做到多房间、有人无人检测、离线判断时这种裸连方式很难维护。我一般会在本地网段里保留一个轻量网关它不一定是独立硬件可以是树莓派、工控机或者一台常开的虚拟机职责是承接设备的上行消息、缓存最后一次状态并把控制指令转发到对应设备。网关层的价值在断网和弱网场景下尤其明显。设备侧重连时不需要直接面对多个服务端模块Web 端也不必关心设备 IP 和端口变化。三层结构里设备层只做「执行命令 上报结果」网关层做协议转换和状态缓存服务端只处理业务逻辑。这个结构下源码包里常见的固件、server、web 三层目录就刚好对应起来你拿到代码时第一件事就是确认设备端代码到底在哪一层。节点推荐硬件执行动作上报数据开关节点ESP32-C3 继电器模块通断灯开关状态、电压调光节点ESP32-S3 PWM 调光板调节亮度当前亮度、功率传感节点ESP32 PIR BH1750无直接动作人体状态、照度、温湿度网关树莓派或 x86 小主机协议转换、消息路由设备在线状态2.2 设备端快速起步MicroPython 固件烧录与最小工程结构做智能照明设备端我通常选 MicroPython 而不是 Arduino原因只有一个源码可读性好改起来快。毕设答辩时老师问「状态上报逻辑在哪」你直接翻开一个 .py 文件给他看比在 .ino 里找回调函数省事得多。涉及高频 PWM 或精确时序时再退回 ESP-IDF/Arduino普通开关和调光场景 MicroPython 完全扛得住。设备端第一步是把固件烧进去操作如下pip install esptool esptool.py --chip esp32 --port COM3 erase_flash esptool.py --chip esp32 --port COM3 --baud 460800 write_flash -z 0x1000 ESP32_GENERIC.bin这里第一行是安装烧写工具第二行擦除整片 flash第三行把 MicroPython 固件写到 0x1000 偏移处。--port COM3在 Linux 环境通常换成/dev/ttyUSB0ESP32_GENERIC.bin按你实际下载的固件文件名替换。烧完用串口终端连接能进入提示符就说明系统起来了。注意不同开发板的板载 LED 引脚不一样GPIO2 只是最常见的默认位置接继电器时还要看继电器是高电平触发还是低电平触发。2.3 灯节点最小控制程序订阅命令、执行开关、回发状态设备侧最核心的一段逻辑就三件事连接 WiFi、订阅控制命令、发布真实状态。下面这段代码可以直接在 MicroPython 环境跑我把注释写在关键位置from machine import Pin import network import time from umqtt.simple import MQTTClient BROKER 192.168.1.10 CLIENT_ID light_living_room TOPIC_CMD bcmnd/living_room/power TOPIC_STAT bstat/living_room/power relay Pin(2, Pin.OUT, value0) # 继电器接 GPIO2 def connect_wifi(ssid, password, timeout10): wlan network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(ssid, password) for _ in range(timeout * 2): if wlan.isconnected(): return True time.sleep(0.5) return False def on_message(topic, payload): if payload bON: relay.value(1) elif payload bOFF: relay.value(0) # 无论命令是什么都回发当前真实状态 client.publish(TOPIC_STAT, payload, retainTrue, qos1) client MQTTClient(CLIENT_ID, BROKER) client.set_callback(on_message) if connect_wifi(your_ssid, your_password): client.connect(clean_sessionFalse) client.subscribe(TOPIC_CMD, qos1) while True: client.wait_msg()这段代码的点在于on_message里收到 ON/OFF 后用同一个 payload 发布到stat/living_room/power并且 retain 置为 True。retainTrue的意思是 broker 会保存这条消息之后新上线的 Web 端订阅这个主题时能立即拿到灯当前是开还是关而不是等下一次状态变化。qos1保证服务端至少收到一次状态代价是极端情况下可能收到重复消息所以命令执行要做成幂等的即收到两次 ON 不会产生额外动作。设备运行时如果client.wait_msg()阻塞异常退出常见原因是 WiFi 掉线后 socket 没自动回收后续可以加一个看门狗定时重连。2.4 用 PIR 和光敏传感器把「有人无人」带进照明控制你可能会看到「基于人数的智能照明系统」这类方向它本质上就是在灯节点旁边挂一个人体红外传感器和照度传感器把人的状态也变成消息流里的一部分。设备侧只需要做一次性判定复杂规则放到服务端否则设备端会越写越重。下面是一个简化思路if pir.value() 1: sensor_events[living_room] {last_seen: time.time(), motion: on}这里pir.value()为 1 表示感应到人我把时间和状态放进字典定时或按事件上报到tele/living_room/state。设备端不做「五分钟没人就关灯」这种逻辑因为关灯动作受时间段、工作状态影响放到服务端后规则可以随时改设备端固件不用重新烧。到这一步开关节点、调光节点、传感节点都已经具备接入能力下一步要解决的是消息本身怎么组织。3. 用MQTT组织智能照明系统的消息流主题规划、QoS与状态回读3.1 主题与载荷约定命令流和状态流必须分开控制命令和设备状态如果混在同一个主题里Web 端没法区分「这条消息是发给灯的」还是「灯已经执行完的反馈」。我常用的主题风格是动作/设备/属性三段式动作分为cmndcommand、statstatus、teletelemetry。三个前缀各有分工cmnd 是服务端发给设备的下行指令stat 是设备执行完成后的状态回执tele 是周期性或事件性遥测数据比如照度、温湿度、心跳。消息类型主题示例QoSretain典型载荷控制下发cmnd/living_room/power1否ON / OFF状态回读stat/living_room/power1是ON / OFF亮度下发cmnd/living_room/dim1否0-100遥测上报tele/living_room/state0否JSON 格式传感器值在线状态tele/living_room/status0否online / offline把命令和状态分开最直接的好处是 Web 端订阅stat//power就能拿到所有灯的真实状态订阅cmnd/#只能看到服务端下了哪些指令两者不会互相污染。是 MQTT 的单层通配符#是多层通配符这套订阅关系在排错时也很有用用mosquitto_sub挂两个终端一个看命令、一个看状态链路哪一段断了立刻能定位。3.2 QoS 与 retain 的取舍状态丢一次就够让人头疼MQTT 的 QoS 有三个等级很多人直接全选 2其实是过度设计。QoS2 的握手开销在弱网设备上会放大延迟而照明系统真正丢不得的是「状态回执」和「控制命令」不是每一条遥测数据。我一般会把控制命令和状态消息设为 QoS1遥测设为 QoS0用 QoS0 省掉大量确认包。这里有个容易踩的坑retain 消息只保留每个主题的最后一条如果设备断网时服务端发来一条 OFF这条消息没有 retain设备恢复连接后它永远不会被送达Web 端看到的状态就是错的。另外clean_session这个参数决定了客户端断开时 broker 是否清空它的会话。设备端如果设clean_sessionFalsebroker 会为它缓存 QoS1 消息和订阅信息断线重连后能补收漏掉的消息代价是 broker 内存里要保留会话状态。对智能照明系统来说设备数量只有几十台时完全开得起上万台时就要权衡了。我的建议是设备端开持久会话服务端保持短会话因为服务端重启后可以从数据库重建状态而设备端不能。3.3 用 mosquitto 搭一个本地消息通道broker 参数怎么配在 Ubuntu 或 Debian 系统上broker 直接用 mosquittosudo apt install -y mosquitto mosquitto-clients sudo systemctl enable --now mosquittomosquitto 默认只监听本机回环地址要让设备能连进来需要加一段配置。常见的做法是新建/etc/mosquitto/conf.d/lighting.conflistener 1883 0.0.0.0 allow_anonymous true persistence true persistence_location /var/lib/mosquitto/listener 1883 0.0.0.0表示在所有网卡上监听 1883 端口allow_anonymous true是只在内网调试时用正式环境一定要改成账户密码认证。persistence true开启消息持久化配合设备端的clean_sessionFalsebroker 重启后才能保留会话和未确认的 QoS1 消息。配置好之后重启服务用ss -lntp | grep 1883确认端口在监听设备端就能连了。3.4 用 mosquitto_sub 和 mosquitto_pub 验证命令链路在写服务端之前先把最底层链路验通。开一个终端订阅所有消息mosquitto_sub -h 127.0.0.1 -t cmnd/# -t stat/# -t tele/# -v-v让每一条消息同时打印主题和载荷方便看清是哪个节点发的。另开一个终端发命令mosquitto_pub -h 127.0.0.1 -t cmnd/living_room/power -m ON -q 1如果设备在线第一个终端里应该先看到cmnd/living_room/power ON紧接着看到stat/living_room/power ON。顺序很关键先看到指令再看到设备回执说明下行和上行两条路都通。如果只看到 cmnd 没有 stat问题在设备端如果两条都没有先检查 broker 的 listener 配置和防火墙。这个验证动作做完服务端代码就可以放心写了。4. 服务端控制面板与自动化联动把智能照明系统的命令变成业务闭环4.1 用 paho-mqtt 订阅状态消息并落库服务端是整个系统的「大脑」但它不应该直接控制设备而是订阅设备上报的状态再通过命令主题下发控制。我常用 paho-mqtt 做消息接入SQLite 做状态存储这套组合在毕业设计和中小型项目里足够轻。下面是一个最小落库程序import sqlite3 import time import paho.mqtt.client as mqtt conn sqlite3.connect(lighting.db, check_same_threadFalse) conn.execute(CREATE TABLE IF NOT EXISTS device_state (device TEXT PRIMARY KEY, value TEXT, ts TEXT)) client mqtt.Client(lighting-server, clean_sessionTrue) def on_message(mqttc, userdata, msg): device msg.topic.split(/)[1] value msg.payload.decode() ts time.strftime(%Y-%m-%d %H:%M:%S) conn.execute(INSERT OR REPLACE INTO device_state(device, value, ts) VALUES(?,?,?), (device, value, ts)) conn.commit() client.on_message on_message client.connect(127.0.0.1, 1883, 60) client.subscribe([(stat//power, 1), (tele//state, 0)]) client.loop_forever()这段代码订阅stat//power和tele//state前者是灯的开关状态后者是传感器遥测。订阅列表用元组数组传入每条主题可以单独指定 QoS这样「状态回执用 QoS1、遥测用 QoS0」的策略在服务端也保持一致。数据库用INSERT OR REPLACE是因为每个设备只需要保留最新状态不需要留历史。注意loop_forever()是阻塞的实际部署时我会把它放到独立进程或线程里Web 接口才能同时对外服务。4.2 用 Flask 提供控制接口下行命令的请求-响应模型Web 控制面板本质上就是给用户一个「开关」按钮点击后由后端向 broker 发布命令。用 Flask 写一个最小接口from flask import Flask, request import paho.mqtt.client as mqtt app Flask(__name__) publisher mqtt.Client(lighting-web) publisher.connect(127.0.0.1, 1883, 60) publisher.loop_start() app.post(/api/device/device/power) def set_power(device): body request.get_json(forceTrue) payload ON if body[on] else OFF info publisher.publish(fcmnd/{device}/power, payload, qos1) if info.rc mqtt.MQTT_ERR_SUCCESS: return {topic: fcmnd/{device}/power, payload: payload} return {error: publish failed}, 500接口接收 POST 请求body 里传{on: true}后端把它转成cmnd/{device}/power主题上的 ON/OFF 消息。publisher.publish返回的info.rc只表示消息是否成功交给 paho 客户端不代表 broker 一定收到了更不代表设备一定执行了所以 Web 端真正的状态刷新还是要靠订阅 stat 主题。loop_start()在后台启动网络循环让同一个 paho 客户端既能发也能收。如果你的 Flask 版本较低app.post需要换成app.route(..., methods[POST])。4.3 自动化规则人数、照度和定时怎么联动智能照明和手动控制的差别在于自动化规则。我习惯把规则写成配置而不是写死在代码里因为用户大概率会反复调条件。以常见的「基于人数的智能照明系统」为例规则表可以这样设计规则名触发条件阈值执行动作无人关灯PIR 状态300 秒无人发布 OFF照度补光光照值小于 200 lux发布亮度 50%晚间定时时间18:00-22:00发布亮度 80%服务端只需要一个循环定期读取最近一次传感器状态逐条判断规则是否命中命中后发布对应命令def run_rules(sensor_events): for device, event in sensor_events.items(): if event[motion] off and time.time() - event[last_seen] 300: client.publish(fcmnd/{device}/power, OFF, qos1) if event[lux] 200 and 18 datetime.now().hour 22: client.publish(fcmnd/{device}/dim, 50, qos1)run_rules里的每个条件和动作都是独立 if这样加规则时不会互相影响。注意执行动作前要做一次「去重」判断比如灯已经处于 OFF 状态就不需要再发一次 OFF否则每次轮询都会产生一条无用消息。规则执行后要短暂延迟再更新 Web 端因为设备回执到达需要时间过早刷新会让界面短暂显示旧状态。5. 智能照明系统的链路自检、丢消息排查与断网自愈5.1 一条命令把设备侧到服务端整条链路验干净设备、broker、服务端都跑起来后我很少先开 Web 界面因为在浏览器里看不到消息中间层。先在终端挂起全量订阅再手动发一条命令观察消息流是否符合预期timeout 60 mosquitto_sub -h 127.0.0.1 -t cmnd/# -t stat/# -t tele/# -v这个订阅覆盖了下行、状态回执和遥测三类消息timeout 60让命令一分钟后自动退出防止忘记清理终端进程。配合mosquitto_pub -t cmnd/living_room/power -m ON -q 1手动触发你会看到 cmnd 先到、stat 后到。如果 stat 没有出现就把订阅范围缩小到stat/living_room/#单独看设备是否在发布。整条链路的验证顺序是broker 通不通、设备连没连、命令到没到、状态回没回这四个问题一次就能定位。提示订阅cmnd/#时会看到所有下发的控制指令如果发现设备收到的指令重复执行多半是 QoS1 重投递导致的检查设备端命令处理是否是幂等的即可。5.2 丢消息的三个检查点丢消息不一定是 broker 的问题我排查时按三个点依次看。第一看主题是否带通配符订阅stat//power和直接订阅stat/#收到的消息内容不一样匹配不对就会出现「消息发出了但没订阅上」的假象。第二看 retain 消息是否被空 payload 清掉了MQTT 协议规定发布空 payload 会删除该主题的保留消息代码里如果误发过空消息新客户端上线就拿到不状态。第三看客户端 ID 是否冲突两个客户端用同一个 client_id 连 broker 时后连的会把先连的踢下线表现就是设备隔几分钟就掉线一次。5.3 断电恢复与离线自愈遗嘱消息加主动回读设备意外断电时然后回来想要恢复正确状态需要两招配合遗嘱消息和上线主动发布。MicroPython 的 umqtt 客户端连接时可以带will_*参数client MQTTClient( CLIENT_ID, BROKER, will_topicbtele/living_room/status, will_messageboffline, will_qos1, ) client.connect(clean_sessionFalse)设备无感知断网超过 keepalive 时间后broker 会自动替设备发布这条遗嘱消息服务端收到后可以把设备标记为离线。但遗嘱只解决「离线通知」解决不了「恢复后状态未知」的问题所以设备重连成功后的第一件事不是接收命令而是立刻发布一次当前状态current bON if relay.value() 1 else bOFF client.publish(bstat/living_room/power, current, retainTrue, qos1)这条消息会覆盖 broker 里保留的旧状态Web 端因此能立即看到真实值而不是重启前的缓存。这也是我调试智能照明系统时最常用的一招设备每次上线都主动广播一次状态让整个系统的状态自愈而不是等用户去点刷新按钮。配合clean_sessionFalse设备重连后还能补收断线期间积压的 QoS1 命令整条链路就完整了。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门