Connecting BLE Devices with Flutter (Part 4) – Advanced BLE Operations

Introduction

Building on our previous discussion in Part 3 of this series, where we established the basics of BLE communication using Flutter, this blog post will dive deeper into advanced BLE operations. We’ll explore managing connections with robust reconnection strategies, handling connection timeouts, and ensuring a stable communication flow with BLE devices using the flutter_blue_plus package.

Learn more about Mobile App Development Services

Managing BLE Connections

Maintaining a stable connection with a BLE device is critical for ensuring seamless interaction. It’s not just about connecting; it’s about ensuring that the connection stays alive and is responsive to both the app and the device’s needs.

Reconnecting After Disconnection:

One of the most important aspects of BLE communication is handling unexpected disconnections. BLE devices can disconnect for various reasons – low battery, going out of range, or interference. A robust BLE application should automatically attempt to reconnect when this happens.

Here’s how to implement automatic reconnection:

class AdvancedBLEConnectionManager {

  static final Map<String, int> _reconnectionAttempts = {};

  static const int _maxReconnectionAttempts = 3;

  static const Duration _reconnectionDelay = Duration(seconds: 5);

 

  static Future<bool> connectWithReconnection(BluetoothDevice device) async {

    try {

      // Connect to device

      await device.connect();

     

      // Setup connection monitoring for reconnection

      _setupConnectionMonitoring(device);

     

      // Reset reconnection attempts on successful connection

      _reconnectionAttempts[device.remoteId.toString()] = 0;

     

      return true;

    } catch (e) {

      print(‘Connection failed: $e’);

      return false;

    }

  }

 

  static void _setupConnectionMonitoring(BluetoothDevice device) {

    device.connectionState.listen((state) {

      if (state == BluetoothConnectionState.disconnected) {

        print(‘Device disconnected – attempting reconnection’);

        _handleDisconnection(device);

      } else if (state == BluetoothConnectionState.connected) {

        print(‘Device reconnected successfully’);

        _reconnectionAttempts[device.remoteId.toString()] = 0;

      }

    });

  }

 

  static void _handleDisconnection(BluetoothDevice device) {

    final deviceId = device.remoteId.toString();

    final attempts = _reconnectionAttempts[deviceId] ?? 0;

   

    if (attempts < _maxReconnectionAttempts) {

      print(‘Attempting reconnection ${attempts + 1}/$_maxReconnectionAttempts’);

     

      Timer(_reconnectionDelay, () async {

        await _attemptReconnection(device);

      });

    } else {

      print(‘Max reconnection attempts reached’);

    }

  }

 

  static Future<void> _attemptReconnection(BluetoothDevice device) async {

    final deviceId = device.remoteId.toString();

    _reconnectionAttempts[deviceId] = (_reconnectionAttempts[deviceId] ?? 0) + 1;

   

    try {

      await device.connect();

      print(‘Reconnection successful’);

    } catch (e) {

      print(‘Reconnection failed: $e’);

      _handleDisconnection(device); // Try again

    }

  }

}

This snippet listens for changes in the device connection state. If the device gets disconnected, it automatically attempts to reconnect up to a maximum number of times with a delay between attempts.

Handling Connection Timeouts:

Connection timeouts are essential for preventing your app from hanging indefinitely when trying to connect to a device that may be unavailable.

static Future<bool> connectWithTimeout(BluetoothDevice device, Duration timeout) async {

  try {

    await device.connect(timeout: timeout);

    return true;

  } on TimeoutException {

    print(‘Connection timeout for ${device.advName}’);

    return false;

  } catch (e) {

    print(‘Connection failed: $e’);

    return false;

  }

}

The connect method can accept a timeout parameter. This ensures that if the device doesn’t connect within the specified time frame, the app can handle the situation gracefully.

Handling Notifications

In BLE, notifications are a way for a device to inform the app about changes to a characteristic without the app having to read it periodically. Advanced notification handling ensures robust and efficient data communication.

Subscribing to Notifications:

Enhanced notification subscription with proper error handling:

static Future<bool> subscribeToNotifications(

BluetoothDevice device,

BluetoothCharacteristic characteristic,

Function(List<int>) onDataReceived,

) async {

try {

if (!characteristic.properties.notify && !characteristic.properties.indicate) {

throw Exception(‘Characteristic does not support notifications’);

}

 

// Subscribe to notifications

final subscription = characteristic.onValueReceived.listen((value) {

print(‘Notification received: $value’);

onDataReceived(value);

});

 

// Cleanup subscription when device disconnects

device.cancelWhenDisconnected(subscription);

 

// Enable notifications

await characteristic.setNotifyValue(true);

print(‘Successfully subscribed to notifications’);

return true;

} catch (e) {

print(‘Error subscribing to notifications: $e’);

return false;

}

}

Here we subscribe to notifications from a characteristic. When the characteristic value changes, the app gets notified and can handle the new data accordingly. The subscription is automatically cancelled when the device disconnects.

Unsubscribing from Notifications:

When notifications are no longer needed, it’s important to unsubscribe to save power and reduce unnecessary data transmission.

static Future<void> unsubscribeFromNotifications(

BluetoothDevice device,

BluetoothCharacteristic characteristic,

) async {

try {

// Disable notifications

await characteristic.setNotifyValue(false);

print(‘Successfully unsubscribed from notifications’);

} catch (e) {

print(‘Error unsubscribing from notifications: $e’);

}

}

Reading and Writing Characteristics with Error Handling

While we covered the basic read and write operations in Part 3, handling potential errors is crucial for robust applications.

Reading Characteristics with Try-Catch:

static Future<List<int>> readCharacteristicWithErrorHandling(

  BluetoothCharacteristic characteristic

) async {

  try {

    if (!characteristic.properties.read) {

      throw Exception(‘Characteristic is not readable’);

    }

   

    List<int> value = await characteristic.read();

    print(‘Successfully read: $value’);

    return value;

  } catch (e) {

    print(‘Error reading characteristic: $e’);

    return [];

  }

}

A try-catch block ensures that any issues during the read operation are caught and managed without crashing the app.

Writing Characteristics with Acknowledgment:

static Future<bool> writeCharacteristicWithAcknowledgment(

  BluetoothCharacteristic characteristic,

  List<int> value

) async {

  try {

    if (!characteristic.properties.write && !characteristic.properties.writeWithoutResponse) {

      throw Exception(‘Characteristic is not writable’);

    }

    // Write with response (acknowledgment)

    await characteristic.write(value, withoutResponse: false);

    print(‘Successfully wrote with acknowledgment: $value’);

    return true;

  } catch (e) {

    print(‘Error writing characteristic: $e’);

    return false;

  }

}

The withoutResponse flag set to false means the app waits for an acknowledgment from the device that the write operation was successful.

Complete Example Implementation

Here’s how to integrate all these advanced features into a complete BLE management system:

class EnhancedBLEManager {

  static Future<void> setupAdvancedConnection(BluetoothDevice device) async {

    // Connect with reconnection management

    final connected = await AdvancedBLEConnectionManager.connectWithReconnection(device);

   

    if (connected) {

      // Discover services

      final services = await device.discoverServices();

     

      // Setup notifications for all notifiable characteristics

      for (final service in services) {

        for (final characteristic in service.characteristics) {

          if (characteristic.properties.notify || characteristic.properties.indicate) {

            await subscribeToNotifications(device, characteristic, (data) {

              print(‘Received notification: $data’);

              // Handle notification data

            });

          }

        }

      }

    }

  }

 

  static Future<void> performDataOperations(BluetoothDevice device) async {

    try {

      final services = await device.discoverServices();

     

      for (final service in services) {

        for (final characteristic in service.characteristics) {

          // Read from readable characteristics

          if (characteristic.properties.read) {

            final data = await readCharacteristicWithErrorHandling(characteristic);

            print(‘Read data: $data’);

          }

         

          // Write to writable characteristics

          if (characteristic.properties.write) {

            final success = await writeCharacteristicWithAcknowledgment(

              characteristic,

              [0x01, 0x02, 0x03]

            );

            print(‘Write success: $success’);

          }

        }

      }

    } catch (e) {

      print(‘Error performing data operations: $e’);

    }

  }

}

Best Practices for Advanced BLE Operations

1. Always Handle Disconnections Gracefully:

device.connectionState.listen((state) {

switch (state) {

case BluetoothConnectionState.connected:

print(‘Connected – ready for operations’);

break;

case BluetoothConnectionState.disconnected:

print(‘Disconnected – cleaning up resources’);

// Cleanup subscriptions and attempt reconnection

break;

default:

print(‘Connection state: $state’);

}

});

2. Use Proper Error Handling:

Future<bool> safeOperation(Function operation) async {

  try {

    await operation();

    return true;

  } on TimeoutException {

    print(‘Operation timed out’);

    return false;

  } on PlatformException catch (e) {

    print(‘Platform error: ${e.message}’);

    return false;

  } catch (e) {

    print(‘Unexpected error: $e’);

    return false;

  }

}

3. Clean Up Resources:

static void cleanupResources() {

  // Cancel all subscriptions

  for (var subscription in subscriptions) {

    subscription.cancel();

  }

 

  // Clear reconnection timers

  for (var timer in reconnectionTimers) {

    timer.cancel();

  }

 

  print(‘All resources cleaned up’);

}

Want a Seamless BLE Implementation for Flutter Apps?

Delve deeper into connecting BLE devices with Flutter. If you are ready to implement these insights in your app, connect with AlphaBOLD for custom development expert guidance and seamless BLE implementation.

Request a consultation

Conclusion

In this installment of our series, we have taken our BLE operations to the next level, integrating advanced techniques for managing connections and handling notifications. These practices are essential for creating a stable and reliable communication channel between your Flutter app and BLE devices.

Key improvements covered in this part include:

  • Automatic Reconnection: Robust reconnection strategies with retry limits and delays
  • Connection Timeout Handling: Preventing indefinite connection attempts
  • Enhanced Notification Management: Proper subscription and unsubscription with error handling
  • Error Handling: Comprehensive try-catch blocks for all BLE operations
  • Write Acknowledgments: Ensuring write operations are confirmed by the device

As always, we encourage you to experiment with these snippets and integrate them into your application logic. In our next post, we will look at troubleshooting common BLE issues and optimizing the BLE communication for better performance and user experience.

Stay tuned, and keep coding! If you have any questions or topics you would like us to cover, feel free to reach out. Your feedback drives our content. Thank you for following along in our BLE with Flutter series!

Explore Recent Blog Posts