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
bumble/drivers/rtk.py CHANGED
@@ -33,6 +33,7 @@ from typing import Tuple
33
33
  import weakref
34
34
 
35
35
 
36
+ from bumble import core
36
37
  from bumble.hci import (
37
38
  hci_vendor_command_op_code,
38
39
  STATUS_SPEC,
@@ -49,6 +50,10 @@ from bumble.drivers import common
49
50
  logger = logging.getLogger(__name__)
50
51
 
51
52
 
53
+ class RtkFirmwareError(core.BaseBumbleError):
54
+ """Error raised when RTK firmware initialization fails."""
55
+
56
+
52
57
  # -----------------------------------------------------------------------------
53
58
  # Constants
54
59
  # -----------------------------------------------------------------------------
@@ -208,15 +213,15 @@ class Firmware:
208
213
  extension_sig = bytes([0x51, 0x04, 0xFD, 0x77])
209
214
 
210
215
  if not firmware.startswith(RTK_EPATCH_SIGNATURE):
211
- raise ValueError("Firmware does not start with epatch signature")
216
+ raise RtkFirmwareError("Firmware does not start with epatch signature")
212
217
 
213
218
  if not firmware.endswith(extension_sig):
214
- raise ValueError("Firmware does not end with extension sig")
219
+ raise RtkFirmwareError("Firmware does not end with extension sig")
215
220
 
216
221
  # The firmware should start with a 14 byte header.
217
222
  epatch_header_size = 14
218
223
  if len(firmware) < epatch_header_size:
219
- raise ValueError("Firmware too short")
224
+ raise RtkFirmwareError("Firmware too short")
220
225
 
221
226
  # Look for the "project ID", starting from the end.
222
227
  offset = len(firmware) - len(extension_sig)
@@ -230,7 +235,7 @@ class Firmware:
230
235
  break
231
236
 
232
237
  if length == 0:
233
- raise ValueError("Invalid 0-length instruction")
238
+ raise RtkFirmwareError("Invalid 0-length instruction")
234
239
 
235
240
  if opcode == 0 and length == 1:
236
241
  project_id = firmware[offset - 1]
@@ -239,7 +244,7 @@ class Firmware:
239
244
  offset -= length
240
245
 
241
246
  if project_id < 0:
242
- raise ValueError("Project ID not found")
247
+ raise RtkFirmwareError("Project ID not found")
243
248
 
244
249
  self.project_id = project_id
245
250
 
@@ -252,7 +257,7 @@ class Firmware:
252
257
  # <PatchLength_1><PatchLength_2>...<PatchLength_N> (16 bits each)
253
258
  # <PatchOffset_1><PatchOffset_2>...<PatchOffset_N> (32 bits each)
254
259
  if epatch_header_size + 8 * num_patches > len(firmware):
255
- raise ValueError("Firmware too short")
260
+ raise RtkFirmwareError("Firmware too short")
256
261
  chip_id_table_offset = epatch_header_size
257
262
  patch_length_table_offset = chip_id_table_offset + 2 * num_patches
258
263
  patch_offset_table_offset = chip_id_table_offset + 4 * num_patches
@@ -266,7 +271,7 @@ class Firmware:
266
271
  "<I", firmware, patch_offset_table_offset + 4 * patch_index
267
272
  )
268
273
  if patch_offset + patch_length > len(firmware):
269
- raise ValueError("Firmware too short")
274
+ raise RtkFirmwareError("Firmware too short")
270
275
 
271
276
  # Get the SVN version for the patch
272
277
  (svn_version,) = struct.unpack_from(
@@ -296,6 +301,8 @@ class Driver(common.Driver):
296
301
  fw_name: str = ""
297
302
  config_name: str = ""
298
303
 
304
+ POST_RESET_DELAY: float = 0.2
305
+
299
306
  DRIVER_INFOS = [
300
307
  # 8723A
301
308
  DriverInfo(
@@ -490,12 +497,24 @@ class Driver(common.Driver):
490
497
 
491
498
  @classmethod
492
499
  async def driver_info_for_host(cls, host):
493
- await host.send_command(HCI_Reset_Command(), check_result=True)
494
- host.ready = True # Needed to let the host know the controller is ready.
500
+ try:
501
+ await host.send_command(
502
+ HCI_Reset_Command(),
503
+ check_result=True,
504
+ response_timeout=cls.POST_RESET_DELAY,
505
+ )
506
+ host.ready = True # Needed to let the host know the controller is ready.
507
+ except asyncio.exceptions.TimeoutError:
508
+ logger.warning("timeout waiting for hci reset, retrying")
509
+ await host.send_command(HCI_Reset_Command(), check_result=True)
510
+ host.ready = True
511
+
512
+ command = HCI_Read_Local_Version_Information_Command()
513
+ response = await host.send_command(command, check_result=True)
514
+ if response.command_opcode != command.op_code:
515
+ logger.error("failed to probe local version information")
516
+ return None
495
517
 
496
- response = await host.send_command(
497
- HCI_Read_Local_Version_Information_Command(), check_result=True
498
- )
499
518
  local_version = response.return_parameters
500
519
 
501
520
  logger.debug(
@@ -645,7 +664,7 @@ class Driver(common.Driver):
645
664
  ):
646
665
  return await self.download_for_rtl8723b()
647
666
 
648
- raise ValueError("ROM not supported")
667
+ raise RtkFirmwareError("ROM not supported")
649
668
 
650
669
  async def init_controller(self):
651
670
  await self.download_firmware()
bumble/gatt.py CHANGED
@@ -39,7 +39,7 @@ from typing import (
39
39
  )
40
40
 
41
41
  from bumble.colors import color
42
- from bumble.core import UUID
42
+ from bumble.core import BaseBumbleError, UUID
43
43
  from bumble.att import Attribute, AttributeValue
44
44
 
45
45
  if TYPE_CHECKING:
@@ -238,22 +238,22 @@ GATT_SEARCH_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x
238
238
  GATT_CONTENT_CONTROL_ID_CHARACTERISTIC = UUID.from_16_bits(0x2BBA, 'Content Control Id')
239
239
 
240
240
  # Telephone Bearer Service (TBS)
241
- GATT_BEARER_PROVIDER_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2BB4, 'Bearer Provider Name')
242
- GATT_BEARER_UCI_CHARACTERISTIC = UUID.from_16_bits(0x2BB5, 'Bearer UCI')
243
- GATT_BEARER_TECHNOLOGY_CHARACTERISTIC = UUID.from_16_bits(0x2BB6, 'Bearer Technology')
244
- GATT_BEARER_URI_SCHEMES_SUPPORTED_LIST_CHARACTERISTIC = UUID.from_16_bits(0x2BB7, 'Bearer URI Schemes Supported List')
245
- GATT_BEARER_SIGNAL_STRENGTH_CHARACTERISTIC = UUID.from_16_bits(0x2BB8, 'Bearer Signal Strength')
246
- GATT_BEARER_SIGNAL_STRENGTH_REPORTING_INTERVAL_CHARACTERISTIC = UUID.from_16_bits(0x2BB9, 'Bearer Signal Strength Reporting Interval')
247
- GATT_BEARER_LIST_CURRENT_CALLS_CHARACTERISTIC = UUID.from_16_bits(0x2BBA, 'Bearer List Current Calls')
248
- GATT_CONTENT_CONTROL_ID_CHARACTERISTIC = UUID.from_16_bits(0x2BBB, 'Content Control ID')
249
- GATT_STATUS_FLAGS_CHARACTERISTIC = UUID.from_16_bits(0x2BBC, 'Status Flags')
250
- GATT_INCOMING_CALL_TARGET_BEARER_URI_CHARACTERISTIC = UUID.from_16_bits(0x2BBD, 'Incoming Call Target Bearer URI')
251
- GATT_CALL_STATE_CHARACTERISTIC = UUID.from_16_bits(0x2BBE, 'Call State')
252
- GATT_CALL_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2BBF, 'Call Control Point')
253
- GATT_CALL_CONTROL_POINT_OPTIONAL_OPCODES_CHARACTERISTIC = UUID.from_16_bits(0x2BC0, 'Call Control Point Optional Opcodes')
254
- GATT_TERMINATION_REASON_CHARACTERISTIC = UUID.from_16_bits(0x2BC1, 'Termination Reason')
255
- GATT_INCOMING_CALL_CHARACTERISTIC = UUID.from_16_bits(0x2BC2, 'Incoming Call')
256
- GATT_CALL_FRIENDLY_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2BC3, 'Call Friendly Name')
241
+ GATT_BEARER_PROVIDER_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2BB3, 'Bearer Provider Name')
242
+ GATT_BEARER_UCI_CHARACTERISTIC = UUID.from_16_bits(0x2BB4, 'Bearer UCI')
243
+ GATT_BEARER_TECHNOLOGY_CHARACTERISTIC = UUID.from_16_bits(0x2BB5, 'Bearer Technology')
244
+ GATT_BEARER_URI_SCHEMES_SUPPORTED_LIST_CHARACTERISTIC = UUID.from_16_bits(0x2BB6, 'Bearer URI Schemes Supported List')
245
+ GATT_BEARER_SIGNAL_STRENGTH_CHARACTERISTIC = UUID.from_16_bits(0x2BB7, 'Bearer Signal Strength')
246
+ GATT_BEARER_SIGNAL_STRENGTH_REPORTING_INTERVAL_CHARACTERISTIC = UUID.from_16_bits(0x2BB8, 'Bearer Signal Strength Reporting Interval')
247
+ GATT_BEARER_LIST_CURRENT_CALLS_CHARACTERISTIC = UUID.from_16_bits(0x2BB9, 'Bearer List Current Calls')
248
+ GATT_CONTENT_CONTROL_ID_CHARACTERISTIC = UUID.from_16_bits(0x2BBA, 'Content Control ID')
249
+ GATT_STATUS_FLAGS_CHARACTERISTIC = UUID.from_16_bits(0x2BBB, 'Status Flags')
250
+ GATT_INCOMING_CALL_TARGET_BEARER_URI_CHARACTERISTIC = UUID.from_16_bits(0x2BBC, 'Incoming Call Target Bearer URI')
251
+ GATT_CALL_STATE_CHARACTERISTIC = UUID.from_16_bits(0x2BBD, 'Call State')
252
+ GATT_CALL_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2BBE, 'Call Control Point')
253
+ GATT_CALL_CONTROL_POINT_OPTIONAL_OPCODES_CHARACTERISTIC = UUID.from_16_bits(0x2BBF, 'Call Control Point Optional Opcodes')
254
+ GATT_TERMINATION_REASON_CHARACTERISTIC = UUID.from_16_bits(0x2BC0, 'Termination Reason')
255
+ GATT_INCOMING_CALL_CHARACTERISTIC = UUID.from_16_bits(0x2BC1, 'Incoming Call')
256
+ GATT_CALL_FRIENDLY_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2BC2, 'Call Friendly Name')
257
257
 
258
258
  # Microphone Control Service (MICS)
259
259
  GATT_MUTE_CHARACTERISTIC = UUID.from_16_bits(0x2BC3, 'Mute')
@@ -275,6 +275,11 @@ GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC = UUID.from_16_bits(0x2BCC, 'Sou
275
275
  GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC = UUID.from_16_bits(0x2BCD, 'Available Audio Contexts')
276
276
  GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC = UUID.from_16_bits(0x2BCE, 'Supported Audio Contexts')
277
277
 
278
+ # Hearing Access Service
279
+ GATT_HEARING_AID_FEATURES_CHARACTERISTIC = UUID.from_16_bits(0x2BDA, 'Hearing Aid Features')
280
+ GATT_HEARING_AID_PRESET_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2BDB, 'Hearing Aid Preset Control Point')
281
+ GATT_ACTIVE_PRESET_INDEX_CHARACTERISTIC = UUID.from_16_bits(0x2BDC, 'Active Preset Index')
282
+
278
283
  # ASHA Service
279
284
  GATT_ASHA_SERVICE = UUID.from_16_bits(0xFDF0, 'Audio Streaming for Hearing Aid')
280
285
  GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC = UUID('6333651e-c481-4a3e-9169-7c902aad37bb', 'ReadOnlyProperties')
@@ -320,6 +325,11 @@ def show_services(services: Iterable[Service]) -> None:
320
325
  print(color(' ' + str(descriptor), 'green'))
321
326
 
322
327
 
328
+ # -----------------------------------------------------------------------------
329
+ class InvalidServiceError(BaseBumbleError):
330
+ """The service is not compliant with the spec/profile"""
331
+
332
+
323
333
  # -----------------------------------------------------------------------------
324
334
  class Service(Attribute):
325
335
  '''
@@ -335,7 +345,7 @@ class Service(Attribute):
335
345
  uuid: Union[str, UUID],
336
346
  characteristics: List[Characteristic],
337
347
  primary=True,
338
- included_services: List[Service] = [],
348
+ included_services: Iterable[Service] = (),
339
349
  ) -> None:
340
350
  # Convert the uuid to a UUID object if it isn't already
341
351
  if isinstance(uuid, str):
@@ -351,7 +361,7 @@ class Service(Attribute):
351
361
  uuid.to_pdu_bytes(),
352
362
  )
353
363
  self.uuid = uuid
354
- self.included_services = included_services[:]
364
+ self.included_services = list(included_services)
355
365
  self.characteristics = characteristics[:]
356
366
  self.primary = primary
357
367
 
@@ -385,7 +395,7 @@ class TemplateService(Service):
385
395
  self,
386
396
  characteristics: List[Characteristic],
387
397
  primary: bool = True,
388
- included_services: List[Service] = [],
398
+ included_services: Iterable[Service] = (),
389
399
  ) -> None:
390
400
  super().__init__(self.UUID, characteristics, primary, included_services)
391
401
 
bumble/gatt_client.py CHANGED
@@ -68,7 +68,7 @@ from .att import (
68
68
  ATT_Error,
69
69
  )
70
70
  from . import core
71
- from .core import UUID, InvalidStateError, ProtocolError
71
+ from .core import UUID, InvalidStateError
72
72
  from .gatt import (
73
73
  GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
74
74
  GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
@@ -253,7 +253,7 @@ class ProfileServiceProxy:
253
253
  SERVICE_CLASS: Type[TemplateService]
254
254
 
255
255
  @classmethod
256
- def from_client(cls, client: Client) -> ProfileServiceProxy:
256
+ def from_client(cls, client: Client) -> Optional[ProfileServiceProxy]:
257
257
  return ServiceProxy.from_client(cls, client, cls.SERVICE_CLASS.UUID)
258
258
 
259
259
 
@@ -283,6 +283,8 @@ class Client:
283
283
  self.services = []
284
284
  self.cached_values = {}
285
285
 
286
+ connection.on('disconnection', self.on_disconnection)
287
+
286
288
  def send_gatt_pdu(self, pdu: bytes) -> None:
287
289
  self.connection.send_l2cap_pdu(ATT_CID, pdu)
288
290
 
@@ -331,9 +333,9 @@ class Client:
331
333
  async def request_mtu(self, mtu: int) -> int:
332
334
  # Check the range
333
335
  if mtu < ATT_DEFAULT_MTU:
334
- raise ValueError(f'MTU must be >= {ATT_DEFAULT_MTU}')
336
+ raise core.InvalidArgumentError(f'MTU must be >= {ATT_DEFAULT_MTU}')
335
337
  if mtu > 0xFFFF:
336
- raise ValueError('MTU must be <= 0xFFFF')
338
+ raise core.InvalidArgumentError('MTU must be <= 0xFFFF')
337
339
 
338
340
  # We can only send one request per connection
339
341
  if self.mtu_exchange_done:
@@ -343,12 +345,7 @@ class Client:
343
345
  self.mtu_exchange_done = True
344
346
  response = await self.send_request(ATT_Exchange_MTU_Request(client_rx_mtu=mtu))
345
347
  if response.op_code == ATT_ERROR_RESPONSE:
346
- raise ProtocolError(
347
- response.error_code,
348
- 'att',
349
- ATT_PDU.error_name(response.error_code),
350
- response,
351
- )
348
+ raise ATT_Error(error_code=response.error_code, message=response)
352
349
 
353
350
  # Compute the final MTU
354
351
  self.connection.att_mtu = min(mtu, response.server_rx_mtu)
@@ -405,7 +402,7 @@ class Client:
405
402
  if not already_known:
406
403
  self.services.append(service)
407
404
 
408
- async def discover_services(self, uuids: Iterable[UUID] = []) -> List[ServiceProxy]:
405
+ async def discover_services(self, uuids: Iterable[UUID] = ()) -> List[ServiceProxy]:
409
406
  '''
410
407
  See Vol 3, Part G - 4.4.1 Discover All Primary Services
411
408
  '''
@@ -934,12 +931,7 @@ class Client:
934
931
  if response is None:
935
932
  raise TimeoutError('read timeout')
936
933
  if response.op_code == ATT_ERROR_RESPONSE:
937
- raise ProtocolError(
938
- response.error_code,
939
- 'att',
940
- ATT_PDU.error_name(response.error_code),
941
- response,
942
- )
934
+ raise ATT_Error(error_code=response.error_code, message=response)
943
935
 
944
936
  # If the value is the max size for the MTU, try to read more unless the caller
945
937
  # specifically asked not to do that
@@ -961,12 +953,7 @@ class Client:
961
953
  ATT_INVALID_OFFSET_ERROR,
962
954
  ):
963
955
  break
964
- raise ProtocolError(
965
- response.error_code,
966
- 'att',
967
- ATT_PDU.error_name(response.error_code),
968
- response,
969
- )
956
+ raise ATT_Error(error_code=response.error_code, message=response)
970
957
 
971
958
  part = response.part_attribute_value
972
959
  attribute_value += part
@@ -1059,12 +1046,7 @@ class Client:
1059
1046
  )
1060
1047
  )
1061
1048
  if response.op_code == ATT_ERROR_RESPONSE:
1062
- raise ProtocolError(
1063
- response.error_code,
1064
- 'att',
1065
- ATT_PDU.error_name(response.error_code),
1066
- response,
1067
- )
1049
+ raise ATT_Error(error_code=response.error_code, message=response)
1068
1050
  else:
1069
1051
  await self.send_command(
1070
1052
  ATT_Write_Command(
@@ -1072,6 +1054,10 @@ class Client:
1072
1054
  )
1073
1055
  )
1074
1056
 
1057
+ def on_disconnection(self, _) -> None:
1058
+ if self.pending_response and not self.pending_response.done():
1059
+ self.pending_response.cancel()
1060
+
1075
1061
  def on_gatt_pdu(self, att_pdu: ATT_PDU) -> None:
1076
1062
  logger.debug(
1077
1063
  f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}'
bumble/gatt_server.py CHANGED
@@ -915,7 +915,7 @@ class Server(EventEmitter):
915
915
  See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request
916
916
  '''
917
917
 
918
- # Check that the attribute exists
918
+ # Check that the attribute exists
919
919
  attribute = self.get_attribute(request.attribute_handle)
920
920
  if attribute is None:
921
921
  self.send_response(
@@ -942,11 +942,19 @@ class Server(EventEmitter):
942
942
  )
943
943
  return
944
944
 
945
- # Accept the value
946
- await attribute.write_value(connection, request.attribute_value)
947
-
948
- # Done
949
- self.send_response(connection, ATT_Write_Response())
945
+ try:
946
+ # Accept the value
947
+ await attribute.write_value(connection, request.attribute_value)
948
+ except ATT_Error as error:
949
+ response = ATT_Error_Response(
950
+ request_opcode_in_error=request.op_code,
951
+ attribute_handle_in_error=request.attribute_handle,
952
+ error_code=error.error_code,
953
+ )
954
+ else:
955
+ # Done
956
+ response = ATT_Write_Response()
957
+ self.send_response(connection, response)
950
958
 
951
959
  @AsyncRunner.run_in_task()
952
960
  async def on_att_write_command(self, connection, request):