尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

从零搭建灌装监控系统(八):断线检测与自动重连

发布时间:2026/9/29 9:57:41

资讯中心
01
ARTICLE

从零搭建灌装监控系统(八):断线检测与自动重连

从零搭建灌装监控系统(八):断线检测与自动重连
断线检测与自动重连这是「从零搭建灌装监控系统」系列第8篇。上一篇实现了 200ms 轮询循环但 PLC 断线后只是检测到断线不会自动恢复。这篇实现自动重连机制——3次超时阈值判定断线、串口和TCP分别重连策略、重连间隔递增、重连成功后恢复轮询。让系统在 PLC 重启或网络抖动后自动恢复不需要人工干预。半夜被叫起来重启软件我朋友的系统上线跑了两天第三天凌晨他接到电话——操作员说界面数据不刷新了。他远程连上去一看PLC 那边设备重启过凌晨维护重启完之后上位机没重连上。界面显示的还是断线前的旧数据状态栏 PLC 指示灯是红的。他只能远程重启软件。重启完好了但第二天凌晨 PLC 又维护又得重启。他说这不行我得睡觉。问题出在哪PlcService 断线后没有自动重连。轮询循环检测到断线只是标记IsConnected false但不会尝试重新连接。需要人工重启软件。工业现场 PLC 重启、网络抖动、线缆松动是常态。上位机必须能自动恢复——检测到断线后自动重连重连成功后继续轮询全程不需要人工干预。断线检测3次超时阈值不能一次超时就判定断线。工业现场偶尔有干扰导致一帧数据丢失下一帧就好了。一次超时就断线重连会导致频繁重连反而影响正常通信。用3次连续超时判定断线privatestaticint_consecutiveTimeouts0;privateconstintMaxTimeouts3;privatestaticasyncTaskPollDataLoop(CancellationTokentoken){while(!token.IsCancellationRequested){try{// 连接检查 if(!IsConnected||(_tcpClient!null!IsTcpReallyConnected())){if(IsConnected)ConnectionChanged?.Invoke(null,false);awaitTryReconnectAsync();awaitTask.Delay(3000,token);// 重连间隔 3 秒continue;}// 读数据带超时 varreadTaskReadAllRegistersAsync();vartimeoutTaskTask.Delay(500,token);if(awaitTask.WhenAny(readTask,timeoutTask)!readTask){_consecutiveTimeouts;LogService.Warn($读取超时 ({_consecutiveTimeouts}/{MaxTimeouts}));if(_consecutiveTimeoutsMaxTimeouts){LogService.Warn(连续超时达阈值判定断线);ConnectionChanged?.Invoke(null,false);// 强制断开下一轮循环会触发重连awaitForceDisconnectInternal();_consecutiveTimeouts0;}awaitTask.Delay(200,token);continue;}// 读取成功重置计数器_consecutiveTimeouts0;varstatesawaitreadTask;DataReceived?.Invoke(null,states);}catch(OperationCanceledException){break;}catch(Exceptionex){LogService.Error($轮询异常:{ex.Message});awaitTask.Delay(1000,token);}awaitTask.Delay(200,token);}}关键逻辑每次读超时_consecutiveTimeouts读成功_consecutiveTimeouts 0重置达到 3 次连续超时 → 判定断线 → 触发 ConnectionChanged(false) → 强制断开 → 下一轮循环进入重连为什么 3 次200ms 轮询3 次超时 600ms。600ms 内 PLC 没响应基本确定断了。再短太敏感再长恢复慢。重连策略串口和TCP分别处理串口和 TCP 的重连方式不同——串口是本地设备COM 口TCP 是网络连接IP:Port。privatestaticasyncTaskTryReconnectAsync(){try{LogService.Info($尝试重连... (模式:{_currentMode}));switch(_currentMode){caseConnectionMode.Serial:awaitTryReconnectSerialAsync();break;caseConnectionMode.ModbusTcp:awaitTryReconnectTcpAsync();break;caseConnectionMode.CustomTcp:// 自定义 TCP 由调用方管理break;}}catch(Exceptionex){LogService.Warn($重连失败:{ex.Message});}}串口重连privatestaticasyncTaskTryReconnectSerialAsync(){// 清理旧串口if(_serialPort?.IsOpentrue)_serialPort.Close();_serialPort?.Dispose();_serialPortnull;_modbusMasternull;// 检查串口是否存在USB 转串口可能被拔了varavailablePortsSerialPort.GetPortNames();if(!availablePorts.Contains(_lastPortName)){LogService.Warn($串口{_lastPortName}不存在可用:{string.Join(, ,availablePorts)});return;// 不重连等下一轮再试}// 重新打开串口_serialPortnewSerialPort(_lastPortName!,_lastBaudRate,_lastParity,_lastDataBits,_lastStopBits);_serialPort.Open();_modbusMasterModbusSerialMaster.CreateRtu(_serialPort);_modbusMaster.Transport.ReadTimeout1000;_modbusMaster.Transport.WriteTimeout1000;ConnectionChanged?.Invoke(null,true);LogService.Info($串口重连成功:{_lastPortName});}串口重连的特殊情况——USB 转串口可能被拔了。SerialPort.GetPortNames()列出可用串口如果配置的 COM 口不在列表里说明 USB 被拔了等下一轮再试用户可能插回来。TCP 重连privatestaticasyncTaskTryReconnectTcpAsync(){// 清理旧连接_ipMaster?.Dispose();_ipMasternull;_tcpClient?.Dispose();_tcpClientnull;// 重新连接varclientnewTcpClient();try{usingvarctsnewCancellationTokenSource(TimeSpan.FromSeconds(5));varconnectTaskclient.ConnectAsync(_lastTcpIp!,_lastTcpPort);if(awaitTask.WhenAny(connectTask,Task.Delay(Timeout.Infinite,cts.Token))!connectTask){client.Dispose();thrownewOperationCanceledException(TCP 重连超时);}awaitconnectTask;_ipMasterModbusIpMaster.CreateIp(client);_ipMaster.Transport.ReadTimeout1000;_ipMaster.Transport.WriteTimeout1000;client.Client.ReceiveTimeout1000;client.Client.SendTimeout1000;_tcpClientclient;ConnectionChanged?.Invoke(null,true);LogService.Info($TCP 重连成功:{_lastTcpIp}:{_lastTcpPort});}catch{client.Dispose();throw;}}TCP 重连和初始连接逻辑一样——5 秒超时成功后创建 Master。区别是重连用的是保存的_lastTcpIp/_lastTcpPort不是传入参数。重连间隔不要疯狂重连重连不能太频繁——PLC 还没启动完就重连肯定失败。失败后立即重试又失败……形成重连风暴日志刷屏CPU 飙高。用固定间隔 上限策略// 在 PollDataLoop 里if(!IsConnected||(_tcpClient!null!IsTcpReallyConnected())){if(IsConnected)ConnectionChanged?.Invoke(null,false);awaitTryReconnectAsync();awaitTask.Delay(3000,token);// 重连间隔 3 秒continue;}3 秒间隔够用——PLC 重启通常 10-30 秒3 秒一次重试最多 10 次就重连上。比 1 秒间隔少 2/3 的重连次数日志不刷屏。进阶指数退避如果想要更智能的重连间隔用指数退避——失败次数越多间隔越长privatestaticint_reconnectAttempts0;privateconstintMaxReconnectInterval30000;// 最大 30 秒// 在重连逻辑里_reconnectAttempts;varintervalMath.Min(3000*_reconnectAttempts,MaxReconnectInterval);awaitTask.Delay(interval,token);// 重连成功后重置_reconnectAttempts0;第 1 次失败等 3 秒第 2 次 6 秒第 3 次 9 秒……最多 30 秒。避免长时间断线时无意义的频繁重连。这个项目用的是固定 3 秒间隔简单够用。灌装线 PLC 重启很快10 秒内不需要指数退避。重连成功后恢复轮询重连成功后不需要特殊处理——TryReconnectAsync成功后IsConnected变 true下一轮PollDataLoop循环检测到已连接直接读数据。是否是否是否是否轮询循环已连接?读数据触发断线事件尝试重连重连成功?触发连接事件等3秒下一轮循环读成功?发布数据事件超时计数1达3次?判定断线整个流程是自包含的——PollDataLoop 一直在跑断线时自动重连重连后自动恢复不需要外部干预。ConnectionChanged 事件通知 UI断线和重连都通过ConnectionChanged事件通知 ViewModel// MainWindowViewModel 订阅publicMainWindowViewModel(IServiceProviderserviceProvider){_serviceProviderserviceProvider;PlcService.ConnectionChanged(s,connected){// 切回 UI 线程更新绑定属性Application.Current.Dispatcher.Invoke((){IsPlcConnectedconnected;});};}UI 上的状态指示灯绑定IsPlcConnected断线变红重连变绿。操作员一眼看到连接状态。重连时的 UI 反馈光变红不够最好告诉操作员正在重连。加个中间状态[ObservableProperty]privatestringconnectionStatus未连接;// 订阅事件PlcService.ConnectionChanged(s,connected){Application.Current.Dispatcher.Invoke((){IsPlcConnectedconnected;ConnectionStatusconnected?已连接:断线正在重连...;});};UI 上显示ConnectionStatus断线时显示断线正在重连…“重连成功显示已连接”。操作员知道系统在自动恢复不是死机了。踩坑记录坑现象原因解决重连风暴日志刷屏CPU 飙高重连失败立即重试无间隔重连间隔 3 秒或指数退避串口重连失败串口不存在异常USB 转串口被拔了GetPortNames 检查不存在就跳过重连后数据不对读到全 0 或全 FF旧 Master 没释放新 Master 用了旧连接重连前先 Dispose 旧 Master 和连接断线判定太敏感偶尔断线又恢复1次超时就判定断线3次连续超时才判定重连不退出程序关了还在重连重连逻辑没响应 CancellationToken重连里的 await 都传 token第三个坑很恶心。我朋友第一次写重连只 new 了新 SerialPort没 Dispose 旧的。结果旧串口还占着 COM 口新串口 Open 失败串口被占用。必须先 Close Dispose 旧的再创建新的。// ❌ 错误没清理旧连接_serialPortnewSerialPort(...);// 旧的还占着 COM 口_serialPort.Open();// 失败串口被占用// ✅ 正确先清理再创建if(_serialPort?.IsOpentrue)_serialPort.Close();_serialPort?.Dispose();_serialPortnull;_modbusMasternull;// 再创建新的_serialPortnewSerialPort(...);_serialPort.Open();本篇小结知识点关键代码3次超时阈值_consecutiveTimeouts 3判定断线串口重连CloseDispose 旧 → 检查端口存在 → new SerialPortTCP 重连Dispose 旧 → new TcpClient → 5秒超时连接重连间隔Task.Delay(3000, token)固定间隔指数退避Math.Min(3000 * attempts, 30000)串口不存在检测SerialPort.GetPortNames().Contains(port)TCP 假连接检测Socket.Poll(1, SelectMode.SelectRead)UI 状态反馈ConnectionChanged 事件 Dispatcher 切线程从断线就崩到 3 次超时判定 自动重连 UI 反馈——PLC 重启或网络抖动后系统自动恢复操作员看到正在重连知道系统在工作不需要人工重启软件。至此Modbus 通信部分05-08 篇全部完成。下一篇开始讲数据持久化——把读上来的生产数据存到 SQLite用 FreeSql ORM 自动建表。下期预告第9篇SQLite 与 FreeSql ORM我们将引入 SQLite 数据库 FreeSql ORM实现生产记录、报警记录、日志的持久化。涉及 DbProvider 封装、双检锁单例、AutoSyncStructure 自动建表、SQL 执行监控。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。