《低功耗藍牙工具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\" />"
其他配置
- 使用HBuilderX打開demo項目,雙擊
manifest.json
選擇 微信小程序配置
添加微信小程序AppID 。
選擇 App圖標配置
添加圖標,使用自動生成即可。
- 選擇HBuilderX
配好設置
,選擇運行配置
選擇 微信開發者工具路徑
,添加對應路徑。
- 配置證書(需要發布在進行操作)
使用HBuilderX打開demo項目,雙擊 App.vue 后,點擊工具欄的發行 -> 原生APP-云打包。
會彈出App打包需要配置的內容,區分 Android 和 iOS ,根據內容自行配置即可。
-
賬戶申請(安卓市場只列舉部分)
平臺 地址 說明 微信小程序 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)(接口地址: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/小程序進行需求梳理,梳理如下:
原型設計
通過上訴需求梳理后,進行原型圖設計和UI圖設計。
原型圖
- 設計工具:Mockplus
- 原型地址:https://run.mockplus.cn/dua7ZgYiBOPE5Wsw/index.html
- 說明:原型不要太考慮美化,因為那是UI設計師、視覺設計師、動效設計師等的事情。原型只要把功能和基本頁面交互規劃準確就可以。
-
交互圖展示:
12.png
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實現通信 、 通過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 。
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>
搜索藍牙設備
搜索藍牙設備需要兩步:
- startBluetoothDevicesDiscovery 調用成功;
- 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>