mirror of
https://github.com/google/bumble.git
synced 2026-04-16 00:25:31 +00:00
use ruff for linting and import sorting
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
13
apps/pair.py
13
apps/pair.py
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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))}"
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()})',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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'')
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
3
bumble/vendor/android/hci.py
vendored
3
bumble/vendor/android/hci.py
vendored
@@ -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))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))}'
|
||||||
|
|||||||
@@ -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()}')
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
|
||||||
@@ -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...')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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...')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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...')
|
||||||
|
|||||||
@@ -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"
|
||||||
21
tasks.py
21
tasks.py
@@ -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:
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user