ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

uni-app H5+蓝牙打印全攻略:从ESC/POS指令到跨端硬件交互

uni-app H5+蓝牙打印全攻略:从ESC/POS指令到跨端硬件交互 1. 项目概述当移动H5应用需要“开口说话”如果你正在用uni-app开发一个需要线下交付的H5应用比如仓库盘点、门店收银、外卖接单那么“打印”这个功能很可能就是打通线上数据与线下物理世界的最后一道关卡。想象一下用户在手机浏览器里完成了一笔订单却需要跑到固定的电脑前去打印小票这个体验的割裂感是致命的。而蓝牙打印机凭借其无线、便携、即连即打的特性成为了移动H5场景下最理想的输出终端。然而传统的Web技术JavaScript在浏览器中无法直接与蓝牙设备交互这是一个出于安全考虑的设计限制。这正是“uni-app H5”方案的价值所在。它并非指某个具体的“H5”框架而是指在uni-app开发中通过其提供的条件编译和原生插件能力让我们的H5应用在特定的容器环境如App、或集成了原生能力的WebView中获得调用手机原生蓝牙API的权限从而连接并驱动蓝牙打印机。这个项目的核心就是解决“如何在纯粹的H5页面里实现与蓝牙打印机的直接对话”并完成从普通文本到复杂二维码的精准打印。整个过程涉及几个关键角色你的uni-app代码、一个能提供原生蓝牙桥接的运行环境通常是打包成的App、一台支持蓝牙串口协议SPP的ESC/POS指令打印机。最终达成的效果是用户在你的H5页面上点击“打印”手机就能自动搜索附近的蓝牙打印机连接后直接发送指令打印机随即吐出清晰的小票或标签上面既有订单详情也有用于核销或追溯的二维码。2. 核心思路与技术选型为什么是“H5”而非纯H5在深入代码之前我们必须理清技术路径。纯浏览器环境下的JavaScript被沙箱严格限制无法访问底层硬件接口这是所有Web前端开发者面对硬件交互时共同的“痛”。因此直接让网页连接蓝牙打印机是一条死胡同。2.1 技术路径对比与决策面对这个需求通常有几种思路服务器端打印H5页面将数据提交到服务器服务器生成PDF或图片再通过网络发送给连接在服务器上的网络打印机。这种方式依赖稳定的网络和固定的打印机完全丧失了移动性和灵活性不适合移动收银、移动盘点等场景。本地客户端软件开发一个桌面客户端内嵌浏览器控件显示H5页面客户端提供本地API供网页调用。这需要用户额外安装软件分发和维护成本高。uni-app 原生渲染也就是我们选择的“H5”路径。这里的“”号加的就是原生能力。为什么这是最优解uni-app本身是一个使用Vue.js开发跨平台应用的前端框架。当我们将uni-app项目运行到App平台如Android、iOS时它并非一个简单的WebView而是一个集成了WebView渲染引擎和原生能力桥接JS Bridge的混合应用容器。在这个容器里uni-app通过条件编译可以调用由平台原生代码Java/Swift实现的API模块。对于蓝牙uni-app官方提供了uni.connectBluetoothDevice等API但这些API在纯H5环境下是不可用的。它们只在App端和各家小程序平台生效。因此我们的项目本质上是一个uni-app的App项目其界面用Vue.js编写看起来和H5一样但运行在App容器中从而获得了蓝牙权限。对于用户而言他们只需要安装一个App里面的所有页面都是熟悉的H5交互体验却能完成硬件操作这就是“H5”体验的核心。2.2 打印机指令协议ESC/POS是关键确定了调用蓝牙的路径下一步要解决“说什么语言”的问题。热敏打印机无论是蓝牙、Wi-Fi还是USB通常都支持一种叫做ESC/POS的指令集。这不是一个高级编程语言而是一套由爱普生公司制定的、通过串口发送的二进制命令集。你可以把它理解为打印机的“机器语言”。每一行文本、每一次换行、加粗、对齐、打印二维码都对应着一段特定的十六进制指令。例如0x1B 0x40是初始化打印机。0x1B 0x61 0x01是设置文本居中对齐。0x1D 0x6B 0x04 ...是打印二维码的指令。我们的任务就是在JavaScript中根据要打印的内容动态拼接出正确的ESC/POS指令字节数组然后通过蓝牙连接将这个字节数组发送给打印机。打印机接收到这些原始指令后就会忠实地执行打印动作。注意不同品牌、型号的打印机对ESC/POS指令的支持程度可能有细微差异尤其是在二维码、中文编码、图形打印等方面。在项目启动前务必找到你所使用打印机的详细指令手册进行核对。市面上常见的佳博、芯烨、商米等品牌的热敏打印机基本都兼容标准的ESC/POS。3. 环境准备与项目初始化理论清晰后我们开始动手搭建。假设你已经具备基本的uni-app开发环境HBuilder X。3.1 创建uni-app项目与基础配置首先我们创建一个新的uni-app项目。在HBuilder X中选择“文件” - “新建” - “项目”选择“uni-app”类型模板选用“默认模板”即可。项目名称可以定为BluetoothPrinterDemo。创建完成后打开项目根目录下的manifest.json文件。这是uni-app的应用配置文件我们需要在此声明蓝牙权限。找到app-plus-distribute-android节点配置权限。对于iOS配置在ios节点下。// manifest.json 部分配置 app-plus: { distribute: { android: { permissions: [ uses-permission android:name\android.permission.BLUETOOTH\/, uses-permission android:name\android.permission.BLUETOOTH_ADMIN\/, uses-permission android:name\android.permission.ACCESS_FINE_LOCATION\/, uses-permission android: name\android.permission.ACCESS_COARSE_LOCATION\/ ] }, ios: { UIBackgroundModes: [bluetooth-central], privacyDescription: { NSBluetoothPeripheralUsageDescription: App需要您的同意来搜索并连接蓝牙打印机, NSBluetoothAlwaysUsageDescription: App需要您的同意来搜索并连接蓝牙打印机 } } } }关键点解析Android除了蓝牙权限还必须申请定位权限。这是因为从Android 6.0开始扫描蓝牙低功耗BLE设备需要位置权限。虽然我们连接的是经典蓝牙SPP但很多API和系统行为仍与此关联申请定位权限是兼容性最佳实践。iOS需要在Info.plist通过manifest.json配置中声明蓝牙使用描述否则应用无法弹出蓝牙搜索界面。UIBackgroundModes中的bluetooth-central是允许应用在后台保持蓝牙连接根据你的实际场景决定是否添加。3.2 页面布局与基础逻辑我们创建一个简单的打印测试页面printer-test.vue。这个页面将包含设备搜索列表、连接按钮、打印内容输入区和执行打印的按钮。template view classcontent button tapstartBluetoothDevicesDiscovery搜索蓝牙设备/button scroll-view scroll-y classdevice-list view v-fordevice in devices :keydevice.deviceId classdevice-item tapconnectDevice(device) text{{device.name || 未知设备}} ({{device.deviceId}})/text text v-ifdevice.connected stylecolor: green;[已连接]/text /view /scroll-view view classinput-area textarea v-modelprintText placeholder请输入要打印的文字... auto-height / input v-modelqrCodeText placeholder请输入二维码内容 / /view button typeprimary :disabled!connectedDeviceId tapprintReceipt打印测试小票/button /view /template script export default { data() { return { devices: [], // 搜索到的设备列表 connectedDeviceId: null, // 当前连接的设备ID printText: 欢迎使用蓝牙打印机测试\n这是一行测试文本。\n, qrCodeText: https://www.example.com/product/12345 }; }, methods: { // 方法将在后续步骤中实现 startBluetoothDevicesDiscovery() {}, connectDevice(device) {}, printReceipt() {} } }; /script style .content { padding: 20rpx; } .device-list { height: 300rpx; border: 1px solid #eee; margin: 20rpx 0; } .device-item { padding: 20rpx; border-bottom: 1px solid #f0f0f0; } .input-area { margin: 30rpx 0; } textarea, input { width: 100%; border: 1px solid #ccc; padding: 15rpx; box-sizing: border-box; margin-bottom: 20rpx; } /style这个页面搭建了一个最基础的UI一个搜索按钮、一个设备列表、两个输入框和一个打印按钮。打印按钮只有在有设备连接时才可用。4. 蓝牙设备搜索与连接实现这是与硬件交互的第一步。我们将使用uni对象提供的蓝牙API。4.1 初始化蓝牙模块与搜索设备在startBluetoothDevicesDiscovery方法中我们需要先打开蓝牙适配器然后开始搜索。async startBluetoothDevicesDiscovery() { // 1. 打开蓝牙适配器 try { const res await uni.openBluetoothAdapter(); console.log(蓝牙适配器打开成功, res); } catch (err) { uni.showToast({ title: 打开蓝牙失败: ${err.errMsg}, icon: none }); console.error(打开蓝牙失败, err); // 常见错误未打开手机蓝牙、不支持蓝牙 if (err.errCode 10001) { uni.showModal({ content: 手机蓝牙未打开是否去设置打开, success: (res) { if (res.confirm) { uni.openSystemBluetoothSetting(); // 此API部分平台支持 } } }); } return; } // 2. 开始搜索设备 uni.onBluetoothDeviceFound((devices) { // 注意devices是数组但每次回调可能只返回一个新发现的设备 const newDevices devices.devices.filter(device { // 过滤掉没有名字的设备可能是其他类型的蓝牙设备 // 重点热敏打印机通常有固定名称前缀如“GBTP”、“XP”、“POS” return device.name device.name.indexOf(GBTP) ! -1; // 以佳博打印机为例 }); newDevices.forEach(device { // 去重避免列表重复添加 if (!this.devices.some(d d.deviceId device.deviceId)) { this.devices.push({ deviceId: device.deviceId, name: device.name, connected: false }); } }); }); uni.startBluetoothDevicesDiscovery({ services: [], // 搜索所有设备。如果知道打印机的服务UUID可以填入以加速过滤 allowDuplicatesKey: false, // 不允许重复上报 success: (res) { console.log(开始搜索设备, res); uni.showToast({ title: 正在搜索设备..., icon: none }); // 搜索10秒后自动停止节省电量 setTimeout(() { this.stopDiscovery(); }, 10000); }, fail: (err) { uni.showToast({ title: 搜索失败: ${err.errMsg}, icon: none }); } }); }, stopDiscovery() { uni.stopBluetoothDevicesDiscovery(); uni.showToast({ title: 搜索已停止, icon: none }); }实操心得onBluetoothDeviceFound是一个监听事件一旦有设备被发现就会触发。它可能在短时间内被多次调用每次返回一个或几个设备。因此在更新设备列表时去重逻辑至关重要否则列表会迅速被重复项填满。搜索时指定services参数可以大幅提高效率。如果你知道打印机的蓝牙服务UUID通常可以在打印机手册或通过通用蓝牙调试App获取填在这里可以只发现目标打印机。留空则会发现所有蓝牙设备需要自己通过设备名称 (device.name) 进行过滤。一定要在适当的时候调用stopBluetoothDevicesDiscovery比如找到目标设备后或者设定一个超时时间。持续搜索非常耗电。4.2 连接目标设备并获取服务当用户点击列表中的设备时触发连接。async connectDevice(device) { if (this.connectedDeviceId) { uni.showModal({ content: 已连接设备 ${this.connectedDeviceId}是否断开并连接新设备, success: async (res) { if (res.confirm) { await this.closeBLEConnection(); // 先断开旧连接 this._connect(device); } } }); return; } this._connect(device); }, async _connect(device) { uni.showLoading({ title: 连接中... }); try { // 1. 创建低功耗蓝牙设备连接 const connectRes await uni.createBLEConnection({ deviceId: device.deviceId, timeout: 10000 // 10秒超时 }); console.log(连接建立成功, connectRes); // 2. 获取蓝牙设备所有服务 (service) const servicesRes await uni.getBLEDeviceServices({ deviceId: device.deviceId }); console.log(获取到服务列表, servicesRes.services); // 3. 遍历服务寻找特征值 (characteristic) // 蓝牙打印机通常使用“串口服务”其UUID通常是固定的0000FFE0-0000-1000-8000-00805F9B34FB const targetServiceId 0000FFE0-0000-1000-8000-00805F9B34FB; const service servicesRes.services.find(s s.uuid.toUpperCase() targetServiceId.toUpperCase()); if (!service) { throw new Error(未找到打印机串口服务); } // 4. 获取该服务下的所有特征值 const characteristicsRes await uni.getBLEDeviceCharacteristics({ deviceId: device.deviceId, serviceId: service.uuid }); console.log(获取到特征值列表, characteristicsRes.characteristics); // 我们需要找到“写”特征用于发送指令和“通知”特征用于接收状态可选 let writeCharId null; let notifyCharId null; for (let char of characteristicsRes.characteristics) { // 判断属性write表示可写notify表示可订阅通知 if (char.properties.write) { writeCharId char.uuid; } if (char.properties.notify || char.properties.indicate) { notifyCharId char.uuid; } } if (!writeCharId) { throw new Error(未找到可写的特征值无法发送打印指令); } // 5. 如果需要接收打印机状态则开启通知 if (notifyCharId) { await uni.notifyBLECharacteristicValueChange({ deviceId: device.deviceId, serviceId: service.uuid, characteristicId: notifyCharId, state: true }); // 监听特征值变化打印机返回的数据 uni.onBLECharacteristicValueChange((res) { console.log(收到打印机通知:, res.value); // 这里可以解析打印机状态如缺纸、开盖等 }); } // 6. 保存连接信息到全局或Vuex供打印时使用 this.connectedDeviceId device.deviceId; this._serviceId service.uuid; this._writeCharId writeCharId; // 更新设备列表中的连接状态 const index this.devices.findIndex(d d.deviceId device.deviceId); if (index ! -1) { this.$set(this.devices[index], connected, true); } uni.showToast({ title: 连接成功, icon: success }); uni.hideLoading(); } catch (err) { console.error(连接过程出错, err); uni.hideLoading(); uni.showToast({ title: 连接失败: ${err.errMsg || err.message}, icon: none }); // 连接失败尝试关闭连接 try { await uni.closeBLEConnection({ deviceId: device.deviceId }); } catch (closeErr) {} } }, async closeBLEConnection() { if (this.connectedDeviceId) { try { await uni.closeBLEConnection({ deviceId: this.connectedDeviceId }); } catch (err) { console.error(断开连接失败, err); } this.connectedDeviceId null; this._serviceId null; this._writeCharId null; // 更新设备列表状态 this.devices.forEach(device { if (device.connected) { device.connected false; } }); } }关键点与避坑指南从“发现”到“连接”的转换uni.startBluetoothDevicesDiscovery发现的是经典蓝牙和低功耗蓝牙BLE设备。但uni.createBLEConnection是低功耗蓝牙BLE的连接方式。绝大多数现代蓝牙热敏打印机尤其是2015年后生产的都支持BLE因为它更省电。如果你的打印机是老式的只支持经典蓝牙SPP上述方法将无法连接。这时你需要使用uni.createBLEConnection的经典蓝牙替代方案但uni-app官方API可能未直接提供。一种变通方案是使用原生插件这超出了本文基础范围。在购买打印机时务必确认其支持BLE。服务UUID是核心0000FFE0-0000-1000-8000-00805F9B34FB是蓝牙串口服务Serial Port Service的标准UUID绝大多数兼容SPP协议的蓝牙打印机都使用这个服务。如果连接后找不到此服务要么是打印机不支持标准SPP要么是连接方式不对经典蓝牙 vs BLE。特征值属性write属性必须为true我们才能通过这个特征值发送数据即打印指令。notify属性用于接收打印机主动上报的状态不是必须的但有了它可以实现更健壮的应用如检测缺纸错误。连接状态管理务必妥善管理连接状态。在连接新设备前断开旧连接在应用退出或页面销毁时主动关闭连接避免资源泄漏。5. ESC/POS指令生成与发送连接建立后最核心的部分来了生成打印机看得懂的指令并发送出去。5.1 构建基础指令工具函数我们先创建一个工具类或一组函数用于生成常见的ESC/POS指令。为了简单我们在同一个Vue文件的methods里或一个单独的printer-command.js模块中定义。// 在 printer-test.vue 的 methods 中或单独的工具文件 const PrinterCommand { // 初始化打印机 INIT: new Uint8Array([0x1B, 0x40]), // 设置对齐方式 0:左对齐 1:居中 2:右对齐 setAlign(align) { return new Uint8Array([0x1B, 0x61, align]); }, // 设置字体大小 (0size7, 通常0是正常1是双倍宽高等) setTextSize(size) { // 注意不同指令集此命令可能不同。这是常见的一种。 // 0x1D 0x21 后跟一个字节高4位为高度倍数低4位为宽度倍数 const n (size 0x0F) | ((size 0x0F) 4); // 宽高相同 return new Uint8Array([0x1D, 0x21, n]); }, // 设置加粗模式 (0:关闭 1:开启) setBold(enabled) { return new Uint8Array([0x1B, 0x45, enabled ? 0x01 : 0x00]); }, // 打印文本并换行 printTextLine(text) { // 将字符串转换为Uint8Array这里假设是GBK编码国内打印机常用 const encoder new TextEncoder(gbk); // 注意浏览器环境可能不支持所有编码可能需要polyfill const textBytes encoder.encode(text \n); // 添加换行符 return textBytes; }, // 走纸n行 feedLines(n) { return new Uint8Array([0x1B, 0x64, n]); }, // 切纸如果打印机支持 cutPaper() { // 全切 return new Uint8Array([0x1D, 0x56, 0x00]); // 部分切纸常用指令0x1D, 0x56, 0x41, 0x00 } };编码问题详解 这是打印中文最常见的大坑。热敏打印机内部通常没有复杂的字体库它们依靠接收到的字节流直接点阵打印。如果发送UTF-8编码的中文打印机很可能会打印出乱码。国内打印机普遍内置的是GBK或GB2312编码字库。因此我们需要将JavaScript的UTF-16字符串转换为GBK字节数组。浏览器原生的TextEncoder通常只支持UTF-8。我们需要一个GBK编码器。有几种方案方案一推荐使用第三方库引入iconv-lite或encoding-japanese等库。在uni-app中可以通过npm安装但需要注意包体积和兼容性。方案二简单项目可用如果打印内容固定或较少可以预先将中文字符串转换成GBK十六进制码硬编码在指令中。方案三利用原生能力在App端可以通过编写原生插件在JavaAndroid或SwiftiOS层进行编码转换这是最彻底但最复杂的方式。为了项目演示我们采用一个简化的方案假设主要打印英文、数字和少量固定中文或者使用一个轻量的GBK编码函数。这里我们引入一个非常简单的实现思路对于ASCII字符0-127直接使用其码点对于常见中文使用一个小的映射表。对于生产环境强烈建议使用成熟的编码库。// 一个极简的、不完整的GBK编码示例函数仅用于演示原理 function simpleTextToGbkBytes(text) { const bytes []; for (let i 0; i text.length; i) { const charCode text.charCodeAt(i); if (charCode 0x7F) { // ASCII bytes.push(charCode); } else { // 这里应该是一个庞大的GBK码表映射此处仅示例“测试”二字 // “测”的GBK编码是 0xB2 0xE2 // “试”的GBK编码是 0xCA 0xD4 // 实际项目中你需要一个完整的码表或库。 // 例如遇到“测”push(0xB2, 0xE2) // 为了演示我们暂时用UTF-8代替但这可能导致打印机乱码 // bytes.push(...new TextEncoder().encode(text[i])); // 这是UTF-8 // 更佳实践引入 iconv-lite 并调用 iconv.encode(text, gbk) } } return new Uint8Array(bytes); } // 修改printTextLine方法 printTextLine(text) { // 使用上面提到的编码库这里用伪代码 // const gbkBytes iconv.encode(text \n, gbk); // 演示中我们暂时用带BOM的UTF-8并告知打印机如果支持 // 更常见的做法是确保打印机设置为GBK然后发送GBK字节。 const encoder new TextEncoder(); const textBytes encoder.encode(text \n); return textBytes; }5.2 生成并打印二维码指令打印二维码是另一个核心需求。ESC/POS指令支持多种二维码模型如Model 2。指令相对复杂需要指定二维码类型、大小、纠错等级和数据内容。// 在 PrinterCommand 对象中添加方法 PrinterCommand.printQRCode function (data, size 6) { // 常用指令格式GS ( k pL pH cn fn n [数据] // fn49 (二维码) n50 (Model 2) // 1. 计算数据长度 const dataBytes this.stringToBytes(data); // 假设有一个将字符串转字节数组的方法 const len dataBytes.length 3; // 数据长度 3个固定参数 const pL len % 256; const pH Math.floor(len / 256); // 2. 构建指令数组 const cmdArray [ 0x1D, 0x28, 0x6B, // GS ( k pL, pH, // pL pH 0x31, 0x50, 0x30, // cn49, fn80, n48? 这里需要查具体手册不同打印机指令有差异 size // 二维码模块大小 (1-16) ]; // 3. 添加数据 cmdArray.push(...dataBytes); // 4. 添加结束符 (有些打印机需要) cmdArray.push(0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30); return new Uint8Array(cmdArray); }; // 一个辅助方法将字符串转为字节数组使用TextEncoder实际应用需处理编码 PrinterCommand.stringToBytes function (str) { const encoder new TextEncoder(); // 注意编码问题 return Array.from(encoder.encode(str)); };重要警告二维码打印指令因打印机品牌和型号差异巨大。上面的代码只是一个通用结构的示例很可能不适用于你的打印机。你必须查阅你的打印机指令手册找到确切的“打印二维码”指令序列。常见的指令头可能是[0x1D, 0x28, 0x6B, ...](GS k) 或[0x1B, 0x5A, ...](ESC Z)。参数顺序、含义如大小、纠错等级也各不相同。5.3 整合指令并发送打印任务现在我们实现最终的printReceipt方法它将组合文本、二维码等指令并通过蓝牙发送。async printReceipt() { if (!this.connectedDeviceId || !this._writeCharId) { uni.showToast({ title: 请先连接打印机, icon: none }); return; } uni.showLoading({ title: 打印中... }); // 1. 构建完整的指令字节流 let commandBuffer []; // 初始化打印机 commandBuffer.push(...PrinterCommand.INIT); // 设置居中对齐 commandBuffer.push(...PrinterCommand.setAlign(1)); // 设置大字体 commandBuffer.push(...PrinterCommand.setTextSize(1)); commandBuffer.push(...PrinterCommand.printTextLine(**测试小票**)); commandBuffer.push(...PrinterCommand.setTextSize(0)); // 恢复默认大小 commandBuffer.push(...PrinterCommand.setAlign(0)); // 左对齐 commandBuffer.push(...PrinterCommand.printTextLine(----------------------------)); commandBuffer.push(...PrinterCommand.setBold(true)); commandBuffer.push(...PrinterCommand.printTextLine(商品名称 数量 单价)); commandBuffer.push(...PrinterCommand.setBold(false)); commandBuffer.push(...PrinterCommand.printTextLine(----------------------------)); commandBuffer.push(...PrinterCommand.printTextLine(测试商品A 2 10.00)); commandBuffer.push(...PrinterCommand.printTextLine(测试商品B 1 25.50)); commandBuffer.push(...PrinterCommand.printTextLine(----------------------------)); commandBuffer.push(...PrinterCommand.setBold(true)); commandBuffer.push(...PrinterCommand.printTextLine(合计 ¥ 45.50)); commandBuffer.push(...PrinterCommand.setBold(false)); commandBuffer.push(...PrinterCommand.printTextLine( )); // 空行 // 打印二维码 commandBuffer.push(...PrinterCommand.setAlign(1)); // 二维码居中 // **注意这里需要替换为你的打印机正确的二维码指令函数** // const qrCmd PrinterCommand.printQRCode(this.qrCodeText, 6); // commandBuffer.push(...qrCmd); // 由于指令不通用这里我们用一个文本替代演示 commandBuffer.push(...PrinterCommand.printTextLine([二维码: ${this.qrCodeText}])); commandBuffer.push(...PrinterCommand.setAlign(0)); commandBuffer.push(...PrinterCommand.feedLines(3)); // 走纸3行 commandBuffer.push(...PrinterCommand.cutPaper()); // 切纸 // 2. 将指令数组转换为 ArrayBuffer const uint8Array new Uint8Array(commandBuffer.flatMap(byte Array.isArray(byte) ? byte : [byte])); const buffer uint8Array.buffer; // 3. 通过蓝牙发送数据 try { await uni.writeBLECharacteristicValue({ deviceId: this.connectedDeviceId, serviceId: this._serviceId, characteristicId: this._writeCharId, value: buffer, }); uni.showToast({ title: 打印指令发送成功, icon: success }); } catch (writeErr) { console.error(写入蓝牙特征值失败, writeErr); uni.showToast({ title: 发送失败: ${writeErr.errMsg}, icon: none }); // 可能是连接已断开需要重置状态 this.connectedDeviceId null; } finally { uni.hideLoading(); } }核心要点writeBLECharacteristicValue是最终的执行函数它接收一个ArrayBuffer对象。我们之前构建的所有指令最终都要合并成一个大的ArrayBuffer。发送是异步的并且有大小限制。蓝牙BLE单次写入的数据包大小有限制通常是20字节左右。uni.writeBLECharacteristicValue内部可能会帮我们分包但为了更可靠对于很长的打印内容比如一张图片我们需要手动分包发送并控制发送间隔避免缓冲区溢出。对于普通小票文本一般无需手动分包。指令顺序很重要。必须先初始化设置格式再打印内容最后走纸和切纸。格式指令如对齐、加粗只对其后打印的内容生效直到被新的格式指令覆盖。6. 常见问题、调试技巧与优化建议在实际开发中你几乎一定会遇到各种问题。下面是我踩过坑后总结的一些经验。6.1 连接与通信问题排查表问题现象可能原因排查步骤与解决方案搜索不到设备1. 手机蓝牙未打开。2. 打印机未进入配对模式通常需要长按某个键直到指示灯快闪。3. 打印机已被其他设备连接。4. Android系统位置权限未开启。5. 打印机不支持BLE老设备。1. 检查手机蓝牙开关。2. 参照打印机说明书进入配对模式。3. 断开打印机与其他设备的连接。4. 检查App权限确保定位权限已授予。5. 尝试用手机系统蓝牙设置搜索确认能否发现。若系统能发现但App不能检查代码过滤条件。若系统也不能发现是打印机问题。连接失败 (10004/10012等错误码)1. 设备距离过远或信号干扰。2. 设备已断开或关机。3. 系统蓝牙底层异常。1. 靠近打印机排除干扰源。2. 重启打印机和手机蓝牙。3. 查看uni-app API文档对应错误码含义。连接成功但找不到服务/特征值1. 使用的服务UUID不正确。2. 打印机是经典蓝牙SPP而非BLE。3. 连接建立后获取服务需要短暂时间。1. 使用蓝牙调试App如nRF Connect扫描设备查看其提供的所有服务UUID找到正确的串口服务。2. 确认打印机型号是否支持BLE。不支持则需用原生插件方案。3. 在createBLEConnection成功后加一个短暂延时再调用getBLEDeviceServices。发送打印指令后无反应1. 特征值write属性为false不可写。2. 发送的指令格式错误或编码错误。3. 打印机缺纸、开盖或故障。4. 指令未包含初始化或结束符。1. 检查getBLEDeviceCharacteristics返回的特征值属性。2.这是最常见原因。用电脑上的串口调试工具如友善串口连接打印机的USB口发送相同的指令测试是否正确。确保中文编码是GBK。3. 检查打印机状态指示灯或尝试打印自检页。4. 确保指令以0x1B 0x40开头并以走纸/切纸指令结束。打印乱码1.中文编码问题发送了UTF-8而非GBK。2. 打印机字库不支持该字符。3. 波特率等通信参数不匹配BLE一般无此问题。1.重中之重。确保文本转字节时使用GBK编码。引入iconv-lite库并测试。2. 尝试打印纯英文数字测试。如果正常就是编码问题。3. 对于经典蓝牙可能需要设置波特率BLE无需设置。二维码打印不出来或格式错乱1. 二维码指令不适用于该打印机型号。2. 指令参数如大小、纠错等级超出范围。3. 二维码数据内容过长超过打印机处理能力。1.必须查阅打印机指令手册找到确切的二维码打印命令和参数格式。2. 先用手册上的示例指令通常是十六进制串测试确保硬件支持。3. 缩短二维码内容如使用短链接或尝试降低纠错等级。6.2 性能与体验优化建议连接缓存不要每次打印都重新搜索和连接。可以在App本地如uni.setStorageSync缓存已成功连接过的设备ID。下次启动时尝试自动重连该设备提升用户体验。指令队列如果快速连续触发多次打印建议创建一个打印任务队列顺序发送指令避免蓝牙通道的写入冲突。状态反馈如果打印机支持通知特征notify务必启用并监听。可以实时获取打印状态如缺纸、忙碌、开盖并在UI上给用户提示而不是让用户茫然等待。超时与重试在writeBLECharacteristicValue外包裹超时逻辑和重试机制。网络不稳定时单次发送失败可以自动重试1-2次。封装成组件或插件将蓝牙连接、指令生成、打印逻辑封装成一个独立的Vue组件或uni-app原生插件方便在多个项目中复用。这是走向工程化的关键一步。测试覆盖准备多台不同品牌、型号的打印机进行测试。ESC/POS是标准但各家的“方言”差异足以让你头疼。确保你的指令生成模块有一定的兼容性处理能力。6.3 关于uni-app x与云端打印的延伸思考你可能注意到热词中有“uni-app x”。uni-app x是下一代uni-app使用uts语言开发性能更强但生态在完善中。对于蓝牙打印这种强原生交互的需求uni-app x理论上能提供更简洁高效的原生代码调用方式但目前相关API和插件可能还不丰富。当前阶段使用本文所述的uni-appVue版方案是更成熟稳妥的选择。此外如果业务场景允许也可以考虑“云端打印”方案H5页面将数据通过网络发送到服务器服务器调用部署在打印店/仓库的打印服务如C-Lodop、PrintNode进行打印。这完全规避了蓝牙兼容性问题但失去了移动性和离线能力。选择哪种方案取决于你的具体业务场景是“人动”移动打印还是“票动”固定点位打印。整个流程走下来从搜索设备时的小心翼翼到连接成功时的喜悦再到指令发送后打印机“咔咔”出纸的成就感这种软硬件结合带来的体验是非常直接的。最关键的是遇到问题时要沉住气用好“蓝牙调试工具”和“串口调试工具”这两把尚方宝剑它们能帮你快速定位是App端的问题还是打印机指令的问题。最后别忘了在真机上充分测试模拟用户可能遇到的各种网络和环境情况这样才能交付一个真正稳定的功能。
返回列表