【Bluetooth Low Energy: BLE】iOS & Androidの最大データ転送量

BLEにおいて、1回の通信で送れるデータ量について調べた話です。

#はじめに

まずはBLEの仕様を確認してみます。[Bluetooth SIG: adopted specifications](https://www.bluetooth.com/specifications/adopted-specifications)からDLできる*The Core Specification*のATT_MTUあたりを読むと、1回の通信で送れるデータ量は、デフォルトでは**23octets**となっています。ATT_MTUとは、Attribute Maximum Transfer Unitの略で、パケットの最大サイズと思っていいでしょう。
実際には、3octetsは占有されているため、ユーザデータで使える容量は**20octets**となります。

20octetsとなると、少量のデータしか送れず、場合によっては厳しい制限となるかもしれません。

20octetsよりも大量のデータを送る場合、BLEには、2つの手段が用意されています。

1. 送信側で送信データをMTUに収まるサイズに分割し、ちょっとずつ送信する。受信側で受信データを結合して復元する(Read Blob Request, Prepare Write Request/Execute Write Request)
2. MTUを拡張し、1回で送れる量を増やす(Exchange MTU Request)

実際には、1と2の合わせ技で実装することもあるとは思いますが、この記事では、2のMTU拡張の方に焦点を絞っていきます。

#Exchange MTU Requestとは

サーバ・クライアント間で取り扱う単一のパケットの最大サイズ(ATT_MTU)を変更します。リクエストはクライアントサイド(Central)からサーバサイド(Peripheral)へします。
ATT_MTUの規格上の最大サイズは**512octets**ですが、機器が対応しているとは限りません。
もし、サーバのMTUとクライアントのMTUが異なる場合は、低い方の値が両者に設定されます。サーバとクライアントで設定されるMTUが異なることはありません。

#実装と検証

iOSとAndroidでExchange MTU Requestを実装し、設定できるMTUの最大値を確認します。なお、1octet=1byteとします。

まずAndroidですが、[公式リファレンスのBluetoothGattクラス](https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html)を見ると、`requestMtu`メソッドがあるので、どうやら実装できそうです。

次にiOSですが、[Bluetooth Accessory Design Guidelines for Apple Products](https://developer.apple.com/hardwaredrivers/BluetoothDesignGuidelines.pdf)の3.10 MTU Sizeや[WWDC 2013 Session 703 Core Bluetoothの資料](http://devstreaming.apple.com/videos/wwdc/2013/703xcx2xljtlq481grxurla4/703/703.pdf)に、Exchange MTUをサポートすると書かれています。しかし、[CoreBluetoothのリファレンス](https://developer.apple.com/library/ios/documentation/CoreBluetooth/Reference/CoreBluetooth_Framework/index.html)を見ても、それらしいメソッドは見つかりませんでした。`CBCentral`クラスにread-onlyな`maximumUpdateValueLength`プロパティがあるくらいです。うーん、どうやってやるんだろう?

##検証ケース

以下の4つのケースで検証します。

|Case|Central|Peripheral|
|:-:|:-:|:-:|
|1|iOS|iOS|
|2|Android|iOS|
|3|iOS|Android|
|4|Android|Android|

検証で使用した端末は以下の通りです。

- iPhone5, iOS7.1.1
- iPad Air2, iOS9.3.4
- iPhone6, iOS10 beta7 (**2016/08/26追記**)
- Xperia Z5 Compact SO-02H, Android 6.0
- Galaxy S4 SC-04E, Android 5.0.1

##ソースコード

iOS、Androidのコードの一部抜粋をそれぞれ記載します。MTUの値を確認したいだけなので、文字列データをReadするだけの単純なコードです。

iOSは、Peripheral側で取得できる`CBPeripheral`オブジェクトの`maximumUpdateValueLength`プロパティで確認します。ちなみに、このプロパティが返す値はユーザデータとして使える容量です。
Androidは、Centralから`requestMtu`メソッドを実行後に呼ばれる`onMtuChanged`コールバックメソッドで確認します。

**iOS(Central)**

```objc

static NSString * const YourCharacteristicUUIDString = /*UUID*/;

...

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
    for (CBCharacteristic *characteristic in service.characteristics) {

        // Read Request
        if ([characteristic.UUID.UUIDString isEqualToString:YourCharacteristicUUIDString]
            && (characteristic.properties & CBCharacteristicPropertyRead) != 0) {
            [peripheral readValueForCharacteristic:characteristic];
        }
    }
}

- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
             error:(NSError *)error {
    NSData *data = characteristic.value;
    NSString *val = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    
    NSLog(@"Update value=%@ data.length=%zdB", val, data.length);
}

...

```

**iOS(Peripheral)**

```objc

...

- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request {
    // Peripheral側のCBCentralオブジェクトでMTUを確認する
    NSLog(@"Received read request: MTU=%zd", request.central.maximumUpdateValueLength);
    
    // Read Response
    if ([request.characteristic.UUID isEqual:self.characteristic.UUID]) {
        request.value = [@"Hello" dataUsingEncoding:NSUTF8StringEncoding];
        [peripheral respondToRequest:request withResult:CBATTErrorSuccess];
    }
    else {
        [peripheral respondToRequest:request withResult:CBATTErrorRequestNotSupported];
    }
}

...

```

**Android(Central)**

```java

private static final String YOUR_SERVICE_UUID_STRING = /*UUID*/;
private static final String YOUR_CHARACTERISTIC_UUID_STRING = /*UUID*/;
private static final String TAG = "YourTag";

...

    private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            // 接続成功した場合
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                // Exchange MTU Request
                // 512Bまで拡張を要求
                if (gatt.requestMtu(512)) {
                    Log.d(TAG, "Requested MTU successfully");
                } else {
                    Log.d(TAG, "Failed to request MTU");
                }

                // MTUを拡張せずにサービスを検索する
//                if (gatt != null) {
//                    if (gatt.discoverServices()) {
//                       Log.d(TAG, "Started discovering services");
//                    } else {
//                       Log.d(TAG, "Failed to start discovering services");
//                    }
//                }
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                for (BluetoothGattService service : gatt.getServices()) {

                    if (!service.getUuid().toString().equalsIgnoreCase(YOUR_SERVICE_UUID_STRING)) {
                        continue;
                    }

                    // Read Request
                    BluetoothGattCharacteristic chara = service.getCharacteristic(UUID.fromString(YOUR_CHARACTERISTIC_UUID_STRING));
                    if (chara == null) {
                        continue;
                    }
                    if ((chara.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) != 0) {
                        gatt.readCharacteristic(chara);
                    }
                }
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            String val = characteristic.getStringValue(0);

            Log.d(TAG, "onCharacteristicRead: " + val);
        }

        @Override
        public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
            Log.d("onMtuChanged: " + mtu);

            // Exchange MTU Requestが完了してからサービスの検出を開始する
            if (gatt != null) {
                if (gatt.discoverServices()) {
                    Log.d(TAG, "Started discovering services");
                } else {
                    Log.d(TAG, "Failed to start discovering services");
                }
            }
        }
    };

...

```

**Android(Peripheral)**

```java

private static final String TAG = "YourTag";

...

   private final BluetoothGattServerCallback gattServerCallback = new BluetoothGattServerCallback() {
        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            characteristic.setValue("HelloWorld");
            gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, characteristic.getValue());
        }

        @Override
        public void onMtuChanged(BluetoothDevice device, int mtu) {
            Log.d(TAG, "onMtuChanged: " + mtu);
        }
    };

...

```

##検証結果

- iOS(Central) <------> iOS(Peripheral)

MTUは**135B**。特に何もしていない(というか何もできない)ので、iOS間は135Bに勝手に設定される?

- Android(Central) <------> iOS(Peripheral)

AndroidでMTU拡張リクエストをしない場合、MTUは**23B**。
AndroidでMTU拡張リクエストをした場合、MTUは**158B**まで拡張できました。

(**2016/08/26追記**)
iPhone6, iOS10 beta7をPeripheralにして試してみました。MTUは**512B**まで拡張できました。

- iOS(Central) <------> Android(Peripheral)

MTUは**135B**。Androidからは特にMTU拡張リクエストをしていませんが、`onMtuChanged`コールバックメソッドが呼ばれました。ということは、iOSから135BまでMTUを拡張するリクエストが勝手に送られている?

(**2016/08/26追記**)
iPhone6, iOS10 beta7をCentralで試したところ、**185B**まで拡張されました。iOS10が正式リリースされたら、他の端末でも確認したいと思います。

- Android(Central) <------> Android(Peripheral)

MTU拡張リクエストをした場合、MTUは**512B**まで拡張できました。

これらの結果を考察すると以下のことが言えそうです。

###検証結果から言えそうなこと

- iOSのMTU上限は**158B**と考えられる
    - (**2016/08/26追記**) iOS10では**512B**が上限と考えられる
- AndroidのMTU上限は**512B**と考えられる
- iOSはCentralの場合、接続時にシステムが自動で135BまでMTUを拡張するリクエストをしているのではないか
    - (**2016/08/26追記**) i0S10は185Bまで拡張をリクエストしているのではないか
- OSバージョンや端末によっては上限が異なる可能性はある

もう少し色々な端末で試してみたいところですが、今回の検証結果はこんな感じです。

#その他

- 検証で使用したGalaxy S4(Android 5.0.1)は、ペリフェラル端末としては利用できませんでした。

```
E/BluetoothAdapter: bluetooth le advertising not supported
```

こんなエラーログが出てしまいました。

- iOS10からは、Peripheralで利用する場合、Info.plistに`NSBluetoothPeripheralUsageDescription`キーを指定し、何のために使うのかを説明しなければいけないようです。指定しないと、以下のようなログが出て、アプリはクラッシュしてしまいます。

```
[access] This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSBluetoothPeripheralUsageDescription key with a string value explaining to the user how the app uses this data.
```

#参考

- [Reinforce-Lab.'s Blog: BLEの通信仕様](http://reinforce-lab.github.io/blog/2013/08/13/blebook-ch2-ble-spec/)
- [iOSのCore Bluetooth / BLEの通信速度](http://qiita.com/shu223/items/352a84a60be75d656ae3)
- [Android Developers: Bluetooth Low Energy](https://developer.android.com/guide/topics/connectivity/bluetooth-le.html)
- iOS ✕ BLE Core Bluetoothプログラミング (書籍)