// 接收数据 // 压差站号: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 }, }