微信小程序连接蓝牙打印机采坑之旅

更新日期: 2019-09-01阅读: 7.1k标签: 蓝牙

目前小程序生态越来越丰富,微信给予了小程序一定的硬件通信能力这是之前 Web 很少尝试的事情。关于蓝牙,常见的就下面几个 api

  • startBluetoothDevicesDiscovery 开始搜寻附近的蓝牙外围设备
    openBluetoothAdapter 初始化蓝牙模块
    wx.onBluetoothDeviceFound 监听寻找到新设备的事件
    wx.writeBLECharacteristicValue 向低功耗蓝牙设备特征值中写入二进制数据
    wx.onBLEConnectionStateChange 监听低功耗蓝牙连接状态的改变事件
    wx.createBLEConnection 连接低功耗蓝牙设备
  • 不过实际我们在实现过程中,我们可能还会遇到一些本身 API 在不同平台上 BUG。


    中文乱码

    打印的时候,第一个发现的问题便是打印机无法正常打印中文字符串。在向蓝牙写数据的时候,我们实际上市向蓝牙发送的 buffer ,因此我们需要将对应的字符串转换成设备可支持的中文转码,比如 GBK GB2312,网上有一些现成的库,安利一个简单的 github:https://github.com/inexorabletash/text-encoding

    这样的话,我们只需要进行引用,然后

    // text-encoding 为引用的代码目录
    import { TextEncoder } from '../text-encoding';  
    this._encoder = new TextEncoder("gb2312", {NONSTANDARD_allowLegacyEncoding: true});
    
    // content 需要打印的字符串
    const uint8Array = this._encoder.encode(content);

    由于小程序包大小的限制,可以手动将 encoding-indexs.js 转换成 JSON 放到远程,然后动态 request 下来。


    设置格式

    我们在打印的时候,往往不是简单的一串串字符串,而是需要进行大小,对齐方式的跳转。打印机是能够接受 ESC/POS

    其中有一些我么需要用到的指令。

    ASCII码  ESC  a   n  
    十进制码  27   97  n

    其中 n 的值表示不同的对齐方式:

  • 0 左对齐

  • 1 中间对齐

  • 2 由对齐

  • 我们则在发给打印机的 buffer 数据里需要包含对应的命令

    [[27, 97,1], [....]]

    打印接下来按照中间对齐进行打印。

    当然除了对齐,我们还能对字体大小和粗细进行调整。


    字体加粗

    ASCII码  ESC   ! n   十进制码  27   33  n
  • 0 取消加粗

  • 8 加粗


  • 字体大小

    ASCII码  ESC   ! n   十进制码  27   33  n
  • 0 正常

  • 16 倍高

  • 32 倍宽

  • 当然,我们其实不用太详细的去了解这些指令,推荐一个打印库,它类似翻译了这些指令,可以按照前端的方式进行打印。https://github.com/benioZhang/miniprogram-bluetoothprinter/tree/master/printer

    const printerJobs = new PrinterJobs();  
        printerJobs.print('')
          .setAlign('ct') // 设置居中对齐
          .setSize(2, 2) // 设置字体大小
          .print(name)
        await printerJobs.printQRCode(qrcode);
        printerJobs.setSize(1, 1)
          .print(printerUtil.inline('地址:', address)) // 两边对齐
          .print('')
          .print(printerUtil.fillLine()) // 打印虚线
          .println();
        const buffer = printerJobs.buffer();


    打印二维码

    当然现在小票上都有二维码,因此在打印二维码的时候,我们需要关注大小和 canvas 的换算。

    其实打印二维码的流程主要是小面这几部

    getImageInfo(微信获取二维码图像信息) -> createCanvasContext(微信创建 Canvas 上下文)  -> drawImage (在 canvas 上绘制二维码) -> canvasGetImageData (获取 canvas 上的像素数据) -> 灰度运算 / 像素转换成点 -> toBuffer(转换成 Buffer 数据发送给蓝牙)  

    我们在上面 printerjobs.js 扩展就是类似下面

    const _drawQRCode = async (path, width, height, callback) => {  
     // 'canvas' mean the canvas id in your view
      const ctx = wx.createCanvasContext('canvas');
      // const ctx = offscreenCanvas.getContext('2d');
      ctx.drawImage(path, 0, 0, width, height, 0, 0, 256, 256);
      ctx.draw(false, () => {
        wx.canvasGetImageData({
          canvasId: 'canvas',
          x: 0,
          y: 0,
          width: 256,
          height: 256,
          success: (res) => {
            let arr = util.convert4to1(res.data);
            let data = util.convert8to1(arr);
            const cmds = [].concat([27, 97, 1], [29, 118, 48, 0, 32, 0, 0, 1], data, [27, 74, 3], [27, 64]);
            const buffer = Buffer.from(cmds, 'gb2312');
            callback(buffer);
          },
          fail: function(error) {
            console.log("error:", error);
          },
          complete: () => {
            // console.log('finished');
          }
        })
      });
    }
    
    printerJobs.prototype.printQRCode = async function (qrcode) {  
      return new Promise((resolve, reject) => {
        wx.getImageInfo({
          src: qrcode,
          success: (result) => {
            setTimeout(() => {
              const path = result.path;
              _drawQRCode(path, result.width, result.height, (cmds) => {
                // const uint8Array = new Uint8Array(cmds);
                this._enqueue(cmds);
                this._enqueue(commands.LF);
                resolve();
              });
            }, 200);
          },
          fail: () => {
            reject('cannot draw image');
          }
        })
      })
    };

    其中大家需要理解

    [27, 97, 1], [29, 118, 48, 0, 32, 0, 0, 1], data, [27, 74, 3], [27, 64]

    这其中 [27, 97, 1] 我们上面已经知道,这其实表示对齐方式,为居中对齐。

    [29, 118, 48, 0, 32, 0, 0, 1] 则是表示打印光栅位图。

    ASCII码    GS  v   0    m  xL  xH  yL  yH  d1...dk
     十进制码    29  118  48  m  xL  xH  yL  yH  d1...dk

    其中 m 表示设置是否放大打印图像,详见 https://www.jianshu.com/p/dd6ca0054298

    xl XH yL yH 需要我们自己进行计算

    比如你的canvas打印的大小为 120 x 120

    xl = 120 / 8 % 256  
    xH = 120 / 8 / 256 // 取整  
    yl = 120 % 256;  
    yH = 120 / 256 // 取整

    所以你的指令就是 [29, 118, 48, 0, 15, 120, 0, 1]。

    如果这个错误,一般打印出来也是乱码。

    convert4to1 和 convert8to1 比较常规的实现:

    function convert4to1(res) {  
      let arr=[];
      for (let i = 0; i < res.length; i++) {
        if (i % 4 == 0) {
          let rule = 0.29900 * res[i] + 0.58700 * res[i + 1] + 0.11400 * res[i + 2];
          if (rule > 200) {
            res[i] = 0;
          } else {
            res[i] = 1;
          }
          arr.push(res[i]);
        }
      }
      return arr;
    }
    
    function convert8to1(arr) {  
      let data = [];
      for (let k = 0; k < arr.length; k += 8) {
        let temp = arr[k] * 128 + arr[k + 1] * 64 + arr[k + 2] * 32 + arr[k + 3] * 16 + arr[k + 4] * 8 + arr[k + 5] * 4 + arr[k + 6] * 2 + arr[k + 7] * 1
        data.push(temp);
      }
      return data;
    }


    Android 蓝牙连接找不到设备

    经常在测试的时候,发现第一次扫码连接正常,而第二次就死活都不进行设备扫描了。主要就是不再触发 onBluetoothDeviceFound 。原因是 Android 我们之前已经建立连接了,但是我们经常重新扫描的时候,并没有断开。因此建议我们手动关闭连接然后再重新打开连接。

    wx.closeBluetoothAdapter();   
    wx.openBluetoothAdapter();


    处理蓝牙断开

    这是两方面的问题,一个蓝牙未打开,而是设备工作中突然断掉(停电或者手误关闭等)

    微信有两个 API 可以帮助你解决这个问题:

    // 处理手机蓝牙打开和关闭
    wx.onBluetoothAdapterStateChange(function (res) {  
                console.log('onBluetoothAdapterStateChange', res);
      if (res.available) {
         // @TODO
      }
    });
    
    // 处理设备突然断掉
    wx.onBLEConnectionStateChange((res) => {  
         if (!res.connected && this.data.connected) {
          @TODO
         }
        });

    原文:https://www.jackpu.com/wei-xin-xiao-cheng-xu-lian-jie-lan-ya-da-yin-ji-cai-keng-zhi-lu/

    链接: https://fly63.com/article/detial/5062

    内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!