Skip to content

Commit

Permalink
MTU (Maximum Transmission Unit) (pauldemarco#363)
Browse files Browse the repository at this point in the history
* Update protobuf

* Add MtuSizeRequest MtuSizeResponse proto messages

* Add BluetoothDeviceCache. Implement requestMtu and mtu. (Android)

The BluetoothDeviceCache class is used to augment the underlying
Bluetooth API cache for missing information, such as mtu size.

* Add quick ListTile to test MTU features.

* Add requestMtu and mtu to BluetoothDevice.

* Doc updates. Run dartfmt on example.

* Update readme.

* Add mtu and requestMtu to iOS.

* Add comment about which type to use for iOS mtu size.

* Update generated iOS project file.
  • Loading branch information
pauldemarco authored Sep 11, 2019
1 parent f3c8b0b commit 6e4410c
Show file tree
Hide file tree
Showing 15 changed files with 639 additions and 212 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,20 @@ characteristic.value.listen((value) {
});
```

### Read the MTU and request larger size
```dart
final mtu = await device.mtu.first;
await device.requestMtu(512);
```

## Reference
### FlutterBlue API
| | Android | iOS | Description |
| :--------------- | :----------------: | :------------------: | :-------------------------------- |
| scan | :white_check_mark: | :white_check_mark: | Starts a scan for Bluetooth Low Energy devices. |
| state | :white_check_mark: | :white_check_mark: | Gets the current state of the Bluetooth Adapter. |
| onStateChanged | :white_check_mark: | :white_check_mark: | Stream of state changes for the Bluetooth Adapter. |
| state | :white_check_mark: | :white_check_mark: | Stream of state changes for the Bluetooth Adapter. |
| isAvailable | :white_check_mark: | :white_check_mark: | Checks whether the device supports Bluetooth. |
| isOn | :white_check_mark: | :white_check_mark: | Checks if Bluetooth functionality is turned on. |

### BluetoothDevice API
| | Android | iOS | Description |
Expand All @@ -110,8 +117,9 @@ characteristic.value.listen((value) {
| disconnect | :white_check_mark: | :white_check_mark: | Cancels an active or pending connection to the device. |
| discoverServices | :white_check_mark: | :white_check_mark: | Discovers services offered by the remote device as well as their characteristics and descriptors. |
| services | :white_check_mark: | :white_check_mark: | Gets a list of services. Requires that discoverServices() has completed. |
| state | :white_check_mark: | :white_check_mark: | Gets the current state of the device. |
| onStateChanged | :white_check_mark: | :white_check_mark: | Notifies of state changes for the device. |
| state | :white_check_mark: | :white_check_mark: | Stream of state changes for the Bluetooth Device. |
| mtu | :white_check_mark: | | Stream of mtu size changes. |
| requestMtu | :white_check_mark: | | Request to change the MTU for the device. |

### BluetoothCharacteristic API
| | Android | iOS | Description |
Expand Down
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ buildscript {

dependencies {
classpath 'com.android.tools.build:gradle:3.3.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
}
}

Expand Down Expand Up @@ -66,7 +66,7 @@ protobuf {
// Configure the protoc executable
protoc {
// Download from repositories
artifact = 'com.google.protobuf:protoc:3.7.0'
artifact = 'com.google.protobuf:protoc:3.9.1'
}
plugins {
javalite {
Expand Down
135 changes: 103 additions & 32 deletions android/src/main/java/com/pauldemarco/flutterblue/FlutterBluePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
Expand Down Expand Up @@ -66,7 +67,7 @@ public class FlutterBluePlugin implements MethodCallHandler, RequestPermissionsR
private final EventChannel stateChannel;
private final BluetoothManager mBluetoothManager;
private BluetoothAdapter mBluetoothAdapter;
private final Map<String, BluetoothGatt> mGattServers = new HashMap<>();
private final Map<String, BluetoothDeviceCache> mDevices = new HashMap<>();
private LogLevel logLevel = LogLevel.EMERGENCY;

// Pending call and result for startScan, in the case where permissions are needed
Expand Down Expand Up @@ -181,7 +182,7 @@ public void onMethodCall(MethodCall call, Result result) {
p.addDevices(ProtoMaker.from(d));
}
result.success(p.build().toByteArray());
log(LogLevel.EMERGENCY, "mGattServers size: " + mGattServers.size());
log(LogLevel.EMERGENCY, "mDevices size: " + mDevices.size());
break;
}

Expand All @@ -200,14 +201,14 @@ public void onMethodCall(MethodCall call, Result result) {
boolean isConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT).contains(device);

// If device is already connected, return error
if(mGattServers.containsKey(deviceId) && isConnected) {
if(mDevices.containsKey(deviceId) && isConnected) {
result.error("already_connected", "connection with device already exists", null);
return;
}

// If device was connected to previously but is now disconnected, attempt a reconnect
if(mGattServers.containsKey(deviceId) && !isConnected) {
if(mGattServers.get(deviceId).connect()){
if(mDevices.containsKey(deviceId) && !isConnected) {
if(mDevices.get(deviceId).gatt.connect()){
result.success(null);
} else {
result.error("reconnect_error", "error when reconnecting to device", null);
Expand All @@ -222,7 +223,7 @@ public void onMethodCall(MethodCall call, Result result) {
} else {
gattServer = device.connectGatt(activity, options.getAndroidAutoConnect(), mGattCallback);
}
mGattServers.put(deviceId, gattServer);
mDevices.put(deviceId, new BluetoothDeviceCache(gattServer));
result.success(null);
break;
}
Expand All @@ -232,8 +233,9 @@ public void onMethodCall(MethodCall call, Result result) {
String deviceId = (String)call.arguments;
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceId);
int state = mBluetoothManager.getConnectionState(device, BluetoothProfile.GATT);
BluetoothGatt gattServer = mGattServers.remove(deviceId);
if(gattServer != null) {
BluetoothDeviceCache cache = mDevices.remove(deviceId);
if(cache != null) {
BluetoothGatt gattServer = cache.gatt;
gattServer.disconnect();
if(state == BluetoothProfile.STATE_DISCONNECTED) {
gattServer.close();
Expand All @@ -251,41 +253,41 @@ public void onMethodCall(MethodCall call, Result result) {
try {
result.success(ProtoMaker.from(device, state).toByteArray());
} catch(Exception e) {
result.error("device_state_error", e.getMessage(), null);
result.error("device_state_error", e.getMessage(), e);
}
break;
}

case "discoverServices":
{
String deviceId = (String)call.arguments;
BluetoothGatt gattServer = mGattServers.get(deviceId);
if(gattServer == null) {
result.error("discover_services_error", "no instance of BluetoothGatt, have you connected first?", null);
return;
}
if(gattServer.discoverServices()) {
result.success(null);
} else {
result.error("discover_services_error", "unknown reason", null);
try {
BluetoothGatt gatt = locateGatt(deviceId);
if(gatt.discoverServices()) {
result.success(null);
} else {
result.error("discover_services_error", "unknown reason", null);
}
} catch(Exception e) {
result.error("discover_services_error", e.getMessage(), e);
}
break;
}

case "services":
{
String deviceId = (String)call.arguments;
BluetoothGatt gattServer = mGattServers.get(deviceId);
if(gattServer == null) {
result.error("get_services_error", "no instance of BluetoothGatt, have you connected first?", null);
return;
}
Protos.DiscoverServicesResult.Builder p = Protos.DiscoverServicesResult.newBuilder();
p.setRemoteId(deviceId);
for(BluetoothGattService s : gattServer.getServices()){
p.addServices(ProtoMaker.from(gattServer.getDevice(), s, gattServer));
try {
BluetoothGatt gatt = locateGatt(deviceId);
Protos.DiscoverServicesResult.Builder p = Protos.DiscoverServicesResult.newBuilder();
p.setRemoteId(deviceId);
for(BluetoothGattService s : gatt.getServices()){
p.addServices(ProtoMaker.from(gatt.getDevice(), s, gatt));
}
result.success(p.build().toByteArray());
} catch(Exception e) {
result.error("get_services_error", e.getMessage(), e);
}
result.success(p.build().toByteArray());
break;
}

Expand Down Expand Up @@ -492,6 +494,52 @@ public void onMethodCall(MethodCall call, Result result) {
break;
}

case "mtu":
{
String deviceId = (String)call.arguments;
BluetoothDeviceCache cache = mDevices.get(deviceId);
if(cache != null) {
Protos.MtuSizeResponse.Builder p = Protos.MtuSizeResponse.newBuilder();
p.setRemoteId(deviceId);
p.setMtu(cache.mtu);
result.success(p.build().toByteArray());
} else {
result.error("mtu", "no instance of BluetoothGatt, have you connected first?", null);
}
break;
}

case "requestMtu":
{
byte[] data = call.arguments();
Protos.MtuSizeRequest request;
try {
request = Protos.MtuSizeRequest.newBuilder().mergeFrom(data).build();
} catch (InvalidProtocolBufferException e) {
result.error("RuntimeException", e.getMessage(), e);
break;
}

BluetoothGatt gatt;
try {
gatt = locateGatt(request.getRemoteId());
int mtu = request.getMtu();
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if(gatt.requestMtu(mtu)) {
result.success(null);
} else {
result.error("requestMtu", "gatt.requestMtu returned false", null);
}
} else {
result.error("requestMtu", "Only supported on devices >= API 21 (Lollipop). This device == " + Build.VERSION.SDK_INT, null);
}
} catch(Exception e) {
result.error("requestMtu", e.getMessage(), e);
}

break;
}

default:
{
result.notImplemented();
Expand All @@ -518,11 +566,12 @@ public boolean onRequestPermissionsResult(
}

private BluetoothGatt locateGatt(String remoteId) throws Exception {
BluetoothGatt gattServer = mGattServers.get(remoteId);
if(gattServer == null) {
BluetoothDeviceCache cache = mDevices.get(remoteId);
if(cache == null || cache.gatt == null) {
throw new Exception("no instance of BluetoothGatt, have you connected first?");
} else {
return cache.gatt;
}
return gattServer;
}

private BluetoothGattCharacteristic locateCharacteristic(BluetoothGatt gattServer, String serviceId, String secondaryServiceId, String characteristicId) throws Exception {
Expand Down Expand Up @@ -710,7 +759,7 @@ private void stopScan18() {
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
log(LogLevel.DEBUG, "[onConnectionStateChange] status: " + status + " newState: " + newState);
if(newState == BluetoothProfile.STATE_DISCONNECTED) {
if(!mGattServers.containsKey(gatt.getDevice().getAddress())) {
if(!mDevices.containsKey(gatt.getDevice().getAddress())) {
gatt.close();
}
}
Expand Down Expand Up @@ -822,6 +871,16 @@ public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
log(LogLevel.DEBUG, "[onMtuChanged] mtu: " + mtu + " status: " + status);
if(status == BluetoothGatt.GATT_SUCCESS) {
if(mDevices.containsKey(gatt.getDevice().getAddress())) {
BluetoothDeviceCache cache = mDevices.get(gatt.getDevice().getAddress());
cache.mtu = mtu;
Protos.MtuSizeResponse.Builder p = Protos.MtuSizeResponse.newBuilder();
p.setRemoteId(gatt.getDevice().getAddress());
p.setMtu(mtu);
invokeMethodUIThread("MtuSize", p.build().toByteArray());
}
}
}
};

Expand All @@ -847,4 +906,16 @@ public void run() {
});
}

// BluetoothDeviceCache contains any other cached information not stored in Android Bluetooth API
// but still needed Dart side.
class BluetoothDeviceCache {
final BluetoothGatt gatt;
int mtu;

BluetoothDeviceCache(BluetoothGatt gatt) {
this.gatt = gatt;
mtu = 20;
}
}

}
Loading

0 comments on commit 6e4410c

Please sign in to comment.