/** * 全局串口通信服务 * 支持双模式:RS485串口直连 / TCP网口转485(塔石LAN869) */ import * as db from '@/db/sqlite.js' import cmd from '@/utils/cmd.js' import storage from '@/utils/storage.js' import { formatDateTime, delay, getTimeDifference, compareTimes } from '@/utils/common.js' import * as Api from '@/api/index.js' import beeper from '@/utils/beeper.js' import store from '@/store/index.js' import wakeLock from '@/utils/wakeLock.js' import tcp485 from '@/utils/tcp485.js' const serialService = { RS485: null, RS232: null, tcp485: null, timer: null, taskTimer: null, heartbeatTimer: null, last485DataTime: null, reconnectCount: 0, maxReconnect: 5, keepRunning: false, get mode485() { return storage.get('rs485Mode') || 'serial' }, // ========== 公开入口 ========== init() { if (this.mode485 === 'tcp') { this.tcp485 = tcp485 console.log('[serialService] TCP485模式') } else { this.RS485 = getApp().globalData.RS485 console.log('[serialService] RS485串口模式') } this.RS232 = getApp().globalData.RS232 this._initStorage() this._initTime() this._initDB() try { this._startRS485() this.startRS232() } catch (e) { console.error('[serialService] 初始化通信失败:', e) uni.showToast({ title: '初始化通信失败', icon: 'error' }) } this._initWatchers() wakeLock.acquire() this.keepRunning = true console.log('[serialService] 已启动') }, shutdown() { this.keepRunning = false this.stopHeartbeatIfNeeded() this.stopRS485() this.stopRS232() if (this.timer) { clearInterval(this.timer); this.timer = null } if (this.taskTimer) { clearInterval(this.taskTimer); this.taskTimer = null } cmd.clearQueue() wakeLock.release() this.addLog('串口通信', '服务已停止') }, async restart() { if (this.mode485 === 'tcp') { this.tcp485 = tcp485 } else { if (!this.RS485) this.RS485 = getApp().globalData.RS485 } if (!this.RS232) this.RS232 = getApp().globalData.RS232 cmd.clearQueue() try { await this._startRS485() this.startRS232() this.keepRunning = true wakeLock.acquire() this.addLog('485通信', '服务已重启') } catch (e) { uni.showToast({ title: '重启通信失败', icon: 'error' }) } }, // ========== 串口启停 ========== async _startRS485() { if (this.mode485 === 'tcp') { await this._startRS485_TCP() } else { this._startRS485_Serial() } }, _startRS485_Serial() { let rs485Conf = storage.get('rs485') || { path: '/dev/ttyUSB2', baudrate: 9600 } this.RS485.setPath(rs485Conf.path) this.RS485.setBaudrate(rs485Conf.baudrate) const state = this.RS485.open() if (state) { uni.showToast({ title: '485已打开', icon: 'none' }) this.RS485.onStartAutoReadData((res) => { try { let hex = this.RS485.byte2HexString(res) this.handle485HexData(hex) } catch (e) { this.addLog('485回调异常', e.message || e) } }) clearInterval(this.timer) this.readData() } else { this.stopHeartbeatIfNeeded() uni.showToast({ title: '485打开失败', icon: 'error' }) throw new Error('[重连] RS485 打开失败') } }, async _startRS485_TCP() { let rs485Tcp = storage.get('rs485Tcp') || {} const host = rs485Tcp.host const port = rs485Tcp.port if (!host || !port) { uni.showToast({ title: '请先配置TCP参数', icon: 'none' }) throw new Error('[TCP485] 缺少TCP配置') } this.tcp485.setHost(host, port) this.tcp485.onLog((name, action) => { this.addLog(name, action) }) try { const state = await this.tcp485.open() if (state) { uni.showToast({ title: 'TCP已连接', icon: 'none' }) this.tcp485.onStartAutoReadData((hexStr) => { try { this.handle485HexData(hexStr) } catch (e) { this.addLog('TCP485回调异常', e.message || e) } }) clearInterval(this.timer) this.readData() console.log('[TCP485] 连接成功', host + ':' + port) } else { throw new Error('[TCP485] 连接失败') } } catch (error) { this.stopHeartbeatIfNeeded() uni.showToast({ title: 'TCP连接失败', icon: 'error' }) throw new Error('[重连] TCP485 连接失败: ' + (error.message || error)) } }, startRS232() { let rs232Conf = storage.get('rs232') || { path: '/dev/ttyUSB1', baudrate: 115200 } this.RS232.setPath(rs232Conf.path) this.RS232.setBaudrate(rs232Conf.baudrate) const state = this.RS232.open() if (state) { uni.showToast({ title: '232已打开', icon: 'none' }) this.RS232.onStartAutoReadData((res) => { try { let hex = this.RS232.byte2HexString(res) this.handle232HexData(hex) } catch (e) { this.addLog('232回调异常', e.message || e) } }) this.startAutoTask() } else { uni.showToast({ title: '232打开失败', icon: 'error' }) throw new Error('[重连] RS232 打开失败') } }, stopRS485() { if (this.mode485 === 'tcp') { this.addLog('485通信', 'TCP连接关闭') if (this.tcp485) { this.tcp485.stopReadPortData() this.tcp485.close() } } else { this.addLog('485通信', '串口关闭') if (this.RS485) { this.RS485.stopReadPortData() this.RS485.close() } } }, stopRS232() { if (this.RS232) { this.RS232.stopReadPortData() this.RS232.close() } }, // ========== 数据轮询 ========== readData() { this.timer = setInterval(async () => { await cmd.getTemp() await delay(2000) await cmd.getPressure() }, 8000) }, startHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null } this.addLog('485通信', '心跳检测已启动') this.heartbeatTimer = setInterval(() => { this.check485Heartbeat() }, 10000) }, stopHeartbeatIfNeeded() { if (!this.heartbeatTimer) return clearInterval(this.heartbeatTimer) this.heartbeatTimer = null this.addLog('485通信', '心跳检测已停止') }, check485Heartbeat() { if (!this.last485DataTime) { console.log('[心跳] 等待首次数据...') return } const now = Date.now() const timeout = 45000 if (now - this.last485DataTime > timeout) { if (this.reconnectCount >= this.maxReconnect) { this.addLog('485通信', '重连' + this.maxReconnect + '次失败,请检查硬件') uni.showToast({ title: '485通信中断,请检查硬件连接', icon: 'none', duration: 3000 }) return } this.addLog('485通信', '数据中断(第' + (this.reconnectCount + 1) + '次重连)') uni.showToast({ title: '485通信中断,正在恢复...', icon: 'none', duration: 2000 }) this.resetRS485Only() } }, // ========== 485隔离重连(不影响RS232) ========== async resetRS485Only() { try { this.reconnectCount++ // 只清485相关的定时器(不清taskTimer!RS232业务继续运行) if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null } if (this.timer) { clearInterval(this.timer); this.timer = null } // 只停485通信(完全不动RS232) if (this.mode485 === 'tcp') { if (this.tcp485) { this.tcp485.stopReadPortData(); this.tcp485.close() } } else { if (this.RS485) { this.RS485.stopReadPortData(); this.RS485.close() } } cmd.clearQueue() const backoff = Math.min(1000 * Math.pow(2, this.reconnectCount - 1), 30000) const waitTime = Math.max(backoff, 2500) console.log('[485重连] 第' + this.reconnectCount + '次, 等待' + waitTime + 'ms (RS232不受影响)') await delay(waitTime) this.last485DataTime = null await this._startRS485() this.addLog('485通信', '485重连完成(第' + this.reconnectCount + '次)[RS232未受影响]') } catch (error) { this.addLog('485通信错误', error.message || '485重置失败') await delay(5000) if (this.reconnectCount < this.maxReconnect) { this.resetRS485Only() } else { this.addLog('串口通信', '485重连' + this.maxReconnect + '次均失败,请检查485线路') uni.showToast({ title: '485通信异常', icon: 'none', duration: 3000 }) this.reconnectCount = 0 } } }, // ========== 统一重连(全局故障用) ========== async resetSerialPorts() { try { this.reconnectCount++ if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null } if (this.timer) { clearInterval(this.timer); this.timer = null } if (this.taskTimer) { clearInterval(this.taskTimer); this.taskTimer = null } // 停止485通信 if (this.mode485 === 'tcp') { if (this.tcp485) { this.tcp485.stopReadPortData(); this.tcp485.close() } } else { if (this.RS485) { this.RS485.stopReadPortData(); this.RS485.close() } } // 停止RS232 if (this.RS232) { this.RS232.stopReadPortData(); this.RS232.close() } cmd.clearQueue() 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) this.last485DataTime = null await this._startRS485() await delay(500) 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(() => { plus.runtime.restart() }, 3000) } } }, // ========== 数据处理 ========== handle485HexData(hex) { this.last485DataTime = Date.now() let d = cmd.parse485Data(hex) if (d.hasOwnProperty('temp')) { store.state.sensor.temp = d.temp } if (d.hasOwnProperty('humi')) { store.state.sensor.humi = d.humi } if (d.hasOwnProperty('pressure')) { let pressure = parseFloat(d.pressure) + parseInt(store.state.run.pressureCom || 0) store.state.sensor.pressure = pressure } if (d.action === 'LeftDoor' && d.status === 'closed') { cmd.LeftDoor(false) uni.showToast({ title: '左门已关闭', icon: 'none' }) this.addLog('左门触点触发', '左门已关闭') } if (d.action === 'RightDoor' && d.status === 'closed') { cmd.RightDoor(false) uni.showToast({ title: '右门已关闭', icon: 'none' }) this.addLog('右门触点触发', '右门已关闭') } if (d.action === 'door' && d.status === 'closed') { try { cmd.LeftDoor(false) cmd.RightDoor(false) uni.showToast({ title: '两门已关闭', icon: 'none' }) } catch (error) { this.addLog('门关闭错误', error.message || error) } } }, handle232HexData(hex) { let d = cmd.parse232dData(hex) if (!d || Object.keys(d).length === 0) return if (d.hasOwnProperty('number')) { uni.$emit('ic', d.number) if (store.state.relay.door === false) { // 门关闭状态 this.checkPersonCard(d.number).then(res => { if (res) { uni.$emit('showDoorSelect', d.number) } else { this.checkEndoCard(d.number).then(res => { if (res) { uni.showToast({ title: '请先开门', icon: 'none' }) } else { uni.showToast({ title: '无效卡片', icon: 'none' }) } }) } }) } else { // 门打开状态 this.checkPersonCard(d.number).then(res => { if (res) { uni.$emit('showDoorSelect', d.number) } else { this.checkEndoCard(d.number).then(res => { if (res) { uni.$emit('showActionSelect', d.number) } else { uni.showToast({ title: '无效卡片', icon: 'none' }) } }) } }) } } }, // ========== 业务任务 ========== startAutoTask() { this.taskTimer = setInterval(() => { this.checkDoorOpenTimer() this.vacuumTask() this.disinfectTask() this.windCloseTask() }, 2000) }, async disinfectTask() { let group = store.state.run.group let now = formatDateTime() let nowTime = now.split(' ')[1] let time = nowTime.substring(0, nowTime.length - 3) let isStart = false for (let i = 0; i < group.length; i++) { if (group[i].status) { let start = group[i].start.h + ':' + group[i].start.m let end = group[i].end.h + ':' + group[i].end.m if (compareTimes(start, time) === 1 && compareTimes(time, end) === 1) { isStart = true break } } } if (store.state.relay.disinfect === false && isStart === true) { cmd.Disinfect(true) await delay(100) cmd.Wind(true) } if (store.state.relay.disinfect === true && isStart === false && store.state.autoDisinfect === true) { cmd.Disinfect(false) } }, async vacuumTask() { let start = store.state.timer.vacuumStart if (start !== '') { let startM = getTimeDifference(start, formatDateTime()) let runTime = store.state.run.vacuumRunTime if (startM.minutes >= runTime) { store.state.timer.vacuumStart = '' cmd.Vacuum(false) } } let end = store.state.timer.vacuumEnd if (end !== '') { let endH = getTimeDifference(end, formatDateTime()) let perHour = store.state.run.vacuumPerHour if (endH.hours >= perHour) { store.state.timer.vacuumEnd = '' cmd.Vacuum(true) await delay(100) cmd.Wind(true) } } }, async windCloseTask() { await delay(100) let { light, vacuum, disinfect, door } = store.state.relay if (light || vacuum || disinfect || door) return if (store.state.sensor.temp > store.state.run.temp) return if (store.state.sensor.humi > store.state.run.humi) return cmd.Wind(false) }, checkDoorOpenTimer() { let openTime = store.state.timer.door if (openTime !== '' && store.state.clean === false) { let diff = getTimeDifference(formatDateTime(openTime), formatDateTime()) if (diff.minutes > 3) { store.state.timer.doorAlert = true uni.$emit('notice', { title: '未关门', content: '请及时关闭' }) } } }, // ========== 门事件 ========== async openDoorEvent() { await cmd.Light(true) await cmd.Vacuum(false) await cmd.Wind(true) }, async closeDoorEvent() { await cmd.Light(false) await cmd.Vacuum(true) await cmd.Wind(true) }, // ========== 卡片验证 ========== async checkPersonCard(ic) { let res1 = await db.selectDataList('user', { ic: ic }) if (res1.length > 0) return true let res2 = await db.selectDataList('user', { ic2: ic }) if (res2.length > 0) return true return false }, async checkEndoCard(ic) { let res1 = await db.selectDataList('endo', { ic: ic }) if (res1.length > 0) return true let res2 = await db.selectDataList('endo', { ic2: ic }) if (res2.length > 0) return true return false }, // ========== 日志 ========== async addLog(name, action) { try { await db.addTabItem('log', { name, action }) } catch (error) {} }, // ========== 内部初始化 ========== _initStorage() { let runStorage = storage.get('run') if (runStorage) store.commit('SET_RUN', { ...runStorage }) let baseStorage = storage.get('base') if (baseStorage) store.commit('SET_BASE', { ...baseStorage }) let config = storage.get('config') if (config) store.state.apiUrl = 'http://' + config.ip + ':' + config.port }, _initTime() { store.state.time = formatDateTime() if (store.state.apiUrl !== '') { Api.time().then(res => { if (res && !isNaN(Date.parse(res))) { store.state.time = res store.state.connect = true } }) } }, async _initDB() { if (!await db.isTable('user')) await db.addUserTable() if (!await db.isTable('log')) await db.addLogTable() if (!await db.isTable('endo')) await db.addEndoTable() if (!await db.isTable('scope')) { await db.addScopeTable() for (let i = 0; i < 16; i++) { await db.addTabItem('scope', { name: '', ic: '', key: i }) } } }, _initWatchers() { store.watch( state => state.relay.disinfect, (newVal) => { if (newVal === true) store.state.timer.disinfect = formatDateTime() if (newVal === false) { let timer = store.state.timer.disinfect if (timer) { let diff = getTimeDifference(timer, formatDateTime()) let local = storage.get('disinfectRun') || 0 storage.set('disinfectRun', diff.minutes + local) } } } ) store.watch( state => state.relay.wind, (newVal, oldVal) => { if (newVal === true && oldVal === false) store.state.timer.wind = formatDateTime() if (newVal === false) { let timer = store.state.timer.wind if (timer) { let diff = getTimeDifference(timer, formatDateTime()) let local = storage.get('windRun') || 0 storage.set('windRun', diff.minutes + local) } } } ) store.watch( state => state.sensor.humi, (newVal) => { if (newVal > store.state.run.humi && store.state.relay.wind === false) { cmd.Wind(true) } } ) store.watch( state => state.sensor.temp, (newVal) => { if (newVal > store.state.run.temp && store.state.relay.wind === false) { cmd.Wind(true) } } ) store.watch( state => state.relay.door, (newVal, oldVal) => { if (newVal === true) { this.openDoorEvent() store.state.timer.door = formatDateTime() } if (newVal === false) { this.closeDoorEvent() this.addLog('门', '手动关门') store.state.timer.door = '' store.state.timer.doorAlert = false } } ) store.watch( state => state.timer.doorAlert, (newVal) => { if (newVal === true) beeper.play() if (newVal === false) beeper.stop() } ) store.watch( state => state.relay.vacuum, (newVal) => { if (newVal === true) store.state.timer.vacuumStart = formatDateTime() if (newVal === false) store.state.timer.vacuumEnd = formatDateTime() } ) }, } export default serialService