This commit is contained in:
2026-06-14 11:33:33 +08:00
parent f5e47f37b3
commit 1a40b0848d
6 changed files with 938 additions and 346 deletions
+220 -179
View File
@@ -1,7 +1,6 @@
/**
* 全局串口通信服务
* 从 start.vue 提取,脱离 Vue 组件生命周期
* 解决 Android 10 后台 Native→JS Bridge 回调被阻塞导致的数据中断问题
* 支持双模式:RS485串口直连 / TCP网口转485(塔石LAN869
*/
import * as db from '@/db/sqlite.js'
@@ -12,28 +11,35 @@ import * as Api from '@/api/index.js'
import beeper from '@/utils/beeper.js'
import store from '@/store/index.js'
import wakeLock from '@/utils/wakeLock.js'
import tcp485 from '@/utils/tcp485.js'
const serialService = {
RS485: null,
RS232: null,
timer: null, // 485轮询定时器 (T2)
taskTimer: null, // 业务任务定时器 (T3)
heartbeatTimer: null, // 心跳检测定时器 (T1)
tcp485: null,
timer: null,
taskTimer: null,
heartbeatTimer: null,
last485DataTime: null,
reconnectCount: 0,
maxReconnect: 5,
keepRunning: false,
get mode485() {
return storage.get('rs485Mode') || 'serial'
},
// ========== 公开入口 ==========
/**
* 初始化串口服务(App.onLaunch 调用)
* 获取串口实例 → 初始化存储 → 打开双串口 → 启动轮询/心跳/Watcher/保活
*/
init() {
this.RS485 = getApp().globalData.RS485
if (this.mode485 === 'tcp') {
this.tcp485 = tcp485
console.log('[serialService] TCP485模式')
} else {
this.RS485 = getApp().globalData.RS485
console.log('[serialService] RS485串口模式')
}
this.RS232 = getApp().globalData.RS232
this._initStorage()
@@ -41,27 +47,19 @@ const serialService = {
this._initDB()
try {
this.startRS485()
this._startRS485()
this.startRS232()
} catch (e) {
console.error('[serialService] 初始化串口失败:', e)
uni.showToast({ title: '初始化串口失败', icon: 'error' })
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()
@@ -71,34 +69,41 @@ const serialService = {
if (this.taskTimer) { clearInterval(this.taskTimer); this.taskTimer = null }
cmd.clearQueue()
wakeLock.release()
this.addLog('串口通信', '服务已停止shutdown')
this.addLog('串口通信', '服务已停止')
},
/**
* 从 modbus 返回后重启
*/
async restart() {
if (!this.RS485 || !this.RS232) {
this.RS485 = getApp().globalData.RS485
this.RS232 = getApp().globalData.RS232
if (this.mode485 === 'tcp') {
this.tcp485 = tcp485
} else {
if (!this.RS485) this.RS485 = getApp().globalData.RS485
}
if (!this.RS232) this.RS232 = getApp().globalData.RS232
// 重启前清理残留队列,避免旧任务使用过期串口引用
cmd.clearQueue()
try {
this.startRS485()
await this._startRS485()
this.startRS232()
this.keepRunning = true
wakeLock.acquire()
this.addLog('485通信', '服务已重启restart')
this.addLog('485通信', '服务已重启')
} catch (e) {
uni.showToast({ title: '重启串口失败', icon: 'error' })
uni.showToast({ title: '重启通信失败', icon: 'error' })
}
},
// ========== 串口启停 ==========
startRS485() {
async _startRS485() {
if (this.mode485 === 'tcp') {
await this._startRS485_TCP()
} else {
this._startRS485_Serial()
}
},
_startRS485_Serial() {
let rs485Conf = storage.get('rs485') || { path: '/dev/ttyUSB2', baudrate: 9600 }
this.RS485.setPath(rs485Conf.path)
this.RS485.setBaudrate(rs485Conf.baudrate)
@@ -113,16 +118,8 @@ const serialService = {
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' })
@@ -130,7 +127,43 @@ const serialService = {
}
},
async startRS232() {
async _startRS485_TCP() {
let rs485Tcp = storage.get('rs485Tcp') || {}
const host = rs485Tcp.host
const port = rs485Tcp.port
if (!host || !port) {
uni.showToast({ title: '请先配置TCP参数', icon: 'none' })
throw new Error('[TCP485] 缺少TCP配置')
}
this.tcp485.setHost(host, port)
try {
const state = await this.tcp485.open()
if (state) {
uni.showToast({ title: 'TCP已连接', icon: 'none' })
this.tcp485.onStartAutoReadData((hexStr) => {
try {
this.handle485HexData(hexStr)
} catch (e) {
this.addLog('TCP485回调异常', e.message || e)
}
})
clearInterval(this.timer)
this.readData()
console.log('[TCP485] 连接成功', host + ':' + port)
} else {
throw new Error('[TCP485] 连接失败')
}
} catch (error) {
this.stopHeartbeatIfNeeded()
uni.showToast({ title: 'TCP连接失败', icon: 'error' })
throw new Error('[重连] TCP485 连接失败: ' + (error.message || error))
}
},
startRS232() {
let rs232Conf = storage.get('rs232') || { path: '/dev/ttyUSB1', baudrate: 115200 }
this.RS232.setPath(rs232Conf.path)
this.RS232.setBaudrate(rs232Conf.baudrate)
@@ -153,11 +186,18 @@ const serialService = {
},
stopRS485() {
// this.stopHeartbeatIfNeeded()
this.addLog('485通信', '串口关闭')
if (this.RS485) {
this.RS485.stopReadPortData()
this.RS485.close()
if (this.mode485 === 'tcp') {
this.addLog('485通信', 'TCP连接关闭')
if (this.tcp485) {
this.tcp485.stopReadPortData()
this.tcp485.close()
}
} else {
this.addLog('485通信', '串口关闭')
if (this.RS485) {
this.RS485.stopReadPortData()
this.RS485.close()
}
}
},
@@ -168,7 +208,8 @@ const serialService = {
}
},
// ========== 数据轮询 & 心跳 ==========
// ========== 数据轮询 ==========
readData() {
this.timer = setInterval(async () => {
await cmd.getTemp()
@@ -178,10 +219,7 @@ const serialService = {
},
startHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null }
this.addLog('485通信', '心跳检测已启动')
this.heartbeatTimer = setInterval(() => {
this.check485Heartbeat()
@@ -195,10 +233,9 @@ const serialService = {
this.addLog('485通信', '心跳检测已停止')
},
// 检测485心跳
check485Heartbeat() {
if (!this.last485DataTime) {
console.log('[心跳] 等待首次485数据...')
console.log('[心跳] 等待首次数据...')
return
}
const now = Date.now()
@@ -207,105 +244,132 @@ const serialService = {
if (now - this.last485DataTime > timeout) {
if (this.reconnectCount >= this.maxReconnect) {
this.addLog('485通信', '重连' + this.maxReconnect + '次失败,请检查硬件')
uni.showToast({
title: '485通信中断,请检查硬件连接',
icon: 'none',
duration: 3000
})
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()
uni.showToast({ title: '485通信中断,正在恢复...', icon: 'none', duration: 2000 })
this.resetRS485Only()
}
},
// ========== 统一重连 ==========
// ========== 485隔离重连(不影响RS232 ==========
async resetRS485Only() {
try {
this.reconnectCount++
// 只清485相关的定时器(不清taskTimer!RS232业务继续运行)
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null }
if (this.timer) { clearInterval(this.timer); this.timer = null }
// 只停485通信(完全不动RS232
if (this.mode485 === 'tcp') {
if (this.tcp485) { this.tcp485.stopReadPortData(); this.tcp485.close() }
} else {
if (this.RS485) { this.RS485.stopReadPortData(); this.RS485.close() }
}
cmd.clearQueue()
const backoff = Math.min(1000 * Math.pow(2, this.reconnectCount - 1), 30000)
const waitTime = Math.max(backoff, 2500)
console.log('[485重连] 第' + this.reconnectCount + '次, 等待' + waitTime + 'ms (RS232不受影响)')
await delay(waitTime)
this.last485DataTime = null
await this._startRS485()
this.addLog('485通信', '485重连完成(第' + this.reconnectCount + '次)[RS232未受影响]')
} catch (error) {
this.addLog('485通信错误', error.message || '485重置失败')
await delay(5000)
if (this.reconnectCount < this.maxReconnect) {
this.resetRS485Only()
} else {
this.addLog('串口通信', '485重连' + this.maxReconnect + '次均失败,请检查485线路')
uni.showToast({ title: '485通信异常', icon: 'none', duration: 3000 })
this.reconnectCount = 0
}
}
},
// ========== 统一重连(全局故障用) ==========
async resetSerialPorts() {
try {
this.reconnectCount++
// 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() }
// 停止485通信
if (this.mode485 === 'tcp') {
if (this.tcp485) { this.tcp485.stopReadPortData(); this.tcp485.close() }
} else {
if (this.RS485) { this.RS485.stopReadPortData(); this.RS485.close() }
}
// 停止RS232
if (this.RS232) { this.RS232.stopReadPortData(); this.RS232.close() }
// 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...`)
console.log('[重连] 第' + this.reconnectCount + '次, 等待' + waitTime + 'ms...')
await delay(waitTime)
// Step 5: 重置时间戳
this.last485DataTime = null
// Step 6: 依次重启(失败时抛出异常触发重试)
if (!this.startRS485()) {
throw new Error('[重连] RS485 启动失败')
}
await this._startRS485()
await delay(500)
if (!this.startRS232()) {
throw new Error('[重连] RS232 启动失败')
}
this.addLog('串口通信', `双串口重连完成(第${this.reconnectCount}次)`)
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}次均失败,即将自动重启应用`)
this.addLog('串口通信', '重连' + this.maxReconnect + '次均失败,即将自动重启应用')
uni.showToast({ title: '通信异常,3秒后重启', icon: 'none', duration: 3000 })
setTimeout(() => {
plus.runtime.restart()
}, 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
let d = cmd.parse485Data(hex)
if (d.hasOwnProperty('temp')) {
store.state.sensor.temp = d.temp
}
if (data.hasOwnProperty('humi')) {
store.state.sensor.humi = data.humi
if (d.hasOwnProperty('humi')) {
store.state.sensor.humi = d.humi
}
if (data.hasOwnProperty('pressure')) {
let pressure = parseFloat(data.pressure) + parseInt(store.state.run.pressureCom)
if (d.hasOwnProperty('pressure')) {
let pressure = parseFloat(d.pressure) + parseInt(store.state.run.pressureCom || 0)
store.state.sensor.pressure = pressure
}
// 门状态上报处理
if (data.action == 'LeftDoor' && data.status == 'closed') {
if (d.action === 'LeftDoor' && d.status === 'closed') {
cmd.LeftDoor(false)
uni.showToast({ title: '左门已关闭', icon: 'none' })
this.addLog('左门触点触发', '左门已关闭')
}
if (data.action == 'RightDoor' && data.status == 'closed') {
if (d.action === 'RightDoor' && d.status === 'closed') {
cmd.RightDoor(false)
uni.showToast({ title: '右门已关闭', icon: 'none' })
this.addLog('右门触点触发', '右门已关闭')
}
if (data.action == 'door' && data.status == 'closed') {
if (d.action === 'door' && d.status === 'closed') {
try {
cmd.LeftDoor(false)
cmd.RightDoor(false)
@@ -317,19 +381,19 @@ const serialService = {
},
handle232HexData(hex) {
let data = cmd.parse232dData(hex)
if (data === {}) return
let d = cmd.parse232dData(hex)
if (!d || Object.keys(d).length === 0) return
if (data.hasOwnProperty('number')) {
uni.$emit('ic', data.number)
if (d.hasOwnProperty('number')) {
uni.$emit('ic', d.number)
if (store.state.relay.door == false) {
if (store.state.relay.door === false) {
// 门关闭状态
this.checkPersonCard(data.number).then(res => {
this.checkPersonCard(d.number).then(res => {
if (res) {
uni.$emit('showDoorSelect', data.number)
uni.$emit('showDoorSelect', d.number)
} else {
this.checkEndoCard(data.number).then(res => {
this.checkEndoCard(d.number).then(res => {
if (res) {
uni.showToast({ title: '请先开门', icon: 'none' })
} else {
@@ -340,13 +404,13 @@ const serialService = {
})
} else {
// 门打开状态
this.checkPersonCard(data.number).then(res => {
this.checkPersonCard(d.number).then(res => {
if (res) {
uni.$emit('showDoorSelect', data.number)
uni.$emit('showDoorSelect', d.number)
} else {
this.checkEndoCard(data.number).then(res => {
this.checkEndoCard(d.number).then(res => {
if (res) {
uni.$emit('showActionSelect', data.number)
uni.$emit('showActionSelect', d.number)
} else {
uni.showToast({ title: '无效卡片', icon: 'none' })
}
@@ -357,7 +421,8 @@ const serialService = {
}
},
// ========== 业务任务定时器 ==========
// ========== 业务任务 ==========
startAutoTask() {
this.taskTimer = setInterval(() => {
this.checkDoorOpenTimer()
@@ -377,25 +442,25 @@ const serialService = {
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) {
if (compareTimes(start, time) === 1 && compareTimes(time, end) === 1) {
isStart = true
break
}
}
}
if (store.state.relay.disinfect == false && isStart == true) {
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) {
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 != '') {
if (start !== '') {
let startM = getTimeDifference(start, formatDateTime())
let runTime = store.state.run.vacuumRunTime
if (startM.minutes >= runTime) {
@@ -404,10 +469,10 @@ const serialService = {
}
}
let end = store.state.timer.vacuumEnd
if (end != '') {
if (end !== '') {
let endH = getTimeDifference(end, formatDateTime())
let perHour = store.state.run.vacuumPerHour
if (endH.hour >= perHour) {
if (endH.hours >= perHour) {
store.state.timer.vacuumEnd = ''
cmd.Vacuum(true)
await delay(100)
@@ -427,16 +492,17 @@ const serialService = {
checkDoorOpenTimer() {
let openTime = store.state.timer.door
if (openTime != '' && store.state.clean == false) {
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)
@@ -450,6 +516,7 @@ const serialService = {
},
// ========== 卡片验证 ==========
async checkPersonCard(ic) {
let res1 = await db.selectDataList('user', { ic: ic })
if (res1.length > 0) return true
@@ -467,33 +534,27 @@ const serialService = {
},
// ========== 日志 ==========
async addLog(name, action) {
try {
await db.addTabItem('log', { name, action })
} catch (error) {}
try { await db.addTabItem('log', { name, action }) } catch (error) {}
},
// ========== 内部初始化方法 ==========
// ========== 内部初始化 ==========
_initStorage() {
let runStorage = storage.get('run')
if (runStorage) {
store.commit('SET_RUN', { ...runStorage })
}
if (runStorage) store.commit('SET_RUN', { ...runStorage })
let baseStorage = storage.get('base')
if (baseStorage) {
store.commit('SET_BASE', { ...baseStorage })
}
if (baseStorage) store.commit('SET_BASE', { ...baseStorage })
let config = storage.get('config')
if (config) {
store.state.apiUrl = 'http://' + config.ip + ':' + config.port
}
if (config) store.state.apiUrl = 'http://' + config.ip + ':' + config.port
},
_initTime() {
store.state.time = formatDateTime()
if (store.state.apiUrl != '') {
if (store.state.apiUrl !== '') {
Api.time().then(res => {
if (res && Date.parse(res) != NaN) {
if (res && !isNaN(Date.parse(res))) {
store.state.time = res
store.state.connect = true
}
@@ -502,11 +563,10 @@ const serialService = {
},
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) {
if (!await db.isTable('user')) await db.addUserTable()
if (!await db.isTable('log')) await db.addLogTable()
if (!await db.isTable('endo')) await db.addEndoTable()
if (!await db.isTable('scope')) {
await db.addScopeTable()
for (let i = 0; i < 16; i++) {
await db.addTabItem('scope', { name: '', ic: '', key: i })
@@ -514,72 +574,59 @@ const serialService = {
}
},
/**
* 使用 store.watch 替代 Vue $watch
* 监听器不绑定任何组件生命周期,随服务常驻内存
*/
_initWatchers() {
// 消毒状态监听
store.watch(
state => state.relay.disinfect,
(newVal, oldVal) => {
if (newVal == true) {
store.state.timer.disinfect = formatDateTime()
}
if (newVal == false) {
(newVal) => {
if (newVal === true) store.state.timer.disinfect = formatDateTime()
if (newVal === false) {
let timer = store.state.timer.disinfect
if (timer != '' && timer != null) {
if (timer) {
let diff = getTimeDifference(timer, formatDateTime())
let local = storage.get('disinfectRun') ? storage.get('disinfectRun') : 0
let local = storage.get('disinfectRun') || 0
storage.set('disinfectRun', diff.minutes + local)
}
}
}
)
// 风机状态监听
store.watch(
state => state.relay.wind,
(newVal, oldVal) => {
if (newVal == true && oldVal == false) {
store.state.timer.wind = formatDateTime()
}
if (newVal == false) {
if (newVal === true && oldVal === false) store.state.timer.wind = formatDateTime()
if (newVal === false) {
let timer = store.state.timer.wind
if (timer != '' && timer != null) {
if (timer) {
let diff = getTimeDifference(timer, formatDateTime())
let local = storage.get('windRun') ? storage.get('windRun') : 0
let local = storage.get('windRun') || 0
storage.set('windRun', diff.minutes + local)
}
}
}
)
// 湿度→风机
store.watch(
state => state.sensor.humi,
(newVal) => {
if (newVal > store.state.run.humi && store.state.relay.wind == false) {
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) {
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) {
if (newVal === true) {
this.openDoorEvent()
store.state.timer.door = formatDateTime()
}
if (newVal == false) {
if (newVal === false) {
this.closeDoorEvent()
this.addLog('门', '手动关门')
store.state.timer.door = ''
@@ -587,24 +634,18 @@ const serialService = {
}
}
)
// 开门报警
store.watch(
state => state.timer.doorAlert,
(newVal, oldVal) => {
if (newVal == true) beeper.play()
if (newVal == false) beeper.stop()
(newVal) => {
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()
}
(newVal) => {
if (newVal === true) store.state.timer.vacuumStart = formatDateTime()
if (newVal === false) store.state.timer.vacuumEnd = formatDateTime()
}
)
},