
1. 工业现场为什么需要这套机制做过工业数据采集的人都有一个共识实验室里跑得通的东西到了现场往往活不过三天。以太网温湿度采集就是典型例子。你用一个带以太网接口的温湿度传感器通过网线接到交换机再连到上位机或者网关看起来链路简单、协议清晰但真正部署到车间、仓库、机房、冷库这些地方之后问题会一个接一个冒出来。我在一个冷链仓储项目里就遇到过这种情况。现场有二十多个温湿度采集节点分布在四个库区每个库区通过一台工业交换机汇聚再统一上传到监控平台。刚上线那几天数据很漂亮曲线平滑、上报准时。结果第二周开始部分节点的数据出现断档有的断了十几分钟才恢复有的恢复之后中间那段数据直接丢了平台上的温度曲线出现了一个大坑。库房管理人员看到曲线断了第一反应是设备坏了跑去现场一看设备指示灯正常网线也没松重启一下又好了。这种问题反复出现靠人工重启根本扛不住。这就是以太网温湿度采集在真实场景下的核心痛点链路不是永远稳定的设备不是永远在线的但数据是不能丢的。尤其是温湿度这类带时间属性的数据一旦断档事后想补都补不回来因为温度是一个瞬时状态你不可能在下午三点去测上午十点的温度。所以断线重连和断点续传这两个机制不是锦上添花的功能而是决定这套系统能不能真正落地的关键。这篇文章面向的是正在做或者准备做以太网温湿度采集系统的嵌入式工程师、物联网开发者和工业现场实施人员。我会从整体设计思路讲起拆解多协议环境下断线重连和断点续传的具体实现方式给出可以直接参考的代码结构和参数配置最后把我踩过的坑和排查经验整理出来。不管你是用STM32加以太网PHY芯片自己画板子还是用ESP32接LAN8720模块做快速验证这套思路都能用得上。2. 整体设计思路与方案选型2.1 为什么不能只靠TCP的可靠性很多人第一反应是我用TCP协议不就行了TCP本身有重传机制链路断了它会自己重连数据丢了它会自己重发我上层什么都不用管。这个想法在理论上没错但在实际工程里有一个致命的前提——TCP的重传和重连是有时间窗口的而且它只能保证连接层面的可靠保证不了应用层面的数据完整。举个具体的例子。你的温湿度采集设备通过TCP连接到服务器采样周期是30秒一次。某时刻网络出现抖动TCP连接断了TCP协议栈开始尝试重连。如果重连在几秒内成功那没问题数据继续发。但如果网络断了五分钟呢这五分钟里设备采集了10条温湿度数据这些数据在TCP层面根本没有机会发出去因为连接已经断了。等连接恢复之后TCP不会帮你把这10条数据补发它只管当前连接建立之后的数据流。这10条数据就永久丢失了。所以结论很明确TCP解决的是传输层的可靠性应用层的数据完整性必须由自己来保证。断线重连解决的是连接恢复的问题断点续传解决的是数据补齐的问题两者缺一不可。2.2 多协议并存的现实工业现场还有一个特点协议不统一。你可能同时面对Modbus TCP、MQTT、HTTP、甚至自定义的TCP私有协议。温湿度传感器有的走Modbus TCP有的走MQTT上报有的通过HTTP POST推送到接口。上位机平台可能同时支持多种接入方式这就要求你的断线重连和断点续传机制不能只针对某一种协议写死而是要抽象出一套通用的框架。我的做法是把整个数据链路分成三层采集层、缓存层、传输层。采集层负责从传感器读取温湿度数据不管底层是Modbus还是I2C还是单总线读到的数据统一封装成一个带时间戳的数据结构。缓存层负责在传输失败时把数据存起来存的地方可以是RAM、Flash、SD卡或者外部EEPROM。传输层负责把数据发出去支持多种协议每种协议有自己的连接管理和重连策略。断线重连发生在传输层断点续传发生在缓存层和传输层的交界处。这样分层的好处是换协议不用动采集和缓存逻辑换存储介质不用动传输逻辑各层独立演进维护成本低。2.3 断点续传的两种策略选择断点续传在实现上有两种主流策略我分别说一下适用场景。第一种是序号确认机制。每条数据带一个自增序号服务器收到之后返回确认设备收到确认才把数据从缓存里删掉。如果没收到确认下次重连之后从最后一个未确认的序号开始重发。这种方式的优点是精确不会重复也不会遗漏缺点是每次发送都要等确认实时性稍差而且服务器端需要维护每个设备的序号状态。第二种是时间窗口补传机制。设备缓存最近一段时间的数据重连之后把断线期间的数据按时间顺序批量补传。服务器端根据时间戳去重。这种方式的优点是实现简单不需要维护复杂的确认状态缺点是如果缓存溢出或者时间窗口设置不合理可能会丢数据或者重复上报。我在实际项目中用的是两种结合的方式正常在线时用序号确认保证实时数据的可靠传输断线重连后用时间窗口批量补传断线期间的数据服务器端根据设备ID加时间戳做去重。这样既保证了实时性又保证了断线期间的数据不丢。3. 核心细节解析与实操要点3.1 断线检测的判据设计断线重连的第一步是准确判断什么时候算断线了。这个问题看起来简单实际上很容易误判。我见过有项目用发送失败就认为断线作为判据结果网络稍微抖动一下TCP发送缓冲区满了导致一次发送失败设备就触发重连频繁重连反而把链路搞得更不稳定。比较稳妥的判据是组合判断我通常用以下几个条件心跳超时设备每隔固定时间比如10秒向服务器发送心跳包如果连续3次心跳没有收到服务器响应判定为断线。这个超时时间不能太短太短容易误判也不能太长太长会导致断线发现不及时。10秒间隔加3次重试意味着最坏情况下30秒发现断线这个响应速度在温湿度采集场景里是够用的。发送失败计数连续N次发送失败N一般取3到5判定为断线。单次失败不触发重连避免网络抖动导致的误判。TCP连接状态如果底层协议栈能提供连接状态查询比如getsockopt返回的错误码可以作为辅助判据。但不要完全依赖它因为TCP连接状态有时候会有延迟。注意心跳间隔和超时次数要根据实际网络环境调整。在无线网桥或者4G回传的场景下网络延迟波动大心跳间隔建议放宽到30秒超时次数增加到5次否则会频繁误判。3.2 重连策略指数退避加随机抖动断线之后的重连策略直接影响到系统的稳定性。最简单的做法是断线后立即重连失败就再立即重连循环往复。这种做法在服务器宕机或者网络大面积故障时会导致所有设备同时疯狂重连形成惊群效应把服务器和网络彻底压垮。我采用的是指数退避加随机抖动的策略。具体来说第一次重连等待1秒第二次等待2秒第三次等待4秒第四次等待8秒以此类推最大等待时间封顶在60秒。同时每次等待时间加上一个随机抖动比如正负20%的随机值避免多个设备在同一时刻发起重连。用代码表示大概是这样// 计算下次重连等待时间毫秒 uint32_t calc_reconnect_delay(uint8_t retry_count) { uint32_t base 1000; // 基础等待1秒 uint32_t max_delay 60000; // 最大等待60秒 // 指数退避1s, 2s, 4s, 8s, 16s, 32s, 60s... uint32_t delay base (retry_count 6 ? 6 : retry_count); if (delay max_delay) delay max_delay; // 加入正负20%随机抖动 uint32_t jitter delay / 5; delay delay - jitter (rand() % (2 * jitter 1)); return delay; }这个策略的好处是网络短暂抖动时1秒后就能重连成功不影响实时性网络长时间故障时重连间隔逐渐拉长不会给服务器造成压力随机抖动避免了大量设备同步重连。3.3 缓存层的设计要点断点续传的核心是缓存。缓存设计要考虑三个问题存哪里、存多少、怎么淘汰。存哪里取决于数据量和硬件条件。如果断线时间短、数据量小用RAM缓存就够了比如在内存里开一个环形缓冲区存最近几百条数据。如果断线时间可能很长比如现场网络不稳定一天可能断好几次每次断几十分钟那就需要掉电不丢失的存储比如Flash或者SD卡。我在冷链项目里用的是外部SPI Flash容量2MB按每条数据32字节算能存六万多条按30秒采样一次能存二十多天的数据完全够用。存多少要根据最坏情况估算。计算公式是缓存容量 最大断线时长 / 采样周期 × 单条数据大小 × 安全系数。安全系数一般取1.5到2留足余量。比如最大断线时长按24小时算采样周期30秒单条数据32字节那需要2880条乘以2的安全系数就是5760条约180KB。选一个256KB的Flash分区就够了。怎么淘汰是很多人忽略的问题。如果缓存满了怎么办我的做法是覆盖最老的数据保证最新的数据一定能存下来。因为对于温湿度监控来说最新的数据价值最高历史数据虽然也重要但在缓存容量有限的情况下优先保新是合理的。当然如果业务要求所有数据都不能丢那就需要更大的存储或者更早的告警机制。3.4 多协议适配的抽象层设计前面提到要把传输层抽象出来具体怎么做我的做法是定义一个统一的传输接口每种协议实现这个接口。接口大概包含以下几个函数typedef struct { int (*init)(void *config); // 初始化协议 int (*connect)(void); // 建立连接 int (*send)(const uint8_t *data, uint16_t len); // 发送数据 int (*recv)(uint8_t *buf, uint16_t len, uint32_t timeout); // 接收数据 int (*is_connected)(void); // 查询连接状态 void (*close)(void); // 关闭连接 } transport_ops_t;Modbus TCP、MQTT、HTTP各自实现这套接口。上层的数据发送逻辑不关心底层用的是什么协议只管调用send函数。断线重连的逻辑也统一在传输层之上通过is_connected判断状态通过connect发起重连。这样新增一种协议只需要实现这套接口不用改动上层的缓存和续传逻辑。提示不同协议的连接概念不一样。TCP有明确的连接状态MQTT有会话状态HTTP是无连接的。对于HTTP这种无连接协议断线的判据要改成连续多次请求失败重连策略也要相应调整不能照搬TCP的那套。4. 实操过程与核心环节实现4.1 硬件平台选型与接线要点先说一下硬件平台。温湿度采集节点常用的方案有两种一种是STM32F407加RMII接口的以太网PHY芯片比如DP83848或者LAN8720另一种是ESP32加LAN8720模块。两种方案我都用过各有优劣。STM32F407的方案稳定性更好适合工业现场长期运行但开发门槛稍高RMII接口的布线有讲究时钟线要走等长差分线要控制阻抗。ESP32的方案开发快自带WiFi和蓝牙适合快速验证和小批量部署但以太网部分的稳定性略逊一筹尤其是在电磁干扰强的车间环境里。关于ESP32连接LAN8720我踩过几个坑这里展开说一下。第一个坑是RMII时钟配置。LAN8720需要一个50MHz的时钟输入这个时钟可以由ESP32的GPIO输出也可以由LAN8720自己的晶振提供。如果用ESP32输出需要在代码里配置CONFIG_ETH_RMII_CLK_OUTPUT_GPIO0而且GPIO0在启动时还有特殊功能容易冲突。我建议直接用LAN8720模块上的晶振省事而且稳定。第二个坑是PHY地址配置。LAN8720的PHY地址由PHYAD0引脚决定悬空时地址是0接地时地址是1。很多模块默认悬空但代码里如果写成1就连不上。这个一定要看模块的原理图确认。第三个坑是复位时序。LAN8720上电后需要一段时间才能正常工作如果ESP32启动太快可能在PHY还没准备好的时候就去初始化导致初始化失败。解决办法是在初始化之前加一个延时或者通过复位引脚主动复位一次。接线方面RMII接口需要连接TXD0、TXD1、TXEN、RXD0、RXD1、CRS_DV、REF_CLK、MDIO、MDC这几根线加上电源和地。具体的接线图各个模块的规格书里都有照着接就行关键是别接错线序尤其是差分线对。4.2 数据采集与时间戳处理温湿度数据的采集本身不复杂SHT30、SHT31、DHT22这些传感器都有成熟的驱动库。关键是时间戳的处理。断点续传依赖时间戳来判断哪些数据已经上传、哪些还没上传所以时间戳的准确性很重要。如果设备有RTC或者能通过网络对时那直接用绝对时间戳。如果没有RTC网络又断了没法对时那就用相对时间戳比如设备启动后的毫秒数或者采样序号。相对时间戳在断线期间也能正常递增重连之后服务器根据设备ID和相对时间戳来排序和去重。我通常的做法是设备启动时通过NTP对时获取一个基准时间之后用本地定时器累加。每次采样时记录当前时间戳。如果断线期间定时器继续走时间戳就不会乱。重连之后如果发现时间偏差太大再重新对时。数据结构大概是这样typedef struct { uint32_t timestamp; // 时间戳秒 int16_t temperature; // 温度放大10倍存储单位0.1℃ uint16_t humidity; // 湿度放大10倍存储单位0.1%RH uint16_t seq; // 序号用于确认机制 uint8_t flags; // 标志位标记是否已上传等 } th_record_t;每条记录16字节加上文件头和对齐实际占32字节左右。这个结构紧凑适合在Flash里存储。4.3 断线重连的完整流程实现断线重连的完整流程我分成几个状态已连接、检测中、重连中、已断开。状态机大概是这样运转的设备正常运行时处于已连接状态每隔10秒发送一次心跳。如果心跳超时或者发送失败计数达到阈值进入检测中状态再尝试一次通信如果还是失败确认断线进入重连中状态。在重连中状态按照指数退避策略等待一段时间后发起重连重连成功则回到已连接重连失败则继续等待下一次重连。如果连续重连失败次数超过上限比如20次进入已断开状态此时降低重连频率比如每5分钟尝试一次避免无谓的资源消耗。代码框架大概是这样typedef enum { STATE_CONNECTED, STATE_CHECKING, STATE_RECONNECTING, STATE_DISCONNECTED } conn_state_t; void conn_state_machine(void) { static conn_state_t state STATE_DISCONNECTED; static uint8_t retry_count 0; static uint32_t last_attempt 0; switch (state) { case STATE_CONNECTED: if (heartbeat_timeout() || send_fail_count 3) { state STATE_CHECKING; } break; case STATE_CHECKING: if (try_communicate() SUCCESS) { state STATE_CONNECTED; retry_count 0; } else { state STATE_RECONNECTING; retry_count 0; } break; case STATE_RECONNECTING: if (millis() - last_attempt calc_reconnect_delay(retry_count)) { if (do_connect() SUCCESS) { state STATE_CONNECTED; retry_count 0; trigger_resume(); // 触发断点续传 } else { retry_count; last_attempt millis(); if (retry_count 20) { state STATE_DISCONNECTED; } } } break; case STATE_DISCONNECTED: if (millis() - last_attempt 300000) { // 5分钟 state STATE_RECONNECTING; retry_count 0; } break; } }这个状态机的关键是重连成功后要触发断点续传。很多人重连成功就以为万事大吉了忘了把断线期间缓存的数据补传上去结果数据还是丢了。4.4 断点续传的具体实现断点续传的触发时机是重连成功之后。具体流程是重连成功后先查询缓存里有多少条未上传的数据然后按时间顺序批量发送。发送过程中如果再次断线记录当前发送到的位置下次重连后从这个位置继续。批量发送的时候要注意几个参数。批量大小不能太大否则单次发送时间太长容易超时也不能太小否则交互次数太多效率低。我一般取20到50条一批根据网络质量调整。发送间隔也要控制不能一股脑全发出去要给服务器处理的时间一般每批之间间隔100到200毫秒。服务器端需要配合做去重。去重的依据是设备ID加时间戳。如果服务器收到重复的数据直接丢弃或者覆盖保证最终存储的数据是唯一的。注意断点续传的过程中如果缓存被写满了新数据会覆盖老数据可能导致正在续传的数据被覆盖。解决办法是在续传期间暂停采集或者把续传的数据先复制到另一个缓冲区。我通常的做法是续传期间正常采集但缓存采用双分区设计一个分区用于正常写入一个分区用于续传读取避免冲突。5. 常见问题与排查技巧实录5.1 断线重连频繁触发怎么办这是最常见的问题。设备明明在线但日志里全是重连记录。排查思路如下先看心跳超时设置是否合理。如果心跳间隔10秒、超时3次那30秒没收到响应就判定断线。但有些网络环境下服务器响应本来就慢30秒可能不够。可以适当放宽到60秒。再看发送失败计数的阈值。如果阈值设成1那任何一次发送失败都会触发重连。网络抖动导致的偶发失败很常见阈值建议设成3到5。还要检查是不是有多个任务在同时操作网络。比如一个任务在发心跳另一个任务在发数据两个任务同时调用socket发送可能导致状态混乱。解决办法是加互斥锁或者把网络操作集中到一个任务里串行处理。5.2 断点续传数据重复或丢失数据重复通常是服务器端去重没做好。检查服务器是否根据设备ID加时间戳做了唯一性约束。如果时间戳精度不够比如只精确到秒同一秒内有多条数据就会冲突。解决办法是时间戳精确到毫秒或者加上序号作为辅助去重依据。数据丢失通常是缓存被覆盖了。检查缓存容量是否足够淘汰策略是否合理。如果缓存满了覆盖了还没上传的数据那就丢了。解决办法是增大缓存或者在缓存快满的时候触发告警提醒运维人员处理。还有一种情况是续传过程中再次断线续传位置没有正确保存。检查代码里是否在每次成功发送后更新了续传位置指针并且这个指针是否持久化存储了。如果只存在RAM里断电就丢了。5.3 多协议切换时的状态混乱如果设备支持多种协议比如优先走MQTTMQTT连不上再走HTTP那切换的时候容易出现状态混乱。常见的问题是MQTT的连接还没完全关闭就去初始化HTTP导致资源冲突。解决办法是协议切换时先彻底关闭当前协议释放所有资源再初始化新协议。关闭操作要确保socket关闭、缓冲区释放、状态复位。可以加一个延时确保底层资源完全释放后再进行下一步。另外不同协议的重连策略要独立配置。MQTT的重连参数和HTTP的重连参数可能不一样不要用同一套配置。5.4 常见问题速查表问题现象可能原因排查方法解决方案频繁重连心跳超时太短查看重连日志时间间隔放宽心跳超时到60秒频繁重连发送失败阈值太低统计发送失败次数阈值调整为3-5次数据重复服务器去重失效检查数据库唯一约束设备ID时间戳序号去重数据丢失缓存被覆盖检查缓存使用率增大缓存或加告警续传中断续传位置未持久化检查断电后位置指针位置指针存入Flash协议切换失败资源未释放检查socket关闭日志切换前彻底关闭旧协议时间戳混乱未对时或定时器溢出检查时间戳递增定期NTP对时缓存写入失败Flash擦写次数超限检查Flash坏块磨损均衡或换存储介质5.5 几个实用的避坑技巧第一个技巧在缓存里加一个已上传标志位。每次成功上传后把标志位置1续传时只扫描标志位为0的数据。这样比维护一个位置指针更可靠因为标志位是跟着数据走的不会因为指针丢失而混乱。第二个技巧心跳包和数据包分开处理。心跳包走独立的轻量级通道数据包走正常通道。这样即使数据通道拥堵心跳还能正常维持不会误判断线。第三个技巧在Flash里预留一个区域存配置参数包括服务器地址、心跳间隔、重连参数等。这样现场调试的时候不用重新烧录固件通过串口或者网页就能改配置。第四个技巧加一个看门狗任务监控网络状态。如果网络任务卡死超过一定时间主动复位网络模块或者重启设备。这个在无人值守的现场特别有用。6. 实际部署中的参数调优经验参数调优这块我结合几个实际项目说一下。冷链仓储项目网络环境相对稳定用的是有线以太网心跳间隔设的10秒超时3次重连退避最大60秒缓存用SPI Flash 2MB采样周期30秒。运行半年断线次数屈指可数每次断线都能在1分钟内恢复数据零丢失。车间环境监测项目电磁干扰强网络抖动频繁。心跳间隔放宽到30秒超时5次重连退避最大120秒缓存用SD卡 8GB采样周期10秒。这个项目断线比较频繁平均每天断两三次但因为有断点续传数据完整性一直保持在99.9%以上。还有一个是临时部署的仓库项目用的是ESP32加LAN8720网络通过无线网桥回传。这个环境最不稳定网桥偶尔会重启。我把心跳间隔设到60秒超时5次重连退避最大300秒缓存用外部Flash 4MB采样周期60秒。虽然断线发现得慢但数据一条没丢因为缓存足够大能存好几个月的量。从这几个项目总结下来参数没有万能值核心原则是网络越不稳定心跳间隔越长、超时次数越多、缓存越大、重连退避上限越高。反过来网络越稳定参数可以越激进响应越快。另外提醒一点缓存容量和采样周期要匹配。采样周期越短单位时间产生的数据越多缓存消耗越快。如果采样周期是10秒缓存只能存一天的数据那断线超过一天就会丢数据。所以要么增大缓存要么在缓存快满时主动告警。我个人在实际操作中的体会是断线重连和断点续传这套机制代码量不大但细节特别多。每一个参数、每一个状态转换、每一个边界条件都可能成为现场故障的根源。最好的办法是在实验室里模拟各种异常场景比如拔网线、重启服务器、模拟网络延迟把各种情况都跑一遍把问题暴露在部署之前。现场调试的成本远高于实验室测试的成本这个账一定要算清楚。