name: shiny-bluetoothle description: Shiny BluetoothLE client/central operations for scanning, connecting, and communicating with BLE peripherals auto_invoke: true triggers: - bluetooth - ble - bluetoothle - bluetooth le - bluetooth low energy - peripheral - gatt - characteristic - scan ble - ble scan - ble connect - IBleManager - IPeripheral - managed scan - ble notification - ble write - ble read - ble descriptor - advertisement - L2CAP - L2Cap - L2CapChannel - ICanL2Cap - OpenL2CapChannel - PSM
Shiny BluetoothLE (Client/Central)
When to Use This Skill
Use this skill when the user needs to:
- Scan for BLE peripherals
- Connect to and communicate with BLE devices
- Read, write, or subscribe to GATT characteristics
- Read or write GATT descriptors
- Implement managed scans with automatic peripheral list management
- Request MTU changes, pair with devices, or perform reliable write transactions
- Read standard BLE services (device information, battery, heart rate)
- Work with BLE advertisement data
- Open L2CAP CoC channels to a peripheral that has published a PSM
Do NOT use this skill for BLE hosting/peripheral mode (advertising, GATT server). That is a separate library (Shiny.BluetoothLE.Hosting).
Library Overview
- NuGet Package:
Shiny.BluetoothLE(Android, iOS/macOS, Windows),Shiny.BluetoothLE.Linux(Linux via BlueZ),Shiny.BluetoothLE.Blazor(Blazor WebAssembly via Web Bluetooth API) - Primary Namespace:
Shiny.BluetoothLE - Managed Scan Namespace:
Shiny.BluetoothLE.Managed - Platforms: Android, iOS/macOS (Apple), Windows, Linux (BlueZ), WebAssembly (Web Bluetooth)
Blazor WebAssembly / Web Bluetooth caveats
The Blazor implementation is built on the browser's Web Bluetooth API and inherits its limitations:
- User-gesture gated. Scans must be kicked off from a click handler. The browser shows a native chooser and Shiny only sees the peripheral(s) the user explicitly selects — there is no ambient/background scanning and no manufacturer data.
- HTTPS or
http://localhostrequired. The API is unavailable on plainhttp://. - No background operation. Scanning and connections stop when the tab is backgrounded or closed.
- Browser support is Chromium-only and requires enabling in some cases. When generating setup instructions or troubleshooting guidance, note the following:
- Chrome / Edge / Brave / Opera (desktop): enabled by default on Windows, macOS, Linux, ChromeOS. Fallback:
chrome://flags/#enable-web-bluetooth(oredge://flags, etc.) → Enabled → restart. Linux also needsexperimental-web-platform-featureson and BlueZ 5.43+. - Chrome / Edge (Android): Android 6.0+. OS location services must be on for the chooser prompt to appear.
- Samsung Internet: enable
internet://flags→ Web Bluetooth. - Safari (macOS / iOS / iPadOS): not supported. On iOS/iPadOS suggest third-party WKWebView-based browsers Bluefy or WebBLE. Stock macOS Safari has no workaround.
- Firefox: not supported on any platform.
- Chrome / Edge / Brave / Opera (desktop): enabled by default on Windows, macOS, Linux, ChromeOS. Fallback:
Setup
Register in your MauiProgram.cs or host builder:
// Basic registration
services.AddBluetoothLE();
// With a delegate for background events (adapter state changes, peripheral connections)
services.AddBluetoothLE<MyBleDelegate>();
// iOS/macOS only - with Apple-specific configuration
services.AddBluetoothLE<MyBleDelegate>(new AppleBleConfiguration(
ShowPowerAlert: true,
RestoreIdentifier: "my-ble-app"
));
The delegate class:
public class MyBleDelegate : BleDelegate
{
public override Task OnAdapterStateChanged(AccessState state)
{
// Handle adapter state changes (foreground or background)
return Task.CompletedTask;
}
public override Task OnPeripheralStateChanged(IPeripheral peripheral)
{
// Handle peripheral connection state changes (foreground or background)
return Task.CompletedTask;
}
}
Code Generation Instructions
When generating BLE client code, follow these conventions:
Always request access before scanning: Call
IBleManager.RequestAccess()orRequestAccessAsync()and verifyAccessState.Availablebefore starting a scan.Use reactive (IObservable) APIs as the primary pattern: The library is built on System.Reactive. Use the
Asyncextension methods only when you need Task-based patterns.Dispose scan subscriptions: Only one scan can be active at a time. Always dispose the scan subscription or call
StopScan()when done.Use string-based UUIDs for services and characteristics: The API uses string UUIDs throughout (e.g.,
"180D"or"0000180d-0000-1000-8000-00805f9b34fb").Prefer
ConnectAsyncfor simple connection flows: It handles waiting for the connected state and has a default 30-second timeout.Always call
CancelConnection()orDisconnectAsync()when done: Connections are not automatically cleaned up.Use
IManagedScanfor UI-bound scanning: It provides anINotifyReadOnlyCollectionthat works with MVVM bindings and handles peripheral deduplication, buffering, and stale removal.Feature detection via interface checks: Optional capabilities (MTU request, pairing, reliable transactions) use feature interfaces. Always use the
Try*orCan*extension methods rather than casting directly.Handle
BleExceptionandBleOperationException: GATT operations can throw these.BleOperationExceptionincludes aGattStatusCode.Connection auto-reconnect:
ConnectionConfig.AutoConnect = true(default) enables automatic reconnection. Set tofalsefor faster initial connections.
L2CAP Channels
Some platforms support L2CAP Connection-Oriented Channels for streaming data without going through GATT. This is exposed as an optional capability — ICanL2Cap — on the platform Peripheral types.
Feature detection
using Shiny.BluetoothLE;
if (peripheral.IsL2CapAvailable())
{
// Backend supports L2CAP
}
Opening a channel
// Safe variant — returns an empty observable on unsupported platforms
peripheral
.TryOpenL2CapChannel(psm: 0x0083, secure: false)
.Subscribe(channel => { /* ... */ });
// Direct access when the cast succeeds
if (peripheral is ICanL2Cap l2cap)
{
l2cap.OpenL2CapChannel(psm: 0x0083, secure: false).Subscribe(channel =>
{
// channel.Psm — the PSM the channel was opened on
// channel.Identifier — the remote peer identifier
// channel.DataReceived — IObservable<byte[]> of incoming bytes
// channel.Write(bytes) — IObservable<Unit> that completes when bytes are queued
});
}
L2CapChannel implements IDisposable — dispose it to close the underlying streams (Apple) or socket (Android).
Reading and writing
using System.Reactive.Threading.Tasks;
channel.DataReceived.Subscribe(
payload => Console.WriteLine($"<- {payload.Length} bytes"),
ex => Console.WriteLine($"Channel error: {ex.Message}"),
() => Console.WriteLine("Remote closed the channel")
);
await channel.Write(payload).ToTask();
DataReceived is hot, emits right-sized byte arrays per read, completes on remote close, and surfaces I/O errors via OnError.
Platform notes
- iOS / Mac Catalyst / macOS:
CBPeripheral.OpenL2CapChannel. Thesecureflag is ignored — security is set by how the peripheral published the channel. - Android:
BluetoothDevice.CreateL2capChannel/CreateInsecureL2capChannel. Requires API 29+. ThrowsInvalidOperationExceptionon older versions. - Windows / Linux / Blazor: not currently supported (
IsL2CapAvailable()returns false).
File Transfer
L2CapChannelExtensions.SendFile(...) streams a file over the channel with progress metrics (throughput, percent-complete, estimated time remaining) that match Shiny.Net.Http.TransferProgress:
using Shiny.BluetoothLE;
await channel.SendFile(
"/path/to/file.bin",
bufferSize: 4096,
onProgress: p => Console.WriteLine(
$"{p.PercentComplete:P0} ({p.BytesTransferred}/{p.BytesToTransfer}) " +
$"{p.BytesPerSecond / 1024} KB/s, ETA {p.EstimatedTimeRemaining}"
),
cancellationToken: ct
);
- Progress emissions cadence ~2s plus a final 100% emission on completion.
- A
Streamoverload exists for non-file sources. PasstotalBytesto enable percent / ETA; passnullandIsDeterministicwill be false,PercentCompletereturns-1,EstimatedTimeRemainingreturnsTimeSpan.Zero.
Namespace Ambiguities
IPeripheral: BothShiny.BluetoothLEandShiny.BluetoothLE.Hostingdefine anIPeripheralinterface. If both packages are referenced, do NOT addShiny.BluetoothLE.Hostingas a global using. Use file-levelusingor FQN (Shiny.BluetoothLE.IPeripheral) to disambiguate.DeviceInfo:Shiny.BluetoothLEhas aDeviceInfoclass that conflicts withMicrosoft.Maui.Devices.DeviceInfoin MAUI apps. Use FQN when needed.
Best Practices
- Use
ScanConfigwithServiceUuidsto filter scans, especially on iOS where background scanning requires a service UUID filter. - For Android, consider
AndroidScanConfigfor scan mode and batching options. - For Android, consider
AndroidConnectionConfigfor connection priority settings. - Always check
CharacteristicPropertiesbefore attempting read/write/notify operations using the convenience extensions (CanRead(),CanWrite(),CanNotify(), etc.). - Use
WriteCharacteristicBlob()for writing large data streams that exceed MTU size. - Use
NotifyCharacteristic()for real-time data streaming from a peripheral -- it handles subscription lifecycle and auto-reconnection. - Buffer or throttle scan results in UI scenarios to avoid performance issues.
- Use
WhenConnected()andWhenDisconnected()convenience extensions for cleaner connection state handling.