Orientação gerada a partir de informações públicas. Apenas para referência.
Universal_BLE é um plugin cross-platform do Flutter que habilita a conectividade Bluetooth Low Energy (BLE) em Android, iOS, macOS, Windows, Linux e Web. Ele fornece uma API unificada para descobrir, conectar e se comunicar com dispositivos BLE, independentemente da plataforma subjacente. Projetado para desenvolvedores que criam aplicações de IoT, rastreadores de saúde ou integrações de dispositivos inteligentes, simplifica operações complexas de BLE com comportamento consistente em diferentes ambientes. O plugin suporta varredura, descoberta de serviços, leitura/escrita de características e tratamento de notificações. Sua natureza de código aberto incentiva contribuições da comunidade e transparência.
Casos de uso
controle de dispositivo IoT
Integração de rastreador de saúde e condicionamento físico
* getAvailabilityState returns a snapshot. Listen to advertisingStateStream, connectionStateStream, and related streams for runtime updates.
** Pass deviceId to notify one client; omit it to notify all subscribed clients (when supported — see getCapabilities().supportsTargetedCharacteristicUpdate).
*** advertisingStateStream, characteristicSubscriptionStream, connectionStateStream, serviceAddedStream, mtuChangedStream.
Important: Before using BLE features, make sure to check the Permissions section to see what setup is needed for your target platform (Android, iOS, macOS, Windows, Linux, or Web).
Scanning
The very first thing you need to do before being able to connect to a device is to discover it by calling startScan();
// Get scan updates from streamUniversalBle.scanStream.listen((BleDevice bleDevice) {
// e.g. Use BleDevice ID to connect
});
// Or set a handlerUniversalBle.onScanResult = (bleDevice) {}
// Perform a scanUniversalBle.startScan();
// Or optionally add a scan filterUniversalBle.startScan(
scanFilter: ScanFilter(
withServices: ["SERVICE_UUID"],
withManufacturerData: [ManufacturerDataFilter(companyIdentifier: 0x004c)],
withNamePrefix: ["NAME_PREFIX"],
)
);
// Stop scanningUniversalBle.stopScan();
// Check if scanningUniversalBle.isScanning();
Before initiating a scan, ensure that Bluetooth is available:
AvailabilityState state = awaitUniversalBle.getBluetoothAvailabilityState();
// Start scan only if Bluetooth is powered onif (state == AvailabilityState.poweredOn) {
UniversalBle.startScan();
}
// Listen to bluetooth availability changes using streamUniversalBle.availabilityStream.listen((state) {
if (state == AvailabilityState.poweredOn) {
UniversalBle.startScan();
}
});
// Or set a handlerUniversalBle.onAvailabilityChange = (state) {};
Already connected devices, connected either through previous sessions, other apps or through system settings, won't show up as scan results. You can get those using getSystemDevices().
// Get already connected devices.// You can set `withServices` to narrow down the results.// On `Apple`, `withServices` is required to get any connected devices. If not passed, several [18XX] generic services will be set by default.List<BleDevice> devices = awaitUniversalBle.getSystemDevices(withServices: []);
For each such device the isSystemDevice property will be true.
You still need to explicitly connect to them before being able to use them.
Scan Filter
You can optionally set a filter when scanning. A filter can have multiple conditions (services, manufacturerData, namePrefix) and all conditions are in OR relation, returning results that match any of the given conditions.
With Services
When setting this parameter, the scan results will only include devices that advertise any of the specified services.
List<String> withServices;
Note: On web you have to specify services before you are able to use them. See the web section for more details.
With ManufacturerData
Use the withManufacturerData parameter to filter devices by manufacturer data. When you pass a list of ManufacturerDataFilter objects to this parameter, the scan results will only include devices that contain any of the specified manufacturer data.
You can filter manufacturer data by company identifier, payload prefix, or payload mask.
Use the withNamePrefix parameter to filter devices by names (case sensitive). When you pass a list of names, the scan results will only include devices that have this name or start with the provided parameter.
List<String> withNamePrefix;
Exclusion Filter
Use exclusion filters to exclude specific devices from scan results:
// Can be connected, disconnected, connecting or disconnectingBleConnectionState connectionState = await bleDevice.connectionState;
Auto-connect
You can enable automatic reconnection by setting the autoConnect parameter to true. When enabled, the system will automatically attempt to reconnect to the device when it becomes available again.
await bleDevice.connect(autoConnect: true);
Background connection events (iOS/macOS)
By default, a suspended app does not get woken for connection events. Set AppleConnectionOptions to have the system alert the user and relaunch your app into the background when a connection event occurs while the app is suspended — e.g. to keep reacting to a previously paired peripheral (auto-reconnect) during a workout while the phone is locked. Requires the bluetooth-central background mode on iOS.
Note: the system may show an alert to the user for these events, and notifyOnNotification fires per characteristic notification, so enable only what you need.
When the Android app is killed by the user (typically swiped away from recents), the OS tears down the FlutterEngine without ever delivering a "disconnect" — the peripheral keeps the link open until its connection supervision timeout (often 10–20 s), leaving the device occupied.
Pass AndroidConnectionOptions(closeGattOnDetach: true) to have the plugin close every known GATT client as soon as the engine detaches, releasing the peripheral immediately. Without the option, whatever the autoConnect value, the link persists until the peripheral's supervision timeout.
Once any connection opts in, the behavior is global for the running process — every current and future GATT client is released on engine teardown. It can be disabled at runtime by connecting with closeGattOnDetach: false; connecting without the option leaves the current value unchanged (a fresh app launch resets it).
Android-only. Explicit disconnect() calls and rotation are unaffected.
Discovering Services
After establishing a connection, services need to be discovered. This method will discover all services and their characteristics.
If you don't call this method then it will be automatically called when you try to get any service or characteristic.
DiscoverServices
Discovers the services offered by the device. Returns a Future<List<BleService>>. After discovery services are cached and each call of this method updates the cache.
List<BleService> services = await bleDevice.discoverServices();
for (var service in services) {
debugPrint('Service UUID: ${service.uuid}');
}
GetService
Retrieves a specific service. Returns a Future<BleService>.
service: The UUID of the service.
preferCached: If true (default), cached services are used. If cache is empty, discoverServices() will be called.
BleService service = await bleDevice.getService('180a');
GetCharacteristic
Retrieves a specific characteristic from a service. Returns a Future<BleCharacteristic>.
service: The UUID of the service.
characteristic: The UUID of the characteristic.
preferCached: If true (default), cached services are used. If cache is empty, discoverServices() will be called
Get BleCharacteristic using bleDevice.getCharacteristic
OnValueReceived
A stream of Uint8List that emits values received from the characteristic. Listen to this stream to receive updates whenever the characteristic's value changes.
Subscribe to notifications for this characteristic. Throws an exception if the characteristic does not support notifications.
await characteristic.notifications.subscribe();
Indications
Subscribe to indications for this characteristic. Throws an exception if the characteristic does not support indications.
await characteristic.indications.subscribe();
Unsubscribe
Unsubscribe from notifications and indications of this characteristic.
await characteristic.unsubscribe();
Check Subscription Status
Check if a characteristic is currently subscribed to:
// Via BleCharacteristic or CharacteristicSubscriptionbool isSubscribed = characteristic.isSubscribed;
// orbool isSubscribed = characteristic.notifications.isSubscribed;
// Or globally via UniversalBlebool isSubscribed = UniversalBle.isSubscribed(deviceId, characteristicId);
// Get all subscribed characteristic UUIDs for a deviceList<String> subscribedChars = UniversalBle.getSubscribedCharacteristics(deviceId);
Pairing
Trigger pairing
Pair on Android, Windows, Linux
await bleDevice.pair();
Pair on Apple and web
For Apple and Web, pairing support depends on the device. Pairing is triggered automatically by the OS when you try to read/write from/to an encrypted characteristic.
Calling bleDevice.pair() will only trigger pairing if the device has an encrypted read characteristic.
If your device only has encrypted write characteristics or you happen to know which encrypted read characteristic you want to use, you can pass it with a pairingCommand.
To discover encrypted characteristics, make sure your device is not paired and use the example app to read/write to all discovered characteristics one by one. If one of them triggers pairing, that means it is encrypted and you can use it to construct BleCommand(service:"SERVICE", characteristic:"ENCRYPTED_CHARACTERISTIC").
Pairing state changes
// Get pairing state updates using stream
bleDevice.pairingStateStream.listen((bool paired) {
// Handle pairing state change
});
Unpair
bleDevice.unpair();
Bluetooth Availability
// Get current Bluetooth availability stateAvailabilityState availabilityState = UniversalBle.getBluetoothAvailabilityState(); // e.g. poweredOff or poweredOn,// Receive Bluetooth availability changesUniversalBle.onAvailabilityChange = (state) {
// Handle the new Bluetooth availability state
};
// Enable Bluetooth programmaticallyUniversalBle.enableBluetooth();
// Disable Bluetooth programmaticallyUniversalBle.disableBluetooth();
Requesting MTU
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
MTU negotiation is largely platform- and stack-managed, and often cannot be
explicitly controlled by applications:
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 Bluetooth 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 Bluetooth 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:
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
Requesting Connection Priority
On Android, you can request a connection parameter update to tune the BLE connection interval. This can yield a 3–7× throughput improvement for data-intensive transfers.
// Before starting high-throughput data transfer:awaitUniversalBle.requestConnectionPriority(
deviceId,
BleConnectionPriority.highPerformance,
);
Note: Only supported on Android. On all other platforms this throws UniversalBleException with code notSupported.
Call this after connecting and after requestMtu(), before beginning data transfer.
The OS may later change connection parameters without your app requesting it (e.g. for power saving), which can reduce throughput. On Android API 26+, set UniversalBle.onConnectionParametersChange and react if needed:
UniversalBle.onConnectionParametersChange = (update) {
if (update.deviceId != deviceId || !update.isSuccess) return;
// Prefer intervalMs for throughput decisions; estimatedPriority is approximate.if (update.intervalMs > 50) {
UniversalBle.requestConnectionPriority(
deviceId,
BleConnectionPriority.highPerformance,
);
}
};
Note: Re-requesting high priority on every update can fight the OS power manager — debounce in app code. Requires Android API 26+ (BleCapabilities.supportsConnectionParametersUpdates).
Reading RSSI
Read the signal strength (RSSI) of a connected device.
int rssi = await bleDevice.readRssi();
⚠️ Note: The device must be connected before reading RSSI.
Platform Limitations
Android / iOS / macOS: Fully supported.
Windows / Linux / Web: Not supported.
Command Queue
By default, commands use QueueType.auto, which automatically picks the best strategy for the current platform with zero configuration. Android uses a per-device queue (its native BLE stack rejects overlapping operations), while all other platforms run commands in parallel (they pipeline natively).
If you want explicit control over how commands are serialized, you can set queueType:
// Run all commands in a single global queue (safest, but slower).UniversalBle.queueType = QueueType.global;
// Create a separate queue for each device.UniversalBle.queueType = QueueType.perDevice;
// Auto-decide per platform (default): Android queues per device, all others run in parallel.UniversalBle.queueType = QueueType.auto;
You can have separate queues by passing an optional queueId. Commands with the same queueId are serialized together, but run in parallel with both QueueType.perDevice and QueueType.global:
Keep in mind that some platforms (e.g. Android) may not handle well devices that fail to process consecutive commands without a minimum interval. Therefore, it is not advised to set queueType to none.
You can get queue updates by setting:
// Get queue state updatesUniversalBle.onQueueUpdate = (String id, int remainingItems) {
debugPrint("Queue: $id Remaining: $remainingItems");
};
To clear a queue:
// Clear global queueUniversalBle.clearQueue(BleCommandQueue.globalQueueId);
// Clear a per-device queue (when queueType is perDevice)UniversalBle.clearQueue(deviceId);
// Clear a custom queue (same string passed as queueId to read/write/etc.)UniversalBle.clearQueue('customQueueId');
// Clear all queuesUniversalBle.clearQueue();
Peripheral Command Queue
UniversalBlePeripheral supports the same queueing configuration (queueType, timeout, clearQueue, and onQueueUpdate) for peripheral commands (e.g. addService, startAdvertising, updateCharacteristicValue):
// Configure peripheral command queue (defaults to QueueType.auto)UniversalBlePeripheral.queueType = QueueType.perDevice;
// Clear peripheral queueUniversalBlePeripheral.clearQueue(deviceId);
// Send peripheral updates with a specific queueIdUniversalBlePeripheral.updateCharacteristicValue(
characteristicId: charUuid,
value: data,
deviceId: deviceId,
queueId: 'customQueueId',
);
Timeout
By default, all commands have a global timeout of 10 seconds.
You can also specify the timeout parameter when sending a command. This will override the global timeout.
Error Handling
Universal BLE provides a unified and type-safe error handling system across all platforms. All errors are represented using the UniversalBleException base class with typed error codes from the UniversalBleErrorCode enum.
Exception Types
UniversalBleException: Base exception class for all BLE errors
ConnectionException: Thrown for connection-related errors
PairingException: Thrown for pairing-related errors
WebBluetoothGloballyDisabled: Thrown when Web Bluetooth is globally disabled
Error Codes
All errors are categorized using the UniversalBleErrorCode enum, which includes codes for:
The error parser automatically converts platform-specific error formats (strings, numeric codes, PlatformExceptions) into the unified UniversalBleErrorCode enum, ensuring consistent error handling across all platforms.
Peripheral Mode
universal_ble provides peripheral mode through UniversalBlePeripheral, so your app can advertise as a peripheral "server" in addition to client mode.
Setup
import'package:universal_ble/universal_ble.dart';
final caps = awaitUniversalBlePeripheral.getCapabilities();
if (!caps.supportsPeripheralMode) return;
final readiness = awaitUniversalBlePeripheral.getAvailabilityState();
if (readiness != PeripheralReadinessState.ready) return;
Service Management
Peripheral GATT services use BlePeripheralService, BlePeripheralCharacteristic, and BlePeripheralDescriptor. Each characteristic requires permissions; descriptors can include an initial value (for example HID Report Reference 0x2908).
On Android, passing localName may temporarily change the system Bluetooth device name (so it can appear in the advertisement). The plugin restores the previous name when advertising stops, if starting advertising fails, or when the plugin is disposed.
On Windows, pass services: [] and manufacturerData for connectionless advertising via BluetoothLEAdvertisementPublisher. No GATT service registration is needed. The company ID must fit 16 bits and the payload must be at most 27 bytes (legacy advertising). Stop advertising before changing the payload. Registered-service advertising still uses GattServiceProvider; combining service UUIDs with manufacturer data is not supported. Windows does not allow a custom localName in either mode. Unsupported combinations return not-supported. Advertising is best effort; listen to advertisingStateStream for asynchronous failures. See Microsoft's publisher restrictions.
On iOS/macOS, CoreBluetooth supports local names and service UUIDs, not manufacturer data. getCapabilities() reflects this restriction. Use a local-name carrier when your protocol must also broadcast on Apple platforms.
Useful to restore in-app state after a process restart (subscription callbacks are not replayed).
import'package:universal_ble/universal_ble.dart';
final subscribers = awaitUniversalBlePeripheral.getSubscribedClients(
batteryLevelChar,
);
for (final deviceId in subscribers) {
final maxNotifyLength =
awaitUniversalBlePeripheral.getMaximumNotifyLength(deviceId);
// maxNotifyLength is null when unknown for this device.
}
Linux/Web currently return unsupported for peripheral mode.
Windows peripheral advertising does not expose all advertising payload customization options from Android/Apple stacks.
iOS/macOS setup (including required Info.plist keys for peripheral usage) is documented in Permissions → iOS / macOS.
UUID Format Agnostic
Universal BLE is agnostic to the UUID format of services and characteristics regardless of the platform the app runs on. When passing a UUID, you can pass it in any format (long/short) or character case (upper/lower case) you want. Universal BLE will take care of necessary conversions, across all platforms, so that you don't need to worry about underlying platform differences.
For consistency, all characteristic and service UUIDs will be returned in lowercase 128-bit format, across all platforms, e.g. 0000180a-0000-1000-8000-00805f9b34fb.
Utility Methods
If you need to convert any UUIDs in your app you can use the following methods.
BleUuidParser.string() converts a string to a 128-bit UUID formatted string:
By default, BLE 5 extended advertisements are scanned (API 26+, unchanged from prior releases). Set legacy: true for legacy BLE 4.x devices (e.g. ESP32).
Universal BLE supports BLE scanning from background services (e.g., using flutter_foreground_task or similar packages) on Android. When running in a background context without an Activity:
If permissions are already granted: Scanning works normally
If permissions are not granted: An error is thrown with the message "Permissions not granted and activity is not available to request them"
Best Practice: Request permissions while your app is in the foreground before starting any background BLE operations:
// Request permissions in foreground (e.g., during app setup)awaitUniversalBle.requestPermissions();
// Later, in your ForegroundTask, scanning will work if permissions were grantedawaitUniversalBle.startScan();
iOS / macOS
For Bluetooth usage (including peripheral mode), add both keys to your app's Info.plist:
NSBluetoothAlwaysUsageDescription: message shown when the app requests Bluetooth access.
NSBluetoothPeripheralUsageDescription: message used for peripheral role access on Apple platforms.
Example:
<key>NSBluetoothAlwaysUsageDescription</key><string>This app uses Bluetooth to scan, connect, and advertise to nearby devices.</string><key>NSBluetoothPeripheralUsageDescription</key><string>This app uses Bluetooth to advertise services to nearby devices.</string>
Use clear, user-facing text that explains why Bluetooth is needed in your app.
macOS entitlements
On macOS, add the following entitlement to both macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements:
Alternatively, enable the Bluetooth capability in Xcode under Signing & Capabilities.
If this entitlement is missing, getBluetoothAvailabilityState() returns AvailabilityState.unsupported.
Permissions are automatically requested when calling startScan(). You can also manually call requestPermissions() if needed.
iOS background state restoration
On iOS, when your app declares the bluetooth-central background mode and Bluetooth permission is already granted, the central manager is created at launch with a CBCentralManagerOptionRestoreIdentifierKey, so CoreBluetooth can relaunch your app in the background when a connected peripheral has activity, and hand the live connection back to the plugin. If permission has not been granted yet, creation is deferred until a central BLE API (such as startScan() or connect()) is called.
To opt in, declare the Uses Bluetooth LE accessories background mode. After enabling it, in Info.plist you should have:
Without the bluetooth-central background mode, CBCentralManager is created lazily on the first central BLE API call and state restoration is disabled.
macOS does not support CoreBluetooth state restoration; this behavior is iOS-only.
On relaunch, the plugin re-adopts the restored peripherals and emits onConnectionChanged for any that are still connected, so your Dart code can resume where it left off.
Windows
Your Bluetooth adapter needs to support at least Bluetooth 4.0. If you have more than 1 adapters, the first one returned from the system will be picked.
When publishing on Windows, you need to declare the following capabilities: bluetooth, radios.
Linux
Your Bluetooth adapter needs to support at least Bluetooth 4.0. If you have more than 1 adapters, the first one returned from the system will be picked.
When publishing on Linux as a snap, you need to declare the bluez plug in snapcraft.yaml.
...plugs:-bluez
Web
On web, the withServices parameter in the ScanFilter is used as optional_services as well as a services filter. You have to set this parameter to ensure that you can access the specified services after connecting to the device. You can leave it empty for the rest of the platforms if your device does not advertise services.
No runtime permissions are required. The requestPermissions() method always succeeds on Web.
Manually Requesting Permissions
Calling requestPermissions() is optional. Permissions are automatically requested when calling startScan(). However, you can manually call requestPermissions() if you want to:
Request permissions before scanning (e.g., to handle permission errors separately)
Ensure permissions are granted before other operations like connect(), read(), write(), etc., which don't automatically request permissions
The requestPermissions() method:
Returns successfully if all permissions are already granted or accepted by the user
Throws a UniversalBleException if permissions are denied by the user
Always succeeds on Windows, Linux, and Web (no runtime permissions required)
Note: When calling startScan(), permissions are automatically requested. To configure location permission requests during scanning, use requestLocationPermission on AndroidOptions (see Android scan options):
No runtime permissions are required. The requestPermissions() method always succeeds on Windows and Linux platforms.
Customizing Platform Implementation of Universal Ble
// Create a class that extends UniversalBlePlatformclassUniversalBleMockextendsUniversalBlePlatform{
// Implement all commands
}
UniversalBle.setInstance(UniversalBleMock());
Logging
Configure logging to help debug Ble operations
Usage
Set the log level during app initialization, default level is none
voidmain() async {
WidgetsFlutterBinding.ensureInitialized();
// Enable verbose logging to see all BLE operationsawaitUniversalBle.setLogLevel(BleLogLevel.verbose);
runApp(MyApp());
}
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.
voidmain() async {
WidgetsFlutterBinding.ensureInitialized();
// Reset BLE state before app initializationawaitresetBleState();
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 webif (!kDebugMode || kIsWeb) return;
// Check Bluetooth availabilityAvailabilityState availabilityState =
awaitUniversalBle.getBluetoothAvailabilityState();
// Skip if Bluetooth is not powered onif (availabilityState != AvailabilityState.poweredOn) {
debugPrint('Reset: Bluetooth is not powered on');
return;
}
// Stop scanningif (awaitUniversalBle.isScanning()) {
debugPrint('Reset: Stopping scan');
awaitUniversalBle.stopScan();
}
// Disconnect all connected devicesList<String> withServices = [];
// On Apple platforms, you must specify services to discover connected devicesif (defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.iOS) {
// Replace with your known device service UUIDs
withServices = ["0x180A"];
}
List<BleDevice> connectedDevices =
awaitUniversalBle.getSystemDevices(withServices: withServices);
for (var device in connectedDevices) {
debugPrint('Reset: Disconnecting device: ${device.deviceId}');
awaitUniversalBle.disconnect(device.deviceId);
}
debugPrint('Reset: Done');
}
For more granular control, you can use the Low-Level API. This API is "Device ID"-based, offering greater flexibility by enabling direct calls without the need for object instances.
App showcase
Here are some of the apps leveraging the power of universal_ble:
Universal BLE - A comprehensive developer tool for exploring and testing Bluetooth Low Energy (BLE) devices. It enables scanning for nearby BLE devices, connecting to peripherals, discovering and exploring services, characteristics, and descriptors. Supports reading and writing characteristic values, enabling notifications and indications, viewing device information and signal strength, and provides detailed logging of BLE operations. Perfect for developers, engineers, and hobbyist tinkerers working with BLE-enabled devices across iOS, Android, macOS, Windows, Linux & Web.
BT Cam - A Bluetooth remote app for DSLR and mirrorless cameras. Compatible with Canon, Nikon, Sony, Fujifilm, GoPro, Olympus, Panasonic, Pentax, and Blackmagic. Built using Universal BLE to connect and control cameras across iOS, Android, macOS, Windows, Linux & Web.
TukToro - Interactive math learning app for kids. Available on iPad and Android tablets, featuring hand-drawn levels, didactic learning games, and ad-free child-safe environment.
BikeControl - Control your favorite trainer app using Zwift Click, Zwift Ride, Zwift Play, Shimano Di2, or other similar devices. It enables virtual gear shifting, steering, workout intensity adjustment, and more across iOS, Android, macOS, Windows, and Linux.
Roll Feathers - Companion app for Bluetooth enabled dice. Connect multiple supported dice (Pixel Dice, GoDice, Virtual Dice), track roll history, and integrate with Home Assistant. Available on Android, iOS, macOS, Windows, Linux, and Web.
OpenEarable - Fully open-source AI platform for ear-based sensing applications with true wireless audio. Features high-precision sensors for biosensing, cardiac monitoring, and motion tracking. Cross-platform support for iOS, Android, and desktop platforms.
Ledger Flutter Plus - A Flutter plugin to scan, connect & sign transactions using Ledger Nano devices via USB & BLE. Supports Android, iOS, and Web platforms for secure cryptocurrency wallet management.
Flutter MIDI Command - Flutter plugin for sending and receiving MIDI messages between Flutter and physical/virtual MIDI devices. Supports USB and BLE transports across iOS, macOS, Android, Linux, and Windows.
NT Helper - Cross-platform Flutter application for editing presets on the Expert Sleepers Disting NT module. Provides comprehensive preset management, algorithm editing, parameter mapping, and routing analysis. Available on Windows, macOS, Linux, iOS, and Android.
MOCs Train Controller - Model train controller using Raspberry Pi Pico W and Flutter. Control trains via Bluetooth Low Energy with support for Android, iOS, and Linux platforms.
💡 Built something cool with Universal BLE?
We'd love to showcase your app here!
Open a pull request and add it to this section.