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

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

一、开发环境

  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仓库

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

相关推荐
挣扎与觉醒中的技术人9 分钟前
【技术干货】三大常见网络攻击类型详解:DDoS/XSS/中间人攻击,原理、危害及防御方案
前端·网络·ddos·xss
zeijiershuai13 分钟前
Vue框架
前端·javascript·vue.js
写完这行代码打球去15 分钟前
没有与此调用匹配的重载
前端·javascript·vue.js
华科云商xiao徐16 分钟前
使用CPR库编写的爬虫程序
前端
狂炫一碗大米饭18 分钟前
Event Loop事件循环机制,那是什么事件?又是怎么循环呢?
前端·javascript·面试
IT、木易20 分钟前
大白话Vue Router 中路由守卫(全局守卫、路由独享守卫、组件内守卫)的种类及应用场景
前端·javascript·vue.js
顾林海20 分钟前
JavaScript 变量与常量全面解析
前端·javascript
程序员小续20 分钟前
React 组件库:跨版本兼容的解决方案!
前端·react.js·面试
乐坏小陈22 分钟前
2025 年你希望用到的现代 JavaScript 模式 【转载】
前端·javascript
生在地上要上天22 分钟前
从600行"状态地狱"到可维护策略模式:一次列表操作限制重构实践
前端