🧾 蓝牙打印机
吾码前端内置 V8.Print 蓝牙直连能力。业务 V8 只负责描述标签或小票,平台统一管理设备选择、连接恢复、型号识别、指令适配、分包与串行发送。目前重点兼容佳博 GP-M322 与 ZICOX CC4;原有佳博 V8 代码无需修改。
一份业务 V8,两种打印机协议
GP-M322 继续逐字节接收原有 TSPL;CC4 会在发送前把
createNew()的标准 TSC 调用安全转换为厂家 CPCL。createNewESC()生成的 ESC/POS 在两条路径上都保持原样。
📸 预览图




先看结论
| 打印机 | 连接页型号 | 标签指令 | 传输 | 旧 V8 是否改动 |
|---|---|---|---|---|
| 佳博 GP-M322 | 自动识别或“佳博 GP-M322” | TSPL 原始字节 | BLE | 不需要 |
| ZICOX CC4 | 自动识别或“ZICOX CC4” | TSC 调用自动转换为 CPCL | BLE;Android 5+App 可回退到 SPP | 常用标准调用不需要 |
| 其它 TSPL 机型 | “其它 TSPL 打印机” | TSPL 原始字节 | BLE | 不需要 |
CC4 厂家资料声明支持 ESC/POS、CPCL、蓝牙双模及 Android/iOS 等平台。吾码只据此启用经过实现和测试的 ESC/POS、CPCL、BLE/SPP 路径,不把演示工程中出现但产品页未声明的 ZPL 当作 CC4 保证能力。型号参数以 ZICOX CC4 官方页面 与实际固件为准。
微服务同时提供普通打印与蓝牙打印
前端微服务运行在隔离的子应用中,不能直接访问父页面的 Vue 组件、Pinia、DOM 或 window.parent.V8。业务页面需要增加“普通打印”时,应通过 microi.host.v1 宿主动作 openPlatformPrint 打开吾码 Print Engine 预览;不要在 子应用里复制平台打印弹窗,也不要跨 iframe 调父页面私有方法。
| 页面选项 | 标准入口 | 适用输出 | 边界 |
|---|---|---|---|
| 普通打印 | openPlatformPrint 宿主动作 | mic_print 模板预览、浏览器/系统打印 | 只打开平台预览,不发送 BLE/SPP 字节 |
| 蓝牙打印 | 平台前端 V8 中的 V8.Print.prepareSend() | TSPL、CPCL、ESC/POS 标签或小票 | 写入成功不等于打印机已经走纸 |
微服务调用普通打印的最小示例:
function callMicroiHost(action, data) {
var host = window.microApp?.getData?.() || {};
var capabilities = host.hostCapabilities;
if (capabilities?.protocol !== 'microi.host.v1'
|| !capabilities?.actions?.includes(action)) {
throw new Error('当前吾码宿主不支持 ' + action);
}
var requestId = 'host-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
window.microApp.dispatch({
type: capabilities.requestType || 'micro-app:host-action',
action: action,
requestId: requestId,
data: data
});
return requestId;
}
function openOrdinaryPrint(printId, businessId) {
var host = window.microApp?.getData?.() || {};
var dataApi = new URL('/apiengine/get-print-data', host.apiBase);
dataApi.searchParams.set('OsClient', host.osClient);
dataApi.searchParams.set('Id', businessId);
return callMicroiHost('openPlatformPrint', {
printId: printId, // mic_print.Id
dataApi: dataApi.toString(),
title: '普通打印'
});
}宿主会校验 printId,并且只接受“当前 apiBase 同源、路径以 /apiengine/ 开头、明确携带且匹配当前 OsClient”的绝对 dataApi。不要把 Token、帐号密码或其它 凭据放进 URL。宿主返回 { accepted:true, printId } 只表示平台打印预览已经打开,不等于 浏览器已执行打印,更不等于打印机已经出纸。
独立打开微服务时没有吾码宿主,hostCapabilities 不存在;此时应隐藏/禁用普通打印按钮, 或使用应用自己的独立预览方案。openPlatformPrint 不是蓝牙代理,不能把 TSPL、CPCL、 ESC/POS 字节作为它的参数。完整宿主动作与结果监听见 前端微服务:子应用调用吾码主框架能力。
为什么旧 V8 可以保持不变
flowchart LR
A["原有业务 V8"] --> B["V8.Print.createNew()"]
B --> C["型号自动识别或手工选择"]
C -->|"GP-M322 / 通用 TSPL"| D["原 TSPL 字节,不改写"]
C -->|"ZICOX CC4"| E["发送前转换为 CPCL"]
D --> F["BLE 串行分包"]
E --> G["BLE,或 Android SPP"]
F --> H["打印机"]
G --> HcreateNew() 仍返回原来的 TSC 构建器。运行时只在字节数组上记录不可枚举的高层操作元数据:
- GP-M322 和通用 TSPL 路径完全不读取这些操作,发送字节与改造前逐字节一致。
- CC4 路径在任何蓝牙写入发生前完成整份 CPCL 转换与校验。
- 无法安全映射的命令会让整次任务以“零写入”失败,不会先打印半张再报错。
- 对外方法名、参数和标准调用顺序保持不变,存量 V8 无需增加品牌判断。
不要对 cmd.getData() 做 Array.from、JSON 序列化或复制后再发送;这些操作会丢失 CC4 转换所需的不可枚举元数据。应直接把同一构建器返回的数组交给 prepareSend。
连接方式与平台边界
| 运行环境 | GP-M322 | CC4 | 说明 |
|---|---|---|---|
| Android 5+App | BLE | BLE 优先,SPP 兜底 | SPP 需先在 Android 系统蓝牙设置完成配对 |
| iOS 5+App | BLE | BLE | 不使用 Android RFCOMM/SPP |
| Chrome / Edge Web Bluetooth | BLE | BLE | 需要 HTTPS/localhost、用户手势与可识别的 BLE 服务 |
| 不支持 Web Bluetooth 的普通 H5 | 不可用 | 不可用 | V8.Print 可能存在,但连接页会明确提示 |
| 微信小程序原生蓝牙 | 独立实现 | 独立实现 | 不属于本页的 Microi.Client Web/5+App 链路 |
Android CC4 的经典蓝牙路径与厂家 Demo 保持一致:优先尝试 RFCOMM 通道 1,受系统限制时再尝试标准 SPP UUID 00001101-0000-1000-8000-00805F9B34FB。吾码通过 5+ Runtime 的原生桥接访问该能力;实现原理可参考 DCloud Native.js 文档和经典蓝牙 SPP 示例。
Android 12 及以上版本在用户主动点击“搜索”时申请 BLUETOOTH_SCAN 与 BLUETOOTH_CONNECT,不会在页面加载或自动重连时突然弹窗。App 云打包/自定义基座还必须启用 DCloud Bluetooth 模块并在宿主清单声明相应权限;用户拒绝后,连接页会提示开启系统“附近的设备”权限。
Web 浏览器不能使用经典蓝牙 SPP。当前只申请常见打印机 BLE 服务 18f0、ff00、49535343-fe7d-4ae5-8fa9-9fafd205e455、e7810a71-73ae-499d-8c15-faa9aef0c3f2。如果某批 CC4 固件只开放 SPP 或使用其它私有 BLE UUID,应在 Android 5+App 使用已配对 SPP,或取得厂家准确 UUID 后扩展平台源码,不能猜测 UUID。
连接打印机
- 打开打印机电源。Android 使用 CC4 经典蓝牙时,先到系统设置完成配对;Android 12+ 首次搜索允许“附近的设备”权限。
- PC/平板点击顶部蓝牙图标;移动端进入【我的 → 蓝牙连接】。
- 型号保持“自动识别(推荐)”。设备广播名没有
ZICOX或CC4时,手工选择“ZICOX CC4”。 - 点击搜索并选择设备。CC4 在 Android 中可选择带
SPP标记的已配对设备。 - 先执行“打印测试”,再进入业务模块打印真实标签。
型号选择与设备一起持久化。意外断线会有限次数自动重连;主动点击“断开连接”会停止重连并忘记设备。
旧 V8 的标准写法
下面代码同时用于 GP-M322 和 CC4,不需要增加品牌分支:
function cleanPrintText(value, maxLength) {
return String(value == null ? '' : value)
.replace(/[\r\n"\x00-\x1f]/g, ' ')
.slice(0, maxLength || 120);
}
async function printProductLabel(product) {
if (!V8.Print) throw new Error('当前前端未加载蓝牙打印能力');
if (!V8.Print.isConnected()) {
var restored = await V8.Print.reconnect();
if (!restored) restored = await V8.Print.OpenBluetoothPage();
if (!restored || !V8.Print.isConnected()) {
throw new Error('未连接蓝牙打印机');
}
}
var cmd = V8.Print.createNew();
cmd.setSize(60, 40);
cmd.setGap(2);
cmd.setSpeed(4);
cmd.setDensity(8);
cmd.setDirection(1);
cmd.setCls();
cmd.setText(20, 20, 'TSS24.BF2', 1, 1, cleanPrintText(product.Name, 40));
cmd.setBarCode(20, 80, '128', 60, 1, 2, 4, cleanPrintText(product.Code, 40));
cmd.setQR(340, 30, 'M', 5, 'A', cleanPrintText(product.Id, 120));
cmd.setPagePrint();
await V8.Print.prepareSend(cmd.getData());
V8.Tips('打印数据已发送', true);
}连接弹窗关闭后 OpenBluetoothPage() 才解析为 Promise<boolean>。首次 Web Bluetooth 选择必须由点击等用户手势直接触发。
型号与连接状态 API
旧业务不必调用型号 API。只有设备名称无法识别或需要诊断时才使用:
// 查看当前解析出的型号配置
var profile = V8.Print.getPrinterProfile();
console.log(profile.id, profile.commandLanguage);
// 广播名无法识别时显式选 CC4;连接页也能完成同一操作
V8.Print.setPrinterProfile('zicox-cc4');
// 恢复按设备名自动识别
V8.Print.setPrinterProfile('auto');setPrinterProfile 可用值为 auto、gprinter-gp-m322、zicox-cc4、generic-tspl。getConnectionState() 额外返回:
| 字段 | 含义 |
|---|---|
transport | 当前 ble 或 spp 通道 |
profileMode | 自动或手工选择值 |
profileId / profileName | 最终生效的型号配置 |
commandLanguage | 当前标签指令 tspl 或 cpcl |
mtu / maxWriteBytes | 5+ BLE 本次连接确认的 MTU / 有效载荷上限;未确认时为 0 / 20,不跨连接缓存 |
recommendedPacketSize | 5+ 佳博按已确认能力推荐 20 / 100;180 仅作为协商后的写入上限,旧宿主可能没有此字段 |
writeType / packetIntervalMs | 5+ 当前写方式与包间等待;Android 佳博优先 writeNoResponse + 8ms,不支持时兼容 write,其它路径保留 20ms |
打印前仍以 isConnected() 和 prepareSend() 为准,不以已保存的 deviceId 或状态快照代替实时连接判断。
5+App 位图发送速度
microi.app 是加载远程 H5 的 5+ 壳,原生蓝牙代码位于 Microi.Client/src/utils/v8-print.js。 浏览器同一张标签快、App 慢时,先对比包长和原生回调耗时,不应直接归因于生产 API。 约 9KB 的位图按 20 字节会产生约 461 次写入,原先每次回调后再等待 20ms 会额外增加约 9 秒。 Android 佳博 GP-M322 在特征支持时优先使用同一 5+ BLE 栈的 writeNoResponse,每包仍等待 API 成功回调并保留约 8ms 的短保护窗口;不并发写入。 这消除了确认写回调在部分 Android 设备上每包约百毫秒造成的十几秒发送等待;若特征不支持无响应写,则保留 write 兼容路径。 5+ BLE 的连接、服务发现和写入统一使用 plus.bluetooth,不能用 uni.writeBLECharacteristicValue 写入由 plus.bluetooth 建立的连接,否则首包可能因两套运行时连接状态不一致而中断。
连接时在服务发现后尝试协商 MTU,最多等 1.5 秒,仅采用回调中返回的实际 mtu。 能力明确时应用必须用 recommendedPacketSize 选择包长;发送端同时按它与 maxWriteBytes 限包。 即使协商得到 183 MTU(180 字节有效载荷),GP-M322 连续发送仍限制为 100 字节稳定档,避免 10008“发送中断”。 缺少 API、失败、超时或空成功回调均保留 20 字节,不能把请求的 183 当作实际协商结果。 旧壳即使无法协商大包,佳博确认写入也使用同一串行发送路径。iOS、其它型号、无响应写与 CC4 SPP 保留原有节奏。
升级远程前端镜像后完全退出并重开在线模式 App,再重新连接打印机;本修复不要求重新打 APK。 自定义微服务若写死“仅 engine === 'web' 才加速”,还需按连接能力更新其策略。 不能在不支持 Web Bluetooth 的 WebView 中通过增加切换选项启用浏览器蓝牙。 回归须同时记录未知 MTU 的旧壳和已确认大包两条路径;模拟字节测试不等于真机速度或物理出纸验收。
搜索不到之前的设备
BLE 外设在 GATT 已连接期间会停止广播。如果上一次连接被并发流程打断、或 App 重启后系统仍保留旧链路,打印机虽然“不在使用中”,却仍连接着手机,标准 BLE 搜索自然找不到它。
- 平台在每次点击“开始搜索蓝牙”后、启动发现前,会先调用
getConnectedBluetoothDevices,关闭应用当前未持有的连接,再开始扫描;正在使用的连接不会被关闭。 - 连接期间被断开/重连事件接管时,平台会显式关闭本流程已经建立的 GATT 链路,不再把“能连上但未登记”的连接留在系统里。
- 以上修复位于远程前端
Microi.Client/src/utils/v8-print.js,升级远程前端镜像后完全退出并重开 App 即可生效,不需要重新打 APK。 - 若仍搜不到:先在系统蓝牙设置里忽略/忘记该打印机,关闭再打开打印机电源与手机蓝牙后重试;也可以直接用“停止搜索 → 开始搜索”触发一次残留连接清理。
CC4 自动转换范围
| 处理方式 | TSC 方法 |
|---|---|
| 安全转换 | setSize、setSpeed、setDensity、setGap、setBline、setFeed、setBackFeed、setDirection(0/1)、setReference、setBar、setBox、setReverse、setText、setQR、setBarCode、setBitmap、setPagePrint |
| 构建时无需输出 | init、setCls |
| 写入前拒绝 | addCommand、setCountry、setCodepage、setFromfeed、setHome、setSound、setLimitfeed、setErase 及未知方法 |
CC4 每份自动转换的标签必须且只能调用一次 setPagePrint()。被拒绝的方法不是“打印机一定不支持”,而是当前平台没有足够证据保证 TSC 与 CPCL 语义等价;失败关闭比发送乱码更安全。确有需求时,应依据该型号 CPCL 手册补充映射和自动化/实机回归,再扩大白名单。
CC4 官方也支持 ESC/POS。使用 createNewESC() 时平台不做 TSC→CPCL 转换,原始 ESC/POS 字节直接发送。
批量与失败恢复
for (var i = 0; i < rows.length; i++) {
try {
await printProductLabel(rows[i]);
} catch (error) {
V8.Tips('第 ' + (i + 1) + ' 张发送失败:' + (error.message || error), false);
break;
}
}- 所有前端 V8 共用一个
V8.Print单例和一条发送队列,仍应逐张await。 - 不用
Promise.all向一台打印机并发发送,也不用固定setTimeout猜测走纸完成。 prepareSend成功只证明字节写入 BLE/SPP 输出流,不代表已走纸、无缺纸或物理打印成功。- 业务落库与打印不是原子事务。保存稳定业务单号和失败位置,由用户确认是否重打。
- 不支持的 CC4 命令、空数据和非法包长会在首个蓝牙写入前失败。
实机验收清单
上线前分别记录 GP-M322 与 CC4 的型号、固件、纸张、终端版本和连接通道,并覆盖:
- 首次授权/配对、自动识别、手工选型、刷新恢复、主动断开和意外重连。
- 中文、数字、长文本、条码、二维码、图片、正反方向、间隙纸与黑标纸。
- 默认 20 字节与实测包长,特别覆盖数据长度恰好整除包长。
- 两台打印机交替选择后,GP-M322 仍接收原 TSPL,CC4 接收 CPCL/ESC-POS。
- 连续 20 张严格串行发送;中途关机、缺纸、离开范围和权限撤销后的失败位置。
- CC4 Android BLE 与已配对 SPP;Web 端只验证固件真实开放的 BLE 服务。
没有目标打印机在场时,自动化测试只能证明协议转换、原字节回归、分包队列和连接状态逻辑,不能代替纸张、字库、浓度、偏移、传感器与物理走纸验收。
与打印引擎的边界
- 打印引擎负责
mic_print、PageObj、PrintObj、A4/PDF/浏览器模板和在线设计。 - 蓝牙打印机负责 TSC/TSPL、CPCL、ESC/POS 原生命令以及近场设备连接。
- 完整前端 API、构建器方法和批量恢复见 V8 客户端函数:蓝牙打印。
两者可以共享同一份业务数据,但模板 JSON 不能直接传给 prepareSend,打印机原始字节也不能当作 PageObj。