Files
endo_an_2/service/serialService.js
T
2026-06-14 11:33:33 +08:00

655 lines
22 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.
/**
* 全局串口通信服务
* 支持双模式:RS485串口直连 / TCP网口转485(塔石LAN869
*/
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'
import tcp485 from '@/utils/tcp485.js'
const serialService = {
RS485: null,
RS232: null,
tcp485: null,
timer: null,
taskTimer: null,
heartbeatTimer: null,
last485DataTime: null,
reconnectCount: 0,
maxReconnect: 5,
keepRunning: false,
get mode485() {
return storage.get('rs485Mode') || 'serial'
},
// ========== 公开入口 ==========
init() {
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()
this._initTime()
this._initDB()
try {
this._startRS485()
this.startRS232()
} catch (e) {
console.error('[serialService] 初始化通信失败:', e)
uni.showToast({ title: '初始化通信失败', icon: 'error' })
}
this._initWatchers()
wakeLock.acquire()
this.keepRunning = true
console.log('[serialService] 已启动')
},
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('串口通信', '服务已停止')
},
async restart() {
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 {
await this._startRS485()
this.startRS232()
this.keepRunning = true
wakeLock.acquire()
this.addLog('485通信', '服务已重启')
} catch (e) {
uni.showToast({ title: '重启通信失败', icon: 'error' })
}
},
// ========== 串口启停 ==========
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)
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()
} else {
this.stopHeartbeatIfNeeded()
uni.showToast({ title: '485打开失败', icon: 'error' })
throw new Error('[重连] RS485 打开失败')
}
},
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)
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() {
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()
}
}
},
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通信', '心跳检测已停止')
},
check485Heartbeat() {
if (!this.last485DataTime) {
console.log('[心跳] 等待首次数据...')
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.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++
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 }
// 停止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() }
cmd.clearQueue()
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)
this.last485DataTime = null
await this._startRS485()
await delay(500)
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(() => { plus.runtime.restart() }, 3000)
}
}
},
// ========== 数据处理 ==========
handle485HexData(hex) {
this.last485DataTime = Date.now()
let d = cmd.parse485Data(hex)
if (d.hasOwnProperty('temp')) {
store.state.sensor.temp = d.temp
}
if (d.hasOwnProperty('humi')) {
store.state.sensor.humi = d.humi
}
if (d.hasOwnProperty('pressure')) {
let pressure = parseFloat(d.pressure) + parseInt(store.state.run.pressureCom || 0)
store.state.sensor.pressure = pressure
}
if (d.action === 'LeftDoor' && d.status === 'closed') {
cmd.LeftDoor(false)
uni.showToast({ title: '左门已关闭', icon: 'none' })
this.addLog('左门触点触发', '左门已关闭')
}
if (d.action === 'RightDoor' && d.status === 'closed') {
cmd.RightDoor(false)
uni.showToast({ title: '右门已关闭', icon: 'none' })
this.addLog('右门触点触发', '右门已关闭')
}
if (d.action === 'door' && d.status === 'closed') {
try {
cmd.LeftDoor(false)
cmd.RightDoor(false)
uni.showToast({ title: '两门已关闭', icon: 'none' })
} catch (error) {
this.addLog('门关闭错误', error.message || error)
}
}
},
handle232HexData(hex) {
let d = cmd.parse232dData(hex)
if (!d || Object.keys(d).length === 0) return
if (d.hasOwnProperty('number')) {
uni.$emit('ic', d.number)
if (store.state.relay.door === false) {
// 门关闭状态
this.checkPersonCard(d.number).then(res => {
if (res) {
uni.$emit('showDoorSelect', d.number)
} else {
this.checkEndoCard(d.number).then(res => {
if (res) {
uni.showToast({ title: '请先开门', icon: 'none' })
} else {
uni.showToast({ title: '无效卡片', icon: 'none' })
}
})
}
})
} else {
// 门打开状态
this.checkPersonCard(d.number).then(res => {
if (res) {
uni.$emit('showDoorSelect', d.number)
} else {
this.checkEndoCard(d.number).then(res => {
if (res) {
uni.$emit('showActionSelect', d.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.hours >= 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 && !isNaN(Date.parse(res))) {
store.state.time = res
store.state.connect = true
}
})
}
},
async _initDB() {
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 })
}
}
},
_initWatchers() {
store.watch(
state => state.relay.disinfect,
(newVal) => {
if (newVal === true) store.state.timer.disinfect = formatDateTime()
if (newVal === false) {
let timer = store.state.timer.disinfect
if (timer) {
let diff = getTimeDifference(timer, formatDateTime())
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) {
let timer = store.state.timer.wind
if (timer) {
let diff = getTimeDifference(timer, formatDateTime())
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) {
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) => {
if (newVal === true) beeper.play()
if (newVal === false) beeper.stop()
}
)
store.watch(
state => state.relay.vacuum,
(newVal) => {
if (newVal === true) store.state.timer.vacuumStart = formatDateTime()
if (newVal === false) store.state.timer.vacuumEnd = formatDateTime()
}
)
},
}
export default serialService