0616
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"appid" : "__UNI__5A0A7D6",
|
"appid" : "__UNI__5A0A7D6",
|
||||||
"description" : "",
|
"description" : "",
|
||||||
"versionName" : "1.2.7",
|
"versionName" : "1.2.7",
|
||||||
"versionCode" : 123,
|
"versionCode" : 125,
|
||||||
"transformPx" : false,
|
"transformPx" : false,
|
||||||
/* 5+App特有相关 */
|
/* 5+App特有相关 */
|
||||||
"app-plus" : {
|
"app-plus" : {
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ const serialService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.tcp485.setHost(host, port)
|
this.tcp485.setHost(host, port)
|
||||||
|
this.tcp485.onLog((name, action) => {
|
||||||
|
this.addLog(name, action)
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const state = await this.tcp485.open()
|
const state = await this.tcp485.open()
|
||||||
|
|||||||
+253
-141
@@ -1,41 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* TCP485 - 基于 plus.android 直接调用 Java 原生 Socket
|
* TCP485 - 基于 plus.android 直接调用 Java 原生 Socket
|
||||||
*
|
*
|
||||||
* 无需任何原生插件/UTS插件,纯 JS 调用 java.net.Socket
|
* 特点:
|
||||||
* 发送/接收均为纯二进制,不会自动添加 0A 等多余字符
|
* 1. 纯 JS 调用 java.net.Socket
|
||||||
*
|
* 2. 发送/接收均为纯二进制,不自动追加 0A 等字符
|
||||||
* 发送: hexStr → ByteArrayOutputStream → toByteArray() → write(byte[]) 一次性发出完整帧
|
* 3. 支持 Modbus 帧切分
|
||||||
* 接收: available() + 逐字节 read() → 缓冲区累积 → 100ms超时判定帧结束
|
* 4. 支持“最后接收时间”判定离线
|
||||||
|
* 5. 支持断线自动重连
|
||||||
|
* 6. 保留心跳机制,但心跳改为“接收心跳”
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import storage from '@/utils/storage.js'
|
import storage from '@/utils/storage.js'
|
||||||
|
|
||||||
// ==================== Java 类引用(懒加载,仅Android) ====================
|
// ==================== Java 类引用(懒加载,仅 Android) ====================
|
||||||
|
|
||||||
var _Socket = null // java.net.Socket
|
var _Socket = null
|
||||||
var _InetSocketAddress = null // java.net.InetSocketAddress
|
var _InetSocketAddress = null
|
||||||
var _BAOS = null // java.io.ByteArrayOutputStream
|
var _BAOS = null
|
||||||
|
|
||||||
// ==================== Modbus 帧切分工具(基于功能码帧长推断,无CRC依赖)====================
|
// ==================== 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) {
|
function extractModbusFrames(buffer) {
|
||||||
var frames = []
|
var frames = []
|
||||||
var pos = 0
|
var pos = 0
|
||||||
@@ -44,26 +28,18 @@ function extractModbusFrames(buffer) {
|
|||||||
var func = buffer[pos + 1]
|
var func = buffer[pos + 1]
|
||||||
var frameLen = -1
|
var frameLen = -1
|
||||||
|
|
||||||
// 根据功能码推算帧长度
|
|
||||||
if (func === 0x03 || func === 0x04) {
|
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) {
|
if (pos + 2 < buffer.length) {
|
||||||
var byteCount = buffer[pos + 2]
|
var byteCount = buffer[pos + 2]
|
||||||
if (byteCount >= 1 && byteCount <= 125) {
|
if (byteCount >= 1 && byteCount <= 125) {
|
||||||
// 有效 byteCount → 响应帧
|
frameLen = 3 + byteCount + 2
|
||||||
frameLen = 3 + byteCount + 2 // 5 + N
|
|
||||||
} else if (pos + 8 <= buffer.length) {
|
} else if (pos + 8 <= buffer.length) {
|
||||||
// byteCount无效 → 按请求帧固定8字节处理
|
|
||||||
frameLen = 8
|
frameLen = 8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (func === 0x06 || func === 0x10) {
|
} else if (func === 0x06 || func === 0x10) {
|
||||||
// 写单/多寄存器响应/异常: 固定8字节
|
|
||||||
frameLen = 8
|
frameLen = 8
|
||||||
} else if (func === 0x01 || func === 0x02) {
|
} else if (func === 0x01 || func === 0x02) {
|
||||||
// 读线圈/离散输入响应: addr+func+byteCount+data+校验
|
|
||||||
if (pos + 2 < buffer.length) {
|
if (pos + 2 < buffer.length) {
|
||||||
var bc = buffer[pos + 2]
|
var bc = buffer[pos + 2]
|
||||||
if (bc >= 1) {
|
if (bc >= 1) {
|
||||||
@@ -71,16 +47,13 @@ function extractModbusFrames(buffer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (func === 0x05 || func === 0x0F) {
|
} else if (func === 0x05 || func === 0x0F) {
|
||||||
// 写单个/多个线圈响应: 固定8字节
|
|
||||||
frameLen = 8
|
frameLen = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
// 能确定帧长且数据够用 → 直接切帧
|
|
||||||
if (frameLen > 0 && pos + frameLen <= buffer.length) {
|
if (frameLen > 0 && pos + frameLen <= buffer.length) {
|
||||||
frames.push(buffer.slice(pos, pos + frameLen))
|
frames.push(buffer.slice(pos, pos + frameLen))
|
||||||
pos += frameLen
|
pos += frameLen
|
||||||
} else {
|
} else {
|
||||||
// 无法推断 或 数据不够 → 跳过1字节继续试探
|
|
||||||
pos++
|
pos++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,19 +64,34 @@ function extractModbusFrames(buffer) {
|
|||||||
// ==================== TCP485 类 ====================
|
// ==================== TCP485 类 ====================
|
||||||
|
|
||||||
class TCP485 {
|
class TCP485 {
|
||||||
|
|
||||||
// ---- 构造 & 属性 ----
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// 连接状态
|
||||||
this.isOpen = false
|
this.isOpen = false
|
||||||
this.isConnecting = false
|
this.isConnecting = false
|
||||||
|
|
||||||
|
// 回调
|
||||||
this.readCallback = null
|
this.readCallback = null
|
||||||
|
|
||||||
|
// 重连
|
||||||
this.reconnectTimer = null
|
this.reconnectTimer = null
|
||||||
this.reconnectDelay = 3000
|
this.reconnectDelay = 3000
|
||||||
|
this.maxReconnectDelay = 30000
|
||||||
|
this._reconnectAttempt = 0
|
||||||
|
this._manualClosed = false
|
||||||
|
|
||||||
|
// “接收心跳”
|
||||||
this.heartbeatTimer = null
|
this.heartbeatTimer = null
|
||||||
|
this.heartbeatCheckInterval = 8000
|
||||||
|
this.noDataTimeout = 18000 // 18秒没有收到任何数据,就认为离线(ms)
|
||||||
|
this.recvGraceTimeout = 5000 // 刚连接后的宽限期,避免设备启动慢就误判 (ms)
|
||||||
|
|
||||||
|
// 连接信息
|
||||||
this.host = ''
|
this.host = ''
|
||||||
this.port = 0
|
this.port = 0
|
||||||
|
|
||||||
|
// 最近接收时间
|
||||||
|
this.lastRecvTime = 0
|
||||||
|
|
||||||
// Promise 控制
|
// Promise 控制
|
||||||
this._connectResolve = null
|
this._connectResolve = null
|
||||||
this._connectReject = null
|
this._connectReject = null
|
||||||
@@ -115,9 +103,12 @@ class TCP485 {
|
|||||||
|
|
||||||
// 轮询相关
|
// 轮询相关
|
||||||
this._pollTimer = null
|
this._pollTimer = null
|
||||||
|
|
||||||
|
// 日志回调(供上层持久化)
|
||||||
|
this.onLogCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 公共接口 ----
|
// ==================== 公共接口 ====================
|
||||||
|
|
||||||
setHost(host, port) {
|
setHost(host, port) {
|
||||||
this.host = host
|
this.host = host
|
||||||
@@ -127,50 +118,51 @@ class TCP485 {
|
|||||||
open() {
|
open() {
|
||||||
var self = this
|
var self = this
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
if (self.isOpen) { resolve(true); return }
|
if (self.isOpen) {
|
||||||
if (self.isConnecting) { reject(new Error('连接中')); return }
|
resolve(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (self.isConnecting) {
|
||||||
|
reject(new Error('连接中'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var cfg = storage.get('rs485Tcp') || {}
|
var cfg = storage.get('rs485Tcp') || {}
|
||||||
var host = self.host || cfg.host
|
var host = self.host || cfg.host
|
||||||
var port = self.port || cfg.port
|
var port = self.port || cfg.port
|
||||||
|
|
||||||
if (!host || !port) {
|
if (!host || !port) {
|
||||||
uni.showToast({ title: '请配置TCP', icon: 'none' })
|
uni.showToast({ title: '请配置TCP', icon: 'none' })
|
||||||
reject(new Error('缺配置'))
|
reject(new Error('缺配置'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self._manualClosed = false
|
||||||
self.isConnecting = true
|
self.isConnecting = true
|
||||||
self._connectResolve = resolve
|
self._connectResolve = resolve
|
||||||
self._connectReject = reject
|
self._connectReject = reject
|
||||||
console.log('[TCP485] 连接 ' + host + ':' + port)
|
|
||||||
|
|
||||||
|
console.log('[TCP485] 连接 ' + host + ':' + port)
|
||||||
self._openSocket(host, port)
|
self._openSocket(host, port)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
console.log('[TCP485] 关闭')
|
console.log('[TCP485] 关闭')
|
||||||
|
this._manualClosed = true
|
||||||
|
|
||||||
this._stopHeartbeat()
|
this._stopHeartbeat()
|
||||||
if (this.reconnectTimer) {
|
this._stopPolling()
|
||||||
clearTimeout(this.reconnectTimer)
|
this._clearReconnectTimer()
|
||||||
this.reconnectTimer = null
|
|
||||||
}
|
this._safeCloseAll()
|
||||||
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.isOpen = false
|
||||||
this.isConnecting = false
|
this.isConnecting = false
|
||||||
this._connectResolve = null
|
this._connectResolve = null
|
||||||
this._connectReject = null
|
this._connectReject = null
|
||||||
|
this.lastRecvTime = 0
|
||||||
|
this._reconnectAttempt = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
sendDataString(hexStr) {
|
sendDataString(hexStr) {
|
||||||
@@ -189,7 +181,18 @@ class TCP485 {
|
|||||||
this.readCallback = null
|
this.readCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 工具方法 ----
|
onLog(callback) {
|
||||||
|
this.onLogCallback = callback || null
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeLog(action) {
|
||||||
|
console.log('[TCP485]', action)
|
||||||
|
if (this.onLogCallback) {
|
||||||
|
try { this.onLogCallback('TCP485', action) } catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工具方法 ====================
|
||||||
|
|
||||||
byte2HexString(b) {
|
byte2HexString(b) {
|
||||||
if (!b) return ''
|
if (!b) return ''
|
||||||
@@ -200,25 +203,84 @@ class TCP485 {
|
|||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_touchRecvTime() {
|
||||||
|
this.lastRecvTime = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
_safeCloseAll() {
|
||||||
|
try {
|
||||||
|
if (this._outputStream) this._outputStream.close()
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
if (this._inputStream) this._inputStream.close()
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
if (this._socket) this._socket.close()
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
this._socket = null
|
||||||
|
this._outputStream = null
|
||||||
|
this._inputStream = null
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopPolling() {
|
||||||
|
if (this._pollTimer) {
|
||||||
|
clearInterval(this._pollTimer)
|
||||||
|
this._pollTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearReconnectTimer() {
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer)
|
||||||
|
this.reconnectTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_scheduleReconnect() {
|
||||||
|
if (this._manualClosed) return
|
||||||
|
if (this.reconnectTimer) return
|
||||||
|
|
||||||
|
var self = this
|
||||||
|
// var delay = Math.min(this.reconnectDelay * Math.pow(2, this._reconnectAttempt), this.maxReconnectDelay)
|
||||||
|
var delay = this.reconnectDelay
|
||||||
|
|
||||||
|
this._writeLog(delay + 'ms后重连,第' + (this._reconnectAttempt + 1) + '次')
|
||||||
|
this.reconnectTimer = setTimeout(function() {
|
||||||
|
self.reconnectTimer = null
|
||||||
|
if (self._manualClosed) return
|
||||||
|
if (self.isOpen || self.isConnecting) return
|
||||||
|
|
||||||
|
self.open()
|
||||||
|
.then(function() {
|
||||||
|
self._reconnectAttempt = 0
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.warn('[TCP485] 重连失败:', err && err.message ? err.message : err)
|
||||||
|
self._reconnectAttempt++
|
||||||
|
self._scheduleReconnect()
|
||||||
|
})
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 内部实现 ====================
|
// ==================== 内部实现 ====================
|
||||||
|
|
||||||
// Java 类懒加载
|
|
||||||
_initJavaClasses() {
|
_initJavaClasses() {
|
||||||
if (_Socket) return true
|
if (_Socket) return true
|
||||||
try {
|
try {
|
||||||
_Socket = plus.android.importClass("java.net.Socket")
|
_Socket = plus.android.importClass('java.net.Socket')
|
||||||
_InetSocketAddress = plus.android.importClass("java.net.InetSocketAddress")
|
_InetSocketAddress = plus.android.importClass('java.net.InetSocketAddress')
|
||||||
_BAOS = plus.android.importClass("java.io.ByteArrayOutputStream")
|
_BAOS = plus.android.importClass('java.io.ByteArrayOutputStream')
|
||||||
return true
|
return true
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
console.error('[TCP485] Java类加载失败:', e)
|
console.error('[TCP485] Java类加载失败:', e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 建立 TCP 连接
|
|
||||||
_openSocket(host, port) {
|
_openSocket(host, port) {
|
||||||
var self = this
|
var self = this
|
||||||
|
|
||||||
if (!this._initJavaClasses()) {
|
if (!this._initJavaClasses()) {
|
||||||
self._onConnectDone(false, '仅支持Android平台')
|
self._onConnectDone(false, '仅支持Android平台')
|
||||||
return
|
return
|
||||||
@@ -233,74 +295,93 @@ class TCP485 {
|
|||||||
this._outputStream = this._socket.getOutputStream()
|
this._outputStream = this._socket.getOutputStream()
|
||||||
this._inputStream = this._socket.getInputStream()
|
this._inputStream = this._socket.getInputStream()
|
||||||
|
|
||||||
console.log('[TCP485] 连接成功')
|
this._writeLog('连接成功')
|
||||||
this.isOpen = true
|
this.isOpen = true
|
||||||
|
this.isConnecting = false
|
||||||
|
|
||||||
|
// 连接成功后,初始化“最后收到数据时间”
|
||||||
|
// 这里不直接置 0,避免刚连上就被误判离线
|
||||||
|
this.lastRecvTime = Date.now()
|
||||||
|
|
||||||
|
this._stopPolling()
|
||||||
|
this._stopHeartbeat()
|
||||||
this._startPolling()
|
this._startPolling()
|
||||||
this._startHeartbeat()
|
this._startHeartbeat()
|
||||||
|
|
||||||
|
this._reconnectAttempt = 0
|
||||||
this._onConnectDone(true)
|
this._onConnectDone(true)
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
console.error('[TCP485] 连接异常:', String(e))
|
console.error('[TCP485] 连接异常:', String(e))
|
||||||
|
this._safeCloseAll()
|
||||||
|
this.isOpen = false
|
||||||
|
this.isConnecting = false
|
||||||
this._onConnectDone(false, String(e))
|
this._onConnectDone(false, String(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送:hex字符串 → ByteArrayOutputStream组装 → 原生byte[] → 一次性write
|
|
||||||
_send(hexStr) {
|
_send(hexStr) {
|
||||||
try {
|
try {
|
||||||
|
if (!hexStr || hexStr.length % 2 !== 0) {
|
||||||
|
throw new Error('hex字符串长度必须为偶数')
|
||||||
|
}
|
||||||
|
|
||||||
var len = hexStr.length / 2
|
var len = hexStr.length / 2
|
||||||
var baos = new _BAOS()
|
var baos = new _BAOS()
|
||||||
|
|
||||||
for (var i = 0; i < len; i++) {
|
for (var i = 0; i < len; i++) {
|
||||||
baos.write(parseInt(hexStr.substr(i * 2, 2), 16) & 0xFF)
|
baos.write(parseInt(hexStr.substr(i * 2, 2), 16) & 0xFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
var javaBytes = baos.toByteArray()
|
var javaBytes = baos.toByteArray()
|
||||||
this._outputStream.write(javaBytes)
|
this._outputStream.write(javaBytes)
|
||||||
this._outputStream.flush()
|
this._outputStream.flush()
|
||||||
|
|
||||||
console.log('[TCP485] 发送: ' + hexStr + ' (' + len + 'B)')
|
console.log('[TCP485] 发送: ' + hexStr + ' (' + len + 'B)')
|
||||||
return true
|
return true
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
console.error('[TCP485] send err:', e)
|
console.error('[TCP485] send err:', e)
|
||||||
|
// 发送失败大概率就是连接已经不可用了
|
||||||
|
this._handleDisconnect('发送失败')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 接收轮询:available() + 逐字节read() → 缓冲区累积 → 双模式切帧
|
|
||||||
//
|
|
||||||
// 双模式策略:
|
|
||||||
// 1) 已知Modbus功能码(0x01~0x10等) → 精确帧长推断,立即切帧,零延迟
|
|
||||||
// 2) 未知/自定义协议数据 → 20ms短超时作为帧间隔,超时后原样输出给上层
|
|
||||||
// 3) 真正的噪声碎片(<4字节且超时) → 丢弃并告警
|
|
||||||
_startPolling() {
|
_startPolling() {
|
||||||
var self = this
|
var self = this
|
||||||
if (self._pollTimer) clearInterval(self._pollTimer)
|
if (self._pollTimer) clearInterval(self._pollTimer)
|
||||||
|
|
||||||
var recvBuffer = []
|
var recvBuffer = []
|
||||||
var lastRecvTime = 0
|
var lastByteTime = 0
|
||||||
|
|
||||||
// 非标准帧的帧间超时(ms),比原来100ms短很多,避免粘包
|
// 非标准帧的帧间超时
|
||||||
var UNKNOWN_FRAME_TIMEOUT = 20
|
var UNKNOWN_FRAME_TIMEOUT = 20
|
||||||
// 噪声阈值:小于此字节数且超时的视为干扰丢弃
|
|
||||||
var NOISE_THRESHOLD = 4
|
var NOISE_THRESHOLD = 4
|
||||||
|
|
||||||
self._pollTimer = setInterval(function () {
|
self._pollTimer = setInterval(function() {
|
||||||
if (!self.isOpen || !self._inputStream) return
|
if (!self.isOpen || !self._inputStream) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var is = self._inputStream
|
var is = self._inputStream
|
||||||
var avail = plus.android.invoke(is, 'available')
|
var avail = plus.android.invoke(is, 'available')
|
||||||
|
|
||||||
if (avail > 0) {
|
if (avail > 0) {
|
||||||
// 有数据 → 读入缓冲区
|
var readCount = 0
|
||||||
|
|
||||||
for (var i = 0; i < avail && i < 256; i++) {
|
for (var i = 0; i < avail && i < 256; i++) {
|
||||||
var b = plus.android.invoke(is, 'read')
|
var b = plus.android.invoke(is, 'read')
|
||||||
|
if (b === -1) {
|
||||||
|
self._handleDisconnect('read返回-1')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (b >= 0) {
|
if (b >= 0) {
|
||||||
recvBuffer.push(b & 0xFF)
|
recvBuffer.push(b & 0xFF)
|
||||||
lastRecvTime = Date.now()
|
readCount++
|
||||||
|
lastByteTime = Date.now()
|
||||||
|
self._touchRecvTime() // 最底层收到信息后,更新时间戳
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ★ 先尝试精确切帧(已知Modbus格式)
|
// 先尝试精确切帧(已知 Modbus 格式)
|
||||||
var result = extractModbusFrames(recvBuffer)
|
var result = extractModbusFrames(recvBuffer)
|
||||||
if (result.frames.length > 0) {
|
if (result.frames.length > 0) {
|
||||||
for (var f = 0; f < result.frames.length; f++) {
|
for (var f = 0; f < result.frames.length; f++) {
|
||||||
@@ -311,72 +392,65 @@ class TCP485 {
|
|||||||
}
|
}
|
||||||
recvBuffer = result.remaining
|
recvBuffer = result.remaining
|
||||||
}
|
}
|
||||||
// remaining 可能包含无法识别的非标准帧数据,走下面超时兜底
|
|
||||||
} else if (recvBuffer.length > 0) {
|
} else if (recvBuffer.length > 0) {
|
||||||
// 无新数据 → 检查是否超时
|
// 无新数据,但缓冲区里还有残留,按超时兜底切
|
||||||
var idleMs = Date.now() - lastRecvTime
|
var idleMs = Date.now() - lastByteTime
|
||||||
if (idleMs > UNKNOWN_FRAME_TIMEOUT) {
|
if (idleMs > UNKNOWN_FRAME_TIMEOUT) {
|
||||||
if (recvBuffer.length >= NOISE_THRESHOLD) {
|
if (recvBuffer.length >= NOISE_THRESHOLD) {
|
||||||
// 足够长 → 当作非标准/自定义协议帧,原样输出给上层
|
|
||||||
var hexStr = self.byte2HexString(recvBuffer)
|
var hexStr = self.byte2HexString(recvBuffer)
|
||||||
console.log('[TCP485] 收到(非标准): ' + hexStr + ' (' + recvBuffer.length + 'B)')
|
console.log('[TCP485] 收到(非标准): ' + hexStr + ' (' + recvBuffer.length + 'B)')
|
||||||
if (self.readCallback) self.readCallback(hexStr)
|
if (self.readCallback) self.readCallback(hexStr)
|
||||||
} else {
|
} else {
|
||||||
// 太短 → 视为噪声碎片丢弃
|
console.warn('[TCP485] 丢弃噪声: ' + self.byte2HexString(recvBuffer) + ' (' + recvBuffer.length + 'B)')
|
||||||
console.warn('[TCP485] 丢弃噪声: ' + self.byte2HexString(recvBuffer)
|
|
||||||
+ ' (' + recvBuffer.length + 'B)')
|
|
||||||
}
|
}
|
||||||
recvBuffer = []
|
recvBuffer = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
console.warn('[TCP485] 读异常:', e)
|
console.warn('[TCP485] 读异常:', e)
|
||||||
|
self._handleDisconnect(String(e))
|
||||||
}
|
}
|
||||||
}, 10)
|
}, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 心跳:30s检查连接是否存活
|
// 保留“心跳”,但心跳改成“接收心跳”
|
||||||
|
// 规则:
|
||||||
|
// 1. 只要持续收到数据,就认为链路存活
|
||||||
|
// 2. 一段时间没有收到任何数据,就认为离线,主动断开并重连
|
||||||
|
// 3. 这个逻辑不依赖设备回复心跳包,适合“只上报不应答”的485场景
|
||||||
_startHeartbeat() {
|
_startHeartbeat() {
|
||||||
this._stopHeartbeat()
|
this._stopHeartbeat()
|
||||||
var s = this
|
|
||||||
this.heartbeatTimer = setInterval(function () {
|
var self = this
|
||||||
if (s.isOpen && s._socket && s._outputStream) {
|
this.heartbeatTimer = setInterval(function() {
|
||||||
try {
|
if (!self.isOpen) return
|
||||||
plus.android.invoke(s._inputStream, 'available')
|
|
||||||
} catch(e) {
|
try {
|
||||||
s._handleDisconnect()
|
// 仅作为底层活性辅助检查,不能单独作为“是否在线”的判断
|
||||||
|
if (!self._inputStream || !self._socket) {
|
||||||
|
self._handleDisconnect('socket或输入流为空')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 这里不是请求式心跳,而是“接收超时心跳”
|
||||||
|
var now = Date.now()
|
||||||
|
|
||||||
|
// 连接后宽限一小段时间,避免设备刚启动时误判
|
||||||
|
if (self.lastRecvTime === 0) return
|
||||||
|
|
||||||
|
var idleMs = now - self.lastRecvTime
|
||||||
|
if (idleMs < self.recvGraceTimeout) return
|
||||||
|
|
||||||
|
if (idleMs > self.noDataTimeout) {
|
||||||
|
self._writeLog('断开: 接收超时')
|
||||||
|
self._handleDisconnect('接收超时')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[TCP485] 心跳检查异常:', e)
|
||||||
|
self._handleDisconnect(String(e))
|
||||||
}
|
}
|
||||||
}, 30000)
|
}, this.heartbeatCheckInterval)
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 回调 & 断线处理 ----
|
|
||||||
|
|
||||||
_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() {
|
_stopHeartbeat() {
|
||||||
@@ -386,15 +460,53 @@ class TCP485 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onConnectDone(success, msg) {
|
||||||
|
if (!this._connectResolve && !this._connectReject) return
|
||||||
|
|
||||||
|
this.isConnecting = false
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
this._connectResolve && this._connectResolve(true)
|
||||||
|
} else {
|
||||||
|
console.error('[TCP485] 连接失败:', msg)
|
||||||
|
this._connectReject && this._connectReject(new Error(msg || '连接失败'))
|
||||||
|
|
||||||
|
// 连接失败也进入重连
|
||||||
|
if (!this._manualClosed) {
|
||||||
|
this._reconnectAttempt++
|
||||||
|
this._scheduleReconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._connectResolve = null
|
||||||
|
this._connectReject = null
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleDisconnect(reason) {
|
||||||
|
if (!this.isOpen && !this.isConnecting) return
|
||||||
|
|
||||||
|
this._writeLog('断开: ' + (reason || ''))
|
||||||
|
|
||||||
|
this.isOpen = false
|
||||||
|
this.isConnecting = false
|
||||||
|
|
||||||
|
this._stopHeartbeat()
|
||||||
|
this._stopPolling()
|
||||||
|
this._safeCloseAll()
|
||||||
|
|
||||||
|
uni.showToast({ title: 'TCP断开', icon: 'none' })
|
||||||
|
|
||||||
|
if (!this._manualClosed) {
|
||||||
|
this._reconnectAttempt++
|
||||||
|
this._scheduleReconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_reconnect() {
|
_reconnect() {
|
||||||
if (this.reconnectTimer) return
|
// 保留这个方法名,兼容外部可能还会调用
|
||||||
var s = this
|
if (this._manualClosed) return
|
||||||
var d = this.reconnectDelay
|
this._reconnectAttempt++
|
||||||
console.log('[TCP485] ' + d + 'ms后重连')
|
this._scheduleReconnect()
|
||||||
this.reconnectTimer = setTimeout(function() {
|
|
||||||
s.reconnectTimer = null
|
|
||||||
if (!s.isOpen && !s.isConnecting) s.open().catch(function() {})
|
|
||||||
}, d)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user