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

@@ -490,7 +490,7 @@ class VendorSpecificMediaCodecInformation(MediaCodecInformation):
'VendorSpecificMediaCodecInformation(',
f' vendor_id: {self.vendor_id:08X} ({name_or_number(COMPANY_IDENTIFIERS, self.vendor_id & 0xFFFF)})',
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:
token.extend(char)
if char == b'\"':
if char == b'"':
in_quotes = False
tokens.append(token[1:-1])
token = bytearray()

View File

@@ -258,7 +258,7 @@ class Protocol:
def send_ipid(self, transaction_label: int, pid: int) -> None:
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'')

View File

@@ -2011,7 +2011,7 @@ class Protocol(utils.EventEmitter):
f"{command} is not a valid AV/C Command Frame"
)
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

View File

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

View File

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

View File

@@ -25,10 +25,11 @@ try:
from bumble.crypto.cryptography import EccKey, aes_cmac, e
except ImportError:
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]
_EccKey = EccKey # For the linter only
# -----------------------------------------------------------------------------
# Logging

View File

@@ -85,7 +85,6 @@ class _AES:
# fmt: on
def __init__(self, key: bytes) -> None:
if len(key) not in (16, 24, 32):
raise core.InvalidArgumentError(f'Invalid key size {len(key)}')
@@ -112,7 +111,6 @@ class _AES:
r_con_pointer = 0
t = kc
while t < round_key_count:
tt = tk[kc - 1]
tk[0] ^= (
(self._S[(tt >> 16) & 0xFF] << 24)
@@ -269,7 +267,6 @@ class _ECB:
class _CBC:
def __init__(self, key: bytes, iv: bytes = bytes(16)) -> None:
if len(iv) != 16:
raise core.InvalidArgumentError(
@@ -302,7 +299,6 @@ class _CBC:
class _CMAC:
def __init__(
self,
key: bytes,
@@ -414,7 +410,6 @@ class _CMAC:
self._last_pt = _xor(second_last, data_block[-bs:])
def digest(self) -> bytes:
bs = self._block_size
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:
logger.debug(
f'advertising set {advertising_handle} '
f'terminated with status {status}'
f'advertising set {advertising_handle} terminated with status {status}'
)
return
@@ -5628,7 +5627,8 @@ class Device(utils.CompositeEventEmitter):
self.host.send_command_sync(
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:
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
@@ -483,9 +483,7 @@ class Driver(common.Driver):
raise DriverError("insufficient device info, missing CNVI or CNVR")
firmware_base_name = (
"ibt-"
f"{device_info[ValueType.CNVI]:04X}-"
f"{device_info[ValueType.CNVR]:04X}"
f"ibt-{device_info[ValueType.CNVI]:04X}-{device_info[ValueType.CNVR]:04X}"
)
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:
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

View File

@@ -361,5 +361,4 @@ class EnumCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T3]):
def decode_value(self, value: bytes) -> _T3:
int_value = int.from_bytes(value, self.byteorder)
a = 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):
@classmethod
def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'):
return {
@@ -147,7 +146,6 @@ class SpecableEnum(utils.OpenIntEnum):
class SpecableFlag(enum.IntFlag):
@classmethod
def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'):
return {
@@ -1786,20 +1784,20 @@ class HCI_Object:
@classmethod
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]]:
result = collections.OrderedDict[str, Any]()
for field in fields:
if isinstance(field, list):
for object_field in object_fields:
if isinstance(object_field, list):
# This is an array field, starting with a 1-byte item count.
item_count = data[offset]
offset += 1
# 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] = []
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(
data, offset, sub_field_type
)
@@ -1807,7 +1805,7 @@ class HCI_Object:
offset += size
continue
field_name, field_type = field
field_name, field_type = object_field
assert isinstance(field_name, str)
field_value, field_size = HCI_Object.parse_field(
data, offset, cast(FieldSpec, field_type)
@@ -1890,26 +1888,26 @@ class HCI_Object:
return field_bytes
@staticmethod
def dict_to_bytes(hci_object, fields):
def dict_to_bytes(hci_object, object_fields):
result = bytearray()
for field in fields:
if isinstance(field, list):
for object_field in object_fields:
if isinstance(object_field, list):
# 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
# 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(
b''.join(
HCI_Object.serialize_field(
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)
)
continue
(field_name, field_type) = field
(field_name, field_type) = object_field
result += HCI_Object.serialize_field(hci_object[field_name], field_type)
return bytes(result)
@@ -1967,15 +1965,15 @@ class HCI_Object:
)
@staticmethod
def format_fields(hci_object, fields, indentation='', value_mappers=None):
if not fields:
def format_fields(hci_object, object_fields, indentation='', value_mappers=None):
if not object_fields:
return ''
# Build array of formatted key:value pairs
field_strings = []
for field in fields:
if isinstance(field, list):
for sub_field in field:
for object_field in object_fields:
if isinstance(object_field, list):
for sub_field in object_field:
sub_field_name, sub_field_type = sub_field
item_count = len(hci_object[sub_field_name])
for i in range(item_count):
@@ -1993,7 +1991,7 @@ class HCI_Object:
)
continue
field_name, field_type = field
field_name, field_type = object_field
field_value = hci_object[field_name]
field_strings.append(
(
@@ -2016,16 +2014,16 @@ class HCI_Object:
@classmethod
def fields_from_dataclass(cls, obj: Any) -> 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.
if not isinstance(
(metadata := field.metadata.get("bumble.hci")), FieldMetadata
(metadata := object_field.metadata.get("bumble.hci")), FieldMetadata
):
continue
if metadata.list_begin:
stack.append([])
if metadata.spec:
stack[-1].append((field.name, metadata.spec))
stack[-1].append((object_field.name, metadata.spec))
if metadata.list_end:
top = stack.pop()
stack[-1].append(top)

View File

@@ -597,7 +597,7 @@ class AgIndicatorState:
supported_values_text = (
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
def call(cls: type[Self]) -> Self:
@@ -1351,7 +1351,7 @@ class AgProtocol(utils.EventEmitter):
logger.warning(f'AG indicator {indicator} is disabled')
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:
"""Starts codec negotiation."""
@@ -1417,7 +1417,7 @@ class AgProtocol(utils.EventEmitter):
operation_code = operation_code[:1] + b'x'
try:
operation = CallHoldOperation(operation_code.decode())
except:
except Exception:
logger.error(f'Invalid operation: {operation_code.decode()}')
self.send_cme_error(CmeError.OPERATION_NOT_SUPPORTED)
return
@@ -1589,7 +1589,7 @@ class AgProtocol(utils.EventEmitter):
def _on_clcc(self) -> None:
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 ''
response = (
f'+CLCC: {call.index}'

View File

@@ -108,8 +108,7 @@ class DataPacketQueue(utils.EventEmitter):
if self._packets:
logger.debug(
f'{self._in_flight} packets in flight, '
f'{len(self._packets)} in queue'
f'{self._in_flight} packets in flight, {len(self._packets)} in queue'
)
def flush(self, connection_handle: int) -> None:
@@ -1394,8 +1393,7 @@ class Host(utils.EventEmitter):
if event.status == hci.HCI_SUCCESS:
# Create/update the connection
logger.debug(
f'### SCO CONNECTION: [0x{event.connection_handle:04X}] '
f'{event.bd_addr}'
f'### SCO CONNECTION: [0x{event.connection_handle:04X}] {event.bd_addr}'
)
self.sco_links[event.connection_handle] = ScoLink(
@@ -1447,7 +1445,7 @@ class Host(utils.EventEmitter):
def on_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')
return

View File

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

View File

@@ -74,7 +74,9 @@ class PandoraDevice:
# open HCI transport & set device host.
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.
await self.device.power_on()

View File

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

View File

@@ -24,9 +24,9 @@ from typing import AsyncGenerator, Optional, Union
import grpc
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_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 (
COMMAND_NOT_UNDERSTOOD,
INVALID_CID_IN_REQUEST,
ConnectRequest,
ConnectResponse,
CreditBasedChannelRequest,
@@ -41,6 +41,7 @@ from pandora.l2cap_pb2 import (
WaitDisconnectionRequest,
WaitDisconnectionResponse,
)
from pandora.l2cap_pb2 import Channel as PandoraChannel # pytype: disable=pyi-error
from bumble.core import InvalidArgumentError, OutOfResourcesError
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
import grpc
from google.protobuf import any_pb2 # pytype: disable=pyi-error
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
from google.protobuf import wrappers_pb2 # pytype: disable=pyi-error
from google.protobuf import (
any_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.security_grpc_aio import SecurityServicer, SecurityStorageServicer
from pandora.security_pb2 import (
@@ -455,7 +457,7 @@ class SecurityService(SecurityServicer):
def pair(*_: Any) -> None:
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]]]] = {
'disconnection': set_failure('connection_died'),

View File

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

View File

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

View File

@@ -137,7 +137,7 @@ class Metadata:
values.append(str(decoded))
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)
)

View File

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

View File

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

View File

@@ -284,7 +284,9 @@ async def open_pyusb_transport(spec: str) -> Transport:
device = await _power_cycle(device) # type: ignore
except Exception as e:
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
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
return usb.core.find(idVendor=device.idVendor, idProduct=device.idProduct) # type: ignore
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

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
'''
return_parameters_fields = [
('status', hci.STATUS_SPEC),
('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
'''
return_parameters_fields = [
('status', hci.STATUS_SPEC),
('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
'''
quality_report_id: int = field(metadata=hci.metadata(1))
packet_types: int = field(metadata=hci.metadata(1))
connection_handle: int = field(metadata=hci.metadata(2))