diff --git a/manifest.json b/manifest.json index 92f88a2..560dd3c 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "appid" : "__UNI__5A0A7D6", "description" : "", "versionName" : "1.2.5", - "versionCode" : 116, + "versionCode" : 117, "transformPx" : false, /* 5+App特有相关 */ "app-plus" : { diff --git a/pages/start.vue b/pages/start.vue index a602d11..e1e0f95 100644 --- a/pages/start.vue +++ b/pages/start.vue @@ -58,7 +58,7 @@ export default { last485DataTime: null, // 上次收到485数据的时间戳 heartbeatTimer: null, // 心跳检测定时器 reconnectCount: 0, // 重连次数计数器 - maxReconnect: 3, // 最大重连次数 + maxReconnect: 5, // 最大重连次数 } }, computed: { @@ -180,9 +180,7 @@ export default { try { this.startRS485() this.startRS232() - } catch (e) { - uni.showToast({ title: '初始化串口失败', icon: 'error' @@ -357,10 +355,15 @@ export default { icon: 'none' }) this.RS485.startAutoReadData((res) =>{ - // console.log('485 data', res) - // 转换成十六进制字符串 - let hex = this.RS485.byte2HexString(res) - this.handle485HexData(hex) + try { + // console.log('485 data', res) + // 转换成十六进制字符串 + let hex = this.RS485.byte2HexString(res) + this.handle485HexData(hex) + } catch (e) { + // console.error('[RS485回调异常]', e) + this.addLog('485回调异常', e.message || e) + } }) // 读取数据 clearInterval(this.timer) @@ -405,9 +408,14 @@ export default { icon: 'none' }) this.RS232.startAutoReadData((res) =>{ - // 转换成十六进制字符串 - let hex = this.RS232.byte2HexString(res) - this.handle232HexData(hex) + try { + // 转换成十六进制字符串 + let hex = this.RS232.byte2HexString(res) + this.handle232HexData(hex) + } catch (e) { + // console.error('[RS232回调异常]', e) + this.addLog('232回调异常', e.message || e) + } }) this.startAutoTask() } @@ -421,8 +429,7 @@ export default { }, stopRS232() { this.RS232.stopReadPortData() - this.RS232.close() - this.RS232 = undefined + this.RS232.close() }, // 启动心跳检测定时器 @@ -445,8 +452,7 @@ export default { if (!this.heartbeatTimer) { // 心跳未运行 return - } - + } clearInterval(this.heartbeatTimer) this.heartbeatTimer = null this.addLog('485通信', '心跳检测已停止') @@ -458,9 +464,8 @@ export default { if (!this.last485DataTime) { return } - const now = Date.now() - const timeout = 30 * 1000 // 30秒超时阈值 + const timeout = 10000 // 10秒超时阈值 // 判断距离上次数据是否超过30秒 if (now - this.last485DataTime > timeout) { @@ -484,56 +489,82 @@ export default { duration: 2000 }) - // 重置监听 - this.resetRS485() + // 统一重置双串口 + this.resetSerialPorts() } }, - // 重置RS485监听:停止旧连接 → 重启新连接 → 恢复心跳 - async resetRS485() { + /** + * 统一双串口重连入口 + * 执行顺序: 停定时器 → 停双串口 → 清空485队列 → 等待释放 → 重启485 → 延迟→ 重启232 + * 失败处理: 指数退避重试 / 耗尽则 plus.runtime.restart() + */ + async resetSerialPorts() { try { this.reconnectCount++ - // 1. 暂停心跳检测(避免重置过程中重复触发) - if (this.heartbeatTimer) { - clearInterval(this.heartbeatTimer) - this.heartbeatTimer = null + // ===== 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 } - // 2. 停止并关闭当前RS485串口 - if (this.RS485) { - this.stopRS485() - // 等待一段时间确保串口完全关闭 - await delay(1500) + // ===== Step 2: 停止两个串口 ===== + if (this.RS485) { + this.RS485.stopReadPortData(); + this.RS485.close() + } + if (this.RS232) { + this.RS232.stopReadPortData(); + this.RS232.close() } - // 3. 清除轮询定时器 - if (this.timer) { - clearInterval(this.timer) - this.timer = null - } - - // 4. 清空RS485队列中积压的失效指令 + // ===== Step 3: 清空RS485消息队列(RS232无队列,无需清理)===== cmd.clearRS485Queue() - // 5. 重置时间戳 + // ===== 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 - // 6. 重新启动RS485通信 + // ===== Step 6: 依次重启两个串口 ===== + // 先启动RS485(会自动重建T1心跳 + T2轮询) this.startRS485() + // 错开启动时间,避免同时打开端口竞争 + await delay(500) + + // 再启动RS232(会自动重建T3 taskTimer) + this.startRS232() + + this.addLog('串口通信', `双串口重连完成(第${this.reconnectCount}次)`) + } catch (error) { - this.addLog('485通信错误', error.message || '重置失败') + this.addLog('串口通信错误', error.message || '重置失败') // 失败后延迟重试 await delay(5000) if (this.reconnectCount < this.maxReconnect) { - // 还有重连机会,继续重连 - this.resetRS485() + this.resetSerialPorts() } else { - // 重连次数耗尽,自动重启应用(彻底恢复native层状态) - this.addLog('485通信', `重连${this.maxReconnect}次均失败,即将自动重启...`) - + // 重连次数耗尽,自动重启应用 + this.addLog('串口通信', `重连${this.maxReconnect}次均失败,即将自动重启应用`) + uni.showToast({ title: '通信异常,3秒后重启', icon: 'none', @@ -556,7 +587,7 @@ export default { await cmd.getPressure() }, 5000) }, - async handle485HexData(hex) { + handle485HexData(hex) { // 更新最后收到数据的时间戳 this.last485DataTime = Date.now() @@ -582,8 +613,6 @@ export default { // 左门触点触发(关门) // 调用cmd.LeftDoor(false)来更新门状态 cmd.LeftDoor(false) - // 触发关门事件 - // this.closeDoorEvent() uni.showToast({ title: '左门已关闭', icon: 'none' @@ -594,8 +623,6 @@ export default { // 右门触点触发(关门) // 调用cmd.RightDoor(false)来更新门状态 cmd.RightDoor(false) - // 触发关门事件 - // this.closeDoorEvent() uni.showToast({ title: '右门已关闭', icon: 'none' @@ -604,11 +631,10 @@ export default { } if (data.action == 'door' && data.status == 'closed') { try { - await cmd.LeftDoor(false) - await delay(200) - await cmd.RightDoor(false) - // 触发关门事件 - await this.closeDoorEvent() + cmd.LeftDoor(false) + cmd.RightDoor(false) + // 触发关门事件,不需要重复触发,监听器触发 + // this.closeDoorEvent() uni.showToast({ title: '两门已关闭', icon: 'none' @@ -764,12 +790,12 @@ export default { closePop() { this.$refs.popup.close() }, - send485Data(cmd) { - this.RS485.sendDataString(cmd); - }, - async send232Data(cmd) { - await this.RS232.sendDataString(cmd); - }, + // send485Data(cmd) { + // this.RS485.sendDataString(cmd); + // }, + // async send232Data(cmd) { + // await this.RS232.sendDataString(cmd); + // }, // 开门事件 async openDoorEvent() { // 关闭真空泵和消毒,打开照明,打开门锁,打开风机 diff --git a/update2.md b/update2.md new file mode 100644 index 0000000..568395e --- /dev/null +++ b/update2.md @@ -0,0 +1,375 @@ +# 串口堵塞解决方案(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数据丢失,但设备控制态由硬件保持 | diff --git a/utils/cmd.js b/utils/cmd.js index b96ab2e..d9cfab0 100644 --- a/utils/cmd.js +++ b/utils/cmd.js @@ -172,6 +172,34 @@ const getRS485QueueSize = () => { } // ========== RS485 优先级消息队列 END ========== +// ========== 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 ========== + export default { // 开左门指令 (通过RS485站号03) - 高优先级 // 中盛4路数字IO模块,功能码06(写单个保持寄存器) @@ -211,7 +239,7 @@ export default { let cmd = '5AA5EE02' + op store.state.relay.wind = status let RS232 = getApp().globalData.RS232 - await RS232.sendDataString(cmd); + await sendRS232WithTimeout(RS232, cmd) }, // 开灯指令 03 Light: async (status) => { @@ -219,7 +247,7 @@ export default { let cmd = '5AA5EE03' + op store.state.relay.light = status let RS232 = getApp().globalData.RS232 - await RS232.sendDataString(cmd); + await sendRS232WithTimeout(RS232, cmd) }, // 开真空指令 04 Vacuum: async (status) => { @@ -227,7 +255,7 @@ export default { let cmd = '5AA5EE04' + op store.state.relay.vacuum = status let RS232 = getApp().globalData.RS232 - await RS232.sendDataString(cmd); + await sendRS232WithTimeout(RS232, cmd) }, // 开关消毒指令 05 Disinfect: async (status) => { @@ -235,7 +263,7 @@ export default { let cmd = '5AA5EE05' + op store.state.relay.disinfect = status let RS232 = getApp().globalData.RS232 - await RS232.sendDataString(cmd); + await sendRS232WithTimeout(RS232, cmd) }, // 获取温湿度(通过RS485站号02)- 低优先级 getTemp: () => {