既成の BLE デバイスを自作プログラムから利用する試み
- BLE デバイス間の通信内容をパケットレベルで読み解いてみる
題材には低価格で出回っているありふれた Anti-Lost 系 BLE デバイス A を選びました。この小さなデバイスには LED・ブザーと押しボタンスイッチが 実装されており、対向スマホアプリとの間で双方向のやりとりが可能なつくりになっています。
デバイス A の UI
デバイス A とアプリの連携に必要な処理
前回の記事から、デバイス A とアプリの連携に関する要素をピックアップしてみます。
- アプリからデバイス A の LED・ブザーをコントロール
- 「Scene 6: アプリからデバイス A の LED・ブザーを操作」
Frame 616
対向アプリ上の所定のボタンを押下すると、Frame 345, Frame 238 の示す、 "「Immediate Alert」サービス配下の「Alert Level」キャラクタリスティック" の Value Handle 0x0025 に値「2(High Alert)」が書き込まれる
デバイス A の LED・ブザーはファームウェアレベルでこの Alert Level キャラクタリスティックに紐づけられている模様。High Alert 値が書き込まれるとそれに反応して短時間 LED とブザーが ON になる
- 「Scene 6: アプリからデバイス A の LED・ブザーを操作」
- デバイス A のボタン押下をアプリへ通知
- 「Scene 5: デバイス A ボタン押下時のアプリへの通知を設定」
Frame 491
Frame 453、Frame 439、Frame 453 の示す通り、ハンドル 0x0036 は、 "ユーザ定義サービス 1(UUID = 0xFFE0)配下のユーザ定義キャラクタリスティック(UUID = 0xFFE1)配下の Client Characteristic Configuration Descriptor (CCCD)" である
当該ユーザ定義キャラクタリスティック(UUID = 0xFFE1)のプロパティには Notify が設定されており、クライアントである対向アプリから CCCD 0x0036 に Notification bit (0x0001) を書き込んでおくことで、このキャラクタリスティックの値が更新された時に Value Handle である 0x0035 経由でアプリ側へ通知(Notification)が行われるようになる
- 「Scene 7: デバイス A のボタンを押すとアプリへ通知」
Frame 711
デバイス A の物理ボタンはファームウェアレベルで Frame 439 の示す "ユーザ定義サービス 1(UUID = 0xFFE0)配下のユーザ定義キャラクタリスティック(UUID = 0xFFE1)" に紐づけられている模様。このボタンを押すと当該キャラクタリスティックの値がデバイス内部で更新され Frame 491 での仕込みに基づきアプリ側へ通知が行われる
- 「Scene 5: デバイス A ボタン押下時のアプリへの通知を設定」
サービス・キャラクタリスティック・デスクリプタの UUID について
BLE ネイティブの世界では所定のキャラクタリスティックやデスクリプタの I/O には各エントリに紐づけられたハンドルが使用されますが、抽象化された Android API での処理対象はオブジェクトです。所定のキャラクタリスティックやデスクリプタのオブジェクトの取得にはそれぞれの UUID が必要であるため、プログラムの記述に際しては、パケットログ上に記録された所定のハンドルがどの UUID のエントリのものであるかを正確に把握する必要があります。
また、サービス - キャラクタリスティック - デスクリプタは階層関係にあるため、所定のエントリのオブジェクトを取得する手続きは最上位にあるサービスのオブジェクトが常に起点となります。
以下に、パケットログから所定のサービス以下の各エントリの UUID を見つける方法と、各 UUID からそれぞれのエントリのオブジェクトを取得する Android コードの例を示します。
パケットログから所定のサービス以下の各エントリの UUID を拾う
- 前回記事中のパケットログ Frame 229 では 0x0001 - 0xffff のハンドル空間を対象に Read By Group Type Request で GATT Primary Service Declaration を照会、そのレスポンスが Frame 231 です
ここではレスポンスに含まれる 3件のレコードのうち 2件めに注目してみます
Opcode: Read By Group Type Response (0x11) : Attribute Data, Handle: 0x000c, Group End Handle: 0x000f Handle: 0x000c Group End Handle: 0x000f Value: 0118 :
- Primary Service Declaration の照会に対する Read By Group Type Response の「Value」には当該サービスの UUID が格納される
- この例では 16ビット UUID = 0x1801 であり、これは、BLE 既定の Generic Attribute サービスを示す
- 当該サービスはハンドルグループ 0x000c - 0x000f を占有する
- Frame 287 では上記のハンドルグループ 0x000c - 0x000f を対象に Read By Type Request で GATT Characteristic Declaration を照会、そのレスポンスが Frame 289 です
レスポンスに注目します
Opcode: Read By Type Response (0x09) : Attribute Data, Handle: 0x000d Handle: 0x000d Value: 200e00052a
- Characteristic Declaration の照会に対する Read By Type Response の「Value」には当該キャラクタリスティックのプロパティ・Value Handle・UUID が格納される(詳細:1a, 2a, 1b)
(※表は BLUETOOTH SPECIFICATION Version 4.2 [Vol 3, Part G] page 532 より) - ここでは「value: 200e00052a」につき、プロパティ = 0x20 (Indicate), Characteristic Value Handle = 0x000e, 16ビット UUID = 0x2A52
- 16ビット UUID = 0x2A52 は、BLE 既定の Service Changed キャラクタリスティックを示す
- Characteristic Declaration の照会に対する Read By Type Response の「Value」には当該キャラクタリスティックのプロパティ・Value Handle・UUID が格納される(詳細:1a, 2a, 1b)
- Frame 302 では残りのハンドル 0x000f についての情報を GATT へ Find Information Request で照会、そのレスポンスが Frame 305 です
レスポンスに注目します
Opcode: Find Information Response (0x05) UUID Format: 16-bit UUIDs (0x01) Handle: 0x000f UUID: Client Characteristic Configuration (0x2902)
- ハンドル 0x000f は Client Characteristic Configuration Descriptor (CCCD) であり、この CCCD は直前の Indicate プロパティを持つ Service Changed キャラクタリスティックに属する
(※CCCD の 16ビット UUID は 0x2902 固定)
- ハンドル 0x000f は Client Characteristic Configuration Descriptor (CCCD) であり、この CCCD は直前の Indicate プロパティを持つ Service Changed キャラクタリスティックに属する
以上のことから、デバイス A 上の Generic Attribute サービスの構成は以下の内容であることがわかります。
Generic Attribute サービス(UUID = 0x1801) | Handle Group = 0x000c - 0x000f | +-- Service Changed キャラクタリスティック(UUID = 0x2A52) | Handle = 0x000d, Value Handle = 0x000e | +-- Client Characteristic Configuration Descriptor (UUID = 0x2902) Handle = 0x000f
所定の UUID のエントリのオブジェクトを取得する
ここまでに登場した UUID はすべて 16ビット値でしたが、16 ビット UUID は Bluetooth 用にアサインされている本来の 128ビット UUID の固定部分(BASE_UUID)を省略した表現です。
- BLUETOOTH SPECIFICATION Version 4.2 [Vol 3, Part B] page 227
2.5 SEARCHING FOR SERVICES : 2.5.1 UUID : To reduce the burden of storing and transferring 128-bit UUID values, a range of UUID values has been pre-allocated for assignment to often-used, registered purposes. The first UUID in this pre-allocated range is known as the Bluetooth Base UUID and has the value 00000000-0000-1000-8000-00805F9B34FB, :
Android API を用いて前出の Generic Attribute サービスと Service Changed キャラクタリスティック、およびその配下の Client Characteristic Configuration Descriptor のオブジェクトを取得するコードのイメージを示します。
private BluetoothGatt mBtGatt; private BluetoothGattCharacteristic mChServiceChanged; private BluetoothGattDescriptor mCCCD; // Generic Attribute サービス の UUID private UUID mUuidSvcGenericAttribute = UUID.fromString("00001801-0000-1000-8000-00805f9b34fb"); // Service Changed キャラクタリスティックの UUID private UUID mUuidChServiceChanged = UUID.fromString("00002a52-0000-1000-8000-00805f9b34fb"); // Client Characteristic Configuration Descriptor の UUID (固定値) private UUID mUuidCCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); : private BluetoothGattCallback mGattCallback = new bleGattCallback(); : // デバイス A へ接続後、GATT の提供する各サービス以下の一覧を取得 mBtGatt.discoverServices();(mCtx, false, mGattCallback); : // GATT イベントハンドラ private class bleGattCallback extends BluetoothGattCallback { @Override // GATT サービス一覧取得完了 public void onServicesDiscovered(BluetoothGatt gatt, int status) { super.onServicesDiscovered(gatt, status); // デバイス A の Generic Attribute サービスの // Service Changed キャラクタリスティックオブジェクトを取得 BluetoothGattService svc = gatt.getService(mUuidSvcGenericAttribute); mChServiceChanged = svc.getCharacteristic(mUuidChServiceChanged); // Service Changed キャラクタリスティックの // Client Characteristic Configulation Descriptor を取得 mCCCD = mChServiceChanged.getDescriptor(mUuidCCCD); : }
ちなみに、アプリ開発の初期に BluetoothGattCallback の onServicesDiscovered() に次の要領のコードを挿入して 各 GATT サービス配下の全エントリの UUID を階層的に出力し保存しておくと何かと便利です。
public void onServicesDiscovered(BluetoothGatt gatt, int status) { super.onServicesDiscovered(gatt, status); List<BluetoothGattService> serviceList = gatt.getServices(); Log.d(TAG, "onServicesDiscovered: serviceList.size=" + serviceList.size()); for (BluetoothGattService s : serviceList) { Log.d(TAG, "onServicesDiscovered: svc uuid=" + s.getUuid().toString()); List<BluetoothGattCharacteristic> chlist = s.getCharacteristics(); Log.d(TAG, "onServicesDiscovered: chrlist.size=" + chlist.size()); for (BluetoothGattCharacteristic c : chlist) { UUID uuid = c.getUuid(); Log.d(TAG, "onServicesDiscovered: chr uuid=" + uuid.toString()); List<BluetoothGattDescriptor> dlist = c.getDescriptors(); Log.d(TAG, "onServicesDiscovered: desclist.size=" + dlist.size()); for (BluetoothGattDescriptor d : dlist) { Log.d(TAG, "onServicesDiscovered: desc uuid=" + d.getUuid()); } } }
デバイス A への接続後に上のコードを実行した際のログです。(※見やすさのためにサービスごとに改行を挿入)
当然ながら、この内容は前回採取したパケットログ内の各エントリの情報と符合しています。
作成したアプリ
以上の内容にもとづいてアプリを作成しました。ごくシンプルなものですが期待通りに動いています。
ソースコード一式
動画:動作の様子
作成した Android アプリとデバイス A の連携の様子を収めた動画です。デバイス A は UI 部分のみを露出しています。
(34秒 アラーム音あり 音量注意)
メモ:実装手順など
private BluetoothAdapter mBtAdapter; private BluetoothLeScanner mBtScanner; private BluetoothDevice mBtDevice; private BluetoothGatt mBtGatt;
1. BluetoothAdapter 〜 BluetoothLeScanner を取得
- BluetoothAdapter - developer.android.com
Represents the local device Bluetooth adapter. The BluetoothAdapter lets you perform fundamental Bluetooth tasks, such as initiate device discovery, query a list of bonded (paired) devices, instantiate a BluetoothDevice using a known MAC address, and create a BluetoothServerSocket to listen for connection requests from other devices, and start a scan for Bluetooth LE devices. : static BluetoothAdapter getDefaultAdapter() Get a handle to the default local Bluetooth adapter.
- BluetoothAdapter - getBluetoothLeScanner - developer.android.com
BluetoothLeScanner getBluetoothLeScanner () Returns a BluetoothLeScanner object for Bluetooth LE scan operations.
- BluetoothLeScanner - developer.android.com
BluetoothLeScanner This class provides methods to perform scan related operations for Bluetooth LE devices. An application can scan for a particular type of Bluetooth LE devices using ScanFilter. It can also request different types of callbacks for delivering the result.
- BluetoothLeScanner - developer.android.com
mBtAdapter = BluetoothAdapter.getDefaultAdapter(); mBtScanner = mBtAdapter.getBluetoothLeScanner();
2. アドバタイジングパケットのスキャン 〜 対象とする BluetoothDevice を取得
- BluetoothLeScanner - startScan - developer.android.com
startScan void startScan (ScanCallback callback) Start Bluetooth LE scan with default parameters and no filters. The scan results will be delivered through callback. Requires BLUETOOTH_ADMIN permission. An app must hold ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission in order to get results. Parameters callback ScanCallback: Callback used to deliver scan results. :
- ScanCallback - developer.android.com
- onScanResult - developer.android.com
onScanResult void onScanResult (int callbackType, ScanResult result) Callback when a BLE advertisement has been found. Parameters callbackType int: Determines how this callback was triggered. Could be one of CALLBACK_TYPE_ALL_MATCHES, CALLBACK_TYPE_FIRST_MATCH or CALLBACK_TYPE_MATCH_LOST result ScanResult: A Bluetooth LE scan result.
- ScanResult - getDevice - developer.android.com
getDevice BluetoothDevice getDevice () Returns the remote bluetooth device identified by the bluetooth device address.
- ScanResult - getDevice - developer.android.com
- onScanResult - developer.android.com
private ScanCallback mScanCallback = new bleScanCallback(); : mBtScanner.startScan(mScanCallback); : private class bleScanCallback extends ScanCallback { @Override public void onScanResult(int callbackType, ScanResult result) { super.onScanResult(callbackType, result); if (.......) { mBtDevice = result.getDevice(); } } @Override public void onScanFailed(int errorCode) { super.onScanFailed(errorCode); Log.e(TAG, "onScanFailed: err=" + errorCode); } }
3. デバイスへの接続
- BluetoothDevice - connectGatt - developer.android.com
BluetoothGatt connectGatt (Context context, boolean autoConnect, BluetoothGattCallback callback) Connect to GATT Server hosted by this device. Caller acts as GATT client. The callback is used to deliver results to Caller, such as connection status as well as any further GATT client operations. The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct GATT client operations. : callback BluetoothGattCallback: GATT callback handler that will receive asynchronous callbacks. :
- BluetoothGattCallback - developer.android.com
- onConnectionStateChange - developer.android.com
void onConnectionStateChange (BluetoothGatt gatt, int status, int newState) Callback indicating when GATT client has connected/disconnected to/from a remote GATT server. : newState int: Returns the new connection state. Can be one of STATE_DISCONNECTED or STATE_CONNECTED :
- onConnectionStateChange - developer.android.com
private BluetoothGattCallback mGattCallback = new bleGattCallback(); : mBtGatt = mBtDevice.connectGatt(mCtx, false, mGattCallback); : private class bleGattCallback extends BluetoothGattCallback { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { super.onConnectionStateChange(gatt, status, newState); if (newState == BluetoothProfile.STATE_CONNECTED) { // 接続確立 - デバイスの GATT サービス一覧の取得へ } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { // 切断完了の処理 } } :
4. GATT サービス - キャラクタリスティック - デスクリプタを探索 〜 必要なオブジェクトを取得
- BluetoothGatt - discoverServices - developer.android.com
boolean discoverServices () Discovers services offered by a remote device as well as their characteristics and descriptors. This is an asynchronous operation. Once service discovery is completed, the onServicesDiscovered(BluetoothGatt, int) callback is triggered. If the discovery was successful, the remote services can be retrieved using the getServices() function. :
- BluetoothGattCallback - onServicesDiscovered - developer.android.com
void onServicesDiscovered (BluetoothGatt gatt, int status) Callback invoked when the list of remote services, characteristics and descriptors for the remote device have been updated, ie new services have been discovered. :
- BluetoothGatt - getService - developer.android.com
getService BluetoothGattService getService (UUID uuid) Returns a BluetoothGattService, if the requested UUID is supported by the remote device. This function requires that service discovery has been completed for the given device. If multiple instances of the same service (as identified by UUID) exist, the first instance of the service is returned. Requires BLUETOOTH permission. Parameters uuid UUID: UUID of the requested service Returns BluetoothGattService BluetoothGattService if supported, or null if the requested service is not offered by the remote device.
- BluetoothGattService - getCharacteristic - developer.android.com
getCharacteristic BluetoothGattCharacteristic getCharacteristic (UUID uuid) Returns a characteristic with a given UUID out of the list of characteristics offered by this service. This is a convenience function to allow access to a given characteristic without enumerating over the list returned by getCharacteristics() manually. If a remote service offers multiple characteristics with the same UUID, the first instance of a characteristic with the given UUID is returned. Parameters uuid UUID Returns BluetoothGattCharacteristic GATT characteristic object or null if no characteristic with the given UUID was found.
- BluetoothGattCharacteristic - getDescriptor - developer.android.com
getDescriptor BluetoothGattDescriptor getDescriptor (UUID uuid) Returns a descriptor with a given UUID out of the list of descriptors for this characteristic. Parameters uuid UUID Returns BluetoothGattDescriptor GATT descriptor object or null if no descriptor with the given UUID was found.
- BluetoothGattCharacteristic - getDescriptor - developer.android.com
- BluetoothGattService - getCharacteristic - developer.android.com
private BluetoothGattCharacteristic mChAlertLevel = null; private BluetoothGattCharacteristic mChUser1 = null; private BluetoothGattDescriptor mDescUser1 = null; // デバイス A の提供するサービス・キャラクタリスティック群の UUID より private UUID mUuidSvcImAlert = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb"); private UUID mUuidChAlertLevel = UUID.fromString("00002a06-0000-1000-8000-00805f9b34fb"); private UUID mUuidSvcUser1 = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb"); private UUID mUuidChUser1 = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb"); // UUID for Client Characteristic Configuration Descriptor // - BLUETOOTH SPECIFICATION Version 4.2 [Vol 3, Part G] page 537 private UUID mUuidCCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); : private BluetoothGattCallback mGattCallback = new bleGattCallback(); : mBtGatt.discoverServices();(mCtx, false, mGattCallback); : private class bleGattCallback extends BluetoothGattCallback { @Override // GATT サービス一覧取得完了 public void onServicesDiscovered(BluetoothGatt gatt, int status) { super.onServicesDiscovered(gatt, status); // デバイス A の Immediate Alert サービスの // Alert Level キャラクタリスティックオブジェクトを取得 BluetoothGattService svc = gatt.getService(mUuidSvcImAlert); mChAlertLevel = svc.getCharacteristic(mUuidChAlertLevel); // デバイス A のユーザ定義サービス 1 の ユーザ定義キャラクタリスティックの // Client Characteristic Configulation Descriptor を取得 svc = gatt.getService(mUuidSvcUser1); mChUser1 = svc.getCharacteristic(mUuidChUser1); mDescUser1 = mChUser1.getDescriptor(mUuidCCCD); : }
5. 所定のキャラクタリスティックからの通知を有効に
- BluetoothGatt - setCharacteristicNotification - developer.android.com
boolean setCharacteristicNotification ( BluetoothGattCharacteristic characteristic, boolean enable) Enable or disable notifications/indications for a given characteristic. Once notifications are enabled for a characteristic, a onCharacteristicChanged(BluetoothGatt, BluetoothGattCharacteristic) callback will be triggered if the remote device indicates that the given characteristic has changed. Requires BLUETOOTH permission. Parameters characteristic BluetoothGattCharacteristic: The characteristic for which to enable notifications enable boolean: Set to true to enable notifications/ indications :
- BluetoothGattDescriptor - setValue - developer.android.com
boolean discoverServices () setValue boolean setValue (byte[] value) Updates the locally stored value of this descriptor. This function modifies the locally stored cached value of this descriptor. To send the value to the remote device, call writeDescriptor(BluetoothGattDescriptor) to send the value to the remote device. Parameters value byte: New value for this descriptor :
- BluetoothGatt - writeDescriptor - developer.android.com
writeDescriptor boolean writeDescriptor (BluetoothGattDescriptor descriptor) Write the value of a given descriptor to the associated remote device. A onDescriptorWrite(BluetoothGatt, BluetoothGattDescriptor, int) callback is triggered to report the result of the write operation. Requires BLUETOOTH permission. Parameters descriptor BluetoothGattDescriptor: Descriptor to write to the associated remote device :
- BluetoothGatt - writeDescriptor - developer.android.com
// デバイス A への Alert 指示用 private byte[] mCmdAlertOff = new byte[] {(byte)0x00}; // OFF (No Alert) private byte[] mCmdAlertOn = new byte[] {(byte)0x02}; // ON (High Alert) : // デバイス A のユーザ定義サービス 1 の ユーザ定義キャラクタリスティックの // Client Characteristic Configulation Descriptor を取得 svc = gatt.getService(mUuidSvcUser1); mChUser1 = svc.getCharacteristic(mUuidChUser1); mDescUser1 = mChUser1.getDescriptor(mUuidCCCD); // 同キャラクタリスティックの値変更時の通知を有功にして // 同 CCCD へ ENABLE_NOTIFICATION_VALUE を書き込んで通知へ待機 mBtGatt.setCharacteristicNotification(mChUser1, true); byte[] val = new byte[] {(byte)0x01, (byte)0x00}; mDescUser1.setValue(val); mBtGatt.writeDescriptor(mDescUser1); : private class bleGattCallback extends BluetoothGattCallback { @Override public void onCharacteristicChanged (BluetoothGatt gatt, BluetoothGattCharacteristic ch) { Log.d(TAG, "onCharacteristicChanged"); // デバイス A のユーザ定義キャラクタリスティック 1 からの通知を受信 if (ch == mChUser1) { Toast.makeText(mCtx, "* P U S H E D *", Toast.LENGTH_SHORT).show(); } } @Override public void onDescriptorWrite (BluetoothGatt gatt, BluetoothGattDescriptor desc, int status) { // writeDescriptor() 結果 super.onDescriptorWrite(gatt, desc, status); Log.d(TAG, "onDescriptorWrite: sts=" + status); if (desc == mDescUser1) { // デバイス A のユーザ定義サービス 1 の ユーザ定義キャラクタリスティックの // Client Characteristic Configulation Descriptor への書き込みが完了 } } :
6. 所定のキャラクタリスティックへの書き込み
- BluetoothGattCharacteristic - setValue - developer.android.com
setValue boolean setValue (byte[] value) Updates the locally stored value of this characteristic. This function modifies the locally stored cached value of this characteristic. To send the value to the remote device, call writeCharacteristic(BluetoothGattCharacteristic) to send the value to the remote device. Parameters value byte: New value for this characteristic :
- BluetoothGatt - writeCharacteristic - developer.android.com
boolean writeCharacteristic ( BluetoothGattCharacteristic characteristic) Writes a given characteristic and its values to the associated remote device. Once the write operation has been completed, the onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int) callback is invoked, reporting the result of the operation. Requires BLUETOOTH permission. Parameters characteristic BluetoothGattCharacteristic: Characteristic to write on the remote device
- BluetoothGatt - writeCharacteristic - developer.android.com
// デバイス A への Alert 指示用 private byte[] mCmdAlertOff = new byte[] {(byte)0x00}; // OFF (No Alert) private byte[] mCmdAlertOn = new byte[] {(byte)0x02}; // ON (High Alert) : mChAlertLevel.setValue(mCmdAlertOn); mBtGatt.writeCharacteristic(mChAlertLevel); : private class bleGattCallback extends BluetoothGattCallback { @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic ch, int status) { // writeCharacteristic 結果 super.onCharacteristicWrite(gatt, ch, status); if (ch == mChAlertLevel) { Log.d(TAG, "mChAlertLevel: onCharacteristicWrite: sts=" + status); } } :
(tanabe)