Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

はじめに

まずはBLEの仕様を確認してみます。Bluetooth SIG: 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クラスを見ると、requestMtuメソッドがあるので、どうやら実装できそうです。

次にiOSですが、Bluetooth Accessory Design Guidelines for Apple Productsの3.10 MTU SizeやWWDC 2013 Session 703 Core Bluetoothの資料に、Exchange MTUをサポートすると書かれています。しかし、CoreBluetoothのリファレンスを見ても、それらしいメソッドは見つかりませんでした。CBCentralクラスにread-onlyなmaximumUpdateValueLengthプロパティがあるくらいです。うーん、どうやってやるんだろう?

検証ケース

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

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

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

  • 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)

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)

...

- (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)

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)

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.

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした