Files
endo_an_2/service/serialService.js
T
2026-06-03 15:49:48 +08:00

614 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 全局串口通信服务
* 从 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