bumble 0.0.222__py3-none-any.whl → 0.0.223__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.
bumble/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.222'
32
- __version_tuple__ = version_tuple = (0, 0, 222)
31
+ __version__ = version = '0.0.223'
32
+ __version_tuple__ = version_tuple = (0, 0, 223)
33
33
 
34
34
  __commit_id__ = commit_id = None
bumble/apps/usb_probe.py CHANGED
@@ -26,6 +26,8 @@
26
26
  # -----------------------------------------------------------------------------
27
27
  # Imports
28
28
  # -----------------------------------------------------------------------------
29
+ from typing import Any
30
+
29
31
  import click
30
32
  import usb1
31
33
 
@@ -166,13 +168,16 @@ def is_bluetooth_hci(device):
166
168
  # -----------------------------------------------------------------------------
167
169
  @click.command()
168
170
  @click.option('--verbose', is_flag=True, default=False, help='Print more details')
169
- def main(verbose):
171
+ @click.option('--hci-only', is_flag=True, default=False, help='only show HCI device')
172
+ @click.option('--manufacturer', help='filter by manufacturer')
173
+ @click.option('--product', help='filter by product')
174
+ def main(verbose: bool, manufacturer: str, product: str, hci_only: bool):
170
175
  bumble.logging.setup_basic_logging('WARNING')
171
176
 
172
177
  load_libusb()
173
178
  with usb1.USBContext() as context:
174
179
  bluetooth_device_count = 0
175
- devices = {}
180
+ devices: dict[tuple[Any, Any], list[str | None]] = {}
176
181
 
177
182
  for device in context.getDeviceIterator(skip_on_error=True):
178
183
  device_class = device.getDeviceClass()
@@ -234,6 +239,14 @@ def main(verbose):
234
239
  f'{basic_transport_name}/{device_serial_number}'
235
240
  )
236
241
 
242
+ # Filter
243
+ if product and device_product != product:
244
+ continue
245
+ if manufacturer and device_manufacturer != manufacturer:
246
+ continue
247
+ if not is_bluetooth_hci(device) and hci_only:
248
+ continue
249
+
237
250
  # Print the results
238
251
  print(
239
252
  color(
bumble/att.py CHANGED
@@ -29,7 +29,7 @@ import enum
29
29
  import functools
30
30
  import inspect
31
31
  import struct
32
- from collections.abc import Awaitable, Callable
32
+ from collections.abc import Awaitable, Callable, Sequence
33
33
  from typing import (
34
34
  TYPE_CHECKING,
35
35
  ClassVar,
@@ -72,34 +72,36 @@ ATT_PSM = 0x001F
72
72
  EATT_PSM = 0x0027
73
73
 
74
74
  class Opcode(hci.SpecableEnum):
75
- ATT_ERROR_RESPONSE = 0x01
76
- ATT_EXCHANGE_MTU_REQUEST = 0x02
77
- ATT_EXCHANGE_MTU_RESPONSE = 0x03
78
- ATT_FIND_INFORMATION_REQUEST = 0x04
79
- ATT_FIND_INFORMATION_RESPONSE = 0x05
80
- ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
81
- ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
82
- ATT_READ_BY_TYPE_REQUEST = 0x08
83
- ATT_READ_BY_TYPE_RESPONSE = 0x09
84
- ATT_READ_REQUEST = 0x0A
85
- ATT_READ_RESPONSE = 0x0B
86
- ATT_READ_BLOB_REQUEST = 0x0C
87
- ATT_READ_BLOB_RESPONSE = 0x0D
88
- ATT_READ_MULTIPLE_REQUEST = 0x0E
89
- ATT_READ_MULTIPLE_RESPONSE = 0x0F
90
- ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
91
- ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
92
- ATT_WRITE_REQUEST = 0x12
93
- ATT_WRITE_RESPONSE = 0x13
94
- ATT_WRITE_COMMAND = 0x52
95
- ATT_SIGNED_WRITE_COMMAND = 0xD2
96
- ATT_PREPARE_WRITE_REQUEST = 0x16
97
- ATT_PREPARE_WRITE_RESPONSE = 0x17
98
- ATT_EXECUTE_WRITE_REQUEST = 0x18
99
- ATT_EXECUTE_WRITE_RESPONSE = 0x19
100
- ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
101
- ATT_HANDLE_VALUE_INDICATION = 0x1D
102
- ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
75
+ ATT_ERROR_RESPONSE = 0x01
76
+ ATT_EXCHANGE_MTU_REQUEST = 0x02
77
+ ATT_EXCHANGE_MTU_RESPONSE = 0x03
78
+ ATT_FIND_INFORMATION_REQUEST = 0x04
79
+ ATT_FIND_INFORMATION_RESPONSE = 0x05
80
+ ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06
81
+ ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07
82
+ ATT_READ_BY_TYPE_REQUEST = 0x08
83
+ ATT_READ_BY_TYPE_RESPONSE = 0x09
84
+ ATT_READ_REQUEST = 0x0A
85
+ ATT_READ_RESPONSE = 0x0B
86
+ ATT_READ_BLOB_REQUEST = 0x0C
87
+ ATT_READ_BLOB_RESPONSE = 0x0D
88
+ ATT_READ_MULTIPLE_REQUEST = 0x0E
89
+ ATT_READ_MULTIPLE_RESPONSE = 0x0F
90
+ ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10
91
+ ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11
92
+ ATT_READ_MULTIPLE_VARIABLE_REQUEST = 0x20
93
+ ATT_READ_MULTIPLE_VARIABLE_RESPONSE = 0x21
94
+ ATT_WRITE_REQUEST = 0x12
95
+ ATT_WRITE_RESPONSE = 0x13
96
+ ATT_WRITE_COMMAND = 0x52
97
+ ATT_SIGNED_WRITE_COMMAND = 0xD2
98
+ ATT_PREPARE_WRITE_REQUEST = 0x16
99
+ ATT_PREPARE_WRITE_RESPONSE = 0x17
100
+ ATT_EXECUTE_WRITE_REQUEST = 0x18
101
+ ATT_EXECUTE_WRITE_RESPONSE = 0x19
102
+ ATT_HANDLE_VALUE_NOTIFICATION = 0x1B
103
+ ATT_HANDLE_VALUE_INDICATION = 0x1D
104
+ ATT_HANDLE_VALUE_CONFIRMATION = 0x1E
103
105
 
104
106
  ATT_REQUESTS = [
105
107
  Opcode.ATT_EXCHANGE_MTU_REQUEST,
@@ -110,9 +112,10 @@ ATT_REQUESTS = [
110
112
  Opcode.ATT_READ_BLOB_REQUEST,
111
113
  Opcode.ATT_READ_MULTIPLE_REQUEST,
112
114
  Opcode.ATT_READ_BY_GROUP_TYPE_REQUEST,
115
+ Opcode.ATT_READ_MULTIPLE_VARIABLE_REQUEST,
113
116
  Opcode.ATT_WRITE_REQUEST,
114
117
  Opcode.ATT_PREPARE_WRITE_REQUEST,
115
- Opcode.ATT_EXECUTE_WRITE_REQUEST
118
+ Opcode.ATT_EXECUTE_WRITE_REQUEST,
116
119
  ]
117
120
 
118
121
  ATT_RESPONSES = [
@@ -125,9 +128,10 @@ ATT_RESPONSES = [
125
128
  Opcode.ATT_READ_BLOB_RESPONSE,
126
129
  Opcode.ATT_READ_MULTIPLE_RESPONSE,
127
130
  Opcode.ATT_READ_BY_GROUP_TYPE_RESPONSE,
131
+ Opcode.ATT_READ_MULTIPLE_VARIABLE_RESPONSE,
128
132
  Opcode.ATT_WRITE_RESPONSE,
129
133
  Opcode.ATT_PREPARE_WRITE_RESPONSE,
130
- Opcode.ATT_EXECUTE_WRITE_RESPONSE
134
+ Opcode.ATT_EXECUTE_WRITE_RESPONSE,
131
135
  ]
132
136
 
133
137
  class ErrorCode(hci.SpecableEnum):
@@ -185,6 +189,18 @@ ATT_INSUFFICIENT_RESOURCES_ERROR = ErrorCode.INSUFFICIENT_RESOURCES
185
189
  ATT_DEFAULT_MTU = 23
186
190
 
187
191
  HANDLE_FIELD_SPEC = {'size': 2, 'mapper': lambda x: f'0x{x:04X}'}
192
+ _SET_OF_HANDLES_METADATA = hci.metadata({
193
+ 'parser': lambda data, offset: (
194
+ len(data),
195
+ [
196
+ struct.unpack_from('<H', data, i)[0]
197
+ for i in range(offset, len(data), 2)
198
+ ],
199
+ ),
200
+ 'serializer': lambda handles: b''.join(
201
+ [struct.pack('<H', handle) for handle in handles]
202
+ ),
203
+ })
188
204
 
189
205
  # fmt: on
190
206
  # pylint: enable=line-too-long
@@ -554,7 +570,7 @@ class ATT_Read_Multiple_Request(ATT_PDU):
554
570
  See Bluetooth spec @ Vol 3, Part F - 3.4.4.7 Read Multiple Request
555
571
  '''
556
572
 
557
- set_of_handles: bytes = dataclasses.field(metadata=hci.metadata("*"))
573
+ set_of_handles: Sequence[int] = dataclasses.field(metadata=_SET_OF_HANDLES_METADATA)
558
574
 
559
575
 
560
576
  # -----------------------------------------------------------------------------
@@ -635,6 +651,55 @@ class ATT_Read_By_Group_Type_Response(ATT_PDU):
635
651
  return result
636
652
 
637
653
 
654
+ # -----------------------------------------------------------------------------
655
+ @ATT_PDU.subclass
656
+ @dataclasses.dataclass
657
+ class ATT_Read_Multiple_Variable_Request(ATT_PDU):
658
+ '''
659
+ See Bluetooth spec @ Vol 3, Part F - 3.4.4.11 Read Multiple Variable Request
660
+ '''
661
+
662
+ set_of_handles: Sequence[int] = dataclasses.field(metadata=_SET_OF_HANDLES_METADATA)
663
+
664
+
665
+ # -----------------------------------------------------------------------------
666
+ @ATT_PDU.subclass
667
+ @dataclasses.dataclass
668
+ class ATT_Read_Multiple_Variable_Response(ATT_PDU):
669
+ '''
670
+ See Bluetooth spec @ Vol 3, Part F - 3.4.4.12 Read Multiple Variable Response
671
+ '''
672
+
673
+ @classmethod
674
+ def _parse_length_value_tuples(
675
+ cls, data: bytes, offset: int
676
+ ) -> tuple[int, list[tuple[int, bytes]]]:
677
+ length_value_tuple_list: list[tuple[int, bytes]] = []
678
+ while offset < len(data):
679
+ length = struct.unpack_from('<H', data, offset)[0]
680
+ length_value_tuple_list.append(
681
+ (length, data[offset + 2 : offset + 2 + length])
682
+ )
683
+ offset += 2 + length
684
+ return (len(data), length_value_tuple_list)
685
+
686
+ length_value_tuple_list: Sequence[tuple[int, bytes]] = dataclasses.field(
687
+ metadata=hci.metadata(
688
+ {
689
+ 'parser': lambda data, offset: ATT_Read_Multiple_Variable_Response._parse_length_value_tuples(
690
+ data, offset
691
+ ),
692
+ 'serializer': lambda length_value_tuple_list: b''.join(
693
+ [
694
+ struct.pack('<H', length) + value
695
+ for length, value in length_value_tuple_list
696
+ ]
697
+ ),
698
+ }
699
+ )
700
+ )
701
+
702
+
638
703
  # -----------------------------------------------------------------------------
639
704
  @ATT_PDU.subclass
640
705
  @dataclasses.dataclass
bumble/avctp.py CHANGED
@@ -235,7 +235,7 @@ class Protocol:
235
235
  )
236
236
  + payload
237
237
  )
238
- self.l2cap_channel.send_pdu(pdu)
238
+ self.l2cap_channel.write(pdu)
239
239
 
240
240
  def send_command(self, transaction_label: int, pid: int, payload: bytes) -> None:
241
241
  logger.debug(
bumble/avdtp.py CHANGED
@@ -268,7 +268,7 @@ class MediaPacketPump:
268
268
  await self.clock.sleep(delay)
269
269
 
270
270
  # Emit
271
- rtp_channel.send_pdu(bytes(packet))
271
+ rtp_channel.write(bytes(packet))
272
272
  logger.debug(
273
273
  f'{color(">>> sending RTP packet:", "green")} {packet}'
274
274
  )
@@ -1519,7 +1519,7 @@ class Protocol(utils.EventEmitter):
1519
1519
  header = bytes([first_header_byte])
1520
1520
 
1521
1521
  # Send one packet
1522
- self.l2cap_channel.send_pdu(header + payload[:max_fragment_size])
1522
+ self.l2cap_channel.write(header + payload[:max_fragment_size])
1523
1523
 
1524
1524
  # Prepare for the next packet
1525
1525
  payload = payload[max_fragment_size:]
@@ -1829,7 +1829,7 @@ class Stream:
1829
1829
 
1830
1830
  def send_media_packet(self, packet: MediaPacket) -> None:
1831
1831
  assert self.rtp_channel
1832
- self.rtp_channel.send_pdu(bytes(packet))
1832
+ self.rtp_channel.write(bytes(packet))
1833
1833
 
1834
1834
  async def configure(self) -> None:
1835
1835
  if self.state != State.IDLE:
bumble/device.py CHANGED
@@ -1383,10 +1383,7 @@ class Peer:
1383
1383
  def create_service_proxy(
1384
1384
  self, proxy_class: type[_PROXY_CLASS]
1385
1385
  ) -> _PROXY_CLASS | None:
1386
- if proxy := proxy_class.from_client(self.gatt_client):
1387
- return cast(_PROXY_CLASS, proxy)
1388
-
1389
- return None
1386
+ return proxy_class.from_client(self.gatt_client)
1390
1387
 
1391
1388
  async def discover_service_and_create_proxy(
1392
1389
  self, proxy_class: type[_PROXY_CLASS]
@@ -1406,7 +1403,7 @@ class Peer:
1406
1403
  async def request_name(self) -> str:
1407
1404
  return await self.connection.request_remote_name()
1408
1405
 
1409
- async def __aenter__(self):
1406
+ async def __aenter__(self) -> Self:
1410
1407
  await self.discover_services()
1411
1408
  for service in self.services:
1412
1409
  await service.discover_characteristics()
bumble/drivers/rtk.py CHANGED
@@ -77,6 +77,7 @@ class RtlProjectId(enum.IntEnum):
77
77
  PROJECT_ID_8852A = 18
78
78
  PROJECT_ID_8852B = 20
79
79
  PROJECT_ID_8852C = 25
80
+ PROJECT_ID_8761C = 51
80
81
 
81
82
 
82
83
  RTK_PROJECT_ID_TO_ROM = {
@@ -92,6 +93,7 @@ RTK_PROJECT_ID_TO_ROM = {
92
93
  18: RTK_ROM_LMP_8852A,
93
94
  20: RTK_ROM_LMP_8852A,
94
95
  25: RTK_ROM_LMP_8852A,
96
+ 51: RTK_ROM_LMP_8761A,
95
97
  }
96
98
 
97
99
  # List of USB (VendorID, ProductID) for Realtek-based devices.
@@ -123,6 +125,10 @@ RTK_USB_PRODUCTS = {
123
125
  (0x2550, 0x8761),
124
126
  (0x2B89, 0x8761),
125
127
  (0x7392, 0xC611),
128
+ # Realtek 8761CUV
129
+ (0x0B05, 0x1BF6),
130
+ (0x0BDA, 0xC761),
131
+ (0x7392, 0xF611),
126
132
  # Realtek 8821AE
127
133
  (0x0B05, 0x17DC),
128
134
  (0x13D3, 0x3414),
@@ -363,6 +369,15 @@ class Driver(common.Driver):
363
369
  fw_name="rtl8761bu_fw.bin",
364
370
  config_name="rtl8761bu_config.bin",
365
371
  ),
372
+ # 8761CU
373
+ DriverInfo(
374
+ rom=RTK_ROM_LMP_8761A,
375
+ hci=(0x0E, 0x00),
376
+ config_needed=False,
377
+ has_rom_version=True,
378
+ fw_name="rtl8761cu_fw.bin",
379
+ config_name="rtl8761cu_config.bin",
380
+ ),
366
381
  # 8822C
367
382
  DriverInfo(
368
383
  rom=RTK_ROM_LMP_8822B,
@@ -420,9 +435,17 @@ class Driver(common.Driver):
420
435
  @staticmethod
421
436
  def find_driver_info(hci_version, hci_subversion, lmp_subversion):
422
437
  for driver_info in Driver.DRIVER_INFOS:
423
- if driver_info.rom == lmp_subversion and driver_info.hci == (
424
- hci_subversion,
425
- hci_version,
438
+ if driver_info.rom == lmp_subversion and (
439
+ driver_info.hci
440
+ == (
441
+ hci_subversion,
442
+ hci_version,
443
+ )
444
+ or driver_info.hci
445
+ == (
446
+ hci_subversion,
447
+ 0x0,
448
+ )
426
449
  ):
427
450
  return driver_info
428
451
 
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