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:
Navideck Labs
2025-12-15 11:38:01 +05:30
committed by GitHub
parent 01b4511704
commit d26e752e73
3 changed files with 123 additions and 76 deletions
+101 -72
View File
@@ -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
(247517) 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 links 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.
+7 -1
View File
@@ -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);
+15 -3
View File
@@ -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, {