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

More than 1 year has 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.


参考