plugins/shiny-client/skills/shiny-bluetoothle/SKILL.md
Shiny BluetoothLE client/central operations for scanning, connecting, and communicating with BLE peripherals
npx skillsauth add shinyorg/skills shiny-bluetoothleInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill when the user needs to:
Do NOT use this skill for BLE hosting/peripheral mode (advertising, GATT server). That is a separate library (Shiny.BluetoothLE.Hosting).
Shiny.BluetoothLE (Android, iOS/macOS, Windows), Shiny.BluetoothLE.Linux (Linux via BlueZ), Shiny.BluetoothLE.Blazor (Blazor WebAssembly via Web Bluetooth API)Shiny.BluetoothLEShiny.BluetoothLE.ManagedThe Blazor implementation is built on the browser's Web Bluetooth API and inherits its limitations:
http://localhost required. The API is unavailable on plain http://.chrome://flags/#enable-web-bluetooth (or edge://flags, etc.) → Enabled → restart. Linux also needs experimental-web-platform-features on and BlueZ 5.43+.internet://flags → Web Bluetooth.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;
}
}
When generating BLE client code, follow these conventions:
Always request access before scanning: Call IBleManager.RequestAccess() or RequestAccessAsync() and verify AccessState.Available before starting a scan.
Use reactive (IObservable) APIs as the primary pattern: The library is built on System.Reactive. Use the Async extension 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 ConnectAsync for simple connection flows: It handles waiting for the connected state and has a default 30-second timeout.
Always call CancelConnection() or DisconnectAsync() when done: Connections are not automatically cleaned up.
Use IManagedScan for UI-bound scanning: It provides an INotifyReadOnlyCollection that 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* or Can* extension methods rather than casting directly.
Handle BleException and BleOperationException: GATT operations can throw these. BleOperationException includes a GattStatusCode.
Connection auto-reconnect: ConnectionConfig.AutoConnect = true (default) enables automatic reconnection. Set to false for faster initial connections.
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.
using Shiny.BluetoothLE;
if (peripheral.IsL2CapAvailable())
{
// Backend supports L2CAP
}
// 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).
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.
CBPeripheral.OpenL2CapChannel. The secure flag is ignored — security is set by how the peripheral published the channel.BluetoothDevice.CreateL2capChannel / CreateInsecureL2capChannel. Requires API 29+. Throws InvalidOperationException on older versions.IsL2CapAvailable() returns false).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
);
Stream overload exists for non-file sources. Pass totalBytes to enable percent / ETA; pass null and IsDeterministic will be false, PercentComplete returns -1, EstimatedTimeRemaining returns TimeSpan.Zero.IPeripheral: Both Shiny.BluetoothLE and Shiny.BluetoothLE.Hosting define an IPeripheral interface. If both packages are referenced, do NOT add Shiny.BluetoothLE.Hosting as a global using. Use file-level using or FQN (Shiny.BluetoothLE.IPeripheral) to disambiguate.DeviceInfo: Shiny.BluetoothLE has a DeviceInfo class that conflicts with Microsoft.Maui.Devices.DeviceInfo in MAUI apps. Use FQN when needed.ScanConfig with ServiceUuids to filter scans, especially on iOS where background scanning requires a service UUID filter.AndroidScanConfig for scan mode and batching options.AndroidConnectionConfig for connection priority settings.CharacteristicProperties before attempting read/write/notify operations using the convenience extensions (CanRead(), CanWrite(), CanNotify(), etc.).WriteCharacteristicBlob() for writing large data streams that exceed MTU size.NotifyCharacteristic() for real-time data streaming from a peripheral -- it handles subscription lifecycle and auto-reconnection.WhenConnected() and WhenDisconnected() convenience extensions for cleaner connection state handling.devops
Guide for implementing push notifications in .NET MAUI apps using Shiny.Push (native FCM/APNs) and Shiny.Push.AzureNotificationHubs
tools
Cross-platform local notification management for .NET MAUI apps using Shiny, supporting scheduled, repeating, and geofence-triggered notifications with channels, badges, and interactive actions.
tools
GPS tracking, geofence monitoring, and motion activity recognition for .NET MAUI, iOS, and Android using Shiny.Locations
data-ai
Background job scheduling and execution for .NET MAUI (iOS/Android native OS schedulers) and in-process jobs for plain .NET, Linux, macOS, and Blazor WASM using Shiny.Jobs