Skip to content
Shiny.NET
Shiny MAUI Shell v7 - App Links, App Shortcuts, & Navigation Interception!Shortcut me to it

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");