Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!
BluetoothLE Hosting Releases
5.5.0 - August 28, 2026
Section titled “5.5.0 - August 28, 2026”Fix
IPeripheral.Mtu and BleServiceContext.Mtu now mean the same thing on every platform. Both were documented as the “negotiated MTU” while the value returned differed by platform - Apple gave the usable ATT payload (MaximumUpdateValueLength), Android, Windows, and Linux gave the raw ATT MTU. Sizing a notification against it was therefore correct on Apple and 3 bytes over the link limit everywhere else, which some stacks silently truncate rather than reject. The property is now the usable payload - the negotiated ATT MTU minus the 3-byte ATT header - on all platforms, matching Shiny.BluetoothLE’s IPeripheral.Mtu. Cap notification and read payloads at Mtu directly; do not subtract the header again. Linux hosting’s pre-negotiation default drops from 23 to 20 for the same reason.BREAKINGFixAndroid
IPeripheral.Mtu was assigned the raw onMtuChanged value with no header subtraction, so it started life as a payload (20) and became an ATT MTU after the central negotiated - two different units on one property. It now subtracts BleConstants.AttHeaderSize consistently. If you were compensating with context.Mtu - 3, remove the subtraction; if you were using Mtu as-is you were overshooting by 3 and are now correct.BREAKINGFixWindows
IPeripheral.Mtu returned GattSession.MaxPduSize, which is the ATT MTU. It now subtracts the 3-byte ATT header. Same migration note as Android.Feature
Source generator for GATT services and L2CAP listeners. Put
[BleService] / [L2CapService] on a partial class and the generator emits the AddService(...) / OpenL2Cap(...) calls, the GattResult wrapping, the IsReplyNeeded/Respond and offset handling, the notify push API (NotifyX / XSubscribers / HasXSubscribers), and AddBleHostedServices() / AttachBleHostedServices(sp) / StartBleHostedAdvertising(name) extension methods on IBleHostingManager. Handler parameters bind by type in any order and any subset — ReadRequest/WriteRequest, the raw byte[], the offset, IPeripheral, a CancellationToken, and the generated context all just work. Nothing reflective is emitted, so it replaces the managed pattern removed in 5.0.0 without giving up AOT-cleanliness. The generator ships inside Shiny.BluetoothLE.Hosting under analyzers/dotnet/cs — nothing extra to install. See the new Source Generator page.Feature
Per-connected-central context. Each
[BleService] class gets a generated {ServiceClass}Context — a partial class you add your own properties to — created lazily per central and passed to any handler that declares it as a parameter. It carries Peripheral, ConnectionId, Mtu, ServiceUuid, Service, and a loosely typed Items bag, in the spirit of SignalR’s Hub.Context. Storage is a ConditionalWeakTable keyed on the peripheral rather than the public IPeripheral.Context slot, so it never collides with app state.Feature
[RequestResponseCharacteristic] registers a characteristic as Write | Notify and pushes the handler’s returned bytes back to the central that wrote — a GATT write response cannot carry a payload, so the reply travels as a notification addressed to that central. Implement the generated OnBleResponseDropped hook to observe the case where the central was not subscribed.Feature
[L2CapService(PsmService = ..., PsmCharacteristic = ...)] publishes the platform-assigned PSM as a GATT read characteristic (two little-endian bytes) on a service in the same compilation, which is the only in-band way a central can learn it. Listeners are opened before AddService, so a read immediately after registration returns a live value. L2CapChannel.ReadAll(cancellationToken) is a new IAsyncEnumerable convenience over the Rx DataReceived observable.Enhancement
Several
[BleService] classes may declare the same service UUID — the generator merges them into a single AddService call, which matters because BleHostingManager keys its services by UUID and would throw on a second registration. Declaring the same characteristic UUID in two merged classes is a compile error.Enhancement
Fourteen compile-time diagnostics (
SBH001-SBH014) cover invalid UUIDs, duplicate handlers for one characteristic, unbindable signatures, dangling PSM publications, and merge conflicts — mistakes that used to surface as a silent no-op or a runtime throw on device.Enhancement
Generated UUIDs are always emitted in the full 128-bit form. Short forms like
"180D" are accepted by Apple’s CBUUID.FromString but throw on Android, which goes through java.util.UUID.fromString — so a service that worked on iOS could fail on Android. This only applies to generated code; write full UUIDs yourself when calling AddService directly.5.4.0 - TBD
Section titled “5.4.0 - TBD”Feature
L2CAP file server.
IBleHostingManager.OpenL2CapFileServer(rootDirectory, secure, configure) publishes a PSM backed by a directory that connected centrals can push files to and pull files from, using the matching UploadFile / DownloadFile helpers in Shiny.BluetoothLE. Configure it with upload/download toggles, MaxUploadSize (refused with TooLarge before a single body byte is read), an overwrite policy, an Authorize hook per request, and OnProgress / OnCompleted / OnError callbacks carrying the peer identifier, file name, and transfer metrics. Peer-supplied names are resolved under the root - absolute paths and ../ traversal are refused before any filesystem access.Feature
IBleHostingManager.HandleL2CapRequests(secure, onRequest, options, onError) publishes a PSM and hands every inbound L2CapFileRequest to your own handler - for serving from a database, generating content on the fly, or any shape the directory server doesn’t cover. Answer with AcceptUpload / AcceptDownload / Reject; a request your handler leaves unanswered is auto-rejected so the peer is never left hanging.Fix
L2CapChannelExtensions.SendFile no longer risks sending corrupt bytes on slower links. The reusable read buffer was handed straight to Write, which only promises the bytes are queued - so the next chunk could overwrite bytes still in flight. Each write now gets its own array.5.0.0 - June 20, 2026
Section titled “5.0.0 - June 20, 2026”BREAKING
Managed characteristic pattern removed for AOT compliance. The
BleGattCharacteristic base class, [BleGattCharacteristic] attribute, AddBleHostedCharacteristic<T>(), AttachRegisteredServices(), and DetachRegisteredServices() are all gone. Compose GATT services in code via IBleHostingManager.AddService(uuid, primary, sb => ...) — typically inside a class registered as an IShinyStartupTask. See the GATT Service page for the new pattern.Enhancement
Rx removed from
Shiny.BluetoothLE.Hosting’s manager surface. IBleHostingManager no longer exposes any IObservable<T> members — characteristic write/read/notification hooks are async Task-based as before. The L2CapChannel record still uses Rx for its bytes-in/bytes-out streams (shared with Shiny.BluetoothLE client).Feature
L2CapChannelExtensions.SendFile(...) — new file-transfer helper on top of an open L2CapChannel with HTTP-transfer-style progress metrics (bytes-per-second, percent-complete, estimated time remaining). Useful for streaming firmware blobs and other large payloads to a connected central. Overloads accept either a file path (length auto-detected) or an arbitrary Stream with an optional totalBytes. Progress callbacks fire ~every 2s plus a final 100% emission on completion. The supporting Shiny.BluetoothLE.TransferProgress record mirrors Shiny.Net.Http.TransferProgress. Lives in Shiny.BluetoothLE.Common, shared with the central library.FixAndroid
BleHostingManager now takes AndroidPlatform through a primary constructor and eagerly initializes its GattServerContext. Previously the context field was declared readonly but never assigned, so every call into the Android hosting manager (advertising, GATT services, request-access, beacon) would have NREd at runtime once the DI container resolved the manager via its compiler-generated parameterless constructor.Feature
L2CAP CoC peripheral hosting shipped —
IBleHostingManager.OpenL2Cap(bool secure, Action<L2CapChannel> onOpen) publishes a PSM and invokes the callback for every accepted central connection. Each L2CapChannel exposes Func<byte[], IObservable<Unit>> Write and IObservable<byte[]> DataReceived, and disposes cleanly via the channel’s IDisposable. Dispose the returned L2CapInstance to stop accepting and release the PSM. Implemented on iOS, Mac Catalyst, macOS (CoreBluetooth CBPeripheralManager.PublishL2CapChannel), Android API 29+ (BluetoothAdapter.ListenUsing[Insecure]L2capChannel), and Linux (BlueZ — raw AF_BLUETOOTH socket with kernel-assigned dynamic PSM ≥ 0x80 and a background accept loop). Windows hosting throws NotSupportedException from OpenL2Cap — WinRT exposes no LE CoC surface.Enhancement
The public
L2CapChannel record moved into Shiny.BluetoothLE.Common (namespace Shiny.BluetoothLE) so both hosting and central libraries share a single type. It now also implements IDisposable with an optional OnDispose hook for platform cleanup (closing streams, releasing sockets).Feature
macOS support added - peripheral / GATT server hosting via CoreBluetooth
Feature
Linux support added via BlueZ / D-Bus - new
Shiny.BluetoothLE.Hosting.Linux package4.0.0 - March 26, 2026
Section titled “4.0.0 - March 26, 2026”Feature
Windows support added (No Background Support at this time)
3.2.0 - December 8, 2023
Section titled “3.2.0 - December 8, 2023”Enhancement
BLE Hosting manager now allows you to check current permissions without requesting
FixAndroid
Managed BLE Services won’t always auto-restart post reboot
3.0.0 - September 5, 2023
Section titled “3.0.0 - September 5, 2023”EnhancementAndroid
RequestAccess now exists - you can specifically target your permissions to take advantage of Android API 31
Enhancement
All characteristic hooks are now async
Enhancement
New “managed” model for characteristics
Enhancement
Advertise iBeacons is now supported - it exists here instead of Shiny.Beacons because all of the advertising code is here


