Table of Contents

Bluetooth Module

The Bluetooth module provides control over the Web Bluetooth API for testing Bluetooth-enabled web applications.

Overview

The Bluetooth module allows you to:

  • Simulate the Bluetooth adapter state (absent, powered off, powered on)
  • Simulate Bluetooth peripherals and advertisements
  • Simulate GATT services, characteristics, and descriptors
  • Simulate GATT connection and disconnection responses
  • Handle device selection prompts from navigator.bluetooth.requestDevice
  • React to characteristic and descriptor events

Accessing the Module

BluetoothModule bluetooth = driver.Bluetooth;

Timeout and Cancellation

All commands in this module accept optional timeoutOverride and CancellationToken parameters. Use timeoutOverride to set a per-command timeout (defaults to BiDiDriver.DefaultCommandTimeout when omitted). Use CancellationToken for cooperative cancellation. See the API Design Guide for details and examples.

Simulating the Bluetooth Adapter

Simulate Adapter State

Use SimulateAdapterAsync to simulate the presence or absence of the Bluetooth adapter, along with its power state. Use AdapterState.PoweredOn, AdapterState.PoweredOff, or AdapterState.Absent:

// Simulate adapter powered on
SimulateAdapterCommandParameters parameters =
    new SimulateAdapterCommandParameters(contextId, AdapterState.PoweredOn);
await driver.Bluetooth.SimulateAdapterAsync(parameters);

// Or simulate adapter absent or powered off
await driver.Bluetooth.SimulateAdapterAsync(
    new SimulateAdapterCommandParameters(contextId, AdapterState.Absent));

Disable Simulation

Use DisableSimulationAsync to turn off Bluetooth simulation for a browsing context:

DisableSimulationCommandParameters parameters =
    new DisableSimulationCommandParameters(contextId);
await driver.Bluetooth.DisableSimulationAsync(parameters);

Simulating Bluetooth Devices

Basic Device Simulation

Use SimulatePreconnectedPeripheralAsync to simulate a Bluetooth peripheral already connected to the page. The device appears as if it were pre-paired:

SimulatePreconnectedPeripheralCommandParameters parameters =
    new SimulatePreconnectedPeripheralCommandParameters(
        contextId,
        "00:00:00:00:00:00",
        "Test Device");
parameters.ManufacturerData.Add(new BluetoothManufacturerData(
    0x004C,
    Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 })));

await driver.Bluetooth.SimulatePreconnectedPeripheralAsync(parameters);
Console.WriteLine("Bluetooth device simulated");

Device with Services

Add known service UUIDs so the device can be discovered by navigator.bluetooth.requestDevice with service filters:

SimulatePreconnectedPeripheralCommandParameters @params =
    new SimulatePreconnectedPeripheralCommandParameters(
        contextId,
        "AA:BB:CC:DD:EE:FF",
        "Heart Rate Monitor");
@params.KnownServiceUUIDs.Add("heart_rate");

await driver.Bluetooth.SimulatePreconnectedPeripheralAsync(@params);
Console.WriteLine("Bluetooth heart rate monitor simulated");

Simulating Advertisements

Basic Advertisement

Use SimulateAdvertisementAsync to simulate Bluetooth Low Energy advertisements. Create a ScanRecord and SimulateAdvertisementScanEntry with device address, RSSI (signal strength), and scan data:

ScanRecord scanRecord = new ScanRecord();
SimulateAdvertisementScanEntry scanEntry = new SimulateAdvertisementScanEntry(
    "00:00:00:00:00:00",
    -50,
    scanRecord);

SimulateAdvertisementCommandParameters @params =
    new SimulateAdvertisementCommandParameters(contextId, scanEntry);

await driver.Bluetooth.SimulateAdvertisementAsync(@params);
Console.WriteLine("Bluetooth advertisement simulated");

RSSI values typically range from -30 (strong) to -90 (weak). Use different values to test signal-strength-dependent behavior:

// Simulate weak signal
ScanRecord scanRecord = new ScanRecord();
SimulateAdvertisementScanEntry weakSignal = new SimulateAdvertisementScanEntry(
    "00:00:00:00:00:00",
    -80,
    scanRecord);
await driver.Bluetooth.SimulateAdvertisementAsync(
    new SimulateAdvertisementCommandParameters(contextId, weakSignal));

// Simulate strong signal
SimulateAdvertisementScanEntry strongSignal = new SimulateAdvertisementScanEntry(
    "00:00:00:00:00:00",
    -30,
    scanRecord);
await driver.Bluetooth.SimulateAdvertisementAsync(
    new SimulateAdvertisementCommandParameters(contextId, strongSignal));

Simulating GATT Services

Use SimulateServiceAsync to add or remove GATT services on a simulated device. Use SimulateServiceType.Add or SimulateServiceType.Remove:

SimulateServiceCommandParameters addParams =
    new SimulateServiceCommandParameters(
        contextId,
        "AA:BB:CC:DD:EE:FF",
        "heart_rate",
        SimulateServiceType.Add);
await driver.Bluetooth.SimulateServiceAsync(addParams);

// Remove service
SimulateServiceCommandParameters removeParams =
    new SimulateServiceCommandParameters(
        contextId,
        "AA:BB:CC:DD:EE:FF",
        "heart_rate",
        SimulateServiceType.Remove);
await driver.Bluetooth.SimulateServiceAsync(removeParams);

Simulating GATT Characteristics

Add or Remove Characteristics

Use SimulateCharacteristicAsync to add or remove characteristics. CharacteristicProperties (read, write, notify, indicate, etc.) is required when Type is SimulateCharacteristicType.Add, and must be left null when it is SimulateCharacteristicType.Remove; a conforming remote end answers with invalid argument otherwise:

CharacteristicProperties properties = new CharacteristicProperties
{
    IsRead = true,
    IsNotify = true,
};

SimulateCharacteristicCommandParameters parameters =
    new SimulateCharacteristicCommandParameters(
        contextId,
        "AA:BB:CC:DD:EE:FF",
        "heart_rate",
        "heart_rate_measurement",
        SimulateCharacteristicType.Add)
    {
        CharacteristicProperties = properties,
    };
await driver.Bluetooth.SimulateCharacteristicAsync(parameters);

Simulate Characteristic Responses

Use SimulateCharacteristicResponseAsync to simulate responses for read, write, subscribe, or unsubscribe operations. Use SimulateCharacteristicResponseType.Read, Write, SubscribeToNotifications, or UnsubscribeFromNotifications. Code 0 indicates success; other values indicate GATT error codes:

// Simulate successful read response (code 0 = success)
SimulateCharacteristicResponseCommandParameters readParams =
    new SimulateCharacteristicResponseCommandParameters(
        contextId,
        "AA:BB:CC:DD:EE:FF",
        "heart_rate",
        "heart_rate_measurement",
        SimulateCharacteristicResponseType.Read,
        0)
    {
        Data = { 0x64 },  // Heart rate value 100
    };
await driver.Bluetooth.SimulateCharacteristicResponseAsync(readParams);

Simulating GATT Descriptors

Add or Remove Descriptors

Use SimulateDescriptorAsync to add or remove descriptors on a characteristic:

SimulateDescriptorCommandParameters parameters =
    new SimulateDescriptorCommandParameters(
        contextId,
        "AA:BB:CC:DD:EE:FF",
        "heart_rate",
        "heart_rate_measurement",
        "gatt.characteristic_user_description",
        SimulateDescriptorType.Add);
await driver.Bluetooth.SimulateDescriptorAsync(parameters);

Simulate Descriptor Responses

Use SimulateDescriptorResponseAsync to simulate read or write descriptor responses. Use SimulateDescriptorResponseType.Read or Write:

SimulateDescriptorResponseCommandParameters parameters =
    new SimulateDescriptorResponseCommandParameters(
        contextId,
        "AA:BB:CC:DD:EE:FF",
        "heart_rate",
        "heart_rate_measurement",
        "gatt.characteristic_user_description",
        SimulateDescriptorResponseType.Read,
        0)
    {
        Data = { 0x48, 0x65, 0x61, 0x72, 0x74, 0x20, 0x52, 0x61, 0x74, 0x65 },
    };
await driver.Bluetooth.SimulateDescriptorResponseAsync(parameters);

Simulating GATT Connection and Disconnection

GATT Connection Response

When the page calls device.gatt.connect(), the OnGattConnectionAttempted event fires. Use SimulateGattConnectionResponseAsync to respond. Code 0 indicates success:

// Code 0 = success
SimulateGattConnectionResponseCommandParameters successParams =
    new SimulateGattConnectionResponseCommandParameters(
        contextId,
        "AA:BB:CC:DD:EE:FF",
        0);
await driver.Bluetooth.SimulateGattConnectionResponseAsync(successParams);

GATT Disconnection

Use SimulateGattDisconnectionAsync to simulate a device disconnecting:

SimulateGattDisconnectionCommandParameters parameters =
    new SimulateGattDisconnectionCommandParameters(contextId, "AA:BB:CC:DD:EE:FF");
await driver.Bluetooth.SimulateGattDisconnectionAsync(parameters);

Handling Device Selection Prompts

When a page calls navigator.bluetooth.requestDevice(), a prompt appears. Subscribe to OnRequestDevicePromptUpdated to receive prompt events, then use HandleRequestDevicePromptAsync to accept (selecting a device) or cancel:

// Accept the prompt and select a device
HandleRequestDevicePromptAcceptCommandParameters acceptParams =
    new HandleRequestDevicePromptAcceptCommandParameters(
        "contextId",
        "promptId",
        "deviceId");
await driver.Bluetooth.HandleRequestDevicePromptAsync(acceptParams);

// Or cancel the prompt
HandleRequestDevicePromptCancelCommandParameters cancelParams =
    new HandleRequestDevicePromptCancelCommandParameters("contextId", "promptId");
await driver.Bluetooth.HandleRequestDevicePromptAsync(cancelParams);

Use HandleRequestDevicePromptAcceptCommandParameters with a device ID to accept, or HandleRequestDevicePromptCancelCommandParameters to cancel.

Events

Subscribe to Bluetooth events via Session.SubscribeAsync after adding observers. Remember the two-step subscription pattern: add observer, then subscribe.

RequestDevicePromptUpdated

Fired when a Bluetooth device selection prompt is shown or updated. Use the event to get the prompt ID and available devices, then call HandleRequestDevicePromptAsync:

driver.Bluetooth.OnRequestDevicePromptUpdated.AddObserver((RequestDevicePromptUpdatedEventArgs e) =>
{
    Console.WriteLine($"Prompt {e.PromptId} in context {e.BrowsingContextId}");
    foreach (RequestDeviceInfo device in e.Devices)
    {
        Console.WriteLine($"  Device: {device.DeviceName} ({device.DeviceId})");
    }
});

SubscribeCommandParameters subscribe =
    new SubscribeCommandParameters(driver.Bluetooth.OnRequestDevicePromptUpdated.EventName);
await driver.Session.SubscribeAsync(subscribe);

GattConnectionAttempted

Fired when the page attempts a GATT connection to a device. Use this to respond with SimulateGattConnectionResponseAsync:

driver.Bluetooth.OnGattConnectionAttempted.AddObserver((GattConnectionAttemptedEventArgs e) =>
{
    Console.WriteLine($"GATT connection attempted: {e.Address} in context {e.BrowsingContextId}");
});

SubscribeCommandParameters subscribe =
    new SubscribeCommandParameters(driver.Bluetooth.OnGattConnectionAttempted.EventName);
await driver.Session.SubscribeAsync(subscribe);

CharacteristicEventGenerated

Fired when the page reads, writes, or subscribes to a characteristic. Use this to respond with SimulateCharacteristicResponseAsync:

driver.Bluetooth.OnCharacteristicEventGenerated.AddObserver((CharacteristicEventGeneratedEventArgs e) =>
{
    Console.WriteLine($"Characteristic {e.CharacteristicUuid} event: {e.Type}");
});

SubscribeCommandParameters subscribe =
    new SubscribeCommandParameters(driver.Bluetooth.OnCharacteristicEventGenerated.EventName);
await driver.Session.SubscribeAsync(subscribe);

DescriptorEventGenerated

Fired when the page reads or writes a descriptor. Use this to respond with SimulateDescriptorResponseAsync:

driver.Bluetooth.OnDescriptorEventGenerated.AddObserver((DescriptorEventGeneratedEventArgs e) =>
{
    Console.WriteLine($"Descriptor {e.DescriptorUuid} event: {e.Type}");
});

SubscribeCommandParameters subscribe =
    new SubscribeCommandParameters(driver.Bluetooth.OnDescriptorEventGenerated.EventName);
await driver.Session.SubscribeAsync(subscribe);

Common Patterns

Testing Bluetooth Scanner

// Simulate a Bluetooth device
SimulatePreconnectedPeripheralCommandParameters deviceParams =
    new SimulatePreconnectedPeripheralCommandParameters(
        contextId,
        "AA:BB:CC:DD:EE:FF",
        "Heart Rate Monitor");
deviceParams.KnownServiceUUIDs.Add("heart_rate");

await driver.Bluetooth.SimulatePreconnectedPeripheralAsync(deviceParams);

// Navigate to page with Bluetooth functionality
await driver.BrowsingContext.NavigateAsync(
    new NavigateCommandParameters(contextId, "https://example.com")
    { Wait = ReadinessState.Complete });

// Trigger Bluetooth scan in page
EvaluateResult result = await driver.Script.EvaluateAsync(
    new EvaluateCommandParameters(
        @"navigator.bluetooth.requestDevice({
            filters: [{ services: ['heart_rate'] }]
        }).then(device => device.name)",
        new ContextTarget(contextId),
        true));

if (result is EvaluateResultSuccess success &&
    success.Result is StringRemoteValue deviceNameValue)
{
    string deviceName = deviceNameValue.Value;
    Console.WriteLine($"Found device: {deviceName}");
}

Testing Multiple Devices

List<string> deviceAddresses = new List<string>
{
    "AA:BB:CC:DD:EE:01",
    "AA:BB:CC:DD:EE:02",
    "AA:BB:CC:DD:EE:03"
};

foreach (string address in deviceAddresses)
{
    SimulatePreconnectedPeripheralCommandParameters @params =
        new SimulatePreconnectedPeripheralCommandParameters(
            contextId,
            address,
            $"Device {address[^2..]}");
    @params.KnownServiceUUIDs.Add("battery_service");

    await driver.Bluetooth.SimulatePreconnectedPeripheralAsync(@params);
}

// Test device discovery
EvaluateResult result = await driver.Script.EvaluateAsync(
    new EvaluateCommandParameters(
        @"navigator.bluetooth.requestDevice({
            filters: [{ services: ['battery_service'] }],
            acceptAllDevices: false
        }).then(device => device.id)",
        new ContextTarget(contextId),
        true));

Testing Device Connection

// Simulate device
SimulatePreconnectedPeripheralCommandParameters deviceParams =
    new SimulatePreconnectedPeripheralCommandParameters(
        contextId,
        "11:22:33:44:55:66",
        "Temperature Sensor");
deviceParams.KnownServiceUUIDs.Add("environmental_sensing");

await driver.Bluetooth.SimulatePreconnectedPeripheralAsync(deviceParams);

// Advertise device
ScanRecord scanRecord = new ScanRecord { UUIDs = { "environmental_sensing" } };
SimulateAdvertisementScanEntry adScanEntry = new SimulateAdvertisementScanEntry(
    "11:22:33:44:55:66",
    -45,
    scanRecord);
await driver.Bluetooth.SimulateAdvertisementAsync(
    new SimulateAdvertisementCommandParameters(contextId, adScanEntry));

// Test connection
EvaluateResult result = await driver.Script.EvaluateAsync(
    new EvaluateCommandParameters(
        @"navigator.bluetooth.requestDevice({
            filters: [{ services: ['environmental_sensing'] }]
        })
        .then(device => device.gatt.connect())
        .then(server => 'connected')
        .catch(err => `error: ${err.message}`)",
        new ContextTarget(contextId),
        true));

if (result is EvaluateResultSuccess success &&
    success.Result is StringRemoteValue connectionValue)
{
    string connectionStatus = connectionValue.Value;
    Console.WriteLine($"Connection status: {connectionStatus}");
}

Browser Support

Browser Support Level
Chrome/Edge ⚠️ Experimental (requires flags)
Firefox ❌ Not supported
Safari ❌ Not supported

Note: Bluetooth module support is experimental and requires specific browser flags to be enabled.

Enabling Bluetooth Support

Chrome/Edge

The Bluetooth module requires Chromium's experimental web platform features, which are enabled by a browser command-line switch supplied when the browser process is started:

--enable-experimental-web-platform-features

This library does not launch browsers (see Browser Setup), so supply the switch through whatever starts the browser: the driver executable's capabilities, your own launcher, or the browser command line directly.

Best Practices

  1. Check browser support: Verify Bluetooth module is available before using
  2. Use realistic device addresses: Follow MAC address format (XX:XX:XX:XX:XX:XX)
  3. Set appropriate RSSI values: Use realistic signal strength values (-30 to -90)
  4. Test connection failures: Simulate both successful and failed connections
  5. Clean up devices: Call DisableSimulationAsync or remove simulated devices between tests
  6. Subscribe before actions: Add observers and call Session.SubscribeAsync before triggering Bluetooth operations in the page

Common Issues

Bluetooth Not Supported

Problem: Module throws "not supported" errors.

Solution:

  • Check browser version (Chrome 90+ recommended)
  • Enable experimental web platform features
  • Use Chrome or Edge (Firefox/Safari not supported)
  • Launch browser with required flags

Device Not Discovered

Problem: Simulated device is not found by Web Bluetooth API.

Solution:

  • Ensure device services match the filter criteria
  • Verify device address format is correct
  • Call SimulateAdvertisementAsync after SimulatePreconnectedPeripheralAsync
  • Check that Web Bluetooth API is enabled in the browser

Manufacturer Data Format

Problem: Manufacturer data not recognized.

Solution:

  • Use correct company identifier codes
  • Pass the data as a base64 string, which is what BluetoothManufacturerData.Data holds
  • Reference Bluetooth Company Identifiers

Next Steps

Further Reading