plugins/shiny-client/skills/shiny-ble-hosting/SKILL.md
Generate code using Shiny.BluetoothLE.Hosting, a BLE peripheral hosting library for .NET with GATT server, advertising, and L2CAP CoC channels
npx skillsauth add shinyorg/skills shiny-ble-hostingInstall 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.
You are an expert in Shiny.BluetoothLE.Hosting, a .NET library for turning a device into a BLE peripheral. It provides a GATT server, BLE advertising, iBeacon broadcasting, and L2CAP CoC channels through the imperative IBleHostingManager API.
The attribute-based managed characteristic pattern (
BleGattCharacteristicbase class,[BleGattCharacteristic]attribute,AddBleHostedCharacteristic<T>,AttachRegisteredServices) was removed for AOT compliance. Use the imperativeAddService(...)builder pattern below — it is the single supported way to expose a GATT service.
Invoke this skill when the user wants to:
Shiny.BluetoothLE.Hosting (Android, iOS/macOS, Mac Catalyst, Windows stub), Shiny.BluetoothLE.Hosting.Linux (Linux via BlueZ)Shiny.BluetoothLE.HostingNotSupportedException for advertising/GATT-server hosting; only the OpenL2Cap API is exposed and it also throws on Windows.Shiny.Core, Shiny.BluetoothLE.CommonThe library exposes a single imperative API: inject IBleHostingManager, call AddService(uuid, primary, builder) to register a GATT service inline.
dotnet add package Shiny.BluetoothLE.Hosting
builder.Services.AddBluetoothLeHosting();
When generating code for Shiny.BluetoothLE.Hosting projects, follow these conventions:
Always request access before advertising or adding services:
var access = await hostingManager.RequestAccess();
if (access != AccessState.Available)
{
// Handle denied/disabled/not supported
return;
}
Use the builder pattern to add services and characteristics inline:
var service = await hostingManager.AddService("12345678-1234-1234-1234-123456789abc", true, sb =>
{
sb.AddCharacteristic("12345678-1234-1234-1234-123456789ab1", cb =>
{
cb.SetRead(request =>
{
var data = System.Text.Encoding.UTF8.GetBytes("Hello");
return Task.FromResult(GattResult.Success(data));
});
cb.SetWrite(request =>
{
var received = request.Data;
if (request.IsReplyNeeded)
request.Respond(GattState.Success);
return Task.CompletedTask;
}, WriteOptions.Write);
cb.SetNotification(sub =>
{
// sub.IsSubscribing tells you if subscribing or unsubscribing
// sub.Peripheral is the central device
return Task.CompletedTask;
}, NotificationOptions.Notify);
});
});
// Advertise with local name and service UUIDs
await hostingManager.StartAdvertising(new AdvertisementOptions(
LocalName: "MyDevice",
ServiceUuids: "12345678-1234-1234-1234-123456789abc"
));
// Advertise with defaults (no name, no service UUIDs)
await hostingManager.StartAdvertising();
// Stop advertising
hostingManager.StopAdvertising();
await hostingManager.AdvertiseBeacon(
uuid: Guid.Parse("12345678-1234-1234-1234-123456789abc"),
major: 1,
minor: 100,
txpower: -59
);
// From an IGattCharacteristic reference
var data = System.Text.Encoding.UTF8.GetBytes("Updated value");
// Notify all subscribed centrals
await characteristic.Notify(data);
// Notify specific centrals
await characteristic.Notify(data, specificPeripheral1, specificPeripheral2);
When WriteRequest.IsReplyNeeded is true, you must call Respond:
cb.SetWrite(request =>
{
try
{
// Process data
if (request.IsReplyNeeded)
request.Respond(GattState.Success);
}
catch
{
if (request.IsReplyNeeded)
request.Respond(GattState.Failure);
}
return Task.CompletedTask;
}, WriteOptions.Write);
Publish an L2CAP PSM that centrals can connect to for streaming data without going through GATT. OpenL2Cap returns an L2CapInstance representing the listener; the onOpen callback fires for every accepted central connection. Each L2CapChannel is itself an IDisposable — dispose it to close that specific central's channel; dispose the L2CapInstance to stop accepting new connections and release the PSM.
using System.Reactive.Threading.Tasks;
using Shiny.BluetoothLE;
using Shiny.BluetoothLE.Hosting;
var instance = await hostingManager.OpenL2Cap(
secure: false,
onOpen: channel =>
{
Console.WriteLine($"Central {channel.Identifier} connected on PSM {channel.Psm}");
channel.DataReceived.Subscribe(
async payload =>
{
// Echo back
await channel.Write(payload).ToTask();
},
ex => Console.WriteLine($"Channel error: {ex.Message}"),
() => channel.Dispose()
);
}
);
Console.WriteLine($"Listening on PSM {instance.Psm}");
// Later, when shutting down:
instance.Dispose();
The platform-assigned PSM is on instance.Psm — advertise it to centrals out-of-band (typically through a GATT characteristic exposed by your service).
Platform notes:
CBPeripheralManager.PublishL2CapChannel(encryptionRequired). The secure flag maps to encryption-required.BluetoothAdapter.ListenUsing[Insecure]L2capChannel. Requires API 29+ — throws InvalidOperationException on older versions.AF_BLUETOOTH / BTPROTO_L2CAP / SOCK_SEQPACKET socket via Shiny.BluetoothLE.Hosting.Linux. PSM is kernel-assigned from the LE dynamic range (≥ 0x80); secure=true maps to BT_SECURITY_MEDIUM, secure=false to BT_SECURITY_LOW. Independent of GATT-server / LE-advertisement hosting (still WIP on Linux) — centrals must learn the device address out-of-band.OpenL2Cap throws NotSupportedException.L2CapChannelExtensions.SendFile(...) streams a file over a connected channel with progress metrics (throughput, percent-complete, ETA) matching the Shiny.Net.Http.TransferProgress shape. Useful for pushing large blobs to a connected central:
using Shiny.BluetoothLE;
using var instance = await hostingManager.OpenL2Cap(secure: false, onOpen: async channel =>
{
await channel.SendFile(
"/path/to/firmware.bin",
bufferSize: 4096,
onProgress: p => Console.WriteLine(
$"{p.PercentComplete:P0} {p.BytesPerSecond / 1024} KB/s ETA {p.EstimatedTimeRemaining}"
)
);
channel.Dispose();
});
A Stream overload is available for non-file sources; pass totalBytes to enable percent / ETA computation.
BleHosting/ folder, one class per GATT serviceFeatures/{Feature}/{Name}HostingService.csIPeripheral: Both Shiny.BluetoothLE (client) and Shiny.BluetoothLE.Hosting define an IPeripheral interface with different members. If both packages are referenced in the same project, do NOT add both namespaces as global usings. Use file-level using directives or FQN (Shiny.BluetoothLE.Hosting.IPeripheral) to disambiguate.RequestAccess() and check the result before any hosting operationsBleGattCharacteristic pattern was removed for AOT compliance. Build services with AddService(uuid, primary, sb => ...) lambdas inside a service class registered in DI so they're easy to unit-testxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) or short 16-bit UUIDs for standard Bluetooth SIG servicesWriteRequest.IsReplyNeeded and call Respond with the appropriate GattStateGattResult.Error(GattState.Failure) in read handlers when an error occursStopAdvertising() and ClearServices() when doneStartAdvertising if already advertisingL2CapInstance and per-central L2CapChannels explicitly -- disposing the instance closes the listener but does not auto-close already-open channelsFor detailed API signatures and examples, see:
reference/api-reference.md - Full API surface, interfaces, enums, records, and usage examplesdevelopment
Guide for generating code that uses Shiny.Data.Sync for reliable, background-capable bidirectional JSON sync over HTTP on iOS, Android, Windows, Linux, macOS, and Blazor WASM
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