简介:这份PDF文档面向微信小程序开发者,尤其是需要实现蓝牙外设通信、对接小票打印机的初中级工程师。内容围绕微信小程序蓝牙API展开,系统讲解从开启蓝牙适配器、搜索附近设备、建立BLE连接,到获取服务ID与特征值、写入打印数据、设置通知接收反馈的完整链路,并强调异步回调处理与加载提示等体验细节。资源包共1个PDF文件,约44KB,篇幅精炼,适合作为手边速查与代码对照材料。目前已有2855人学习下载,说明其在同类蓝牙打印场景中具备一定参考价值。读者可从中获得可直接借鉴的实例代码片段、关键API调用顺序、特征值筛选逻辑以及连接失败与找不到读写特征值时的排错思路,帮助快速搭建可运行的小程序蓝牙打印原型,减少自行摸索成本。
1. 从一次收银台卡纸说起:微信小程序蓝牙连接小票打印机的真实门槛
很多做零售、餐饮 SaaS 的团队都会遇到这个需求:收银员在微信小程序里点“打印小票”,旁边那台 58mm 热敏小票打印机就开始吐纸。听起来简单,但真正动手时,第一道坎往往不是打印指令,而是蓝牙连接本身——iOS 和 Android 的差异、设备 ID 每次扫描都变、连接后写特征值失败、中文乱码、切后台断连,这些问题会一个接一个冒出来。
微信小程序提供了wx.openBluetoothAdapter、wx.startBluetoothDevicesDiscovery、wx.createBLEConnection这一整套低功耗蓝牙(BLE)API,理论上可以完成从扫描到写入的全部流程。但小票打印机大多是经典蓝牙(SPP)或 BLE 双模设备,小程序只能走 BLE 通道,所以选型阶段就要确认打印机支持 BLE 透传。这篇文章面向已经会写小程序页面、但对蓝牙通信不熟的开发者,把扫描、连接、服务发现、指令拼装、异常重连这条链路拆开讲清楚,代码可以直接抄进项目里改。
2. 微信小程序 BLE 连接小票打印机的完整链路与关键 API
2.1 为什么小票打印机必须走 BLE 而不是经典蓝牙
微信小程序的蓝牙能力只开放了低功耗蓝牙(BLE)接口,没有暴露经典蓝牙 SPP 的 socket 通道。市面上常见的 58mm 小票打印机,如果只支持经典蓝牙,在小程序里是搜不到、连不上的。所以采购或对接前,一定要确认设备支持 BLE 4.0 及以上,并且厂商提供了 BLE 透传的写特征值(write characteristic)。
常见做法是:先用手机系统蓝牙或厂商 App 确认设备能被 BLE 扫描工具发现,再拿到它的deviceId、serviceId、characteristicId三个关键标识。这三个值在小程序里分别对应wx.createBLEConnection、wx.getBLEDeviceServices、wx.getBLEDeviceCharacteristics的返回结果。
注意:iOS 上
deviceId是系统分配的 UUID,同一台打印机在不同手机上不一样;Android 上通常是 MAC 地址。不要把deviceId硬编码进代码,每次使用前重新扫描获取。
2.2 扫描、连接、服务发现的最小可用代码
下面这段代码把“初始化适配器 → 开始扫描 → 连接 → 拿服务 → 拿特征值”串成一条链,每一步都做了错误分支。
// pages/printer/printer.js Page({ data: { deviceId: '', serviceId: '', characteristicId: '' }, // 1. 初始化蓝牙适配器 initBluetooth() { wx.openBluetoothAdapter({ success: () => { this.startDiscovery(); }, fail: (err) => { // 10001 表示蓝牙未开启 console.error('适配器初始化失败', err); wx.showToast({ title: '请打开手机蓝牙', icon: 'none' }); } }); }, // 2. 开始扫描,按名称过滤目标打印机 startDiscovery() { wx.startBluetoothDevicesDiscovery({ allowDuplicatesKey: false, success: () => { wx.onBluetoothDeviceFound((res) => { const device = res.devices.find(d => d.name && d.name.includes('Printer')); if (device) { this.setData({ deviceId: device.deviceId }); wx.stopBluetoothDevicesDiscovery(); // 找到即停,省电 this.connectDevice(device.deviceId); } }); } }); }, // 3. 建立连接 connectDevice(deviceId) { wx.createBLEConnection({ deviceId, timeout: 10000, success: () => { this.getServices(deviceId); }, fail: (err) => { console.error('连接失败', err); } }); }, // 4. 获取服务列表,找到写特征值 getServices(deviceId) { wx.getBLEDeviceServices({ deviceId, success: (res) => { // 常见打印机主服务 UUID 以 FF00 或 E7810A71 开头 const target = res.services.find(s => s.uuid.toUpperCase().includes('FF00')) || res.services[0]; this.setData({ serviceId: target.uuid }); this.getCharacteristics(deviceId, target.uuid); } }); }, // 5. 获取特征值,筛选支持 write 的那个 getCharacteristics(deviceId, serviceId) { wx.getBLEDeviceCharacteristics({ deviceId, serviceId, success: (res) => { const writeChar = res.characteristics.find(c => c.properties.write); if (writeChar) { this.setData({ characteristicId: writeChar.uuid }); } } }); } });逻辑说明:openBluetoothAdapter是所有 BLE 操作的前置条件,失败最常见的原因是用户没开蓝牙或系统未授权。startBluetoothDevicesDiscovery的allowDuplicatesKey: false可以避免同一设备重复上报,减少回调压力。找到目标设备后立刻stopBluetoothDevicesDiscovery,因为扫描和连接同时进行会显著降低连接成功率,这是很多人第一次做蓝牙时踩的坑。
参数说明:createBLEConnection的timeout单位是毫秒,默认值偏短,建议设到 10000。getBLEDeviceServices返回的services数组里,主服务通常排在最前,但不同厂商 UUID 不同,稳妥做法是按已知前缀匹配,匹配不到再取第一个。
2.3 连接参数与常见失败码对照
| 失败场景 | 典型 errCode | 处理方式 |
|---|---|---|
| 蓝牙未开启 | 10001 | 引导用户打开系统蓝牙 |
| 设备未找到 | 10002 | 检查打印机是否已配对占用 |
| 连接超时 | 10003 | 先 stopDiscovery 再重连 |
| 服务未发现 | 10004 | 延迟 500ms 再调 getBLEDeviceServices |
| 特征值不支持写 | 10005 | 换一个 characteristic 或确认设备型号 |
这张表建议直接放进项目的错误处理函数里,按 errCode 给出不同的用户提示,而不是统一弹“连接失败”。
3. 小票打印指令拼装:从文本到 ESC/POS 字节流
3.1 ESC/POS 指令集在小程序里的字节处理
小票打印机普遍兼容 ESC/POS 指令集,核心是把文本和格式控制符转成ArrayBuffer,再通过wx.writeBLECharacteristicValue写入。小程序里没有 Node 的 Buffer,需要用Uint8Array手动拼。
// utils/escpos.js // 将字符串按 GBK 编码转成字节数组(打印机通常用 GBK) function strToGBKBytes(str) { // 小程序无内置 GBK 编码,需引入 encoding 库或使用厂商提供的映射表 // 这里以 UTF-8 演示结构,实际项目替换为 GBK 编码函数 const utf8 = unescape(encodeURIComponent(str)); const bytes = []; for (let i = 0; i < utf8.length; i++) { bytes.push(utf8.charCodeAt(i)); } return bytes; } // 拼装一张小票的完整指令 function buildReceipt(lines) { const cmd = []; // 初始化打印机 cmd.push(0x1B, 0x40); // 居中 + 放大字体打印标题 cmd.push(0x1B, 0x61, 0x01); cmd.push(0x1D, 0x21, 0x11); cmd.push(...strToGBKBytes('销售小票\n')); // 恢复默认字体,左对齐 cmd.push(0x1D, 0x21, 0x00); cmd.push(0x1B, 0x61, 0x00); // 逐行打印内容 lines.forEach(line => { cmd.push(...strToGBKBytes(line + '\n')); }); // 走纸 3 行并切纸 cmd.push(0x1B, 0x64, 0x03); cmd.push(0x1D, 0x56, 0x42, 0x00); return new Uint8Array(cmd).buffer; } module.exports = { buildReceipt };逻辑说明:0x1B 0x40是初始化指令,每次打印前必须发,否则上一次的格式会残留。0x1B 0x61控制对齐,参数0x00左对齐、0x01居中。0x1D 0x21控制字体大小,0x11表示宽高各放大一倍。切纸指令0x1D 0x56 0x42 0x00是全切,部分机型用0x1D 0x56 0x41 0x00半切。
参数说明:中文必须用 GBK 编码,UTF-8 直接写会乱码。小程序本身没有 GBK 编码函数,常见做法是引入一个轻量的编码转换库,或者让后端把文本转成 GBK 字节数组的 base64 再下发,前端只负责拼指令头尾。
3.2 分包写入与写入节奏控制
BLE 单次写入有长度限制,iOS 通常 20 字节,Android 可协商到 512 字节。一张小票动辄几百字节,必须分包。
// 分包写入,每包 20 字节,间隔 20ms function writeInChunks(deviceId, serviceId, characteristicId, buffer) { const chunkSize = 20; const data = new Uint8Array(buffer); let offset = 0; function writeNext() { if (offset >= data.length) return; const chunk = data.slice(offset, offset + chunkSize); wx.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId, value: chunk.buffer, success: () => { offset += chunkSize; setTimeout(writeNext, 20); // 控制节奏,避免丢包 }, fail: (err) => { console.error('写入失败', err); } }); } writeNext(); }逻辑说明:setTimeout的 20ms 间隔是经验值,太快会导致打印机缓冲区溢出丢指令,太慢则打印明显卡顿。如果打印机支持 MTU 协商,可以在连接后调用wx.setBLEMTU把单包提到 200 字节以上,减少分包次数。
注意:
wx.setBLEMTU只在 Android 上有效,iOS 由系统自动协商,不要依赖它做跨平台统一。
4. 真机调试与断连重连的实战排错
4.1 iOS 与 Android 的连接差异排查
iOS 上deviceId是 UUID,每次扫描可能不同,且系统会缓存已配对设备,导致onBluetoothDeviceFound有时不回调。解决办法是监听wx.onBluetoothAdapterStateChange,在适配器可用后延迟 300ms 再开始扫描。Android 上deviceId是 MAC 地址,相对稳定,但部分机型需要先申请定位权限才能扫描到 BLE 设备。
真机调试时,开发者工具里的蓝牙模拟基本不可用,必须用真机。常见现象是:开发者工具能连上,真机连不上——这通常是权限或系统蓝牙缓存问题。处理步骤是:关闭手机蓝牙再打开、删除系统里该打印机的配对记录、重启小程序。
4.2 监听连接状态并实现自动重连
BLE 连接在切后台、信号干扰、打印机休眠时都会断开,必须监听onBLEConnectionStateChange。
// 在连接成功后注册监听 wx.onBLEConnectionStateChange((res) => { if (!res.connected) { console.warn('连接已断开', res.deviceId); // 延迟 1s 重连,避免频繁重试 setTimeout(() => { this.connectDevice(res.deviceId); }, 1000); } });逻辑说明:重连前要确保deviceId仍然有效,如果设备已关机,重连会一直失败,需要加一个最大重试次数(比如 3 次),超过后提示用户检查打印机电源。另外,重连成功后必须重新调用getBLEDeviceServices和getBLEDeviceCharacteristics,因为服务句柄可能已经变化。
参数说明:onBLEConnectionStateChange是全局监听,注册一次即可,不要在每个页面重复注册,否则会触发多次回调。建议在app.js的onLaunch里统一管理蓝牙状态。
4.3 打印内容乱码与切纸异常的定位方法
乱码九成是编码问题:确认打印机支持的编码(GBK 居多),确认发送的字节数组没有被二次转码。切纸异常通常是切纸指令不被机型支持,可以先用厂商提供的测试指令集逐条验证。定位方法很简单:先只发初始化指令和一行英文,确认能打;再加中文,确认编码;最后加切纸,确认指令。每加一步就真机验证一次,比一次性拼完整张小票再排查要快得多。
5. 把打印能力封装成可复用模块的进阶技巧
5.1 用 Promise 封装蓝牙链路,避免回调地狱
上面的代码是回调风格,实际项目里建议封装成 Promise,页面里用async/await调用。
// utils/ble.js function openAdapter() { return new Promise((resolve, reject) => { wx.openBluetoothAdapter({ success: resolve, fail: reject }); }); } function connect(deviceId) { return new Promise((resolve, reject) => { wx.createBLEConnection({ deviceId, timeout: 10000, success: resolve, fail: reject }); }); } // 页面里使用 async printReceipt() { try { await openAdapter(); await connect(this.data.deviceId); const buffer = buildReceipt(this.data.lines); await writeInChunks(this.data.deviceId, this.data.serviceId, this.data.characteristicId, buffer); wx.showToast({ title: '打印成功' }); } catch (e) { wx.showToast({ title: '打印失败', icon: 'none' }); } }逻辑说明:Promise 封装后,每个蓝牙步骤的失败都能被try/catch统一捕获,页面逻辑更清晰。注意writeInChunks也要改成返回 Promise,在最后一包写完时 resolve。
5.2 打印队列与并发控制
收银场景可能连续点多次打印,如果并发写入,BLE 通道会乱序。做法是维护一个打印队列,前一张打完再打下一张。
| 队列状态 | 处理动作 |
|---|---|
| 空闲 | 立即执行打印 |
| 打印中 | 新任务入队,等待 |
| 队列长度 > 5 | 拒绝新任务,提示稍后 |
队列用数组实现,每次打印完成后shift出下一个任务。这个机制能有效避免“点了打印没反应”或“打出半张”的问题。
5.3 一个容易被忽略的细节:打印完成后延迟断开
打印指令写入成功不代表打印机已经打完,尤其是走纸和切纸需要时间。如果写完立刻断开连接,最后几包指令可能还没被打印机处理。稳妥做法是在写入完成后延迟 500ms 到 1s 再断开,或者监听打印机的状态特征值(部分机型支持)确认打印完成。这个延迟值可以根据机型调整,58mm 打印机一般 500ms 足够,80mm 打印机建议 800ms 以上。
本文还有配套的精品资源,点击获取