Update README and BLE API documentation for MTU handling (#202)
- Clarified MTU request behavior and platform limitations in README.md. - Added best practices for MTU management in cross-platform BLE applications. - Enhanced documentation for `requestMtu` method in UniversalBle and BleDeviceExtension to reflect best-effort nature of MTU requests and platform-specific behaviors. Co-authored-by: Foti Dim <foti@navideck.com>
This commit is contained in:
@@ -405,88 +405,56 @@ UniversalBle.disableBluetooth();
|
||||
|
||||
```dart
|
||||
int mtu = await bleDevice.requestMtu(256);
|
||||
```
|
||||
````
|
||||
|
||||
> ⚠️ Note: Requesting an MTU is a *best-effort* operation.
|
||||
> On many platforms the final MTU is fully controlled by the OS and remote device.
|
||||
|
||||
#### Platform Limitations
|
||||
|
||||
On most platforms, the MTU can only be queried but not manually set:
|
||||
MTU negotiation is largely platform- and stack-managed, and often cannot be
|
||||
explicitly controlled by applications:
|
||||
|
||||
- **iOS/macOS**: System automatically sets MTU to 185 bytes maximum
|
||||
- **Android 14+**: System automatically sets MTU to 517 bytes for the first GATT client
|
||||
- **Windows**: MTU can only be queried
|
||||
- **Linux**: MTU can only be queried
|
||||
- **Web**: No mechanism to query or modify MTU size
|
||||
* **iOS / macOS**
|
||||
|
||||
* MTU is fully OS-managed; apps cannot request or set it.
|
||||
* Historically ~185 bytes, but modern devices may negotiate larger MTUs
|
||||
(≈247–517) automatically.
|
||||
|
||||
* **Android**
|
||||
|
||||
* **Android ≤ 13**: Apps may request MTU once per connection (up to 517).
|
||||
If never requested, the default MTU is 23.
|
||||
* **Android 14+**: The first GATT client effectively drives MTU negotiation
|
||||
to 517 (or the link’s maximum); subsequent MTU requests are ignored.
|
||||
|
||||
* **Windows**
|
||||
|
||||
* MTU is automatically negotiated by the OS.
|
||||
* Apps cannot set it; they can only query the effective PDU size.
|
||||
|
||||
* **Linux (BlueZ)**
|
||||
|
||||
* MTU is negotiated automatically by default.
|
||||
* The standard D-Bus GATT API does not expose MTU control.
|
||||
* MTU can be requested via BlueZ tools or lower-level APIs, but most apps
|
||||
treat it as stack-defined.
|
||||
|
||||
* **Web**
|
||||
|
||||
* MTU is negotiated internally by the browser/OS.
|
||||
* No API exists to query or modify the MTU size.
|
||||
|
||||
#### Best Practices
|
||||
|
||||
When developing cross-platform BLE applications and devices:
|
||||
|
||||
- Design for default MTU size (23 bytes) as default
|
||||
- Dynamically adapt to use larger packet sizes when the system provides them
|
||||
- Take advantage of the increased throughput when available without requiring it
|
||||
- Implement data fragmentation for larger transfers
|
||||
- Handle platform-specific MTU size based on current value
|
||||
* Always design for the default ATT MTU (23 bytes)
|
||||
* Treat MTU requests as opportunistic, not guaranteed
|
||||
* Dynamically adapt packet sizes based on the negotiated MTU
|
||||
* Implement application-level fragmentation for larger payloads
|
||||
* Take advantage of higher MTUs when available, without depending on them
|
||||
|
||||
#### Resetting State on Hot Restart
|
||||
|
||||
During Flutter hot restart in debug mode, the app state is reset but native Bluetooth connections and scan operations may persist. This can lead to connection issues or stale state.
|
||||
|
||||
<details>
|
||||
<summary>Use the following helper function to properly clean up BLE state before your app restarts.</summary>
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// Reset BLE state before app initialization
|
||||
await resetBleState();
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
/// Resets BLE state by stopping scans and disconnecting all devices.
|
||||
/// Make sure you have Bluetooth permissions before calling this function.
|
||||
Future<void> resetBleState() async {
|
||||
// Skip reset in release mode or on web
|
||||
if (!kDebugMode || kIsWeb) return;
|
||||
|
||||
// Check Bluetooth availability
|
||||
AvailabilityState availabilityState =
|
||||
await UniversalBle.getBluetoothAvailabilityState();
|
||||
|
||||
// Skip if Bluetooth is not powered on
|
||||
if (availabilityState != AvailabilityState.poweredOn) {
|
||||
debugPrint('Reset: Bluetooth is not powered on');
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop scanning
|
||||
if (await UniversalBle.isScanning()) {
|
||||
debugPrint('Reset: Stopping scan');
|
||||
await UniversalBle.stopScan();
|
||||
}
|
||||
|
||||
// Disconnect all connected devices
|
||||
List<String> withServices = [];
|
||||
|
||||
// On Apple platforms, you must specify services to discover connected devices
|
||||
if (defaultTargetPlatform == TargetPlatform.macOS ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS) {
|
||||
// Replace with your known device service UUIDs
|
||||
withServices = ["0x180A"];
|
||||
}
|
||||
|
||||
List<BleDevice> connectedDevices =
|
||||
await UniversalBle.getSystemDevices(withServices: withServices);
|
||||
|
||||
for (var device in connectedDevices) {
|
||||
debugPrint('Reset: Disconnecting device: ${device.deviceId}');
|
||||
await UniversalBle.disconnect(device.deviceId);
|
||||
}
|
||||
|
||||
debugPrint('Reset: Done');
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Command Queue
|
||||
|
||||
@@ -770,6 +738,67 @@ void main() async {
|
||||
}
|
||||
```
|
||||
|
||||
## Resetting State on Hot Restart
|
||||
|
||||
During Flutter hot restart in debug mode, the app state is reset but native Bluetooth connections and scan operations may persist. This can lead to connection issues or stale state.
|
||||
|
||||
<details>
|
||||
<summary>Use the following helper function to properly clean up BLE state before your app restarts.</summary>
|
||||
|
||||
```dart
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// Reset BLE state before app initialization
|
||||
await resetBleState();
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
/// Resets BLE state by stopping scans and disconnecting all devices.
|
||||
/// Make sure you have Bluetooth permissions before calling this function.
|
||||
Future<void> resetBleState() async {
|
||||
// Skip reset in release mode or on web
|
||||
if (!kDebugMode || kIsWeb) return;
|
||||
|
||||
// Check Bluetooth availability
|
||||
AvailabilityState availabilityState =
|
||||
await UniversalBle.getBluetoothAvailabilityState();
|
||||
|
||||
// Skip if Bluetooth is not powered on
|
||||
if (availabilityState != AvailabilityState.poweredOn) {
|
||||
debugPrint('Reset: Bluetooth is not powered on');
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop scanning
|
||||
if (await UniversalBle.isScanning()) {
|
||||
debugPrint('Reset: Stopping scan');
|
||||
await UniversalBle.stopScan();
|
||||
}
|
||||
|
||||
// Disconnect all connected devices
|
||||
List<String> withServices = [];
|
||||
|
||||
// On Apple platforms, you must specify services to discover connected devices
|
||||
if (defaultTargetPlatform == TargetPlatform.macOS ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS) {
|
||||
// Replace with your known device service UUIDs
|
||||
withServices = ["0x180A"];
|
||||
}
|
||||
|
||||
List<BleDevice> connectedDevices =
|
||||
await UniversalBle.getSystemDevices(withServices: withServices);
|
||||
|
||||
for (var device in connectedDevices) {
|
||||
debugPrint('Reset: Disconnecting device: ${device.deviceId}');
|
||||
await UniversalBle.disconnect(device.deviceId);
|
||||
}
|
||||
|
||||
debugPrint('Reset: Done');
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Low level API
|
||||
|
||||
For more granular control, you can use the [Low-Level API](README.low_level.md). This API is "Device ID"-based, offering greater flexibility by enabling direct calls without the need for object instances.
|
||||
|
||||
@@ -22,7 +22,13 @@ extension BleDeviceExtension on BleDevice {
|
||||
/// Disconnects from the device.
|
||||
Future<void> disconnect() => UniversalBle.disconnect(deviceId);
|
||||
|
||||
/// Requests a specific MTU (Maximum Transmission Unit) size for the connection.
|
||||
/// Requests an MTU (Maximum Transmission Unit) value for the connection.
|
||||
///
|
||||
/// **⚠️ Note:** Requesting an MTU is a *best-effort* operation. The final MTU is
|
||||
/// often controlled by the OS and remote device. Returns the negotiated MTU value,
|
||||
/// which may differ from `expectedMtu`.
|
||||
///
|
||||
/// See [UniversalBle.requestMtu] for platform limitations and best practices.
|
||||
Future<int> requestMtu(int expectedMtu) =>
|
||||
UniversalBle.requestMtu(deviceId, expectedMtu);
|
||||
|
||||
|
||||
@@ -313,9 +313,21 @@ class UniversalBle {
|
||||
);
|
||||
}
|
||||
|
||||
/// Request MTU value.
|
||||
/// It will **attempt** to set the MTU (Maximum Transmission Unit) but it is not guaranteed to succeed due to platform limitations.
|
||||
/// It will always return the current MTU.
|
||||
/// Requests an MTU (Maximum Transmission Unit) value for the connection.
|
||||
///
|
||||
/// **⚠️ Note:** Requesting an MTU is a *best-effort* operation. On many platforms
|
||||
/// the final MTU is fully controlled by the OS and remote device. This method
|
||||
/// returns the current/negotiated MTU value, which may differ from `expectedMtu`.
|
||||
///
|
||||
/// **Platform Limitations:**
|
||||
/// * **iOS/macOS**: MTU is OS-managed; apps cannot request it (~185-517 bytes auto-negotiated)
|
||||
/// * **Android ≤13**: May request once per connection (up to 517), default is 23
|
||||
/// * **Android 14+**: First GATT client drives MTU to 517; subsequent requests ignored
|
||||
/// * **Windows/Linux**: MTU is automatically negotiated; apps can only query it
|
||||
/// * **Web**: Not supported (no API available)
|
||||
///
|
||||
/// **Best Practices:** Design for default ATT MTU (23 bytes), treat requests as
|
||||
/// opportunistic, and implement fragmentation for larger payloads.
|
||||
static Future<int> requestMtu(
|
||||
String deviceId,
|
||||
int expectedMtu, {
|
||||
|
||||
Reference in New Issue
Block a user