bumble 0.0.195__py3-none-any.whl → 0.0.199__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 (61) 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/apps/pair.py +32 -5
  8. bumble/at.py +12 -6
  9. bumble/att.py +56 -40
  10. bumble/avc.py +8 -5
  11. bumble/avctp.py +3 -2
  12. bumble/avdtp.py +7 -3
  13. bumble/avrcp.py +2 -1
  14. bumble/codecs.py +17 -13
  15. bumble/colors.py +6 -2
  16. bumble/core.py +37 -7
  17. bumble/decoder.py +14 -10
  18. bumble/device.py +382 -111
  19. bumble/drivers/rtk.py +32 -13
  20. bumble/gatt.py +30 -20
  21. bumble/gatt_client.py +15 -29
  22. bumble/gatt_server.py +14 -6
  23. bumble/hci.py +322 -32
  24. bumble/hid.py +24 -28
  25. bumble/host.py +20 -6
  26. bumble/l2cap.py +24 -17
  27. bumble/link.py +8 -3
  28. bumble/pandora/__init__.py +3 -0
  29. bumble/pandora/l2cap.py +310 -0
  30. bumble/profiles/aics.py +520 -0
  31. bumble/profiles/ascs.py +739 -0
  32. bumble/profiles/asha.py +295 -0
  33. bumble/profiles/bap.py +1 -874
  34. bumble/profiles/bass.py +440 -0
  35. bumble/profiles/csip.py +4 -4
  36. bumble/profiles/gap.py +110 -0
  37. bumble/profiles/hap.py +665 -0
  38. bumble/profiles/heart_rate_service.py +4 -3
  39. bumble/profiles/le_audio.py +43 -9
  40. bumble/profiles/mcp.py +448 -0
  41. bumble/profiles/pacs.py +210 -0
  42. bumble/profiles/tmap.py +89 -0
  43. bumble/profiles/vcp.py +5 -3
  44. bumble/rfcomm.py +4 -2
  45. bumble/sdp.py +13 -11
  46. bumble/smp.py +43 -12
  47. bumble/snoop.py +5 -4
  48. bumble/transport/__init__.py +8 -2
  49. bumble/transport/android_emulator.py +9 -3
  50. bumble/transport/android_netsim.py +9 -7
  51. bumble/transport/common.py +46 -18
  52. bumble/transport/pyusb.py +21 -4
  53. bumble/transport/unix.py +56 -0
  54. bumble/transport/usb.py +57 -46
  55. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/METADATA +41 -41
  56. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/RECORD +60 -49
  57. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/WHEEL +1 -1
  58. bumble/profiles/asha_service.py +0 -193
  59. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/LICENSE +0 -0
  60. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/entry_points.txt +0 -0
  61. {bumble-0.0.195.dist-info → bumble-0.0.199.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/apps/pair.py CHANGED
@@ -46,6 +46,12 @@ from bumble.att import (
46
46
  ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
47
47
  ATT_INSUFFICIENT_ENCRYPTION_ERROR,
48
48
  )
49
+ from bumble.utils import AsyncRunner
50
+
51
+ # -----------------------------------------------------------------------------
52
+ # Constants
53
+ # -----------------------------------------------------------------------------
54
+ POST_PAIRING_DELAY = 1
49
55
 
50
56
 
51
57
  # -----------------------------------------------------------------------------
@@ -235,8 +241,10 @@ def on_connection(connection, request):
235
241
 
236
242
  # Listen for pairing events
237
243
  connection.on('pairing_start', on_pairing_start)
238
- connection.on('pairing', lambda keys: on_pairing(connection.peer_address, keys))
239
- connection.on('pairing_failure', on_pairing_failure)
244
+ connection.on('pairing', lambda keys: on_pairing(connection, keys))
245
+ connection.on(
246
+ 'pairing_failure', lambda reason: on_pairing_failure(connection, reason)
247
+ )
240
248
 
241
249
  # Listen for encryption changes
242
250
  connection.on(
@@ -270,19 +278,24 @@ def on_pairing_start():
270
278
 
271
279
 
272
280
  # -----------------------------------------------------------------------------
273
- def on_pairing(address, keys):
281
+ @AsyncRunner.run_in_task()
282
+ async def on_pairing(connection, keys):
274
283
  print(color('***-----------------------------------', 'cyan'))
275
- print(color(f'*** Paired! (peer identity={address})', 'cyan'))
284
+ print(color(f'*** Paired! (peer identity={connection.peer_address})', 'cyan'))
276
285
  keys.print(prefix=color('*** ', 'cyan'))
277
286
  print(color('***-----------------------------------', 'cyan'))
287
+ await asyncio.sleep(POST_PAIRING_DELAY)
288
+ await connection.disconnect()
278
289
  Waiter.instance.terminate()
279
290
 
280
291
 
281
292
  # -----------------------------------------------------------------------------
282
- def on_pairing_failure(reason):
293
+ @AsyncRunner.run_in_task()
294
+ async def on_pairing_failure(connection, reason):
283
295
  print(color('***-----------------------------------', 'red'))
284
296
  print(color(f'*** Pairing failed: {smp_error_name(reason)}', 'red'))
285
297
  print(color('***-----------------------------------', 'red'))
298
+ await connection.disconnect()
286
299
  Waiter.instance.terminate()
287
300
 
288
301
 
@@ -293,6 +306,7 @@ async def pair(
293
306
  mitm,
294
307
  bond,
295
308
  ctkd,
309
+ identity_address,
296
310
  linger,
297
311
  io,
298
312
  oob,
@@ -382,11 +396,18 @@ async def pair(
382
396
  oob_contexts = None
383
397
 
384
398
  # Set up a pairing config factory
399
+ if identity_address == 'public':
400
+ identity_address_type = PairingConfig.AddressType.PUBLIC
401
+ elif identity_address == 'random':
402
+ identity_address_type = PairingConfig.AddressType.RANDOM
403
+ else:
404
+ identity_address_type = None
385
405
  device.pairing_config_factory = lambda connection: PairingConfig(
386
406
  sc=sc,
387
407
  mitm=mitm,
388
408
  bonding=bond,
389
409
  oob=oob_contexts,
410
+ identity_address_type=identity_address_type,
390
411
  delegate=Delegate(mode, connection, io, prompt),
391
412
  )
392
413
 
@@ -457,6 +478,10 @@ class LogHandler(logging.Handler):
457
478
  help='Enable CTKD',
458
479
  show_default=True,
459
480
  )
481
+ @click.option(
482
+ '--identity-address',
483
+ type=click.Choice(['random', 'public']),
484
+ )
460
485
  @click.option('--linger', default=False, is_flag=True, help='Linger after pairing')
461
486
  @click.option(
462
487
  '--io',
@@ -493,6 +518,7 @@ def main(
493
518
  mitm,
494
519
  bond,
495
520
  ctkd,
521
+ identity_address,
496
522
  linger,
497
523
  io,
498
524
  oob,
@@ -518,6 +544,7 @@ def main(
518
544
  mitm,
519
545
  bond,
520
546
  ctkd,
547
+ identity_address,
521
548
  linger,
522
549
  io,
523
550
  oob,
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/att.py CHANGED
@@ -23,6 +23,7 @@
23
23
  # Imports
24
24
  # -----------------------------------------------------------------------------
25
25
  from __future__ import annotations
26
+
26
27
  import enum
27
28
  import functools
28
29
  import inspect
@@ -41,6 +42,7 @@ from typing import (
41
42
 
42
43
  from pyee import EventEmitter
43
44
 
45
+ from bumble import utils
44
46
  from bumble.core import UUID, name_or_number, ProtocolError
45
47
  from bumble.hci import HCI_Object, key_with_value
46
48
  from bumble.colors import color
@@ -145,43 +147,57 @@ ATT_RESPONSES = [
145
147
  ATT_EXECUTE_WRITE_RESPONSE
146
148
  ]
147
149
 
148
- ATT_INVALID_HANDLE_ERROR = 0x01
149
- ATT_READ_NOT_PERMITTED_ERROR = 0x02
150
- ATT_WRITE_NOT_PERMITTED_ERROR = 0x03
151
- ATT_INVALID_PDU_ERROR = 0x04
152
- ATT_INSUFFICIENT_AUTHENTICATION_ERROR = 0x05
153
- ATT_REQUEST_NOT_SUPPORTED_ERROR = 0x06
154
- ATT_INVALID_OFFSET_ERROR = 0x07
155
- ATT_INSUFFICIENT_AUTHORIZATION_ERROR = 0x08
156
- ATT_PREPARE_QUEUE_FULL_ERROR = 0x09
157
- ATT_ATTRIBUTE_NOT_FOUND_ERROR = 0x0A
158
- ATT_ATTRIBUTE_NOT_LONG_ERROR = 0x0B
159
- ATT_INSUFFICIENT_ENCRYPTION_KEY_SIZE_ERROR = 0x0C
160
- ATT_INVALID_ATTRIBUTE_LENGTH_ERROR = 0x0D
161
- ATT_UNLIKELY_ERROR_ERROR = 0x0E
162
- ATT_INSUFFICIENT_ENCRYPTION_ERROR = 0x0F
163
- ATT_UNSUPPORTED_GROUP_TYPE_ERROR = 0x10
164
- ATT_INSUFFICIENT_RESOURCES_ERROR = 0x11
165
-
166
- ATT_ERROR_NAMES = {
167
- ATT_INVALID_HANDLE_ERROR: 'ATT_INVALID_HANDLE_ERROR',
168
- ATT_READ_NOT_PERMITTED_ERROR: 'ATT_READ_NOT_PERMITTED_ERROR',
169
- ATT_WRITE_NOT_PERMITTED_ERROR: 'ATT_WRITE_NOT_PERMITTED_ERROR',
170
- ATT_INVALID_PDU_ERROR: 'ATT_INVALID_PDU_ERROR',
171
- ATT_INSUFFICIENT_AUTHENTICATION_ERROR: 'ATT_INSUFFICIENT_AUTHENTICATION_ERROR',
172
- ATT_REQUEST_NOT_SUPPORTED_ERROR: 'ATT_REQUEST_NOT_SUPPORTED_ERROR',
173
- ATT_INVALID_OFFSET_ERROR: 'ATT_INVALID_OFFSET_ERROR',
174
- ATT_INSUFFICIENT_AUTHORIZATION_ERROR: 'ATT_INSUFFICIENT_AUTHORIZATION_ERROR',
175
- ATT_PREPARE_QUEUE_FULL_ERROR: 'ATT_PREPARE_QUEUE_FULL_ERROR',
176
- ATT_ATTRIBUTE_NOT_FOUND_ERROR: 'ATT_ATTRIBUTE_NOT_FOUND_ERROR',
177
- ATT_ATTRIBUTE_NOT_LONG_ERROR: 'ATT_ATTRIBUTE_NOT_LONG_ERROR',
178
- ATT_INSUFFICIENT_ENCRYPTION_KEY_SIZE_ERROR: 'ATT_INSUFFICIENT_ENCRYPTION_KEY_SIZE_ERROR',
179
- ATT_INVALID_ATTRIBUTE_LENGTH_ERROR: 'ATT_INVALID_ATTRIBUTE_LENGTH_ERROR',
180
- ATT_UNLIKELY_ERROR_ERROR: 'ATT_UNLIKELY_ERROR_ERROR',
181
- ATT_INSUFFICIENT_ENCRYPTION_ERROR: 'ATT_INSUFFICIENT_ENCRYPTION_ERROR',
182
- ATT_UNSUPPORTED_GROUP_TYPE_ERROR: 'ATT_UNSUPPORTED_GROUP_TYPE_ERROR',
183
- ATT_INSUFFICIENT_RESOURCES_ERROR: 'ATT_INSUFFICIENT_RESOURCES_ERROR'
184
- }
150
+ class ErrorCode(utils.OpenIntEnum):
151
+ '''
152
+ See
153
+
154
+ * Bluetooth spec @ Vol 3, Part F - 3.4.1.1 Error Response
155
+ * Core Specification Supplement: Common Profile And Service Error Codes
156
+ '''
157
+ INVALID_HANDLE = 0x01
158
+ READ_NOT_PERMITTED = 0x02
159
+ WRITE_NOT_PERMITTED = 0x03
160
+ INVALID_PDU = 0x04
161
+ INSUFFICIENT_AUTHENTICATION = 0x05
162
+ REQUEST_NOT_SUPPORTED = 0x06
163
+ INVALID_OFFSET = 0x07
164
+ INSUFFICIENT_AUTHORIZATION = 0x08
165
+ PREPARE_QUEUE_FULL = 0x09
166
+ ATTRIBUTE_NOT_FOUND = 0x0A
167
+ ATTRIBUTE_NOT_LONG = 0x0B
168
+ INSUFFICIENT_ENCRYPTION_KEY_SIZE = 0x0C
169
+ INVALID_ATTRIBUTE_LENGTH = 0x0D
170
+ UNLIKELY_ERROR = 0x0E
171
+ INSUFFICIENT_ENCRYPTION = 0x0F
172
+ UNSUPPORTED_GROUP_TYPE = 0x10
173
+ INSUFFICIENT_RESOURCES = 0x11
174
+ DATABASE_OUT_OF_SYNC = 0x12
175
+ VALUE_NOT_ALLOWED = 0x13
176
+ # 0x80 – 0x9F: Application Error
177
+ # 0xE0 – 0xFF: Common Profile and Service Error Codes
178
+ WRITE_REQUEST_REJECTED = 0xFC
179
+ CCCD_IMPROPERLY_CONFIGURED = 0xFD
180
+ PROCEDURE_ALREADY_IN_PROGRESS = 0xFE
181
+ OUT_OF_RANGE = 0xFF
182
+
183
+ # Backward Compatible Constants
184
+ ATT_INVALID_HANDLE_ERROR = ErrorCode.INVALID_HANDLE
185
+ ATT_READ_NOT_PERMITTED_ERROR = ErrorCode.READ_NOT_PERMITTED
186
+ ATT_WRITE_NOT_PERMITTED_ERROR = ErrorCode.WRITE_NOT_PERMITTED
187
+ ATT_INVALID_PDU_ERROR = ErrorCode.INVALID_PDU
188
+ ATT_INSUFFICIENT_AUTHENTICATION_ERROR = ErrorCode.INSUFFICIENT_AUTHENTICATION
189
+ ATT_REQUEST_NOT_SUPPORTED_ERROR = ErrorCode.REQUEST_NOT_SUPPORTED
190
+ ATT_INVALID_OFFSET_ERROR = ErrorCode.INVALID_OFFSET
191
+ ATT_INSUFFICIENT_AUTHORIZATION_ERROR = ErrorCode.INSUFFICIENT_AUTHORIZATION
192
+ ATT_PREPARE_QUEUE_FULL_ERROR = ErrorCode.PREPARE_QUEUE_FULL
193
+ ATT_ATTRIBUTE_NOT_FOUND_ERROR = ErrorCode.ATTRIBUTE_NOT_FOUND
194
+ ATT_ATTRIBUTE_NOT_LONG_ERROR = ErrorCode.ATTRIBUTE_NOT_LONG
195
+ ATT_INSUFFICIENT_ENCRYPTION_KEY_SIZE_ERROR = ErrorCode.INSUFFICIENT_ENCRYPTION_KEY_SIZE
196
+ ATT_INVALID_ATTRIBUTE_LENGTH_ERROR = ErrorCode.INVALID_ATTRIBUTE_LENGTH
197
+ ATT_UNLIKELY_ERROR_ERROR = ErrorCode.UNLIKELY_ERROR
198
+ ATT_INSUFFICIENT_ENCRYPTION_ERROR = ErrorCode.INSUFFICIENT_ENCRYPTION
199
+ ATT_UNSUPPORTED_GROUP_TYPE_ERROR = ErrorCode.UNSUPPORTED_GROUP_TYPE
200
+ ATT_INSUFFICIENT_RESOURCES_ERROR = ErrorCode.INSUFFICIENT_RESOURCES
185
201
 
186
202
  ATT_DEFAULT_MTU = 23
187
203
 
@@ -245,9 +261,9 @@ class ATT_PDU:
245
261
  def pdu_name(op_code):
246
262
  return name_or_number(ATT_PDU_NAMES, op_code, 2)
247
263
 
248
- @staticmethod
249
- def error_name(error_code):
250
- return name_or_number(ATT_ERROR_NAMES, error_code, 2)
264
+ @classmethod
265
+ def error_name(cls, error_code: int) -> str:
266
+ return ErrorCode(error_code).name
251
267
 
252
268
  @staticmethod
253
269
  def subclass(fields):