bumble 0.0.213__py3-none-any.whl → 0.0.214__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/device.py CHANGED
@@ -1752,6 +1752,8 @@ class Connection(utils.CompositeEventEmitter):
1752
1752
  EVENT_CIS_REQUEST = "cis_request"
1753
1753
  EVENT_CIS_ESTABLISHMENT = "cis_establishment"
1754
1754
  EVENT_CIS_ESTABLISHMENT_FAILURE = "cis_establishment_failure"
1755
+ EVENT_LE_SUBRATE_CHANGE = "le_subrate_change"
1756
+ EVENT_LE_SUBRATE_CHANGE_FAILURE = "le_subrate_change_failure"
1755
1757
 
1756
1758
  @utils.composite_listener
1757
1759
  class Listener:
@@ -1787,6 +1789,12 @@ class Connection(utils.CompositeEventEmitter):
1787
1789
  connection_interval: float # Connection interval, in milliseconds. [LE only]
1788
1790
  peripheral_latency: int # Peripheral latency, in number of intervals. [LE only]
1789
1791
  supervision_timeout: float # Supervision timeout, in milliseconds.
1792
+ subrate_factor: int = (
1793
+ 1 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
1794
+ )
1795
+ continuation_number: int = (
1796
+ 0 # See Bluetooth spec Vol 6, Part B - 4.5.1 Connection events
1797
+ )
1790
1798
 
1791
1799
  def __init__(
1792
1800
  self,
@@ -2058,6 +2066,7 @@ class DeviceConfiguration:
2058
2066
  le_simultaneous_enabled: bool = False
2059
2067
  le_privacy_enabled: bool = False
2060
2068
  le_rpa_timeout: int = DEVICE_DEFAULT_LE_RPA_TIMEOUT
2069
+ le_subrate_enabled: bool = False
2061
2070
  classic_enabled: bool = False
2062
2071
  classic_sc_enabled: bool = True
2063
2072
  classic_ssp_enabled: bool = True
@@ -2410,6 +2419,7 @@ class Device(utils.CompositeEventEmitter):
2410
2419
  self.le_privacy_enabled = config.le_privacy_enabled
2411
2420
  self.le_rpa_timeout = config.le_rpa_timeout
2412
2421
  self.le_rpa_periodic_update_task: Optional[asyncio.Task] = None
2422
+ self.le_subrate_enabled = config.le_subrate_enabled
2413
2423
  self.classic_enabled = config.classic_enabled
2414
2424
  self.cis_enabled = config.cis_enabled
2415
2425
  self.classic_sc_enabled = config.classic_sc_enabled
@@ -2789,6 +2799,15 @@ class Device(utils.CompositeEventEmitter):
2789
2799
  check_result=True,
2790
2800
  )
2791
2801
 
2802
+ if self.le_subrate_enabled:
2803
+ await self.send_command(
2804
+ hci.HCI_LE_Set_Host_Feature_Command(
2805
+ bit_number=hci.LeFeature.CONNECTION_SUBRATING_HOST_SUPPORT,
2806
+ bit_value=1,
2807
+ ),
2808
+ check_result=True,
2809
+ )
2810
+
2792
2811
  if self.config.channel_sounding_enabled:
2793
2812
  await self.send_command(
2794
2813
  hci.HCI_LE_Set_Host_Feature_Command(
@@ -6189,11 +6208,23 @@ class Device(utils.CompositeEventEmitter):
6189
6208
  f'{connection.peer_address} as {connection.role_name}, '
6190
6209
  f'{connection_parameters}'
6191
6210
  )
6192
- connection.parameters = Connection.Parameters(
6193
- connection_parameters.connection_interval * 1.25,
6194
- connection_parameters.peripheral_latency,
6195
- connection_parameters.supervision_timeout * 10.0,
6196
- )
6211
+ if (
6212
+ connection.parameters.connection_interval
6213
+ != connection_parameters.connection_interval * 1.25
6214
+ ):
6215
+ connection.parameters = Connection.Parameters(
6216
+ connection_parameters.connection_interval * 1.25,
6217
+ connection_parameters.peripheral_latency,
6218
+ connection_parameters.supervision_timeout * 10.0,
6219
+ )
6220
+ else:
6221
+ connection.parameters = Connection.Parameters(
6222
+ connection_parameters.connection_interval * 1.25,
6223
+ connection_parameters.peripheral_latency,
6224
+ connection_parameters.supervision_timeout * 10.0,
6225
+ connection.parameters.subrate_factor,
6226
+ connection.parameters.continuation_number,
6227
+ )
6197
6228
  connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
6198
6229
 
6199
6230
  @host_event_handler
@@ -6226,6 +6257,25 @@ class Device(utils.CompositeEventEmitter):
6226
6257
  )
6227
6258
  connection.emit(connection.EVENT_CONNECTION_PHY_UPDATE_FAILURE, error)
6228
6259
 
6260
+ @host_event_handler
6261
+ @with_connection_from_handle
6262
+ def on_le_subrate_change(
6263
+ self,
6264
+ connection: Connection,
6265
+ subrate_factor: int,
6266
+ peripheral_latency: int,
6267
+ continuation_number: int,
6268
+ supervision_timeout: int,
6269
+ ):
6270
+ connection.parameters = Connection.Parameters(
6271
+ connection.parameters.connection_interval,
6272
+ peripheral_latency,
6273
+ supervision_timeout * 10.0,
6274
+ subrate_factor,
6275
+ continuation_number,
6276
+ )
6277
+ connection.emit(connection.EVENT_LE_SUBRATE_CHANGE)
6278
+
6229
6279
  @host_event_handler
6230
6280
  @with_connection_from_handle
6231
6281
  def on_connection_att_mtu_update(self, connection, att_mtu):
bumble/hci.py CHANGED
@@ -5315,6 +5315,37 @@ class HCI_LE_Set_Host_Feature_Command(HCI_Command):
5315
5315
  bit_value: int = field(metadata=metadata(1))
5316
5316
 
5317
5317
 
5318
+ # -----------------------------------------------------------------------------
5319
+ @HCI_Command.command
5320
+ @dataclasses.dataclass
5321
+ class HCI_LE_Set_Default_Subrate_Command(HCI_Command):
5322
+ '''
5323
+ See Bluetooth spec @ 7.8.123 LE Set Default Subrate command
5324
+ '''
5325
+
5326
+ subrate_min: int = field(metadata=metadata(2))
5327
+ subrate_max: int = field(metadata=metadata(2))
5328
+ max_latency: int = field(metadata=metadata(2))
5329
+ continuation_number: int = field(metadata=metadata(2))
5330
+ supervision_timeout: int = field(metadata=metadata(2))
5331
+
5332
+
5333
+ # -----------------------------------------------------------------------------
5334
+ @HCI_Command.command
5335
+ @dataclasses.dataclass
5336
+ class HCI_LE_Subrate_Request_Command(HCI_Command):
5337
+ '''
5338
+ See Bluetooth spec @ 7.8.124 LE Subrate Request command
5339
+ '''
5340
+
5341
+ connection_handle: int = field(metadata=metadata(2))
5342
+ subrate_min: int = field(metadata=metadata(2))
5343
+ subrate_max: int = field(metadata=metadata(2))
5344
+ max_latency: int = field(metadata=metadata(2))
5345
+ continuation_number: int = field(metadata=metadata(2))
5346
+ supervision_timeout: int = field(metadata=metadata(2))
5347
+
5348
+
5318
5349
  # -----------------------------------------------------------------------------
5319
5350
  @HCI_Command.command
5320
5351
  @dataclasses.dataclass
@@ -6460,6 +6491,22 @@ class HCI_LE_BIGInfo_Advertising_Report_Event(HCI_LE_Meta_Event):
6460
6491
  encryption: int = field(metadata=metadata(1))
6461
6492
 
6462
6493
 
6494
+ # -----------------------------------------------------------------------------
6495
+ @HCI_LE_Meta_Event.event
6496
+ @dataclasses.dataclass
6497
+ class HCI_LE_Subrate_Change_Event(HCI_LE_Meta_Event):
6498
+ '''
6499
+ See Bluetooth spec @ 7.7.65.35 LE Subrate Change event
6500
+ '''
6501
+
6502
+ status: int = field(metadata=metadata(STATUS_SPEC))
6503
+ connection_handle: int = field(metadata=metadata(2))
6504
+ subrate_factor: int = field(metadata=metadata(2))
6505
+ peripheral_latency: int = field(metadata=metadata(2))
6506
+ continuation_number: int = field(metadata=metadata(2))
6507
+ supervision_timeout: int = field(metadata=metadata(2))
6508
+
6509
+
6463
6510
  # -----------------------------------------------------------------------------
6464
6511
  @HCI_LE_Meta_Event.event
6465
6512
  @dataclasses.dataclass
bumble/host.py CHANGED
@@ -1645,5 +1645,15 @@ class Host(utils.EventEmitter):
1645
1645
  def on_hci_le_cs_subevent_result_continue_event(self, event):
1646
1646
  self.emit('cs_subevent_result_continue', event)
1647
1647
 
1648
+ def on_hci_le_subrate_change_event(self, event: hci.HCI_LE_Subrate_Change_Event):
1649
+ self.emit(
1650
+ 'le_subrate_change',
1651
+ event.connection_handle,
1652
+ event.subrate_factor,
1653
+ event.peripheral_latency,
1654
+ event.continuation_number,
1655
+ event.supervision_timeout,
1656
+ )
1657
+
1648
1658
  def on_hci_vendor_event(self, event):
1649
1659
  self.emit('vendor_event', event)
bumble/l2cap.py CHANGED
@@ -213,7 +213,7 @@ class L2CAP_Control_Frame:
213
213
  fields: ClassVar[hci.Fields] = ()
214
214
  code: int = dataclasses.field(default=0, init=False)
215
215
  name: str = dataclasses.field(default='', init=False)
216
- _data: Optional[bytes] = dataclasses.field(default=None, init=False)
216
+ _payload: Optional[bytes] = dataclasses.field(default=None, init=False)
217
217
 
218
218
  identifier: int
219
219
 
@@ -223,7 +223,8 @@ class L2CAP_Control_Frame:
223
223
 
224
224
  subclass = L2CAP_Control_Frame.classes.get(code)
225
225
  if subclass is None:
226
- instance = L2CAP_Control_Frame(pdu)
226
+ instance = L2CAP_Control_Frame(identifier=identifier)
227
+ instance.payload = pdu[4:]
227
228
  instance.code = CommandCode(code)
228
229
  instance.name = instance.code.name
229
230
  return instance
@@ -232,11 +233,11 @@ class L2CAP_Control_Frame:
232
233
  identifier=identifier,
233
234
  )
234
235
  frame.identifier = identifier
235
- frame.data = pdu[4:]
236
- if length != len(pdu):
236
+ frame.payload = pdu[4:]
237
+ if length != len(frame.payload):
237
238
  logger.warning(
238
239
  color(
239
- f'!!! length mismatch: expected {len(pdu) - 4} but got {length}',
240
+ f'!!! length mismatch: expected {length} but got {len(frame.payload)}',
240
241
  'red',
241
242
  )
242
243
  )
@@ -273,34 +274,20 @@ class L2CAP_Control_Frame:
273
274
 
274
275
  return subclass
275
276
 
276
- def __init__(self, pdu: Optional[bytes] = None, **kwargs) -> None:
277
- self.identifier = kwargs.get('identifier', 0)
278
- if self.fields:
279
- if kwargs:
280
- hci.HCI_Object.init_from_fields(self, self.fields, kwargs)
281
- if pdu is None:
282
- data = hci.HCI_Object.dict_to_bytes(kwargs, self.fields)
283
- pdu = (
284
- bytes([self.code, self.identifier])
285
- + struct.pack('<H', len(data))
286
- + data
287
- )
288
- self.data = pdu[4:] if pdu else b''
289
-
290
277
  @property
291
- def data(self) -> bytes:
292
- if self._data is None:
293
- self._data = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
294
- return self._data
278
+ def payload(self) -> bytes:
279
+ if self._payload is None:
280
+ self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
281
+ return self._payload
295
282
 
296
- @data.setter
297
- def data(self, parameters: bytes) -> None:
298
- self._data = parameters
283
+ @payload.setter
284
+ def payload(self, payload: bytes) -> None:
285
+ self._payload = payload
299
286
 
300
287
  def __bytes__(self) -> bytes:
301
288
  return (
302
- struct.pack('<BBH', self.code, self.identifier, len(self.data) + 4)
303
- + self.data
289
+ struct.pack('<BBH', self.code, self.identifier, len(self.payload))
290
+ + self.payload
304
291
  )
305
292
 
306
293
  def __str__(self) -> str:
@@ -308,8 +295,8 @@ class L2CAP_Control_Frame:
308
295
  if fields := getattr(self, 'fields', None):
309
296
  result += ':\n' + hci.HCI_Object.format_fields(self.__dict__, fields, ' ')
310
297
  else:
311
- if len(self.data) > 1:
312
- result += f': {self.data.hex()}'
298
+ if len(self.payload) > 1:
299
+ result += f': {self.payload.hex()}'
313
300
  return result
314
301
 
315
302
 
bumble/link.py CHANGED
@@ -159,29 +159,29 @@ class LocalLink:
159
159
  asyncio.get_running_loop().call_soon(self.on_connection_complete)
160
160
 
161
161
  def on_disconnection_complete(
162
- self, central_address, peripheral_address, disconnect_command
162
+ self, initiating_address, target_address, disconnect_command
163
163
  ):
164
164
  # Find the controller that initiated the disconnection
165
- if not (central_controller := self.find_controller(central_address)):
165
+ if not (initiating_controller := self.find_controller(initiating_address)):
166
166
  logger.warning('!!! Initiating controller not found')
167
167
  return
168
168
 
169
169
  # Disconnect from the first controller with a matching address
170
- if peripheral_controller := self.find_controller(peripheral_address):
171
- peripheral_controller.on_link_central_disconnected(
172
- central_address, disconnect_command.reason
170
+ if target_controller := self.find_controller(target_address):
171
+ target_controller.on_link_disconnected(
172
+ initiating_address, disconnect_command.reason
173
173
  )
174
174
 
175
- central_controller.on_link_peripheral_disconnection_complete(
175
+ initiating_controller.on_link_disconnection_complete(
176
176
  disconnect_command, HCI_SUCCESS
177
177
  )
178
178
 
179
- def disconnect(self, central_address, peripheral_address, disconnect_command):
179
+ def disconnect(self, initiating_address, target_address, disconnect_command):
180
180
  logger.debug(
181
- f'$$$ DISCONNECTION {central_address} -> '
182
- f'{peripheral_address}: reason = {disconnect_command.reason}'
181
+ f'$$$ DISCONNECTION {initiating_address} -> '
182
+ f'{target_address}: reason = {disconnect_command.reason}'
183
183
  )
184
- args = [central_address, peripheral_address, disconnect_command]
184
+ args = [initiating_address, target_address, disconnect_command]
185
185
  asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
186
186
 
187
187
  # pylint: disable=too-many-arguments
bumble/logging.py ADDED
@@ -0,0 +1,65 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # -----------------------------------------------------------------------------
16
+ # Imports
17
+ # -----------------------------------------------------------------------------
18
+ import functools
19
+ import logging
20
+ import os
21
+
22
+ from bumble import colors
23
+
24
+
25
+ # -----------------------------------------------------------------------------
26
+ class ColorFormatter(logging.Formatter):
27
+ _colorizers = {
28
+ logging.DEBUG: functools.partial(colors.color, fg="white"),
29
+ logging.INFO: functools.partial(colors.color, fg="green"),
30
+ logging.WARNING: functools.partial(colors.color, fg="yellow"),
31
+ logging.ERROR: functools.partial(colors.color, fg="red"),
32
+ logging.CRITICAL: functools.partial(colors.color, fg="black", bg="red"),
33
+ }
34
+
35
+ _formatters = {
36
+ level: logging.Formatter(
37
+ fmt=colorizer("{asctime}.{msecs:03.0f} {levelname:.1} {name}: ")
38
+ + "{message}",
39
+ datefmt="%H:%M:%S",
40
+ style="{",
41
+ )
42
+ for level, colorizer in _colorizers.items()
43
+ }
44
+
45
+ def format(self, record: logging.LogRecord) -> str:
46
+ return self._formatters[record.levelno].format(record)
47
+
48
+
49
+ def setup_basic_logging(default_level: str = "INFO") -> None:
50
+ """
51
+ Set up basic logging with logging.basicConfig, configured with a simple formatter
52
+ that prints out the date and log level in color.
53
+ If the BUMBLE_LOGLEVEL environment variable is set to the name of a log level, it
54
+ is used. Otherwise the default_level argument is used.
55
+
56
+ Args:
57
+ default_level: default logging level
58
+
59
+ """
60
+ handler = logging.StreamHandler()
61
+ handler.setFormatter(ColorFormatter())
62
+ logging.basicConfig(
63
+ level=os.environ.get("BUMBLE_LOGLEVEL", default_level).upper(),
64
+ handlers=[handler],
65
+ )
@@ -49,7 +49,7 @@ _SERVICERS_HOOKS: list[Callable[[PandoraDevice, Config, grpc.aio.Server], None]]
49
49
 
50
50
 
51
51
  def register_servicer_hook(
52
- hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None]
52
+ hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None],
53
53
  ) -> None:
54
54
  _SERVICERS_HOOKS.append(hook)
55
55