零、一个看似自相矛盾的需求
前面的 Mesh 系列文章反复强调了一件事:LoRa 带宽极低,单信道不到 6 kbps。在这个前提下,如果有人对你说——
"我想在这个 LoRa Mesh 上跑 MQTT,10Hz 频率,大量消息。"
你的第一反应大概率是:不可能。
但先别急。这个需求并非凭空想象——工业传感器、无人机遥测、车辆追踪、机器人状态上报,这些场景天然需要高频数据流。即便在低带宽 Mesh 上,我们也可以通过一套组合策略让"10Hz MQTT over LoRa"在某些约束条件下变得可行。
这篇文章不讲"能不能",讲的是"在什么条件下能,以及怎么做"。
一、先算账:10Hz 到底要吃掉多少带宽
1.1 标准 MQTT 在 LoRa 上的自杀式开销
一个最小化的 MQTT v3.1.1 PUBLISH 报文的结构:
1 2 3 4 5 6 7 8 9 10 11
| ┌──────────────┬────────┬───────┬─────────┬──────────┐ │ Fixed Header │ Topic │ Msg ID│ Payload │ 总计 │ │ 2~5 字节 │ Len+N │ 2字节 │ P │ │ └──────────────┴────────┴───────┴─────────┴──────────┘
假设 Topic = "sensor/temp/room1" (19 字节), Payload = 4 字节(float) 最小 PUBLISH 报文 ≈ 2 + 2 + 19 + 2 + 4 = 29 字节
加上 QoS 1 的两轮 ACK: PUBACK ≈ 4 字节 → 每次可靠传输 ≈ 29 + 4 = 33 字节
|
33 字节 × 8 bit/字节 = 264 bit。10 条/秒 = 2640 bps。
看起来不到 5.5 kbps 的一半?别急,这只是一条消息在空中的大小。在 Mesh 网络中:
1 2 3 4 5 6
| Mesh 路由开销(以 Meshtastic 为例): - 消息头: ~20 字节(源、目标、跳数、消息 ID) - 实际在空中传输: 33 + 20 = 53 字节
加上洪泛:平均每个消息被转发 15 次(20 节点网络) 空中总流量 = 53 × 8 × 10 × 15 = 63,600 bps ≈ 63.6 kbps
|
63.6 kbps 的总空中流量需求 vs 5.5 kbps 的单信道容量——差距是 11.5 倍。
这就是为什么标准 MQTT + Meshtastic 洪泛 + 10Hz = 网络瞬间崩溃。
1.2 在 Reticulum 路由模式下的改善
Reticulum 的路由寻址替代洪泛后,同样的场景:
1 2 3 4
| 平均跳数: 3(而非 15 次洪泛) 空中总流量 = 53 × 8 × 10 × 3 = 12,720 bps ≈ 12.7 kbps
仍然超出 5.5 kbps 约 2.3 倍。
|
结论:仅靠'用 Reticulum 替代 Meshtastic'还不足以支撑 10Hz。需要在协议栈的每一层做优化。
二、第一刀:MQTT-SN,为低带宽而生
2.1 MQTT-SN 和标准 MQTT 的差异
MQTT-SN(MQTT for Sensor Networks)是专门为低带宽、低功耗网络设计的变体。核心差异:
| 特性 |
标准 MQTT |
MQTT-SN |
| Topic 表示 |
完整字符串 sensor/temp/room1 |
2 字节 Topic ID |
| 传输层 |
TCP(需要稳定连接) |
UDP(无连接) |
| QoS -1 |
无此模式 |
预定义 Topic,无需注册 |
| Keep Alive |
PINGREQ/PINGRESP 心跳 |
GW 代为管理,终端可休眠 |
| 连接建立 |
三次握手 + CONNECT |
单包注册 |
| 最小 PUBLISH |
~29 字节 |
~7 字节 |
2.2 MQTT-SN 最小报文拆解
1 2 3 4 5 6 7 8 9 10 11 12 13
| MQTT-SN PUBLISH (QoS -1, 预定义 Topic): ┌─────────┬──────────┬─────────┐ │ Length │ Flags │ Payload │ │ 1 字节 │ 1 字节 │ P │ └─────────┴──────────┴─────────┘
Flags 字段包含: - Topic ID Type: 2 bit (预定义 Topic ID = 无需注册) - Topic ID: 10+ bit (可表示 1024+ 个预定义 Topic) - QoS: 2 bit (设为 -1 时无 ACK) - DUP/Retain: 各 1 bit
总开销 = 2 字节(Length + Flags)!
|
对比标准 MQTT 的 29 字节 → 减少 93% 的协议开销。
2.3 回到算账
MQTT-SN 优化后:
1 2 3 4
| 单条消息在空中: 7 字节(PUBLISH) + 22 字节(Mesh头) = 29 字节 空中总流量(3跳路由) = 29 × 8 × 10 × 3 = 6,960 bps ≈ 7 kbps
仍然超出 5.5 kbps 约 27%。
|
协议开销已经砍到极致,但 Mesh 路由转发仍然放大了流量。需要继续优化。
三、第二刀:批量打包——用时间换带宽
3.1 从"每秒 10 条"到"每秒 1 批"
10Hz 意味着每 100ms 一个采样点。如果应用能容忍 1 秒的延迟(对于大多数遥测场景完全可以接受),可以把 10 个采样点打包成一个批量消息:
1 2 3 4 5 6 7 8 9 10
| 单条 PUBLISH(1 个采样点)→ 7 字节 批量 PUBLISH(10 个采样点)→ 1 + 1 + 4×10 = 42 字节
每秒发 1 条批量消息 vs 每秒发 10 条单点消息:
单点方案: 29 × 10 × 3 = 870 字节/秒(空中) 批量方案: (22 + 42) × 1 × 3 = 192 字节/秒(空中)
空中流量减少 78%。 192 × 8 = 1,536 bps — 仅占 5.5 kbps 的 28%
|
到了这里,10Hz 等效数据率在 3 跳 Mesh 上已经完全可行。
3.2 批量格式设计:Protobuf
手工 struct 打包虽然紧凑,但缺乏可扩展性——加一个字段就得改解析逻辑。Protobuf 兼顾紧凑性和向后兼容,且生态成熟。
1 2 3 4 5 6 7 8 9
| syntax = "proto3";
message SensorBatch { uint32 topic_id = 1; double start_time = 2; uint32 interval_ms = 3; repeated float samples = 4 [packed=true]; }
|
packed=true 是关键:重复的 float 值不会被逐个 Tag+Value 编码,而是共享一个 Tag,后面跟一个紧凑的长度前缀 + 连续值——与 struct 手工打包的紧凑度几乎等同。
编译与使用:
1
| protoc --python_out=. sensor_batch.proto
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from sensor_batch_pb2 import SensorBatch
def pack_batch(topic_id: int, samples: list[float], start_time: float, interval_ms: int = 100) -> bytes: """Protobuf 序列化,10 个采样点 ≈ 52 字节(比 struct 多 5 字节 Tag 开销)""" batch = SensorBatch() batch.topic_id = topic_id batch.start_time = start_time batch.interval_ms = interval_ms batch.samples.extend(samples) return batch.SerializeToString()
def unpack_batch(data: bytes) -> SensorBatch: batch = SensorBatch() batch.ParseFromString(data) return batch
|
Protobuf 比手工 struct 多出约 5 字节的 Tag/Varint 开销,换来的是:
- 模式演化:新增字段(如
battery_voltage)不破坏旧固件
- 跨语言:C++/Python/Java/Go/Rust 共享同一份
.proto
- 生态工具:
protoc 直接生成序列化/反序列化代码,零手写解析器
3.3 进一步压缩:差分 Protobuf
如果连续采样值变化缓慢(温度、湿度),Protobuf 版本也可以与差分编码结合——单独定义一个精简 message:
1 2 3 4 5 6 7 8 9 10
| syntax = "proto3";
message SensorDiff { uint32 topic_id = 1; double start_time = 2; uint32 interval_ms = 3; sint32 base = 4; bytes deltas = 5; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import struct from sensor_diff_pb2 import SensorDiff
def diff_encode_proto(topic_id: int, samples: list[float], start_time: float, scale: float = 100) -> bytes: """差分编码 + Protobuf,10 个采样点 ≈ 18 字节""" msg = SensorDiff() msg.topic_id = topic_id msg.start_time = start_time msg.interval_ms = 100 msg.base = int(samples[0] * scale) diffs = bytearray() for i in range(1, len(samples)): diff = int((samples[i] - samples[i-1]) * scale) diff = max(-128, min(127, diff)) diffs.append(diff & 0xFF) msg.deltas = bytes(diffs) return msg.SerializeToString()
|
极端优化下,10Hz 温度传感数据在 LoRa 上的空中占用不到 20% 的单信道容量。
四、第三刀:多信道并行——用频率换带宽
4.1 CN470 频段的可用信道
中国 LoRa CN470-510 频段有多个可用信道:
1 2 3 4 5 6 7 8
| CN470 上行信道(部分): Channel 0: 470.3 MHz Channel 1: 470.5 MHz Channel 2: 470.7 MHz ... Channel 7: 471.9 MHz
共 8 个 125kHz 信道可用(受限于 LoRaWAN 规范定义,非 LoRaWAN 场景可自行规划)
|
如果同时使用 4 个信道,总容量就是 5.5 × 4 = 22 kbps。
4.2 按优先级分信道
1 2 3 4 5 6
| 信道分配策略(4 信道方案):
Ch 0 (470.3 MHz): 路由与网络管理(Reticulum Announce、路径探测) Ch 1 (470.5 MHz): 高优消息(紧急告警、控制指令,QoS 1) Ch 2 (470.7 MHz): 批量遥测上行(传感器数据,QoS -1,10Hz 批量) Ch 3 (470.9 MHz): 大文件/固件升级(低优先级,QoS -1,深夜传输)
|
在 Reticulum 中,每个接口可以绑定不同的 LoRa 模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| interfaces: - type: RNodeInterface frequency: 470300000 bandwidth: 125000 spreading_factor: 7 name: "Ch0-Control" - type: RNodeInterface frequency: 470700000 bandwidth: 125000 spreading_factor: 7 name: "Ch2-Telemetry"
|
硬件成本增加:每增加一个信道,多加一个 Heltec V3(35 元)+ 天线(5 元)= 40 元。4 信道方案总硬件增量 120 元——极为廉价。
五、端到端架构:从传感器到云端的完整链路
5.1 整体架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| [传感器节点群] (10Hz 采样) │ │ MQTT-SN PUBLISH (批量, QoS -1) │ 通过 Reticulum Mesh (470.7 MHz 遥测信道) ↓ [MQTT-SN Gateway] (Orange Pi + Heltec V3 × 2) │ │ ① 接收 MQTT-SN 批量包 │ ② 解包、校验、时序重建 │ ③ 转换为标准 MQTT (QoS 1) │ ④ 通过互联网/Wi-Fi 上行到云端 MQTT Broker ↓ [EMQX / Mosquitto Broker] (云端) │ ↓ [数据消费者] (InfluxDB / Grafana / 控制台)
|
5.2 Gateway 节点核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| """ MQTT-SN over Reticulum Gateway 运行在 Orange Pi 上,桥接 LoRa Mesh 和互联网 MQTT Broker """ import time import paho.mqtt.client as mqtt import RNS from sensor_batch_pb2 import SensorBatch
MESH_TOPIC_MAP = { 0x01: "sensor/temperature/batch", 0x02: "sensor/humidity/batch", 0x03: "sensor/gps/batch", 0x10: "control/relay/command", } MQTT_BROKER = "192.168.1.100"
mqtt_client = mqtt.Client() mqtt_client.connect(MQTT_BROKER, 1883) mqtt_client.loop_start()
def on_mesh_packet(message, packet): batch = SensorBatch() try: batch.ParseFromString(message) except: return topic_name = MESH_TOPIC_MAP.get(batch.topic_id) if topic_name is None: return for i, value in enumerate(batch.samples): ts = batch.start_time + i * batch.interval_ms / 1000.0 from sensor_batch_pb2 import SensorPoint point = SensorPoint() point.timestamp = ts point.value = value mqtt_client.publish(topic_name, point.SerializeToString(), qos=1)
reticulum = RNS.Reticulum() identity = RNS.Identity()
mesh_iface = reticulum.get_interfaces()[1] mesh_iface.on_packet = on_mesh_packet
print("[Gateway] MQTT-SN → MQTT Bridge running (Protobuf)...") while True: time.sleep(1)
|
Mesh 内和 MQTT 上行全程使用 Protobuf,云端消费者(InfluxDB/Grafana)通过同一份 .proto 生成的代码反序列化,无需任何文本解析。
5.3 传感器节点代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| """ 传感器节点:每 100ms 采样,每秒批量发送一次 """ import time import random import RNS from sensor_batch_pb2 import SensorBatch
reticulum = RNS.Reticulum() identity = RNS.Identity() destination = RNS.Destination( identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "mqtt_sn_gateway", "telemetry_batch" )
TOPIC_TEMP = 0x01 BATCH_SIZE = 10 SAMPLE_INTERVAL = 0.1
while True: batch = SensorBatch() batch.topic_id = TOPIC_TEMP batch.start_time = time.time() batch.interval_ms = int(SAMPLE_INTERVAL * 1000)
for _ in range(BATCH_SIZE): value = 25.0 + random.uniform(-0.5, 0.5) batch.samples.append(value) time.sleep(SAMPLE_INTERVAL) RNS.Packet(destination, batch.SerializeToString()).send()
|
六、总结:10Hz MQTT over LoRa Mesh 的可行性矩阵
| 优化策略 |
单条消息大小 |
空中流量(3跳) |
占信道容量 |
可行性 |
| 标准 MQTT + 洪泛 |
33 字节 |
63.6 kbps |
1156% |
✗ |
| 标准 MQTT + 路由 |
33 字节 |
12.7 kbps |
231% |
✗ |
| MQTT-SN + 路由 |
7 字节 |
7.0 kbps |
127% |
△ 临界 |
| MQTT-SN + 批量 |
42 字节(批) |
1.5 kbps |
28% |
✓ |
| + 差分量化和多信道 |
20 字节(批) |
0.7 kbps |
4% |
✓ 充裕 |
最终答案:在 Reticulum 路由模式 + MQTT-SN 协议 + 批量打包 + 差分编码的组合策略下,10Hz 等效数据率在 3 跳 LoRa Mesh 上完全可行,且留有充裕的信道余量给其他流量。
关键约束条件(必须满足)
- 延迟容忍 ≥ 1 秒:批量打包带来了 1 秒的端到端延迟(采完 10 个点才发)
- QoS 0 或 -1:高频遥测不能用 QoS 1(每条消息都需要 ACK),必须接受偶尔的丢包
- Protobuf 序列化:放弃 JSON/文本,全程 Protobuf——Mesh 内和 MQTT 上行共用同一份
.proto 定义
- 路由模式,非洪泛:必须用 Reticulum 或 MeshCore,不能用 Meshtastic 的默认洪泛
- Topic 预定义:MQTT-SN 的 Topic ID 需要在 Gateway 和传感器端预先约定
如果不满足这些条件怎么办?
- 如果必须 QoS 1 可靠传输,把频率降到 2Hz 以下
- 如果必须 JSON 格式,把频率降到 1Hz 以下,并启用 GZIP 压缩
- 如果必须用 Meshtastic(洪泛),把频率降到 0.2Hz 以下(每 5 秒一条)
- 如果必须 < 100ms 延迟,放弃 LoRa,改用 Wi-Fi Mesh 或私有 LTE
低频 Mesh 通信和传统互联网之间有一条宽阔的工程鸿沟——跨越鸿沟的不是任何单一技术,而是协议选择、数据压缩、批量策略和信道规划的合力。