Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ac98427
Add peripheral mode support
fotiDim Mar 27, 2026
66d2295
Add cache clearing functionality for peripherals and improve synchron…
fotiDim Mar 27, 2026
5a879b7
Enhance BLE plugin with encrypted notify/indicate properties and impr…
fotiDim Mar 27, 2026
fb57381
Improve synchronization in BLE plugin by adding synchronized blocks f…
fotiDim Mar 27, 2026
641f610
Refactor BLE plugin to improve advertising functionality and error ha…
fotiDim Mar 27, 2026
6966359
Enhance BLE advertising by modifying manufacturer data format to incl…
fotiDim Mar 27, 2026
11c9837
feat(peripheral): add getSubscribedCentrals for HID subscription reco…
fotiDim Mar 27, 2026
4d75b6e
Format files
fotiDim Mar 27, 2026
034a671
feat(peripheral): forward BleDescriptor value to native GATT
fotiDim Mar 27, 2026
de30588
fix(darwin): avoid duplicate PigeonError in universal_ble pod
fotiDim Mar 27, 2026
6d70908
Remove redundant APIs
fotiDim Mar 30, 2026
9962168
Unify pigeon
fotiDim Mar 30, 2026
6087060
Fix compilation
fotiDim Mar 30, 2026
20ca418
Remove initialize API
fotiDim Mar 30, 2026
3742aab
Remove initialize API
fotiDim Mar 30, 2026
83e64fa
Refactor BLE peripheral API to enhance functionality and clarity
fotiDim Mar 30, 2026
ef0e623
Refactor Bluetooth availability methods for consistency and clarity
fotiDim Mar 30, 2026
4971c5e
Refactor BLE peripheral API to improve clarity and functionality
fotiDim Mar 30, 2026
1aca78d
Enhance BLE peripheral API with new methods and improved functionality
fotiDim Mar 30, 2026
5ab1881
Enhance README documentation for BLE peripheral API
fotiDim Mar 30, 2026
a25ffcd
Update CHANGELOG and add CONTRIBUTING guidelines; refactor permission…
fotiDim Apr 1, 2026
278f023
Enhance BLE peripheral functionality and documentation
fotiDim Apr 1, 2026
3289158
Add Podfile for iOS and macOS; update xcconfig includes
fotiDim Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## 1.3.0
## 2.0.0
* Add peripheral mode on Android, iOS, macOS, and Windows
* Add `requestConnectionPriority` to allow tuning BLE connection intervals on Android
* BREAKING CHANGE: `getBluetoothAvailabilityState` renamed to `getAvailabilityState`
* Add SPM support on Apple

## 1.2.0
Expand Down
71 changes: 71 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Contributing

Thank you for helping improve Universal BLE. This document describes how we work in this repository and what we expect from contributions.

## Reporting issues

Use [GitHub Issues](https://github.com/Navideck/universal_ble/issues). Include the platform (Android, iOS, macOS, Windows, Linux, Web), Flutter/Dart versions, and a minimal way to reproduce the problem when possible.

## Pull requests

- Open PRs against the `main` branch.
- Keep changes focused: one logical concern per PR is easier to review than a large mixed refactor.
- Update `CHANGELOG.md` when the change is user-visible (new behavior, fixes, or breaking API changes). Follow the existing bullet style (`* …`, `* BREAKING CHANGE: …` where appropriate).
- Do not commit secrets, local paths, or generated build artifacts unrelated to the change.

## Environment

Requirements are defined in `pubspec.yaml` (Dart SDK and Flutter). Use a stable Flutter channel unless a maintainer asks otherwise.

From the repo root:

```sh
flutter pub get
```

## Checks to run before opening a PR

These mirror [.github/workflows/pull_request.yml](.github/workflows/pull_request.yml):

```sh
flutter analyze
flutter test
flutter test --platform chrome
```

Fix any analyzer issues. The project uses [flutter_lints](https://pub.dev/packages/flutter_lints) via [analysis_options.yaml](analysis_options.yaml) (which includes `package:flutter_lints/flutter.yaml`).

Format Dart code with the SDK formatter:

```sh
dart format .
```

If you only touched specific files, you may format those paths instead of the whole tree.

## Pigeon and generated code

Host–native APIs are defined in [pigeon/universal_ble.dart](pigeon/universal_ble.dart). If you change that file, regenerate outputs and include them in the same PR:

```sh
./build_pigeon.sh
```

That runs `dart run pigeon --input pigeon/universal_ble.dart` and formats `lib/src/universal_ble_pigeon/universal_ble.g.dart`. Regenerated Kotlin, Swift, and C++ files land under `android/`, `darwin/`, and `windows/` as configured in the Pigeon `@ConfigurePigeon` block—keep those in sync with the Dart definitions.

## Code conventions

- **Dart:** Follow effective Dart style, existing naming in `lib/`, and analyzer rules. Prefer extending existing patterns (platform interface → pigeon channel → native implementations) over new parallel abstractions unless discussed first.
- **Native:** Match the style and structure of the surrounding file on each platform (Kotlin, Swift, C++). When a Pigeon API changes, update every generated implementation and any hand-written glue so all targets stay consistent.
- **Tests:** Add or extend tests under `test/` when behavior is non-trivial or regression-prone. Use `flutter_test` like the existing suite.
- **Example:** If the change affects how integrators use the plugin, consider updating the `example/` app so it stays a working reference.

## Platform-specific APIs and parameters

- **Single-platform features:** Prefer not adding a new public API when only one platform can implement it, unless none of the existing APIs can be extended or adapted to cover the behavior. For example, something like Android-only `requestConnectionPriority` should only become its own method if `connect`, `platformConfig`, or another existing entry point cannot reasonably subsume it.
- **Single-platform parameters:** When a value applies to one platform only, attach it via a platform-scoped bag (for example `startScan` takes an optional `platformConfig` object for options that only affect a given OS). That keeps the main method signature stable and makes it obvious which settings are platform-specific.
- **Shared parameters:** If more than one platform supports the same option, add it as a normal method parameter on the shared API. Document that implementations on platforms without that capability must ignore the parameter (no-op or documented limitation).

## License

By contributing, you agree that your contributions will be licensed under the same terms as the project: [BSD 3-Clause License](LICENSE).
230 changes: 222 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
- [Error Handling](#error-handling)
- [UUID Format Agnostic](#uuid-format-agnostic)
- [Permissions](#permissions)
- [Peripheral Mode](#peripheral-mode)

## API Support

### Client Mode (`UniversalBle`)

| | Android | iOS | macOS | Windows | Linux | Web |
| :---------------------------- | :-----: | :-: | :---: | :-----: | :---: | :-: |
| startScan/stopScan | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
Expand All @@ -50,14 +53,36 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
| unpair | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
| isPaired | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| onPairingStateChange | ✔️ | ⏺ | ⏺ | ✔️ | ✔️ | ⏺ |
| getBluetoothAvailabilityState | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| getAvailabilityState | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| enable/disable Bluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
| onAvailabilityChange | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| requestMtu | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| requestConnectionPriority | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ |
| readRssi | ✔️ | ✔️ | ✔️ | ❌ | 🚧 | ❌ |
| requestPermissions | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |

### Peripheral Mode (`UniversalBlePeripheral`)

| API | Android | iOS | macOS | Windows | Linux | Web |
| :----------------------- | :-----: | :-: | :---: | :-----: | :---: | :-: |
| getStaticCapabilities | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| getReadinessState\* | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| getAdvertisingState | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| addService | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| removeService | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| clearServices | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| getServices | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| startAdvertising | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| stopAdvertising | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| updateCharacteristicValue\*\* | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| getSubscribedClients | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| getMaximumNotifyLength | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| events stream\*\*\* | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |

\* `getReadinessState` returns a snapshot state. Use `eventStream` for ongoing runtime changes.
\*\* `updateCharacteristicValue` supports broadcast to all subscribed devices or a specific device via `PeripheralUpdateTarget`.
\*\*\* events include advertising state changes, MTU changes, subscription changes, and related peripheral events.

## Getting Started

Add universal_ble in your pubspec.yaml:
Expand All @@ -70,6 +95,7 @@ dependencies:
and import it wherever you want to use it:

```dart
import 'dart:typed_data';
import 'package:universal_ble/universal_ble.dart';
```

Expand Down Expand Up @@ -110,7 +136,7 @@ UniversalBle.isScanning();
Before initiating a scan, ensure that Bluetooth is available:

```dart
AvailabilityState state = await UniversalBle.getBluetoothAvailabilityState();
AvailabilityState state = await UniversalBle.getAvailabilityState();
// Start scan only if Bluetooth is powered on
if (state == AvailabilityState.poweredOn) {
UniversalBle.startScan();
Expand Down Expand Up @@ -402,7 +428,7 @@ bleDevice.unpair();

```dart
// Get current Bluetooth availability state
AvailabilityState availabilityState = UniversalBle.getBluetoothAvailabilityState(); // e.g. poweredOff or poweredOn,
AvailabilityState availabilityState = UniversalBle.getAvailabilityState(); // e.g. poweredOff or poweredOn,

// Receive Bluetooth availability changes
UniversalBle.onAvailabilityChange = (state) {
Expand Down Expand Up @@ -440,7 +466,7 @@ explicitly controlled by applications:

* **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
* **Android 14+**: The first Bluetooth client effectively drives MTU negotiation
to 517 (or the link’s maximum); subsequent MTU requests are ignored.

* **Windows**
Expand All @@ -451,7 +477,7 @@ explicitly controlled by applications:
* **Linux (BlueZ)**

* MTU is negotiated automatically by default.
* The standard D-Bus GATT API does not expose MTU control.
* 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.

Expand Down Expand Up @@ -606,6 +632,175 @@ try {

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 `UniversalBlePeripheralClient`, so your app can advertise as a peripheral "server" in addition to client mode.

### Setup

```dart
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:universal_ble/universal_ble.dart';

final peripheral = UniversalBlePeripheralClient();

final caps = await peripheral.getStaticCapabilities();
if (!caps.supportsPeripheralMode) return;

final readiness = await peripheral.getReadinessState();
if (readiness != UniversalBlePeripheralReadinessState.ready) return;
```

### Service Management

```dart
await peripheral.addService(
BleService("0000180F-0000-1000-8000-00805F9B34FB", [
BleCharacteristic(
"00002A19-0000-1000-8000-00805F9B34FB",
[CharacteristicProperty.read, CharacteristicProperty.notify],
[BleDescriptor("00002902-0000-1000-8000-00805F9B34FB")],
),
]),
primary: true,
);

await peripheral.addService(
BleService("0000180D-0000-1000-8000-00805F9B34FB", [
BleCharacteristic(
"00002A37-0000-1000-8000-00805F9B34FB",
[
CharacteristicProperty.read,
CharacteristicProperty.notify,
CharacteristicProperty.write,
],
[],
),
]),
);

final services = await peripheral.getServices();
await peripheral.removeService(const PeripheralServiceId("0000180D-0000-1000-8000-00805F9B34FB"));
```

### Advertising

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**, `GattServiceProvider`-based advertising does not support `localName`, manufacturer data, or a scan-response flag; omit them (as below) or the call will return a not-supported error.

```dart
final isWindows = !kIsWeb && defaultTargetPlatform == TargetPlatform.windows;

await peripheral.startAdvertising(
services: const [
PeripheralServiceId("0000180F-0000-1000-8000-00805F9B34FB"),
],
localName: isWindows ? null : "UniversalBlePeripheral",
manufacturerData: isWindows
? null
: ManufacturerData(
0x012D,
Uint8List.fromList([0x03, 0x00, 0x64, 0x00]),
),
addManufacturerDataInScanResponse: isWindows
? false
: caps.supportsManufacturerDataInScanResponse,
);

final advertisingState = await peripheral.getAdvertisingState();
if (advertisingState == UniversalBlePeripheralAdvertisingState.advertising) {
// Peripheral is advertising.
}

await peripheral.stopAdvertising();
```

### Request Handlers

```dart
peripheral.setRequestHandlers(
PeripheralRequestHandlers(
onReadRequest: (deviceId, characteristicId, offset, value) {
return BleReadRequestResult(value: value ?? Uint8List(0));
},
onWriteRequest: (deviceId, characteristicId, offset, value) {
return const BleWriteRequestResult();
},
onDescriptorReadRequest:
(deviceId, characteristicId, descriptorId, offset, value) {
return BleReadRequestResult(value: value ?? Uint8List(0));
},
onDescriptorWriteRequest:
(deviceId, characteristicId, descriptorId, offset, value) {
return const BleWriteRequestResult();
},
),
);
```

### Characteristic Updates

```dart
await peripheral.updateCharacteristicValue(
characteristicId: const PeripheralCharacteristicId(
"00002A19-0000-1000-8000-00805F9B34FB",
),
value: Uint8List.fromList([92]),
);
```

### Subscribed Clients and Notify Length

```dart
final subscribers = await peripheral.getSubscribedClients(
const PeripheralCharacteristicId("00002A19-0000-1000-8000-00805F9B34FB"),
);

for (final deviceId in subscribers) {
final maxNotifyLength = await peripheral.getMaximumNotifyLength(deviceId);
// maxNotifyLength can be null when unknown for this device.
}
```

### Event Stream

```dart
final sub = peripheral.eventStream.listen((event) {
switch (event) {
case UniversalBlePeripheralAdvertisingStateChanged():
// event.state / event.error
break;
case UniversalBlePeripheralCharacteristicSubscriptionChanged():
// event.deviceId / event.characteristicId / event.isSubscribed
break;
case UniversalBlePeripheralConnectionStateChanged():
// event.deviceId / event.connected
break;
case UniversalBlePeripheralMtuChanged():
// event.deviceId / event.mtu
break;
case UniversalBlePeripheralServiceAdded():
// event.serviceId / event.error
break;
}
});
```

### Breaking changes

- `isSupported()` was replaced by `getStaticCapabilities().supportsPeripheralMode`.
- `isAdvertising()` was replaced by `getAdvertisingState()`.
- Static callback setters were replaced by `eventStream` + `setRequestHandlers(...)`.
- `UniversalBlePeripheralClient` is the recommended API; `UniversalBlePeripheral` remains as a singleton facade.

### Platform notes

- 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](#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.
Expand Down Expand Up @@ -692,7 +887,21 @@ await UniversalBle.startScan();

### iOS / macOS

Add `NSBluetoothPeripheralUsageDescription` and `NSBluetoothAlwaysUsageDescription` to Info.plist of your iOS and macOS app.
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:

```xml
<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.

Add the `Bluetooth` capability to the macOS app from Xcode.

Expand Down Expand Up @@ -825,7 +1034,7 @@ Future<void> resetBleState() async {

// Check Bluetooth availability
AvailabilityState availabilityState =
await UniversalBle.getBluetoothAvailabilityState();
await UniversalBle.getAvailabilityState();

// Skip if Bluetooth is not powered on
if (availabilityState != AvailabilityState.poweredOn) {
Expand Down Expand Up @@ -865,7 +1074,12 @@ Future<void> resetBleState() async {

## Example app

This repo includes an [example app](example/) you can run to try the API. For a full-blown app, check [Universal-BLE](https://github.com/Navideck/Universal-BLE).
This repo includes an [example app](example/) with two tabs:

- `Client`: scanning and device communication workflows.
- `Peripheral`: peripheral server and advertising workflows.

For a full-blown app, check [Universal-BLE](https://github.com/Navideck/Universal-BLE).

## Low level API

Expand Down
Loading
Loading