This commit is contained in:
2026-06-02 15:18:26 +08:00
parent 05a72ac7dd
commit 67d64cc02d
4 changed files with 494 additions and 65 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"appid" : "__UNI__5A0A7D6",
"description" : "",
"versionName" : "1.2.5",
"versionCode" : 116,
"versionCode" : 117,
"transformPx" : false,
/* 5+App */
"app-plus" : {
+73 -47
View File
@@ -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) =>{
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) =>{
try {
// 转换成十六进制字符串
let hex = this.RS232.byte2HexString(res)
this.handle232HexData(hex)
} catch (e) {
// console.error('[RS232回调异常]', e)
this.addLog('232回调异常', e.message || e)
}
})
this.startAutoTask()
}
@@ -422,7 +430,6 @@ export default {
stopRS232() {
this.RS232.stopReadPortData()
this.RS232.close()
this.RS232 = undefined
},
// 启动心跳检测定时器
@@ -446,7 +453,6 @@ export default {
// 心跳未运行
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,55 +489,81 @@ export default {
duration: 2000
})
// 重置监听
this.resetRS485()
// 统一重置双串口
this.resetSerialPorts()
}
},
// 重置RS485监听:停止旧连接 → 重启新连接 → 恢复心跳
async resetRS485() {
/**
* 统一双串口重连入口
* 执行顺序: 停定时器 → 停双串口 → 清空485队列 → 等待释放 → 重启485 → 延迟→ 重启232
* 失败处理: 指数退避重试 / 耗尽则 plus.runtime.restart()
*/
async resetSerialPorts() {
try {
this.reconnectCount++
// 1. 暂停心跳检测(避免重置过程中重复触发)
// ===== Step 1: 停止所有定时器(必须最先执行!)=====
// T1: 心跳检测定时器
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null
}
// 2. 停止并关闭当前RS485串口
if (this.RS485) {
this.stopRS485()
// 等待一段时间确保串口完全关闭
await delay(1500)
}
// 3. 清除轮询定时器
// T2: 485轮询定时器
if (this.timer) {
clearInterval(this.timer)
clearInterval(this.timer);
this.timer = null
}
// T3: 业务任务定时器(关键!原resetRS485遗漏此项)
if (this.taskTimer) {
clearInterval(this.taskTimer);
this.taskTimer = null
}
// 4. 清空RS485队列中积压的失效指令
// ===== 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()
// 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秒后重启',
@@ -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() {
// 关闭真空泵和消毒,打开照明,打开门锁,打开风机
+375
View File
@@ -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数据丢失,但设备控制态由硬件保持 |
+32 -4
View File
@@ -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: () => {