'use strict'; /* Bluetooth Constants */ /** * HCI Error Codes. * Used for simulateGATT{Dis}ConnectionResponse. For a complete list of * possible error codes see BT 4.2 Vol 2 Part D 1.3 List Of Error Codes. */ const HCI_SUCCESS = 0x0000; const HCI_CONNECTION_TIMEOUT = 0x0008; /** * GATT Error codes. * Used for GATT operations responses. BT 4.2 Vol 3 Part F 3.4.1.1 Error * Response */ const GATT_SUCCESS = 0x0000; const GATT_INVALID_HANDLE = 0x0001; /* Bluetooth UUID Constants */ /* Service UUIDs */ var blocklist_test_service_uuid = '611c954a-263b-4f4a-aab6-01ddb953f985'; var request_disconnection_service_uuid = '01d7d889-7451-419f-aeb8-d65e7b9277af'; /* Characteristic UUIDs */ var blocklist_exclude_reads_characteristic_uuid = 'bad1c9a2-9a5b-4015-8b60-1579bbbf2135'; var request_disconnection_characteristic_uuid = '01d7d88a-7451-419f-aeb8-d65e7b9277af'; /* Descriptor UUIDs */ var blocklist_test_descriptor_uuid = 'bad2ddcf-60db-45cd-bef9-fd72b153cf7c'; var blocklist_exclude_reads_descriptor_uuid = 'bad3ec61-3cc3-4954-9702-7977df514114'; /** * Helper objects that associate Bluetooth names, aliases, and UUIDs. These are * useful for tests that check that the same result is produces when using all * three methods of referring to a Bluetooth UUID. */ var generic_access = { alias: 0x1800, name: 'generic_access', uuid: '00001800-0000-1000-8000-00805f9b34fb' }; var device_name = { alias: 0x2a00, name: 'gap.device_name', uuid: '00002a00-0000-1000-8000-00805f9b34fb' }; var reconnection_address = { alias: 0x2a03, name: 'gap.reconnection_address', uuid: '00002a03-0000-1000-8000-00805f9b34fb' }; var heart_rate = { alias: 0x180d, name: 'heart_rate', uuid: '0000180d-0000-1000-8000-00805f9b34fb' }; var health_thermometer = { alias: 0x1809, name: 'health_thermometer', uuid: '00001809-0000-1000-8000-00805f9b34fb' }; var body_sensor_location = { alias: 0x2a38, name: 'body_sensor_location', uuid: '00002a38-0000-1000-8000-00805f9b34fb' }; var glucose = { alias: 0x1808, name: 'glucose', uuid: '00001808-0000-1000-8000-00805f9b34fb' }; var battery_service = { alias: 0x180f, name: 'battery_service', uuid: '0000180f-0000-1000-8000-00805f9b34fb' }; var battery_level = { alias: 0x2A19, name: 'battery_level', uuid: '00002a19-0000-1000-8000-00805f9b34fb' }; var user_description = { alias: 0x2901, name: 'gatt.characteristic_user_description', uuid: '00002901-0000-1000-8000-00805f9b34fb' }; var client_characteristic_configuration = { alias: 0x2902, name: 'gatt.client_characteristic_configuration', uuid: '00002902-0000-1000-8000-00805f9b34fb' }; var measurement_interval = { alias: 0x2a21, name: 'measurement_interval', uuid: '00002a21-0000-1000-8000-00805f9b34fb' }; /** * An advertisement packet object that simulates a Health Thermometer device. * @type {ScanResult} */ const health_thermometer_ad_packet = { deviceAddress: '09:09:09:09:09:09', rssi: -10, scanRecord: { name: 'Health Thermometer', uuids: [health_thermometer.uuid], }, }; /** * An advertisement packet object that simulates a Heart Rate device. * @type {ScanResult} */ const heart_rate_ad_packet = { deviceAddress: '08:08:08:08:08:08', rssi: -10, scanRecord: { name: 'Heart Rate', uuids: [heart_rate.uuid], }, }; const uuid1234 = BluetoothUUID.getService(0x1234); const uuid5678 = BluetoothUUID.getService(0x5678); const uuidABCD = BluetoothUUID.getService(0xABCD); const manufacturer1Data = new Uint8Array([1, 2]); const manufacturer2Data = new Uint8Array([3, 4]); const uuid1234Data = new Uint8Array([5, 6]); const uuid5678Data = new Uint8Array([7, 8]); const uuidABCDData = new Uint8Array([9, 10]); // TODO(crbug.com/1163207): Add the blocklist link. // Fake manufacturer data following iBeacon format listed in // https://en.wikipedia.org/wiki/IBeacon, which will be blocked according to [TBD blocklist link]. const blocklistedManufacturerId = 0x4c; const blocklistedManufacturerData = new Uint8Array([ 0x02, 0x15, 0xb3, 0xeb, 0x8d, 0xb1, 0x30, 0xa5, 0x44, 0x8d, 0xb4, 0xac, 0xfb, 0x68, 0xc9, 0x23, 0xa3, 0x0e, 0x00, 0x00, 0x00, 0x00, 0xbf ]); // Fake manufacturer data that is not in [TBD blocklist link]. const nonBlocklistedManufacturerId = 0x0001; const nonBlocklistedManufacturerData = new Uint8Array([1, 2]); /** * An advertisement packet object that simulates a device that advertises * service and manufacturer data. * @type {ScanResult} */ const service_and_manufacturer_data_ad_packet = { deviceAddress: '07:07:07:07:07:07', rssi: -10, scanRecord: { name: 'LE Device', uuids: [uuid1234], manufacturerData: {0x0001: manufacturer1Data, 0x0002: manufacturer2Data}, serviceData: { [uuid1234]: uuid1234Data, [uuid5678]: uuid5678Data, [uuidABCD]: uuidABCDData } } }; /** Bluetooth Helpers */ /** * Helper class to create a BluetoothCharacteristicProperties object using an * array of strings corresponding to the property bit to set. */ class TestCharacteristicProperties { /** @param {Array} properties */ constructor(properties) { this.broadcast = false; this.read = false; this.writeWithoutResponse = false; this.write = false; this.notify = false; this.indicate = false; this.authenticatedSignedWrites = false; this.reliableWrite = false; this.writableAuxiliaries = false; properties.forEach(val => { if (this.hasOwnProperty(val)) this[val] = true; else throw `Invalid member '${val}'`; }); } } /** * Produces an array of BluetoothLEScanFilterInit objects containing the list of * services in |services| and various permutations of the other * BluetoothLEScanFilterInit properties. This method is used to test that the * |services| are valid so the other properties do not matter. * @param {BluetoothServiceUUID} services * @returns {Array} A list of options containing * |services| and various permutations of other options. */ function generateRequestDeviceArgsWithServices(services = ['heart_rate']) { return [ {filters: [{services: services}]}, {filters: [{services: services, name: 'Name'}]}, {filters: [{services: services, namePrefix: 'Pre'}]}, { filters: [ {services: services, manufacturerData: [{companyIdentifier: 0x0001}]} ] }, { filters: [{ services: services, name: 'Name', namePrefix: 'Pre', manufacturerData: [{companyIdentifier: 0x0001}] }] }, {filters: [{services: services}], optionalServices: ['heart_rate']}, { filters: [{services: services, name: 'Name'}], optionalServices: ['heart_rate'] }, { filters: [{services: services, namePrefix: 'Pre'}], optionalServices: ['heart_rate'] }, { filters: [ {services: services, manufacturerData: [{companyIdentifier: 0x0001}]} ], optionalServices: ['heart_rate'] }, { filters: [{ services: services, name: 'Name', namePrefix: 'Pre', manufacturerData: [{companyIdentifier: 0x0001}] }], optionalServices: ['heart_rate'] } ]; } /** * Causes |fake_peripheral| to disconnect and returns a promise that resolves * once `gattserverdisconnected` has been fired on |device|. * @param {BluetoothDevice} device The device to check if the * `gattserverdisconnected` promise was fired. * @param {FakePeripheral} fake_peripheral The device fake that represents * |device|. * @returns {Promise>} A promise that resolves when the device has * successfully disconnected. */ function simulateGATTDisconnectionAndWait(device, fake_peripheral) { return Promise.all([ eventPromise(device, 'gattserverdisconnected'), fake_peripheral.simulateGATTDisconnection(), ]); } /** @type {FakeCentral} The fake adapter for the current test. */ let fake_central = null; async function initializeFakeCentral({state = 'powered-on'}) { if (!fake_central) { fake_central = await navigator.bluetooth.test.simulateCentral({state}); } } /** * A dictionary for specifying fake Bluetooth device setup options. * @typedef {{address: !string, name: !string, * manufacturerData: !Object>, * knownServiceUUIDs: !Array, connectable: !boolean, * serviceDiscoveryComplete: !boolean}} */ let FakeDeviceOptions; /** * @typedef {{fakeDeviceOptions: FakeDeviceOptions, * requestDeviceOptions: RequestDeviceOptions}} */ let SetupOptions; /** * Default options for setting up a Bluetooth device. * @type {FakeDeviceOptions} */ const fakeDeviceOptionsDefault = { address: '00:00:00:00:00:00', name: 'LE Device', manufacturerData: {}, knownServiceUUIDs: [], connectable: false, serviceDiscoveryComplete: false, }; /** * A dictionary containing the fake Bluetooth device object. The dictionary can * optionally contain its fake services and its BluetoothDevice counterpart. * @typedef {{fake_peripheral: !FakePeripheral, * fake_services: Object, * device: BluetoothDevice}} */ let FakeDevice; /** * Creates a SetupOptions object using |setupOptionsDefault| as the base options * object with the options from |setupOptionsOverride| overriding these * defaults. * @param {SetupOptions} setupOptionsDefault The default options object to use * as the base. * @param {SetupOptions} setupOptionsOverride The options to override the * defaults with. * @returns {SetupOptions} The merged setup options containing the defaults with * the overrides applied. */ function createSetupOptions(setupOptionsDefault, setupOptionsOverride) { // Merge the properties of |setupOptionsDefault| and |setupOptionsOverride| // without modifying |setupOptionsDefault|. let fakeDeviceOptions = Object.assign( {...setupOptionsDefault.fakeDeviceOptions}, setupOptionsOverride.fakeDeviceOptions); let requestDeviceOptions = Object.assign( {...setupOptionsDefault.requestDeviceOptions}, setupOptionsOverride.requestDeviceOptions); return {fakeDeviceOptions, requestDeviceOptions}; } /** * Adds a preconnected device with the given options. A preconnected device is a * device that has been paired with the system previously. This can be done if, * for example, the user pairs the device using the OS'es settings. * * By default, the preconnected device will be set up using the * |fakeDeviceOptionsDefault| and will not use a RequestDeviceOption object. * This means that the device will not be requested during the setup. * * If |setupOptionsOverride| is provided, these options will override the * defaults. If |setupOptionsOverride| includes the requestDeviceOptions * property, then the device will be requested using those options. * @param {SetupOptions} setupOptionsOverride An object containing options for * setting up a fake Bluetooth device and for requesting the device. * @returns {Promise} The device fake initialized with the * parameter values. */ async function setUpPreconnectedFakeDevice(setupOptionsOverride) { await initializeFakeCentral({state: 'powered-on'}); let setupOptions = createSetupOptions( {fakeDeviceOptions: fakeDeviceOptionsDefault}, setupOptionsOverride); // Simulate the fake peripheral. let preconnectedDevice = {}; preconnectedDevice.fake_peripheral = await fake_central.simulatePreconnectedPeripheral({ address: setupOptions.fakeDeviceOptions.address, name: setupOptions.fakeDeviceOptions.name, manufacturerData: setupOptions.fakeDeviceOptions.manufacturerData, knownServiceUUIDs: setupOptions.fakeDeviceOptions.knownServiceUUIDs, }); if (setupOptions.fakeDeviceOptions.connectable) { await preconnectedDevice.fake_peripheral.setNextGATTConnectionResponse( {code: HCI_SUCCESS}); } // Add known services. preconnectedDevice.fake_services = new Map(); for (let service of setupOptions.fakeDeviceOptions.knownServiceUUIDs) { let fake_service = await preconnectedDevice.fake_peripheral.addFakeService( {uuid: service}); preconnectedDevice.fake_services.set(service, fake_service); } // Request the device if the request option isn't empty. if (Object.keys(setupOptions.requestDeviceOptions).length !== 0) { preconnectedDevice.device = await requestDeviceWithTrustedClick(setupOptions.requestDeviceOptions); } // Set up services discovered state. if (setupOptions.fakeDeviceOptions.serviceDiscoveryComplete) { await preconnectedDevice.fake_peripheral.setNextGATTDiscoveryResponse( {code: HCI_SUCCESS}); } return preconnectedDevice; } /** Blocklisted GATT Device Helper Methods */ /** @type {FakeDeviceOptions} */ const blocklistFakeDeviceOptionsDefault = { address: '11:11:11:11:11:11', name: 'Blocklist Device', knownServiceUUIDs: ['generic_access', blocklist_test_service_uuid], connectable: true, serviceDiscoveryComplete: true }; /** @type {RequestDeviceOptions} */ const blocklistRequestDeviceOptionsDefault = { filters: [{services: [blocklist_test_service_uuid]}] }; /** @type {SetupOptions} */ const blocklistSetupOptionsDefault = { fakeDeviceOptions: blocklistFakeDeviceOptionsDefault, requestDeviceOptions: blocklistRequestDeviceOptionsDefault }; /** * Returns an object containing a BluetoothDevice discovered using |options|, * its corresponding FakePeripheral and FakeRemoteGATTServices. * The simulated device is called 'Blocklist Device' and it has one known * service UUID |blocklist_test_service_uuid|. The |blocklist_test_service_uuid| * service contains two characteristics: * - |blocklist_exclude_reads_characteristic_uuid| (read, write) * - 'gap.peripheral_privacy_flag' (read, write) * The 'gap.peripheral_privacy_flag' characteristic contains three descriptors: * - |blocklist_test_descriptor_uuid| * - |blocklist_exclude_reads_descriptor_uuid| * - 'gatt.client_characteristic_configuration' * These are special UUIDs that have been added to the blocklist found at * https://github.com/WebBluetoothCG/registries/blob/master/gatt_blocklist.txt * There are also test UUIDs that have been added to the test environment which * other implementations should add as test UUIDs as well. * The device has been connected to and its attributes are ready to be * discovered. * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, * fake_blocklist_test_service: FakeRemoteGATTService, * fake_blocklist_exclude_reads_characteristic: * FakeRemoteGATTCharacteristic, * fake_blocklist_exclude_writes_characteristic: * FakeRemoteGATTCharacteristic, * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor}>} An * object containing the BluetoothDevice object and its corresponding * GATT fake objects. */ async function getBlocklistDevice(setupOptionsOverride = {}) { let setupOptions = createSetupOptions(blocklistSetupOptionsDefault, setupOptionsOverride); let fakeDevice = await setUpPreconnectedFakeDevice(setupOptions); await fakeDevice.device.gatt.connect(); let fake_blocklist_test_service = fakeDevice.fake_services.get(blocklist_test_service_uuid); let fake_blocklist_exclude_reads_characteristic = await fake_blocklist_test_service.addFakeCharacteristic({ uuid: blocklist_exclude_reads_characteristic_uuid, properties: ['read', 'write'], }); let fake_blocklist_exclude_writes_characteristic = await fake_blocklist_test_service.addFakeCharacteristic({ uuid: 'gap.peripheral_privacy_flag', properties: ['read', 'write'], }); let fake_blocklist_descriptor = await fake_blocklist_exclude_writes_characteristic.addFakeDescriptor( {uuid: blocklist_test_descriptor_uuid}); let fake_blocklist_exclude_reads_descriptor = await fake_blocklist_exclude_writes_characteristic.addFakeDescriptor( {uuid: blocklist_exclude_reads_descriptor_uuid}); let fake_blocklist_exclude_writes_descriptor = await fake_blocklist_exclude_writes_characteristic.addFakeDescriptor( {uuid: 'gatt.client_characteristic_configuration'}); return { device: fakeDevice.device, fake_peripheral: fakeDevice.fake_peripheral, fake_blocklist_test_service, fake_blocklist_exclude_reads_characteristic, fake_blocklist_exclude_writes_characteristic, fake_blocklist_descriptor, fake_blocklist_exclude_reads_descriptor, fake_blocklist_exclude_writes_descriptor, }; } /** * Returns an object containing a Blocklist Test BluetoothRemoteGattService and * its corresponding FakeRemoteGATTService. * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, * fake_blocklist_test_service: FakeRemoteGATTService, * fake_blocklist_exclude_reads_characteristic: * FakeRemoteGATTCharacteristic, * fake_blocklist_exclude_writes_characteristic: * FakeRemoteGATTCharacteristic, * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, * service: BluetoothRemoteGATTService, * fake_service: FakeBluetoothRemoteGATTService}>} An object containing the * BluetoothDevice object and its corresponding GATT fake objects. */ async function getBlocklistTestService() { let result = await getBlocklistDevice(); let service = await result.device.gatt.getPrimaryService(blocklist_test_service_uuid); return Object.assign(result, { service, fake_service: result.fake_blocklist_test_service, }); } /** * Returns an object containing a blocklisted BluetoothRemoteGATTCharacteristic * that excludes reads and its corresponding FakeRemoteGATTCharacteristic. * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, * fake_blocklist_test_service: FakeRemoteGATTService, * fake_blocklist_exclude_reads_characteristic: * FakeRemoteGATTCharacteristic, * fake_blocklist_exclude_writes_characteristic: * FakeRemoteGATTCharacteristic, * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, * service: BluetoothRemoteGATTService, * fake_service: FakeBluetoothRemoteGATTService, * characteristic: BluetoothRemoteGATTCharacteristic, * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic}>} An object * containing the BluetoothDevice object and its corresponding GATT fake * objects. */ async function getBlocklistExcludeReadsCharacteristic() { let result = await getBlocklistTestService(); let characteristic = await result.service.getCharacteristic( blocklist_exclude_reads_characteristic_uuid); return Object.assign(result, { characteristic, fake_characteristic: result.fake_blocklist_exclude_reads_characteristic }); } /** * Returns an object containing a blocklisted BluetoothRemoteGATTCharacteristic * that excludes writes and its corresponding FakeRemoteGATTCharacteristic. * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, * fake_blocklist_test_service: FakeRemoteGATTService, * fake_blocklist_exclude_reads_characteristic: * FakeRemoteGATTCharacteristic, * fake_blocklist_exclude_writes_characteristic: * FakeRemoteGATTCharacteristic, * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, * service: BluetoothRemoteGATTService, * fake_service: FakeBluetoothRemoteGATTService, * characteristic: BluetoothRemoteGATTCharacteristic, * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic}>} An object * containing the BluetoothDevice object and its corresponding GATT fake * objects. */ async function getBlocklistExcludeWritesCharacteristic() { let result = await getBlocklistTestService(); let characteristic = await result.service.getCharacteristic('gap.peripheral_privacy_flag'); return Object.assign(result, { characteristic, fake_characteristic: result.fake_blocklist_exclude_writes_characteristic }); } /** * Returns an object containing a blocklisted BluetoothRemoteGATTDescriptor that * excludes reads and its corresponding FakeRemoteGATTDescriptor. * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, * fake_blocklist_test_service: FakeRemoteGATTService, * fake_blocklist_exclude_reads_characteristic: * FakeRemoteGATTCharacteristic, * fake_blocklist_exclude_writes_characteristic: * FakeRemoteGATTCharacteristic, * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, * service: BluetoothRemoteGATTService, * fake_service: FakeBluetoothRemoteGATTService, * characteristic: BluetoothRemoteGATTCharacteristic, * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic, * descriptor: BluetoothRemoteGATTDescriptor, * fake_descriptor: FakeBluetoothRemoteGATTDescriptor}>} An object * containing the BluetoothDevice object and its corresponding GATT fake * objects. */ async function getBlocklistExcludeReadsDescriptor() { let result = await getBlocklistExcludeWritesCharacteristic(); let descriptor = await result.characteristic.getDescriptor( blocklist_exclude_reads_descriptor_uuid); return Object.assign(result, { descriptor, fake_descriptor: result.fake_blocklist_exclude_reads_descriptor }); } /** * Returns an object containing a blocklisted BluetoothRemoteGATTDescriptor that * excludes writes and its corresponding FakeRemoteGATTDescriptor. * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, * fake_blocklist_test_service: FakeRemoteGATTService, * fake_blocklist_exclude_reads_characteristic: * FakeRemoteGATTCharacteristic, * fake_blocklist_exclude_writes_characteristic: * FakeRemoteGATTCharacteristic, * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, * service: BluetoothRemoteGATTService, * fake_service: FakeBluetoothRemoteGATTService, * characteristic: BluetoothRemoteGATTCharacteristic, * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic, * descriptor: BluetoothRemoteGATTDescriptor, * fake_descriptor: FakeBluetoothRemoteGATTDescriptor}>} An object * containing the BluetoothDevice object and its corresponding GATT fake * objects. */ async function getBlocklistExcludeWritesDescriptor() { let result = await getBlocklistExcludeWritesCharacteristic(); let descriptor = await result.characteristic.getDescriptor( 'gatt.client_characteristic_configuration'); return Object.assign(result, { descriptor: descriptor, fake_descriptor: result.fake_blocklist_exclude_writes_descriptor, }); } /** Bluetooth HID Device Helper Methods */ /** @type {FakeDeviceOptions} */ const connectedHIDFakeDeviceOptionsDefault = { address: '10:10:10:10:10:10', name: 'HID Device', knownServiceUUIDs: [ 'generic_access', 'device_information', 'human_interface_device', ], connectable: true, serviceDiscoveryComplete: false }; /** @type {RequestDeviceOptions} */ const connectedHIDRequestDeviceOptionsDefault = { filters: [{services: ['device_information']}], optionalServices: ['human_interface_device'] }; /** @type {SetupOptions} */ const connectedHIDSetupOptionsDefault = { fakeDeviceOptions: connectedHIDFakeDeviceOptionsDefault, requestDeviceOptions: connectedHIDRequestDeviceOptionsDefault }; /** * Similar to getHealthThermometerDevice except the GATT discovery * response has not been set yet so more attributes can still be added. * TODO(crbug.com/719816): Add descriptors. * @param {RequestDeviceOptions} options The options for requesting a Bluetooth * Device. * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object * containing a requested BluetoothDevice and its fake counter part. */ async function getConnectedHIDDevice( requestDeviceOptionsOverride, fakeDeviceOptionsOverride) { let setupOptions = createSetupOptions(connectedHIDSetupOptionsDefault, { fakeDeviceOptions: fakeDeviceOptionsOverride, requestDeviceOptions: requestDeviceOptionsOverride }); let fakeDevice = await setUpPreconnectedFakeDevice(setupOptions); await fakeDevice.device.gatt.connect(); // Blocklisted Characteristic: // https://github.com/WebBluetoothCG/registries/blob/master/gatt_blocklist.txt let dev_info = fakeDevice.fake_services.get('device_information'); await dev_info.addFakeCharacteristic({ uuid: 'serial_number_string', properties: ['read'], }); return fakeDevice; } /** * Returns a BluetoothDevice discovered using |options| and its * corresponding FakePeripheral. * The simulated device is called 'HID Device' it has three known service * UUIDs: 'generic_access', 'device_information', 'human_interface_device'. * The primary service with 'device_information' UUID has a characteristics * with UUID 'serial_number_string'. The device has been connected to and its * attributes are ready to be discovered. * @param {RequestDeviceOptions} options The options for requesting a Bluetooth * Device. * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object * containing a requested BluetoothDevice and its fake counter part. */ async function getHIDDevice(options) { let result = await getConnectedHIDDevice(options, {serviceDiscoveryComplete: true}); return result; } /** Health Thermometer Bluetooth Device Helper Methods */ /** @type {FakeDeviceOptions} */ const healthTherometerFakeDeviceOptionsDefault = { address: '09:09:09:09:09:09', name: 'Health Thermometer', manufacturerData: {0x0001: manufacturer1Data, 0x0002: manufacturer2Data}, knownServiceUUIDs: ['generic_access', 'health_thermometer'], }; /** * Returns a FakeDevice that corresponds to a simulated pre-connected device * called 'Health Thermometer'. The device has two known serviceUUIDs: * 'generic_access' and 'health_thermometer' and some fake manufacturer data. * @returns {Promise} The device fake initialized as a Health * Thermometer device. */ async function setUpHealthThermometerDevice(setupOptionsOverride = {}) { let setupOptions = createSetupOptions( {fakeDeviceOptions: healthTherometerFakeDeviceOptionsDefault}, setupOptionsOverride); return await setUpPreconnectedFakeDevice(setupOptions); } /** * Returns the same fake device as setUpHealthThermometerDevice() except * that connecting to the peripheral will succeed. * @returns {Promise} The device fake initialized as a * connectable Health Thermometer device. */ async function setUpConnectableHealthThermometerDevice() { let fake_device = await setUpHealthThermometerDevice( {fakeDeviceOptions: {connectable: true}}); return fake_device; } /** * Populates a fake_device with various fakes appropriate for a health * thermometer. This resolves to an associative array composed of the fakes, * including the |fake_peripheral|. * @param {FakeDevice} fake_device The Bluetooth fake to populate GATT * services, characteristics, and descriptors on. * @returns {Promise<{fake_peripheral: FakePeripheral, * fake_generic_access: FakeRemoteGATTService, * fake_health_thermometer: FakeRemoteGATTService, * fake_measurement_interval: FakeRemoteGATTCharacteristic, * fake_cccd: FakeRemoteGATTDescriptor, * fake_user_description: FakeRemoteGATTDescriptor, * fake_temperature_measurement: FakeRemoteGATTCharacteristic, * fake_temperature_type: FakeRemoteGATTCharacteristic}>} The FakePeripheral * passed into this method along with the fake GATT services, characteristics, * and descriptors added to it. */ async function populateHealthThermometerFakes(fake_device) { let fake_peripheral = fake_device.fake_peripheral; let fake_generic_access = fake_device.fake_services.get('generic_access'); let fake_health_thermometer = fake_device.fake_services.get('health_thermometer'); let fake_measurement_interval = await fake_health_thermometer.addFakeCharacteristic({ uuid: 'measurement_interval', properties: ['read', 'write', 'indicate'], }); let fake_user_description = await fake_measurement_interval.addFakeDescriptor({ uuid: 'gatt.characteristic_user_description', }); let fake_cccd = await fake_measurement_interval.addFakeDescriptor({ uuid: 'gatt.client_characteristic_configuration', }); let fake_temperature_measurement = await fake_health_thermometer.addFakeCharacteristic({ uuid: 'temperature_measurement', properties: ['indicate'], }); let fake_temperature_type = await fake_health_thermometer.addFakeCharacteristic({ uuid: 'temperature_type', properties: ['read'], }); return { fake_peripheral, fake_generic_access, fake_health_thermometer, fake_measurement_interval, fake_cccd, fake_user_description, fake_temperature_measurement, fake_temperature_type, }; } /** * Returns the same device and fake peripheral as getHealthThermometerDevice() * after another frame (an iframe we insert) discovered the device, * connected to it and discovered its services. * @param {RequestDeviceOptions} options The options for requesting a Bluetooth * Device. * @returns {Promise<{device: BluetoothDevice, fakes: { * fake_peripheral: FakePeripheral, * fake_generic_access: FakeRemoteGATTService, * fake_health_thermometer: FakeRemoteGATTService, * fake_measurement_interval: FakeRemoteGATTCharacteristic, * fake_cccd: FakeRemoteGATTDescriptor, * fake_user_description: FakeRemoteGATTDescriptor, * fake_temperature_measurement: FakeRemoteGATTCharacteristic, * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object * containing a requested BluetoothDevice and all of the GATT fake * objects. */ async function getHealthThermometerDeviceWithServicesDiscovered(options) { let iframe = document.createElement('iframe'); let fake_device = await setUpConnectableHealthThermometerDevice(); let fakes = populateHealthThermometerFakes(fake_device); await fake_device.fake_peripheral.setNextGATTDiscoveryResponse({ code: HCI_SUCCESS, }); await new Promise(resolve => { let src = '/bluetooth/resources/health-thermometer-iframe.html'; // TODO(509038): Can be removed once LayoutTests/bluetooth/* that // use health-thermometer-iframe.html have been moved to // LayoutTests/external/wpt/bluetooth/* if (window.location.pathname.includes('/LayoutTests/')) { src = '../../../external/wpt/bluetooth/resources/health-thermometer-iframe.html'; } iframe.src = src; document.body.appendChild(iframe); iframe.addEventListener('load', resolve); }); await new Promise((resolve, reject) => { callWithTrustedClick(() => { iframe.contentWindow.postMessage( {type: 'DiscoverServices', options: options}, '*'); }); function messageHandler(messageEvent) { if (messageEvent.data == 'DiscoveryComplete') { window.removeEventListener('message', messageHandler); resolve(); } else { reject(new Error(`Unexpected message: ${messageEvent.data}`)); } } window.addEventListener('message', messageHandler); }); let device = await requestDeviceWithTrustedClick(options); await device.gatt.connect(); return Object.assign({device}, fakes); } /** * Returns the device requested and connected in the given iframe context and * fakes from populateHealthThermometerFakes(). * @param {object} iframe The iframe element set up by the caller document. * @returns {Promise<{device: BluetoothDevice, fakes: { * fake_peripheral: FakePeripheral, * fake_generic_access: FakeRemoteGATTService, * fake_health_thermometer: FakeRemoteGATTService, * fake_measurement_interval: FakeRemoteGATTCharacteristic, * fake_cccd: FakeRemoteGATTDescriptor, * fake_user_description: FakeRemoteGATTDescriptor, * fake_temperature_measurement: FakeRemoteGATTCharacteristic, * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object * containing a requested BluetoothDevice and all of the GATT fake * objects. */ async function getHealthThermometerDeviceFromIframe(iframe) { const fake_device = await setUpConnectableHealthThermometerDevice(); const fakes = await populateHealthThermometerFakes(fake_device); await new Promise(resolve => { let src = '/bluetooth/resources/health-thermometer-iframe.html'; iframe.src = src; document.body.appendChild(iframe); iframe.addEventListener('load', resolve, {once: true}); }); await new Promise((resolve, reject) => { callWithTrustedClick(() => { iframe.contentWindow.postMessage( { type: 'RequestAndConnect', options: {filters: [{services: [health_thermometer.name]}]} }, '*'); }); function messageHandler(messageEvent) { if (messageEvent.data == 'Connected') { window.removeEventListener('message', messageHandler); resolve(); } else { reject(new Error(`Unexpected message: ${messageEvent.data}`)); } } window.addEventListener('message', messageHandler, {once: true}); }); const devices = await iframe.contentWindow.navigator.bluetooth.getDevices(); assert_equals(devices.length, 1); return Object.assign({device: devices[0]}, {fakes}); } /** * Similar to getHealthThermometerDevice() except the device * is not connected and thus its services have not been * discovered. * @param {RequestDeviceOptions} options The options for requesting a Bluetooth * Device. * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object * containing a requested BluetoothDevice and its fake counter part. */ async function getDiscoveredHealthThermometerDevice(options = { filters: [{services: ['health_thermometer']}] }) { return await setUpHealthThermometerDevice({requestDeviceOptions: options}); } /** * Similar to getHealthThermometerDevice() except the device has no services, * characteristics, or descriptors. * @param {RequestDeviceOptions} options The options for requesting a Bluetooth * Device. * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object * containing a requested BluetoothDevice and its fake counter part. */ async function getEmptyHealthThermometerDevice(options) { let fake_device = await getDiscoveredHealthThermometerDevice(options); let fake_generic_access = fake_device.fake_services.get('generic_access'); let fake_health_thermometer = fake_device.fake_services.get('health_thermometer'); // Remove services that have been set up by previous steps. await fake_generic_access.remove(); await fake_health_thermometer.remove(); await fake_device.fake_peripheral.setNextGATTConnectionResponse( {code: HCI_SUCCESS}); await fake_device.device.gatt.connect(); await fake_device.fake_peripheral.setNextGATTDiscoveryResponse( {code: HCI_SUCCESS}); return fake_device; } /** * Similar to getHealthThermometerService() except the service has no * characteristics or included services. * @param {RequestDeviceOptions} options The options for requesting a Bluetooth * Device. * @returns {service: BluetoothRemoteGATTService, * fake_health_thermometer: FakeRemoteGATTService} An object containing the * health themometer service object and its corresponding fake. */ async function getEmptyHealthThermometerService(options) { let result = await getDiscoveredHealthThermometerDevice(options); await result.fake_peripheral.setNextGATTConnectionResponse( {code: HCI_SUCCESS}); await result.device.gatt.connect(); let fake_health_thermometer = await result.fake_peripheral.addFakeService({uuid: 'health_thermometer'}); await result.fake_peripheral.setNextGATTDiscoveryResponse( {code: HCI_SUCCESS}); let service = await result.device.gatt.getPrimaryService('health_thermometer'); return { service: service, fake_health_thermometer: fake_health_thermometer, }; } /** * Similar to getHealthThermometerDevice except the GATT discovery * response has not been set yet so more attributes can still be added. * @param {RequestDeviceOptions} options The options for requesting a Bluetooth * Device. * @returns {Promise<{device: BluetoothDevice, fakes: { * fake_peripheral: FakePeripheral, * fake_generic_access: FakeRemoteGATTService, * fake_health_thermometer: FakeRemoteGATTService, * fake_measurement_interval: FakeRemoteGATTCharacteristic, * fake_cccd: FakeRemoteGATTDescriptor, * fake_user_description: FakeRemoteGATTDescriptor, * fake_temperature_measurement: FakeRemoteGATTCharacteristic, * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object * containing a requested BluetoothDevice and all of the GATT fake * objects. */ async function getConnectedHealthThermometerDevice(options) { let fake_device = await getDiscoveredHealthThermometerDevice(options); await fake_device.fake_peripheral.setNextGATTConnectionResponse({ code: HCI_SUCCESS, }); let fakes = await populateHealthThermometerFakes(fake_device); await fake_device.device.gatt.connect(); return Object.assign({device: fake_device.device}, fakes); } /** * Returns an object containing a BluetoothDevice discovered using |options|, * its corresponding FakePeripheral and FakeRemoteGATTServices. * The simulated device is called 'Health Thermometer' it has two known service * UUIDs: 'generic_access' and 'health_thermometer' which correspond to two * services with the same UUIDs. The 'health thermometer' service contains three * characteristics: * - 'temperature_measurement' (indicate), * - 'temperature_type' (read), * - 'measurement_interval' (read, write, indicate) * The 'measurement_interval' characteristic contains a * 'gatt.client_characteristic_configuration' descriptor and a * 'characteristic_user_description' descriptor. * The device has been connected to and its attributes are ready to be * discovered. * @param {RequestDeviceOptions} options The options for requesting a Bluetooth * Device. * @returns {Promise<{device: BluetoothDevice, fakes: { * fake_peripheral: FakePeripheral, * fake_generic_access: FakeRemoteGATTService, * fake_health_thermometer: FakeRemoteGATTService, * fake_measurement_interval: FakeRemoteGATTCharacteristic, * fake_cccd: FakeRemoteGATTDescriptor, * fake_user_description: FakeRemoteGATTDescriptor, * fake_temperature_measurement: FakeRemoteGATTCharacteristic, * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object * containing a requested BluetoothDevice and all of the GATT fake * objects. */ async function getHealthThermometerDevice(options) { let result = await getConnectedHealthThermometerDevice(options); await result.fake_peripheral.setNextGATTDiscoveryResponse({ code: HCI_SUCCESS, }); return result; } /** * Similar to getHealthThermometerDevice except that the peripheral has two * 'health_thermometer' services. * @param {RequestDeviceOptions} options The options for requesting a Bluetooth * Device. * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, * fake_generic_access: FakeRemoteGATTService, fake_health_thermometer1: * FakeRemoteGATTService, fake_health_thermometer2: FakeRemoteGATTService}>} An * object containing a requested Bluetooth device and two fake health * thermometer GATT services. */ async function getTwoHealthThermometerServicesDevice(options) { let result = await getConnectedHealthThermometerDevice(options); let fake_health_thermometer2 = await result.fake_peripheral.addFakeService({uuid: 'health_thermometer'}); await result.fake_peripheral.setNextGATTDiscoveryResponse( {code: HCI_SUCCESS}); return { device: result.device, fake_peripheral: result.fake_peripheral, fake_generic_access: result.fake_generic_access, fake_health_thermometer1: result.fake_health_thermometer, fake_health_thermometer2: fake_health_thermometer2 }; } /** * Returns an object containing a Health Thermometer BluetoothRemoteGattService * and its corresponding FakeRemoteGATTService. * @returns {Promise<{device: BluetoothDevice, fakes: { * fake_peripheral: FakePeripheral, * fake_generic_access: FakeRemoteGATTService, * fake_health_thermometer: FakeRemoteGATTService, * fake_measurement_interval: FakeRemoteGATTCharacteristic, * fake_cccd: FakeRemoteGATTDescriptor, * fake_user_description: FakeRemoteGATTDescriptor, * fake_temperature_measurement: FakeRemoteGATTCharacteristic, * fake_temperature_type: FakeRemoteGATTCharacteristic, * service: BluetoothRemoteGATTService, * fake_service: FakeRemoteGATTService}}>} An object * containing a requested BluetoothDevice and all of the GATT fake * objects. */ async function getHealthThermometerService() { let result = await getHealthThermometerDevice(); let service = await result.device.gatt.getPrimaryService('health_thermometer'); return Object.assign(result, { service, fake_service: result.fake_health_thermometer, }); } /** * Returns an object containing a Measurement Interval * BluetoothRemoteGATTCharacteristic and its corresponding * FakeRemoteGATTCharacteristic. * @returns {Promise<{device: BluetoothDevice, fakes: { * fake_peripheral: FakePeripheral, * fake_generic_access: FakeRemoteGATTService, * fake_health_thermometer: FakeRemoteGATTService, * fake_measurement_interval: FakeRemoteGATTCharacteristic, * fake_cccd: FakeRemoteGATTDescriptor, * fake_user_description: FakeRemoteGATTDescriptor, * fake_temperature_measurement: FakeRemoteGATTCharacteristic, * fake_temperature_type: FakeRemoteGATTCharacteristic, * service: BluetoothRemoteGATTService, * fake_service: FakeRemoteGATTService, * characteristic: BluetoothRemoteGATTCharacteristic, * fake_characteristic: FakeRemoteGATTCharacteristic}}>} An object * containing a requested BluetoothDevice and all of the GATT fake * objects. */ async function getMeasurementIntervalCharacteristic() { let result = await getHealthThermometerService(); let characteristic = await result.service.getCharacteristic('measurement_interval'); return Object.assign(result, { characteristic, fake_characteristic: result.fake_measurement_interval, }); } /** * Returns an object containing a User Description * BluetoothRemoteGATTDescriptor and its corresponding * FakeRemoteGATTDescriptor. * @returns {Promise<{device: BluetoothDevice, fakes: { * fake_peripheral: FakePeripheral, * fake_generic_access: FakeRemoteGATTService, * fake_health_thermometer: FakeRemoteGATTService, * fake_measurement_interval: FakeRemoteGATTCharacteristic, * fake_cccd: FakeRemoteGATTDescriptor, * fake_user_description: FakeRemoteGATTDescriptor, * fake_temperature_measurement: FakeRemoteGATTCharacteristic, * fake_temperature_type: FakeRemoteGATTCharacteristic, * service: BluetoothRemoteGATTService, * fake_service: FakeRemoteGATTService, * characteristic: BluetoothRemoteGATTCharacteristic, * fake_characteristic: FakeRemoteGATTCharacteristic * descriptor: BluetoothRemoteGATTDescriptor, * fake_descriptor: FakeRemoteGATTDescriptor}}>} An object * containing a requested BluetoothDevice and all of the GATT fake * objects. */ async function getUserDescriptionDescriptor() { let result = await getMeasurementIntervalCharacteristic(); let descriptor = await result.characteristic.getDescriptor( 'gatt.characteristic_user_description'); return Object.assign(result, { descriptor, fake_descriptor: result.fake_user_description, }); } /** Heart Rate Bluetooth Device Helper Methods */ /** @type {FakeDeviceOptions} */ const heartRateFakeDeviceOptionsDefault = { address: '08:08:08:08:08:08', name: 'Heart Rate', knownServiceUUIDs: ['generic_access', 'heart_rate'], connectable: false, serviceDiscoveryComplete: false, }; /** @type {RequestDeviceOptions} */ const heartRateRequestDeviceOptionsDefault = { filters: [{services: ['heart_rate']}] }; async function getHeartRateDevice(setupOptionsOverride) { let setupOptions = createSetupOptions( {fakeDeviceOptions: heartRateFakeDeviceOptionsDefault}, setupOptionsOverride); return await setUpPreconnectedFakeDevice(setupOptions); } /** * Returns an array containing two FakePeripherals corresponding * to the simulated devices. * @returns {Promise>} The device fakes initialized as * Health Thermometer and Heart Rate devices. */ async function setUpHealthThermometerAndHeartRateDevices() { await initializeFakeCentral({state: 'powered-on'}); return Promise.all([ fake_central.simulatePreconnectedPeripheral({ address: '09:09:09:09:09:09', name: 'Health Thermometer', manufacturerData: {}, knownServiceUUIDs: ['generic_access', 'health_thermometer'], }), fake_central.simulatePreconnectedPeripheral({ address: '08:08:08:08:08:08', name: 'Heart Rate', manufacturerData: {}, knownServiceUUIDs: ['generic_access', 'heart_rate'], }) ]); }