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

376 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 串口堵塞解决方案(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数据丢失,但设备控制态由硬件保持 |