API Reference

Complete technical reference for Plugin.Bluetooth interfaces, types, and conventions.

API Conventions

Async-First Design

All I/O operations in Plugin.Bluetooth are asynchronous and follow .NET async/await patterns:

// Use ValueTask for commonly synchronous operations
ValueTask<bool> HasScannerPermissionsAsync();

// Use Task for operations that are always asynchronous
Task StartScanningAsync(ScanningOptions? options = null);

Key principles:

  • All async methods end with Async suffix
  • Operations that may complete synchronously use ValueTask or ValueTask<T>
  • Operations that are always asynchronous use Task or Task<T>
  • Never block on async methods (no .Wait() or .Result)

CancellationToken Support

Most async operations accept a CancellationToken parameter for cooperative cancellation:

await scanner.StartScanningAsync(
    options: scanOptions,
    timeout: TimeSpan.FromSeconds(30),
    cancellationToken: cancellationToken
);

Usage guidelines:

  • Pass CancellationToken.None if cancellation is not needed
  • Default value is always default(CancellationToken)
  • Operations throw OperationCanceledException when cancelled

Timeout Handling

Operations support optional timeout parameters to prevent indefinite waiting:

// Default timeout (varies by operation, typically 30s)
await device.ConnectAsync();

// Custom timeout
await device.ConnectAsync(
    timeout: TimeSpan.FromSeconds(10)
);

// No timeout
await device.ConnectAsync(
    timeout: Timeout.InfiniteTimeSpan
);

Timeout behavior:

  • Most operations have sensible default timeouts
  • Pass null to use the default timeout
  • Pass Timeout.InfiniteTimeSpan for no timeout
  • Operations throw TimeoutException when they exceed the timeout

State Management

Interfaces expose state through properties and events:

// Check current state
if (scanner.IsRunning)
{
    await scanner.StopScanningAsync();
}

// Listen to state changes
scanner.RunningStateChanged += (s, e) =>
{
    Console.WriteLine($"Scanner running: {scanner.IsRunning}");
};

// Conditional operations with "IfNeeded" variants
await scanner.StartScanningIfNeededAsync(); // No-op if already running
await scanner.StopScanningIfNeededAsync();  // No-op if already stopped

State properties:

  • IsRunning - Whether the operation is active
  • IsStarting - Whether the operation is starting
  • IsStopping - Whether the operation is stopping
  • IsConnecting / IsConnected / IsDisconnecting - Connection states

Memory Efficiency

The API uses modern .NET memory types for efficient data handling:

// Read characteristic value
ReadOnlyMemory<byte> value = await characteristic.ReadValueAsync();

// Use as span for zero-copy access
ReadOnlySpan<byte> span = characteristic.ValueSpan;

// Listen for value updates
characteristic.ValueUpdated += (s, e) =>
{
    ReadOnlyMemory<byte> newValue = e.NewValue;
    ReadOnlyMemory<byte> oldValue = e.OldValue;
};

Memory types:

  • ReadOnlyMemory<byte> - For asynchronous value handling
  • ReadOnlySpan<byte> - For synchronous, high-performance access
  • No unnecessary allocations or copies

Disposal Pattern

All major interfaces implement IAsyncDisposable:

await using var scanner = adapter.CreateScanner();
await scanner.StartScanningAsync();
// Automatically stopped and disposed

// Or manually
IBluetoothRemoteDevice device = scanner.GetDevice(id);
await device.ConnectAsync();
// ... use device ...
await device.DisposeAsync(); // Disconnect and cleanup

Disposal guidelines:

  • Use await using for automatic cleanup
  • Dispose operations stop ongoing activities
  • Disposal is idempotent (safe to call multiple times)
  • Always dispose devices when done to free resources

Collection Management

The API exposes collections as IReadOnlyList<T> with change notification:

// Access current devices
IReadOnlyList<IBluetoothRemoteDevice> devices = scanner.Devices;

// Listen for additions
scanner.DevicesAdded += (s, e) =>
{
    foreach (var device in e.Items)
    {
        Console.WriteLine($"Found: {device.Name}");
    }
};

// Listen for removals
scanner.DevicesRemoved += (s, e) =>
{
    foreach (var device in e.Items)
    {
        Console.WriteLine($"Lost: {device.Name}");
    }
};

// Listen for any change
scanner.DeviceListChanged += (s, e) =>
{
    var added = e.AddedItems;
    var removed = e.RemovedItems;
};

Collection features:

  • Immutable snapshots via IReadOnlyList<T>
  • Fine-grained change events (Added, Removed, Changed)
  • Thread-safe enumeration
  • LINQ compatible

Permission Handling

Permission checks and requests follow a consistent pattern:

// Check without prompting
bool hasPermission = await scanner.HasScannerPermissionsAsync();

// Request permission (may show system dialog)
try
{
    await scanner.RequestScannerPermissionsAsync();
}
catch (BluetoothPermissionException ex)
{
    Console.WriteLine($"Permission denied: {ex.Message}");
    // Check InnerException for platform-specific details
}

Permission strategies:

  • Check before requesting to avoid unnecessary prompts
  • Handle BluetoothPermissionException for denials
  • Platform differences documented in XML comments
  • See PermissionRequestStrategy for configuration options

Error Handling

The API uses a structured exception hierarchy:

try
{
    await characteristic.ReadValueAsync();
}
catch (DeviceNotConnectedException ex)
{
    // Device disconnected
    await ex.Device.ConnectAsync();
}
catch (CharacteristicCantReadException ex)
{
    // Characteristic doesn't support read
    Console.WriteLine($"Can't read {ex.RemoteCharacteristic.Name}");
}
catch (CharacteristicReadException ex)
{
    // Read operation failed
    Console.WriteLine($"Read failed: {ex.Message}");
}
catch (TimeoutException)
{
    // Operation timed out
}
catch (OperationCanceledException)
{
    // Operation was cancelled
}

Exception handling:

  • Specific exceptions for each error case
  • Base classes for category-level catching
  • Context properties (Device, Characteristic, etc.)
  • Static guard methods for precondition checking
  • See Exceptions for full hierarchy

Configuration Options

Operations accept configuration through options objects:

// Scanning configuration
var scanOptions = new ScanningOptions
{
    ScanMode = BluetoothScanMode.LowLatency,
    ServiceUuids = [serviceUuid]
};
await scanner.StartScanningAsync(scanOptions);

// Connection configuration
var connectionOptions = new ConnectionOptions
{
    WaitForAdvertisementBeforeConnecting = true,
    ConnectionRetry = RetryOptions.Default
};
await device.ConnectAsync(connectionOptions);

// Broadcasting configuration
var broadcastOptions = new BroadcastingOptions
{
    LocalDeviceName = "MyDevice",
    IncludeDeviceName = true
};
await broadcaster.StartBroadcastingAsync(broadcastOptions);

See Dependency Injection and the other pages under Docs/Configuration/ for the full property reference per options class.

Options pattern:

  • Immutable (init-only) configuration records
  • Default values for all properties
  • Platform-specific options documented
  • To change options while running, stop then start again with new options (or options with { ... }) — there is no Update*OptionsAsync method on any interface

Namespaces

Bluetooth.Abstractions

Core abstractions and shared types:

  • IBluetoothAdapter - Main entry point for Bluetooth functionality
  • BluetoothException - Base exception type
  • CharacteristicProperties, CharacteristicPermissions - Core enums
  • PhyMode, Manufacturer - Hardware-related enums

Related concepts: Getting Started

Bluetooth.Abstractions.Scanning

Central/client role - scanning and connecting to peripherals:

  • IBluetoothScanner - Device discovery and scanning
  • IBluetoothRemoteDevice - Remote device connection and services
  • IBluetoothRemoteService - GATT service representation
  • IBluetoothRemoteCharacteristic - GATT characteristic read/write/notify
  • IBluetoothRemoteDescriptor - GATT descriptor access
  • Extensive exception hierarchy for error handling

Related concepts: Device Scanning, Connecting to Devices

Bluetooth.Abstractions.Broadcasting

Peripheral/server role - advertising and serving GATT services:

  • IBluetoothBroadcaster - Advertising and service management
  • IBluetoothLocalService - Local GATT service creation
  • IBluetoothLocalCharacteristic - Local characteristic with read/write callbacks
  • IBluetoothLocalDescriptor - Local descriptor management
  • IBluetoothConnectedDevice - Connected central device information

Related concepts: Broadcasting

Platform-Specific Behavior

The API is designed for cross-platform use but respects platform differences:

// This call works on all platforms
await device.RequestConnectionPriorityAsync(BluetoothConnectionPriority.High);

// But behavior differs:
// - Android: Actually changes connection parameters
// - iOS/macOS: System-managed, method is no-op
// - Windows: System-managed, method is no-op

How to handle platform differences:

  1. Check XML documentation for platform-specific remarks
  2. Platform differences are noted with <b>Platform Support:</b> sections
  3. No-op implementations don't throw exceptions
  4. Use conditional compilation if platform-specific behavior is required

Thread Safety

General thread safety:

  • All public methods are thread-safe
  • Events may be raised on any thread
  • Property reads are thread-safe
  • Collections are thread-safe for enumeration

UI thread marshalling:

// Events may fire on background threads
scanner.DevicesAdded += async (s, e) =>
{
    // Marshal to UI thread if needed
    await MainThread.InvokeOnMainThreadAsync(() =>
    {
        foreach (var device in e.Items)
        {
            DeviceList.Add(device);
        }
    });
};

Best Practices

Resource Management

// GOOD: Automatic cleanup
await using var scanner = adapter.CreateScanner();
await scanner.StartScanningAsync();

// GOOD: Manual cleanup
var device = scanner.GetDevice(id);
try
{
    await device.ConnectAsync();
    // Use device...
}
finally
{
    await device.DisposeAsync();
}

// BAD: No cleanup (resource leak)
var scanner = adapter.CreateScanner();
await scanner.StartScanningAsync();
// Scanner keeps running, battery drain

Connection Reliability

// GOOD: Handle unexpected disconnections
device.UnexpectedDisconnection += async (s, e) =>
{
    logger.LogWarning($"Lost connection: {e.Reason}");

    // Attempt reconnect
    await Task.Delay(1000);
    await device.ConnectIfNeededAsync();
};

// GOOD: Verify connection before operations
DeviceNotConnectedException.ThrowIfNotConnected(device);
await characteristic.ReadValueAsync();

// GOOD: Use conditional connect
await device.ConnectIfNeededAsync(); // Safe to call multiple times

Error Recovery

// GOOD: Specific exception handling with retry
int retries = 3;
while (retries > 0)
{
    try
    {
        return await characteristic.ReadValueAsync();
    }
    catch (CharacteristicReadException ex) when (retries > 1)
    {
        logger.LogWarning($"Read failed, retrying... ({retries} left)");
        retries--;
        await Task.Delay(500);
    }
}

Performance Optimization

// GOOD: Use ValueSpan for synchronous access
byte[] ProcessValue()
{
    ReadOnlySpan<byte> span = characteristic.ValueSpan;
    return span.ToArray(); // Only allocate if needed
}

// GOOD: Batch operations
await device.ConnectAsync();
var service = await device.GetServiceAsync(serviceUuid);
var chars = await Task.WhenAll(
    service.GetCharacteristicAsync(uuid1),
    service.GetCharacteristicAsync(uuid2),
    service.GetCharacteristicAsync(uuid3)
);

// BAD: Sequential when parallel would work
await device.ConnectAsync();
var service = await device.GetServiceAsync(serviceUuid);
var char1 = await service.GetCharacteristicAsync(uuid1);
var char2 = await service.GetCharacteristicAsync(uuid2);
var char3 = await service.GetCharacteristicAsync(uuid3);

See Also