Files
endo_an_2/utils/tcp485.js
T
2026-06-16 09:22:08 +08:00

513 lines
12 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.
/**
* TCP485 - 基于 plus.android 直接调用 Java 原生 Socket
*
* 特点:
* 1. 纯 JS 调用 java.net.Socket
* 2. 发送/接收均为纯二进制,不自动追加 0A 等字符
* 3. 支持 Modbus 帧切分
* 4. 支持“最后接收时间”判定离线
* 5. 支持断线自动重连
* 6. 保留心跳机制,但心跳改为“接收心跳”
*/
import storage from '@/utils/storage.js'
// ==================== Java 类引用(懒加载,仅 Android ====================
var _Socket = null
var _InetSocketAddress = null
var _BAOS = null
// ==================== Modbus 帧切分工具(基于功能码帧长推断,无 CRC 依赖)====================
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) {
if (pos + 2 < buffer.length) {
var byteCount = buffer[pos + 2]
if (byteCount >= 1 && byteCount <= 125) {
frameLen = 3 + byteCount + 2
} else if (pos + 8 <= buffer.length) {
frameLen = 8
}
}
} else if (func === 0x06 || func === 0x10) {
frameLen = 8
} else if (func === 0x01 || func === 0x02) {
if (pos + 2 < buffer.length) {
var bc = buffer[pos + 2]
if (bc >= 1) {
frameLen = 3 + bc + 2
}
}
} else if (func === 0x05 || func === 0x0F) {
frameLen = 8
}
if (frameLen > 0 && pos + frameLen <= buffer.length) {
frames.push(buffer.slice(pos, pos + frameLen))
pos += frameLen
} else {
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.maxReconnectDelay = 30000
this._reconnectAttempt = 0
this._manualClosed = false
// “接收心跳”
this.heartbeatTimer = null
this.heartbeatCheckInterval = 8000
this.noDataTimeout = 18000 // 18秒没有收到任何数据,就认为离线(ms)
this.recvGraceTimeout = 5000 // 刚连接后的宽限期,避免设备启动慢就误判 (ms)
// 连接信息
this.host = ''
this.port = 0
// 最近接收时间
this.lastRecvTime = 0
// Promise 控制
this._connectResolve = null
this._connectReject = null
// Java 原生对象
this._socket = null
this._outputStream = null
this._inputStream = null
// 轮询相关
this._pollTimer = null
// 日志回调(供上层持久化)
this.onLogCallback = 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._manualClosed = false
self.isConnecting = true
self._connectResolve = resolve
self._connectReject = reject
console.log('[TCP485] 连接 ' + host + ':' + port)
self._openSocket(host, port)
})
}
close() {
console.log('[TCP485] 关闭')
this._manualClosed = true
this._stopHeartbeat()
this._stopPolling()
this._clearReconnectTimer()
this._safeCloseAll()
this.isOpen = false
this.isConnecting = false
this._connectResolve = null
this._connectReject = null
this.lastRecvTime = 0
this._reconnectAttempt = 0
}
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
}
onLog(callback) {
this.onLogCallback = callback || null
}
_writeLog(action) {
console.log('[TCP485]', action)
if (this.onLogCallback) {
try { this.onLogCallback('TCP485', action) } catch (e) {}
}
}
// ==================== 工具方法 ====================
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
}
_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)
}
// ==================== 内部实现 ====================
_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
}
}
_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()
this._writeLog('连接成功')
this.isOpen = true
this.isConnecting = false
// 连接成功后,初始化“最后收到数据时间”
// 这里不直接置 0,避免刚连上就被误判离线
this.lastRecvTime = Date.now()
this._stopPolling()
this._stopHeartbeat()
this._startPolling()
this._startHeartbeat()
this._reconnectAttempt = 0
this._onConnectDone(true)
} catch (e) {
console.error('[TCP485] 连接异常:', String(e))
this._safeCloseAll()
this.isOpen = false
this.isConnecting = false
this._onConnectDone(false, String(e))
}
}
_send(hexStr) {
try {
if (!hexStr || hexStr.length % 2 !== 0) {
throw new Error('hex字符串长度必须为偶数')
}
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)
// 发送失败大概率就是连接已经不可用了
this._handleDisconnect('发送失败')
return false
}
}
_startPolling() {
var self = this
if (self._pollTimer) clearInterval(self._pollTimer)
var recvBuffer = []
var lastByteTime = 0
// 非标准帧的帧间超时
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) {
var readCount = 0
for (var i = 0; i < avail && i < 256; i++) {
var b = plus.android.invoke(is, 'read')
if (b === -1) {
self._handleDisconnect('read返回-1')
return
}
if (b >= 0) {
recvBuffer.push(b & 0xFF)
readCount++
lastByteTime = Date.now()
self._touchRecvTime() // 最底层收到信息后,更新时间戳
}
}
// 先尝试精确切帧(已知 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
}
} else if (recvBuffer.length > 0) {
// 无新数据,但缓冲区里还有残留,按超时兜底切
var idleMs = Date.now() - lastByteTime
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)
self._handleDisconnect(String(e))
}
}, 10)
}
// 保留“心跳”,但心跳改成“接收心跳”
// 规则:
// 1. 只要持续收到数据,就认为链路存活
// 2. 一段时间没有收到任何数据,就认为离线,主动断开并重连
// 3. 这个逻辑不依赖设备回复心跳包,适合“只上报不应答”的485场景
_startHeartbeat() {
this._stopHeartbeat()
var self = this
this.heartbeatTimer = setInterval(function() {
if (!self.isOpen) return
try {
// 仅作为底层活性辅助检查,不能单独作为“是否在线”的判断
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))
}
}, this.heartbeatCheckInterval)
}
_stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
_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() {
// 保留这个方法名,兼容外部可能还会调用
if (this._manualClosed) return
this._reconnectAttempt++
this._scheduleReconnect()
}
}
export default new TCP485()