# 串口堵塞解决方案(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` 之前,插入以下代码: ```javascript // ========== 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为例):** ```javascript 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); }, ``` **改为:** ```javascript 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行 ```javascript // 原: maxReconnect: 3, // 改: maxReconnect: 10, ``` #### 修改点2: startRS485() 回调加 try-catch 异常隔离 **位置:** 第359-364行 **原代码:** ```javascript this.RS485.startAutoReadData((res) =>{ // console.log('485 data', res) // 转换成十六进制字符串 let hex = this.RS485.byte2HexString(res) this.handle485HexData(hex) }) ``` **改为:** ```javascript 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行 **原代码:** ```javascript this.RS232.startAutoReadData((res) =>{ // 转换成十六进制字符串 let hex = this.RS232.byte2HexString(res) this.handle232HexData(hex) }) ``` **改为:** ```javascript 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行 **原代码:** ```javascript stopRS232() { this.RS232.stopReadPortData() this.RS232.close() this.RS232 = undefined // ← 删除这行!保留对象引用以便重连 }, ``` **改为:** ```javascript stopRS232() { if (this.RS232) { this.RS232.stopReadPortData() this.RS232.close() } }, ``` > **原因**: 原代码将 `this.RS232` 设为 `undefined` 后,后续 `startRS232()` 无法通过该对象重新打开串口。 #### 修改点5: 新增 resetSerialPorts() 替代原 resetRS485() **完整新方法**(约60行),替代第493-550行的 `resetRS485()`: ```javascript /** * 统一双串口重连入口 * 执行顺序: 停定时器 → 停双串口 → 清空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行 **原代码:** ```javascript // 重置监听 this.resetRS485() ``` **改为:** ```javascript // 统一重置双串口 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数据丢失,但设备控制态由硬件保持 |