modbus 512 断线重连 db browser for sqlite










断线重连

csharp 复制代码
private async Task HeartbeatLoopAsync(CancellationToken token)
{
    // 监工一直循环干活,直到工长喊停工(token.IsCancellationRequested)
    while (!token.IsCancellationRequested)
    {
        try
        {
            // 每隔一段时间检查一次(最少200ms,默认你设置的心跳间隔)
            await Task.Delay(Math.Max(200, HeartbeatIntervalMs), token).ConfigureAwait(false);
        }
        catch (OperationCanceledException) 
        { 
            break; // 工长喊停工,监工立刻下班
        }

        // 拿一下:工人最后一次成功读到数据的时间
        var last = _lastSuccessfulReadUtc;
        if (last == DateTime.MinValue) continue; // 还没读过,跳过

        // 算一下:现在距离最后一次成功读,过了多久?
        var age = DateTime.UtcNow - last;

        // 如果:没过超时时间 → 不管
        // 如果:过了超时时间,并且之前没报错过 → 判定【掉线了】
        if (!_heartbeatLost && age.TotalMilliseconds > HeartbeatTimeoutMs)
        {
            _heartbeatLost = true; // 标记:掉线了
            try { OnHeartbeatLost?.Invoke(); } catch { } // 通知外面:掉线了

            // 如果开了【自动重连】→ 开始自救
            if (AutoReconnect)
            {
                try { Disconnect(); } catch { } // 先彻底断开(清工人、清串口)

                // 开始重试:最多试 ReconnectMaxAttempts 次
                for (int i = 0; i < ReconnectMaxAttempts && !token.IsCancellationRequested; i++)
                {
                    try
                    {
                        // 重试间隔:指数退避(100ms → 200ms → 400ms → 800ms...)
                        Thread.Sleep(ReconnectBaseDelayMs * (int)Math.Pow(2, i));

                        // 重新打开串口
                        if (OpenSerial(_lastPortName, _lastBaudRate))
                        {
                            // 重建Modbus
                            _modbusMaster = ModbusSerialMaster.CreateRtu(_serialPort);
                            _modbusMaster.Transport.Retries = 1;
                            _modbusMaster.Transport.WaitToRetryMilliseconds = 50;
                            break; // 重连成功,跳出循环
                        }
                    }
                    catch
                    {
                        // 失败了不管,继续重试
                    }
                }
            }
        }
    }
}
csharp 复制代码
 await Task.Delay(Math.Max(200, HeartbeatIntervalMs), token).ConfigureAwait(false);

我用最通俗、最扎心 的大白话告诉你:
监工必须休息!不休息,你的电脑CPU会直接炸掉,软件卡死动不了!

结合你这行代码,逐点给你讲死👇

csharp 复制代码
await Task.Delay(Math.Max(200, HeartbeatIntervalMs), token).ConfigureAwait(false);

1. 监工不休息会发生什么?(恐怖后果)

监工的工作是循环检查设备有没有掉线

csharp 复制代码
while (!token.IsCancellationRequested)
{
    // 检查掉线...
}

如果没有这行 Delay 休息

  • 监工会一秒钟循环几万次,疯狂检查
  • 电脑CPU直接占满(100%)
  • 软件界面卡死、点不动
  • 设备发热、卡顿,工业现场直接报废

这就是死循环!绝对不能允许!


2. 这行代码的真正作用:让监工定时巡逻,而不是疯跑

  • Task.Delay(时间) = 监工歇一会儿,再去检查
  • 不是偷懒,是工业软件的保命写法
  • 比如你设置心跳间隔 1000ms → 监工歇1秒,检查一次
  • 既不浪费CPU,又能实时监控设备状态

3. 为什么要加 Math.Max(200, ...)?(双重保险)

  • 防止你把心跳间隔设太小(比如10ms)
  • 强制最少休息200毫秒
  • 彻底杜绝监工疯跑、CPU爆满

4. 为什么要带 token

  • 监工休息的时候,也能听见工会代表的停工命令
  • 一喊停,立刻停止休息,直接下班
  • 不耽误线程退出

终极大白话总结

监工休息 = 为了不浪费电脑资源,不让软件卡死!
定时歇一会儿,再去巡逻检查,这是工业监控软件的标准规矩!

这行代码不是多余的,是保命的

这里的 token 是干嘛的?(停工保命用)

大白话翻译:

监工休息的时候,手里必须攥着「停工令牌」!

核心作用:

监工正在休息(Delay)

如果你关闭窗口、调用 Disconnect() → 工会代表(_cts)喊 停工!

这个 token 会立刻打断休息,让监工马上停止循环、下班走人

如果没有 token:监工休息到一半,你喊停工他听不见,会一直赖在后台,变成僵尸线程,导致串口占用、软件卡死

极简记忆:

token = 监工休息时的「紧急停工按钮」

二、.ConfigureAwait(false) 是干嘛的?(界面流畅用)

大白话翻译:

监工休息完,就在后台自己干活,别来打扰界面(窗口)!

核心作用(工业软件必加):

你的软件有窗口界面(主线程),还有后台监工 / 工人(子线程)

默认情况下,后台任务休息完会跑回界面线程,抢资源

加了 .ConfigureAwait(false):

监工全程在后台后台干活

不抢界面资源

窗口绝不卡顿、绝不假死

极简记忆:

ConfigureAwait (false) = 后台任务不打扰界面,保证软件流畅不卡

catch (OperationCanceledException) { break; }

核心大白话翻译

监工收到「强制停工通知」了!立刻结束休息、退出循环、下班走人!

完整场景(结合上面的 Delay 代码)

监工正在休息 await Task.Delay(...)

你关闭窗口 / 调用 Disconnect() → 工会代表喊 Cancel() 停工

手里的 token 立刻生效,强行打断休息

系统会抛出 OperationCanceledException(取消操作异常)

catch 抓住这个异常 → 执行 break

直接跳出监工的死循环,监工彻底下班

它是干嘛用的?(必须要写)

不写这行:监工会因为被打断休息而报错崩溃

写了这行:优雅停工,不报错、不残留、不占用串口资源

工业软件必须这么写,保证线程安全退出

var age = DateTime.UtcNow - last;

计算:从「工人最后一次成功读到数据」到「现在」,一共过去了多长时间!

计算:从「工人最后一次成功读到数据」到「现在」,一共过去了多长时间!

_heartbeatLost

作用:标记设备是否已经掉线、是否已经触发过报警和重连

age.TotalMilliseconds

刚才算出来的 → 工人多久没读到数据了(总毫秒数)

HeartbeatTimeoutMs

你设定的 心跳超时时间(比如 3000 毫秒 = 3 秒)

→ 这是底线:超过这个时间没数据,就算掉线!

工人摸鱼 / 断连的时间,超过了我设定的最大容忍时间 → 判定设备掉线!

AutoReconnect

csharp 复制代码
if (AutoReconnect)
{
    try { Disconnect(); } catch { }
    for (int i = 0; i < ReconnectMaxAttempts && !token.IsCancellationRequested; i++)
    {
        try
        {
            Thread.Sleep(ReconnectBaseDelayMs * (int)Math.Pow(2, i));
            if (OpenSerial(_lastPortName, _lastBaudRate))
            {
                _modbusMaster = ModbusSerialMaster.CreateRtu(_serialPort);
                _modbusMaster.Transport.Retries = 1;
                _modbusMaster.Transport.WaitToRetryMilliseconds = 50;
                break;
            }
        }
        catch
        {
        }
    }

1. if (AutoReconnect)

→ 你允许监工自动救场吗?

允许 = 继续执行重连

不允许 = 掉线就不管了

2. try { Disconnect(); } catch { }

→ 第一步:彻底清场!

调用你写的满分 Disconnect():

工会代表喊停工 → 工人 / 监工全部安全下班 → 关闭串口 → 销毁所有资源

必须先清场,才能重连,防止线程打架、端口占用!

3. for (int i = 0; i < 最大重试次数 && !token.IsCancellationRequested; i++)

→ 循环重试,守两个规矩:

最多试 ReconnectMaxAttempts 次(不无限死磕)

工会代表喊停工,立刻停止重试(不顽固)

4. Thread.Sleep(基础延迟 * 2^i)

→ 指数退避重试(工业核心!)

第一次等 100ms

第二次等 200ms

第三次等 400ms

第四次等 800ms

...

越重试等越久,不疯狂占用 CPU,不刷屏攻击串口

5. OpenSerial(串口名, 波特率)

→ 尝试重新打开串口工位

打开成功 = 硬件恢复连接

失败 = 继续下一次重试

6. 重建 _modbusMaster

→ 串口打开成功 → 拿新的 Modbus 通信工具

配置好参数,准备让新工人上岗读数据

7. break

→ 重连成功!结束重试循环!

终极总结(这套逻辑为什么稳?)

先清场,再重连:绝对不线程冲突

有限次数重试:不卡死软件

指数退避:不浪费 CPU 资源

随时响应停工命令:安全退出

全程 try-catch:绝不崩溃

相关推荐
LJianK13 小时前
乐观锁算线程同步吗?
java·开发语言·jvm
何故染尘優4 小时前
面试八股文-01
java·jvm·面试
青柠代码录4 小时前
【JVM】面试题-Java中有哪些引用类型
java·jvm
cms小程序插件【官方】5 小时前
pbootcms版AI自动发文插件升级到3.0版本,支持多组关键词
jvm·人工智能
AI人工智能+电脑小能手5 小时前
【大白话说Java面试题 第47题】【JVM篇】第7题:Young GC 和 Full GC 分别采用什么算法?
java·jvm·后端·算法·面试
青柠代码录5 小时前
【JVM】面试题-Parallel 回收器
jvm
p***76985 小时前
docker compose安装mindoc 后添加https访问反向代理配置教程
jvm·docker·https
techdashen5 小时前
Agent 的第三次浪潮:Cloudflare Project Think 是什么,要解决什么问题
jvm·数据库·oracle
woxihuan12345614 小时前
SQL删除数据时存在依赖关系_设置外键级联删除ON DELETE
jvm·数据库·python