最近在做小票打印这块,项目需求是IOS和安卓两种都要实现,开始做的时候也是一脸懵,然后网上找了不少资料,踩了一堆坑,看了好多文章,结果还好成了
蓝牙打印机一般分为两种打印模式,票据打印、标签打印
公司买的渣渣打印机连开发文档都没有,害我走了不少坑,让我开发买的时候也不咨询咨询我
目前微信小程序连接蓝牙打印机 wx.createBLEConnection 测试在IOS设备上没有问题,在部分安卓手机上会出现异常(表现为,连接是会弹出系统配对框,不管点取消还是输入配对码后点确定,都会立马断开连接。如果不输入也不取消则会在30秒以内自动断开蓝牙打印机)
现在采用的方式是各给安卓和IOS写一套蓝牙打印的命令
IOS
// ====================蓝牙操作================== //初始化蓝牙模块 openBluetoothAdapter() { if (app.sysinfo.provider == 1) { // 开启蓝牙 app.onBluetooth() setTimeout(() => { this.android_search() }, 2000) return false; } this.closeBluetoothAdapter() uni.openBluetoothAdapter({ success: (res) => { console.log("初始化蓝牙模块: " + JSON.stringify(res)); this.startBluetoothDevicesDiscovery() }, fail: (res) => { if (res.errCode === 10001) { uni.onBluetoothAdapterStateChange((res) => { console.log('监听蓝牙适配器状态变化事件', res) if (res.available == false) { app.global_printing = {} this.connected = false this.chs = [] this.canWrite = false } if (res.available) { this.startBluetoothDevicesDiscovery() } }) } if (res.errCode) { app.alert('初始化蓝牙失败,错误码:' + res.errCode) return false; } app.alert(res.errMsg) } }) }, //获取本机蓝牙适配器状态 getBluetoothAdapterState() { uni.getBluetoothAdapterState({ success: (res) => { console.log('获取本机蓝牙适配器状态。', JSON.stringify(res)) if (res.discovering) { this.onBluetoothDeviceFound() } else if (res.available) { this.startBluetoothDevicesDiscovery() } }, fail: (res) => { console.log('error:获取本机蓝牙适配器状态失败', JSON.stringify(res)) setTimeout(() => { this.getBluetoothAdapterState() }, 500) } }) }, //开始搜寻附近的蓝牙外围设备 startBluetoothDevicesDiscovery() { console.log(this.discoveryStarted); if (this.discoveryStarted) { return } console.log('开始搜索蓝牙设备'); this.discoveryStarted = true this.onBluetoothDeviceFound() setTimeout(() => { uni.startBluetoothDevicesDiscovery({ allowDuplicatesKey: true, success: (res) => { console.log('startBluetoothDevicesDiscovery success', JSON.stringify( res)) }, fail: (res) => { if (res.errCode == '10001') { app.alert('当前蓝牙适配器不可用') } else { app.alert('搜索蓝牙失败,状态码:' + res.errCode) } } }) }, 500) }, // 停止搜索 stopBluetoothDevicesDiscovery() { uni.stopBluetoothDevicesDiscovery() this.discoveryStarted = false }, //寻找到新设备的事件的回调函数 onBluetoothDeviceFound() { console.log('寻找到新设备的事件的回调函数'); uni.onBluetoothDeviceFound((res) => { console.log(res); res.devices.forEach(device => { if (!device.name && !device.localName) { return } const foundDevices = this.devices const idx = this.inArray(foundDevices, 'deviceId', device.deviceId) if (idx === -1) { this.devices.push(device) } else { this.devices[idx] = device } }) }) }, //连接低功耗蓝牙设备 createBLEConnection(e) { uni.showLoading({ title: '设备连接中', mask: true }); const ds = e.currentTarget.dataset const deviceId = ds.deviceId const name = ds.name if (app.sysinfo.provider == 1) { if (ds.pair !== true) { this.android_search(deviceId) } else { console.log('已配对') } var device = null, BAdapter = null, BluetoothAdapter = null, uuid = null, main = null, bluetoothSocket = null; var mac_address = deviceId var main = plus.android.runtimeMainActivity(); BluetoothAdapter = plus.android.importClass("android.bluetooth.BluetoothAdapter"); var UUID = plus.android.importClass("java.util.UUID"); uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); BAdapter = BluetoothAdapter.getDefaultAdapter(); device = BAdapter.getRemoteDevice(mac_address); plus.android.importClass(device); bluetoothSocket = device.createInsecureRfcommSocketToServiceRecord(uuid); plus.android.importClass(bluetoothSocket); if (!bluetoothSocket.isConnected()) { console.log('检测到设备未连接,尝试连接....'); bluetoothSocket.connect(); } this.connected = true this.name = name this.deviceId = deviceId this.canWrite = true app.global_printing = { name: name, deviceId: deviceId } app.saveData1('global_printing', app.global_printing) uni.hideLoading(); return false; } uni.createBLEConnection({ deviceId, success: (res) => { this.connected = true this.name = name this.deviceId = deviceId app.global_printing = { name: name, deviceId: deviceId } this.onBLEConnectionStateChange() // 防止获取失败 setTimeout(() => { this.getBLEDeviceServices(deviceId) }, 1000) }, fail: (res) => { uni.hideLoading(); app.Toast('设备连接失败') console.log("蓝牙连接失败:", res); } }) this.stopBluetoothDevicesDiscovery() }, //获取蓝牙设备所有服务(service) getBLEDeviceServices(deviceId) { uni.getBLEDeviceServices({ deviceId, success: (res) => { console.log("获取蓝牙服务成功:" + JSON.stringify(res)) if (res.services.length == 0) { uni.hideLoading(); app.alert('没有获取到蓝牙服务,无法打印001') app.global_printing = {} return false } for (let i = 0; i < res.services.length; i++) { if (res.services[i].isPrimary) { this.getBLEDeviceCharacteristics(deviceId, res.services[i].uuid) return } } }, fail: (res) => { setTimeout(() => { this.getBLEDeviceServices(deviceId) }, 500) console.log("获取蓝牙服务失败:" + JSON.stringify(res)) } }) }, //获取蓝牙设备某个服务中所有特征值(characteristic) getBLEDeviceCharacteristics(deviceId, serviceId) { console.log('获取蓝牙设备某个服务中所有特征值', deviceId, serviceId) uni.getBLEDeviceCharacteristics({ deviceId, serviceId, success: (res) => { console.log('获取蓝牙设备某个服务中所有特征值 success', JSON.stringify(res)) uni.hideLoading(); if (res.characteristics.length == 0) { app.alert('没有获取到蓝牙服务,无法打印002') app.global_printing = {} return false } for (let i = 0; i < res.characteristics.length; i++) { let item = res.characteristics[i] if (item.properties.read) { uni.readBLECharacteristicValue({ deviceId, serviceId, characteristicId: item.uuid, }) } if (item.properties.write) { this.canWrite = true app.global_printing._deviceId = deviceId app.global_printing._serviceId = serviceId app.global_printing._characteristicId = item.uuid app.saveData1('global_printing', app.global_printing) //this.writeBLECharacteristicValue() } if (item.properties.notify || item.properties.indicate) { uni.notifyBLECharacteristicValueChange({ deviceId, serviceId, characteristicId: item.uuid, state: true, }) } } }, fail(res) { console.error('获取特征值失败:', res) } }) // 操作之前先监听,保证第一时间获取数据 uni.onBLECharacteristicValueChange((characteristic) => { console.log(this.data.chs); const idx = this.inArray(this.data.chs, 'uuid', characteristic.characteristicId) const data = {} if (idx === -1) { this.chs[this.data.chs.length] = { uuid: characteristic.characteristicId, value: ab2hex(characteristic.value) } } else { this.chs[idx] = { uuid: characteristic.characteristicId, value: ab2hex(characteristic.value) } } }) }, onBLEConnectionStateChange() { uni.onBLEConnectionStateChange((res) => { // 该方法回调中可以用于处理连接意外断开等异常情况 console.log(`蓝牙连接状态改变device ${res.deviceId} state has changed, connected: ${res.connected}`) if (res.connected == false) { app.global_printing = {} this.connected = false this.chs = [] this.canWrite = false } }) }, //断开与低功耗蓝牙设备的连接 closeBLEConnection() { app.global_printing = {} uni.closeBLEConnection({ deviceId: this.deviceId }) this.connected = false this.chs = [] this.canWrite = false }, //关闭蓝牙模块 closeBluetoothAdapter() { app.global_printing = {} uni.closeBluetoothAdapter() this.discoveryStarted = false }, //发送数据 sendStr(bufferstr, success, fail) { var that = this; uni.writeBLECharacteristicValue({ deviceId: app.global_printing._deviceId, serviceId: app.global_printing._serviceId, characteristicId: app.global_printing._characteristicId, value: bufferstr, success: function(res) { success(res); console.log('发送的数据:' + bufferstr) // console.log('message发送成功') }, fail: function(res) { fail(res) console.log("数据发送失败:" + JSON.stringify(res)) }, complete: function(res) { // console.log("发送完成:" + JSON.stringify(res)) } }) }, //遍历发送数据 printCode(arr) { var that = this; if (arr.length > 0) { this.sendStr(arr[0], function(success) { arr.shift(); that.printCode(arr); }, function(error) { app.alert('打印失败,错误码:' + error.errCode) app.printing_status = false console.log(error); }); return false; } setTimeout(function() { app.printing_status = false console.log('打印结束'); }, 1000); },
Android
就相对简单方便,采用Native.js直接调用Native Java接口通道,通过plus.android调用安卓原生系统API。
原生安卓文档 https://developer.android.google.cn/reference/android/bluetooth/BluetoothAdapter?hl=en
// ======================Android============ // 搜索蓝牙设备 android_search(address = '') { //搜索、配对 var main = plus.android.runtimeMainActivity(); var IntentFilter = plus.android.importClass('android.content.IntentFilter'); var BluetoothAdapter = plus.android.importClass("android.bluetooth.BluetoothAdapter"); var BluetoothDevice = plus.android.importClass("android.bluetooth.BluetoothDevice"); var BAdapter = BluetoothAdapter.getDefaultAdapter(); console.log("开始搜索设备"); var filter = new IntentFilter(); var bdevice = new BluetoothDevice(); var on = null; var un = null; console.log('正在搜索请稍候'); BAdapter.startDiscovery(); //开启搜索 var receiver; receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', { onReceive: (context, intent) => { //实现onReceiver回调函数 plus.android.importClass(intent); //通过intent实例引入intent类,方便以后的‘.’操作 // console.log(intent.getAction()); //获取action if (intent.getAction() == "android.bluetooth.adapter.action.DISCOVERY_FINISHED") { main.unregisterReceiver(receiver); //取消监听 console.log("搜索结束") } else { var BleDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); //判断是否配对 if (BleDevice.getBondState() == bdevice.BOND_NONE) { console.log("未配对蓝牙设备:" + BleDevice.getName() + ' ' + BleDevice.getAddress()); //参数如果跟取得的mac地址一样就配对 if (address == BleDevice.getAddress()) { if (BleDevice.createBond()) { //配对命令.createBond() if (BleDevice.getName() != null) { console.log("配对成功蓝牙设备:" + BleDevice.getName() + ' ' + BleDevice.getAddress()); // app.Toast("配对成功蓝牙设备:" + BleDevice.getName()) } } else { console.log('配对失败') } } else { if (BleDevice.getName() != on) { //判断防止重复添加 on = BleDevice.getName(); if (BleDevice.getName() != null) { this.devices.push({ deviceId: BleDevice.getAddress(), name: BleDevice.getName() }) console.log("搜索到蓝牙设备:" + BleDevice.getName() + ' ' + BleDevice.getAddress()); } } } } else { if (BleDevice.getName() != un) { //判断防止重复添加 un = BleDevice.getName(); if (BleDevice.getName() != null) { this.devices.push({ deviceId: BleDevice.getAddress(), name: BleDevice.getName() + ' (已配对)', pair: true }) console.log("已配对蓝牙设备:" + BleDevice.getName() + ' ' + BleDevice.getAddress()); } } } } } }); filter.addAction(bdevice.ACTION_FOUND); filter.addAction(BAdapter.ACTION_DISCOVERY_STARTED); filter.addAction(BAdapter.ACTION_DISCOVERY_FINISHED); filter.addAction(BAdapter.ACTION_STATE_CHANGED); main.registerReceiver(receiver, filter); //注册监听 }, // 打印 android_printCode(arr) { var that = this; // 打印 var device = null, BAdapter = null, BluetoothAdapter = null, uuid = null, main = null, bluetoothSocket = null; var mac_address = app.global_printing.deviceId var main = plus.android.runtimeMainActivity(); BluetoothAdapter = plus.android.importClass("android.bluetooth.BluetoothAdapter"); var UUID = plus.android.importClass("java.util.UUID"); uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); BAdapter = BluetoothAdapter.getDefaultAdapter(); try { device = BAdapter.getRemoteDevice(mac_address); plus.android.importClass(device); bluetoothSocket = device.createInsecureRfcommSocketToServiceRecord(uuid); plus.android.importClass(bluetoothSocket); } catch (e) { console.log('asasssds-d=da=da-dsd'); app.printing_status = false app.alert('打印失败') return false; } if (!bluetoothSocket.isConnected()) { console.log('检测到设备未连接,尝试连接....'); bluetoothSocket.connect(); } console.log('设备已连接'); if (bluetoothSocket.isConnected()) { var outputStream = bluetoothSocket.getOutputStream(); plus.android.importClass(outputStream); for (var i = 0; i < arr.length; i++) { outputStream.write(arr[i]); } outputStream.flush(); device = null //这里关键 bluetoothSocket.close(); //必须关闭蓝牙连接否则意外断开的话打印错误 } setTimeout(function() { app.printing_status = false console.log('打印结束'); }, 1000); },
打印指令(更多打印指令参考 https://www.jianshu.com/p/dd6ca0054298)
/** * 复位打印机 */ public static final byte[] RESET = {0x1b, 0x40}; /** * 左对齐 */ public static final byte[] ALIGN_LEFT = {0x1b, 0x61, 0x00}; /** * 中间对齐 */ public static final byte[] ALIGN_CENTER = {0x1b, 0x61, 0x01}; /** * 右对齐 */ public static final byte[] ALIGN_RIGHT = {0x1b, 0x61, 0x02}; /** * 选择加粗模式 */ public static final byte[] BOLD = {0x1b, 0x45, 0x01}; /** * 取消加粗模式 */ public static final byte[] BOLD_CANCEL = {0x1b, 0x45, 0x00}; /** * 宽高加倍 */ public static final byte[] DOUBLE_HEIGHT_WIDTH = {0x1d, 0x21, 0x11}; /** * 宽加倍 */ public static final byte[] DOUBLE_WIDTH = {0x1d, 0x21, 0x10}; /** * 高加倍 */ public static final byte[] DOUBLE_HEIGHT = {0x1d, 0x21, 0x01}; /** * 字体不放大 */ public static final byte[] NORMAL = {0x1d, 0x21, 0x00}; /** * 设置默认行间距 */ public static final byte[] LINE_SPACING_DEFAULT = {0x1b, 0x32};
关于二维码的打印
通过上面的文章我们可以知道
我们需要读取生成后的二维码的像素点的rgba,再将图片数据先4合1判断0还是1(0代表打印1代表不打印),紧接着八合1,因为一个字节有8位。最后使用打印机的位图指令逐行扫描打印
4合1
本想着二维码不是黑就是白,肯定不是255就是0,其实还是会有一小部分是其他数值的,这个要注意哦,每4位是一个像素点的rgba,然后黑白色的rgb就是(0,0,0)和(255,255,255),所以每四位只把第一位黑白化,然后将每四位的第一位取出来作为新的数组,当rule>200的时候,值取0,表示不打印,否则取1,表示打印;
8合1
假如我们取出来的8位数是[0,0,0,0,0,0,0,1],这个时候8合1,我们需要进行进制转换,从右往左是2的零次方,2的一次方,等等,依次上加,实际是 0 * 27 + 0 * 26 + 0 * 25 + 0 * 24 + 0 * 23 + 0 * 22 + 0 * 21 + 1 * 20,这个数就是我们要的最终数据的其中之一。
将数据转换成ArrayBuffer,其次打印必须要有指令!参考网址以及标准的ESC-POS指令集,下面代码中的数字都是指令,另外,由于我这边的打印机支持的是gb2312格式,所以在转成ArrayBuffer的同时,还需要把编码格式转成正确的格式。
不过有一点我是要说下的,要注意ios和安卓的不同,安卓一次只能写入不超过20字节(ios具体不清楚,目测120字节),建议是直接截取数据data.slice(20, byteLength),打印成功再次回调,循环打印。
// 二维码 qr(text,callback) { let that = this; const ctx = uni.createCanvasContext('myQrcode'); ctx.clearRect(0, 0, 240, 240); drawQrcode({ canvasId: 'myQrcode', text: String(text), width: 120, height: 120, callback(e) { // setTimeout(() => { // 获取图片数据 uni.canvasGetImageData({ canvasId: 'myQrcode', x: 0, y: 0, width: 240, height: 240, success(res) { let arr = that.convert4to1(res.data); let data = that.convert8to1(arr); const cmds = [].concat([27, 97, 1], [29, 118, 48, 0, 30, 0, 240, 0 ], data, [27, 74, 3], [27, 64]); const buffer = toArrayBuffer(Buffer.from(cmds, 'gb2312')); // 二维码 for (let i = 0; i < buffer.byteLength; i = i + 120) { that.arrPrint.push(buffer.slice(i, i + 120)); } callback() } }) // }, 3000); } }); },
1、toArrayBuffer ,是个组件,要安装的,https://www.npmjs.com/package/to-array-buffer 或者你用这种写法也可以const buffer = new Uint8Array(Buffer.from(cmds, ‘gb2312’)).buffer;
2、注意查看自己的数据是否正确,画图的数据有问题的话,也可能打印出黑块;
3、数据要算!!!要算!!要算!! ,比如我画图是160*160 ,然后我打印数据拼接的指令[29, 118, 48, 0, 20, 0, 160, 0]这个里面的20和160 这个就是算的,参考上方文章看下原因,大概就是1:8,然后画图和读图的数据一致
相关函数
(经过反复测试得出,打印纸一行最大字节数是32字节,这里指的是普通的票据打印机)
打印三列或者两列,是需要自己计算空格进行填充,没有现成的指令噢
总宽度 – 左侧文字长度 – 右侧文字长度 就是空格的长度。
/** * 打印两列 * * @param leftText 左侧文字 * @param rightText 右侧文字 * @return */ printTwoData(leftText, rightText) { var sb = '' var leftTextLength = this.getBytesLength(leftText); var rightTextLength = this.getBytesLength(rightText); sb += leftText // 计算两侧文字中间的空格 var marginBetweenMiddleAndRight = 32 - leftTextLength - rightTextLength; for (var i = 0; i < marginBetweenMiddleAndRight; i++) { sb += ' ' } sb += rightText return sb.toString(); }, /** * 打印三列 * * @param leftText 左侧文字 * @param middleText 中间文字 * @param rightText 右侧文字 * @return */ printThreeData(leftText, middleText, rightText) { var sb = '' // 左边最多显示 8 个汉字 + 两个点 if (leftText.length > 8) { leftText = leftText.substring(0, 8) + ".."; } var leftTextLength = this.getBytesLength(leftText); var middleTextLength = this.getBytesLength(middleText); var rightTextLength = this.getBytesLength(rightText); sb += leftText // 计算左侧文字和中间文字的空格长度 var marginBetweenLeftAndMiddle = 20 - leftTextLength - middleTextLength / 2; for (var i = 0; i < marginBetweenLeftAndMiddle; i++) { sb += ' ' } sb += middleText // 计算右侧文字和中间文字的空格长度 var marginBetweenMiddleAndRight = 12 - middleTextLength / 2 - rightTextLength; for (var i = 0; i < marginBetweenMiddleAndRight; i++) { sb += ' ' } sb += rightText // 打印的时候发现,最右边的文字总是偏右一个字符,所以需要删除一个空格 // sb.delete(sb.length() - 1, sb.length()).append(rightText); return sb.toString(); }, max(n1, n2) { return Math.max(n1, n2) }, len(arr) { arr = arr || [] return arr.length }, //4合1 convert4to1(res) { let arr = []; for (let i = 0; i < res.length; i++) { if (i % 4 == 0) { let rule = 0.29900 * res[i] + 0.58700 * res[i + 1] + 0.11400 * res[i + 2]; if (rule > 200) { res[i] = 0; } else { res[i] = 1; } arr.push(res[i]); } } return arr; }, //8合1 convert8to1(arr) { let data = []; for (let k = 0; k < arr.length; k += 8) { let temp = arr[k] * 128 + arr[k + 1] * 64 + arr[k + 2] * 32 + arr[k + 3] * 16 + arr[k + 4] * 8 + arr[k + 5] * 4 + arr[k + 6] * 2 + arr[k + 7] * 1 data.push(temp); } return data; }, inArray(arr, key, val) { for (let i = 0; i < arr.length; i++) { if (arr[i][key] === val) { return i; } } return -1; }, // ArrayBuffer转16进度字符串示例 ab2hex(buffer) { var hexArr = Array.prototype.map.call( new Uint8Array(buffer), function(bit) { return ('00' + bit.toString(16)).slice(-2) } ) return hexArr.join(''); }, // 计算文字占用长度 getBytesLength(str) { var num = str.length; //先用num保存一下字符串的长度(可以理解为:先假设每个字符都只占用一个字节) for (var i = 0; i < str.length; i++) { //遍历字符串 if (str.charCodeAt(i) > 255) { //判断某个字符是否占用两个字节,如果是,num再+1 num++; } } return num; //返回最终的num,既是字符串总的字节长度 }
不是我那一卷打印失败的打印纸丢了,不然就让你们看看什么叫做 第一次做打印机开发的程序员
这次是真大干货,篇幅也很长,辛苦了我
厉害了 简直一模一样 以后可以开一家不饿了么公司了
腾云奇袭技能释放时,神明将跳到筋斗云上,并留下一个完美的分身
楼主,向打印机发送给buffer的地方的代码可以详细一点嘛?我目前卡在这一步 很迷惑 可以加我QQ 2930962607
@ 李鱼儿啊 @李鱼儿啊:你得说清楚你的问题
@ 阿珏 @阿珏:我是uniApp的自定义组件模式开发 我是使用的native.js连接的Android手机的蓝牙 页面使用的是web-view的子窗口加载的本地html文件 因为需要使用html2canvas.js插件来截取页面生成图片 在插件的回调方法中可以获取到canvas的dom对象 因此可以获取到imageData的像素点信息 然后我使用你写得4合1 以及 8合1 生成数据 然后在发送到打印机的write方法时 我写了try catch 来捕获发送时的异常 发现问题出现在生成的数据上面 有时候catch会报错讲试图获取一个null Array的长度 有时候会直接报错一个空{} json对象 比较重要的点是 你在buffer数据的转换上面使用到了 node.js 的Buffer对象 但是我这边并不能使用 希望楼主可以帮忙解答一下 方便的话加一下QQ 2930962607
@ 李鱼儿啊 @李鱼儿啊:很抱歉,关于这个Android打印图片数据的问题,我这边也没有完全解决掉,没办法给你更好的解决方案。Android打印这块可以参考一下GoogleAndroid官方的文档,发送数据处理这块应该是有问题的。
这,不是类似于喵喵机的吗,正好我买个了喵喵机P1
@ 『乐 易』 @『乐 易』:这,貌似不是很一样,而且那不是叫咕咕机吗
@ 阿珏 @阿珏:功能一样,只不过喵喵机小一点而已,而且是喵喵机不是咕咕鸡
@ 『乐 易』 @『乐 易』:好吧好吧,原来还有一个叫喵喵机的玩意
很强!有饿了么订单条内味儿了
@ mengkun @mengkun:我们是不饿了么
非技术的路过。
@ repostone @repostone:牛逼就完事了