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/ascs.py
CHANGED
|
@@ -18,10 +18,13 @@
|
|
|
18
18
|
# -----------------------------------------------------------------------------
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
+
from dataclasses import dataclass, field
|
|
21
22
|
import enum
|
|
23
|
+
import functools
|
|
22
24
|
import logging
|
|
23
25
|
import struct
|
|
24
|
-
from typing import Any,
|
|
26
|
+
from typing import Any, Optional, Union, TypeVar
|
|
27
|
+
from collections.abc import Sequence
|
|
25
28
|
|
|
26
29
|
from bumble import utils
|
|
27
30
|
from bumble import colors
|
|
@@ -48,11 +51,11 @@ class ASE_Operation:
|
|
|
48
51
|
See Audio Stream Control Service - 5 ASE Control operations.
|
|
49
52
|
'''
|
|
50
53
|
|
|
51
|
-
classes:
|
|
52
|
-
op_code:
|
|
54
|
+
classes: dict[int, type[ASE_Operation]] = {}
|
|
55
|
+
op_code: Opcode
|
|
53
56
|
name: str
|
|
54
57
|
fields: Optional[Sequence[Any]] = None
|
|
55
|
-
ase_id:
|
|
58
|
+
ase_id: Sequence[int]
|
|
56
59
|
|
|
57
60
|
class Opcode(enum.IntEnum):
|
|
58
61
|
# fmt: off
|
|
@@ -65,51 +68,30 @@ class ASE_Operation:
|
|
|
65
68
|
UPDATE_METADATA = 0x07
|
|
66
69
|
RELEASE = 0x08
|
|
67
70
|
|
|
68
|
-
@
|
|
69
|
-
def from_bytes(pdu: bytes) -> ASE_Operation:
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_bytes(cls, pdu: bytes) -> ASE_Operation:
|
|
70
73
|
op_code = pdu[0]
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def inner(cls: Type[ASE_Operation]):
|
|
87
|
-
try:
|
|
88
|
-
operation = ASE_Operation.Opcode[cls.__name__[4:].upper()]
|
|
89
|
-
cls.name = operation.name
|
|
90
|
-
cls.op_code = operation
|
|
91
|
-
except:
|
|
92
|
-
raise KeyError(f'PDU name {cls.name} not found in Ase_Operation.Opcode')
|
|
93
|
-
cls.fields = fields
|
|
94
|
-
|
|
95
|
-
# Register a factory for this class
|
|
96
|
-
ASE_Operation.classes[cls.op_code] = cls
|
|
97
|
-
|
|
98
|
-
return cls
|
|
99
|
-
|
|
100
|
-
return inner
|
|
101
|
-
|
|
102
|
-
def __init__(self, pdu: Optional[bytes] = None, **kwargs) -> None:
|
|
103
|
-
if self.fields is not None and kwargs:
|
|
104
|
-
hci.HCI_Object.init_from_fields(self, self.fields, kwargs)
|
|
105
|
-
if pdu is None:
|
|
106
|
-
pdu = bytes([self.op_code]) + hci.HCI_Object.dict_to_bytes(
|
|
107
|
-
kwargs, self.fields
|
|
108
|
-
)
|
|
109
|
-
self.pdu = pdu
|
|
75
|
+
clazz = ASE_Operation.classes[op_code]
|
|
76
|
+
return clazz(
|
|
77
|
+
**hci.HCI_Object.dict_from_bytes(pdu, offset=1, fields=clazz.fields)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
_OP = TypeVar("_OP", bound="ASE_Operation")
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def subclass(cls, clazz: type[_OP]) -> type[_OP]:
|
|
84
|
+
clazz.name = f"ASE_{clazz.op_code.name.upper()}"
|
|
85
|
+
clazz.fields = hci.HCI_Object.fields_from_dataclass(clazz)
|
|
86
|
+
# Register a factory for this class
|
|
87
|
+
ASE_Operation.classes[clazz.op_code] = clazz
|
|
88
|
+
return clazz
|
|
110
89
|
|
|
111
|
-
|
|
112
|
-
|
|
90
|
+
@functools.cached_property
|
|
91
|
+
def pdu(self) -> bytes:
|
|
92
|
+
return bytes([self.op_code]) + hci.HCI_Object.dict_to_bytes(
|
|
93
|
+
self.__dict__, self.fields
|
|
94
|
+
)
|
|
113
95
|
|
|
114
96
|
def __bytes__(self) -> bytes:
|
|
115
97
|
return self.pdu
|
|
@@ -124,105 +106,128 @@ class ASE_Operation:
|
|
|
124
106
|
return result
|
|
125
107
|
|
|
126
108
|
|
|
127
|
-
@ASE_Operation.subclass
|
|
128
|
-
|
|
129
|
-
[
|
|
130
|
-
('ase_id', 1),
|
|
131
|
-
('target_latency', 1),
|
|
132
|
-
('target_phy', 1),
|
|
133
|
-
('codec_id', hci.CodingFormat.parse_from_bytes),
|
|
134
|
-
('codec_specific_configuration', 'v'),
|
|
135
|
-
],
|
|
136
|
-
]
|
|
137
|
-
)
|
|
109
|
+
@ASE_Operation.subclass
|
|
110
|
+
@dataclass
|
|
138
111
|
class ASE_Config_Codec(ASE_Operation):
|
|
139
112
|
'''
|
|
140
113
|
See Audio Stream Control Service 5.1 - Config Codec Operation
|
|
141
114
|
'''
|
|
142
115
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
('max_sdu', 2),
|
|
159
|
-
('retransmission_number', 1),
|
|
160
|
-
('max_transport_latency', 2),
|
|
161
|
-
('presentation_delay', 3),
|
|
162
|
-
],
|
|
163
|
-
]
|
|
164
|
-
)
|
|
116
|
+
op_code = ASE_Operation.Opcode.CONFIG_CODEC
|
|
117
|
+
|
|
118
|
+
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
|
|
119
|
+
target_latency: Sequence[int] = field(metadata=hci.metadata(1))
|
|
120
|
+
target_phy: Sequence[int] = field(metadata=hci.metadata(1))
|
|
121
|
+
codec_id: Sequence[hci.CodingFormat] = field(
|
|
122
|
+
metadata=hci.metadata(hci.CodingFormat.parse_from_bytes)
|
|
123
|
+
)
|
|
124
|
+
codec_specific_configuration: Sequence[bytes] = field(
|
|
125
|
+
metadata=hci.metadata('v', list_end=True)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@ASE_Operation.subclass
|
|
130
|
+
@dataclass
|
|
165
131
|
class ASE_Config_QOS(ASE_Operation):
|
|
166
132
|
'''
|
|
167
133
|
See Audio Stream Control Service 5.2 - Config Qos Operation
|
|
168
134
|
'''
|
|
169
135
|
|
|
170
|
-
|
|
171
|
-
cis_id: List[int]
|
|
172
|
-
sdu_interval: List[int]
|
|
173
|
-
framing: List[int]
|
|
174
|
-
phy: List[int]
|
|
175
|
-
max_sdu: List[int]
|
|
176
|
-
retransmission_number: List[int]
|
|
177
|
-
max_transport_latency: List[int]
|
|
178
|
-
presentation_delay: List[int]
|
|
136
|
+
op_code = ASE_Operation.Opcode.CONFIG_QOS
|
|
179
137
|
|
|
138
|
+
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
|
|
139
|
+
cig_id: Sequence[int] = field(metadata=hci.metadata(1))
|
|
140
|
+
cis_id: Sequence[int] = field(metadata=hci.metadata(1))
|
|
141
|
+
sdu_interval: Sequence[int] = field(metadata=hci.metadata(3))
|
|
142
|
+
framing: Sequence[int] = field(metadata=hci.metadata(1))
|
|
143
|
+
phy: Sequence[int] = field(metadata=hci.metadata(1))
|
|
144
|
+
max_sdu: Sequence[int] = field(metadata=hci.metadata(2))
|
|
145
|
+
retransmission_number: Sequence[int] = field(metadata=hci.metadata(1))
|
|
146
|
+
max_transport_latency: Sequence[int] = field(metadata=hci.metadata(2))
|
|
147
|
+
presentation_delay: Sequence[int] = field(metadata=hci.metadata(3, list_end=True))
|
|
180
148
|
|
|
181
|
-
|
|
149
|
+
|
|
150
|
+
@ASE_Operation.subclass
|
|
151
|
+
@dataclass
|
|
182
152
|
class ASE_Enable(ASE_Operation):
|
|
183
153
|
'''
|
|
184
154
|
See Audio Stream Control Service 5.3 - Enable Operation
|
|
185
155
|
'''
|
|
186
156
|
|
|
187
|
-
|
|
157
|
+
op_code = ASE_Operation.Opcode.ENABLE
|
|
158
|
+
|
|
159
|
+
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
|
|
160
|
+
metadata: Sequence[bytes] = field(metadata=hci.metadata('v', list_end=True))
|
|
188
161
|
|
|
189
162
|
|
|
190
|
-
@ASE_Operation.subclass
|
|
163
|
+
@ASE_Operation.subclass
|
|
164
|
+
@dataclass
|
|
191
165
|
class ASE_Receiver_Start_Ready(ASE_Operation):
|
|
192
166
|
'''
|
|
193
167
|
See Audio Stream Control Service 5.4 - Receiver Start Ready Operation
|
|
194
168
|
'''
|
|
195
169
|
|
|
170
|
+
op_code = ASE_Operation.Opcode.RECEIVER_START_READY
|
|
171
|
+
|
|
172
|
+
ase_id: Sequence[int] = field(
|
|
173
|
+
metadata=hci.metadata(1, list_begin=True, list_end=True)
|
|
174
|
+
)
|
|
175
|
+
|
|
196
176
|
|
|
197
|
-
@ASE_Operation.subclass
|
|
177
|
+
@ASE_Operation.subclass
|
|
178
|
+
@dataclass
|
|
198
179
|
class ASE_Disable(ASE_Operation):
|
|
199
180
|
'''
|
|
200
181
|
See Audio Stream Control Service 5.5 - Disable Operation
|
|
201
182
|
'''
|
|
202
183
|
|
|
184
|
+
op_code = ASE_Operation.Opcode.DISABLE
|
|
203
185
|
|
|
204
|
-
|
|
186
|
+
ase_id: Sequence[int] = field(
|
|
187
|
+
metadata=hci.metadata(1, list_begin=True, list_end=True)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@ASE_Operation.subclass
|
|
192
|
+
@dataclass
|
|
205
193
|
class ASE_Receiver_Stop_Ready(ASE_Operation):
|
|
206
194
|
'''
|
|
207
195
|
See Audio Stream Control Service 5.6 - Receiver Stop Ready Operation
|
|
208
196
|
'''
|
|
209
197
|
|
|
198
|
+
op_code = ASE_Operation.Opcode.RECEIVER_STOP_READY
|
|
199
|
+
|
|
200
|
+
ase_id: Sequence[int] = field(
|
|
201
|
+
metadata=hci.metadata(1, list_begin=True, list_end=True)
|
|
202
|
+
)
|
|
203
|
+
|
|
210
204
|
|
|
211
|
-
@ASE_Operation.subclass
|
|
205
|
+
@ASE_Operation.subclass
|
|
206
|
+
@dataclass
|
|
212
207
|
class ASE_Update_Metadata(ASE_Operation):
|
|
213
208
|
'''
|
|
214
209
|
See Audio Stream Control Service 5.7 - Update Metadata Operation
|
|
215
210
|
'''
|
|
216
211
|
|
|
217
|
-
|
|
212
|
+
op_code = ASE_Operation.Opcode.UPDATE_METADATA
|
|
218
213
|
|
|
214
|
+
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
|
|
215
|
+
metadata: Sequence[bytes] = field(metadata=hci.metadata('v', list_end=True))
|
|
219
216
|
|
|
220
|
-
|
|
217
|
+
|
|
218
|
+
@ASE_Operation.subclass
|
|
219
|
+
@dataclass
|
|
221
220
|
class ASE_Release(ASE_Operation):
|
|
222
221
|
'''
|
|
223
222
|
See Audio Stream Control Service 5.8 - Release Operation
|
|
224
223
|
'''
|
|
225
224
|
|
|
225
|
+
op_code = ASE_Operation.Opcode.RELEASE
|
|
226
|
+
|
|
227
|
+
ase_id: Sequence[int] = field(
|
|
228
|
+
metadata=hci.metadata(1, list_begin=True, list_end=True)
|
|
229
|
+
)
|
|
230
|
+
|
|
226
231
|
|
|
227
232
|
class AseResponseCode(enum.IntEnum):
|
|
228
233
|
# fmt: off
|
|
@@ -276,6 +281,8 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
276
281
|
DISABLING = 0x05
|
|
277
282
|
RELEASING = 0x06
|
|
278
283
|
|
|
284
|
+
EVENT_STATE_CHANGE = "state_change"
|
|
285
|
+
|
|
279
286
|
cis_link: Optional[device.CisLink] = None
|
|
280
287
|
|
|
281
288
|
# Additional parameters in CODEC_CONFIGURED State
|
|
@@ -329,25 +336,23 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
329
336
|
value=gatt.CharacteristicValue(read=self.on_read),
|
|
330
337
|
)
|
|
331
338
|
|
|
332
|
-
self.service.device.on(
|
|
333
|
-
|
|
339
|
+
self.service.device.on(
|
|
340
|
+
self.service.device.EVENT_CIS_REQUEST, self.on_cis_request
|
|
341
|
+
)
|
|
342
|
+
self.service.device.on(
|
|
343
|
+
self.service.device.EVENT_CIS_ESTABLISHMENT, self.on_cis_establishment
|
|
344
|
+
)
|
|
334
345
|
|
|
335
|
-
def on_cis_request(
|
|
336
|
-
self,
|
|
337
|
-
acl_connection: device.Connection,
|
|
338
|
-
cis_handle: int,
|
|
339
|
-
cig_id: int,
|
|
340
|
-
cis_id: int,
|
|
341
|
-
) -> None:
|
|
346
|
+
def on_cis_request(self, cis_link: device.CisLink) -> None:
|
|
342
347
|
if (
|
|
343
|
-
cig_id == self.cig_id
|
|
344
|
-
and cis_id == self.cis_id
|
|
348
|
+
cis_link.cig_id == self.cig_id
|
|
349
|
+
and cis_link.cis_id == self.cis_id
|
|
345
350
|
and self.state == self.State.ENABLING
|
|
346
351
|
):
|
|
347
352
|
utils.cancel_on_event(
|
|
348
|
-
acl_connection,
|
|
353
|
+
cis_link.acl_connection,
|
|
349
354
|
'flush',
|
|
350
|
-
self.service.device.accept_cis_request(
|
|
355
|
+
self.service.device.accept_cis_request(cis_link),
|
|
351
356
|
)
|
|
352
357
|
|
|
353
358
|
def on_cis_establishment(self, cis_link: device.CisLink) -> None:
|
|
@@ -356,7 +361,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
356
361
|
and cis_link.cis_id == self.cis_id
|
|
357
362
|
and self.state == self.State.ENABLING
|
|
358
363
|
):
|
|
359
|
-
cis_link.on(
|
|
364
|
+
cis_link.on(cis_link.EVENT_DISCONNECTION, self.on_cis_disconnection)
|
|
360
365
|
|
|
361
366
|
async def post_cis_established():
|
|
362
367
|
await cis_link.setup_data_path(direction=self.role)
|
|
@@ -378,7 +383,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
378
383
|
target_phy: int,
|
|
379
384
|
codec_id: hci.CodingFormat,
|
|
380
385
|
codec_specific_configuration: bytes,
|
|
381
|
-
) ->
|
|
386
|
+
) -> tuple[AseResponseCode, AseReasonCode]:
|
|
382
387
|
if self.state not in (
|
|
383
388
|
self.State.IDLE,
|
|
384
389
|
self.State.CODEC_CONFIGURED,
|
|
@@ -414,7 +419,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
414
419
|
retransmission_number: int,
|
|
415
420
|
max_transport_latency: int,
|
|
416
421
|
presentation_delay: int,
|
|
417
|
-
) ->
|
|
422
|
+
) -> tuple[AseResponseCode, AseReasonCode]:
|
|
418
423
|
if self.state not in (
|
|
419
424
|
AseStateMachine.State.CODEC_CONFIGURED,
|
|
420
425
|
AseStateMachine.State.QOS_CONFIGURED,
|
|
@@ -438,7 +443,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
438
443
|
|
|
439
444
|
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
440
445
|
|
|
441
|
-
def on_enable(self, metadata: bytes) ->
|
|
446
|
+
def on_enable(self, metadata: bytes) -> tuple[AseResponseCode, AseReasonCode]:
|
|
442
447
|
if self.state != AseStateMachine.State.QOS_CONFIGURED:
|
|
443
448
|
return (
|
|
444
449
|
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
|
@@ -450,7 +455,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
450
455
|
|
|
451
456
|
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
452
457
|
|
|
453
|
-
def on_receiver_start_ready(self) ->
|
|
458
|
+
def on_receiver_start_ready(self) -> tuple[AseResponseCode, AseReasonCode]:
|
|
454
459
|
if self.state != AseStateMachine.State.ENABLING:
|
|
455
460
|
return (
|
|
456
461
|
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
|
@@ -459,7 +464,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
459
464
|
self.state = self.State.STREAMING
|
|
460
465
|
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
461
466
|
|
|
462
|
-
def on_disable(self) ->
|
|
467
|
+
def on_disable(self) -> tuple[AseResponseCode, AseReasonCode]:
|
|
463
468
|
if self.state not in (
|
|
464
469
|
AseStateMachine.State.ENABLING,
|
|
465
470
|
AseStateMachine.State.STREAMING,
|
|
@@ -474,7 +479,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
474
479
|
self.state = self.State.DISABLING
|
|
475
480
|
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
476
481
|
|
|
477
|
-
def on_receiver_stop_ready(self) ->
|
|
482
|
+
def on_receiver_stop_ready(self) -> tuple[AseResponseCode, AseReasonCode]:
|
|
478
483
|
if (
|
|
479
484
|
self.role != AudioRole.SOURCE
|
|
480
485
|
or self.state != AseStateMachine.State.DISABLING
|
|
@@ -488,7 +493,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
488
493
|
|
|
489
494
|
def on_update_metadata(
|
|
490
495
|
self, metadata: bytes
|
|
491
|
-
) ->
|
|
496
|
+
) -> tuple[AseResponseCode, AseReasonCode]:
|
|
492
497
|
if self.state not in (
|
|
493
498
|
AseStateMachine.State.ENABLING,
|
|
494
499
|
AseStateMachine.State.STREAMING,
|
|
@@ -500,7 +505,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
500
505
|
self.metadata = le_audio.Metadata.from_bytes(metadata)
|
|
501
506
|
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
502
507
|
|
|
503
|
-
def on_release(self) ->
|
|
508
|
+
def on_release(self) -> tuple[AseResponseCode, AseReasonCode]:
|
|
504
509
|
if self.state == AseStateMachine.State.IDLE:
|
|
505
510
|
return (
|
|
506
511
|
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
|
@@ -510,7 +515,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
510
515
|
|
|
511
516
|
async def remove_cis_async():
|
|
512
517
|
if self.cis_link:
|
|
513
|
-
await self.cis_link.remove_data_path(self.role)
|
|
518
|
+
await self.cis_link.remove_data_path([self.role])
|
|
514
519
|
self.state = self.State.IDLE
|
|
515
520
|
await self.service.device.notify_subscribers(self, self.value)
|
|
516
521
|
|
|
@@ -525,7 +530,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
525
530
|
def state(self, new_state: State) -> None:
|
|
526
531
|
logger.debug(f'{self} state change -> {colors.color(new_state.name, "cyan")}')
|
|
527
532
|
self._state = new_state
|
|
528
|
-
self.emit(
|
|
533
|
+
self.emit(self.EVENT_STATE_CHANGE)
|
|
529
534
|
|
|
530
535
|
@property
|
|
531
536
|
def value(self):
|
|
@@ -584,7 +589,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
584
589
|
# Readonly. Do nothing in the setter.
|
|
585
590
|
pass
|
|
586
591
|
|
|
587
|
-
def on_read(self, _:
|
|
592
|
+
def on_read(self, _: device.Connection) -> bytes:
|
|
588
593
|
return self.value
|
|
589
594
|
|
|
590
595
|
def __str__(self) -> str:
|
|
@@ -598,7 +603,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
598
603
|
class AudioStreamControlService(gatt.TemplateService):
|
|
599
604
|
UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE
|
|
600
605
|
|
|
601
|
-
ase_state_machines:
|
|
606
|
+
ase_state_machines: dict[int, AseStateMachine]
|
|
602
607
|
ase_control_point: gatt.Characteristic[bytes]
|
|
603
608
|
_active_client: Optional[device.Connection] = None
|
|
604
609
|
|
|
@@ -643,7 +648,9 @@ class AudioStreamControlService(gatt.TemplateService):
|
|
|
643
648
|
ase.state = AseStateMachine.State.IDLE
|
|
644
649
|
self._active_client = None
|
|
645
650
|
|
|
646
|
-
def on_write_ase_control_point(
|
|
651
|
+
def on_write_ase_control_point(
|
|
652
|
+
self, connection: device.Connection, data: bytes
|
|
653
|
+
) -> None:
|
|
647
654
|
if not self._active_client and connection:
|
|
648
655
|
self._active_client = connection
|
|
649
656
|
connection.once('disconnection', self._on_client_disconnected)
|
|
@@ -652,7 +659,7 @@ class AudioStreamControlService(gatt.TemplateService):
|
|
|
652
659
|
responses = []
|
|
653
660
|
logger.debug(f'*** ASCS Write {operation} ***')
|
|
654
661
|
|
|
655
|
-
if operation
|
|
662
|
+
if isinstance(operation, ASE_Config_Codec):
|
|
656
663
|
for ase_id, *args in zip(
|
|
657
664
|
operation.ase_id,
|
|
658
665
|
operation.target_latency,
|
|
@@ -661,7 +668,7 @@ class AudioStreamControlService(gatt.TemplateService):
|
|
|
661
668
|
operation.codec_specific_configuration,
|
|
662
669
|
):
|
|
663
670
|
responses.append(self.on_operation(operation.op_code, ase_id, args))
|
|
664
|
-
elif operation
|
|
671
|
+
elif isinstance(operation, ASE_Config_QOS):
|
|
665
672
|
for ase_id, *args in zip(
|
|
666
673
|
operation.ase_id,
|
|
667
674
|
operation.cig_id,
|
|
@@ -675,20 +682,20 @@ class AudioStreamControlService(gatt.TemplateService):
|
|
|
675
682
|
operation.presentation_delay,
|
|
676
683
|
):
|
|
677
684
|
responses.append(self.on_operation(operation.op_code, ase_id, args))
|
|
678
|
-
elif operation
|
|
679
|
-
ASE_Operation.Opcode.ENABLE,
|
|
680
|
-
ASE_Operation.Opcode.UPDATE_METADATA,
|
|
681
|
-
):
|
|
685
|
+
elif isinstance(operation, (ASE_Enable, ASE_Update_Metadata)):
|
|
682
686
|
for ase_id, *args in zip(
|
|
683
687
|
operation.ase_id,
|
|
684
688
|
operation.metadata,
|
|
685
689
|
):
|
|
686
690
|
responses.append(self.on_operation(operation.op_code, ase_id, args))
|
|
687
|
-
elif
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
691
|
+
elif isinstance(
|
|
692
|
+
operation,
|
|
693
|
+
(
|
|
694
|
+
ASE_Receiver_Start_Ready,
|
|
695
|
+
ASE_Disable,
|
|
696
|
+
ASE_Receiver_Stop_Ready,
|
|
697
|
+
ASE_Release,
|
|
698
|
+
),
|
|
692
699
|
):
|
|
693
700
|
for ase_id in operation.ase_id:
|
|
694
701
|
responses.append(self.on_operation(operation.op_code, ase_id, []))
|
|
@@ -717,8 +724,8 @@ class AudioStreamControlService(gatt.TemplateService):
|
|
|
717
724
|
class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy):
|
|
718
725
|
SERVICE_CLASS = AudioStreamControlService
|
|
719
726
|
|
|
720
|
-
sink_ase:
|
|
721
|
-
source_ase:
|
|
727
|
+
sink_ase: list[gatt_client.CharacteristicProxy[bytes]]
|
|
728
|
+
source_ase: list[gatt_client.CharacteristicProxy[bytes]]
|
|
722
729
|
ase_control_point: gatt_client.CharacteristicProxy[bytes]
|
|
723
730
|
|
|
724
731
|
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
bumble/profiles/asha.py
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import enum
|
|
20
20
|
import struct
|
|
21
21
|
import logging
|
|
22
|
-
from typing import
|
|
22
|
+
from typing import Optional, Callable, Union, Any
|
|
23
23
|
|
|
24
24
|
from bumble import l2cap
|
|
25
25
|
from bumble import utils
|
|
@@ -88,6 +88,11 @@ class AudioStatus(utils.OpenIntEnum):
|
|
|
88
88
|
class AshaService(gatt.TemplateService):
|
|
89
89
|
UUID = gatt.GATT_ASHA_SERVICE
|
|
90
90
|
|
|
91
|
+
EVENT_STARTED = "started"
|
|
92
|
+
EVENT_STOPPED = "stopped"
|
|
93
|
+
EVENT_DISCONNECTED = "disconnected"
|
|
94
|
+
EVENT_VOLUME_CHANGED = "volume_changed"
|
|
95
|
+
|
|
91
96
|
audio_sink: Optional[Callable[[bytes], Any]]
|
|
92
97
|
active_codec: Optional[Codec] = None
|
|
93
98
|
audio_type: Optional[AudioType] = None
|
|
@@ -98,7 +103,7 @@ class AshaService(gatt.TemplateService):
|
|
|
98
103
|
def __init__(
|
|
99
104
|
self,
|
|
100
105
|
capability: int,
|
|
101
|
-
hisyncid: Union[
|
|
106
|
+
hisyncid: Union[list[int], bytes],
|
|
102
107
|
device: Device,
|
|
103
108
|
psm: int = 0,
|
|
104
109
|
audio_sink: Optional[Callable[[bytes], Any]] = None,
|
|
@@ -195,7 +200,7 @@ class AshaService(gatt.TemplateService):
|
|
|
195
200
|
|
|
196
201
|
# Handler for audio control commands
|
|
197
202
|
async def _on_audio_control_point_write(
|
|
198
|
-
self, connection:
|
|
203
|
+
self, connection: Connection, value: bytes
|
|
199
204
|
) -> None:
|
|
200
205
|
_logger.debug(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
|
|
201
206
|
opcode = value[0]
|
|
@@ -211,14 +216,14 @@ class AshaService(gatt.TemplateService):
|
|
|
211
216
|
f'volume={self.volume}, '
|
|
212
217
|
f'other_state={self.other_state}'
|
|
213
218
|
)
|
|
214
|
-
self.emit(
|
|
219
|
+
self.emit(self.EVENT_STARTED)
|
|
215
220
|
elif opcode == OpCode.STOP:
|
|
216
221
|
_logger.debug('### STOP')
|
|
217
222
|
self.active_codec = None
|
|
218
223
|
self.audio_type = None
|
|
219
224
|
self.volume = None
|
|
220
225
|
self.other_state = None
|
|
221
|
-
self.emit(
|
|
226
|
+
self.emit(self.EVENT_STOPPED)
|
|
222
227
|
elif opcode == OpCode.STATUS:
|
|
223
228
|
_logger.debug('### STATUS: %s', PeripheralStatus(value[1]).name)
|
|
224
229
|
|
|
@@ -231,7 +236,7 @@ class AshaService(gatt.TemplateService):
|
|
|
231
236
|
self.audio_type = None
|
|
232
237
|
self.volume = None
|
|
233
238
|
self.other_state = None
|
|
234
|
-
self.emit(
|
|
239
|
+
self.emit(self.EVENT_DISCONNECTED)
|
|
235
240
|
|
|
236
241
|
connection.once('disconnection', on_disconnection)
|
|
237
242
|
|
|
@@ -242,10 +247,10 @@ class AshaService(gatt.TemplateService):
|
|
|
242
247
|
)
|
|
243
248
|
|
|
244
249
|
# Handler for volume control
|
|
245
|
-
def _on_volume_write(self, connection:
|
|
250
|
+
def _on_volume_write(self, connection: Connection, value: bytes) -> None:
|
|
246
251
|
_logger.debug(f'--- VOLUME Write:{value[0]}')
|
|
247
252
|
self.volume = value[0]
|
|
248
|
-
self.emit(
|
|
253
|
+
self.emit(self.EVENT_VOLUME_CHANGED)
|
|
249
254
|
|
|
250
255
|
# Register an L2CAP CoC server
|
|
251
256
|
def _on_connection(self, channel: l2cap.LeCreditBasedChannel) -> None:
|
bumble/profiles/bap.py
CHANGED
|
@@ -24,7 +24,6 @@ import enum
|
|
|
24
24
|
import struct
|
|
25
25
|
import functools
|
|
26
26
|
import logging
|
|
27
|
-
from typing import List
|
|
28
27
|
from typing_extensions import Self
|
|
29
28
|
|
|
30
29
|
from bumble import core
|
|
@@ -282,7 +281,7 @@ class UnicastServerAdvertisingData:
|
|
|
282
281
|
# -----------------------------------------------------------------------------
|
|
283
282
|
|
|
284
283
|
|
|
285
|
-
def bits_to_channel_counts(data: int) ->
|
|
284
|
+
def bits_to_channel_counts(data: int) -> list[int]:
|
|
286
285
|
pos = 0
|
|
287
286
|
counts = []
|
|
288
287
|
while data != 0:
|
|
@@ -527,7 +526,7 @@ class BasicAudioAnnouncement:
|
|
|
527
526
|
codec_id: hci.CodingFormat
|
|
528
527
|
codec_specific_configuration: CodecSpecificConfiguration
|
|
529
528
|
metadata: le_audio.Metadata
|
|
530
|
-
bis:
|
|
529
|
+
bis: list[BasicAudioAnnouncement.BIS]
|
|
531
530
|
|
|
532
531
|
def __bytes__(self) -> bytes:
|
|
533
532
|
metadata_bytes = bytes(self.metadata)
|
|
@@ -545,7 +544,7 @@ class BasicAudioAnnouncement:
|
|
|
545
544
|
)
|
|
546
545
|
|
|
547
546
|
presentation_delay: int
|
|
548
|
-
subgroups:
|
|
547
|
+
subgroups: list[BasicAudioAnnouncement.Subgroup]
|
|
549
548
|
|
|
550
549
|
@classmethod
|
|
551
550
|
def from_bytes(cls, data: bytes) -> Self:
|
bumble/profiles/csip.py
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
import enum
|
|
21
21
|
import struct
|
|
22
|
-
from typing import Optional
|
|
22
|
+
from typing import Optional
|
|
23
23
|
|
|
24
24
|
from bumble import core
|
|
25
25
|
from bumble import crypto
|
|
@@ -164,12 +164,10 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
|
|
|
164
164
|
|
|
165
165
|
super().__init__(characteristics)
|
|
166
166
|
|
|
167
|
-
async def on_sirk_read(self, connection:
|
|
167
|
+
async def on_sirk_read(self, connection: device.Connection) -> bytes:
|
|
168
168
|
if self.set_identity_resolving_key_type == SirkType.PLAINTEXT:
|
|
169
169
|
sirk_bytes = self.set_identity_resolving_key
|
|
170
170
|
else:
|
|
171
|
-
assert connection
|
|
172
|
-
|
|
173
171
|
if connection.transport == core.PhysicalTransport.LE:
|
|
174
172
|
key = await connection.device.get_long_term_key(
|
|
175
173
|
connection_handle=connection.handle, rand=b'', ediv=0
|
|
@@ -230,7 +228,7 @@ class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
|
|
|
230
228
|
):
|
|
231
229
|
self.set_member_rank = characteristics[0]
|
|
232
230
|
|
|
233
|
-
async def read_set_identity_resolving_key(self) ->
|
|
231
|
+
async def read_set_identity_resolving_key(self) -> tuple[SirkType, bytes]:
|
|
234
232
|
'''Reads SIRK and decrypts if encrypted.'''
|
|
235
233
|
response = await self.set_identity_resolving_key.read_value()
|
|
236
234
|
if len(response) != SET_IDENTITY_RESOLVING_KEY_LENGTH + 1:
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# Imports
|
|
18
18
|
# -----------------------------------------------------------------------------
|
|
19
19
|
import struct
|
|
20
|
-
from typing import Optional
|
|
20
|
+
from typing import Optional
|
|
21
21
|
|
|
22
22
|
from bumble.gatt import (
|
|
23
23
|
GATT_DEVICE_INFORMATION_SERVICE,
|
|
@@ -60,7 +60,7 @@ class DeviceInformationService(TemplateService):
|
|
|
60
60
|
hardware_revision: Optional[str] = None,
|
|
61
61
|
firmware_revision: Optional[str] = None,
|
|
62
62
|
software_revision: Optional[str] = None,
|
|
63
|
-
system_id: Optional[
|
|
63
|
+
system_id: Optional[tuple[int, int]] = None, # (OUI, Manufacturer ID)
|
|
64
64
|
ieee_regulatory_certification_data_list: Optional[bytes] = None,
|
|
65
65
|
# TODO: pnp_id
|
|
66
66
|
):
|