Android低功耗藍芽Gatt連線教學:使用Kotlin實作
前言
我花了一些時間複習之前工作所實作的低功耗藍牙連接。
由於我擔心會忘記,
所以想重新回顧一下並做個紀錄,
希望也能幫助到需要實作的各位。
Android 12之後新增了 權限相關處理,大家可以注意一下!
這邊是我處理的方式,大家可以參考:
最終目標是這樣
可以串回之前幾篇jetpack compose的練習
讓資料變成真實存在的資料
且最後能連接gatt藍芽

基本概念
首先介紹下
藍芽掃描的方法
大致上有三種
BluetoothAdapter.startDiscovery() -> 掃描經典藍芽和BLE藍芽兩種
BluetoothAdapter.startLeScan() -> 用來掃描低功耗藍芽 ---- 已被棄用
BluetoothLeScanner.startScan() -> 新的BLE掃描方法
不過看了API內的註解
目前startLeScan已被棄用
是在api21時被棄用
我也順便查了各個發現藍芽裝置的API來做比較
- 掃描過程通常執行12秒
- 是異步調用
-
透過註冊廣播來執行不同步驟,如:
ACTION_DISCOVERY_STARTED -> 當Discovery開始 ACTION_DISCOVERY_FINISHED -> 當Discovery完成 BluetoothDevice.ACTION_FOUND -> 發現藍芽裝置 </a> </li> <li> <a href="javascript:void(0)">當執行連接藍芽裝置時 不能處於startDiscovery中 需呼叫cancelDiscovery()來結束發現 </a> </li> <li> <a href="javascript:void(0)"> Discovery並非由Activity管理 而是system service 所以為了以防萬一必需使用cancelDiscovery() 確保Discovery沒有在執行 避免在連線藍芽裝置時 device還在Discovery </a> </li> <li> <a href="javascript:void(0)">Discovery只能發現目前是可被發現的藍芽裝置 </a> </li> <li> <a href="javascript:void(0)">可觀察ACTION_STATE_CHANGED是否為STATE_ON 如果當前藍芽state並非STATE_ON則API會返回false 用於確定目前是可獲得更新的值的狀態 <img src="/images/bluetooth/android_state.png" alt="Cover" class="w-full prose-img"> </a> </li> <li> <a href="javascript:void(0)">如果使用的目標版本小於等於Build.VERSION_CODES#R 則需要向使用者要求Manifest.permission#BLUETOOTH_ADMIN權限 <img src="/images/bluetooth/android_R.png" alt="Cover" class="w-full prose-img" > </a> </li> <li> <a href="javascript:void(0)"> 如果使用的目標版本大於等於Build.VERSION_CODES#S 則需要向使用者要求Manifest.permission#BLUETOOTH_SCAN權限 <img src="/images/bluetooth/android_S.png" alt="Cover" class="w-full prose-img" > </a> </li> <li> <a href="javascript:void(0)">除此之外 你可以要求Manifest.permission#ACCESS_FINE_LOCATION權限 來增加可交互的藍芽裝置種類 當然你也可以在<b>uses-permission</b>新增usesPermissionFlags="neverForLocation" tag 來避免要求位置權限 但同時可以搜尋到的裝置種類會有所限制 </a> </li> </ol>
- 開始 Bluetooth LE 掃描,掃描結果會透過callback返回
- 因為這個沒有帶filters,
所以省電的預設當螢幕關閉會stopScan, 重新開啟後會resume</a></li> <li><a href="javascript:void(0)">如果使用的目標版本大於等於Build.VERSION_CODES#Q , 則需要向使用者要求Manifest.permission#ACCESS_FINE_LOCATION權限</a></li> <li> <a href="javascript:void(0)">如果使用的目標版本小於等於Build.VERSION_CODES#R , 則需要向使用者要求Manifest.permission#BLUETOOTH_ADMIN權限 <img src="/images/bluetooth/android_R.png" alt="Cover" class="w-full prose-img"> </a> </li> <li><a href="javascript:void(0)">如果使用的目標版本大於等於Build.VERSION_CODES#S, 則需要向使用者要求Manifest.permission#BLUETOOTH_SCAN權限 <img src="/images/bluetooth/android_S.png" alt="Cover" class="w-full prose-img"> </a> </li> <li><a href="javascript:void(0)">除此之外 你可以要求Manifest.permission#ACCESS_FINE_LOCATION權限, 來增加可交互的藍芽裝置種類, 當然你也可以在<uses-permission>新增usesPermissionFlags="neverForLocation" tag, 來避免要求位置權限, 但同時可以搜尋到的裝置種類會有所限制</a></li>
- 特性包含上方startScan ( callback:ScanCallback ) 的 六條
-
透過ScanFilter 去篩選掃描的結果,
主要支援下面幾項, <img src="/images/bluetooth/android_filter.png" alt="bluetooth android filter" style="width: 80%" class="prose-img"> </a> </li> <li><a href="https://developer.android.com/reference/android/bluetooth/le/ScanSettings#summary" target="_blank" rel="noopener noreferrer"> 也透過ScanSettings去設定要針對返回的callback去做怎樣處理, 如:返回每個過濾成功的資料、只返回第一個過濾成功的資料...等等</a></li>
實際開發:如何進行藍芽掃描
在manifest中加入上述所需權限
可以通用
requestMultiplePermissions(Manifest.permission.ACCESS_FINE_LOCATION,...
private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
註冊監聽BluetoothDevice.ACTION_FOUND
val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
requireContext().registerReceiver(receiver, filter)
繼承一個BroadcastReceiver
然後使用receiver type的形式返回結果bleDevice
private val receiver = DeviceListBoardCast { bleDevice ->
deviceViewModel.addDevice(bleDevice)
}
其中取資料的方式是,到時候掃描出來的資料可以從這幾個拿
val device: BluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)!!
val rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE).toInt()
val uuidExtra = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID)
繼承的BroadcastReceiver實作
前面已經拿到bluetoothAdapter跟註冊廣播監聽了
所以開始分別可以用startDiscovery、cancelDiscovery 來開始或結束掃描
bluetoothAdapter.startDiscovery()
bluetoothAdapter.cancelDiscovery()
這個函式
主要是進行掃描的開關
並搭配viewmodel與coroutine
做到用viewmodel紀錄刷新狀態,並透過coroutine掃描指定秒數 x 秒
如果不需要用到那麼複雜的話
直接用startDiscovery、cancelDiscovery去開發就行了
掃描的結果會返回剛剛DeviceListBoardCast {}內,
這邊根據自己專案去調整就行
我是用viewmodel來觀察資料
private val receiver = DeviceListBoardCast { bleDevice ->
deviceViewModel.addDevice(bleDevice)
}
實際開發:如何進行藍芽連線
這邊使用service的方式去串接
首先建立一個service
並建立Binder
用來onBind時返回實例給fragment去調用
在該service內創建一個 initialize()函式
用在之後bindservice時可以調用初始化
接著寫個gattCallback實例
這邊主要是onConnectionStateChange、onServicesDiscovered、onCharacteristicRead
分別是當有新的連接狀態改變、新的服務被發現、新的東西讀到後
返回的callback
這邊主要可以根據你的需求
去做判斷
-
onConnectionStateChange ->返回藍芽狀態
-
當執行discoverServices() 去找現有的ble
當找到會進onServicesDiscovered
- 有個方法setCharacteristicNotification
這個是去啟用notify
去找特定的Characteristic
(這邊Characteristic就看跟硬體的協議或定義)
當藍芽裝置數值有改變就會用onCharacteristicChanged通知你
- 然後writeCharacteristic可以讓你寫值進指定的Characteristic
一樣當有結果會進到
onCharacteristicWrite
gattCallback範例:
其實主要就是以下兩段去做連線
val device = bluetoothAdapter!!.getRemoteDevice(address)
跟
bluetoothGatt = device.connectGatt(this, false, gattCallback)
把要連線的adress丟進去
拿到想要連線的BluetoothDevice
再用device內的方法connectGatt去綁定Gatt裝置
當然同時要丟入前面寫好的gattCallback
前面只是做一連串的null確認
確保app 不會因null而crash
在 gattCallback 的實例化中,
你會發現有一個名為 broadcastUpdate 的方法。
這個方法主要是用來發送廣播訊息,
你可以根據自己的需求去定義遇到什麼情況要做什麼事,
或要回傳什麼廣播訊息。
簡單的連接與尋找裝置
大概就是這樣
接著藍芽最重要的就是終端之間的通訊
所以如果想要收送資料
必需要找出service與characteristic
這邊先上個圖

所以我們透過以下方法找出:
將前面透過廣播取得的gatt service帶入
就可以透過遍歷去取得characteristic
那因為android官方已經有幫你包好characteristic的類了
所以你要讀取只要透過相關function呼叫:
並且他會在之前定義的BluetoothGattCallback內的
onCharacteristicRead返回給你
你只要定義好接收廣播就可以得到資料
另外藍芽裡面也有一種notify的方法:
一樣返回結果
BluetoothGattCallback裡面
onCharacteristicChanged去看
如果想看怎麼透過第三方工具
擷取藍芽封包可以參考:
藍芽模組筆記:有 經典藍芽(BT) 與 低功耗藍牙(LTE)
泛指藍芽4.0以下的模組
一般用於資料量比較大的傳輸
如:語音、音樂、較高資料量傳輸等
經典藍芽模組可再細分為
傳統藍芽模組和高速藍芽模組
傳統藍芽模組在2004年推出
主要代表是支援藍芽2.1協議的模組
傳統藍芽有3個功率級別
Class1 / Class2 / Class3
分別支援100m / 10m / 1m的傳輸距離
高速藍芽模組在2009年推出
速率提高到約24Mbps
是傳統藍芽模組的八倍
泛指藍芽4.0或更高的模組
藍芽低功耗技術是低成本、短距離
可工作在2.4GHz ISM射頻頻段
因為BLE技術採用非常快速的連線方式
因此平時可以處於“非連線”狀態(節省能源)
Android手機藍芽4.x都是雙模藍芽(既有經典藍芽也有低功耗藍芽)
Kotlin + jetpack compose 藍芽app範例
最後我之前寫了一個範例,最近終於整理上來,有需要的可以參考看看 可參考此篇