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

什么是 LightBLE?

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

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

​ 快速体验

本文亮点

  • 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、文件传输、局域网等等。

​ 而蓝牙核心协议(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],模板选择 默认模板 后,点击创建。

项目运行

​ 使用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权限:

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

"<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 配好设置,选择 运行配置

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


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

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

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

  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接口,能一套代码完成以上功能。这也是本文选择使用 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/小程序进行需求梳理,梳理如下:

原型设计

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

原型图

  • 设计工具:Mockplus
  • 原型地址:https://run.mockplus.cn/dua7ZgYiBOPE5Wsw/index.html
  • 说明:原型不要太考虑美化,因为那是UI设计师、视觉设计师、动效设计师等的事情。原型只要把功能和基本页面交互规划准确就可以。
  • 交互图展示:

UI图

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

前置准备

  • LightBLE :最新版,便于理解及测试 。下载着陆页:https://i2kai.com 。
  • 安卓手机 :Android 4.3 及以上,开启手机调试模式。
  • iOS手机 :iOS 6.0 及以上,上架或分发需配置开发证书,开发不用。
  • 微信小程序 :申请账号获取APPID 或 使用测试号,测试号申请地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/sandbox.html 。
  • ESP32 :Peripheral 真实体验,可不要(替代方案:多找一部手机安装 LightBLE 来模拟Peripheral;电脑版推荐 lightblue 来模拟Peripheral ,将来有机会把桌面端也写上)。[将来会写ESP32的教程]
  • 资源(思维导图、原型图、设计稿和基础工程):https://www.aliyundrive.com/s/sEE5xycpMpG 提取码: 60dm
  • 源码:https://gitee.com/luoyaosheng/smart-ble

第四章 项目实战

基础框架

​ 从资源目录中打开 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实现通信∗∗和∗∗通过ref 实现通信** 和 **通过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 。

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

Cnetral

初始化蓝牙

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

// 方便调用,定义方法 bleOpenBluetoothAdapter(){}
<script>
...bleOpenBluetoothAdapter: function() {let that = thisuni.openBluetoothAdapter({//mode: 'cnetral',// 模式为 cnetral ,此处可不填写success(res) {// 蓝牙正常打开,开始搜索蓝牙设备that.bleStartBluetoothDevicesDiscovery()},fail(res) {// 已经初始化过的情况,需要从 fail 单独处理为 successif (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 = thisuni.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 = thisuni.onBluetoothDeviceFound(function(obj) {let list = obj.devicesfor (let i = 0; i < list.length; i++) {// 添加(过滤重复数据)that.belDeviceAdd(list[i])}// 列表数据整理(条件筛选)that.dataRegularization()})
},
...
</script>

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

条件筛选

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

<script>
...
// 设备加入,过滤已添加设备
belDeviceAdd: function(dev) {// 遍历确认是否存在设备let selectIdx = -1for (let i = 0; i < this.list.length; i++) {let item = this.list[i]if (item.deviceId == dev.deviceId) {selectIdx = ibreak}}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.localNamelet add = true// 空名过滤if (this.FilterEmpty) {if (!name) add = false}// 过滤器 - RSSIif (!(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// 满足条件,添加仅 listif (add) list.push(itemObj)}this.showList = list
},
...
</script>
连接设备

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

<script>
...
createBLEConnection: function(devId) {let that = thisuni.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 = thisuni.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 方法将字符串转ArrayBufferlet text = this.formatValue == 0 ? this.$Tool.hex_to_ascii(this.writeText) : this.writeTextlet 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 = thisuni.notifyBLECharacteristicValueChange({deviceId: deviceId,serviceId: serviceId,characteristicId: characteristicId,state: state,//是否启用 notifysuccess(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-WEIXINwx.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-WEIXINwx.createBLEPeripheralServer({success: (result) => {let server = result.serverserver.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.LogFileNameuni.getStorage({key: key,complete(res) {let list = []if (res.data != "") list = res.datalist.push(log)uni.setStorage({key: key,data: list})}})
}
// 读取日志列表
getLogs: function() {let key = this.$Config.Conf.LogFileNamelet 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 = thisuni.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>

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

什么是 LightBLE?

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

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

​ 快速体验

本文亮点

  • 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、文件传输、局域网等等。

​ 而蓝牙核心协议(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],模板选择 默认模板 后,点击创建。

项目运行

​ 使用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权限:

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

"<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-云打包。

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

  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接口,能一套代码完成以上功能。这也是本文选择使用 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/小程序进行需求梳理,梳理如下:

原型设计

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

原型图

  • 设计工具:Mockplus
  • 原型地址:https://run.mockplus.cn/dua7ZgYiBOPE5Wsw/index.html
  • 说明:原型不要太考虑美化,因为那是UI设计师、视觉设计师、动效设计师等的事情。原型只要把功能和基本页面交互规划准确就可以。
  • 交互图展示:

UI图

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

前置准备

  • LightBLE :最新版,便于理解及测试 。下载着陆页:https://i2kai.com 。
  • 安卓手机 :Android 4.3 及以上,开启手机调试模式。
  • iOS手机 :iOS 6.0 及以上,上架或分发需配置开发证书,开发不用。
  • 微信小程序 :申请账号获取APPID 或 使用测试号,测试号申请地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/sandbox.html 。
  • ESP32 :Peripheral 真实体验,可不要(替代方案:多找一部手机安装 LightBLE 来模拟Peripheral;电脑版推荐 lightblue 来模拟Peripheral ,将来有机会把桌面端也写上)。[将来会写ESP32的教程]
  • 资源(思维导图、原型图、设计稿和基础工程):https://www.aliyundrive.com/s/sEE5xycpMpG 提取码: 60dm
  • 源码:https://gitee.com/luoyaosheng/smart-ble

第四章 项目实战

基础框架

​ 从资源目录中打开 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实现通信∗∗和∗∗通过ref 实现通信** 和 **通过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 。

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

Cnetral

初始化蓝牙

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

// 方便调用,定义方法 bleOpenBluetoothAdapter(){}
<script>
...bleOpenBluetoothAdapter: function() {let that = thisuni.openBluetoothAdapter({//mode: 'cnetral',// 模式为 cnetral ,此处可不填写success(res) {// 蓝牙正常打开,开始搜索蓝牙设备that.bleStartBluetoothDevicesDiscovery()},fail(res) {// 已经初始化过的情况,需要从 fail 单独处理为 successif (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 = thisuni.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 = thisuni.onBluetoothDeviceFound(function(obj) {let list = obj.devicesfor (let i = 0; i < list.length; i++) {// 添加(过滤重复数据)that.belDeviceAdd(list[i])}// 列表数据整理(条件筛选)that.dataRegularization()})
},
...
</script>

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

条件筛选

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

<script>
...
// 设备加入,过滤已添加设备
belDeviceAdd: function(dev) {// 遍历确认是否存在设备let selectIdx = -1for (let i = 0; i < this.list.length; i++) {let item = this.list[i]if (item.deviceId == dev.deviceId) {selectIdx = ibreak}}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.localNamelet add = true// 空名过滤if (this.FilterEmpty) {if (!name) add = false}// 过滤器 - RSSIif (!(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// 满足条件,添加仅 listif (add) list.push(itemObj)}this.showList = list
},
...
</script>
连接设备

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

<script>
...
createBLEConnection: function(devId) {let that = thisuni.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 = thisuni.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 方法将字符串转ArrayBufferlet text = this.formatValue == 0 ? this.$Tool.hex_to_ascii(this.writeText) : this.writeTextlet 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 = thisuni.notifyBLECharacteristicValueChange({deviceId: deviceId,serviceId: serviceId,characteristicId: characteristicId,state: state,//是否启用 notifysuccess(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-WEIXINwx.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-WEIXINwx.createBLEPeripheralServer({success: (result) => {let server = result.serverserver.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.LogFileNameuni.getStorage({key: key,complete(res) {let list = []if (res.data != "") list = res.datalist.push(log)uni.setStorage({key: key,data: list})}})
}
// 读取日志列表
getLogs: function() {let key = this.$Config.Conf.LogFileNamelet 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 = thisuni.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>

低功耗蓝牙工具APP开发实战相关推荐

  1. Hybrid App开发实战

    Hybrid App开发实战 [引言]近年来随着移动设备类型的变多,操作系统的变多,用户需求的增加,对于每个项目启动前,大家都会考虑到的成本,团队成员,技术成熟度,时间,项目需求等一堆的因素.因此,开 ...

  2. 好书推荐:21天入门 低功耗蓝牙5.x开发

    低功耗蓝牙5 及其后续版本围绕物联网创新应用而不断更新迭代,如何高效的学习低功耗蓝牙5 相关知识,并通过实践来掌握其开发方法,是广大学子和开发人员非常感兴趣的内容.作为低功耗蓝牙技术在国内最早推广及应 ...

  3. Vue.js 3.0快速入门(附电影购票APP开发实战源码)

    前言 文档笔记来源:kuangshenstudy,清华大学出版社,结合视频资源食用更佳,相关资源源码在文末,有需要自取. 一.概述 Vue是什么? Vue.js是基于JavaScript的一套MVVC ...

  4. iPhone App开发实战手册

    <iPhone App开发实战手册> 基本信息 作者: (美)霍肯伯里(Hockenberry,C.) 译者: 高京 历勤勇 施迪宏 出版社:电子工业出版社 ISBN:9787121176 ...

  5. 最新仿映客直播APP开发实战项目IOS开发实战8天(最全最新)

    最新仿映客直播APP开发实战项目IOS开发实战8天 第 1 章:直播准备 1: [录播] 课程大纲介绍 09:56 2: [录播] 了解直播技术和腾讯云直播 09:54 3: [录播] 基础封装 23 ...

  6. 《HTML5移动网站与App开发实战》简介

    #好书推荐##好书奇遇季#<HTML5移动网站与App开发实战>,京东当当天猫都有发售.定价79元,网店打折销售更便宜.本书内容非常系统全面,配套示例源码与PPT课件. 本书由浅入深出.全 ...

  7. Cordova+React+OnsenUI+Redux新闻App开发实战教程-姜博-专题视频课程

    Cordova+React+OnsenUI+Redux新闻App开发实战教程-779人已学习 课程介绍         Cordova+React+OnsenUI+Redux新闻App开发实战视频培训 ...

  8. h5端登录是什么意思_H5混合式APP开发实战案例终结篇

    随着H5的功能不断完善,使用前端技术来开发安装在手机上的APP已经成为了许多人的选择,而且也有许多成熟的商业使用案例.本专栏注重实战,没有铺垫过多理论知识,因为实践出真知,实践是最好的学习方式.我选择 ...

  9. (头条新闻)Cordova+React+OnsenUI+Redux新闻App开发实战教程

    前言 伴随着HTML5技术的普及力度与日俱增, 混合应用开发已经备受关注, 百家争鸣的技术框架,如何做好技术选型,搭建最稳健的架构,快速的持续集成,是一个跨平台App开发的关键所在,所以本套视频教程凭 ...

最新文章

  1. poj 3275(传递闭包)
  2. python中ThreadLocal的理解与使用
  3. 企业客户都满意的ToB产品运营秘诀
  4. html游戏禁止微信浏览器下拉,如何用电脑模拟微信浏览器浏览禁止PC打开的微网站...
  5. 实验五 操作系统之存储管理
  6. 前端学习 -- 颜色
  7. 配置isc-dhcrelay需要注意的事项
  8. html 获取下一个兄弟节点,js jquery获取当前元素的兄弟级 上一个 下一个元素
  9. 1.8 Linux用户与用户组文件权限
  10. 少儿C++编程如何入门
  11. Qt学习之路八——利用qt对数据库进行操作
  12. vue+html5实现分类、商品分类、类别、菜单的左右层级布局两种不同精美样式
  13. linux内存扩展,linux 扩展内存
  14. 视频封装格式篇--MP4
  15. OpenCV4一部分函数目录
  16. 工业4.0 资产管理壳学习笔记(1)
  17. xp无法访问win7计算机,手把手为你处理解决XP不能访问win7共享文件的方法
  18. Augustus部署
  19. 天猫忌惮京东开放平台壮大 欲借“二选一”形成垄断
  20. 【EVB-335X-II试用体验】 基于Yocto的嵌入式的敏捷项目开发:以电子相册为例

热门文章

  1. vue通过currentTarget获取节点属性
  2. html自动滚动代码,html+css+javascript实现列表循环滚动示例代码
  3. ssd在linux下专为内存设备,linux下SSD优化
  4. Objective-C中对IPhone设备震动的调用
  5. 每周一磁 · 剩磁 Br
  6. 三星9300刷机详解 (转载)
  7. 零基础真的不能学游戏建模吗?
  8. Linux下用eclipse调试C++并行程序(MPI)
  9. A002-185-2526(期末作业)
  10. 微分方程的数值解法——常微分方程——差分法(1)