Compare commits
4 Commits
7b911acf7a
...
67d64cc02d
| Author | SHA1 | Date | |
|---|---|---|---|
| 67d64cc02d | |||
| 05a72ac7dd | |||
| 92165c914a | |||
| 562881cc6c |
@@ -8,3 +8,6 @@ data/
|
||||
.vscode/launch.json
|
||||
unpackage/
|
||||
pnpm-lock.yaml
|
||||
.vscode/
|
||||
.hbuilderx/
|
||||
.codegraph/
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
"name" : "endoscope",
|
||||
"appid" : "__UNI__5A0A7D6",
|
||||
"description" : "",
|
||||
"versionName" : "1.2.4",
|
||||
"versionCode" : 113,
|
||||
"versionName" : "1.2.5",
|
||||
"versionCode" : 117,
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
|
||||
+6
-3
@@ -603,17 +603,19 @@ export default {
|
||||
<style scoped>
|
||||
.page{
|
||||
/* padding: 10px; */
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.content{
|
||||
flex: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
padding: 10px 20px;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.block {
|
||||
height: 120px;
|
||||
@@ -713,7 +715,8 @@ export default {
|
||||
.column{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -379,10 +379,10 @@ export default {
|
||||
}
|
||||
},
|
||||
temp() {
|
||||
this.RS485.sendDataString(cmd.getTemp())
|
||||
this.RS485.sendDataString('020300000002C438')
|
||||
},
|
||||
pressure() {
|
||||
this.RS485.sendDataString(cmd.getPressure())
|
||||
this.RS485.sendDataString('01030001000295CB')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+124
-8
@@ -16,8 +16,8 @@
|
||||
<th>工号</th>
|
||||
<th>卡号</th>
|
||||
<th>卡号2</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
<tbody class="ant-table-tbody">
|
||||
<tr v-for="(item, index) in list" :key="index">
|
||||
@@ -27,6 +27,12 @@
|
||||
<td>{{ item.number }}</td>
|
||||
<td>{{ item.ic }}</td>
|
||||
<td>{{ item.ic2 }}</td>
|
||||
<td>
|
||||
<view class="action-btns">
|
||||
<view class="ant-btn edit-btn" @click="edit(item)">编辑</view>
|
||||
<!-- <view class="ant-btn delete-btn" @click="remove(item)">删除</view> -->
|
||||
</view>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -102,7 +108,7 @@
|
||||
<uni-row>
|
||||
<uni-col>
|
||||
<view class="form-btn">
|
||||
<view class="ant-btn" @click="submit">保存</view>
|
||||
<view class="ant-btn" @click="submit">{{ isEdit ? '更新' : '保存' }}</view>
|
||||
<view class="ant-btn" style="margin-left: 20px;" @click="close">取消</view>
|
||||
</view>
|
||||
</uni-col>
|
||||
@@ -134,6 +140,8 @@ export default {
|
||||
],
|
||||
page: 1,
|
||||
totalPage: 0,
|
||||
isEdit: false,
|
||||
editId: null,
|
||||
form: {
|
||||
name: '',
|
||||
ic: '',
|
||||
@@ -149,6 +157,8 @@ export default {
|
||||
methods: {
|
||||
|
||||
add() {
|
||||
this.isEdit = false
|
||||
this.editId = null
|
||||
this.$refs.popup.open()
|
||||
this.form = {
|
||||
name: '',
|
||||
@@ -167,8 +177,53 @@ export default {
|
||||
},
|
||||
close() {
|
||||
this.$refs.popup.close()
|
||||
this.isEdit = false
|
||||
this.editId = null
|
||||
uni.$off('ic')
|
||||
},
|
||||
edit(item) {
|
||||
this.isEdit = true
|
||||
this.editId = item.id
|
||||
this.form = {
|
||||
name: item.name,
|
||||
ic: item.ic,
|
||||
ic2: item.ic2 || '',
|
||||
number: item.number || '',
|
||||
type: item.type || ''
|
||||
}
|
||||
this.$refs.popup.open()
|
||||
uni.$on('ic', (val) => {
|
||||
if (!this.form.ic) {
|
||||
this.form.ic = val
|
||||
} else if (!this.form.ic2) {
|
||||
this.form.ic2 = val
|
||||
}
|
||||
})
|
||||
},
|
||||
async remove(item) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定要删除 ${item.name} 吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await db.deleteInformationType('user', { id: item.id })
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'none'
|
||||
})
|
||||
this.getList()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async submit() {
|
||||
if (this.form.name == '' || this.form.ic == '') {
|
||||
return false
|
||||
@@ -179,17 +234,33 @@ export default {
|
||||
await db.addUserTable()
|
||||
}
|
||||
try {
|
||||
await db.addTabItem(tableName, { ...this.form })
|
||||
if (this.isEdit) {
|
||||
// 编辑模式
|
||||
await db.updateSQL(tableName, {
|
||||
name: this.form.name,
|
||||
ic: this.form.ic,
|
||||
ic2: this.form.ic2,
|
||||
number: this.form.number,
|
||||
type: this.form.type
|
||||
}, 'id', this.editId)
|
||||
uni.showToast({
|
||||
title: '更新成功',
|
||||
icon: 'none'
|
||||
})
|
||||
} else {
|
||||
// 新增模式
|
||||
await db.addTabItem(tableName, { ...this.form })
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
this.$refs.popup.close()
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'none'
|
||||
})
|
||||
this.getList()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
title: this.isEdit ? '更新失败' : '保存失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
@@ -295,4 +366,49 @@ export default {
|
||||
.form-btn{
|
||||
margin-top: 30px;
|
||||
}
|
||||
.action-btns{
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
.edit-btn{
|
||||
/* background: #1890ff;
|
||||
border-color: #1890ff; */
|
||||
width: 60px;
|
||||
height: 36px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.delete-btn{
|
||||
/* background: #ff4d4f;
|
||||
border-color: #ff4d4f; */
|
||||
width: 60px;
|
||||
height: 36px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.edit-btn:hover{
|
||||
/* background: #40a9ff;
|
||||
border-color: #40a9ff; */
|
||||
}
|
||||
.delete-btn:hover{
|
||||
/* background: #ff7875;
|
||||
border-color: #ff7875; */
|
||||
}
|
||||
|
||||
/* 表格内容自动换行 */
|
||||
.ant-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.ant-table th,
|
||||
.ant-table td {
|
||||
padding: 10px 8px;
|
||||
text-align: center;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 0;
|
||||
}
|
||||
</style>
|
||||
+135
-147
@@ -55,11 +55,10 @@ export default {
|
||||
RS232: undefined,
|
||||
timer: null, // 定时器读取485接口数据
|
||||
taskTimer: null,
|
||||
leftDoorCycleTimer: null, // 左门循环定时器
|
||||
last485DataTime: null, // 上次收到485数据的时间戳
|
||||
heartbeatTimer: null, // 心跳检测定时器
|
||||
reconnectCount: 0, // 重连次数计数器
|
||||
maxReconnect: 3, // 最大重连次数
|
||||
maxReconnect: 5, // 最大重连次数
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -119,9 +118,9 @@ export default {
|
||||
|
||||
uni.$on('closeDoor', async (data) => {
|
||||
try {
|
||||
cmd.LeftDoor(false)
|
||||
await delay(100)
|
||||
cmd.RightDoor(false)
|
||||
await cmd.LeftDoor(false)
|
||||
await delay(200)
|
||||
await cmd.RightDoor(false)
|
||||
this.addLog('门', '触摸屏关门')
|
||||
// 触发关门事件
|
||||
this.closeDoorEvent()
|
||||
@@ -129,9 +128,6 @@ export default {
|
||||
this.addLog('手动关门错误', error)
|
||||
}
|
||||
})
|
||||
|
||||
// 启动左门循环定时器
|
||||
// this.startLeftDoorCycle()
|
||||
|
||||
},
|
||||
onShow() {
|
||||
@@ -166,11 +162,6 @@ export default {
|
||||
clearInterval(this.taskTimer)
|
||||
this.taskTimer = null
|
||||
}
|
||||
// 清理左门循环定时器
|
||||
if (this.leftDoorCycleTimer) {
|
||||
clearInterval(this.leftDoorCycleTimer)
|
||||
this.leftDoorCycleTimer = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
@@ -189,10 +180,7 @@ export default {
|
||||
try {
|
||||
this.startRS485()
|
||||
this.startRS232()
|
||||
// 启动心跳检测
|
||||
this.startHeartbeat()
|
||||
} catch (e) {
|
||||
// console.log(e)
|
||||
uni.showToast({
|
||||
title: '初始化串口失败',
|
||||
icon: 'error'
|
||||
@@ -367,22 +355,37 @@ 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)
|
||||
this.readData()
|
||||
|
||||
// 启动心跳检测
|
||||
this.startHeartbeat()
|
||||
|
||||
// 串口打开成功后,重置重连计数器和时间戳
|
||||
this.reconnectCount = 0
|
||||
this.last485DataTime = Date.now()
|
||||
|
||||
// 记录通信恢复日志
|
||||
this.addLog('485通信', '通信已恢复(重连成功)')
|
||||
}
|
||||
// this.addLog('485通信', '通信已恢复(重连成功)')
|
||||
} else {
|
||||
// 确保心跳是关闭的
|
||||
this.stopHeartbeatIfNeeded()
|
||||
uni.showToast({
|
||||
title: '485打开失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
async startRS232() {
|
||||
// 获取本地串口存储端口
|
||||
@@ -405,22 +408,28 @@ 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()
|
||||
}
|
||||
},
|
||||
stopRS485() {
|
||||
// 停止心跳检测
|
||||
this.stopHeartbeatIfNeeded()
|
||||
this.addLog('485通信', '串口关闭')
|
||||
this.RS485.stopReadPortData()
|
||||
this.RS485.close()
|
||||
},
|
||||
stopRS232() {
|
||||
this.RS232.stopReadPortData()
|
||||
this.RS232.close()
|
||||
this.RS232 = undefined
|
||||
this.RS232.close()
|
||||
},
|
||||
|
||||
// 启动心跳检测定时器
|
||||
@@ -430,7 +439,6 @@ export default {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
this.addLog('485通信', '心跳检测已启动')
|
||||
|
||||
@@ -439,6 +447,16 @@ export default {
|
||||
this.check485Heartbeat()
|
||||
}, 10000)
|
||||
},
|
||||
// 停止心跳检测(安全方法,如果没运行也不会报错)
|
||||
stopHeartbeatIfNeeded() {
|
||||
if (!this.heartbeatTimer) {
|
||||
// 心跳未运行
|
||||
return
|
||||
}
|
||||
clearInterval(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
this.addLog('485通信', '心跳检测已停止')
|
||||
},
|
||||
|
||||
// 检测485数据心跳,如果超时则触发重连
|
||||
check485Heartbeat() {
|
||||
@@ -446,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) {
|
||||
@@ -472,66 +489,105 @@ 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
|
||||
}
|
||||
// ===== Step 3: 清空RS485消息队列(RS232无队列,无需清理)=====
|
||||
cmd.clearRS485Queue()
|
||||
|
||||
// 4. 重置时间戳
|
||||
// ===== 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
|
||||
|
||||
// 5. 重新启动RS485通信
|
||||
// ===== Step 6: 依次重启两个串口 =====
|
||||
// 先启动RS485(会自动重建T1心跳 + T2轮询)
|
||||
this.startRS485()
|
||||
|
||||
// 6. 延迟后重新启动心跳检测(等待RS485初始化完成)
|
||||
await delay(3000)
|
||||
this.startHeartbeat()
|
||||
// 错开启动时间,避免同时打开端口竞争
|
||||
await delay(500)
|
||||
|
||||
this.addLog('485通信', '监听已重置(第' + this.reconnectCount + '次)')
|
||||
// 再启动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 {
|
||||
// 重连次数耗尽,自动重启应用
|
||||
this.addLog('串口通信', `重连${this.maxReconnect}次均失败,即将自动重启应用`)
|
||||
|
||||
uni.showToast({
|
||||
title: '通信异常,3秒后重启',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.restart()
|
||||
// #endif
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
readData() {
|
||||
this.timer = setInterval(async () => {
|
||||
cmd.getTemp()
|
||||
await cmd.getTemp()
|
||||
await delay(1000)
|
||||
cmd.getPressure()
|
||||
await cmd.getPressure()
|
||||
}, 5000)
|
||||
},
|
||||
async handle485HexData(hex) {
|
||||
handle485HexData(hex) {
|
||||
// 更新最后收到数据的时间戳
|
||||
this.last485DataTime = Date.now()
|
||||
|
||||
@@ -557,8 +613,6 @@ export default {
|
||||
// 左门触点触发(关门)
|
||||
// 调用cmd.LeftDoor(false)来更新门状态
|
||||
cmd.LeftDoor(false)
|
||||
// 触发关门事件
|
||||
// this.closeDoorEvent()
|
||||
uni.showToast({
|
||||
title: '左门已关闭',
|
||||
icon: 'none'
|
||||
@@ -569,8 +623,6 @@ export default {
|
||||
// 右门触点触发(关门)
|
||||
// 调用cmd.RightDoor(false)来更新门状态
|
||||
cmd.RightDoor(false)
|
||||
// 触发关门事件
|
||||
// this.closeDoorEvent()
|
||||
uni.showToast({
|
||||
title: '右门已关闭',
|
||||
icon: 'none'
|
||||
@@ -579,11 +631,10 @@ export default {
|
||||
}
|
||||
if (data.action == 'door' && data.status == 'closed') {
|
||||
try {
|
||||
await cmd.LeftDoor(false)
|
||||
await delay(100)
|
||||
await cmd.RightDoor(false)
|
||||
// 触发关门事件
|
||||
await this.closeDoorEvent()
|
||||
cmd.LeftDoor(false)
|
||||
cmd.RightDoor(false)
|
||||
// 触发关门事件,不需要重复触发,监听器触发
|
||||
// this.closeDoorEvent()
|
||||
uni.showToast({
|
||||
title: '两门已关闭',
|
||||
icon: 'none'
|
||||
@@ -666,50 +717,7 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
// handle232HexData(hex) {
|
||||
// // 处理232接口上报的数据
|
||||
// let data = cmd.parse232dData(hex)
|
||||
// if (data == {}) return
|
||||
// if (data.action == 'leave' && data.type == 'internal') {
|
||||
// // 更新内镜数据
|
||||
// let key = data.address - 1
|
||||
// uni.$emit('notice', { title: data.address, content: '内镜取出'})
|
||||
// this.updateScope(key, 'leave', '')
|
||||
// // 添加日志数据
|
||||
// this.addLog('位置'+key, '内镜取出')
|
||||
// }
|
||||
// if (data.action == 'enter' && data.type == 'internal') {
|
||||
// // 验证内镜信息
|
||||
// this.checkEndoCard(data.number).then(res => {
|
||||
// if (res) {
|
||||
// uni.$emit('notice', { title: data.number, content: '内镜存入'})
|
||||
// // 更新内镜数据
|
||||
// let key = data.address - 1
|
||||
// this.updateScope(key, 'enter', data.number)
|
||||
// // 添加日志数据
|
||||
// this.addLog(data.number, '内镜存入')
|
||||
// }
|
||||
// })
|
||||
// uni.$emit('ic', data.number)
|
||||
// }
|
||||
// if (data.action == 'enter' && data.type == 'person') {
|
||||
// // 验证人员信息
|
||||
// this.checkPersonCard(data.number).then(res => {
|
||||
// if (res) {
|
||||
// // 开门事件
|
||||
// cmd.Door(true)
|
||||
// // this.openDoorEvent()
|
||||
// uni.$emit('notice', { title: data.number, content: '刷卡开门'})
|
||||
// // 添加日志数据
|
||||
// this.addLog(data.number, '刷卡开门')
|
||||
// }
|
||||
// })
|
||||
// uni.$emit('ic', data.number)
|
||||
// }
|
||||
// if (data.action == 'door') {
|
||||
// cmd.Door(false)
|
||||
// }
|
||||
// },
|
||||
|
||||
// 密码框提交
|
||||
inputModalSubmit(val) {
|
||||
let password = this.$store.state.base.screenPw
|
||||
@@ -782,29 +790,30 @@ 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() {
|
||||
// 关闭真空泵和消毒,打开照明,打开门锁,打开风机
|
||||
cmd.Vacuum(false)
|
||||
await delay(300)
|
||||
cmd.Light(true)
|
||||
await delay(300)
|
||||
await delay(500)
|
||||
cmd.Vacuum(false)
|
||||
await delay(500)
|
||||
cmd.Wind(true)
|
||||
},
|
||||
// 关门事件
|
||||
async closeDoorEvent() {
|
||||
// 关闭照明,打开真空泵、消毒
|
||||
cmd.Light(false) // 照明关闭指令
|
||||
await delay(300)
|
||||
cmd.Vacuum(true) //真空泵开启指令
|
||||
await delay(300)
|
||||
cmd.Wind(true)
|
||||
await delay(500)
|
||||
await cmd.Light(false) // 照明关闭指令
|
||||
await delay(500)
|
||||
await cmd.Vacuum(true) //真空泵开启指令
|
||||
await delay(500)
|
||||
await cmd.Wind(true)
|
||||
},
|
||||
// 验证人员卡是否可以开门
|
||||
async checkPersonCard(ic) {
|
||||
@@ -966,27 +975,6 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 左门循环开关(调试用,独立定时器)
|
||||
* 每5秒切换一次左门状态
|
||||
*/
|
||||
startLeftDoorCycle() {
|
||||
clearInterval(this.leftDoorCycleTimer)
|
||||
let isOpen = false
|
||||
this.leftDoorCycleTimer = setInterval(async () => {
|
||||
try {
|
||||
if (isOpen) {
|
||||
cmd.LeftDoor(false)
|
||||
} else {
|
||||
cmd.LeftDoor(true)
|
||||
}
|
||||
isOpen = !isOpen
|
||||
} catch (error) {
|
||||
this.addLog('左门循环错误', error.message || error)
|
||||
}
|
||||
}, 5000)
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+375
@@ -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数据丢失,但设备控制态由硬件保持 |
|
||||
+205
-17
@@ -16,8 +16,192 @@
|
||||
// let RS232 = getApp().globalData.RS232
|
||||
import store from '@/store/index.js'
|
||||
|
||||
// ========== RS485 优先级消息队列 START ==========
|
||||
const PRIORITY = {
|
||||
HIGH: 0, // 用户操作(开门/关门/控制指令)
|
||||
LOW: 1, // 定时轮询(getTemp/getPressure)
|
||||
}
|
||||
|
||||
const rs485Queue = []
|
||||
let isRS485Sending = false
|
||||
const RS485_SEND_DELAY = 200 // 每条指令间隔200ms,确保总线空闲
|
||||
const MAX_QUEUE_SIZE = 3 // 队列最大长度
|
||||
|
||||
/**
|
||||
* 将任务按优先级插入队列
|
||||
* 规则:
|
||||
* 1. 队列最大长度为3
|
||||
* 2. 重复指令(相同cmd)不入队,直接丢弃
|
||||
* 3. 队列满时,高优先级任务挤掉低优先级任务,否则踢掉最老的(队首)
|
||||
*/
|
||||
const enqueueTask = (task) => {
|
||||
// 规则1: 去重 - 已存在相同cmd的任务则丢弃
|
||||
const isDuplicate = rs485Queue.some(t => t.cmd === task.cmd)
|
||||
if (isDuplicate) {
|
||||
// console.warn('[RS485队列] 丢弃重复任务:', task.cmd)
|
||||
return false
|
||||
}
|
||||
|
||||
// 规则2: 队列满了,先腾位置
|
||||
if (rs485Queue.length >= MAX_QUEUE_SIZE) {
|
||||
if (task.priority === PRIORITY.HIGH) {
|
||||
// 高优先级:优先挤掉队列中最后一个低优先级任务
|
||||
let lastLowIndex = -1
|
||||
for (let i = rs485Queue.length - 1; i >= 0; i--) {
|
||||
if (rs485Queue[i].priority === PRIORITY.LOW) {
|
||||
lastLowIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (lastLowIndex !== -1) {
|
||||
// console.warn('[RS485队列] 高优挤掉低优:', rs485Queue[lastLowIndex].cmd)
|
||||
rs485Queue.splice(lastLowIndex, 1)
|
||||
} else {
|
||||
// 没有低优先级可挤,踢掉队首最老的
|
||||
// console.warn('[RS485队列] 满,踢掉队首:', rs485Queue[0].cmd)
|
||||
rs485Queue.shift()
|
||||
}
|
||||
} else {
|
||||
// 低优先级:直接踢掉队首最老的
|
||||
// console.warn('[RS485队列] 满,踢掉队首:', rs485Queue[0].cmd)
|
||||
rs485Queue.shift()
|
||||
}
|
||||
}
|
||||
|
||||
// 规则3: 按优先级插入
|
||||
if (task.priority === PRIORITY.HIGH) {
|
||||
let insertIndex = rs485Queue.findIndex(t => t.priority === PRIORITY.LOW)
|
||||
if (insertIndex === -1) {
|
||||
rs485Queue.push(task)
|
||||
} else {
|
||||
rs485Queue.splice(insertIndex, 0, task)
|
||||
}
|
||||
} else {
|
||||
rs485Queue.push(task)
|
||||
}
|
||||
|
||||
// console.log(`[RS485队列] 入队成功, 长度:${rs485Queue.length}, cmd:${task.cmd}`)
|
||||
return true
|
||||
}
|
||||
|
||||
// ========== RS485 发送超时保护 ==========
|
||||
const RS485_SEND_TIMEOUT = 2000 // 发送超时2秒
|
||||
|
||||
/**
|
||||
* 带超时的RS485发送封装
|
||||
* 防止底层串口驱动卡死导致Promise永久pending,造成队列死锁
|
||||
*/
|
||||
const sendWithTimeout = (rs485Instance, cmd) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
console.warn(`[RS485] 发送超时(${RS485_SEND_TIMEOUT}ms), cmd: ${cmd}`)
|
||||
reject(new Error('RS485发送超时'))
|
||||
}, RS485_SEND_TIMEOUT)
|
||||
|
||||
const result = rs485Instance.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)
|
||||
}
|
||||
})
|
||||
}
|
||||
// ========== RS485 发送超时保护 END ==========
|
||||
|
||||
const processRS485Queue = async () => {
|
||||
if (isRS485Sending || rs485Queue.length === 0) {
|
||||
return
|
||||
}
|
||||
isRS485Sending = true
|
||||
const task = rs485Queue.shift()
|
||||
try {
|
||||
let RS485 = getApp().globalData.RS485
|
||||
if (!RS485) {
|
||||
throw new Error('RS485 not initialized')
|
||||
}
|
||||
// 使用带超时的发送封装,防止Promise永久pending导致队列死锁
|
||||
await sendWithTimeout(RS485, task.cmd)
|
||||
// 发送后延迟,确保总线空闲再发下一条
|
||||
await new Promise(r => setTimeout(r, RS485_SEND_DELAY))
|
||||
task.resolve()
|
||||
} catch (error) {
|
||||
console.error('RS485 send error:', error)
|
||||
task.reject(error)
|
||||
} finally {
|
||||
isRS485Sending = false
|
||||
processRS485Queue()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送RS485指令(带优先级)
|
||||
* @param {string} cmd - 十六进制指令字符串
|
||||
* @param {number} priority - 优先级,默认HIGH
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const enqueueRS485Send = (cmd, priority = PRIORITY.HIGH) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
enqueueTask({ cmd, priority, resolve, reject })
|
||||
processRS485Queue()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空队列(重连时调用,拒绝所有待发送请求)
|
||||
*/
|
||||
const clearRS485Queue = () => {
|
||||
while (rs485Queue.length > 0) {
|
||||
const task = rs485Queue.shift()
|
||||
task.reject(new Error('RS485队列已清空(重连中)'))
|
||||
}
|
||||
isRS485Sending = false // 强制释放发送锁,防止队列死锁
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前队列长度(调试用)
|
||||
*/
|
||||
const getRS485QueueSize = () => {
|
||||
return rs485Queue.length
|
||||
}
|
||||
// ========== 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)
|
||||
// 开左门指令 (通过RS485站号03) - 高优先级
|
||||
// 中盛4路数字IO模块,功能码06(写单个保持寄存器)
|
||||
LeftDoor: async (status) => {
|
||||
// 左门开门: 03 06 00 00 00 01 49 E8
|
||||
@@ -26,12 +210,10 @@ export default {
|
||||
store.state.relay.leftDoor = status
|
||||
// door状态 = leftDoor OR rightDoor (任一门打开则door为true)
|
||||
store.state.relay.door = store.state.relay.leftDoor || store.state.relay.rightDoor
|
||||
let RS485 = getApp().globalData.RS485
|
||||
await RS485.sendDataString(cmd)
|
||||
|
||||
await enqueueRS485Send(cmd, PRIORITY.HIGH)
|
||||
},
|
||||
|
||||
// 开右门指令 (通过RS485站号03)
|
||||
// 开右门指令 (通过RS485站号03) - 高优先级
|
||||
// 中盛4路数字IO模块,功能码06(写单个保持寄存器)
|
||||
RightDoor: async (status) => {
|
||||
// 右门开门: 03 06 00 01 00 01 18 28
|
||||
@@ -40,8 +222,7 @@ export default {
|
||||
store.state.relay.rightDoor = status
|
||||
// door状态 = leftDoor OR rightDoor (任一门打开则door为true)
|
||||
store.state.relay.door = store.state.relay.leftDoor || store.state.relay.rightDoor
|
||||
let RS485 = getApp().globalData.RS485
|
||||
await RS485.sendDataString(cmd)
|
||||
await enqueueRS485Send(cmd, PRIORITY.HIGH)
|
||||
},
|
||||
|
||||
// 开门关门控制 01
|
||||
@@ -58,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) => {
|
||||
@@ -66,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) => {
|
||||
@@ -74,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) => {
|
||||
@@ -82,20 +263,27 @@ 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: () => {
|
||||
let RS485 = getApp().globalData.RS485
|
||||
let cmd = '020300000002C438';
|
||||
RS485.sendDataString(cmd);
|
||||
return enqueueRS485Send(cmd, PRIORITY.LOW)
|
||||
},
|
||||
|
||||
// 获取压差指令
|
||||
// 获取压差指令(通过RS485站号01)- 低优先级
|
||||
getPressure: () => {
|
||||
let RS485 = getApp().globalData.RS485
|
||||
let cmd = '01030001000295CB'
|
||||
RS485.sendDataString(cmd);
|
||||
return enqueueRS485Send(cmd, PRIORITY.LOW)
|
||||
},
|
||||
|
||||
// 导出队列管理方法
|
||||
clearRS485Queue,
|
||||
getRS485QueueSize,
|
||||
|
||||
// 重置RS485队列锁(供外部重连时调用)
|
||||
resetRS485Lock() {
|
||||
clearRS485Queue() // 内部已包含清队列 + 释放锁
|
||||
},
|
||||
|
||||
// 解析门卡数据,返回卡号
|
||||
|
||||
Reference in New Issue
Block a user