bumble 0.0.220__py3-none-any.whl → 0.0.222__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 (102) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +5 -5
  3. bumble/apps/auracast.py +746 -473
  4. bumble/apps/bench.py +4 -5
  5. bumble/apps/console.py +5 -10
  6. bumble/apps/controller_info.py +12 -7
  7. bumble/apps/controller_loopback.py +1 -2
  8. bumble/apps/device_info.py +2 -3
  9. bumble/apps/gatt_dump.py +0 -1
  10. bumble/apps/lea_unicast/app.py +1 -1
  11. bumble/apps/pair.py +49 -46
  12. bumble/apps/pandora_server.py +2 -2
  13. bumble/apps/player/player.py +10 -12
  14. bumble/apps/rfcomm_bridge.py +10 -11
  15. bumble/apps/scan.py +1 -3
  16. bumble/apps/speaker/speaker.py +3 -4
  17. bumble/at.py +4 -5
  18. bumble/att.py +91 -25
  19. bumble/audio/io.py +5 -3
  20. bumble/avc.py +1 -2
  21. bumble/avctp.py +2 -3
  22. bumble/avdtp.py +53 -57
  23. bumble/avrcp.py +25 -27
  24. bumble/codecs.py +15 -15
  25. bumble/colors.py +7 -8
  26. bumble/controller.py +663 -391
  27. bumble/core.py +41 -49
  28. bumble/crypto/__init__.py +2 -1
  29. bumble/crypto/builtin.py +2 -8
  30. bumble/data_types.py +2 -1
  31. bumble/decoder.py +2 -3
  32. bumble/device.py +171 -142
  33. bumble/drivers/__init__.py +3 -2
  34. bumble/drivers/intel.py +6 -8
  35. bumble/drivers/rtk.py +1 -1
  36. bumble/gatt.py +9 -9
  37. bumble/gatt_adapters.py +6 -6
  38. bumble/gatt_client.py +110 -60
  39. bumble/gatt_server.py +209 -139
  40. bumble/hci.py +87 -74
  41. bumble/helpers.py +5 -5
  42. bumble/hfp.py +27 -26
  43. bumble/hid.py +9 -9
  44. bumble/host.py +44 -50
  45. bumble/keys.py +17 -17
  46. bumble/l2cap.py +1070 -218
  47. bumble/link.py +26 -159
  48. bumble/ll.py +200 -0
  49. bumble/pairing.py +14 -15
  50. bumble/pandora/__init__.py +2 -2
  51. bumble/pandora/device.py +6 -4
  52. bumble/pandora/host.py +19 -10
  53. bumble/pandora/l2cap.py +8 -9
  54. bumble/pandora/security.py +18 -16
  55. bumble/pandora/utils.py +4 -4
  56. bumble/profiles/aics.py +6 -8
  57. bumble/profiles/ams.py +3 -5
  58. bumble/profiles/ancs.py +11 -11
  59. bumble/profiles/ascs.py +5 -5
  60. bumble/profiles/asha.py +10 -9
  61. bumble/profiles/bass.py +9 -3
  62. bumble/profiles/battery_service.py +1 -2
  63. bumble/profiles/csip.py +9 -10
  64. bumble/profiles/device_information_service.py +16 -17
  65. bumble/profiles/gap.py +3 -4
  66. bumble/profiles/gatt_service.py +0 -1
  67. bumble/profiles/gmap.py +12 -13
  68. bumble/profiles/hap.py +3 -3
  69. bumble/profiles/heart_rate_service.py +7 -8
  70. bumble/profiles/le_audio.py +1 -1
  71. bumble/profiles/mcp.py +28 -28
  72. bumble/profiles/pacs.py +13 -17
  73. bumble/profiles/pbp.py +16 -0
  74. bumble/profiles/vcs.py +2 -2
  75. bumble/profiles/vocs.py +6 -9
  76. bumble/rfcomm.py +19 -18
  77. bumble/sdp.py +12 -11
  78. bumble/smp.py +20 -30
  79. bumble/snoop.py +2 -1
  80. bumble/tools/generate_company_id_list.py +1 -1
  81. bumble/tools/intel_util.py +2 -2
  82. bumble/tools/rtk_fw_download.py +1 -1
  83. bumble/tools/rtk_util.py +1 -1
  84. bumble/transport/__init__.py +1 -2
  85. bumble/transport/android_emulator.py +2 -3
  86. bumble/transport/android_netsim.py +49 -40
  87. bumble/transport/common.py +9 -9
  88. bumble/transport/file.py +1 -2
  89. bumble/transport/hci_socket.py +2 -3
  90. bumble/transport/pty.py +3 -5
  91. bumble/transport/pyusb.py +8 -5
  92. bumble/transport/serial.py +1 -2
  93. bumble/transport/vhci.py +1 -2
  94. bumble/transport/ws_server.py +2 -3
  95. bumble/utils.py +22 -9
  96. bumble/vendor/android/hci.py +4 -2
  97. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/METADATA +3 -2
  98. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/RECORD +102 -101
  99. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/WHEEL +0 -0
  100. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/entry_points.txt +0 -0
  101. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/licenses/LICENSE +0 -0
  102. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/top_level.txt +0 -0
@@ -24,7 +24,8 @@ from __future__ import annotations
24
24
  import logging
25
25
  import pathlib
26
26
  import platform
27
- from typing import TYPE_CHECKING, Iterable, Optional
27
+ from collections.abc import Iterable
28
+ from typing import TYPE_CHECKING
28
29
 
29
30
  from bumble.drivers import intel, rtk
30
31
  from bumble.drivers.common import Driver
@@ -41,7 +42,7 @@ logger = logging.getLogger(__name__)
41
42
  # -----------------------------------------------------------------------------
42
43
  # Functions
43
44
  # -----------------------------------------------------------------------------
44
- async def get_driver_for_host(host: Host) -> Optional[Driver]:
45
+ async def get_driver_for_host(host: Host) -> Driver | None:
45
46
  """Probe diver classes until one returns a valid instance for a host, or none is
46
47
  found.
47
48
  If a "driver" HCI metadata entry is present, only that driver class will be probed.
bumble/drivers/intel.py CHANGED
@@ -29,7 +29,7 @@ import os
29
29
  import pathlib
30
30
  import platform
31
31
  import struct
32
- from typing import TYPE_CHECKING, Any, Optional
32
+ from typing import TYPE_CHECKING, Any
33
33
 
34
34
  from bumble import core, hci, utils
35
35
  from bumble.drivers import common
@@ -353,8 +353,8 @@ class Driver(common.Driver):
353
353
  self.reset_complete = asyncio.Event()
354
354
 
355
355
  # Parse configuration options from the driver name.
356
- self.ddc_addon: Optional[bytes] = None
357
- self.ddc_override: Optional[bytes] = None
356
+ self.ddc_addon: bytes | None = None
357
+ self.ddc_override: bytes | None = None
358
358
  driver = host.hci_metadata.get("driver")
359
359
  if driver is not None and driver.startswith("intel/"):
360
360
  for key, value in [
@@ -380,7 +380,7 @@ class Driver(common.Driver):
380
380
 
381
381
  if (vendor_id, product_id) not in INTEL_USB_PRODUCTS:
382
382
  logger.debug(
383
- f"USB device ({vendor_id:04X}, {product_id:04X}) " "not in known list"
383
+ f"USB device ({vendor_id:04X}, {product_id:04X}) not in known list"
384
384
  )
385
385
  return False
386
386
 
@@ -483,9 +483,7 @@ class Driver(common.Driver):
483
483
  raise DriverError("insufficient device info, missing CNVI or CNVR")
484
484
 
485
485
  firmware_base_name = (
486
- "ibt-"
487
- f"{device_info[ValueType.CNVI]:04X}-"
488
- f"{device_info[ValueType.CNVR]:04X}"
486
+ f"ibt-{device_info[ValueType.CNVI]:04X}-{device_info[ValueType.CNVR]:04X}"
489
487
  )
490
488
  logger.debug(f"FW base name: {firmware_base_name}")
491
489
 
@@ -604,7 +602,7 @@ class Driver(common.Driver):
604
602
 
605
603
  await self.load_ddc_if_any(firmware_base_name)
606
604
 
607
- async def load_ddc_if_any(self, firmware_base_name: Optional[str] = None) -> None:
605
+ async def load_ddc_if_any(self, firmware_base_name: str | None = None) -> None:
608
606
  """
609
607
  Check for and load any Device Data Configuration (DDC) blobs.
610
608
 
bumble/drivers/rtk.py CHANGED
@@ -484,7 +484,7 @@ class Driver(common.Driver):
484
484
 
485
485
  if (vendor_id, product_id) not in RTK_USB_PRODUCTS:
486
486
  logger.debug(
487
- f"USB device ({vendor_id:04X}, {product_id:04X}) " "not in known list"
487
+ f"USB device ({vendor_id:04X}, {product_id:04X}) not in known list"
488
488
  )
489
489
  return False
490
490
 
bumble/gatt.py CHANGED
@@ -28,9 +28,10 @@ import enum
28
28
  import functools
29
29
  import logging
30
30
  import struct
31
- from typing import Iterable, Optional, Sequence, TypeVar, Union
31
+ from collections.abc import Iterable, Sequence
32
+ from typing import TypeVar
32
33
 
33
- from bumble.att import Attribute, AttributeValue
34
+ from bumble.att import Attribute, AttributeValue, AttributeValueV2
34
35
  from bumble.colors import color
35
36
  from bumble.core import UUID, BaseBumbleError
36
37
 
@@ -227,7 +228,6 @@ GATT_MEDIA_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x
227
228
  GATT_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_CHARACTERISTIC = UUID.from_16_bits(0x2BA5, 'Media Control Point Opcodes Supported')
228
229
  GATT_SEARCH_RESULTS_OBJECT_ID_CHARACTERISTIC = UUID.from_16_bits(0x2BA6, 'Search Results Object ID')
229
230
  GATT_SEARCH_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2BA7, 'Search Control Point')
230
- GATT_CONTENT_CONTROL_ID_CHARACTERISTIC = UUID.from_16_bits(0x2BBA, 'Content Control Id')
231
231
 
232
232
  # Telephone Bearer Service (TBS)
233
233
  GATT_BEARER_PROVIDER_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2BB3, 'Bearer Provider Name')
@@ -356,7 +356,7 @@ class Service(Attribute):
356
356
 
357
357
  def __init__(
358
358
  self,
359
- uuid: Union[str, UUID],
359
+ uuid: str | UUID,
360
360
  characteristics: Iterable[Characteristic],
361
361
  primary=True,
362
362
  included_services: Iterable[Service] = (),
@@ -379,7 +379,7 @@ class Service(Attribute):
379
379
  self.characteristics = list(characteristics)
380
380
  self.primary = primary
381
381
 
382
- def get_advertising_data(self) -> Optional[bytes]:
382
+ def get_advertising_data(self) -> bytes | None:
383
383
  """
384
384
  Get Service specific advertising data
385
385
  Defined by each Service, default value is empty
@@ -503,10 +503,10 @@ class Characteristic(Attribute[_T]):
503
503
 
504
504
  def __init__(
505
505
  self,
506
- uuid: Union[str, bytes, UUID],
506
+ uuid: str | bytes | UUID,
507
507
  properties: Characteristic.Properties,
508
- permissions: Union[str, Attribute.Permissions],
509
- value: Union[AttributeValue[_T], _T, None] = None,
508
+ permissions: str | Attribute.Permissions,
509
+ value: AttributeValue[_T] | _T | None = None,
510
510
  descriptors: Sequence[Descriptor] = (),
511
511
  ):
512
512
  super().__init__(uuid, permissions, value)
@@ -579,7 +579,7 @@ class Descriptor(Attribute):
579
579
  def __str__(self) -> str:
580
580
  if isinstance(self.value, bytes):
581
581
  value_str = self.value.hex()
582
- elif isinstance(self.value, CharacteristicValue):
582
+ elif isinstance(self.value, (AttributeValue, AttributeValueV2)):
583
583
  value_str = '<dynamic>'
584
584
  else:
585
585
  value_str = '<...>'
bumble/gatt_adapters.py CHANGED
@@ -22,7 +22,8 @@
22
22
  from __future__ import annotations
23
23
 
24
24
  import struct
25
- from typing import Any, Callable, Generic, Iterable, Literal, Optional, TypeVar
25
+ from collections.abc import Callable, Iterable
26
+ from typing import Any, Generic, Literal, TypeVar
26
27
 
27
28
  from bumble import utils
28
29
  from bumble.core import InvalidOperationError
@@ -74,8 +75,8 @@ class DelegatedCharacteristicAdapter(CharacteristicAdapter[_T]):
74
75
  def __init__(
75
76
  self,
76
77
  characteristic: Characteristic,
77
- encode: Optional[Callable[[_T], bytes]] = None,
78
- decode: Optional[Callable[[bytes], _T]] = None,
78
+ encode: Callable[[_T], bytes] | None = None,
79
+ decode: Callable[[bytes], _T] | None = None,
79
80
  ):
80
81
  super().__init__(characteristic)
81
82
  self.encode = encode
@@ -101,8 +102,8 @@ class DelegatedCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T]):
101
102
  def __init__(
102
103
  self,
103
104
  characteristic_proxy: CharacteristicProxy,
104
- encode: Optional[Callable[[_T], bytes]] = None,
105
- decode: Optional[Callable[[bytes], _T]] = None,
105
+ encode: Callable[[_T], bytes] | None = None,
106
+ decode: Callable[[bytes], _T] | None = None,
106
107
  ):
107
108
  super().__init__(characteristic_proxy)
108
109
  self.encode = encode
@@ -361,5 +362,4 @@ class EnumCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T3]):
361
362
 
362
363
  def decode_value(self, value: bytes) -> _T3:
363
364
  int_value = int.from_bytes(value, self.byteorder)
364
- a = self.cls(int_value)
365
365
  return self.cls(int_value)
bumble/gatt_client.py CHANGED
@@ -26,21 +26,20 @@
26
26
  from __future__ import annotations
27
27
 
28
28
  import asyncio
29
+ import functools
29
30
  import logging
30
31
  import struct
32
+ from collections.abc import Callable, Iterable
31
33
  from datetime import datetime
32
34
  from typing import (
33
35
  TYPE_CHECKING,
34
36
  Any,
35
- Callable,
36
37
  Generic,
37
- Iterable,
38
- Optional,
39
38
  TypeVar,
40
- Union,
39
+ overload,
41
40
  )
42
41
 
43
- from bumble import att, core, utils
42
+ from bumble import att, core, l2cap, utils
44
43
  from bumble.colors import color
45
44
  from bumble.core import UUID, InvalidStateError
46
45
  from bumble.gatt import (
@@ -57,12 +56,12 @@ from bumble.gatt import (
57
56
  )
58
57
  from bumble.hci import HCI_Constant
59
58
 
59
+ if TYPE_CHECKING:
60
+ from bumble import device as device_module
61
+
60
62
  # -----------------------------------------------------------------------------
61
63
  # Typing
62
64
  # -----------------------------------------------------------------------------
63
- if TYPE_CHECKING:
64
- from bumble.device import Connection
65
-
66
65
  _T = TypeVar('_T')
67
66
 
68
67
  # -----------------------------------------------------------------------------
@@ -192,7 +191,7 @@ class CharacteristicProxy(AttributeProxy[_T]):
192
191
  self.descriptors_discovered = False
193
192
  self.subscribers = {} # Map from subscriber to proxy subscriber
194
193
 
195
- def get_descriptor(self, descriptor_type: UUID) -> Optional[DescriptorProxy]:
194
+ def get_descriptor(self, descriptor_type: UUID) -> DescriptorProxy | None:
196
195
  for descriptor in self.descriptors:
197
196
  if descriptor.type == descriptor_type:
198
197
  return descriptor
@@ -204,7 +203,7 @@ class CharacteristicProxy(AttributeProxy[_T]):
204
203
 
205
204
  async def subscribe(
206
205
  self,
207
- subscriber: Optional[Callable[[_T], Any]] = None,
206
+ subscriber: Callable[[_T], Any] | None = None,
208
207
  prefer_notify: bool = True,
209
208
  ) -> None:
210
209
  if subscriber is not None:
@@ -253,7 +252,7 @@ class ProfileServiceProxy:
253
252
  SERVICE_CLASS: type[TemplateService]
254
253
 
255
254
  @classmethod
256
- def from_client(cls, client: Client) -> Optional[ProfileServiceProxy]:
255
+ def from_client(cls, client: Client) -> ProfileServiceProxy | None:
257
256
  return ServiceProxy.from_client(cls, client, cls.SERVICE_CLASS.UUID)
258
257
 
259
258
 
@@ -264,16 +263,14 @@ class Client:
264
263
  services: list[ServiceProxy]
265
264
  cached_values: dict[int, tuple[datetime, bytes]]
266
265
  notification_subscribers: dict[
267
- int, set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
266
+ int, set[CharacteristicProxy | Callable[[bytes], Any]]
268
267
  ]
269
- indication_subscribers: dict[
270
- int, set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
271
- ]
272
- pending_response: Optional[asyncio.futures.Future[att.ATT_PDU]]
273
- pending_request: Optional[att.ATT_PDU]
268
+ indication_subscribers: dict[int, set[CharacteristicProxy | Callable[[bytes], Any]]]
269
+ pending_response: asyncio.futures.Future[att.ATT_PDU] | None
270
+ pending_request: att.ATT_PDU | None
274
271
 
275
- def __init__(self, connection: Connection) -> None:
276
- self.connection = connection
272
+ def __init__(self, bearer: att.Bearer) -> None:
273
+ self.bearer = bearer
277
274
  self.mtu_exchange_done = False
278
275
  self.request_semaphore = asyncio.Semaphore(1)
279
276
  self.pending_request = None
@@ -283,21 +280,78 @@ class Client:
283
280
  self.services = []
284
281
  self.cached_values = {}
285
282
 
286
- connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
283
+ if att.is_enhanced_bearer(bearer):
284
+ bearer.on(bearer.EVENT_CLOSE, self.on_disconnection)
285
+ self._bearer_id = (
286
+ f'[0x{bearer.connection.handle:04X}|CID=0x{bearer.source_cid:04X}]'
287
+ )
288
+ # Fill the mtu.
289
+ bearer.on_att_mtu_update(att.ATT_DEFAULT_MTU)
290
+ self.connection = bearer.connection
291
+ else:
292
+ bearer.on(bearer.EVENT_DISCONNECTION, self.on_disconnection)
293
+ self._bearer_id = f'[0x{bearer.handle:04X}]'
294
+ self.connection = bearer
295
+
296
+ @overload
297
+ @classmethod
298
+ async def connect_eatt(
299
+ cls,
300
+ connection: device_module.Connection,
301
+ spec: l2cap.LeCreditBasedChannelSpec | None = None,
302
+ ) -> Client: ...
303
+
304
+ @overload
305
+ @classmethod
306
+ async def connect_eatt(
307
+ cls,
308
+ connection: device_module.Connection,
309
+ spec: l2cap.LeCreditBasedChannelSpec | None = None,
310
+ count: int = 1,
311
+ ) -> list[Client]: ...
312
+
313
+ @classmethod
314
+ async def connect_eatt(
315
+ cls,
316
+ connection: device_module.Connection,
317
+ spec: l2cap.LeCreditBasedChannelSpec | None = None,
318
+ count: int = 1,
319
+ ) -> list[Client] | Client:
320
+ channels = await connection.device.l2cap_channel_manager.create_enhanced_credit_based_channels(
321
+ connection,
322
+ spec or l2cap.LeCreditBasedChannelSpec(psm=att.EATT_PSM),
323
+ count,
324
+ )
325
+
326
+ def on_pdu(client: Client, pdu: bytes):
327
+ client.on_gatt_pdu(att.ATT_PDU.from_bytes(pdu))
328
+
329
+ clients = [cls(channel) for channel in channels]
330
+ for channel, client in zip(channels, clients):
331
+ channel.sink = functools.partial(on_pdu, client)
332
+ channel.att_mtu = att.ATT_DEFAULT_MTU
333
+ return clients[0] if count == 1 else clients
334
+
335
+ @property
336
+ def mtu(self) -> int:
337
+ return self.bearer.att_mtu
338
+
339
+ @mtu.setter
340
+ def mtu(self, value: int) -> None:
341
+ self.bearer.on_att_mtu_update(value)
287
342
 
288
343
  def send_gatt_pdu(self, pdu: bytes) -> None:
289
- self.connection.send_l2cap_pdu(att.ATT_CID, pdu)
344
+ if att.is_enhanced_bearer(self.bearer):
345
+ self.bearer.write(pdu)
346
+ else:
347
+ self.bearer.send_l2cap_pdu(att.ATT_CID, pdu)
290
348
 
291
349
  async def send_command(self, command: att.ATT_PDU) -> None:
292
- logger.debug(
293
- f'GATT Command from client: [0x{self.connection.handle:04X}] {command}'
294
- )
350
+ logger.debug(f'GATT Command from client: {self._bearer_id} {command}')
295
351
  self.send_gatt_pdu(bytes(command))
296
352
 
297
353
  async def send_request(self, request: att.ATT_PDU):
298
- logger.debug(
299
- f'GATT Request from client: [0x{self.connection.handle:04X}] {request}'
300
- )
354
+ logger.debug(f'GATT Request from client: {self._bearer_id} {request}')
301
355
 
302
356
  # Wait until we can send (only one pending command at a time for the connection)
303
357
  response = None
@@ -326,10 +380,7 @@ class Client:
326
380
  def send_confirmation(
327
381
  self, confirmation: att.ATT_Handle_Value_Confirmation
328
382
  ) -> None:
329
- logger.debug(
330
- f'GATT Confirmation from client: [0x{self.connection.handle:04X}] '
331
- f'{confirmation}'
332
- )
383
+ logger.debug(f'GATT Confirmation from client: {self._bearer_id} {confirmation}')
333
384
  self.send_gatt_pdu(bytes(confirmation))
334
385
 
335
386
  async def request_mtu(self, mtu: int) -> int:
@@ -341,7 +392,7 @@ class Client:
341
392
 
342
393
  # We can only send one request per connection
343
394
  if self.mtu_exchange_done:
344
- return self.connection.att_mtu
395
+ return self.mtu
345
396
 
346
397
  # Send the request
347
398
  self.mtu_exchange_done = True
@@ -352,15 +403,15 @@ class Client:
352
403
  raise att.ATT_Error(error_code=response.error_code, message=response)
353
404
 
354
405
  # Compute the final MTU
355
- self.connection.att_mtu = min(mtu, response.server_rx_mtu)
406
+ self.mtu = min(mtu, response.server_rx_mtu)
356
407
 
357
- return self.connection.att_mtu
408
+ return self.mtu
358
409
 
359
410
  def get_services_by_uuid(self, uuid: UUID) -> list[ServiceProxy]:
360
411
  return [service for service in self.services if service.uuid == uuid]
361
412
 
362
413
  def get_characteristics_by_uuid(
363
- self, uuid: UUID, service: Optional[ServiceProxy] = None
414
+ self, uuid: UUID, service: ServiceProxy | None = None
364
415
  ) -> list[CharacteristicProxy[bytes]]:
365
416
  services = [service] if service else self.services
366
417
  return [
@@ -369,13 +420,14 @@ class Client:
369
420
  if c.uuid == uuid
370
421
  ]
371
422
 
372
- def get_attribute_grouping(self, attribute_handle: int) -> Optional[
373
- Union[
374
- ServiceProxy,
375
- tuple[ServiceProxy, CharacteristicProxy],
376
- tuple[ServiceProxy, CharacteristicProxy, DescriptorProxy],
377
- ]
378
- ]:
423
+ def get_attribute_grouping(
424
+ self, attribute_handle: int
425
+ ) -> (
426
+ ServiceProxy
427
+ | tuple[ServiceProxy, CharacteristicProxy]
428
+ | tuple[ServiceProxy, CharacteristicProxy, DescriptorProxy]
429
+ | None
430
+ ):
379
431
  """
380
432
  Get the attribute(s) associated with an attribute handle
381
433
  """
@@ -478,7 +530,7 @@ class Client:
478
530
 
479
531
  return services
480
532
 
481
- async def discover_service(self, uuid: Union[str, UUID]) -> list[ServiceProxy]:
533
+ async def discover_service(self, uuid: str | UUID) -> list[ServiceProxy]:
482
534
  '''
483
535
  See Vol 3, Part G - 4.4.2 Discover Primary Service by Service UUID
484
536
  '''
@@ -612,7 +664,7 @@ class Client:
612
664
  return included_services
613
665
 
614
666
  async def discover_characteristics(
615
- self, uuids, service: Optional[ServiceProxy]
667
+ self, uuids, service: ServiceProxy | None
616
668
  ) -> list[CharacteristicProxy[bytes]]:
617
669
  '''
618
670
  See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2
@@ -699,9 +751,9 @@ class Client:
699
751
 
700
752
  async def discover_descriptors(
701
753
  self,
702
- characteristic: Optional[CharacteristicProxy] = None,
703
- start_handle: Optional[int] = None,
704
- end_handle: Optional[int] = None,
754
+ characteristic: CharacteristicProxy | None = None,
755
+ start_handle: int | None = None,
756
+ end_handle: int | None = None,
705
757
  ) -> list[DescriptorProxy]:
706
758
  '''
707
759
  See Vol 3, Part G - 4.7.1 Discover All Characteristic Descriptors
@@ -810,7 +862,7 @@ class Client:
810
862
  async def subscribe(
811
863
  self,
812
864
  characteristic: CharacteristicProxy,
813
- subscriber: Optional[Callable[[Any], Any]] = None,
865
+ subscriber: Callable[[Any], Any] | None = None,
814
866
  prefer_notify: bool = True,
815
867
  ) -> None:
816
868
  # If we haven't already discovered the descriptors for this characteristic,
@@ -860,7 +912,7 @@ class Client:
860
912
  async def unsubscribe(
861
913
  self,
862
914
  characteristic: CharacteristicProxy,
863
- subscriber: Optional[Callable[[Any], Any]] = None,
915
+ subscriber: Callable[[Any], Any] | None = None,
864
916
  force: bool = False,
865
917
  ) -> None:
866
918
  '''
@@ -925,7 +977,7 @@ class Client:
925
977
  await self.write_value(cccd, b'\x00\x00', with_response=True)
926
978
 
927
979
  async def read_value(
928
- self, attribute: Union[int, AttributeProxy], no_long_read: bool = False
980
+ self, attribute: int | AttributeProxy, no_long_read: bool = False
929
981
  ) -> bytes:
930
982
  '''
931
983
  See Vol 3, Part G - 4.8.1 Read Characteristic Value
@@ -946,7 +998,7 @@ class Client:
946
998
  # If the value is the max size for the MTU, try to read more unless the caller
947
999
  # specifically asked not to do that
948
1000
  attribute_value = response.attribute_value
949
- if not no_long_read and len(attribute_value) == self.connection.att_mtu - 1:
1001
+ if not no_long_read and len(attribute_value) == self.mtu - 1:
950
1002
  logger.debug('using READ BLOB to get the rest of the value')
951
1003
  offset = len(attribute_value)
952
1004
  while True:
@@ -970,7 +1022,7 @@ class Client:
970
1022
  part = response.part_attribute_value
971
1023
  attribute_value += part
972
1024
 
973
- if len(part) < self.connection.att_mtu - 1:
1025
+ if len(part) < self.mtu - 1:
974
1026
  break
975
1027
 
976
1028
  offset += len(part)
@@ -980,7 +1032,7 @@ class Client:
980
1032
  return attribute_value
981
1033
 
982
1034
  async def read_characteristics_by_uuid(
983
- self, uuid: UUID, service: Optional[ServiceProxy]
1035
+ self, uuid: UUID, service: ServiceProxy | None
984
1036
  ) -> list[bytes]:
985
1037
  '''
986
1038
  See Vol 3, Part G - 4.8.2 Read Using Characteristic UUID
@@ -1038,7 +1090,7 @@ class Client:
1038
1090
 
1039
1091
  async def write_value(
1040
1092
  self,
1041
- attribute: Union[int, AttributeProxy],
1093
+ attribute: int | AttributeProxy,
1042
1094
  value: bytes,
1043
1095
  with_response: bool = False,
1044
1096
  ) -> None:
@@ -1066,14 +1118,13 @@ class Client:
1066
1118
  )
1067
1119
  )
1068
1120
 
1069
- def on_disconnection(self, _) -> None:
1121
+ def on_disconnection(self, *args) -> None:
1122
+ del args # unused.
1070
1123
  if self.pending_response and not self.pending_response.done():
1071
1124
  self.pending_response.cancel()
1072
1125
 
1073
1126
  def on_gatt_pdu(self, att_pdu: att.ATT_PDU) -> None:
1074
- logger.debug(
1075
- f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}'
1076
- )
1127
+ logger.debug(f'GATT Response to client: {self._bearer_id} {att_pdu}')
1077
1128
  if att_pdu.op_code in att.ATT_RESPONSES:
1078
1129
  if self.pending_request is None:
1079
1130
  # Not expected!
@@ -1103,8 +1154,7 @@ class Client:
1103
1154
  else:
1104
1155
  logger.warning(
1105
1156
  color(
1106
- '--- Ignoring GATT Response from '
1107
- f'[0x{self.connection.handle:04X}]: ',
1157
+ '--- Ignoring GATT Response from ' f'{self._bearer_id}: ',
1108
1158
  'red',
1109
1159
  )
1110
1160
  + str(att_pdu)