嵌入式工业网关的断点续传编程:网络恢复后自动补传离线期间的采集数据
工业网关断点续传不是“断网了把数据写文件、联网了读出来发”这么粗糙。核心就一条:采集线程永远不阻塞,上行失败只进本地缓存,收到业务ACK才清记录。网络断几小时、网关掉电重启,数据都该按采集时间顺序补回去。
一、架构:采集与补传彻底解耦
采集线程 ─▶ RAM环形缓冲 ─▶ 批量落盘(Flash/FRAM/eMMC)
│
未确认记录区
│
补传线程(网络OK才动)
│
MQTT/QoS1或HTTP
│
服务端ACK回写
采集侧只push,不等socket;补传侧独立线程扫缓存。 关键纪律:采集线程绝不因网络卡住,否则一台设备掉线拖垮整网关。
二、缓存记录格式
每条采集值带采集时间戳+seq,服务端按时间戳入库去重:
typedef struct __attribute__((packed)){
uint32_t id; // 自增主键,补传顺序
uint64_t ts_ms; // 采集时刻,不是发送时刻
uint16_t point_id;
int32_t value;
uint8_t qual; // 0正常 1估算 2超时
uint8_t sent; // 0未传 1已确认
uint16_t crc;
} rec_t;
裸机用定长二进制环区,Linux网关用SQLite表(id,ts_ms,point_id,value,qual,sent),索引建(sent,ts_ms)。
三、采集落盘:批量刷,别每条写
高频采集每点都写Flash,擦写寿命几天就废。正确做法RAM攒批:
#define BATCH_N 64
static rec_t wb[BATCH_N];
static int wi;
void sample_push(uint16_t pid,int32_t v){
rec_t*r=&wb[wi];
r->id = next_id++;
r->ts_ms = rtc_ms();
r->point_id=pid; r->value=v; r->qual=0;
r->sent=0; r->crc=crc16(r,sizeof(*r)-2);
if(++wi>=BATCH_N){
flash_append(wb,sizeof(wb)); // 批量落盘
wi=0;
}
}
/* 定时2s或满批刷一次,防掉电丢内存 */
void sample_flush(void){
if(wi) flash_append(wb,wi*sizeof(rec_t));
wi=0;
}
断网期间也照常落盘;网络恢复线程只读sent=0记录。
四、断网检测:别只信网卡link
以太网口up≠云端通。应用层心跳+发送超时双判:
void net_monitor(void){
if(mqtt_ping_timeout(3)){ // 连续3次无PINGRESP
g_net_online=0;
}
}
void on_mqtt_connect(void){
g_net_online=1;
resume_upload(); // 触发补传
}
掉线状态里采集照常写缓存,不发网络包。
五、补传状态机
按id升序取窗口,收到ACK才标sent,失败就停:
#define WIN 32
void upload_task(void){
if(!g_net_online) return;
rec_t buf[WIN];
int n = cache_load_unsent(buf, WIN); // SELECT ... WHERE sent=0 ORDER BY id
for(int i=0;i<n;i++){
if(post_one(&buf[i])!=0){ // HTTP 2xx / MQTT PUBACK
break; // 当前条失败,后面保留
}
cache_mark_sent(buf[i].id); // UPDATE sent=1
}
}
几个硬规矩:
发成功再删/标sent,别先删再发→发送中掉电就丢
MQTT用QoS1,PUBACK丢失会重发→服务端按point_id+ts_ms去重
批次合并DELETE,别逐条删,SQLite写放大炸
补传限速,刚恢复别几万条一起灌爆broker
六、掉电保护
RAM环缓掉电清空,所以:
关键记录进FRAM/NOR,配置/断点指针也放这
eMMC/SD走SQLite WAL,synchronous=NORMAL
电源加掉电检测+PVD,中断里只做sample_flush()最小写入
写记录加commit字节或CRC,重启扫描丢弃半截记录
超级电容留50~200ms写窗口
七、缓存满策略
长断网缓存爆了必须决断:
工业默认丢最老保最新(FIFO覆盖)
关键工序可改“停采保历史”,但实时监控会空窗
水位超80%上报告警,别等满才处理
if(cache_usage()>0.9f){
cache_drop_oldest(16); // 批量裁旧,不单条删
warn_upload_backlog();
}
八、落坑清单
采集线程里阻塞发MQTT→整链卡死
每条样本写Flash→寿命耗尽
用接收时间入库→补传数据时间错乱
先删缓存再等ACK→掉电丢数据
QoS0当可靠传输→断网必丢
不标sent字段→重启重复全传
恢复瞬间全量重发→broker被打挂
内存队列当持久层→断电全没
只信网卡link→公网断了还在发
缓存满不处理→OOM网关重启
九、落地顺序
定义rec_t:ts_ms+point_id+value+sent+crc
采集线程只写RAM环缓,满批/定时刷Flash
网络状态用心跳判,不只看link
补传线程按id升序取窗口
post成功才标sent,失败停当前批
MQTT QoS1+服务端时间戳去重
掉电中断只做最小flush,FRAM存断点
缓存水位超阈值裁旧+告警
断点续传的本质是“采集和上行解耦成两块”:采集侧 guaranteed store,上行侧 best-effort forward,中间靠本地缓存扛住断网和掉电。 真正稳的网关不是不断网,而是断网时数据进盘、恢复时按序补、重复靠时间戳去重、掉电重启能从断点继续。别在采集中断里碰socket,也别把内存队列当黑匣子——这两点守住,工业现场4G抖几小时,MES里数据也只是晚到,不会缺。





