bumble 0.0.211__py3-none-any.whl → 0.0.213__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/a2dp.py +6 -0
- bumble/apps/README.md +0 -3
- bumble/apps/auracast.py +11 -9
- bumble/apps/bench.py +482 -31
- bumble/apps/console.py +5 -5
- bumble/apps/controller_info.py +47 -10
- bumble/apps/controller_loopback.py +7 -3
- bumble/apps/controllers.py +2 -2
- bumble/apps/device_info.py +2 -2
- bumble/apps/gatt_dump.py +2 -2
- bumble/apps/gg_bridge.py +2 -2
- bumble/apps/hci_bridge.py +2 -2
- bumble/apps/l2cap_bridge.py +2 -2
- bumble/apps/lea_unicast/app.py +6 -1
- bumble/apps/pair.py +204 -43
- bumble/apps/pandora_server.py +2 -2
- bumble/apps/rfcomm_bridge.py +1 -1
- bumble/apps/scan.py +2 -2
- bumble/apps/show.py +4 -2
- bumble/apps/speaker/speaker.html +1 -0
- bumble/apps/speaker/speaker.js +113 -62
- bumble/apps/speaker/speaker.py +126 -18
- bumble/at.py +4 -4
- bumble/att.py +15 -18
- bumble/avc.py +7 -7
- bumble/avctp.py +5 -5
- bumble/avdtp.py +138 -88
- bumble/avrcp.py +52 -58
- bumble/colors.py +2 -2
- bumble/controller.py +84 -23
- bumble/core.py +13 -7
- bumble/{crypto.py → crypto/__init__.py} +11 -95
- bumble/crypto/builtin.py +652 -0
- bumble/crypto/cryptography.py +84 -0
- bumble/device.py +688 -345
- bumble/drivers/__init__.py +2 -2
- bumble/drivers/common.py +0 -2
- bumble/drivers/intel.py +40 -40
- bumble/drivers/rtk.py +28 -35
- bumble/gatt.py +7 -9
- bumble/gatt_adapters.py +4 -5
- bumble/gatt_client.py +31 -34
- bumble/gatt_server.py +15 -17
- bumble/hci.py +2635 -2878
- bumble/helpers.py +4 -5
- bumble/hfp.py +76 -57
- bumble/hid.py +24 -12
- bumble/host.py +117 -34
- bumble/keys.py +68 -52
- bumble/l2cap.py +329 -403
- bumble/link.py +6 -270
- bumble/pairing.py +23 -20
- bumble/pandora/__init__.py +1 -1
- bumble/pandora/config.py +2 -2
- bumble/pandora/device.py +6 -6
- bumble/pandora/host.py +38 -39
- bumble/pandora/l2cap.py +4 -4
- bumble/pandora/security.py +73 -57
- bumble/pandora/utils.py +3 -3
- bumble/profiles/aics.py +3 -5
- bumble/profiles/ancs.py +3 -1
- bumble/profiles/ascs.py +143 -136
- bumble/profiles/asha.py +13 -8
- bumble/profiles/bap.py +3 -4
- bumble/profiles/csip.py +3 -5
- bumble/profiles/device_information_service.py +2 -2
- bumble/profiles/gap.py +2 -2
- bumble/profiles/gatt_service.py +1 -3
- bumble/profiles/hap.py +42 -58
- bumble/profiles/le_audio.py +4 -4
- bumble/profiles/mcp.py +16 -13
- bumble/profiles/vcs.py +8 -10
- bumble/profiles/vocs.py +6 -9
- bumble/rfcomm.py +27 -18
- bumble/rtp.py +1 -2
- bumble/sdp.py +2 -2
- bumble/smp.py +71 -69
- bumble/tools/rtk_util.py +2 -2
- bumble/transport/__init__.py +2 -16
- bumble/transport/android_netsim.py +5 -5
- bumble/transport/common.py +4 -4
- bumble/transport/pyusb.py +2 -2
- bumble/utils.py +2 -5
- bumble/vendor/android/hci.py +118 -200
- bumble/vendor/zephyr/hci.py +32 -27
- {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/METADATA +5 -5
- {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/RECORD +92 -93
- {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/WHEEL +1 -1
- {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/entry_points.txt +0 -1
- bumble/apps/link_relay/__init__.py +0 -0
- bumble/apps/link_relay/link_relay.py +0 -289
- bumble/apps/link_relay/logging.yml +0 -21
- {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/top_level.txt +0 -0
bumble/profiles/gap.py
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
# -----------------------------------------------------------------------------
|
|
20
20
|
import logging
|
|
21
21
|
import struct
|
|
22
|
-
from typing import Optional,
|
|
22
|
+
from typing import Optional, Union
|
|
23
23
|
|
|
24
24
|
from bumble.core import Appearance
|
|
25
25
|
from bumble.gatt import (
|
|
@@ -54,7 +54,7 @@ class GenericAccessService(TemplateService):
|
|
|
54
54
|
appearance_characteristic: Characteristic[bytes]
|
|
55
55
|
|
|
56
56
|
def __init__(
|
|
57
|
-
self, device_name: str, appearance: Union[Appearance,
|
|
57
|
+
self, device_name: str, appearance: Union[Appearance, tuple[int, int], int] = 0
|
|
58
58
|
):
|
|
59
59
|
if isinstance(appearance, int):
|
|
60
60
|
appearance_int = appearance
|
bumble/profiles/gatt_service.py
CHANGED
|
@@ -127,9 +127,7 @@ class GenericAttributeProfileService(gatt.TemplateService):
|
|
|
127
127
|
|
|
128
128
|
return b''
|
|
129
129
|
|
|
130
|
-
def get_database_hash(self, connection: device.Connection
|
|
131
|
-
assert connection
|
|
132
|
-
|
|
130
|
+
def get_database_hash(self, connection: device.Connection) -> bytes:
|
|
133
131
|
m = b''.join(
|
|
134
132
|
[
|
|
135
133
|
self.get_attribute_data(attribute)
|
bumble/profiles/hap.py
CHANGED
|
@@ -20,7 +20,7 @@ import asyncio
|
|
|
20
20
|
import functools
|
|
21
21
|
from dataclasses import dataclass, field
|
|
22
22
|
import logging
|
|
23
|
-
from typing import Any,
|
|
23
|
+
from typing import Any, Optional, Union
|
|
24
24
|
|
|
25
25
|
from bumble import att, gatt, gatt_adapters, gatt_client
|
|
26
26
|
from bumble.core import InvalidArgumentError, InvalidStateError
|
|
@@ -228,23 +228,25 @@ class HearingAccessService(gatt.TemplateService):
|
|
|
228
228
|
hearing_aid_preset_control_point: gatt.Characteristic[bytes]
|
|
229
229
|
active_preset_index_characteristic: gatt.Characteristic[bytes]
|
|
230
230
|
active_preset_index: int
|
|
231
|
-
active_preset_index_per_device:
|
|
231
|
+
active_preset_index_per_device: dict[Address, int]
|
|
232
232
|
|
|
233
233
|
device: Device
|
|
234
234
|
|
|
235
235
|
server_features: HearingAidFeatures
|
|
236
|
-
preset_records:
|
|
236
|
+
preset_records: dict[int, PresetRecord] # key is the preset index
|
|
237
237
|
read_presets_request_in_progress: bool
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
other_server_in_binaural_set: Optional[HearingAccessService] = None
|
|
240
|
+
|
|
241
|
+
preset_changed_operations_history_per_device: dict[
|
|
242
|
+
Address, list[PresetChangedOperation]
|
|
241
243
|
]
|
|
242
244
|
|
|
243
245
|
# Keep an updated list of connected client to send notification to
|
|
244
|
-
currently_connected_clients:
|
|
246
|
+
currently_connected_clients: set[Connection]
|
|
245
247
|
|
|
246
248
|
def __init__(
|
|
247
|
-
self, device: Device, features: HearingAidFeatures, presets:
|
|
249
|
+
self, device: Device, features: HearingAidFeatures, presets: list[PresetRecord]
|
|
248
250
|
) -> None:
|
|
249
251
|
self.active_preset_index_per_device = {}
|
|
250
252
|
self.read_presets_request_in_progress = False
|
|
@@ -266,13 +268,13 @@ class HearingAccessService(gatt.TemplateService):
|
|
|
266
268
|
# associate the lowest index as the current active preset at startup
|
|
267
269
|
self.active_preset_index = sorted(self.preset_records.keys())[0]
|
|
268
270
|
|
|
269
|
-
@device.on(
|
|
271
|
+
@device.on(device.EVENT_CONNECTION)
|
|
270
272
|
def on_connection(connection: Connection) -> None:
|
|
271
|
-
@connection.on(
|
|
273
|
+
@connection.on(connection.EVENT_DISCONNECTION)
|
|
272
274
|
def on_disconnection(_reason) -> None:
|
|
273
275
|
self.currently_connected_clients.remove(connection)
|
|
274
276
|
|
|
275
|
-
@connection.on(
|
|
277
|
+
@connection.on(connection.EVENT_PAIRING)
|
|
276
278
|
def on_pairing(*_: Any) -> None:
|
|
277
279
|
self.on_incoming_paired_connection(connection)
|
|
278
280
|
|
|
@@ -333,11 +335,10 @@ class HearingAccessService(gatt.TemplateService):
|
|
|
333
335
|
# Update the active preset index if needed
|
|
334
336
|
await self.notify_active_preset_for_connection(connection)
|
|
335
337
|
|
|
336
|
-
|
|
338
|
+
connection.cancel_on_disconnection(on_connection_async())
|
|
337
339
|
|
|
338
|
-
def _on_read_active_preset_index(
|
|
339
|
-
|
|
340
|
-
) -> bytes:
|
|
340
|
+
def _on_read_active_preset_index(self, connection: Connection) -> bytes:
|
|
341
|
+
del connection # Unused
|
|
341
342
|
return bytes([self.active_preset_index])
|
|
342
343
|
|
|
343
344
|
# TODO this need to be triggered when device is unbonded
|
|
@@ -345,18 +346,13 @@ class HearingAccessService(gatt.TemplateService):
|
|
|
345
346
|
self.preset_changed_operations_history_per_device.pop(addr)
|
|
346
347
|
|
|
347
348
|
async def _on_write_hearing_aid_preset_control_point(
|
|
348
|
-
self, connection:
|
|
349
|
+
self, connection: Connection, value: bytes
|
|
349
350
|
):
|
|
350
|
-
assert connection
|
|
351
|
-
|
|
352
351
|
opcode = HearingAidPresetControlPointOpcode(value[0])
|
|
353
352
|
handler = getattr(self, '_on_' + opcode.name.lower())
|
|
354
353
|
await handler(connection, value)
|
|
355
354
|
|
|
356
|
-
async def _on_read_presets_request(
|
|
357
|
-
self, connection: Optional[Connection], value: bytes
|
|
358
|
-
):
|
|
359
|
-
assert connection
|
|
355
|
+
async def _on_read_presets_request(self, connection: Connection, value: bytes):
|
|
360
356
|
if connection.att_mtu < 49: # 2.5. GATT sub-procedure requirements
|
|
361
357
|
logging.warning(f'HAS require MTU >= 49: {connection}')
|
|
362
358
|
|
|
@@ -385,7 +381,7 @@ class HearingAccessService(gatt.TemplateService):
|
|
|
385
381
|
utils.AsyncRunner.spawn(self._read_preset_response(connection, presets))
|
|
386
382
|
|
|
387
383
|
async def _read_preset_response(
|
|
388
|
-
self, connection: Connection, presets:
|
|
384
|
+
self, connection: Connection, presets: list[PresetRecord]
|
|
389
385
|
):
|
|
390
386
|
# If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Read Presets Request operation aborted and shall not either continue or restart the operation when the client reconnects.
|
|
391
387
|
try:
|
|
@@ -471,10 +467,7 @@ class HearingAccessService(gatt.TemplateService):
|
|
|
471
467
|
for connection in self.currently_connected_clients:
|
|
472
468
|
await self._preset_changed_operation(connection)
|
|
473
469
|
|
|
474
|
-
async def _on_write_preset_name(
|
|
475
|
-
self, connection: Optional[Connection], value: bytes
|
|
476
|
-
):
|
|
477
|
-
assert connection
|
|
470
|
+
async def _on_write_preset_name(self, connection: Connection, value: bytes):
|
|
478
471
|
|
|
479
472
|
if self.read_presets_request_in_progress:
|
|
480
473
|
raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS)
|
|
@@ -522,10 +515,7 @@ class HearingAccessService(gatt.TemplateService):
|
|
|
522
515
|
for connection in self.currently_connected_clients:
|
|
523
516
|
await self.notify_active_preset_for_connection(connection)
|
|
524
517
|
|
|
525
|
-
async def set_active_preset(
|
|
526
|
-
self, connection: Optional[Connection], value: bytes
|
|
527
|
-
) -> None:
|
|
528
|
-
assert connection
|
|
518
|
+
async def set_active_preset(self, value: bytes) -> None:
|
|
529
519
|
index = value[1]
|
|
530
520
|
preset = self.preset_records.get(index, None)
|
|
531
521
|
if (
|
|
@@ -542,16 +532,11 @@ class HearingAccessService(gatt.TemplateService):
|
|
|
542
532
|
self.active_preset_index = index
|
|
543
533
|
await self.notify_active_preset()
|
|
544
534
|
|
|
545
|
-
async def _on_set_active_preset(
|
|
546
|
-
self
|
|
547
|
-
):
|
|
548
|
-
await self.set_active_preset(connection, value)
|
|
535
|
+
async def _on_set_active_preset(self, _: Connection, value: bytes):
|
|
536
|
+
await self.set_active_preset(value)
|
|
549
537
|
|
|
550
|
-
async def set_next_or_previous_preset(
|
|
551
|
-
self, connection: Optional[Connection], is_previous
|
|
552
|
-
):
|
|
538
|
+
async def set_next_or_previous_preset(self, is_previous):
|
|
553
539
|
'''Set the next or the previous preset as active'''
|
|
554
|
-
assert connection
|
|
555
540
|
|
|
556
541
|
if self.active_preset_index == 0x00:
|
|
557
542
|
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
|
|
@@ -580,48 +565,47 @@ class HearingAccessService(gatt.TemplateService):
|
|
|
580
565
|
self.active_preset_index = first_preset.index
|
|
581
566
|
await self.notify_active_preset()
|
|
582
567
|
|
|
583
|
-
async def _on_set_next_preset(
|
|
584
|
-
self
|
|
585
|
-
) -> None:
|
|
586
|
-
await self.set_next_or_previous_preset(connection, False)
|
|
568
|
+
async def _on_set_next_preset(self, _: Connection, __value__: bytes) -> None:
|
|
569
|
+
await self.set_next_or_previous_preset(False)
|
|
587
570
|
|
|
588
|
-
async def _on_set_previous_preset(
|
|
589
|
-
self
|
|
590
|
-
) -> None:
|
|
591
|
-
await self.set_next_or_previous_preset(connection, True)
|
|
571
|
+
async def _on_set_previous_preset(self, _: Connection, __value__: bytes) -> None:
|
|
572
|
+
await self.set_next_or_previous_preset(True)
|
|
592
573
|
|
|
593
574
|
async def _on_set_active_preset_synchronized_locally(
|
|
594
|
-
self,
|
|
575
|
+
self, _: Connection, value: bytes
|
|
595
576
|
):
|
|
596
577
|
if (
|
|
597
578
|
self.server_features.preset_synchronization_support
|
|
598
|
-
== PresetSynchronizationSupport.
|
|
579
|
+
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
|
599
580
|
):
|
|
600
581
|
raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
|
|
601
|
-
await self.set_active_preset(
|
|
602
|
-
|
|
582
|
+
await self.set_active_preset(value)
|
|
583
|
+
if self.other_server_in_binaural_set:
|
|
584
|
+
await self.other_server_in_binaural_set.set_active_preset(value)
|
|
603
585
|
|
|
604
586
|
async def _on_set_next_preset_synchronized_locally(
|
|
605
|
-
self,
|
|
587
|
+
self, _: Connection, __value__: bytes
|
|
606
588
|
):
|
|
607
589
|
if (
|
|
608
590
|
self.server_features.preset_synchronization_support
|
|
609
|
-
== PresetSynchronizationSupport.
|
|
591
|
+
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
|
610
592
|
):
|
|
611
593
|
raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
|
|
612
|
-
await self.set_next_or_previous_preset(
|
|
613
|
-
|
|
594
|
+
await self.set_next_or_previous_preset(False)
|
|
595
|
+
if self.other_server_in_binaural_set:
|
|
596
|
+
await self.other_server_in_binaural_set.set_next_or_previous_preset(False)
|
|
614
597
|
|
|
615
598
|
async def _on_set_previous_preset_synchronized_locally(
|
|
616
|
-
self,
|
|
599
|
+
self, _: Connection, __value__: bytes
|
|
617
600
|
):
|
|
618
601
|
if (
|
|
619
602
|
self.server_features.preset_synchronization_support
|
|
620
|
-
== PresetSynchronizationSupport.
|
|
603
|
+
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
|
621
604
|
):
|
|
622
605
|
raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
|
|
623
|
-
await self.set_next_or_previous_preset(
|
|
624
|
-
|
|
606
|
+
await self.set_next_or_previous_preset(True)
|
|
607
|
+
if self.other_server_in_binaural_set:
|
|
608
|
+
await self.other_server_in_binaural_set.set_next_or_previous_preset(True)
|
|
625
609
|
|
|
626
610
|
|
|
627
611
|
# -----------------------------------------------------------------------------
|
bumble/profiles/le_audio.py
CHANGED
|
@@ -19,7 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
import dataclasses
|
|
20
20
|
import enum
|
|
21
21
|
import struct
|
|
22
|
-
from typing import Any
|
|
22
|
+
from typing import Any
|
|
23
23
|
from typing_extensions import Self
|
|
24
24
|
|
|
25
25
|
from bumble.profiles import bap
|
|
@@ -108,13 +108,13 @@ class Metadata:
|
|
|
108
108
|
return self.data
|
|
109
109
|
|
|
110
110
|
@classmethod
|
|
111
|
-
def from_bytes(cls:
|
|
111
|
+
def from_bytes(cls: type[Self], data: bytes) -> Self:
|
|
112
112
|
return cls(tag=Metadata.Tag(data[0]), data=data[1:])
|
|
113
113
|
|
|
114
114
|
def __bytes__(self) -> bytes:
|
|
115
115
|
return bytes([len(self.data) + 1, self.tag]) + self.data
|
|
116
116
|
|
|
117
|
-
entries:
|
|
117
|
+
entries: list[Entry] = dataclasses.field(default_factory=list)
|
|
118
118
|
|
|
119
119
|
def pretty_print(self, indent: str) -> str:
|
|
120
120
|
"""Convenience method to generate a string with one key-value pair per line."""
|
|
@@ -140,7 +140,7 @@ class Metadata:
|
|
|
140
140
|
)
|
|
141
141
|
|
|
142
142
|
@classmethod
|
|
143
|
-
def from_bytes(cls:
|
|
143
|
+
def from_bytes(cls: type[Self], data: bytes) -> Self:
|
|
144
144
|
entries = []
|
|
145
145
|
offset = 0
|
|
146
146
|
length = len(data)
|
bumble/profiles/mcp.py
CHANGED
|
@@ -29,7 +29,7 @@ from bumble import gatt
|
|
|
29
29
|
from bumble import gatt_client
|
|
30
30
|
from bumble import utils
|
|
31
31
|
|
|
32
|
-
from typing import
|
|
32
|
+
from typing import Optional, ClassVar, TYPE_CHECKING
|
|
33
33
|
from typing_extensions import Self
|
|
34
34
|
|
|
35
35
|
# -----------------------------------------------------------------------------
|
|
@@ -167,7 +167,7 @@ class ObjectId(int):
|
|
|
167
167
|
'''See Media Control Service 4.4.2. Object ID field.'''
|
|
168
168
|
|
|
169
169
|
@classmethod
|
|
170
|
-
def create_from_bytes(cls:
|
|
170
|
+
def create_from_bytes(cls: type[Self], data: bytes) -> Self:
|
|
171
171
|
return cls(int.from_bytes(data, byteorder='little', signed=False))
|
|
172
172
|
|
|
173
173
|
def __bytes__(self) -> bytes:
|
|
@@ -182,7 +182,7 @@ class GroupObjectType:
|
|
|
182
182
|
object_id: ObjectId
|
|
183
183
|
|
|
184
184
|
@classmethod
|
|
185
|
-
def from_bytes(cls:
|
|
185
|
+
def from_bytes(cls: type[Self], data: bytes) -> Self:
|
|
186
186
|
return cls(
|
|
187
187
|
object_type=ObjectType(data[0]),
|
|
188
188
|
object_id=ObjectId.create_from_bytes(data[1:]),
|
|
@@ -287,11 +287,8 @@ class MediaControlService(gatt.TemplateService):
|
|
|
287
287
|
)
|
|
288
288
|
|
|
289
289
|
async def on_media_control_point(
|
|
290
|
-
self, connection:
|
|
290
|
+
self, connection: device.Connection, data: bytes
|
|
291
291
|
) -> None:
|
|
292
|
-
if not connection:
|
|
293
|
-
raise core.InvalidStateError()
|
|
294
|
-
|
|
295
292
|
opcode = MediaControlPointOpcode(data[0])
|
|
296
293
|
|
|
297
294
|
await connection.device.notify_subscriber(
|
|
@@ -313,7 +310,7 @@ class MediaControlServiceProxy(
|
|
|
313
310
|
):
|
|
314
311
|
SERVICE_CLASS = MediaControlService
|
|
315
312
|
|
|
316
|
-
_CHARACTERISTICS: ClassVar[
|
|
313
|
+
_CHARACTERISTICS: ClassVar[dict[str, core.UUID]] = {
|
|
317
314
|
'media_player_name': gatt.GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC,
|
|
318
315
|
'media_player_icon_object_id': gatt.GATT_MEDIA_PLAYER_ICON_OBJECT_ID_CHARACTERISTIC,
|
|
319
316
|
'media_player_icon_url': gatt.GATT_MEDIA_PLAYER_ICON_URL_CHARACTERISTIC,
|
|
@@ -338,6 +335,12 @@ class MediaControlServiceProxy(
|
|
|
338
335
|
'content_control_id': gatt.GATT_CONTENT_CONTROL_ID_CHARACTERISTIC,
|
|
339
336
|
}
|
|
340
337
|
|
|
338
|
+
EVENT_MEDIA_STATE = "media_state"
|
|
339
|
+
EVENT_TRACK_CHANGED = "track_changed"
|
|
340
|
+
EVENT_TRACK_TITLE = "track_title"
|
|
341
|
+
EVENT_TRACK_DURATION = "track_duration"
|
|
342
|
+
EVENT_TRACK_POSITION = "track_position"
|
|
343
|
+
|
|
341
344
|
media_player_name: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
|
342
345
|
media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
|
343
346
|
media_player_icon_url: Optional[gatt_client.CharacteristicProxy[bytes]] = None
|
|
@@ -432,20 +435,20 @@ class MediaControlServiceProxy(
|
|
|
432
435
|
self.media_control_point_notifications.put_nowait(data)
|
|
433
436
|
|
|
434
437
|
def _on_media_state(self, data: bytes) -> None:
|
|
435
|
-
self.emit(
|
|
438
|
+
self.emit(self.EVENT_MEDIA_STATE, MediaState(data[0]))
|
|
436
439
|
|
|
437
440
|
def _on_track_changed(self, data: bytes) -> None:
|
|
438
441
|
del data
|
|
439
|
-
self.emit(
|
|
442
|
+
self.emit(self.EVENT_TRACK_CHANGED)
|
|
440
443
|
|
|
441
444
|
def _on_track_title(self, data: bytes) -> None:
|
|
442
|
-
self.emit(
|
|
445
|
+
self.emit(self.EVENT_TRACK_TITLE, data.decode("utf-8"))
|
|
443
446
|
|
|
444
447
|
def _on_track_duration(self, data: bytes) -> None:
|
|
445
|
-
self.emit(
|
|
448
|
+
self.emit(self.EVENT_TRACK_DURATION, struct.unpack_from('<i', data)[0])
|
|
446
449
|
|
|
447
450
|
def _on_track_position(self, data: bytes) -> None:
|
|
448
|
-
self.emit(
|
|
451
|
+
self.emit(self.EVENT_TRACK_POSITION, struct.unpack_from('<i', data)[0])
|
|
449
452
|
|
|
450
453
|
|
|
451
454
|
class GenericMediaControlServiceProxy(MediaControlServiceProxy):
|
bumble/profiles/vcs.py
CHANGED
|
@@ -20,7 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
import dataclasses
|
|
21
21
|
import enum
|
|
22
22
|
|
|
23
|
-
from typing import
|
|
23
|
+
from typing import Sequence
|
|
24
24
|
|
|
25
25
|
from bumble import att
|
|
26
26
|
from bumble import utils
|
|
@@ -91,6 +91,8 @@ class VolumeState:
|
|
|
91
91
|
class VolumeControlService(gatt.TemplateService):
|
|
92
92
|
UUID = gatt.GATT_VOLUME_CONTROL_SERVICE
|
|
93
93
|
|
|
94
|
+
EVENT_VOLUME_STATE_CHANGE = "volume_state_change"
|
|
95
|
+
|
|
94
96
|
volume_state: gatt.Characteristic[bytes]
|
|
95
97
|
volume_control_point: gatt.Characteristic[bytes]
|
|
96
98
|
volume_flags: gatt.Characteristic[bytes]
|
|
@@ -144,14 +146,12 @@ class VolumeControlService(gatt.TemplateService):
|
|
|
144
146
|
included_services=list(included_services),
|
|
145
147
|
)
|
|
146
148
|
|
|
147
|
-
def _on_read_volume_state(self, _connection:
|
|
149
|
+
def _on_read_volume_state(self, _connection: device.Connection) -> bytes:
|
|
148
150
|
return bytes(VolumeState(self.volume_setting, self.muted, self.change_counter))
|
|
149
151
|
|
|
150
152
|
def _on_write_volume_control_point(
|
|
151
|
-
self, connection:
|
|
153
|
+
self, connection: device.Connection, value: bytes
|
|
152
154
|
) -> None:
|
|
153
|
-
assert connection
|
|
154
|
-
|
|
155
155
|
opcode = VolumeControlPointOpcode(value[0])
|
|
156
156
|
change_counter = value[1]
|
|
157
157
|
|
|
@@ -161,12 +161,10 @@ class VolumeControlService(gatt.TemplateService):
|
|
|
161
161
|
handler = getattr(self, '_on_' + opcode.name.lower())
|
|
162
162
|
if handler(*value[2:]):
|
|
163
163
|
self.change_counter = (self.change_counter + 1) % 256
|
|
164
|
-
|
|
165
|
-
connection
|
|
166
|
-
'disconnection',
|
|
167
|
-
connection.device.notify_subscribers(attribute=self.volume_state),
|
|
164
|
+
connection.cancel_on_disconnection(
|
|
165
|
+
connection.device.notify_subscribers(attribute=self.volume_state)
|
|
168
166
|
)
|
|
169
|
-
self.emit(
|
|
167
|
+
self.emit(self.EVENT_VOLUME_STATE_CHANGE)
|
|
170
168
|
|
|
171
169
|
def _on_relative_volume_down(self) -> bool:
|
|
172
170
|
old_volume = self.volume_setting
|
bumble/profiles/vocs.py
CHANGED
|
@@ -86,7 +86,7 @@ class VolumeOffsetState:
|
|
|
86
86
|
assert self.attribute is not None
|
|
87
87
|
await connection.device.notify_subscribers(attribute=self.attribute)
|
|
88
88
|
|
|
89
|
-
def on_read(self, _connection:
|
|
89
|
+
def on_read(self, _connection: Connection) -> bytes:
|
|
90
90
|
return bytes(self)
|
|
91
91
|
|
|
92
92
|
|
|
@@ -103,11 +103,10 @@ class VocsAudioLocation:
|
|
|
103
103
|
audio_location = AudioLocation(struct.unpack('<I', data)[0])
|
|
104
104
|
return cls(audio_location)
|
|
105
105
|
|
|
106
|
-
def on_read(self, _connection:
|
|
106
|
+
def on_read(self, _connection: Connection) -> bytes:
|
|
107
107
|
return bytes(self)
|
|
108
108
|
|
|
109
|
-
async def on_write(self, connection:
|
|
110
|
-
assert connection
|
|
109
|
+
async def on_write(self, connection: Connection, value: bytes) -> None:
|
|
111
110
|
assert self.attribute
|
|
112
111
|
|
|
113
112
|
self.audio_location = AudioLocation(int.from_bytes(value, 'little'))
|
|
@@ -118,8 +117,7 @@ class VocsAudioLocation:
|
|
|
118
117
|
class VolumeOffsetControlPoint:
|
|
119
118
|
volume_offset_state: VolumeOffsetState
|
|
120
119
|
|
|
121
|
-
async def on_write(self, connection:
|
|
122
|
-
assert connection
|
|
120
|
+
async def on_write(self, connection: Connection, value: bytes) -> None:
|
|
123
121
|
|
|
124
122
|
opcode = value[0]
|
|
125
123
|
if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET:
|
|
@@ -159,11 +157,10 @@ class AudioOutputDescription:
|
|
|
159
157
|
def __bytes__(self) -> bytes:
|
|
160
158
|
return self.audio_output_description.encode('utf-8')
|
|
161
159
|
|
|
162
|
-
def on_read(self, _connection:
|
|
160
|
+
def on_read(self, _connection: Connection) -> bytes:
|
|
163
161
|
return bytes(self)
|
|
164
162
|
|
|
165
|
-
async def on_write(self, connection:
|
|
166
|
-
assert connection
|
|
163
|
+
async def on_write(self, connection: Connection, value: bytes) -> None:
|
|
167
164
|
assert self.attribute
|
|
168
165
|
|
|
169
166
|
self.audio_output_description = value.decode('utf-8')
|
bumble/rfcomm.py
CHANGED
|
@@ -22,7 +22,7 @@ import asyncio
|
|
|
22
22
|
import collections
|
|
23
23
|
import dataclasses
|
|
24
24
|
import enum
|
|
25
|
-
from typing import Callable,
|
|
25
|
+
from typing import Callable, Optional, Union, TYPE_CHECKING
|
|
26
26
|
from typing_extensions import Self
|
|
27
27
|
|
|
28
28
|
|
|
@@ -123,7 +123,7 @@ RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
|
|
|
123
123
|
# -----------------------------------------------------------------------------
|
|
124
124
|
def make_service_sdp_records(
|
|
125
125
|
service_record_handle: int, channel: int, uuid: Optional[UUID] = None
|
|
126
|
-
) ->
|
|
126
|
+
) -> list[sdp.ServiceAttribute]:
|
|
127
127
|
"""
|
|
128
128
|
Create SDP records for an RFComm service given a channel number and an
|
|
129
129
|
optional UUID. A Service Class Attribute is included only if the UUID is not None.
|
|
@@ -169,7 +169,7 @@ def make_service_sdp_records(
|
|
|
169
169
|
|
|
170
170
|
|
|
171
171
|
# -----------------------------------------------------------------------------
|
|
172
|
-
async def find_rfcomm_channels(connection: Connection) ->
|
|
172
|
+
async def find_rfcomm_channels(connection: Connection) -> dict[int, list[UUID]]:
|
|
173
173
|
"""Searches all RFCOMM channels and their associated UUID from SDP service records.
|
|
174
174
|
|
|
175
175
|
Args:
|
|
@@ -188,7 +188,7 @@ async def find_rfcomm_channels(connection: Connection) -> Dict[int, List[UUID]]:
|
|
|
188
188
|
],
|
|
189
189
|
)
|
|
190
190
|
for attribute_lists in search_result:
|
|
191
|
-
service_classes:
|
|
191
|
+
service_classes: list[UUID] = []
|
|
192
192
|
channel: Optional[int] = None
|
|
193
193
|
for attribute in attribute_lists:
|
|
194
194
|
# The layout is [[L2CAP_PROTOCOL], [RFCOMM_PROTOCOL, RFCOMM_CHANNEL]].
|
|
@@ -275,7 +275,7 @@ class RFCOMM_Frame:
|
|
|
275
275
|
self.fcs = compute_fcs(bytes([self.address, self.control]) + self.length)
|
|
276
276
|
|
|
277
277
|
@staticmethod
|
|
278
|
-
def parse_mcc(data) ->
|
|
278
|
+
def parse_mcc(data) -> tuple[int, bool, bytes]:
|
|
279
279
|
mcc_type = data[0] >> 2
|
|
280
280
|
c_r = bool((data[0] >> 1) & 1)
|
|
281
281
|
length = data[1]
|
|
@@ -442,6 +442,9 @@ class RFCOMM_MCC_MSC:
|
|
|
442
442
|
|
|
443
443
|
# -----------------------------------------------------------------------------
|
|
444
444
|
class DLC(utils.EventEmitter):
|
|
445
|
+
EVENT_OPEN = "open"
|
|
446
|
+
EVENT_CLOSE = "close"
|
|
447
|
+
|
|
445
448
|
class State(enum.IntEnum):
|
|
446
449
|
INIT = 0x00
|
|
447
450
|
CONNECTING = 0x01
|
|
@@ -529,7 +532,7 @@ class DLC(utils.EventEmitter):
|
|
|
529
532
|
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
|
530
533
|
|
|
531
534
|
self.change_state(DLC.State.CONNECTED)
|
|
532
|
-
self.emit(
|
|
535
|
+
self.emit(self.EVENT_OPEN)
|
|
533
536
|
|
|
534
537
|
def on_ua_frame(self, _frame: RFCOMM_Frame) -> None:
|
|
535
538
|
if self.state == DLC.State.CONNECTING:
|
|
@@ -550,7 +553,7 @@ class DLC(utils.EventEmitter):
|
|
|
550
553
|
self.disconnection_result.set_result(None)
|
|
551
554
|
self.disconnection_result = None
|
|
552
555
|
self.multiplexer.on_dlc_disconnection(self)
|
|
553
|
-
self.emit(
|
|
556
|
+
self.emit(self.EVENT_CLOSE)
|
|
554
557
|
else:
|
|
555
558
|
logger.warning(
|
|
556
559
|
color(
|
|
@@ -733,7 +736,7 @@ class DLC(utils.EventEmitter):
|
|
|
733
736
|
self.disconnection_result.cancel()
|
|
734
737
|
self.disconnection_result = None
|
|
735
738
|
self.change_state(DLC.State.RESET)
|
|
736
|
-
self.emit(
|
|
739
|
+
self.emit(self.EVENT_CLOSE)
|
|
737
740
|
|
|
738
741
|
def __str__(self) -> str:
|
|
739
742
|
return (
|
|
@@ -763,11 +766,13 @@ class Multiplexer(utils.EventEmitter):
|
|
|
763
766
|
DISCONNECTED = 0x05
|
|
764
767
|
RESET = 0x06
|
|
765
768
|
|
|
769
|
+
EVENT_DLC = "dlc"
|
|
770
|
+
|
|
766
771
|
connection_result: Optional[asyncio.Future]
|
|
767
772
|
disconnection_result: Optional[asyncio.Future]
|
|
768
773
|
open_result: Optional[asyncio.Future]
|
|
769
|
-
acceptor: Optional[Callable[[int], Optional[
|
|
770
|
-
dlcs:
|
|
774
|
+
acceptor: Optional[Callable[[int], Optional[tuple[int, int]]]]
|
|
775
|
+
dlcs: dict[int, DLC]
|
|
771
776
|
|
|
772
777
|
def __init__(self, l2cap_channel: l2cap.ClassicChannel, role: Role) -> None:
|
|
773
778
|
super().__init__()
|
|
@@ -785,7 +790,7 @@ class Multiplexer(utils.EventEmitter):
|
|
|
785
790
|
# Become a sink for the L2CAP channel
|
|
786
791
|
l2cap_channel.sink = self.on_pdu
|
|
787
792
|
|
|
788
|
-
l2cap_channel.on(
|
|
793
|
+
l2cap_channel.on(l2cap_channel.EVENT_CLOSE, self.on_l2cap_channel_close)
|
|
789
794
|
|
|
790
795
|
def change_state(self, new_state: State) -> None:
|
|
791
796
|
logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
|
|
@@ -901,7 +906,7 @@ class Multiplexer(utils.EventEmitter):
|
|
|
901
906
|
self.dlcs[pn.dlci] = dlc
|
|
902
907
|
|
|
903
908
|
# Re-emit the handshake completion event
|
|
904
|
-
dlc.on(
|
|
909
|
+
dlc.on(dlc.EVENT_OPEN, lambda: self.emit(self.EVENT_DLC, dlc))
|
|
905
910
|
|
|
906
911
|
# Respond to complete the handshake
|
|
907
912
|
dlc.accept()
|
|
@@ -1076,13 +1081,15 @@ class Client:
|
|
|
1076
1081
|
|
|
1077
1082
|
# -----------------------------------------------------------------------------
|
|
1078
1083
|
class Server(utils.EventEmitter):
|
|
1084
|
+
EVENT_START = "start"
|
|
1085
|
+
|
|
1079
1086
|
def __init__(
|
|
1080
1087
|
self, device: Device, l2cap_mtu: int = RFCOMM_DEFAULT_L2CAP_MTU
|
|
1081
1088
|
) -> None:
|
|
1082
1089
|
super().__init__()
|
|
1083
1090
|
self.device = device
|
|
1084
|
-
self.acceptors:
|
|
1085
|
-
self.dlc_configs:
|
|
1091
|
+
self.acceptors: dict[int, Callable[[DLC], None]] = {}
|
|
1092
|
+
self.dlc_configs: dict[int, tuple[int, int]] = {}
|
|
1086
1093
|
|
|
1087
1094
|
# Register ourselves with the L2CAP channel manager
|
|
1088
1095
|
self.l2cap_server = device.create_l2cap_server(
|
|
@@ -1122,7 +1129,9 @@ class Server(utils.EventEmitter):
|
|
|
1122
1129
|
|
|
1123
1130
|
def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
|
1124
1131
|
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
|
1125
|
-
l2cap_channel.on(
|
|
1132
|
+
l2cap_channel.on(
|
|
1133
|
+
l2cap_channel.EVENT_OPEN, lambda: self.on_l2cap_channel_open(l2cap_channel)
|
|
1134
|
+
)
|
|
1126
1135
|
|
|
1127
1136
|
def on_l2cap_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
|
1128
1137
|
logger.debug(f'$$$ L2CAP channel open: {l2cap_channel}')
|
|
@@ -1130,12 +1139,12 @@ class Server(utils.EventEmitter):
|
|
|
1130
1139
|
# Create a new multiplexer for the channel
|
|
1131
1140
|
multiplexer = Multiplexer(l2cap_channel, Multiplexer.Role.RESPONDER)
|
|
1132
1141
|
multiplexer.acceptor = self.accept_dlc
|
|
1133
|
-
multiplexer.on(
|
|
1142
|
+
multiplexer.on(multiplexer.EVENT_DLC, self.on_dlc)
|
|
1134
1143
|
|
|
1135
1144
|
# Notify
|
|
1136
|
-
self.emit(
|
|
1145
|
+
self.emit(self.EVENT_START, multiplexer)
|
|
1137
1146
|
|
|
1138
|
-
def accept_dlc(self, channel_number: int) -> Optional[
|
|
1147
|
+
def accept_dlc(self, channel_number: int) -> Optional[tuple[int, int]]:
|
|
1139
1148
|
return self.dlc_configs.get(channel_number)
|
|
1140
1149
|
|
|
1141
1150
|
def on_dlc(self, dlc: DLC) -> None:
|
bumble/rtp.py
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
import struct
|
|
20
|
-
from typing import List
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
# -----------------------------------------------------------------------------
|
|
@@ -60,7 +59,7 @@ class MediaPacket:
|
|
|
60
59
|
sequence_number: int,
|
|
61
60
|
timestamp: int,
|
|
62
61
|
ssrc: int,
|
|
63
|
-
csrc_list:
|
|
62
|
+
csrc_list: list[int],
|
|
64
63
|
payload_type: int,
|
|
65
64
|
payload: bytes,
|
|
66
65
|
) -> None:
|
bumble/sdp.py
CHANGED
|
@@ -19,7 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
import asyncio
|
|
20
20
|
import logging
|
|
21
21
|
import struct
|
|
22
|
-
from typing import Iterable, NewType, Optional, Union, Sequence,
|
|
22
|
+
from typing import Iterable, NewType, Optional, Union, Sequence, TYPE_CHECKING
|
|
23
23
|
from typing_extensions import Self
|
|
24
24
|
|
|
25
25
|
from bumble import core, l2cap
|
|
@@ -547,7 +547,7 @@ class SDP_PDU:
|
|
|
547
547
|
SDP_SERVICE_ATTRIBUTE_REQUEST: SDP_SERVICE_ATTRIBUTE_RESPONSE,
|
|
548
548
|
SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST: SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE,
|
|
549
549
|
}
|
|
550
|
-
sdp_pdu_classes: dict[int,
|
|
550
|
+
sdp_pdu_classes: dict[int, type[SDP_PDU]] = {}
|
|
551
551
|
name = None
|
|
552
552
|
pdu_id = 0
|
|
553
553
|
|