Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

BluetoothLE Releases

FixAndroid
ConnectionConfig.AutoConnect now actually reconnects a dropped link on Android. ConnectGatt’s autoConnect flag only keeps the platform’s background reconnect pending while the GATT client stays open, and OnConnectionStateChange has to close that client on every disconnect - closing it is what releases Android’s 7-client limit and terminates in-flight operations. Nothing re-opened it, so a peripheral that was powered off or carried out of range stayed Disconnected indefinitely even though the option is documented as enabling automatic reconnection. Connect(new ConnectionConfig(AutoConnect: true)) now arms a subscription that re-issues ConnectGatt when the link drops - the same shape Apple has always had - so the platform’s own background reconnect is restored rather than replaced by a retry loop: one ConnectGatt is issued and Android holds it pending until the peripheral returns. Reconnect attempts are throttled to one per second so a peripheral that rejects the reconnect cannot produce a status 133 connect/disconnect storm, an explicit CancelConnection() tears the subscription down before it can undo the cancel, and a Disconnected emitted while a fresh connect is already in flight is ignored. Fixes #1647.
FixAndroid
IBleManager.GetKnownPeripheral(uuid) now resolves a persisted identifier without a scan. The lookup only ever searched the in-process peripheral cache, which is populated by scanning, so after a process restart it could never hit and reconnecting to a saved device required a fresh scan on every cold start. It now falls back to BluetoothAdapter.GetRemoteDevice - decoding the MAC back out of the identifier that Peripheral.GetUuid encoded it into - and returns the same cached peripheral instance a later scan would hand back, matching what Apple already does with RetrievePeripheralsWithIdentifiers. Note that Android has no way to ask whether the OS has seen an unbonded LE device, so a well-formed identifier now always resolves and an absent peripheral fails at connect rather than at lookup.
Fix
IPeripheral.Mtu is documented correctly at last. Mtu, ICanRequestMtu.RequestMtu, TryRequestMtu, and TryRequestMtuAsync all described their value as the “negotiated MTU”, but every platform returns the usable ATT payload - the negotiated ATT MTU minus the 3-byte ATT header. Anyone who took the docs literally and subtracted the header before fragmenting removed it twice; worse, anyone who “corrected” working code on the strength of the docs, or fed Mtu to an API that genuinely wants an ATT MTU, ended up 3 bytes over the link limit, which some stacks silently truncate rather than reject. The behaviour is unchanged on Android, Apple, and Windows - Shiny.BluetoothLE’s own WriteCharacteristicBlob has always chunked to Mtu as a payload - only the XML docs and the site were wrong. Note the deliberate asymmetry: RequestMtu(512) takes an ATT MTU (it goes straight to BluetoothGatt.requestMtu()) and emits 509, the payload.
FixLinux
IPeripheral.Mtu on BlueZ reported 512 - an ATT MTU, not the payload every other platform returns. Code that fragmented to peripheral.Mtu was writing 3 bytes over the link’s actual limit on every operation. It now reports 509, matching the contract above.
Feature
BleConstants (in Shiny.BluetoothLE.Common, so it is available to both the client and hosting packages) exposes AttHeaderSize (3), DefaultAttMtu (23), and DefaultPayloadSize (20) for converting between the two units without a magic number - var attMtu = peripheral.Mtu + BleConstants.AttHeaderSize;.
Feature
L2CAP file transfers - upload & download. peripheral.UploadFile(psm, path, ...) and peripheral.DownloadFile(psm, remoteName, localPath, ...) open a channel, move the file, and close it again; UploadFileWithProgress / DownloadFileWithProgress are the Rx flavours. They speak a small framed protocol so both peers agree on the file name and the exact byte count before any body bytes move - which is what makes percent-complete and ETA real on both ends, matching the metrics Shiny.Net.Http reports. Stream overloads are available on both directions, and L2CapTransferOptions tunes buffer size, progress interval, and idle timeout. Pair with IBleHostingManager.OpenL2CapFileServer(...) on the peripheral.
Feature
OpenL2CapChannelAsync(psm, secure, ct) - awaitable form of ICanL2Cap.OpenL2CapChannel that throws NotSupportedException on platforms without L2CAP instead of returning an empty observable. Use it to move several files over one channel: channel.UploadFile(...) / channel.DownloadFile(...) are the same helpers, minus the open/close per file.
Feature
A central can now also serve transfers. channel.ReadFileRequest(...) waits for the peer’s next request and returns an L2CapFileRequest to answer with AcceptUpload / AcceptDownload / Reject. New supporting types L2CapTransferResult (bytes, elapsed, average throughput), L2CapTransferOptions, L2CapTransferError, and L2CapTransferException live in Shiny.BluetoothLE.Common, so the same surface works from the central and hosting sides.
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, and a short read could trail stale bytes from the previous chunk. Each write now gets its own array.
FixiOS
A disconnect that races an in-flight GATT operation no longer permanently deadlocks the BLE operation queue. CoreBluetooth delegate callbacks are the only thing that completes a queued operation, and a dead peripheral never fires them again — so an operation parked on one (most visibly a WriteCharacteristic(withResponse: false) waiting on canSendWriteWithoutResponse flow control) held the operation lock forever, blocking every subsequent operation across all peripherals until the app was restarted. Every queued Apple operation — service/characteristic/descriptor discovery, reads, both write modes, and ReadRssi — now aborts with a BleException when the peripheral disconnects mid-operation, releasing the lock so reconnect and retry work normally. Applies to iOS, Mac Catalyst, and macOS.
FixAndroid
The same disconnect-mid-operation deadlock is fixed on Android. Once the link drops, the BluetoothGatt client is closed and its callbacks never fire again, so a parked read, write-with-response, descriptor read/write (including the CCCD write that re-arms notifications on reconnect), service discovery, or ReadRssi held the operation lock indefinitely. These now abort with a BleException on disconnect — including the explicit disconnect path, where Gatt.Close() suppresses the framework’s own state-change callback. Write-without-response was never affected on Android; it does not wait on flow control.
FixAndroid
NotifyCharacteristic no longer drops the first notification(s) for peripherals that stream data the instant the subscription is enabled. The notification listener is now wired up before the CCCD descriptor write turns the peripheral on (it was previously hooked only after the write was acknowledged, leaving a window where the earliest OnCharacteristicChanged callbacks were lost).
FixiOS
NotifyCharacteristic now subscribes to characteristic updates before calling SetNotifyValue(true), closing the same notify-before-listen race on Apple platforms (iOS, Mac Catalyst, macOS).
FixAndroid
Scanning now discovers devices again. A previous change forced setLegacy(false) alone on Android 8+, which on most chipsets suppresses the legacy advertisements that virtually all BLE peripherals send. Scans now report both legacy AND Bluetooth 5 extended advertisements automatically, by pairing setLegacy(false) with all-PHY scanning and only enabling it when the chipset reports IsLeExtendedAdvertisingSupported (otherwise falling back to a legacy scan). Force a legacy-only scan with new AndroidScanConfig(IncludeExtendedAdvertisements: false).
FixAndroid
RequestAccess() no longer crash the app ~5 seconds after the OS permission dialog appears. The internal 5-second guard was incorrectly timing the user’s interaction with the dialog; it now only bounds the wait for an Activity to become available, so the user may take as long as they like to respond.
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). 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 new Shiny.BluetoothLE.TransferProgress record mirrors Shiny.Net.Http.TransferProgress so consumers have an identical mental model across HTTP and L2CAP transfers. Lives in Shiny.BluetoothLE.Common, shared with the hosting library.
Feature
L2CAP CoC central-role support shipped via the optional ICanL2Cap capability on IPeripheral. Call peripheral.OpenL2CapChannel(psm, secure) (or the safe TryOpenL2CapChannel(...) extension on the base IPeripheral) to open a streaming channel to a peripheral that has published a PSM. Supported on iOS, Mac Catalyst, macOS (CoreBluetooth CBPeripheral.OpenL2CapChannel), Android API 29+ (BluetoothDevice.CreateL2capChannel / CreateInsecureL2capChannel), and Linux (BlueZ — raw AF_BLUETOOTH / BTPROTO_L2CAP / SOCK_SEQPACKET socket; LE dynamic PSMs ≥ 0x80 do not need CAP_NET_RAW). On Apple platforms the secure flag is ignored — security is determined by how the peripheral published the channel. On Linux the flag toggles BT_SECURITY_LOW/MEDIUM via setsockopt(SOL_BLUETOOTH, BT_SECURITY).
Enhancement
The public L2CapChannel record moved into Shiny.BluetoothLE.Common (namespace Shiny.BluetoothLE) so both central and hosting libraries share one type. The record now implements IDisposable with an optional OnDispose hook for closing streams and disposing sockets.
FixAndroid
Shiny.BluetoothLE.Extensions.ListenForData(BluetoothSocket) now reads from socket.InputStream (was incorrectly reading OutputStream) and emits a right-sized copy of each chunk instead of the full 8 KB buffer. The observable now completes on EOF and surfaces read errors via OnError.
FixiOS
Shiny.BluetoothLE.Extensions.ListenForData(NSInputStream) now drains all bytes available per HasBytesAvailable event, emits a right-sized copy per read (was leaking the full 8 KB shared buffer to every subscriber), and completes on NSStreamEvent.EndEncountered.
Feature
macOS support added via CoreBluetooth (central role) - Shiny.BluetoothLE
Feature
Linux support added via BlueZ / D-Bus (central role) - new Shiny.BluetoothLE.Linux package
Feature
Blazor WebAssembly (Web) support added via the browser Web Bluetooth API - new Shiny.BluetoothLE.Blazor package. Central role only; scans require a user gesture, HTTPS, and a Chromium-based browser
FixWindows
Fix BLE state cleanup after disconnect and reconnect - properly releases GATT resources and disposes stale peripherals
FixAndroid
Fix the classic “status 133 after a few reconnects” trap — Connect() now closes any prior BluetoothGatt before opening a new client, so reconnect loops no longer leak GATT clients into Android’s per-app limit.
FixAndroid
Connection state and connection-failure observables are now replay-safe (BehaviorSubject / time-windowed ReplaySubject), so subscribers that hook up immediately after calling Connect() no longer miss the resulting state change or failure.
FixAndroid
OnConnectionStateChange now dispatches subscriber notifications off the single-threaded GATT binder callback thread, removing a class of deadlocks where awaiting subscribers blocked further BLE callbacks.
FixAndroid
Notifier teardown re-resolves the characteristic against the current BluetoothGatt instead of a captured (possibly closed) reference, preventing spurious status 133 errors on the next operation after a disconnect/reconnect cycle.
FixAndroid
Starting a scan no longer evicts peripherals that are currently in the Connecting state.
FixiOS
IBleManager.GetKnownPeripheral(uuid) now calls CoreBluetooth’s RetrievePeripheralsWithIdentifiers, so callers can reconnect to a previously-paired device by UUID after a process restart without first running a scan.
FixiOS
IBleManager.GetConnectedPeripherals() now seeds from RetrieveConnectedPeripherals so devices connected by other apps or restored sessions are visible.
FixiOS
Auto-reconnect now issues CancelPeripheralConnection before each retry (iOS otherwise holds the previous pending connection slot) and additionally retries on FailedToConnectPeripheral, fixing cold-start failures that previously never retried because no Disconnected event was emitted.
FixWindows
Service and characteristic lookups on the hot path now use BluetoothCacheMode.Uncached, so a reconnect always re-discovers fresh GattDeviceService / GattCharacteristic handles instead of silently operating on dead, OS-cached ones.
FixWindows
Peripheral connections now acquire a GattSession with MaintainConnection = true, so the OS keeps the LE link up across idle periods and automatically re-establishes it when a device returns in range — eliminating “the connection dropped while idle” symptoms.
FixWindows
IPeripheral references now survive a disconnect/reconnect cycle. Previously the manager replaced the wrapper instance on every reconnect, leaving any caller-held reference dead with “Device is disposed” errors. The wrapper now refreshes its underlying BluetoothLEDevice in place.
FixWindows
A transient ConnectionStatus = Disconnected dip immediately after a successful service discovery no longer cancels the connect attempt — the connect now succeeds, and a real subsequent disconnect is handled by the normal ConnectionStatusChanged path.
Feature
Windows support added (No Background Support at this time)
Enhancement
ManagedScanResult is now passed with the full advertisement data in case user needs access to native internals
EnhancementAndroid
Improved manufacturer data parsing in ad data
FixAndroid
BLE Delegate now reports proper status changes for enabled
Fix
ManagedScan now uses thread safe BindingList
FixAndroid
BLE scan now disables legacy scanning for new android versions
Fix
More thread safetying for ManagedScan
FixAndroid
Ensure peripheral cleanup matches iOS
FixAndroid
BLE Delegate was not responding with Available when adapter was reenabled
FixAndroid
Disable legacy scanner on newer Android versions
FixAndroid
ManagedScanResult now has a property for the raw advertisement data
FixAndroid
Additional thread safety on managed scan events
FixAndroid
IBleDelegate now reports adapter state properly
FixAndroid
BleDelegate does not fire for disconnected event on Android
FixAndroid
Unsubscribing from a connection may be temporarily unstable if sub/unsub is performed rapidly
FixAndroid
Reduce logging severity for characteristic events
FixiOS
IsScanning flag was not being set
Fix
Characteristic extension (GetAllCharacteristics) was only returning characteristics from last service
Fix
Characteristic async extension signature fixes
Enhancement
BLE manager now allows you to check current permissions without requesting
EnhancementAndroid
RequestAccess(bool connect) now allows you to additionally request access to GATT connections (defaults to true). This allows Shiny to use Android API 31 properly. It will always ask for scan permissions.
BREAKINGAndroid
Adapter control is no longer support through the Shiny API, but you do have raw access to the native adapter if needed
BREAKING
Managed scan now require you to set scan configuration values in Start instead of the constructor & property setters
BREAKING
The API has been simplified and no longer requires you to maintain (and refresh) instances of services/characteristics/descriptors
BREAKING
Managed peripheral is now gone. This functionality is now built into the main API.
BREAKINGEnhancementAndroid
Android MTU requests are moved to the IPeripheral.Connect(AndroidConnectionConfig)