Peripheral
Overview
Section titled “Overview”IPeripheral represents a BLE device discovered during scanning. It provides connection management and GATT operations.
| Property | Type | Description |
|---|---|---|
Uuid |
string |
Unique identifier for this peripheral |
Name |
string? |
Local name (may be null) |
Mtu |
int |
Usable payload per GATT operation - the negotiated ATT MTU minus the 3-byte ATT header. Starts at 20 |
Status |
ConnectionState |
Current connection state |
Connecting
Section titled “Connecting”IPeripheral peripheral; // from scan result
// Fire and forget - connects when in rangeperipheral.Connect();
// Async - waits for connection to establishawait peripheral.ConnectAsync(cancelToken: cts.Token, timeout: TimeSpan.FromSeconds(10));Auto Connect
Section titled “Auto Connect”AutoConnect (on by default) keeps the peripheral connected for you - the connection is re-established when the
peripheral comes back into range, is power-cycled, or the user toggles Bluetooth off and on, without you having to
watch WhenDisconnected() and call Connect() again.
peripheral.Connect(new ConnectionConfig(AutoConnect: true));
// opt out - faster initial connection, but you own reconnectingperipheral.Connect(new ConnectionConfig(AutoConnect: false));Setting false speeds up the initial connection (the OS connects to the peripheral it can see right now instead of
arming a background connect), at the cost of reconnecting yourself.
An explicit CancelConnection() is final - it tears the auto-reconnect down, so a deliberate disconnect never
reconnects behind your back. Call Connect() again to arm it once more.
Bluetooth turned off and on
Section titled “Bluetooth turned off and on”Applies to iOS, Mac Catalyst, macOS, and Android. Users toggle Bluetooth in Settings - often because something is
misbehaving - and neither OS reports the resulting drop per peripheral. CoreBluetooth does not call
didDisconnectPeripheral when the adapter powers down, and several Android devices deliver no GATT connection-state
callback either. Shiny watches the adapter itself and treats a power-down as a disconnect:
- Every peripheral that was
ConnectedorConnectinggets the full teardown a real disconnect gets - notifiers cleared, in-flight operations broken so the operation queue is not left holding its lock, and on Android the GATT client closed and service discovery re-armed. WhenStatusChanged()emitsDisconnected, so the status stream agrees withIPeripheral.Status(which reads the platform live and reportedDisconnectedall along).- When the adapter comes back, every peripheral connected with
AutoConnect: trueis reconnected.
Connects issued while the adapter is off are parked, not dropped. ConnectPeripheral below PoweredOn on Apple
and ConnectGatt with the adapter off on Android are silent no-ops that never report back, so Shiny holds the request
and replays it when the adapter returns - your own Connect() from OnAdapterStateChanged included. A
CancelConnection() discards anything parked, as you would expect.
Starting a scan while a peripheral is waiting to reconnect is safe: Scan() prunes the manager’s peripheral cache, but
it no longer evicts a peripheral with an armed auto-reconnect or a parked connect. Scanning as a fallback while the link
is down - a natural thing to do - will not quietly cancel the reconnect.
Disconnecting
Section titled “Disconnecting”peripheral.CancelConnection();
// or asyncawait peripheral.DisconnectAsync();Always call CancelConnection() when you’re done with a peripheral. Not doing so will leave the connection open and drain the device battery.
Monitoring Connection Status
Section titled “Monitoring Connection Status”peripheral .WhenStatusChanged() .Subscribe(state => { // ConnectionState: Connecting, Connected, Disconnecting, Disconnected });
// Convenience extensionsperipheral.WhenConnected().Subscribe(p => { /* connected */ });peripheral.WhenDisconnected().Subscribe(p => { /* disconnected */ });Connection Failures
Section titled “Connection Failures”peripheral .WhenConnectionFailed() .Subscribe(ex => { // BleException with details about the failure });MTU Negotiation
Section titled “MTU Negotiation”The ATT MTU (Maximum Transmission Unit) determines how much data fits in a single GATT operation. Every BLE link starts at the spec minimum of 23 bytes, of which 3 are the ATT header - so 20 bytes of payload.
// Check if ATT MTU requests are supportedif (peripheral.CanRequestMtu()){ // 512 is the requested ATT MTU; the result is the usable payload var payloadSize = await peripheral.TryRequestMtuAsync(512); Console.WriteLine($"Usable payload: {payloadSize} bytes"); // 509 when 512 is granted}
// Fragment to this value as-isforeach (var chunk in data.Chunk(peripheral.Mtu)) await peripheral.WriteCharacteristicAsync(serviceUuid, charUuid, chunk);Need the ATT MTU itself - to hand to a peer protocol that negotiates its own framing, for example? Add the header back:
var attMtu = peripheral.Mtu + BleConstants.AttHeaderSize;Pairing
Section titled “Pairing”// Check if pairing is availableif (peripheral.IsPairingRequestsAvailable()){ var result = await peripheral .TryPairingRequest() .ToTask();
if (result == true) Console.WriteLine("Paired successfully");}
// Check current pairing statusvar status = peripheral.TryGetPairingStatus();// PairingState: NotPaired, PairedReading RSSI
Section titled “Reading RSSI”var rssi = await peripheral.ReadRssiAsync();Console.WriteLine($"RSSI: {rssi} dBm");Native Platform Objects
Section titled “Native Platform Objects”Diagnostics and inspection tools (a debug overlay, an agent such as MAUI DevFlow) can get the platform object behind the manager or a peripheral without casting to each platform’s class:
object? central = bleManager.GetNativeObject();object? device = peripheral.GetNativeObject();
// or typed, in platform codevar cb = peripheral.GetNativeObject<CoreBluetooth.CBPeripheral>();| Platform | IBleManager |
IPeripheral |
|---|---|---|
| iOS, Mac Catalyst, macOS | CBCentralManager (null until Bluetooth is first used) |
CBPeripheral |
| Android | BluetoothManager |
BluetoothDevice |
| Windows | BluetoothAdapter of the selected radio (null until a radio is resolved) |
BluetoothLEDevice (null once the connection is cancelled) |
| Linux | BlueZ adapter object path, e.g. /org/bluez/hci0 (null until the bus is first used) |
BlueZ device object path, e.g. /org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF |
| Blazor WebAssembly | null | null |
Reading these never creates anything: on Apple it doesn’t create the CBCentralManager or raise the Bluetooth permission prompt. They come from INativeBleManager and INativePeripheral, and GetNativeObject() returns null when the platform has neither.
To find out when the manager’s object appears, subscribe to WhenNativeObjectLoaded(). It emits the object straight away if it already exists, then again when the platform creates it. That happens the first time the app uses Bluetooth on Apple and Linux, and each time a radio is selected on Windows. Subscribing doesn’t create the object, and on Blazor WebAssembly the observable completes without emitting.
bleManager .WhenNativeObjectLoaded() .Subscribe(native => inspector.Attach(native));The observable wraps the INativeBleManager.NativeObjectLoaded event. If you use the event directly, hook it before reading NativeObject, because it never fires on Android: the BluetoothManager exists from the moment the manager is constructed. An exception thrown by a handler is logged and doesn’t affect the Bluetooth call that created the object.
Each platform class also has typed properties. Android has Native and Gatt. Apple has Manager and Native, and reading Manager does create the central. Windows has SelectedRadio, SelectedAdapter, Watcher, Native and DeviceInfo. Linux has Connection, AdapterObjectPath and ObjectPath.
Supplying your own native manager
Section titled “Supplying your own native manager”SetNativeObjectAsync hands Shiny a native object you created yourself. Call it before the app first uses Bluetooth: once the manager exists it throws InvalidOperationException, and the wrong type throws ArgumentException. It raises NativeObjectLoaded and logs a warning.
// before anything scans, connects or requests accessawait bleManager.SetNativeObjectAsync(myCentralManager);| Platform | Accepts | Typed method on BleManager |
Notes |
|---|---|---|---|
| iOS, Mac Catalyst | CBCentralManager |
SetManager(central) |
Shiny becomes its delegate. With an IBleDelegate registered, create it with a restore identifier, or background restoration is lost. |
| macOS | CBCentralManager |
SetManager(central) |
Shiny becomes its delegate. |
| Android | BluetoothManager |
SetNative(manager) |
Only before the first scan or peripheral. |
| Windows | BluetoothAdapter |
SetAdapterAsync(adapter) |
Must support Low Energy, and can’t be set while scanning. It selects where access and radio state come from. The advertisement watcher always scans on the system default adapter. |
| Linux | Adapter object path string, or DBusConnection |
SetConnection(connection, adapterObjectPath) |
Use the path to pick another adapter, such as /org/bluez/hci1. The connection must already be connected, and Shiny won’t dispose it. |
| Blazor WebAssembly | — | — | Throws NotSupportedException. |


