低功耗蓝牙工具APP开发实战

《低功耗蓝牙工具APP开发实战》

什么是 LightBLE?

​ 一个功能比较全面的蓝牙调试工具。支持所有使用蓝牙4.0低功耗的设备接入调试,提供蓝牙设备搜索、读取服务、浏览特征等操作。

​ 当前支持iPhone安卓微信小程序,后续将陆续支持Mac、Windows、Linux、网页端Chrome及其他可能使用的系统。

快速体验

SmartBLE 二维码

本文亮点

  • BLE入门知识,图表形式简洁易懂
  • 大量实战代码,由浅入深讲解
  • 代码开源分享,涵盖陆续扩展版本
  • 资源永久分享,导图、设计图都有

你能收获什么?

  • 快速掌握BLE基础知识
  • 学会uni开发并上架市场
  • 获得 LightBLE 思维导图、原型图、设计原稿
  • 获得一套完整的、可运行的LightBLE代码

适合人群

  • 想快速掌握BLE的小伙伴
  • 需要开发BLE的程序员
  • 想要获得快速BLE调试框架的爱好者

工具与语言

  • 需求规整:XMind
  • 原型设计:Mockplus
  • UI设计:Sketch
  • 硬件:智能手机(安卓4.3以上 或 iOS6.0以上)
  • 开发工具:hbuilderx
  • 开发框架:Uni-App
  • 协议:蓝牙4.0
  • 开发语言:Vue 、HTML、CSS3

本文结构

第一章:初识BLE

​ 尽可能用简短的语句、图片及表格来阐述BLE的入门知识。

第二章:Uni-app快速入门

​ 对标官网,通过另外一种方式快速掌握基本使用方法。

第三章:需求分析

​ 用思维导图讲解需求迭代,用原型和UI图展示效果。

第四章:项目实战

​ 统一基础工程讲解BLE开发,减少不必要的学习成本。

第一章 初识BLE

BLE简介

​ BLE全称是BlueTooth Low Energy,即低功耗蓝牙,目前主要广泛应用于IoT产品领域。

​ 低功耗蓝牙与经典蓝牙使用相同的2.4GHz无线电频率,因此双模设备可以共享同一个天线。但值得注意的是,低功耗蓝牙不能向后兼容原有的蓝牙协议(也就是经典蓝牙)。

​ 本文开发使用的是蓝牙4.0,包括传统蓝牙模块部分和低功耗蓝牙模块部分,是一个双模标准,BLE是蓝牙4.0中的单模模式(注:在2016年由蓝牙技术联盟提出蓝牙5.0技术标准)。

蓝牙模块

设备状态[重点]

状态名 中文名 说明
tandby 待机状态 设备没有传输和发送数据,并且没有连接到任何设备
advertiser 广播状态 周期性广播状态
Scanner 扫描状态 主动寻找正在广播的设备
Initiator 发起连接状态 主动向扫描设备发起连接
Master 主设备 作为主设备连接到其他设备
Slave 从设备 作为从设备连接到其他设备

工作状态[重点]

状态名 中文名
standby 准备
dvertising 广播
Scanning 监听扫描
Initiating 发起连接
Connected 已连接

状态切换图

状态切换图

设备类型

类型 中文名 说明
Cnetral 主机 常作为client端,如手机、PC
Peripheral 从机 常作为Service端,如鼠标、血压计
Observer 观察者
Broadcast 广播者

中心设备和外设交互[重点]

中心设备和外设交互

​ 从上图可以看出,手机或者MAC可以做为中心设备,鼠标和血压计作为外设。外设(有数据)发起发起广播,中心设备(类似APP向服务端索取数据)收到广播会去扫描外设和监听收到的信息。

协议栈

​ 蓝牙协议规定了两个层次的协议,分别为蓝牙核心协议(Bluetooth Core)和蓝牙应用层协议(Bluetooth Application)。蓝牙核心协议关注对蓝牙核心技术的描述和规范,它只提供基础的机制,并不关心如何使用这些机制;蓝牙应用层协议,是在蓝牙核心协议的基础上,根据具体的应用需求,百花齐放,定义出各种各样的策略,如FTP、文件传输、局域网等等。

BLE协议栈

​ 而蓝牙核心协议(Bluetooth Core)又包含BLE Controller和BLE Host两部分。这两部分在不同的蓝牙技术中(BR/EDR、AMP、LE),承担角色略有不同,但大致的功能是相同的。Controller负责定义RF、Baseband等偏硬件的规范,并在这之上抽象出用于通信的逻辑链路(Logical Link);Host负责在逻辑链路的基础上,进行更为友好的封装,这样就可以屏蔽掉蓝牙技术的细节,让Bluetooth Application更为方便的使用。

名称 英文 说明
物理层 Physical Layer PHY层用来指定BLE所用的无线频段,调制解调方式和方法等。
链路层 Link Layer LL层是整个BLE协议栈的核心。
主机控制接口层 Host Controller Interface HCI主要用于2颗芯片实现BLE协议栈的场合,用来规范两者之间的通信协议和通信命令等。是可选的。
通用访问配置文件层 Generic access profile GAP是对LL层payload(有效数据包)如何进行解析的两种方式中的一种,而且是最简单的那一种。目前主要用来进行广播,扫描和发起连接等。
逻辑链路控制及自适应协议层 Logical Link Control and Adaptation Protocol L2CAP对LL进行了一次简单封装,LL只关心传输的数据本身,L2CAP就要区分是加密通道还是普通通道,同时还要对连接间隔进行管理。
安全管理层 Security Manager SMP用来管理BLE连接的加密和安全的,如何保证连接的安全性,同时不影响用户的体验,这些都是SMP要考虑的工作。
属性协议层 Attribute protocol 简单来说,ATT层用来定义用户命令及命令操作的数据,比如读取某个数据或者写某个数据。
通用属性配置文件层 Generic Attribute profile GATT用来规范attribute中的数据内容,并运用group(分组)的概念对attribute进行分类管理。

服务与特征[重点]

服务与特征

一个外设可以包含一个或多个服务(Service),服务是用于实现装置的功能或特征数据相关联的行为集合。而每个服务又对应多个特征(CBCharacteristic),特征提供外设服务进一步的细节。

数据包

BLE 发送数据时**最多允许20个字节**,但仍可以通过分包方式,使发送内容长度的扩充。

第二章 Uni-app快速入门

简介

uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台。

​ 需要更详细的介绍,请进入到uni-app官网查看,官网地址:https://uniapp.dcloud.io/

​ 本章节是官网的个人提炼,便于快速进入后期项目开发做准备。

快速入门

创建工程

​ 下载开发工具:HBuilderX ,选择App开发版,下载地址:https://www.dcloud.io/hbuilderx.html

​ 在点击工具栏里的文件 -> 新建 -> 项目:

新建项目

​ 选择 uni-app ,填入项目名称[此处取名demo],模板选择 默认模板 后,点击创建。
创建hello App

项目运行

​ 使用HBuilderX打开demo项目,双击 App.vue 后,点击工具栏的运行 -> 内置浏览器运行,即可在浏览器里面体验uni-app 的 H5 版【首次运行会提示需要安装,安装后再次点击即可】。
运行到内置浏览器

​ 了解其他运行效果,可以官网查看,也可以自行操作,此处就不再累赘。

目录结构说明

​ 如果您看到的目录与下面不一致,试着创建新项目,模板选择Hello uni-app

┌─ components            uni-app组件目录
│  └─ comp-a.vue         可复用的a组件
├─ hybrid                存放本地网页的目录
├─ platforms             存放各平台专用页面的目录
├─ pages                 业务页面文件存放的目录
│  ├─ index
│  │  └─ index.vue       index页面
│  └─ list
│     └─ list.vue        list页面
├─ static                存放应用引用静态资源(如图片、视频等)的目录,注意:静态资源只能存放于此
├─ wxcomponents          存放小程序组件的目录
├─ main.js               Vue初始化入口文件
├─ App.vue               应用配置,用来配置App全局样式以及监听 应用生命周期
├─ manifest.json         配置应用名称、appid、logo、版本等打包信息
└─ pages.json            配置页面路由、导航条、选项卡等页面类信息

项目配置

​ 使用HBuilderX打开demo项目,双击 manifest.json

​ 配置 AppID 和 Vue 版本:
基础配置

​ 添加 BLE 模块支持:
开启BLE模块

​ 添加BLE权限:
添加自动权限配置

​ 如不配置自动添加权限,则找下以下几个选项,勾选:

"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />",  
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />",  
"<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\" />",  
"<uses-permission android:name=\"android.permission.BLUETOOTH\" />"

其他配置

  1. 使用HBuilderX打开demo项目,双击 manifest.json

​ 选择 微信小程序配置 添加微信小程序AppID 。

​ 选择 App图标配置 添加图标,使用自动生成即可。

  1. 选择HBuilderX 配好设置,选择 运行配置

​ 选择 微信开发者工具路径,添加对应路径。

添加微信开发工具路径

  1. 配置证书(需要发布在进行操作)

​ 使用HBuilderX打开demo项目,双击 App.vue 后,点击工具栏的发行 -> 原生APP-云打包。

image-20220224151934867

​ 会弹出App打包需要配置的内容,区分 Android 和 iOS ,根据内容自行配置即可。

image-20220224152116648

  1. 账户申请(安卓市场只列举部分)

    平台 地址 说明
    微信小程序 https://mp.weixin.qq.com LightBLE 开发及发布,公司或个人均可
    iOS开发者 https://developer.apple.com 需要 99美元/年 的开发者账户,公司或个人均可
    小米 https://dev.mi.com Android端发布使用,发布需要软著
    oppo https://open.oppomobile.com Android端发布使用,发布需要软著
    华为 https://developer.huawei.com Android端发布使用,发布需要软著
    应用宝 https://open.qq.com/app_plus Android端发布使用,发布需要软著

第三章 需求分析

需求分解

​ LightBLE定位为一个轻量级BLE调试助手,具体功能通过思维导图示如下:
思维分解

​ 经过上图规整,再对比uni-app提供的[BLE接口](https://uniapp.dcloud.io/api/system/ble),能一套代码完成以上功能。这也是本文选择使用 uni-app进行开发并讲解的原因。

​ 低功耗蓝牙 API 平台差异说明(刚好满足App和微信小程序)

App H5 微信小程序 支付宝小程序 百度小程序 字节跳动小程序 飞书小程序 QQ小程序 快手小程序
× × × × ×

蓝牙API(接口地址:https://uniapp.dcloud.io/api/system/bluetooth

API 说明
uni.openBluetoothAdapter(OBJECT) 初始化蓝牙模块
uni.startBluetoothDevicesDiscovery(OBJECT) 开始搜寻附近的蓝牙外围设备。
此操作比较耗费系统资源,请在搜索并连接到设备后调用 uni.stopBluetoothDevicesDiscovery 方法停止搜索
uni.onBluetoothDeviceFound(CALLBACK) 监听寻找到新设备的事件
uni.stopBluetoothDevicesDiscovery(OBJECT) 停止搜寻附近的蓝牙外围设备。
若已经找到需要的蓝牙设备并不需要继续搜索时,建议调用该接口停止蓝牙搜索。
uni.onBluetoothAdapterStateChange(CALLBACK) 监听蓝牙适配器状态变化事件。
uni.getConnectedBluetoothDevices(OBJECT) 根据 uuid 获取处于已连接状态的设备。
uni.getBluetoothDevices(OBJECT) 获取在蓝牙模块生效期间所有已发现的蓝牙设备。
包括已经和本机处于连接状态的设备。
uni.getBluetoothAdapterState(OBJECT) 获取本机蓝牙适配器状态。
uni.closeBluetoothAdapter(OBJECT) 关闭蓝牙模块。
调用该方法将断开所有已建立的连接并释放系统资源。建议在使用蓝牙流程后,与 uni.openBluetoothAdapter 成对调用。

低功耗蓝牙 API (接口地址:https://uniapp.dcloud.io/api/system/ble

API 说明
uni.setBLEMTU(OBJECT) 设置蓝牙最大传输单元。
需在 uni.createBLEConnection调用成功后调用,mtu 设置范围 (22,512)。安卓5.1以上有效。
uni.writeBLECharacteristicValue(OBJECT) 向低功耗蓝牙设备特征值中写入二进制数据。
注意:必须设备的特征值支持 write 才可以成功调用。
uni.readBLECharacteristicValue(OBJECT) 读取低功耗蓝牙设备的特征值的二进制数据值。
注意:必须设备的特征值支持 read 才可以成功调用。
uni.onBLEConnectionStateChange(CALLBACK) 监听低功耗蓝牙连接状态的改变事件。
包括开发者主动连接或断开连接,设备丢失,连接异常断开等等。
uni.onBLECharacteristicValueChange(CALLBACK) 监听低功耗蓝牙设备的特征值变化事件。
必须先启用 notifyBLECharacteristicValueChange 接口才能接收到设备推送的 notification。
uni.notifyBLECharacteristicValueChange(OBJECT) 启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值。
注意:必须设备的特征值支持 notify 或者 indicate 才可以成功调用。 另外,必须先启用 notifyBLECharacteristicValueChange才能监听到设备 characteristicValueChange 事件
uni.getBLEDeviceServices(OBJECT) 获取蓝牙设备所有服务(service)。
uni.getBLEDeviceRSSI(OBJECT) 获取蓝牙设备的信号强度。
uni.getBLEDeviceCharacteristics(OBJECT) 获取蓝牙设备某个服务中所有特征值(characteristic)。
uni.createBLEConnection(OBJECT) 连接低功耗蓝牙设备。
若APP在之前已有搜索过某个蓝牙设备,并成功建立连接,可直接传入之前搜索获取的 deviceId 直接尝试连接该设备,无需进行搜索操作。
uni.closeBLEConnection(OBJECT) 断开与低功耗蓝牙设备的连接。

​ 通过上述分析,再次针对APP/小程序进行需求梳理,梳理如下:

LightBLE

原型设计

​ 通过上诉需求梳理后,进行原型图设计和UI图设计。

原型图

LightBLE

UI图

  • 设计工具:Sketch
  • 在线设计图:蓝湖 (建议使用,会在项目开发中提及)
  • 说明:Sketch 当前只能使用Mac进行设计,所以无法打开Sketch的读者,请双击资源[在下节前置准备提及]中的 LightBLE-HTML/index.html 跳转到浏览器,便可以看到 LightBLE 的UI设计图。
  • UI设计图展示:

UI设计图

前置准备

第四章 项目实战

基础框架

​ 从资源目录中打开 LightBLE 项目,可以看到以下结构

┌─ common                    通用配置
│  ├─ animate.css        动画[当前版本还未使用]
│  ├─ common.css         业务全局css
│  ├─ config.js          业务全局配置
│  ├─ free.css           通用css
│  ├─ iconfont.css       图标
│  ├─ mock.js                  模拟数据
│  └─ tool.js              工具函数集合
┌─ components            uni-app组件目录
│  ├─ divider.vue        分割线
│  ├─ logItem.vue        日志列表item
│  ├─ scannerItem.vue    扫描列表item
│  ├─ sk-switch.vue      自定义Switch
│  └─ spread.vue         水波纹动效[搜索/广播中无数据时展示]
├─ pages                 业务页面文件存放的目录
│  ├─ advertiser                 广播相关页面
│  │  └─ ...       
│  ├─ scanner                        扫描相关页面
│  │  └─ ... 
│  ├─ setting                        设置相关页面
│  │  └─ ... 
│  └─ tabbar                       tabbar相关页面
│     └─ ...                 
├─ static                存放应用引用静态资源(如图片、视频等)的目录,注意:静态资源只能存放于此
│  ├─ imgs                           图片
│  │  └─ ...       
│  ├─ tabbar                         tabbar专属图片
│  │  └─ ... 
│  ├─ iconfont.ttf           iconfont资源文件 
│  └─ logo.png                   logo
├─ main.js               Vue初始化入口文件
├─ App.vue               应用配置,用来配置App全局样式以及监听 应用生命周期
├─ manifest.json         配置应用名称、appid、logo、版本等打包信息
└─ pages.json            配置页面路由、导航条、选项卡等页面类信息

工程内容说明

文件名 说明
config.js 填写 常量 和 枚举
free.css 通用css ,即使没有UI设计情况下,仍能做出好产品
common.css 业务全局css,通常包括 全局背景色、业务色、字体、圆角、边距及自定义全局使用css等
mock.js 此处只是模拟假数据,没有真正使用 mock
tool.js 工具函数集合。例如项目中使用到的 秒转化格式、ArrayBuffer转换、hex和ascii转换及uuid 获取等方法
main.js 添加 全局组件、toos.js、config.js 和 mock.js ,方便全局使用
App.vue 添加 全局css

组件使用

基本介绍

​ 组件有两种方式引用:全局组件 和 局部组件。

# 全局组件 通过 main.js 进行配置
# 本项目引入 分割线 divider.vue 
// 全局注册
import divider from './components/divider.vue'
// 全局注册
Vue.component('divider', divider)
# 局部组件 通过 vue 进行设置
# 下面用 scannerItem.vue 做说明

...
<script>
// 组件注册:此处 将 scannerItem 注册为 item,并在 components 引入
import item from '@/components/scannerItem.vue'
...
    export default {
        components: {
            item, // 引入组件
            ...
        },
        data() {
            ...
        },
        ...
    }

// 组件使用
<template>
    <view>
        ...
        // 全局组件:分割线
        <divider></divider> 

        // item: 组件scannerItem 
        // :itemObj="obj" 组件属性赋值
        // @click.native="itemAction(idx)" 组件函数引用
        <item :itemObj="obj" @click.native="itemAction(idx)"></item> 
        ...
    </view>
</template>

<script>
...
</script>
属性赋值

​ 组件的使用中包含 属性赋值 怎么通过组件对外开放。

// 使用 scannerItem.vue 做介绍
<template>
    <view class="item flex flex-column p-1">
        ...
    </view>
</template>

<script>
    export default {
        name: "scannerItem",  // 组件名称
        data() {
            return {
                // 此次属性,只能组件内使用
                ...
            }
        },
        computed: {
            ...
        },
        // 通过 props 使属性允许外部赋值
        props: {
                itemObj:                // 属性名称
                {
          type: Object, // 属性类型
          value:                // 属性值
          {
            name: '设备名',
            deviceId: 1234567890,
            RSSI: -1,
            advertisData: [],
            advertisServiceUUIDs: [],
            localName: '',
            serviceData: {}
          }
                }
        }
        ...

​ 代码可以看出,通过 props 编写就能实现父组件给子组件属性赋值。实际上还可以通过使用 $emit 将属性传递给父组件,实现属性的双向绑定功能。

父子组件通信

​ 尽量项目中只使用了props 使用父子组件的通信,但考虑到未来扩展,我们将通过四段简单代码来演示父子组件基础引用通过prop实现通信通过$ref 实现通信通过$emit 实现通信

# 基础引用

// 父组件
<template>
 <div>
    <h1>我是父组件。</h1>
    <child></child>
 </div>
</template>

<script>
    import Child from '../components/child.vue'
    export default {
        components: {
            Child
        }
    }
</script>

// 子组件
<template>
 <h3>我是子组件。</h3>
</template>

<script>
    ...
</script>
# 通过prop实现通信

// 父组件
<template>
 <div>
   <h1>我是父组件。</h1>
   <!-- 静态赋值。-->
   <child message="我是静态子组件。"></child>
   <!-- 动态赋值。-->
   <child v-bind:message="msg"></child>
 </div>
</template>

<script>
import Child from '../components/child.vue'
export default {
 components: {Child},
 data() {
   return {
    msg: '我是动态子组件。'
   }
 }
}
</script>

// 子组件
<template>
 <h3>{{message}}</h3>
</template>
<script>
 export default {
    props: ['message']    // 不写也支持
 }
</script>
# 通过$ref 实现通信

// 父组件
<template>
 <div>
 <h1>我是父组件。</h1>
 <child ref="msg"></child>
 </div>
</template>

<script>
 import Child from '../components/child.vue'
 export default {
    components: {Child},
    mounted: function () {
    this.$refs.msg.getMessage('我是子组件。')
    }
 }
</script>

// 子组件
<template>
 <h3>{{message}}</h3>
</template>
<script>
 export default {
 data(){
  return{
    message:''
  }
 },
 methods:{
    getMessage(m){
      this.message = m
    }
   }
 }
</script>
# 通过$emit 实现通信

// 父组件
<template>
 <div>
 <h1>{{title}}</h1>
 <!-- 子组件通过 $emit 抛出 getMessage ,父组件绑定到 showMsg 。-->
 <child @getMessage="showMsg"></child> 
 </div>
</template>

<script>
 import Child from '../components/child.vue'
 export default {
 components: {Child},
 data(){
  return{
    title:''
  }
 },
 methods:{
  showMsg(title){
      this.title = title
    }
    }
 }
</script>

// 子组件
<template>
 <h3>我是子组件。</h3>
</template>
<script>
 export default {
   mounted: function () {
    this.$emit('getMessage', '我是父组件。') // 父组件通过 getMessage 进行方法绑定
   }
 }
</script>

注意事项

  • 本项目重点在于蓝牙的基本开发操作,所以在基础工程中已配置好不同平台的兼容处理。

  • 由于 uni-app 暂时没有作为 外设设备 的接口,所以当前只有程序小程序版本支付外设模式,并不支持自定义。

  • 项目中包含两种图片引入方式:本地图片 和 iconfont 。

  • 考虑总体时间,动画先引入,后期会同步提交到 GitHubGitee

Cnetral

初始化蓝牙

​ 主要目的是为了检测蓝牙是否打开。

// 方便调用,定义方法 bleOpenBluetoothAdapter(){}
<script>
...

bleOpenBluetoothAdapter: function() {
    let that = this
    uni.openBluetoothAdapter({
        //mode: 'cnetral',// 模式为 cnetral ,此处可不填写
        success(res) {
            // 蓝牙正常打开,开始搜索蓝牙设备
            that.bleStartBluetoothDevicesDiscovery()
        },
        fail(res) {
            // 已经初始化过的情况,需要从 fail 单独处理为 success
            if (res.errMsg == 'openBluetoothAdapter:fail already opened') {
        // 蓝牙正常打开,开始搜索蓝牙设备
                that.bleStartBluetoothDevicesDiscovery()
      } else {
        // 错误情况,弹出提示
        uni.showToast({
          icon: 'none',
          title: res.errMsg
        })
      }
        },
        complete(res) {
            // 不论成功与否,暂停下拉刷新效果
            uni.stopPullDownRefresh()
        }
    })
}

...
</script>
搜索蓝牙设备

​ 搜索蓝牙设备需要两步:

    1. startBluetoothDevicesDiscovery 调用成功;
    1. onBluetoothDeviceFound  监听寻找到新设备。
// 方便调用,定义方法 bleOpenBluetoothAdapter(){}
<script>
...

// 开始搜寻附近的蓝牙外围设备
bleStartBluetoothDevicesDiscovery: function() {
    let that = this
    uni.startBluetoothDevicesDiscovery({
    // services: ['FEE7'],  增加条件
    // interval: 0,
    allowDuplicatesKey: false,//是否允许重复上报同一设备。
    success(res) {
      // 开启搜索成功后,监听寻找到新设备的事件
      that.bleOnBluetoothDeviceFound()
    },
    fail(res) {
      // 如果已经开启搜索未关闭,同样 监听寻找到新设备的事件
      if (res.errMsg == 'startBluetoothDevicesDiscovery:fail already discovering devices') {
        that.bleOnBluetoothDeviceFound()
      } else {
        // 错误提示
        uni.showToast({
          icon: 'none',
          title: res.errMsg
        })
      }
    }
  })
},

// 监听寻找到新设备的事件
bleOnBluetoothDeviceFound: function() {
    let that = this
    uni.onBluetoothDeviceFound(function(obj) {
        let list = obj.devices
        for (let i = 0; i < list.length; i++) {
      // 添加(过滤重复数据)
            that.belDeviceAdd(list[i])
        }
    // 列表数据整理(条件筛选)
        that.dataRegularization()
        })
},
...
</script>

​ 综合考虑,将开始搜寻附近的蓝牙外围设备的搜索条件,放到数据整理函数dataRegularization中,从而避免多次操作 startBluetoothDevicesDiscovery

条件筛选

​ 通过上面代码知道,搜索到设备信息均为未赛选过滤数据。所以我们增加了两个方法来优化数据。

<script>
...
// 设备加入,过滤已添加设备
belDeviceAdd: function(dev) {
  // 遍历确认是否存在设备
    let selectIdx = -1
    for (let i = 0; i < this.list.length; i++) {
        let item = this.list[i]
        if (item.deviceId == dev.deviceId) {
            selectIdx = i
            break
        }
    }

    if (selectIdx == -1) {
    // 不存在则追加
        this.list.push(dev)
    } else {
    // 存在则替换
        this.list[selectIdx] = dev
    }
},

 // 数据整理 
dataRegularization: function() {
    let list = []
    for (let i = 0; i < this.list.length; i++) {
        let itemObj = this.list[i]
        // 考虑可能不存在名称处理
        let name = itemObj.name ? itemObj.name : itemObj.localName
        let add = true
        // 空名过滤
        if (this.FilterEmpty) {
            if (!name) add = false
        }
        // 过滤器 - RSSI
        if (!(itemObj.RSSI > this.FilterRSSI)) add = false
        // 过滤器 - 名称
        if (this.FilterName.length > 0 && (!name || name.indexOf(this.FilterName)) < 0) add = false
        // 过滤器 - UUID   
        if (itemObj.deviceId.indexOf(this.FilterUUID) < 0) add = false

    // 满足条件,添加仅 list
        if (add) list.push(itemObj)
    }
    this.showList = list
},
...
</script>
连接设备

​ 通过上节获取的设备信息,选择一个设备并传递 deviceId 连接。

<script>
...
createBLEConnection: function(devId) {
  let that = this
  uni.createBLEConnection({
    deviceId: devId,
    success(res) {
      // 配置连接成功后,是否自动断开搜索
      if (that.ConnectAutoStop) {
        uni.stopBluetoothDevicesDiscovery()
      }
      // 连接成功后,获取该设备服务列表
      uni.getBLEDeviceServices({
        deviceId: devId,
        success(res) {
          let services = []
          for (let i = 0; i < res.services.length; i++) {
            services.push(res.services[i].uuid)
          }
          // 过滤重复项
          services = [...new Set(services)]
          // 通过服务,发现特征值
          for (let i = 0; i < services.length; i++) {
            setTimeout(function() {
              uni.getBLEDeviceCharacteristics({
                deviceId: devId,
                serviceId: services[i],
                complete(res) {
                  that.addService(services[i], res)
                 }
              })
            }, (i + 1) * 300) // 此步骤很重要,通过每个延迟发送请求来避免同时发送请求出现的bug
          }
        }
      })
    }
  })
}
...
</script>

​ 连接上设备后,就可以进行设备的读、写、通知等操作。代码中的注意字段请仔细阅读

<script>
...
// 读取低功耗蓝牙设备的特征值的二进制数据值。
// 注意:必须设备的特征值支持 read 才可以成功调用。
bleReadBLECharacteristicValue:function(deviceId,serviceId,characteristicId) {
  let that = this
  uni.readBLECharacteristicValue({
    deviceId: deviceId,
    serviceId: serviceId,
    characteristicId: characteristicId,
    success(res) {
      // 监听低功耗蓝牙设备的特征值变化事件
      uni.onBLECharacteristicValueChange(function(res1){
        // 通过 tool.js 的方法转化数据
        let readText = that.$Tool.ab2hex(res1.value)
        that.readText = that.readText + "\n" + readText
      })
    }
  })
},
// 向低功耗蓝牙设备特征值中写入二进制数据。
// 注意:必须设备的特征值支持 write 才可以成功调用。
bleWriteBLECharacteristicValue:function(deviceId,serviceId,characteristicId) {
  let that = this

  // 通过 tool.js 方法将字符串转ArrayBuffer
  let text = this.formatValue == 0 ? this.$Tool.hex_to_ascii(this.writeText) : this.writeText
    let buffer = that.$Tool.str2ab(text)

  uni.readBLECharacteristicValue({
    deviceId: deviceId,
    serviceId: serviceId,
    characteristicId: characteristicId,
    value: buffer,
    success(res) {
      uni.showToast({
        icon: 'none',
        title: '写入成功'
      })
    },
    fail(){
      uni.showToast({
        icon: 'none',
        title: '写入失败'
      })
    }
  })
}
// 启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值。
// 注意:必须设备的特征值支持 notify 或者 indicate 才可以成功调用。 
// 另外,必须先启用 notifyBLECharacteristicValueChange 才能监听到设备 characteristicValueChange 事件 
belNotifyBLECharacteristicValueChange:function(deviceId,serviceId,characteristicId,state) {
  let that = this
  uni.notifyBLECharacteristicValueChange({
    deviceId: deviceId,
    serviceId: serviceId,
    characteristicId: characteristicId,
    state: state,//是否启用 notify
    success(res) {
      // 监听低功耗蓝牙设备的特征值变化事件
      uni.onBLECharacteristicValueChange(function(res1){
        // 通过 tool.js 的方法转化数据
        let notifyText = that.$Tool.ab2hex(res1.value)
        that.readText = that.notifyText + "\n" + notifyText
      })
    }
  })
}
...
</script>

Peripheral

​ 查看 uni-app 蓝牙相关文档,并没有作为外设的API。转而查看微信小程序蓝牙相关文档,发现是有外设API(尽管不全),再结合uni可以直接调用微信小程序API,所以下面代码使用微信小程序代码来展示。(APP需要自己做组件或使用原生,后期处理后会更新到代码库中。)

<script>
...
// 需要开启蓝牙并设置mode为peripheral
bleOpenBluetoothAdapter:function(deviceName) {
  let that = this
  // #ifdef MP-WEIXIN
  wx.openBluetoothAdapter({
    mode: 'peripheral',
    success(res) {
      // 开启外设
      that.bleCreateBLEPeripheralServer(deviceName)
    },
    fail(res) {
      if (res.errMsg == 'openBluetoothAdapter:fail already opened') {
         // 开启外设
        that.bleCreateBLEPeripheralServer(deviceName)
      } else {
        uni.showToast({
          icon: 'none',
          title: res.errMsg
        })
    }
  })
  // #endif
}
bleCreateBLEPeripheralServer:function(deviceName) {
    let that = this
    // #ifdef MP-WEIXIN
    wx.createBLEPeripheralServer({
      success: (result) => {
        let server = result.server
        server.startAdvertising({
          advertiseRequest: {
            connected: true,
            deviceName: deviceName,
          }
        }).then(
          (res) => {
            console.log('advertising', res)
          },
          (res) => {
            console.warn('ad fail', res)
          }
        )
      },
      fail: (res) => {
        uni.showToast({
          icon: 'none',
          title: '创建服务失败'
        })
      }
    })
    // #endif
}                      
...
</script>

通用

日志存储
<script>
...
// 存日志
saveLog: function(log) {
  let key = this.$Config.Conf.LogFileName
  uni.getStorage({
    key: key,
    complete(res) {
      let list = []
      if (res.data != "") list = res.data
      list.push(log)
      uni.setStorage({
        key: key,
        data: list
      })
    }
  })
}
// 读取日志列表
getLogs: function() {
    let key = this.$Config.Conf.LogFileName
    let list = []
    try {
        const res = uni.getStorageSync(key)
        if (res != "") list = res
        } catch (e) {
            console.log("try catch: ", e)
        }
        return list
}
...
</script>
日志格式
<script>
...
// 日志存储格式
let log = {
    time: (new Date()).getTime(),
    type: that.$Config.LogType.Connent,
    id: devId,
    msg: ''
}
// 日志类型,在 config.js 中
const LogType = {
    Connent: 1, // 已连接
    NoticeOpen: 2, //Notification开启
    CharacteristicRead: 3, //读取特征值
    MsgRead: 4, //接收信息
    NoticeRead: 5, //通知消息
    MsgWrite: 6, //写入消息
    Error: 10, //错误
}
...
</script>
全局变量

​ 本次项目选择使用数据缓存到本地,全局key对存储内容管理从而实现全局变量的方式。

<script>
...
// 举例:设置页面:连接后是否停止扫描
// 存储Key:ConnectAutoStop [参看 config.js 中 Conf ]
// 需要用到页面
onLoad(){
  ...

  let that = this
    uni.getStorage({
        key: 'ConnectAutoStop',
        success: function(res) {
      // 存在则直接赋值
            that.checked = res.data
        },
        fail(res) {
      // 不存在则读取配置文件默认值
            that.checked = that.$Config.Conf.ConnectAutoStop
      // 同时将数值存储
            that.setStorageConnectAutoStop(that.checked)
        }
    })

  ...
}

// 有些页面需要,可以从 onLoad 切换到 onShow 
...
</script>

发表评论

您的电子邮箱地址不会被公开。