use ruff for linting and import sorting

This commit is contained in:
Gilles Boccon-Gibod
2025-12-29 19:28:45 -08:00
parent ad0753b959
commit 4fb501a0ef
59 changed files with 182 additions and 274 deletions

View File

@@ -646,7 +646,6 @@ async def run_assist(
async def run_pair(transport: str, address: str) -> None: async def run_pair(transport: str, address: str) -> None:
async with create_device(transport) as device: async with create_device(transport) as device:
# Connect to the server # Connect to the server
print(f'=== Connecting to {address}...') print(f'=== Connecting to {address}...')
async with device.connect_as_gatt(address) as peer: async with device.connect_as_gatt(address) as peer:

View File

@@ -1096,9 +1096,7 @@ class DeviceListener(Device.Listener, Connection.Listener):
if self.app.connected_peer.connection.is_encrypted if self.app.connected_peer.connection.is_encrypted
else 'not encrypted' else 'not encrypted'
) )
self.app.append_to_output( self.app.append_to_output(f'connection encryption change: {encryption_state}')
'connection encryption change: ' f'{encryption_state}'
)
def on_connection_data_length_change(self): def on_connection_data_length_change(self):
self.app.append_to_output( self.app.append_to_output(

View File

@@ -275,7 +275,7 @@ async def async_main(
( (
f'min={min(latencies):.2f}, ' f'min={min(latencies):.2f}, '
f'max={max(latencies):.2f}, ' f'max={max(latencies):.2f}, '
f'average={sum(latencies)/len(latencies):.2f},' f'average={sum(latencies) / len(latencies):.2f},'
), ),
[f'{latency:.4}' for latency in latencies], [f'{latency:.4}' for latency in latencies],
'\n', '\n',

View File

@@ -215,7 +215,6 @@ async def show_device_info(peer, done: Optional[asyncio.Future]) -> None:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def async_main(device_config, encrypt, transport, address_or_name): async def async_main(device_config, encrypt, transport, address_or_name):
async with await open_transport(transport) as (hci_source, hci_sink): async with await open_transport(transport) as (hci_source, hci_sink):
# Create a device # Create a device
if device_config: if device_config:
device = Device.from_config_file_with_hci( device = Device.from_config_file_with_hci(

View File

@@ -61,7 +61,6 @@ async def dump_gatt_db(peer, done):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def async_main(device_config, encrypt, transport, address_or_name): async def async_main(device_config, encrypt, transport, address_or_name):
async with await open_transport(transport) as (hci_source, hci_sink): async with await open_transport(transport) as (hci_source, hci_sink):
# Create a device # Create a device
if device_config: if device_config:
device = Device.from_config_file_with_hci( device = Device.from_config_file_with_hci(

View File

@@ -268,7 +268,6 @@ class UiServer:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Speaker: class Speaker:
def __init__( def __init__(
self, self,
device_config_path: str | None, device_config_path: str | None,

View File

@@ -527,7 +527,9 @@ async def pair(
if advertise_appearance: if advertise_appearance:
advertise_appearance = advertise_appearance.upper() advertise_appearance = advertise_appearance.upper()
try: try:
advertise_appearance_int = int(advertise_appearance) appearance = data_types.Appearance.from_int(
int(advertise_appearance)
)
except ValueError: except ValueError:
category, subcategory = advertise_appearance.split('/') category, subcategory = advertise_appearance.split('/')
try: try:
@@ -545,12 +547,11 @@ async def pair(
except ValueError: except ValueError:
print(color(f'Invalid subcategory {subcategory}', 'red')) print(color(f'Invalid subcategory {subcategory}', 'red'))
return return
advertise_appearance_int = int( appearance = data_types.Appearance(
Appearance(category_enum, subcategory_enum) category_enum, subcategory_enum
) )
advertising_data_types.append(
data_types.Appearance(category_enum, subcategory_enum) advertising_data_types.append(appearance)
)
device.advertising_data = bytes(AdvertisingData(advertising_data_types)) device.advertising_data = bytes(AdvertisingData(advertising_data_types))
await device.start_advertising( await device.start_advertising(
auto_restart=True, auto_restart=True,

View File

@@ -19,7 +19,7 @@ ROOTCANAL_PORT_CUTTLEFISH = 7300
@click.option( @click.option(
'--transport', '--transport',
help='HCI transport', help='HCI transport',
default=f'tcp-client:127.0.0.1:<rootcanal-port>', default='tcp-client:127.0.0.1:<rootcanal-port>',
) )
@click.option( @click.option(
'--config', '--config',

View File

@@ -47,14 +47,13 @@ from bumble.avdtp import (
AVDTP_DELAY_REPORTING_SERVICE_CATEGORY, AVDTP_DELAY_REPORTING_SERVICE_CATEGORY,
MediaCodecCapabilities, MediaCodecCapabilities,
MediaPacketPump, MediaPacketPump,
find_avdtp_service_with_connection,
) )
from bumble.avdtp import Protocol as AvdtpProtocol from bumble.avdtp import Protocol as AvdtpProtocol
from bumble.avdtp import find_avdtp_service_with_connection
from bumble.avrcp import Protocol as AvrcpProtocol from bumble.avrcp import Protocol as AvrcpProtocol
from bumble.colors import color from bumble.colors import color
from bumble.core import AdvertisingData from bumble.core import AdvertisingData, DeviceClass, PhysicalTransport
from bumble.core import ConnectionError as BumbleConnectionError from bumble.core import ConnectionError as BumbleConnectionError
from bumble.core import DeviceClass, PhysicalTransport
from bumble.device import Connection, Device, DeviceConfiguration from bumble.device import Connection, Device, DeviceConfiguration
from bumble.hci import HCI_CONNECTION_ALREADY_EXISTS_ERROR, Address, HCI_Constant from bumble.hci import HCI_CONNECTION_ALREADY_EXISTS_ERROR, Address, HCI_Constant
from bumble.pairing import PairingConfig from bumble.pairing import PairingConfig
@@ -381,11 +380,11 @@ class Player:
print(f">>> {color(address.to_string(False), 'yellow')}:") print(f">>> {color(address.to_string(False), 'yellow')}:")
print(f" Device Class (raw): {class_of_device:06X}") print(f" Device Class (raw): {class_of_device:06X}")
major_class_name = DeviceClass.major_device_class_name(major_device_class) major_class_name = DeviceClass.major_device_class_name(major_device_class)
print(" Device Major Class: " f"{major_class_name}") print(f" Device Major Class: {major_class_name}")
minor_class_name = DeviceClass.minor_device_class_name( minor_class_name = DeviceClass.minor_device_class_name(
major_device_class, minor_device_class major_device_class, minor_device_class
) )
print(" Device Minor Class: " f"{minor_class_name}") print(f" Device Minor Class: {minor_class_name}")
print( print(
" Device Services: " " Device Services: "
f"{', '.join(DeviceClass.service_class_labels(service_classes))}" f"{', '.join(DeviceClass.service_class_labels(service_classes))}"

View File

@@ -217,9 +217,7 @@ async def scan(
@click.option( @click.option(
'--irk', '--irk',
metavar='<IRK_HEX>:<ADDRESS>', metavar='<IRK_HEX>:<ADDRESS>',
help=( help=('Use this IRK for resolving private addresses (may be used more than once)'),
'Use this IRK for resolving private addresses ' '(may be used more than once)'
),
multiple=True, multiple=True,
) )
@click.option( @click.option(

View File

@@ -490,7 +490,7 @@ class VendorSpecificMediaCodecInformation(MediaCodecInformation):
'VendorSpecificMediaCodecInformation(', 'VendorSpecificMediaCodecInformation(',
f' vendor_id: {self.vendor_id:08X} ({name_or_number(COMPANY_IDENTIFIERS, self.vendor_id & 0xFFFF)})', f' vendor_id: {self.vendor_id:08X} ({name_or_number(COMPANY_IDENTIFIERS, self.vendor_id & 0xFFFF)})',
f' codec_id: {self.codec_id:04X}', f' codec_id: {self.codec_id:04X}',
f' value: {self.value.hex()}' ')', f' value: {self.value.hex()})',
] ]
) )

View File

@@ -36,7 +36,7 @@ def tokenize_parameters(buffer: bytes) -> list[bytes]:
if in_quotes: if in_quotes:
token.extend(char) token.extend(char)
if char == b'\"': if char == b'"':
in_quotes = False in_quotes = False
tokens.append(token[1:-1]) tokens.append(token[1:-1])
token = bytearray() token = bytearray()

View File

@@ -258,7 +258,7 @@ class Protocol:
def send_ipid(self, transaction_label: int, pid: int) -> None: def send_ipid(self, transaction_label: int, pid: int) -> None:
logger.debug( logger.debug(
">>> AVCTP ipid: " f"transaction_label={transaction_label}, " f"pid={pid}" f">>> AVCTP ipid: transaction_label={transaction_label}, pid={pid}"
) )
self.send_message(transaction_label, False, True, pid, b'') self.send_message(transaction_label, False, True, pid, b'')

View File

@@ -2011,7 +2011,7 @@ class Protocol(utils.EventEmitter):
f"{command} is not a valid AV/C Command Frame" f"{command} is not a valid AV/C Command Frame"
) )
logger.debug( logger.debug(
f"<<< AVCTP Command, transaction_label={transaction_label}: " f"{command}" f"<<< AVCTP Command, transaction_label={transaction_label}: {command}"
) )
# Only addressing the unit, or the PANEL subunit with subunit ID 0 is supported # Only addressing the unit, or the PANEL subunit with subunit ID 0 is supported

View File

@@ -163,23 +163,23 @@ class AacAudioRtpPacket:
cls, reader: BitReader, channel_configuration: int, audio_object_type: int cls, reader: BitReader, channel_configuration: int, audio_object_type: int
) -> Self: ) -> Self:
# GASpecificConfig - ISO/EIC 14496-3 Table 4.1 # GASpecificConfig - ISO/EIC 14496-3 Table 4.1
frame_length_flag = reader.read(1) reader.read(1) # frame_length_flag
depends_on_core_coder = reader.read(1) depends_on_core_coder = reader.read(1)
if depends_on_core_coder: if depends_on_core_coder:
core_coder_delay = reader.read(14) reader.read(14) # core_coder_delay
extension_flag = reader.read(1) extension_flag = reader.read(1)
if not channel_configuration: if not channel_configuration:
raise core.InvalidPacketError('program_config_element not supported') raise core.InvalidPacketError('program_config_element not supported')
if audio_object_type in (6, 20): if audio_object_type in (6, 20):
layer_nr = reader.read(3) reader.read(3) # layer_nr
if extension_flag: if extension_flag:
if audio_object_type == 22: if audio_object_type == 22:
num_of_sub_frame = reader.read(5) reader.read(5) # num_of_sub_frame
layer_length = reader.read(11) reader.read(11) # layer_length
if audio_object_type in (17, 19, 20, 23): if audio_object_type in (17, 19, 20, 23):
aac_section_data_resilience_flags = reader.read(1) reader.read(1) # aac_section_data_resilience_flags
aac_scale_factor_data_resilience_flags = reader.read(1) reader.read(1) # aac_scale_factor_data_resilience_flags
aac_spectral_data_resilience_flags = reader.read(1) reader.read(1) # aac_spectral_data_resilience_flags
extension_flag_3 = reader.read(1) extension_flag_3 = reader.read(1)
if extension_flag_3 == 1: if extension_flag_3 == 1:
raise core.InvalidPacketError('extensionFlag3 == 1 not supported') raise core.InvalidPacketError('extensionFlag3 == 1 not supported')
@@ -364,10 +364,10 @@ class AacAudioRtpPacket:
if audio_mux_version_a != 0: if audio_mux_version_a != 0:
raise core.InvalidPacketError('audioMuxVersionA != 0 not supported') raise core.InvalidPacketError('audioMuxVersionA != 0 not supported')
if audio_mux_version == 1: if audio_mux_version == 1:
tara_buffer_fullness = AacAudioRtpPacket.read_latm_value(reader) AacAudioRtpPacket.read_latm_value(reader) # tara_buffer_fullness
stream_cnt = 0 # stream_cnt = 0
all_streams_same_time_framing = reader.read(1) reader.read(1) # all_streams_same_time_framing
num_sub_frames = reader.read(6) reader.read(6) # num_sub_frames
num_program = reader.read(4) num_program = reader.read(4)
if num_program != 0: if num_program != 0:
raise core.InvalidPacketError('num_program != 0 not supported') raise core.InvalidPacketError('num_program != 0 not supported')
@@ -391,9 +391,9 @@ class AacAudioRtpPacket:
reader.skip(asc_len) reader.skip(asc_len)
frame_length_type = reader.read(3) frame_length_type = reader.read(3)
if frame_length_type == 0: if frame_length_type == 0:
latm_buffer_fullness = reader.read(8) reader.read(8) # latm_buffer_fullness
elif frame_length_type == 1: elif frame_length_type == 1:
frame_length = reader.read(9) reader.read(9) # frame_length
else: else:
raise core.InvalidPacketError( raise core.InvalidPacketError(
f'frame_length_type {frame_length_type} not supported' f'frame_length_type {frame_length_type} not supported'
@@ -413,7 +413,7 @@ class AacAudioRtpPacket:
break break
crc_check_present = reader.read(1) crc_check_present = reader.read(1)
if crc_check_present: if crc_check_present:
crc_checksum = reader.read(8) reader.read(8) # crc_checksum
return cls(other_data_present, other_data_len_bits, audio_specific_config) return cls(other_data_present, other_data_len_bits, audio_specific_config)

View File

@@ -25,10 +25,8 @@ import random
import struct import struct
from typing import TYPE_CHECKING, Any, Optional, Union, cast from typing import TYPE_CHECKING, Any, Optional, Union, cast
from bumble import hci from bumble import hci, link, ll, lmp
from bumble import link
from bumble import link as bumble_link from bumble import link as bumble_link
from bumble import ll, lmp
from bumble.colors import color from bumble.colors import color
from bumble.core import PhysicalTransport from bumble.core import PhysicalTransport
@@ -170,10 +168,6 @@ class AdvertisingSet:
def send_extended_advertising_data(self) -> None: def send_extended_advertising_data(self) -> None:
if self.controller.link: if self.controller.link:
properties = (
self.parameters.advertising_event_properties if self.parameters else 0
)
address = self.address address = self.address
assert address assert address

View File

@@ -25,10 +25,11 @@ try:
from bumble.crypto.cryptography import EccKey, aes_cmac, e from bumble.crypto.cryptography import EccKey, aes_cmac, e
except ImportError: except ImportError:
logging.getLogger(__name__).debug( logging.getLogger(__name__).debug(
"Unable to import cryptography, use built-in primitives." "Unable to import cryptography, using built-in primitives."
) )
from bumble.crypto.builtin import EccKey, aes_cmac, e # type: ignore[assignment] from bumble.crypto.builtin import EccKey, aes_cmac, e # type: ignore[assignment]
_EccKey = EccKey # For the linter only
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging

View File

@@ -85,7 +85,6 @@ class _AES:
# fmt: on # fmt: on
def __init__(self, key: bytes) -> None: def __init__(self, key: bytes) -> None:
if len(key) not in (16, 24, 32): if len(key) not in (16, 24, 32):
raise core.InvalidArgumentError(f'Invalid key size {len(key)}') raise core.InvalidArgumentError(f'Invalid key size {len(key)}')
@@ -112,7 +111,6 @@ class _AES:
r_con_pointer = 0 r_con_pointer = 0
t = kc t = kc
while t < round_key_count: while t < round_key_count:
tt = tk[kc - 1] tt = tk[kc - 1]
tk[0] ^= ( tk[0] ^= (
(self._S[(tt >> 16) & 0xFF] << 24) (self._S[(tt >> 16) & 0xFF] << 24)
@@ -269,7 +267,6 @@ class _ECB:
class _CBC: class _CBC:
def __init__(self, key: bytes, iv: bytes = bytes(16)) -> None: def __init__(self, key: bytes, iv: bytes = bytes(16)) -> None:
if len(iv) != 16: if len(iv) != 16:
raise core.InvalidArgumentError( raise core.InvalidArgumentError(
@@ -302,7 +299,6 @@ class _CBC:
class _CMAC: class _CMAC:
def __init__( def __init__(
self, self,
key: bytes, key: bytes,
@@ -414,7 +410,6 @@ class _CMAC:
self._last_pt = _xor(second_last, data_block[-bs:]) self._last_pt = _xor(second_last, data_block[-bs:])
def digest(self) -> bytes: def digest(self) -> bytes:
bs = self._block_size bs = self._block_size
if self._mac_tag is not None and not self._update_after_digest: if self._mac_tag is not None and not self._update_after_digest:

View File

@@ -5248,8 +5248,7 @@ class Device(utils.CompositeEventEmitter):
if status != hci.HCI_SUCCESS: if status != hci.HCI_SUCCESS:
logger.debug( logger.debug(
f'advertising set {advertising_handle} ' f'advertising set {advertising_handle} terminated with status {status}'
f'terminated with status {status}'
) )
return return
@@ -5628,7 +5627,8 @@ class Device(utils.CompositeEventEmitter):
self.host.send_command_sync( self.host.send_command_sync(
hci.HCI_Accept_Connection_Request_Command( hci.HCI_Accept_Connection_Request_Command(
bd_addr=bd_addr, role=0x01 # Remain the peripheral bd_addr=bd_addr,
role=0x01, # Remain the peripheral
) )
) )

View File

@@ -380,7 +380,7 @@ class Driver(common.Driver):
if (vendor_id, product_id) not in INTEL_USB_PRODUCTS: if (vendor_id, product_id) not in INTEL_USB_PRODUCTS:
logger.debug( logger.debug(
f"USB device ({vendor_id:04X}, {product_id:04X}) " "not in known list" f"USB device ({vendor_id:04X}, {product_id:04X}) not in known list"
) )
return False return False
@@ -483,9 +483,7 @@ class Driver(common.Driver):
raise DriverError("insufficient device info, missing CNVI or CNVR") raise DriverError("insufficient device info, missing CNVI or CNVR")
firmware_base_name = ( firmware_base_name = (
"ibt-" f"ibt-{device_info[ValueType.CNVI]:04X}-{device_info[ValueType.CNVR]:04X}"
f"{device_info[ValueType.CNVI]:04X}-"
f"{device_info[ValueType.CNVR]:04X}"
) )
logger.debug(f"FW base name: {firmware_base_name}") logger.debug(f"FW base name: {firmware_base_name}")

View File

@@ -484,7 +484,7 @@ class Driver(common.Driver):
if (vendor_id, product_id) not in RTK_USB_PRODUCTS: if (vendor_id, product_id) not in RTK_USB_PRODUCTS:
logger.debug( logger.debug(
f"USB device ({vendor_id:04X}, {product_id:04X}) " "not in known list" f"USB device ({vendor_id:04X}, {product_id:04X}) not in known list"
) )
return False return False

View File

@@ -361,5 +361,4 @@ class EnumCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T3]):
def decode_value(self, value: bytes) -> _T3: def decode_value(self, value: bytes) -> _T3:
int_value = int.from_bytes(value, self.byteorder) int_value = int.from_bytes(value, self.byteorder)
a = self.cls(int_value)
return self.cls(int_value) return self.cls(int_value)

View File

@@ -119,7 +119,6 @@ def phy_list_to_bits(phys: Optional[Iterable[Phy]]) -> int:
class SpecableEnum(utils.OpenIntEnum): class SpecableEnum(utils.OpenIntEnum):
@classmethod @classmethod
def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'): def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'):
return { return {
@@ -147,7 +146,6 @@ class SpecableEnum(utils.OpenIntEnum):
class SpecableFlag(enum.IntFlag): class SpecableFlag(enum.IntFlag):
@classmethod @classmethod
def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'): def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'):
return { return {
@@ -1786,20 +1784,20 @@ class HCI_Object:
@classmethod @classmethod
def dict_and_offset_from_bytes( def dict_and_offset_from_bytes(
cls, data: bytes, offset: int, fields: Fields cls, data: bytes, offset: int, object_fields: Fields
) -> tuple[int, collections.OrderedDict[str, Any]]: ) -> tuple[int, collections.OrderedDict[str, Any]]:
result = collections.OrderedDict[str, Any]() result = collections.OrderedDict[str, Any]()
for field in fields: for object_field in object_fields:
if isinstance(field, list): if isinstance(object_field, list):
# This is an array field, starting with a 1-byte item count. # This is an array field, starting with a 1-byte item count.
item_count = data[offset] item_count = data[offset]
offset += 1 offset += 1
# Set fields first, because item_count might be 0. # Set fields first, because item_count might be 0.
for sub_field_name, _ in field: for sub_field_name, _ in object_field:
result[sub_field_name] = [] result[sub_field_name] = []
for _ in range(item_count): for _ in range(item_count):
for sub_field_name, sub_field_type in field: for sub_field_name, sub_field_type in object_field:
value, size = HCI_Object.parse_field( value, size = HCI_Object.parse_field(
data, offset, sub_field_type data, offset, sub_field_type
) )
@@ -1807,7 +1805,7 @@ class HCI_Object:
offset += size offset += size
continue continue
field_name, field_type = field field_name, field_type = object_field
assert isinstance(field_name, str) assert isinstance(field_name, str)
field_value, field_size = HCI_Object.parse_field( field_value, field_size = HCI_Object.parse_field(
data, offset, cast(FieldSpec, field_type) data, offset, cast(FieldSpec, field_type)
@@ -1890,26 +1888,26 @@ class HCI_Object:
return field_bytes return field_bytes
@staticmethod @staticmethod
def dict_to_bytes(hci_object, fields): def dict_to_bytes(hci_object, object_fields):
result = bytearray() result = bytearray()
for field in fields: for object_field in object_fields:
if isinstance(field, list): if isinstance(object_field, list):
# The field is an array. The serialized form starts with a 1-byte # The field is an array. The serialized form starts with a 1-byte
# item count. We use the length of the first array field as the # item count. We use the length of the first array field as the
# array count, since all array fields have the same number of items. # array count, since all array fields have the same number of items.
item_count = len(hci_object[field[0][0]]) item_count = len(hci_object[object_field[0][0]])
result += bytes([item_count]) + b''.join( result += bytes([item_count]) + b''.join(
b''.join( b''.join(
HCI_Object.serialize_field( HCI_Object.serialize_field(
hci_object[sub_field_name][i], sub_field_type hci_object[sub_field_name][i], sub_field_type
) )
for sub_field_name, sub_field_type in field for sub_field_name, sub_field_type in object_field
) )
for i in range(item_count) for i in range(item_count)
) )
continue continue
(field_name, field_type) = field (field_name, field_type) = object_field
result += HCI_Object.serialize_field(hci_object[field_name], field_type) result += HCI_Object.serialize_field(hci_object[field_name], field_type)
return bytes(result) return bytes(result)
@@ -1967,15 +1965,15 @@ class HCI_Object:
) )
@staticmethod @staticmethod
def format_fields(hci_object, fields, indentation='', value_mappers=None): def format_fields(hci_object, object_fields, indentation='', value_mappers=None):
if not fields: if not object_fields:
return '' return ''
# Build array of formatted key:value pairs # Build array of formatted key:value pairs
field_strings = [] field_strings = []
for field in fields: for object_field in object_fields:
if isinstance(field, list): if isinstance(object_field, list):
for sub_field in field: for sub_field in object_field:
sub_field_name, sub_field_type = sub_field sub_field_name, sub_field_type = sub_field
item_count = len(hci_object[sub_field_name]) item_count = len(hci_object[sub_field_name])
for i in range(item_count): for i in range(item_count):
@@ -1993,7 +1991,7 @@ class HCI_Object:
) )
continue continue
field_name, field_type = field field_name, field_type = object_field
field_value = hci_object[field_name] field_value = hci_object[field_name]
field_strings.append( field_strings.append(
( (
@@ -2016,16 +2014,16 @@ class HCI_Object:
@classmethod @classmethod
def fields_from_dataclass(cls, obj: Any) -> list[Any]: def fields_from_dataclass(cls, obj: Any) -> list[Any]:
stack: list[list[Any]] = [[]] stack: list[list[Any]] = [[]]
for field in dataclasses.fields(obj): for object_field in dataclasses.fields(obj):
# Fields without metadata should be ignored. # Fields without metadata should be ignored.
if not isinstance( if not isinstance(
(metadata := field.metadata.get("bumble.hci")), FieldMetadata (metadata := object_field.metadata.get("bumble.hci")), FieldMetadata
): ):
continue continue
if metadata.list_begin: if metadata.list_begin:
stack.append([]) stack.append([])
if metadata.spec: if metadata.spec:
stack[-1].append((field.name, metadata.spec)) stack[-1].append((object_field.name, metadata.spec))
if metadata.list_end: if metadata.list_end:
top = stack.pop() top = stack.pop()
stack[-1].append(top) stack[-1].append(top)

View File

@@ -597,7 +597,7 @@ class AgIndicatorState:
supported_values_text = ( supported_values_text = (
f'({",".join(str(v) for v in self.supported_values)})' f'({",".join(str(v) for v in self.supported_values)})'
) )
return f'(\"{self.indicator.value}\",{supported_values_text})' return f'("{self.indicator.value}",{supported_values_text})'
@classmethod @classmethod
def call(cls: type[Self]) -> Self: def call(cls: type[Self]) -> Self:
@@ -1351,7 +1351,7 @@ class AgProtocol(utils.EventEmitter):
logger.warning(f'AG indicator {indicator} is disabled') logger.warning(f'AG indicator {indicator} is disabled')
indicator_state.current_status = value indicator_state.current_status = value
self.send_response(f'+CIEV: {index+1},{value}') self.send_response(f'+CIEV: {index + 1},{value}')
async def negotiate_codec(self, codec: AudioCodec) -> None: async def negotiate_codec(self, codec: AudioCodec) -> None:
"""Starts codec negotiation.""" """Starts codec negotiation."""
@@ -1417,7 +1417,7 @@ class AgProtocol(utils.EventEmitter):
operation_code = operation_code[:1] + b'x' operation_code = operation_code[:1] + b'x'
try: try:
operation = CallHoldOperation(operation_code.decode()) operation = CallHoldOperation(operation_code.decode())
except: except Exception:
logger.error(f'Invalid operation: {operation_code.decode()}') logger.error(f'Invalid operation: {operation_code.decode()}')
self.send_cme_error(CmeError.OPERATION_NOT_SUPPORTED) self.send_cme_error(CmeError.OPERATION_NOT_SUPPORTED)
return return
@@ -1589,7 +1589,7 @@ class AgProtocol(utils.EventEmitter):
def _on_clcc(self) -> None: def _on_clcc(self) -> None:
for call in self.calls: for call in self.calls:
number_text = f',\"{call.number}\"' if call.number is not None else '' number_text = f',"{call.number}"' if call.number is not None else ''
type_text = f',{call.type}' if call.type is not None else '' type_text = f',{call.type}' if call.type is not None else ''
response = ( response = (
f'+CLCC: {call.index}' f'+CLCC: {call.index}'

View File

@@ -108,8 +108,7 @@ class DataPacketQueue(utils.EventEmitter):
if self._packets: if self._packets:
logger.debug( logger.debug(
f'{self._in_flight} packets in flight, ' f'{self._in_flight} packets in flight, {len(self._packets)} in queue'
f'{len(self._packets)} in queue'
) )
def flush(self, connection_handle: int) -> None: def flush(self, connection_handle: int) -> None:
@@ -1394,8 +1393,7 @@ class Host(utils.EventEmitter):
if event.status == hci.HCI_SUCCESS: if event.status == hci.HCI_SUCCESS:
# Create/update the connection # Create/update the connection
logger.debug( logger.debug(
f'### SCO CONNECTION: [0x{event.connection_handle:04X}] ' f'### SCO CONNECTION: [0x{event.connection_handle:04X}] {event.bd_addr}'
f'{event.bd_addr}'
) )
self.sco_links[event.connection_handle] = ScoLink( self.sco_links[event.connection_handle] = ScoLink(
@@ -1447,7 +1445,7 @@ class Host(utils.EventEmitter):
def on_hci_le_data_length_change_event( def on_hci_le_data_length_change_event(
self, event: hci.HCI_LE_Data_Length_Change_Event self, event: hci.HCI_LE_Data_Length_Change_Event
): ):
if (connection := self.connections.get(event.connection_handle)) is None: if event.connection_handle not in self.connections:
logger.warning('!!! DATA LENGTH CHANGE: unknown handle') logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
return return

View File

@@ -331,7 +331,6 @@ class InformationEnhancedControlField(EnhancedControlField):
@dataclasses.dataclass @dataclasses.dataclass
class SupervisoryEnhancedControlField(EnhancedControlField): class SupervisoryEnhancedControlField(EnhancedControlField):
supervision_function: int = ControlField.SupervisoryFunction.RR supervision_function: int = ControlField.SupervisoryFunction.RR
poll: int = 0 poll: int = 0
req_seq: int = 0 req_seq: int = 0
@@ -884,7 +883,6 @@ class Processor:
# TODO: Handle retransmission # TODO: Handle retransmission
class EnhancedRetransmissionProcessor(Processor): class EnhancedRetransmissionProcessor(Processor):
MAX_SEQ_NUM = 64 MAX_SEQ_NUM = 64
@dataclasses.dataclass @dataclasses.dataclass

View File

@@ -74,7 +74,9 @@ class PandoraDevice:
# open HCI transport & set device host. # open HCI transport & set device host.
self._hci = await transport.open_transport(self._hci_name) self._hci = await transport.open_transport(self._hci_name)
self.device.host = Host(controller_source=self._hci.source, controller_sink=self._hci.sink) # type: ignore[no-untyped-call] self.device.host = Host(
controller_source=self._hci.source, controller_sink=self._hci.sink
) # type: ignore[no-untyped-call]
# power-on. # power-on.
await self.device.power_on() await self.device.power_on()

View File

@@ -21,8 +21,10 @@ from typing import AsyncGenerator, Optional, cast
import grpc import grpc
import grpc.aio import grpc.aio
from google.protobuf import any_pb2 # pytype: disable=pyi-error from google.protobuf import (
from google.protobuf import empty_pb2 # pytype: disable=pyi-error any_pb2, # pytype: disable=pyi-error
empty_pb2, # pytype: disable=pyi-error
)
from pandora import host_pb2 from pandora import host_pb2
from pandora.host_grpc_aio import HostServicer from pandora.host_grpc_aio import HostServicer
from pandora.host_pb2 import ( from pandora.host_pb2 import (
@@ -302,7 +304,9 @@ class HostService(HostServicer):
await disconnection_future await disconnection_future
self.log.debug("Disconnected") self.log.debug("Disconnected")
finally: finally:
connection.remove_listener(connection.EVENT_DISCONNECTION, on_disconnection) # type: ignore connection.remove_listener(
connection.EVENT_DISCONNECTION, on_disconnection
) # type: ignore
return empty_pb2.Empty() return empty_pb2.Empty()
@@ -539,7 +543,7 @@ class HostService(HostServicer):
await bumble.utils.cancel_on_event( await bumble.utils.cancel_on_event(
self.device, 'flush', self.device.stop_advertising() self.device, 'flush', self.device.stop_advertising()
) )
except: except Exception:
pass pass
@utils.rpc @utils.rpc
@@ -609,7 +613,7 @@ class HostService(HostServicer):
await bumble.utils.cancel_on_event( await bumble.utils.cancel_on_event(
self.device, 'flush', self.device.stop_scanning() self.device, 'flush', self.device.stop_scanning()
) )
except: except Exception:
pass pass
@utils.rpc @utils.rpc
@@ -644,14 +648,18 @@ class HostService(HostServicer):
) )
finally: finally:
self.device.remove_listener(self.device.EVENT_INQUIRY_COMPLETE, complete_handler) # type: ignore self.device.remove_listener(
self.device.remove_listener(self.device.EVENT_INQUIRY_RESULT, result_handler) # type: ignore self.device.EVENT_INQUIRY_COMPLETE, complete_handler
) # type: ignore
self.device.remove_listener(
self.device.EVENT_INQUIRY_RESULT, result_handler
) # type: ignore
try: try:
self.log.debug('Stop inquiry') self.log.debug('Stop inquiry')
await bumble.utils.cancel_on_event( await bumble.utils.cancel_on_event(
self.device, 'flush', self.device.stop_discovery() self.device, 'flush', self.device.stop_discovery()
) )
except: except Exception:
pass pass
@utils.rpc @utils.rpc

View File

@@ -24,9 +24,9 @@ from typing import AsyncGenerator, Optional, Union
import grpc import grpc
from google.protobuf import any_pb2, empty_pb2 # pytype: disable=pyi-error from google.protobuf import any_pb2, empty_pb2 # pytype: disable=pyi-error
from pandora.l2cap_grpc_aio import L2CAPServicer # pytype: disable=pyi-error from pandora.l2cap_grpc_aio import L2CAPServicer # pytype: disable=pyi-error
from pandora.l2cap_pb2 import COMMAND_NOT_UNDERSTOOD, INVALID_CID_IN_REQUEST
from pandora.l2cap_pb2 import Channel as PandoraChannel # pytype: disable=pyi-error
from pandora.l2cap_pb2 import ( from pandora.l2cap_pb2 import (
COMMAND_NOT_UNDERSTOOD,
INVALID_CID_IN_REQUEST,
ConnectRequest, ConnectRequest,
ConnectResponse, ConnectResponse,
CreditBasedChannelRequest, CreditBasedChannelRequest,
@@ -41,6 +41,7 @@ from pandora.l2cap_pb2 import (
WaitDisconnectionRequest, WaitDisconnectionRequest,
WaitDisconnectionResponse, WaitDisconnectionResponse,
) )
from pandora.l2cap_pb2 import Channel as PandoraChannel # pytype: disable=pyi-error
from bumble.core import InvalidArgumentError, OutOfResourcesError from bumble.core import InvalidArgumentError, OutOfResourcesError
from bumble.device import Device from bumble.device import Device

View File

@@ -21,9 +21,11 @@ from collections.abc import Awaitable
from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Union from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Union
import grpc import grpc
from google.protobuf import any_pb2 # pytype: disable=pyi-error from google.protobuf import (
from google.protobuf import empty_pb2 # pytype: disable=pyi-error any_pb2, # pytype: disable=pyi-error
from google.protobuf import wrappers_pb2 # pytype: disable=pyi-error empty_pb2, # pytype: disable=pyi-error
wrappers_pb2, # pytype: disable=pyi-error
)
from pandora.host_pb2 import Connection from pandora.host_pb2 import Connection
from pandora.security_grpc_aio import SecurityServicer, SecurityStorageServicer from pandora.security_grpc_aio import SecurityServicer, SecurityStorageServicer
from pandora.security_pb2 import ( from pandora.security_pb2 import (
@@ -455,7 +457,7 @@ class SecurityService(SecurityServicer):
def pair(*_: Any) -> None: def pair(*_: Any) -> None:
if self.need_pairing(connection, level): if self.need_pairing(connection, level):
pair_task = asyncio.create_task(connection.pair()) bumble.utils.AsyncRunner.spawn(connection.pair())
listeners: dict[str, Callable[..., Union[None, Awaitable[None]]]] = { listeners: dict[str, Callable[..., Union[None, Awaitable[None]]]] = {
'disconnection': set_failure('connection_died'), 'disconnection': set_failure('connection_died'),

View File

@@ -199,7 +199,6 @@ class AudioInputControlPoint:
gain_settings_properties: GainSettingsProperties gain_settings_properties: GainSettingsProperties
async def on_write(self, connection: Connection, value: bytes) -> None: async def on_write(self, connection: Connection, value: bytes) -> None:
opcode = AudioInputControlPointOpCode(value[0]) opcode = AudioInputControlPointOpCode(value[0])
if opcode == AudioInputControlPointOpCode.SET_GAIN_SETTING: if opcode == AudioInputControlPointOpCode.SET_GAIN_SETTING:

View File

@@ -40,7 +40,6 @@ class GenericAttributeProfileService(gatt.TemplateService):
database_hash_enabled: bool = True, database_hash_enabled: bool = True,
service_change_enabled: bool = True, service_change_enabled: bool = True,
) -> None: ) -> None:
if server_supported_features is not None: if server_supported_features is not None:
self.server_supported_features_characteristic = gatt.Characteristic( self.server_supported_features_characteristic = gatt.Characteristic(
uuid=gatt.GATT_SERVER_SUPPORTED_FEATURES_CHARACTERISTIC, uuid=gatt.GATT_SERVER_SUPPORTED_FEATURES_CHARACTERISTIC,

View File

@@ -137,7 +137,7 @@ class Metadata:
values.append(str(decoded)) values.append(str(decoded))
return '\n'.join( return '\n'.join(
f'{indent}{key}: {" " * (max_key_length-len(key))}{value}' f'{indent}{key}: {" " * (max_key_length - len(key))}{value}'
for key, value in zip(keys, values) for key, value in zip(keys, values)
) )

View File

@@ -118,7 +118,6 @@ class VolumeOffsetControlPoint:
volume_offset_state: VolumeOffsetState volume_offset_state: VolumeOffsetState
async def on_write(self, connection: Connection, value: bytes) -> None: async def on_write(self, connection: Connection, value: bytes) -> None:
opcode = value[0] opcode = value[0]
if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET: if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET:
raise ATT_Error(ErrorCode.OPCODE_NOT_SUPPORTED) raise ATT_Error(ErrorCode.OPCODE_NOT_SUPPORTED)
@@ -177,7 +176,6 @@ class VolumeOffsetControlService(TemplateService):
audio_location: Optional[VocsAudioLocation] = None, audio_location: Optional[VocsAudioLocation] = None,
audio_output_description: Optional[AudioOutputDescription] = None, audio_output_description: Optional[AudioOutputDescription] = None,
) -> None: ) -> None:
self.volume_offset_state = ( self.volume_offset_state = (
VolumeOffsetState() if volume_offset_state is None else volume_offset_state VolumeOffsetState() if volume_offset_state is None else volume_offset_state
) )

View File

@@ -528,7 +528,7 @@ class ServiceAttribute:
def to_string(self, with_colors=False): def to_string(self, with_colors=False):
if with_colors: if with_colors:
return ( return (
f'Attribute(id={color(self.id_name(self.id),"magenta")},' f'Attribute(id={color(self.id_name(self.id), "magenta")},'
f'value={self.value})' f'value={self.value})'
) )

View File

@@ -997,7 +997,6 @@ class Session:
self.send_command(response) self.send_command(response)
def send_pairing_confirm_command(self) -> None: def send_pairing_confirm_command(self) -> None:
if self.pairing_method != PairingMethod.OOB: if self.pairing_method != PairingMethod.OOB:
self.r = crypto.r() self.r = crypto.r()
logger.debug(f'generated random: {self.r.hex()}') logger.debug(f'generated random: {self.r.hex()}')
@@ -1833,7 +1832,6 @@ class Session:
self.send_public_key_command() self.send_public_key_command()
def next_steps() -> None: def next_steps() -> None:
if self.pairing_method in ( if self.pairing_method in (
PairingMethod.JUST_WORKS, PairingMethod.JUST_WORKS,
PairingMethod.NUMERIC_COMPARISON, PairingMethod.NUMERIC_COMPARISON,

View File

@@ -284,7 +284,9 @@ async def open_pyusb_transport(spec: str) -> Transport:
device = await _power_cycle(device) # type: ignore device = await _power_cycle(device) # type: ignore
except Exception as e: except Exception as e:
logging.debug(e, stack_info=True) logging.debug(e, stack_info=True)
logging.info(f"Unable to power cycle {hex(device.idVendor)} {hex(device.idProduct)}") # type: ignore logging.info(
f"Unable to power cycle {hex(device.idVendor)} {hex(device.idProduct)}"
) # type: ignore
# Collect the metadata # Collect the metadata
device_metadata = {'vendor_id': device.idVendor, 'product_id': device.idProduct} device_metadata = {'vendor_id': device.idVendor, 'product_id': device.idProduct}
@@ -370,7 +372,9 @@ async def _power_cycle(device: UsbDevice) -> UsbDevice:
# Device needs to be find again otherwise it will appear as disconnected # Device needs to be find again otherwise it will appear as disconnected
return usb.core.find(idVendor=device.idVendor, idProduct=device.idProduct) # type: ignore return usb.core.find(idVendor=device.idVendor, idProduct=device.idProduct) # type: ignore
except USBError: except USBError:
logger.exception(f"Adjustment needed: Please revise the udev rule for device {hex(device.idVendor)}:{hex(device.idProduct)} for proper recognition.") # type: ignore logger.exception(
f"Adjustment needed: Please revise the udev rule for device {hex(device.idVendor)}:{hex(device.idProduct)} for proper recognition."
) # type: ignore
return device return device

View File

@@ -51,6 +51,7 @@ class HCI_LE_Get_Vendor_Capabilities_Command(hci.HCI_Command):
''' '''
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#vendor-specific-capabilities See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#vendor-specific-capabilities
''' '''
return_parameters_fields = [ return_parameters_fields = [
('status', hci.STATUS_SPEC), ('status', hci.STATUS_SPEC),
('max_advt_instances', 1), ('max_advt_instances', 1),
@@ -137,6 +138,7 @@ class HCI_Get_Controller_Activity_Energy_Info_Command(hci.HCI_Command):
''' '''
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_get_controller_activity_energy_info See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_get_controller_activity_energy_info
''' '''
return_parameters_fields = [ return_parameters_fields = [
('status', hci.STATUS_SPEC), ('status', hci.STATUS_SPEC),
('total_tx_time_ms', 4), ('total_tx_time_ms', 4),
@@ -229,6 +231,7 @@ class HCI_Bluetooth_Quality_Report_Event(HCI_Android_Vendor_Event):
''' '''
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#bluetooth-quality-report-sub-event See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#bluetooth-quality-report-sub-event
''' '''
quality_report_id: int = field(metadata=hci.metadata(1)) quality_report_id: int = field(metadata=hci.metadata(1))
packet_types: int = field(metadata=hci.metadata(1)) packet_types: int = field(metadata=hci.metadata(1))
connection_handle: int = field(metadata=hci.metadata(2)) connection_handle: int = field(metadata=hci.metadata(2))

View File

@@ -59,28 +59,28 @@ class Keyboard:
def print_keyboard_report(self) -> None: def print_keyboard_report(self) -> None:
print(color('\tKeyboard Input Received', 'green', None, 'bold')) print(color('\tKeyboard Input Received', 'green', None, 'bold'))
print(color(f'Keys:', 'white', None, 'bold')) print(color('Keys:', 'white', None, 'bold'))
for i in range(1, 7): for i in range(1, 7):
print( print(
color(f' Key{i}{" ":>8s}= ', 'cyan', None, 'bold'), self.report[i + 1] color(f' Key{i}{" ":>8s}= ', 'cyan', None, 'bold'), self.report[i + 1]
) )
print(color(f'\nModifier Keys:', 'white', None, 'bold')) print(color('\nModifier Keys:', 'white', None, 'bold'))
print( print(
color(f' Left Ctrl : ', 'cyan'), color(' Left Ctrl : ', 'cyan'),
f'{self.report[0][0] == 1!s:<5}', # type: ignore f'{self.report[0][0] == 1!s:<5}', # type: ignore
color(f' Left Shift : ', 'cyan'), color(' Left Shift : ', 'cyan'),
f'{self.report[0][1] == 1!s:<5}', # type: ignore f'{self.report[0][1] == 1!s:<5}', # type: ignore
color(f' Left ALT : ', 'cyan'), color(' Left ALT : ', 'cyan'),
f'{self.report[0][2] == 1!s:<5}', # type: ignore f'{self.report[0][2] == 1!s:<5}', # type: ignore
color(f' Left GUI : ', 'cyan'), color(' Left GUI : ', 'cyan'),
f'{self.report[0][3] == 1!s:<5}\n', # type: ignore f'{self.report[0][3] == 1!s:<5}\n', # type: ignore
color(f' Right Ctrl : ', 'cyan'), color(' Right Ctrl : ', 'cyan'),
f'{self.report[0][4] == 1!s:<5}', # type: ignore f'{self.report[0][4] == 1!s:<5}', # type: ignore
color(f' Right Shift : ', 'cyan'), color(' Right Shift : ', 'cyan'),
f'{self.report[0][5] == 1!s:<5}', # type: ignore f'{self.report[0][5] == 1!s:<5}', # type: ignore
color(f' Right ALT : ', 'cyan'), color(' Right ALT : ', 'cyan'),
f'{self.report[0][6] == 1!s:<5}', # type: ignore f'{self.report[0][6] == 1!s:<5}', # type: ignore
color(f' Right GUI : ', 'cyan'), color(' Right GUI : ', 'cyan'),
f'{self.report[0][7] == 1!s:<5}', # type: ignore f'{self.report[0][7] == 1!s:<5}', # type: ignore
) )
@@ -117,23 +117,23 @@ class Mouse:
def print_mouse_report(self) -> None: def print_mouse_report(self) -> None:
print(color('\tMouse Input Received', 'green', None, 'bold')) print(color('\tMouse Input Received', 'green', None, 'bold'))
print( print(
color(f' Button 1 (primary/trigger) = ', 'cyan'), color(' Button 1 (primary/trigger) = ', 'cyan'),
self.report[0][0] == 1, # type: ignore self.report[0][0] == 1, # type: ignore
color(f'\n Button 2 (secondary) = ', 'cyan'), color('\n Button 2 (secondary) = ', 'cyan'),
self.report[0][1] == 1, # type: ignore self.report[0][1] == 1, # type: ignore
color(f'\n Button 3 (tertiary) = ', 'cyan'), color('\n Button 3 (tertiary) = ', 'cyan'),
self.report[0][2] == 1, # type: ignore self.report[0][2] == 1, # type: ignore
color(f'\n Button4 = ', 'cyan'), color('\n Button4 = ', 'cyan'),
self.report[0][3] == 1, # type: ignore self.report[0][3] == 1, # type: ignore
color(f'\n Button5 = ', 'cyan'), color('\n Button5 = ', 'cyan'),
self.report[0][4] == 1, # type: ignore self.report[0][4] == 1, # type: ignore
color(f'\n X (X-axis displacement) = ', 'cyan'), color('\n X (X-axis displacement) = ', 'cyan'),
self.report[1], self.report[1],
color(f'\n Y (Y-axis displacement) = ', 'cyan'), color('\n Y (Y-axis displacement) = ', 'cyan'),
self.report[2], self.report[2],
color(f'\n Wheel = ', 'cyan'), color('\n Wheel = ', 'cyan'),
self.report[3], self.report[3],
color(f'\n AC PAN = ', 'cyan'), color('\n AC PAN = ', 'cyan'),
self.report[4], self.report[4],
) )
@@ -142,7 +142,6 @@ class Mouse:
class ReportParser: class ReportParser:
@staticmethod @staticmethod
def parse_input_report(input_report: bytes) -> None: def parse_input_report(input_report: bytes) -> None:
report_id = input_report[0] # pylint: disable=unsubscriptable-object report_id = input_report[0] # pylint: disable=unsubscriptable-object
report_length = len(input_report) report_length = len(input_report)

View File

@@ -3,7 +3,6 @@ from mobly.controllers import android_device
class OneDeviceBenchTest(base_test.BaseTestClass): class OneDeviceBenchTest(base_test.BaseTestClass):
def setup_class(self): def setup_class(self):
self.ads = self.register_controller(android_device) self.ads = self.register_controller(android_device)
self.dut = self.ads[0] self.dut = self.ads[0]

View File

@@ -33,9 +33,10 @@ from bumble.sdp import (
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
DataElement,
ServiceAttribute,
) )
from bumble.sdp import Client as SDP_Client from bumble.sdp import Client as SDP_Client
from bumble.sdp import DataElement, ServiceAttribute
from bumble.transport import open_transport from bumble.transport import open_transport

View File

@@ -30,6 +30,7 @@ from bumble.profiles.ancs import (
NotificationAttributeId, NotificationAttributeId,
) )
from bumble.transport import open_transport from bumble.transport import open_transport
from bumble.utils import AsyncRunner
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
_cached_app_names: dict[str, str] = {} _cached_app_names: dict[str, str] = {}
@@ -192,9 +193,7 @@ async def main() -> None:
ancs_client.on("notification", on_ancs_notification) ancs_client.on("notification", on_ancs_notification)
# Process all notifications in a task. # Process all notifications in a task.
notification_processing_task = asyncio.create_task( AsyncRunner.spawn(process_notifications(ancs_client))
process_notifications(ancs_client)
)
# Accept a TCP connection to handle commands. # Accept a TCP connection to handle commands.
tcp_server = await asyncio.start_server( tcp_server = await asyncio.start_server(

View File

@@ -51,7 +51,6 @@ async def main() -> None:
devices[1].cis_enabled = True devices[1].cis_enabled = True
await asyncio.gather(*[device.power_on() for device in devices]) await asyncio.gather(*[device.power_on() for device in devices])
advertising_set = await devices[0].create_advertising_set()
connection = await devices[1].connect( connection = await devices[1].connect(
devices[0].random_address, own_address_type=OwnAddressType.RANDOM devices[0].random_address, own_address_type=OwnAddressType.RANDOM

View File

@@ -38,11 +38,11 @@ class DiscoveryListener(Device.Listener):
print(f'>>> {color(address, "yellow")}:') print(f'>>> {color(address, "yellow")}:')
print(f' Device Class (raw): {class_of_device:06X}') print(f' Device Class (raw): {class_of_device:06X}')
major_class_name = DeviceClass.major_device_class_name(major_device_class) major_class_name = DeviceClass.major_device_class_name(major_device_class)
print(' Device Major Class: ' f'{major_class_name}') print(f' Device Major Class: {major_class_name}')
minor_class_name = DeviceClass.minor_device_class_name( minor_class_name = DeviceClass.minor_device_class_name(
major_device_class, minor_device_class major_device_class, minor_device_class
) )
print(' Device Minor Class: ' f'{minor_class_name}') print(f' Device Minor Class: {minor_class_name}')
print( print(
' Device Services: ' ' Device Services: '
f'{", ".join(DeviceClass.service_class_labels(service_classes))}' f'{", ".join(DeviceClass.service_class_labels(service_classes))}'

View File

@@ -32,7 +32,6 @@ from bumble.transport import open_transport
async def main( async def main(
config_file: str, transport: str, mode: int, peer_address: str, psm: int config_file: str, transport: str, mode: int, peer_address: str, psm: int
) -> None: ) -> None:
print('<<< connecting to HCI...') print('<<< connecting to HCI...')
async with await open_transport(transport) as hci_transport: async with await open_transport(transport) as hci_transport:
print('<<< connected') print('<<< connected')
@@ -59,7 +58,6 @@ async def main(
active_channel: l2cap.ClassicChannel | None = None active_channel: l2cap.ClassicChannel | None = None
def on_connection(channel: l2cap.ClassicChannel): def on_connection(channel: l2cap.ClassicChannel):
def on_sdu(sdu: bytes): def on_sdu(sdu: bytes):
print(f'<<< {sdu.decode()}') print(f'<<< {sdu.decode()}')

View File

@@ -1,86 +0,0 @@
# Copyright 2021-2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import asyncio
import sys
import bumble.logging
from bumble.colors import color
from bumble.controller import Controller
from bumble.device import Device
from bumble.hci import Address
from bumble.link import LocalLink
from bumble.transport import open_transport
# -----------------------------------------------------------------------------
class ScannerListener(Device.Listener):
def on_advertisement(self, advertisement):
address_type_string = ('P', 'R', 'PI', 'RI')[advertisement.address.address_type]
address_color = 'yellow' if advertisement.is_connectable else 'red'
if address_type_string.startswith('P'):
type_color = 'green'
else:
type_color = 'cyan'
print(
f'>>> {color(advertisement.address, address_color)} '
f'[{color(address_type_string, type_color)}]: '
f'RSSI={advertisement.rssi}, {advertisement.data}'
)
# -----------------------------------------------------------------------------
async def main() -> None:
if len(sys.argv) != 2:
print('Usage: run_controller.py <transport-spec>')
print('example: run_controller_with_scanner.py serial:/dev/pts/14,1000000')
return
print('>>> connecting to HCI...')
async with await open_transport(sys.argv[1]) as hci_transport:
print('>>> connected')
# Create a local link
link = LocalLink()
# Create a first controller using the packet source/sink as its host interface
controller1 = Controller(
'C1',
host_source=hci_transport.source,
host_sink=hci_transport.sink,
link=link,
public_address='E0:E1:E2:E3:E4:E5',
)
# Create a second controller using the same link
controller2 = Controller('C2', link=link)
# Create a device with a scanner listener
device = Device.with_hci(
'Bumble', Address('F0:F1:F2:F3:F4:F5'), controller2, controller2
)
device.listener = ScannerListener()
await device.power_on()
await device.start_scanning()
await hci_transport.source.wait_for_termination()
# -----------------------------------------------------------------------------
bumble.logging.setup_basic_logging('DEBUG')
asyncio.run(main())

View File

@@ -36,7 +36,7 @@ async def main() -> None:
'Usage: run_csis_servers.py <config-file> ' 'Usage: run_csis_servers.py <config-file> '
'<transport-spec-for-device-1> <transport-spec-for-device-2>' '<transport-spec-for-device-1> <transport-spec-for-device-2>'
) )
print('example: run_csis_servers.py device1.json ' 'hci-socket:0 hci-socket:1') print('example: run_csis_servers.py device1.json hci-socket:0 hci-socket:1')
return return
print('<<< connecting to HCI...') print('<<< connecting to HCI...')

View File

@@ -90,7 +90,7 @@ async def main() -> None:
), ),
scan_response_data=bytes(scan_response_data3), scan_response_data=bytes(scan_response_data3),
) )
print("Selected TX power 3:", set2.selected_tx_power) print("Selected TX power 3:", set3.selected_tx_power)
await hci_transport.source.terminated await hci_transport.source.terminated

View File

@@ -30,9 +30,8 @@ from bumble.core import (
PhysicalTransport, PhysicalTransport,
) )
from bumble.device import Device from bumble.device import Device
from bumble.hid import HID_CONTROL_PSM, HID_INTERRUPT_PSM from bumble.hid import HID_CONTROL_PSM, HID_INTERRUPT_PSM, Message
from bumble.hid import Device as HID_Device from bumble.hid import Device as HID_Device
from bumble.hid import Message
from bumble.sdp import ( from bumble.sdp import (
SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
@@ -423,7 +422,6 @@ deviceData = DeviceData()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def keyboard_device(hid_device: HID_Device): async def keyboard_device(hid_device: HID_Device):
# Start a Websocket server to receive events from a web page # Start a Websocket server to receive events from a web page
async def serve(websocket: websockets.asyncio.server.ServerConnection): async def serve(websocket: websockets.asyncio.server.ServerConnection):
global deviceData global deviceData

View File

@@ -67,7 +67,6 @@ SDP_HID_SSR_HOST_MIN_TIMEOUT_ATTRIBUTE_ID = 0x0210
async def get_hid_device_sdp_record(connection): async def get_hid_device_sdp_record(connection):
# Connect to the SDP Server # Connect to the SDP Server
sdp_client = SDP_Client(connection) sdp_client = SDP_Client(connection)
await sdp_client.connect() await sdp_client.connect()
@@ -84,7 +83,7 @@ async def get_hid_device_sdp_record(connection):
if len(service_record_handles) < 1: if len(service_record_handles) < 1:
await sdp_client.disconnect() await sdp_client.disconnect()
raise Exception( raise Exception(
color(f'BT HID Device service not found on peer device!!!!', 'red') color('BT HID Device service not found on peer device!!!!', 'red')
) )
# For BT_HUMAN_INTERFACE_DEVICE_SERVICE service, get all its attributes # For BT_HUMAN_INTERFACE_DEVICE_SERVICE service, get all its attributes
@@ -92,8 +91,8 @@ async def get_hid_device_sdp_record(connection):
attributes = await sdp_client.get_attributes( attributes = await sdp_client.get_attributes(
service_record_handle, [SDP_ALL_ATTRIBUTES_RANGE] service_record_handle, [SDP_ALL_ATTRIBUTES_RANGE]
) )
print(color(f'SERVICE {service_record_handle:04X} attributes:', 'yellow')) print(color('SERVICE {service_record_handle:04X} attributes:', 'yellow'))
print(color(f'SDP attributes for HID device', 'magenta')) print(color('SDP attributes for HID device', 'magenta'))
for attribute in attributes: for attribute in attributes:
if attribute.id == SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID: if attribute.id == SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID:
print( print(
@@ -287,7 +286,7 @@ async def main() -> None:
def on_hid_interrupt_data_cb(pdu: bytes): def on_hid_interrupt_data_cb(pdu: bytes):
report_type = pdu[0] & 0x0F report_type = pdu[0] & 0x0F
if len(pdu) == 1: if len(pdu) == 1:
print(color(f'Warning: No report received', 'yellow')) print(color('Warning: No report received', 'yellow'))
return return
report_length = len(pdu[1:]) report_length = len(pdu[1:])
report_id = pdu[1] report_id = pdu[1]

View File

@@ -55,7 +55,7 @@ from bumble.transport import open_transport
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def main() -> None: async def main() -> None:
if len(sys.argv) < 3: if len(sys.argv) < 3:
print('Usage: run_mcp_client.py <config-file>' '<transport-spec-for-device>') print('Usage: run_mcp_client.py <config-file><transport-spec-for-device>')
return return
print('<<< connecting to HCI...') print('<<< connecting to HCI...')

View File

@@ -28,9 +28,10 @@ from bumble.sdp import (
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
DataElement,
ServiceAttribute,
) )
from bumble.sdp import Client as SDP_Client from bumble.sdp import Client as SDP_Client
from bumble.sdp import DataElement, ServiceAttribute
from bumble.transport import open_transport from bumble.transport import open_transport

View File

@@ -57,7 +57,7 @@ def dumps_volume_state(volume_setting: int, muted: int, change_counter: int) ->
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def main() -> None: async def main() -> None:
if len(sys.argv) < 3: if len(sys.argv) < 3:
print('Usage: run_vcp_renderer.py <config-file>' '<transport-spec-for-device>') print('Usage: run_vcp_renderer.py <config-file><transport-spec-for-device>')
return return
print('<<< connecting to HCI...') print('<<< connecting to HCI...')

View File

@@ -48,12 +48,12 @@ development = [
"bt-test-interfaces >= 0.0.6", "bt-test-interfaces >= 0.0.6",
"grpcio-tools >= 1.62.1", "grpcio-tools >= 1.62.1",
"invoke >= 1.7.3", "invoke >= 1.7.3",
"isort ~= 5.13.2",
"mobly >= 1.12.2", "mobly >= 1.12.2",
"mypy == 1.12.0", "mypy == 1.12.0",
"nox >= 2022", "nox >= 2022",
"pylint == 3.3.1", "pylint == 3.3.1",
"pyyaml >= 6.0", "pyyaml >= 6.0",
"ruff == 0.14.10",
"types-appdirs >= 1.4.3", "types-appdirs >= 1.4.3",
"types-invoke >= 1.7.3", "types-invoke >= 1.7.3",
"types-protobuf >= 4.21.0", "types-protobuf >= 4.21.0",
@@ -208,3 +208,11 @@ ignore_missing_imports = true
[tool.isort] [tool.isort]
profile = "black" profile = "black"
skip = ["_version.py", "grpc_protobuf"] skip = ["_version.py", "grpc_protobuf"]
[tool.ruff]
exclude = [
"bumble/transport/grpc_protobuf"
]
[tool.ruff.format]
quote-style = "preserve"

View File

@@ -127,6 +127,9 @@ def lint(ctx, disable='C,R', errors_only=False):
print(f">>> Running the linter{qualifier}...") print(f">>> Running the linter{qualifier}...")
try: try:
print("+++ Checking with ruff...")
ctx.run("ruff check")
print("+++ Checking with pylint...")
ctx.run(f"pylint {' '.join(options)} bumble apps examples tasks.py") ctx.run(f"pylint {' '.join(options)} bumble apps examples tasks.py")
print("The linter is happy. ✅ 😊 🐝") print("The linter is happy. ✅ 😊 🐝")
except UnexpectedExit as exc: except UnexpectedExit as exc:
@@ -138,20 +141,24 @@ def lint(ctx, disable='C,R', errors_only=False):
@task @task
def format_code(ctx, check=False, diff=False): def format_code(ctx, check=False, diff=False):
options = []
if check:
options.append("--check")
if diff:
options.append("--diff")
print(">>> Sorting imports...") print(">>> Sorting imports...")
options = []
try: try:
ctx.run(f"isort {' '.join(options)} .") if diff:
options.append("--diff")
else:
options.append("--fix")
ctx.run(f"ruff check --select I {' '.join(options)} .")
except UnexpectedExit as exc: except UnexpectedExit as exc:
print("Please run 'invoke project.format' or 'isort .' to format the code. ❌") print("Please run 'invoke project.format' or 'isort .' to format the code. ❌")
raise Exit(code=1) from exc raise Exit(code=1) from exc
print(">>> Running the formatter...") print(">>> Running the formatter...")
options = []
if check:
options.append("--check")
if diff:
options.append("--diff")
try: try:
ctx.run(f"black -S {' '.join(options)} .") ctx.run(f"black -S {' '.join(options)} .")
except UnexpectedExit as exc: except UnexpectedExit as exc:

View File

@@ -301,9 +301,7 @@ async def test_pacs():
await devices.setup_connection() await devices.setup_connection()
peer = device.Peer(devices.connections[1]) peer = device.Peer(devices.connections[1])
pacs_client = await peer.discover_service_and_create_proxy( await peer.discover_service_and_create_proxy(PublishedAudioCapabilitiesServiceProxy)
PublishedAudioCapabilitiesServiceProxy
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -23,9 +23,10 @@ from unittest import mock
import pytest import pytest
from bumble import device, gatt, hci, utils from bumble import gatt, hci, utils
from bumble.core import PhysicalTransport from bumble.core import PhysicalTransport
from bumble.device import ( from bumble.device import (
Advertisement,
AdvertisingEventProperties, AdvertisingEventProperties,
AdvertisingParameters, AdvertisingParameters,
CigParameters, CigParameters,
@@ -146,7 +147,7 @@ async def test_device_connect_parallel():
) )
) )
assert (yield) == None assert (yield) is None
def d1_flow(): def d1_flow():
packet = HCI_Packet.from_bytes((yield)) packet = HCI_Packet.from_bytes((yield))
@@ -180,7 +181,7 @@ async def test_device_connect_parallel():
) )
) )
assert (yield) == None assert (yield) is None
def d2_flow(): def d2_flow():
packet = HCI_Packet.from_bytes((yield)) packet = HCI_Packet.from_bytes((yield))
@@ -214,7 +215,7 @@ async def test_device_connect_parallel():
) )
) )
assert (yield) == None assert (yield) is None
d0.host.set_packet_sink(Sink(d0_flow())) d0.host.set_packet_sink(Sink(d0_flow()))
d1.host.set_packet_sink(Sink(d1_flow())) d1.host.set_packet_sink(Sink(d1_flow()))
@@ -239,10 +240,10 @@ async def test_device_connect_parallel():
] ]
) )
assert type(c01) == Connection assert isinstance(c01, Connection)
assert type(c02) == Connection assert isinstance(c02, Connection)
assert type(a10) == Connection assert isinstance(a10, Connection)
assert type(a20) == Connection assert isinstance(a20, Connection)
assert c01.handle == a10.handle and c01.handle == 0x100 assert c01.handle == a10.handle and c01.handle == 0x100
assert c02.handle == a20.handle and c02.handle == 0x101 assert c02.handle == a20.handle and c02.handle == 0x101
@@ -317,7 +318,7 @@ async def test_advertising_and_scanning():
await dev.power_on() await dev.power_on()
# Start scanning # Start scanning
advertisements = asyncio.Queue[device.Advertisement]() advertisements = asyncio.Queue[Advertisement]()
devices[1].on(devices[1].EVENT_ADVERTISEMENT, advertisements.put_nowait) devices[1].on(devices[1].EVENT_ADVERTISEMENT, advertisements.put_nowait)
await devices[1].start_scanning() await devices[1].start_scanning()

View File

@@ -40,7 +40,7 @@ class Devices:
self.link = LocalLink() self.link = LocalLink()
addresses = [":".join([f"F{i}"] * 6) for i in range(num_devices)] addresses = [":".join([f"F{i}"] * 6) for i in range(num_devices)]
self.controllers = [ self.controllers = [
Controller(f'C{i+i}', link=self.link, public_address=addresses[i]) Controller(f'C{i + i}', link=self.link, public_address=addresses[i])
for i in range(num_devices) for i in range(num_devices)
] ]
self.devices = [ self.devices = [

View File

@@ -21,11 +21,11 @@ import urllib.error
import urllib.request import urllib.request
import click import click
from bumble.tools import rtk_util
import bumble.logging import bumble.logging
from bumble.colors import color from bumble.colors import color
from bumble.drivers import rtk from bumble.drivers import rtk
from bumble.tools import rtk_util
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging