一、前言
在实际物流配送业务中,电子签收在部分场景下仍无法完全替代纸质回单。配送员在门店现场需要打印配送单据,与收货方逐项核对并完成签字确认。纸质回单不仅是履约凭证,也是后续对账与责任追溯的重要依据。
但在真实配送环境中,作业条件通常较为复杂:
- 无固定网络或 PC 设备
- 网络环境不稳定,甚至可能离线
- 作业地点分散且高度流动
在这种场景下,传统依赖固定设备与网络的打印方案难以落地。另一方面,配送业务的单据流转、任务执行与签收确认均在小程序中完成。
基于上述业务形态和环境约束,我们采用了如下方案:
小程序 + 便携式热敏打印机 + BLE
实现移动端直连打印能力,完成现场打印与签收闭环。
本文将围绕该方案,从工程实现角度拆解 BLE 打印链路,包含以下几个方面:
- 为什么选择 BLE:对比经典蓝牙与 BLE 的差异,明确选型依据
- BLE 通信模型:理解 GATT 如何抽象打印机能力
- 连接建立与生命周期管理:从权限校验到连接释放的完整流程
- ESC/POS 指令模型:构建打印机可识别的指令数据
- 数据传输机制:解决分包、队列与发送可靠性问题
- 图片打印:实现从彩色图像到黑白点阵的转换
二、为什么选择 BLE,而不是传统蓝牙?
在移动端驱动便携式热敏打印机的方案中,蓝牙是最常见的设备通信方式。然而,我们日常所说的"蓝牙",并非单一技术标准,而是两个独立演进的无线通信分支。
蓝牙技术分支:经典蓝牙 vs BLE
蓝牙技术联盟自 1999 年发布蓝牙 1.0 以来,技术规范经历了多次迭代。其中最关键的分水岭出现在 2010 年------蓝牙 4.0 规范的发布,首次将低功耗蓝牙(Bluetooth Low Energy,BLE)作为独立技术分支引入,与此前的经典蓝牙(BR/EDR)形成并行体系。
经典蓝牙(BR/EDR):
- 诞生于蓝牙 1.0 ~ 3.0
- 设计目标:替代短距离有线电缆,承载持续数据流
- 典型应用:音频传输(A2DP)、免提通话(HFP)、文件传输
- 特点:持续连接、高吞吐、功耗较高
低功耗蓝牙(BLE):
- 随蓝牙 4.0 引入,并在 5.x 持续增强
- 设计目标:以极低功耗支持间歇性、小数据量通信
- 典型应用:传感器设备、IoT 终端、便携式打印机
- 特点:短连接、小数据包、超低功耗
可以用一句话概括两者差异:
经典蓝牙是为"持续对话"设计的,而 BLE 是为"偶尔说一句"设计的。
从工程角度看:
- 经典蓝牙解决的是"持续数据传输"问题
- BLE 解决的是"高效指令交互"问题
虽然共享"蓝牙"之名,但两者在协议模型、连接机制与功耗设计上完全不同。接下来将从连接模型、系统支持与通信特性等维度展开对比。
连接模型差异
经典蓝牙(BR/EDR)通常基于SPP(Serial Port Profile ,串口仿真协议 ) 向上层提供数据传输能力,本质上是建立一条持续的字节流通道,主要用于串口数据通信等场景。其特点是:
- 面向流式数据传输
- 提供连续字节流
- 需要应用层自行实现分帧与协议切分
而 BLE 基于 GATT(Generic Attribute Profile,通用属性规范),将设备能力抽象为服务(Service)与特征值(Characteristic)的层级化数据结构,通信方式是围绕"特征值(Characteristic)"进行的读写与通知机制,更适合:
- 小数据包
- 指令型交互
- 间歇性通信
在打印场景中,本质上传输的是 ESC/POS 指令流------一组结构清晰、长度有限的二进制命令序列,而不是持续大流量数据,因此:
GATT 模型天然更适合打印这种"指令驱动型通信"。
系统与小程序能力约束
在小程序运行环境中,蓝牙能力通过 JSAPI 封装,其底层依赖操作系统蓝牙协议栈。
经典蓝牙的困境:
- iOS 对经典蓝牙串口协议(SPP/RFCOMM)实施严格的 MFi 认证管控,未经认证的设备无法被第三方 App 通过公开 API 访问。
- Android 虽理论上支持经典蓝牙 SPP,但各厂商系统定制层碎片化严重,且 Android 12+ 对蓝牙权限的收紧进一步增加了连接的不确定性。
- 在小程序体系中,蓝牙能力主要围绕 BLE(GATT)模型开放。经典蓝牙相关能力即便在部分平台存在,也缺乏统一标准与跨平台一致性,且在 iOS 上受限于 MFi 机制基本不可用。
因此,在工程实践中:
经典蓝牙难以作为稳定、可控的通信方案使用。
BLE 的确定性优势:
- iOS 自 iOS 5 起通过 CoreBluetooth 框架完全开放 BLE 协议栈。
- Android 自 4.3(API 18)起原生支持 BLE 中心模式。
- 小程序提供完整的 BLE 链路 API。
换句话说:
BLE 是"系统优先支持"的标准能力,而经典蓝牙存在平台差异性风险。
连接稳定性与重连成本
在门店实际使用中,打印设备通常呈现"短连接、多设备、频繁切换"的特征。经典蓝牙的连接过程通常包括:
- 设备发现(Inquiry / Page)
- 配对(Pairing)
- 绑定(Bonding)
- 建链(SPP Channel Establish)
这一过程在实际设备上往往存在:
- 首次连接耗时 2-5 秒,体验迟滞;
- 配对状态强依赖系统蓝牙缓存,清除缓存后需重新配对;
- 多设备切换时,原绑定关系可能干扰新连接;
- 异常断连后,底层恢复机制不稳定,偶发需重启蓝牙服务。
而 BLE 的连接模型相对轻量:
- 无强制配对流程(可采用 Just Works 模式,无弹窗交互)
- 连接建立速度通常在 100-300 ms;
- 断开后重连仅需重新发起
connect请求,无历史绑定负担; - 天然适配"按单连接、用完即断"的业务模式。
因此:
BLE 更适合"按需连接、用完即断"的业务模式。
数据传输模型更适合打印协议
热敏打印本质是 ESC/POS 指令流输出,其数据特征为:
- 数据量小(单次回单通常为 1-10 KB);
- 结构固定(初始化命令 + 文本行 + 条码/二维码位图 + 走纸切纸命令);
- 强时序要求(指令顺序不可乱,丢包将导致格式错乱或走纸异常)。
BLE 的写入方式(Write Characteristic / Write Without Response)配合 MTU 分包机制,可以很好支持:
- 小包分片传输(单次写入数据受 ATT MTU 限制,默认约 20 字节,部分设备可协商至更大);
- 应用层流控(根据
write回调成功率控制发送节奏); - 避免阻塞 UI 线程(API 均为异步回调设计);
- 支持 Notify 状态回传(实时感知打印机缺纸、过热等异常)。
相比之下:
BLE 的"受限分包模型"反而更适合 ESC/POS 这种指令流传输。
多维度对比总览
从工程落地角度来看,将经典蓝牙与 BLE 的核心差异归纳如下:
| 维度 | 经典蓝牙 | BLE |
|---|---|---|
| 通信模型 | 流式 | 特征值 |
| 协议 | SPP | GATT |
| 连接 | 长连接 | 短连接 |
| 建连耗时 | 2~5s | 100~300ms |
| 功耗 | 高 | 低 |
| iOS 支持 | 受限 | 完全开放 |
| 小程序支持 | 不统一 | 标准支持 |
小结
综合协议模型、系统支持以及小程序运行环境的约束可以看到:
经典蓝牙虽然在带宽与成熟度上具备优势,但在移动端尤其是 iOS 与小程序体系中,存在明显的可达性与一致性问题;而 BLE 在系统支持、连接模型与工程可控性上更符合当前场景。因此,在小程序驱动便携式打印机的场景下:
BLE 并不是"更优选择",而是在现有平台约束下"可落地且稳定"的通信方案。
在明确选择 BLE 后,需要理解其核心通信机制------GATT。这是理解后续设备连接、服务发现与数据交互流程的认知基础。
三、BLE 通信模型
在 BLE 中,并不存在类似串口或 Socket 的持续数据通道。与经典蓝牙基于 SPP 提供"流式传输"不同,BLE 的通信建立在 GATT(Generic Attribute Profile)模型之上。
GATT 将设备能力抽象为一组层级化的数据结构,所有数据交互都围绕这些结构展开,而不是通过一条持续的数据流进行传输。
GATT 的基本结构
在 GATT 模型中,一个 BLE 设备可以抽象为为一棵层级结构
服务(Service)
服务是设备某项功能的逻辑集合,由一个或多个特征值组成。简单来说就是:
设备"对外声明的功能模块"------它能提供哪些能力
每个服务通过 UUID(通用唯一标识符) 唯一标识。UUID 可以分两类:
- 16-bit 标准 UUID
- 128-bit 自定义
16-bit 标准 UUID(SIG 定义)
格式:
0000xxxx-0000-1000-8000-00805f9b34fb
其中:
xxxx= 标准服务编号- 后缀
0000-1000-8000-00805f9b34fb= Bluetooth Base UUID
常见标准 Service(SIG 定义)
| Service | UUID | 含义 |
|---|---|---|
| Generic Access | 0x1800 | 设备基础信息 |
| Generic Attribute | 0x1801 | GATT 控制服务 |
| Device Information | 0x180A | 设备信息 |
| Battery Service | 0x180F | 电池信息 |
| Heart Rate | 0x180D | 心率(穿戴设备) |
特点:
- 属于 BLE 官方标准
- 这些服务本身不承载打印数据,但在工程中可用于读取设备信息、电量等辅助功能
128-bit 自定义 UUID
格式:
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
例如:
49535343-FE7D-4AE5-8FA9-9FAFD205E4550000FFF0-0000-1000-8000-00805F9B34FB
特点
- 厂商自定义
- 用于:
- 打印机
- 扫码枪
- IoT 设备
- ❗ 是否支持打印完全取决于厂商实现
示例
serviceId 示意图:

注意:ios 系统的serviceId 会进行简化,如下图:

特征值(Characteristic)
特征值是服务下的具体数据节点,是客户端真正进行读写操作的对象。可以把它理解为:
每个功能模块下的"具体操作入口"------能力如何被访问
一个特征值通常包含三个关键信息:
- UUID:该特征值的唯一标识。
- Properties(操作属性) :定义支持的操作类型,常见值包括:
Read:可读Write:可写(带响应)Write Without Response:无响应写入Notify:可通知
- Value(值):实际存储的数据。
在打印场景中,最核心的特征值有两类:
- 写入特征值 :Property 包含
Write或Write Without Response,用于向打印机发送 ESC/POS 指令流。 - 通知特征值 :Property 包含
Notify,用于打印机主动向客户端上报状态(缺纸、打印完成等)。
示例图如下:

描述符(Descriptor)与 CCCD
描述符是特征值的附加元数据,用于补充说明特征值的行为。其中最重要的是 CCCD(Client Characteristic Configuration Descriptor,客户端特征配置描述符),
CCCD 的作用,是让客户端配置某个特征值是否启用通知或指示:
- 启用
Notify:通常写入0x0001 - 启用
Indicate:通常写入0x0002
对于支持 Notify 的特征值,客户端必须向 CCCD 写入 0x0001 才能启用通知功能。此后当打印机状态发生变化时,才会主动向客户端推送数据。当你调用notifyBLECharacteristicValueChange 并设置 state: true 时,小程序底层就是在向该特征值的 CCCD 描述符写入 0x0001。
Write 两种写入方式
向写入特征值发送数据时,BLE 提供两种写入模式:
- Write Request(带响应写入):客户端每发送一包数据,打印机必须回复 Write Response 以确认接收成功。优点是可靠------应用层能明确感知每一包是否送达;缺点是吞吐量较低,每次写入都需等待对端确认。
- Write Command / Write Without Response(无响应写入):客户端连续发送数据包,打印机不回复确认。这种方式显著提升了传输速率,但可靠性依赖于底层链路层的重传机制。
在打印场景中,由于单次回单数据量较小(1-10KB),且对实时性要求较高,Write Without Response 是常用选择。
Notify 状态主动上报
Notify 是 BLE 设备主动向客户端推送数据的能力。在打印场景中,Notify 用于接收打印机的状态反馈,典型事件包括:
- 打印完成
- 缺纸报警
- 机盖打开
- 热敏头过热保护
- 缓冲区状态(可接收新数据)
Notify 的本质是设备主动推送状态变化,而非客户端轮询。客户端订阅成功后,只要打印机状态发生变化,设备就可以主动推送数据给小程序。
小结
GATT 模型将 BLE 打印机的交互简化为两个核心操作:
- 向"写入特征值"写入数据 → 发送 ESC/POS 打印指令
- 订阅"通知特征值" → 接收打印机状态上报
有了这套模型,后续的 BLE 连接建立、服务发现、特征筛选、写入与订阅流程就会非常清晰。\ 在理解了 BLE 的 GATT 通信模型之后,接下来将进入实际工程实现层面,介绍在小程序中如何完成 BLE 打印设备的扫描、连接建立与断开管理流程。
