微信小程序中实现蓝牙打印

近期调试现有的项目,涉及到了微信蓝牙打印,因此特意总结一下开发思路以及实现逻辑。

一、开发环境

  1. 微信开发者工具 V1.06.2303220;调试基础库 3.3.4
  2. 低功耗蓝牙打印机(设备的特征支持 write)蓝牙版本 4.2 ;中心设备/主机 模式;中文格式为 gbk 编码
  3. iOS 6.5.6,Android 6.5.7以上

二、测试机型

  1. OPPO Android 13 =》可正常连接打印
  2. HarmonyOs 4.0.0 =》 可正常连接打印
  3. 型号 iPhone14 Pro Max,版本 iPhone 17.1.2

注意事项

在 iPhone14 Pro Max,版本 iPhone 17.1.2 中,手机打开蓝牙在其他设备列表 无法显示该打印机设备,但是通过小程序中搜索蓝牙设备是能找到该设备的 可直接打印。

需要注意的是,该iPhone 机型下,写入特征值数据与关闭蓝牙逻辑流程跟安卓机不同;因此在处理逻辑时需要注意,否则iPhone 在未完成打印之前就会关闭蓝牙导致数据无法正常打印。

安卓:

iPhone

三、说明

1、角色/工作模式

蓝牙低功耗协议给设备定义了若干角色,或称工作模式;本次讲解中,使用的是中心设备/主机 (Central)
概念

中心设备可以扫描外围设备,并在发现有外围设备存在后与之建立连接,之后就可以使用外围设备提供的服务(Service)。

一般而言,手机会担任中心设备的角色,利用外围设备提供的数据进行处理或展示等等。小程序提供低功耗蓝牙接口是默认设定手机为中心设备的。

2、整体思路

首先我先用思维导图,简单描述一下蓝牙打印的处理逻辑,让大家了解一下整体的路线,
正常情况下,基础路线:

四、功能实现

本文中我将蓝牙打印分为两部分:正常连接指定设备的蓝牙传递数据信息进行打印

1、蓝牙功能实现

(1)初始化蓝牙模块

我们在进行蓝牙相关操作的时候,首先就是要:初始化蓝牙模块。

注意事项:

  1. 用户是否打开了蓝牙,进行提示,并监听蓝牙适配器状态变化,当用户开启蓝牙后再次触发后续逻辑
  2. 要防止用户拒绝使用蓝牙申请,并针对用户的误操作引导用户再次授权蓝牙
  3. iOS 上开启主机/从机(外围设备)模式时需分别调用一次,并指定对应的 mode
js 复制代码
/**
 * 初始化蓝牙模块
 */
function openBlueTooth() {
  wx.openBluetoothAdapter({
    mode: 'central', // 蓝牙模式,可作为主/从设备,仅 iOS 需要。
    success: (res) => {
      wx.showLoading({
        title: '蓝牙已开启,扫描设备',
      });
      getBlueToothDevices();
    },
    fail: (err) => {
      let {
        errno, errCode
      } = err;

      if (errno === 103) {
        wx.showModal({
          title: '提示',
          content: '用户未授权使用蓝牙申请,请点击右上角三个点-设置-蓝牙,设置为允许',
        })
      } else if(errCode === 10001) {
        // 用户蓝牙开关未开启或者手机不支持蓝牙功能
        // 此时小程序蓝牙模块已经初始化完成
        wx.showModal({
          title: '提示',
          content: '请确认手机支持蓝牙,并将蓝牙打开',
          complete: () => {
            wx.onBluetoothAdapterStateChange(function (res) {
              let {
                available
              } = res;
              if(available) {
                // 蓝牙适配器可用
                wx.showLoading({
                  title: '蓝牙已开启,扫描设备',
                });
                // 开始搜寻附近的蓝牙外围设备
                getBlueToothDevices();
              }
            })
          }
        })
        
      } else {
        wx.showToast({
          title: '请打开手机蓝牙并开启微信定位授权',
          duration: 3000,
          icon: 'none'
        });
      }
    }
  });
}

(2)搜索蓝牙设备

js 复制代码
/**
 * 开始搜寻附近的蓝牙外围设备。
 */
function getBlueToothDevices() {
  wx.startBluetoothDevicesDiscovery({
    // 上报设备的间隔,单位 ms。0 表示找到新设备立即上报,其他数值根据传入的间隔上报。
    interval: 1000,
    success: () => {
      /**
       * 监听搜索到新设备的事件
       */
      wx.onBluetoothDeviceFound((res) => {
        // 扫描到设备停止扫描
        wx.stopBluetoothDevicesDiscovery();

        let devices = res.devices;
        let deviceId = "";
        devices.forEach(item => {
          // 根据自己的设备标识来判断是否搜索到了
          // 这里采用的标识是 "Jucsan"
          if (item.name.indexOf("Jucsan") > -1) {
            deviceId = item.deviceId;
          }
        });

        if (deviceId != "") {
          // 连接蓝牙设备
          connectDevice(deviceId);
        } else {
          wx.showToast({
            title: '未找到设备',
            icon: 'error',
            duration: 2000
          });
        }
      });
    },
    fail: () => {
      wx.hideLoading();
      wx.showToast({
        title: '搜寻蓝牙设备失败',
        duration: 4000,
      })
    }
  })
}

(3)连接蓝牙设备

js 复制代码
/**
 * 连接蓝牙设备
 */
function connectDevice(deviceId) {
  wx.showLoading({
    title: '设备连接中...'
  })
  /**
   * 连接 蓝牙低功耗中心设备
   */
  wx.createBLEConnection({
    deviceId,
    success: () => {
      // 获得设备服务
      getDeviceService(deviceId);
    },
    fail: function () {
      wx.hideLoading();
      wx.showToast({
        title: '连接设备失败',
        icon: 'error',
        duration: 4000
      });
    }
  });
}

(4)获得设备服务

说明:在这一环节,我们获取蓝牙低功耗设备所有服务 (service),然后获取到我们需要的对应 蓝牙设备服务的UUID

js 复制代码
function getDeviceService(deviceId) {
  wx.showLoading({
    title: '获取已连接设备服务...'
  })
  /**
   * 获取蓝牙低功耗设备所有服务 (service)。
   * 注意:根据自己真实的设备服务进行调试
   */
  wx.getBLEDeviceServices({
    deviceId,
    success: (res) => {
      let services = res.services;
      if (services.length > 2) {
        let serviceId = services[2].uuid;
        // 获取设备服务特征值
        getDeviceServiceCharacteristic(deviceId, serviceId);
      }
    }
  });
}

(5)获取设备服务特征值

js 复制代码
function getDeviceServiceCharacteristic(deviceId, serviceId) {
  wx.getBLEDeviceCharacteristics({
    deviceId,
    serviceId,
    success: (res) => {
      let characteristics = res.characteristics;
      characteristics.forEach((character) => {
        if (character.properties.write) {
          writeCharacterId = character.uuid;
        }
      });

      if (writeCharacterId != "") {
        //  找到写入特征值 
        printDeviceId = deviceId;
        printServiceId = serviceId;
        writeCharacterId = writeCharacterId;
        startPrint();
      }
    }
  });
}

2、进行数据打印

注意事项:

  1. 目前采用的是 gbk 编码打印中文,使用的蓝牙打印机默认为 gbk编码。
js 复制代码
/**
 * 进行打印操作
 */
function startPrint() {
  wx.showLoading({
    title: '打印数据中...'
  })

  writeCharacteristicValue("\r\n");
  writeCharacteristicValue(" JUCSAN智能物联网终端数据报表\r\n");
  writeCharacteristicValue(" 设备ID:123456 \r\n");
  writeCharacteristicValue("\r\n");
  writeCharacteristicValue("\r\n", true);

  closeBlueToothPrint();
}

(1)写入特征值

注意事项:

  1. 小程序不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙 4.0 单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过 20 字节
js 复制代码
/**
 * 写入特征值
 */
function writeCharacteristicValue(printValue, isCloseBlueTooth = false) {
  let printValueTarget = gbkToArray(printValue);
  let printUnitLength = 20; 
  let printStartIndex = 0;
  let printEndIndex = 0;

  while (printStartIndex < printValueTarget.byteLength) {
    // 结束索引
    printEndIndex = printStartIndex + printUnitLength;

    if (printEndIndex > printValueTarget.byteLength) {
      printEndIndex = printValueTarget.byteLength;
    }
    let printValueUnit = printValueTarget.slice(printStartIndex, printEndIndex);
    writeUnit(printValueUnit, isCloseBlueTooth);

    // 开始索引
    printStartIndex = printEndIndex;
  }
}

转换代码

js 复制代码
function gbkToArray(content) {
  /**
   * gb2312 是中文映射表 可显示 2000多个汉字
   * TextEncoder 文本编码器: 文本 =》 二进制字节流
   * TextDecoder 文本译码器: 字节流 =》 文本
   */
  var _encoder = new TextEncoder("gb2312", {
    NONSTANDARD_allowLegacyEncoding: true
  });
  // content 需要打印的字符串
  const val = _encoder.encode(content);
  return val.buffer;
}

写入特征值

注意事项:

  1. 在实际测试中要注意当前手机的蓝牙是否与打印机蓝牙兼容:
    例如: 在使用 iPhone 14, IOS 17.1.2 测试中,无法搜索到 蓝牙4.2版本的打印机,但可以正常连接到打印机。我们需要注意的是安卓机与iPhone中关闭蓝牙逻辑触发的时机不同,要对该情况进行兼容。
  2. 在实际项目中,我们为了防止打印大量数据,发送打印指令过快而导致设备打印数据冲突,打印格式错误,可以打印50条然后延时1秒继续打印。
js 复制代码
function writeUnit(value, isCloseBlueTooth) {
  wx.writeBLECharacteristicValue({
    deviceId: printDeviceId,
    serviceId: printServiceId,
    characteristicId: writeCharacterId,
    value: value,
    // 蓝牙特征值的写模式设置,有两种模式,iOS 优先 write,安卓优先 writeNoResponse 。(基础库 2.22.0 开始支持)
    writeType: isIos ? 'write' : 'writeNoResponse',
    success: function () {
      if (isCloseBlueTooth) {
        setTimeout(() => {
          closeBlueToothPrint();
        }, 500);
      }
    },
    fail: function (res) {
      wx.hideLoading();
      let {
        errCode
      } = res;

      if (errCode === 10005) {
        wx.showModal({
          title: '提示',
          content: '没有找到指定特征',
        })
      } else if (errCode === 10012) {
        wx.showModal({
          title: '提示',
          content: '连接超时',
        })
      } else {
        wx.showModal({
          title: '提示',
          content: '写入失败',
        })
      }
    },
    complete: (res) => {
      console.log('写入二进制数据 complete - res', res);
    }
  });
}

3、关闭蓝牙

js 复制代码
  /**
   * 打印完毕,关闭蓝牙
   * @param {*} content 
   */
    function closeBlueToothPrint() {
      wx.hideLoading();
      wx.showModal({
        title: '数据打印完毕!',
      })
      // 关闭蓝牙
      wx.closeBluetoothAdapter({
        success(res) {
          console.log('关闭蓝牙', res)
        }
      })
    }

总结

到这里,我们就了解了微信小程序中蓝牙打印的基础流程了。对于这些涉及不同型号机型、硬件的功能,我们需要多进行真机调试,因为难免在部分机型中会出现一些错误需要进行兼容。

其实回顾起来整体流程也并不复杂,实际项目中,我们只需要通过后端接口获取打印的数据,然后将蓝牙打印的逻辑进行封装调用即可。完整的代码可以看我github仓库

积累开发过程中的点点滴滴,持续成长,让我们不断地精进提升。与君共勉!!!

相关推荐
无咎.lsy2 分钟前
vue之vuex的使用及举例
前端·javascript·vue.js
fishmemory7sec10 分钟前
Electron 主进程与渲染进程、预加载preload.js
前端·javascript·electron
fishmemory7sec12 分钟前
Electron 使⽤ electron-builder 打包应用
前端·javascript·electron
豆豆1 小时前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建
twins35202 小时前
解决Vue应用中遇到路由刷新后出现 404 错误
前端·javascript·vue.js
qiyi.sky2 小时前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
煸橙干儿~~2 小时前
分析JS Crash(进程崩溃)
java·前端·javascript
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_027】3.4 让 D3 数据适应屏幕(下)—— D3 分段比例尺的用法
前端·javascript·信息可视化·数据可视化·d3.js·d3比例尺·分段比例尺
l1x1n03 小时前
No.3 笔记 | Web安全基础:Web1.0 - 3.0 发展史
前端·http·html
昨天;明天。今天。3 小时前
案例-任务清单
前端·javascript·css