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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Copyright 2021-2022 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# -----------------------------------------------------------------------------
|
|
16
|
+
# Imports
|
|
17
|
+
# -----------------------------------------------------------------------------
|
|
18
|
+
import asyncio
|
|
19
|
+
import os
|
|
20
|
+
import logging
|
|
21
|
+
from typing import Callable, Iterable, Optional
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
|
|
25
|
+
from bumble.core import ProtocolError
|
|
26
|
+
from bumble.colors import color
|
|
27
|
+
from bumble.device import Device, Peer
|
|
28
|
+
from bumble.gatt import Service
|
|
29
|
+
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
|
|
30
|
+
from bumble.profiles.battery_service import BatteryServiceProxy
|
|
31
|
+
from bumble.profiles.gap import GenericAccessServiceProxy
|
|
32
|
+
from bumble.profiles.tmap import TelephonyAndMediaAudioServiceProxy
|
|
33
|
+
from bumble.transport import open_transport_or_link
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# -----------------------------------------------------------------------------
|
|
37
|
+
async def try_show(function: Callable, *args, **kwargs) -> None:
|
|
38
|
+
try:
|
|
39
|
+
await function(*args, **kwargs)
|
|
40
|
+
except ProtocolError as error:
|
|
41
|
+
print(color('ERROR:', 'red'), error)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# -----------------------------------------------------------------------------
|
|
45
|
+
def show_services(services: Iterable[Service]) -> None:
|
|
46
|
+
for service in services:
|
|
47
|
+
print(color(str(service), 'cyan'))
|
|
48
|
+
|
|
49
|
+
for characteristic in service.characteristics:
|
|
50
|
+
print(color(' ' + str(characteristic), 'magenta'))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# -----------------------------------------------------------------------------
|
|
54
|
+
async def show_gap_information(
|
|
55
|
+
gap_service: GenericAccessServiceProxy,
|
|
56
|
+
):
|
|
57
|
+
print(color('### Generic Access Profile', 'yellow'))
|
|
58
|
+
|
|
59
|
+
if gap_service.device_name:
|
|
60
|
+
print(
|
|
61
|
+
color(' Device Name:', 'green'),
|
|
62
|
+
await gap_service.device_name.read_value(),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if gap_service.appearance:
|
|
66
|
+
print(
|
|
67
|
+
color(' Appearance: ', 'green'),
|
|
68
|
+
await gap_service.appearance.read_value(),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
print()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# -----------------------------------------------------------------------------
|
|
75
|
+
async def show_device_information(
|
|
76
|
+
device_information_service: DeviceInformationServiceProxy,
|
|
77
|
+
):
|
|
78
|
+
print(color('### Device Information', 'yellow'))
|
|
79
|
+
|
|
80
|
+
if device_information_service.manufacturer_name:
|
|
81
|
+
print(
|
|
82
|
+
color(' Manufacturer Name:', 'green'),
|
|
83
|
+
await device_information_service.manufacturer_name.read_value(),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if device_information_service.model_number:
|
|
87
|
+
print(
|
|
88
|
+
color(' Model Number: ', 'green'),
|
|
89
|
+
await device_information_service.model_number.read_value(),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if device_information_service.serial_number:
|
|
93
|
+
print(
|
|
94
|
+
color(' Serial Number: ', 'green'),
|
|
95
|
+
await device_information_service.serial_number.read_value(),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if device_information_service.firmware_revision:
|
|
99
|
+
print(
|
|
100
|
+
color(' Firmware Revision:', 'green'),
|
|
101
|
+
await device_information_service.firmware_revision.read_value(),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
print()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# -----------------------------------------------------------------------------
|
|
108
|
+
async def show_battery_level(
|
|
109
|
+
battery_service: BatteryServiceProxy,
|
|
110
|
+
):
|
|
111
|
+
print(color('### Battery Information', 'yellow'))
|
|
112
|
+
|
|
113
|
+
if battery_service.battery_level:
|
|
114
|
+
print(
|
|
115
|
+
color(' Battery Level:', 'green'),
|
|
116
|
+
await battery_service.battery_level.read_value(),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
print()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# -----------------------------------------------------------------------------
|
|
123
|
+
async def show_tmas(
|
|
124
|
+
tmas: TelephonyAndMediaAudioServiceProxy,
|
|
125
|
+
):
|
|
126
|
+
print(color('### Telephony And Media Audio Service', 'yellow'))
|
|
127
|
+
|
|
128
|
+
if tmas.role:
|
|
129
|
+
print(
|
|
130
|
+
color(' Role:', 'green'),
|
|
131
|
+
await tmas.role.read_value(),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
print()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# -----------------------------------------------------------------------------
|
|
138
|
+
async def show_device_info(peer, done: Optional[asyncio.Future]) -> None:
|
|
139
|
+
try:
|
|
140
|
+
# Discover all services
|
|
141
|
+
print(color('### Discovering Services and Characteristics', 'magenta'))
|
|
142
|
+
await peer.discover_services()
|
|
143
|
+
for service in peer.services:
|
|
144
|
+
await service.discover_characteristics()
|
|
145
|
+
|
|
146
|
+
print(color('=== Services ===', 'yellow'))
|
|
147
|
+
show_services(peer.services)
|
|
148
|
+
print()
|
|
149
|
+
|
|
150
|
+
if gap_service := peer.create_service_proxy(GenericAccessServiceProxy):
|
|
151
|
+
await try_show(show_gap_information, gap_service)
|
|
152
|
+
|
|
153
|
+
if device_information_service := peer.create_service_proxy(
|
|
154
|
+
DeviceInformationServiceProxy
|
|
155
|
+
):
|
|
156
|
+
await try_show(show_device_information, device_information_service)
|
|
157
|
+
|
|
158
|
+
if battery_service := peer.create_service_proxy(BatteryServiceProxy):
|
|
159
|
+
await try_show(show_battery_level, battery_service)
|
|
160
|
+
|
|
161
|
+
if tmas := peer.create_service_proxy(TelephonyAndMediaAudioServiceProxy):
|
|
162
|
+
await try_show(show_tmas, tmas)
|
|
163
|
+
|
|
164
|
+
if done is not None:
|
|
165
|
+
done.set_result(None)
|
|
166
|
+
except asyncio.CancelledError:
|
|
167
|
+
print(color('!!! Operation canceled', 'red'))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# -----------------------------------------------------------------------------
|
|
171
|
+
async def async_main(device_config, encrypt, transport, address_or_name):
|
|
172
|
+
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
|
173
|
+
|
|
174
|
+
# Create a device
|
|
175
|
+
if device_config:
|
|
176
|
+
device = Device.from_config_file_with_hci(
|
|
177
|
+
device_config, hci_source, hci_sink
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
device = Device.with_hci(
|
|
181
|
+
'Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink
|
|
182
|
+
)
|
|
183
|
+
await device.power_on()
|
|
184
|
+
|
|
185
|
+
if address_or_name:
|
|
186
|
+
# Connect to the target peer
|
|
187
|
+
print(color('>>> Connecting...', 'green'))
|
|
188
|
+
connection = await device.connect(address_or_name)
|
|
189
|
+
print(color('>>> Connected', 'green'))
|
|
190
|
+
|
|
191
|
+
# Encrypt the connection if required
|
|
192
|
+
if encrypt:
|
|
193
|
+
print(color('+++ Encrypting connection...', 'blue'))
|
|
194
|
+
await connection.encrypt()
|
|
195
|
+
print(color('+++ Encryption established', 'blue'))
|
|
196
|
+
|
|
197
|
+
await show_device_info(Peer(connection), None)
|
|
198
|
+
else:
|
|
199
|
+
# Wait for a connection
|
|
200
|
+
done = asyncio.get_running_loop().create_future()
|
|
201
|
+
device.on(
|
|
202
|
+
'connection',
|
|
203
|
+
lambda connection: asyncio.create_task(
|
|
204
|
+
show_device_info(Peer(connection), done)
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
await device.start_advertising(auto_restart=True)
|
|
208
|
+
|
|
209
|
+
print(color('### Waiting for connection...', 'blue'))
|
|
210
|
+
await done
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# -----------------------------------------------------------------------------
|
|
214
|
+
@click.command()
|
|
215
|
+
@click.option('--device-config', help='Device configuration', type=click.Path())
|
|
216
|
+
@click.option('--encrypt', help='Encrypt the connection', is_flag=True, default=False)
|
|
217
|
+
@click.argument('transport')
|
|
218
|
+
@click.argument('address-or-name', required=False)
|
|
219
|
+
def main(device_config, encrypt, transport, address_or_name):
|
|
220
|
+
"""
|
|
221
|
+
Dump the GATT database on a remote device. If ADDRESS_OR_NAME is not specified,
|
|
222
|
+
wait for an incoming connection.
|
|
223
|
+
"""
|
|
224
|
+
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
|
225
|
+
asyncio.run(async_main(device_config, encrypt, transport, address_or_name))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# -----------------------------------------------------------------------------
|
|
229
|
+
if __name__ == '__main__':
|
|
230
|
+
main()
|
bumble/apps/gatt_dump.py
CHANGED
|
@@ -75,11 +75,15 @@ async def async_main(device_config, encrypt, transport, address_or_name):
|
|
|
75
75
|
|
|
76
76
|
if address_or_name:
|
|
77
77
|
# Connect to the target peer
|
|
78
|
+
print(color('>>> Connecting...', 'green'))
|
|
78
79
|
connection = await device.connect(address_or_name)
|
|
80
|
+
print(color('>>> Connected', 'green'))
|
|
79
81
|
|
|
80
82
|
# Encrypt the connection if required
|
|
81
83
|
if encrypt:
|
|
84
|
+
print(color('+++ Encrypting connection...', 'blue'))
|
|
82
85
|
await connection.encrypt()
|
|
86
|
+
print(color('+++ Encryption established', 'blue'))
|
|
83
87
|
|
|
84
88
|
await dump_gatt_db(Peer(connection), None)
|
|
85
89
|
else:
|
bumble/apps/lea_unicast/app.py
CHANGED
|
@@ -33,7 +33,6 @@ import ctypes
|
|
|
33
33
|
import wasmtime
|
|
34
34
|
import wasmtime.loader
|
|
35
35
|
import liblc3 # type: ignore
|
|
36
|
-
import logging
|
|
37
36
|
|
|
38
37
|
import click
|
|
39
38
|
import aiohttp.web
|
|
@@ -43,7 +42,7 @@ from bumble.core import AdvertisingData
|
|
|
43
42
|
from bumble.colors import color
|
|
44
43
|
from bumble.device import Device, DeviceConfiguration, AdvertisingParameters
|
|
45
44
|
from bumble.transport import open_transport
|
|
46
|
-
from bumble.profiles import bap
|
|
45
|
+
from bumble.profiles import ascs, bap, pacs
|
|
47
46
|
from bumble.hci import Address, CodecID, CodingFormat, HCI_IsoDataPacket
|
|
48
47
|
|
|
49
48
|
# -----------------------------------------------------------------------------
|
|
@@ -57,8 +56,8 @@ logger = logging.getLogger(__name__)
|
|
|
57
56
|
DEFAULT_UI_PORT = 7654
|
|
58
57
|
|
|
59
58
|
|
|
60
|
-
def _sink_pac_record() ->
|
|
61
|
-
return
|
|
59
|
+
def _sink_pac_record() -> pacs.PacRecord:
|
|
60
|
+
return pacs.PacRecord(
|
|
62
61
|
coding_format=CodingFormat(CodecID.LC3),
|
|
63
62
|
codec_specific_capabilities=bap.CodecSpecificCapabilities(
|
|
64
63
|
supported_sampling_frequencies=(
|
|
@@ -79,8 +78,8 @@ def _sink_pac_record() -> bap.PacRecord:
|
|
|
79
78
|
)
|
|
80
79
|
|
|
81
80
|
|
|
82
|
-
def _source_pac_record() ->
|
|
83
|
-
return
|
|
81
|
+
def _source_pac_record() -> pacs.PacRecord:
|
|
82
|
+
return pacs.PacRecord(
|
|
84
83
|
coding_format=CodingFormat(CodecID.LC3),
|
|
85
84
|
codec_specific_capabilities=bap.CodecSpecificCapabilities(
|
|
86
85
|
supported_sampling_frequencies=(
|
|
@@ -447,7 +446,7 @@ class Speaker:
|
|
|
447
446
|
)
|
|
448
447
|
|
|
449
448
|
self.device.add_service(
|
|
450
|
-
|
|
449
|
+
pacs.PublishedAudioCapabilitiesService(
|
|
451
450
|
supported_source_context=bap.ContextType(0xFFFF),
|
|
452
451
|
available_source_context=bap.ContextType(0xFFFF),
|
|
453
452
|
supported_sink_context=bap.ContextType(0xFFFF), # All context types
|
|
@@ -461,10 +460,10 @@ class Speaker:
|
|
|
461
460
|
)
|
|
462
461
|
)
|
|
463
462
|
|
|
464
|
-
|
|
463
|
+
ascs_service = ascs.AudioStreamControlService(
|
|
465
464
|
self.device, sink_ase_id=[1], source_ase_id=[2]
|
|
466
465
|
)
|
|
467
|
-
self.device.add_service(
|
|
466
|
+
self.device.add_service(ascs_service)
|
|
468
467
|
|
|
469
468
|
advertising_data = bytes(
|
|
470
469
|
AdvertisingData(
|
|
@@ -479,13 +478,13 @@ class Speaker:
|
|
|
479
478
|
),
|
|
480
479
|
(
|
|
481
480
|
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
|
482
|
-
bytes(
|
|
481
|
+
bytes(pacs.PublishedAudioCapabilitiesService.UUID),
|
|
483
482
|
),
|
|
484
483
|
]
|
|
485
484
|
)
|
|
486
485
|
) + bytes(bap.UnicastServerAdvertisingData())
|
|
487
486
|
|
|
488
|
-
def on_pdu(pdu: HCI_IsoDataPacket, ase:
|
|
487
|
+
def on_pdu(pdu: HCI_IsoDataPacket, ase: ascs.AseStateMachine):
|
|
489
488
|
codec_config = ase.codec_specific_configuration
|
|
490
489
|
assert isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
491
490
|
pcm = decode(
|
|
@@ -495,12 +494,12 @@ class Speaker:
|
|
|
495
494
|
)
|
|
496
495
|
self.device.abort_on('disconnection', self.ui_server.send_audio(pcm))
|
|
497
496
|
|
|
498
|
-
def on_ase_state_change(ase:
|
|
499
|
-
if ase.state ==
|
|
497
|
+
def on_ase_state_change(ase: ascs.AseStateMachine) -> None:
|
|
498
|
+
if ase.state == ascs.AseStateMachine.State.STREAMING:
|
|
500
499
|
codec_config = ase.codec_specific_configuration
|
|
501
500
|
assert isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
502
501
|
assert ase.cis_link
|
|
503
|
-
if ase.role ==
|
|
502
|
+
if ase.role == ascs.AudioRole.SOURCE:
|
|
504
503
|
ase.cis_link.abort_on(
|
|
505
504
|
'disconnection',
|
|
506
505
|
lc3_source_task(
|
|
@@ -516,10 +515,10 @@ class Speaker:
|
|
|
516
515
|
)
|
|
517
516
|
else:
|
|
518
517
|
ase.cis_link.sink = functools.partial(on_pdu, ase=ase)
|
|
519
|
-
elif ase.state ==
|
|
518
|
+
elif ase.state == ascs.AseStateMachine.State.CODEC_CONFIGURED:
|
|
520
519
|
codec_config = ase.codec_specific_configuration
|
|
521
520
|
assert isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
522
|
-
if ase.role ==
|
|
521
|
+
if ase.role == ascs.AudioRole.SOURCE:
|
|
523
522
|
setup_encoders(
|
|
524
523
|
codec_config.sampling_frequency.hz,
|
|
525
524
|
codec_config.frame_duration.us,
|
|
@@ -532,7 +531,7 @@ class Speaker:
|
|
|
532
531
|
codec_config.audio_channel_allocation.channel_count,
|
|
533
532
|
)
|
|
534
533
|
|
|
535
|
-
for ase in
|
|
534
|
+
for ase in ascs_service.ase_state_machines.values():
|
|
536
535
|
ase.on('state_change', functools.partial(on_ase_state_change, ase=ase))
|
|
537
536
|
|
|
538
537
|
await self.device.power_on()
|
bumble/at.py
CHANGED
|
@@ -14,13 +14,19 @@
|
|
|
14
14
|
|
|
15
15
|
from typing import List, Union
|
|
16
16
|
|
|
17
|
+
from bumble import core
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AtParsingError(core.InvalidPacketError):
|
|
21
|
+
"""Error raised when parsing AT commands fails."""
|
|
22
|
+
|
|
17
23
|
|
|
18
24
|
def tokenize_parameters(buffer: bytes) -> List[bytes]:
|
|
19
25
|
"""Split input parameters into tokens.
|
|
20
26
|
Removes space characters outside of double quote blocks:
|
|
21
27
|
T-rec-V-25 - 5.2.1 Command line general format: "Space characters (IA5 2/0)
|
|
22
28
|
are ignored [..], unless they are embedded in numeric or string constants"
|
|
23
|
-
Raises
|
|
29
|
+
Raises AtParsingError in case of invalid input string."""
|
|
24
30
|
|
|
25
31
|
tokens = []
|
|
26
32
|
in_quotes = False
|
|
@@ -43,11 +49,11 @@ def tokenize_parameters(buffer: bytes) -> List[bytes]:
|
|
|
43
49
|
token = bytearray()
|
|
44
50
|
elif char == b'(':
|
|
45
51
|
if len(token) > 0:
|
|
46
|
-
raise
|
|
52
|
+
raise AtParsingError("open_paren following regular character")
|
|
47
53
|
tokens.append(char)
|
|
48
54
|
elif char == b'"':
|
|
49
55
|
if len(token) > 0:
|
|
50
|
-
raise
|
|
56
|
+
raise AtParsingError("quote following regular character")
|
|
51
57
|
in_quotes = True
|
|
52
58
|
token.extend(char)
|
|
53
59
|
else:
|
|
@@ -59,7 +65,7 @@ def tokenize_parameters(buffer: bytes) -> List[bytes]:
|
|
|
59
65
|
|
|
60
66
|
def parse_parameters(buffer: bytes) -> List[Union[bytes, list]]:
|
|
61
67
|
"""Parse the parameters using the comma and parenthesis separators.
|
|
62
|
-
Raises
|
|
68
|
+
Raises AtParsingError in case of invalid input string."""
|
|
63
69
|
|
|
64
70
|
tokens = tokenize_parameters(buffer)
|
|
65
71
|
accumulator: List[list] = [[]]
|
|
@@ -73,7 +79,7 @@ def parse_parameters(buffer: bytes) -> List[Union[bytes, list]]:
|
|
|
73
79
|
accumulator.append([])
|
|
74
80
|
elif token == b')':
|
|
75
81
|
if len(accumulator) < 2:
|
|
76
|
-
raise
|
|
82
|
+
raise AtParsingError("close_paren without matching open_paren")
|
|
77
83
|
accumulator[-1].append(current)
|
|
78
84
|
current = accumulator.pop()
|
|
79
85
|
else:
|
|
@@ -81,5 +87,5 @@ def parse_parameters(buffer: bytes) -> List[Union[bytes, list]]:
|
|
|
81
87
|
|
|
82
88
|
accumulator[-1].append(current)
|
|
83
89
|
if len(accumulator) > 1:
|
|
84
|
-
raise
|
|
90
|
+
raise AtParsingError("missing close_paren")
|
|
85
91
|
return accumulator[0]
|
bumble/avc.py
CHANGED
|
@@ -20,6 +20,7 @@ import enum
|
|
|
20
20
|
import struct
|
|
21
21
|
from typing import Dict, Type, Union, Tuple
|
|
22
22
|
|
|
23
|
+
from bumble import core
|
|
23
24
|
from bumble.utils import OpenIntEnum
|
|
24
25
|
|
|
25
26
|
|
|
@@ -88,7 +89,9 @@ class Frame:
|
|
|
88
89
|
short_name = subclass.__name__.replace("ResponseFrame", "")
|
|
89
90
|
category_class = ResponseFrame
|
|
90
91
|
else:
|
|
91
|
-
raise
|
|
92
|
+
raise core.InvalidArgumentError(
|
|
93
|
+
f"invalid subclass name {subclass.__name__}"
|
|
94
|
+
)
|
|
92
95
|
|
|
93
96
|
uppercase_indexes = [
|
|
94
97
|
i for i in range(len(short_name)) if short_name[i].isupper()
|
|
@@ -106,7 +109,7 @@ class Frame:
|
|
|
106
109
|
@staticmethod
|
|
107
110
|
def from_bytes(data: bytes) -> Frame:
|
|
108
111
|
if data[0] >> 4 != 0:
|
|
109
|
-
raise
|
|
112
|
+
raise core.InvalidPacketError("first 4 bits must be 0s")
|
|
110
113
|
|
|
111
114
|
ctype_or_response = data[0] & 0xF
|
|
112
115
|
subunit_type = Frame.SubunitType(data[1] >> 3)
|
|
@@ -122,7 +125,7 @@ class Frame:
|
|
|
122
125
|
# Extended to the next byte
|
|
123
126
|
extension = data[2]
|
|
124
127
|
if extension == 0:
|
|
125
|
-
raise
|
|
128
|
+
raise core.InvalidPacketError("extended subunit ID value reserved")
|
|
126
129
|
if extension == 0xFF:
|
|
127
130
|
subunit_id = 5 + 254 + data[3]
|
|
128
131
|
opcode_offset = 4
|
|
@@ -131,7 +134,7 @@ class Frame:
|
|
|
131
134
|
opcode_offset = 3
|
|
132
135
|
|
|
133
136
|
elif subunit_id == 6:
|
|
134
|
-
raise
|
|
137
|
+
raise core.InvalidPacketError("reserved subunit ID")
|
|
135
138
|
|
|
136
139
|
opcode = Frame.OperationCode(data[opcode_offset])
|
|
137
140
|
operands = data[opcode_offset + 1 :]
|
|
@@ -448,7 +451,7 @@ class PassThroughFrame:
|
|
|
448
451
|
operation_data: bytes,
|
|
449
452
|
) -> None:
|
|
450
453
|
if len(operation_data) > 255:
|
|
451
|
-
raise
|
|
454
|
+
raise core.InvalidArgumentError("operation data must be <= 255 bytes")
|
|
452
455
|
self.state_flag = state_flag
|
|
453
456
|
self.operation_id = operation_id
|
|
454
457
|
self.operation_data = operation_data
|
bumble/avctp.py
CHANGED
|
@@ -23,6 +23,7 @@ from typing import Callable, cast, Dict, Optional
|
|
|
23
23
|
|
|
24
24
|
from bumble.colors import color
|
|
25
25
|
from bumble import avc
|
|
26
|
+
from bumble import core
|
|
26
27
|
from bumble import l2cap
|
|
27
28
|
|
|
28
29
|
# -----------------------------------------------------------------------------
|
|
@@ -275,7 +276,7 @@ class Protocol:
|
|
|
275
276
|
self, pid: int, handler: Protocol.CommandHandler
|
|
276
277
|
) -> None:
|
|
277
278
|
if pid not in self.command_handlers or self.command_handlers[pid] != handler:
|
|
278
|
-
raise
|
|
279
|
+
raise core.InvalidArgumentError("command handler not registered")
|
|
279
280
|
del self.command_handlers[pid]
|
|
280
281
|
|
|
281
282
|
def register_response_handler(
|
|
@@ -287,5 +288,5 @@ class Protocol:
|
|
|
287
288
|
self, pid: int, handler: Protocol.ResponseHandler
|
|
288
289
|
) -> None:
|
|
289
290
|
if pid not in self.response_handlers or self.response_handlers[pid] != handler:
|
|
290
|
-
raise
|
|
291
|
+
raise core.InvalidArgumentError("response handler not registered")
|
|
291
292
|
del self.response_handlers[pid]
|
bumble/avdtp.py
CHANGED
|
@@ -43,6 +43,7 @@ from .core import (
|
|
|
43
43
|
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
|
44
44
|
InvalidStateError,
|
|
45
45
|
ProtocolError,
|
|
46
|
+
InvalidArgumentError,
|
|
46
47
|
name_or_number,
|
|
47
48
|
)
|
|
48
49
|
from .a2dp import (
|
|
@@ -700,7 +701,7 @@ class Message: # pylint:disable=attribute-defined-outside-init
|
|
|
700
701
|
signal_identifier_str = name[:-7]
|
|
701
702
|
message_type = Message.MessageType.RESPONSE_REJECT
|
|
702
703
|
else:
|
|
703
|
-
raise
|
|
704
|
+
raise InvalidArgumentError('invalid class name')
|
|
704
705
|
|
|
705
706
|
subclass.message_type = message_type
|
|
706
707
|
|
|
@@ -2162,6 +2163,9 @@ class LocalStreamEndPoint(StreamEndPoint, EventEmitter):
|
|
|
2162
2163
|
def on_abort_command(self):
|
|
2163
2164
|
self.emit('abort')
|
|
2164
2165
|
|
|
2166
|
+
def on_delayreport_command(self, delay: int):
|
|
2167
|
+
self.emit('delay_report', delay)
|
|
2168
|
+
|
|
2165
2169
|
def on_rtp_channel_open(self):
|
|
2166
2170
|
self.emit('rtp_channel_open')
|
|
2167
2171
|
|
bumble/avrcp.py
CHANGED
|
@@ -55,6 +55,7 @@ from bumble.sdp import (
|
|
|
55
55
|
)
|
|
56
56
|
from bumble.utils import AsyncRunner, OpenIntEnum
|
|
57
57
|
from bumble.core import (
|
|
58
|
+
InvalidArgumentError,
|
|
58
59
|
ProtocolError,
|
|
59
60
|
BT_L2CAP_PROTOCOL_ID,
|
|
60
61
|
BT_AVCTP_PROTOCOL_ID,
|
|
@@ -1411,7 +1412,7 @@ class Protocol(pyee.EventEmitter):
|
|
|
1411
1412
|
def notify_track_changed(self, identifier: bytes) -> None:
|
|
1412
1413
|
"""Notify the connected peer of a Track change."""
|
|
1413
1414
|
if len(identifier) != 8:
|
|
1414
|
-
raise
|
|
1415
|
+
raise InvalidArgumentError("identifier must be 8 bytes")
|
|
1415
1416
|
self.notify_event(TrackChangedEvent(identifier))
|
|
1416
1417
|
|
|
1417
1418
|
def notify_playback_position_changed(self, position: int) -> None:
|
bumble/codecs.py
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
from dataclasses import dataclass
|
|
20
20
|
|
|
21
|
+
from bumble import core
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
# -----------------------------------------------------------------------------
|
|
23
25
|
class BitReader:
|
|
@@ -40,7 +42,7 @@ class BitReader:
|
|
|
40
42
|
""" "Read up to 32 bits."""
|
|
41
43
|
|
|
42
44
|
if bits > 32:
|
|
43
|
-
raise
|
|
45
|
+
raise core.InvalidArgumentError('maximum read size is 32')
|
|
44
46
|
|
|
45
47
|
if self.bits_cached >= bits:
|
|
46
48
|
# We have enough bits.
|
|
@@ -53,7 +55,7 @@ class BitReader:
|
|
|
53
55
|
feed_size = len(feed_bytes)
|
|
54
56
|
feed_int = int.from_bytes(feed_bytes, byteorder='big')
|
|
55
57
|
if 8 * feed_size + self.bits_cached < bits:
|
|
56
|
-
raise
|
|
58
|
+
raise core.InvalidArgumentError('trying to read past the data')
|
|
57
59
|
self.byte_position += feed_size
|
|
58
60
|
|
|
59
61
|
# Combine the new cache and the old cache
|
|
@@ -68,7 +70,7 @@ class BitReader:
|
|
|
68
70
|
|
|
69
71
|
def read_bytes(self, count: int):
|
|
70
72
|
if self.bit_position + 8 * count > 8 * len(self.data):
|
|
71
|
-
raise
|
|
73
|
+
raise core.InvalidArgumentError('not enough data')
|
|
72
74
|
|
|
73
75
|
if self.bit_position % 8:
|
|
74
76
|
# Not byte aligned
|
|
@@ -113,7 +115,7 @@ class AacAudioRtpPacket:
|
|
|
113
115
|
|
|
114
116
|
@staticmethod
|
|
115
117
|
def program_config_element(reader: BitReader):
|
|
116
|
-
raise
|
|
118
|
+
raise core.InvalidPacketError('program_config_element not supported')
|
|
117
119
|
|
|
118
120
|
@dataclass
|
|
119
121
|
class GASpecificConfig:
|
|
@@ -140,7 +142,7 @@ class AacAudioRtpPacket:
|
|
|
140
142
|
aac_spectral_data_resilience_flags = reader.read(1)
|
|
141
143
|
extension_flag_3 = reader.read(1)
|
|
142
144
|
if extension_flag_3 == 1:
|
|
143
|
-
raise
|
|
145
|
+
raise core.InvalidPacketError('extensionFlag3 == 1 not supported')
|
|
144
146
|
|
|
145
147
|
@staticmethod
|
|
146
148
|
def audio_object_type(reader: BitReader):
|
|
@@ -216,7 +218,7 @@ class AacAudioRtpPacket:
|
|
|
216
218
|
reader, self.channel_configuration, self.audio_object_type
|
|
217
219
|
)
|
|
218
220
|
else:
|
|
219
|
-
raise
|
|
221
|
+
raise core.InvalidPacketError(
|
|
220
222
|
f'audioObjectType {self.audio_object_type} not supported'
|
|
221
223
|
)
|
|
222
224
|
|
|
@@ -260,7 +262,7 @@ class AacAudioRtpPacket:
|
|
|
260
262
|
else:
|
|
261
263
|
audio_mux_version_a = 0
|
|
262
264
|
if audio_mux_version_a != 0:
|
|
263
|
-
raise
|
|
265
|
+
raise core.InvalidPacketError('audioMuxVersionA != 0 not supported')
|
|
264
266
|
if audio_mux_version == 1:
|
|
265
267
|
tara_buffer_fullness = AacAudioRtpPacket.latm_value(reader)
|
|
266
268
|
stream_cnt = 0
|
|
@@ -268,10 +270,10 @@ class AacAudioRtpPacket:
|
|
|
268
270
|
num_sub_frames = reader.read(6)
|
|
269
271
|
num_program = reader.read(4)
|
|
270
272
|
if num_program != 0:
|
|
271
|
-
raise
|
|
273
|
+
raise core.InvalidPacketError('num_program != 0 not supported')
|
|
272
274
|
num_layer = reader.read(3)
|
|
273
275
|
if num_layer != 0:
|
|
274
|
-
raise
|
|
276
|
+
raise core.InvalidPacketError('num_layer != 0 not supported')
|
|
275
277
|
if audio_mux_version == 0:
|
|
276
278
|
self.audio_specific_config = AacAudioRtpPacket.AudioSpecificConfig(
|
|
277
279
|
reader
|
|
@@ -284,7 +286,7 @@ class AacAudioRtpPacket:
|
|
|
284
286
|
)
|
|
285
287
|
audio_specific_config_len = reader.bit_position - marker
|
|
286
288
|
if asc_len < audio_specific_config_len:
|
|
287
|
-
raise
|
|
289
|
+
raise core.InvalidPacketError('audio_specific_config_len > asc_len')
|
|
288
290
|
asc_len -= audio_specific_config_len
|
|
289
291
|
reader.skip(asc_len)
|
|
290
292
|
frame_length_type = reader.read(3)
|
|
@@ -293,7 +295,9 @@ class AacAudioRtpPacket:
|
|
|
293
295
|
elif frame_length_type == 1:
|
|
294
296
|
frame_length = reader.read(9)
|
|
295
297
|
else:
|
|
296
|
-
raise
|
|
298
|
+
raise core.InvalidPacketError(
|
|
299
|
+
f'frame_length_type {frame_length_type} not supported'
|
|
300
|
+
)
|
|
297
301
|
|
|
298
302
|
self.other_data_present = reader.read(1)
|
|
299
303
|
if self.other_data_present:
|
|
@@ -318,12 +322,12 @@ class AacAudioRtpPacket:
|
|
|
318
322
|
|
|
319
323
|
def __init__(self, reader: BitReader, mux_config_present: int):
|
|
320
324
|
if mux_config_present == 0:
|
|
321
|
-
raise
|
|
325
|
+
raise core.InvalidPacketError('muxConfigPresent == 0 not supported')
|
|
322
326
|
|
|
323
327
|
# AudioMuxElement - ISO/EIC 14496-3 Table 1.41
|
|
324
328
|
use_same_stream_mux = reader.read(1)
|
|
325
329
|
if use_same_stream_mux:
|
|
326
|
-
raise
|
|
330
|
+
raise core.InvalidPacketError('useSameStreamMux == 1 not supported')
|
|
327
331
|
self.stream_mux_config = AacAudioRtpPacket.StreamMuxConfig(reader)
|
|
328
332
|
|
|
329
333
|
# We only support:
|