美文网首页
uniapp实现蓝牙小票打印功能

uniapp实现蓝牙小票打印功能

作者: 2025丶10丶16 | 来源:发表于2021-06-17 22:40 被阅读0次

    最近的一个项目增加了小票蓝牙打印的功能,由于之前对蓝牙打印机了解不多,所以遇到的坑比较多,花了点时间把蓝牙连接、打印模块封装成通用组件,并写了个打印的例子,这里做个记录,以防忘记。

    组件:组件例子

    项目需要实现的是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,找不到特征值的情况,需要在失败回调中记录断点,继续发送后续的包


    如有错误的地方,欢迎评论指出
    转载请注明出处

    相关文章

      网友评论

          本文标题:uniapp实现蓝牙小票打印功能

          本文链接:https://www.haomeiwen.com/subject/beyhyltx.html