bumble 0.0.195__py3-none-any.whl → 0.0.198__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bumble/_version.py +2 -2
- bumble/apps/auracast.py +351 -66
- bumble/apps/console.py +5 -20
- bumble/apps/device_info.py +230 -0
- bumble/apps/gatt_dump.py +4 -0
- bumble/apps/lea_unicast/app.py +16 -17
- bumble/at.py +12 -6
- bumble/avc.py +8 -5
- bumble/avctp.py +3 -2
- bumble/avdtp.py +5 -1
- bumble/avrcp.py +2 -1
- bumble/codecs.py +17 -13
- bumble/colors.py +6 -2
- bumble/core.py +37 -7
- bumble/device.py +382 -111
- bumble/drivers/rtk.py +13 -8
- bumble/gatt.py +6 -1
- bumble/gatt_client.py +10 -4
- bumble/hci.py +50 -25
- bumble/hid.py +24 -28
- bumble/host.py +4 -0
- bumble/l2cap.py +24 -17
- bumble/link.py +8 -3
- bumble/profiles/ascs.py +739 -0
- bumble/profiles/bap.py +1 -874
- bumble/profiles/bass.py +440 -0
- bumble/profiles/csip.py +4 -4
- bumble/profiles/gap.py +110 -0
- bumble/profiles/heart_rate_service.py +4 -3
- bumble/profiles/le_audio.py +43 -9
- bumble/profiles/mcp.py +448 -0
- bumble/profiles/pacs.py +210 -0
- bumble/profiles/tmap.py +89 -0
- bumble/rfcomm.py +4 -2
- bumble/sdp.py +13 -11
- bumble/smp.py +20 -8
- bumble/snoop.py +5 -4
- bumble/transport/__init__.py +8 -2
- bumble/transport/android_emulator.py +9 -3
- bumble/transport/android_netsim.py +9 -7
- bumble/transport/common.py +46 -18
- bumble/transport/pyusb.py +2 -2
- bumble/transport/unix.py +56 -0
- bumble/transport/usb.py +57 -46
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/METADATA +41 -41
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/RECORD +50 -42
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/WHEEL +1 -1
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/LICENSE +0 -0
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/top_level.txt +0 -0
bumble/_version.py
CHANGED
bumble/apps/auracast.py
CHANGED
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
import asyncio
|
|
20
|
+
import contextlib
|
|
20
21
|
import dataclasses
|
|
21
22
|
import logging
|
|
22
23
|
import os
|
|
23
|
-
from typing import cast, Dict, Optional, Tuple
|
|
24
|
+
from typing import cast, Any, AsyncGenerator, Coroutine, Dict, Optional, Tuple
|
|
24
25
|
|
|
25
26
|
import click
|
|
26
27
|
import pyee
|
|
@@ -32,6 +33,7 @@ import bumble.device
|
|
|
32
33
|
import bumble.gatt
|
|
33
34
|
import bumble.hci
|
|
34
35
|
import bumble.profiles.bap
|
|
36
|
+
import bumble.profiles.bass
|
|
35
37
|
import bumble.profiles.pbp
|
|
36
38
|
import bumble.transport
|
|
37
39
|
import bumble.utils
|
|
@@ -46,14 +48,16 @@ logger = logging.getLogger(__name__)
|
|
|
46
48
|
# -----------------------------------------------------------------------------
|
|
47
49
|
# Constants
|
|
48
50
|
# -----------------------------------------------------------------------------
|
|
49
|
-
AURACAST_DEFAULT_DEVICE_NAME =
|
|
50
|
-
AURACAST_DEFAULT_DEVICE_ADDRESS = bumble.hci.Address(
|
|
51
|
+
AURACAST_DEFAULT_DEVICE_NAME = 'Bumble Auracast'
|
|
52
|
+
AURACAST_DEFAULT_DEVICE_ADDRESS = bumble.hci.Address('F0:F1:F2:F3:F4:F5')
|
|
53
|
+
AURACAST_DEFAULT_SYNC_TIMEOUT = 5.0
|
|
54
|
+
AURACAST_DEFAULT_ATT_MTU = 256
|
|
51
55
|
|
|
52
56
|
|
|
53
57
|
# -----------------------------------------------------------------------------
|
|
54
|
-
#
|
|
58
|
+
# Scan For Broadcasts
|
|
55
59
|
# -----------------------------------------------------------------------------
|
|
56
|
-
class
|
|
60
|
+
class BroadcastScanner(pyee.EventEmitter):
|
|
57
61
|
@dataclasses.dataclass
|
|
58
62
|
class Broadcast(pyee.EventEmitter):
|
|
59
63
|
name: str
|
|
@@ -79,22 +83,6 @@ class BroadcastDiscoverer:
|
|
|
79
83
|
self.sync.on('periodic_advertisement', self.on_periodic_advertisement)
|
|
80
84
|
self.sync.on('biginfo_advertisement', self.on_biginfo_advertisement)
|
|
81
85
|
|
|
82
|
-
self.establishment_timeout_task = asyncio.create_task(
|
|
83
|
-
self.wait_for_establishment()
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
async def wait_for_establishment(self) -> None:
|
|
87
|
-
await asyncio.sleep(5.0)
|
|
88
|
-
if self.sync.state == bumble.device.PeriodicAdvertisingSync.State.PENDING:
|
|
89
|
-
print(
|
|
90
|
-
color(
|
|
91
|
-
'!!! Periodic advertisement sync not established in time, '
|
|
92
|
-
'canceling',
|
|
93
|
-
'red',
|
|
94
|
-
)
|
|
95
|
-
)
|
|
96
|
-
await self.sync.terminate()
|
|
97
|
-
|
|
98
86
|
def update(self, advertisement: bumble.device.Advertisement) -> None:
|
|
99
87
|
self.rssi = advertisement.rssi
|
|
100
88
|
for service_data in advertisement.data.get_all(
|
|
@@ -139,6 +127,8 @@ class BroadcastDiscoverer:
|
|
|
139
127
|
data,
|
|
140
128
|
)
|
|
141
129
|
|
|
130
|
+
self.emit('update')
|
|
131
|
+
|
|
142
132
|
def print(self) -> None:
|
|
143
133
|
print(
|
|
144
134
|
color('Broadcast:', 'yellow'),
|
|
@@ -227,13 +217,12 @@ class BroadcastDiscoverer:
|
|
|
227
217
|
)
|
|
228
218
|
|
|
229
219
|
def on_sync_establishment(self) -> None:
|
|
230
|
-
self.
|
|
231
|
-
self.emit('change')
|
|
220
|
+
self.emit('sync_establishment')
|
|
232
221
|
|
|
233
222
|
def on_sync_loss(self) -> None:
|
|
234
223
|
self.basic_audio_announcement = None
|
|
235
224
|
self.biginfo = None
|
|
236
|
-
self.emit('
|
|
225
|
+
self.emit('sync_loss')
|
|
237
226
|
|
|
238
227
|
def on_periodic_advertisement(
|
|
239
228
|
self, advertisement: bumble.device.PeriodicAdvertisement
|
|
@@ -268,37 +257,21 @@ class BroadcastDiscoverer:
|
|
|
268
257
|
filter_duplicates: bool,
|
|
269
258
|
sync_timeout: float,
|
|
270
259
|
):
|
|
260
|
+
super().__init__()
|
|
271
261
|
self.device = device
|
|
272
262
|
self.filter_duplicates = filter_duplicates
|
|
273
263
|
self.sync_timeout = sync_timeout
|
|
274
|
-
self.broadcasts: Dict[bumble.hci.Address,
|
|
275
|
-
self.status_message = ''
|
|
264
|
+
self.broadcasts: Dict[bumble.hci.Address, BroadcastScanner.Broadcast] = {}
|
|
276
265
|
device.on('advertisement', self.on_advertisement)
|
|
277
266
|
|
|
278
|
-
async def
|
|
279
|
-
self.status_message = color('Scanning...', 'green')
|
|
267
|
+
async def start(self) -> None:
|
|
280
268
|
await self.device.start_scanning(
|
|
281
269
|
active=False,
|
|
282
270
|
filter_duplicates=False,
|
|
283
271
|
)
|
|
284
272
|
|
|
285
|
-
def
|
|
286
|
-
|
|
287
|
-
print('\033[H')
|
|
288
|
-
print('\033[0J')
|
|
289
|
-
print('\033[H')
|
|
290
|
-
|
|
291
|
-
# Print the status message
|
|
292
|
-
print(self.status_message)
|
|
293
|
-
print("==========================================")
|
|
294
|
-
|
|
295
|
-
# Print all broadcasts
|
|
296
|
-
for broadcast in self.broadcasts.values():
|
|
297
|
-
broadcast.print()
|
|
298
|
-
print('------------------------------------------')
|
|
299
|
-
|
|
300
|
-
# Clear the screen to the bottom
|
|
301
|
-
print('\033[0J')
|
|
273
|
+
async def stop(self) -> None:
|
|
274
|
+
await self.device.stop_scanning()
|
|
302
275
|
|
|
303
276
|
def on_advertisement(self, advertisement: bumble.device.Advertisement) -> None:
|
|
304
277
|
if (
|
|
@@ -311,7 +284,6 @@ class BroadcastDiscoverer:
|
|
|
311
284
|
|
|
312
285
|
if broadcast := self.broadcasts.get(advertisement.address):
|
|
313
286
|
broadcast.update(advertisement)
|
|
314
|
-
self.refresh()
|
|
315
287
|
return
|
|
316
288
|
|
|
317
289
|
bumble.utils.AsyncRunner.spawn(
|
|
@@ -331,41 +303,318 @@ class BroadcastDiscoverer:
|
|
|
331
303
|
name,
|
|
332
304
|
periodic_advertising_sync,
|
|
333
305
|
)
|
|
334
|
-
broadcast.on('change', self.refresh)
|
|
335
306
|
broadcast.update(advertisement)
|
|
336
307
|
self.broadcasts[advertisement.address] = broadcast
|
|
337
308
|
periodic_advertising_sync.on('loss', lambda: self.on_broadcast_loss(broadcast))
|
|
338
|
-
self.
|
|
339
|
-
f'+Found {len(self.broadcasts)} broadcasts', 'green'
|
|
340
|
-
)
|
|
341
|
-
self.refresh()
|
|
309
|
+
self.emit('new_broadcast', broadcast)
|
|
342
310
|
|
|
343
311
|
def on_broadcast_loss(self, broadcast: Broadcast) -> None:
|
|
344
312
|
del self.broadcasts[broadcast.sync.advertiser_address]
|
|
345
313
|
bumble.utils.AsyncRunner.spawn(broadcast.sync.terminate())
|
|
314
|
+
self.emit('broadcast_loss', broadcast)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class PrintingBroadcastScanner:
|
|
318
|
+
def __init__(
|
|
319
|
+
self, device: bumble.device.Device, filter_duplicates: bool, sync_timeout: float
|
|
320
|
+
) -> None:
|
|
321
|
+
self.scanner = BroadcastScanner(device, filter_duplicates, sync_timeout)
|
|
322
|
+
self.scanner.on('new_broadcast', self.on_new_broadcast)
|
|
323
|
+
self.scanner.on('broadcast_loss', self.on_broadcast_loss)
|
|
324
|
+
self.scanner.on('update', self.refresh)
|
|
325
|
+
self.status_message = ''
|
|
326
|
+
|
|
327
|
+
async def start(self) -> None:
|
|
328
|
+
self.status_message = color('Scanning...', 'green')
|
|
329
|
+
await self.scanner.start()
|
|
330
|
+
|
|
331
|
+
def on_new_broadcast(self, broadcast: BroadcastScanner.Broadcast) -> None:
|
|
346
332
|
self.status_message = color(
|
|
347
|
-
f'
|
|
333
|
+
f'+Found {len(self.scanner.broadcasts)} broadcasts', 'green'
|
|
348
334
|
)
|
|
335
|
+
broadcast.on('change', self.refresh)
|
|
336
|
+
broadcast.on('update', self.refresh)
|
|
349
337
|
self.refresh()
|
|
350
338
|
|
|
339
|
+
def on_broadcast_loss(self, broadcast: BroadcastScanner.Broadcast) -> None:
|
|
340
|
+
self.status_message = color(
|
|
341
|
+
f'-Found {len(self.scanner.broadcasts)} broadcasts', 'green'
|
|
342
|
+
)
|
|
343
|
+
self.refresh()
|
|
351
344
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
)
|
|
345
|
+
def refresh(self) -> None:
|
|
346
|
+
# Clear the screen from the top
|
|
347
|
+
print('\033[H')
|
|
348
|
+
print('\033[0J')
|
|
349
|
+
print('\033[H')
|
|
350
|
+
|
|
351
|
+
# Print the status message
|
|
352
|
+
print(self.status_message)
|
|
353
|
+
print("==========================================")
|
|
354
|
+
|
|
355
|
+
# Print all broadcasts
|
|
356
|
+
for broadcast in self.scanner.broadcasts.values():
|
|
357
|
+
broadcast.print()
|
|
358
|
+
print('------------------------------------------')
|
|
359
|
+
|
|
360
|
+
# Clear the screen to the bottom
|
|
361
|
+
print('\033[0J')
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@contextlib.asynccontextmanager
|
|
365
|
+
async def create_device(transport: str) -> AsyncGenerator[bumble.device.Device, Any]:
|
|
355
366
|
async with await bumble.transport.open_transport(transport) as (
|
|
356
367
|
hci_source,
|
|
357
368
|
hci_sink,
|
|
358
369
|
):
|
|
359
|
-
|
|
360
|
-
AURACAST_DEFAULT_DEVICE_NAME,
|
|
361
|
-
AURACAST_DEFAULT_DEVICE_ADDRESS,
|
|
370
|
+
device_config = bumble.device.DeviceConfiguration(
|
|
371
|
+
name=AURACAST_DEFAULT_DEVICE_NAME,
|
|
372
|
+
address=AURACAST_DEFAULT_DEVICE_ADDRESS,
|
|
373
|
+
keystore='JsonKeyStore',
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
device = bumble.device.Device.from_config_with_hci(
|
|
377
|
+
device_config,
|
|
362
378
|
hci_source,
|
|
363
379
|
hci_sink,
|
|
364
380
|
)
|
|
365
381
|
await device.power_on()
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
382
|
+
|
|
383
|
+
yield device
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
async def find_broadcast_by_name(
|
|
387
|
+
device: bumble.device.Device, name: Optional[str]
|
|
388
|
+
) -> BroadcastScanner.Broadcast:
|
|
389
|
+
result = asyncio.get_running_loop().create_future()
|
|
390
|
+
|
|
391
|
+
def on_broadcast_change(broadcast: BroadcastScanner.Broadcast) -> None:
|
|
392
|
+
if broadcast.basic_audio_announcement and not result.done():
|
|
393
|
+
print(color('Broadcast basic audio announcement received', 'green'))
|
|
394
|
+
result.set_result(broadcast)
|
|
395
|
+
|
|
396
|
+
def on_new_broadcast(broadcast: BroadcastScanner.Broadcast) -> None:
|
|
397
|
+
if name is None or broadcast.name == name:
|
|
398
|
+
print(color('Broadcast found:', 'green'), broadcast.name)
|
|
399
|
+
broadcast.on('change', lambda: on_broadcast_change(broadcast))
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
print(color(f'Skipping broadcast {broadcast.name}'))
|
|
403
|
+
|
|
404
|
+
scanner = BroadcastScanner(device, False, AURACAST_DEFAULT_SYNC_TIMEOUT)
|
|
405
|
+
scanner.on('new_broadcast', on_new_broadcast)
|
|
406
|
+
await scanner.start()
|
|
407
|
+
|
|
408
|
+
broadcast = await result
|
|
409
|
+
await scanner.stop()
|
|
410
|
+
|
|
411
|
+
return broadcast
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
async def run_scan(
|
|
415
|
+
filter_duplicates: bool, sync_timeout: float, transport: str
|
|
416
|
+
) -> None:
|
|
417
|
+
async with create_device(transport) as device:
|
|
418
|
+
if not device.supports_le_periodic_advertising:
|
|
419
|
+
print(color('Periodic advertising not supported', 'red'))
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
scanner = PrintingBroadcastScanner(device, filter_duplicates, sync_timeout)
|
|
423
|
+
await scanner.start()
|
|
424
|
+
await asyncio.get_running_loop().create_future()
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
async def run_assist(
|
|
428
|
+
broadcast_name: Optional[str],
|
|
429
|
+
source_id: Optional[int],
|
|
430
|
+
command: str,
|
|
431
|
+
transport: str,
|
|
432
|
+
address: str,
|
|
433
|
+
) -> None:
|
|
434
|
+
async with create_device(transport) as device:
|
|
435
|
+
if not device.supports_le_periodic_advertising:
|
|
436
|
+
print(color('Periodic advertising not supported', 'red'))
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
# Connect to the server
|
|
440
|
+
print(f'=== Connecting to {address}...')
|
|
441
|
+
connection = await device.connect(address)
|
|
442
|
+
peer = bumble.device.Peer(connection)
|
|
443
|
+
print(f'=== Connected to {peer}')
|
|
444
|
+
|
|
445
|
+
print("+++ Encrypting connection...")
|
|
446
|
+
await peer.connection.encrypt()
|
|
447
|
+
print("+++ Connection encrypted")
|
|
448
|
+
|
|
449
|
+
# Request a larger MTU
|
|
450
|
+
mtu = AURACAST_DEFAULT_ATT_MTU
|
|
451
|
+
print(color(f'$$$ Requesting MTU={mtu}', 'yellow'))
|
|
452
|
+
await peer.request_mtu(mtu)
|
|
453
|
+
|
|
454
|
+
# Get the BASS service
|
|
455
|
+
bass = await peer.discover_service_and_create_proxy(
|
|
456
|
+
bumble.profiles.bass.BroadcastAudioScanServiceProxy
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Check that the service was found
|
|
460
|
+
if not bass:
|
|
461
|
+
print(color('!!! Broadcast Audio Scan Service not found', 'red'))
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
# Subscribe to and read the broadcast receive state characteristics
|
|
465
|
+
for i, broadcast_receive_state in enumerate(bass.broadcast_receive_states):
|
|
466
|
+
try:
|
|
467
|
+
await broadcast_receive_state.subscribe(
|
|
468
|
+
lambda value, i=i: print(
|
|
469
|
+
f"{color(f'Broadcast Receive State Update [{i}]:', 'green')} {value}"
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
except bumble.core.ProtocolError as error:
|
|
473
|
+
print(
|
|
474
|
+
color(
|
|
475
|
+
f'!!! Failed to subscribe to Broadcast Receive State characteristic:',
|
|
476
|
+
'red',
|
|
477
|
+
),
|
|
478
|
+
error,
|
|
479
|
+
)
|
|
480
|
+
value = await broadcast_receive_state.read_value()
|
|
481
|
+
print(
|
|
482
|
+
f'{color(f"Initial Broadcast Receive State [{i}]:", "green")} {value}'
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if command == 'monitor-state':
|
|
486
|
+
await peer.sustain()
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
if command == 'add-source':
|
|
490
|
+
# Find the requested broadcast
|
|
491
|
+
await bass.remote_scan_started()
|
|
492
|
+
if broadcast_name:
|
|
493
|
+
print(color('Scanning for broadcast:', 'cyan'), broadcast_name)
|
|
494
|
+
else:
|
|
495
|
+
print(color('Scanning for any broadcast', 'cyan'))
|
|
496
|
+
broadcast = await find_broadcast_by_name(device, broadcast_name)
|
|
497
|
+
|
|
498
|
+
if broadcast.broadcast_audio_announcement is None:
|
|
499
|
+
print(color('No broadcast audio announcement found', 'red'))
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
if (
|
|
503
|
+
broadcast.basic_audio_announcement is None
|
|
504
|
+
or not broadcast.basic_audio_announcement.subgroups
|
|
505
|
+
):
|
|
506
|
+
print(color('No subgroups found', 'red'))
|
|
507
|
+
return
|
|
508
|
+
|
|
509
|
+
# Add the source
|
|
510
|
+
print(color('Adding source:', 'blue'), broadcast.sync.advertiser_address)
|
|
511
|
+
await bass.add_source(
|
|
512
|
+
broadcast.sync.advertiser_address,
|
|
513
|
+
broadcast.sync.sid,
|
|
514
|
+
broadcast.broadcast_audio_announcement.broadcast_id,
|
|
515
|
+
bumble.profiles.bass.PeriodicAdvertisingSyncParams.SYNCHRONIZE_TO_PA_PAST_AVAILABLE,
|
|
516
|
+
0xFFFF,
|
|
517
|
+
[
|
|
518
|
+
bumble.profiles.bass.SubgroupInfo(
|
|
519
|
+
bumble.profiles.bass.SubgroupInfo.ANY_BIS,
|
|
520
|
+
bytes(broadcast.basic_audio_announcement.subgroups[0].metadata),
|
|
521
|
+
)
|
|
522
|
+
],
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Initiate a PA Sync Transfer
|
|
526
|
+
await broadcast.sync.transfer(peer.connection)
|
|
527
|
+
|
|
528
|
+
# Notify the sink that we're done scanning.
|
|
529
|
+
await bass.remote_scan_stopped()
|
|
530
|
+
|
|
531
|
+
await peer.sustain()
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
if command == 'modify-source':
|
|
535
|
+
if source_id is None:
|
|
536
|
+
print(color('!!! modify-source requires --source-id'))
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
# Find the requested broadcast
|
|
540
|
+
await bass.remote_scan_started()
|
|
541
|
+
if broadcast_name:
|
|
542
|
+
print(color('Scanning for broadcast:', 'cyan'), broadcast_name)
|
|
543
|
+
else:
|
|
544
|
+
print(color('Scanning for any broadcast', 'cyan'))
|
|
545
|
+
broadcast = await find_broadcast_by_name(device, broadcast_name)
|
|
546
|
+
|
|
547
|
+
if broadcast.broadcast_audio_announcement is None:
|
|
548
|
+
print(color('No broadcast audio announcement found', 'red'))
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
if (
|
|
552
|
+
broadcast.basic_audio_announcement is None
|
|
553
|
+
or not broadcast.basic_audio_announcement.subgroups
|
|
554
|
+
):
|
|
555
|
+
print(color('No subgroups found', 'red'))
|
|
556
|
+
return
|
|
557
|
+
|
|
558
|
+
# Modify the source
|
|
559
|
+
print(
|
|
560
|
+
color('Modifying source:', 'blue'),
|
|
561
|
+
source_id,
|
|
562
|
+
)
|
|
563
|
+
await bass.modify_source(
|
|
564
|
+
source_id,
|
|
565
|
+
bumble.profiles.bass.PeriodicAdvertisingSyncParams.SYNCHRONIZE_TO_PA_PAST_NOT_AVAILABLE,
|
|
566
|
+
0xFFFF,
|
|
567
|
+
[
|
|
568
|
+
bumble.profiles.bass.SubgroupInfo(
|
|
569
|
+
bumble.profiles.bass.SubgroupInfo.ANY_BIS,
|
|
570
|
+
bytes(broadcast.basic_audio_announcement.subgroups[0].metadata),
|
|
571
|
+
)
|
|
572
|
+
],
|
|
573
|
+
)
|
|
574
|
+
await peer.sustain()
|
|
575
|
+
return
|
|
576
|
+
|
|
577
|
+
if command == 'remove-source':
|
|
578
|
+
if source_id is None:
|
|
579
|
+
print(color('!!! remove-source requires --source-id'))
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
# Remove the source
|
|
583
|
+
print(color('Removing source:', 'blue'), source_id)
|
|
584
|
+
await bass.remove_source(source_id)
|
|
585
|
+
await peer.sustain()
|
|
586
|
+
return
|
|
587
|
+
|
|
588
|
+
print(color(f'!!! invalid command {command}'))
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
async def run_pair(transport: str, address: str) -> None:
|
|
592
|
+
async with create_device(transport) as device:
|
|
593
|
+
|
|
594
|
+
# Connect to the server
|
|
595
|
+
print(f'=== Connecting to {address}...')
|
|
596
|
+
async with device.connect_as_gatt(address) as peer:
|
|
597
|
+
print(f'=== Connected to {peer}')
|
|
598
|
+
|
|
599
|
+
print("+++ Initiating pairing...")
|
|
600
|
+
await peer.connection.pair()
|
|
601
|
+
print("+++ Paired")
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def run_async(async_command: Coroutine) -> None:
|
|
605
|
+
try:
|
|
606
|
+
asyncio.run(async_command)
|
|
607
|
+
except bumble.core.ProtocolError as error:
|
|
608
|
+
if error.error_namespace == 'att' and error.error_code in list(
|
|
609
|
+
bumble.profiles.bass.ApplicationError
|
|
610
|
+
):
|
|
611
|
+
message = bumble.profiles.bass.ApplicationError(error.error_code).name
|
|
612
|
+
else:
|
|
613
|
+
message = str(error)
|
|
614
|
+
|
|
615
|
+
print(
|
|
616
|
+
color('!!! An error occurred while executing the command:', 'red'), message
|
|
617
|
+
)
|
|
369
618
|
|
|
370
619
|
|
|
371
620
|
# -----------------------------------------------------------------------------
|
|
@@ -379,7 +628,7 @@ def auracast(
|
|
|
379
628
|
ctx.ensure_object(dict)
|
|
380
629
|
|
|
381
630
|
|
|
382
|
-
@auracast.command('
|
|
631
|
+
@auracast.command('scan')
|
|
383
632
|
@click.option(
|
|
384
633
|
'--filter-duplicates', is_flag=True, default=False, help='Filter duplicates'
|
|
385
634
|
)
|
|
@@ -387,14 +636,50 @@ def auracast(
|
|
|
387
636
|
'--sync-timeout',
|
|
388
637
|
metavar='SYNC_TIMEOUT',
|
|
389
638
|
type=float,
|
|
390
|
-
default=
|
|
639
|
+
default=AURACAST_DEFAULT_SYNC_TIMEOUT,
|
|
391
640
|
help='Sync timeout (in seconds)',
|
|
392
641
|
)
|
|
393
642
|
@click.argument('transport')
|
|
394
643
|
@click.pass_context
|
|
395
|
-
def
|
|
396
|
-
"""
|
|
397
|
-
|
|
644
|
+
def scan(ctx, filter_duplicates, sync_timeout, transport):
|
|
645
|
+
"""Scan for public broadcasts"""
|
|
646
|
+
run_async(run_scan(filter_duplicates, sync_timeout, transport))
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@auracast.command('assist')
|
|
650
|
+
@click.option(
|
|
651
|
+
'--broadcast-name',
|
|
652
|
+
metavar='BROADCAST_NAME',
|
|
653
|
+
help='Broadcast Name to tune to',
|
|
654
|
+
)
|
|
655
|
+
@click.option(
|
|
656
|
+
'--source-id',
|
|
657
|
+
metavar='SOURCE_ID',
|
|
658
|
+
type=int,
|
|
659
|
+
help='Source ID (for remove-source command)',
|
|
660
|
+
)
|
|
661
|
+
@click.option(
|
|
662
|
+
'--command',
|
|
663
|
+
type=click.Choice(
|
|
664
|
+
['monitor-state', 'add-source', 'modify-source', 'remove-source']
|
|
665
|
+
),
|
|
666
|
+
required=True,
|
|
667
|
+
)
|
|
668
|
+
@click.argument('transport')
|
|
669
|
+
@click.argument('address')
|
|
670
|
+
@click.pass_context
|
|
671
|
+
def assist(ctx, broadcast_name, source_id, command, transport, address):
|
|
672
|
+
"""Scan for broadcasts on behalf of a audio server"""
|
|
673
|
+
run_async(run_assist(broadcast_name, source_id, command, transport, address))
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
@auracast.command('pair')
|
|
677
|
+
@click.argument('transport')
|
|
678
|
+
@click.argument('address')
|
|
679
|
+
@click.pass_context
|
|
680
|
+
def pair(ctx, transport, address):
|
|
681
|
+
"""Pair with an audio server"""
|
|
682
|
+
run_async(run_pair(transport, address))
|
|
398
683
|
|
|
399
684
|
|
|
400
685
|
def main():
|
bumble/apps/console.py
CHANGED
|
@@ -63,6 +63,7 @@ from bumble.transport import open_transport_or_link
|
|
|
63
63
|
from bumble.gatt import Characteristic, Service, CharacteristicDeclaration, Descriptor
|
|
64
64
|
from bumble.gatt_client import CharacteristicProxy
|
|
65
65
|
from bumble.hci import (
|
|
66
|
+
Address,
|
|
66
67
|
HCI_Constant,
|
|
67
68
|
HCI_LE_1M_PHY,
|
|
68
69
|
HCI_LE_2M_PHY,
|
|
@@ -289,11 +290,7 @@ class ConsoleApp:
|
|
|
289
290
|
device_config, hci_source, hci_sink
|
|
290
291
|
)
|
|
291
292
|
else:
|
|
292
|
-
random_address = (
|
|
293
|
-
f"{random.randint(192,255):02X}" # address is static random
|
|
294
|
-
)
|
|
295
|
-
for random_byte in random.sample(range(255), 5):
|
|
296
|
-
random_address += f":{random_byte:02X}"
|
|
293
|
+
random_address = Address.generate_static_address()
|
|
297
294
|
self.append_to_log(f"Setting random address: {random_address}")
|
|
298
295
|
self.device = Device.with_hci(
|
|
299
296
|
'Bumble', random_address, hci_source, hci_sink
|
|
@@ -503,21 +500,9 @@ class ConsoleApp:
|
|
|
503
500
|
self.show_error('not connected')
|
|
504
501
|
return
|
|
505
502
|
|
|
506
|
-
|
|
507
|
-
self.
|
|
508
|
-
|
|
509
|
-
self.append_to_output(
|
|
510
|
-
f'found {len(self.connected_peer.services)} services,'
|
|
511
|
-
' discovering characteristics...'
|
|
512
|
-
)
|
|
513
|
-
await self.connected_peer.discover_characteristics()
|
|
514
|
-
self.append_to_output('found characteristics, discovering descriptors...')
|
|
515
|
-
for service in self.connected_peer.services:
|
|
516
|
-
for characteristic in service.characteristics:
|
|
517
|
-
await self.connected_peer.discover_descriptors(characteristic)
|
|
518
|
-
self.append_to_output('discovery completed')
|
|
519
|
-
|
|
520
|
-
self.show_remote_services(self.connected_peer.services)
|
|
503
|
+
self.append_to_output('Service Discovery starting...')
|
|
504
|
+
await self.connected_peer.discover_all()
|
|
505
|
+
self.append_to_output('Service Discovery done!')
|
|
521
506
|
|
|
522
507
|
async def discover_attributes(self):
|
|
523
508
|
if not self.connected_peer:
|