diff --git a/manifest.json b/manifest.json
index a971765..90814f6 100644
--- a/manifest.json
+++ b/manifest.json
@@ -2,8 +2,8 @@
"name" : "endoscope",
"appid" : "__UNI__5A0A7D6",
"description" : "",
- "versionName" : "1.2.6",
- "versionCode" : 121,
+ "versionName" : "1.2.7",
+ "versionCode" : 123,
"transformPx" : false,
/* 5+App特有相关 */
"app-plus" : {
@@ -91,8 +91,7 @@
}
}
}
- },
- "nativePlugins" : {}
+ }
},
/* 快应用特有相关 */
"quickapp" : {},
diff --git a/pages/modbus/modbus.vue b/pages/modbus/modbus.vue
index 9ecfda9..496f76a 100644
--- a/pages/modbus/modbus.vue
+++ b/pages/modbus/modbus.vue
@@ -1,57 +1,134 @@
+
-
+
-
-
-
-
-
+
+
+
+
+
-
- 连接
-
-
- 断开
-
-
- 保存
-
-
-
-
-
-
-
-
-
-
-
+
+ 保存模式
-
-
- 发送
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 连接
+
+
+ 断开
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 发送
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存参数
+
+
+
+
+ TCP连接
+
+
+ TCP断开
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 发送
+
+
+
+
+
+
+
+
+
@@ -59,8 +136,8 @@
@@ -75,16 +152,17 @@
连接
-
+
断开
保存
-
+
+
@@ -97,74 +175,81 @@
发送
-
+
+
-
+
+
+
开风机
-
+
开照明
-
+
开真空
-
+
开紫外
-
+
开等离子
-
+
-
关风机
-
+
关照明
-
+
关真空
-
+
关紫外
-
+
关等离子
-
+
+
-
+
+
+
温湿度
-
+
压差
-
+
+
-
-
+
+
+
-
+
{{ item }}
@@ -173,28 +258,27 @@
清空
-
+
-
+
-
@@ -393,11 +519,11 @@ export default {
.page{
color: #fff;
text-align: left;
-}
+ }
:deep(.uni-select){
min-height: 40px;
color: rgba(0, 0, 0, .85);
-}
+ }
.uni-input{
height: 40px;
border: 1px solid #d9d9d9;
@@ -406,27 +532,27 @@ export default {
text-align: left;
padding: 0 6px;
white-space: 6px;
-}
+ }
:deep(.uni-section){
background-color: transparent;
-}
+ }
.page :deep(.uni-section .uni-section-header__content){
line-height: 20px;
font-weight: 800;
text-align: right;
-}
+ }
:deep(.uni-section .uni-section-header){
padding: 10px;
-}
+ }
.ant-btn:hover,
.ant-btn:focus,
.ant-btn:active{
color: rgba(0, 0, 0, 0.85);
-}
+ }
.receive-item{
padding-left: 10px;
height: 400px;
overflow-y: auto;
-}
+ }
diff --git a/pages/setting.vue b/pages/setting.vue
index c6d7f0e..3401deb 100644
--- a/pages/setting.vue
+++ b/pages/setting.vue
@@ -40,6 +40,7 @@ import PageHeader from '@/components/PageHeader.vue'
import PageFooter from '@/components/PageFooter.vue'
import Notice from '@/components/Notice.vue';
import storage from '@/utils/storage.js';
+import * as db from '@/db/sqlite.js'
import * as Api from '@/api/index.js'
export default {
components: {
diff --git a/service/serialService.js b/service/serialService.js
index ade2116..bac4d23 100644
--- a/service/serialService.js
+++ b/service/serialService.js
@@ -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()
}
)
},
diff --git a/utils/cmd.js b/utils/cmd.js
index 17866b4..973bcd1 100644
--- a/utils/cmd.js
+++ b/utils/cmd.js
@@ -15,6 +15,8 @@
// 卡片放入和离开 起始位20, 所有卡片通过同一通道读取,通过数据库比对判断类型
// let RS232 = getApp().globalData.RS232
import store from '@/store/index.js'
+import storage from '@/utils/storage.js'
+import tcp485 from '@/utils/tcp485.js'
// ========== 统一消息队列 START ==========
// RS485 和 RS232 共用同一队列,通过 type 字段区分串口实例
@@ -30,6 +32,19 @@ const SEND_DELAY = 150 // 发送间隔150ms
const MAX_QUEUE_SIZE = 5 // 队列最大长度
const SEND_TIMEOUT = 1000 // 统一超时1秒
+/**
+ * 获取当前485通信实例(根据模式动态选择)
+ * @returns {object} RS485实例或TCP485实例
+ */
+const get485Instance = () => {
+ const mode = storage.get('rs485Mode') || 'serial'
+ if (mode === 'tcp') {
+ return tcp485
+ } else {
+ return getApp().globalData.RS485
+ }
+}
+
/**
* 将任务按优先级插入队列
* 规则:
@@ -106,7 +121,7 @@ const sendWithTimeout = (instance, cmd) => {
/**
* 处理队列中的下一个任务
- * 根据 task.type 自动获取对应串口实例(RS485 或 RS232)
+ * 根据 task.type 自动获取对应串口实例(RS485/RS232/TCP485)
*/
const processQueue = async () => {
if (isSending || queue.length === 0) {
@@ -116,8 +131,17 @@ const processQueue = async () => {
const task = queue.shift()
try {
- // 根据 type 获取对应的串口实例
- let instance = getApp().globalData[task.type]
+ // 根据 type 获取对应的通信实例
+ let instance = null
+
+ if (task.type === 'RS485') {
+ // 动态选择485通信实例(串口或TCP)
+ instance = get485Instance()
+ } else if (task.type === 'RS232') {
+ // RS232始终使用串口
+ instance = getApp().globalData.RS232
+ }
+
if (!instance) {
throw new Error(`${task.type} not initialized`)
}
diff --git a/utils/tcp485.js b/utils/tcp485.js
new file mode 100644
index 0000000..20a2d27
--- /dev/null
+++ b/utils/tcp485.js
@@ -0,0 +1,401 @@
+/**
+ * TCP485 - 基于 plus.android 直接调用 Java 原生 Socket
+ *
+ * 无需任何原生插件/UTS插件,纯 JS 调用 java.net.Socket
+ * 发送/接收均为纯二进制,不会自动添加 0A 等多余字符
+ *
+ * 发送: hexStr → ByteArrayOutputStream → toByteArray() → write(byte[]) 一次性发出完整帧
+ * 接收: available() + 逐字节 read() → 缓冲区累积 → 100ms超时判定帧结束
+ */
+
+import storage from '@/utils/storage.js'
+
+// ==================== Java 类引用(懒加载,仅Android) ====================
+
+var _Socket = null // java.net.Socket
+var _InetSocketAddress = null // java.net.InetSocketAddress
+var _BAOS = null // java.io.ByteArrayOutputStream
+
+// ==================== Modbus 帧切分工具(基于功能码帧长推断,无CRC依赖)====================
+
+/**
+ * 从缓冲区中提取所有完整的 Modbus 帧
+ *
+ * 切帧策略(不依赖CRC校验):
+ * 1. 从位置0开始解析帧头: 地址 + 功能码
+ * 2. 根据功能码 + 字节计数字段 推算整帧长度
+ * 3. 数据足够则直接切出,继续处理剩余数据
+ * 4. 无法推断帧长 → 跳过1字节继续试探
+ *
+ * 支持的功能码:
+ * 0x03/0x04: 读寄存器响应 → addr(1)+func(1)+byteCount(1)+data(N)+tail(2) = 5+N
+ * 0x06/0x10: 写寄存器响应 → 固定8字节
+ * 0x01/0x02: 读线圈/离散输入 → addr(1)+func(1)+byteCount(1)+data(N)+tail(2) = 5+N
+ * 0x05/0x0F: 写线圈响应 → 固定8字节
+ *
+ * @param {number[]} buffer - 接收缓冲区
+ * @returns {{ frames: number[][], remaining: number[] }}
+ */
+function extractModbusFrames(buffer) {
+ var frames = []
+ var pos = 0
+
+ while (pos + 4 <= buffer.length) {
+ var func = buffer[pos + 1]
+ var frameLen = -1
+
+ // 根据功能码推算帧长度
+ if (func === 0x03 || func === 0x04) {
+ // 读保持/输入寄存器:
+ // 响应帧: addr+func+byteCount(N)+data(N*2)+校验(2) = 5+N
+ // 请求帧: addr+func+起始地址(2)+寄存器数(2)+CRC = 固定8字节
+ if (pos + 2 < buffer.length) {
+ var byteCount = buffer[pos + 2]
+ if (byteCount >= 1 && byteCount <= 125) {
+ // 有效 byteCount → 响应帧
+ frameLen = 3 + byteCount + 2 // 5 + N
+ } else if (pos + 8 <= buffer.length) {
+ // byteCount无效 → 按请求帧固定8字节处理
+ frameLen = 8
+ }
+ }
+ } else if (func === 0x06 || func === 0x10) {
+ // 写单/多寄存器响应/异常: 固定8字节
+ frameLen = 8
+ } else if (func === 0x01 || func === 0x02) {
+ // 读线圈/离散输入响应: addr+func+byteCount+data+校验
+ if (pos + 2 < buffer.length) {
+ var bc = buffer[pos + 2]
+ if (bc >= 1) {
+ frameLen = 3 + bc + 2
+ }
+ }
+ } else if (func === 0x05 || func === 0x0F) {
+ // 写单个/多个线圈响应: 固定8字节
+ frameLen = 8
+ }
+
+ // 能确定帧长且数据够用 → 直接切帧
+ if (frameLen > 0 && pos + frameLen <= buffer.length) {
+ frames.push(buffer.slice(pos, pos + frameLen))
+ pos += frameLen
+ } else {
+ // 无法推断 或 数据不够 → 跳过1字节继续试探
+ pos++
+ }
+ }
+
+ return { frames: frames, remaining: buffer.slice(pos) }
+}
+
+// ==================== TCP485 类 ====================
+
+class TCP485 {
+
+ // ---- 构造 & 属性 ----
+
+ constructor() {
+ this.isOpen = false
+ this.isConnecting = false
+ this.readCallback = null
+ this.reconnectTimer = null
+ this.reconnectDelay = 3000
+ this.heartbeatTimer = null
+ this.host = ''
+ this.port = 0
+
+ // Promise 控制
+ this._connectResolve = null
+ this._connectReject = null
+
+ // Java 原生对象
+ this._socket = null
+ this._outputStream = null
+ this._inputStream = null
+
+ // 轮询相关
+ this._pollTimer = null
+ }
+
+ // ---- 公共接口 ----
+
+ setHost(host, port) {
+ this.host = host
+ this.port = parseInt(port, 10)
+ }
+
+ open() {
+ var self = this
+ return new Promise(function(resolve, reject) {
+ if (self.isOpen) { resolve(true); return }
+ if (self.isConnecting) { reject(new Error('连接中')); return }
+
+ var cfg = storage.get('rs485Tcp') || {}
+ var host = self.host || cfg.host
+ var port = self.port || cfg.port
+ if (!host || !port) {
+ uni.showToast({ title: '请配置TCP', icon: 'none' })
+ reject(new Error('缺配置'))
+ return
+ }
+
+ self.isConnecting = true
+ self._connectResolve = resolve
+ self._connectReject = reject
+ console.log('[TCP485] 连接 ' + host + ':' + port)
+
+ self._openSocket(host, port)
+ })
+ }
+
+ close() {
+ console.log('[TCP485] 关闭')
+ this._stopHeartbeat()
+ if (this.reconnectTimer) {
+ clearTimeout(this.reconnectTimer)
+ this.reconnectTimer = null
+ }
+ try {
+ if (this._outputStream) this._outputStream.close()
+ if (this._inputStream) this._inputStream.close()
+ if (this._socket) this._socket.close()
+ } catch(e) {}
+ this._socket = null
+ this._outputStream = null
+ this._inputStream = null
+ if (this._pollTimer) {
+ clearInterval(this._pollTimer)
+ this._pollTimer = null
+ }
+ this.isOpen = false
+ this.isConnecting = false
+ this._connectResolve = null
+ this._connectReject = null
+ }
+
+ sendDataString(hexStr) {
+ if (!this.isOpen || !this._outputStream) {
+ console.log('[TCP485] 未连接')
+ return false
+ }
+ return this._send(hexStr)
+ }
+
+ onStartAutoReadData(cb) {
+ this.readCallback = cb
+ }
+
+ stopReadPortData() {
+ this.readCallback = null
+ }
+
+ // ---- 工具方法 ----
+
+ byte2HexString(b) {
+ if (!b) return ''
+ var h = ''
+ for (var i = 0; i < b.length; i++) {
+ h += ('0' + (b[i] & 0xFF).toString(16)).slice(-2).toUpperCase()
+ }
+ return h
+ }
+
+ // ==================== 内部实现 ====================
+
+ // Java 类懒加载
+ _initJavaClasses() {
+ if (_Socket) return true
+ try {
+ _Socket = plus.android.importClass("java.net.Socket")
+ _InetSocketAddress = plus.android.importClass("java.net.InetSocketAddress")
+ _BAOS = plus.android.importClass("java.io.ByteArrayOutputStream")
+ return true
+ } catch(e) {
+ console.error('[TCP485] Java类加载失败:', e)
+ return false
+ }
+ }
+
+ // 建立 TCP 连接
+ _openSocket(host, port) {
+ var self = this
+ if (!this._initJavaClasses()) {
+ self._onConnectDone(false, '仅支持Android平台')
+ return
+ }
+
+ try {
+ this._socket = new _Socket()
+ var addr = new _InetSocketAddress(host, parseInt(port, 10))
+ this._socket.connect(addr, 5000)
+ this._socket.setTcpNoDelay(true)
+
+ this._outputStream = this._socket.getOutputStream()
+ this._inputStream = this._socket.getInputStream()
+
+ console.log('[TCP485] 连接成功')
+ this.isOpen = true
+
+ this._startPolling()
+ this._startHeartbeat()
+
+ this._onConnectDone(true)
+ } catch(e) {
+ console.error('[TCP485] 连接异常:', String(e))
+ this._onConnectDone(false, String(e))
+ }
+ }
+
+ // 发送:hex字符串 → ByteArrayOutputStream组装 → 原生byte[] → 一次性write
+ _send(hexStr) {
+ try {
+ var len = hexStr.length / 2
+ var baos = new _BAOS()
+ for (var i = 0; i < len; i++) {
+ baos.write(parseInt(hexStr.substr(i * 2, 2), 16) & 0xFF)
+ }
+ var javaBytes = baos.toByteArray()
+ this._outputStream.write(javaBytes)
+ this._outputStream.flush()
+
+ console.log('[TCP485] 发送: ' + hexStr + ' (' + len + 'B)')
+ return true
+ } catch(e) {
+ console.error('[TCP485] send err:', e)
+ return false
+ }
+ }
+
+ // 接收轮询:available() + 逐字节read() → 缓冲区累积 → 双模式切帧
+ //
+ // 双模式策略:
+ // 1) 已知Modbus功能码(0x01~0x10等) → 精确帧长推断,立即切帧,零延迟
+ // 2) 未知/自定义协议数据 → 20ms短超时作为帧间隔,超时后原样输出给上层
+ // 3) 真正的噪声碎片(<4字节且超时) → 丢弃并告警
+ _startPolling() {
+ var self = this
+ if (self._pollTimer) clearInterval(self._pollTimer)
+
+ var recvBuffer = []
+ var lastRecvTime = 0
+
+ // 非标准帧的帧间超时(ms),比原来100ms短很多,避免粘包
+ var UNKNOWN_FRAME_TIMEOUT = 20
+ // 噪声阈值:小于此字节数且超时的视为干扰丢弃
+ var NOISE_THRESHOLD = 4
+
+ self._pollTimer = setInterval(function () {
+ if (!self.isOpen || !self._inputStream) return
+ try {
+ var is = self._inputStream
+ var avail = plus.android.invoke(is, 'available')
+
+ if (avail > 0) {
+ // 有数据 → 读入缓冲区
+ for (var i = 0; i < avail && i < 256; i++) {
+ var b = plus.android.invoke(is, 'read')
+ if (b >= 0) {
+ recvBuffer.push(b & 0xFF)
+ lastRecvTime = Date.now()
+ }
+ }
+
+ // ★ 先尝试精确切帧(已知Modbus格式)
+ var result = extractModbusFrames(recvBuffer)
+ if (result.frames.length > 0) {
+ for (var f = 0; f < result.frames.length; f++) {
+ var frame = result.frames[f]
+ var hexStr = self.byte2HexString(frame)
+ console.log('[TCP485] 收到: ' + hexStr + ' (' + frame.length + 'B)')
+ if (self.readCallback) self.readCallback(hexStr)
+ }
+ recvBuffer = result.remaining
+ }
+ // remaining 可能包含无法识别的非标准帧数据,走下面超时兜底
+ } else if (recvBuffer.length > 0) {
+ // 无新数据 → 检查是否超时
+ var idleMs = Date.now() - lastRecvTime
+ if (idleMs > UNKNOWN_FRAME_TIMEOUT) {
+ if (recvBuffer.length >= NOISE_THRESHOLD) {
+ // 足够长 → 当作非标准/自定义协议帧,原样输出给上层
+ var hexStr = self.byte2HexString(recvBuffer)
+ console.log('[TCP485] 收到(非标准): ' + hexStr + ' (' + recvBuffer.length + 'B)')
+ if (self.readCallback) self.readCallback(hexStr)
+ } else {
+ // 太短 → 视为噪声碎片丢弃
+ console.warn('[TCP485] 丢弃噪声: ' + self.byte2HexString(recvBuffer)
+ + ' (' + recvBuffer.length + 'B)')
+ }
+ recvBuffer = []
+ }
+ }
+ } catch(e) {
+ console.warn('[TCP485] 读异常:', e)
+ }
+ }, 10)
+ }
+
+ // 心跳:30s检查连接是否存活
+ _startHeartbeat() {
+ this._stopHeartbeat()
+ var s = this
+ this.heartbeatTimer = setInterval(function () {
+ if (s.isOpen && s._socket && s._outputStream) {
+ try {
+ plus.android.invoke(s._inputStream, 'available')
+ } catch(e) {
+ s._handleDisconnect()
+ }
+ }
+ }, 30000)
+ }
+
+ // ---- 回调 & 断线处理 ----
+
+ _onConnectDone(success, msg) {
+ if (!this._connectResolve) return
+ this.isConnecting = false
+ if (success) {
+ this._connectResolve(true)
+ } else {
+ console.error('[TCP485] 连接失败:', msg)
+ this._connectReject(new Error(msg || '连接失败'))
+ }
+ this._connectResolve = null
+ this._connectReject = null
+ }
+
+ _handleDisconnect() {
+ if (!this.isOpen) return
+ console.log('[TCP485] 断开')
+ this.isOpen = false
+ this.isConnecting = false
+ this._stopHeartbeat()
+ if (this._pollTimer) {
+ clearInterval(this._pollTimer)
+ this._pollTimer = null
+ }
+ uni.showToast({ title: 'TCP断开', icon: 'none' })
+ this._reconnect()
+ }
+
+ _stopHeartbeat() {
+ if (this.heartbeatTimer) {
+ clearInterval(this.heartbeatTimer)
+ this.heartbeatTimer = null
+ }
+ }
+
+ _reconnect() {
+ if (this.reconnectTimer) return
+ var s = this
+ var d = this.reconnectDelay
+ console.log('[TCP485] ' + d + 'ms后重连')
+ this.reconnectTimer = setTimeout(function() {
+ s.reconnectTimer = null
+ if (!s.isOpen && !s.isConnecting) s.open().catch(function() {})
+ }, d)
+ }
+}
+
+export default new TCP485()