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.
Files changed (50) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/auracast.py +351 -66
  3. bumble/apps/console.py +5 -20
  4. bumble/apps/device_info.py +230 -0
  5. bumble/apps/gatt_dump.py +4 -0
  6. bumble/apps/lea_unicast/app.py +16 -17
  7. bumble/at.py +12 -6
  8. bumble/avc.py +8 -5
  9. bumble/avctp.py +3 -2
  10. bumble/avdtp.py +5 -1
  11. bumble/avrcp.py +2 -1
  12. bumble/codecs.py +17 -13
  13. bumble/colors.py +6 -2
  14. bumble/core.py +37 -7
  15. bumble/device.py +382 -111
  16. bumble/drivers/rtk.py +13 -8
  17. bumble/gatt.py +6 -1
  18. bumble/gatt_client.py +10 -4
  19. bumble/hci.py +50 -25
  20. bumble/hid.py +24 -28
  21. bumble/host.py +4 -0
  22. bumble/l2cap.py +24 -17
  23. bumble/link.py +8 -3
  24. bumble/profiles/ascs.py +739 -0
  25. bumble/profiles/bap.py +1 -874
  26. bumble/profiles/bass.py +440 -0
  27. bumble/profiles/csip.py +4 -4
  28. bumble/profiles/gap.py +110 -0
  29. bumble/profiles/heart_rate_service.py +4 -3
  30. bumble/profiles/le_audio.py +43 -9
  31. bumble/profiles/mcp.py +448 -0
  32. bumble/profiles/pacs.py +210 -0
  33. bumble/profiles/tmap.py +89 -0
  34. bumble/rfcomm.py +4 -2
  35. bumble/sdp.py +13 -11
  36. bumble/smp.py +20 -8
  37. bumble/snoop.py +5 -4
  38. bumble/transport/__init__.py +8 -2
  39. bumble/transport/android_emulator.py +9 -3
  40. bumble/transport/android_netsim.py +9 -7
  41. bumble/transport/common.py +46 -18
  42. bumble/transport/pyusb.py +2 -2
  43. bumble/transport/unix.py +56 -0
  44. bumble/transport/usb.py +57 -46
  45. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/METADATA +41 -41
  46. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/RECORD +50 -42
  47. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/WHEEL +1 -1
  48. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/LICENSE +0 -0
  49. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/entry_points.txt +0 -0
  50. {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:
@@ -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() -> bap.PacRecord:
61
- return bap.PacRecord(
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() -> bap.PacRecord:
83
- return bap.PacRecord(
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
- bap.PublishedAudioCapabilitiesService(
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
- ascs = bap.AudioStreamControlService(
463
+ ascs_service = ascs.AudioStreamControlService(
465
464
  self.device, sink_ase_id=[1], source_ase_id=[2]
466
465
  )
467
- self.device.add_service(ascs)
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(bap.PublishedAudioCapabilitiesService.UUID),
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: bap.AseStateMachine):
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: bap.AseStateMachine) -> None:
499
- if ase.state == bap.AseStateMachine.State.STREAMING:
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 == bap.AudioRole.SOURCE:
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 == bap.AseStateMachine.State.CODEC_CONFIGURED:
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 == bap.AudioRole.SOURCE:
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 ascs.ase_state_machines.values():
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 ValueError in case of invalid input string."""
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 ValueError("open_paren following regular character")
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 ValueError("quote following regular character")
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 ValueError in case of invalid input string."""
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 ValueError("close_paren without matching open_paren")
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 ValueError("missing close_paren")
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 ValueError(f"invalid subclass name {subclass.__name__}")
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 ValueError("first 4 bits must be 0s")
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 ValueError("extended subunit ID value reserved")
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 ValueError("reserved subunit ID")
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 ValueError("operation data must be <= 255 bytes")
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 ValueError("command handler not registered")
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 ValueError("response handler not registered")
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 ValueError('invalid class name')
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 ValueError("identifier must be 8 bytes")
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 ValueError('maximum read size is 32')
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 ValueError('trying to read past the data')
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 ValueError('not enough data')
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 ValueError('program_config_element not supported')
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 ValueError('extensionFlag3 == 1 not supported')
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 ValueError(
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 ValueError('audioMuxVersionA != 0 not supported')
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 ValueError('num_program != 0 not supported')
273
+ raise core.InvalidPacketError('num_program != 0 not supported')
272
274
  num_layer = reader.read(3)
273
275
  if num_layer != 0:
274
- raise ValueError('num_layer != 0 not supported')
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 ValueError('audio_specific_config_len > asc_len')
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 ValueError(f'frame_length_type {frame_length_type} not supported')
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 ValueError('muxConfigPresent == 0 not supported')
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 ValueError('useSameStreamMux == 1 not supported')
330
+ raise core.InvalidPacketError('useSameStreamMux == 1 not supported')
327
331
  self.stream_mux_config = AacAudioRtpPacket.StreamMuxConfig(reader)
328
332
 
329
333
  # We only support: