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 5176376..a3594ea 100644 Binary files a/uni_modules/android-serialport/encrypt and b/uni_modules/android-serialport/encrypt differ 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 2a3b133..22cba8d 100644 Binary files a/uni_modules/android-serialport/utssdk/app-android/index.uts and b/uni_modules/android-serialport/utssdk/app-android/index.uts differ diff --git a/uni_modules/android-serialport/utssdk/app-android/libs/serialport-release.aar b/uni_modules/android-serialport/utssdk/app-android/libs/serialport-release.aar index 2b973c7..4c50519 100644 Binary files a/uni_modules/android-serialport/utssdk/app-android/libs/serialport-release.aar and b/uni_modules/android-serialport/utssdk/app-android/libs/serialport-release.aar differ 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 + }, +}