From d26e752e730155fe89a5bc888658b3e36d1700c8 Mon Sep 17 00:00:00 2001 From: Navideck Labs <130186950+navidecklabs@users.noreply.github.com> Date: Mon, 15 Dec 2025 11:38:01 +0530 Subject: [PATCH] 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 --- README.md | 173 +++++++++++-------- lib/src/extensions/ble_device_extension.dart | 8 +- lib/src/universal_ble.dart | 18 +- 3 files changed, 123 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index c0240c5..5e84cd8 100644 --- a/README.md +++ b/README.md @@ -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. - -
-Use the following helper function to properly clean up BLE state before your app restarts. - -```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 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 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 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'); -} -``` - -
## 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. + +
+Use the following helper function to properly clean up BLE state before your app restarts. + +```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 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 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 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'); +} +``` + +
+ ## 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. diff --git a/lib/src/extensions/ble_device_extension.dart b/lib/src/extensions/ble_device_extension.dart index 55ca002..753b721 100644 --- a/lib/src/extensions/ble_device_extension.dart +++ b/lib/src/extensions/ble_device_extension.dart @@ -22,7 +22,13 @@ extension BleDeviceExtension on BleDevice { /// Disconnects from the device. Future 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 requestMtu(int expectedMtu) => UniversalBle.requestMtu(deviceId, expectedMtu); diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index 05c614d..f485841 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -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 requestMtu( String deviceId, int expectedMtu, {