美文网首页
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实现蓝牙小票打印功能

    最近的一个项目增加了小票蓝牙打印的功能,由于之前对蓝牙打印机了解不多,所以遇到的坑比较多,花了点时间把蓝牙连接、打...

  • iOS CoreBluetooth 的使用讲解

    最近研究了iOS下连接蓝牙打印机,实现打印购物小票的功能,对iOS中BLE 4.0的使用有了一定的了解,这里记录一...

  • 打印机

    iOS开发之蓝牙/Socket链接小票打印机(一)iOS开发之蓝牙/Socket链接小票打印机(二) iOS so...

  • iOS开发 蓝牙打印小票

    要求:手机通过蓝牙连接蓝牙打印机,在手机上点击‘打印’,打印机就打印出小票(小票就跟送外卖的那种)。 设备:BT5...

  • 蓝牙打印小票

    //蓝牙搜索的类 @interface QueryPrinterViewController (){ UIT...

  • Android蓝牙打印小票,仿美团外卖小票打印

    这个一个Android蓝牙打印小票demo,类似美团外卖小票打印自适应排版小票格式,一行两列和三列轻松搞定,文本长...

  • iOS蓝牙4.0打印小票功能的实现

    公司业务有涉及到订单模块,客户需要连接蓝牙打印机打印订单小票。所以本文就记录一下iOS蓝牙打印的相关知识以及实际开...

  • iOS 蓝牙打印小票

    前言: 最近做了款蓝牙打印的功能,包含蓝牙自动连接,蓝牙搜索,连接之后进行打印。总结了下知识点,写了一个简单的De...

  • iOS蓝牙打印小票

    最近搞了个蓝牙打印小票的小东西,先上效果 数据格式 json: 使用: 话不多说上Demo:GitHub地址

  • iOS Bluetooth 打印小票(二)

    在上一篇中介绍了打印小票所需要的命令,这一篇介绍Bluetooth连接蓝牙和打印小票的全过程。 CoreBluet...

网友评论

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

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