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