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つの手段が用意されています。
- 送信側で送信データをMTUに収まるサイズに分割し、ちょっとずつ送信する。受信側で受信データを結合して復元する(Read Blob Request, Prepare Write Request/Execute Write Request)
- 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つのケースで検証します。
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)
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.
#参考
- Reinforce-Lab.'s Blog: BLEの通信仕様
- iOSのCore Bluetooth / BLEの通信速度
- Android Developers: Bluetooth Low Energy
- iOS ✕ BLE Core Bluetoothプログラミング (書籍)