更新记录
1.2.0(2026-06-14)
新增
- 帧头+长度域通用分帧:
startFramedListening第三参支持{ type: 'header-length', header, lengthOffset, lengthSize, ... }配置- 通用解决绝大多数自定义工业协议粘包问题(帧头搜索 → 长度域解析 → CRC校验 → 逐帧提取)
- 支持可选的 CRC-16/MODBUS 校验,自动丢弃噪声帧
- 支持 1/2 字节长度域(大端序),
lengthIncludesCRC控制长度语义 - 原理:通过帧头逐字节搜索定位帧起始 + 长度域计算帧总长 + while 循环连续提取多帧
- 高频上报场景下不会粘包(while 循环从 buffer 中连续剥离完整帧)
- HeaderLengthConfig 类型:
interface.uts新增完整类型定义
改进
- 代码清理:移除
tryExtractFrames()modbus-rtu 分支中的死代码检查(while 条件已保证数据充足) - 废弃标记:
startListening标注为@deprecated,引导用户迁移到startFramedListening(后者即使不传分帧也有空闲超时分帧能力) - 文档完善:readme.md 补充
header-length完整用法、分帧策略对比表、废弃 API 迁移指引
分帧策略全家桶
| 模式 | 新增/已有 | 适用场景 |
|---|---|---|
header-length ⭐ |
新增 | 自定义工业协议(帧头+长度域+CRC) |
modbus-rtu |
已有 | Modbus RTU 设备 |
fixed 固定帧长 |
已有 | 帧长固定的简单协议 |
custom 自定义函数 |
已有 | 复杂/特殊协议 |
| 纯超时 | 已有 | 低频上报/无帧格式协议 |
平台兼容性
uni-app(4.87)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | - | - | √ | √ | √ | - | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
uni-app x(4.87)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| - | - | √ | - | - | - |
其他
| 多语言 | 暗黑模式 | 宽屏模式 |
|---|---|---|
| × | × | √ |
z-serialport - 工业级串口通信插件
基于 Android RandomAccessFile + stty 实现的工业级 RS485/RS232 串口通信插件。
功能
- 支持 RS485 / RS232 串口通信
- 基于 RandomAccessFile 文件操作,稳定性高
- 支持常规波特率(1200 ~ 921600)
- 支持奇偶校验(none / odd / even)、数据位(7/8)、停止位(1/2)
- 支持 485 方向控制 GPIO
- 帧超时监听(空闲超时分帧,自动解决粘包)
- 支持 Modbus RTU / 固定帧长 / 自定义分帧器 / 帧头+长度域通用分帧
- 轮询接收模式
- 精确字节读取
- 缓冲区管理
- 多实例支持(同时连接多个串口)
- 演示页面适配横竖屏(横屏左右双栏 / 竖屏上下滚动)
兼容平台
- Android ✅(完整实现)
- uni-app (vue3) ✅
快速开始
1. 引入
import { openPort, listPorts } from '@/uni_modules/z-serialport/utssdk/app-android/index.uts'
2. 扫描设备
const ports = listPorts()
// 返回串口设备列表,如 ['/dev/ttyS0', '/dev/ttyS1', '/dev/ttyUSB0']
3. 打开串口
const port = null
openPort({
config: {
devicePath: '/dev/ttyS0', // [必填] 串口设备路径,如 /dev/ttyS0、/dev/ttyUSB0
baudRate: 9600, // [必填] 波特率:1200 ~ 921600
dataBits: 8, // [必填] 数据位:7 或 8
stopBits: 1, // [必填] 停止位:1 或 2
parity: 'none', // [必填] 校验位:'none' | 'odd' | 'even'
bufferSize: 4096, // [可选] 缓冲区大小(字节),默认 4096
readTimeout: 100, // [可选] 读取超时(毫秒),默认 0(不超时)
directionGpio: -1 // [可选] 485方向控制GPIO编号,默认 -1(不启用)
},
success: (res) => {
port = res.port // res.port: SerialPort 实例,res.devicePath: 设备路径
console.log('串口已打开:', res.devicePath)
},
fail: (err) => {
console.error('打开失败', err.errCode, err.errMsg) // err.errCode: 错误码,err.errMsg: 错误信息
}
})
4. 接收数据(帧超时监听,推荐)
// 纯空闲超时分帧(通用)
port.startFramedListening((data) => {
const hex = Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' ')
console.log('收到帧:', hex)
}, 10) // 第2参 [可选] 字节间超时 ms,默认 10
// 停止监听
port.stopFramedListening()
5. 发送数据
port.send({
hex: '01 03 00 0A 00 01 A5 C9', // [必填] HEX字符串,空格可选,大小写均可
success: (res) => { // [可选]
console.log('发送成功:', res.bytesWritten, '字节') // res.bytesWritten: 实际发送字节数
},
fail: (err) => { // [可选]
console.error('发送失败:', err.errMsg) // err.errCode: 错误码,err.errMsg: 错误信息
}
})
6. 关闭串口
port.close({
complete: () => {
console.log('串口已关闭')
port = null
}
})
演示页面
插件内置完整演示页面 uni_modules/z-serialport/example/example.vue,包含:
- 设备扫描 — 自动扫描所有可用串口设备
- 连接配置 — 设备路径 / 波特率 / 数据位 / 停止位 / 校验位下拉选择
- 数据发送 — HEX 格式输入,支持空格分隔
- 帧超时监听 — 空闲超时自动分帧,可配置超时时间
- 轮询模式 — 兼容模式,定时读取缓冲
- 接收数据 — 实时显示最近 5 条 HEX 数据
- 通信日志 — 时间戳记录所有收发/状态日志
布局适配:
- 横屏:左右双栏 50/50 平分,左配置/发送,右接收/日志
- 竖屏:上下堆叠布局,整屏滚动
API 参考
openPort(options)
打开串口,返回 SerialPort 实例
import { openPort } from '@/uni_modules/z-serialport/utssdk/app-android/index.uts'
openPort({
config: {
devicePath: '/dev/ttyS0', // [必填] 串口设备路径,如 /dev/ttyS0、/dev/ttyUSB0
baudRate: 9600, // [必填] 波特率:1200 ~ 921600
dataBits: 8, // [必填] 数据位:7 或 8
stopBits: 1, // [必填] 停止位:1 或 2
parity: 'none', // [必填] 校验位:'none' | 'odd' | 'even'
bufferSize: 4096, // [可选] 缓冲区大小(字节),默认 4096
readTimeout: 100, // [可选] 读取超时(毫秒),默认 0(不超时)
directionGpio: -1 // [可选] 485方向控制GPIO编号,默认 -1(不启用)
},
success: (res) => {
const port = res.port // res.port: SerialPort 实例,res.devicePath: 设备路径
// port.send(...) / port.startFramedListening(...) / ...
},
fail: (err) => { console.error('打开失败', err.errMsg) } // err.errCode: 错误码,err.errMsg: 错误信息
})
port.send(options)
发送数据(传入 HEX 字符串,UTS 内部解析为字节数组写入)
port.send({
hex: '01 03 00 0A', // [必填] HEX字符串,空格可选
success: (res) => { ... }, // [可选]
fail: (err) => { ... } // [可选]
})
port.startFramedListening(callback, interByteTimeout?, frameDelimiter?, onError?)
帧超时监听(推荐,自动分帧解决粘包)
双重策略:协议级分帧优先(即时提取完整帧)+ 空闲超时兜底(残留字节超时后整体交付)
// 纯超时分帧(通用,适合 RS232/自定义协议)
port.startFramedListening((data) => {
console.log('收到:', port.bytesToHex(data))
}, 10) // 第2参 [可选] 超时 ms,默认 10
// Modbus RTU 协议分帧(自动 CRC-16 校验,丢弃噪声帧)
port.startFramedListening(callback, 10, 'modbus-rtu') // 第3参 [可选] 'modbus-rtu' | 固定长度数字 | 自定义函数 | HeaderLengthConfig
// 固定帧长度
port.startFramedListening(callback, 10, 8)
// 帧头+长度域 通用分帧(推荐,解决绝大多数自定义工业协议粘包问题)⭐
port.startFramedListening(callback, 10, {
type: 'header-length', // 分帧类型
header: [0xAA, 0xBB], // [必填] 帧头同步字节数组
lengthOffset: 2, // [必填] 长度域在帧内的起始偏移(含帧头,如帧头2字节则填2)
lengthSize: 1, // [必填] 长度域占字节数:1(最大255)或 2(最大65535,大端序)
crcBytes: 2, // [可选] 帧尾CRC字节数,0=不校验,默认0
crcType: 'modbus', // [可选] CRC类型:'modbus'(CRC-16/MODBUS)| 'none',默认'modbus'
lengthIncludesCRC: false, // [可选] 长度域值是否已包含CRC字节,默认false(长度=纯数据字节数)
minFrameLen: 5, // [可选] 最小合法帧总长,用于过滤噪声,默认4
maxFrameLen: 255 // [可选] 最大合法帧总长,防止异常长度,默认256
})
// 自定义分帧函数
port.startFramedListening(callback, 10, (buffer) => {
// 返回一条完整帧,或 null 表示数据不足
if (buffer.length < 4) return null
const len = buffer[2] + 5
return buffer.length >= len ? buffer.slice(0, len) : null
})
// 传入 onError 捕获监听异常(第4参 [可选])
port.startFramedListening(callback, 10, undefined, (err) => {
console.error('监听异常:', err?.errMsg)
})
// 停止
port.stopFramedListening()
分帧策略对比:
| 策略 | 粘包处理 | CRC校验 | 协议依赖 | 推荐场景 |
|---|---|---|---|---|
header-length |
✅ 帧头搜索+帧长计算 | ✅ 可选 | 需帧头+长度域 | 自定义工业协议(推荐) |
modbus-rtu |
✅ 功能码推导帧长 | ✅ 自动 | Modbus RTU | Modbus 设备 |
fixed 固定帧长 |
✅ 等长切割 | ❌ | 帧长固定 | 简单协议 |
custom 自定义函数 |
✅ 自行实现 | ✅ 自行实现 | 自定义 | 复杂/特殊协议 |
| 纯超时 | ⚠️ 依赖帧间间隙 | ❌ | 无 | 低频上报/RS232 |
startPolling(callback, interval?, onError?)
轮询模式,定时读取缓冲区
port.startPolling((data) => {
console.log('轮询收到:', bytesToHex(data))
}, 50) // 第2参 [可选] 轮询间隔 ms,默认 50
port.stopPolling()
同步方法
const status = port.getStatus() // { isOpen: boolean, devicePath: string }
const health = port.getHealthStatus() // 连接健康状态(工业长期运行必需)
const count = port.available() // 可读字节数,-1 表示 IO 异常
port.clearBuffer() // 清空缓冲区
port.setDirection(true) // 485 方向控制(true=发送/false=接收)
port.getHealthStatus() - 工业级断线检测
返回串口连接健康快照,用于心跳检测和自动重连判断:
const health = port.getHealthStatus()
// {
// isOpen: true, // 串口是否打开
// lastDataTime: 1234567, // 上次收到数据的时间戳(ms),0=尚未收到
// consecutiveErrors: 0, // 连续读错误次数(收到数据自动归零)
// idleMs: 520, // 距上次收到数据的空闲时长(ms),-1=从未收到
// bufferSize: 0 // 帧缓冲区待处理字节数
// }
典型用法(参考 example.vue 中的完整实现):
// 每 2 秒检查一次
setInterval(() => {
const h = port.getHealthStatus()
if (h.consecutiveErrors >= 5) {
console.warn('连续 IO 错误,设备可能断开')
reconnect() // 触发重连逻辑
}
if (h.idleMs >= 30000) {
console.warn('30 秒无数据响应')
reconnect()
}
}, 2000)
断线检测原理:
consecutiveErrors:底层inputStream.read()或available()抛出 IOException 时递增,成功读取数据时归零。连续 5 次以上通常意味着设备已物理断开idleMs:成功读取数据时更新lastDataTime,空闲超过阈值说明对端设备可能停止响应
注意:available() 方法现在在 IO 异常时返回 -1(之前返回 0),用于区分"无数据"和"流已损坏"。
port.startListening() — 已废弃
startListening已废弃,请迁移到startFramedListening。后者即使不传分帧参数,也能通过空闲超时机制自动划分帧边界。旧 API 将每批原始字节直接交付,无帧边界概念。
数据解析方法
串口接收回调拿到的是 number[] 字节数组,需转 HEX 字符串展示或做协议解析:
// 字节数组 → HEX 字符串(回调中最常用)
// 例:[0x01, 0x03, 0x00, 0x0A] → "01 03 00 0A"
port.bytesToHex(bytes) // 等同 port.bytesToHex(data)
// HEX 字符串 → 字节数组(手动组装时用)
// 例:"01 03 00 0A" → [1, 3, 0, 10]
port.hexToBytes(hex) // hex 支持空格分隔,大小写均可
close()
关闭串口
port.close({
complete: () => { console.log('已关闭') }
})
错误码
所有错误遵循 uni 错误规范,errSubject 固定为 "z-serialport",错误码以 90_ 开头。
错误对象结构
{
errSubject: "z-serialport",
errCode: 90_10001, // 错误码(见下表)
errMsg: "设备不存在,请检查串口路径:/dev/ttyS5" // 标准消息 + 额外上下文
}
错误码一览
| 错误码 | 说明 | 触发场景 | 排查建议 |
|---|---|---|---|
90_10001 |
设备不存在 | openPort 时目标 /dev/tty* 路径不存在 |
① 调用 listPorts() 确认设备列表 ② 检查物理连接/USB 线 ③ 确认设备文件名(部分板子为 ttyS1/ttyS2) |
90_10002 |
打开串口失败 | openPort 时 RandomAccessFile 打开失败或创建 FileInputStream/FileOutputStream 失败 |
① 检查 app 是否持有串口读写权限(android.permission) ② 确认设备未被其他进程占用(lsof \| grep tty) ③ SELinux 是否阻止访问 |
90_10003 |
配置串口参数失败 | openPort 时 stty 命令执行失败(超时/参数不兼容/设备忙) |
① 检查波特率/数据位/停止位/校验位组合是否合法 ② 确认内核驱动支持该参数(部分内核不支持 921600) ③ 检查设备是否处于异常状态(回显 stty -F /dev/ttyS0) |
90_10004 |
串口未打开 | send/receive/close 等操作时串口实例未连接,或直接调用了模块级旧 API(如 send() 而非 port.send()) |
① 确认已调用 openPort 并成功返回 ② 使用 openPort 返回的 port 实例方法,而非模块级导出函数 |
90_10005 |
发送数据失败 | HEX 格式错误(长度非偶数/含非法字符)、发送数据为空、outputStream.write() 写入异常 |
① 校验 HEX 字符串格式(如 "01 03 00 0A") ② 确认串口仍处于连接状态(port.getStatus()) ③ 检查输出缓冲区是否已满 |
90_10006 |
接收数据失败 | 帧监听/轮询/事件监听的循环读取异常、用户回调函数内部抛错 | ① 检查 onData 回调函数逻辑是否有 bug ② 确认设备未被拔出(物理断线会持续触发此错误) ③ 监听循环会自动重试,无需手动重启 |
90_10007 |
操作超时 | receive() 同步读取在指定时间内未收到足够数据 |
① 增大 timeout 参数 ② 检查设备是否正常响应 ③ 降低波特率或增大 readTimeout |
90_10008 |
GPIO 控制失败 | RS485 方向脚 GPIO 导出失败、value 文件写入失败、或 GPIO 编号无效 | ① 确认 directionGpio 编号与实际硬件一致 ② 检查 /sys/class/gpio/ 下 GPIO 是否已正确导出 ③ 确认 GPIO 未被其他进程占用 |
90_10009 |
未知错误 | 操作中发生预期外的异常(如 UTS 对象创建失败、内部类型转换异常) | 查看 errMsg 中的异常详情,反馈给插件维护者 |
应用层处理示例
import { openPort } from '@/uni_modules/z-serialport/utssdk/app-android/index.uts'
openPort({
config: { /* ... */ },
fail: (err) => {
switch (err.errCode) {
case 90_10001:
uni.showToast({ title: '串口设备不存在,请检查连接' })
break
case 90_10002:
uni.showToast({ title: '无权限访问,请检查应用权限' })
break
case 90_10003:
uni.showToast({ title: '参数配置失败: ' + err.errMsg })
break
default:
uni.showToast({ title: '打开失败: ' + err.errMsg })
}
}
})
// 监听异常处理(90_10006)
port.startFramedListening(onData, 10, undefined, (err) => {
if (err.errCode === 90_10006) {
console.warn('接收异常,监听自动重试中: ' + err.errMsg)
// 可在此累加计数,超过阈值触发上层重连
}
})
技术架构
- Android 层 (
index.uts): 核心串口操作,使用 RandomAccessFile + stty 命令 - 接口层 (
interface.uts): 类型定义、回调签名 - 错误层 (
unierror.uts): 错误码定义、UniError 实现

收藏人数:
购买源码授权版(
试用
使用 HBuilderX 导入示例项目
赞赏(0)
下载 7
赞赏 0
下载 12240581
赞赏 1921
赞赏
京公网安备:11010802035340号