Introduction
Bluetooth Low Energy (BLE) is a technology that enables wireless communication between devices. BLE is commonly used in IoT devices and other wearable technologies, and it is supported by most modern smartphones. If you’re building a Flutter app that needs to communicate with BLE devices, the flutter_blue_plus package is a great resource. This package makes it easy to connect to, discover, and interact with nearby BLE devices in Flutter. In this blog post, we’ll go over the advanced aspects of BLE communication including service discovery, characteristic operations, and real-time data exchange using flutter_blue_plus 1.35.5.
Service Discovery
After establishing a connection to a BLE device (as covered in Part 2), the next step is to discover the services and characteristics that the device offers. BLE devices organize their functionality into services, and each service contains one or more characteristics that represent specific data points or controls.
Understanding BLE Structure
- Service: A collection of related characteristics (e.g., Heart Rate Service, Battery Service)
- Characteristic: A specific data point within a service (e.g., Heart Rate Measurement, Battery Level)
- Properties: What operations are supported (Read, Write, Notify, Indicate)
Looking for Mobile App Development Services
Discovering Services
To discover services, you need to call the discoverServices method on your connected BluetoothDevice. Here’s how to implement comprehensive service discovery:
class BLEServiceDiscovery {
static Future<List<BluetoothService>> discoverServices(BluetoothDevice device) async {
try {
print(‘Discovering services for device: ${device.advName}’);
// Discover all services
List<BluetoothService> services = await device.discoverServices();
// Log discovered services and characteristics
print(‘Found ${services.length} services:’);
for (var service in services) {
print(‘Service: ${service.uuid} – ${service.characteristics.length} characteristics’);
for (var characteristic in service.characteristics) {
print(‘ Characteristic: ${characteristic.uuid}’);
print(‘ Properties: Read:${characteristic.properties.read} Write:${characteristic.properties.write} Notify:${characteristic.properties.notify}’);
}
}
return services;
} catch (e) {
print(‘Error discovering services: $e’);
return [];
}
}
}
Service Discovery in Practice
Here’s how you would integrate service discovery into your app:
Future<void> _discoverAndProcessServices() async {
if (connectedDevice == null) return;
try {
// Discover services
List<BluetoothService> services = await BLEServiceDiscovery.discoverServices(connectedDevice!);
// Process each service
for (BluetoothService service in services) {
print(‘Processing service: ${service.uuid}’);
// Access characteristics
for (BluetoothCharacteristic characteristic in service.characteristics) {
print(‘Found characteristic: ${characteristic.uuid}’);
// Check what operations are supported
if (characteristic.properties.read) {
print(‘ – Supports READ’);
}
if (characteristic.properties.write) {
print(‘ – Supports WRITE’);
}
if (characteristic.properties.notify) {
print(‘ – Supports NOTIFY’);
}
}
}
} catch (e) {
print(‘Error during service discovery: $e’);
}
}
Reading and Writing Characteristics
Once you have discovered the services and characteristics, you can interact with them by reading values, writing data, and subscribing to notifications.
Reading Characteristic Values:
Reading allows you to get the current value of a characteristic. Here’s how to implement robust characteristic reading:
class BLECharacteristicReader {
static Future<List<int>> readCharacteristic(BluetoothCharacteristic characteristic) async {
try {
// Check if characteristic supports reading
if (!characteristic.properties.read) {
print(‘Characteristic ${characteristic.uuid} does not support reading’);
return [];
}
// Read the characteristic value
List<int> value = await characteristic.read();
print(‘Read from ${characteristic.uuid}: $value’);
print(‘As string: ${String.fromCharCodes(value)}’);
return value;
} catch (e) {
print(‘Error reading characteristic: $e’);
return [];
}
}
// Read all readable characteristics from a service
static Future<Map<String, List<int>>> readAllCharacteristics(BluetoothService service) async {
Map<String, List<int>> results = {};
for (BluetoothCharacteristic characteristic in service.characteristics) {
if (characteristic.properties.read) {
List<int> value = await readCharacteristic(characteristic);
results[characteristic.uuid.toString()] = value;
}
}
return results;
}
}
Writing Characteristic Values:
Writing allows you to send data to the BLE device. Here’s how to implement characteristic writing:
class BLECharacteristicWriter {
static Future<bool> writeCharacteristic(
BluetoothCharacteristic characteristic,
List<int> value,
{bool withResponse = true}
) async {
try {
// Check if characteristic supports writing
if (!characteristic.properties.write && !characteristic.properties.writeWithoutResponse) {
print(‘Characteristic ${characteristic.uuid} does not support writing’);
return false;
}
// Write the value
await characteristic.write(value, withoutResponse: !withResponse);
print(‘Written to ${characteristic.uuid}: $value’);
return true;
} catch (e) {
print(‘Error writing characteristic: $e’);
return false;
}
}
// Write string data
static Future<bool> writeString(BluetoothCharacteristic characteristic, String text) async {
List<int> data = text.codeUnits;
return await writeCharacteristic(characteristic, data);
}
// Write command with specific format
static Future<bool> writeCommand(BluetoothCharacteristic characteristic, int command, List<int> parameters) async {
List<int> data = [command, …parameters];
return await writeCharacteristic(characteristic, data);
}
}
Practical Example: Device Control
Here’s a practical example showing how to control a BLE device:
class BLEDeviceController {
static BluetoothCharacteristic? controlCharacteristic;
static BluetoothCharacteristic? statusCharacteristic;
static Future<void> initializeDevice(BluetoothDevice device) async {
try {
// Discover services
List<BluetoothService> services = await device.discoverServices();
// Find the control service (example UUID)
for (var service in services) {
if (service.uuid.toString().toLowerCase().contains(‘12345678’)) {
// Find control and status characteristics
for (var characteristic in service.characteristics) {
if (characteristic.uuid.toString().toLowerCase().contains(‘abcd’)) {
controlCharacteristic = characteristic;
} else if (characteristic.uuid.toString().toLowerCase().contains(‘efgh’)) {
statusCharacteristic = characteristic;
}
}
}
}
print(‘Device initialized successfully’);
} catch (e) {
print(‘Error initializing device: $e’);
}
}
static Future<void> turnOnDevice() async {
if (controlCharacteristic != null) {
await BLECharacteristicWriter.writeCommand(controlCharacteristic!, 0x01, []);
}
}
static Future<void> turnOffDevice() async {
if (controlCharacteristic != null) {
await BLECharacteristicWriter.writeCommand(controlCharacteristic!, 0x00, []);
}
}
static Future<List<int>> getDeviceStatus() async {
if (statusCharacteristic != null) {
return await BLECharacteristicReader.readCharacteristic(statusCharacteristic!);
}
return [];
}
}
Handling Notifications
Subscribing to Notifications:
Here’s how to implement robust notification handling:
class BLENotificationManager {
static Map<String, StreamSubscription> _subscriptions = {};
static Future<bool> subscribeToNotifications(
BluetoothDevice device,
BluetoothCharacteristic characteristic,
Function(List<int>) onDataReceived,
) async {
try {
// Check if characteristic supports notifications
if (!characteristic.properties.notify && !characteristic.properties.indicate) {
print(‘Characteristic ${characteristic.uuid} does not support notifications’);
return false;
}
// Create unique subscription key
String subscriptionKey = ‘${device.remoteId}_${characteristic.uuid}’;
// Cancel existing subscription if any
_subscriptions[subscriptionKey]?.cancel();
// Subscribe to characteristic notifications
final subscription = characteristic.onValueReceived.listen((value) {
print(‘Notification from ${characteristic.uuid}: $value’);
onDataReceived(value);
});
// Automatically cancel subscription when device disconnects
device.cancelWhenDisconnected(subscription);
_subscriptions[subscriptionKey] = subscription;
// Enable notifications on the characteristic
await characteristic.setNotifyValue(true);
print(‘Successfully subscribed to notifications for ${characteristic.uuid}’);
return true;
} catch (e) {
print(‘Error subscribing to notifications: $e’);
return false;
}
}
static Future<void> unsubscribeFromNotifications(
BluetoothDevice device,
BluetoothCharacteristic characteristic,
) async {
try {
// Disable notifications
await characteristic.setNotifyValue(false);
// Cancel subscription
String subscriptionKey = ‘${device.remoteId}_${characteristic.uuid}’;
_subscriptions[subscriptionKey]?.cancel();
_subscriptions.remove(subscriptionKey);
print(‘Unsubscribed from notifications for ${characteristic.uuid}’);
} catch (e) {
print(‘Error unsubscribing from notifications: $e’);
}
}
static void cleanupAllSubscriptions() {
for (var subscription in _subscriptions.values) {
subscription.cancel();
}
_subscriptions.clear();
}
}
Real-time Data Processing:
Here’s an example of processing real-time sensor data:
class SensorDataProcessor {
static void processSensorData(List<int> rawData) {
try {
// Example: Process heart rate data
if (rawData.length >= 2) {
int heartRate = rawData[1];
print(‘Heart Rate: $heartRate BPM’);
// Update UI or trigger actions based on heart rate
_updateHeartRateUI(heartRate);
}
// Example: Process temperature data
if (rawData.length >= 4) {
int tempRaw = (rawData[2] << 8) | rawData[3];
double temperature = tempRaw / 100.0; // Convert to celsius
print(‘Temperature: ${temperature}°C’);
_updateTemperatureUI(temperature);
}
} catch (e) {
print(‘Error processing sensor data: $e’);
}
}
static void _updateHeartRateUI(int heartRate) {
// Update your UI components here
print(‘Updating heart rate display: $heartRate’);
}
static void _updateTemperatureUI(double temperature) {
// Update your UI components here
print(‘Updating temperature display: $temperature’);
}
}
Complete Notification Example:
Here’s a complete example showing how to set up notifications for multiple characteristics:
class MultiSensorManager {
static Future<void> setupSensorNotifications(BluetoothDevice device) async {
try {
// Discover services
List<BluetoothService> services = await device.discoverServices();
// Find sensor service
for (var service in services) {
if (service.uuid.toString().toLowerCase().contains(‘sensor’)) {
// Subscribe to all notifiable characteristics
for (var characteristic in service.characteristics) {
if (characteristic.properties.notify) {
await BLENotificationManager.subscribeToNotifications(
device,
characteristic,
(data) => _handleSensorData(characteristic.uuid.toString(), data),
);
}
}
}
}
} catch (e) {
print(‘Error setting up sensor notifications: $e’);
}
}
static void _handleSensorData(String characteristicUuid, List<int> data) {
switch (characteristicUuid.toLowerCase()) {
case ‘heart_rate_uuid’:
SensorDataProcessor.processSensorData(data);
break;
case ‘temperature_uuid’:
SensorDataProcessor.processSensorData(data);
break;
default:
print(‘Unknown sensor data from $characteristicUuid: $data’);
}
}
}
Transform your App Experience with AlphaBOLD's Flutter Expertise!
Take the next step in mobile development. Contact AlphaBOLD for innovative Flutter solutions and begin your journey to a superior app experience with a complimentary consultation.
Request a ConsultationAdvanced Features
Using lastValueStream:
flutter_blue_plus provides lastValueStream as an alternative to onValueReceived. It’s particularly useful for characteristics that support both read and write operations:
class AdvancedCharacteristicHandler {
static void setupLastValueStream(BluetoothCharacteristic characteristic) {
final subscription = characteristic.lastValueStream.listen((value) {
print(‘Value updated: $value’);
// This stream is updated for:
// – Read operations
// – Write operations
// – Notification arrivals
});
// Don’t forget to cancel the subscription when done
// subscription.cancel();
}
}
Error Handling and Retry Logic:
Implement robust error handling for BLE operations:
class BLEErrorHandler {
static Future<List<int>> readWithRetry(
BluetoothCharacteristic characteristic,
{int maxRetries = 3}
) async {
for (int i = 0; i < maxRetries; i++) {
try {
return await characteristic.read();
} catch (e) {
print(‘Read attempt ${i + 1} failed: $e’);
if (i == maxRetries – 1) rethrow;
await Future.delayed(Duration(seconds: 1));
}
}
return [];
}
static Future<bool> writeWithRetry(
BluetoothCharacteristic characteristic,
List<int> value,
{int maxRetries = 3}
) async {
for (int i = 0; i < maxRetries; i++) {
try {
await characteristic.write(value);
return true;
} catch (e) {
print(‘Write attempt ${i + 1} failed: $e’);
if (i == maxRetries – 1) return false;
await Future.delayed(Duration(seconds: 1));
}
}
return false;
}
}
Best Practices
1. Always Check Permissions and Properties:
static Future<bool> validateCharacteristic(
BluetoothCharacteristic characteristic,
String operation,
) async {
switch (operation.toLowerCase()) {
case ‘read’:
return characteristic.properties.read;
case ‘write’:
return characteristic.properties.write || characteristic.properties.writeWithoutResponse;
case ‘notify’:
return characteristic.properties.notify || characteristic.properties.indicate;
default:
return false;
}
}
2. Proper Resource Management:
class BLEResourceManager {
static void cleanupOnDisconnect(BluetoothDevice device) {
device.connectionState.listen((state) {
if (state == BluetoothConnectionState.disconnected) {
// Cancel all subscriptions
BLENotificationManager.cleanupAllSubscriptions();
// Clear cached data
_clearCachedData();
print(‘Cleaned up resources for disconnected device’);
}
});
}
static void _clearCachedData() {
// Clear any cached characteristic references
// Reset application state
}
}
3. MTU Optimization:
class MTUManager {
static Future<int> negotiateOptimalMTU(BluetoothDevice device) async {
try {
// Request larger MTU for better performance
await device.requestMtu(512);
// Listen for MTU changes
device.mtu.listen((mtu) {
print(‘MTU updated to: $mtu’);
});
return await device.mtu.first;
} catch (e) {
print(‘Error negotiating MTU: $e’);
return 23; // Default MTU
}
}
}
Elevate your Mobile Experience with AlphaBOLD's Flutter Development!
Discover how our expert Flutter development services can transform your app ideas into reality. Benefit from AlphaBOLD's comprehensive support and innovative solutions tailored to your needs.
Request a ConsultationConclusion
In this blog post, we’ve explored the advanced aspects of BLE communication using flutter_blue_plus 1.35.5. We’ve covered service discovery, characteristic operations, and real-time data handling through notifications. These techniques form the foundation for building sophisticated BLE applications that can interact with a wide range of IoT devices and sensors.
Key improvements in this updated version include:
- Modern flutter_blue_plus 1.35.5 API usage
- Comprehensive service discovery implementation
- Robust characteristic reading and writing
- Advanced notification handling with proper cleanup
- Error handling and retry logic
- Best practices for resource management
With these code snippets as a foundation, you should be able to build sophisticated Flutter applications that can communicate effectively with BLE devices. The updated API provides better performance, improved error handling, and more reliable connections.
In the next part of our series, we’ll explore advanced BLE operations including connection management, background processing, and handling multiple simultaneous connections.
We trust that you found our article both informative and comprehensive. We look forward to your continued readership and hope to provide you with even more valuable content in the near future. Thank you for your time and consideration, and we hope to welcome you back soon! 😊
Explore Recent Blog Posts








