655 lines
22 KiB
JavaScript
655 lines
22 KiB
JavaScript
/**
|
||
* 全局串口通信服务
|
||
* 支持双模式: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
|