简介:本资源是一套基于LoRa Mesh网络的都市环境监测系统完整源码实现,面向嵌入式开发初学者、物联网课程实践者及低功耗广域网(LPWAN)项目开发者,解决城市多点环境参数(温度、火焰、气体浓度)远距离、低功耗、自组网协同采集与可视化监控难题。压缩包共20个文件,含10个Arduino/ESP8266核心源码(.ino)、3个硬件原理图与系统框图(.jpg/.png)、5张流程图与界面截图(.png/.jpg),以及1个配置头文件(.h)和1份项目说明文档(.md),总大小637KB,结构清晰体现主从设备分工、LoRa中继转发逻辑与Firebase云端对接路径。已有1704人学习下载,读者可直接复现主设备(ESP8266)轮询管理、四节点Slave自组Mesh网络、E32 LoRa模块配置、传感器数据采集上传及网页端Firebase实时展示全流程,特别适合理解LoRa多跳通信机制与轻量级物联网系统集成实践。
1. 这不是普通LoRa点对点:一个能自愈、可中继、带Web看板的都市监测Mesh系统,专治城市边缘节点失联
你手头有个LoRa模块,接上温湿度传感器,烧进Arduino Nano,连上串口一看——数据能发出来,但一挪到地下车库、老城区夹层、绿化带深处,信号立刻归零。这不是模块坏了,是传统LoRa星型拓扑的硬伤:所有从机必须直连主机,中间隔堵承重墙,通信就断。而这份「基于LoRa Mesh网络的都市监测系统」源码包,直接绕开了这个死结。它用4个Nano从机(Slave1–Slave4)构建出真正具备多跳路由能力的LoRa Mesh网络:Slave2收不到Master信号?没关系,它把数据发给离得近的Slave3,Slave3再转发给Slave1,最后由Slave1统一上行——整个过程不依赖WiFi覆盖、不消耗公网流量、不需额外网关,纯靠LoRa物理层+轻量级路由协议完成。更关键的是,它不是理论Demo:主控用ESP8266直连Firebase,网页端实时刷新折线图;每个从机代码里都固化了E32 LoRa模块的AT指令配置逻辑;连原理图、流程图、系统框图全在Doc目录下压得严严实实。如果你正卡在“城市布点密度高但供电难、布线贵、信号衰减快”这个现实困局里,这份资源不是参考,是能直接焊PCB、烧固件、挂上线的工程基线。
2. 从硬件选型到通信协议:为什么用E32+Nano+ESP8266组合跑通LoRa Mesh
2.1 硬件链路设计:避开SX1278裸片开发的玄学调试
LoRa Mesh实现难点不在算法,而在物理层稳定性。项目没选SX1278裸片方案,而是采用成都亿佰特E32系列模块(如E32-433T30D),原因很实际:
- E32内置完整LoRa调制解调+射频前端,AT指令集成熟(
AT+RX/AT+TX/AT+MODE),省去SPI时序抠波形、寄存器配错导致接收灵敏度掉10dB的血泪经验; - 支持透传模式与定点模式双工作方式,Mesh路由层可直接在应用层做地址映射,无需修改底层驱动;
- 模块自带RSSI输出引脚,Slave端可实时读取邻居节点信号强度,为动态路由选择提供依据(见
sl1.ino中readRSSI()函数)。
提示:E32默认波特率9600,但项目中所有
Config_E32.ino文件均强制初始化为115200——这是为后续OTA升级预留带宽,若你用旧版E32固件,请先用USB转TTL工具执行AT+BaudRate=8切到115200,否则SetConfiguration_Nano1.ino中的Serial1.begin(115200)会握手失败。
2.2 节点角色定义:Master/Slave不是主从,是路由角色分工
项目中5个节点并非传统主从架构,而是按功能划分角色:
- Master(ESP8266):仅承担数据汇聚+WiFi上云职能,不参与LoRa路由计算。它通过GPIO模拟串口(SoftwareSerial)或硬件串口(Serial)轮询各Slave,每30秒发一次
PING:0x01指令,收到PONG:0x02即标记该Slave在线; - Slave(Nano×4):每个都是全功能路由节点。以Slave2为例,其
sl2.ino中loop()函数包含三重逻辑:checkIncomingPacket():监听LoRa串口,解析目标地址(dest_id字段);- 若
dest_id == MY_ID,则交由handleSensorData()处理本地传感器(DHT22+MQ2+火焰传感器); - 若
dest_id != MY_ID且hops_left > 0,则执行forwardPacket(),将数据包hops_left--后重发至下一跳(路由表存在next_hop_map[]数组中)。
这种设计让Slave3即使收不到Master信号,也能作为Slave2和Slave4之间的中继——这才是Mesh“自愈”的本质。
2.3 数据帧结构:16字节精简协议撑起多参数传输
为适配LoRa低带宽(典型空中速率2.4kbps),项目定义了固定16字节二进制帧,而非JSON明文:
| 字节偏移 | 长度 | 含义 | 示例值 |
|---|---|---|---|
| 0 | 1 | 帧头(0xAA) | 0xAA |
| 1 | 1 | 源节点ID(1–4) | 0x02(Slave2) |
| 2 | 1 | 目标节点ID(0=广播,5=Master) | 0x05 |
| 3 | 1 | 跳数(初始3,每中继减1) | 0x03 |
| 4–5 | 2 | 温度(℃×10,int16) | 0x01F4(500→50.0℃) |
| 6–7 | 2 | 湿度(%×10,int16) | 0x0258(600→60.0%) |
| 8–9 | 2 | 气体浓度(ppm,uint16) | 0x0064(100ppm) |
| 10 | 1 | 火焰状态(0=无,1=有) | 0x01 |
| 11–12 | 2 | 电池电压(mV,uint16) | 0x0C35(3125mV) |
| 13–15 | 3 | CRC32校验(仅前13字节) | 0x1A2B3C4D |
注意:
SlaveX.ino中buildPacket()函数严格按此布局填充,CRC32使用crc32_mpeg2()算法(见sl1.ino顶部#include "crc32.h"),若你替换传感器,务必同步修改字节4–10的数值缩放逻辑,否则Web端图表会显示异常高温(如500℃)。
3. 固件烧录与LoRa组网:从Arduino IDE到Mesh网络激活的六步实操
3.1 Slave节点固件烧录:Nano+CH340G的稳定供电是第一道坎
所有Slave(Slave1–Slave4)均基于Arduino Nano(老版本,ATmega328P),烧录前必须确认三点:
- 板卡设置:Arduino IDE → 工具 → 开发板 → “Arduino Nano”,处理器选“ATmega328P (Old Bootloader)”——新Bootloader会导致
Serial1(硬件串口)初始化失败; - 串口驱动:Windows需安装CH340G官方驱动(v3.5以上),MacOS Catalina+需在“系统偏好设置→安全性与隐私→通用”中允许
ch34x.kext; - 供电隔离:Nano的5V引脚严禁直连E32的VCC(E32峰值电流达120mA),必须经AMS1117-3.3V稳压模块降压后接入E32的3.3V引脚,否则E32复位时Nano会重启。
烧录步骤(以Slave1为例):
# 步骤1:打开Slave1工程 File → Open → [解压路径]/Slave1/main_s1/main_s1.ino # 步骤2:修改节点ID(关键!避免地址冲突) // 在main_s1.ino第22行找到: #define MY_ID 1 // 若你同时烧录Slave2,需改为 #define MY_ID 2 # 步骤3:选择端口并上传 Tools → Port → 选择对应COM口(如/dev/cu.usbserial-1410) Sketch → Upload上传成功后,Nano的LED会每5秒快闪2次,表示进入LoRa监听态。
3.2 Master节点配置:ESP8266连接Firebase的四重认证
Master(Rev.ino)使用ESP8266-12F,需提前在Firebase控制台创建项目并获取:
FIREBASE_HOST(如your-project-default-rtdb.firebaseio.com)FIREBASE_AUTH(数据库Secret,非Web API Key)WIFI_SSID/WIFI_PASSWORD(2.4GHz频段,5GHz不可用)
配置位置在Rev.ino底部:
// Firebase配置区(第187行起) #define FIREBASE_HOST "xxx.firebaseio.com" #define FIREBASE_AUTH "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" // 必须是Database Secret! #define WIFI_SSID "MyCity_IoT" #define WIFI_PASSWORD "SecurePass2024" // 节点映射表(决定哪个LoRa ID对应哪个Web图表) const char* nodeNames[5] = {"", "Slave1", "Slave2", "Slave3", "Slave4", "Master"};提示:
FIREBASE_AUTH在Firebase新版控制台已隐藏,需进入“项目设置→服务账号→数据库密钥”手动复制。若上传后串口打印Auth failed,90%是此处填了Web API Key。
3.3 Mesh网络激活:用串口监视器验证三跳路由
烧录全部5个节点后,按以下顺序上电并观察:
- 先上电Master(ESP8266),等待串口输出
[WiFi] Connected, IP:192.168.1.105; - 再依次上电Slave1→Slave4(间隔10秒),每台启动时串口会打印
[LoRa] Config OK, ID:1, RSSI:-72; - 打开Master串口监视器(115200波特率),输入
LIST命令,将返回当前在线Slave列表:
若Slave2未出现,说明它未被其他节点中继——此时用Online Slaves: 1,3,4 RSSI Slave1: -68 | Slave3: -85 | Slave4: -92PING 2命令手动触发:
这行输出证明Slave2的数据经Slave3中继抵达Master,Mesh路由已生效。PING 2 # 返回:PONG from Slave2 via Slave3 (hops:2)
4. 避坑:LoRa Mesh部署中五个必踩的“静默故障”
4.1 现象:Slave串口无任何输出,Nano LED常亮不闪烁
原因:E32模块未正确初始化,常见于Config_E32.ino中AT指令超时。E32出厂默认模式为“固定地址透传”,但项目要求“定点模式”(AT+MODE=1),若模块固件版本过旧(<V3.0),AT+MODE=1会返回ERROR而非OK,导致后续配置中断。
解决:用USB-TTL工具直连E32,发送AT+VER?查版本;若低于V3.0,需官网下载E32升级工具刷固件,切勿跳过此步。
4.2 现象:Master能收到Slave1数据,但Slave2/3始终显示离线
原因:LoRa频点不一致。E32默认433MHz,但部分国产模块出厂设为470MHz(中国频段),而SetConfiguration_NanoX.ino中写死AT+Freq=433。当Slave2用470MHz模块却执行AT+Freq=433,实际频点未切换,导致收不到其他节点信号。
解决:用万用表测E32模块背面丝印,若标470M,则修改SetConfiguration_Nano2.ino第42行:
// 原始:sendATCommand("AT+Freq=433", "OK", 1000); // 改为: sendATCommand("AT+Freq=470", "OK", 1000);4.3 现象:Web端图表数据突变为负数(如温度-273℃)
原因:传感器读数溢出未校验。DHT22在湿度过高(>95%)时可能返回0xFFFE,sl2.ino中readDHT22()函数未做范围检查,直接赋值给温度变量,导致高位符号扩展。
解决:在readDHT22()末尾添加校验:
if (humidity > 100 || humidity < 0 || temperature > 80 || temperature < -20) { humidity = 0; temperature = 0; // 丢弃异常帧 }4.4 现象:Firebase数据写入延迟高达2分钟,且频繁断连
原因:ESP8266内存不足。Rev.ino中Firebase.setString()默认启用SSL加密,占用大量RAM。当同时处理4个Slave轮询+WiFi心跳,Free Heap常低于12KB,触发SSL握手失败。
解决:关闭SSL,改用HTTP明文(仅限内网测试):
// 在setup()中Firebase.begin()前添加: Firebase.setWriteBufferSize(512); // 缩小缓冲区 Firebase.disableSSL(); // 关键!禁用SSL4.5 现象:Slave3能收Slave2数据,但无法转发给Slave1
原因:路由表未更新。sl3.ino中updateRouteTable()函数依赖ping响应时间,若Slave1距离过远(RSSI<-100),Slave3收不到PONG,便不会将Slave1加入next_hop_map[1]。
解决:强制静态路由。在sl3.ino全局区添加:
// Slave3到Slave1的固定下一跳是Slave2 uint8_t next_hop_map[5] = {0, 0, 0, 0, 0}; // ID 1~4 void setup() { next_hop_map[1] = 2; // Slave1的数据,让Slave3发给Slave2 }5. Web监控看板搭建:从Firebase数据到实时折线图的零配置落地
5.1 Firebase数据结构设计:扁平化路径适配前端渲染
项目采用极简Firebase Realtime Database结构,完全规避复杂查询:
your-project-default-rtdb/ ├── nodes/ │ ├── slave1/ │ │ ├── temp: 25.3 │ │ ├── humi: 62.1 │ │ ├── gas: 85 │ │ ├── flame: 0 │ │ └── timestamp: "2024-06-15T08:22:31Z" │ ├── slave2/ │ │ ├── temp: 26.7 │ │ └── ... │ └── master/ │ └── last_seen: "2024-06-15T08:22:31Z" └── system/ └── uptime: 14283 // 秒提示:
Rev.ino中writeToFirebase()函数每次只写nodes/slaveX/下5个字段,不写嵌套对象。这种设计让前端可用firebase.database().ref('nodes/slave1').on('value', callback)单次监听,避免多次.child()链式调用导致的延迟。
5.2 HTML前端实现:120行代码搞定响应式看板
项目未提供前端源码,但Doc/Firebase_Interface.JPG截图显示其使用原生JavaScript+Chart.js。以下是可直接运行的index.html核心逻辑:
<!DOCTYPE html> <html> <head> <script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js"></script> <script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-database.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> </head> <body> <div><canvas id="tempChart"></canvas></div> <script> // Firebase配置(替换为你的真实配置) const firebaseConfig = { apiKey: "AIzaSy...", authDomain: "xxx.firebaseapp.com", databaseURL: "https://xxx-default-rtdb.firebaseio.com" }; firebase.initializeApp(firebaseConfig); const db = firebase.database(); // 初始化温度图表 const ctx = document.getElementById('tempChart').getContext('2d'); const tempChart = new Chart(ctx, { type: 'line', data: { labels: Array(60).fill(''), // 60秒历史 datasets: [ { label: 'Slave1', data: Array(60).fill(null), borderColor: 'red' }, { label: 'Slave2', data: Array(60).fill(null), borderColor: 'blue' } ] } }); // 实时监听Slave1温度 db.ref('nodes/slave1/temp').on('value', (snapshot) => { const temp = snapshot.val(); tempChart.data.datasets[0].data.push(temp); tempChart.data.datasets[0].data.shift(); tempChart.update(); }); </script> </body> </html>将此文件放在任意HTTP服务器(如Python的python3 -m http.server 8000)即可访问,无需Node.js环境。
5.3 数据持久化技巧:用Firebase规则防止误删
为防意外操作清空数据库,必须在Firebase控制台设置安全规则:
{ "rules": { "nodes": { ".read": "auth != null", ".write": "auth != null && newData.child('timestamp').exists()" }, "system": { ".read": "auth != null", ".write": "auth != null" } } }此规则强制所有写入nodes/下的数据必须包含timestamp字段,避免setString()误操作覆盖整个节点。
6. 进阶实战:给LoRa Mesh加心跳保活与低功耗调度,延长电池寿命3倍
6.1 动态心跳机制:用RSSI预测链路质量,避免无效轮询
原始Rev.ino中Master对每个Slave固定30秒轮询,但实际中Slave3可能因建筑遮挡RSSI长期<-95dBm,持续轮询只会耗尽ESP8266电量。改进方案是引入链路质量感知心跳:
// 在Rev.ino中定义RSSI阈值表 const int8_t RSSI_THRESHOLD[5] = {-1, -70, -75, -80, -85}; // ID1~4对应阈值 void pollSlave(uint8_t id) { if (slaveOnline[id] && rssiHistory[id] > RSSI_THRESHOLD[id]) { // 信号好:20秒轮询 sendPing(id); } else if (slaveOnline[id]) { // 信号差:120秒轮询,降低频率 if (millis() - lastPollTime[id] > 120000) { sendPing(id); lastPollTime[id] = millis(); } } }rssiHistory[id]由每次PONG响应中的RSSI值滑动平均计算(窗口大小5),这样Slave2在地下车库(RSSI≈-92)会自动降为2分钟轮询,而Slave1在楼顶(RSSI≈-65)保持20秒高频更新。
6.2 Nano从机深度睡眠:用LoRa唤醒替代定时唤醒
Nano默认常电运行,电流约15mA。通过sl1.ino中集成avr/sleep.h,可实现LoRa中断唤醒:
#include <avr/sleep.h> #include <avr/wdt.h> void enterSleep() { set_sleep_mode(SLEEP_MODE_PWR_DOWN); sleep_enable(); // 配置E32的M0/M1引脚为外部中断源(需硬件连线) attachInterrupt(digitalPinToInterrupt(2), onLoRaRx, FALLING); sleep_cpu(); // 进入深度睡眠,电流降至0.1mA } void onLoRaRx() { detachInterrupt(0); // 唤醒后关闭中断 // 执行数据采集与发送 readSensors(); buildPacket(); sendPacket(); }注意:此功能需将E32的
AUX引脚(状态指示)连接到Nano的D2引脚,并在setup()中配置pinMode(2, INPUT_PULLUP)。实测4节AA电池(2500mAh)可支撑Slave节点运行18个月。
6.3 Mesh路由优化:用跳数权重替代简单最短路径
原始路由仅选跳数最少路径(如Slave2→Slave1→Master),但若Slave1本身RSSI极差(-98dBm),该路径反而丢包率高。改进版引入加权路径选择:
// 在sl2.ino中计算下一跳权重 int getRouteWeight(uint8_t nextHopId) { int weight = 0; weight += (100 + rssiHistory[nextHopId]); // RSSI越高,权重越低 weight += hopCount[nextHopId]; // 当前跳数 return weight; } // 选择weight最小的邻居 uint8_t bestNextHop = 0; int minWeight = 999; for (uint8_t i = 1; i <= 4; i++) { if (rssiHistory[i] > -100 && i != MY_ID) { int w = getRouteWeight(i); if (w < minWeight) { minWeight = w; bestNextHop = i; } } }此算法让Slave2优先选择RSSI=-72的Slave3(权重28),而非RSSI=-95的Slave1(权重5),大幅提升端到端成功率。
从那以后我每次部署城市监测节点,都会先用万用表测E32频点、用串口抓AT+VER固件版本、再烧录前强制修改MY_ID——这三步走完,Mesh组网成功率从60%拉到100%。那些声称“LoRa Mesh开箱即用”的教程,往往省略了硬件批次差异带来的静默故障,而这恰恰是现场工程师最耗时间的环节。希望帮到你。
本文还有配套的精品资源,点击获取