Files
endo_an_2/utils/cmd.js
T

418 lines
14 KiB
JavaScript
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.
// 接收数据
// 压差站号:01,温湿度站号:02,门控制站号:03
// 压差 01 03 02 12 34 B5 33
// 温湿度 01 03 04 02 98 00 BC 7B D5
// IC卡 20 01 00 08 04 00 00 00 0e 26 fe ab 8f 03
// ID卡 20 01 00 05 49 00 b5 6a b5 d8 03
// 卡离开 20 01 00 01 02 fd 03
// 门控制指令(中盛4路数字IO模块,功能码06):
// 左门开门: 03 06 00 00 00 01 49 E8
// 左门关门: 03 06 00 00 00 00 88 28
// 右门开门: 03 06 00 01 00 01 18 28
// 右门关门: 03 06 00 01 00 00 D9 E8
// 卡片放入和离开 起始位20, 所有卡片通过同一通道读取,通过数据库比对判断类型
// let RS232 = getApp().globalData.RS232
import store from '@/store/index.js'
import storage from '@/utils/storage.js'
import tcp485 from '@/utils/tcp485.js'
// ========== 统一消息队列 START ==========
// RS485 和 RS232 共用同一队列,通过 type 字段区分串口实例
// 互斥锁确保同一时刻只有一个串口在发送指令,杜绝双串口并发竞态
const PRIORITY = {
HIGH: 0, // 用户操作(开门/关门/控制指令)
LOW: 1, // 定时轮询(getTemp/getPressure
}
const queue = []
let isSending = false
const SEND_DELAY = 150 // 发送间隔150ms
const MAX_QUEUE_SIZE = 5 // 队列最大长度
const SEND_TIMEOUT = 1000 // 统一超时1秒
/**
* 获取当前485通信实例(根据模式动态选择)
* @returns {object} RS485实例或TCP485实例
*/
const get485Instance = () => {
const mode = storage.get('rs485Mode') || 'serial'
if (mode === 'tcp') {
return tcp485
} else {
return getApp().globalData.RS485
}
}
/**
* 将任务按优先级插入队列
* 规则:
* 1. 队列最大长度为5
* 2. 重复指令(相同type+cmd)不入队,直接丢弃
* 3. 队列满时,高优先级任务挤掉低优先级任务,否则踢掉最老的(队首)
*/
const enqueueTask = (task) => {
// 规则1: 去重 - 已存在相同type+cmd的任务则丢弃
const isDuplicate = queue.some(t => t.cmd === task.cmd && t.type === task.type)
if (isDuplicate) {
return false
}
// 规则2: 队列满了,先腾位置
if (queue.length >= MAX_QUEUE_SIZE) {
if (task.priority === PRIORITY.HIGH) {
// 高优先级:优先挤掉队列中最后一个低优先级任务
let lastLowIndex = -1
for (let i = queue.length - 1; i >= 0; i--) {
if (queue[i].priority === PRIORITY.LOW) {
lastLowIndex = i
break
}
}
if (lastLowIndex !== -1) {
queue.splice(lastLowIndex, 1)
} else {
// 没有低优先级可挤,踢掉队首最老的
queue.shift()
}
} else {
// 低优先级:直接踢掉队首最老的
queue.shift()
}
}
// 规则3: 按优先级插入
if (task.priority === PRIORITY.HIGH) {
let insertIndex = queue.findIndex(t => t.priority === PRIORITY.LOW)
if (insertIndex === -1) {
queue.push(task)
} else {
queue.splice(insertIndex, 0, task)
}
} else {
queue.push(task)
}
return true
}
/**
* 带超时的统一发送封装
* 防止底层串口驱动卡死导致Promise永久pending,造成队列死锁
*/
const sendWithTimeout = (instance, cmd) => {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
console.warn(`[统一队列] 发送超时(${SEND_TIMEOUT}ms), cmd: ${cmd}`)
reject(new Error('发送超时'))
}, SEND_TIMEOUT)
const result = instance.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)
}
})
}
/**
* 处理队列中的下一个任务
* 根据 task.type 自动获取对应串口实例(RS485/RS232/TCP485
*/
const processQueue = async () => {
if (isSending || queue.length === 0) {
return
}
isSending = true
const task = queue.shift()
try {
// 根据 type 获取对应的通信实例
let instance = null
if (task.type === 'RS485') {
// 动态选择485通信实例(串口或TCP)
instance = get485Instance()
} else if (task.type === 'RS232') {
// RS232始终使用串口
instance = getApp().globalData.RS232
}
if (!instance) {
throw new Error(`${task.type} not initialized`)
}
// 统一发送(带超时保护)
await sendWithTimeout(instance, task.cmd)
// 发送后延迟,确保总线空闲再发下一条
await new Promise(r => setTimeout(r, SEND_DELAY))
task.resolve()
} catch (error) {
console.error('[统一队列] send error:', error)
task.reject(error)
} finally {
isSending = false
processQueue() // 处理下一条
}
}
/**
* 入队接口
* @param {string} type - 'RS485' 或 'RS232'
* @param {string} cmd - 十六进制指令字符串
* @param {number} priority - 优先级,默认HIGH
* @returns {Promise}
*/
const enqueue = (type, cmd, priority = PRIORITY.HIGH) => {
return new Promise((resolve, reject) => {
enqueueTask({ type, cmd, priority, resolve, reject })
processQueue()
})
}
/**
* 清空队列(重连时调用,拒绝所有待发送请求)
*/
const clearQueue = () => {
while (queue.length > 0) {
const task = queue.shift()
task.reject(new Error('队列已清空(重连中)'))
}
isSending = false // 强制释放发送锁,防止队列死锁
}
/**
* 获取当前队列长度(调试用)
*/
const getQueueSize = () => {
return queue.length
}
// ========== RS485 优先级消息队列 END ==========
export default {
// 开左门指令 (通过RS485站号03) - 高优先级
// 中盛4路数字IO模块,功能码06(写单个保持寄存器)
LeftDoor: async (status) => {
// 左门开门: 03 06 00 00 00 01 49 E8
// 左门关门: 03 06 00 00 00 00 88 28
let cmd = status ? '03060000000149E8' : '0306000000008828'
try {
await enqueue('RS485', cmd, PRIORITY.HIGH)
// 状态更新移到await之后,防止Vue Watcher提前触发RS232并发
store.state.relay.leftDoor = status
// door状态 = leftDoor OR rightDoor (任一门打开则door为true)
store.state.relay.door = store.state.relay.leftDoor || store.state.relay.rightDoor
} catch (error) {
console.error('[LeftDoor] error:', error)
throw error
}
},
// 开右门指令 (通过RS485站号03) - 高优先级
// 中盛4路数字IO模块,功能码06(写单个保持寄存器)
RightDoor: async (status) => {
// 右门开门: 03 06 00 01 00 01 18 28
// 右门关门: 03 06 00 01 00 00 D9 E8
let cmd = status ? '0306000100011828' : '030600010000D9E8'
try {
await enqueue('RS485', cmd, PRIORITY.HIGH)
// 状态更新移到await之后,防止Vue Watcher提前触发RS232并发
store.state.relay.rightDoor = status
// door状态 = leftDoor OR rightDoor (任一门打开则door为true)
store.state.relay.door = store.state.relay.leftDoor || store.state.relay.rightDoor
} catch (error) {
console.error('[RightDoor] error:', error)
throw error
}
},
// 开风机指令 02 (通过RS232) - 高优先级
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);
},
// 开灯指令 03 (通过RS232) - 高优先级
Light: async (status) => {
let op = status ? '01' : '00'
let cmd = '5AA5EE03' + op
store.state.relay.light = status
let RS232 = getApp().globalData.RS232
await RS232.sendDataString(cmd);
},
// 开真空指令 04 (通过RS232) - 高优先级
Vacuum: async (status) => {
let op = status ? '01' : '00'
let cmd = '5AA5EE04' + op
store.state.relay.vacuum = status
let RS232 = getApp().globalData.RS232
await RS232.sendDataString(cmd);
},
// 开关消毒指令 05 (通过RS232) - 高优先级
Disinfect: async (status) => {
let op = status ? '01' : '00'
let cmd = '5AA5EE05' + op
store.state.relay.disinfect = status
let RS232 = getApp().globalData.RS232
await RS232.sendDataString(cmd);
},
// 获取温湿度(通过RS485站号02)- 低优先级
getTemp: () => {
let cmd = '020300000002C438';
return enqueue('RS485', cmd, PRIORITY.LOW)
},
// 获取压差指令(通过RS485站号01)- 低优先级
getPressure: () => {
let cmd = '01030001000295CB'
return enqueue('RS485', cmd, PRIORITY.LOW)
},
// 导出队列管理方法
clearQueue,
getQueueSize,
// 重置队列锁(供外部重连时调用)
resetLock() {
clearQueue() // 内部已包含清队列 + 释放锁
},
// 解析门卡数据,返回卡号
parse232dData: (hexString) => {
// IC卡 20 01 00 08 04 00 00 00 0e 26 fe ab 8f 03
// ID卡 20 01 00 05 49 00 b5 6a b5 d8 03
// 卡离开 20 01 00 01 02 fd 03
// 01 地址, 04 00 卡类型,0e 26 fe ab 卡号,8f 校验 03固定值
// 人员卡地址号0,卡号4个字节,倒数4个字节,倒数一位03固定值,倒数二位校验
// 内镜卡地址号1-16,卡号4个字节,倒数4个字节,倒数一位03固定值,倒数二位校验
// 卡离开8位,卡进入11位和14位
// 移除空格和换行符
const hexStr = hexString.replace(/\s+/g, '');
// 验证输入
if (hexStr.length === 0) {
throw new Error('请输入Modbus十六进制数据');
}
if (hexStr.length % 2 !== 0) {
throw new Error('请输入有效的Modbus十六进制数据');
}
// 解析后的数据对象
let data = {};
// 刷卡数据 起始字节20,第二字节站号,结束字节03
if (hexStr.substr(0, 2) == '20' && hexStr.substr(hexStr.length - 2, 2) == '03') {
// 卡数据
let hexNum = hexStr.substr(hexStr.length - 12, 8)
// 第二位是地址
let addHex = hexStr.substr(2, 2)
// 转换成10进制地址号
let address = parseInt(addHex, 16)
// 解析后的数据 { address: 0, number: 12345678, cardType: 'ic', action: 'enter', type: 'person' }
data.address = address
if (hexStr.length == 28) {
// 14字节是IC卡号
let icNo = parseInt(hexNum, 16)
data.number = icNo
data.cardType = 'ic'
data.action = 'enter'
}
if (hexStr.length == 22) {
// 11字节是ID卡号
let idNo = parseInt(hexNum, 16)
data.number = idNo
data.cardType = 'id'
data.action = 'enter'
}
if (hexStr.length == 14) {
// 7字节是卡离开
data.action = 'leave'
}
if (address == 0) {
// 人员卡
data.type = 'person'
}
if (address >= 1 && address <= 16) {
// 内镜卡
data.type = 'internal'
}
}
// 关门上报指令
// if (hexStr.toUpperCase() == '5AA50000000000FF') {
// // 开门数据 8个字节
// data.action = 'door'
// }
return data
},
parse485Data: (hexString) => {
// 移除空格和换行符
const hexStr = hexString.replace(/\s+/g, '');
// 压差站号:01,温湿度站号:02
let addHex = hexStr.substr(0, 2)
let data = {}
if (hexStr.length == 14 || hexStr.length == 18) {
if (addHex == '01') {
let pressure = parseInt(hexStr.substr(6, 4), 16);
data.pressure = pressure / 10
}
if (addHex == '02') {
let humi = parseInt(hexStr.substr(6, 4), 16);
let temp = parseInt(hexStr.substr(10, 4), 16);
// 温湿度上报数据异常处理,取正常温湿度范围内数据
temp = (temp / 10).toFixed(1)
if (temp >= 0 && temp < 100) {
data.temp = temp
}
humi = (humi / 10).toFixed(1)
if (humi >= 0 && humi < 100) {
data.humi = humi
}
}
if (addHex == '03') {
// 中盛4路数字IO控制器, 站号:03
// 左门开门: 03 06 00 01 00 01 18 28
// 左门关门: 03 06 00 01 00 00 D9 E8
// 03040200010130
// 右门开门: 03 06 00 00 00 01 49 E8
// 右门关门: 03 06 00 00 00 00 88 28
// 03040200024131
// 关门状态上报(功能码04,读输入寄存器):
// DI状态变化上报,解析左右门触点触发
let funcHex = hexStr.substr(8, 2);
if (funcHex == '01') {
// 左门关
data.action = 'LeftDoor'
data.status = 'closed'
}
if (funcHex == '02') {
// 右门关
data.action = 'RightDoor'
data.status = 'closed'
}
if (funcHex == '03') {
// 2个门关
data.action = 'door'
data.status = 'closed'
}
// console.log('parse485Data:',hexStr, data)
}
}
return data
},
}