Skip to content

Commit 41eddf8

Browse files
authored
feat: Support GATT descriptor event (#3443)
1 parent 61588da commit 41eddf8

File tree

8 files changed

+251
-14
lines changed

8 files changed

+251
-14
lines changed

src/bidiMapper/BidiNoOpParser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ export class BidiNoOpParser implements BidiCommandParameterParser {
7070
): Bluetooth.SimulateDescriptorParameters {
7171
return params as Bluetooth.SimulateDescriptorParameters;
7272
}
73+
parseSimulateDescriptorResponseParameters(
74+
params: unknown,
75+
): Bluetooth.SimulateDescriptorResponseParameters {
76+
return params as Bluetooth.SimulateDescriptorResponseParameters;
77+
}
7378
parseSimulateGattConnectionResponseParameters(
7479
params: unknown,
7580
): Bluetooth.SimulateGattConnectionResponseParameters {

src/bidiMapper/BidiParser.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export interface BidiCommandParameterParser {
5454
parseSimulateDescriptorParameters(
5555
params: unknown,
5656
): Bluetooth.SimulateDescriptorParameters;
57+
parseSimulateDescriptorResponseParameters(
58+
params: unknown,
59+
): Bluetooth.SimulateDescriptorResponseParameters;
5760
parseSimulateGattConnectionResponseParameters(
5861
params: unknown,
5962
): Bluetooth.SimulateGattConnectionResponseParameters;

src/bidiMapper/CommandProcessor.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,10 @@ export class CommandProcessor extends EventEmitter<CommandProcessorEventsMap> {
190190
this.#parser.parseSimulateDescriptorParameters(command.params),
191191
);
192192
case 'bluetooth.simulateDescriptorResponse':
193-
throw new UnknownErrorException(
194-
`Method ${command.method} is not implemented.`,
193+
return await this.#bluetoothProcessor.simulateDescriptorResponse(
194+
this.#parser.parseSimulateDescriptorResponseParameters(
195+
command.params,
196+
),
195197
);
196198
case 'bluetooth.simulateGattConnectionResponse':
197199
return await this.#bluetoothProcessor.simulateGattConnectionResponse(

src/bidiMapper/modules/bluetooth/BluetoothProcessor.ts

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,18 @@ class BluetoothDevice {
8484
export class BluetoothProcessor {
8585
#eventManager: EventManager;
8686
#browsingContextStorage: BrowsingContextStorage;
87-
#bluetoothDevices: Map<string, BluetoothDevice>;
87+
#bluetoothDevices = new Map<string, BluetoothDevice>();
8888
// A map from a characteristic id from CDP to its BluetoothCharacteristic object.
89-
#bluetoothCharacteristics: Map<string, BluetoothCharacteristic>;
89+
#bluetoothCharacteristics = new Map<string, BluetoothCharacteristic>();
90+
// A map from a descriptor id from CDP to its BluetoothDescriptor object.
91+
#bluetoothDescriptors = new Map<string, BluetoothDescriptor>();
9092

9193
constructor(
9294
eventManager: EventManager,
9395
browsingContextStorage: BrowsingContextStorage,
9496
) {
9597
this.#eventManager = eventManager;
9698
this.#browsingContextStorage = browsingContextStorage;
97-
this.#bluetoothDevices = new Map();
98-
this.#bluetoothCharacteristics = new Map();
9999
}
100100

101101
#getDevice(address: string): BluetoothDevice {
@@ -163,6 +163,7 @@ export class BluetoothProcessor {
163163
);
164164
this.#bluetoothDevices.clear();
165165
this.#bluetoothCharacteristics.clear();
166+
this.#bluetoothDescriptors.clear();
166167
await context.cdpTarget.browserCdpClient.sendCommand(
167168
'BluetoothEmulation.enable',
168169
{
@@ -182,6 +183,7 @@ export class BluetoothProcessor {
182183
);
183184
this.#bluetoothDevices.clear();
184185
this.#bluetoothCharacteristics.clear();
186+
this.#bluetoothDescriptors.clear();
185187
return {};
186188
}
187189

@@ -334,14 +336,13 @@ export class BluetoothProcessor {
334336
descriptorUuid: params.descriptorUuid,
335337
},
336338
);
337-
characteristic.descriptors.set(
339+
const descriptor = new BluetoothDescriptor(
340+
response.descriptorId,
338341
params.descriptorUuid,
339-
new BluetoothDescriptor(
340-
response.descriptorId,
341-
params.descriptorUuid,
342-
characteristic,
343-
),
342+
characteristic,
344343
);
344+
characteristic.descriptors.set(params.descriptorUuid, descriptor);
345+
this.#bluetoothDescriptors.set(descriptor.id, descriptor);
345346
return {};
346347
}
347348
case 'remove': {
@@ -356,6 +357,7 @@ export class BluetoothProcessor {
356357
},
357358
);
358359
characteristic.descriptors.delete(params.descriptorUuid);
360+
this.#bluetoothDescriptors.delete(descriptor.id);
359361
return {};
360362
}
361363
default:
@@ -365,6 +367,34 @@ export class BluetoothProcessor {
365367
}
366368
}
367369

370+
async simulateDescriptorResponse(
371+
params: Bluetooth.SimulateDescriptorResponseParameters,
372+
): Promise<EmptyResult> {
373+
const context = this.#browsingContextStorage.getContext(params.context);
374+
const device = this.#getDevice(params.address);
375+
const service = this.#getService(device, params.serviceUuid);
376+
const characteristic = this.#getCharacteristic(
377+
service,
378+
params.characteristicUuid,
379+
);
380+
const descriptor = this.#getDescriptor(
381+
characteristic,
382+
params.descriptorUuid,
383+
);
384+
await context.cdpTarget.browserCdpClient.sendCommand(
385+
'BluetoothEmulation.simulateDescriptorOperationResponse',
386+
{
387+
descriptorId: descriptor.id,
388+
type: params.type,
389+
code: params.code,
390+
...(params.data && {
391+
data: btoa(String.fromCharCode(...params.data)),
392+
}),
393+
},
394+
);
395+
return {};
396+
}
397+
368398
async simulateGattConnectionResponse(
369399
params: Bluetooth.SimulateGattConnectionResponseParameters,
370400
): Promise<EmptyResult> {
@@ -529,6 +559,33 @@ export class BluetoothProcessor {
529559
);
530560
},
531561
);
562+
cdpTarget.browserCdpClient.on(
563+
'BluetoothEmulation.descriptorOperationReceived',
564+
(event) => {
565+
if (!this.#bluetoothDescriptors.has(event.descriptorId)) {
566+
return;
567+
}
568+
const descriptor = this.#bluetoothDescriptors.get(event.descriptorId)!;
569+
this.#eventManager.registerEvent(
570+
{
571+
type: 'event',
572+
method: 'bluetooth.descriptorEventGenerated',
573+
params: {
574+
context: cdpTarget.id,
575+
address: descriptor.characteristic.service.device.address,
576+
serviceUuid: descriptor.characteristic.service.uuid,
577+
characteristicUuid: descriptor.characteristic.uuid,
578+
descriptorUuid: descriptor.uuid,
579+
type: event.type,
580+
...(event.data && {
581+
data: Array.from(atob(event.data), (c) => c.charCodeAt(0)),
582+
}),
583+
},
584+
},
585+
cdpTarget.id,
586+
);
587+
},
588+
);
532589
}
533590

534591
async handleRequestDevicePrompt(

src/bidiTab/BidiParser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ export class BidiParser implements BidiCommandParameterParser {
6969
): Bluetooth.SimulateDescriptorParameters {
7070
return Parser.Bluetooth.parseSimulateDescriptorParams(params);
7171
}
72+
parseSimulateDescriptorResponseParameters(
73+
params: unknown,
74+
): Bluetooth.SimulateDescriptorResponseParameters {
75+
return Parser.Bluetooth.parseSimulateDescriptorResponseParams(params);
76+
}
7277
parseSimulateGattConnectionResponseParameters(
7378
params: unknown,
7479
): Bluetooth.SimulateGattConnectionResponseParameters {

src/protocol-parser/protocol-parser.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,15 @@ export namespace Bluetooth {
496496
WebDriverBidiBluetooth.Bluetooth.SimulateDescriptorParametersSchema,
497497
) as Protocol.Bluetooth.SimulateDescriptorParameters;
498498
}
499+
export function parseSimulateDescriptorResponseParams(
500+
params: unknown,
501+
): Protocol.Bluetooth.SimulateDescriptorResponseParameters {
502+
return parseObject(
503+
params,
504+
WebDriverBidiBluetooth.Bluetooth
505+
.SimulateDescriptorResponseParametersSchema,
506+
) as Protocol.Bluetooth.SimulateDescriptorResponseParameters;
507+
}
499508
export function parseSimulateGattConnectionResponseParams(
500509
params: unknown,
501510
): Protocol.Bluetooth.SimulateGattConnectionResponseParameters {

src/protocol/chromium-bidi.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export namespace Bluetooth {
107107
RequestDevicePromptUpdated = 'bluetooth.requestDevicePromptUpdated',
108108
GattConnectionAttempted = 'bluetooth.gattConnectionAttempted',
109109
CharacteristicEventGenerated = 'bluetooth.characteristicEventGenerated',
110+
DescriptorEventGenerated = 'bluetooth.descriptorEventGenerated',
110111
}
111112
}
112113

@@ -137,7 +138,8 @@ export type CommandResponse =
137138
export type BluetoothEvent =
138139
| ExternalSpecEvent<WebDriverBidiBluetooth.Bluetooth.RequestDevicePromptUpdated>
139140
| ExternalSpecEvent<WebDriverBidiBluetooth.Bluetooth.GattConnectionAttempted>
140-
| ExternalSpecEvent<WebDriverBidiBluetooth.Bluetooth.CharacteristicEventGenerated>;
141+
| ExternalSpecEvent<WebDriverBidiBluetooth.Bluetooth.CharacteristicEventGenerated>
142+
| ExternalSpecEvent<WebDriverBidiBluetooth.Bluetooth.DescriptorEventGenerated>;
141143
export type Event = WebDriverBidi.Event | Cdp.Event | BluetoothEvent;
142144

143145
export const EVENT_NAMES = new Set([

tests/bluetooth/test_descriptor_emulation.py

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515

1616
import pytest
1717
import pytest_asyncio
18-
from test_helpers import execute_command
18+
from test_helpers import (execute_command, send_JSON_command, subscribe,
19+
wait_for_event)
1920

2021
from . import (BATTERY_SERVICE_UUID,
2122
CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR_UUID,
@@ -25,6 +26,25 @@
2526
create_gatt_connection, disable_simulation, setup_device,
2627
setup_granted_device, simulate_descriptor, simulate_service)
2728

29+
DESCRIPTOR_EVENT_GENERATED = 'bluetooth.descriptorEventGenerated'
30+
31+
32+
async def setup_descriptor(websocket, context_id: str, html, service_uuid: str,
33+
characteristic_uuid: str, characteristic_properties,
34+
descriptor_uuid: str):
35+
device_address = await setup_granted_device(websocket, context_id, html,
36+
[service_uuid])
37+
await create_gatt_connection(websocket, context_id)
38+
await simulate_service(websocket, context_id, device_address, service_uuid,
39+
'add')
40+
await add_characteristic(websocket, context_id, device_address,
41+
service_uuid, characteristic_uuid,
42+
characteristic_properties)
43+
await simulate_descriptor(websocket, context_id, device_address,
44+
service_uuid, characteristic_uuid,
45+
descriptor_uuid, 'add')
46+
return device_address
47+
2848

2949
async def get_descriptors(websocket, context_id: str, service_uuid: str,
3050
characteristic_uuid: str) -> list[str]:
@@ -265,3 +285,137 @@ async def test_bluetooth_add_descriptor_to_unknown_characteristic(
265285
websocket, context_id, device_address, HEART_RATE_SERVICE_UUID,
266286
MEASUREMENT_INTERVAL_CHARACTERISTIC_UUID,
267287
CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR_UUID, 'add')
288+
289+
290+
@pytest.mark.asyncio
291+
@pytest.mark.parametrize('capabilities', [{
292+
'goog:chromeOptions': {
293+
'args': ['--enable-features=WebBluetooth']
294+
}
295+
}],
296+
indirect=True)
297+
async def test_bluetooth_descriptor_write_event(websocket, context_id, html):
298+
await setup_descriptor(websocket, context_id, html,
299+
HEART_RATE_SERVICE_UUID,
300+
MEASUREMENT_INTERVAL_CHARACTERISTIC_UUID,
301+
{'write': True},
302+
CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR_UUID)
303+
await subscribe(websocket, [DESCRIPTOR_EVENT_GENERATED])
304+
expected_data = [42, 27]
305+
await send_JSON_command(
306+
websocket, {
307+
'method': 'script.evaluate',
308+
'params': {
309+
'expression': f'''
310+
(async () => {{
311+
const service = await device.gatt.getPrimaryService('{HEART_RATE_SERVICE_UUID}');
312+
const characteristic = await service.getCharacteristic('{MEASUREMENT_INTERVAL_CHARACTERISTIC_UUID}');
313+
const descriptor = await characteristic.getDescriptor('{CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR_UUID}');
314+
await descriptor.writeValue(new Uint8Array({expected_data}));
315+
}})();
316+
''',
317+
'awaitPromise': False,
318+
'target': {
319+
'context': context_id,
320+
},
321+
'userActivation': True
322+
}
323+
})
324+
event = await wait_for_event(websocket, DESCRIPTOR_EVENT_GENERATED)
325+
assert event['params'] == {
326+
'context': context_id,
327+
'address': FAKE_DEVICE_ADDRESS,
328+
'serviceUuid': HEART_RATE_SERVICE_UUID,
329+
'characteristicUuid': MEASUREMENT_INTERVAL_CHARACTERISTIC_UUID,
330+
'descriptorUuid': CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR_UUID,
331+
'type': 'write',
332+
'data': expected_data,
333+
}
334+
335+
await execute_command(
336+
websocket, {
337+
'method': 'bluetooth.simulateDescriptorResponse',
338+
'params': {
339+
'context': context_id,
340+
'address': FAKE_DEVICE_ADDRESS,
341+
'serviceUuid': HEART_RATE_SERVICE_UUID,
342+
'characteristicUuid': MEASUREMENT_INTERVAL_CHARACTERISTIC_UUID,
343+
'descriptorUuid': CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR_UUID,
344+
'type': 'write',
345+
'code': 0x0
346+
}
347+
})
348+
349+
350+
@pytest.mark.asyncio
351+
@pytest.mark.parametrize('capabilities', [{
352+
'goog:chromeOptions': {
353+
'args': ['--enable-features=WebBluetooth']
354+
}
355+
}],
356+
indirect=True)
357+
async def test_bluetooth_descriptor_read_event(websocket, context_id, html):
358+
await setup_descriptor(websocket, context_id, html,
359+
HEART_RATE_SERVICE_UUID,
360+
MEASUREMENT_INTERVAL_CHARACTERISTIC_UUID,
361+
{'read': True},
362+
CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR_UUID)
363+
await subscribe(websocket, [DESCRIPTOR_EVENT_GENERATED])
364+
await send_JSON_command(
365+
websocket, {
366+
'method': 'script.evaluate',
367+
'params': {
368+
'expression': f'''
369+
let readResult;
370+
(async () => {{
371+
const service = await device.gatt.getPrimaryService('{HEART_RATE_SERVICE_UUID}');
372+
const characteristic = await service.getCharacteristic('{MEASUREMENT_INTERVAL_CHARACTERISTIC_UUID}');
373+
const descriptor = await characteristic.getDescriptor('{CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR_UUID}');
374+
readResult = await descriptor.readValue();
375+
}})();
376+
''',
377+
'awaitPromise': False,
378+
'target': {
379+
'context': context_id,
380+
},
381+
'userActivation': True
382+
}
383+
})
384+
event = await wait_for_event(websocket, DESCRIPTOR_EVENT_GENERATED)
385+
assert event['params'] == {
386+
'context': context_id,
387+
'address': FAKE_DEVICE_ADDRESS,
388+
'serviceUuid': HEART_RATE_SERVICE_UUID,
389+
'characteristicUuid': MEASUREMENT_INTERVAL_CHARACTERISTIC_UUID,
390+
'descriptorUuid': CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR_UUID,
391+
'type': 'read',
392+
}
393+
394+
expected_data = [1, 2]
395+
await execute_command(
396+
websocket, {
397+
'method': 'bluetooth.simulateDescriptorResponse',
398+
'params': {
399+
'context': context_id,
400+
'address': FAKE_DEVICE_ADDRESS,
401+
'serviceUuid': HEART_RATE_SERVICE_UUID,
402+
'characteristicUuid': MEASUREMENT_INTERVAL_CHARACTERISTIC_UUID,
403+
'descriptorUuid': CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR_UUID,
404+
'type': 'read',
405+
'code': 0x0,
406+
'data': expected_data
407+
}
408+
})
409+
response = await execute_command(
410+
websocket, {
411+
'method': 'script.evaluate',
412+
'params': {
413+
'expression': 'String.fromCharCode(...new Uint8Array(readResult.buffer))',
414+
'awaitPromise': False,
415+
'target': {
416+
'context': context_id,
417+
},
418+
'userActivation': True
419+
}
420+
})
421+
assert [ord(c) for c in response['result']['value']] == expected_data

0 commit comments

Comments
 (0)