bumble 0.0.222__py3-none-any.whl → 0.0.224__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 (43) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/controller_info.py +90 -114
  3. bumble/apps/controller_loopback.py +11 -9
  4. bumble/apps/gg_bridge.py +1 -1
  5. bumble/apps/hci_bridge.py +3 -1
  6. bumble/apps/l2cap_bridge.py +1 -1
  7. bumble/apps/rfcomm_bridge.py +1 -1
  8. bumble/apps/scan.py +10 -4
  9. bumble/apps/speaker/speaker.py +1 -1
  10. bumble/apps/usb_probe.py +15 -2
  11. bumble/att.py +97 -32
  12. bumble/avctp.py +1 -1
  13. bumble/avdtp.py +3 -3
  14. bumble/avrcp.py +366 -190
  15. bumble/bridge.py +10 -2
  16. bumble/controller.py +14 -1
  17. bumble/core.py +1 -1
  18. bumble/device.py +999 -577
  19. bumble/drivers/intel.py +45 -39
  20. bumble/drivers/rtk.py +102 -43
  21. bumble/gatt.py +2 -2
  22. bumble/gatt_client.py +5 -4
  23. bumble/gatt_server.py +100 -1
  24. bumble/hci.py +1367 -844
  25. bumble/hid.py +2 -2
  26. bumble/host.py +339 -157
  27. bumble/l2cap.py +13 -6
  28. bumble/pandora/l2cap.py +1 -1
  29. bumble/profiles/battery_service.py +25 -34
  30. bumble/profiles/heart_rate_service.py +130 -121
  31. bumble/rfcomm.py +1 -1
  32. bumble/sdp.py +2 -2
  33. bumble/smp.py +8 -3
  34. bumble/snoop.py +111 -1
  35. bumble/transport/android_netsim.py +1 -1
  36. bumble/vendor/android/hci.py +108 -86
  37. bumble/vendor/zephyr/hci.py +24 -18
  38. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/METADATA +4 -3
  39. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/RECORD +43 -43
  40. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/WHEEL +1 -1
  41. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/entry_points.txt +0 -0
  42. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/licenses/LICENSE +0 -0
  43. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/top_level.txt +0 -0
bumble/drivers/intel.py CHANGED
@@ -89,52 +89,55 @@ HCI_INTEL_WRITE_BOOT_PARAMS_COMMAND = hci.hci_vendor_command_op_code(0x000E)
89
89
  hci.HCI_Command.register_commands(globals())
90
90
 
91
91
 
92
- @hci.HCI_Command.command
93
92
  @dataclasses.dataclass
94
- class HCI_Intel_Read_Version_Command(hci.HCI_Command):
95
- param0: int = dataclasses.field(metadata=hci.metadata(1))
93
+ class HCI_Intel_Read_Version_ReturnParameters(hci.HCI_StatusReturnParameters):
94
+ tlv: bytes = hci.field(metadata=hci.metadata('*'))
95
+
96
96
 
97
- return_parameters_fields = [
98
- ("status", hci.STATUS_SPEC),
99
- ("tlv", "*"),
100
- ]
97
+ @hci.HCI_SyncCommand.sync_command(HCI_Intel_Read_Version_ReturnParameters)
98
+ @dataclasses.dataclass
99
+ class HCI_Intel_Read_Version_Command(
100
+ hci.HCI_SyncCommand[HCI_Intel_Read_Version_ReturnParameters]
101
+ ):
102
+ param0: int = dataclasses.field(metadata=hci.metadata(1))
101
103
 
102
104
 
103
- @hci.HCI_Command.command
105
+ @hci.HCI_SyncCommand.sync_command(hci.HCI_StatusReturnParameters)
104
106
  @dataclasses.dataclass
105
- class Hci_Intel_Secure_Send_Command(hci.HCI_Command):
107
+ class Hci_Intel_Secure_Send_Command(
108
+ hci.HCI_SyncCommand[hci.HCI_StatusReturnParameters]
109
+ ):
106
110
  data_type: int = dataclasses.field(metadata=hci.metadata(1))
107
111
  data: bytes = dataclasses.field(metadata=hci.metadata("*"))
108
112
 
109
- return_parameters_fields = [
110
- ("status", 1),
111
- ]
113
+
114
+ @dataclasses.dataclass
115
+ class HCI_Intel_Reset_ReturnParameters(hci.HCI_ReturnParameters):
116
+ data: bytes = hci.field(metadata=hci.metadata('*'))
112
117
 
113
118
 
114
- @hci.HCI_Command.command
119
+ @hci.HCI_SyncCommand.sync_command(HCI_Intel_Reset_ReturnParameters)
115
120
  @dataclasses.dataclass
116
- class HCI_Intel_Reset_Command(hci.HCI_Command):
121
+ class HCI_Intel_Reset_Command(hci.HCI_SyncCommand[HCI_Intel_Reset_ReturnParameters]):
117
122
  reset_type: int = dataclasses.field(metadata=hci.metadata(1))
118
123
  patch_enable: int = dataclasses.field(metadata=hci.metadata(1))
119
124
  ddc_reload: int = dataclasses.field(metadata=hci.metadata(1))
120
125
  boot_option: int = dataclasses.field(metadata=hci.metadata(1))
121
126
  boot_address: int = dataclasses.field(metadata=hci.metadata(4))
122
127
 
123
- return_parameters_fields = [
124
- ("data", "*"),
125
- ]
128
+
129
+ @dataclasses.dataclass
130
+ class HCI_Intel_Write_Device_Config_ReturnParameters(hci.HCI_StatusReturnParameters):
131
+ params: bytes = hci.field(metadata=hci.metadata('*'))
126
132
 
127
133
 
128
- @hci.HCI_Command.command
134
+ @hci.HCI_SyncCommand.sync_command(HCI_Intel_Write_Device_Config_ReturnParameters)
129
135
  @dataclasses.dataclass
130
- class Hci_Intel_Write_Device_Config_Command(hci.HCI_Command):
136
+ class HCI_Intel_Write_Device_Config_Command(
137
+ hci.HCI_SyncCommand[HCI_Intel_Write_Device_Config_ReturnParameters]
138
+ ):
131
139
  data: bytes = dataclasses.field(metadata=hci.metadata("*"))
132
140
 
133
- return_parameters_fields = [
134
- ("status", hci.STATUS_SPEC),
135
- ("params", "*"),
136
- ]
137
-
138
141
 
139
142
  # -----------------------------------------------------------------------------
140
143
  # Functions
@@ -402,7 +405,7 @@ class Driver(common.Driver):
402
405
  self.host.on_hci_event_packet(event)
403
406
  return
404
407
 
405
- if not event.return_parameters == hci.HCI_SUCCESS:
408
+ if not event.return_parameters.status == hci.HCI_SUCCESS:
406
409
  raise DriverError("HCI_Command_Complete_Event error")
407
410
 
408
411
  if self.max_in_flight_firmware_load_commands != event.num_hci_command_packets:
@@ -641,8 +644,8 @@ class Driver(common.Driver):
641
644
  while ddc_data:
642
645
  ddc_len = 1 + ddc_data[0]
643
646
  ddc_payload = ddc_data[:ddc_len]
644
- await self.host.send_command(
645
- Hci_Intel_Write_Device_Config_Command(data=ddc_payload)
647
+ await self.host.send_sync_command(
648
+ HCI_Intel_Write_Device_Config_Command(data=ddc_payload)
646
649
  )
647
650
  ddc_data = ddc_data[ddc_len:]
648
651
 
@@ -660,31 +663,34 @@ class Driver(common.Driver):
660
663
 
661
664
  async def read_device_info(self) -> dict[ValueType, Any]:
662
665
  self.host.ready = True
663
- response = await self.host.send_command(hci.HCI_Reset_Command())
664
- if not (
665
- isinstance(response, hci.HCI_Command_Complete_Event)
666
- and response.return_parameters
667
- in (hci.HCI_UNKNOWN_HCI_COMMAND_ERROR, hci.HCI_SUCCESS)
666
+ response1 = await self.host.send_sync_command_raw(hci.HCI_Reset_Command())
667
+ if not isinstance(
668
+ response1.return_parameters, hci.HCI_StatusReturnParameters
669
+ ) or response1.return_parameters.status not in (
670
+ hci.HCI_UNKNOWN_HCI_COMMAND_ERROR,
671
+ hci.HCI_SUCCESS,
668
672
  ):
669
673
  # When the controller is in operational mode, the response is a
670
674
  # successful response.
671
675
  # When the controller is in bootloader mode,
672
676
  # HCI_UNKNOWN_HCI_COMMAND_ERROR is the expected response. Anything
673
677
  # else is a failure.
674
- logger.warning(f"unexpected response: {response}")
678
+ logger.warning(f"unexpected response: {response1}")
675
679
  raise DriverError("unexpected HCI response")
676
680
 
677
681
  # Read the firmware version.
678
- response = await self.host.send_command(
682
+ response2 = await self.host.send_sync_command_raw(
679
683
  HCI_Intel_Read_Version_Command(param0=0xFF)
680
684
  )
681
- if not isinstance(response, hci.HCI_Command_Complete_Event):
682
- raise DriverError("unexpected HCI response")
683
-
684
- if response.return_parameters.status != 0: # type: ignore
685
+ if (
686
+ not isinstance(
687
+ response2.return_parameters, HCI_Intel_Read_Version_ReturnParameters
688
+ )
689
+ or response2.return_parameters.status != 0
690
+ ):
685
691
  raise DriverError("HCI_Intel_Read_Version_Command error")
686
692
 
687
- tlvs = _parse_tlv(response.return_parameters.tlv) # type: ignore
693
+ tlvs = _parse_tlv(response2.return_parameters.tlv) # type: ignore
688
694
 
689
695
  # Convert the list to a dict. That's Ok here because we only expect each type
690
696
  # to appear just once.
bumble/drivers/rtk.py CHANGED
@@ -16,6 +16,7 @@ Support for Realtek USB dongles.
16
16
  Based on various online bits of information, including the Linux kernel.
17
17
  (see `drivers/bluetooth/btrtl.c`)
18
18
  """
19
+ from __future__ import annotations
19
20
 
20
21
  import asyncio
21
22
  import enum
@@ -31,10 +32,14 @@ import weakref
31
32
  # Imports
32
33
  # -----------------------------------------------------------------------------
33
34
  from dataclasses import dataclass, field
35
+ from typing import TYPE_CHECKING
34
36
 
35
37
  from bumble import core, hci
36
38
  from bumble.drivers import common
37
39
 
40
+ if TYPE_CHECKING:
41
+ from bumble.host import Host
42
+
38
43
  # -----------------------------------------------------------------------------
39
44
  # Logging
40
45
  # -----------------------------------------------------------------------------
@@ -77,6 +82,7 @@ class RtlProjectId(enum.IntEnum):
77
82
  PROJECT_ID_8852A = 18
78
83
  PROJECT_ID_8852B = 20
79
84
  PROJECT_ID_8852C = 25
85
+ PROJECT_ID_8761C = 51
80
86
 
81
87
 
82
88
  RTK_PROJECT_ID_TO_ROM = {
@@ -92,6 +98,7 @@ RTK_PROJECT_ID_TO_ROM = {
92
98
  18: RTK_ROM_LMP_8852A,
93
99
  20: RTK_ROM_LMP_8852A,
94
100
  25: RTK_ROM_LMP_8852A,
101
+ 51: RTK_ROM_LMP_8761A,
95
102
  }
96
103
 
97
104
  # List of USB (VendorID, ProductID) for Realtek-based devices.
@@ -122,7 +129,12 @@ RTK_USB_PRODUCTS = {
122
129
  (0x2357, 0x0604),
123
130
  (0x2550, 0x8761),
124
131
  (0x2B89, 0x8761),
132
+ (0x2C0A, 0x8761),
125
133
  (0x7392, 0xC611),
134
+ # Realtek 8761CUV
135
+ (0x0B05, 0x1BF6),
136
+ (0x0BDA, 0xC761),
137
+ (0x7392, 0xF611),
126
138
  # Realtek 8821AE
127
139
  (0x0B05, 0x17DC),
128
140
  (0x13D3, 0x3414),
@@ -182,23 +194,36 @@ HCI_RTK_DROP_FIRMWARE_COMMAND = hci.hci_vendor_command_op_code(0x66)
182
194
  hci.HCI_Command.register_commands(globals())
183
195
 
184
196
 
185
- @hci.HCI_Command.command
186
197
  @dataclass
187
- class HCI_RTK_Read_ROM_Version_Command(hci.HCI_Command):
188
- return_parameters_fields = [("status", hci.STATUS_SPEC), ("version", 1)]
198
+ class HCI_RTK_Read_ROM_Version_ReturnParameters(hci.HCI_StatusReturnParameters):
199
+ version: int = field(metadata=hci.metadata(1))
200
+
201
+
202
+ @hci.HCI_SyncCommand.sync_command(HCI_RTK_Read_ROM_Version_ReturnParameters)
203
+ @dataclass
204
+ class HCI_RTK_Read_ROM_Version_Command(
205
+ hci.HCI_SyncCommand[HCI_RTK_Read_ROM_Version_ReturnParameters]
206
+ ):
207
+ pass
208
+
209
+
210
+ @dataclass
211
+ class HCI_RTK_Download_ReturnParameters(hci.HCI_StatusReturnParameters):
212
+ index: int = field(metadata=hci.metadata(1))
189
213
 
190
214
 
191
- @hci.HCI_Command.command
215
+ @hci.HCI_SyncCommand.sync_command(HCI_RTK_Download_ReturnParameters)
192
216
  @dataclass
193
- class HCI_RTK_Download_Command(hci.HCI_Command):
217
+ class HCI_RTK_Download_Command(hci.HCI_SyncCommand[HCI_RTK_Download_ReturnParameters]):
194
218
  index: int = field(metadata=hci.metadata(1))
195
219
  payload: bytes = field(metadata=hci.metadata(RTK_FRAGMENT_LENGTH))
196
- return_parameters_fields = [("status", hci.STATUS_SPEC), ("index", 1)]
197
220
 
198
221
 
199
- @hci.HCI_Command.command
222
+ @hci.HCI_SyncCommand.sync_command(hci.HCI_GenericReturnParameters)
200
223
  @dataclass
201
- class HCI_RTK_Drop_Firmware_Command(hci.HCI_Command):
224
+ class HCI_RTK_Drop_Firmware_Command(
225
+ hci.HCI_SyncCommand[hci.HCI_GenericReturnParameters]
226
+ ):
202
227
  pass
203
228
 
204
229
 
@@ -363,6 +388,15 @@ class Driver(common.Driver):
363
388
  fw_name="rtl8761bu_fw.bin",
364
389
  config_name="rtl8761bu_config.bin",
365
390
  ),
391
+ # 8761CU
392
+ DriverInfo(
393
+ rom=RTK_ROM_LMP_8761A,
394
+ hci=(0x0E, 0x00),
395
+ config_needed=False,
396
+ has_rom_version=True,
397
+ fw_name="rtl8761cu_fw.bin",
398
+ config_name="rtl8761cu_config.bin",
399
+ ),
366
400
  # 8822C
367
401
  DriverInfo(
368
402
  rom=RTK_ROM_LMP_8822B,
@@ -420,9 +454,17 @@ class Driver(common.Driver):
420
454
  @staticmethod
421
455
  def find_driver_info(hci_version, hci_subversion, lmp_subversion):
422
456
  for driver_info in Driver.DRIVER_INFOS:
423
- if driver_info.rom == lmp_subversion and driver_info.hci == (
424
- hci_subversion,
425
- hci_version,
457
+ if driver_info.rom == lmp_subversion and (
458
+ driver_info.hci
459
+ == (
460
+ hci_subversion,
461
+ hci_version,
462
+ )
463
+ or driver_info.hci
464
+ == (
465
+ hci_subversion,
466
+ 0x0,
467
+ )
426
468
  ):
427
469
  return driver_info
428
470
 
@@ -467,7 +509,7 @@ class Driver(common.Driver):
467
509
  return None
468
510
 
469
511
  @staticmethod
470
- def check(host):
512
+ def check(host: Host) -> bool:
471
513
  if not host.hci_metadata:
472
514
  logger.debug("USB metadata not found")
473
515
  return False
@@ -491,37 +533,44 @@ class Driver(common.Driver):
491
533
  return True
492
534
 
493
535
  @staticmethod
494
- async def get_loaded_firmware_version(host):
495
- response = await host.send_command(HCI_RTK_Read_ROM_Version_Command())
496
-
497
- if response.return_parameters.status != hci.HCI_SUCCESS:
536
+ async def get_loaded_firmware_version(host: Host) -> int | None:
537
+ response1 = await host.send_sync_command_raw(HCI_RTK_Read_ROM_Version_Command())
538
+ if (
539
+ not isinstance(
540
+ response1.return_parameters, HCI_RTK_Read_ROM_Version_ReturnParameters
541
+ )
542
+ or response1.return_parameters.status != hci.HCI_SUCCESS
543
+ ):
498
544
  return None
499
545
 
500
- response = await host.send_command(
501
- hci.HCI_Read_Local_Version_Information_Command(), check_result=True
502
- )
503
- return (
504
- response.return_parameters.hci_subversion << 16
505
- | response.return_parameters.lmp_subversion
546
+ response2 = await host.send_sync_command(
547
+ hci.HCI_Read_Local_Version_Information_Command()
506
548
  )
549
+ return response2.hci_subversion << 16 | response2.lmp_subversion
507
550
 
508
551
  @classmethod
509
- async def driver_info_for_host(cls, host):
552
+ async def driver_info_for_host(cls, host: Host) -> DriverInfo | None:
510
553
  try:
511
- await host.send_command(
554
+ await host.send_sync_command(
512
555
  hci.HCI_Reset_Command(),
513
- check_result=True,
514
556
  response_timeout=cls.POST_RESET_DELAY,
515
557
  )
516
558
  host.ready = True # Needed to let the host know the controller is ready.
517
559
  except asyncio.exceptions.TimeoutError:
518
560
  logger.warning("timeout waiting for hci reset, retrying")
519
- await host.send_command(hci.HCI_Reset_Command(), check_result=True)
561
+ await host.send_sync_command(hci.HCI_Reset_Command())
520
562
  host.ready = True
521
563
 
522
- command = hci.HCI_Read_Local_Version_Information_Command()
523
- response = await host.send_command(command, check_result=True)
524
- if response.command_opcode != command.op_code:
564
+ response = await host.send_sync_command_raw(
565
+ hci.HCI_Read_Local_Version_Information_Command()
566
+ )
567
+ if (
568
+ not isinstance(
569
+ response.return_parameters,
570
+ hci.HCI_Read_Local_Version_Information_ReturnParameters,
571
+ )
572
+ or response.return_parameters.status != hci.HCI_SUCCESS
573
+ ):
525
574
  logger.error("failed to probe local version information")
526
575
  return None
527
576
 
@@ -546,7 +595,7 @@ class Driver(common.Driver):
546
595
  return driver_info
547
596
 
548
597
  @classmethod
549
- async def for_host(cls, host, force=False):
598
+ async def for_host(cls, host: Host, force: bool = False):
550
599
  # Check that a driver is needed for this host
551
600
  if not force and not cls.check(host):
552
601
  return None
@@ -601,15 +650,21 @@ class Driver(common.Driver):
601
650
 
602
651
  # TODO: load the firmware
603
652
 
604
- async def download_for_rtl8723b(self):
653
+ async def download_for_rtl8723b(self) -> int | None:
605
654
  if self.driver_info.has_rom_version:
606
- response = await self.host.send_command(
607
- HCI_RTK_Read_ROM_Version_Command(), check_result=True
655
+ response1 = await self.host.send_sync_command_raw(
656
+ HCI_RTK_Read_ROM_Version_Command()
608
657
  )
609
- if response.return_parameters.status != hci.HCI_SUCCESS:
658
+ if (
659
+ not isinstance(
660
+ response1.return_parameters,
661
+ HCI_RTK_Read_ROM_Version_ReturnParameters,
662
+ )
663
+ or response1.return_parameters.status != hci.HCI_SUCCESS
664
+ ):
610
665
  logger.warning("can't get ROM version")
611
666
  return None
612
- rom_version = response.return_parameters.version
667
+ rom_version = response1.return_parameters.version
613
668
  logger.debug(f"ROM version before download: {rom_version:04X}")
614
669
  else:
615
670
  rom_version = 0
@@ -644,21 +699,25 @@ class Driver(common.Driver):
644
699
  fragment_offset = fragment_index * RTK_FRAGMENT_LENGTH
645
700
  fragment = payload[fragment_offset : fragment_offset + RTK_FRAGMENT_LENGTH]
646
701
  logger.debug(f"downloading fragment {fragment_index}")
647
- await self.host.send_command(
648
- HCI_RTK_Download_Command(index=download_index, payload=fragment),
649
- check_result=True,
702
+ await self.host.send_sync_command(
703
+ HCI_RTK_Download_Command(index=download_index, payload=fragment)
650
704
  )
651
705
 
652
706
  logger.debug("download complete!")
653
707
 
654
708
  # Read the version again
655
- response = await self.host.send_command(
656
- HCI_RTK_Read_ROM_Version_Command(), check_result=True
709
+ response2 = await self.host.send_sync_command_raw(
710
+ HCI_RTK_Read_ROM_Version_Command()
657
711
  )
658
- if response.return_parameters.status != hci.HCI_SUCCESS:
712
+ if (
713
+ not isinstance(
714
+ response2.return_parameters, HCI_RTK_Read_ROM_Version_ReturnParameters
715
+ )
716
+ or response2.return_parameters.status != hci.HCI_SUCCESS
717
+ ):
659
718
  logger.warning("can't get ROM version")
660
719
  else:
661
- rom_version = response.return_parameters.version
720
+ rom_version = response2.return_parameters.version
662
721
  logger.debug(f"ROM version after download: {rom_version:02X}")
663
722
 
664
723
  return firmware.version
@@ -680,7 +739,7 @@ class Driver(common.Driver):
680
739
 
681
740
  async def init_controller(self):
682
741
  await self.download_firmware()
683
- await self.host.send_command(hci.HCI_Reset_Command(), check_result=True)
742
+ await self.host.send_sync_command(hci.HCI_Reset_Command())
684
743
  logger.info(f"loaded FW image {self.driver_info.fw_name}")
685
744
 
686
745
 
bumble/gatt.py CHANGED
@@ -29,7 +29,7 @@ import functools
29
29
  import logging
30
30
  import struct
31
31
  from collections.abc import Iterable, Sequence
32
- from typing import TypeVar
32
+ from typing import ClassVar, TypeVar
33
33
 
34
34
  from bumble.att import Attribute, AttributeValue, AttributeValueV2
35
35
  from bumble.colors import color
@@ -403,7 +403,7 @@ class TemplateService(Service):
403
403
  to expose their UUID as a class property
404
404
  '''
405
405
 
406
- UUID: UUID
406
+ UUID: ClassVar[UUID]
407
407
 
408
408
  def __init__(
409
409
  self,
bumble/gatt_client.py CHANGED
@@ -34,11 +34,14 @@ from datetime import datetime
34
34
  from typing import (
35
35
  TYPE_CHECKING,
36
36
  Any,
37
+ ClassVar,
37
38
  Generic,
38
39
  TypeVar,
39
40
  overload,
40
41
  )
41
42
 
43
+ from typing_extensions import Self
44
+
42
45
  from bumble import att, core, l2cap, utils
43
46
  from bumble.colors import color
44
47
  from bumble.core import UUID, InvalidStateError
@@ -249,10 +252,10 @@ class ProfileServiceProxy:
249
252
  Base class for profile-specific service proxies
250
253
  '''
251
254
 
252
- SERVICE_CLASS: type[TemplateService]
255
+ SERVICE_CLASS: ClassVar[type[TemplateService]]
253
256
 
254
257
  @classmethod
255
- def from_client(cls, client: Client) -> ProfileServiceProxy | None:
258
+ def from_client(cls, client: Client) -> Self | None:
256
259
  return ServiceProxy.from_client(cls, client, cls.SERVICE_CLASS.UUID)
257
260
 
258
261
 
@@ -285,8 +288,6 @@ class Client:
285
288
  self._bearer_id = (
286
289
  f'[0x{bearer.connection.handle:04X}|CID=0x{bearer.source_cid:04X}]'
287
290
  )
288
- # Fill the mtu.
289
- bearer.on_att_mtu_update(att.ATT_DEFAULT_MTU)
290
291
  self.connection = bearer.connection
291
292
  else:
292
293
  bearer.on(bearer.EVENT_DISCONNECTION, self.on_disconnection)
bumble/gatt_server.py CHANGED
@@ -115,7 +115,6 @@ class Server(utils.EventEmitter):
115
115
  channel.connection.handle,
116
116
  channel.source_cid,
117
117
  )
118
- channel.att_mtu = att.ATT_DEFAULT_MTU
119
118
  channel.sink = lambda pdu: self.on_gatt_pdu(
120
119
  channel, att.ATT_PDU.from_bytes(pdu)
121
120
  )
@@ -777,6 +776,18 @@ class Server(utils.EventEmitter):
777
776
  error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
778
777
  )
779
778
 
779
+ if (
780
+ request.starting_handle == 0x0000
781
+ or request.starting_handle > request.ending_handle
782
+ ):
783
+ response = att.ATT_Error_Response(
784
+ request_opcode_in_error=request.op_code,
785
+ attribute_handle_in_error=request.starting_handle,
786
+ error_code=att.ATT_INVALID_HANDLE_ERROR,
787
+ )
788
+ self.send_response(bearer, response)
789
+ return
790
+
780
791
  attributes: list[tuple[int, bytes]] = []
781
792
  for attribute in (
782
793
  attribute
@@ -977,6 +988,94 @@ class Server(utils.EventEmitter):
977
988
 
978
989
  self.send_response(bearer, response)
979
990
 
991
+ @utils.AsyncRunner.run_in_task()
992
+ async def on_att_read_multiple_request(
993
+ self, bearer: att.Bearer, request: att.ATT_Read_Multiple_Request
994
+ ):
995
+ '''
996
+ See Bluetooth spec Vol 3, Part F - 3.4.4.7 Read Multiple Request.
997
+ '''
998
+ response: att.ATT_PDU
999
+
1000
+ pdu_space_available = bearer.att_mtu - 1
1001
+ values: list[bytes] = []
1002
+
1003
+ for handle in request.set_of_handles:
1004
+ if not (attribute := self.get_attribute(handle)):
1005
+ response = att.ATT_Error_Response(
1006
+ request_opcode_in_error=request.op_code,
1007
+ attribute_handle_in_error=handle,
1008
+ error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
1009
+ )
1010
+ self.send_response(bearer, response)
1011
+ return
1012
+ # No need to catch permission errors here, since these attributes
1013
+ # must all be world-readable
1014
+ attribute_value = await attribute.read_value(bearer)
1015
+ # Check the attribute value size
1016
+ max_attribute_size = min(bearer.att_mtu - 1, 251)
1017
+ if len(attribute_value) > max_attribute_size:
1018
+ # We need to truncate
1019
+ attribute_value = attribute_value[:max_attribute_size]
1020
+
1021
+ # Check if there is enough space
1022
+ entry_size = len(attribute_value)
1023
+ if pdu_space_available < entry_size:
1024
+ break
1025
+
1026
+ # Add the attribute to the list
1027
+ values.append(attribute_value)
1028
+ pdu_space_available -= entry_size
1029
+
1030
+ response = att.ATT_Read_Multiple_Response(set_of_values=b''.join(values))
1031
+ self.send_response(bearer, response)
1032
+
1033
+ @utils.AsyncRunner.run_in_task()
1034
+ async def on_att_read_multiple_variable_request(
1035
+ self, bearer: att.Bearer, request: att.ATT_Read_Multiple_Variable_Request
1036
+ ):
1037
+ '''
1038
+ See Bluetooth spec Vol 3, Part F - 3.4.4.11 Read Multiple Variable Request.
1039
+ '''
1040
+ response: att.ATT_PDU
1041
+
1042
+ pdu_space_available = bearer.att_mtu - 1
1043
+ length_value_tuple_list: list[tuple[int, bytes]] = []
1044
+
1045
+ for handle in request.set_of_handles:
1046
+ if not (attribute := self.get_attribute(handle)):
1047
+ response = att.ATT_Error_Response(
1048
+ request_opcode_in_error=request.op_code,
1049
+ attribute_handle_in_error=handle,
1050
+ error_code=att.ATT_ATTRIBUTE_NOT_FOUND_ERROR,
1051
+ )
1052
+ self.send_response(bearer, response)
1053
+ return
1054
+ # No need to catch permission errors here, since these attributes
1055
+ # must all be world-readable
1056
+ attribute_value = await attribute.read_value(bearer)
1057
+ length = len(attribute_value)
1058
+ # Check the attribute value size
1059
+ max_attribute_size = min(bearer.att_mtu - 3, 251)
1060
+ if len(attribute_value) > max_attribute_size:
1061
+ # We need to truncate
1062
+ attribute_value = attribute_value[:max_attribute_size]
1063
+
1064
+ # Check if there is enough space
1065
+ entry_size = 2 + len(attribute_value)
1066
+
1067
+ # Add the attribute to the list
1068
+ length_value_tuple_list.append((length, attribute_value))
1069
+ pdu_space_available -= entry_size
1070
+
1071
+ if pdu_space_available <= 0:
1072
+ break
1073
+
1074
+ response = att.ATT_Read_Multiple_Variable_Response(
1075
+ length_value_tuple_list=length_value_tuple_list
1076
+ )
1077
+ self.send_response(bearer, response)
1078
+
980
1079
  @utils.AsyncRunner.run_in_task()
981
1080
  async def on_att_write_request(
982
1081
  self, bearer: att.Bearer, request: att.ATT_Write_Request