前言

開發公司的 Android 產品中有一個是跟連線控制相關的,除了用 TCP/IP 的連線外,我們也希望加入藍牙連線的功能,使用 Bluetooth Low Energy 的方式連線,減少電量消耗

幾年前慢慢流行起來的穿戴式裝置,還有各種用途的 sensor communication,這些裝置幾乎都是用藍芽連線的方式來傳輸資料跟控制其他裝置,如何在有限的電量下達成最長的使用時間就是一件很重要的事,BLE 在現今可以說是非常廣泛應用的一個技術了

Android 當然也有對應的 API,方便支援 BLE 的 Android 手機能跟其他 BLE 裝置進行互動

這篇記錄下當時在 Android 裝置上加入 BLE 的一些筆記跟心得

BLE 架構

比起原本的 Bluetooth, 提供更低功耗的連線機制, 在裝置之間傳送小量的檔案, 與鄰近的 sensor 互動

相關術語

  • Generic Attribute Profile (GATT)
    • GATT profile 在 BLE link 上通用的規格, 傳送包裝成 attributes 的短資料, 所有 Low Energy 應用的資料都 base 在 GATT
  • Attribute Protocol (ATT)
    • GATT 建構在 ATT 的上面, ATT 盡可能使用最少的 bytes 來描述資料, 每個 attribute 用 Universally Unique Identifier (UUID) 來個別描述, UUID 是128bit 的 string ID, 被 ATT 傳輸的 attribute 被格式化為 characteristics 和 services
  • Characteristic
    • a single value 和 0-n descriptors that describe the value (type, analogous to a class)
  • Descriptor
    • defined attributes that describe a characteristic value
  • Service
    • collection of characteristics
    • For example, service called “Heart Rate Monitor” that includes characteristics such as “heart rate measurement”.

Overview

一個 Profile 可以有好幾個 Service,是一組服務的集合,這些服務組合起來就是一個使用場景,比如小米手環內有一個計算使用者當前步數的服務

一個 Service 可以包含好幾個 Characteristic

一個 Characteristic 可以包含 Properties,Value 還有多個 Descriptor,Characteristic 具有 Read, Write, Notify 等權限,BLE 設備連接成功後對它進行讀寫的動作實際上就是對 Characteristic 進行

BLE 以 UUID 來識別多個 Service,所以在讀寫 BLE 數據時都要有相應的 UUID 來針對不同的 Service 作反應

UUID

A UUID is a universally unique identifier that is guaranteed to be unique across all space and all time

128-bit 含有數字與英文的一串字串,不分大小寫 (not case-sensitive)

Bluetooth SIG 有公開一個 UUID list 針對不同廠商開發的 Service 有他們自己獨立的 UUID,方便其他需要跟這個 Service 互動的裝置,這些是被 Bluetooth SIG 保留的 IDs (前面的 16-bit),不能被任何 custom service 所使用

128-bit value = 16-bit-value * 2^96 + BluetoothBaseUUID

BluetoothBaseUUID = 00000000-0000-1000-8000-00805f9b34fb

像是 Battery Service 是 0x180F

Battery Service UUID = 0000180F-0000-1000-8000-00805f9b34fb

Custom UUID

承上所述,16-bit 搭配 Base UUID 的設置是給被保留的名單,我們要盡可能不要使用到保留區段,像是 XXXXXXXX-0000-1000-8000-00805f9b34fb (XXXXXXXX = 任何數字),自己的小專案或是還在內部開發的測試階段使用是沒關係,release 的話就要避免

可以使用隨機的 128-bit 來設置 UUID,比如 795090c7-420d-4048-a24e-18e60180e23c 這種亂數

隨機產生是最方便的做法,但這樣做不能保證完全沒有別的應用有使用到相同的 UUID (機率很低就是),最好的辦法就是公司可以跟 Bluetooth SIG 申請獨立的 UUID

連線機制

通常分為 Central 和 Peripheral

Central 角色的裝置會搜尋 (scan) 發出廣播 (advertisement) 的 Peripheral 裝置

所以如果是以單向 Client 連線到 Server 的模型,Server = Peripheral,Client = Central,會有好幾個 Client 同時連線於一個 Server

這裡有一個很好的 client-server 圖例

兩台 Android 裝置誰要當 Central 還是 Peripheral,就要看應用方式是什麼樣來決定

另外要注意的是 BLE 不像經典藍牙有固定的 mac address, 每次連線 BLE 的 mac address 都會變,且藍牙通訊是跳頻的,只有在 37, 38, 39 這三個廣播頻道進行廣播,雙方設備在某一時刻同時跳到同一個頻段上才能進行連接,就會有機率會連不上裝置,尤其在一直連續斷掉重連的時候

Android dev

官方文件對於如何加入 BLE 有滿不錯的 step by step 教學了 (注意這是 Client 端的部分)

廣播與搜尋

要特別注意的就是如果你的 Client 端應用,想要限制只會搜尋到特定的服務的話,在 scan 的時候就要做 filter

Server 端這做要設置好特定的 UUID 讓 Client 可以指定,在 scanning 時只掃這組特定的 UUID,才不會掃到其他的 Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Server side
private void startAdvertise(byte[] androidId) {
AdvertiseSettings mAdvSettings = new AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_LOW)
.setConnectable(true)
.build();
AdvertiseData mAdvData = new AdvertiseData.Builder()
.setIncludeTxPowerLevel(true)
.addServiceUuid(new ParcelUuid(ADV_SERVICE_UUID))
.addManufacturerData(65535, androidId)
.build();
AdvertiseData mAdvScan = new AdvertiseData.Builder()
.setIncludeDeviceName(true)
.build();
mBluetoothLeAdvertiser.startAdvertising(mAdvSettings, mAdvData, mAdvScan, mAdvertiseCallback);
}

傳送資料

前面提過 BLE 傳送資料其實是利用對 Characteristic 的讀寫來交換資料

當初在開發的時候,記得另外一個比較會讓人混淆的就是讀寫 call back 的相關 API

Client 傳訊息給 Server 的機制是 writeCharacteristic

Server 如果收到 Client 的訊息,會到 onCharacteristicWriteRequest 這個 call back function,這時候就可以把資料取出來

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device,
int requestId,
BluetoothGattCharacteristic characteristic,
boolean preparedWrite, boolean responseNeeded,
int offset,
byte[] value) {

super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite
, responseNeeded, offset, value);
// retrieve your data, value or characteristic
}

Server 要傳訊息給 Client 的機制是先改變目前給 Client 讀取的 Characteristic 的內容,這裡的 BluetoothGattCharacteristic 的設置要記得加上特定的 property

1
2
3
4
5
6
7
mUpdateCharacteristic = new BluetoothGattCharacteristic(UPDATE_CHARACTERISTIC_UUID,
BluetoothGattCharacteristic.PROPERTY_READ |
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ);

// put your updated data
mUpdateCharacteristic.setValue(data); // data type is byte[]

接著再 call notifyCharacteristicChanged,notify BLE Client (在已經建立連線的狀態下),來讀取新的資料

1
2
3
4
5
6
7
8
9
//true for indicate, false for notification(w/o ACK)
boolean indicate = (characteristic.getProperties()
& BluetoothGattCharacteristic.PROPERTY_INDICATE)
== BluetoothGattCharacteristic.PROPERTY_INDICATE;
if (mGattServer != null) {
mGattServer.notifyCharacteristicChanged(device, characteristic, indicate);
} else {
// null handle
}

後記

Android BLE 的 API 滿多的,所以一開始會覺得很雜,而且到後面遇到一些連線問題時會發現細節很多,在開發的過程中修了不少地方

一開始要弄懂最快的方式就是照著範例寫好簡單的 Server 與 Client 端後,在每個 call back 都印 log 出來跑一次看看,就會清楚他們之間的 Flow

連線穩定度我覺得有點看兩台 Android 裝置是什麼,我們遇過不同 Android 手機連同一台 Android 裝置的時候有不一樣的連線狀況,Client 與 Server 端的程式版本明明一樣

開發 Android 最煩的大概就是不同系統廠的手機會有不一樣的毛病…這部分只能說無解,所幸後來流行 pure Android system,各廠牌都有出,賣得也不錯,希望 pure Android 能慢慢成為市場主流XD

圖片與參考資料來源