低功耗藍牙工具APP開發實戰

《低功耗藍牙工具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 已連接

狀態切換圖

狀態切換圖.png

設備類型

類型 中文名 說明
Cnetral 主機 常作為client端,如手機、PC
Peripheral 從機 常作為Service端,如鼠標、血壓計
Observer 觀察者
Broadcast 廣播者

中心設備和外設交互[重點]

中心設備與外設.png

從上圖可以看出,手機或者MAC可以做為中心設備,鼠標和血壓計作為外設。外設(有數據)發起發起廣播,中心設備(類似APP向服務端索取數據)收到廣播會去掃描外設和監聽收到的信息。

協議棧

藍牙協議規定了兩個層次的協議,分別為藍牙核心協議(Bluetooth Core)和藍牙應用層協議(Bluetooth Application)。藍牙核心協議關注對藍牙核心技術的描述和規范,它只提供基礎的機制,并不關心如何使用這些機制;藍牙應用層協議,是在藍牙核心協議的基礎上,根據具體的應用需求,百花齊放,定義出各種各樣的策略,如FTP、文件傳輸、局域網等等。

BLE協議棧.png

而藍牙核心協議(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進行分類管理。

服務與特征[重點]

服務與特征.png

一個外設可以包含一個或多個服務(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

在點擊工具欄里的文件 -> 新建 -> 項目:

創建項目.png

選擇 uni-app ,填入項目名稱[此處取名demo],模板選擇 默認模板 后,點擊創建。

選擇模板創建項目.png

項目運行

使用HBuilderX打開demo項目,雙擊 App.vue 后,點擊工具欄的運行 -> 內置瀏覽器運行,即可在瀏覽器里面體驗uni-app 的 H5 版【首次運行會提示需要安裝,安裝后再次點擊即可】。


項目運行.png

了解其他運行效果,可以官網查看,也可以自行操作,此處就不再累贅。

目錄結構說明

如果您看到的目錄與下面不一致,試著創建新項目,模板選擇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 版本:

項目配置.png

添加 BLE 模塊支持:


添加BLE模塊支持.png

添加BLE權限:


添加BLE權限.png

如不配置自動添加權限,則找下以下幾個選項,勾選:

"<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 配好設置,選擇 運行配置

選擇 微信開發者工具路徑,添加對應路徑。

7.png

  1. 配置證書(需要發布在進行操作)

使用HBuilderX打開demo項目,雙擊 App.vue 后,點擊工具欄的發行 -> 原生APP-云打包。


8.png

會彈出App打包需要配置的內容,區分 Android 和 iOS ,根據內容自行配置即可。

9.png
  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調試助手,具體功能通過思維導圖示如下:


10.png

經過上圖規整,再對比uni-app提供的BLE接口,能一套代碼完成以上功能。這也是本文選擇使用 uni-app進行開發并講解的原因。

低功耗藍牙 API 平臺差異說明(剛好滿足App和微信小程序)

App H5 微信小程序 支付寶小程序 百度小程序 字節跳動小程序 飛書小程序 QQ小程序 快手小程序
× × × × ×
[藍牙API](https://uniapp.dcloud.io/api/system/bluetooth)(接口地址:https://uniapp.dcloud.io/api/system/bluetooth)
API 說明
uni.openBluetoothAdapter(OBJECT) 初始化藍牙模塊
uni.startBluetoothDevicesDiscovery(OBJECT) 開始搜尋附近的藍牙外圍設備。<br />此操作比較耗費系統資源,請在搜索并連接到設備后調用 uni.stopBluetoothDevicesDiscovery 方法停止搜索
uni.onBluetoothDeviceFound(CALLBACK) 監聽尋找到新設備的事件
uni.stopBluetoothDevicesDiscovery(OBJECT) 停止搜尋附近的藍牙外圍設備。<br />若已經找到需要的藍牙設備并不需要繼續搜索時,建議調用該接口停止藍牙搜索。
uni.onBluetoothAdapterStateChange(CALLBACK) 監聽藍牙適配器狀態變化事件。
uni.getConnectedBluetoothDevices(OBJECT) 根據 uuid 獲取處于已連接狀態的設備。
uni.getBluetoothDevices(OBJECT) 獲取在藍牙模塊生效期間所有已發現的藍牙設備。<br />包括已經和本機處于連接狀態的設備。
uni.getBluetoothAdapterState(OBJECT) 獲取本機藍牙適配器狀態。
uni.closeBluetoothAdapter(OBJECT) 關閉藍牙模塊。<br />調用該方法將斷開所有已建立的連接并釋放系統資源。建議在使用藍牙流程后,與 uni.openBluetoothAdapter 成對調用。
[低功耗藍牙 API](https://uniapp.dcloud.io/api/system/ble) (接口地址:https://uniapp.dcloud.io/api/system/ble )
API 說明
uni.setBLEMTU(OBJECT) 設置藍牙最大傳輸單元。<br />需在 uni.createBLEConnection調用成功后調用,mtu 設置范圍 (22,512)。安卓5.1以上有效。
uni.writeBLECharacteristicValue(OBJECT) 向低功耗藍牙設備特征值中寫入二進制數據。<br />注意:必須設備的特征值支持 write 才可以成功調用。
uni.readBLECharacteristicValue(OBJECT) 讀取低功耗藍牙設備的特征值的二進制數據值。<br />注意:必須設備的特征值支持 read 才可以成功調用。
uni.onBLEConnectionStateChange(CALLBACK) 監聽低功耗藍牙連接狀態的改變事件。<br />包括開發者主動連接或斷開連接,設備丟失,連接異常斷開等等。
uni.onBLECharacteristicValueChange(CALLBACK) 監聽低功耗藍牙設備的特征值變化事件。<br />必須先啟用 notifyBLECharacteristicValueChange 接口才能接收到設備推送的 notification。
uni.notifyBLECharacteristicValueChange(OBJECT) 啟用低功耗藍牙設備特征值變化時的 notify 功能,訂閱特征值。<br />注意:必須設備的特征值支持 notify 或者 indicate 才可以成功調用。 另外,必須先啟用 notifyBLECharacteristicValueChange才能監聽到設備 characteristicValueChange 事件
uni.getBLEDeviceServices(OBJECT) 獲取藍牙設備所有服務(service)。
uni.getBLEDeviceRSSI(OBJECT) 獲取藍牙設備的信號強度。
uni.getBLEDeviceCharacteristics(OBJECT) 獲取藍牙設備某個服務中所有特征值(characteristic)。
uni.createBLEConnection(OBJECT) 連接低功耗藍牙設備。<br />若APP在之前已有搜索過某個藍牙設備,并成功建立連接,可直接傳入之前搜索獲取的 deviceId 直接嘗試連接該設備,無需進行搜索操作。
uni.closeBLEConnection(OBJECT) 斷開與低功耗藍牙設備的連接。

通過上述分析,再次針對APP/小程序進行需求梳理,梳理如下:

11.png

原型設計

通過上訴需求梳理后,進行原型圖設計和UI圖設計。

原型圖

  • 設計工具:Mockplus
  • 原型地址:https://run.mockplus.cn/dua7ZgYiBOPE5Wsw/index.html
  • 說明:原型不要太考慮美化,因為那是UI設計師、視覺設計師、動效設計師等的事情。原型只要把功能和基本頁面交互規劃準確就可以。
  • 交互圖展示:


    12.png

UI圖

  • 設計工具:Sketch
  • 在線設計圖:藍湖 (建議使用,會在項目開發中提及)
  • 說明:Sketch 當前只能使用Mac進行設計,所以無法打開Sketch的讀者,請雙擊資源[在下節前置準備提及]中的 LightBLE-HTML/index.html 跳轉到瀏覽器,便可以看到 LightBLE 的UI設計圖。
  • UI設計圖展示:
13.png

前置準備

第四章 項目實戰

基礎框架

從資源目錄中打開 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 調用成功;
  2. 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>
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容