最近的一个项目增加了小票蓝牙打印的功能,由于之前对蓝牙打印机了解不多,所以遇到的坑比较多,花了点时间把蓝牙连接、打印模块封装成通用组件,并写了个打印的例子,这里做个记录,以防忘记。
组件:组件例子
项目需要实现的是App端连接蓝牙打印机,打印的内容包括:
image.png
1、公司logo(本地图片)
2、指定格式与排版的文本数据(直线、中英文)
3、签名的图片数据(接口传回的网络图片地址)
首先要想能打印,蓝牙肯定要连接上,开发前了解一下低功耗蓝牙连接操作流程:
低功耗蓝牙连接流程
按照api来走一遍从连接到打印的流程:
这里我将蓝牙连接和打印的流程封装在组件使用,父组件中只需在onPrint方法中拼接指令,通过bufferData属性将拼接好的指令数据传入组件内部即可进行打印操作,onPrintSuccess和onPrintFail分别为打印成功与失败的回调:
<kk-printer ref="kkprinter" :bufferData="bufferData" @onPrint="onPrint" @onPrintSuccess="onPrintSuccess" @onPrintFail="onPrintFail"></kk-printer>
组件内部的实现如下:
1、点击打印按钮,打开蓝牙适配器(openBluetoothAdapter),并获取已连接的设备(getConnectedBluetoothDevices),如果没有已连接的设备则打开搜索设备弹框进行设备搜索(第2步),若设备已连接,则执行打印方法(第4步)
blesdk.openBlue().then((res)=>{
//获取已连接设备
blesdk.getConnectedBluetoothDevices().then((res)=>{
//若没有已连接设备,弹框搜索设备
console.log(res,this.deviceId,this.serviceId,this.writeId,this.bufferData,this.onPrintSuccess)
if(res.devices.length == 0){
this.isShowSearch = true
}else{
let datalen=20;
if (plus.os.name != 'Android')
{
datalen=180;
}
this.isPrinting = true;
this.$emit('onPrint');
this.$nextTick(()=>{
console.log(1,this.bufferData)
if(this.bufferData!=''){
let buffer = gbk.strToGBKByte(this.bufferData)
console.log(2,buffer)
let opt = {
deviceId: this.deviceId,
serviceId: this.serviceId,
characteristicId: this.writeId,
value:buffer,
lasterSuccess: this.onPrintSuccess,
onceLength:datalen
}
console.log(3,opt)
blesdk.sendDataToDevice(opt);
this.isPrinting = false;
}
})
}
}).catch((err)=>{
blesdk.catchToast(err);
})
}).catch((err)=>{
blesdk.catchToast(err);
})
这里的blesdk是为了方便使用,把uniapp蓝牙相关的api统一放到一个文件中,并将方法转为异步,其中还包括添加CPCL指令的字符拼接方法。gbk是一个用于将数据转码为打印机能够接受的数据格式的模块
export function uniAsyncPromise(name, options) {
return new Promise((resolve, reject) => {
uni[name]({
...(options || {}),
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
}
});
});
}
export function openBlue() {
return uniAsyncPromise('openBluetoothAdapter')
}
...
/**
* toast显示捕获的蓝牙异常
*/
export function catchToast(err) {
const errMsg = {
10000: '未初始化蓝牙模块',
10001: '蓝牙未打开',
10002: '没有找到指定设备',
10003: '连接失败',
10004: '没有找到指定服务',
10005: '没有找到指定特征值',
10006: '当前连接已断开',
10007: '当前特征值不支持此操作',
10008: '系统上报异常',
10009: '系统版本低于 4.3 不支持BLE'
};
let coode = err.errCode ? err.errCode.toString() : '';
let msg = errMsg[coode];
plus.nativeUI.toast(msg || coode, {
align: 'center',
verticalAlign: 'center'
});
}
2、打开搜索设备弹框(isShowSearch控制弹框显示隐藏)
点击开始搜索(startBluetoothDevicesDiscovery),并监听搜索到的新设备(onfindBlueDevices),蓝牙搜索的操作比较耗费系统资源,所以建议在连接上设备、页面销毁时关闭搜索(stopBlueDevicesDiscovery),这里,我加了两个按钮控制搜索的开关
//开始搜索设备
searchBtnTap(){
blesdk.startBluetoothDevicesDiscovery();
this.isSearching = true;
blesdk.onfindBlueDevices(this.onGetDevice)
},
//停止搜索设备
stopSearchBtnTap(){
blesdk.stopBlueDevicesDiscovery();
this.isSearching = false;
},
由于加了筛选条件(rssi和设备名、设备ID)因此需要对onfindBlueDevices监听到的设备列表进行筛选
computed:{
filterDeviceList(){
let devices = this.devicesList;
let name = this.filterName;
let rssi = this.filterRSSI;
//按RSSI过滤
let filterDevices1 = devices.filter((item)=>{
return item.RSSI > rssi
})
console.log(filterDevices1)
// 按名字过滤
let filterDevices2
if(name!=''){
filterDevices2 = filterDevices1.filter((item)=>{
return (item.name.indexOf(name) >= 0 || item.deviceId.indexOf(name) >= 0)
})
}else{
filterDevices2 = filterDevices1
}
// 根据广播数据提取MAC地址
for (let i = 0; i < filterDevices2.length;i++) {
if (filterDevices2[i].hasOwnProperty('advertisData')){
if (filterDevices2[i].advertisData.byteLength == 8) {
filterDevices2[i].advMac = util.buf2hex(filterDevices2[i].advertisData.slice(2, 7));
}
}
}
return filterDevices2
}
},
3、设备列表点击选择连接设备
①设备列表中的每一项都可以获取到设备的name、deviceId等信息,连接时我们需要的就是deviceId,创建蓝牙连接(createBLEConnection),在这之前可以通过onBLEConnectionStateChange监听连接状态的变化
handleConnectDevice(device){
let deviceId = device.deviceId;
let name = device.name;
this.deviceId = deviceId;
uni.onBLEConnectionStateChange((res)=>{
console.log('连接',res)
if(res.connected){
plus.nativeUI.toast('设备'+ res.deviceId + '已连接',{
verticalAlign:'center'
})
}else{
plus.nativeUI.toast('设备'+ res.deviceId + '已断开连接',{
verticalAlign:'center'
})
}
})
blesdk.createBLEConnection(deviceId, this.onConnectSuccess, this.onConnectFail);
},
②连接成功后顺便把搜索设备开关关掉。连接成功后需要通过deviceId获取设备服务(getBLEDeviceServices),这里获取时需要给方法设个延时,否则获取出来的serviceId会是空的
onConnectSuccess(res){
this.stopSearchBtnTap()
blesdk.getBLEDeviceServices(this.deviceId, this.onGetServicesSuccess, this.onGetServicesFail);
},
③获取设备服务成功后会返回servicesId数组,接着我们需要用deviceId和serviceId来获取特征值(getDeviceCharacteristics)
onGetServicesSuccess(res){
console.log('获取服务',res)
this.services = res.serviceId;
blesdk.getDeviceCharacteristics(this.deviceId, this.services, this.onGetCharacterSuccess, this.onGetCharacterFail);
},
④获取到特征值之后需要找个变量将特征值暂存,因为后续向打印机发送数据时需要用到特征值。关闭搜索弹框
onGetCharacterSuccess(res){
console.log('获取特征值成功',res)
this.serviceId = res.serviceId;
this.writeId = res.writeId;
this.readId = res.readId;
this.isShowSearch = false;
},
4、在连接上设备后,点击打印按钮,这时就可以开始拼接打印数据了。在第1步中做过判断如果有已连接设备,则开始拼接数据并打印,这里将拼接的任务交给父页面(onPrint),拼接完成后通过bufferData传入,在bufferData数据更新后开始将数据发送给设备,所需的参数即opt中的参数,deviceId为设备id;serviceId为服务id;characteristicId为特征码;value为写入的数据,需转成GBK格式;lasterSuccess为数据全部发送成功的回调;onceLength为分包发送的每个数据包长度,因为安卓和iOS有不同,所以加个判断。sendDataToDevice中封装了分包发送的方法。
let datalen=20;
if (plus.os.name != 'Android')
{
datalen=180;
}
this.isPrinting = true;
this.$emit('onPrint');
this.$nextTick(()=>{
if(this.bufferData!=''){
let buffer = gbk.strToGBKByte(this.bufferData)
let opt = {
deviceId: this.deviceId,
serviceId: this.serviceId,
characteristicId: this.writeId,
value:buffer,
lasterSuccess: this.onPrintSuccess,
onceLength:datalen
}
blesdk.sendDataToDevice(opt);
this.isPrinting = false;
}
5、父页面的onPrint中拼接bufferData数据,添加CPCL指令的方法放在bluetoolth.js中(@/components/kk-printer/utils/bluetoolth.js),以下为组件示例展示的一部分常用的指令拼接方法,可查看项目中的@/components/kk-printer/utils/bluetoolth.js文件了解指令封装方法的具体实现
import * as blesdk from './utils/bluetoolth';
import util from './utils/util.js';
let strCmd =blesdk.CreatCPCLPage(560,500,1,0);
strCmd += blesdk.addCPCLBox(0,0,560,400,3);
strCmd += blesdk.addCPCLLine(0,210,560,210,3);
strCmd += blesdk.addCPCLText(10,0,'4','3',0,'8.14');
strCmd += blesdk.addCPCLBarCode(270,0,'128',80,0,1,1,'00051');
strCmd += blesdk.addCPCLText(290,80,'7','2',0,'00051');
strCmd += blesdk.addCPCLText(40,110,'3','0',0,'CHICKEN FEET (BONELESS)-Copy-Copy');
strCmd += blesdk.addCPCLSETMAG(2,2);
strCmd += blesdk.addCPCLText(40,150,'55','0',0,'无骨鸡爪 一盒(约1.5磅)');
strCmd += blesdk.addCPCLSETMAG(0,0);
strCmd += blesdk.addCPCLText(350,180,'7','2',0,'2019-08-12');
strCmd += blesdk.addCPCLLocation(2);
strCmd += blesdk.addCPCLQRCode(0,220,'M', 2, 6, 'qr code test');
strCmd += blesdk.addCPCLPrint();
this.bufferData = strCmd;
6、实际项目中使用:
①页面引入并使用components文件夹中的kk-printer组件
image.png
②使用组件
<view class="fixed-btn-wrap">
<view class="sign-btn" @tap="onSignTap">签名</view>
<view class="print-btn">
<kk-printer ref="kkprinter" :bufferData="bufferData" @onPrint="onPrint"></kk-printer>
</view>
</view>
拼接数据时将不同的打印需求分不同方法拼接
onPrint(successCallback){
let p1 = this.logoToStr('/static/img/receipt-log.png');
let p2 = this.addBaseInfo();
let p3 = this.addTicketsInfo();
let p4 = this.addSignInfo('canvasTallyMan','理货员');
let p5 = this.addSignInfo('canvasYard','库场');
let p6 = this.addSignInfo('canvasTrucker','司机',true);
Promise.all([p1,p2,p3,p4,p5,p6]).then((res)=>{
this.bufferData = res.join('');
this.$nextTick(()=>{
successCallback&&successCallback()
})
})
},
打印logo图片、打印签名图片,在canvasGetImageData前需要注意加个延时或等待draw()完成后执行,不然获取到的图像像素点数据会全是0。使用addCPCLImageCmd时注意调整灰度值threshold,灰度值过高或过低会导致低于灰度值的图像像素点在方法中被筛掉,变成0
logoToStr(src){
return new Promise((resolve,reject)=>{
const ctx = uni.createCanvasContext('tempCanvas');
uni.getImageInfo({
src: src,
success: (res) => {
const w = res.width;
const h = res.height;
ctx.drawImage(src, 0, 0, 350, 42);
ctx.draw();
setTimeout(() => {
uni.canvasGetImageData({
canvasId: 'tempCanvas',
x: 0, y: 0,
width: w,
height: h,
success: (res) => {
const pix = res.data;
let strCmd = blesdk.CreatCPCLPage(576,h+10,1);
strCmd += blesdk.addCPCLImageCmd(0,0,{imageData:pix, width:w, height:h, threshold:30,isSign:false});
strCmd += blesdk.addCPCLPrint();
resolve(strCmd)
},
});
}, 500);
},
});
})
},
addBaseInfo:打印基本信息
addTicketsInfo:打印提单信息
这两个都是打印文本与排版的,没有什么难点
addBaseInfo(){
return new Promise((resolve,reject)=>{
let strCmd =blesdk.CreatCPCLPage(560,355,1,0);
strCmd += blesdk.addCPCLLine(0,0,560,0,2);
strCmd += blesdk.addCPCLLocation(2);
strCmd += blesdk.addCPCLText(0,15,'8','0',0,'理货小票');
strCmd += blesdk.addCPCLLocation(0);
strCmd += blesdk.addCPCLText(0,55,'8','0',0,'船名 ');
strCmd += blesdk.addCPCLText(80,55,'3','0',0,this.tallyDoc.vesselName);
strCmd += blesdk.addCPCLText(0,95,'8','0',0,'航次 ');
strCmd += blesdk.addCPCLText(80,95,'3','0',0,this.tallyDoc.voyageNo);
strCmd += blesdk.addCPCLText(0,135,'8','0',0,'编号 ');
strCmd += blesdk.addCPCLText(80,135,'3','0',0,this.getDisplayValue(this.tallyDoc.tallyDocNo));
strCmd += blesdk.addCPCLLine(0,190,560,190,2);
strCmd += blesdk.addCPCLText(0,220,'8','0',0,'日期 ');
strCmd += blesdk.addCPCLText(80,220,'8','0',0,this.getData(this.tallyDoc.startTime));
strCmd += blesdk.addCPCLText(280,220,'8','0',0,'时间 ');
strCmd += blesdk.addCPCLText(360,220,'8','0',0,this.getTime(this.tallyDoc.startTime));
strCmd += blesdk.addCPCLText(0,260,'8','0',0,'车号 ');
strCmd += blesdk.addCPCLText(80,260,'8','0',0,this.getDisplayValue(this.tallyDoc.tractorNo));
strCmd += blesdk.addCPCLText(280,260,'8','0',0,'班次 ');
strCmd += blesdk.addCPCLText(360,260,'8','0',0,this.getShiftNoForCN(this.tallyDoc.shiftNo));
strCmd += blesdk.addCPCLText(0,300,'8','0',0,'件数 ');
strCmd += blesdk.addCPCLText(80,300,'8','0',0,this.getDisplayValue(this.tallyDoc.pkgNum)+'件');
strCmd += blesdk.addCPCLText(280,300,'8','0',0,'舱口 ');
strCmd += blesdk.addCPCLText(360,300,'8','0',0,this.tallyDoc.hatchName);
strCmd += blesdk.addCPCLPrint();
resolve(strCmd)
})
},
最后讲一下数据(bufferData)拼接的注意点:
①向蓝牙打印机发送数据打印,发送的任何内容都应该要转成二进制数据,而且蓝牙打印的文本编码是GBK的,发送中文需转成GBK编码再转成二进制数据发送,包括发送打印机指令也要转成二进制数据发送
②蓝牙打印机一次接收的二级制数据有限制,不同的系统不同的蓝牙设备限制可能不同,建议一次20个字节,做递归分包发送
③发送完要打印的内容后,一定要发送一个打印的指令才能顺利打印 (部分指令不需要)
④在分包发送的时候,由于设备连接不稳定,经常会出现10007,找不到特征值的情况,需要在失败回调中记录断点,继续发送后续的包
如有错误的地方,欢迎评论指出
转载请注明出处
网友评论