最近的一個項目增加了小票藍牙打印的功能鸠窗,由于之前對藍牙打印機了解不多,所以遇到的坑比較多比被,花了點時間把藍牙連接色难、打印模塊封裝成通用組件,并寫了個打印的例子等缀,這里做個記錄枷莉,以防忘記。
組件:組件例子
項目需要實現(xiàn)的是App端連接藍牙打印機尺迂,打印的內(nèi)容包括:
1笤妙、公司logo(本地圖片)
2、指定格式與排版的文本數(shù)據(jù)(直線噪裕、中英文)
3蹲盘、簽名的圖片數(shù)據(jù)(接口傳回的網(wǎng)絡(luò)圖片地址)
首先要想能打印,藍牙肯定要連接上膳音,開發(fā)前了解一下低功耗藍牙連接操作流程:
按照api來走一遍從連接到打印的流程:
這里我將藍牙連接和打印的流程封裝在組件使用召衔,父組件中只需在onPrint方法中拼接指令,通過bufferData屬性將拼接好的指令數(shù)據(jù)傳入組件內(nèi)部即可進行打印操作祭陷,onPrintSuccess和onPrintFail分別為打印成功與失敗的回調(diào):
<kk-printer ref="kkprinter" :bufferData="bufferData" @onPrint="onPrint" @onPrintSuccess="onPrintSuccess" @onPrintFail="onPrintFail"></kk-printer>
組件內(nèi)部的實現(xiàn)如下:
1苍凛、點擊打印按鈕,打開藍牙適配器(openBluetoothAdapter),并獲取已連接的設(shè)備(getConnectedBluetoothDevices)兵志,如果沒有已連接的設(shè)備則打開搜索設(shè)備彈框進行設(shè)備搜索(第2步)醇蝴,若設(shè)備已連接,則執(zhí)行打印方法(第4步)
blesdk.openBlue().then((res)=>{
//獲取已連接設(shè)備
blesdk.getConnectedBluetoothDevices().then((res)=>{
//若沒有已連接設(shè)備毒姨,彈框搜索設(shè)備
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藍牙相關(guān)的api統(tǒng)一放到一個文件中,并將方法轉(zhuǎn)為異步,其中還包括添加CPCL指令的字符拼接方法弧呐。gbk是一個用于將數(shù)據(jù)轉(zhuǎn)碼為打印機能夠接受的數(shù)據(jù)格式的模塊
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: '沒有找到指定設(shè)備',
10003: '連接失敗',
10004: '沒有找到指定服務(wù)',
10005: '沒有找到指定特征值',
10006: '當前連接已斷開',
10007: '當前特征值不支持此操作',
10008: '系統(tǒng)上報異常',
10009: '系統(tǒng)版本低于 4.3 不支持BLE'
};
let coode = err.errCode ? err.errCode.toString() : '';
let msg = errMsg[coode];
plus.nativeUI.toast(msg || coode, {
align: 'center',
verticalAlign: 'center'
});
}
2闸迷、打開搜索設(shè)備彈框(isShowSearch控制彈框顯示隱藏)
點擊開始搜索(startBluetoothDevicesDiscovery),并監(jiān)聽搜索到的新設(shè)備(onfindBlueDevices)俘枫,藍牙搜索的操作比較耗費系統(tǒng)資源腥沽,所以建議在連接上設(shè)備、頁面銷毀時關(guān)閉搜索(stopBlueDevicesDiscovery),這里鸠蚪,我加了兩個按鈕控制搜索的開關(guān)
//開始搜索設(shè)備
searchBtnTap(){
blesdk.startBluetoothDevicesDiscovery();
this.isSearching = true;
blesdk.onfindBlueDevices(this.onGetDevice)
},
//停止搜索設(shè)備
stopSearchBtnTap(){
blesdk.stopBlueDevicesDiscovery();
this.isSearching = false;
},
由于加了篩選條件(rssi和設(shè)備名今阳、設(shè)備ID)因此需要對onfindBlueDevices監(jiān)聽到的設(shè)備列表進行篩選
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
}
// 根據(jù)廣播數(shù)據(jù)提取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师溅、設(shè)備列表點擊選擇連接設(shè)備
①設(shè)備列表中的每一項都可以獲取到設(shè)備的name、deviceId等信息盾舌,連接時我們需要的就是deviceId墓臭,創(chuàng)建藍牙連接(createBLEConnection),在這之前可以通過onBLEConnectionStateChange監(jiān)聽連接狀態(tài)的變化
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('設(shè)備'+ res.deviceId + '已連接',{
verticalAlign:'center'
})
}else{
plus.nativeUI.toast('設(shè)備'+ res.deviceId + '已斷開連接',{
verticalAlign:'center'
})
}
})
blesdk.createBLEConnection(deviceId, this.onConnectSuccess, this.onConnectFail);
},
②連接成功后順便把搜索設(shè)備開關(guān)關(guān)掉妖谴。連接成功后需要通過deviceId獲取設(shè)備服務(wù)(getBLEDeviceServices),這里獲取時需要給方法設(shè)個延時窿锉,否則獲取出來的serviceId會是空的
onConnectSuccess(res){
this.stopSearchBtnTap()
blesdk.getBLEDeviceServices(this.deviceId, this.onGetServicesSuccess, this.onGetServicesFail);
},
③獲取設(shè)備服務(wù)成功后會返回servicesId數(shù)組,接著我們需要用deviceId和serviceId來獲取特征值(getDeviceCharacteristics)
onGetServicesSuccess(res){
console.log('獲取服務(wù)',res)
this.services = res.serviceId;
blesdk.getDeviceCharacteristics(this.deviceId, this.services, this.onGetCharacterSuccess, this.onGetCharacterFail);
},
④獲取到特征值之后需要找個變量將特征值暫存膝舅,因為后續(xù)向打印機發(fā)送數(shù)據(jù)時需要用到特征值嗡载。關(guān)閉搜索彈框
onGetCharacterSuccess(res){
console.log('獲取特征值成功',res)
this.serviceId = res.serviceId;
this.writeId = res.writeId;
this.readId = res.readId;
this.isShowSearch = false;
},
4、在連接上設(shè)備后仍稀,點擊打印按鈕洼滚,這時就可以開始拼接打印數(shù)據(jù)了。在第1步中做過判斷如果有已連接設(shè)備技潘,則開始拼接數(shù)據(jù)并打印遥巴,這里將拼接的任務(wù)交給父頁面(onPrint),拼接完成后通過bufferData傳入,在bufferData數(shù)據(jù)更新后開始將數(shù)據(jù)發(fā)送給設(shè)備享幽,所需的參數(shù)即opt中的參數(shù)挪哄,deviceId為設(shè)備id;serviceId為服務(wù)id琉闪;characteristicId為特征碼;value為寫入的數(shù)據(jù)砸彬,需轉(zhuǎn)成GBK格式颠毙;lasterSuccess為數(shù)據(jù)全部發(fā)送成功的回調(diào);onceLength為分包發(fā)送的每個數(shù)據(jù)包長度砂碉,因為安卓和iOS有不同蛀蜜,所以加個判斷。sendDataToDevice中封裝了分包發(fā)送的方法增蹭。
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數(shù)據(jù),添加CPCL指令的方法放在bluetoolth.js中(@/components/kk-printer/utils/bluetoolth.js)滋迈,以下為組件示例展示的一部分常用的指令拼接方法霎奢,可查看項目中的@/components/kk-printer/utils/bluetoolth.js文件了解指令封裝方法的具體實現(xiàn)
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組件
②使用組件
<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>
拼接數(shù)據(jù)時將不同的打印需求分不同方法拼接
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()完成后執(zhí)行,不然獲取到的圖像像素點數(shù)據(jù)會全是0碍彭。使用addCPCLImageCmd時注意調(diào)整灰度值threshold晤硕,灰度值過高或過低會導(dǎo)致低于灰度值的圖像像素點在方法中被篩掉悼潭,變成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,'件數(shù) ');
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)
})
},
最后講一下數(shù)據(jù)(bufferData)拼接的注意點:
①向藍牙打印機發(fā)送數(shù)據(jù)打印舞箍,發(fā)送的任何內(nèi)容都應(yīng)該要轉(zhuǎn)成二進制數(shù)據(jù)舰褪,而且藍牙打印的文本編碼是GBK的,發(fā)送中文需轉(zhuǎn)成GBK編碼再轉(zhuǎn)成二進制數(shù)據(jù)發(fā)送疏橄,包括發(fā)送打印機指令也要轉(zhuǎn)成二進制數(shù)據(jù)發(fā)送
②藍牙打印機一次接收的二級制數(shù)據(jù)有限制占拍,不同的系統(tǒng)不同的藍牙設(shè)備限制可能不同,建議一次20個字節(jié)软族,做遞歸分包發(fā)送
③發(fā)送完要打印的內(nèi)容后刷喜,一定要發(fā)送一個打印的指令才能順利打印 (部分指令不需要)
④在分包發(fā)送的時候,由于設(shè)備連接不穩(wěn)定立砸,經(jīng)常會出現(xiàn)10007掖疮,找不到特征值的情況,需要在失敗回調(diào)中記錄斷點颗祝,繼續(xù)發(fā)送后續(xù)的包
如有錯誤的地方浊闪,歡迎評論指出
轉(zhuǎn)載請注明出處