/** * 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()