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/_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.224'
32
+ __version_tuple__ = version_tuple = (0, 0, 224)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -27,23 +27,17 @@ from bumble.core import name_or_number
27
27
  from bumble.hci import (
28
28
  HCI_LE_READ_BUFFER_SIZE_COMMAND,
29
29
  HCI_LE_READ_BUFFER_SIZE_V2_COMMAND,
30
- HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND,
31
30
  HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND,
32
- HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND,
31
+ HCI_LE_READ_MINIMUM_SUPPORTED_CONNECTION_INTERVAL_COMMAND,
33
32
  HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
34
33
  HCI_READ_BD_ADDR_COMMAND,
35
34
  HCI_READ_BUFFER_SIZE_COMMAND,
36
35
  HCI_READ_LOCAL_NAME_COMMAND,
37
- HCI_SUCCESS,
38
- CodecID,
39
36
  HCI_Command,
40
- HCI_Command_Complete_Event,
41
- HCI_Command_Status_Event,
42
37
  HCI_LE_Read_Buffer_Size_Command,
43
38
  HCI_LE_Read_Buffer_Size_V2_Command,
44
- HCI_LE_Read_Maximum_Advertising_Data_Length_Command,
45
39
  HCI_LE_Read_Maximum_Data_Length_Command,
46
- HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command,
40
+ HCI_LE_Read_Minimum_Supported_Connection_Interval_Command,
47
41
  HCI_LE_Read_Suggested_Default_Data_Length_Command,
48
42
  HCI_Read_BD_ADDR_Command,
49
43
  HCI_Read_Buffer_Size_Command,
@@ -59,85 +53,81 @@ from bumble.host import Host
59
53
  from bumble.transport import open_transport
60
54
 
61
55
 
62
- # -----------------------------------------------------------------------------
63
- def command_succeeded(response):
64
- if isinstance(response, HCI_Command_Status_Event):
65
- return response.status == HCI_SUCCESS
66
- if isinstance(response, HCI_Command_Complete_Event):
67
- return response.return_parameters.status == HCI_SUCCESS
68
- return False
69
-
70
-
71
56
  # -----------------------------------------------------------------------------
72
57
  async def get_classic_info(host: Host) -> None:
73
58
  if host.supports_command(HCI_READ_BD_ADDR_COMMAND):
74
- response = await host.send_command(HCI_Read_BD_ADDR_Command())
75
- if command_succeeded(response):
76
- print()
77
- print(
78
- color('Public Address:', 'yellow'),
79
- response.return_parameters.bd_addr.to_string(False),
80
- )
59
+ response1 = await host.send_sync_command(HCI_Read_BD_ADDR_Command())
60
+ print()
61
+ print(
62
+ color('Public Address:', 'yellow'),
63
+ response1.bd_addr.to_string(False),
64
+ )
81
65
 
82
66
  if host.supports_command(HCI_READ_LOCAL_NAME_COMMAND):
83
- response = await host.send_command(HCI_Read_Local_Name_Command())
84
- if command_succeeded(response):
85
- print()
86
- print(
87
- color('Local Name:', 'yellow'),
88
- map_null_terminated_utf8_string(response.return_parameters.local_name),
89
- )
67
+ response2 = await host.send_sync_command(HCI_Read_Local_Name_Command())
68
+ print()
69
+ print(
70
+ color('Local Name:', 'yellow'),
71
+ map_null_terminated_utf8_string(response2.local_name),
72
+ )
90
73
 
91
74
 
92
75
  # -----------------------------------------------------------------------------
93
76
  async def get_le_info(host: Host) -> None:
94
77
  print()
95
78
 
96
- if host.supports_command(HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND):
97
- response = await host.send_command(
98
- HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command()
99
- )
100
- if command_succeeded(response):
101
- print(
102
- color('LE Number Of Supported Advertising Sets:', 'yellow'),
103
- response.return_parameters.num_supported_advertising_sets,
104
- '\n',
105
- )
79
+ print(
80
+ color('LE Number Of Supported Advertising Sets:', 'yellow'),
81
+ host.number_of_supported_advertising_sets,
82
+ '\n',
83
+ )
106
84
 
107
- if host.supports_command(HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND):
108
- response = await host.send_command(
109
- HCI_LE_Read_Maximum_Advertising_Data_Length_Command()
110
- )
111
- if command_succeeded(response):
112
- print(
113
- color('LE Maximum Advertising Data Length:', 'yellow'),
114
- response.return_parameters.max_advertising_data_length,
115
- '\n',
116
- )
85
+ print(
86
+ color('LE Maximum Advertising Data Length:', 'yellow'),
87
+ host.maximum_advertising_data_length,
88
+ '\n',
89
+ )
117
90
 
118
91
  if host.supports_command(HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND):
119
- response = await host.send_command(HCI_LE_Read_Maximum_Data_Length_Command())
120
- if command_succeeded(response):
121
- print(
122
- color('Maximum Data Length:', 'yellow'),
123
- (
124
- f'tx:{response.return_parameters.supported_max_tx_octets}/'
125
- f'{response.return_parameters.supported_max_tx_time}, '
126
- f'rx:{response.return_parameters.supported_max_rx_octets}/'
127
- f'{response.return_parameters.supported_max_rx_time}'
128
- ),
129
- '\n',
130
- )
92
+ response1 = await host.send_sync_command(
93
+ HCI_LE_Read_Maximum_Data_Length_Command()
94
+ )
95
+ print(
96
+ color('LE Maximum Data Length:', 'yellow'),
97
+ (
98
+ f'tx:{response1.supported_max_tx_octets}/'
99
+ f'{response1.supported_max_tx_time}, '
100
+ f'rx:{response1.supported_max_rx_octets}/'
101
+ f'{response1.supported_max_rx_time}'
102
+ ),
103
+ )
131
104
 
132
105
  if host.supports_command(HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND):
133
- response = await host.send_command(
106
+ response2 = await host.send_sync_command(
134
107
  HCI_LE_Read_Suggested_Default_Data_Length_Command()
135
108
  )
136
- if command_succeeded(response):
109
+ print(
110
+ color('LE Suggested Default Data Length:', 'yellow'),
111
+ f'{response2.suggested_max_tx_octets}/'
112
+ f'{response2.suggested_max_tx_time}',
113
+ '\n',
114
+ )
115
+
116
+ if host.supports_command(HCI_LE_READ_MINIMUM_SUPPORTED_CONNECTION_INTERVAL_COMMAND):
117
+ response3 = await host.send_sync_command(
118
+ HCI_LE_Read_Minimum_Supported_Connection_Interval_Command()
119
+ )
120
+ print(
121
+ color('LE Minimum Supported Connection Interval:', 'yellow'),
122
+ f'{response3.minimum_supported_connection_interval * 125} µs',
123
+ )
124
+ for group in range(len(response3.group_min)):
137
125
  print(
138
- color('Suggested Default Data Length:', 'yellow'),
139
- f'{response.return_parameters.suggested_max_tx_octets}/'
140
- f'{response.return_parameters.suggested_max_tx_time}',
126
+ f' Group {group}: '
127
+ f'{response3.group_min[group] * 125} µs to '
128
+ f'{response3.group_max[group] * 125} µs '
129
+ 'by increments of '
130
+ f'{response3.group_stride[group] * 125} µs',
141
131
  '\n',
142
132
  )
143
133
 
@@ -151,37 +141,31 @@ async def get_flow_control_info(host: Host) -> None:
151
141
  print()
152
142
 
153
143
  if host.supports_command(HCI_READ_BUFFER_SIZE_COMMAND):
154
- response = await host.send_command(
155
- HCI_Read_Buffer_Size_Command(), check_result=True
156
- )
144
+ response1 = await host.send_sync_command(HCI_Read_Buffer_Size_Command())
157
145
  print(
158
146
  color('ACL Flow Control:', 'yellow'),
159
- f'{response.return_parameters.hc_total_num_acl_data_packets} '
160
- f'packets of size {response.return_parameters.hc_acl_data_packet_length}',
147
+ f'{response1.hc_total_num_acl_data_packets} '
148
+ f'packets of size {response1.hc_acl_data_packet_length}',
161
149
  )
162
150
 
163
151
  if host.supports_command(HCI_LE_READ_BUFFER_SIZE_V2_COMMAND):
164
- response = await host.send_command(
165
- HCI_LE_Read_Buffer_Size_V2_Command(), check_result=True
166
- )
152
+ response2 = await host.send_sync_command(HCI_LE_Read_Buffer_Size_V2_Command())
167
153
  print(
168
154
  color('LE ACL Flow Control:', 'yellow'),
169
- f'{response.return_parameters.total_num_le_acl_data_packets} '
170
- f'packets of size {response.return_parameters.le_acl_data_packet_length}',
155
+ f'{response2.total_num_le_acl_data_packets} '
156
+ f'packets of size {response2.le_acl_data_packet_length}',
171
157
  )
172
158
  print(
173
159
  color('LE ISO Flow Control:', 'yellow'),
174
- f'{response.return_parameters.total_num_iso_data_packets} '
175
- f'packets of size {response.return_parameters.iso_data_packet_length}',
160
+ f'{response2.total_num_iso_data_packets} '
161
+ f'packets of size {response2.iso_data_packet_length}',
176
162
  )
177
163
  elif host.supports_command(HCI_LE_READ_BUFFER_SIZE_COMMAND):
178
- response = await host.send_command(
179
- HCI_LE_Read_Buffer_Size_Command(), check_result=True
180
- )
164
+ response3 = await host.send_sync_command(HCI_LE_Read_Buffer_Size_Command())
181
165
  print(
182
166
  color('LE ACL Flow Control:', 'yellow'),
183
- f'{response.return_parameters.total_num_le_acl_data_packets} '
184
- f'packets of size {response.return_parameters.le_acl_data_packet_length}',
167
+ f'{response3.total_num_le_acl_data_packets} '
168
+ f'packets of size {response3.le_acl_data_packet_length}',
185
169
  )
186
170
 
187
171
 
@@ -190,52 +174,44 @@ async def get_codecs_info(host: Host) -> None:
190
174
  print()
191
175
 
192
176
  if host.supports_command(HCI_Read_Local_Supported_Codecs_V2_Command.op_code):
193
- response = await host.send_command(
194
- HCI_Read_Local_Supported_Codecs_V2_Command(), check_result=True
177
+ response1 = await host.send_sync_command(
178
+ HCI_Read_Local_Supported_Codecs_V2_Command()
195
179
  )
196
180
  print(color('Codecs:', 'yellow'))
197
181
 
198
182
  for codec_id, transport in zip(
199
- response.return_parameters.standard_codec_ids,
200
- response.return_parameters.standard_codec_transports,
183
+ response1.standard_codec_ids,
184
+ response1.standard_codec_transports,
201
185
  ):
202
- transport_name = HCI_Read_Local_Supported_Codecs_V2_Command.Transport(
203
- transport
204
- ).name
205
- codec_name = CodecID(codec_id).name
206
- print(f' {codec_name} - {transport_name}')
186
+ print(f' {codec_id.name} - {transport.name}')
207
187
 
208
- for codec_id, transport in zip(
209
- response.return_parameters.vendor_specific_codec_ids,
210
- response.return_parameters.vendor_specific_codec_transports,
188
+ for vendor_codec_id, vendor_transport in zip(
189
+ response1.vendor_specific_codec_ids,
190
+ response1.vendor_specific_codec_transports,
211
191
  ):
212
- transport_name = HCI_Read_Local_Supported_Codecs_V2_Command.Transport(
213
- transport
214
- ).name
215
- company = name_or_number(COMPANY_IDENTIFIERS, codec_id >> 16)
216
- print(f' {company} / {codec_id & 0xFFFF} - {transport_name}')
192
+ company = name_or_number(COMPANY_IDENTIFIERS, vendor_codec_id >> 16)
193
+ print(f' {company} / {vendor_codec_id & 0xFFFF} - {vendor_transport.name}')
217
194
 
218
- if not response.return_parameters.standard_codec_ids:
195
+ if not response1.standard_codec_ids:
219
196
  print(' No standard codecs')
220
- if not response.return_parameters.vendor_specific_codec_ids:
197
+ if not response1.vendor_specific_codec_ids:
221
198
  print(' No Vendor-specific codecs')
222
199
 
223
200
  if host.supports_command(HCI_Read_Local_Supported_Codecs_Command.op_code):
224
- response = await host.send_command(
225
- HCI_Read_Local_Supported_Codecs_Command(), check_result=True
201
+ response2 = await host.send_sync_command(
202
+ HCI_Read_Local_Supported_Codecs_Command()
226
203
  )
227
204
  print(color('Codecs (BR/EDR):', 'yellow'))
228
- for codec_id in response.return_parameters.standard_codec_ids:
229
- codec_name = CodecID(codec_id).name
230
- print(f' {codec_name}')
205
+ for codec_id in response2.standard_codec_ids:
206
+ print(f' {codec_id.name}')
231
207
 
232
- for codec_id in response.return_parameters.vendor_specific_codec_ids:
233
- company = name_or_number(COMPANY_IDENTIFIERS, codec_id >> 16)
234
- print(f' {company} / {codec_id & 0xFFFF}')
208
+ for vendor_codec_id in response2.vendor_specific_codec_ids:
209
+ company = name_or_number(COMPANY_IDENTIFIERS, vendor_codec_id >> 16)
210
+ print(f' {company} / {vendor_codec_id & 0xFFFF}')
235
211
 
236
- if not response.return_parameters.standard_codec_ids:
212
+ if not response2.standard_codec_ids:
237
213
  print(' No standard codecs')
238
- if not response.return_parameters.vendor_specific_codec_ids:
214
+ if not response2.vendor_specific_codec_ids:
239
215
  print(' No Vendor-specific codecs')
240
216
 
241
217
 
@@ -85,7 +85,7 @@ class Loopback:
85
85
  print(color('@@@ Received last packet', 'green'))
86
86
  self.done.set()
87
87
 
88
- async def run(self):
88
+ async def run(self) -> None:
89
89
  """Run a loopback throughput test"""
90
90
  print(color('>>> Connecting to HCI...', 'green'))
91
91
  async with await open_transport(self.transport) as (
@@ -100,11 +100,15 @@ class Loopback:
100
100
  # make sure data can fit in one l2cap pdu
101
101
  l2cap_header_size = 4
102
102
 
103
- max_packet_size = (
103
+ packet_queue = (
104
104
  host.acl_packet_queue
105
105
  if host.acl_packet_queue
106
106
  else host.le_acl_packet_queue
107
- ).max_packet_size - l2cap_header_size
107
+ )
108
+ if packet_queue is None:
109
+ print(color('!!! No packet queue', 'red'))
110
+ return
111
+ max_packet_size = packet_queue.max_packet_size - l2cap_header_size
108
112
  if self.packet_size > max_packet_size:
109
113
  print(
110
114
  color(
@@ -128,20 +132,18 @@ class Loopback:
128
132
  loopback_mode = LoopbackMode.LOCAL
129
133
 
130
134
  print(color('### Setting loopback mode', 'blue'))
131
- await host.send_command(
135
+ await host.send_sync_command(
132
136
  HCI_Write_Loopback_Mode_Command(loopback_mode=LoopbackMode.LOCAL),
133
- check_result=True,
134
137
  )
135
138
 
136
139
  print(color('### Checking loopback mode', 'blue'))
137
- response = await host.send_command(
138
- HCI_Read_Loopback_Mode_Command(), check_result=True
139
- )
140
- if response.return_parameters.loopback_mode != loopback_mode:
140
+ response = await host.send_sync_command(HCI_Read_Loopback_Mode_Command())
141
+ if response.loopback_mode != loopback_mode:
141
142
  print(color('!!! Loopback mode mismatch', 'red'))
142
143
  return
143
144
 
144
145
  await self.connection_event.wait()
146
+ assert self.connection_handle is not None
145
147
  print(color('### Connected', 'cyan'))
146
148
 
147
149
  print(color('=== Start sending', 'magenta'))
bumble/apps/gg_bridge.py CHANGED
@@ -352,7 +352,7 @@ async def run(
352
352
  await bridge.start()
353
353
 
354
354
  # Wait until the source terminates
355
- await hci_source.wait_for_termination()
355
+ await hci_source.terminated
356
356
 
357
357
 
358
358
  @click.command()
bumble/apps/hci_bridge.py CHANGED
@@ -81,7 +81,9 @@ async def async_main():
81
81
  response = hci.HCI_Command_Complete_Event(
82
82
  num_hci_command_packets=1,
83
83
  command_opcode=hci_packet.op_code,
84
- return_parameters=bytes([hci.HCI_SUCCESS]),
84
+ return_parameters=hci.HCI_StatusReturnParameters(
85
+ status=hci.HCI_ErrorCode.SUCCESS
86
+ ),
85
87
  )
86
88
  # Return a packet with 'respond to sender' set to True
87
89
  return (bytes(response), True)
@@ -268,7 +268,7 @@ async def run(device_config, hci_transport, bridge):
268
268
  await bridge.start(device)
269
269
 
270
270
  # Wait until the transport terminates
271
- await hci_source.wait_for_termination()
271
+ await hci_source.terminated
272
272
 
273
273
 
274
274
  # -----------------------------------------------------------------------------
@@ -421,7 +421,7 @@ async def run(device_config, hci_transport, bridge):
421
421
  await bridge.start(device)
422
422
 
423
423
  # Wait until the transport terminates
424
- await hci_source.wait_for_termination()
424
+ await hci_source.terminated
425
425
  except core.ConnectionError as error:
426
426
  print(color(f"!!! Bluetooth connection failed: {error}", "red"))
427
427
  except Exception as error:
bumble/apps/scan.py CHANGED
@@ -22,7 +22,7 @@ import click
22
22
  import bumble.logging
23
23
  from bumble import data_types
24
24
  from bumble.colors import color
25
- from bumble.device import Advertisement, Device
25
+ from bumble.device import Advertisement, Device, DeviceConfiguration
26
26
  from bumble.hci import HCI_LE_1M_PHY, HCI_LE_CODED_PHY, Address, HCI_Constant
27
27
  from bumble.keys import JsonKeyStore
28
28
  from bumble.smp import AddressResolver
@@ -144,8 +144,14 @@ async def scan(
144
144
  device_config, hci_source, hci_sink
145
145
  )
146
146
  else:
147
- device = Device.with_hci(
148
- 'Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink
147
+ device = Device.from_config_with_hci(
148
+ DeviceConfiguration(
149
+ name='Bumble',
150
+ address=Address('F0:F1:F2:F3:F4:F5'),
151
+ keystore='JsonKeyStore',
152
+ ),
153
+ hci_source,
154
+ hci_sink,
149
155
  )
150
156
 
151
157
  await device.power_on()
@@ -190,7 +196,7 @@ async def scan(
190
196
  scanning_phys=scanning_phys,
191
197
  )
192
198
 
193
- await hci_source.wait_for_termination()
199
+ await hci_source.terminated
194
200
 
195
201
 
196
202
  # -----------------------------------------------------------------------------
@@ -726,7 +726,7 @@ class Speaker:
726
726
  print("Waiting for connection...")
727
727
  await self.advertise()
728
728
 
729
- await hci_source.wait_for_termination()
729
+ await hci_source.terminated
730
730
 
731
731
  for output in self.outputs:
732
732
  await output.stop()
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