/** * 全局串口通信服务 * 从 start.vue 提取,脱离 Vue 组件生命周期 * 解决 Android 10 后台 Native→JS Bridge 回调被阻塞导致的数据中断问题 */ 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' const serialService = { RS485: null, RS232: null, timer: null, // 485轮询定时器 (T2) taskTimer: null, // 业务任务定时器 (T3) heartbeatTimer: null, // 心跳检测定时器 (T1) last485DataTime: null, reconnectCount: 0, maxReconnect: 5, keepRunning: false, // ========== 公开入口 ========== /** * 初始化串口服务(App.onLaunch 调用) * 获取串口实例 → 初始化存储 → 打开双串口 → 启动轮询/心跳/Watcher/保活 */ init() { this.RS485 = getApp().globalData.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保持CPU活跃,防止后台Bridge回调被阻塞 wakeLock.acquire() this.keepRunning = true console.log('[serialService] 已启动') }, /** * 完全停止(去 modbus 等需要独占串口的场景调用) * stopSerialPorts + 停止保活 */ 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('串口通信', '服务已停止(shutdown)') }, /** * 从 modbus 返回后重启 */ async restart() { if (!this.RS485 || !this.RS232) { this.RS485 = getApp().globalData.RS485 this.RS232 = getApp().globalData.RS232 } // 重启前清理残留队列,避免旧任务使用过期串口引用 cmd.clearQueue() try { this.startRS485() this.startRS232() this.keepRunning = true wakeLock.acquire() this.addLog('485通信', '服务已重启(restart)') } catch (e) { uni.showToast({ title: '重启串口失败', icon: 'error' }) } }, // ========== 串口启停 ========== startRS485() { 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() // 心跳检测 // this.startHeartbeat() // if (this.reconnectCount > 0) { // this.reconnectCount-- // } } else { this.stopHeartbeatIfNeeded() uni.showToast({ title: '485打开失败', icon: 'error' }) throw new Error('[重连] RS485 打开失败') } }, async 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() { // this.stopHeartbeatIfNeeded() 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通信', '心跳检测已停止') }, // 检测485心跳 check485Heartbeat() { if (!this.last485DataTime) { console.log('[心跳] 等待首次485数据...') 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.resetSerialPorts() } }, // ========== 统一重连 ========== async resetSerialPorts() { try { this.reconnectCount++ // Step 1: 停止所有定时器 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 } // Step 2: 停止两个串口 if (this.RS485) { this.RS485.stopReadPortData(); this.RS485.close() } if (this.RS232) { this.RS232.stopReadPortData(); this.RS232.close() } // Step 3: 清空队列 cmd.clearQueue() // Step 4: 等待端口释放(指数退避) 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: 依次重启(失败时抛出异常触发重试) if (!this.startRS485()) { throw new Error('[重连] RS485 启动失败') } await delay(500) if (!this.startRS232()) { throw new Error('[重连] RS232 启动失败') } 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 data = cmd.parse485Data(hex) if (data.hasOwnProperty('temp')) { store.state.sensor.temp = data.temp } if (data.hasOwnProperty('humi')) { store.state.sensor.humi = data.humi } if (data.hasOwnProperty('pressure')) { let pressure = parseFloat(data.pressure) + parseInt(store.state.run.pressureCom) store.state.sensor.pressure = pressure } // 门状态上报处理 if (data.action == 'LeftDoor' && data.status == 'closed') { cmd.LeftDoor(false) uni.showToast({ title: '左门已关闭', icon: 'none' }) this.addLog('左门触点触发', '左门已关闭') } if (data.action == 'RightDoor' && data.status == 'closed') { cmd.RightDoor(false) uni.showToast({ title: '右门已关闭', icon: 'none' }) this.addLog('右门触点触发', '右门已关闭') } if (data.action == 'door' && data.status == 'closed') { try { cmd.LeftDoor(false) cmd.RightDoor(false) uni.showToast({ title: '两门已关闭', icon: 'none' }) } catch (error) { this.addLog('门关闭错误', error.message || error) } } }, handle232HexData(hex) { let data = cmd.parse232dData(hex) if (data === {}) return if (data.hasOwnProperty('number')) { uni.$emit('ic', data.number) if (store.state.relay.door == false) { // 门关闭状态 this.checkPersonCard(data.number).then(res => { if (res) { uni.$emit('showDoorSelect', data.number) } else { this.checkEndoCard(data.number).then(res => { if (res) { uni.showToast({ title: '请先开门', icon: 'none' }) } else { uni.showToast({ title: '无效卡片', icon: 'none' }) } }) } }) } else { // 门打开状态 this.checkPersonCard(data.number).then(res => { if (res) { uni.$emit('showDoorSelect', data.number) } else { this.checkEndoCard(data.number).then(res => { if (res) { uni.$emit('showActionSelect', data.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.hour >= 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 && Date.parse(res) != NaN) { store.state.time = res store.state.connect = true } }) } }, async _initDB() { await db.isTable('user').then(res => { if (!res) db.addUserTable() }) await db.isTable('log').then(res => { if (!res) db.addLogTable() }) await db.isTable('endo').then(res => { if (!res) db.addEndoTable() }) let scope = await db.isTable('scope') if (!scope) { await db.addScopeTable() for (let i = 0; i < 16; i++) { await db.addTabItem('scope', { name: '', ic: '', key: i }) } } }, /** * 使用 store.watch 替代 Vue $watch * 监听器不绑定任何组件生命周期,随服务常驻内存 */ _initWatchers() { // 消毒状态监听 store.watch( state => state.relay.disinfect, (newVal, oldVal) => { if (newVal == true) { store.state.timer.disinfect = formatDateTime() } if (newVal == false) { let timer = store.state.timer.disinfect if (timer != '' && timer != null) { let diff = getTimeDifference(timer, formatDateTime()) let local = storage.get('disinfectRun') ? 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 != '' && timer != null) { let diff = getTimeDifference(timer, formatDateTime()) let local = storage.get('windRun') ? 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, oldVal) => { if (newVal == true) beeper.play() if (newVal == false) beeper.stop() } ) // 真空泵状态 store.watch( state => state.relay.vacuum, (newVal, oldVal) => { if (newVal == true) { store.state.timer.vacuumStart = formatDateTime() } if (newVal == false) { store.state.timer.vacuumEnd = formatDateTime() } } ) }, } export default serialService