news 2026/4/24 6:11:30

uniapp开发微信小程序低功耗蓝牙打印(佳博打印机已测试)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
uniapp开发微信小程序低功耗蓝牙打印(佳博打印机已测试)

1.微信公众平台隐私协议添加蓝牙协议,审核通过即可


2.蓝牙打印流程

2.1 搜索蓝牙
首先我们需要先初始化蓝牙模块,在进行搜索蓝牙。在监听到附近蓝牙设备时,记录他的名称和deviceId。
初始化蓝牙(openBluetoothAdapter):查看蓝牙是否可用,若初始化失败,则是蓝牙未打开,提示用户打开蓝牙。若已经打开蓝牙,则准备进行蓝牙搜索。

2.2 连接蓝牙
搜索蓝牙(startBluetoothDevicesDiscovery):开始搜索蓝牙设备。这一步需要和关闭搜索蓝牙(stopBluetoothDevicesDiscovery)成对使用,长时间占用搜索设备,浪费资源,在查找到需要的设备连接之后需要主动去停止搜索设备。
搜索出附近蓝牙设备后,获取蓝牙设备的deviceId传入createBLEConnection方法中。在连接蓝牙设备时,我们需要注意的是保证尽量成对的调用 createBLEConnection 和 closeBLEConnection 接口。安卓如果多次调用 createBLEConnection 创建连接,有可能导致系统持有同一设备多个连接的实例,导致调用 closeBLEConnection 的时候并不能真正的断开与设备的连接。我们将连接成功的蓝牙信息存到currDev中,以便直接连接,无需进行搜索操作。获取已搜索到的蓝牙列表(getBluetoothDevices):查看所有已经发现的蓝牙设备getBluetoothDevices。在这一步可以查看到以前已经获取到的蓝牙设备deviceId。可以在这一步中查看以前已经连接到的设备,主动去尝试连接这个设备。

3.连接蓝牙设备
连接蓝牙设备(createBLEConnection):通过设备的deviceId来连接设备。在这里如果APP若是已经连接过此低功耗蓝牙设备,可以直接传入之前设备ID进行尝试连接。这一步的连接操作需要和关闭连接closeBLEConnection成对操作。如果多次调用创建连接,有可能会导致系统持有一个设备的多个连接实例,导致在调用关闭连接的时候不能真正关闭连接。

4.获取服务
获取设备所有服务(getBLEDeviceServices):在连接设备之后,APP需要主动去获取设备的所有服务(services),设备会返回给APP设备的服务列表(services)包含设备服务的UUID,该服务是否为主服务。

获取服务特征值(getBLEDeviceCharacteristic):在获取设备的服务列表之后,根据自己设备的蓝牙协议接口文档,根据指定的服务ID(serviceId)使用获取服务特征值方法传入两个参数设备ID(deviceId)和服务ID(serviceId)向设备获取该服务中的所有的特征值(characteristic),设备会向APP返回该服务中的所有特征值列表,列表包含设备特征值的UUID,该特征值支持的操作类型。

5.写入命令
向设备写入控制命令(writeBLECharacteristicValue):可以向设备写入(发送)控制命令writeBLECharacteristicValue,此方法是向低功耗蓝牙设备特征值写入二进制数据。需要注意只有该特征值的属性支持write才可以调用此方法。在此方法调用成功后,设备特征值发生改变,就会触发onBLECharacteristicValueChange回调,主动返回特征值数据。普通蓝牙则需要根据打印机相对指令进行设置,一般使用tsc.js,传递蓝牙指令.

<template> <view> <view style="text-align: center; margin-bottom: 20rpx"> <view >说明:首次使用请先打开手机蓝牙,点击下方“连接”按钮绑定蓝牙打印机。</view > </view> <view style="padding-bottom: 3px"> <view> <view>产品名称:</view> </view> <view> { { jdcername }} </view> </view> <view style="padding-bottom: 3px"> <view> <view>打印机名称:</view> </view> <view> { { devicename }} <text v-show="devicename" style="color: #09be4f">(已连接)</text> <text v-show="!devicename" style="color: #dc4e41">未连接打印机</text> <text @click="showPrinterList" style="color: #007aff; margin-left: 20px" >连接</text > </view> <view style="padding: 0px 10px"> <button type="primary" plain size="mini" style="margin-top: 5px" @click="handlePrint" > 打印 </button> </view> </view> <uni-popup ref="popupnew" :type="type" :animation="false" :maskClick="true" @change="change" > <view style=" background-color: #fff; padding: 15px; width: 600rpx; font-size: 38rpx; " > <view v-for="(item, index) in deviceList" :key="index"> <view style=" display: flex; justify-content: space-between; margin-bottom: 20rpx; " > <text>名称:{ { item.name }}</text> <view style="color: #007aff; cursor: pointer" @click="connectDevice(item)" >连接</view > </view> </view> <view v-if="deviceList.length === 0 && scanning" style="text-align: center" >扫描中...</view > <view v-if="deviceList.length === 0 && !scanning" style="text-align: center" >未发现设备,请下拉刷新</view > </view> </uni-popup> </view> </template> <script setup lang="ts"> import { $URL } from "../../../api/gbk.js"; import { ref, onMounted, onUnmounted } from "vue"; // ========== 全局数据 ========== let Globalindex = { ret: 0, statmessage: "", jdcercode: "http://distss.com/12342474241150839802", jdcerbasename: "家庭农场", stockbillid: 200288, jdcername: "产品名称:白菜 数量(重量):5公斤", jdcerorigin: "上海斜沟崖", // 没有标点,无需修改 jdcertel: "联系方式:4006257518", // 冒号改为全角 jdcerdate: "开具日期:2026年4月22日", isinternalqc: true, // 内部质量控制 isselfexamed: true, // 自我检测合格 isrequexamed: true, // 委托检测合格 }; // import { Globalindex } from "../../../global/globalindex.ts"; // ========== 蓝牙相关状态 ========== const deviceList = ref([]); const devicename = ref(""); const deviceitems = ref({}); let connectedDeviceId = ""; let writeCharacteristic = null; let serviceId = ""; const type = ref("center"); const popupnew = ref(null); const jdcername = Globalindex.jdcername; let scanning = ref(false); // 打印指令缓冲区 let command = ref([]); // ========== 工具函数(编码转换)========== // ASCII 字符串转字节数组(用于 TSPL 命令) function asciiToBytes(str: string): Uint8Array { const bytes = []; for (let i = 0; i < str.length; i++) { bytes.push(str.charCodeAt(i)); } return new Uint8Array(bytes); } // 中文字符串转 GBK 字节数组(利用 $URL.encode 返回的十六进制串) function chineseToBytes(str: string): Uint8Array { const hex = $URL.encode(str); // 例如 "C4E3BAC3" const bytes = []; for (let i = 0; i < hex.length; i += 2) { bytes.push(parseInt(hex.substr(i, 2), 16)); } return new Uint8Array(bytes); } // 通用 GBK 转字节(与 chineseToBytes 相同,用于 ESC/POS 分支兼容) function gbkToBytes(str: string): Uint8Array { return chineseToBytes(str); } // 添加纯 ASCII 命令到缓冲区 function addCommand(content: string) { const bytes = asciiToBytes(content); for (let i = 0; i < bytes.length; i++) { command.value.push(bytes[i]); } } // ========== TSPL 指令函数 ========== function setSize(pageWidth: number, pageHeight: number) { addCommand(`SIZE ${pageWidth} mm, ${pageHeight} mm\r\n`); } function setGap(printGap: number) { addCommand(`GAP ${printGap} mm,0 mm\r\n`); } function setDirection(n: number) { addCommand(`DIRECTION ${n}\r\n`); } function setDensity(n: number) { addCommand(`DENSITY ${n}\r\n`); } function setCls() { addCommand("CLS\r\n"); } function setPagePrint() { addCommand("PRINT 1,1\r\n"); } function setQR( x: number, y: number, level: string, width: number, mode: string, content: string, ) { addCommand(`QRCODE ${x},${y},${level},${width},${mode},0,"${content}"\r\n`); } function setBar(x: number, y: number, width: number, height: number) { addCommand(`BAR ${x},${y},${width},${height}\r\n`); } // 支持中文的 setText function setText( x: number, y: number, font: string | number, x_: number, y_: number, str: string, ) { // 字符宽度(点),可根据实际字体微调 const CHINESE_WIDTH = 24; // 中文字符宽度(TSS24.BF2 通常为24点) const ASCII_WIDTH = 12; // 半角字符宽度(数字、字母、空格、标点) // 1. 将字符串拆分为连续的 ASCII 段和非 ASCII 段 const segments: { text: string; isAscii: boolean }[] = []; let current = ""; let currentIsAscii: boolean | null = null; for (const ch of str) { const isAscii = ch.charCodeAt(0) < 128; if (currentIsAscii === null) { currentIsAscii = isAscii; current = ch; } else if (currentIsAscii === isAscii) { current += ch; } else { segments.push({ text: current, isAscii: currentIsAscii }); current = ch; currentIsAscii = isAscii; } } if (current) segments.push({ text: current, isAscii: currentIsAscii }); // 2. 依次发送每个片段,累加 X 坐标 let currentX = x; for (const seg of segments) { // 转义双引号和反斜杠(TSPL 协议要求) let escaped = seg.text.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); if (!seg.isAscii) { // 非 ASCII 片段(中文、全角符号)使用 GBK 编码 const prefix = `TEXT ${currentX},${y},"${font}",0,${x_},${y_},"`; const prefixBytes = asciiToBytes(prefix); const gbkBytes = chineseToBytes(escaped); const suffixBytes = asciiToBytes('"\r\n'); const total = new Uint8Array( prefixBytes.length + gbkBytes.length + suffixBytes.length, ); total.set(prefixBytes, 0); total.set(gbkBytes, prefixBytes.length); total.set(suffixBytes, prefixBytes.length + gbkBytes.length); for (let i = 0; i < total.length; i++) command.value.push(total[i]); currentX += seg.text.length * CHINESE_WIDTH; } else { // ASCII 片段(数字、字母、半角标点、空格)直接使用 ASCII 命令 addCommand( `TEXT ${currentX},${y},"${font}",0,${x_},${y_},"${escaped}"\r\n`, ); currentX += seg.text.length * ASCII_WIDTH; } } } // ========== 构建 TSPL 打印数据(佳博/通用)========== function buildGPAOrPBAData(): Uint8Array { console.log("buildGPAOrPBAData"); command.value = []; // 设置中文代码页(必须放在最前面) addCommand("CODEPAGE 936\r\n"); setSize(50, 50); setGap(5); setDirection(1); setDensity(0); setCls(); // 二维码 setQR(280, 260, "L", 3, "A", Globalindex.jdcercode); // isinternalqc: false, // 内部质量控制 // isselfexamed: false, // 自我检测合格 // isrequexamed: false, // 委托检测合格 if (Globalindex.isinternalqc) { setText(-5, 105, "TSS24.BF2", 1, 1, "√"); } if (Globalindex.isselfexamed) { setText(150, 105, "TSS24.BF2", 1, 1, "√"); } if (Globalindex.isrequexamed) { setText(270, 105, "TSS24.BF2", 1, 1, "√"); } setText(0, 145, "TSS24.BF2", 1, 1, Globalindex.jdcername); setText(0, 180, "TSS24.BF2", 1, 1, "产地:"); setText(0, 210, "TSS24.BF2", 1, 1, Globalindex.jdcerorigin); setText(0, 240, "TSS24.BF2", 1, 1, Globalindex.jdcertel); setText(0, 270, "TSS24.BF2", 1, 1, Globalindex.jdcerdate); setText(0, 300, "TSS24.BF2", 1, 1, "生产者盖章或签名:"); setText(0, 330, "TSS24.BF2", 1, 1, Globalindex.jdcerbasename); setPagePrint(); return new Uint8Array(command.value); } // ========== 构建 ESC/POS 打印数据(VB 打印机)========== // 辅助函数:字符串转十六进制(用于二维码) function stringtoHex(str: string): string { let val = ""; for (let i = 0; i < str.length; i++) { if (val == "") val = str.charCodeAt(i).toString(16); else val += str.charCodeAt(i).toString(16); } return val; } // 字节数组转十六进制字符串 function bufToHex(buffer: Uint8Array): string { return Array.prototype.map .call(buffer, (x) => ("00" + x.toString(16)).slice(-2)) .join(""); } // 十六进制字符串转字节数组 function hexToBytes(hex: string): Uint8Array { hex = hex.replace(/\s/g, "").toLowerCase(); let bytes = []; for (let i = 0; i < hex.length; i += 2) { let byte = parseInt(hex.substr(i, 2), 16); if (byte > 127) byte = byte - 256; bytes.push(byte); } return new Uint8Array(bytes); } function buildVBData(): Uint8Array { let samplecode = stringtoHex(Globalindex.jdcercode); let qrcode = "1D 51 55 00"; // 居左55点距 qrcode += "1D57061D6B200101"; qrcode += samplecode; qrcode += "00"; qrcode += "0A"; qrcode += "0A"; let str_a = Globalindex.jdcername; let arrayBuffer_a = gbkToBytes(str_a); let hex_a = "1B40"; // 打印机初始化 hex_a = "1B 24 12 00"; // 居左12点距 hex_a += bufToHex(arrayBuffer_a); hex_a += "00"; hex_a += "0D"; let str_c = Globalindex.jdcerbasename; let arrayBuffer_c = gbkToBytes(str_c); let hex_c = "1B 24 06 00"; // 居左06点距 hex_c += bufToHex(arrayBuffer_c); hex_c += "00"; hex_c += "1B69"; let finalHex = hex_a + qrcode + hex_c; return hexToBytes(finalHex); } // ========== 根据打印机类型构建数据 ========== function buildPrintData(): Uint8Array { let reg = /Printer_/i; if (devicename.value == "V2B3_639544B") { return buildVBData(); } else if ( devicename.value == "GP-D320FX_A7ED" || reg.test(devicename.value) ) { return buildGPAOrPBAData(); } else { return buildGPAOrPBAData(); } } // ========== 蓝牙操作函数 ========== function initBluetooth() { wx.openBluetoothAdapter({ success: () => { console.log("蓝牙适配器初始化成功"); wx.onBluetoothAdapterStateChange((res) => { console.log("蓝牙状态变化", res); if (!res.available) { devicename.value = ""; connectedDeviceId = ""; uni.showToast({ title: "蓝牙已断开", icon: "none" }); } }); }, fail: (err) => { console.error("蓝牙适配器初始化失败", err); uni.showModal({ title: "提示", content: "请打开手机蓝牙并授权位置权限", showCancel: false, }); }, }); } function showPrinterList() { type.value = "center"; popupnew.value.open(); startScan(); } function startScan() { if (scanning.value) return; scanning.value = true; deviceList.value = []; wx.stopBluetoothDevicesDiscovery({ complete: () => { wx.startBluetoothDevicesDiscovery({ allowDuplicatesKey: false, success: () => { console.log("开始扫描设备"); wx.onBluetoothDeviceFound((res) => { const devices = res.devices; devices.forEach((device) => { if ( device.name && !deviceList.value.some((d) => d.deviceId === device.deviceId) ) { deviceList.value.push({ name: device.name, deviceId: device.deviceId, RSSI: device.RSSI, }); } }); }); setTimeout(() => { if (scanning.value) stopScan(); }, 10000); }, fail: (err) => { console.error("开始扫描失败", err); scanning.value = false; uni.showToast({ title: "扫描失败,请重试", icon: "none" }); }, }); }, }); } function stopScan() { wx.stopBluetoothDevicesDiscovery({ success: () => { scanning.value = false; console.log("停止扫描"); }, }); } async function connectDevice(item) { uni.showLoading({ title: "连接中...", mask: true }); try { stopScan(); if (connectedDeviceId) await closeBluetoothConnection(); await new Promise((resolve, reject) => { wx.createBLEConnection({ deviceId: item.deviceId, success: resolve, fail: reject, }); }); connectedDeviceId = item.deviceId; devicename.value = item.name; deviceitems.value = item; const servicesRes = await new Promise((resolve, reject) => { wx.getBLEDeviceServices({ deviceId: co
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/24 6:10:07

2026年GEO流量战略:四大AI短视频矩阵获客系统深度横评

2026年&#xff0c;短视频获客早已告别“堆人内卷”的粗放时代&#xff0c;正式进入“AI赋能安全护航”的存量竞争新阶段。对企业主而言&#xff0c;纠结“哪款AI矩阵系统更靠谱”时&#xff0c;选择标准早已跳出单纯的“分发功能”&#xff0c;转向三个核心维度&#xff1a;底…

作者头像 李华
网站建设 2026/4/24 6:10:05

源代码想加密?推荐六款源代码加密软件,码住收藏了

核心资产的隐形守护者在科技圈有一句共识&#xff1a;代码是程序员的心血&#xff0c;更是企业的生命线。想象一下&#xff0c;一家初创公司熬了无数个通修研发出的核心算法&#xff0c;如果因为一名离职员工随手带走的U盘&#xff0c;或者一次不经意的邮件外发而流向竞争对手&…

作者头像 李华
网站建设 2026/4/24 6:09:10

docker 容器文件 hostconfig.json 和 config.v2.json 的区别

在 Docker 的底层存储结构&#xff08;通常位于 /var/lib/docker/containers/<container_id>/&#xff09;中&#xff0c;hostconfig.json 和 config.v2.json 是两个最核心的元数据文件。 它们共同定义了一个容器的完整状态&#xff0c;但分工明确&#xff1a;一个管“内…

作者头像 李华
网站建设 2026/4/24 6:09:03

Vulnhub election

Vulnhub election扫描主机扫描端口扫描目录访问页面Apache默认页面访问扫出来的目录像账号又像目录&#xff0c;拼接访问试下&#xff0c;只有election能访问接着扫每个都看一下登录页面输入id接着扫不怕麻烦就每个都访问&#xff0c;依经验来看logs下概率大些拿到账号密码可能…

作者头像 李华
网站建设 2026/4/24 6:08:17

从论文到实践:阿里云XRDMA通信库如何重塑大规模RDMA应用生态

1. RDMA技术为何需要"中间件"&#xff1f; RDMA技术就像给数据中心装上了高速公路&#xff0c;但这条路上却缺少交通指示灯和导航系统。我第一次接触RDMA时&#xff0c;被它的性能数据震撼到了——200Gbps带宽、0.6微秒延迟&#xff0c;这比传统TCP快了整整一个数量级…

作者头像 李华