"use strict"; /** * Copyright 2024 Google LLC. * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.BluetoothProcessor = void 0; const protocol_js_1 = require("../../../protocol/protocol.js"); /** Represents a base Bluetooth GATT item. */ class BluetoothGattItem { id; uuid; constructor(id, uuid) { this.id = id; this.uuid = uuid; } } /** Represents a Bluetooth descriptor. */ class BluetoothDescriptor extends BluetoothGattItem { characteristic; constructor(id, uuid, characteristic) { super(id, uuid); this.characteristic = characteristic; } } /** Represents a Bluetooth characteristic. */ class BluetoothCharacteristic extends BluetoothGattItem { descriptors = new Map(); service; constructor(id, uuid, service) { super(id, uuid); this.service = service; } } /** Represents a Bluetooth service. */ class BluetoothService extends BluetoothGattItem { characteristics = new Map(); device; constructor(id, uuid, device) { super(id, uuid); this.device = device; } } /** Represents a Bluetooth device. */ class BluetoothDevice { address; services = new Map(); constructor(address) { this.address = address; } } class BluetoothProcessor { #eventManager; #browsingContextStorage; #bluetoothDevices = new Map(); // A map from a characteristic id from CDP to its BluetoothCharacteristic object. #bluetoothCharacteristics = new Map(); // A map from a descriptor id from CDP to its BluetoothDescriptor object. #bluetoothDescriptors = new Map(); constructor(eventManager, browsingContextStorage) { this.#eventManager = eventManager; this.#browsingContextStorage = browsingContextStorage; } #getDevice(address) { const device = this.#bluetoothDevices.get(address); if (!device) { throw new protocol_js_1.InvalidArgumentException(`Bluetooth device with address ${address} does not exist`); } return device; } #getService(device, serviceUuid) { const service = device.services.get(serviceUuid); if (!service) { throw new protocol_js_1.InvalidArgumentException(`Service with UUID ${serviceUuid} on device ${device.address} does not exist`); } return service; } #getCharacteristic(service, characteristicUuid) { const characteristic = service.characteristics.get(characteristicUuid); if (!characteristic) { throw new protocol_js_1.InvalidArgumentException(`Characteristic with UUID ${characteristicUuid} does not exist for service ${service.uuid} on device ${service.device.address}`); } return characteristic; } #getDescriptor(characteristic, descriptorUuid) { const descriptor = characteristic.descriptors.get(descriptorUuid); if (!descriptor) { throw new protocol_js_1.InvalidArgumentException(`Descriptor with UUID ${descriptorUuid} does not exist for characteristic ${characteristic.uuid} on service ${characteristic.service.uuid} on device ${characteristic.service.device.address}`); } return descriptor; } async simulateAdapter(params) { if (params.state === undefined) { // The bluetooth.simulateAdapter Command // Step 4.2. If params["state"] does not exist, return error with error code invalid argument. // https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command throw new protocol_js_1.InvalidArgumentException(`Parameter "state" is required for creating a Bluetooth adapter`); } const context = this.#browsingContextStorage.getContext(params.context); // Bluetooth spec requires overriding the existing adapter (step 6). From the CDP // perspective, we need to disable the emulation first. // https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.disable'); this.#bluetoothDevices.clear(); this.#bluetoothCharacteristics.clear(); this.#bluetoothDescriptors.clear(); await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.enable', { state: params.state, leSupported: params.leSupported ?? true, }); return {}; } async disableSimulation(params) { const context = this.#browsingContextStorage.getContext(params.context); await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.disable'); this.#bluetoothDevices.clear(); this.#bluetoothCharacteristics.clear(); this.#bluetoothDescriptors.clear(); return {}; } async simulatePreconnectedPeripheral(params) { if (this.#bluetoothDevices.has(params.address)) { throw new protocol_js_1.InvalidArgumentException(`Bluetooth device with address ${params.address} already exists`); } const context = this.#browsingContextStorage.getContext(params.context); await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulatePreconnectedPeripheral', { address: params.address, name: params.name, knownServiceUuids: params.knownServiceUuids, manufacturerData: params.manufacturerData, }); this.#bluetoothDevices.set(params.address, new BluetoothDevice(params.address)); return {}; } async simulateAdvertisement(params) { const context = this.#browsingContextStorage.getContext(params.context); await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateAdvertisement', { entry: params.scanEntry, }); return {}; } async simulateCharacteristic(params) { const device = this.#getDevice(params.address); const service = this.#getService(device, params.serviceUuid); const context = this.#browsingContextStorage.getContext(params.context); switch (params.type) { case 'add': { if (params.characteristicProperties === undefined) { throw new protocol_js_1.InvalidArgumentException(`Parameter "characteristicProperties" is required for adding a Bluetooth characteristic`); } if (service.characteristics.has(params.characteristicUuid)) { throw new protocol_js_1.InvalidArgumentException(`Characteristic with UUID ${params.characteristicUuid} already exists`); } const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addCharacteristic', { serviceId: service.id, characteristicUuid: params.characteristicUuid, properties: params.characteristicProperties, }); const characteristic = new BluetoothCharacteristic(response.characteristicId, params.characteristicUuid, service); service.characteristics.set(params.characteristicUuid, characteristic); this.#bluetoothCharacteristics.set(characteristic.id, characteristic); return {}; } case 'remove': { if (params.characteristicProperties !== undefined) { throw new protocol_js_1.InvalidArgumentException(`Parameter "characteristicProperties" should not be provided for removing a Bluetooth characteristic`); } const characteristic = this.#getCharacteristic(service, params.characteristicUuid); await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeCharacteristic', { characteristicId: characteristic.id, }); service.characteristics.delete(params.characteristicUuid); this.#bluetoothCharacteristics.delete(characteristic.id); return {}; } default: throw new protocol_js_1.InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`); } } async simulateCharacteristicResponse(params) { const context = this.#browsingContextStorage.getContext(params.context); const device = this.#getDevice(params.address); const service = this.#getService(device, params.serviceUuid); const characteristic = this.#getCharacteristic(service, params.characteristicUuid); await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateCharacteristicOperationResponse', { characteristicId: characteristic.id, type: params.type, code: params.code, ...(params.data && { data: btoa(String.fromCharCode(...params.data)), }), }); return {}; } async simulateDescriptor(params) { const device = this.#getDevice(params.address); const service = this.#getService(device, params.serviceUuid); const characteristic = this.#getCharacteristic(service, params.characteristicUuid); const context = this.#browsingContextStorage.getContext(params.context); switch (params.type) { case 'add': { if (characteristic.descriptors.has(params.descriptorUuid)) { throw new protocol_js_1.InvalidArgumentException(`Descriptor with UUID ${params.descriptorUuid} already exists`); } const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addDescriptor', { characteristicId: characteristic.id, descriptorUuid: params.descriptorUuid, }); const descriptor = new BluetoothDescriptor(response.descriptorId, params.descriptorUuid, characteristic); characteristic.descriptors.set(params.descriptorUuid, descriptor); this.#bluetoothDescriptors.set(descriptor.id, descriptor); return {}; } case 'remove': { const descriptor = this.#getDescriptor(characteristic, params.descriptorUuid); await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeDescriptor', { descriptorId: descriptor.id, }); characteristic.descriptors.delete(params.descriptorUuid); this.#bluetoothDescriptors.delete(descriptor.id); return {}; } default: throw new protocol_js_1.InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`); } } async simulateDescriptorResponse(params) { const context = this.#browsingContextStorage.getContext(params.context); const device = this.#getDevice(params.address); const service = this.#getService(device, params.serviceUuid); const characteristic = this.#getCharacteristic(service, params.characteristicUuid); const descriptor = this.#getDescriptor(characteristic, params.descriptorUuid); await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateDescriptorOperationResponse', { descriptorId: descriptor.id, type: params.type, code: params.code, ...(params.data && { data: btoa(String.fromCharCode(...params.data)), }), }); return {}; } async simulateGattConnectionResponse(params) { const context = this.#browsingContextStorage.getContext(params.context); await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTOperationResponse', { address: params.address, type: 'connection', code: params.code, }); return {}; } async simulateGattDisconnection(params) { const context = this.#browsingContextStorage.getContext(params.context); await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTDisconnection', { address: params.address, }); return {}; } async simulateService(params) { const device = this.#getDevice(params.address); const context = this.#browsingContextStorage.getContext(params.context); switch (params.type) { case 'add': { if (device.services.has(params.uuid)) { throw new protocol_js_1.InvalidArgumentException(`Service with UUID ${params.uuid} already exists`); } const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addService', { address: params.address, serviceUuid: params.uuid, }); device.services.set(params.uuid, new BluetoothService(response.serviceId, params.uuid, device)); return {}; } case 'remove': { const service = this.#getService(device, params.uuid); await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeService', { serviceId: service.id, }); device.services.delete(params.uuid); return {}; } default: throw new protocol_js_1.InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`); } } onCdpTargetCreated(cdpTarget) { cdpTarget.cdpClient.on('DeviceAccess.deviceRequestPrompted', (event) => { this.#eventManager.registerEvent({ type: 'event', method: 'bluetooth.requestDevicePromptUpdated', params: { context: cdpTarget.id, prompt: event.id, devices: event.devices, }, }, cdpTarget.id); }); cdpTarget.browserCdpClient.on('BluetoothEmulation.gattOperationReceived', async (event) => { switch (event.type) { case 'connection': this.#eventManager.registerEvent({ type: 'event', method: 'bluetooth.gattConnectionAttempted', params: { context: cdpTarget.id, address: event.address, }, }, cdpTarget.id); return; case 'discovery': // Chromium Web Bluetooth simulation generates this GATT discovery event when // a page attempts to get services for a given Bluetooth device for the first time. // This 'get services' operation is put on hold until a GATT discovery response // is sent to the simulation. // Note: Web Bluetooth automation (see https://webbluetoothcg.github.io/web-bluetooth/#automated-testing) // does not support simulating a GATT discovery response. This is because simulated services, characteristics, // or descriptors are immediately visible to the simulation, meaning it doesn't have a distinct // DISCOVERY state. Therefore, this code simulates a successful GATT discovery // response upon receiving this event. await cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTOperationResponse', { address: event.address, type: 'discovery', code: 0x0, }); } }); cdpTarget.browserCdpClient.on('BluetoothEmulation.characteristicOperationReceived', (event) => { if (!this.#bluetoothCharacteristics.has(event.characteristicId)) { return; } let type; if (event.type === 'write') { // write-default-deprecated comes from // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothremotegattcharacteristic-writevalue, // which is deprecated so not supported. if (event.writeType === 'write-default-deprecated') { return; } type = event.writeType; } else { type = event.type; } const characteristic = this.#bluetoothCharacteristics.get(event.characteristicId); this.#eventManager.registerEvent({ type: 'event', method: 'bluetooth.characteristicEventGenerated', params: { context: cdpTarget.id, address: characteristic.service.device.address, serviceUuid: characteristic.service.uuid, characteristicUuid: characteristic.uuid, type, ...(event.data && { data: Array.from(atob(event.data), (c) => c.charCodeAt(0)), }), }, }, cdpTarget.id); }); cdpTarget.browserCdpClient.on('BluetoothEmulation.descriptorOperationReceived', (event) => { if (!this.#bluetoothDescriptors.has(event.descriptorId)) { return; } const descriptor = this.#bluetoothDescriptors.get(event.descriptorId); this.#eventManager.registerEvent({ type: 'event', method: 'bluetooth.descriptorEventGenerated', params: { context: cdpTarget.id, address: descriptor.characteristic.service.device.address, serviceUuid: descriptor.characteristic.service.uuid, characteristicUuid: descriptor.characteristic.uuid, descriptorUuid: descriptor.uuid, type: event.type, ...(event.data && { data: Array.from(atob(event.data), (c) => c.charCodeAt(0)), }), }, }, cdpTarget.id); }); } async handleRequestDevicePrompt(params) { const context = this.#browsingContextStorage.getContext(params.context); if (params.accept) { await context.cdpTarget.cdpClient.sendCommand('DeviceAccess.selectPrompt', { id: params.prompt, deviceId: params.device, }); } else { await context.cdpTarget.cdpClient.sendCommand('DeviceAccess.cancelPrompt', { id: params.prompt, }); } return {}; } } exports.BluetoothProcessor = BluetoothProcessor; //# sourceMappingURL=BluetoothProcessor.js.map