Skip to content

🧾 蓝牙打印机

吾码前端内置 V8.Print 蓝牙直连能力。业务 V8 只负责描述标签或小票,平台统一管理设备选择、连接恢复、型号识别、指令适配、分包与串行发送。目前重点兼容佳博 GP-M322 与 ZICOX CC4;原有佳博 V8 代码无需修改。

一份业务 V8,两种打印机协议

GP-M322 继续逐字节接收原有 TSPL;CC4 会在发送前把 createNew() 的标准 TSC 调用安全转换为厂家 CPCL。createNewESC() 生成的 ESC/POS 在两条路径上都保持原样。

📸 预览图

蓝牙打印机输出主构件二维码标签实拍
主构件二维码标签实机打印结果。
蓝牙打印机输出业务标签与 Microi 测试页实拍
业务标签与 Microi 蓝牙打印测试结果。
Microi 移动端蓝牙连接入口和已连接设备状态
移动端查看当前蓝牙打印机及连接状态。
Microi 移动端微服务选择蓝牙打印规格
微服务内选择 75×60 mm 或 100×60 mm 打印规格。

先看结论

打印机连接页型号标签指令传输旧 V8 是否改动
佳博 GP-M322自动识别或“佳博 GP-M322”TSPL 原始字节BLE不需要
ZICOX CC4自动识别或“ZICOX CC4”TSC 调用自动转换为 CPCLBLE;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 标签或小票写入成功不等于打印机已经走纸

微服务调用普通打印的最小示例:

javascript
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 可以保持不变

mermaid
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 --> H

createNew() 仍返回原来的 TSC 构建器。运行时只在字节数组上记录不可枚举的高层操作元数据:

  • GP-M322 和通用 TSPL 路径完全不读取这些操作,发送字节与改造前逐字节一致。
  • CC4 路径在任何蓝牙写入发生前完成整份 CPCL 转换与校验。
  • 无法安全映射的命令会让整次任务以“零写入”失败,不会先打印半张再报错。
  • 对外方法名、参数和标准调用顺序保持不变,存量 V8 无需增加品牌判断。

不要对 cmd.getData()Array.from、JSON 序列化或复制后再发送;这些操作会丢失 CC4 转换所需的不可枚举元数据。应直接把同一构建器返回的数组交给 prepareSend

连接方式与平台边界

运行环境GP-M322CC4说明
Android 5+AppBLEBLE 优先,SPP 兜底SPP 需先在 Android 系统蓝牙设置完成配对
iOS 5+AppBLEBLE不使用 Android RFCOMM/SPP
Chrome / Edge Web BluetoothBLEBLE需要 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_SCANBLUETOOTH_CONNECT,不会在页面加载或自动重连时突然弹窗。App 云打包/自定义基座还必须启用 DCloud Bluetooth 模块并在宿主清单声明相应权限;用户拒绝后,连接页会提示开启系统“附近的设备”权限。

Web 浏览器不能使用经典蓝牙 SPP。当前只申请常见打印机 BLE 服务 18f0ff0049535343-fe7d-4ae5-8fa9-9fafd205e455e7810a71-73ae-499d-8c15-faa9aef0c3f2。如果某批 CC4 固件只开放 SPP 或使用其它私有 BLE UUID,应在 Android 5+App 使用已配对 SPP,或取得厂家准确 UUID 后扩展平台源码,不能猜测 UUID。

连接打印机

  1. 打开打印机电源。Android 使用 CC4 经典蓝牙时,先到系统设置完成配对;Android 12+ 首次搜索允许“附近的设备”权限。
  2. PC/平板点击顶部蓝牙图标;移动端进入【我的 → 蓝牙连接】。
  3. 型号保持“自动识别(推荐)”。设备广播名没有 ZICOXCC4 时,手工选择“ZICOX CC4”。
  4. 点击搜索并选择设备。CC4 在 Android 中可选择带 SPP 标记的已配对设备。
  5. 先执行“打印测试”,再进入业务模块打印真实标签。

型号选择与设备一起持久化。意外断线会有限次数自动重连;主动点击“断开连接”会停止重连并忘记设备。

旧 V8 的标准写法

下面代码同时用于 GP-M322 和 CC4,不需要增加品牌分支:

javascript
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。只有设备名称无法识别或需要诊断时才使用:

javascript
// 查看当前解析出的型号配置
var profile = V8.Print.getPrinterProfile();
console.log(profile.id, profile.commandLanguage);

// 广播名无法识别时显式选 CC4;连接页也能完成同一操作
V8.Print.setPrinterProfile('zicox-cc4');

// 恢复按设备名自动识别
V8.Print.setPrinterProfile('auto');

setPrinterProfile 可用值为 autogprinter-gp-m322zicox-cc4generic-tsplgetConnectionState() 额外返回:

字段含义
transport当前 blespp 通道
profileMode自动或手工选择值
profileId / profileName最终生效的型号配置
commandLanguage当前标签指令 tsplcpcl
mtu / maxWriteBytes5+ BLE 本次连接确认的 MTU / 有效载荷上限;未确认时为 0 / 20,不跨连接缓存
recommendedPacketSize5+ 佳博按已确认能力推荐 20 / 100180 仅作为协商后的写入上限,旧宿主可能没有此字段
writeType / packetIntervalMs5+ 当前写方式与包间等待;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 方法
安全转换setSizesetSpeedsetDensitysetGapsetBlinesetFeedsetBackFeedsetDirection(0/1)setReferencesetBarsetBoxsetReversesetTextsetQRsetBarCodesetBitmapsetPagePrint
构建时无需输出initsetCls
写入前拒绝addCommandsetCountrysetCodepagesetFromfeedsetHomesetSoundsetLimitfeedsetErase 及未知方法

CC4 每份自动转换的标签必须且只能调用一次 setPagePrint()。被拒绝的方法不是“打印机一定不支持”,而是当前平台没有足够证据保证 TSC 与 CPCL 语义等价;失败关闭比发送乱码更安全。确有需求时,应依据该型号 CPCL 手册补充映射和自动化/实机回归,再扩大白名单。

CC4 官方也支持 ESC/POS。使用 createNewESC() 时平台不做 TSC→CPCL 转换,原始 ESC/POS 字节直接发送。

批量与失败恢复

javascript
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 的型号、固件、纸张、终端版本和连接通道,并覆盖:

  1. 首次授权/配对、自动识别、手工选型、刷新恢复、主动断开和意外重连。
  2. 中文、数字、长文本、条码、二维码、图片、正反方向、间隙纸与黑标纸。
  3. 默认 20 字节与实测包长,特别覆盖数据长度恰好整除包长。
  4. 两台打印机交替选择后,GP-M322 仍接收原 TSPL,CC4 接收 CPCL/ESC-POS。
  5. 连续 20 张严格串行发送;中途关机、缺纸、离开范围和权限撤销后的失败位置。
  6. CC4 Android BLE 与已配对 SPP;Web 端只验证固件真实开放的 BLE 服务。

没有目标打印机在场时,自动化测试只能证明协议转换、原字节回归、分包队列和连接状态逻辑,不能代替纸张、字库、浓度、偏移、传感器与物理走纸验收。

与打印引擎的边界

  • 打印引擎负责 mic_printPageObjPrintObj、A4/PDF/浏览器模板和在线设计。
  • 蓝牙打印机负责 TSC/TSPL、CPCL、ESC/POS 原生命令以及近场设备连接。
  • 完整前端 API、构建器方法和批量恢复见 V8 客户端函数:蓝牙打印

两者可以共享同一份业务数据,但模板 JSON 不能直接传给 prepareSend,打印机原始字节也不能当作 PageObj

MIT License.