From f5e47f37b330c058e4c5e7242e0e7797d7ded21e Mon Sep 17 00:00:00 2001 From: zhangzhen Date: Wed, 3 Jun 2026 15:49:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=B2=E5=8F=A3=E6=8F=92?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.vue | 3 + manifest.json | 4 +- pages/index.vue | 6 + pages/modbus/modbus.vue | 6 +- pages/start.vue | 909 ++---------------- service/serialService.js | 613 ++++++++++++ uni_modules/android-serialport/changelog.md | 12 + uni_modules/android-serialport/encrypt | Bin 144 -> 144 bytes uni_modules/android-serialport/package.json | 119 ++- uni_modules/android-serialport/readme.md | 159 ++- .../utssdk/app-android/index.uts | Bin 5536 -> 6976 bytes .../app-android/libs/serialport-release.aar | Bin 31692 -> 26497 bytes update.md | 425 -------- update2.md | 375 -------- utils/cmd.js | 291 +++--- utils/wakeLock.js | 55 ++ 16 files changed, 1106 insertions(+), 1871 deletions(-) create mode 100644 service/serialService.js delete mode 100644 update.md delete mode 100644 update2.md create mode 100644 utils/wakeLock.js diff --git a/App.vue b/App.vue index 0bd478f..996bcf0 100644 --- a/App.vue +++ b/App.vue @@ -2,6 +2,7 @@ // #ifdef APP import { SerialPortHelper } from "@/uni_modules/android-serialport"; import * as db from '@/db/sqlite.js' + import serialService from '@/service/serialService.js' // #endif export default { globalData: { @@ -14,6 +15,8 @@ this.globalData.RS485 = new SerialPortHelper(); this.globalData.RS232 = new SerialPortHelper(); db.openDb() + // 启动全局串口服务(脱离页面生命周期) + serialService.init() // #endif }, onShow: function() { diff --git a/manifest.json b/manifest.json index 560dd3c..a971765 100644 --- a/manifest.json +++ b/manifest.json @@ -2,8 +2,8 @@ "name" : "endoscope", "appid" : "__UNI__5A0A7D6", "description" : "", - "versionName" : "1.2.5", - "versionCode" : 117, + "versionName" : "1.2.6", + "versionCode" : 121, "transformPx" : false, /* 5+App特有相关 */ "app-plus" : { diff --git a/pages/index.vue b/pages/index.vue index cf0e49d..66091e7 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -198,6 +198,7 @@ import * as db from '@/db/sqlite.js' import cmd from '@/utils/cmd.js' import storage from '@/utils/storage.js' import * as Api from '@/api/index.js' +import serialService from '@/service/serialService.js' export default { components: { PageFooter, PageHeader, Notice, SlotNotice @@ -288,6 +289,11 @@ export default { }, onShow() { + // 检测串口服务是否运行(从modbus返回时需要重启) + if (serialService && !serialService.keepRunning) { + // console.log('[index] 检测到串口服务已停止,正在重启...') + serialService.restart() + } uni.$on('notice', (data) => { this.showNotice(data) }) uni.$on('showDoorSelect', (cardNumber) => { this.showDoorSelect(cardNumber) }) uni.$on('showActionSelect', (cardNumber) => { this.showActionSelect(cardNumber) }) diff --git a/pages/modbus/modbus.vue b/pages/modbus/modbus.vue index c1d7d69..9ecfda9 100644 --- a/pages/modbus/modbus.vue +++ b/pages/modbus/modbus.vue @@ -223,7 +223,7 @@ export default { onLoad() { this.RS485 = getApp().globalData.RS485 this.RS232 = getApp().globalData.RS232 - // console.log('this.RS485', this.RS485.isOpen()) + console.log('this.RS485', this.RS485.isOpen()) this.getDevice() // uni.$emit('debug', { disconnect: true }) }, @@ -292,7 +292,7 @@ export default { this.RS485.setBaudrate(this.rs485Baudrate); this.rs485State = this.RS485.open(); if(this.rs485State){ - this.RS485.startAutoReadData((res) =>{ + this.RS485.onStartAutoReadData((res) =>{ console.log(this.RS485.byte2HexString(res)) this.received.unshift(this.RS485.byte2HexString(res)) }) @@ -309,7 +309,7 @@ export default { this.RS232.setBaudrate(this.rs232Baudrate); this.rs232State = this.RS232.open(); if(this.rs232State){ - this.RS232.startAutoReadData((res) =>{ + this.RS232.onStartAutoReadData((res) =>{ this.received.unshift(this.RS232.byte2HexString(res)) }) } diff --git a/pages/start.vue b/pages/start.vue index e1e0f95..8330c4f 100644 --- a/pages/start.vue +++ b/pages/start.vue @@ -34,76 +34,55 @@ @@ -1017,7 +237,6 @@ export default { .ant-btn.ant-btn-lg{ font-size: 28px; height: 60px; - /* line-height: 50px; */ border-radius: 50px; width: 180px; } @@ -1036,4 +255,4 @@ export default { position: absolute; right: 10px; } - \ No newline at end of file + diff --git a/service/serialService.js b/service/serialService.js new file mode 100644 index 0000000..ade2116 --- /dev/null +++ b/service/serialService.js @@ -0,0 +1,613 @@ +/** + * 全局串口通信服务 + * 从 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 diff --git a/uni_modules/android-serialport/changelog.md b/uni_modules/android-serialport/changelog.md index bc8ec32..cc301e8 100644 --- a/uni_modules/android-serialport/changelog.md +++ b/uni_modules/android-serialport/changelog.md @@ -1,3 +1,15 @@ +## 1.0.16(2026-02-06) +改16k +## 1.0.15(2025-12-15) +优化 +## 1.0.13(2025-12-03) +优化 +## 1.0.12(2025-12-02) +优化 +## 1.0.11(2025-11-20) +优化 +## 1.0.10(2025-10-21) +优化 ## 1.0.9(2025-04-19) 优化 diff --git a/uni_modules/android-serialport/encrypt b/uni_modules/android-serialport/encrypt index 5176376c5b7854d23c6ffa90a42829ad51f3f3a9..a3594ea0f7fd133534b7c64526de0bd7077d509d 100644 GIT binary patch delta 119 zcmV--0EqvP0gwTZAaH-!MaSI0aF2?W*gg`Jd`E-Y|NT*?#=4}WwC=rD5jG~S>^$pE z-H&0jp0TU+2XUg$YW^bVBGVmf9rxq(1*#GJOALPFup+?D08P*H#uq~<2iLDe^oED^ ZZtI6<5uBO=YS$+f$&4rRA!gShHLB_0J|zGE delta 119 zcmV--0EqvP0gwTZAaG*N7(`Ia%G8jlTVZhCbLO1IS!Md3&l~<=e4*1!hM}-&gloUF z{T1;oCT9ZnLyD*+YM*>CBc)TWd8owqelX>~bpm5D-1@MHhl^|#=9*v}{G?*rr~j-s ZYQ8I4uM!s`usS_);{fO32QK5|%U?~SJsSW3 diff --git a/uni_modules/android-serialport/package.json b/uni_modules/android-serialport/package.json index 1cf2e56..7b5ddaf 100644 --- a/uni_modules/android-serialport/package.json +++ b/uni_modules/android-serialport/package.json @@ -1,16 +1,18 @@ { "id": "android-serialport", - "displayName": "Android平台串口通信 serialport(长期维护)", - "version": "1.0.9", + "displayName": "Android串口通信 串口开发 serialport 多串口(长期维护)", + "version": "1.0.16", "description": "android/安卓平台串口通信uts,支持串口号、波特率、数据位、校验位、停止位、流控等参数设置(长期维护)", "keywords": [ - "SerialPort串口", - "UTS", - "安卓串口,233,485" + "串口", + "SerialPort", + "串口通信,233,485" ], "repository": "", "engines": { - "HBuilderX": "^3.91" + "HBuilderX": "^3.91", + "uni-app": "^4.31", + "uni-app-x": "^3.8.11" }, "dcloudext": { "type": "uts", @@ -19,7 +21,7 @@ "price": "19.99" }, "sourcecode": { - "price": "101.00" + "price": "199.00" } }, "contact": { @@ -28,59 +30,76 @@ "declaration": { "ads": "无", "data": "插件不采集任何数据\n使用过程中有任何问题可以加qq 1530948626 联系,随时修复", - "permissions": "必须ROOT" + "permissions": "需要系统授权串口权限" }, - "npmurl": "" + "npmurl": "", + "darkmode": "√", + "i18n": "√", + "widescreen": "√" }, "uni_modules": { "dependencies": [], "encrypt": [], "platforms": { - "cloud": { - "tcb": "y", - "aliyun": "y", - "alipay": "y" - }, "client": { - "Vue": { - "vue2": "u", - "vue3": "y" - }, - "App": { - "app-android": { - "minVersion": "21" + "uni-app": { + "vue": { + "vue2": "√", + "vue3": "√" + }, + "web": { + "safari": "-", + "chrome": "-" + }, + "app": { + "vue": "√", + "nvue": "√", + "android": { + "extVersion": "", + "minVersion": "21" }, - "app-ios": "n", - "app-harmony": "u" + "ios": "-", + "harmony": "-" + }, + "mp": { + "weixin": "-", + "alipay": "-", + "toutiao": "-", + "baidu": "-", + "kuaishou": "-", + "jd": "-", + "harmony": "-", + "qq": "-", + "lark": "-", + "xhs": "-" + }, + "quickapp": { + "huawei": "-", + "union": "-" + } }, - "H5-mobile": { - "Safari": "n", - "Android Browser": "n", - "微信浏览器(Android)": "n", - "QQ浏览器(Android)": "n" - }, - "H5-pc": { - "Chrome": "n", - "IE": "n", - "Edge": "n", - "Firefox": "n", - "Safari": "n" - }, - "小程序": { - "微信": "n", - "阿里": "n", - "百度": "n", - "字节跳动": "n", - "QQ": "n", - "钉钉": "n", - "快手": "n", - "飞书": "n", - "京东": "n" - }, - "快应用": { - "华为": "n", - "联盟": "n" + "uni-app-x": { + "web": { + "safari": "-", + "chrome": "-" + }, + "app": { + "android": { + "extVersion": "", + "minVersion": "21" + }, + "ios": "-", + "harmony": "-" + }, + "mp": { + "weixin": "-" + } } + }, + "cloud": { + "tcb": "x", + "aliyun": "x", + "alipay": "x" } } } diff --git a/uni_modules/android-serialport/readme.md b/uni_modules/android-serialport/readme.md index ff1558d..c62e623 100644 --- a/uni_modules/android-serialport/readme.md +++ b/uni_modules/android-serialport/readme.md @@ -13,6 +13,7 @@ 5. 运行 -》 运行到手机或模拟器-》运行到Androidapp基座-》选择使用自定义基座运行-》选择手机-》运行 ### uni-app x项目(uvue)中调用示例:(调试需要自定义基座) + ``` import {SerialPortHelper} from "@/uni_modules/android-serialport"; @@ -29,7 +30,7 @@ helper.setBaudrate(9600); // helper.flags(0) var state= helper.open(); if(state){ - helper.startAutoReadData(function(res){ + helper.onStartAutoReadData(function(res:number[]){ // console.log(res[0]) console.log(helper.byte2HexString(res)) }) @@ -51,7 +52,10 @@ if(state){ } ``` + + ### uni-app 项目(vue nvue)中调用示例:(调试需要自定义基座) + ``` import {SerialPortHelper} from "@/uni_modules/android-serialport"; @@ -62,7 +66,7 @@ import {SerialPortHelper} from "@/uni_modules/android-serialport"; helper.setBaudrate(9600); var state= helper.open(); if(state){ - helper.startAutoReadData(function(res){ + helper.onStartAutoReadData(function(res){ console.log(helper.uint8ArrayToHexString(res)) }) var b=new Uint8Array(5); @@ -78,22 +82,56 @@ import {SerialPortHelper} from "@/uni_modules/android-serialport"; ``` ### 开发文档 + + + ### 获取设备列表 #### getAllDevice() return 串口列表 +uniappx +~~~ + var portList:string[]=helper.getAllDevice() +~~~ + +uniapp +~~~ + var portList=helper.getAllDevice() +~~~ ### 设置串口参数 setPath 设置路径 + ~~~ + helper.setPath("/dev/ttyS7"); + ~~~ #### setBaudrate 设置波特率 +~~~ +helper.setBaudrate(9600); +~~~ #### dataBits 数据位 默认8,可选值为5~8 + ~~~ + helper.dataBits(8); + + ~~~ #### parity 校验位 parity 0:无校验位(默认);1:奇校验位 + ~~~ + helper.parity(0); + + ~~~ + ### stopBits 停止位 stopBits 默认1;1:1位停止位;2:2位停止位 +~~~ + helper.stopBits(1); +~~~ ### flags 标志 flags 默认0 +~~~ + + helper.flags(0) +~~~ ### 打开串口 @@ -101,50 +139,149 @@ flags 标志 flags 默认0 #### open 需要调用才可打开 return 返回是否打开成功 + ~~~ + var open= helper.open() + ~~~ #### isOpen return 返回是否打开成功 + ~~~ + var open= helper.isOpen() + ~~~ ### 关闭串口 - close + ~~~ + var open= helper.close() + ~~~ + + ### 开启自动读取串口数据,需要先打开串口 -#### startAutoReadData - 回调读取的数据function 数据类型为ByteArray +#### onStartAutoReadData + 回调读取的数据 + uniappx + ~~~ +helper.onStartAutoReadData(function(res:number[]){ + console.log(helper.byte2HexString(res)) +}) + ~~~ + uniapp + ~~~ + helper.onStartAutoReadData(function(res){ + console.log(helper.byte2HexString(res)) + }) + ~~~ ### 关闭自动读取串口数据 #### stopReadPortData - + ~~~ +helper.stopReadPortData() + ~~~ ### 手动读取串口数据 手动读取之前需先打开串口 且不可和自动读串口数据同时调用 #### available() return 返回-1 表示无可读数据 其它为可读数据长度 +~~~ + var len= helper.available() +~~~ -#### readData +#### onReadData 参数1 需要读取数据的长度 number 参数2 开始读取位置 number 参数3 读取数据结束位置 number -参数4 回调读取的数据function 数据类型为ByteArray +参数4 回调读取的数据function 数据类型为number[] +uniappx +~~~ + helper.onReadData(10,0,10,function(b:number[]){ + + }) +~~~ + +uniapp +~~~ + helper.onReadData(10,0,10,function(b){ + + }) +~~~ + ### 发送数据 -#### sendData +#### sendData (uniappx) 参数1 需要发送的ByteArray +~~~ + var b:ByteArray=new ByteArray(5); + b[0]=0x55; + b[1]=0x00; + b[2]=0x12; + b[3]=0x00; + b[4]=0x7B; + helper.sendData(b) +~~~ -#### sendDataString +#### sendDataString() 参数1 string 类型的16进制的字符串 例如 "550120057B" +~~~ + helper.sendDataString("550120057B") +~~~ + +#### sendDataUnit8Array (uniapp) +~~~ +var b=new Uint8Array(5); + b[0]=0x55; + b[1]=0x00; + b[2]=0x12; + b[3]=0x00; + b[4]=0x7B; + helper.sendDataUnit8Array(b) +~~~ + ### 辅助转换方法 -### 将16进制字符串转换为ByteArray +### 将16进制字符串转换为ByteArray (uniappx) #### parseHexStr2Byte 参数1 为16进制字符串 +~~~ +var by = helper.parseHexStr2Byte("55 66 a1") +~~~ ### ByteArray转换成16进制字符串 #### byte2HexString 参数1 ByteArray +~~~ +var b:ByteArray=new ByteArray(5); + b[0]=0x55; + b[1]=0x00; + b[2]=0x12; + b[3]=0x00; + b[4]=0x7B; +var by = helper.byte2HexString(b) +~~~ + + +### 字符转16进制拼接字符 +### string2ByteStrWithCharset +参数1 普通文本 + +参数2 传utf-8 gbk +~~~ +var hex=helper.string2ByteStrWithCharset("测试","utf-8") +~~~ + +### 进制拼接字符转文本 +### byte2StringWithCharset +参数1 16进制拼接字符 + +参数2 传utf-8 gbk +~~~ +var text=helper.string2ByteStrWithCharset("E6B58BE8AF95","utf-8") +~~~ + + + ### ByteArray 辅助操作方法 #### 将两个ByteArray进行合并 diff --git a/uni_modules/android-serialport/utssdk/app-android/index.uts b/uni_modules/android-serialport/utssdk/app-android/index.uts index 2a3b1330be511fb0155bac88cf90b54228202a90..22cba8d79a3c9394e69f2577f6161f7f371d4f81 100644 GIT binary patch literal 6976 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 SD6k<**bX;a<>-94n{GM|w}aUL literal 5536 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 bUh`jo|HcIv*k9VHFAs#5tI5FV*SG%%)ntjo literal 31692 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
  • ?*IYVb;$Q>qsJ9KA*gI|gF> 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 diff --git a/update.md b/update.md deleted file mode 100644 index d9d4a82..0000000 --- a/update.md +++ /dev/null @@ -1,425 +0,0 @@ -# RS485 数据上报中断处理方案 - -## 一、问题描述 - -项目运行过程中,RS485 总线上报的温湿度、压差、门状态等传感器数据可能中断。当前代码中**没有**数据中断检测、超时告警和自动重连机制。需要增加心跳监听、中断检测和自动重置功能。 - ---- - -## 二、涉及文件 - -| 文件路径 | 修改说明 | -|----------|----------| -| `pages/start.vue` | 新增心跳检测、中断监听和自动重置逻辑 | - ---- - -## 三、修改内容 - -### 3.1 `data()` — 新增字段 - -在 `data()` 的 return 对象中新增四个字段: - -**修改位置**:`pages/start.vue` 第 51~59 行 - -**修改前**: -```javascript -data() { - return { - clickTimes: 0, - RS485: undefined, - RS232: undefined, - timer: null, // 定时器读取485接口数据 - taskTimer: null, - leftDoorCycleTimer: null, // 左门循环定时器 - } -}, -``` - -**修改后**: -```javascript -data() { - return { - clickTimes: 0, - RS485: undefined, - RS232: undefined, - timer: null, // 定时器读取485接口数据 - taskTimer: null, - leftDoorCycleTimer: null, // 左门循环定时器 - last485DataTime: null, // 上次收到485数据的时间戳 - heartbeatTimer: null, // 心跳检测定时器 - reconnectCount: 0, // 重连次数计数器 - maxReconnect: 3, // 最大重连次数 - } -}, -``` - ---- - -### 3.2 `handle485HexData()` — 记录数据时间戳 - -**修改位置**:`pages/start.vue` 第 393 行 - -在 `handle485HexData(hex)` 方法**开头**增加时间戳记录: - -```javascript -async handle485HexData(hex) { - // 更新最后收到数据的时间戳 - this.last485DataTime = Date.now() - - // 处理485接口上报的数据 - // 温湿度temp, humi, 压差pressure, 门状态door - let data = cmd.parse485Data(hex) - // ... 后续所有原有代码保持不变 ... -``` - ---- - -### 3.3 `initSerialport()` — 启动心跳检测 - -**修改位置**:`pages/start.vue` 第 159~170 行 - -**修改前**: -```javascript -initSerialport() { - try { - this.startRS485() - this.startRS232() - } catch (e) { - uni.showToast({ - title: '初始化串口失败', - icon: 'error' - }) - } -}, -``` - -**修改后**: -```javascript -initSerialport() { - try { - this.startRS485() - this.startRS232() - // 启动心跳检测 - this.startHeartbeat() - } catch (e) { - uni.showToast({ - title: '初始化串口失败', - icon: 'error' - }) - } -}, -``` - ---- - -### 3.4 `methods` — 新增三个方法 - -在 `methods` 对象中新增以下三个方法,建议放在 `readData()` 方法(第 386 行)**之前**: - -#### 3.4.1 `startHeartbeat()` — 启动心跳检测 - -```javascript -// 启动心跳检测定时器 -startHeartbeat() { - // 清除已有的心跳定时器 - if (this.heartbeatTimer) { - clearInterval(this.heartbeatTimer) - this.heartbeatTimer = null - } - - // 记录日志 - this.addLog('485通信', '心跳检测已启动') - - // 每10秒检查一次485数据是否正常上报 - this.heartbeatTimer = setInterval(() => { - this.check485Heartbeat() - }, 10000) -}, -``` - -#### 3.4.2 `check485Heartbeat()` — 心跳检测逻辑 - -```javascript -// 检测485数据心跳,如果超时则触发重连 -check485Heartbeat() { - // 如果还没有收到过数据,跳过检查(等待初始化完成) - if (!this.last485DataTime) { - return - } - - const now = Date.now() - const timeout = 30 * 1000 // 30秒超时阈值 - - // 判断距离上次数据是否超过30秒 - 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.resetRS485() - } -}, -``` - -#### 3.4.3 `resetRS485()` — 重置RS485监听 - -```javascript -// 重置RS485监听:停止旧连接 → 重启新连接 → 恢复心跳 -async resetRS485() { - try { - this.reconnectCount++ - - // 1. 暂停心跳检测(避免重置过程中重复触发) - if (this.heartbeatTimer) { - clearInterval(this.heartbeatTimer) - this.heartbeatTimer = null - } - - // 2. 停止并关闭当前RS485串口 - if (this.RS485) { - this.stopRS485() - // 等待一段时间确保串口完全关闭 - await delay(1500) - } - - // 3. 清除轮询定时器 - if (this.timer) { - clearInterval(this.timer) - this.timer = null - } - - // 4. 重置时间戳 - this.last485DataTime = null - - // 5. 重新启动RS485通信 - this.startRS485() - - // 6. 延迟后重新启动心跳检测(等待RS485初始化完成) - await delay(3000) - this.startHeartbeat() - - this.addLog('485通信', '监听已重置(第' + this.reconnectCount + '次)') - - } catch (error) { - this.addLog('485通信错误', error.message || '重置失败') - - // 失败后延迟重试 - await delay(5000) - if (this.reconnectCount < this.maxReconnect) { - this.resetRS485() - } - } -}, -``` - ---- - -### 3.5 `startRS485()` — 成功后重置计数器和记录日志 - -**修改位置**:`pages/start.vue` 第 318~348 行 - -在 `startRS485()` 方法的 `state` 判断块中,新增计数器重置和通信恢复日志: - -```javascript -startRS485() { - // 获取本地串口存储端口 - let rs485Conf = storage.get('rs485') - if (rs485Conf) { - // 设置串口和波特率 - } else { - rs485Conf = { - 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.startAutoReadData((res) =>{ - let hex = this.RS485.byte2HexString(res) - this.handle485HexData(hex) - }) - // 读取数据 - clearInterval(this.timer) - this.readData() - - // 【新增】串口打开成功后,重置重连计数器和时间戳 - this.reconnectCount = 0 - this.last485DataTime = Date.now() - - // 【新增】记录通信恢复日志 - this.addLog('485通信', '通信已恢复(重连成功)') - } -}, -``` - ---- - -### 3.6 `stopRS485()` — 添加日志 - -**修改位置**:`pages/start.vue` 第 377~380 行 - -**修改前**: -```javascript -stopRS485() { - this.RS485.stopReadPortData() - this.RS485.close() -}, -``` - -**修改后**: -```javascript -stopRS485() { - this.addLog('485通信', '串口关闭') - this.RS485.stopReadPortData() - this.RS485.close() -}, -``` - ---- - -### 3.7 页面销毁时清理定时器 - -**修改位置**:`pages/start.vue`,在 `onHide()` 生命周期之后、`methods` 之前添加 `onUnload`: - -```javascript -onUnload() { - // 记录日志 - this.addLog('485通信', '页面销毁,已停止所有定时器') - - // 清理心跳定时器 - if (this.heartbeatTimer) { - clearInterval(this.heartbeatTimer) - this.heartbeatTimer = null - } - // 清理485轮询定时器 - if (this.timer) { - clearInterval(this.timer) - this.timer = null - } - // 清理任务定时器 - if (this.taskTimer) { - clearInterval(this.taskTimer) - this.taskTimer = null - } - // 清理左门循环定时器 - if (this.leftDoorCycleTimer) { - clearInterval(this.leftDoorCycleTimer) - this.leftDoorCycleTimer = null - } -}, -``` - ---- - -## 四、工作流程图 - -``` -启动RS485 ──▶ startHeartbeat() ──▶ 每10秒检查 - │ - ┌───────────────────▼────────────────────┐ - │ last485DataTime 距现在超过 30 秒? │ - └───┬───────────────────────┬────────────┘ - │ 否 │ 是 - ▼ ▼ - 继续等待 检查 reconnectCount - │ - ┌───────────────┴───────────────┐ - │ < 3 │ >= 3 - ▼ ▼ - resetRS485() 弹窗告警,停止重连 - │ "请检查硬件连接" - ┌───────────┼───────────┐ - ▼ ▼ ▼ - stopRS485 重置时间戳 startRS485 - │ │ - ▼ ▼ - 关闭串口 重新打开串口监听 - │ - ▼ - reconnectCount=0 - last485DataTime重置 - │ - ▼ - startHeartbeat() -``` - ---- - -## 五、关键参数说明 - -| 参数 | 值 | 说明 | -|------|-----|------| -| 心跳检测间隔 | 10 秒 | `setInterval` 的执行间隔 | -| 数据超时阈值 | 30 秒 | 超过此时间未收到数据触发重连 | -| 最大重连次数 | 3 次 | 超过后停止自动重连,需手动干预 | -| 重置等待时间 | 1500ms | 关闭串口后等待的时间 | -| 重连初始化等待 | 3000ms | 重新打开串口后等待稳定 | - -> 这些参数可根据实际硬件响应速度调整。 - ---- - -## 六、日志记录说明 - -所有关键操作都会记录到 SQLite 数据库的 `log` 表中,方便后续排查问题。 - -### 6.1 日志表示例 - -| 时间 | name | action | -|------|------|--------| -| 14:30:00 | 485通信 | 心跳检测已启动 | -| 14:35:30 | 485通信 | 数据中断(第1次重连) | -| 14:35:35 | 485通信 | 监听已重置(第1次) | -| 14:36:00 | 485通信 | 数据中断(第2次重连) | -| 14:36:05 | 485通信 | 监听已重置(第2次) | -| 14:36:35 | 485通信 | 重连3次失败,请检查硬件 | -| 15:00:00 | 485通信 | 通信已恢复(重连成功) | -| 15:30:00 | 485通信 | 页面销毁,已停止所有定时器 | - -### 6.2 日志查看方式 - -日志存储在本地 SQLite 数据库的 `log` 表中,可以通过"sqlite调试"页面查看。 - ---- - -## 七、测试验证 - -1. **正常情况**:启动后每5秒通过 `readData` 主动查询,传感器定时上报,`last485DataTime` 持续更新,心跳检测通过 -2. **模拟中断**:断开485传感器线缆,确认30秒后触发重连提示,3次后弹窗告警 -3. **模拟恢复**:在重连过程中重新接入传感器,确认 `startRS485` 成功后将计数器归零 -4. **页面切换**:离开 `start.vue` 时确认所有定时器已清除 -5. **日志验证**:检查 SQLite 数据库中的 `log` 表,确认所有关键操作都有记录 - ---- - -## 八、注意事项 - -1. **不需要控制台输出**:所有日志都记录到数据库,方便在生产环境排查问题 -2. **重连失败不需要手动恢复**:超过最大重连次数后,只能重启应用或检查硬件 -3. **参数调整**:如果硬件响应较慢,可以适当增加超时阈值和等待时间 -4. **左门循环定时器**:这是独立的调试功能,不受485中断处理影响 diff --git a/update2.md b/update2.md deleted file mode 100644 index 568395e..0000000 --- a/update2.md +++ /dev/null @@ -1,375 +0,0 @@ -# 串口堵塞解决方案(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数据丢失,但设备控制态由硬件保持 | diff --git a/utils/cmd.js b/utils/cmd.js index d9cfab0..17866b4 100644 --- a/utils/cmd.js +++ b/utils/cmd.js @@ -16,179 +16,84 @@ // let RS232 = getApp().globalData.RS232 import store from '@/store/index.js' -// ========== RS485 优先级消息队列 START ========== +// ========== 统一消息队列 START ========== +// RS485 和 RS232 共用同一队列,通过 type 字段区分串口实例 +// 互斥锁确保同一时刻只有一个串口在发送指令,杜绝双串口并发竞态 const PRIORITY = { HIGH: 0, // 用户操作(开门/关门/控制指令) - LOW: 1, // 定时轮询(getTemp/getPressure) + LOW: 1, // 定时轮询(getTemp/getPressure) } -const rs485Queue = [] -let isRS485Sending = false -const RS485_SEND_DELAY = 200 // 每条指令间隔200ms,确保总线空闲 -const MAX_QUEUE_SIZE = 3 // 队列最大长度 +const queue = [] +let isSending = false +const SEND_DELAY = 150 // 发送间隔150ms +const MAX_QUEUE_SIZE = 5 // 队列最大长度 +const SEND_TIMEOUT = 1000 // 统一超时1秒 /** * 将任务按优先级插入队列 * 规则: - * 1. 队列最大长度为3 - * 2. 重复指令(相同cmd)不入队,直接丢弃 + * 1. 队列最大长度为5 + * 2. 重复指令(相同type+cmd)不入队,直接丢弃 * 3. 队列满时,高优先级任务挤掉低优先级任务,否则踢掉最老的(队首) */ const enqueueTask = (task) => { - // 规则1: 去重 - 已存在相同cmd的任务则丢弃 - const isDuplicate = rs485Queue.some(t => t.cmd === task.cmd) + // 规则1: 去重 - 已存在相同type+cmd的任务则丢弃 + const isDuplicate = queue.some(t => t.cmd === task.cmd && t.type === task.type) if (isDuplicate) { - // console.warn('[RS485队列] 丢弃重复任务:', task.cmd) return false } // 规则2: 队列满了,先腾位置 - if (rs485Queue.length >= MAX_QUEUE_SIZE) { + if (queue.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) { + for (let i = queue.length - 1; i >= 0; i--) { + if (queue[i].priority === PRIORITY.LOW) { lastLowIndex = i break } } if (lastLowIndex !== -1) { - // console.warn('[RS485队列] 高优挤掉低优:', rs485Queue[lastLowIndex].cmd) - rs485Queue.splice(lastLowIndex, 1) + queue.splice(lastLowIndex, 1) } else { // 没有低优先级可挤,踢掉队首最老的 - // console.warn('[RS485队列] 满,踢掉队首:', rs485Queue[0].cmd) - rs485Queue.shift() + queue.shift() } } else { // 低优先级:直接踢掉队首最老的 - // console.warn('[RS485队列] 满,踢掉队首:', rs485Queue[0].cmd) - rs485Queue.shift() + queue.shift() } } // 规则3: 按优先级插入 if (task.priority === PRIORITY.HIGH) { - let insertIndex = rs485Queue.findIndex(t => t.priority === PRIORITY.LOW) + let insertIndex = queue.findIndex(t => t.priority === PRIORITY.LOW) if (insertIndex === -1) { - rs485Queue.push(task) + queue.push(task) } else { - rs485Queue.splice(insertIndex, 0, task) + queue.splice(insertIndex, 0, task) } } else { - rs485Queue.push(task) + queue.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) => { +const sendWithTimeout = (instance, 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) + console.warn(`[统一队列] 发送超时(${SEND_TIMEOUT}ms), cmd: ${cmd}`) + reject(new Error('发送超时')) + }, 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) + const result = instance.sendDataString(cmd) if (result && typeof result.then === 'function') { result.then(res => { clearTimeout(timer); resolve(res) }) .catch(err => { clearTimeout(timer); reject(err) }) @@ -198,7 +103,73 @@ const sendRS232WithTimeout = (rs232Instance, cmd) => { } }) } -// ========== RS232 超时保护 END ========== + +/** + * 处理队列中的下一个任务 + * 根据 task.type 自动获取对应串口实例(RS485 或 RS232) + */ +const processQueue = async () => { + if (isSending || queue.length === 0) { + return + } + isSending = true + const task = queue.shift() + + try { + // 根据 type 获取对应的串口实例 + let instance = getApp().globalData[task.type] + 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 +} +// ========== 统一消息队列 END ========== export default { // 开左门指令 (通过RS485站号03) - 高优先级 @@ -207,10 +178,16 @@ export default { // 左门开门: 03 06 00 00 00 01 49 E8 // 左门关门: 03 06 00 00 00 00 88 28 let cmd = status ? '03060000000149E8' : '0306000000008828' - store.state.relay.leftDoor = status - // door状态 = leftDoor OR rightDoor (任一门打开则door为true) - store.state.relay.door = store.state.relay.leftDoor || store.state.relay.rightDoor - await enqueueRS485Send(cmd, PRIORITY.HIGH) + 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) - 高优先级 @@ -219,71 +196,65 @@ export default { // 右门开门: 03 06 00 01 00 01 18 28 // 右门关门: 03 06 00 01 00 00 D9 E8 let cmd = status ? '0306000100011828' : '030600010000D9E8' - store.state.relay.rightDoor = status - // door状态 = leftDoor OR rightDoor (任一门打开则door为true) - store.state.relay.door = store.state.relay.leftDoor || store.state.relay.rightDoor - await enqueueRS485Send(cmd, PRIORITY.HIGH) + 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 + } }, - // 开门关门控制 01 - // Door: async (status) => { - // let op = status ? '01' : '00' - // let cmd = '5AA5EE01' + op - // store.state.relay.door = status - // let RS232 = getApp().globalData.RS232 - // await RS232.sendDataString(cmd); - // }, - // 开风机指令 02 + // 开风机指令 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 sendRS232WithTimeout(RS232, cmd) + return enqueue('RS232', cmd, PRIORITY.HIGH) }, - // 开灯指令 03 + // 开灯指令 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 sendRS232WithTimeout(RS232, cmd) + return enqueue('RS232', cmd, PRIORITY.HIGH) }, - // 开真空指令 04 + // 开真空指令 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 sendRS232WithTimeout(RS232, cmd) + return enqueue('RS232', cmd, PRIORITY.HIGH) }, - // 开关消毒指令 05 + // 开关消毒指令 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 sendRS232WithTimeout(RS232, cmd) + return enqueue('RS232', cmd, PRIORITY.HIGH) }, // 获取温湿度(通过RS485站号02)- 低优先级 getTemp: () => { let cmd = '020300000002C438'; - return enqueueRS485Send(cmd, PRIORITY.LOW) + return enqueue('RS485', cmd, PRIORITY.LOW) }, // 获取压差指令(通过RS485站号01)- 低优先级 getPressure: () => { let cmd = '01030001000295CB' - return enqueueRS485Send(cmd, PRIORITY.LOW) + return enqueue('RS485', cmd, PRIORITY.LOW) }, // 导出队列管理方法 - clearRS485Queue, - getRS485QueueSize, + clearQueue, + getQueueSize, - // 重置RS485队列锁(供外部重连时调用) - resetRS485Lock() { - clearRS485Queue() // 内部已包含清队列 + 释放锁 + // 重置队列锁(供外部重连时调用) + resetLock() { + clearQueue() // 内部已包含清队列 + 释放锁 }, // 解析门卡数据,返回卡号 diff --git a/utils/wakeLock.js b/utils/wakeLock.js new file mode 100644 index 0000000..c37d5bb --- /dev/null +++ b/utils/wakeLock.js @@ -0,0 +1,55 @@ +/** + * CPU WakeLock 保活工具 + * 申请 PARTIAL_WAKE_LOCK 保持CPU调度活跃 + * 防止 Android 10 后台 Native→JS Bridge 回调被阻塞 + */ +let wakeLock = null + +export default { + /** + * 申请WakeLock(App启动/串口服务启动时调用) + */ + acquire() { + // #ifdef APP-PLUS + try { + if (wakeLock) return // 已持有 + + const main = plus.android.runtimeMainActivity() + const PowerManager = plus.android.importClass('android.os.PowerManager') + const pm = main.getSystemService('power') + + // PARTIAL_WAKE_LOCK: 仅保持CPU运行,不亮屏、不锁键盘 + wakeLock = pm.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + 'endoscope:serial_service' + ) + // 设置引用计数为false,避免多次acquire需对应次数release + plus.android.invoke(wakeLock, 'setReferenceCounted', false) + wakeLock.acquire() + + console.log('[wakeLock] PARTIAL_WAKE_LOCK 已获取') + } catch (e) { + console.error('[wakeLock] 获取失败:', e) + } + // #endif + }, + + /** + * 释放WakeLock(串口服务完全停止时调用) + */ + release() { + // #ifdef APP-PLUS + try { + if (wakeLock) { + if (wakeLock.isHeld()) { + wakeLock.release() + console.log('[wakeLock] 已释放') + } + wakeLock = null + } + } catch (e) { + console.error('[wakeLock] 释放失败:', e) + } + // #endif + }, +}