@nokecy/qc-bluetooth-print
v1.1.0
Published
Android 蓝牙打印插件(UTS 原生)。同时支持经典蓝牙 SPP(RFCOMM)和 BLE(GATT 自动协商 MTU + 分包写)。避开 plus.android JS 桥的同步/异步不确定行为。
Maintainers
Readme
qc-bluetooth-print
Android 蓝牙打印插件(UTS 原生)。同时支持:
- 经典蓝牙 SPP(RFCOMM)—— 已配对的小票/标签热敏打印机
- BLE 低功耗蓝牙(GATT,自动协商 MTU + 分包写)—— BLE-only 打印机
直接调用 android.bluetooth.* 原生类,避开 plus.android JS 桥的同步/异步不确定行为。
解决什么问题
uni-app App 端做经典蓝牙打印,长期被一组顽固问题困扰:
- 连续打印第二张失败,要退出页面再进
isConnected()报已连接,但write不出纸- 第一次连接 1.5 秒返回但 socket 没真连上,第二次才好
根因都是 plus.android.invoke JS 桥让 socket.connect() 立即返回(实际未阻塞)、isConnected() 在打印机端单方断开时不可靠、状态机层重复 connect 导致 socket 端点泄漏。
本插件用 UTS(编译成 Kotlin)直接调原生 API:
socket.connect()在 IO 线程真正阻塞到 RFCOMM 建立完成- 写入失败 → 主动 close socket + 清状态,下次自动建新链路
- 同地址乐观复用,由 write 失败做最终判活
- 不再有「state 显示 CONNECTED 但 socket 已死」的幽灵状态
安装与同步
# 1. 安装 npm 包
npm install @nokecy/qc-bluetooth-print
# 或 pnpm
pnpm add @nokecy/qc-bluetooth-print
# 2. 同步插件到项目的 src/uni_modules/
npx qc-bluetooth-print syncsync 命令会把 node_modules/@nokecy/qc-bluetooth-print/uni_modules/qc-bluetooth-print/ 完整复制到当前项目的 src/uni_modules/qc-bluetooth-print/(如果存在会询问是否覆盖,加 --force 强制覆盖)。
为什么需要 sync 步骤
uni-app 的 UTS 插件机制要求源码必须在 src/uni_modules/<plugin>/ 目录下才能被 HBuilderX 编译。npm 包本身不能直接被 HBuilderX 识别——这是 uni_modules 的设计约束,所有 UTS 类 npm 包都需要这一步。
打包要求
UTS 插件需要自定义基座才能在真机调试:
- HBuilderX 中:运行 → 运行到 Android App 基座 → 制作自定义调试基座
- 等基座打包完成(5-10 分钟)
- 真机运行时勾选「使用自定义基座」
API — 经典蓝牙 SPP
import * as qcbt from '@/uni_modules/qc-bluetooth-print'
// 获取系统已配对的蓝牙设备列表
const devices = qcbt.getBondedDevices()
// → [{ name: 'XP-365B', address: 'XX:XX:XX:XX:XX:XX' }, ...]
// 连接(同地址且 socket 仍活着会复用,不重连)
await qcbt.connect('XX:XX:XX:XX:XX:XX')
// 发送 CPCL / TSPL 文本指令(默认 GBK 编码,支持中文)
await qcbt.writeString('! 0 200 200 200 1\r\nTEXT 4 0 0 0 Hello\r\nPRINT\r\n')
// 发送 ESC/POS 字节指令(含位图)
await qcbt.writeBytes([0x1b, 0x40, 0x1b, 0x61, 0x01])
// 主动断开
qcbt.disconnect()
// 查询当前连接状态
qcbt.isConnected()API — BLE 低功耗蓝牙
import * as qcbt from '@/uni_modules/qc-bluetooth-print'
// 扫描发现 BLE 设备(每发现一台调一次 callback)
qcbt.bleStartScan((device) => {
console.log('BLE found:', device.name, device.address)
})
// 必须自己设置超时停止扫描(避免耗电)
setTimeout(() => qcbt.bleStopScan(), 8000)
// 连接(自动 discoverServices + 找首个可写 char + 协商 MTU 247)
await qcbt.bleConnect('XX:XX:XX:XX:XX:XX')
// 查看协商到的 MTU(决定单次写入容量 = mtu - 3)
console.log('BLE MTU:', qcbt.bleGetMtu()) // 典型值 247
// 发送字符串(自动按 mtu-3 分包 + 串行写入)
await qcbt.bleWriteString('! 0 200 200 200 1\r\nTEXT 4 0 0 0 Hello\r\nPRINT\r\n')
// 发送字节数组
await qcbt.bleWriteBytes([0x1b, 0x40, /* ... */])
// 断开
qcbt.bleDisconnect()
// 是否已连接
qcbt.bleIsConnected()SPP vs BLE 怎么选
| 你的打印机 | 用哪组 API | |---|---| | 设备能在系统「已配对蓝牙设备」里看到 | 经典蓝牙 SPP | | 设备必须扫描发现,不出现在已配对列表 | BLE | | 同时支持两种 | 优先用 SPP,更稳定 |
平台支持
| 平台 | 支持 | |------|------| | App Android (5.0+) | ✅ 经典蓝牙 SPP | | App iOS | ❌ 需要 MFi 认证,复杂度大,未实现 | | 小程序 | ❌ | | H5 | ❌ |
设计取舍
- 薄包装:不封装 CPCL/TSPL/ESC 指令,前端只做透传。指令字符串/字节流由调用方(或后端)生成
- 单连接:模块级 socket 单例,符合打印机使用场景
- 失败即清理:写入失败立即
close + 置 null,下次connect()自动重建——这是经典蓝牙唯一可靠的失效检测
已知风险点
UTS 是 DCloud 自研语法(编译到 Kotlin),有几处细节可能因 HBuilderX 版本不同而编译失败。完整 fallback 写法见 uni_modules/qc-bluetooth-print/readme.md。
License
MIT © nokecy
