This commit is contained in:
2026-06-14 11:33:33 +08:00
parent f5e47f37b3
commit 1a40b0848d
6 changed files with 938 additions and 346 deletions
+27 -3
View File
@@ -15,6 +15,8 @@
// 卡片放入和离开 起始位20, 所有卡片通过同一通道读取,通过数据库比对判断类型
// let RS232 = getApp().globalData.RS232
import store from '@/store/index.js'
import storage from '@/utils/storage.js'
import tcp485 from '@/utils/tcp485.js'
// ========== 统一消息队列 START ==========
// RS485 和 RS232 共用同一队列,通过 type 字段区分串口实例
@@ -30,6 +32,19 @@ const SEND_DELAY = 150 // 发送间隔150ms
const MAX_QUEUE_SIZE = 5 // 队列最大长度
const SEND_TIMEOUT = 1000 // 统一超时1秒
/**
* 获取当前485通信实例(根据模式动态选择)
* @returns {object} RS485实例或TCP485实例
*/
const get485Instance = () => {
const mode = storage.get('rs485Mode') || 'serial'
if (mode === 'tcp') {
return tcp485
} else {
return getApp().globalData.RS485
}
}
/**
* 将任务按优先级插入队列
* 规则:
@@ -106,7 +121,7 @@ const sendWithTimeout = (instance, cmd) => {
/**
* 处理队列中的下一个任务
* 根据 task.type 自动获取对应串口实例(RS485RS232
* 根据 task.type 自动获取对应串口实例(RS485/RS232/TCP485
*/
const processQueue = async () => {
if (isSending || queue.length === 0) {
@@ -116,8 +131,17 @@ const processQueue = async () => {
const task = queue.shift()
try {
// 根据 type 获取对应的串口实例
let instance = getApp().globalData[task.type]
// 根据 type 获取对应的通信实例
let instance = null
if (task.type === 'RS485') {
// 动态选择485通信实例(串口或TCP)
instance = get485Instance()
} else if (task.type === 'RS232') {
// RS232始终使用串口
instance = getApp().globalData.RS232
}
if (!instance) {
throw new Error(`${task.type} not initialized`)
}
+401
View File
@@ -0,0 +1,401 @@
/**
* 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()