uniapp實現(xiàn)藍牙小票打印功能

最近的一個項目增加了小票藍牙打印的功能鸠窗,由于之前對藍牙打印機了解不多,所以遇到的坑比較多比被,花了點時間把藍牙連接色难、打印模塊封裝成通用組件,并寫了個打印的例子等缀,這里做個記錄枷莉,以防忘記。

組件:組件例子

項目需要實現(xiàn)的是App端連接藍牙打印機尺迂,打印的內(nèi)容包括:


image.png

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組件


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>

拼接數(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)載請注明出處

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市螺戳,隨后出現(xiàn)的幾起案子搁宾,更是在濱河造成了極大的恐慌,老刑警劉巖倔幼,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盖腿,死亡現(xiàn)場離奇詭異,居然都是意外死亡损同,警方通過查閱死者的電腦和手機翩腐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來膏燃,“玉大人茂卦,你說我怎么就攤上這事∽榱ǎ” “怎么了等龙?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長伶贰。 經(jīng)常有香客問我蛛砰,道長,這世上最難降的妖魔是什么黍衙? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任暴备,我火速辦了婚禮,結(jié)果婚禮上们豌,老公的妹妹穿的比我還像新娘涯捻。我一直安慰自己浅妆,他們只是感情好,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布障癌。 她就那樣靜靜地躺著凌外,像睡著了一般。 火紅的嫁衣襯著肌膚如雪涛浙。 梳的紋絲不亂的頭發(fā)上康辑,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機與錄音轿亮,去河邊找鬼疮薇。 笑死,一個胖子當著我的面吹牛我注,可吹牛的內(nèi)容都是我干的按咒。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼但骨,長吁一口氣:“原來是場噩夢啊……” “哼励七!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起奔缠,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤掠抬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后校哎,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體两波,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年闷哆,在試婚紗的時候發(fā)現(xiàn)自己被綠了雨女。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡阳准,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出馏臭,到底是詐尸還是另有隱情野蝇,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布括儒,位于F島的核電站绕沈,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏帮寻。R本人自食惡果不足惜乍狐,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望固逗。 院中可真熱鬧浅蚪,春花似錦藕帜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至盗誊,卻和暖如春时甚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背哈踱。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工荒适, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人开镣。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓刀诬,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哑子。 傳聞我的和親對象是個殘疾皇子舅列,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容