Bluetooth
Pick a Bluetooth Low Energy device, connect to its GATT server, and read, write or subscribe to its characteristics.
@inject Bit.Butil.Bluetooth bluetoothMDN reference
IsSupported and GetAvailability before offering the feature, and keep
every call inside a try/catch.
RequestDevice must run inside a user gesture, and the permission it produces covers
only the services the request named. A device picked with no OptionalServices can be
connected to and nothing else - every getPrimaryService for an unnamed service is a
SecurityError. The defaults below ask for battery_service and
device_information, which most peripherals expose.
IsSupported reports whether navigator.bluetooth exists at all; GetAvailability reports whether this machine has a radio the browser can use, which is the difference between 'your browser cannot' and 'your Bluetooth is off'.
@inject Bit.Butil.Bluetooth bluetooth
var supported = await bluetooth.IsSupported();
var available = await bluetooth.GetAvailability();RequestDevice opens the browser's chooser and returns a handle to the device the user picked. GetDevices returns the devices this origin was already granted, without a prompt.
private BluetoothDevice? _device;
_device = await bluetooth.RequestDevice(new BluetoothRequestOptions
{
AcceptAllDevices = true,
OptionalServices = ["battery_service", "device_information"]
});
// or, without a prompt:
var granted = await bluetooth.GetDevices();Connect opens the GATT server. GetServices lists the primary services the grant covers, and GetCharacteristics lists one service's characteristics with the operations each of them supports.
{
// The handle from RequestDevice above. A grant is per device, so this is the only way back to
// one the user has already chosen.
private BluetoothDevice? _device;
private async Task Browse()
{
await _device!.Connect();
foreach (var service in await _device.GetServices())
{
foreach (var c in await _device.GetCharacteristics(service.Uuid))
{
// c.Uuid, c.Read, c.Write, c.Notify, ...
}
}
await _device.Disconnect();
}
}Read returns the characteristic's current bytes; Write sends bytes to it, either acknowledged or fire-and-forget. The battery level characteristic below is a single byte holding a percentage.
{
private BluetoothDevice? _device; // from RequestDevice, and connected
private async Task ReadWrite()
{
var value = await _device!.Read("battery_service", "battery_level");
var percent = value?[0];
await _device.Write("0xFFE0", "0xFFE1", [0x01, 0x02], withResponse: true);
}
}SubscribeValueChanged asks the device to push a characteristic's value as it changes - a heart-rate strap, a thermometer. SubscribeDisconnected fires when the link drops, which for a battery-powered peripheral happens on its own schedule.
IAsyncDisposable
{
private BluetoothDevice? _device; // from RequestDevice, and connected
private ButilSubscription? _values;
private ButilSubscription? _dropped;
private async Task Listen()
{
_values = await _device!.SubscribeValueChanged(
"heart_rate", "heart_rate_measurement",
bytes => Console.WriteLine(bytes.Length));
_dropped = await _device.SubscribeDisconnected(
() => Console.WriteLine("device went away"));
}
public async ValueTask DisposeAsync()
{
if (_values is not null) await _values.DisposeAsync();
if (_dropped is not null) await _dropped.DisposeAsync();
}
}Forget drops this origin's permission for the device, so it stops appearing in GetDevices until the user picks it again. Not every Chromium version implements it - false means it was unavailable.
{
private BluetoothDevice? _device; // from RequestDevice
// Drops the grant, so GetDevices stops returning it and the picker has to be shown again.
private async Task Revoke()
{
var revoked = await _device!.Forget();
_device = null;
}
}API reference
ValueTask<bool> IsSupported()ValueTask<bool> GetAvailability()ValueTask<BluetoothDevice?> RequestDevice(BluetoothRequestOptions? options = null)ValueTask<BluetoothDevice[]> GetDevices()ValueTask DisposeAsync()void InvokeBluetoothValueChanged(Guid id, byte[] value)void InvokeBluetoothDisconnected(Guid id)BluetoothDeviceInfo Info { get; }ValueTask<bool> Connect()ValueTask Disconnect()ValueTask<bool> IsConnected()ValueTask<BluetoothServiceInfo[]> GetServices()ValueTask<BluetoothCharacteristicInfo[]> GetCharacteristics(string serviceUuid)ValueTask<byte[]?> Read(string serviceUuid, string characteristicUuid)ValueTask<bool> Write(string serviceUuid, string characteristicUuid, byte[] data, bool withResponse = true)ValueTask<ButilSubscription> SubscribeValueChanged(string serviceUuid, string characteristicUuid, Action<byte[]> handler)ValueTask<ButilSubscription> SubscribeDisconnected(Action handler)ValueTask<bool> Forget()ValueTask DisposeAsync()class BluetoothRequestOptions { BluetoothFilter[]? Filters; bool AcceptAllDevices; string[]? OptionalServices; }class BluetoothFilter { string[]? Services; string? Name; string? NamePrefix; }class BluetoothServiceInfo { string Uuid; bool IsPrimary; }class BluetoothCharacteristicInfo { string Uuid; string ServiceUuid; bool Broadcast; bool Read; bool WriteWithoutResponse; bool Write; bool Notify; bool Indicate; }