Files
endo_an_2/utils/tcp485.js
T
2026-06-14 11:33:33 +08:00

402 lines
11 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
*
* 无需任何原生插件/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()