微信小程序蓝牙连接小票打印机:从扫描到打印的完整实战指南

微信小程序蓝牙连接小票打印机:从扫描到打印的完整实战指南 简介这份PDF文档面向微信小程序开发者尤其是需要实现蓝牙外设通信、对接小票打印机的初中级工程师。内容围绕微信小程序蓝牙API展开系统讲解从开启蓝牙适配器、搜索附近设备、建立BLE连接到获取服务ID与特征值、写入打印数据、设置通知接收反馈的完整链路并强调异步回调处理与加载提示等体验细节。资源包共1个PDF文件约44KB篇幅精炼适合作为手边速查与代码对照材料。目前已有2855人学习下载说明其在同类蓝牙打印场景中具备一定参考价值。读者可从中获得可直接借鉴的实例代码片段、关键API调用顺序、特征值筛选逻辑以及连接失败与找不到读写特征值时的排错思路帮助快速搭建可运行的小程序蓝牙打印原型减少自行摸索成本。1. 从一次收银台卡纸说起微信小程序蓝牙连接小票打印机的真实门槛很多做零售、餐饮 SaaS 的团队都会遇到这个需求收银员在微信小程序里点“打印小票”旁边那台 58mm 热敏小票打印机就开始吐纸。听起来简单但真正动手时第一道坎往往不是打印指令而是蓝牙连接本身——iOS 和 Android 的差异、设备 ID 每次扫描都变、连接后写特征值失败、中文乱码、切后台断连这些问题会一个接一个冒出来。微信小程序提供了wx.openBluetoothAdapter、wx.startBluetoothDevicesDiscovery、wx.createBLEConnection这一整套低功耗蓝牙BLEAPI理论上可以完成从扫描到写入的全部流程。但小票打印机大多是经典蓝牙SPP或 BLE 双模设备小程序只能走 BLE 通道所以选型阶段就要确认打印机支持 BLE 透传。这篇文章面向已经会写小程序页面、但对蓝牙通信不熟的开发者把扫描、连接、服务发现、指令拼装、异常重连这条链路拆开讲清楚代码可以直接抄进项目里改。2. 微信小程序 BLE 连接小票打印机的完整链路与关键 API2.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 以上。本文还有配套的精品资源点击获取