Files
endo_an_2/update2.md
T
2026-06-02 15:18:26 +08:00

12 KiB
Raw Blame History

串口堵塞解决方案(start.vue + cmd.js- 精简版

背景

  • RS485: 波特率 9600(慢),有队列+心跳+重连但只3次
  • RS232: 波特率 115200(快),完全裸奔无任何保护
  • 现象: 长时间运行后双串口都停止工作,无法打开端口

核心设计决策

  • RS232 不设独立心跳,依赖 485 心跳超时触发双串口统一重连
  • 485 和 232 共用同一套重连机制
  • RS232 不用队列,仅加超时封装防止 Promise 永久 pending(理由见下方分析)

为什么 RS232 不需要队列?

特征 RS485 RS232
波特率 9600 (慢) 115200 (快12倍)
通信方式 半双工总线,多设备争用 点对点,独占
是否有定时轮询 每秒2条(getTemp+getPressure) 无轮询
任务优先级冲突 开门 vs 轮询抢总线 不存在

RS232 实际调用场景 — 全部是事件驱动,无定时器持续产生任务:

cmd.Wind()      → 温湿度Watcher触发 + 流程控制
cmd.Light()     → 用户手动点击 / 自动流程
cmd.Vacuum()    → 自动流程(清洗)
cmd.Disinfect() → 用户手动 / 自动流程

关键结论:

  1. 发送极快 — 10字节 @115200 ≈ 0.87ms 完成
  2. 无并发源 — 没有定时器轮询产生并发任务
  3. 调用方已 await — 流程里都是串行执行
  4. 幂等操作 — 重复发送同指令多次结果一样

修改文件清单

文件1: utils/cmd.js — 新增 RS232 超时封装(约15行)

在 RS485 队列代码块结束后、export default 之前,插入以下代码:

// ========== RS232 超时保护 START ==========
// RS232波特率115200极快(~1ms/条),无需队列,仅需超时保护防止Promise永久pending

const RS232_SEND_TIMEOUT = 1000  // 发送超时1秒(对115200绰绰有余)

/**
 * 带超时的RS232发送封装
 * 防止底层串口驱动卡死导致Promise永久pending
 */
const sendRS232WithTimeout = (rs232Instance, cmd) => {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            console.warn(`[RS232] 发送超时(${RS232_SEND_TIMEOUT}ms), cmd: ${cmd}`)
            reject(new Error('RS232发送超时'))
        }, RS232_SEND_TIMEOUT)

        const result = rs232Instance.sendDataString(cmd)
        if (result && typeof result.then === 'function') {
            result.then(res => { clearTimeout(timer); resolve(res) })
                  .catch(err => { clearTimeout(timer); reject(err) })
        } else {
            clearTimeout(timer)
            resolve(result)
        }
    })
}
// ========== RS232 超时保护 END ==========

修改4个发送方法 — Wind/Light/Vacuum/Disinfect

将以下4个方法改为使用超时封装:

原代码 (以Wind为例):

Wind: async (status) => { 
    let op = status ? '01' : '00'
    let cmd = '5AA5EE02' + op
    store.state.relay.wind = status
    let RS232 = getApp().globalData.RS232
    await RS232.sendDataString(cmd);
},

改为:

Wind: async (status) => { 
    let op = status ? '01' : '00'
    let cmd = '5AA5EE02' + op
    store.state.relay.wind = status
    let RS232 = getApp().globalData.RS232
    await sendRS232WithTimeout(RS232, cmd)
},

同样修改 Light、Vacuum、Disinfect 三个方法。

注意: 无需额外导出任何内容。sendRS232WithTimeout 是模块内部私有函数,不暴露给外部。


文件2: pages/start.vue — 核心改造

修改点1: data() 中 maxReconnect 从3改为10

位置: 第61行

// 原:
maxReconnect: 3,
// 改:
maxReconnect: 10,

修改点2: startRS485() 回调加 try-catch 异常隔离

位置: 第359-364行 原代码:

this.RS485.startAutoReadData((res) =>{ 
    // console.log('485 data', res)
    // 转换成十六进制字符串
    let hex = this.RS485.byte2HexString(res)
    this.handle485HexData(hex)
}) 

改为:

this.RS485.startAutoReadData((res) =>{ 
    try {
        // console.log('485 data', res)
        // 转换成十六进制字符串
        let hex = this.RS485.byte2HexString(res)
        this.handle485HexData(hex)
    } catch (e) {
        console.error('[RS485回调异常]', e)
    }
}) 

修改点3: startRS232() 回调加 try-catch 异常隔离

位置: 第407-411行 原代码:

this.RS232.startAutoReadData((res) =>{ 
    // 转换成十六进制字符串
    let hex = this.RS232.byte2HexString(res)
    this.handle232HexData(hex)
})

改为:

this.RS232.startAutoReadData((res) =>{ 
    try {
        // 转换成十六进制字符串
        let hex = this.RS232.byte2HexString(res)
        this.handle232HexData(hex)
    } catch (e) {
        console.error('[RS232回调异常]', e)
    }
})

修改点4: stopRS232() 移除 = undefined

位置: 第422-426行 原代码:

stopRS232() { 
    this.RS232.stopReadPortData()
    this.RS232.close()
    this.RS232 = undefined   // ← 删除这行!保留对象引用以便重连
},

改为:

stopRS232() { 
    if (this.RS232) {
        this.RS232.stopReadPortData()
        this.RS232.close()
    }
},

原因: 原代码将 this.RS232 设为 undefined 后,后续 startRS232() 无法通过该对象重新打开串口。

修改点5: 新增 resetSerialPorts() 替代原 resetRS485()

完整新方法(约60行),替代第493-550行的 resetRS485():

/**
 * 统一双串口重连入口
 * 执行顺序: 停定时器 → 停双串口 → 清空485队列 → 等待释放 → 重启485 → 延迟→ 重启232
 * 失败处理: 指数退避重试 / 耗尽则 plus.runtime.restart()
 */
async resetSerialPorts() {
    try {
        this.reconnectCount++

        // ===== Step 1: 停止所有定时器(必须最先执行!)=====
        // T1: 心跳检测定时器
        if (this.heartbeatTimer) { 
            clearInterval(this.heartbeatTimer); 
            this.heartbeatTimer = null 
        }
        // T2: 485轮询定时器
        if (this.timer) { 
            clearInterval(this.timer); 
            this.timer = null 
        }
        // T3: 业务任务定时器(关键!原resetRS485遗漏此项)
        if (this.taskTimer) { 
            clearInterval(this.taskTimer); 
            this.taskTimer = null 
        }

        // ===== Step 2: 停止两个串口 =====
        if (this.RS485) { 
            this.RS485.stopReadPortData(); 
            this.RS485.close() 
        }
        if (this.RS232) { 
            this.RS232.stopReadPortData(); 
            this.RS232.close() 
        }

        // ===== Step 3: 清空RS485消息队列(RS232无队列,无需清理)=====
        cmd.clearRS485Queue()

        // ===== Step 4: 等待端口释放(指数退避,最少2500ms)=====
        const backoff = Math.min(1000 * Math.pow(2, this.reconnectCount - 1), 30000)
        const waitTime = Math.max(backoff, 2500)
        console.log(`[重连] 第${this.reconnectCount}次, 等待${waitTime}ms...`)
        await delay(waitTime)

        // ===== Step 5: 重置时间戳 =====
        this.last485DataTime = null

        // ===== Step 6: 依次重启两个串口 =====
        // 先启动RS485(会自动重建T1心跳 + T2轮询)
        this.startRS485()

        // 错开启动时间,避免同时打开端口竞争
        await delay(500)

        // 再启动RS232(会自动重建T3 taskTimer
        this.startRS232()

        this.addLog('串口通信', `双串口重连完成(第${this.reconnectCount}次)`)

    } catch (error) {
        this.addLog('串口通信错误', error.message || '重置失败')

        // 失败后延迟重试
        await delay(5000)
        if (this.reconnectCount < this.maxReconnect) {
            this.resetSerialPorts()
        } else {
            // 重连次数耗尽,自动重启应用
            this.addLog('串口通信', `重连${this.maxReconnect}次均失败,即将自动重启应用`)
            
            uni.showToast({
                title: '通信异常,3秒后重启',
                icon: 'none',
                duration: 3000
            })

            setTimeout(() => {
                // #ifdef APP-PLUS
                plus.runtime.restart()
                // #endif
            }, 3000)
        }
    }
},

注意: 写入此方法后,删除原 resetRS485() 方法(第493-550行)。

修改点6: check485Heartbeat() 调用 resetSerialPorts()

位置: 第488行 原代码:

// 重置监听
this.resetRS485()

改为:

// 统一重置双串口
this.resetSerialPorts()

重连流程图

485心跳检测(每10秒)
    │
    ▼
距上次数据 > 30秒?
    │
    ├── 否 → 正常运行
    │
    └── 是 → reconnectCount >= maxReconnect(10)?
                │
                ├── 是 → 提示检查硬件 → 结束
                │
                └── 否 → resetSerialPorts()
                            │
                            ├─ Step1: 停 T1(heartbeatTimer) + T2(timer) + T3(taskTimer)
                            ├─ Step2: 停 RS485 + 停 RS232
                            ├─ Step3: clearRS485Queue() RS232无队列跳过)
                            ├─ Step4: 指数退避等待 (min 2500ms, max 30000ms)
                            ├─ Step5: last485DataTime = null
                            └─ Step6: startRS485() → delay(500ms) → startRS232()
                                        │
                                        ├── 成功 → 正常运行(各方法内部自动重建定时器/监听)
                                        │
                                        └── 失败 → retry < 10 ?
                                                    │
                                                    ├── 是 → delay(5s) → resetSerialPorts()
                                                    │
                                                    └── 否 → plus.runtime.restart() 重启应用

定时器清单

编号 变量名 间隔 用途 所在方法
T1 heartbeatTimer 10秒 RS485心跳检测 startHeartbeat() / check485Heartbeat()
T2 timer 5秒 RS485轮询(getTemp+getPressure) readData()
T3 taskTimer 2秒 业务任务(vacuum/disinfect/windClose) startAutoTask()

关键修复: 原始 resetRS485() 只清理 T1 和 T2漏掉 T3。在等待1.5秒期间 T3 继续向正在关闭的 RS232 发送指令,导致端口状态异常。


修改量汇总

# 文件 修改内容 新增行数
1 utils/cmd.js 新增sendRS232WithTimeout超时封装(替代原55行列队) ~15行
2 utils/cmd.js 修改Wind/Light/Vacuum/Disinfect 4个方法改用超时封装 改4处
3 pages/start.vue maxReconnect: 3→10 1行
4 pages/start.vue startRS485回调加try-catch ~6行
5 pages/start.vue startRS232回调加try-catch ~6行
6 pages/start.vue stopRS232移除=undefined -1行
7 pages/start.vue 新增resetSerialPorts()替换resetRS485() ~60行(净增)
8 pages/start.vue check485Heartbeat调用改resetSerialPorts 1行

总计: 约 91行 新增/修改(比原队列方案少~40行),跨 2个文件

精简项对比原方案:

  • clearRS232Queue 导出 → 不再需要
  • onUnload 清理 RS232 队列 → 不再需要
  • resetSerialPorts 中 clearRS232Queue() → 不再需要

风险评估

风险项 等级 说明 缓解措施
RS232无队列并发 极低 115200极快+事件驱动+await串行,实际不会并发 如遇问题可随时加回队列
重连期间功能暂停 重连需2.5~30s,此期间无数据上报 业务容忍;超30s自动重启
应用重启丢状态 plus.runtime.restart() 会恢复页面栈 Vuex/store数据丢失,但设备控制态由硬件保持