Skip to content
Shiny.NET
Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans

Peripheral

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
IPeripheral peripheral; // from scan result
// Fire and forget - connects when in range
peripheral.Connect();
// Async - waits for connection to establish
await peripheral.ConnectAsync(cancelToken: cts.Token, timeout: TimeSpan.FromSeconds(10));

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 reconnecting
peripheral.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.

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 Connected or Connecting gets 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() emits Disconnected, so the status stream agrees with IPeripheral.Status (which reads the platform live and reported Disconnected all along).
  • When the adapter comes back, every peripheral connected with AutoConnect: true is 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.

peripheral.CancelConnection();
// or async
await 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.

peripheral
.WhenStatusChanged()
.Subscribe(state =>
{
// ConnectionState: Connecting, Connected, Disconnecting, Disconnected
});
// Convenience extensions
peripheral.WhenConnected().Subscribe(p => { /* connected */ });
peripheral.WhenDisconnected().Subscribe(p => { /* disconnected */ });
peripheral
.WhenConnectionFailed()
.Subscribe(ex =>
{
// BleException with details about the failure
});

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 supported
if (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-is
foreach (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;
// Check if pairing is available
if (peripheral.IsPairingRequestsAvailable())
{
var result = await peripheral
.TryPairingRequest()
.ToTask();
if (result == true)
Console.WriteLine("Paired successfully");
}
// Check current pairing status
var status = peripheral.TryGetPairingStatus();
// PairingState: NotPaired, Paired
var rssi = await peripheral.ReadRssiAsync();
Console.WriteLine($"RSSI: {rssi} dBm");

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 code
var 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.

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 access
await 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.