在公共空间、园区、楼宇的物联网项目中,底层设备协议繁杂:电表多为 DL/T 645,温湿度、氨气、液位等传感器多为 Modbus RTU,部分新设备又走 MQTT。现场网络还极不稳定,4G 波动、信号遮挡、断网是常态。如果所有协议解析、数据清洗都放在云端,不仅延迟高,断网期间数据还会永久丢失。本文从嵌入式实现角度,讨论如何在边缘计算网关本地完成多协议解析、数据清洗和断网续传。
协议解析与统一数据模型
Modbus RTU 的帧结构相对简单,典型组成包括从站地址、功能码、数据长度、寄存器数据、CRC 校验。例如读取保持寄存器时,主站下发:
[从站地址][03H][起始寄存器高][起始寄存器低][寄存器数量高][寄存器数量低][CRC低][CRC高]
DL/T 645 的帧格式则不同,常见为:
68H | A0~A5 | 68H | 控制码 | 数据长度 | 数据域 | CS | 16H
其中 68H 为帧起始符,A0~A5 为表地址,控制码决定读/写操作,数据域携带寄存器值或电量数据,CS 为校验和。两种协议在物理层、帧结构、校验方式上都不一致,直接交给云端处理会让上层业务承担过多适配成本。
更合理的做法是在网关侧完成协议解析,然后映射到统一数据模型。示例如下:
typedef struct {
uint32_t timestamp; /* 采集时间戳,单位秒 */
uint16_t device_id; /* 设备ID */
uint8_t protocol; /* 0: Modbus, 1: DL/T645 */
float voltage; /* 电压 V */
float current; /* 电流 A */
float active_power; /* 有功功率 kW */
uint8_t is_clean; /* 0: 原始数据, 1: 清洗后数据 */
} StandardDataPoint;
解析 Modbus 时,可根据寄存器地址映射电压、电流、功率;解析 DL/T 645 时,则从数据域中提取对应编码字段。解析完成后,统一写入 StandardDataPoint,后续清洗、缓存、上传都基于同一结构体处理。
边缘数据清洗:去毛刺,但不丢原始数据
现场采集的数据经常会出现毛刺。通信干扰、传感器瞬态异常、设备重启、电源波动,都可能让某一次采样值明显偏离正常范围。如果直接上传,云端报表会出现跳变;如果简单丢弃,又可能丢失真实异常。
一个较实用的做法是:滑动窗口均值 + 3-Sigma 异常剔除,同时保留原始数据。
import math
def clean_data_point(values, window=5, k=3.0):
if len(values) < 2:
return values[-1], True
window_data = values[-window:]
mean = sum(window_data) / len(window_data)
variance = sum((x - mean) ** 2 for x in window_data) / len(window_data)
std = math.sqrt(variance)
current = values[-1]
if std < 1e-6:
return current, True
if abs(current - mean) > k * std:
return mean, False # 异常值,用均值替代,但原始值仍入库
return current, True
这里的关键不是"把异常值删掉",而是区分两条数据链路:
- 原始通道 :所有原始采样值入库,
is_clean = 0,用于审计、追溯、故障定位。 - 清洗通道 :经过滤波后的数据入库,
is_clean = 1,用于平台展示、报表和告警。
这样即使清洗算法误判,也能通过原始数据回溯。边缘侧保留原始数据,是后续断网续传和审计对账的基础。
断网缓存与断点续传
现场网络不稳定时,网关不能因为云端暂时不可达就丢弃数据。通常做法是把数据先写入本地轻量数据库,网络恢复后再按时间顺序补传。Redis 并不适合这种场景,因为它默认内存存储,断电后数据容易丢失;SQLite 或本地时序库更稳妥。
本地建表示例:
CREATE TABLE IF NOT EXISTS data_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL,
device_id INTEGER NOT NULL,
protocol INTEGER NOT NULL,
voltage REAL,
current REAL,
active_power REAL,
is_clean INTEGER NOT NULL DEFAULT 0,
uploaded INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_ts_uploaded
ON data_points(timestamp, uploaded);
采集线程每次解析完成后,将数据插入本地表,uploaded = 0 表示尚未上传。上传线程在网络正常时查询未上传数据:
SELECT * FROM data_points
WHERE uploaded = 0
ORDER BY timestamp ASC
LIMIT 100;
每批上传成功后,再更新状态:
UPDATE data_points
SET uploaded = 1
WHERE id IN (/* 本批已上传ID */);
断点续传的核心逻辑是时间戳比对。网络恢复后,网关可以先向云端查询最新已接收时间戳 T_cloud,然后从本地查询:
SELECT * FROM data_points
WHERE timestamp > :T_cloud AND uploaded = 0
ORDER BY timestamp ASC
LIMIT 100;
补传流程如下:
- 网关检测网络恢复;
- 请求云端最新
T_cloud; - 本地查询
timestamp > T_cloud且未上传的数据; - 按时间顺序分批补传;
- 每批确认后再标记
uploaded = 1; - 若补传中途再次断网,下次恢复后继续从
T_cloud比对。
这种方式可以避免重复上传,也能防止断网期间数据空洞。对碳核查、能耗审计类项目尤其重要,因为缺失的数据段会直接影响统计完整性。
工程实现中的几个注意点
- 时间同步:网关本地时间必须可靠,建议通过 NTP 校时;断网期间可依赖 RTC 维持时间连续性。
- 事务写入:本地入库应使用事务,避免断电导致半条记录。
- 背压控制:如果断网时间过长,本地存储可能打满,需要设定保留策略,例如保留最近 30 天原始数据,超期数据归档或告警。
- 双通道一致性:原始数据和清洗数据应共享同一时间戳和设备 ID,便于后续对账。
- 上传幂等 :云端接收接口应支持按
timestamp + device_id去重,防止补传时产生重复记录。
小结
边缘计算网关在物联网数据链路中的价值,主要体现在三点:屏蔽底层协议差异、保证数据完整性、降低云端处理压力。Modbus 和 DL/T 645 只是常见起点,实际项目中还会遇到 MQTT、BACnet、私有 TCP 协议等。无论上层协议如何变化,边缘侧的职责相对稳定:把不同来源的数据解析成统一模型,在本地完成初步清洗,断网时可靠缓存,网络恢复后按时间戳有序补传。
这类实现没有太多"炫酷"的地方,但决定了整套系统在现场能不能长期跑稳。欢迎同行在评论区讨论边缘侧的协议适配、清洗策略和本地存储方案。