Compare commits

..

1 Commits

Author SHA1 Message Date
Charlie Boutier 7237619d3b A2DP example: Codec selection based on file type
Currently support SBC and AAC
2025-05-08 14:24:42 -07:00
155 changed files with 5952 additions and 6561 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install ".[build,test,development]" python -m pip install ".[build,examples,test,development]"
- name: Check - name: Check
run: | run: |
invoke project.pre-commit invoke project.pre-commit
+3
View File
@@ -12,6 +12,9 @@ Apps
## `show.py` ## `show.py`
Parse a file with HCI packets and print the details of each packet in a human readable form Parse a file with HCI packets and print the details of each packet in a human readable form
## `link_relay.py`
Simple WebSocket relay for virtual RemoteLink instances to communicate with each other through.
## `hci_bridge.py` ## `hci_bridge.py`
This app acts as a simple bridge between two HCI transports, with a host on one side and This app acts as a simple bridge between two HCI transports, with a host on one side and
a controller on the other. All the HCI packets bridged between the two are printed on the console a controller on the other. All the HCI packets bridged between the two are printed on the console
+9 -11
View File
@@ -29,7 +29,9 @@ from typing import (
Any, Any,
AsyncGenerator, AsyncGenerator,
Coroutine, Coroutine,
Deque,
Optional, Optional,
Tuple,
) )
import click import click
@@ -128,8 +130,8 @@ class BroadcastScanner(bumble.utils.EventEmitter):
broadcast_audio_announcement: Optional[bap.BroadcastAudioAnnouncement] = None broadcast_audio_announcement: Optional[bap.BroadcastAudioAnnouncement] = None
basic_audio_announcement: Optional[bap.BasicAudioAnnouncement] = None basic_audio_announcement: Optional[bap.BasicAudioAnnouncement] = None
appearance: Optional[core.Appearance] = None appearance: Optional[core.Appearance] = None
biginfo: Optional[bumble.device.BigInfoAdvertisement] = None biginfo: Optional[bumble.device.BIGInfoAdvertisement] = None
manufacturer_data: Optional[tuple[str, bytes]] = None manufacturer_data: Optional[Tuple[str, bytes]] = None
def __post_init__(self) -> None: def __post_init__(self) -> None:
super().__init__() super().__init__()
@@ -255,10 +257,8 @@ class BroadcastScanner(bumble.utils.EventEmitter):
print(color(' SDU Interval: ', 'magenta'), self.biginfo.sdu_interval) print(color(' SDU Interval: ', 'magenta'), self.biginfo.sdu_interval)
print(color(' Max SDU: ', 'magenta'), self.biginfo.max_sdu) print(color(' Max SDU: ', 'magenta'), self.biginfo.max_sdu)
print(color(' PHY: ', 'magenta'), self.biginfo.phy.name) print(color(' PHY: ', 'magenta'), self.biginfo.phy.name)
print(color(' Framing: ', 'magenta'), self.biginfo.framing.name) print(color(' Framed: ', 'magenta'), self.biginfo.framed)
print( print(color(' Encrypted: ', 'magenta'), self.biginfo.encrypted)
color(' Encryption: ', 'magenta'), self.biginfo.encryption.name
)
def on_sync_establishment(self) -> None: def on_sync_establishment(self) -> None:
self.emit('sync_establishment') self.emit('sync_establishment')
@@ -288,7 +288,7 @@ class BroadcastScanner(bumble.utils.EventEmitter):
self.emit('change') self.emit('change')
def on_biginfo_advertisement( def on_biginfo_advertisement(
self, advertisement: bumble.device.BigInfoAdvertisement self, advertisement: bumble.device.BIGInfoAdvertisement
) -> None: ) -> None:
self.biginfo = advertisement self.biginfo = advertisement
self.emit('change') self.emit('change')
@@ -748,9 +748,7 @@ async def run_receive(
sample_rate_hz=sampling_frequency.hz, sample_rate_hz=sampling_frequency.hz,
num_channels=num_bis, num_channels=num_bis,
) )
lc3_queues: list[collections.deque[bytes]] = [ lc3_queues: list[Deque[bytes]] = [collections.deque() for i in range(num_bis)]
collections.deque() for i in range(num_bis)
]
packet_stats = [0, 0] packet_stats = [0, 0]
audio_output = await audio_io.create_audio_output(output) audio_output = await audio_io.create_audio_output(output)
@@ -766,7 +764,7 @@ async def run_receive(
) )
) )
def sink(queue: collections.deque[bytes], packet: hci.HCI_IsoDataPacket): def sink(queue: Deque[bytes], packet: hci.HCI_IsoDataPacket):
# TODO: re-assemble fragments and detect errors # TODO: re-assemble fragments and detect errors
queue.append(packet.iso_sdu_fragment) queue.append(packet.iso_sdu_fragment)
+31 -480
View File
@@ -23,7 +23,6 @@ import os
import statistics import statistics
import struct import struct
import time import time
from typing import Optional
import click import click
@@ -36,15 +35,7 @@ from bumble.core import (
CommandTimeoutError, CommandTimeoutError,
) )
from bumble.colors import color from bumble.colors import color
from bumble.core import ConnectionPHY from bumble.device import Connection, ConnectionParametersPreferences, Device, Peer
from bumble.device import (
CigParameters,
CisLink,
Connection,
ConnectionParametersPreferences,
Device,
Peer,
)
from bumble.gatt import Characteristic, CharacteristicValue, Service from bumble.gatt import Characteristic, CharacteristicValue, Service
from bumble.hci import ( from bumble.hci import (
HCI_LE_1M_PHY, HCI_LE_1M_PHY,
@@ -54,7 +45,6 @@ from bumble.hci import (
HCI_Constant, HCI_Constant,
HCI_Error, HCI_Error,
HCI_StatusError, HCI_StatusError,
HCI_IsoDataPacket,
) )
from bumble.sdp import ( from bumble.sdp import (
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID, SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
@@ -65,7 +55,7 @@ from bumble.sdp import (
DataElement, DataElement,
ServiceAttribute, ServiceAttribute,
) )
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
import bumble.rfcomm import bumble.rfcomm
import bumble.core import bumble.core
from bumble.utils import AsyncRunner from bumble.utils import AsyncRunner
@@ -85,28 +75,17 @@ DEFAULT_CENTRAL_ADDRESS = 'F0:F0:F0:F0:F0:F0'
DEFAULT_CENTRAL_NAME = 'Speed Central' DEFAULT_CENTRAL_NAME = 'Speed Central'
DEFAULT_PERIPHERAL_ADDRESS = 'F1:F1:F1:F1:F1:F1' DEFAULT_PERIPHERAL_ADDRESS = 'F1:F1:F1:F1:F1:F1'
DEFAULT_PERIPHERAL_NAME = 'Speed Peripheral' DEFAULT_PERIPHERAL_NAME = 'Speed Peripheral'
DEFAULT_ADVERTISING_INTERVAL = 100
SPEED_SERVICE_UUID = '50DB505C-8AC4-4738-8448-3B1D9CC09CC5' SPEED_SERVICE_UUID = '50DB505C-8AC4-4738-8448-3B1D9CC09CC5'
SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53' SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53'
SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D' SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D'
DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE' DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'
DEFAULT_L2CAP_PSM = 128 DEFAULT_L2CAP_PSM = 128
DEFAULT_L2CAP_MAX_CREDITS = 128 DEFAULT_L2CAP_MAX_CREDITS = 128
DEFAULT_L2CAP_MTU = 1024 DEFAULT_L2CAP_MTU = 1024
DEFAULT_L2CAP_MPS = 1024 DEFAULT_L2CAP_MPS = 1024
DEFAULT_ISO_MAX_SDU_C_TO_P = 251
DEFAULT_ISO_MAX_SDU_P_TO_C = 251
DEFAULT_ISO_SDU_INTERVAL_C_TO_P = 10000
DEFAULT_ISO_SDU_INTERVAL_P_TO_C = 10000
DEFAULT_ISO_MAX_TRANSPORT_LATENCY_C_TO_P = 35
DEFAULT_ISO_MAX_TRANSPORT_LATENCY_P_TO_C = 35
DEFAULT_ISO_RTN_C_TO_P = 3
DEFAULT_ISO_RTN_P_TO_C = 3
DEFAULT_LINGER_TIME = 1.0 DEFAULT_LINGER_TIME = 1.0
DEFAULT_POST_CONNECTION_WAIT_TIME = 1.0 DEFAULT_POST_CONNECTION_WAIT_TIME = 1.0
@@ -123,14 +102,14 @@ def le_phy_name(phy_id):
) )
def print_connection_phy(phy: ConnectionPHY) -> None: def print_connection_phy(phy):
logging.info( logging.info(
color('@@@ PHY: ', 'yellow') + f'TX:{le_phy_name(phy.tx_phy)}/' color('@@@ PHY: ', 'yellow') + f'TX:{le_phy_name(phy.tx_phy)}/'
f'RX:{le_phy_name(phy.rx_phy)}' f'RX:{le_phy_name(phy.rx_phy)}'
) )
def print_connection(connection: Connection) -> None: def print_connection(connection):
params = [] params = []
if connection.transport == PhysicalTransport.LE: if connection.transport == PhysicalTransport.LE:
params.append( params.append(
@@ -155,34 +134,6 @@ def print_connection(connection: Connection) -> None:
logging.info(color('@@@ Connection: ', 'yellow') + ' '.join(params)) logging.info(color('@@@ Connection: ', 'yellow') + ' '.join(params))
def print_cis_link(cis_link: CisLink) -> None:
logging.info(color("@@@ CIS established", "green"))
logging.info(color('@@@ ISO interval: ', 'green') + f"{cis_link.iso_interval}ms")
logging.info(color('@@@ NSE: ', 'green') + f"{cis_link.nse}")
logging.info(color('@@@ Central->Peripheral:', 'green'))
if cis_link.phy_c_to_p is not None:
logging.info(
color('@@@ PHY: ', 'green') + f"{cis_link.phy_c_to_p.name}"
)
logging.info(
color('@@@ Latency: ', 'green') + f"{cis_link.transport_latency_c_to_p}µs"
)
logging.info(color('@@@ BN: ', 'green') + f"{cis_link.bn_c_to_p}")
logging.info(color('@@@ FT: ', 'green') + f"{cis_link.ft_c_to_p}")
logging.info(color('@@@ Max PDU: ', 'green') + f"{cis_link.max_pdu_c_to_p}")
logging.info(color('@@@ Peripheral->Central:', 'green'))
if cis_link.phy_p_to_c is not None:
logging.info(
color('@@@ PHY: ', 'green') + f"{cis_link.phy_p_to_c.name}"
)
logging.info(
color('@@@ Latency: ', 'green') + f"{cis_link.transport_latency_p_to_c}µs"
)
logging.info(color('@@@ BN: ', 'green') + f"{cis_link.bn_p_to_c}")
logging.info(color('@@@ FT: ', 'green') + f"{cis_link.ft_p_to_c}")
logging.info(color('@@@ Max PDU: ', 'green') + f"{cis_link.max_pdu_p_to_c}")
def make_sdp_records(channel): def make_sdp_records(channel):
return { return {
0x00010001: [ 0x00010001: [
@@ -246,51 +197,6 @@ async def switch_roles(connection, role):
logging.info(f'{color("### Role switch failed:", "red")} {error}') logging.info(f'{color("### Role switch failed:", "red")} {error}')
async def pre_power_on(device: Device, classic: bool) -> None:
device.classic_enabled = classic
# Set up a pairing config factory with minimal requirements.
device.config.keystore = "JsonKeyStore"
device.pairing_config_factory = lambda _: PairingConfig(
sc=False, mitm=False, bonding=False
)
async def post_power_on(
device: Device,
le_scan: Optional[tuple[int, int]],
le_advertise: Optional[int],
classic_page_scan: bool,
classic_inquiry_scan: bool,
) -> None:
if classic_page_scan:
logging.info(color("*** Enabling page scan", "blue"))
await device.set_connectable(True)
if classic_inquiry_scan:
logging.info(color("*** Enabling inquiry scan", "blue"))
await device.set_discoverable(True)
if le_scan:
scan_window, scan_interval = le_scan
logging.info(
color(
f"*** Starting LE scanning [{scan_window}ms/{scan_interval}ms]",
"blue",
)
)
await device.start_scanning(
scan_interval=scan_interval, scan_window=scan_window
)
if le_advertise:
logging.info(color(f"*** Starting LE advertising [{le_advertise}ms]", "blue"))
await device.start_advertising(
advertising_interval_min=le_advertise,
advertising_interval_max=le_advertise,
auto_restart=True,
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Packet # Packet
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -508,8 +414,7 @@ class Sender:
self.bytes_sent += len(packet) self.bytes_sent += len(packet)
await self.packet_io.send_packet(packet) await self.packet_io.send_packet(packet)
if self.packet_io.can_receive(): await self.done.wait()
await self.done.wait()
run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else '' run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
logging.info(color(f'=== {run_counter} Done!', 'magenta')) logging.info(color(f'=== {run_counter} Done!', 'magenta'))
@@ -539,9 +444,6 @@ class Sender:
) )
self.done.set() self.done.set()
def is_sender(self):
return True
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Receiver # Receiver
@@ -589,8 +491,7 @@ class Receiver:
logging.info( logging.info(
color( color(
f'!!! Unexpected packet, expected {self.expected_packet_index} ' f'!!! Unexpected packet, expected {self.expected_packet_index} '
f'but received {packet.sequence}', f'but received {packet.sequence}'
'red',
) )
) )
@@ -633,9 +534,6 @@ class Receiver:
await self.done.wait() await self.done.wait()
logging.info(color('=== Done!', 'magenta')) logging.info(color('=== Done!', 'magenta'))
def is_sender(self):
return False
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Ping # Ping
@@ -771,8 +669,7 @@ class Ping:
color( color(
f'!!! Unexpected packet, ' f'!!! Unexpected packet, '
f'expected {self.next_expected_packet_index} ' f'expected {self.next_expected_packet_index} '
f'but received {packet.sequence}', f'but received {packet.sequence}'
'red',
) )
) )
@@ -780,9 +677,6 @@ class Ping:
self.done.set() self.done.set()
return return
def is_sender(self):
return True
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Pong # Pong
@@ -827,8 +721,7 @@ class Pong:
logging.info( logging.info(
color( color(
f'!!! Unexpected packet, expected {self.expected_packet_index} ' f'!!! Unexpected packet, expected {self.expected_packet_index} '
f'but received {packet.sequence}', f'but received {packet.sequence}'
'red',
) )
) )
@@ -850,9 +743,6 @@ class Pong:
await self.done.wait() await self.done.wait()
logging.info(color('=== Done!', 'magenta')) logging.info(color('=== Done!', 'magenta'))
def is_sender(self):
return False
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# GattClient # GattClient
@@ -1016,9 +906,6 @@ class StreamedPacketIO:
# pylint: disable-next=not-callable # pylint: disable-next=not-callable
self.io_sink(struct.pack('>H', len(packet)) + packet) self.io_sink(struct.pack('>H', len(packet)) + packet)
def can_receive(self):
return True
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# L2capClient # L2capClient
@@ -1290,96 +1177,6 @@ class RfcommServer(StreamedPacketIO):
await self.dlc.drain() await self.dlc.drain()
# -----------------------------------------------------------------------------
# IsoClient
# -----------------------------------------------------------------------------
class IsoClient(StreamedPacketIO):
def __init__(
self,
device: Device,
) -> None:
super().__init__()
self.device = device
self.ready = asyncio.Event()
self.cis_link: Optional[CisLink] = None
async def on_connection(
self, connection: Connection, cis_link: CisLink, sender: bool
) -> None:
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
self.cis_link = cis_link
self.io_sink = cis_link.write
await cis_link.setup_data_path(
cis_link.Direction.HOST_TO_CONTROLLER
if sender
else cis_link.Direction.CONTROLLER_TO_HOST
)
cis_link.sink = self.on_iso_packet
self.ready.set()
def on_iso_packet(self, iso_packet: HCI_IsoDataPacket) -> None:
self.on_packet(iso_packet.iso_sdu_fragment)
def on_disconnection(self, _):
pass
async def drain(self):
if self.cis_link is None:
return
await self.cis_link.drain()
def can_receive(self):
return False
# -----------------------------------------------------------------------------
# IsoServer
# -----------------------------------------------------------------------------
class IsoServer(StreamedPacketIO):
def __init__(
self,
device: Device,
):
super().__init__()
self.device = device
self.cis_link: Optional[CisLink] = None
self.ready = asyncio.Event()
logging.info(
color(
'### Listening for ISO connection',
'yellow',
)
)
async def on_connection(
self, connection: Connection, cis_link: CisLink, sender: bool
) -> None:
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
self.io_sink = cis_link.write
await cis_link.setup_data_path(
cis_link.Direction.HOST_TO_CONTROLLER
if sender
else cis_link.Direction.CONTROLLER_TO_HOST
)
cis_link.sink = self.on_iso_packet
self.ready.set()
def on_iso_packet(self, iso_packet: HCI_IsoDataPacket) -> None:
self.on_packet(iso_packet.iso_sdu_fragment)
def on_disconnection(self, _):
pass
async def drain(self):
if self.cis_link is None:
return
await self.cis_link.drain()
def can_receive(self):
return False
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Central # Central
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -1388,52 +1185,26 @@ class Central(Connection.Listener):
self, self,
transport, transport,
peripheral_address, peripheral_address,
classic,
scenario_factory, scenario_factory,
mode_factory, mode_factory,
connection_interval, connection_interval,
phy, phy,
authenticate, authenticate,
encrypt, encrypt,
iso,
iso_sdu_interval_c_to_p,
iso_sdu_interval_p_to_c,
iso_max_sdu_c_to_p,
iso_max_sdu_p_to_c,
iso_max_transport_latency_c_to_p,
iso_max_transport_latency_p_to_c,
iso_rtn_c_to_p,
iso_rtn_p_to_c,
classic,
extended_data_length, extended_data_length,
role_switch, role_switch,
le_scan,
le_advertise,
classic_page_scan,
classic_inquiry_scan,
): ):
super().__init__() super().__init__()
self.transport = transport self.transport = transport
self.peripheral_address = peripheral_address self.peripheral_address = peripheral_address
self.classic = classic self.classic = classic
self.iso = iso
self.iso_sdu_interval_c_to_p = iso_sdu_interval_c_to_p
self.iso_sdu_interval_p_to_c = iso_sdu_interval_p_to_c
self.iso_max_sdu_c_to_p = iso_max_sdu_c_to_p
self.iso_max_sdu_p_to_c = iso_max_sdu_p_to_c
self.iso_max_transport_latency_c_to_p = iso_max_transport_latency_c_to_p
self.iso_max_transport_latency_p_to_c = iso_max_transport_latency_p_to_c
self.iso_rtn_c_to_p = iso_rtn_c_to_p
self.iso_rtn_p_to_c = iso_rtn_p_to_c
self.scenario_factory = scenario_factory self.scenario_factory = scenario_factory
self.mode_factory = mode_factory self.mode_factory = mode_factory
self.authenticate = authenticate self.authenticate = authenticate
self.encrypt = encrypt or authenticate self.encrypt = encrypt or authenticate
self.extended_data_length = extended_data_length self.extended_data_length = extended_data_length
self.role_switch = role_switch self.role_switch = role_switch
self.le_scan = le_scan
self.le_advertise = le_advertise
self.classic_page_scan = classic_page_scan
self.classic_inquiry_scan = classic_inquiry_scan
self.device = None self.device = None
self.connection = None self.connection = None
@@ -1470,7 +1241,7 @@ class Central(Connection.Listener):
async def run(self): async def run(self):
logging.info(color('>>> Connecting to HCI...', 'green')) logging.info(color('>>> Connecting to HCI...', 'green'))
async with await open_transport(self.transport) as ( async with await open_transport_or_link(self.transport) as (
hci_source, hci_source,
hci_sink, hci_sink,
): ):
@@ -1483,22 +1254,18 @@ class Central(Connection.Listener):
mode = self.mode_factory(self.device) mode = self.mode_factory(self.device)
scenario = self.scenario_factory(mode) scenario = self.scenario_factory(mode)
self.device.classic_enabled = self.classic self.device.classic_enabled = self.classic
self.device.cis_enabled = self.iso
# Set up a pairing config factory with minimal requirements. # Set up a pairing config factory with minimal requirements.
self.device.config.keystore = "JsonKeyStore"
self.device.pairing_config_factory = lambda _: PairingConfig( self.device.pairing_config_factory = lambda _: PairingConfig(
sc=False, mitm=False, bonding=False sc=False, mitm=False, bonding=False
) )
await pre_power_on(self.device, self.classic)
await self.device.power_on() await self.device.power_on()
await post_power_on(
self.device, if self.classic:
self.le_scan, await self.device.set_discoverable(False)
self.le_advertise, await self.device.set_connectable(False)
self.classic_page_scan,
self.classic_inquiry_scan,
)
logging.info( logging.info(
color(f'### Connecting to {self.peripheral_address}...', 'cyan') color(f'### Connecting to {self.peripheral_address}...', 'cyan')
@@ -1573,72 +1340,7 @@ class Central(Connection.Listener):
) )
) )
# Setup ISO streams. await mode.on_connection(self.connection)
if self.iso:
if scenario.is_sender():
sdu_interval_c_to_p = (
self.iso_sdu_interval_c_to_p or DEFAULT_ISO_SDU_INTERVAL_C_TO_P
)
sdu_interval_p_to_c = self.iso_sdu_interval_p_to_c or 0
max_transport_latency_c_to_p = (
self.iso_max_transport_latency_c_to_p
or DEFAULT_ISO_MAX_TRANSPORT_LATENCY_C_TO_P
)
max_transport_latency_p_to_c = (
self.iso_max_transport_latency_p_to_c or 0
)
max_sdu_c_to_p = (
self.iso_max_sdu_c_to_p or DEFAULT_ISO_MAX_SDU_C_TO_P
)
max_sdu_p_to_c = self.iso_max_sdu_p_to_c or 0
rtn_c_to_p = self.iso_rtn_c_to_p or DEFAULT_ISO_RTN_C_TO_P
rtn_p_to_c = self.iso_rtn_p_to_c or 0
else:
sdu_interval_p_to_c = (
self.iso_sdu_interval_p_to_c or DEFAULT_ISO_SDU_INTERVAL_P_TO_C
)
sdu_interval_c_to_p = self.iso_sdu_interval_c_to_p or 0
max_transport_latency_p_to_c = (
self.iso_max_transport_latency_p_to_c
or DEFAULT_ISO_MAX_TRANSPORT_LATENCY_P_TO_C
)
max_transport_latency_c_to_p = (
self.iso_max_transport_latency_c_to_p or 0
)
max_sdu_p_to_c = (
self.iso_max_sdu_p_to_c or DEFAULT_ISO_MAX_SDU_P_TO_C
)
max_sdu_c_to_p = self.iso_max_sdu_c_to_p or 0
rtn_p_to_c = self.iso_rtn_p_to_c or DEFAULT_ISO_RTN_P_TO_C
rtn_c_to_p = self.iso_rtn_c_to_p or 0
cis_handles = await self.device.setup_cig(
CigParameters(
cig_id=1,
sdu_interval_c_to_p=sdu_interval_c_to_p,
sdu_interval_p_to_c=sdu_interval_p_to_c,
max_transport_latency_c_to_p=max_transport_latency_c_to_p,
max_transport_latency_p_to_c=max_transport_latency_p_to_c,
cis_parameters=[
CigParameters.CisParameters(
cis_id=2,
max_sdu_c_to_p=max_sdu_c_to_p,
max_sdu_p_to_c=max_sdu_p_to_c,
rtn_c_to_p=rtn_c_to_p,
rtn_p_to_c=rtn_p_to_c,
)
],
)
)
cis_link = (
await self.device.create_cis([(cis_handles[0], self.connection)])
)[0]
print_cis_link(cis_link)
await mode.on_connection(
self.connection, cis_link, scenario.is_sender()
)
else:
await mode.on_connection(self.connection)
await scenario.run() await scenario.run()
await asyncio.sleep(DEFAULT_LINGER_TIME) await asyncio.sleep(DEFAULT_LINGER_TIME)
@@ -1674,38 +1376,24 @@ class Peripheral(Device.Listener, Connection.Listener):
scenario_factory, scenario_factory,
mode_factory, mode_factory,
classic, classic,
iso,
extended_data_length, extended_data_length,
role_switch, role_switch,
le_scan,
le_advertise,
classic_page_scan,
classic_inquiry_scan,
): ):
self.transport = transport self.transport = transport
self.classic = classic self.classic = classic
self.iso = iso
self.scenario_factory = scenario_factory self.scenario_factory = scenario_factory
self.mode_factory = mode_factory self.mode_factory = mode_factory
self.extended_data_length = extended_data_length self.extended_data_length = extended_data_length
self.role_switch = role_switch self.role_switch = role_switch
self.le_scan = le_scan
self.classic_page_scan = classic_page_scan
self.classic_inquiry_scan = classic_inquiry_scan
self.scenario = None self.scenario = None
self.mode = None self.mode = None
self.device = None self.device = None
self.connection = None self.connection = None
self.connected = asyncio.Event() self.connected = asyncio.Event()
if le_advertise:
self.le_advertise = le_advertise
else:
self.le_advertise = 0 if classic else DEFAULT_ADVERTISING_INTERVAL
async def run(self): async def run(self):
logging.info(color('>>> Connecting to HCI...', 'green')) logging.info(color('>>> Connecting to HCI...', 'green'))
async with await open_transport(self.transport) as ( async with await open_transport_or_link(self.transport) as (
hci_source, hci_source,
hci_sink, hci_sink,
): ):
@@ -1719,22 +1407,20 @@ class Peripheral(Device.Listener, Connection.Listener):
self.mode = self.mode_factory(self.device) self.mode = self.mode_factory(self.device)
self.scenario = self.scenario_factory(self.mode) self.scenario = self.scenario_factory(self.mode)
self.device.classic_enabled = self.classic self.device.classic_enabled = self.classic
self.device.cis_enabled = self.iso
# Set up a pairing config factory with minimal requirements. # Set up a pairing config factory with minimal requirements.
self.device.config.keystore = "JsonKeyStore"
self.device.pairing_config_factory = lambda _: PairingConfig( self.device.pairing_config_factory = lambda _: PairingConfig(
sc=False, mitm=False, bonding=False sc=False, mitm=False, bonding=False
) )
await pre_power_on(self.device, self.classic)
await self.device.power_on() await self.device.power_on()
await post_power_on(
self.device, if self.classic:
self.le_scan, await self.device.set_discoverable(True)
self.le_advertise, await self.device.set_connectable(True)
self.classic or self.classic_page_scan, else:
self.classic or self.classic_inquiry_scan, await self.device.start_advertising(auto_restart=True)
)
if self.classic: if self.classic:
logging.info( logging.info(
@@ -1756,21 +1442,7 @@ class Peripheral(Device.Listener, Connection.Listener):
logging.info(color('### Connected', 'cyan')) logging.info(color('### Connected', 'cyan'))
print_connection(self.connection) print_connection(self.connection)
if self.iso: await self.mode.on_connection(self.connection)
async def on_cis_request(cis_link: CisLink) -> None:
logging.info(color("@@@ Accepting CIS", "green"))
await self.device.accept_cis_request(cis_link)
print_cis_link(cis_link)
await self.mode.on_connection(
self.connection, cis_link, self.scenario.is_sender()
)
self.connection.on(self.connection.EVENT_CIS_REQUEST, on_cis_request)
else:
await self.mode.on_connection(self.connection)
await self.scenario.run() await self.scenario.run()
await asyncio.sleep(DEFAULT_LINGER_TIME) await asyncio.sleep(DEFAULT_LINGER_TIME)
@@ -1779,14 +1451,10 @@ class Peripheral(Device.Listener, Connection.Listener):
self.connection = connection self.connection = connection
self.connected.set() self.connected.set()
# Stop being discoverable and connectable if possible # Stop being discoverable and connectable
if self.classic: if self.classic:
if not self.classic_inquiry_scan: AsyncRunner.spawn(self.device.set_discoverable(False))
logging.info(color("*** Stopping inquiry scan", "blue")) AsyncRunner.spawn(self.device.set_connectable(False))
AsyncRunner.spawn(self.device.set_discoverable(False))
if not self.classic_page_scan:
logging.info(color("*** Stopping page scan", "blue"))
AsyncRunner.spawn(self.device.set_connectable(False))
# Request a new data length if needed # Request a new data length if needed
if not self.classic and self.extended_data_length: if not self.classic and self.extended_data_length:
@@ -1807,9 +1475,7 @@ class Peripheral(Device.Listener, Connection.Listener):
self.scenario.reset() self.scenario.reset()
if self.classic: if self.classic:
logging.info(color("*** Enabling inquiry scan", "blue"))
AsyncRunner.spawn(self.device.set_discoverable(True)) AsyncRunner.spawn(self.device.set_discoverable(True))
logging.info(color("*** Enabling page scan", "blue"))
AsyncRunner.spawn(self.device.set_connectable(True)) AsyncRunner.spawn(self.device.set_connectable(True))
def on_connection_parameters_update(self): def on_connection_parameters_update(self):
@@ -1882,12 +1548,6 @@ def create_mode_factory(ctx, default_mode):
credits_threshold=ctx.obj['rfcomm_credits_threshold'], credits_threshold=ctx.obj['rfcomm_credits_threshold'],
) )
if mode == 'iso-server':
return IsoServer(device)
if mode == 'iso-client':
return IsoClient(device)
raise ValueError('invalid mode') raise ValueError('invalid mode')
return create_mode return create_mode
@@ -1915,9 +1575,6 @@ def create_scenario_factory(ctx, default_scenario):
return Receiver(packet_io, ctx.obj['linger']) return Receiver(packet_io, ctx.obj['linger'])
if scenario == 'ping': if scenario == 'ping':
if isinstance(packet_io, (IsoClient, IsoServer)):
raise ValueError('ping not supported with ISO')
return Ping( return Ping(
packet_io, packet_io,
start_delay=ctx.obj['start_delay'], start_delay=ctx.obj['start_delay'],
@@ -1929,9 +1586,6 @@ def create_scenario_factory(ctx, default_scenario):
) )
if scenario == 'pong': if scenario == 'pong':
if isinstance(packet_io, (IsoClient, IsoServer)):
raise ValueError('pong not supported with ISO')
return Pong(packet_io, ctx.obj['linger']) return Pong(packet_io, ctx.obj['linger'])
raise ValueError('invalid scenario') raise ValueError('invalid scenario')
@@ -1955,8 +1609,6 @@ def create_scenario_factory(ctx, default_scenario):
'l2cap-server', 'l2cap-server',
'rfcomm-client', 'rfcomm-client',
'rfcomm-server', 'rfcomm-server',
'iso-client',
'iso-server',
] ]
), ),
) )
@@ -1969,7 +1621,6 @@ def create_scenario_factory(ctx, default_scenario):
) )
@click.option( @click.option(
'--extended-data-length', '--extended-data-length',
metavar='<TX-OCTETS>/<TX-TIME>',
help='Request a data length upon connection, specified as tx_octets/tx_time', help='Request a data length upon connection, specified as tx_octets/tx_time',
) )
@click.option( @click.option(
@@ -1977,26 +1628,6 @@ def create_scenario_factory(ctx, default_scenario):
type=click.Choice(['central', 'peripheral']), type=click.Choice(['central', 'peripheral']),
help='Request role switch upon connection (central or peripheral)', help='Request role switch upon connection (central or peripheral)',
) )
@click.option(
'--le-scan',
metavar='<WINDOW>/<INTERVAL>',
help='Perform an LE scan with a given window and interval (milliseconds)',
)
@click.option(
'--le-advertise',
metavar='<INTERVAL>',
help='Advertise with a given interval (milliseconds)',
)
@click.option(
'--classic-page-scan',
is_flag=True,
help='Enable Classic page scanning',
)
@click.option(
'--classic-inquiry-scan',
is_flag=True,
help='Enable Classic enquiry scanning',
)
@click.option( @click.option(
'--rfcomm-channel', '--rfcomm-channel',
type=int, type=int,
@@ -2122,10 +1753,6 @@ def bench(
att_mtu, att_mtu,
extended_data_length, extended_data_length,
role_switch, role_switch,
le_scan,
le_advertise,
classic_page_scan,
classic_inquiry_scan,
packet_size, packet_size,
packet_count, packet_count,
start_delay, start_delay,
@@ -2174,12 +1801,7 @@ def bench(
else None else None
) )
ctx.obj['role_switch'] = role_switch ctx.obj['role_switch'] = role_switch
ctx.obj['le_scan'] = [float(x) for x in le_scan.split('/')] if le_scan else None
ctx.obj['le_advertise'] = float(le_advertise) if le_advertise else None
ctx.obj['classic_page_scan'] = classic_page_scan
ctx.obj['classic_inquiry_scan'] = classic_inquiry_scan
ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server') ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
ctx.obj['iso'] = mode in ('iso-client', 'iso-server')
@bench.command() @bench.command()
@@ -2201,94 +1823,28 @@ def bench(
@click.option('--phy', type=click.Choice(['1m', '2m', 'coded']), help='PHY to use') @click.option('--phy', type=click.Choice(['1m', '2m', 'coded']), help='PHY to use')
@click.option('--authenticate', is_flag=True, help='Authenticate (RFComm only)') @click.option('--authenticate', is_flag=True, help='Authenticate (RFComm only)')
@click.option('--encrypt', is_flag=True, help='Encrypt the connection (RFComm only)') @click.option('--encrypt', is_flag=True, help='Encrypt the connection (RFComm only)')
@click.option(
'--iso-sdu-interval-c-to-p',
type=int,
help='ISO SDU central -> peripheral (microseconds)',
)
@click.option(
'--iso-sdu-interval-p-to-c',
type=int,
help='ISO SDU interval peripheral -> central (microseconds)',
)
@click.option(
'--iso-max-sdu-c-to-p',
type=int,
help='ISO max SDU central -> peripheral',
)
@click.option(
'--iso-max-sdu-p-to-c',
type=int,
help='ISO max SDU peripheral -> central',
)
@click.option(
'--iso-max-transport-latency-c-to-p',
type=int,
help='ISO max transport latency central -> peripheral (milliseconds)',
)
@click.option(
'--iso-max-transport-latency-p-to-c',
type=int,
help='ISO max transport latency peripheral -> central (milliseconds)',
)
@click.option(
'--iso-rtn-c-to-p',
type=int,
help='ISO RTN central -> peripheral (integer count)',
)
@click.option(
'--iso-rtn-p-to-c',
type=int,
help='ISO RTN peripheral -> central (integer count)',
)
@click.pass_context @click.pass_context
def central( def central(
ctx, ctx, transport, peripheral_address, connection_interval, phy, authenticate, encrypt
transport,
peripheral_address,
connection_interval,
phy,
authenticate,
encrypt,
iso_sdu_interval_c_to_p,
iso_sdu_interval_p_to_c,
iso_max_sdu_c_to_p,
iso_max_sdu_p_to_c,
iso_max_transport_latency_c_to_p,
iso_max_transport_latency_p_to_c,
iso_rtn_c_to_p,
iso_rtn_p_to_c,
): ):
"""Run as a central (initiates the connection)""" """Run as a central (initiates the connection)"""
scenario_factory = create_scenario_factory(ctx, 'send') scenario_factory = create_scenario_factory(ctx, 'send')
mode_factory = create_mode_factory(ctx, 'gatt-client') mode_factory = create_mode_factory(ctx, 'gatt-client')
classic = ctx.obj['classic']
async def run_central(): async def run_central():
await Central( await Central(
transport, transport,
peripheral_address, peripheral_address,
classic,
scenario_factory, scenario_factory,
mode_factory, mode_factory,
connection_interval, connection_interval,
phy, phy,
authenticate, authenticate,
encrypt or authenticate, encrypt or authenticate,
ctx.obj['iso'],
iso_sdu_interval_c_to_p,
iso_sdu_interval_p_to_c,
iso_max_sdu_c_to_p,
iso_max_sdu_p_to_c,
iso_max_transport_latency_c_to_p,
iso_max_transport_latency_p_to_c,
iso_rtn_c_to_p,
iso_rtn_p_to_c,
ctx.obj['classic'],
ctx.obj['extended_data_length'], ctx.obj['extended_data_length'],
ctx.obj['role_switch'], ctx.obj['role_switch'],
ctx.obj['le_scan'],
ctx.obj['le_advertise'],
ctx.obj['classic_page_scan'],
ctx.obj['classic_inquiry_scan'],
).run() ).run()
asyncio.run(run_central()) asyncio.run(run_central())
@@ -2308,13 +1864,8 @@ def peripheral(ctx, transport):
scenario_factory, scenario_factory,
mode_factory, mode_factory,
ctx.obj['classic'], ctx.obj['classic'],
ctx.obj['iso'],
ctx.obj['extended_data_length'], ctx.obj['extended_data_length'],
ctx.obj['role_switch'], ctx.obj['role_switch'],
ctx.obj['le_scan'],
ctx.obj['le_advertise'],
ctx.obj['classic_page_scan'],
ctx.obj['classic_inquiry_scan'],
).run() ).run()
asyncio.run(run_peripheral()) asyncio.run(run_peripheral())
+3 -3
View File
@@ -55,7 +55,7 @@ from prompt_toolkit.layout import (
from bumble import __version__ from bumble import __version__
import bumble.core import bumble.core
from bumble import colors from bumble import colors
from bumble.core import UUID, AdvertisingData from bumble.core import UUID, AdvertisingData, PhysicalTransport
from bumble.device import ( from bumble.device import (
ConnectionParametersPreferences, ConnectionParametersPreferences,
ConnectionPHY, ConnectionPHY,
@@ -64,7 +64,7 @@ from bumble.device import (
Peer, Peer,
) )
from bumble.utils import AsyncRunner from bumble.utils import AsyncRunner
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
from bumble.gatt import Characteristic, Service, CharacteristicDeclaration, Descriptor from bumble.gatt import Characteristic, Service, CharacteristicDeclaration, Descriptor
from bumble.gatt_client import CharacteristicProxy from bumble.gatt_client import CharacteristicProxy
from bumble.hci import ( from bumble.hci import (
@@ -291,7 +291,7 @@ class ConsoleApp:
async def run_async(self, device_config, transport): async def run_async(self, device_config, transport):
rssi_monitoring_task = asyncio.create_task(self.rssi_monitor_loop()) rssi_monitoring_task = asyncio.create_task(self.rssi_monitor_loop())
async with await open_transport(transport) as (hci_source, hci_sink): async with await open_transport_or_link(transport) as (hci_source, hci_sink):
if device_config: if device_config:
self.device = Device.from_config_file_with_hci( self.device = Device.from_config_file_with_hci(
device_config, hci_source, hci_sink device_config, hci_source, hci_sink
+10 -47
View File
@@ -58,7 +58,7 @@ from bumble.hci import (
HCI_Read_Local_Version_Information_Command, HCI_Read_Local_Version_Information_Command,
) )
from bumble.host import Host from bumble.host import Host
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -242,43 +242,28 @@ async def get_codecs_info(host: Host) -> None:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def async_main( async def async_main(latency_probes, transport):
latency_probes, latency_probe_interval, latency_probe_command, transport
):
print('<<< connecting to HCI...') print('<<< connecting to HCI...')
async with await open_transport(transport) as (hci_source, hci_sink): async with await open_transport_or_link(transport) as (hci_source, hci_sink):
print('<<< connected') print('<<< connected')
host = Host(hci_source, hci_sink) host = Host(hci_source, hci_sink)
await host.reset() await host.reset()
# Measure the latency if requested # Measure the latency if requested
# (we add an extra probe at the start, that we ignore, just to ensure that
# the transport is primed)
latencies = [] latencies = []
if latency_probes: if latency_probes:
if latency_probe_command: for _ in range(latency_probes):
probe_hci_command = HCI_Command.from_bytes(
bytes.fromhex(latency_probe_command)
)
else:
probe_hci_command = HCI_Read_Local_Version_Information_Command()
for iteration in range(1 + latency_probes):
if latency_probe_interval:
await asyncio.sleep(latency_probe_interval / 1000)
start = time.time() start = time.time()
await host.send_command(probe_hci_command) await host.send_command(HCI_Read_Local_Version_Information_Command())
if iteration: latencies.append(1000 * (time.time() - start))
latencies.append(1000 * (time.time() - start))
print( print(
color('HCI Command Latency:', 'yellow'), color('HCI Command Latency:', 'yellow'),
( (
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],
'\n', '\n',
) )
@@ -326,32 +311,10 @@ async def async_main(
type=int, type=int,
help='Send N commands to measure HCI transport latency statistics', help='Send N commands to measure HCI transport latency statistics',
) )
@click.option(
'--latency-probe-interval',
metavar='INTERVAL',
type=int,
help='Interval between latency probes (milliseconds)',
)
@click.option(
'--latency-probe-command',
metavar='COMMAND_HEX',
help=(
'Probe command (HCI Command packet bytes, in hex. Use 0177FC00 for'
' a loopback test with the HCI remote proxy app)'
),
)
@click.argument('transport') @click.argument('transport')
def main(latency_probes, latency_probe_interval, latency_probe_command, transport): def main(latency_probes, transport):
logging.basicConfig( logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(), asyncio.run(async_main(latency_probes, transport))
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
datefmt="%H:%M:%S",
)
asyncio.run(
async_main(
latency_probes, latency_probe_interval, latency_probe_command, transport
)
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
+3 -7
View File
@@ -29,7 +29,7 @@ from bumble.hci import (
LoopbackMode, LoopbackMode,
) )
from bumble.host import Host from bumble.host import Host
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
import click import click
@@ -88,7 +88,7 @@ class Loopback:
async def run(self): async def run(self):
"""Run a loopback throughput test""" """Run a loopback throughput test"""
print(color('>>> Connecting to HCI...', 'green')) print(color('>>> Connecting to HCI...', 'green'))
async with await open_transport(self.transport) as ( async with await open_transport_or_link(self.transport) as (
hci_source, hci_source,
hci_sink, hci_sink,
): ):
@@ -194,11 +194,7 @@ class Loopback:
) )
@click.argument('transport') @click.argument('transport')
def main(packet_size, packet_count, transport): def main(packet_size, packet_count, transport):
logging.basicConfig( logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
datefmt="%H:%M:%S",
)
loopback = Loopback(packet_size, packet_count, transport) loopback = Loopback(packet_size, packet_count, transport)
asyncio.run(loopback.run()) asyncio.run(loopback.run())
+2 -2
View File
@@ -22,7 +22,7 @@ import os
from bumble.controller import Controller from bumble.controller import Controller
from bumble.link import LocalLink from bumble.link import LocalLink
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -42,7 +42,7 @@ async def async_main():
transports = [] transports = []
controllers = [] controllers = []
for index, transport_name in enumerate(sys.argv[1:]): for index, transport_name in enumerate(sys.argv[1:]):
transport = await open_transport(transport_name) transport = await open_transport_or_link(transport_name)
transports.append(transport) transports.append(transport)
controller = Controller( controller = Controller(
f'C{index}', f'C{index}',
+2 -2
View File
@@ -32,7 +32,7 @@ from bumble.profiles.gap import GenericAccessServiceProxy
from bumble.profiles.pacs import PublishedAudioCapabilitiesServiceProxy from bumble.profiles.pacs import PublishedAudioCapabilitiesServiceProxy
from bumble.profiles.tmap import TelephonyAndMediaAudioServiceProxy from bumble.profiles.tmap import TelephonyAndMediaAudioServiceProxy
from bumble.profiles.vcs import VolumeControlServiceProxy from bumble.profiles.vcs import VolumeControlServiceProxy
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -215,7 +215,7 @@ 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_or_link(transport) as (hci_source, hci_sink):
# Create a device # Create a device
if device_config: if device_config:
+2 -2
View File
@@ -24,7 +24,7 @@ import bumble.core
from bumble.colors import color from bumble.colors import color
from bumble.device import Device, Peer from bumble.device import Device, Peer
from bumble.gatt import show_services from bumble.gatt import show_services
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -60,7 +60,7 @@ 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_or_link(transport) as (hci_source, hci_sink):
# Create a device # Create a device
if device_config: if device_config:
+2 -2
View File
@@ -27,7 +27,7 @@ from bumble.device import Device, Peer
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.gatt import Service, Characteristic, CharacteristicValue from bumble.gatt import Service, Characteristic, CharacteristicValue
from bumble.utils import AsyncRunner from bumble.utils import AsyncRunner
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
from bumble.hci import HCI_Constant from bumble.hci import HCI_Constant
@@ -325,7 +325,7 @@ async def run(
receive_port, receive_port,
): ):
print('<<< connecting to HCI...') print('<<< connecting to HCI...')
async with await open_transport(hci_transport) as (hci_source, hci_sink): async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
print('<<< connected') print('<<< connected')
# Instantiate a bridge object # Instantiate a bridge object
+2 -2
View File
@@ -46,14 +46,14 @@ async def async_main():
return return
print('>>> connecting to HCI...') print('>>> connecting to HCI...')
async with await transport.open_transport(sys.argv[1]) as ( async with await transport.open_transport_or_link(sys.argv[1]) as (
hci_host_source, hci_host_source,
hci_host_sink, hci_host_sink,
): ):
print('>>> connected') print('>>> connected')
print('>>> connecting to HCI...') print('>>> connecting to HCI...')
async with await transport.open_transport(sys.argv[2]) as ( async with await transport.open_transport_or_link(sys.argv[2]) as (
hci_controller_source, hci_controller_source,
hci_controller_sink, hci_controller_sink,
): ):
+2 -2
View File
@@ -22,7 +22,7 @@ import click
from bumble import l2cap from bumble import l2cap
from bumble.colors import color from bumble.colors import color
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
from bumble.device import Device from bumble.device import Device
from bumble.utils import FlowControlAsyncPipe from bumble.utils import FlowControlAsyncPipe
from bumble.hci import HCI_Constant from bumble.hci import HCI_Constant
@@ -258,7 +258,7 @@ class ClientBridge:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def run(device_config, hci_transport, bridge): async def run(device_config, hci_transport, bridge):
print('<<< connecting to HCI...') print('<<< connecting to HCI...')
async with await open_transport(hci_transport) as (hci_source, hci_sink): async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
print('<<< connected') print('<<< connected')
device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink) device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink)
+1 -6
View File
@@ -337,12 +337,7 @@ class Speaker:
), ),
( (
AdvertisingData.FLAGS, AdvertisingData.FLAGS,
bytes( bytes([AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG]),
[
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
| AdvertisingData.BR_EDR_NOT_SUPPORTED_FLAG
]
),
), ),
( (
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
View File
+289
View File
@@ -0,0 +1,289 @@
# 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 sys
import logging
import json
import asyncio
import argparse
import uuid
import os
from urllib.parse import urlparse
import websockets
from bumble.colors import color
# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)
# ----------------------------------------------------------------------------
# Constants
# ----------------------------------------------------------------------------
DEFAULT_RELAY_PORT = 10723
# ----------------------------------------------------------------------------
# Utils
# ----------------------------------------------------------------------------
def error_to_json(error):
return json.dumps({'error': error})
def error_to_result(error):
return f'result:{error_to_json(error)}'
async def broadcast_message(message, connections):
# Send to all the connections
tasks = [connection.send_message(message) for connection in connections]
if tasks:
await asyncio.gather(*tasks)
# ----------------------------------------------------------------------------
# Connection class
# ----------------------------------------------------------------------------
class Connection:
"""
A Connection represents a client connected to the relay over a websocket
"""
def __init__(self, room, websocket):
self.room = room
self.websocket = websocket
self.address = str(uuid.uuid4())
async def send_message(self, message):
try:
logger.debug(color(f'->{self.address}: {message}', 'yellow'))
return await self.websocket.send(message)
except websockets.exceptions.WebSocketException as error:
logger.info(f'! client "{self}" disconnected: {error}')
await self.cleanup()
async def send_error(self, error):
return await self.send_message(f'result:{error_to_json(error)}')
async def receive_message(self):
try:
message = await self.websocket.recv()
logger.debug(color(f'<-{self.address}: {message}', 'blue'))
return message
except websockets.exceptions.WebSocketException as error:
logger.info(color(f'! client "{self}" disconnected: {error}', 'red'))
await self.cleanup()
async def cleanup(self):
if self.room:
await self.room.remove_connection(self)
def set_address(self, address):
logger.info(f'Connection address changed: {self.address} -> {address}')
self.address = address
def __str__(self):
return (
f'Connection(address="{self.address}", '
f'client={self.websocket.remote_address[0]}:'
f'{self.websocket.remote_address[1]})'
)
# ----------------------------------------------------------------------------
# Room class
# ----------------------------------------------------------------------------
class Room:
"""
A Room is a collection of bridged connections
"""
def __init__(self, relay, name):
self.relay = relay
self.name = name
self.observers = []
self.connections = []
async def add_connection(self, connection):
logger.info(f'New participant in {self.name}: {connection}')
self.connections.append(connection)
await self.broadcast_message(connection, f'joined:{connection.address}')
async def remove_connection(self, connection):
if connection in self.connections:
self.connections.remove(connection)
await self.broadcast_message(connection, f'left:{connection.address}')
def find_connections_by_address(self, address):
return [c for c in self.connections if c.address == address]
async def bridge_connection(self, connection):
while True:
# Wait for a message
message = await connection.receive_message()
# Skip empty messages
if message is None:
return
# Parse the message to decide how to handle it
if message.startswith('@'):
# This is a targeted message
await self.on_targeted_message(connection, message)
elif message.startswith('/'):
# This is an RPC request
await self.on_rpc_request(connection, message)
else:
await connection.send_message(
f'result:{error_to_json("error: invalid message")}'
)
async def broadcast_message(self, sender, message):
'''
Send to all connections in the room except back to the sender
'''
await broadcast_message(message, [c for c in self.connections if c != sender])
async def on_rpc_request(self, connection, message):
command, *params = message.split(' ', 1)
if handler := getattr(
self, f'on_{command[1:].lower().replace("-","_")}_command', None
):
try:
result = await handler(connection, params)
except Exception as error:
result = error_to_result(error)
else:
result = error_to_result('unknown command')
await connection.send_message(result or 'result:{}')
async def on_targeted_message(self, connection, message):
target, *payload = message.split(' ', 1)
if not payload:
return error_to_json('missing arguments')
payload = payload[0]
target = target[1:]
# Determine what targets to send to
if target == '*':
# Send to all connections in the room except the connection from which the
# message was received
connections = [c for c in self.connections if c != connection]
else:
connections = self.find_connections_by_address(target)
if not connections:
# Unicast with no recipient, let the sender know
await connection.send_message(f'unreachable:{target}')
# Send to targets
await broadcast_message(f'message:{connection.address}/{payload}', connections)
async def on_set_address_command(self, connection, params):
if not params:
return error_to_result('missing address')
current_address = connection.address
new_address = params[0]
connection.set_address(new_address)
await self.broadcast_message(
connection, f'address-changed:from={current_address},to={new_address}'
)
# ----------------------------------------------------------------------------
class Relay:
"""
A relay accepts connections with the following url: ws://<hostname>/<room>.
Participants in a room can communicate with each other
"""
def __init__(self, port):
self.port = port
self.rooms = {}
self.observers = []
def start(self):
logger.info(f'Starting Relay on port {self.port}')
# pylint: disable-next=no-member
return websockets.serve(self.serve, '0.0.0.0', self.port, ping_interval=None)
async def serve_as_controller(self, connection):
pass
async def serve(self, websocket, path):
logger.debug(f'New connection with path {path}')
# Parse the path
parsed = urlparse(path)
# Check if this is a controller client
if parsed.path == '/':
return await self.serve_as_controller(Connection('', websocket))
# Find or create a room for this connection
room_name = parsed.path[1:].split('/')[0]
if room_name not in self.rooms:
self.rooms[room_name] = Room(self, room_name)
room = self.rooms[room_name]
# Add the connection to the room
connection = Connection(room, websocket)
await room.add_connection(connection)
# Bridge until the connection is closed
await room.bridge_connection(connection)
# ----------------------------------------------------------------------------
def main():
# Check the Python version
if sys.version_info < (3, 6, 1):
print('ERROR: Python 3.6.1 or higher is required')
sys.exit(1)
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
# Parse arguments
arg_parser = argparse.ArgumentParser(description='Bumble Link Relay')
arg_parser.add_argument('--log-level', default='INFO', help='logger level')
arg_parser.add_argument('--log-config', help='logger config file (YAML)')
arg_parser.add_argument(
'--port', type=int, default=DEFAULT_RELAY_PORT, help='Port to listen on'
)
args = arg_parser.parse_args()
# Setup logger
if args.log_config:
from logging import config # pylint: disable=import-outside-toplevel
config.fileConfig(args.log_config)
else:
logging.basicConfig(level=getattr(logging, args.log_level.upper()))
# Start a relay
relay = Relay(args.port)
asyncio.get_event_loop().run_until_complete(relay.start())
asyncio.get_event_loop().run_forever()
# ----------------------------------------------------------------------------
if __name__ == '__main__':
main()
+21
View File
@@ -0,0 +1,21 @@
[loggers]
keys=root
[handlers]
keys=stream_handler
[formatters]
keys=formatter
[logger_root]
level=DEBUG
handlers=stream_handler
[handler_stream_handler]
class=StreamHandler
level=DEBUG
formatter=formatter
args=(sys.stderr,)
[formatter_formatter]
format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s
+11 -19
View File
@@ -26,7 +26,7 @@ from prompt_toolkit.shortcuts import PromptSession
from bumble.a2dp import make_audio_sink_service_sdp_records from bumble.a2dp import make_audio_sink_service_sdp_records
from bumble.colors import color from bumble.colors import color
from bumble.device import Device, Peer from bumble.device import Device, Peer
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
from bumble.pairing import OobData, PairingDelegate, PairingConfig from bumble.pairing import OobData, PairingDelegate, PairingConfig
from bumble.smp import OobContext, OobLegacyContext from bumble.smp import OobContext, OobLegacyContext
from bumble.smp import error_name as smp_error_name from bumble.smp import error_name as smp_error_name
@@ -349,7 +349,7 @@ async def pair(
Waiter.instance = Waiter(linger=linger) Waiter.instance = Waiter(linger=linger)
print('<<< connecting to HCI...') print('<<< connecting to HCI...')
async with await open_transport(hci_transport) as (hci_source, hci_sink): async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
print('<<< connected') print('<<< connected')
# Create a device to manage the host # Create a device to manage the host
@@ -402,19 +402,14 @@ async def pair(
# Create an OOB context if needed # Create an OOB context if needed
if oob: if oob:
our_oob_context = OobContext() our_oob_context = OobContext()
if oob == '-': shared_data = (
shared_data = None None
legacy_context = OobLegacyContext() if oob == '-'
else: else OobData.from_ad(
oob_data = OobData.from_ad(
AdvertisingData.from_bytes(bytes.fromhex(oob)) AdvertisingData.from_bytes(bytes.fromhex(oob))
) ).shared_data
shared_data = oob_data.shared_data )
legacy_context = oob_data.legacy_context legacy_context = OobLegacyContext()
if legacy_context is None and not sc:
print(color('OOB pairing in legacy mode requires TK', 'red'))
return
oob_contexts = PairingConfig.OobConfig( oob_contexts = PairingConfig.OobConfig(
our_context=our_oob_context, our_context=our_oob_context,
peer_data=shared_data, peer_data=shared_data,
@@ -424,9 +419,7 @@ async def pair(
print(color('@@@ OOB Data:', 'yellow')) print(color('@@@ OOB Data:', 'yellow'))
if shared_data is None: if shared_data is None:
oob_data = OobData( oob_data = OobData(
address=device.random_address, address=device.random_address, shared_data=our_oob_context.share()
shared_data=our_oob_context.share(),
legacy_context=(None if sc else legacy_context),
) )
print( print(
color( color(
@@ -434,8 +427,7 @@ async def pair(
'yellow', 'yellow',
) )
) )
if legacy_context: print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
print(color('@@@-----------------------------------', 'yellow')) print(color('@@@-----------------------------------', 'yellow'))
else: else:
oob_contexts = None oob_contexts = None
+2 -2
View File
@@ -4,7 +4,7 @@ import logging
import json import json
from bumble.pandora import PandoraDevice, Config, serve from bumble.pandora import PandoraDevice, Config, serve
from typing import Any from typing import Dict, Any
BUMBLE_SERVER_GRPC_PORT = 7999 BUMBLE_SERVER_GRPC_PORT = 7999
ROOTCANAL_PORT_CUTTLEFISH = 7300 ROOTCANAL_PORT_CUTTLEFISH = 7300
@@ -39,7 +39,7 @@ def main(grpc_port: int, rootcanal_port: int, transport: str, config: str) -> No
asyncio.run(serve(device, config=server_config, port=grpc_port)) asyncio.run(serve(device, config=server_config, port=grpc_port))
def retrieve_config(config: str) -> dict[str, Any]: def retrieve_config(config: str) -> Dict[str, Any]:
if not config: if not config:
return {} return {}
+1 -1
View File
@@ -406,7 +406,7 @@ class ClientBridge:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def run(device_config, hci_transport, bridge): async def run(device_config, hci_transport, bridge):
print("<<< connecting to HCI...") print("<<< connecting to HCI...")
async with await transport.open_transport(hci_transport) as ( async with await transport.open_transport_or_link(hci_transport) as (
hci_source, hci_source,
hci_sink, hci_sink,
): ):
+2 -2
View File
@@ -22,7 +22,7 @@ import click
from bumble.colors import color from bumble.colors import color
from bumble.device import Device from bumble.device import Device
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
from bumble.keys import JsonKeyStore from bumble.keys import JsonKeyStore
from bumble.smp import AddressResolver from bumble.smp import AddressResolver
from bumble.device import Advertisement from bumble.device import Advertisement
@@ -127,7 +127,7 @@ async def scan(
transport, transport,
): ):
print('<<< connecting to HCI...') print('<<< connecting to HCI...')
async with await open_transport(transport) as (hci_source, hci_sink): async with await open_transport_or_link(transport) as (hci_source, hci_sink):
print('<<< connected') print('<<< connected')
if device_config: if device_config:
+2 -4
View File
@@ -16,7 +16,6 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import datetime import datetime
import importlib
import logging import logging
import os import os
import struct import struct
@@ -155,10 +154,9 @@ class Printer:
def main(format, vendor, filename): def main(format, vendor, filename):
for vendor_name in vendor: for vendor_name in vendor:
if vendor_name == 'android': if vendor_name == 'android':
# Prevent being deleted by linter. import bumble.vendor.android.hci
importlib.import_module('bumble.vendor.android.hci')
elif vendor_name == 'zephyr': elif vendor_name == 'zephyr':
importlib.import_module('bumble.vendor.zephyr.hci') import bumble.vendor.zephyr.hci
input = open(filename, 'rb') input = open(filename, 'rb')
if format == 'h4': if format == 'h4':
-1
View File
@@ -15,7 +15,6 @@
<tr><td>Codec</td><td><span id="codecText"></span></td></tr> <tr><td>Codec</td><td><span id="codecText"></span></td></tr>
<tr><td>Packets</td><td><span id="packetsReceivedText"></span></td></tr> <tr><td>Packets</td><td><span id="packetsReceivedText"></span></td></tr>
<tr><td>Bytes</td><td><span id="bytesReceivedText"></span></td></tr> <tr><td>Bytes</td><td><span id="bytesReceivedText"></span></td></tr>
<tr><td>Bitrate</td><td><span id="bitrate"></span></td></tr>
</table> </table>
</td> </td>
<td> <td>
+62 -113
View File
@@ -7,19 +7,17 @@ let connectionText;
let codecText; let codecText;
let packetsReceivedText; let packetsReceivedText;
let bytesReceivedText; let bytesReceivedText;
let bitrateText;
let streamStateText; let streamStateText;
let connectionStateText; let connectionStateText;
let controlsDiv; let controlsDiv;
let audioOnButton; let audioOnButton;
let audioDecoder; let mediaSource;
let audioCodec; let sourceBuffer;
let audioElement;
let audioContext; let audioContext;
let audioAnalyzer; let audioAnalyzer;
let audioFrequencyBinCount; let audioFrequencyBinCount;
let audioFrequencyData; let audioFrequencyData;
let nextAudioStartPosition = 0;
let audioStartTime = 0;
let packetsReceived = 0; let packetsReceived = 0;
let bytesReceived = 0; let bytesReceived = 0;
let audioState = "stopped"; let audioState = "stopped";
@@ -31,17 +29,20 @@ let bandwidthCanvas;
let bandwidthCanvasContext; let bandwidthCanvasContext;
let bandwidthBinCount; let bandwidthBinCount;
let bandwidthBins = []; let bandwidthBins = [];
let bitrateSamples = [];
const FFT_WIDTH = 800; const FFT_WIDTH = 800;
const FFT_HEIGHT = 256; const FFT_HEIGHT = 256;
const BANDWIDTH_WIDTH = 500; const BANDWIDTH_WIDTH = 500;
const BANDWIDTH_HEIGHT = 100; const BANDWIDTH_HEIGHT = 100;
const BITRATE_WINDOW = 30;
function hexToBytes(hex) {
return Uint8Array.from(hex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
}
function init() { function init() {
initUI(); initUI();
initAudioContext(); initMediaSource();
initAudioElement();
initAnalyzer(); initAnalyzer();
connect(); connect();
@@ -55,7 +56,6 @@ function initUI() {
codecText = document.getElementById("codecText"); codecText = document.getElementById("codecText");
packetsReceivedText = document.getElementById("packetsReceivedText"); packetsReceivedText = document.getElementById("packetsReceivedText");
bytesReceivedText = document.getElementById("bytesReceivedText"); bytesReceivedText = document.getElementById("bytesReceivedText");
bitrateText = document.getElementById("bitrate");
streamStateText = document.getElementById("streamStateText"); streamStateText = document.getElementById("streamStateText");
connectionStateText = document.getElementById("connectionStateText"); connectionStateText = document.getElementById("connectionStateText");
audioSupportMessageText = document.getElementById("audioSupportMessageText"); audioSupportMessageText = document.getElementById("audioSupportMessageText");
@@ -67,9 +67,17 @@ function initUI() {
requestAnimationFrame(onAnimationFrame); requestAnimationFrame(onAnimationFrame);
} }
function initAudioContext() { function initMediaSource() {
audioContext = new AudioContext(); mediaSource = new MediaSource();
audioContext.onstatechange = () => console.log("AudioContext state:", audioContext.state); mediaSource.onsourceopen = onMediaSourceOpen;
mediaSource.onsourceclose = onMediaSourceClose;
mediaSource.onsourceended = onMediaSourceEnd;
}
function initAudioElement() {
audioElement = document.getElementById("audio");
audioElement.src = URL.createObjectURL(mediaSource);
// audioElement.controls = true;
} }
function initAnalyzer() { function initAnalyzer() {
@@ -86,16 +94,24 @@ function initAnalyzer() {
bandwidthCanvasContext = bandwidthCanvas.getContext('2d'); bandwidthCanvasContext = bandwidthCanvas.getContext('2d');
bandwidthCanvasContext.fillStyle = "rgb(255, 255, 255)"; bandwidthCanvasContext.fillStyle = "rgb(255, 255, 255)";
bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT); bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
}
function startAnalyzer() {
// FFT
if (audioElement.captureStream !== undefined) {
audioContext = new AudioContext();
audioAnalyzer = audioContext.createAnalyser();
audioAnalyzer.fftSize = 128;
audioFrequencyBinCount = audioAnalyzer.frequencyBinCount;
audioFrequencyData = new Uint8Array(audioFrequencyBinCount);
const stream = audioElement.captureStream();
const source = audioContext.createMediaStreamSource(stream);
source.connect(audioAnalyzer);
}
// Bandwidth
bandwidthBinCount = BANDWIDTH_WIDTH / 2; bandwidthBinCount = BANDWIDTH_WIDTH / 2;
bandwidthBins = []; bandwidthBins = [];
bitrateSamples = [];
audioAnalyzer = audioContext.createAnalyser();
audioAnalyzer.fftSize = 128;
audioFrequencyBinCount = audioAnalyzer.frequencyBinCount;
audioFrequencyData = new Uint8Array(audioFrequencyBinCount);
audioAnalyzer.connect(audioContext.destination)
} }
function setConnectionText(message) { function setConnectionText(message) {
@@ -132,8 +148,7 @@ function onAnimationFrame() {
bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT); bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
bandwidthCanvasContext.fillStyle = `rgb(100, 100, 100)`; bandwidthCanvasContext.fillStyle = `rgb(100, 100, 100)`;
for (let t = 0; t < bandwidthBins.length; t++) { for (let t = 0; t < bandwidthBins.length; t++) {
const bytesReceived = bandwidthBins[t] const lineHeight = (bandwidthBins[t] / 1000) * BANDWIDTH_HEIGHT;
const lineHeight = (bytesReceived / 1000) * BANDWIDTH_HEIGHT;
bandwidthCanvasContext.fillRect(t * 2, BANDWIDTH_HEIGHT - lineHeight, 2, lineHeight); bandwidthCanvasContext.fillRect(t * 2, BANDWIDTH_HEIGHT - lineHeight, 2, lineHeight);
} }
@@ -141,14 +156,28 @@ function onAnimationFrame() {
requestAnimationFrame(onAnimationFrame); requestAnimationFrame(onAnimationFrame);
} }
function onMediaSourceOpen() {
console.log(this.readyState);
sourceBuffer = mediaSource.addSourceBuffer("audio/aac");
}
function onMediaSourceClose() {
console.log(this.readyState);
}
function onMediaSourceEnd() {
console.log(this.readyState);
}
async function startAudio() { async function startAudio() {
try { try {
console.log("starting audio..."); console.log("starting audio...");
audioOnButton.disabled = true; audioOnButton.disabled = true;
audioState = "starting"; audioState = "starting";
audioContext.resume(); await audioElement.play();
console.log("audio started"); console.log("audio started");
audioState = "playing"; audioState = "playing";
startAnalyzer();
} catch(error) { } catch(error) {
console.error(`play failed: ${error}`); console.error(`play failed: ${error}`);
audioState = "stopped"; audioState = "stopped";
@@ -156,47 +185,12 @@ async function startAudio() {
} }
} }
function onDecodedAudio(audioData) { function onAudioPacket(packet) {
const bufferSource = audioContext.createBufferSource() if (audioState != "stopped") {
// Queue the audio packet.
const now = audioContext.currentTime; sourceBuffer.appendBuffer(packet);
let nextAudioStartTime = audioStartTime + (nextAudioStartPosition / audioData.sampleRate);
if (nextAudioStartTime < now) {
console.log("starting new audio time base")
audioStartTime = now;
nextAudioStartTime = now;
nextAudioStartPosition = 0;
} else {
console.log(`audio buffer scheduled in ${nextAudioStartTime - now}`)
} }
const audioBuffer = audioContext.createBuffer(
audioData.numberOfChannels,
audioData.numberOfFrames,
audioData.sampleRate
);
for (let channel = 0; channel < audioData.numberOfChannels; channel++) {
audioData.copyTo(
audioBuffer.getChannelData(channel),
{
planeIndex: channel,
format: "f32-planar"
}
)
}
bufferSource.buffer = audioBuffer;
bufferSource.connect(audioAnalyzer)
bufferSource.start(nextAudioStartTime);
nextAudioStartPosition += audioData.numberOfFrames;
}
function onCodecError(error) {
console.log("Codec error:", error)
}
async function onAudioPacket(packet) {
packetsReceived += 1; packetsReceived += 1;
packetsReceivedText.innerText = packetsReceived; packetsReceivedText.innerText = packetsReceived;
bytesReceived += packet.byteLength; bytesReceived += packet.byteLength;
@@ -206,48 +200,6 @@ async function onAudioPacket(packet) {
if (bandwidthBins.length > bandwidthBinCount) { if (bandwidthBins.length > bandwidthBinCount) {
bandwidthBins.shift(); bandwidthBins.shift();
} }
bitrateSamples[bitrateSamples.length] = {ts: Date.now(), bytes: packet.byteLength}
if (bitrateSamples.length > BITRATE_WINDOW) {
bitrateSamples.shift();
}
if (bitrateSamples.length >= 2) {
const windowBytes = bitrateSamples.reduce((accumulator, x) => accumulator + x.bytes, 0) - bitrateSamples[0].bytes;
const elapsed = bitrateSamples[bitrateSamples.length-1].ts - bitrateSamples[0].ts;
const bitrate = Math.floor(8 * windowBytes / elapsed)
bitrateText.innerText = `${bitrate} kb/s`
}
if (audioState == "stopped") {
return;
}
if (audioDecoder === undefined) {
let audioConfig;
if (audioCodec == 'aac') {
audioConfig = {
codec: 'mp4a.40.2',
sampleRate: 44100, // ignored
numberOfChannels: 2, // ignored
}
} else if (audioCodec == 'opus') {
audioConfig = {
codec: 'opus',
sampleRate: 48000, // ignored
numberOfChannels: 2, // ignored
}
}
audioDecoder = new AudioDecoder({ output: onDecodedAudio, error: onCodecError });
audioDecoder.configure(audioConfig)
}
const encodedAudio = new EncodedAudioChunk({
type: "key",
data: packet,
timestamp: 0,
transfer: [packet],
});
audioDecoder.decode(encodedAudio);
} }
function onChannelOpen() { function onChannelOpen() {
@@ -297,19 +249,16 @@ function onChannelMessage(message) {
} }
} }
async function onHelloMessage(params) { function onHelloMessage(params) {
codecText.innerText = params.codec; codecText.innerText = params.codec;
if (params.codec != "aac") {
if (params.codec == "aac" || params.codec == "opus") { audioOnButton.disabled = true;
audioCodec = params.codec audioSupportMessageText.innerText = "Only AAC can be played, audio will be disabled";
audioSupportMessageText.style.display = "inline-block";
} else {
audioSupportMessageText.innerText = ""; audioSupportMessageText.innerText = "";
audioSupportMessageText.style.display = "none"; audioSupportMessageText.style.display = "none";
} else {
audioOnButton.disabled = true;
audioSupportMessageText.innerText = "Only AAC and Opus can be played, audio will be disabled";
audioSupportMessageText.style.display = "inline-block";
} }
if (params.streamState) { if (params.streamState) {
setStreamState(params.streamState); setStreamState(params.streamState);
} }
+18 -126
View File
@@ -25,7 +25,7 @@ import os
import logging import logging
import pathlib import pathlib
import subprocess import subprocess
from typing import Optional from typing import Dict, List, Optional
import weakref import weakref
import click import click
@@ -50,10 +50,8 @@ from bumble.a2dp import (
make_audio_sink_service_sdp_records, make_audio_sink_service_sdp_records,
A2DP_SBC_CODEC_TYPE, A2DP_SBC_CODEC_TYPE,
A2DP_MPEG_2_4_AAC_CODEC_TYPE, A2DP_MPEG_2_4_AAC_CODEC_TYPE,
A2DP_NON_A2DP_CODEC_TYPE,
SbcMediaCodecInformation, SbcMediaCodecInformation,
AacMediaCodecInformation, AacMediaCodecInformation,
OpusMediaCodecInformation,
) )
from bumble.utils import AsyncRunner from bumble.utils import AsyncRunner
from bumble.codecs import AacAudioRtpPacket from bumble.codecs import AacAudioRtpPacket
@@ -80,8 +78,6 @@ class AudioExtractor:
return AacAudioExtractor() return AacAudioExtractor()
if codec == 'sbc': if codec == 'sbc':
return SbcAudioExtractor() return SbcAudioExtractor()
if codec == 'opus':
return OpusAudioExtractor()
def extract_audio(self, packet: MediaPacket) -> bytes: def extract_audio(self, packet: MediaPacket) -> bytes:
raise NotImplementedError() raise NotImplementedError()
@@ -106,13 +102,6 @@ class SbcAudioExtractor:
return packet.payload[1:] return packet.payload[1:]
# -----------------------------------------------------------------------------
class OpusAudioExtractor:
def extract_audio(self, packet: MediaPacket) -> bytes:
# TODO: parse fields
return packet.payload[1:]
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Output: class Output:
async def start(self) -> None: async def start(self) -> None:
@@ -246,7 +235,7 @@ class FfplayOutput(QueuedOutput):
await super().start() await super().start()
self.subprocess = await asyncio.create_subprocess_shell( self.subprocess = await asyncio.create_subprocess_shell(
f'ffplay -probesize 32 -f {self.codec} pipe:0', f'ffplay -f {self.codec} pipe:0',
stdin=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
@@ -410,24 +399,10 @@ class Speaker:
STARTED = 2 STARTED = 2
SUSPENDED = 3 SUSPENDED = 3
def __init__( def __init__(self, device_config, transport, codec, discover, outputs, ui_port):
self,
device_config,
transport,
codec,
sampling_frequencies,
bitrate,
vbr,
discover,
outputs,
ui_port,
):
self.device_config = device_config self.device_config = device_config
self.transport = transport self.transport = transport
self.codec = codec self.codec = codec
self.sampling_frequencies = sampling_frequencies
self.bitrate = bitrate
self.vbr = vbr
self.discover = discover self.discover = discover
self.ui_port = ui_port self.ui_port = ui_port
self.device = None self.device = None
@@ -448,7 +423,7 @@ class Speaker:
# Create an HTTP server for the UI # Create an HTTP server for the UI
self.ui_server = UiServer(speaker=self, port=ui_port) self.ui_server = UiServer(speaker=self, port=ui_port)
def sdp_records(self) -> dict[int, list[ServiceAttribute]]: def sdp_records(self) -> Dict[int, List[ServiceAttribute]]:
service_record_handle = 0x00010001 service_record_handle = 0x00010001
return { return {
service_record_handle: make_audio_sink_service_sdp_records( service_record_handle: make_audio_sink_service_sdp_records(
@@ -463,56 +438,32 @@ class Speaker:
if self.codec == 'sbc': if self.codec == 'sbc':
return self.sbc_codec_capabilities() return self.sbc_codec_capabilities()
if self.codec == 'opus':
return self.opus_codec_capabilities()
raise RuntimeError('unsupported codec') raise RuntimeError('unsupported codec')
def aac_codec_capabilities(self) -> MediaCodecCapabilities: def aac_codec_capabilities(self) -> MediaCodecCapabilities:
supported_sampling_frequencies = AacMediaCodecInformation.SamplingFrequency(0)
for sampling_frequency in self.sampling_frequencies or [
8000,
11025,
12000,
16000,
22050,
24000,
32000,
44100,
48000,
]:
supported_sampling_frequencies |= (
AacMediaCodecInformation.SamplingFrequency.from_int(sampling_frequency)
)
return MediaCodecCapabilities( return MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE, media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE, media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
media_codec_information=AacMediaCodecInformation( media_codec_information=AacMediaCodecInformation(
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC, object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
sampling_frequency=supported_sampling_frequencies, sampling_frequency=AacMediaCodecInformation.SamplingFrequency.SF_48000
| AacMediaCodecInformation.SamplingFrequency.SF_44100,
channels=AacMediaCodecInformation.Channels.MONO channels=AacMediaCodecInformation.Channels.MONO
| AacMediaCodecInformation.Channels.STEREO, | AacMediaCodecInformation.Channels.STEREO,
vbr=1 if self.vbr else 0, vbr=1,
bitrate=self.bitrate or 256000, bitrate=256000,
), ),
) )
def sbc_codec_capabilities(self) -> MediaCodecCapabilities: def sbc_codec_capabilities(self) -> MediaCodecCapabilities:
supported_sampling_frequencies = SbcMediaCodecInformation.SamplingFrequency(0)
for sampling_frequency in self.sampling_frequencies or [
16000,
32000,
44100,
48000,
]:
supported_sampling_frequencies |= (
SbcMediaCodecInformation.SamplingFrequency.from_int(sampling_frequency)
)
return MediaCodecCapabilities( return MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE, media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_SBC_CODEC_TYPE, media_codec_type=A2DP_SBC_CODEC_TYPE,
media_codec_information=SbcMediaCodecInformation( media_codec_information=SbcMediaCodecInformation(
sampling_frequency=supported_sampling_frequencies, sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL | SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
| SbcMediaCodecInformation.ChannelMode.STEREO | SbcMediaCodecInformation.ChannelMode.STEREO
@@ -530,25 +481,6 @@ class Speaker:
), ),
) )
def opus_codec_capabilities(self) -> MediaCodecCapabilities:
supported_sampling_frequencies = OpusMediaCodecInformation.SamplingFrequency(0)
for sampling_frequency in self.sampling_frequencies or [48000]:
supported_sampling_frequencies |= (
OpusMediaCodecInformation.SamplingFrequency.from_int(sampling_frequency)
)
return MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_NON_A2DP_CODEC_TYPE,
media_codec_information=OpusMediaCodecInformation(
frame_size=OpusMediaCodecInformation.FrameSize.FS_10MS
| OpusMediaCodecInformation.FrameSize.FS_20MS,
channel_mode=OpusMediaCodecInformation.ChannelMode.MONO
| OpusMediaCodecInformation.ChannelMode.STEREO
| OpusMediaCodecInformation.ChannelMode.DUAL_MONO,
sampling_frequency=supported_sampling_frequencies,
),
)
async def dispatch_to_outputs(self, function): async def dispatch_to_outputs(self, function):
for output in self.outputs: for output in self.outputs:
await function(output) await function(output)
@@ -743,26 +675,7 @@ def speaker_cli(ctx, device_config):
@click.command() @click.command()
@click.option( @click.option(
'--codec', '--codec', type=click.Choice(['sbc', 'aac']), default='aac', show_default=True
type=click.Choice(['sbc', 'aac', 'opus']),
default='aac',
show_default=True,
)
@click.option(
'--sampling-frequency',
metavar='SAMPLING-FREQUENCY',
type=int,
multiple=True,
help='Enable a sampling frequency (may be specified more than once)',
)
@click.option(
'--bitrate',
metavar='BITRATE',
type=int,
help='Supported bitrate (AAC only)',
)
@click.option(
'--vbr/--no-vbr', is_flag=True, default=True, help='Enable VBR (AAC only)'
) )
@click.option( @click.option(
'--discover', is_flag=True, help='Discover remote endpoints once connected' '--discover', is_flag=True, help='Discover remote endpoints once connected'
@@ -793,16 +706,7 @@ def speaker_cli(ctx, device_config):
@click.option('--device-config', metavar='FILENAME', help='Device configuration file') @click.option('--device-config', metavar='FILENAME', help='Device configuration file')
@click.argument('transport') @click.argument('transport')
def speaker( def speaker(
transport, transport, codec, connect_address, discover, output, ui_port, device_config
codec,
sampling_frequency,
bitrate,
vbr,
connect_address,
discover,
output,
ui_port,
device_config,
): ):
"""Run the speaker.""" """Run the speaker."""
@@ -817,27 +721,15 @@ def speaker(
output = list(filter(lambda x: x != '@ffplay', output)) output = list(filter(lambda x: x != '@ffplay', output))
asyncio.run( asyncio.run(
Speaker( Speaker(device_config, transport, codec, discover, output, ui_port).run(
device_config, connect_address
transport, )
codec,
sampling_frequency,
bitrate,
vbr,
discover,
output,
ui_port,
).run(connect_address)
) )
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def main(): def main():
logging.basicConfig( logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper(),
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
datefmt="%H:%M:%S",
)
speaker() speaker()
-6
View File
@@ -479,12 +479,6 @@ class OpusMediaCodecInformation(VendorSpecificMediaCodecInformation):
class SamplingFrequency(enum.IntFlag): class SamplingFrequency(enum.IntFlag):
SF_48000 = 1 << 0 SF_48000 = 1 << 0
@classmethod
def from_int(cls, sampling_frequency: int) -> Self:
if sampling_frequency != 48000:
raise ValueError("no such sampling frequency")
return cls(1)
VENDOR_ID: ClassVar[int] = 0x000000E0 VENDOR_ID: ClassVar[int] = 0x000000E0
CODEC_ID: ClassVar[int] = 0x0001 CODEC_ID: ClassVar[int] = 0x0001
+4 -4
View File
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from typing import Union from typing import List, Union
from bumble import core from bumble import core
@@ -21,7 +21,7 @@ class AtParsingError(core.InvalidPacketError):
"""Error raised when parsing AT commands fails.""" """Error raised when parsing AT commands fails."""
def tokenize_parameters(buffer: bytes) -> list[bytes]: def tokenize_parameters(buffer: bytes) -> List[bytes]:
"""Split input parameters into tokens. """Split input parameters into tokens.
Removes space characters outside of double quote blocks: Removes space characters outside of double quote blocks:
T-rec-V-25 - 5.2.1 Command line general format: "Space characters (IA5 2/0) T-rec-V-25 - 5.2.1 Command line general format: "Space characters (IA5 2/0)
@@ -63,12 +63,12 @@ def tokenize_parameters(buffer: bytes) -> list[bytes]:
return [bytes(token) for token in tokens if len(token) > 0] return [bytes(token) for token in tokens if len(token) > 0]
def parse_parameters(buffer: bytes) -> list[Union[bytes, list]]: def parse_parameters(buffer: bytes) -> List[Union[bytes, list]]:
"""Parse the parameters using the comma and parenthesis separators. """Parse the parameters using the comma and parenthesis separators.
Raises AtParsingError in case of invalid input string.""" Raises AtParsingError in case of invalid input string."""
tokens = tokenize_parameters(buffer) tokens = tokenize_parameters(buffer)
accumulator: list[list] = [[]] accumulator: List[list] = [[]]
current: Union[bytes, list] = bytes() current: Union[bytes, list] = bytes()
for token in tokens: for token in tokens:
+16 -10
View File
@@ -32,6 +32,10 @@ from typing import (
Awaitable, Awaitable,
Callable, Callable,
Generic, Generic,
Dict,
List,
Optional,
Type,
TypeVar, TypeVar,
Union, Union,
TYPE_CHECKING, TYPE_CHECKING,
@@ -247,7 +251,7 @@ class ATT_PDU:
See Bluetooth spec @ Vol 3, Part F - 3.3 ATTRIBUTE PDU See Bluetooth spec @ Vol 3, Part F - 3.3 ATTRIBUTE PDU
''' '''
pdu_classes: dict[int, type[ATT_PDU]] = {} pdu_classes: Dict[int, Type[ATT_PDU]] = {}
op_code = 0 op_code = 0
name: str name: str
@@ -766,25 +770,27 @@ class AttributeValue(Generic[_T]):
def __init__( def __init__(
self, self,
read: Union[ read: Union[
Callable[[Connection], _T], Callable[[Optional[Connection]], _T],
Callable[[Connection], Awaitable[_T]], Callable[[Optional[Connection]], Awaitable[_T]],
None, None,
] = None, ] = None,
write: Union[ write: Union[
Callable[[Connection, _T], None], Callable[[Optional[Connection], _T], None],
Callable[[Connection, _T], Awaitable[None]], Callable[[Optional[Connection], _T], Awaitable[None]],
None, None,
] = None, ] = None,
): ):
self._read = read self._read = read
self._write = write self._write = write
def read(self, connection: Connection) -> Union[_T, Awaitable[_T]]: def read(self, connection: Optional[Connection]) -> Union[_T, Awaitable[_T]]:
if self._read is None: if self._read is None:
raise InvalidOperationError('AttributeValue has no read function') raise InvalidOperationError('AttributeValue has no read function')
return self._read(connection) return self._read(connection)
def write(self, connection: Connection, value: _T) -> Union[Awaitable[None], None]: def write(
self, connection: Optional[Connection], value: _T
) -> Union[Awaitable[None], None]:
if self._write is None: if self._write is None:
raise InvalidOperationError('AttributeValue has no write function') raise InvalidOperationError('AttributeValue has no write function')
return self._write(connection, value) return self._write(connection, value)
@@ -814,7 +820,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
# The check for `p.name is not None` here is needed because for InFlag # The check for `p.name is not None` here is needed because for InFlag
# enums, the .name property can be None, when the enum value is 0, # enums, the .name property can be None, when the enum value is 0,
# so the type hint for .name is Optional[str]. # so the type hint for .name is Optional[str].
enum_list: list[str] = [p.name for p in cls if p.name is not None] enum_list: List[str] = [p.name for p in cls if p.name is not None]
enum_list_str = ",".join(enum_list) enum_list_str = ",".join(enum_list)
raise TypeError( raise TypeError(
f"Attribute::permissions error:\nExpected a string containing any of the keys, separated by commas: {enum_list_str}\nGot: {permissions_str}" f"Attribute::permissions error:\nExpected a string containing any of the keys, separated by commas: {enum_list_str}\nGot: {permissions_str}"
@@ -865,7 +871,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
def decode_value(self, value: bytes) -> _T: def decode_value(self, value: bytes) -> _T:
return value # type: ignore return value # type: ignore
async def read_value(self, connection: Connection) -> bytes: async def read_value(self, connection: Optional[Connection]) -> bytes:
if ( if (
(self.permissions & self.READ_REQUIRES_ENCRYPTION) (self.permissions & self.READ_REQUIRES_ENCRYPTION)
and connection is not None and connection is not None
@@ -907,7 +913,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
return b'' if value is None else self.encode_value(value) return b'' if value is None else self.encode_value(value)
async def write_value(self, connection: Connection, value: bytes) -> None: async def write_value(self, connection: Optional[Connection], value: bytes) -> None:
if ( if (
(self.permissions & self.WRITE_REQUIRES_ENCRYPTION) (self.permissions & self.WRITE_REQUIRES_ENCRYPTION)
and connection is not None and connection is not None
+7 -7
View File
@@ -18,7 +18,7 @@
from __future__ import annotations from __future__ import annotations
import enum import enum
import struct import struct
from typing import Union from typing import Dict, Type, Union, Tuple
from bumble import core from bumble import core
from bumble import utils from bumble import utils
@@ -213,11 +213,11 @@ class CommandFrame(Frame):
NOTIFY = 0x03 NOTIFY = 0x03
GENERAL_INQUIRY = 0x04 GENERAL_INQUIRY = 0x04
subclasses: dict[Frame.OperationCode, type[CommandFrame]] = {} subclasses: Dict[Frame.OperationCode, Type[CommandFrame]] = {}
ctype: CommandType ctype: CommandType
@staticmethod @staticmethod
def parse_operands(operands: bytes) -> tuple: def parse_operands(operands: bytes) -> Tuple:
raise NotImplementedError raise NotImplementedError
def __init__( def __init__(
@@ -251,11 +251,11 @@ class ResponseFrame(Frame):
CHANGED = 0x0D CHANGED = 0x0D
INTERIM = 0x0F INTERIM = 0x0F
subclasses: dict[Frame.OperationCode, type[ResponseFrame]] = {} subclasses: Dict[Frame.OperationCode, Type[ResponseFrame]] = {}
response: ResponseCode response: ResponseCode
@staticmethod @staticmethod
def parse_operands(operands: bytes) -> tuple: def parse_operands(operands: bytes) -> Tuple:
raise NotImplementedError raise NotImplementedError
def __init__( def __init__(
@@ -282,7 +282,7 @@ class VendorDependentFrame:
vendor_dependent_data: bytes vendor_dependent_data: bytes
@staticmethod @staticmethod
def parse_operands(operands: bytes) -> tuple: def parse_operands(operands: bytes) -> Tuple:
return ( return (
struct.unpack(">I", b"\x00" + operands[:3])[0], struct.unpack(">I", b"\x00" + operands[:3])[0],
operands[3:], operands[3:],
@@ -432,7 +432,7 @@ class PassThroughFrame:
operation_data: bytes operation_data: bytes
@staticmethod @staticmethod
def parse_operands(operands: bytes) -> tuple: def parse_operands(operands: bytes) -> Tuple:
return ( return (
PassThroughFrame.StateFlag(operands[0] >> 7), PassThroughFrame.StateFlag(operands[0] >> 7),
PassThroughFrame.OperationId(operands[0] & 0x7F), PassThroughFrame.OperationId(operands[0] & 0x7F),
+3 -3
View File
@@ -19,7 +19,7 @@ from __future__ import annotations
from enum import IntEnum from enum import IntEnum
import logging import logging
import struct import struct
from typing import Callable, cast, Optional from typing import Callable, cast, Dict, Optional
from bumble.colors import color from bumble.colors import color
from bumble import avc from bumble import avc
@@ -146,9 +146,9 @@ class MessageAssembler:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Protocol: class Protocol:
CommandHandler = Callable[[int, avc.CommandFrame], None] CommandHandler = Callable[[int, avc.CommandFrame], None]
command_handlers: dict[int, CommandHandler] # Command handlers, by PID command_handlers: Dict[int, CommandHandler] # Command handlers, by PID
ResponseHandler = Callable[[int, Optional[avc.ResponseFrame]], None] ResponseHandler = Callable[[int, Optional[avc.ResponseFrame]], None]
response_handlers: dict[int, ResponseHandler] # Response handlers, by PID response_handlers: Dict[int, ResponseHandler] # Response handlers, by PID
next_transaction_label: int next_transaction_label: int
message_assembler: MessageAssembler message_assembler: MessageAssembler
+20 -16
View File
@@ -24,8 +24,12 @@ import warnings
from typing import ( from typing import (
Any, Any,
Awaitable, Awaitable,
Dict,
Type,
Tuple,
Optional, Optional,
Callable, Callable,
List,
AsyncGenerator, AsyncGenerator,
Iterable, Iterable,
Union, Union,
@@ -223,7 +227,7 @@ AVDTP_STATE_NAMES = {
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def find_avdtp_service_with_sdp_client( async def find_avdtp_service_with_sdp_client(
sdp_client: sdp.Client, sdp_client: sdp.Client,
) -> Optional[tuple[int, int]]: ) -> Optional[Tuple[int, int]]:
''' '''
Find an AVDTP service, using a connected SDP client, and return its version, Find an AVDTP service, using a connected SDP client, and return its version,
or None if none is found or None if none is found
@@ -253,7 +257,7 @@ async def find_avdtp_service_with_sdp_client(
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def find_avdtp_service_with_connection( async def find_avdtp_service_with_connection(
connection: device.Connection, connection: device.Connection,
) -> Optional[tuple[int, int]]: ) -> Optional[Tuple[int, int]]:
''' '''
Find an AVDTP service, for a connection, and return its version, Find an AVDTP service, for a connection, and return its version,
or None if none is found or None if none is found
@@ -447,7 +451,7 @@ class ServiceCapabilities:
service_category: int, service_capabilities_bytes: bytes service_category: int, service_capabilities_bytes: bytes
) -> ServiceCapabilities: ) -> ServiceCapabilities:
# Select the appropriate subclass # Select the appropriate subclass
cls: type[ServiceCapabilities] cls: Type[ServiceCapabilities]
if service_category == AVDTP_MEDIA_CODEC_SERVICE_CATEGORY: if service_category == AVDTP_MEDIA_CODEC_SERVICE_CATEGORY:
cls = MediaCodecCapabilities cls = MediaCodecCapabilities
else: else:
@@ -462,7 +466,7 @@ class ServiceCapabilities:
return instance return instance
@staticmethod @staticmethod
def parse_capabilities(payload: bytes) -> list[ServiceCapabilities]: def parse_capabilities(payload: bytes) -> List[ServiceCapabilities]:
capabilities = [] capabilities = []
while payload: while payload:
service_category = payload[0] service_category = payload[0]
@@ -495,7 +499,7 @@ class ServiceCapabilities:
self.service_category = service_category self.service_category = service_category
self.service_capabilities_bytes = service_capabilities_bytes self.service_capabilities_bytes = service_capabilities_bytes
def to_string(self, details: Optional[list[str]] = None) -> str: def to_string(self, details: Optional[List[str]] = None) -> str:
attributes = ','.join( attributes = ','.join(
[name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)] [name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)]
+ (details or []) + (details or [])
@@ -608,7 +612,7 @@ class Message: # pylint:disable=attribute-defined-outside-init
RESPONSE_REJECT = 3 RESPONSE_REJECT = 3
# Subclasses, by signal identifier and message type # Subclasses, by signal identifier and message type
subclasses: dict[int, dict[int, type[Message]]] = {} subclasses: Dict[int, Dict[int, Type[Message]]] = {}
message_type: MessageType message_type: MessageType
signal_identifier: int signal_identifier: int
@@ -753,7 +757,7 @@ class Discover_Response(Message):
See Bluetooth AVDTP spec - 8.6.2 Stream End Point Discovery Response See Bluetooth AVDTP spec - 8.6.2 Stream End Point Discovery Response
''' '''
endpoints: list[EndPointInfo] endpoints: List[EndPointInfo]
def init_from_payload(self): def init_from_payload(self):
self.endpoints = [] self.endpoints = []
@@ -1198,10 +1202,10 @@ class DelayReport_Reject(Simple_Reject):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Protocol(utils.EventEmitter): class Protocol(utils.EventEmitter):
local_endpoints: list[LocalStreamEndPoint] local_endpoints: List[LocalStreamEndPoint]
remote_endpoints: dict[int, DiscoveredStreamEndPoint] remote_endpoints: Dict[int, DiscoveredStreamEndPoint]
streams: dict[int, Stream] streams: Dict[int, Stream]
transaction_results: list[Optional[asyncio.Future[Message]]] transaction_results: List[Optional[asyncio.Future[Message]]]
channel_connector: Callable[[], Awaitable[l2cap.ClassicChannel]] channel_connector: Callable[[], Awaitable[l2cap.ClassicChannel]]
EVENT_OPEN = "open" EVENT_OPEN = "open"
@@ -1219,7 +1223,7 @@ class Protocol(utils.EventEmitter):
@staticmethod @staticmethod
async def connect( async def connect(
connection: device.Connection, version: tuple[int, int] = (1, 3) connection: device.Connection, version: Tuple[int, int] = (1, 3)
) -> Protocol: ) -> Protocol:
channel = await connection.create_l2cap_channel( channel = await connection.create_l2cap_channel(
spec=l2cap.ClassicChannelSpec(psm=AVDTP_PSM) spec=l2cap.ClassicChannelSpec(psm=AVDTP_PSM)
@@ -1229,7 +1233,7 @@ class Protocol(utils.EventEmitter):
return protocol return protocol
def __init__( def __init__(
self, l2cap_channel: l2cap.ClassicChannel, version: tuple[int, int] = (1, 3) self, l2cap_channel: l2cap.ClassicChannel, version: Tuple[int, int] = (1, 3)
) -> None: ) -> None:
super().__init__() super().__init__()
self.l2cap_channel = l2cap_channel self.l2cap_channel = l2cap_channel
@@ -1498,7 +1502,7 @@ class Protocol(utils.EventEmitter):
return response return response
async def start_transaction(self) -> tuple[int, asyncio.Future[Message]]: async def start_transaction(self) -> Tuple[int, asyncio.Future[Message]]:
# Wait until we can start a new transaction # Wait until we can start a new transaction
await self.transaction_semaphore.acquire() await self.transaction_semaphore.acquire()
@@ -1699,7 +1703,7 @@ class Protocol(utils.EventEmitter):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Listener(utils.EventEmitter): class Listener(utils.EventEmitter):
servers: dict[int, Protocol] servers: Dict[int, Protocol]
EVENT_CONNECTION = "connection" EVENT_CONNECTION = "connection"
@@ -1731,7 +1735,7 @@ class Listener(utils.EventEmitter):
@classmethod @classmethod
def for_device( def for_device(
cls, device: device.Device, version: tuple[int, int] = (1, 3) cls, device: device.Device, version: Tuple[int, int] = (1, 3)
) -> Listener: ) -> Listener:
listener = Listener(registrar=None, version=version) listener = Listener(registrar=None, version=version)
l2cap_server = device.create_l2cap_server( l2cap_server = device.create_l2cap_server(
+53 -41
View File
@@ -26,11 +26,14 @@ from typing import (
Awaitable, Awaitable,
Callable, Callable,
cast, cast,
Dict,
Iterable, Iterable,
List, List,
Optional, Optional,
Sequence, Sequence,
SupportsBytes, SupportsBytes,
Tuple,
Type,
TypeVar, TypeVar,
Union, Union,
) )
@@ -50,10 +53,19 @@ from bumble.sdp import (
ServiceAttribute, ServiceAttribute,
) )
from bumble import utils from bumble import utils
from bumble import core from bumble.core import (
InvalidArgumentError,
ProtocolError,
BT_L2CAP_PROTOCOL_ID,
BT_AVCTP_PROTOCOL_ID,
BT_AV_REMOTE_CONTROL_SERVICE,
BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE,
BT_AV_REMOTE_CONTROL_TARGET_SERVICE,
)
from bumble import l2cap from bumble import l2cap
from bumble import avc from bumble import avc
from bumble import avctp from bumble import avctp
from bumble import utils
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -72,10 +84,10 @@ AVRCP_BLUETOOTH_SIG_COMPANY_ID = 0x001958
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def make_controller_service_sdp_records( def make_controller_service_sdp_records(
service_record_handle: int, service_record_handle: int,
avctp_version: tuple[int, int] = (1, 4), avctp_version: Tuple[int, int] = (1, 4),
avrcp_version: tuple[int, int] = (1, 6), avrcp_version: Tuple[int, int] = (1, 6),
supported_features: int = 1, supported_features: int = 1,
) -> list[ServiceAttribute]: ) -> List[ServiceAttribute]:
# TODO: support a way to compute the supported features from a feature list # TODO: support a way to compute the supported features from a feature list
avctp_version_int = avctp_version[0] << 8 | avctp_version[1] avctp_version_int = avctp_version[0] << 8 | avctp_version[1]
avrcp_version_int = avrcp_version[0] << 8 | avrcp_version[1] avrcp_version_int = avrcp_version[0] << 8 | avrcp_version[1]
@@ -93,8 +105,8 @@ def make_controller_service_sdp_records(
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
DataElement.sequence( DataElement.sequence(
[ [
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE), DataElement.uuid(BT_AV_REMOTE_CONTROL_SERVICE),
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE), DataElement.uuid(BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE),
] ]
), ),
), ),
@@ -104,13 +116,13 @@ def make_controller_service_sdp_records(
[ [
DataElement.sequence( DataElement.sequence(
[ [
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID), DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
DataElement.unsigned_integer_16(avctp.AVCTP_PSM), DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
] ]
), ),
DataElement.sequence( DataElement.sequence(
[ [
DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID), DataElement.uuid(BT_AVCTP_PROTOCOL_ID),
DataElement.unsigned_integer_16(avctp_version_int), DataElement.unsigned_integer_16(avctp_version_int),
] ]
), ),
@@ -123,7 +135,7 @@ def make_controller_service_sdp_records(
[ [
DataElement.sequence( DataElement.sequence(
[ [
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE), DataElement.uuid(BT_AV_REMOTE_CONTROL_SERVICE),
DataElement.unsigned_integer_16(avrcp_version_int), DataElement.unsigned_integer_16(avrcp_version_int),
] ]
), ),
@@ -140,10 +152,10 @@ def make_controller_service_sdp_records(
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def make_target_service_sdp_records( def make_target_service_sdp_records(
service_record_handle: int, service_record_handle: int,
avctp_version: tuple[int, int] = (1, 4), avctp_version: Tuple[int, int] = (1, 4),
avrcp_version: tuple[int, int] = (1, 6), avrcp_version: Tuple[int, int] = (1, 6),
supported_features: int = 0x23, supported_features: int = 0x23,
) -> list[ServiceAttribute]: ) -> List[ServiceAttribute]:
# TODO: support a way to compute the supported features from a feature list # TODO: support a way to compute the supported features from a feature list
avctp_version_int = avctp_version[0] << 8 | avctp_version[1] avctp_version_int = avctp_version[0] << 8 | avctp_version[1]
avrcp_version_int = avrcp_version[0] << 8 | avrcp_version[1] avrcp_version_int = avrcp_version[0] << 8 | avrcp_version[1]
@@ -161,7 +173,7 @@ def make_target_service_sdp_records(
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
DataElement.sequence( DataElement.sequence(
[ [
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_TARGET_SERVICE), DataElement.uuid(BT_AV_REMOTE_CONTROL_TARGET_SERVICE),
] ]
), ),
), ),
@@ -171,13 +183,13 @@ def make_target_service_sdp_records(
[ [
DataElement.sequence( DataElement.sequence(
[ [
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID), DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
DataElement.unsigned_integer_16(avctp.AVCTP_PSM), DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
] ]
), ),
DataElement.sequence( DataElement.sequence(
[ [
DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID), DataElement.uuid(BT_AVCTP_PROTOCOL_ID),
DataElement.unsigned_integer_16(avctp_version_int), DataElement.unsigned_integer_16(avctp_version_int),
] ]
), ),
@@ -190,7 +202,7 @@ def make_target_service_sdp_records(
[ [
DataElement.sequence( DataElement.sequence(
[ [
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE), DataElement.uuid(BT_AV_REMOTE_CONTROL_SERVICE),
DataElement.unsigned_integer_16(avrcp_version_int), DataElement.unsigned_integer_16(avrcp_version_int),
] ]
), ),
@@ -279,7 +291,7 @@ class Command:
pdu_id: Protocol.PduId pdu_id: Protocol.PduId
parameter: bytes parameter: bytes
def to_string(self, properties: dict[str, str]) -> str: def to_string(self, properties: Dict[str, str]) -> str:
properties_str = ",".join( properties_str = ",".join(
[f"{name}={value}" for name, value in properties.items()] [f"{name}={value}" for name, value in properties.items()]
) )
@@ -325,7 +337,7 @@ class GetPlayStatusCommand(Command):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class GetElementAttributesCommand(Command): class GetElementAttributesCommand(Command):
identifier: int identifier: int
attribute_ids: list[MediaAttributeId] attribute_ids: List[MediaAttributeId]
@classmethod @classmethod
def from_bytes(cls, pdu: bytes) -> GetElementAttributesCommand: def from_bytes(cls, pdu: bytes) -> GetElementAttributesCommand:
@@ -397,7 +409,7 @@ class Response:
pdu_id: Protocol.PduId pdu_id: Protocol.PduId
parameter: bytes parameter: bytes
def to_string(self, properties: dict[str, str]) -> str: def to_string(self, properties: Dict[str, str]) -> str:
properties_str = ",".join( properties_str = ",".join(
[f"{name}={value}" for name, value in properties.items()] [f"{name}={value}" for name, value in properties.items()]
) )
@@ -442,7 +454,7 @@ class NotImplementedResponse(Response):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class GetCapabilitiesResponse(Response): class GetCapabilitiesResponse(Response):
capability_id: GetCapabilitiesCommand.CapabilityId capability_id: GetCapabilitiesCommand.CapabilityId
capabilities: list[Union[SupportsBytes, bytes]] capabilities: List[Union[SupportsBytes, bytes]]
@classmethod @classmethod
def from_bytes(cls, pdu: bytes) -> GetCapabilitiesResponse: def from_bytes(cls, pdu: bytes) -> GetCapabilitiesResponse:
@@ -455,7 +467,7 @@ class GetCapabilitiesResponse(Response):
capability_id = GetCapabilitiesCommand.CapabilityId(pdu[0]) capability_id = GetCapabilitiesCommand.CapabilityId(pdu[0])
capability_count = pdu[1] capability_count = pdu[1]
capabilities: list[Union[SupportsBytes, bytes]] capabilities: List[Union[SupportsBytes, bytes]]
if capability_id == GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED: if capability_id == GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED:
capabilities = [EventId(pdu[2 + x]) for x in range(capability_count)] capabilities = [EventId(pdu[2 + x]) for x in range(capability_count)]
else: else:
@@ -528,13 +540,13 @@ class GetPlayStatusResponse(Response):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class GetElementAttributesResponse(Response): class GetElementAttributesResponse(Response):
attributes: list[MediaAttribute] attributes: List[MediaAttribute]
@classmethod @classmethod
def from_bytes(cls, pdu: bytes) -> GetElementAttributesResponse: def from_bytes(cls, pdu: bytes) -> GetElementAttributesResponse:
num_attributes = pdu[0] num_attributes = pdu[0]
offset = 1 offset = 1
attributes: list[MediaAttribute] = [] attributes: List[MediaAttribute] = []
for _ in range(num_attributes): for _ in range(num_attributes):
( (
attribute_id_int, attribute_id_int,
@@ -805,7 +817,7 @@ class PlayerApplicationSettingChangedEvent(Event):
attribute_id: ApplicationSetting.AttributeId attribute_id: ApplicationSetting.AttributeId
value_id: utils.OpenIntEnum value_id: utils.OpenIntEnum
player_application_settings: list[Setting] player_application_settings: List[Setting]
@classmethod @classmethod
def from_bytes(cls, pdu: bytes) -> PlayerApplicationSettingChangedEvent: def from_bytes(cls, pdu: bytes) -> PlayerApplicationSettingChangedEvent:
@@ -927,7 +939,7 @@ class VolumeChangedEvent(Event):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
EVENT_SUBCLASSES: dict[EventId, type[Event]] = { EVENT_SUBCLASSES: Dict[EventId, Type[Event]] = {
EventId.PLAYBACK_STATUS_CHANGED: PlaybackStatusChangedEvent, EventId.PLAYBACK_STATUS_CHANGED: PlaybackStatusChangedEvent,
EventId.PLAYBACK_POS_CHANGED: PlaybackPositionChangedEvent, EventId.PLAYBACK_POS_CHANGED: PlaybackPositionChangedEvent,
EventId.TRACK_CHANGED: TrackChangedEvent, EventId.TRACK_CHANGED: TrackChangedEvent,
@@ -955,14 +967,14 @@ class Delegate:
def __init__(self, status_code: Protocol.StatusCode) -> None: def __init__(self, status_code: Protocol.StatusCode) -> None:
self.status_code = status_code self.status_code = status_code
supported_events: list[EventId] supported_events: List[EventId]
volume: int volume: int
def __init__(self, supported_events: Iterable[EventId] = ()) -> None: def __init__(self, supported_events: Iterable[EventId] = ()) -> None:
self.supported_events = list(supported_events) self.supported_events = list(supported_events)
self.volume = 0 self.volume = 0
async def get_supported_events(self) -> list[EventId]: async def get_supported_events(self) -> List[EventId]:
return self.supported_events return self.supported_events
async def set_absolute_volume(self, volume: int) -> None: async def set_absolute_volume(self, volume: int) -> None:
@@ -1112,8 +1124,8 @@ class Protocol(utils.EventEmitter):
receive_response_state: Optional[ReceiveResponseState] receive_response_state: Optional[ReceiveResponseState]
avctp_protocol: Optional[avctp.Protocol] avctp_protocol: Optional[avctp.Protocol]
free_commands: asyncio.Queue free_commands: asyncio.Queue
pending_commands: dict[int, PendingCommand] # Pending commands, by label pending_commands: Dict[int, PendingCommand] # Pending commands, by label
notification_listeners: dict[EventId, NotificationListener] notification_listeners: Dict[EventId, NotificationListener]
@staticmethod @staticmethod
def _check_vendor_dependent_frame( def _check_vendor_dependent_frame(
@@ -1178,7 +1190,7 @@ class Protocol(utils.EventEmitter):
@staticmethod @staticmethod
def _check_response( def _check_response(
response_context: ResponseContext, expected_type: type[_R] response_context: ResponseContext, expected_type: Type[_R]
) -> _R: ) -> _R:
if isinstance(response_context, Protocol.FinalResponse): if isinstance(response_context, Protocol.FinalResponse):
if ( if (
@@ -1199,7 +1211,7 @@ class Protocol(utils.EventEmitter):
def _delegate_command( def _delegate_command(
self, transaction_label: int, command: Command, method: Awaitable self, transaction_label: int, command: Command, method: Awaitable
) -> None: ) -> None:
async def call() -> None: async def call():
try: try:
await method await method
except Delegate.Error as error: except Delegate.Error as error:
@@ -1218,7 +1230,7 @@ class Protocol(utils.EventEmitter):
utils.AsyncRunner.spawn(call()) utils.AsyncRunner.spawn(call())
async def get_supported_events(self) -> list[EventId]: async def get_supported_events(self) -> List[EventId]:
"""Get the list of events supported by the connected peer.""" """Get the list of events supported by the connected peer."""
response_context = await self.send_avrcp_command( response_context = await self.send_avrcp_command(
avc.CommandFrame.CommandType.STATUS, avc.CommandFrame.CommandType.STATUS,
@@ -1241,7 +1253,7 @@ class Protocol(utils.EventEmitter):
async def get_element_attributes( async def get_element_attributes(
self, element_identifier: int, attribute_ids: Sequence[MediaAttributeId] self, element_identifier: int, attribute_ids: Sequence[MediaAttributeId]
) -> list[MediaAttribute]: ) -> List[MediaAttribute]:
"""Get element attributes from the connected peer.""" """Get element attributes from the connected peer."""
response_context = await self.send_avrcp_command( response_context = await self.send_avrcp_command(
avc.CommandFrame.CommandType.STATUS, avc.CommandFrame.CommandType.STATUS,
@@ -1323,7 +1335,7 @@ class Protocol(utils.EventEmitter):
async def monitor_player_application_settings( async def monitor_player_application_settings(
self, self,
) -> AsyncIterator[list[PlayerApplicationSettingChangedEvent.Setting]]: ) -> AsyncIterator[List[PlayerApplicationSettingChangedEvent.Setting]]:
"""Monitor Player Application Setting changes from the connected peer.""" """Monitor Player Application Setting changes from the connected peer."""
async for event in self.monitor_events( async for event in self.monitor_events(
EventId.PLAYER_APPLICATION_SETTING_CHANGED, 0 EventId.PLAYER_APPLICATION_SETTING_CHANGED, 0
@@ -1403,7 +1415,7 @@ class Protocol(utils.EventEmitter):
def notify_track_changed(self, identifier: bytes) -> None: def notify_track_changed(self, identifier: bytes) -> None:
"""Notify the connected peer of a Track change.""" """Notify the connected peer of a Track change."""
if len(identifier) != 8: if len(identifier) != 8:
raise core.InvalidArgumentError("identifier must be 8 bytes") raise InvalidArgumentError("identifier must be 8 bytes")
self.notify_event(TrackChangedEvent(identifier)) self.notify_event(TrackChangedEvent(identifier))
def notify_playback_position_changed(self, position: int) -> None: def notify_playback_position_changed(self, position: int) -> None:
@@ -1670,7 +1682,7 @@ class Protocol(utils.EventEmitter):
else: else:
logger.debug("unexpected PDU ID") logger.debug("unexpected PDU ID")
pending_command.response.set_exception( pending_command.response.set_exception(
core.ProtocolError( ProtocolError(
error_code=None, error_code=None,
error_namespace="avrcp", error_namespace="avrcp",
details="unexpected PDU ID", details="unexpected PDU ID",
@@ -1679,7 +1691,7 @@ class Protocol(utils.EventEmitter):
else: else:
logger.debug("unexpected response code") logger.debug("unexpected response code")
pending_command.response.set_exception( pending_command.response.set_exception(
core.ProtocolError( ProtocolError(
error_code=None, error_code=None,
error_namespace="avrcp", error_namespace="avrcp",
details="unexpected response code", details="unexpected response code",
@@ -1857,12 +1869,12 @@ class Protocol(utils.EventEmitter):
) -> None: ) -> None:
logger.debug(f"<<< AVRCP command PDU: {command}") logger.debug(f"<<< AVRCP command PDU: {command}")
async def get_supported_events() -> None: async def get_supported_events():
if ( if (
command.capability_id command.capability_id
!= GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED != GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED
): ):
raise core.InvalidArgumentError() raise Protocol.InvalidParameterError
supported_events = await self.delegate.get_supported_events() supported_events = await self.delegate.get_supported_events()
self.send_avrcp_response( self.send_avrcp_response(
@@ -1878,7 +1890,7 @@ class Protocol(utils.EventEmitter):
) -> None: ) -> None:
logger.debug(f"<<< AVRCP command PDU: {command}") logger.debug(f"<<< AVRCP command PDU: {command}")
async def set_absolute_volume() -> None: async def set_absolute_volume():
await self.delegate.set_absolute_volume(command.volume) await self.delegate.set_absolute_volume(command.volume)
effective_volume = await self.delegate.get_absolute_volume() effective_volume = await self.delegate.get_absolute_volume()
self.send_avrcp_response( self.send_avrcp_response(
@@ -1894,7 +1906,7 @@ class Protocol(utils.EventEmitter):
) -> None: ) -> None:
logger.debug(f"<<< AVRCP command PDU: {command}") logger.debug(f"<<< AVRCP command PDU: {command}")
async def register_notification() -> None: async def register_notification():
# Check if the event is supported. # Check if the event is supported.
supported_events = await self.delegate.get_supported_events() supported_events = await self.delegate.get_supported_events()
if command.event_id not in supported_events: if command.event_id not in supported_events:
+2 -2
View File
@@ -13,7 +13,7 @@
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from functools import partial from functools import partial
from typing import Optional, Union from typing import List, Optional, Union
class ColorError(ValueError): class ColorError(ValueError):
@@ -65,7 +65,7 @@ def color(
bg: Optional[ColorSpec] = None, bg: Optional[ColorSpec] = None,
style: Optional[str] = None, style: Optional[str] = None,
) -> str: ) -> str:
codes: list[ColorSpec] = [] codes: List[ColorSpec] = []
if fg: if fg:
codes.append(_color_code(fg, 30)) codes.append(_color_code(fg, 30))
+23 -84
View File
@@ -27,7 +27,7 @@ from bumble.colors import color
from bumble.core import ( from bumble.core import (
PhysicalTransport, PhysicalTransport,
) )
from bumble import hci
from bumble.hci import ( from bumble.hci import (
HCI_ACL_DATA_PACKET, HCI_ACL_DATA_PACKET,
HCI_COMMAND_DISALLOWED_ERROR, HCI_COMMAND_DISALLOWED_ERROR,
@@ -63,7 +63,7 @@ from bumble.hci import (
HCI_Packet, HCI_Packet,
HCI_Role_Change_Event, HCI_Role_Change_Event,
) )
from typing import Optional, Union, Any, TYPE_CHECKING from typing import Optional, Union, Dict, Any, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from bumble.link import LocalLink from bumble.link import LocalLink
@@ -108,9 +108,7 @@ class Connection:
def on_hci_acl_data_packet(self, packet): def on_hci_acl_data_packet(self, packet):
self.assembler.feed_packet(packet) self.assembler.feed_packet(packet)
self.controller.send_hci_packet( self.controller.send_hci_packet(
HCI_Number_Of_Completed_Packets_Event( HCI_Number_Of_Completed_Packets_Event([(self.handle, 1)])
connection_handles=[self.handle], num_completed_packets=[1]
)
) )
def on_acl_pdu(self, data): def on_acl_pdu(self, data):
@@ -134,17 +132,17 @@ class Controller:
self.hci_sink = None self.hci_sink = None
self.link = link self.link = link
self.central_connections: dict[Address, Connection] = ( self.central_connections: Dict[Address, Connection] = (
{} {}
) # Connections where this controller is the central ) # Connections where this controller is the central
self.peripheral_connections: dict[Address, Connection] = ( self.peripheral_connections: Dict[Address, Connection] = (
{} {}
) # Connections where this controller is the peripheral ) # Connections where this controller is the peripheral
self.classic_connections: dict[Address, Connection] = ( self.classic_connections: Dict[Address, Connection] = (
{} {}
) # Connections in BR/EDR ) # Connections in BR/EDR
self.central_cis_links: dict[int, CisLink] = {} # CIS links by handle self.central_cis_links: Dict[int, CisLink] = {} # CIS links by handle
self.peripheral_cis_links: dict[int, CisLink] = {} # CIS links by handle self.peripheral_cis_links: Dict[int, CisLink] = {} # CIS links by handle
self.hci_version = HCI_VERSION_BLUETOOTH_CORE_5_0 self.hci_version = HCI_VERSION_BLUETOOTH_CORE_5_0
self.hci_revision = 0 self.hci_revision = 0
@@ -394,7 +392,7 @@ class Controller:
peer_address=peer_address, peer_address=peer_address,
link=self.link, link=self.link,
transport=PhysicalTransport.LE, transport=PhysicalTransport.LE,
link_type=HCI_Connection_Complete_Event.LinkType.ACL, link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
) )
self.peripheral_connections[peer_address] = connection self.peripheral_connections[peer_address] = connection
logger.debug(f'New PERIPHERAL connection handle: 0x{connection_handle:04X}') logger.debug(f'New PERIPHERAL connection handle: 0x{connection_handle:04X}')
@@ -454,7 +452,7 @@ class Controller:
peer_address=peer_address, peer_address=peer_address,
link=self.link, link=self.link,
transport=PhysicalTransport.LE, transport=PhysicalTransport.LE,
link_type=HCI_Connection_Complete_Event.LinkType.ACL, link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
) )
self.central_connections[peer_address] = connection self.central_connections[peer_address] = connection
logger.debug( logger.debug(
@@ -544,14 +542,15 @@ class Controller:
acl_packet = HCI_AclDataPacket(connection.handle, 2, 0, len(data), data) acl_packet = HCI_AclDataPacket(connection.handle, 2, 0, len(data), data)
self.send_hci_packet(acl_packet) self.send_hci_packet(acl_packet)
def on_link_advertising_data(self, sender_address: Address, data: bytes): def on_link_advertising_data(self, sender_address, data):
# Ignore if we're not scanning # Ignore if we're not scanning
if self.le_scan_enable == 0: if self.le_scan_enable == 0:
return return
# Send a scan report # Send a scan report
report = HCI_LE_Advertising_Report_Event.Report( report = HCI_LE_Advertising_Report_Event.Report(
event_type=HCI_LE_Advertising_Report_Event.EventType.ADV_IND, HCI_LE_Advertising_Report_Event.Report.FIELDS,
event_type=HCI_LE_Advertising_Report_Event.ADV_IND,
address_type=sender_address.address_type, address_type=sender_address.address_type,
address=sender_address, address=sender_address,
data=data, data=data,
@@ -561,7 +560,8 @@ class Controller:
# Simulate a scan response # Simulate a scan response
report = HCI_LE_Advertising_Report_Event.Report( report = HCI_LE_Advertising_Report_Event.Report(
event_type=HCI_LE_Advertising_Report_Event.EventType.SCAN_RSP, HCI_LE_Advertising_Report_Event.Report.FIELDS,
event_type=HCI_LE_Advertising_Report_Event.SCAN_RSP,
address_type=sender_address.address_type, address_type=sender_address.address_type,
address=sender_address, address=sender_address,
data=data, data=data,
@@ -618,8 +618,8 @@ class Controller:
cis_sync_delay=0, cis_sync_delay=0,
transport_latency_c_to_p=0, transport_latency_c_to_p=0,
transport_latency_p_to_c=0, transport_latency_p_to_c=0,
phy_c_to_p=1, phy_c_to_p=0,
phy_p_to_c=1, phy_p_to_c=0,
nse=0, nse=0,
bn_c_to_p=0, bn_c_to_p=0,
bn_p_to_c=0, bn_p_to_c=0,
@@ -695,7 +695,7 @@ class Controller:
peer_address=peer_address, peer_address=peer_address,
link=self.link, link=self.link,
transport=PhysicalTransport.BR_EDR, transport=PhysicalTransport.BR_EDR,
link_type=HCI_Connection_Complete_Event.LinkType.ACL, link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
) )
self.classic_connections[peer_address] = connection self.classic_connections[peer_address] = connection
logger.debug( logger.debug(
@@ -709,7 +709,7 @@ class Controller:
connection_handle=connection_handle, connection_handle=connection_handle,
bd_addr=peer_address, bd_addr=peer_address,
encryption_enabled=False, encryption_enabled=False,
link_type=HCI_Connection_Complete_Event.LinkType.ACL, link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
) )
) )
else: else:
@@ -720,7 +720,7 @@ class Controller:
connection_handle=0, connection_handle=0,
bd_addr=peer_address, bd_addr=peer_address,
encryption_enabled=False, encryption_enabled=False,
link_type=HCI_Connection_Complete_Event.LinkType.ACL, link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
) )
) )
@@ -945,7 +945,7 @@ class Controller:
) )
) )
self.link.classic_sco_connect( self.link.classic_sco_connect(
self, connection.peer_address, HCI_Connection_Complete_Event.LinkType.ESCO self, connection.peer_address, HCI_Connection_Complete_Event.ESCO_LINK_TYPE
) )
def on_hci_enhanced_accept_synchronous_connection_request_command(self, command): def on_hci_enhanced_accept_synchronous_connection_request_command(self, command):
@@ -974,71 +974,10 @@ class Controller:
) )
) )
self.link.classic_accept_sco_connection( self.link.classic_accept_sco_connection(
self, connection.peer_address, HCI_Connection_Complete_Event.LinkType.ESCO self, connection.peer_address, HCI_Connection_Complete_Event.ESCO_LINK_TYPE
) )
def on_hci_sniff_mode_command(self, command: hci.HCI_Sniff_Mode_Command): def on_hci_switch_role_command(self, command):
'''
See Bluetooth spec Vol 4, Part E - 7.2.2 Sniff Mode command
'''
if self.link is None:
self.send_hci_packet(
hci.HCI_Command_Status_Event(
status=hci.HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
num_hci_command_packets=1,
command_opcode=command.op_code,
)
)
return
self.send_hci_packet(
hci.HCI_Command_Status_Event(
status=HCI_SUCCESS,
num_hci_command_packets=1,
command_opcode=command.op_code,
)
)
self.send_hci_packet(
hci.HCI_Mode_Change_Event(
status=HCI_SUCCESS,
connection_handle=command.connection_handle,
current_mode=hci.HCI_Mode_Change_Event.Mode.SNIFF,
interval=2,
)
)
def on_hci_exit_sniff_mode_command(self, command: hci.HCI_Exit_Sniff_Mode_Command):
'''
See Bluetooth spec Vol 4, Part E - 7.2.3 Exit Sniff Mode command
'''
if self.link is None:
self.send_hci_packet(
hci.HCI_Command_Status_Event(
status=hci.HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
num_hci_command_packets=1,
command_opcode=command.op_code,
)
)
return
self.send_hci_packet(
hci.HCI_Command_Status_Event(
status=HCI_SUCCESS,
num_hci_command_packets=1,
command_opcode=command.op_code,
)
)
self.send_hci_packet(
hci.HCI_Mode_Change_Event(
status=HCI_SUCCESS,
connection_handle=command.connection_handle,
current_mode=hci.HCI_Mode_Change_Event.Mode.ACTIVE,
interval=2,
)
)
def on_hci_switch_role_command(self, command: hci.HCI_Switch_Role_Command):
''' '''
See Bluetooth spec Vol 4, Part E - 7.2.8 Switch Role command See Bluetooth spec Vol 4, Part E - 7.2.8 Switch Role command
''' '''
+95 -11
View File
@@ -1,6 +1,6 @@
# Copyright 2021-2025 Google LLC # Copyright 2021-2022 Google LLC
# #
# Licensed under the Apache License, Version 2.0 (the "License") # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
@@ -12,6 +12,12 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# -----------------------------------------------------------------------------
# Crypto support
#
# See Bluetooth spec Vol 3, Part H - 2.2 CRYPTOGRAPHIC TOOLBOX
# -----------------------------------------------------------------------------
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -19,15 +25,19 @@ from __future__ import annotations
import logging import logging
import operator import operator
import secrets
try: import secrets
from bumble.crypto.cryptography import EccKey, e, aes_cmac from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
except ImportError: from cryptography.hazmat.primitives.asymmetric.ec import (
logging.getLogger(__name__).debug( generate_private_key,
"Unable to import cryptography, use built-in primitives." ECDH,
) EllipticCurvePrivateKey,
from bumble.crypto.builtin import EccKey, e, aes_cmac # type: ignore[assignment] EllipticCurvePublicNumbers,
EllipticCurvePrivateNumbers,
SECP256R1,
)
from cryptography.hazmat.primitives import cmac
from typing import Tuple
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -36,6 +46,55 @@ except ImportError:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
# Classes
# -----------------------------------------------------------------------------
class EccKey:
def __init__(self, private_key: EllipticCurvePrivateKey) -> None:
self.private_key = private_key
@classmethod
def generate(cls) -> EccKey:
private_key = generate_private_key(SECP256R1())
return cls(private_key)
@classmethod
def from_private_key_bytes(
cls, d_bytes: bytes, x_bytes: bytes, y_bytes: bytes
) -> EccKey:
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
x = int.from_bytes(x_bytes, byteorder='big', signed=False)
y = int.from_bytes(y_bytes, byteorder='big', signed=False)
private_key = EllipticCurvePrivateNumbers(
d, EllipticCurvePublicNumbers(x, y, SECP256R1())
).private_key()
return cls(private_key)
@property
def x(self) -> bytes:
return (
self.private_key.public_key()
.public_numbers()
.x.to_bytes(32, byteorder='big')
)
@property
def y(self) -> bytes:
return (
self.private_key.public_key()
.public_numbers()
.y.to_bytes(32, byteorder='big')
)
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
public_key = EllipticCurvePublicNumbers(x, y, SECP256R1()).public_key()
shared_key = self.private_key.exchange(ECDH(), public_key)
return shared_key
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Functions # Functions
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -73,6 +132,19 @@ def r() -> bytes:
return secrets.token_bytes(16) return secrets.token_bytes(16)
# -----------------------------------------------------------------------------
def e(key: bytes, data: bytes) -> bytes:
'''
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
'''
cipher = Cipher(algorithms.AES(reverse(key)), modes.ECB())
encryptor = cipher.encryptor()
return reverse(encryptor.update(reverse(data)))
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def ah(k: bytes, r: bytes) -> bytes: # pylint: disable=redefined-outer-name def ah(k: bytes, r: bytes) -> bytes: # pylint: disable=redefined-outer-name
''' '''
@@ -115,6 +187,18 @@ def s1(k: bytes, r1: bytes, r2: bytes) -> bytes:
return e(k, r2[0:8] + r1[0:8]) return e(k, r2[0:8] + r1[0:8])
# -----------------------------------------------------------------------------
def aes_cmac(m: bytes, k: bytes) -> bytes:
'''
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
NOTE: the input and output of this internal function are in big-endian byte order
'''
mac = cmac.CMAC(algorithms.AES(k))
mac.update(m)
return mac.finalize()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes: def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes:
''' '''
@@ -125,7 +209,7 @@ def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def f5(w: bytes, n1: bytes, n2: bytes, a1: bytes, a2: bytes) -> tuple[bytes, bytes]: def f5(w: bytes, n1: bytes, n2: bytes, a1: bytes, a2: bytes) -> Tuple[bytes, bytes]:
''' '''
See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation
Function f5 Function f5
-652
View File
@@ -1,652 +0,0 @@
# Copyright 2021-2025 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.
# The implementation is modified from:
# * AES - https://github.com/ricmoo/pyaes by Richard Moore under MIT License
# * CMAC - https://github.com/pycrypto/pycrypto by contributors under pycrypto License.
# -----------------------------------------------------------------------------
# Built-in implementation of cryptography primitives.
#
# Note: It's very dangerous to use this library in the real world.
# -----------------------------------------------------------------------------
from __future__ import annotations
import dataclasses
import functools
import copy
import secrets
import struct
from typing import Optional
from bumble import core
def _compact_word(word: bytes) -> int:
return int.from_bytes(word, "big")
def _shift_bytes(bs: bytes, xor_lsb: int = 0) -> bytes:
return ((int.from_bytes(bs, "big") << 1) ^ xor_lsb).to_bytes(len(bs) + 1, "big")[1:]
def _xor(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))
# Based *largely* on the Rijndael implementation
# See: http://csrc.nist.gov/publications/FIPS/FIPS197/FIPS-197.pdf
class _AES:
'''Encapsulates the AES block cipher.
You generally should not need this. Use the AESModeOfOperation classes
below instead.'''
# fmt: off
# Number of rounds by key size
_NUMBER_OF_ROUNDS = {16: 10, 24: 12, 32: 14}
# Round constant words
_RCON = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ]
# S-box and Inverse S-box (S is for Substitution)
_S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ]
_S_INV =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ]
# Transformations for encryption
_T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ]
_T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ]
_T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ]
_T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ]
# Transformations for decryption
_T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ]
_T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ]
_T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ]
_T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ]
# Transformations for decryption key expansion
_U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ]
_U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ]
_U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ]
_U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ]
# 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)}')
rounds = self._NUMBER_OF_ROUNDS[len(key)]
# Encryption round keys
self._ke = [[0] * 4 for i in range(rounds + 1)]
# Decryption round keys
self._kd = [[0] * 4 for i in range(rounds + 1)]
round_key_count = (rounds + 1) * 4
kc = len(key) // 4
# Convert the key into ints
tk = [struct.unpack('>i', key[i : i + 4])[0] for i in range(0, len(key), 4)]
# Copy values into round key arrays
for i in range(0, kc):
self._ke[i // 4][i % 4] = tk[i]
self._kd[rounds - (i // 4)][i % 4] = tk[i]
# Key expansion (FIPS-197 section 5.2)
r_con_pointer = 0
t = kc
while t < round_key_count:
tt = tk[kc - 1]
tk[0] ^= (
(self._S[(tt >> 16) & 0xFF] << 24)
^ (self._S[(tt >> 8) & 0xFF] << 16)
^ (self._S[tt & 0xFF] << 8)
^ self._S[(tt >> 24) & 0xFF]
^ (self._RCON[r_con_pointer] << 24)
)
r_con_pointer += 1
if kc != 8:
for i in range(1, kc):
tk[i] ^= tk[i - 1]
# Key expansion for 256-bit keys is "slightly different" (FIPS-197)
else:
for i in range(1, kc // 2):
tk[i] ^= tk[i - 1]
tt = tk[kc // 2 - 1]
tk[kc // 2] ^= (
self._S[tt & 0xFF]
^ (self._S[(tt >> 8) & 0xFF] << 8)
^ (self._S[(tt >> 16) & 0xFF] << 16)
^ (self._S[(tt >> 24) & 0xFF] << 24)
)
for i in range(kc // 2 + 1, kc):
tk[i] ^= tk[i - 1]
# Copy values into round key arrays
j = 0
while j < kc and t < round_key_count:
self._ke[t // 4][t % 4] = tk[j]
self._kd[rounds - (t // 4)][t % 4] = tk[j]
j += 1
t += 1
# Inverse-Cipher-ify the decryption round key (FIPS-197 section 5.3)
for r in range(1, rounds):
for j in range(0, 4):
tt = self._kd[r][j]
self._kd[r][j] = (
self._U1[(tt >> 24) & 0xFF]
^ self._U2[(tt >> 16) & 0xFF]
^ self._U3[(tt >> 8) & 0xFF]
^ self._U4[tt & 0xFF]
)
def encrypt(self, plaintext: bytes) -> bytes:
"""Encrypt a block of plain text using the AES block cipher."""
if len(plaintext) != 16:
raise core.InvalidArgumentError(f'wrong block length {len(plaintext)}')
rounds = len(self._ke) - 1
(s1, s2, s3) = [1, 2, 3]
a = [0, 0, 0, 0]
# Convert plaintext to (ints ^ key)
t = [
(_compact_word(plaintext[4 * i : 4 * i + 4]) ^ self._ke[0][i])
for i in range(0, 4)
]
# Apply round transforms
for r in range(1, rounds):
for i in range(0, 4):
a[i] = (
self._T1[(t[i] >> 24) & 0xFF]
^ self._T2[(t[(i + s1) % 4] >> 16) & 0xFF]
^ self._T3[(t[(i + s2) % 4] >> 8) & 0xFF]
^ self._T4[t[(i + s3) % 4] & 0xFF]
^ self._ke[r][i]
)
t = copy.copy(a)
# The last round is special
result = []
for i in range(0, 4):
tt = self._ke[rounds][i]
result.append((self._S[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
result.append((self._S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
result.append((self._S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
result.append((self._S[t[(i + s3) % 4] & 0xFF] ^ tt) & 0xFF)
return bytes(result)
def decrypt(self, cipher_text: bytes) -> bytes:
"""Decrypt a block of cipher text using the AES block cipher."""
if len(cipher_text) != 16:
raise core.InvalidArgumentError(f'wrong block length {len(cipher_text)}')
rounds = len(self._kd) - 1
(s1, s2, s3) = [3, 2, 1]
a = [0, 0, 0, 0]
# Convert ciphertext to (ints ^ key)
t = [
(_compact_word(cipher_text[4 * i : 4 * i + 4]) ^ self._kd[0][i])
for i in range(0, 4)
]
# Apply round transforms
for r in range(1, rounds):
for i in range(0, 4):
a[i] = (
self._T5[(t[i] >> 24) & 0xFF]
^ self._T6[(t[(i + s1) % 4] >> 16) & 0xFF]
^ self._T7[(t[(i + s2) % 4] >> 8) & 0xFF]
^ self._T8[t[(i + s3) % 4] & 0xFF]
^ self._kd[r][i]
)
t = copy.copy(a)
# The last round is special
result = []
for i in range(0, 4):
tt = self._kd[rounds][i]
result.append((self._S_INV[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
result.append(
(self._S_INV[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF
)
result.append(
(self._S_INV[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF
)
result.append((self._S_INV[t[(i + s3) % 4] & 0xFF] ^ tt) & 0xFF)
return bytes(result)
class _ECB:
def __init__(self, key: bytes):
self._aes = _AES(key)
def encrypt(self, plaintext: bytes) -> bytes:
return b"".join(
[
self._aes.encrypt(
plaintext[offset : offset + 16].ljust(16, b"\x00") # Pad 0.
)
for offset in range(0, len(plaintext), 16)
]
)
def decrypt(self, cipher_text: bytes) -> bytes:
return b"".join(
[
self._aes.encrypt(cipher_text[offset : offset + 16])
for offset in range(0, len(cipher_text), 16)
]
)
class _CBC:
def __init__(self, key: bytes, iv: bytes = bytes(16)) -> None:
if len(iv) != 16:
raise core.InvalidArgumentError(
f'initialization vector must be 16 bytes, get {len(iv)}'
)
else:
self._last_cipher_block = iv
self._aes = _AES(key)
def encrypt(self, plaintext: bytes) -> bytes:
cipher_text = b""
for offset in range(0, len(plaintext), 16):
pre_cipher_block = _xor(
plaintext[offset : offset + 16], self._last_cipher_block
)
self._last_cipher_block = self._aes.encrypt(pre_cipher_block)
cipher_text += self._last_cipher_block
return cipher_text
def decrypt(self, cipher_text: bytes) -> bytes:
plaintext = b""
for offset in range(0, len(cipher_text), 16):
plaintext += _xor(
self._aes.decrypt(cipher_text[offset : offset + 16]),
self._last_cipher_block,
)
self._last_cipher_block = cipher_text[offset : offset + 16]
return plaintext
class _CMAC:
def __init__(
self,
key: bytes,
msg: bytes = bytes(16),
mac_len: int = 16,
update_after_digest: bool = False,
) -> None:
self.digest_size = mac_len
self._key = key
self._block_size = bs = 16
self._mac_tag: Optional[bytes] = None
self._update_after_digest = update_after_digest
# Section 5.3 of NIST SP 800 38B and Appendix B
if bs == 8:
const_Rb = 0x1B
self._max_size = 8 * (2**21)
elif bs == 16:
const_Rb = 0x87
self._max_size = 16 * (2**48)
else:
raise core.InvalidArgumentError(
f"CMAC requires a cipher with a block size of 8 or 16 bytes, not {bs}"
)
# Compute sub-keys
zero_block = bytes(bs)
self._ecb = _ECB(key)
L = self._ecb.encrypt(zero_block)
if L[0] & 0x80:
self._k1 = _shift_bytes(L, const_Rb)
else:
self._k1 = _shift_bytes(L)
if self._k1[0] & 0x80:
self._k2 = _shift_bytes(self._k1, const_Rb)
else:
self._k2 = _shift_bytes(self._k1)
# Initialize CBC cipher with zero IV
self._cbc = _CBC(key, zero_block)
# Cache for outstanding data to authenticate
self._cache = bytearray(bs)
self._cache_n = 0
# Last piece of cipher text produced
self._last_ct = zero_block
# Last block that was encrypted with AES
self._last_pt: Optional[bytes] = None
# Counter for total message size
self._data_size = 0
if msg:
self.update(msg)
def update(self, msg: bytes) -> _CMAC:
"""Authenticate the next chunk of message.
Args:
data (byte string/byte array/memoryview): The next chunk of data
"""
if self._mac_tag is not None and not self._update_after_digest:
raise core.InvalidStateError(
"update() cannot be called after digest() or verify()"
)
self._data_size += len(msg)
bs = self._block_size
if self._cache_n > 0:
filler = min(bs - self._cache_n, len(msg))
self._cache[self._cache_n : self._cache_n + filler] = msg[:filler]
self._cache_n += filler
if self._cache_n < bs:
return self
msg = msg[filler:]
self._update(self._cache)
self._cache_n = 0
remain = len(msg) % bs
if remain > 0:
self._update(msg[:-remain])
self._cache[:remain] = msg[-remain:]
else:
self._update(msg)
self._cache_n = remain
return self
def _update(self, data_block: bytes) -> None:
"""Update a block aligned to the block boundary"""
bs = self._block_size
assert len(data_block) % bs == 0
if len(data_block) == 0:
return
ct = self._cbc.encrypt(data_block)
if len(data_block) == bs:
second_last = self._last_ct
else:
second_last = ct[-bs * 2 : -bs]
self._last_ct = ct[-bs:]
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:
return self._mac_tag
if self._data_size > self._max_size:
raise core.InvalidArgumentError("MAC is unsafe for this message")
if self._cache_n == 0 and self._data_size > 0 and self._last_pt:
# Last block was full
pt = _xor(self._last_pt, self._k1)
else:
# Last block is partial (or message length is zero)
partial = self._cache[:]
partial[self._cache_n :] = b'\x80' + b'\x00' * (bs - self._cache_n - 1)
pt = _xor(_xor(self._last_ct, partial), self._k2)
self._mac_tag = self._ecb.encrypt(pt)[: self.digest_size]
return self._mac_tag
# Define the original Point class for clarity and conversion purposes
@dataclasses.dataclass
class _Point:
"""Represents a point on the elliptic curve in affine coordinates."""
curve: _EllipticCurve
x: int = 0
y: int = 0
infinite: bool = False
@dataclasses.dataclass(frozen=True)
class _JacobianPoint:
"""Represents a point on the elliptic curve in Jacobian coordinates."""
curve: _EllipticCurve
x: int = 1 # For point at infinity (1:1:0)
y: int = 1
z: int = 0 # z = 0 indicates point at infinity
@classmethod
def point_at_infinity(cls, curve: _EllipticCurve) -> _JacobianPoint:
return _JacobianPoint(curve=curve, x=1, y=1, z=0)
@classmethod
def from_affine(cls, affine_point: _Point) -> _JacobianPoint:
if affine_point.infinite:
return _JacobianPoint.point_at_infinity(affine_point.curve)
# A simple conversion is (x, y, 1)
return _JacobianPoint(
curve=affine_point.curve, x=affine_point.x, y=affine_point.y, z=1
)
def to_affine(self) -> _Point:
if self.z == 0:
return _Point(infinite=True, curve=self.curve)
p = self.curve.p
inv_z = pow(self.z, -1, p)
affine_x = (self.x * inv_z**2) % p
affine_y = (self.y * inv_z**3) % p
return _Point(curve=self.curve, x=affine_x, y=affine_y, infinite=False)
def double(self) -> _JacobianPoint:
if self.z == 0 or self.y == 0:
return _JacobianPoint.point_at_infinity(self.curve)
s = 4 * self.x * self.y**2
m = 3 * self.x**2 + self.curve.a * self.z**4
x2 = m**2 - 2 * s
y2 = m * (s - x2) - 8 * self.y**4
z2 = 2 * self.y * self.z
p = self.curve.p
return _JacobianPoint(curve=self.curve, x=x2 % p, y=y2 % p, z=z2 % p)
def __add__(self, other: _JacobianPoint) -> _JacobianPoint:
if self.z == 0 and other.z == 0:
return _JacobianPoint.point_at_infinity(self.curve)
elif self.z == 0:
return other
elif other.z == 0:
return self
x1 = self.x
y1 = self.y
z1 = self.z
x2 = other.x
y2 = other.y
z2 = other.z
p = self.curve.p
u1 = (x1 * z2**2) % p
u2 = (x2 * z1**2) % p
s1 = (y1 * z2**3) % p
s2 = (y2 * z1**3) % p
if u1 == u2:
if s1 != s2:
return _JacobianPoint.point_at_infinity(self.curve)
else:
return self.double()
else:
h = u2 - u1
r = s2 - s1
h3 = h**3 % p
u1h2 = (u1 * h**2) % p
x3 = r**2 - h3 - 2 * u1h2
y3 = r * (u1h2 - x3) - s1 * h3
z3 = h * z1 * z2
return _JacobianPoint(self.curve, x3 % p, y3 % p, z3 % p)
def __mul__(self, k: int) -> _JacobianPoint:
addend = self
result = _JacobianPoint.point_at_infinity(self.curve)
while k > 0:
if k % 2 != 0:
result = result + addend
addend = addend.double()
k = k >> 1
return result
def __rmul__(self, k: int) -> _JacobianPoint:
return self * k
@dataclasses.dataclass
class _EllipticCurve:
p: int
a: int
b: int
n: int
g_x: int
g_y: int
_generator_jacobian: _JacobianPoint = dataclasses.field(init=False)
def __post_init__(self):
self._generator_jacobian = _JacobianPoint(
curve=self, x=self.g_x, y=self.g_y, z=1
)
@dataclasses.dataclass
class PrivateKey:
key: int
curve: _EllipticCurve
def generate_private_key(self) -> PrivateKey:
"""Generates a random private key."""
return self.PrivateKey(key=secrets.randbelow(self.n), curve=self)
def generate_public_key(self, private_key: int) -> _Point:
"""Generates a public key from a private key using Jacobian coordinates for scalar multiplication."""
public_key_jacobian = self._generator_jacobian * private_key
return public_key_jacobian.to_affine()
def ecdh_shared_secret(self, private_key: int, other_public_key: _Point) -> bytes:
"""Computes the shared secret using ECDH."""
other_public_key_jacobian = _JacobianPoint.from_affine(other_public_key)
shared_point_jacobian = other_public_key_jacobian * private_key
shared_point_affine = shared_point_jacobian.to_affine()
if shared_point_affine.infinite:
raise core.InvalidPacketError(
"Shared secret calculation resulted in the point at infinite"
)
return shared_point_affine.x.to_bytes(32, 'big')
@classmethod
def SECP256R1(cls) -> _EllipticCurve:
p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
a = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 # Curve order
g_x = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
g_y = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
return _EllipticCurve(p=p, a=a, b=b, n=n, g_x=g_x, g_y=g_y)
class EccKey:
def __init__(self, private_key: _EllipticCurve.PrivateKey) -> None:
self.private_key = private_key
@functools.cached_property
def x(self) -> bytes:
return self.private_key.curve.generate_public_key(
self.private_key.key
).x.to_bytes(32, byteorder='big')
@functools.cached_property
def y(self) -> bytes:
return self.private_key.curve.generate_public_key(
self.private_key.key
).y.to_bytes(32, byteorder='big')
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
return self.private_key.curve.ecdh_shared_secret(
self.private_key.key,
_Point(x=x, y=y, curve=self.private_key.curve),
)
@classmethod
def generate(cls) -> EccKey:
return EccKey(_EllipticCurve.SECP256R1().generate_private_key())
@classmethod
def from_private_key_bytes(cls, d_bytes: bytes) -> EccKey:
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
return EccKey(_EllipticCurve.PrivateKey(d, _EllipticCurve.SECP256R1()))
def e(key: bytes, data: bytes) -> bytes:
'''
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
'''
return _ECB(key[::-1]).encrypt(data[::-1])[::-1]
def aes_cmac(m: bytes, k: bytes) -> bytes:
'''
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
NOTE: the input and output of this internal function are in big-endian byte order
'''
return _CMAC(key=k, msg=m).digest()
-84
View File
@@ -1,84 +0,0 @@
# Copyright 2021-2025 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.
from __future__ import annotations
import functools
from cryptography.hazmat.primitives import ciphers
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import cmac
def e(key: bytes, data: bytes) -> bytes:
'''
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
'''
cipher = ciphers.Cipher(algorithms.AES(key[::-1]), modes.ECB())
encryptor = cipher.encryptor()
return encryptor.update(data[::-1])[::-1]
class EccKey:
def __init__(self, private_key: ec.EllipticCurvePrivateKey) -> None:
self.private_key = private_key
@classmethod
def generate(cls) -> EccKey:
return EccKey(ec.generate_private_key(ec.SECP256R1()))
@classmethod
def from_private_key_bytes(cls, d_bytes: bytes) -> EccKey:
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
return EccKey(ec.derive_private_key(d, ec.SECP256R1()))
@functools.cached_property
def x(self) -> bytes:
return (
self.private_key.public_key()
.public_numbers()
.x.to_bytes(32, byteorder='big')
)
@functools.cached_property
def y(self) -> bytes:
return (
self.private_key.public_key()
.public_numbers()
.y.to_bytes(32, byteorder='big')
)
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
return self.private_key.exchange(
ec.ECDH(),
ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1()).public_key(),
)
def aes_cmac(m: bytes, k: bytes) -> bytes:
'''
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
NOTE: the input and output of this internal function are in big-endian byte order
'''
mac = cmac.CMAC(algorithms.AES(k))
mac.update(m)
return mac.finalize()
+183 -350
View File
@@ -35,10 +35,12 @@ import secrets
import sys import sys
from typing import ( from typing import (
Any, Any,
Awaitable,
Callable, Callable,
ClassVar, ClassVar,
Deque,
Dict,
Optional, Optional,
Type,
TypeVar, TypeVar,
Union, Union,
cast, cast,
@@ -85,7 +87,6 @@ from bumble.profiles import gatt_service
if TYPE_CHECKING: if TYPE_CHECKING:
from bumble.transport.common import TransportSource, TransportSink from bumble.transport.common import TransportSource, TransportSink
_T = TypeVar('_T')
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -98,9 +99,9 @@ logger = logging.getLogger(__name__)
# fmt: off # fmt: off
# pylint: disable=line-too-long # pylint: disable=line-too-long
DEVICE_MIN_SCAN_INTERVAL = 2.5 DEVICE_MIN_SCAN_INTERVAL = 25
DEVICE_MAX_SCAN_INTERVAL = 10240 DEVICE_MAX_SCAN_INTERVAL = 10240
DEVICE_MIN_SCAN_WINDOW = 2.5 DEVICE_MIN_SCAN_WINDOW = 25
DEVICE_MAX_SCAN_WINDOW = 10240 DEVICE_MAX_SCAN_WINDOW = 10240
DEVICE_MIN_LE_RSSI = -127 DEVICE_MIN_LE_RSSI = -127
DEVICE_MAX_LE_RSSI = 20 DEVICE_MAX_LE_RSSI = 20
@@ -139,9 +140,6 @@ DEVICE_DEFAULT_ADVERTISING_TX_POWER = (
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0 DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT = 5.0 DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT = 5.0
DEVICE_DEFAULT_LE_RPA_TIMEOUT = 15 * 60 # 15 minutes (in seconds) DEVICE_DEFAULT_LE_RPA_TIMEOUT = 15 * 60 # 15 minutes (in seconds)
DEVICE_DEFAULT_ISO_CIS_MAX_SDU = 251
DEVICE_DEFAULT_ISO_CIS_RTN = 10
DEVICE_DEFAULT_ISO_CIS_MAX_TRANSPORT_LATENCY = 100
# fmt: on # fmt: on
# pylint: enable=line-too-long # pylint: enable=line-too-long
@@ -204,35 +202,25 @@ class Advertisement:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class LegacyAdvertisement(Advertisement): class LegacyAdvertisement(Advertisement):
@classmethod @classmethod
def from_advertising_report( def from_advertising_report(cls, report):
cls, report: hci.HCI_LE_Advertising_Report_Event.Report
) -> Self:
return cls( return cls(
address=report.address, address=report.address,
rssi=report.rssi, rssi=report.rssi,
is_legacy=True, is_legacy=True,
is_connectable=( is_connectable=report.event_type
report.event_type in (
in ( hci.HCI_LE_Advertising_Report_Event.ADV_IND,
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND, hci.HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND,
)
), ),
is_directed=( is_directed=report.event_type
report.event_type == hci.HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
== hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND is_scannable=report.event_type
), in (
is_scannable=( hci.HCI_LE_Advertising_Report_Event.ADV_IND,
report.event_type hci.HCI_LE_Advertising_Report_Event.ADV_SCAN_IND,
in (
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_SCAN_IND,
)
),
is_scan_response=(
report.event_type
== hci.HCI_LE_Advertising_Report_Event.EventType.SCAN_RSP
), ),
is_scan_response=report.event_type
== hci.HCI_LE_Advertising_Report_Event.SCAN_RSP,
data_bytes=report.data, data_bytes=report.data,
) )
@@ -240,20 +228,18 @@ class LegacyAdvertisement(Advertisement):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class ExtendedAdvertisement(Advertisement): class ExtendedAdvertisement(Advertisement):
@classmethod @classmethod
def from_advertising_report( def from_advertising_report(cls, report):
cls, report: hci.HCI_LE_Extended_Advertising_Report_Event.Report
) -> Self:
# fmt: off # fmt: off
# pylint: disable=line-too-long # pylint: disable=line-too-long
return cls( return cls(
address = report.address, address = report.address,
rssi = report.rssi, rssi = report.rssi,
is_legacy = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.LEGACY_ADVERTISING_PDU_USED) != 0, is_legacy = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.LEGACY_ADVERTISING_PDU_USED) != 0,
is_anonymous = report.address.address_type == hci.HCI_LE_Extended_Advertising_Report_Event.ANONYMOUS_ADDRESS_TYPE, is_anonymous = report.address.address_type == hci.HCI_LE_Extended_Advertising_Report_Event.ANONYMOUS_ADDRESS_TYPE,
is_connectable = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.CONNECTABLE_ADVERTISING) != 0, is_connectable = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.CONNECTABLE_ADVERTISING) != 0,
is_directed = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.DIRECTED_ADVERTISING) != 0, is_directed = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.DIRECTED_ADVERTISING) != 0,
is_scannable = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.SCANNABLE_ADVERTISING) != 0, is_scannable = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.SCANNABLE_ADVERTISING) != 0,
is_scan_response = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.SCAN_RESPONSE) != 0, is_scan_response = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.SCAN_RESPONSE) != 0,
is_complete = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_COMPLETE, is_complete = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_COMPLETE,
is_truncated = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME, is_truncated = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME,
primary_phy = report.primary_phy, primary_phy = report.primary_phy,
@@ -450,7 +436,7 @@ class AdvertisingEventProperties:
@classmethod @classmethod
def from_advertising_type( def from_advertising_type(
cls: type[AdvertisingEventProperties], cls: Type[AdvertisingEventProperties],
advertising_type: AdvertisingType, advertising_type: AdvertisingType,
) -> AdvertisingEventProperties: ) -> AdvertisingEventProperties:
return cls( return cls(
@@ -492,18 +478,7 @@ class PeriodicAdvertisement:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@dataclass @dataclass
class BigInfoAdvertisement: class BIGInfoAdvertisement:
class Framing(utils.OpenIntEnum):
# fmt: off
UNFRAMED = 0X00
FRAMED_SEGMENTABLE_MODE = 0X01
FRAMED_UNSEGMENTED_MODE = 0X02
class Encryption(utils.OpenIntEnum):
# fmt: off
UNENCRYPTED = 0x00
ENCRYPTED = 0x01
address: hci.Address address: hci.Address
sid: int sid: int
num_bis: int num_bis: int
@@ -516,8 +491,8 @@ class BigInfoAdvertisement:
sdu_interval: int sdu_interval: int
max_sdu: int max_sdu: int
phy: hci.Phy phy: hci.Phy
framing: Framing framed: bool
encryption: Encryption encrypted: bool
@classmethod @classmethod
def from_report(cls, address: hci.Address, sid: int, report) -> Self: def from_report(cls, address: hci.Address, sid: int, report) -> Self:
@@ -534,8 +509,8 @@ class BigInfoAdvertisement:
report.sdu_interval, report.sdu_interval,
report.max_sdu, report.max_sdu,
hci.Phy(report.phy), hci.Phy(report.phy),
cls.Framing(report.framing), report.framing != 0,
cls.Encryption(report.encryption), report.encryption != 0,
) )
@@ -1027,7 +1002,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
def on_biginfo_advertising_report(self, report) -> None: def on_biginfo_advertising_report(self, report) -> None:
self.emit( self.emit(
self.EVENT_BIGINFO_ADVERTISEMENT, self.EVENT_BIGINFO_ADVERTISEMENT,
BigInfoAdvertisement.from_report(self.advertiser_address, self.sid, report), BIGInfoAdvertisement.from_report(self.advertiser_address, self.sid, report),
) )
def __str__(self) -> str: def __str__(self) -> str:
@@ -1045,24 +1020,14 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@dataclass @dataclass
class BigParameters: class BigParameters:
class Packing(utils.OpenIntEnum):
# fmt: off
SEQUENTIAL = 0x00
INTERLEAVED = 0x01
class Framing(utils.OpenIntEnum):
# fmt: off
UNFRAMED = 0x00
FRAMED = 0x01
num_bis: int num_bis: int
sdu_interval: int # SDU interval, in microseconds sdu_interval: int
max_sdu: int max_sdu: int
max_transport_latency: int # Max transport latency, in milliseconds max_transport_latency: int
rtn: int rtn: int
phy: hci.PhyBit = hci.PhyBit.LE_2M phy: hci.PhyBit = hci.PhyBit.LE_2M
packing: Packing = Packing.SEQUENTIAL packing: int = 0
framing: Framing = Framing.UNFRAMED framing: int = 0
broadcast_code: bytes | None = None broadcast_code: bytes | None = None
@@ -1085,15 +1050,15 @@ class Big(utils.EventEmitter):
state: State = State.PENDING state: State = State.PENDING
# Attributes provided by BIG Create Complete event # Attributes provided by BIG Create Complete event
big_sync_delay: int = 0 # Sync delay, in microseconds big_sync_delay: int = 0
transport_latency_big: int = 0 # Transport latency, in microseconds transport_latency_big: int = 0
phy: hci.Phy = hci.Phy.LE_1M phy: int = 0
nse: int = 0 nse: int = 0
bn: int = 0 bn: int = 0
pto: int = 0 pto: int = 0
irc: int = 0 irc: int = 0
max_pdu: int = 0 max_pdu: int = 0
iso_interval: float = 0.0 # ISO interval, in milliseconds iso_interval: float = 0.0
bis_links: Sequence[BisLink] = () bis_links: Sequence[BisLink] = ()
def __post_init__(self) -> None: def __post_init__(self) -> None:
@@ -1378,7 +1343,7 @@ class Peer:
return self.gatt_client.get_characteristics_by_uuid(uuid, service) return self.gatt_client.get_characteristics_by_uuid(uuid, service)
def create_service_proxy( def create_service_proxy(
self, proxy_class: type[_PROXY_CLASS] self, proxy_class: Type[_PROXY_CLASS]
) -> Optional[_PROXY_CLASS]: ) -> Optional[_PROXY_CLASS]:
if proxy := proxy_class.from_client(self.gatt_client): if proxy := proxy_class.from_client(self.gatt_client):
return cast(_PROXY_CLASS, proxy) return cast(_PROXY_CLASS, proxy)
@@ -1386,7 +1351,7 @@ class Peer:
return None return None
async def discover_service_and_create_proxy( async def discover_service_and_create_proxy(
self, proxy_class: type[_PROXY_CLASS] self, proxy_class: Type[_PROXY_CLASS]
) -> Optional[_PROXY_CLASS]: ) -> Optional[_PROXY_CLASS]:
# Discover the first matching service and its characteristics # Discover the first matching service and its characteristics
services = await self.discover_service(proxy_class.SERVICE_CLASS.UUID) services = await self.discover_service(proxy_class.SERVICE_CLASS.UUID)
@@ -1499,7 +1464,7 @@ class _IsoLink:
check_result=True, check_result=True,
) )
async def remove_data_path(self, directions: Iterable[_IsoLink.Direction]) -> int: async def remove_data_path(self, direction: _IsoLink.Direction) -> int:
"""Remove a data path with controller on given direction. """Remove a data path with controller on given direction.
Args: Args:
@@ -1511,9 +1476,7 @@ class _IsoLink:
response = await self.device.send_command( response = await self.device.send_command(
hci.HCI_LE_Remove_ISO_Data_Path_Command( hci.HCI_LE_Remove_ISO_Data_Path_Command(
connection_handle=self.handle, connection_handle=self.handle,
data_path_direction=sum( data_path_direction=direction,
1 << direction for direction in set(directions)
),
), ),
check_result=False, check_result=False,
) )
@@ -1523,74 +1486,10 @@ class _IsoLink:
"""Write an ISO SDU.""" """Write an ISO SDU."""
self.device.host.send_iso_sdu(connection_handle=self.handle, sdu=sdu) self.device.host.send_iso_sdu(connection_handle=self.handle, sdu=sdu)
async def get_tx_time_stamp(self) -> tuple[int, int, int]:
response = await self.device.host.send_command(
hci.HCI_LE_Read_ISO_TX_Sync_Command(connection_handle=self.handle),
check_result=True,
)
return (
response.return_parameters.packet_sequence_number,
response.return_parameters.tx_time_stamp,
response.return_parameters.time_offset,
)
@property @property
def data_packet_queue(self) -> DataPacketQueue | None: def data_packet_queue(self) -> DataPacketQueue | None:
return self.device.host.get_data_packet_queue(self.handle) return self.device.host.get_data_packet_queue(self.handle)
async def drain(self) -> None:
if data_packet_queue := self.data_packet_queue:
await data_packet_queue.drain(self.handle)
# -----------------------------------------------------------------------------
@dataclass
class CigParameters:
class WorstCaseSca(utils.OpenIntEnum):
# fmt: off
SCA_251_TO_500_PPM = 0x00
SCA_151_TO_250_PPM = 0x01
SCA_101_TO_150_PPM = 0x02
SCA_76_TO_100_PPM = 0x03
SCA_51_TO_75_PPM = 0x04
SCA_31_TO_50_PPM = 0x05
SCA_21_TO_30_PPM = 0x06
SCA_0_TO_20_PPM = 0x07
class Packing(utils.OpenIntEnum):
# fmt: off
SEQUENTIAL = 0x00
INTERLEAVED = 0x01
class Framing(utils.OpenIntEnum):
# fmt: off
UNFRAMED = 0x00
FRAMED = 0x01
@dataclass
class CisParameters:
cis_id: int
max_sdu_c_to_p: int = DEVICE_DEFAULT_ISO_CIS_MAX_SDU
max_sdu_p_to_c: int = DEVICE_DEFAULT_ISO_CIS_MAX_SDU
phy_c_to_p: hci.PhyBit = hci.PhyBit.LE_2M
phy_p_to_c: hci.PhyBit = hci.PhyBit.LE_2M
rtn_c_to_p: int = DEVICE_DEFAULT_ISO_CIS_RTN # Number of C->P retransmissions
rtn_p_to_c: int = DEVICE_DEFAULT_ISO_CIS_RTN # Number of P->C retransmissions
cig_id: int
cis_parameters: list[CisParameters]
sdu_interval_c_to_p: int # C->P SDU interval, in microseconds
sdu_interval_p_to_c: int # P->C SDU interval, in microseconds
worst_case_sca: WorstCaseSca = WorstCaseSca.SCA_251_TO_500_PPM
packing: Packing = Packing.SEQUENTIAL
framing: Framing = Framing.UNFRAMED
max_transport_latency_c_to_p: int = (
DEVICE_DEFAULT_ISO_CIS_MAX_TRANSPORT_LATENCY # Max C->P transport latency, in milliseconds
)
max_transport_latency_p_to_c: int = (
DEVICE_DEFAULT_ISO_CIS_MAX_TRANSPORT_LATENCY # Max C->P transport latency, in milliseconds
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@dataclass @dataclass
@@ -1604,20 +1503,6 @@ class CisLink(utils.EventEmitter, _IsoLink):
handle: int # CIS handle assigned by Controller (in LE_Set_CIG_Parameters Complete or LE_CIS_Request events) handle: int # CIS handle assigned by Controller (in LE_Set_CIG_Parameters Complete or LE_CIS_Request events)
cis_id: int # CIS ID assigned by Central device cis_id: int # CIS ID assigned by Central device
cig_id: int # CIG ID assigned by Central device cig_id: int # CIG ID assigned by Central device
cig_sync_delay: int = 0 # CIG sync delay, in microseconds
cis_sync_delay: int = 0 # CIS sync delay, in microseconds
transport_latency_c_to_p: int = 0 # C->P transport latency, in microseconds
transport_latency_p_to_c: int = 0 # P->C transport latency, in microseconds
phy_c_to_p: Optional[hci.Phy] = None
phy_p_to_c: Optional[hci.Phy] = None
nse: int = 0
bn_c_to_p: int = 0
bn_p_to_c: int = 0
ft_c_to_p: int = 0
ft_p_to_c: int = 0
max_pdu_c_to_p: int = 0
max_pdu_p_to_c: int = 0
iso_interval: float = 0.0 # ISO interval, in milliseconds
state: State = State.PENDING state: State = State.PENDING
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
@@ -1660,7 +1545,7 @@ class IsoPacketStream:
self.iso_link = iso_link self.iso_link = iso_link
self.data_packet_queue = iso_link.data_packet_queue self.data_packet_queue = iso_link.data_packet_queue
self.data_packet_queue.on('flow', self._on_flow) self.data_packet_queue.on('flow', self._on_flow)
self._thresholds: collections.deque[int] = collections.deque() self._thresholds: Deque[int] = collections.deque()
self._semaphore = asyncio.Semaphore(max_queue_size) self._semaphore = asyncio.Semaphore(max_queue_size)
def _on_flow(self) -> None: def _on_flow(self) -> None:
@@ -1700,7 +1585,6 @@ class Connection(utils.CompositeEventEmitter):
peer_resolvable_address: Optional[hci.Address] peer_resolvable_address: Optional[hci.Address]
peer_le_features: Optional[hci.LeFeatureMask] peer_le_features: Optional[hci.LeFeatureMask]
role: hci.Role role: hci.Role
parameters: Parameters
encryption: int encryption: int
encryption_key_size: int encryption_key_size: int
authenticated: bool authenticated: bool
@@ -1710,8 +1594,6 @@ class Connection(utils.CompositeEventEmitter):
pairing_peer_authentication_requirements: Optional[int] pairing_peer_authentication_requirements: Optional[int]
cs_configs: dict[int, ChannelSoundingConfig] # Config ID to Configuration cs_configs: dict[int, ChannelSoundingConfig] # Config ID to Configuration
cs_procedures: dict[int, ChannelSoundingProcedure] # Config ID to Procedures cs_procedures: dict[int, ChannelSoundingProcedure] # Config ID to Procedures
classic_mode: int = hci.HCI_Mode_Change_Event.Mode.ACTIVE
classic_interval: int = 0
EVENT_CONNECTION_ATT_MTU_UPDATE = "connection_att_mtu_update" EVENT_CONNECTION_ATT_MTU_UPDATE = "connection_att_mtu_update"
EVENT_DISCONNECTION = "disconnection" EVENT_DISCONNECTION = "disconnection"
@@ -1738,8 +1620,6 @@ class Connection(utils.CompositeEventEmitter):
EVENT_CHANNEL_SOUNDING_CONFIG_REMOVED = "channel_sounding_config_removed" EVENT_CHANNEL_SOUNDING_CONFIG_REMOVED = "channel_sounding_config_removed"
EVENT_CHANNEL_SOUNDING_PROCEDURE_FAILURE = "channel_sounding_procedure_failure" EVENT_CHANNEL_SOUNDING_PROCEDURE_FAILURE = "channel_sounding_procedure_failure"
EVENT_CHANNEL_SOUNDING_PROCEDURE = "channel_sounding_procedure" EVENT_CHANNEL_SOUNDING_PROCEDURE = "channel_sounding_procedure"
EVENT_MODE_CHANGE = "mode_change"
EVENT_MODE_CHANGE_FAILURE = "mode_change_failure"
EVENT_ROLE_CHANGE = "role_change" EVENT_ROLE_CHANGE = "role_change"
EVENT_ROLE_CHANGE_FAILURE = "role_change_failure" EVENT_ROLE_CHANGE_FAILURE = "role_change_failure"
EVENT_CLASSIC_PAIRING = "classic_pairing" EVENT_CLASSIC_PAIRING = "classic_pairing"
@@ -1748,10 +1628,6 @@ class Connection(utils.CompositeEventEmitter):
EVENT_PAIRING = "pairing" EVENT_PAIRING = "pairing"
EVENT_PAIRING_FAILURE = "pairing_failure" EVENT_PAIRING_FAILURE = "pairing_failure"
EVENT_SECURITY_REQUEST = "security_request" EVENT_SECURITY_REQUEST = "security_request"
EVENT_LINK_KEY = "link_key"
EVENT_CIS_REQUEST = "cis_request"
EVENT_CIS_ESTABLISHMENT = "cis_establishment"
EVENT_CIS_ESTABLISHMENT_FAILURE = "cis_establishment_failure"
@utils.composite_listener @utils.composite_listener
class Listener: class Listener:
@@ -2007,12 +1883,6 @@ class Connection(utils.CompositeEventEmitter):
def data_packet_queue(self) -> DataPacketQueue | None: def data_packet_queue(self) -> DataPacketQueue | None:
return self.device.host.get_data_packet_queue(self.handle) return self.device.host.get_data_packet_queue(self.handle)
def cancel_on_disconnection(self, awaitable: Awaitable[_T]) -> Awaitable[_T]:
"""
Helper method to call `utils.cancel_on_event` for the 'disconnection' event
"""
return utils.cancel_on_event(self, self.EVENT_DISCONNECTION, awaitable)
async def __aenter__(self): async def __aenter__(self):
return self return self
@@ -2083,9 +1953,9 @@ class DeviceConfiguration:
gatt_service_enabled: bool = True gatt_service_enabled: bool = True
def __post_init__(self) -> None: def __post_init__(self) -> None:
self.gatt_services: list[dict[str, Any]] = [] self.gatt_services: list[Dict[str, Any]] = []
def load_from_dict(self, config: dict[str, Any]) -> None: def load_from_dict(self, config: Dict[str, Any]) -> None:
config = copy.deepcopy(config) config = copy.deepcopy(config)
# Load simple properties # Load simple properties
@@ -2145,13 +2015,13 @@ class DeviceConfiguration:
self.load_from_dict(json.load(file)) self.load_from_dict(json.load(file))
@classmethod @classmethod
def from_file(cls: type[Self], filename: str) -> Self: def from_file(cls: Type[Self], filename: str) -> Self:
config = cls() config = cls()
config.load_from_file(filename) config.load_from_file(filename)
return config return config
@classmethod @classmethod
def from_dict(cls: type[Self], config: dict[str, Any]) -> Self: def from_dict(cls: Type[Self], config: Dict[str, Any]) -> Self:
device_config = cls() device_config = cls()
device_config.load_from_dict(config) device_config.load_from_dict(config)
return device_config return device_config
@@ -2248,22 +2118,22 @@ class Device(utils.CompositeEventEmitter):
advertising_data: bytes advertising_data: bytes
scan_response_data: bytes scan_response_data: bytes
cs_capabilities: ChannelSoundingCapabilities | None = None cs_capabilities: ChannelSoundingCapabilities | None = None
connections: dict[int, Connection] connections: Dict[int, Connection]
pending_connections: dict[hci.Address, Connection] pending_connections: Dict[hci.Address, Connection]
classic_pending_accepts: dict[ classic_pending_accepts: Dict[
hci.Address, hci.Address,
list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]], list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]],
] ]
advertisement_accumulators: dict[hci.Address, AdvertisementDataAccumulator] advertisement_accumulators: Dict[hci.Address, AdvertisementDataAccumulator]
periodic_advertising_syncs: list[PeriodicAdvertisingSync] periodic_advertising_syncs: list[PeriodicAdvertisingSync]
config: DeviceConfiguration config: DeviceConfiguration
legacy_advertiser: Optional[LegacyAdvertiser] legacy_advertiser: Optional[LegacyAdvertiser]
sco_links: dict[int, ScoLink] sco_links: Dict[int, ScoLink]
cis_links: dict[int, CisLink] cis_links: Dict[int, CisLink]
bigs: dict[int, Big] bigs: dict[int, Big]
bis_links: dict[int, BisLink] bis_links: dict[int, BisLink]
big_syncs: dict[int, BigSync] big_syncs: dict[int, BigSync]
_pending_cis: dict[int, tuple[int, int]] _pending_cis: Dict[int, tuple[int, int]]
gatt_service: gatt_service.GenericAttributeProfileService | None = None gatt_service: gatt_service.GenericAttributeProfileService | None = None
EVENT_ADVERTISEMENT = "advertisement" EVENT_ADVERTISEMENT = "advertisement"
@@ -2423,8 +2293,8 @@ class Device(utils.CompositeEventEmitter):
self.address_generation_offload = config.address_generation_offload self.address_generation_offload = config.address_generation_offload
# Extended advertising. # Extended advertising.
self.extended_advertising_sets: dict[int, AdvertisingSet] = {} self.extended_advertising_sets: Dict[int, AdvertisingSet] = {}
self.connecting_extended_advertising_sets: dict[int, AdvertisingSet] = {} self.connecting_extended_advertising_sets: Dict[int, AdvertisingSet] = {}
# Legacy advertising. # Legacy advertising.
# The advertising and scan response data, as well as the advertising interval # The advertising and scan response data, as well as the advertising interval
@@ -4400,11 +4270,11 @@ class Device(utils.CompositeEventEmitter):
self.smp_manager.pairing_config_factory = pairing_config_factory self.smp_manager.pairing_config_factory = pairing_config_factory
@property @property
def smp_session_proxy(self) -> type[smp.Session]: def smp_session_proxy(self) -> Type[smp.Session]:
return self.smp_manager.session_proxy return self.smp_manager.session_proxy
@smp_session_proxy.setter @smp_session_proxy.setter
def smp_session_proxy(self, session_proxy: type[smp.Session]) -> None: def smp_session_proxy(self, session_proxy: Type[smp.Session]) -> None:
self.smp_manager.session_proxy = session_proxy self.smp_manager.session_proxy = session_proxy
async def pair(self, connection): async def pair(self, connection):
@@ -4488,7 +4358,9 @@ class Device(utils.CompositeEventEmitter):
raise hci.HCI_StatusError(result) raise hci.HCI_StatusError(result)
# Wait for the authentication to complete # Wait for the authentication to complete
await connection.cancel_on_disconnection(pending_authentication) await utils.cancel_on_event(
connection, Connection.EVENT_DISCONNECTION, pending_authentication
)
finally: finally:
connection.remove_listener( connection.remove_listener(
connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication
@@ -4575,7 +4447,9 @@ class Device(utils.CompositeEventEmitter):
raise hci.HCI_StatusError(result) raise hci.HCI_StatusError(result)
# Wait for the result # Wait for the result
await connection.cancel_on_disconnection(pending_encryption) await utils.cancel_on_event(
connection, Connection.EVENT_DISCONNECTION, pending_encryption
)
finally: finally:
connection.remove_listener( connection.remove_listener(
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
@@ -4619,7 +4493,9 @@ class Device(utils.CompositeEventEmitter):
f'{hci.HCI_Constant.error_name(result.status)}' f'{hci.HCI_Constant.error_name(result.status)}'
) )
raise hci.HCI_StatusError(result) raise hci.HCI_StatusError(result)
await connection.cancel_on_disconnection(pending_role_change) await utils.cancel_on_event(
connection, Connection.EVENT_DISCONNECTION, pending_role_change
)
finally: finally:
connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change) connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change)
connection.remove_listener( connection.remove_listener(
@@ -4679,39 +4555,48 @@ class Device(utils.CompositeEventEmitter):
@utils.experimental('Only for testing.') @utils.experimental('Only for testing.')
async def setup_cig( async def setup_cig(
self, self,
parameters: CigParameters, cig_id: int,
cis_id: Sequence[int],
sdu_interval: tuple[int, int],
framing: int,
max_sdu: tuple[int, int],
retransmission_number: int,
max_transport_latency: tuple[int, int],
) -> list[int]: ) -> list[int]:
"""Sends hci.HCI_LE_Set_CIG_Parameters_Command. """Sends hci.HCI_LE_Set_CIG_Parameters_Command.
Args: Args:
parameters: CIG parameters. cig_id: CIG_ID.
cis_id: CID ID list.
sdu_interval: SDU intervals of (Central->Peripheral, Peripheral->Cental).
framing: Un-framing(0) or Framing(1).
max_sdu: Max SDU counts of (Central->Peripheral, Peripheral->Cental).
retransmission_number: retransmission_number.
max_transport_latency: Max transport latencies of
(Central->Peripheral, Peripheral->Cental).
Returns: Returns:
List of created CIS handles corresponding to the same order of [cid_id]. List of created CIS handles corresponding to the same order of [cid_id].
""" """
num_cis = len(parameters.cis_parameters) num_cis = len(cis_id)
response = await self.send_command( response = await self.send_command(
hci.HCI_LE_Set_CIG_Parameters_Command( hci.HCI_LE_Set_CIG_Parameters_Command(
cig_id=parameters.cig_id, cig_id=cig_id,
sdu_interval_c_to_p=parameters.sdu_interval_c_to_p, sdu_interval_c_to_p=sdu_interval[0],
sdu_interval_p_to_c=parameters.sdu_interval_p_to_c, sdu_interval_p_to_c=sdu_interval[1],
worst_case_sca=parameters.worst_case_sca, worst_case_sca=0x00, # 251-500 ppm
packing=int(parameters.packing), packing=0x00, # Sequential
framing=int(parameters.framing), framing=framing,
max_transport_latency_c_to_p=parameters.max_transport_latency_c_to_p, max_transport_latency_c_to_p=max_transport_latency[0],
max_transport_latency_p_to_c=parameters.max_transport_latency_p_to_c, max_transport_latency_p_to_c=max_transport_latency[1],
cis_id=[cis.cis_id for cis in parameters.cis_parameters], cis_id=cis_id,
max_sdu_c_to_p=[ max_sdu_c_to_p=[max_sdu[0]] * num_cis,
cis.max_sdu_c_to_p for cis in parameters.cis_parameters max_sdu_p_to_c=[max_sdu[1]] * num_cis,
], phy_c_to_p=[hci.HCI_LE_2M_PHY] * num_cis,
max_sdu_p_to_c=[ phy_p_to_c=[hci.HCI_LE_2M_PHY] * num_cis,
cis.max_sdu_p_to_c for cis in parameters.cis_parameters rtn_c_to_p=[retransmission_number] * num_cis,
], rtn_p_to_c=[retransmission_number] * num_cis,
phy_c_to_p=[cis.phy_c_to_p for cis in parameters.cis_parameters],
phy_p_to_c=[cis.phy_p_to_c for cis in parameters.cis_parameters],
rtn_c_to_p=[cis.rtn_c_to_p for cis in parameters.cis_parameters],
rtn_p_to_c=[cis.rtn_p_to_c for cis in parameters.cis_parameters],
), ),
check_result=True, check_result=True,
) )
@@ -4719,17 +4604,19 @@ class Device(utils.CompositeEventEmitter):
# Ideally, we should manage CIG lifecycle, but they are not useful for Unicast # Ideally, we should manage CIG lifecycle, but they are not useful for Unicast
# Server, so here it only provides a basic functionality for testing. # Server, so here it only provides a basic functionality for testing.
cis_handles = response.return_parameters.connection_handle[:] cis_handles = response.return_parameters.connection_handle[:]
for cis, cis_handle in zip(parameters.cis_parameters, cis_handles): for id, cis_handle in zip(cis_id, cis_handles):
self._pending_cis[cis_handle] = (cis.cis_id, parameters.cig_id) self._pending_cis[cis_handle] = (id, cig_id)
return cis_handles return cis_handles
# [LE only] # [LE only]
@utils.experimental('Only for testing.') @utils.experimental('Only for testing.')
async def create_cis( async def create_cis(
self, cis_acl_pairs: Sequence[tuple[int, Connection]] self, cis_acl_pairs: Sequence[tuple[int, int]]
) -> list[CisLink]: ) -> list[CisLink]:
for cis_handle, acl_connection in cis_acl_pairs: for cis_handle, acl_handle in cis_acl_pairs:
acl_connection = self.lookup_connection(acl_handle)
assert acl_connection
cis_id, cig_id = self._pending_cis.pop(cis_handle) cis_id, cig_id = self._pending_cis.pop(cis_handle)
self.cis_links[cis_handle] = CisLink( self.cis_links[cis_handle] = CisLink(
device=self, device=self,
@@ -4749,8 +4636,8 @@ class Device(utils.CompositeEventEmitter):
if pending_future := pending_cis_establishments.get(cis_link.handle): if pending_future := pending_cis_establishments.get(cis_link.handle):
pending_future.set_result(cis_link) pending_future.set_result(cis_link)
def on_cis_establishment_failure(cis_link: CisLink, status: int) -> None: def on_cis_establishment_failure(cis_handle: int, status: int) -> None:
if pending_future := pending_cis_establishments.get(cis_link.handle): if pending_future := pending_cis_establishments.get(cis_handle):
pending_future.set_exception(hci.HCI_Error(status)) pending_future.set_exception(hci.HCI_Error(status))
watcher.on(self, self.EVENT_CIS_ESTABLISHMENT, on_cis_establishment) watcher.on(self, self.EVENT_CIS_ESTABLISHMENT, on_cis_establishment)
@@ -4760,7 +4647,7 @@ class Device(utils.CompositeEventEmitter):
await self.send_command( await self.send_command(
hci.HCI_LE_Create_CIS_Command( hci.HCI_LE_Create_CIS_Command(
cis_connection_handle=[p[0] for p in cis_acl_pairs], cis_connection_handle=[p[0] for p in cis_acl_pairs],
acl_connection_handle=[p[1].handle for p in cis_acl_pairs], acl_connection_handle=[p[1] for p in cis_acl_pairs],
), ),
check_result=True, check_result=True,
) )
@@ -4769,21 +4656,26 @@ class Device(utils.CompositeEventEmitter):
# [LE only] # [LE only]
@utils.experimental('Only for testing.') @utils.experimental('Only for testing.')
async def accept_cis_request(self, cis_link: CisLink) -> None: async def accept_cis_request(self, handle: int) -> CisLink:
"""[LE Only] Accepts an incoming CIS request. """[LE Only] Accepts an incoming CIS request.
This method returns when the CIS is established, or raises an exception if When the specified CIS handle is already created, this method returns the
the CIS establishment fails. existed CIS link object immediately.
Args: Args:
handle: CIS handle to accept. handle: CIS handle to accept.
Returns:
CIS link object on the given handle.
""" """
if not (cis_link := self.cis_links.get(handle)):
raise InvalidStateError(f'No pending CIS request of handle {handle}')
# There might be multiple ASE sharing a CIS channel. # There might be multiple ASE sharing a CIS channel.
# If one of them has accepted the request, the others should just leverage it. # If one of them has accepted the request, the others should just leverage it.
async with self._cis_lock: async with self._cis_lock:
if cis_link.state == CisLink.State.ESTABLISHED: if cis_link.state == CisLink.State.ESTABLISHED:
return return cis_link
with closing(utils.EventWatcher()) as watcher: with closing(utils.EventWatcher()) as watcher:
pending_establishment = asyncio.get_running_loop().create_future() pending_establishment = asyncio.get_running_loop().create_future()
@@ -4802,24 +4694,26 @@ class Device(utils.CompositeEventEmitter):
) )
await self.send_command( await self.send_command(
hci.HCI_LE_Accept_CIS_Request_Command( hci.HCI_LE_Accept_CIS_Request_Command(connection_handle=handle),
connection_handle=cis_link.handle
),
check_result=True, check_result=True,
) )
await pending_establishment await pending_establishment
return cis_link
# Mypy believes this is reachable when context is an ExitStack.
raise UnreachableError()
# [LE only] # [LE only]
@utils.experimental('Only for testing.') @utils.experimental('Only for testing.')
async def reject_cis_request( async def reject_cis_request(
self, self,
cis_link: CisLink, handle: int,
reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR, reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
) -> None: ) -> None:
await self.send_command( await self.send_command(
hci.HCI_LE_Reject_CIS_Request_Command( hci.HCI_LE_Reject_CIS_Request_Command(
connection_handle=cis_link.handle, reason=reason connection_handle=handle, reason=reason
), ),
check_result=True, check_result=True,
) )
@@ -5172,12 +5066,12 @@ class Device(utils.CompositeEventEmitter):
# [Classic only] # [Classic only]
@host_event_handler @host_event_handler
def on_link_key(self, bd_addr: hci.Address, link_key: bytes, key_type: int) -> None: def on_link_key(self, bd_addr, link_key, key_type):
# Store the keys in the key store # Store the keys in the key store
if self.keystore: if self.keystore:
authenticated = key_type in ( authenticated = key_type in (
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192, hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256, hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
) )
pairing_keys = PairingKeys( pairing_keys = PairingKeys(
link_key=PairingKeys.Key(value=link_key, authenticated=authenticated), link_key=PairingKeys.Key(value=link_key, authenticated=authenticated),
@@ -5357,7 +5251,7 @@ class Device(utils.CompositeEventEmitter):
big.bis_links = [BisLink(handle=handle, big=big) for handle in bis_handles] big.bis_links = [BisLink(handle=handle, big=big) for handle in bis_handles]
big.big_sync_delay = big_sync_delay big.big_sync_delay = big_sync_delay
big.transport_latency_big = transport_latency_big big.transport_latency_big = transport_latency_big
big.phy = hci.Phy(phy) big.phy = phy
big.nse = nse big.nse = nse
big.bn = bn big.bn = bn
big.pto = pto big.pto = pto
@@ -5624,8 +5518,8 @@ class Device(utils.CompositeEventEmitter):
# Handle SCO request. # Handle SCO request.
if link_type in ( if link_type in (
hci.HCI_Connection_Complete_Event.LinkType.SCO, hci.HCI_Connection_Complete_Event.SCO_LINK_TYPE,
hci.HCI_Connection_Complete_Event.LinkType.ESCO, hci.HCI_Connection_Complete_Event.ESCO_LINK_TYPE,
): ):
if connection := self.find_connection_by_bd_addr( if connection := self.find_connection_by_bd_addr(
bd_addr, transport=PhysicalTransport.BR_EDR bd_addr, transport=PhysicalTransport.BR_EDR
@@ -5733,7 +5627,7 @@ class Device(utils.CompositeEventEmitter):
# [Classic only] # [Classic only]
@host_event_handler @host_event_handler
@with_connection_from_address @with_connection_from_address
def on_authentication_io_capability_request(self, connection: Connection): def on_authentication_io_capability_request(self, connection):
# Ask what the pairing config should be for this connection # Ask what the pairing config should be for this connection
pairing_config = self.pairing_config_factory(connection) pairing_config = self.pairing_config_factory(connection)
@@ -5741,13 +5635,13 @@ class Device(utils.CompositeEventEmitter):
authentication_requirements = ( authentication_requirements = (
# No Bonding # No Bonding
( (
hci.AuthenticationRequirements.MITM_NOT_REQUIRED_NO_BONDING, hci.HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
hci.AuthenticationRequirements.MITM_REQUIRED_NO_BONDING, hci.HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
), ),
# General Bonding # General Bonding
( (
hci.AuthenticationRequirements.MITM_NOT_REQUIRED_GENERAL_BONDING, hci.HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
hci.AuthenticationRequirements.MITM_REQUIRED_GENERAL_BONDING, hci.HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
), ),
)[1 if pairing_config.bonding else 0][1 if pairing_config.mitm else 0] )[1 if pairing_config.bonding else 0][1 if pairing_config.mitm else 0]
@@ -5802,30 +5696,30 @@ class Device(utils.CompositeEventEmitter):
raise UnreachableError() raise UnreachableError()
# See Bluetooth spec @ Vol 3, Part C 5.2.2.6 # See Bluetooth spec @ Vol 3, Part C 5.2.2.6
methods: dict[int, dict[int, Callable[[], Awaitable[bool]]]] = { methods = {
hci.IoCapability.DISPLAY_ONLY: { hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: {
hci.IoCapability.DISPLAY_ONLY: display_auto_confirm, hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: display_auto_confirm,
hci.IoCapability.DISPLAY_YES_NO: display_confirm, hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: display_confirm,
hci.IoCapability.KEYBOARD_ONLY: na, hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm, hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
}, },
hci.IoCapability.DISPLAY_YES_NO: { hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: {
hci.IoCapability.DISPLAY_ONLY: display_auto_confirm, hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: display_auto_confirm,
hci.IoCapability.DISPLAY_YES_NO: display_confirm, hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: display_confirm,
hci.IoCapability.KEYBOARD_ONLY: na, hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm, hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
}, },
hci.IoCapability.KEYBOARD_ONLY: { hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: {
hci.IoCapability.DISPLAY_ONLY: na, hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: na,
hci.IoCapability.DISPLAY_YES_NO: na, hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: na,
hci.IoCapability.KEYBOARD_ONLY: na, hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm, hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
}, },
hci.IoCapability.NO_INPUT_NO_OUTPUT: { hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: {
hci.IoCapability.DISPLAY_ONLY: confirm, hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: confirm,
hci.IoCapability.DISPLAY_YES_NO: confirm, hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: confirm,
hci.IoCapability.KEYBOARD_ONLY: auto_confirm, hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: auto_confirm,
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm, hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
}, },
} }
@@ -5833,7 +5727,9 @@ class Device(utils.CompositeEventEmitter):
async def reply() -> None: async def reply() -> None:
try: try:
if await connection.cancel_on_disconnection(method()): if await utils.cancel_on_event(
connection, Connection.EVENT_DISCONNECTION, method()
):
await self.host.send_command( await self.host.send_command(
hci.HCI_User_Confirmation_Request_Reply_Command( hci.HCI_User_Confirmation_Request_Reply_Command(
bd_addr=connection.peer_address bd_addr=connection.peer_address
@@ -5860,8 +5756,10 @@ class Device(utils.CompositeEventEmitter):
async def reply() -> None: async def reply() -> None:
try: try:
number = await connection.cancel_on_disconnection( number = await utils.cancel_on_event(
pairing_config.delegate.get_number() connection,
Connection.EVENT_DISCONNECTION,
pairing_config.delegate.get_number(),
) )
if number is not None: if number is not None:
await self.host.send_command( await self.host.send_command(
@@ -5881,19 +5779,6 @@ class Device(utils.CompositeEventEmitter):
utils.AsyncRunner.spawn(reply()) utils.AsyncRunner.spawn(reply())
# [Classic only]
@host_event_handler
@with_connection_from_handle
def on_mode_change(
self, connection: Connection, status: int, current_mode: int, interval: int
):
if status == hci.HCI_SUCCESS:
connection.classic_mode = current_mode
connection.classic_interval = interval
connection.emit(connection.EVENT_MODE_CHANGE)
else:
connection.emit(connection.EVENT_MODE_CHANGE_FAILURE, status)
# [Classic only] # [Classic only]
@host_event_handler @host_event_handler
@with_connection_from_address @with_connection_from_address
@@ -5904,11 +5789,13 @@ class Device(utils.CompositeEventEmitter):
io_capability = pairing_config.delegate.classic_io_capability io_capability = pairing_config.delegate.classic_io_capability
# Respond # Respond
if io_capability == hci.IoCapability.KEYBOARD_ONLY: if io_capability == hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY:
# Ask the user to enter a string # Ask the user to enter a string
async def get_pin_code(): async def get_pin_code():
pin_code = await connection.cancel_on_disconnection( pin_code = await utils.cancel_on_event(
pairing_config.delegate.get_string(16) connection,
Connection.EVENT_DISCONNECTION,
pairing_config.delegate.get_string(16),
) )
if pin_code is not None: if pin_code is not None:
@@ -5946,8 +5833,10 @@ class Device(utils.CompositeEventEmitter):
pairing_config = self.pairing_config_factory(connection) pairing_config = self.pairing_config_factory(connection)
# Show the passkey to the user # Show the passkey to the user
connection.cancel_on_disconnection( utils.cancel_on_event(
pairing_config.delegate.display_number(passkey, digits=6) connection,
Connection.EVENT_DISCONNECTION,
pairing_config.delegate.display_number(passkey, digits=6),
) )
# [Classic only] # [Classic only]
@@ -6034,63 +5923,24 @@ class Device(utils.CompositeEventEmitter):
f'cis_id=[0x{cis_id:02X}] ***' f'cis_id=[0x{cis_id:02X}] ***'
) )
# LE_CIS_Established event doesn't provide info, so we must store them here. # LE_CIS_Established event doesn't provide info, so we must store them here.
cis_link = CisLink( self.cis_links[cis_handle] = CisLink(
device=self, device=self,
acl_connection=acl_connection, acl_connection=acl_connection,
handle=cis_handle, handle=cis_handle,
cig_id=cig_id, cig_id=cig_id,
cis_id=cis_id, cis_id=cis_id,
) )
self.cis_links[cis_handle] = cis_link self.emit(self.EVENT_CIS_REQUEST, acl_connection, cis_handle, cig_id, cis_id)
acl_connection.emit(acl_connection.EVENT_CIS_REQUEST, cis_link)
self.emit(self.EVENT_CIS_REQUEST, cis_link)
# [LE only] # [LE only]
@host_event_handler @host_event_handler
@utils.experimental('Only for testing') @utils.experimental('Only for testing')
def on_cis_establishment( def on_cis_establishment(self, cis_handle: int) -> None:
self,
cis_handle: int,
cig_sync_delay: int,
cis_sync_delay: int,
transport_latency_c_to_p: int,
transport_latency_p_to_c: int,
phy_c_to_p: int,
phy_p_to_c: int,
nse: int,
bn_c_to_p: int,
bn_p_to_c: int,
ft_c_to_p: int,
ft_p_to_c: int,
max_pdu_c_to_p: int,
max_pdu_p_to_c: int,
iso_interval: int,
) -> None:
if cis_handle not in self.cis_links:
logger.warning("CIS link not found")
return
cis_link = self.cis_links[cis_handle] cis_link = self.cis_links[cis_handle]
cis_link.state = CisLink.State.ESTABLISHED cis_link.state = CisLink.State.ESTABLISHED
assert cis_link.acl_connection assert cis_link.acl_connection
# Update the CIS
cis_link.cig_sync_delay = cig_sync_delay
cis_link.cis_sync_delay = cis_sync_delay
cis_link.transport_latency_c_to_p = transport_latency_c_to_p
cis_link.transport_latency_p_to_c = transport_latency_p_to_c
cis_link.phy_c_to_p = hci.Phy(phy_c_to_p)
cis_link.phy_p_to_c = hci.Phy(phy_p_to_c)
cis_link.nse = nse
cis_link.bn_c_to_p = bn_c_to_p
cis_link.bn_p_to_c = bn_p_to_c
cis_link.ft_c_to_p = ft_c_to_p
cis_link.ft_p_to_c = ft_p_to_c
cis_link.max_pdu_c_to_p = max_pdu_c_to_p
cis_link.max_pdu_p_to_c = max_pdu_p_to_c
cis_link.iso_interval = iso_interval * 1.25
logger.debug( logger.debug(
f'*** CIS Establishment ' f'*** CIS Establishment '
f'{cis_link.acl_connection.peer_address}, ' f'{cis_link.acl_connection.peer_address}, '
@@ -6100,27 +5950,16 @@ class Device(utils.CompositeEventEmitter):
) )
cis_link.emit(cis_link.EVENT_ESTABLISHMENT) cis_link.emit(cis_link.EVENT_ESTABLISHMENT)
cis_link.acl_connection.emit(
cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT, cis_link
)
self.emit(self.EVENT_CIS_ESTABLISHMENT, cis_link) self.emit(self.EVENT_CIS_ESTABLISHMENT, cis_link)
# [LE only] # [LE only]
@host_event_handler @host_event_handler
@utils.experimental('Only for testing') @utils.experimental('Only for testing')
def on_cis_establishment_failure(self, cis_handle: int, status: int) -> None: def on_cis_establishment_failure(self, cis_handle: int, status: int) -> None:
if (cis_link := self.cis_links.pop(cis_handle, None)) is None:
logger.warning("CIS link not found")
return
logger.debug(f'*** CIS Establishment Failure: cis=[0x{cis_handle:04X}] ***') logger.debug(f'*** CIS Establishment Failure: cis=[0x{cis_handle:04X}] ***')
cis_link.emit(cis_link.EVENT_ESTABLISHMENT_FAILURE, status) if cis_link := self.cis_links.pop(cis_handle):
cis_link.acl_connection.emit( cis_link.emit(cis_link.EVENT_ESTABLISHMENT_FAILURE, status)
cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT_FAILURE, self.emit(self.EVENT_CIS_ESTABLISHMENT_FAILURE, cis_handle, status)
cis_link,
status,
)
self.emit(self.EVENT_CIS_ESTABLISHMENT_FAILURE, cis_link, status)
# [LE only] # [LE only]
@host_event_handler @host_event_handler
@@ -6134,7 +5973,7 @@ class Device(utils.CompositeEventEmitter):
@host_event_handler @host_event_handler
@with_connection_from_handle @with_connection_from_handle
def on_connection_encryption_change( def on_connection_encryption_change(
self, connection: Connection, encryption: int, encryption_key_size: int self, connection, encryption, encryption_key_size
): ):
logger.debug( logger.debug(
f'*** Connection Encryption Change: [0x{connection.handle:04X}] ' f'*** Connection Encryption Change: [0x{connection.handle:04X}] '
@@ -6147,14 +5986,14 @@ class Device(utils.CompositeEventEmitter):
if ( if (
not connection.authenticated not connection.authenticated
and connection.transport == PhysicalTransport.BR_EDR and connection.transport == PhysicalTransport.BR_EDR
and encryption == hci.HCI_Encryption_Change_Event.Enabled.AES_CCM and encryption == hci.HCI_Encryption_Change_Event.AES_CCM
): ):
connection.authenticated = True connection.authenticated = True
connection.sc = True connection.sc = True
if ( if (
not connection.authenticated not connection.authenticated
and connection.transport == PhysicalTransport.LE and connection.transport == PhysicalTransport.LE
and encryption == hci.HCI_Encryption_Change_Event.Enabled.E0_OR_AES_CCM and encryption == hci.HCI_Encryption_Change_Event.E0_OR_AES_CCM
): ):
connection.authenticated = True connection.authenticated = True
connection.sc = True connection.sc = True
@@ -6181,19 +6020,13 @@ class Device(utils.CompositeEventEmitter):
@host_event_handler @host_event_handler
@with_connection_from_handle @with_connection_from_handle
def on_connection_parameters_update( def on_connection_parameters_update(self, connection, connection_parameters):
self, connection: Connection, connection_parameters: core.ConnectionParameters
):
logger.debug( logger.debug(
f'*** Connection Parameters Update: [0x{connection.handle:04X}] ' f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
f'{connection.peer_address} as {connection.role_name}, ' f'{connection.peer_address} as {connection.role_name}, '
f'{connection_parameters}' f'{connection_parameters}'
) )
connection.parameters = Connection.Parameters( connection.parameters = connection_parameters
connection_parameters.connection_interval * 1.25,
connection_parameters.peripheral_latency,
connection_parameters.supervision_timeout * 10.0,
)
connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE) connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
@host_event_handler @host_event_handler
+2 -2
View File
@@ -23,7 +23,7 @@ from __future__ import annotations
import logging import logging
import pathlib import pathlib
import platform import platform
from typing import Iterable, Optional, TYPE_CHECKING from typing import Dict, Iterable, Optional, Type, TYPE_CHECKING
from bumble.drivers import rtk, intel from bumble.drivers import rtk, intel
from bumble.drivers.common import Driver from bumble.drivers.common import Driver
@@ -45,7 +45,7 @@ async def get_driver_for_host(host: Host) -> Optional[Driver]:
found. found.
If a "driver" HCI metadata entry is present, only that driver class will be probed. If a "driver" HCI metadata entry is present, only that driver class will be probed.
""" """
driver_classes: dict[str, type[Driver]] = {"rtk": rtk.Driver, "intel": intel.Driver} driver_classes: Dict[str, Type[Driver]] = {"rtk": rtk.Driver, "intel": intel.Driver}
probe_list: Iterable[str] probe_list: Iterable[str]
if driver_name := host.hci_metadata.get("driver"): if driver_name := host.hci_metadata.get("driver"):
# Only probe a single driver # Only probe a single driver
+2
View File
@@ -20,6 +20,8 @@ Common types for drivers.
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import abc import abc
from bumble import core
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Classes # Classes
+38 -38
View File
@@ -28,7 +28,7 @@ import os
import pathlib import pathlib
import platform import platform
import struct import struct
from typing import Any, Optional, TYPE_CHECKING from typing import Any, Deque, Optional, TYPE_CHECKING
from bumble import core from bumble import core
from bumble.drivers import common from bumble.drivers import common
@@ -50,7 +50,6 @@ logger = logging.getLogger(__name__)
INTEL_USB_PRODUCTS = { INTEL_USB_PRODUCTS = {
(0x8087, 0x0032), # AX210 (0x8087, 0x0032), # AX210
(0x8087, 0x0033), # AX211
(0x8087, 0x0036), # BE200 (0x8087, 0x0036), # BE200
} }
@@ -90,51 +89,54 @@ HCI_INTEL_WRITE_BOOT_PARAMS_COMMAND = hci.hci_vendor_command_op_code(0x000E)
hci.HCI_Command.register_commands(globals()) hci.HCI_Command.register_commands(globals())
@hci.HCI_Command.command @hci.HCI_Command.command(
@dataclasses.dataclass fields=[
class HCI_Intel_Read_Version_Command(hci.HCI_Command): ("param0", 1),
param0: int = dataclasses.field(metadata=hci.metadata(1)) ],
return_parameters_fields=[
return_parameters_fields = [
("status", hci.STATUS_SPEC), ("status", hci.STATUS_SPEC),
("tlv", "*"), ("tlv", "*"),
] ],
)
class HCI_Intel_Read_Version_Command(hci.HCI_Command):
pass
@hci.HCI_Command.command @hci.HCI_Command.command(
@dataclasses.dataclass fields=[("data_type", 1), ("data", "*")],
class Hci_Intel_Secure_Send_Command(hci.HCI_Command): return_parameters_fields=[
data_type: int = dataclasses.field(metadata=hci.metadata(1))
data: bytes = dataclasses.field(metadata=hci.metadata("*"))
return_parameters_fields = [
("status", 1), ("status", 1),
] ],
)
class Hci_Intel_Secure_Send_Command(hci.HCI_Command):
pass
@hci.HCI_Command.command @hci.HCI_Command.command(
@dataclasses.dataclass fields=[
class HCI_Intel_Reset_Command(hci.HCI_Command): ("reset_type", 1),
reset_type: int = dataclasses.field(metadata=hci.metadata(1)) ("patch_enable", 1),
patch_enable: int = dataclasses.field(metadata=hci.metadata(1)) ("ddc_reload", 1),
ddc_reload: int = dataclasses.field(metadata=hci.metadata(1)) ("boot_option", 1),
boot_option: int = dataclasses.field(metadata=hci.metadata(1)) ("boot_address", 4),
boot_address: int = dataclasses.field(metadata=hci.metadata(4)) ],
return_parameters_fields=[
return_parameters_fields = [
("data", "*"), ("data", "*"),
] ],
)
class HCI_Intel_Reset_Command(hci.HCI_Command):
pass
@hci.HCI_Command.command @hci.HCI_Command.command(
@dataclasses.dataclass fields=[("data", "*")],
class Hci_Intel_Write_Device_Config_Command(hci.HCI_Command): return_parameters_fields=[
data: bytes = dataclasses.field(metadata=hci.metadata("*"))
return_parameters_fields = [
("status", hci.STATUS_SPEC), ("status", hci.STATUS_SPEC),
("params", "*"), ("params", "*"),
] ],
)
class Hci_Intel_Write_Device_Config_Command(hci.HCI_Command):
pass
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -291,7 +293,6 @@ class HardwareVariant(utils.OpenIntEnum):
# This is a just a partial list. # This is a just a partial list.
# Add other constants here as new hardware is encountered and tested. # Add other constants here as new hardware is encountered and tested.
TYPHOON_PEAK = 0x17 TYPHOON_PEAK = 0x17
GARFIELD_PEAK = 0x19
GALE_PEAK = 0x1C GALE_PEAK = 0x1C
@@ -345,7 +346,7 @@ class Driver(common.Driver):
def __init__(self, host: Host) -> None: def __init__(self, host: Host) -> None:
self.host = host self.host = host
self.max_in_flight_firmware_load_commands = 1 self.max_in_flight_firmware_load_commands = 1
self.pending_firmware_load_commands: collections.deque[hci.HCI_Command] = ( self.pending_firmware_load_commands: Deque[hci.HCI_Command] = (
collections.deque() collections.deque()
) )
self.can_send_firmware_load_command = asyncio.Event() self.can_send_firmware_load_command = asyncio.Event()
@@ -470,7 +471,6 @@ class Driver(common.Driver):
raise DriverError("hardware platform not supported") raise DriverError("hardware platform not supported")
if hardware_info.variant not in ( if hardware_info.variant not in (
HardwareVariant.TYPHOON_PEAK, HardwareVariant.TYPHOON_PEAK,
HardwareVariant.GARFIELD_PEAK,
HardwareVariant.GALE_PEAK, HardwareVariant.GALE_PEAK,
): ):
raise DriverError("hardware variant not supported") raise DriverError("hardware variant not supported")
+35 -28
View File
@@ -20,7 +20,7 @@ Based on various online bits of information, including the Linux kernel.
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from dataclasses import dataclass, field from dataclasses import dataclass
import asyncio import asyncio
import enum import enum
import logging import logging
@@ -29,11 +29,19 @@ import os
import pathlib import pathlib
import platform import platform
import struct import struct
from typing import Tuple
import weakref import weakref
from bumble import core from bumble import core
from bumble import hci from bumble.hci import (
hci_vendor_command_op_code,
STATUS_SPEC,
HCI_SUCCESS,
HCI_Command,
HCI_Reset_Command,
HCI_Read_Local_Version_Information_Command,
)
from bumble.drivers import common from bumble.drivers import common
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -175,29 +183,27 @@ RTK_USB_PRODUCTS = {
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# HCI Commands # HCI Commands
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
HCI_RTK_READ_ROM_VERSION_COMMAND = hci.hci_vendor_command_op_code(0x6D) HCI_RTK_READ_ROM_VERSION_COMMAND = hci_vendor_command_op_code(0x6D)
HCI_RTK_DOWNLOAD_COMMAND = hci.hci_vendor_command_op_code(0x20) HCI_RTK_DOWNLOAD_COMMAND = hci_vendor_command_op_code(0x20)
HCI_RTK_DROP_FIRMWARE_COMMAND = hci.hci_vendor_command_op_code(0x66) HCI_RTK_DROP_FIRMWARE_COMMAND = hci_vendor_command_op_code(0x66)
hci.HCI_Command.register_commands(globals()) HCI_Command.register_commands(globals())
@hci.HCI_Command.command @HCI_Command.command(return_parameters_fields=[("status", STATUS_SPEC), ("version", 1)])
@dataclass class HCI_RTK_Read_ROM_Version_Command(HCI_Command):
class HCI_RTK_Read_ROM_Version_Command(hci.HCI_Command): pass
return_parameters_fields = [("status", hci.STATUS_SPEC), ("version", 1)]
@hci.HCI_Command.command @HCI_Command.command(
@dataclass fields=[("index", 1), ("payload", RTK_FRAGMENT_LENGTH)],
class HCI_RTK_Download_Command(hci.HCI_Command): return_parameters_fields=[("status", STATUS_SPEC), ("index", 1)],
index: int = field(metadata=hci.metadata(1)) )
payload: bytes = field(metadata=hci.metadata(RTK_FRAGMENT_LENGTH)) class HCI_RTK_Download_Command(HCI_Command):
return_parameters_fields = [("status", hci.STATUS_SPEC), ("index", 1)] pass
@hci.HCI_Command.command @HCI_Command.command()
@dataclass class HCI_RTK_Drop_Firmware_Command(HCI_Command):
class HCI_RTK_Drop_Firmware_Command(hci.HCI_Command):
pass pass
@@ -288,7 +294,7 @@ class Driver(common.Driver):
@dataclass @dataclass
class DriverInfo: class DriverInfo:
rom: int rom: int
hci: tuple[int, int] hci: Tuple[int, int]
config_needed: bool config_needed: bool
has_rom_version: bool has_rom_version: bool
has_msft_ext: bool = False has_msft_ext: bool = False
@@ -493,17 +499,17 @@ class Driver(common.Driver):
async def driver_info_for_host(cls, host): async def driver_info_for_host(cls, host):
try: try:
await host.send_command( await host.send_command(
hci.HCI_Reset_Command(), HCI_Reset_Command(),
check_result=True, check_result=True,
response_timeout=cls.POST_RESET_DELAY, response_timeout=cls.POST_RESET_DELAY,
) )
host.ready = True # Needed to let the host know the controller is ready. host.ready = True # Needed to let the host know the controller is ready.
except asyncio.exceptions.TimeoutError: except asyncio.exceptions.TimeoutError:
logger.warning("timeout waiting for hci reset, retrying") logger.warning("timeout waiting for hci reset, retrying")
await host.send_command(hci.HCI_Reset_Command(), check_result=True) await host.send_command(HCI_Reset_Command(), check_result=True)
host.ready = True host.ready = True
command = hci.HCI_Read_Local_Version_Information_Command() command = HCI_Read_Local_Version_Information_Command()
response = await host.send_command(command, check_result=True) response = await host.send_command(command, check_result=True)
if response.command_opcode != command.op_code: if response.command_opcode != command.op_code:
logger.error("failed to probe local version information") logger.error("failed to probe local version information")
@@ -590,7 +596,7 @@ class Driver(common.Driver):
response = await self.host.send_command( response = await self.host.send_command(
HCI_RTK_Read_ROM_Version_Command(), check_result=True HCI_RTK_Read_ROM_Version_Command(), check_result=True
) )
if response.return_parameters.status != hci.HCI_SUCCESS: if response.return_parameters.status != HCI_SUCCESS:
logger.warning("can't get ROM version") logger.warning("can't get ROM version")
return return
rom_version = response.return_parameters.version rom_version = response.return_parameters.version
@@ -628,8 +634,9 @@ class Driver(common.Driver):
fragment = payload[fragment_offset : fragment_offset + RTK_FRAGMENT_LENGTH] fragment = payload[fragment_offset : fragment_offset + RTK_FRAGMENT_LENGTH]
logger.debug(f"downloading fragment {fragment_index}") logger.debug(f"downloading fragment {fragment_index}")
await self.host.send_command( await self.host.send_command(
HCI_RTK_Download_Command(index=download_index, payload=fragment), HCI_RTK_Download_Command(
check_result=True, index=download_index, payload=fragment, check_result=True
)
) )
logger.debug("download complete!") logger.debug("download complete!")
@@ -638,7 +645,7 @@ class Driver(common.Driver):
response = await self.host.send_command( response = await self.host.send_command(
HCI_RTK_Read_ROM_Version_Command(), check_result=True HCI_RTK_Read_ROM_Version_Command(), check_result=True
) )
if response.return_parameters.status != hci.HCI_SUCCESS: if response.return_parameters.status != HCI_SUCCESS:
logger.warning("can't get ROM version") logger.warning("can't get ROM version")
else: else:
rom_version = response.return_parameters.version rom_version = response.return_parameters.version
@@ -661,7 +668,7 @@ class Driver(common.Driver):
async def init_controller(self): async def init_controller(self):
await self.download_firmware() await self.download_firmware()
await self.host.send_command(hci.HCI_Reset_Command(), check_result=True) await self.host.send_command(HCI_Reset_Command(), check_result=True)
logger.info(f"loaded FW image {self.driver_info.fw_name}") logger.info(f"loaded FW image {self.driver_info.fw_name}")
+9 -5
View File
@@ -27,7 +27,7 @@ import enum
import functools import functools
import logging import logging
import struct import struct
from typing import Iterable, Optional, Sequence, TypeVar, Union from typing import Iterable, List, Optional, Sequence, TypeVar, Union
from bumble.colors import color from bumble.colors import color
from bumble.core import BaseBumbleError, UUID from bumble.core import BaseBumbleError, UUID
@@ -350,8 +350,8 @@ class Service(Attribute):
''' '''
uuid: UUID uuid: UUID
characteristics: list[Characteristic] characteristics: List[Characteristic]
included_services: list[Service] included_services: List[Service]
def __init__( def __init__(
self, self,
@@ -474,7 +474,7 @@ class Characteristic(Attribute[_T]):
# The check for `p.name is not None` here is needed because for InFlag # The check for `p.name is not None` here is needed because for InFlag
# enums, the .name property can be None, when the enum value is 0, # enums, the .name property can be None, when the enum value is 0,
# so the type hint for .name is Optional[str]. # so the type hint for .name is Optional[str].
enum_list: list[str] = [p.name for p in cls if p.name is not None] enum_list: List[str] = [p.name for p in cls if p.name is not None]
enum_list_str = ",".join(enum_list) enum_list_str = ",".join(enum_list)
raise TypeError( raise TypeError(
f"Characteristic.Properties::from_string() error:\nExpected a string containing any of the keys, separated by , or |: {enum_list_str}\nGot: {properties_str}" f"Characteristic.Properties::from_string() error:\nExpected a string containing any of the keys, separated by , or |: {enum_list_str}\nGot: {properties_str}"
@@ -579,7 +579,11 @@ class Descriptor(Attribute):
if isinstance(self.value, bytes): if isinstance(self.value, bytes):
value_str = self.value.hex() value_str = self.value.hex()
elif isinstance(self.value, CharacteristicValue): elif isinstance(self.value, CharacteristicValue):
value_str = '<dynamic>' value = self.value.read(None)
if isinstance(value, bytes):
value_str = value.hex()
else:
value_str = '<async>'
else: else:
value_str = '<...>' value_str = '<...>'
return ( return (
+5 -4
View File
@@ -28,6 +28,7 @@ from typing import (
Iterable, Iterable,
Literal, Literal,
Optional, Optional,
Type,
TypeVar, TypeVar,
) )
@@ -269,7 +270,7 @@ class SerializableCharacteristicAdapter(CharacteristicAdapter[_T2]):
`to_bytes` and `__bytes__` methods, respectively. `to_bytes` and `__bytes__` methods, respectively.
''' '''
def __init__(self, characteristic: Characteristic, cls: type[_T2]) -> None: def __init__(self, characteristic: Characteristic, cls: Type[_T2]) -> None:
super().__init__(characteristic) super().__init__(characteristic)
self.cls = cls self.cls = cls
@@ -288,7 +289,7 @@ class SerializableCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T2]):
''' '''
def __init__( def __init__(
self, characteristic_proxy: CharacteristicProxy, cls: type[_T2] self, characteristic_proxy: CharacteristicProxy, cls: Type[_T2]
) -> None: ) -> None:
super().__init__(characteristic_proxy) super().__init__(characteristic_proxy)
self.cls = cls self.cls = cls
@@ -310,7 +311,7 @@ class EnumCharacteristicAdapter(CharacteristicAdapter[_T3]):
def __init__( def __init__(
self, self,
characteristic: Characteristic, characteristic: Characteristic,
cls: type[_T3], cls: Type[_T3],
length: int, length: int,
byteorder: Literal['little', 'big'] = 'little', byteorder: Literal['little', 'big'] = 'little',
): ):
@@ -346,7 +347,7 @@ class EnumCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T3]):
def __init__( def __init__(
self, self,
characteristic_proxy: CharacteristicProxy, characteristic_proxy: CharacteristicProxy,
cls: type[_T3], cls: Type[_T3],
length: int, length: int,
byteorder: Literal['little', 'big'] = 'little', byteorder: Literal['little', 'big'] = 'little',
): ):
+31 -26
View File
@@ -31,10 +31,15 @@ from datetime import datetime
from typing import ( from typing import (
Any, Any,
Callable, Callable,
Dict,
Generic, Generic,
Iterable, Iterable,
List,
Optional, Optional,
Set,
Tuple,
Union, Union,
Type,
TypeVar, TypeVar,
TYPE_CHECKING, TYPE_CHECKING,
) )
@@ -144,8 +149,8 @@ class AttributeProxy(utils.EventEmitter, Generic[_T]):
class ServiceProxy(AttributeProxy): class ServiceProxy(AttributeProxy):
uuid: UUID uuid: UUID
characteristics: list[CharacteristicProxy[bytes]] characteristics: List[CharacteristicProxy[bytes]]
included_services: list[ServiceProxy] included_services: List[ServiceProxy]
@staticmethod @staticmethod
def from_client(service_class, client: Client, service_uuid: UUID): def from_client(service_class, client: Client, service_uuid: UUID):
@@ -194,8 +199,8 @@ class ServiceProxy(AttributeProxy):
class CharacteristicProxy(AttributeProxy[_T]): class CharacteristicProxy(AttributeProxy[_T]):
properties: Characteristic.Properties properties: Characteristic.Properties
descriptors: list[DescriptorProxy] descriptors: List[DescriptorProxy]
subscribers: dict[Any, Callable[[_T], Any]] subscribers: Dict[Any, Callable[[_T], Any]]
EVENT_UPDATE = "update" EVENT_UPDATE = "update"
@@ -272,7 +277,7 @@ class ProfileServiceProxy:
Base class for profile-specific service proxies Base class for profile-specific service proxies
''' '''
SERVICE_CLASS: type[TemplateService] SERVICE_CLASS: Type[TemplateService]
@classmethod @classmethod
def from_client(cls, client: Client) -> Optional[ProfileServiceProxy]: def from_client(cls, client: Client) -> Optional[ProfileServiceProxy]:
@@ -283,13 +288,13 @@ class ProfileServiceProxy:
# GATT Client # GATT Client
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Client: class Client:
services: list[ServiceProxy] services: List[ServiceProxy]
cached_values: dict[int, tuple[datetime, bytes]] cached_values: Dict[int, Tuple[datetime, bytes]]
notification_subscribers: dict[ notification_subscribers: Dict[
int, set[Union[CharacteristicProxy, Callable[[bytes], Any]]] int, Set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
] ]
indication_subscribers: dict[ indication_subscribers: Dict[
int, set[Union[CharacteristicProxy, Callable[[bytes], Any]]] int, Set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
] ]
pending_response: Optional[asyncio.futures.Future[ATT_PDU]] pending_response: Optional[asyncio.futures.Future[ATT_PDU]]
pending_request: Optional[ATT_PDU] pending_request: Optional[ATT_PDU]
@@ -374,12 +379,12 @@ class Client:
return self.connection.att_mtu return self.connection.att_mtu
def get_services_by_uuid(self, uuid: UUID) -> list[ServiceProxy]: def get_services_by_uuid(self, uuid: UUID) -> List[ServiceProxy]:
return [service for service in self.services if service.uuid == uuid] return [service for service in self.services if service.uuid == uuid]
def get_characteristics_by_uuid( def get_characteristics_by_uuid(
self, uuid: UUID, service: Optional[ServiceProxy] = None self, uuid: UUID, service: Optional[ServiceProxy] = None
) -> list[CharacteristicProxy[bytes]]: ) -> List[CharacteristicProxy[bytes]]:
services = [service] if service else self.services services = [service] if service else self.services
return [ return [
c c
@@ -390,8 +395,8 @@ class Client:
def get_attribute_grouping(self, attribute_handle: int) -> Optional[ def get_attribute_grouping(self, attribute_handle: int) -> Optional[
Union[ Union[
ServiceProxy, ServiceProxy,
tuple[ServiceProxy, CharacteristicProxy], Tuple[ServiceProxy, CharacteristicProxy],
tuple[ServiceProxy, CharacteristicProxy, DescriptorProxy], Tuple[ServiceProxy, CharacteristicProxy, DescriptorProxy],
] ]
]: ]:
""" """
@@ -424,7 +429,7 @@ class Client:
if not already_known: if not already_known:
self.services.append(service) self.services.append(service)
async def discover_services(self, uuids: Iterable[UUID] = ()) -> list[ServiceProxy]: async def discover_services(self, uuids: Iterable[UUID] = ()) -> List[ServiceProxy]:
''' '''
See Vol 3, Part G - 4.4.1 Discover All Primary Services See Vol 3, Part G - 4.4.1 Discover All Primary Services
''' '''
@@ -496,7 +501,7 @@ class Client:
return services return services
async def discover_service(self, uuid: Union[str, UUID]) -> list[ServiceProxy]: async def discover_service(self, uuid: Union[str, UUID]) -> List[ServiceProxy]:
''' '''
See Vol 3, Part G - 4.4.2 Discover Primary Service by Service UUID See Vol 3, Part G - 4.4.2 Discover Primary Service by Service UUID
''' '''
@@ -567,7 +572,7 @@ class Client:
async def discover_included_services( async def discover_included_services(
self, service: ServiceProxy self, service: ServiceProxy
) -> list[ServiceProxy]: ) -> List[ServiceProxy]:
''' '''
See Vol 3, Part G - 4.5.1 Find Included Services See Vol 3, Part G - 4.5.1 Find Included Services
''' '''
@@ -575,7 +580,7 @@ class Client:
starting_handle = service.handle starting_handle = service.handle
ending_handle = service.end_group_handle ending_handle = service.end_group_handle
included_services: list[ServiceProxy] = [] included_services: List[ServiceProxy] = []
while starting_handle <= ending_handle: while starting_handle <= ending_handle:
response = await self.send_request( response = await self.send_request(
ATT_Read_By_Type_Request( ATT_Read_By_Type_Request(
@@ -631,7 +636,7 @@ class Client:
async def discover_characteristics( async def discover_characteristics(
self, uuids, service: Optional[ServiceProxy] self, uuids, service: Optional[ServiceProxy]
) -> list[CharacteristicProxy[bytes]]: ) -> List[CharacteristicProxy[bytes]]:
''' '''
See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2 See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2
Discover Characteristics by UUID Discover Characteristics by UUID
@@ -644,12 +649,12 @@ class Client:
services = [service] if service else self.services services = [service] if service else self.services
# Perform characteristic discovery for each service # Perform characteristic discovery for each service
discovered_characteristics: list[CharacteristicProxy[bytes]] = [] discovered_characteristics: List[CharacteristicProxy[bytes]] = []
for service in services: for service in services:
starting_handle = service.handle starting_handle = service.handle
ending_handle = service.end_group_handle ending_handle = service.end_group_handle
characteristics: list[CharacteristicProxy[bytes]] = [] characteristics: List[CharacteristicProxy[bytes]] = []
while starting_handle <= ending_handle: while starting_handle <= ending_handle:
response = await self.send_request( response = await self.send_request(
ATT_Read_By_Type_Request( ATT_Read_By_Type_Request(
@@ -720,7 +725,7 @@ class Client:
characteristic: Optional[CharacteristicProxy] = None, characteristic: Optional[CharacteristicProxy] = None,
start_handle: Optional[int] = None, start_handle: Optional[int] = None,
end_handle: Optional[int] = None, end_handle: Optional[int] = None,
) -> list[DescriptorProxy]: ) -> List[DescriptorProxy]:
''' '''
See Vol 3, Part G - 4.7.1 Discover All Characteristic Descriptors See Vol 3, Part G - 4.7.1 Discover All Characteristic Descriptors
''' '''
@@ -733,7 +738,7 @@ class Client:
else: else:
return [] return []
descriptors: list[DescriptorProxy] = [] descriptors: List[DescriptorProxy] = []
while starting_handle <= ending_handle: while starting_handle <= ending_handle:
response = await self.send_request( response = await self.send_request(
ATT_Find_Information_Request( ATT_Find_Information_Request(
@@ -782,7 +787,7 @@ class Client:
return descriptors return descriptors
async def discover_attributes(self) -> list[AttributeProxy[bytes]]: async def discover_attributes(self) -> List[AttributeProxy[bytes]]:
''' '''
Discover all attributes, regardless of type Discover all attributes, regardless of type
''' '''
@@ -997,7 +1002,7 @@ class Client:
async def read_characteristics_by_uuid( async def read_characteristics_by_uuid(
self, uuid: UUID, service: Optional[ServiceProxy] self, uuid: UUID, service: Optional[ServiceProxy]
) -> list[bytes]: ) -> List[bytes]:
''' '''
See Vol 3, Part G - 4.8.2 Read Using Characteristic UUID See Vol 3, Part G - 4.8.2 Read Using Characteristic UUID
''' '''
+15 -8
View File
@@ -29,9 +29,13 @@ import logging
from collections import defaultdict from collections import defaultdict
import struct import struct
from typing import ( from typing import (
Dict,
Iterable, Iterable,
List,
Optional, Optional,
Tuple,
TypeVar, TypeVar,
Type,
TYPE_CHECKING, TYPE_CHECKING,
) )
@@ -99,10 +103,10 @@ GATT_SERVER_DEFAULT_MAX_MTU = 517
# GATT Server # GATT Server
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Server(utils.EventEmitter): class Server(utils.EventEmitter):
attributes: list[Attribute] attributes: List[Attribute]
services: list[Service] services: List[Service]
attributes_by_handle: dict[int, Attribute] attributes_by_handle: Dict[int, Attribute]
subscribers: dict[int, dict[int, bytes]] subscribers: Dict[int, Dict[int, bytes]]
indication_semaphores: defaultdict[int, asyncio.Semaphore] indication_semaphores: defaultdict[int, asyncio.Semaphore]
pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]] pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]]
@@ -132,7 +136,7 @@ class Server(utils.EventEmitter):
def next_handle(self) -> int: def next_handle(self) -> int:
return 1 + len(self.attributes) return 1 + len(self.attributes)
def get_advertising_service_data(self) -> dict[Attribute, bytes]: def get_advertising_service_data(self) -> Dict[Attribute, bytes]:
return { return {
attribute: data attribute: data
for attribute in self.attributes for attribute in self.attributes
@@ -156,7 +160,7 @@ class Server(utils.EventEmitter):
AttributeGroupType = TypeVar('AttributeGroupType', Service, Characteristic) AttributeGroupType = TypeVar('AttributeGroupType', Service, Characteristic)
def get_attribute_group( def get_attribute_group(
self, handle: int, group_type: type[AttributeGroupType] self, handle: int, group_type: Type[AttributeGroupType]
) -> Optional[AttributeGroupType]: ) -> Optional[AttributeGroupType]:
return next( return next(
( (
@@ -182,7 +186,7 @@ class Server(utils.EventEmitter):
def get_characteristic_attributes( def get_characteristic_attributes(
self, service_uuid: UUID, characteristic_uuid: UUID self, service_uuid: UUID, characteristic_uuid: UUID
) -> Optional[tuple[CharacteristicDeclaration, Characteristic]]: ) -> Optional[Tuple[CharacteristicDeclaration, Characteristic]]:
service_handle = self.get_service_attribute(service_uuid) service_handle = self.get_service_attribute(service_uuid)
if not service_handle: if not service_handle:
return None return None
@@ -311,8 +315,11 @@ class Server(utils.EventEmitter):
self.add_service(service) self.add_service(service)
def read_cccd( def read_cccd(
self, connection: Connection, characteristic: Characteristic self, connection: Optional[Connection], characteristic: Characteristic
) -> bytes: ) -> bytes:
if connection is None:
return bytes([0, 0])
subscribers = self.subscribers.get(connection.handle) subscribers = self.subscribers.get(connection.handle)
cccd = None cccd = None
if subscribers: if subscribers:
+2897 -2589
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -34,8 +34,9 @@ from bumble.att import ATT_CID, ATT_PDU
from bumble.smp import SMP_CID, SMP_Command from bumble.smp import SMP_CID, SMP_Command
from bumble.core import name_or_number from bumble.core import name_or_number
from bumble.l2cap import ( from bumble.l2cap import (
CommandCode,
L2CAP_PDU, L2CAP_PDU,
L2CAP_CONNECTION_REQUEST,
L2CAP_CONNECTION_RESPONSE,
L2CAP_SIGNALING_CID, L2CAP_SIGNALING_CID,
L2CAP_LE_SIGNALING_CID, L2CAP_LE_SIGNALING_CID,
L2CAP_Control_Frame, L2CAP_Control_Frame,
@@ -105,14 +106,14 @@ class PacketTracer:
self.analyzer.emit(control_frame) self.analyzer.emit(control_frame)
# Check if this signals a new channel # Check if this signals a new channel
if control_frame.code == CommandCode.L2CAP_CONNECTION_REQUEST: if control_frame.code == L2CAP_CONNECTION_REQUEST:
connection_request = cast(L2CAP_Connection_Request, control_frame) connection_request = cast(L2CAP_Connection_Request, control_frame)
self.psms[connection_request.source_cid] = connection_request.psm self.psms[connection_request.source_cid] = connection_request.psm
elif control_frame.code == CommandCode.L2CAP_CONNECTION_RESPONSE: elif control_frame.code == L2CAP_CONNECTION_RESPONSE:
connection_response = cast(L2CAP_Connection_Response, control_frame) connection_response = cast(L2CAP_Connection_Response, control_frame)
if ( if (
connection_response.result connection_response.result
== L2CAP_Connection_Response.Result.CONNECTION_SUCCESSFUL == L2CAP_Connection_Response.CONNECTION_SUCCESSFUL
): ):
if self.peer and ( if self.peer and (
psm := self.peer.psms.get(connection_response.source_cid) psm := self.peer.psms.get(connection_response.source_cid)
+37 -32
View File
@@ -26,9 +26,14 @@ import enum
import traceback import traceback
import re import re
from typing import ( from typing import (
Dict,
List,
Union, Union,
Set,
Any, Any,
Optional, Optional,
Type,
Tuple,
ClassVar, ClassVar,
Iterable, Iterable,
TYPE_CHECKING, TYPE_CHECKING,
@@ -370,7 +375,7 @@ class CallLineIdentification:
cli_validity: Optional[int] = None cli_validity: Optional[int] = None
@classmethod @classmethod
def parse_from(cls, parameters: list[bytes]) -> Self: def parse_from(cls: Type[Self], parameters: List[bytes]) -> Self:
return cls( return cls(
number=parameters[0].decode(), number=parameters[0].decode(),
type=int(parameters[1]), type=int(parameters[1]),
@@ -500,9 +505,9 @@ STATUS_CODES = {
@dataclasses.dataclass @dataclasses.dataclass
class HfConfiguration: class HfConfiguration:
supported_hf_features: list[HfFeature] supported_hf_features: List[HfFeature]
supported_hf_indicators: list[HfIndicator] supported_hf_indicators: List[HfIndicator]
supported_audio_codecs: list[AudioCodec] supported_audio_codecs: List[AudioCodec]
@dataclasses.dataclass @dataclasses.dataclass
@@ -530,7 +535,7 @@ class AtResponse:
parameters: list parameters: list
@classmethod @classmethod
def parse_from(cls: type[Self], buffer: bytearray) -> Self: def parse_from(cls: Type[Self], buffer: bytearray) -> Self:
code_and_parameters = buffer.split(b':') code_and_parameters = buffer.split(b':')
parameters = ( parameters = (
code_and_parameters[1] if len(code_and_parameters) > 1 else bytearray() code_and_parameters[1] if len(code_and_parameters) > 1 else bytearray()
@@ -558,7 +563,7 @@ class AtCommand:
) )
@classmethod @classmethod
def parse_from(cls: type[Self], buffer: bytearray) -> Self: def parse_from(cls: Type[Self], buffer: bytearray) -> Self:
if not (match := cls._PARSE_PATTERN.fullmatch(buffer.decode())): if not (match := cls._PARSE_PATTERN.fullmatch(buffer.decode())):
if buffer.startswith(b'ATA'): if buffer.startswith(b'ATA'):
return cls(code='A', sub_code=AtCommand.SubCode.NONE, parameters=[]) return cls(code='A', sub_code=AtCommand.SubCode.NONE, parameters=[])
@@ -593,7 +598,7 @@ class AgIndicatorState:
""" """
indicator: AgIndicator indicator: AgIndicator
supported_values: set[int] supported_values: Set[int]
current_status: int current_status: int
index: Optional[int] = None index: Optional[int] = None
enabled: bool = True enabled: bool = True
@@ -611,14 +616,14 @@ class AgIndicatorState:
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:
"""Default call indicator state.""" """Default call indicator state."""
return cls( return cls(
indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0 indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0
) )
@classmethod @classmethod
def callsetup(cls: type[Self]) -> Self: def callsetup(cls: Type[Self]) -> Self:
"""Default callsetup indicator state.""" """Default callsetup indicator state."""
return cls( return cls(
indicator=AgIndicator.CALL_SETUP, indicator=AgIndicator.CALL_SETUP,
@@ -627,7 +632,7 @@ class AgIndicatorState:
) )
@classmethod @classmethod
def callheld(cls: type[Self]) -> Self: def callheld(cls: Type[Self]) -> Self:
"""Default call indicator state.""" """Default call indicator state."""
return cls( return cls(
indicator=AgIndicator.CALL_HELD, indicator=AgIndicator.CALL_HELD,
@@ -636,14 +641,14 @@ class AgIndicatorState:
) )
@classmethod @classmethod
def service(cls: type[Self]) -> Self: def service(cls: Type[Self]) -> Self:
"""Default service indicator state.""" """Default service indicator state."""
return cls( return cls(
indicator=AgIndicator.SERVICE, supported_values={0, 1}, current_status=0 indicator=AgIndicator.SERVICE, supported_values={0, 1}, current_status=0
) )
@classmethod @classmethod
def signal(cls: type[Self]) -> Self: def signal(cls: Type[Self]) -> Self:
"""Default signal indicator state.""" """Default signal indicator state."""
return cls( return cls(
indicator=AgIndicator.SIGNAL, indicator=AgIndicator.SIGNAL,
@@ -652,14 +657,14 @@ class AgIndicatorState:
) )
@classmethod @classmethod
def roam(cls: type[Self]) -> Self: def roam(cls: Type[Self]) -> Self:
"""Default roam indicator state.""" """Default roam indicator state."""
return cls( return cls(
indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0 indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0
) )
@classmethod @classmethod
def battchg(cls: type[Self]) -> Self: def battchg(cls: Type[Self]) -> Self:
"""Default battery charge indicator state.""" """Default battery charge indicator state."""
return cls( return cls(
indicator=AgIndicator.BATTERY_CHARGE, indicator=AgIndicator.BATTERY_CHARGE,
@@ -727,13 +732,13 @@ class HfProtocol(utils.EventEmitter):
"""Termination signal for run() loop.""" """Termination signal for run() loop."""
supported_hf_features: int supported_hf_features: int
supported_audio_codecs: list[AudioCodec] supported_audio_codecs: List[AudioCodec]
supported_ag_features: int supported_ag_features: int
supported_ag_call_hold_operations: list[CallHoldOperation] supported_ag_call_hold_operations: List[CallHoldOperation]
ag_indicators: list[AgIndicatorState] ag_indicators: List[AgIndicatorState]
hf_indicators: dict[HfIndicator, HfIndicatorState] hf_indicators: Dict[HfIndicator, HfIndicatorState]
dlc: rfcomm.DLC dlc: rfcomm.DLC
command_lock: asyncio.Lock command_lock: asyncio.Lock
@@ -831,7 +836,7 @@ class HfProtocol(utils.EventEmitter):
cmd: str, cmd: str,
timeout: float = 1.0, timeout: float = 1.0,
response_type: AtResponseType = AtResponseType.NONE, response_type: AtResponseType = AtResponseType.NONE,
) -> Union[None, AtResponse, list[AtResponse]]: ) -> Union[None, AtResponse, List[AtResponse]]:
""" """
Sends an AT command and wait for the peer response. Sends an AT command and wait for the peer response.
Wait for the AT responses sent by the peer, to the status code. Wait for the AT responses sent by the peer, to the status code.
@@ -848,7 +853,7 @@ class HfProtocol(utils.EventEmitter):
async with self.command_lock: async with self.command_lock:
logger.debug(f">>> {cmd}") logger.debug(f">>> {cmd}")
self.dlc.write(cmd + '\r') self.dlc.write(cmd + '\r')
responses: list[AtResponse] = [] responses: List[AtResponse] = []
while True: while True:
result = await asyncio.wait_for( result = await asyncio.wait_for(
@@ -1068,7 +1073,7 @@ class HfProtocol(utils.EventEmitter):
# code, with the value indicating (call=0). # code, with the value indicating (call=0).
await self.execute_command("AT+CHUP") await self.execute_command("AT+CHUP")
async def query_current_calls(self) -> list[CallInfo]: async def query_current_calls(self) -> List[CallInfo]:
"""4.32.1 Query List of Current Calls in AG. """4.32.1 Query List of Current Calls in AG.
Return: Return:
@@ -1199,27 +1204,27 @@ class AgProtocol(utils.EventEmitter):
EVENT_MICROPHONE_VOLUME = "microphone_volume" EVENT_MICROPHONE_VOLUME = "microphone_volume"
supported_hf_features: int supported_hf_features: int
supported_hf_indicators: set[HfIndicator] supported_hf_indicators: Set[HfIndicator]
supported_audio_codecs: list[AudioCodec] supported_audio_codecs: List[AudioCodec]
supported_ag_features: int supported_ag_features: int
supported_ag_call_hold_operations: list[CallHoldOperation] supported_ag_call_hold_operations: List[CallHoldOperation]
ag_indicators: list[AgIndicatorState] ag_indicators: List[AgIndicatorState]
hf_indicators: collections.OrderedDict[HfIndicator, HfIndicatorState] hf_indicators: collections.OrderedDict[HfIndicator, HfIndicatorState]
dlc: rfcomm.DLC dlc: rfcomm.DLC
read_buffer: bytearray read_buffer: bytearray
active_codec: AudioCodec active_codec: AudioCodec
calls: list[CallInfo] calls: List[CallInfo]
indicator_report_enabled: bool indicator_report_enabled: bool
inband_ringtone_enabled: bool inband_ringtone_enabled: bool
cme_error_enabled: bool cme_error_enabled: bool
cli_notification_enabled: bool cli_notification_enabled: bool
call_waiting_enabled: bool call_waiting_enabled: bool
_remained_slc_setup_features: set[HfFeature] _remained_slc_setup_features: Set[HfFeature]
def __init__(self, dlc: rfcomm.DLC, configuration: AgConfiguration) -> None: def __init__(self, dlc: rfcomm.DLC, configuration: AgConfiguration) -> None:
super().__init__() super().__init__()
@@ -1689,7 +1694,7 @@ def make_hf_sdp_records(
rfcomm_channel: int, rfcomm_channel: int,
configuration: HfConfiguration, configuration: HfConfiguration,
version: ProfileVersion = ProfileVersion.V1_8, version: ProfileVersion = ProfileVersion.V1_8,
) -> list[sdp.ServiceAttribute]: ) -> List[sdp.ServiceAttribute]:
""" """
Generates the SDP record for HFP Hands-Free support. Generates the SDP record for HFP Hands-Free support.
@@ -1775,7 +1780,7 @@ def make_ag_sdp_records(
rfcomm_channel: int, rfcomm_channel: int,
configuration: AgConfiguration, configuration: AgConfiguration,
version: ProfileVersion = ProfileVersion.V1_8, version: ProfileVersion = ProfileVersion.V1_8,
) -> list[sdp.ServiceAttribute]: ) -> List[sdp.ServiceAttribute]:
""" """
Generates the SDP record for HFP Audio-Gateway support. Generates the SDP record for HFP Audio-Gateway support.
@@ -1855,7 +1860,7 @@ def make_ag_sdp_records(
async def find_hf_sdp_record( async def find_hf_sdp_record(
connection: device.Connection, connection: device.Connection,
) -> Optional[tuple[int, ProfileVersion, HfSdpFeature]]: ) -> Optional[Tuple[int, ProfileVersion, HfSdpFeature]]:
"""Searches a Hands-Free SDP record from remote device. """Searches a Hands-Free SDP record from remote device.
Args: Args:
@@ -1907,7 +1912,7 @@ async def find_hf_sdp_record(
async def find_ag_sdp_record( async def find_ag_sdp_record(
connection: device.Connection, connection: device.Connection,
) -> Optional[tuple[int, ProfileVersion, AgSdpFeature]]: ) -> Optional[Tuple[int, ProfileVersion, AgSdpFeature]]:
"""Searches an Audio-Gateway SDP record from remote device. """Searches an Audio-Gateway SDP record from remote device.
Args: Args:
@@ -2005,7 +2010,7 @@ class EscoParameters:
transmit_codec_frame_size: int = 60 transmit_codec_frame_size: int = 60
receive_codec_frame_size: int = 60 receive_codec_frame_size: int = 60
def asdict(self) -> dict[str, Any]: def asdict(self) -> Dict[str, Any]:
# dataclasses.asdict() will recursively deep-copy the entire object, # dataclasses.asdict() will recursively deep-copy the entire object,
# which is expensive and breaks CodingFormat object, so let it simply copy here. # which is expensive and breaks CodingFormat object, so let it simply copy here.
return self.__dict__ return self.__dict__
+35 -94
View File
@@ -26,7 +26,10 @@ from typing import (
Any, Any,
Awaitable, Awaitable,
Callable, Callable,
Deque,
Dict,
Optional, Optional,
Set,
cast, cast,
TYPE_CHECKING, TYPE_CHECKING,
) )
@@ -38,6 +41,7 @@ from bumble.snoop import Snooper
from bumble import drivers from bumble import drivers
from bumble import hci from bumble import hci
from bumble.core import ( from bumble.core import (
PhysicalTransport,
PhysicalTransport, PhysicalTransport,
ConnectionPHY, ConnectionPHY,
ConnectionParameters, ConnectionParameters,
@@ -71,11 +75,6 @@ class DataPacketQueue(utils.EventEmitter):
max_packet_size: int max_packet_size: int
class PerConnectionState:
def __init__(self) -> None:
self.in_flight = 0
self.drained = asyncio.Event()
def __init__( def __init__(
self, self,
max_packet_size: int, max_packet_size: int,
@@ -86,16 +85,11 @@ class DataPacketQueue(utils.EventEmitter):
self.max_packet_size = max_packet_size self.max_packet_size = max_packet_size
self.max_in_flight = max_in_flight self.max_in_flight = max_in_flight
self._in_flight = 0 # Total number of packets in flight across all connections self._in_flight = 0 # Total number of packets in flight across all connections
self._connection_state: dict[int, DataPacketQueue.PerConnectionState] = ( self._in_flight_per_connection: dict[int, int] = collections.defaultdict(
collections.defaultdict(DataPacketQueue.PerConnectionState) int
) ) # Number of packets in flight per connection
self._drained_per_connection: dict[int, asyncio.Event] = (
collections.defaultdict(asyncio.Event)
)
self._send = send self._send = send
self._packets: collections.deque[tuple[hci.HCI_Packet, int]] = ( self._packets: Deque[tuple[hci.HCI_Packet, int]] = collections.deque()
collections.deque()
)
self._queued = 0 self._queued = 0
self._completed = 0 self._completed = 0
@@ -143,40 +137,36 @@ class DataPacketQueue(utils.EventEmitter):
self._completed += flushed_count self._completed += flushed_count
self._packets = collections.deque(packets_to_keep) self._packets = collections.deque(packets_to_keep)
if connection_state := self._connection_state.pop(connection_handle, None): if connection_handle in self._in_flight_per_connection:
in_flight = connection_state.in_flight in_flight = self._in_flight_per_connection[connection_handle]
self._completed += in_flight self._completed += in_flight
self._in_flight -= in_flight self._in_flight -= in_flight
connection_state.drained.set() del self._in_flight_per_connection[connection_handle]
def _check_queue(self) -> None: def _check_queue(self) -> None:
while self._packets and self._in_flight < self.max_in_flight: while self._packets and self._in_flight < self.max_in_flight:
packet, connection_handle = self._packets.pop() packet, connection_handle = self._packets.pop()
self._send(packet) self._send(packet)
self._in_flight += 1 self._in_flight += 1
connection_state = self._connection_state[connection_handle] self._in_flight_per_connection[connection_handle] += 1
connection_state.in_flight += 1
connection_state.drained.clear()
def on_packets_completed(self, packet_count: int, connection_handle: int) -> None: def on_packets_completed(self, packet_count: int, connection_handle: int) -> None:
"""Mark one or more packets associated with a connection as completed.""" """Mark one or more packets associated with a connection as completed."""
if connection_handle not in self._connection_state: if connection_handle not in self._in_flight_per_connection:
logger.warning( logger.warning(
f'received completion for unknown connection {connection_handle}' f'received completion for unknown connection {connection_handle}'
) )
return return
connection_state = self._connection_state[connection_handle] in_flight_for_connection = self._in_flight_per_connection[connection_handle]
if packet_count <= connection_state.in_flight: if packet_count <= in_flight_for_connection:
connection_state.in_flight -= packet_count self._in_flight_per_connection[connection_handle] -= packet_count
else: else:
logger.warning( logger.warning(
f'{packet_count} completed for {connection_handle} ' f'{packet_count} completed for {connection_handle} '
f'but only {connection_state.in_flight} in flight' f'but only {in_flight_for_connection} in flight'
) )
connection_state.in_flight = 0 self._in_flight_per_connection[connection_handle] = 0
if connection_state.in_flight == 0:
connection_state.drained.set()
if packet_count <= self._in_flight: if packet_count <= self._in_flight:
self._in_flight -= packet_count self._in_flight -= packet_count
@@ -191,13 +181,6 @@ class DataPacketQueue(utils.EventEmitter):
self._check_queue() self._check_queue()
self.emit('flow') self.emit('flow')
async def drain(self, connection_handle: int) -> None:
"""Wait until there are no pending packets for a connection."""
if not (connection_state := self._connection_state.get(connection_handle)):
raise ValueError('no such connection')
await connection_state.drained.wait()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Connection: class Connection:
@@ -251,16 +234,16 @@ class IsoLink:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Host(utils.EventEmitter): class Host(utils.EventEmitter):
connections: dict[int, Connection] connections: Dict[int, Connection]
cis_links: dict[int, IsoLink] cis_links: Dict[int, IsoLink]
bis_links: dict[int, IsoLink] bis_links: Dict[int, IsoLink]
sco_links: dict[int, ScoLink] sco_links: Dict[int, ScoLink]
bigs: dict[int, set[int]] bigs: dict[int, set[int]]
acl_packet_queue: Optional[DataPacketQueue] = None acl_packet_queue: Optional[DataPacketQueue] = None
le_acl_packet_queue: Optional[DataPacketQueue] = None le_acl_packet_queue: Optional[DataPacketQueue] = None
iso_packet_queue: Optional[DataPacketQueue] = None iso_packet_queue: Optional[DataPacketQueue] = None
hci_sink: Optional[TransportSink] = None hci_sink: Optional[TransportSink] = None
hci_metadata: dict[str, Any] hci_metadata: Dict[str, Any]
long_term_key_provider: Optional[ long_term_key_provider: Optional[
Callable[[int, bytes, int], Awaitable[Optional[bytes]]] Callable[[int, bytes, int], Awaitable[Optional[bytes]]]
] ]
@@ -830,7 +813,7 @@ class Host(utils.EventEmitter):
) != 0 ) != 0
@property @property
def supported_commands(self) -> set[int]: def supported_commands(self) -> Set[int]:
return set( return set(
op_code op_code
for op_code, mask in hci.HCI_SUPPORTED_COMMANDS_MASKS.items() for op_code, mask in hci.HCI_SUPPORTED_COMMANDS_MASKS.items()
@@ -853,8 +836,8 @@ class Host(utils.EventEmitter):
def on_packet(self, packet: bytes) -> None: def on_packet(self, packet: bytes) -> None:
try: try:
hci_packet = hci.HCI_Packet.from_bytes(packet) hci_packet = hci.HCI_Packet.from_bytes(packet)
except Exception: except Exception as error:
logger.exception('!!! error parsing packet from bytes') logger.warning(f'!!! error parsing packet from bytes: {error}')
return return
if self.ready or ( if self.ready or (
@@ -1144,19 +1127,11 @@ class Host(utils.EventEmitter):
else: else:
self.emit('connection_phy_update_failure', connection.handle, event.status) self.emit('connection_phy_update_failure', connection.handle, event.status)
def on_hci_le_advertising_report_event( def on_hci_le_advertising_report_event(self, event):
self,
event: (
hci.HCI_LE_Advertising_Report_Event
| hci.HCI_LE_Extended_Advertising_Report_Event
),
):
for report in event.reports: for report in event.reports:
self.emit('advertising_report', report) self.emit('advertising_report', report)
def on_hci_le_extended_advertising_report_event( def on_hci_le_extended_advertising_report_event(self, event):
self, event: hci.HCI_LE_Extended_Advertising_Report_Event
):
self.on_hci_le_advertising_report_event(event) self.on_hci_le_advertising_report_event(event)
def on_hci_le_advertising_set_terminated_event(self, event): def on_hci_le_advertising_set_terminated_event(self, event):
@@ -1287,24 +1262,7 @@ class Host(utils.EventEmitter):
self.cis_links[event.connection_handle] = IsoLink( self.cis_links[event.connection_handle] = IsoLink(
handle=event.connection_handle, packet_queue=self.iso_packet_queue handle=event.connection_handle, packet_queue=self.iso_packet_queue
) )
self.emit( self.emit('cis_establishment', event.connection_handle)
'cis_establishment',
event.connection_handle,
event.cig_sync_delay,
event.cis_sync_delay,
event.transport_latency_c_to_p,
event.transport_latency_p_to_c,
event.phy_c_to_p,
event.phy_p_to_c,
event.nse,
event.bn_c_to_p,
event.bn_p_to_c,
event.ft_c_to_p,
event.ft_p_to_c,
event.max_pdu_c_to_p,
event.max_pdu_p_to_c,
event.iso_interval,
)
else: else:
self.emit( self.emit(
'cis_establishment_failure', event.connection_handle, event.status 'cis_establishment_failure', event.connection_handle, event.status
@@ -1392,15 +1350,6 @@ class Host(utils.EventEmitter):
def on_hci_synchronous_connection_changed_event(self, event): def on_hci_synchronous_connection_changed_event(self, event):
pass pass
def on_hci_mode_change_event(self, event: hci.HCI_Mode_Change_Event):
self.emit(
'mode_change',
event.connection_handle,
event.status,
event.current_mode,
event.interval,
)
def on_hci_role_change_event(self, event): def on_hci_role_change_event(self, event):
if event.status == hci.HCI_SUCCESS: if event.status == hci.HCI_SUCCESS:
logger.debug( logger.debug(
@@ -1416,10 +1365,6 @@ class Host(utils.EventEmitter):
self.emit('role_change_failure', event.bd_addr, event.status) self.emit('role_change_failure', event.bd_addr, event.status)
def on_hci_le_data_length_change_event(self, event): def on_hci_le_data_length_change_event(self, event):
if (connection := self.connections.get(event.connection_handle)) is None:
logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
return
self.emit( self.emit(
'connection_data_length_change', 'connection_data_length_change',
event.connection_handle, event.connection_handle,
@@ -1440,7 +1385,7 @@ class Host(utils.EventEmitter):
event.status, event.status,
) )
def on_hci_encryption_change_event(self, event: hci.HCI_Encryption_Change_Event): def on_hci_encryption_change_event(self, event):
# Notify the client # Notify the client
if event.status == hci.HCI_SUCCESS: if event.status == hci.HCI_SUCCESS:
self.emit( self.emit(
@@ -1454,9 +1399,7 @@ class Host(utils.EventEmitter):
'connection_encryption_failure', event.connection_handle, event.status 'connection_encryption_failure', event.connection_handle, event.status
) )
def on_hci_encryption_change_v2_event( def on_hci_encryption_change_v2_event(self, event):
self, event: hci.HCI_Encryption_Change_V2_Event
):
# Notify the client # Notify the client
if event.status == hci.HCI_SUCCESS: if event.status == hci.HCI_SUCCESS:
self.emit( self.emit(
@@ -1577,15 +1520,13 @@ class Host(utils.EventEmitter):
self.emit('inquiry_complete') self.emit('inquiry_complete')
def on_hci_inquiry_result_with_rssi_event(self, event): def on_hci_inquiry_result_with_rssi_event(self, event):
for bd_addr, class_of_device, rssi in zip( for response in event.responses:
event.bd_addr, event.class_of_device, event.rssi
):
self.emit( self.emit(
'inquiry_result', 'inquiry_result',
bd_addr, response.bd_addr,
class_of_device, response.class_of_device,
b'', b'',
rssi, response.rssi,
) )
def on_hci_extended_inquiry_result_event(self, event): def on_hci_extended_inquiry_result_event(self, event):
+5 -5
View File
@@ -26,7 +26,7 @@ import dataclasses
import logging import logging
import os import os
import json import json
from typing import TYPE_CHECKING, Optional, Any from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Any
from typing_extensions import Self from typing_extensions import Self
from bumble.colors import color from bumble.colors import color
@@ -157,7 +157,7 @@ class KeyStore:
async def get(self, _name: str) -> Optional[PairingKeys]: async def get(self, _name: str) -> Optional[PairingKeys]:
return None return None
async def get_all(self) -> list[tuple[str, PairingKeys]]: async def get_all(self) -> List[Tuple[str, PairingKeys]]:
return [] return []
async def delete_all(self) -> None: async def delete_all(self) -> None:
@@ -272,7 +272,7 @@ class JsonKeyStore(KeyStore):
@classmethod @classmethod
def from_device( def from_device(
cls: type[Self], device: Device, filename: Optional[str] = None cls: Type[Self], device: Device, filename: Optional[str] = None
) -> Self: ) -> Self:
if not filename: if not filename:
# Extract the filename from the config if there is one # Extract the filename from the config if there is one
@@ -356,7 +356,7 @@ class JsonKeyStore(KeyStore):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class MemoryKeyStore(KeyStore): class MemoryKeyStore(KeyStore):
all_keys: dict[str, PairingKeys] all_keys: Dict[str, PairingKeys]
def __init__(self) -> None: def __init__(self) -> None:
self.all_keys = {} self.all_keys = {}
@@ -371,5 +371,5 @@ class MemoryKeyStore(KeyStore):
async def get(self, name: str) -> Optional[PairingKeys]: async def get(self, name: str) -> Optional[PairingKeys]:
return self.all_keys.get(name) return self.all_keys.get(name)
async def get_all(self) -> list[tuple[str, PairingKeys]]: async def get_all(self) -> List[Tuple[str, PairingKeys]]:
return list(self.all_keys.items()) return list(self.all_keys.items())
+393 -309
View File
File diff suppressed because it is too large Load Diff
+270 -6
View File
@@ -17,20 +17,26 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import logging import logging
import asyncio import asyncio
from functools import partial
from bumble import core from bumble.core import (
PhysicalTransport,
InvalidStateError,
)
from bumble.colors import color
from bumble.hci import ( from bumble.hci import (
Address, Address,
Role, Role,
HCI_SUCCESS, HCI_SUCCESS,
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR, HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
HCI_CONNECTION_TIMEOUT_ERROR,
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR, HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
HCI_PAGE_TIMEOUT_ERROR, HCI_PAGE_TIMEOUT_ERROR,
HCI_Connection_Complete_Event, HCI_Connection_Complete_Event,
) )
from bumble import controller from bumble import controller
from typing import Optional from typing import Optional, Set
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging # Logging
@@ -59,7 +65,7 @@ class LocalLink:
Link bus for controllers to communicate with each other Link bus for controllers to communicate with each other
''' '''
controllers: set[controller.Controller] controllers: Set[controller.Controller]
def __init__(self): def __init__(self):
self.controllers = set() self.controllers = set()
@@ -109,10 +115,10 @@ class LocalLink:
def send_acl_data(self, sender_controller, destination_address, transport, data): def send_acl_data(self, sender_controller, destination_address, transport, data):
# Send the data to the first controller with a matching address # Send the data to the first controller with a matching address
if transport == core.PhysicalTransport.LE: if transport == PhysicalTransport.LE:
destination_controller = self.find_controller(destination_address) destination_controller = self.find_controller(destination_address)
source_address = sender_controller.random_address source_address = sender_controller.random_address
elif transport == core.PhysicalTransport.BR_EDR: elif transport == PhysicalTransport.BR_EDR:
destination_controller = self.find_classic_controller(destination_address) destination_controller = self.find_classic_controller(destination_address)
source_address = sender_controller.public_address source_address = sender_controller.public_address
else: else:
@@ -268,7 +274,7 @@ class LocalLink:
responder_controller.on_classic_connection_request( responder_controller.on_classic_connection_request(
initiator_controller.public_address, initiator_controller.public_address,
HCI_Connection_Complete_Event.LinkType.ACL, HCI_Connection_Complete_Event.ACL_LINK_TYPE,
) )
def classic_accept_connection( def classic_accept_connection(
@@ -378,3 +384,261 @@ class LocalLink:
responder_controller.on_classic_sco_connection_complete( responder_controller.on_classic_sco_connection_complete(
initiator_controller.public_address, HCI_SUCCESS, link_type initiator_controller.public_address, HCI_SUCCESS, link_type
) )
# -----------------------------------------------------------------------------
class RemoteLink:
'''
A Link implementation that communicates with other virtual controllers via a
WebSocket relay
'''
def __init__(self, uri):
self.controller = None
self.uri = uri
self.execution_queue = asyncio.Queue()
self.websocket = asyncio.get_running_loop().create_future()
self.rpc_result = None
self.pending_connection = None
self.central_connections = set() # List of addresses that we have connected to
self.peripheral_connections = (
set()
) # List of addresses that have connected to us
# Connect and run asynchronously
asyncio.create_task(self.run_connection())
asyncio.create_task(self.run_executor_loop())
def add_controller(self, controller):
if self.controller:
raise InvalidStateError('controller already set')
self.controller = controller
def remove_controller(self, controller):
if self.controller != controller:
raise InvalidStateError('controller mismatch')
self.controller = None
def get_pending_connection(self):
return self.pending_connection
def get_pending_classic_connection(self):
return self.pending_classic_connection
async def wait_until_connected(self):
await self.websocket
def execute(self, async_function):
self.execution_queue.put_nowait(async_function())
async def run_executor_loop(self):
logger.debug('executor loop starting')
while True:
item = await self.execution_queue.get()
try:
await item
except Exception as error:
logger.warning(
f'{color("!!! Exception in async handler:", "red")} {error}'
)
async def run_connection(self):
import websockets # lazy import
# Connect to the relay
logger.debug(f'connecting to {self.uri}')
# pylint: disable-next=no-member
websocket = await websockets.connect(self.uri)
self.websocket.set_result(websocket)
logger.debug(f'connected to {self.uri}')
while True:
message = await websocket.recv()
logger.debug(f'received message: {message}')
keyword, *payload = message.split(':', 1)
handler_name = f'on_{keyword}_received'
handler = getattr(self, handler_name, None)
if handler:
await handler(payload[0] if payload else None)
def close(self):
if self.websocket.done():
logger.debug('closing websocket')
websocket = self.websocket.result()
asyncio.create_task(websocket.close())
async def on_result_received(self, result):
if self.rpc_result:
self.rpc_result.set_result(result)
async def on_left_received(self, address):
if address in self.central_connections:
self.controller.on_link_peripheral_disconnected(Address(address))
self.central_connections.remove(address)
if address in self.peripheral_connections:
self.controller.on_link_central_disconnected(
address, HCI_CONNECTION_TIMEOUT_ERROR
)
self.peripheral_connections.remove(address)
async def on_unreachable_received(self, target):
await self.on_left_received(target)
async def on_message_received(self, message):
sender, *payload = message.split('/', 1)
if payload:
keyword, *payload = payload[0].split(':', 1)
handler_name = f'on_{keyword}_message_received'
handler = getattr(self, handler_name, None)
if handler:
await handler(sender, payload[0] if payload else None)
async def on_advertisement_message_received(self, sender, advertisement):
try:
self.controller.on_link_advertising_data(
Address(sender), bytes.fromhex(advertisement)
)
except Exception:
logger.exception('exception')
async def on_acl_message_received(self, sender, acl_data):
try:
self.controller.on_link_acl_data(Address(sender), bytes.fromhex(acl_data))
except Exception:
logger.exception('exception')
async def on_connect_message_received(self, sender, _):
# Remember the connection
self.peripheral_connections.add(sender)
# Notify the controller
logger.debug(f'connection from central {sender}')
self.controller.on_link_central_connected(Address(sender))
# Accept the connection by responding to it
await self.send_targeted_message(sender, 'connected')
async def on_connected_message_received(self, sender, _):
if not self.pending_connection:
logger.warning('received a connection ack, but no connection is pending')
return
# Remember the connection
self.central_connections.add(sender)
# Notify the controller
logger.debug(f'connected to peripheral {self.pending_connection.peer_address}')
self.controller.on_link_peripheral_connection_complete(
self.pending_connection, HCI_SUCCESS
)
async def on_disconnect_message_received(self, sender, message):
# Notify the controller
params = parse_parameters(message)
reason = int(params.get('reason', str(HCI_CONNECTION_TIMEOUT_ERROR)))
self.controller.on_link_central_disconnected(Address(sender), reason)
# Forget the connection
if sender in self.peripheral_connections:
self.peripheral_connections.remove(sender)
async def on_encrypted_message_received(self, sender, _):
# TODO parse params to get real args
self.controller.on_link_encrypted(Address(sender), bytes(8), 0, bytes(16))
async def send_rpc_command(self, command):
# Ensure we have a connection
websocket = await self.websocket
# Create a future value to hold the eventual result
assert self.rpc_result is None
self.rpc_result = asyncio.get_running_loop().create_future()
# Send the command
await websocket.send(command)
# Wait for the result
rpc_result = await self.rpc_result
self.rpc_result = None
logger.debug(f'rpc_result: {rpc_result}')
# TODO: parse the result
async def send_targeted_message(self, target, message):
# Ensure we have a connection
websocket = await self.websocket
# Send the message
await websocket.send(f'@{target} {message}')
async def notify_address_changed(self):
await self.send_rpc_command(f'/set-address {self.controller.random_address}')
def on_address_changed(self, controller):
logger.info(f'address changed for {controller}: {controller.random_address}')
# Notify the relay of the change
self.execute(self.notify_address_changed)
async def send_advertising_data_to_relay(self, data):
await self.send_targeted_message('*', f'advertisement:{data.hex()}')
def send_advertising_data(self, _, data):
self.execute(partial(self.send_advertising_data_to_relay, data))
async def send_acl_data_to_relay(self, peer_address, data):
await self.send_targeted_message(peer_address, f'acl:{data.hex()}')
def send_acl_data(self, _, peer_address, _transport, data):
# TODO: handle different transport
self.execute(partial(self.send_acl_data_to_relay, peer_address, data))
async def send_connection_request_to_relay(self, peer_address):
await self.send_targeted_message(peer_address, 'connect')
def connect(self, _, le_create_connection_command):
if self.pending_connection:
logger.warning('connection already pending')
return
self.pending_connection = le_create_connection_command
self.execute(
partial(
self.send_connection_request_to_relay,
str(le_create_connection_command.peer_address),
)
)
def on_disconnection_complete(self, disconnect_command):
self.controller.on_link_peripheral_disconnection_complete(
disconnect_command, HCI_SUCCESS
)
def disconnect(self, central_address, peripheral_address, disconnect_command):
logger.debug(
f'disconnect {central_address} -> '
f'{peripheral_address}: reason = {disconnect_command.reason}'
)
self.execute(
partial(
self.send_targeted_message,
peripheral_address,
f'disconnect:reason={disconnect_command.reason}',
)
)
asyncio.get_running_loop().call_soon(
self.on_disconnection_complete, disconnect_command
)
def on_connection_encrypted(self, _, peripheral_address, rand, ediv, ltk):
asyncio.get_running_loop().call_soon(
self.controller.on_link_encrypted, peripheral_address, rand, ediv, ltk
)
self.execute(
partial(
self.send_targeted_message,
peripheral_address,
f'encrypted:ltk={ltk.hex()}',
)
)
+19 -22
View File
@@ -18,10 +18,15 @@
from __future__ import annotations from __future__ import annotations
import enum import enum
from dataclasses import dataclass from dataclasses import dataclass
import secrets from typing import Optional, Tuple
from typing import Optional
from bumble import hci from bumble.hci import (
Address,
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
HCI_DISPLAY_ONLY_IO_CAPABILITY,
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
HCI_KEYBOARD_ONLY_IO_CAPABILITY,
)
from bumble.smp import ( from bumble.smp import (
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY, SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
SMP_KEYBOARD_ONLY_IO_CAPABILITY, SMP_KEYBOARD_ONLY_IO_CAPABILITY,
@@ -44,7 +49,7 @@ from bumble.core import AdvertisingData, LeRole
class OobData: class OobData:
"""OOB data that can be sent from one device to another.""" """OOB data that can be sent from one device to another."""
address: Optional[hci.Address] = None address: Optional[Address] = None
role: Optional[LeRole] = None role: Optional[LeRole] = None
shared_data: Optional[OobSharedData] = None shared_data: Optional[OobSharedData] = None
legacy_context: Optional[OobLegacyContext] = None legacy_context: Optional[OobLegacyContext] = None
@@ -56,7 +61,7 @@ class OobData:
shared_data_r: Optional[bytes] = None shared_data_r: Optional[bytes] = None
for ad_type, ad_data in ad.ad_structures: for ad_type, ad_data in ad.ad_structures:
if ad_type == AdvertisingData.LE_BLUETOOTH_DEVICE_ADDRESS: if ad_type == AdvertisingData.LE_BLUETOOTH_DEVICE_ADDRESS:
instance.address = hci.Address(ad_data) instance.address = Address(ad_data)
elif ad_type == AdvertisingData.LE_ROLE: elif ad_type == AdvertisingData.LE_ROLE:
instance.role = LeRole(ad_data[0]) instance.role = LeRole(ad_data[0])
elif ad_type == AdvertisingData.LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE: elif ad_type == AdvertisingData.LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE:
@@ -124,11 +129,11 @@ class PairingDelegate:
# Default mapping from abstract to Classic I/O capabilities. # Default mapping from abstract to Classic I/O capabilities.
# Subclasses may override this if they prefer a different mapping. # Subclasses may override this if they prefer a different mapping.
CLASSIC_IO_CAPABILITIES_MAP = { CLASSIC_IO_CAPABILITIES_MAP = {
NO_OUTPUT_NO_INPUT: hci.IoCapability.NO_INPUT_NO_OUTPUT, NO_OUTPUT_NO_INPUT: HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
KEYBOARD_INPUT_ONLY: hci.IoCapability.KEYBOARD_ONLY, KEYBOARD_INPUT_ONLY: HCI_KEYBOARD_ONLY_IO_CAPABILITY,
DISPLAY_OUTPUT_ONLY: hci.IoCapability.DISPLAY_ONLY, DISPLAY_OUTPUT_ONLY: HCI_DISPLAY_ONLY_IO_CAPABILITY,
DISPLAY_OUTPUT_AND_YES_NO_INPUT: hci.IoCapability.DISPLAY_YES_NO, DISPLAY_OUTPUT_AND_YES_NO_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT: hci.IoCapability.DISPLAY_YES_NO, DISPLAY_OUTPUT_AND_KEYBOARD_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
} }
io_capability: IoCapability io_capability: IoCapability
@@ -154,7 +159,7 @@ class PairingDelegate:
# pylint: disable=line-too-long # pylint: disable=line-too-long
return self.CLASSIC_IO_CAPABILITIES_MAP.get( return self.CLASSIC_IO_CAPABILITIES_MAP.get(
self.io_capability, hci.IoCapability.NO_INPUT_NO_OUTPUT self.io_capability, HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
) )
@property @property
@@ -200,7 +205,7 @@ class PairingDelegate:
# [LE only] # [LE only]
async def key_distribution_response( async def key_distribution_response(
self, peer_initiator_key_distribution: int, peer_responder_key_distribution: int self, peer_initiator_key_distribution: int, peer_responder_key_distribution: int
) -> tuple[int, int]: ) -> Tuple[int, int]:
""" """
Return the key distribution response in an SMP protocol context. Return the key distribution response in an SMP protocol context.
@@ -217,22 +222,14 @@ class PairingDelegate:
), ),
) )
async def generate_passkey(self) -> int:
"""
Return a passkey value between 0 and 999999 (inclusive).
"""
# By default, generate a random passkey.
return secrets.randbelow(1000000)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class PairingConfig: class PairingConfig:
"""Configuration for the Pairing protocol.""" """Configuration for the Pairing protocol."""
class AddressType(enum.IntEnum): class AddressType(enum.IntEnum):
PUBLIC = hci.Address.PUBLIC_DEVICE_ADDRESS PUBLIC = Address.PUBLIC_DEVICE_ADDRESS
RANDOM = hci.Address.RANDOM_DEVICE_ADDRESS RANDOM = Address.RANDOM_DEVICE_ADDRESS
@dataclass @dataclass
class OobConfig: class OobConfig:
+1 -1
View File
@@ -45,7 +45,7 @@ __all__ = [
# Add servicers hooks. # Add servicers hooks.
_SERVICERS_HOOKS: list[Callable[[PandoraDevice, Config, grpc.aio.Server], None]] = [] _SERVICERS_HOOKS: List[Callable[[PandoraDevice, Config, grpc.aio.Server], None]] = []
def register_servicer_hook( def register_servicer_hook(
+2 -2
View File
@@ -15,7 +15,7 @@
from __future__ import annotations from __future__ import annotations
from bumble.pairing import PairingConfig, PairingDelegate from bumble.pairing import PairingConfig, PairingDelegate
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any, Dict
@dataclass @dataclass
@@ -32,7 +32,7 @@ class Config:
PairingDelegate.DEFAULT_KEY_DISTRIBUTION PairingDelegate.DEFAULT_KEY_DISTRIBUTION
) )
def load_from_dict(self, config: dict[str, Any]) -> None: def load_from_dict(self, config: Dict[str, Any]) -> None:
io_capability_name: str = config.get( io_capability_name: str = config.get(
'io_capability', 'no_output_no_input' 'io_capability', 'no_output_no_input'
).upper() ).upper()
+6 -6
View File
@@ -32,7 +32,7 @@ from bumble.sdp import (
DataElement, DataElement,
ServiceAttribute, ServiceAttribute,
) )
from typing import Any, Optional from typing import Any, Dict, List, Optional
# Default rootcanal HCI TCP address # Default rootcanal HCI TCP address
@@ -49,13 +49,13 @@ class PandoraDevice:
# Bumble device instance & configuration. # Bumble device instance & configuration.
device: Device device: Device
config: dict[str, Any] config: Dict[str, Any]
# HCI transport name & instance. # HCI transport name & instance.
_hci_name: str _hci_name: str
_hci: Optional[transport.Transport] # type: ignore[name-defined] _hci: Optional[transport.Transport] # type: ignore[name-defined]
def __init__(self, config: dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
self.config = config self.config = config
self.device = _make_device(config) self.device = _make_device(config)
self._hci_name = config.get( self._hci_name = config.get(
@@ -95,14 +95,14 @@ class PandoraDevice:
await self.close() await self.close()
await self.open() await self.open()
def info(self) -> Optional[dict[str, str]]: def info(self) -> Optional[Dict[str, str]]:
return { return {
'public_bd_address': str(self.device.public_address), 'public_bd_address': str(self.device.public_address),
'random_address': str(self.device.random_address), 'random_address': str(self.device.random_address),
} }
def _make_device(config: dict[str, Any]) -> Device: def _make_device(config: Dict[str, Any]) -> Device:
"""Initialize an idle Bumble device instance.""" """Initialize an idle Bumble device instance."""
# initialize bumble device. # initialize bumble device.
@@ -117,7 +117,7 @@ def _make_device(config: dict[str, Any]) -> Device:
# TODO(b/267540823): remove when Pandora A2dp is supported # TODO(b/267540823): remove when Pandora A2dp is supported
def _make_sdp_records(rfcomm_channel: int) -> dict[int, list[ServiceAttribute]]: def _make_sdp_records(rfcomm_channel: int) -> Dict[int, List[ServiceAttribute]]:
return { return {
0x00010001: [ 0x00010001: [
ServiceAttribute( ServiceAttribute(
+28 -27
View File
@@ -73,6 +73,7 @@ from pandora.host_pb2 import (
ConnectResponse, ConnectResponse,
DataTypes, DataTypes,
DisconnectRequest, DisconnectRequest,
DiscoverabilityMode,
InquiryResponse, InquiryResponse,
PrimaryPhy, PrimaryPhy,
ReadLocalAddressResponse, ReadLocalAddressResponse,
@@ -85,9 +86,9 @@ from pandora.host_pb2 import (
WaitConnectionResponse, WaitConnectionResponse,
WaitDisconnectionRequest, WaitDisconnectionRequest,
) )
from typing import AsyncGenerator, Optional, cast from typing import AsyncGenerator, Dict, List, Optional, Set, Tuple, cast
PRIMARY_PHY_MAP: dict[int, PrimaryPhy] = { PRIMARY_PHY_MAP: Dict[int, PrimaryPhy] = {
# Default value reported by Bumble for legacy Advertising reports. # Default value reported by Bumble for legacy Advertising reports.
# FIXME(uael): `None` might be a better value, but Bumble need to change accordingly. # FIXME(uael): `None` might be a better value, but Bumble need to change accordingly.
0: PRIMARY_1M, 0: PRIMARY_1M,
@@ -95,26 +96,26 @@ PRIMARY_PHY_MAP: dict[int, PrimaryPhy] = {
3: PRIMARY_CODED, 3: PRIMARY_CODED,
} }
SECONDARY_PHY_MAP: dict[int, SecondaryPhy] = { SECONDARY_PHY_MAP: Dict[int, SecondaryPhy] = {
0: SECONDARY_NONE, 0: SECONDARY_NONE,
1: SECONDARY_1M, 1: SECONDARY_1M,
2: SECONDARY_2M, 2: SECONDARY_2M,
3: SECONDARY_CODED, 3: SECONDARY_CODED,
} }
PRIMARY_PHY_TO_BUMBLE_PHY_MAP: dict[PrimaryPhy, Phy] = { PRIMARY_PHY_TO_BUMBLE_PHY_MAP: Dict[PrimaryPhy, Phy] = {
PRIMARY_1M: Phy.LE_1M, PRIMARY_1M: Phy.LE_1M,
PRIMARY_CODED: Phy.LE_CODED, PRIMARY_CODED: Phy.LE_CODED,
} }
SECONDARY_PHY_TO_BUMBLE_PHY_MAP: dict[SecondaryPhy, Phy] = { SECONDARY_PHY_TO_BUMBLE_PHY_MAP: Dict[SecondaryPhy, Phy] = {
SECONDARY_NONE: Phy.LE_1M, SECONDARY_NONE: Phy.LE_1M,
SECONDARY_1M: Phy.LE_1M, SECONDARY_1M: Phy.LE_1M,
SECONDARY_2M: Phy.LE_2M, SECONDARY_2M: Phy.LE_2M,
SECONDARY_CODED: Phy.LE_CODED, SECONDARY_CODED: Phy.LE_CODED,
} }
OWN_ADDRESS_MAP: dict[host_pb2.OwnAddressType, OwnAddressType] = { OWN_ADDRESS_MAP: Dict[host_pb2.OwnAddressType, OwnAddressType] = {
host_pb2.PUBLIC: OwnAddressType.PUBLIC, host_pb2.PUBLIC: OwnAddressType.PUBLIC,
host_pb2.RANDOM: OwnAddressType.RANDOM, host_pb2.RANDOM: OwnAddressType.RANDOM,
host_pb2.RESOLVABLE_OR_PUBLIC: OwnAddressType.RESOLVABLE_OR_PUBLIC, host_pb2.RESOLVABLE_OR_PUBLIC: OwnAddressType.RESOLVABLE_OR_PUBLIC,
@@ -123,7 +124,7 @@ OWN_ADDRESS_MAP: dict[host_pb2.OwnAddressType, OwnAddressType] = {
class HostService(HostServicer): class HostService(HostServicer):
waited_connections: set[int] waited_connections: Set[int]
def __init__( def __init__(
self, grpc_server: grpc.aio.Server, device: Device, config: Config self, grpc_server: grpc.aio.Server, device: Device, config: Config
@@ -617,7 +618,7 @@ class HostService(HostServicer):
self.log.debug('Inquiry') self.log.debug('Inquiry')
inquiry_queue: asyncio.Queue[ inquiry_queue: asyncio.Queue[
Optional[tuple[Address, int, AdvertisingData, int]] Optional[Tuple[Address, int, AdvertisingData, int]]
] = asyncio.Queue() ] = asyncio.Queue()
complete_handler = self.device.on( complete_handler = self.device.on(
self.device.EVENT_INQUIRY_COMPLETE, lambda: inquiry_queue.put_nowait(None) self.device.EVENT_INQUIRY_COMPLETE, lambda: inquiry_queue.put_nowait(None)
@@ -669,10 +670,10 @@ class HostService(HostServicer):
return empty_pb2.Empty() return empty_pb2.Empty()
def unpack_data_types(self, dt: DataTypes) -> AdvertisingData: def unpack_data_types(self, dt: DataTypes) -> AdvertisingData:
ad_structures: list[tuple[int, bytes]] = [] ad_structures: List[Tuple[int, bytes]] = []
uuids: list[str] uuids: List[str]
datas: dict[str, bytes] datas: Dict[str, bytes]
def uuid128_from_str(uuid: str) -> bytes: def uuid128_from_str(uuid: str) -> bytes:
"""Decode a 128-bit uuid encoded as XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX """Decode a 128-bit uuid encoded as XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
@@ -886,50 +887,50 @@ class HostService(HostServicer):
def pack_data_types(self, ad: AdvertisingData) -> DataTypes: def pack_data_types(self, ad: AdvertisingData) -> DataTypes:
dt = DataTypes() dt = DataTypes()
uuids: list[UUID] uuids: List[UUID]
s: str s: str
i: int i: int
ij: tuple[int, int] ij: Tuple[int, int]
uuid_data: tuple[UUID, bytes] uuid_data: Tuple[UUID, bytes]
data: bytes data: bytes
if uuids := cast( if uuids := cast(
list[UUID], List[UUID],
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS), ad.get(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS),
): ):
dt.incomplete_service_class_uuids16.extend( dt.incomplete_service_class_uuids16.extend(
list(map(lambda x: x.to_hex_str('-'), uuids)) list(map(lambda x: x.to_hex_str('-'), uuids))
) )
if uuids := cast( if uuids := cast(
list[UUID], List[UUID],
ad.get(AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS), ad.get(AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS),
): ):
dt.complete_service_class_uuids16.extend( dt.complete_service_class_uuids16.extend(
list(map(lambda x: x.to_hex_str('-'), uuids)) list(map(lambda x: x.to_hex_str('-'), uuids))
) )
if uuids := cast( if uuids := cast(
list[UUID], List[UUID],
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS), ad.get(AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS),
): ):
dt.incomplete_service_class_uuids32.extend( dt.incomplete_service_class_uuids32.extend(
list(map(lambda x: x.to_hex_str('-'), uuids)) list(map(lambda x: x.to_hex_str('-'), uuids))
) )
if uuids := cast( if uuids := cast(
list[UUID], List[UUID],
ad.get(AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS), ad.get(AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS),
): ):
dt.complete_service_class_uuids32.extend( dt.complete_service_class_uuids32.extend(
list(map(lambda x: x.to_hex_str('-'), uuids)) list(map(lambda x: x.to_hex_str('-'), uuids))
) )
if uuids := cast( if uuids := cast(
list[UUID], List[UUID],
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS), ad.get(AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS),
): ):
dt.incomplete_service_class_uuids128.extend( dt.incomplete_service_class_uuids128.extend(
list(map(lambda x: x.to_hex_str('-'), uuids)) list(map(lambda x: x.to_hex_str('-'), uuids))
) )
if uuids := cast( if uuids := cast(
list[UUID], List[UUID],
ad.get(AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS), ad.get(AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS),
): ):
dt.complete_service_class_uuids128.extend( dt.complete_service_class_uuids128.extend(
@@ -944,42 +945,42 @@ class HostService(HostServicer):
if i := cast(int, ad.get(AdvertisingData.CLASS_OF_DEVICE)): if i := cast(int, ad.get(AdvertisingData.CLASS_OF_DEVICE)):
dt.class_of_device = i dt.class_of_device = i
if ij := cast( if ij := cast(
tuple[int, int], Tuple[int, int],
ad.get(AdvertisingData.PERIPHERAL_CONNECTION_INTERVAL_RANGE), ad.get(AdvertisingData.PERIPHERAL_CONNECTION_INTERVAL_RANGE),
): ):
dt.peripheral_connection_interval_min = ij[0] dt.peripheral_connection_interval_min = ij[0]
dt.peripheral_connection_interval_max = ij[1] dt.peripheral_connection_interval_max = ij[1]
if uuids := cast( if uuids := cast(
list[UUID], List[UUID],
ad.get(AdvertisingData.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS), ad.get(AdvertisingData.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS),
): ):
dt.service_solicitation_uuids16.extend( dt.service_solicitation_uuids16.extend(
list(map(lambda x: x.to_hex_str('-'), uuids)) list(map(lambda x: x.to_hex_str('-'), uuids))
) )
if uuids := cast( if uuids := cast(
list[UUID], List[UUID],
ad.get(AdvertisingData.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS), ad.get(AdvertisingData.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS),
): ):
dt.service_solicitation_uuids32.extend( dt.service_solicitation_uuids32.extend(
list(map(lambda x: x.to_hex_str('-'), uuids)) list(map(lambda x: x.to_hex_str('-'), uuids))
) )
if uuids := cast( if uuids := cast(
list[UUID], List[UUID],
ad.get(AdvertisingData.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS), ad.get(AdvertisingData.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS),
): ):
dt.service_solicitation_uuids128.extend( dt.service_solicitation_uuids128.extend(
list(map(lambda x: x.to_hex_str('-'), uuids)) list(map(lambda x: x.to_hex_str('-'), uuids))
) )
if uuid_data := cast( if uuid_data := cast(
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_16_BIT_UUID) Tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_16_BIT_UUID)
): ):
dt.service_data_uuid16[uuid_data[0].to_hex_str('-')] = uuid_data[1] dt.service_data_uuid16[uuid_data[0].to_hex_str('-')] = uuid_data[1]
if uuid_data := cast( if uuid_data := cast(
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_32_BIT_UUID) Tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_32_BIT_UUID)
): ):
dt.service_data_uuid32[uuid_data[0].to_hex_str('-')] = uuid_data[1] dt.service_data_uuid32[uuid_data[0].to_hex_str('-')] = uuid_data[1]
if uuid_data := cast( if uuid_data := cast(
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_128_BIT_UUID) Tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_128_BIT_UUID)
): ):
dt.service_data_uuid128[uuid_data[0].to_hex_str('-')] = uuid_data[1] dt.service_data_uuid128[uuid_data[0].to_hex_str('-')] = uuid_data[1]
if data := cast(bytes, ad.get(AdvertisingData.PUBLIC_TARGET_ADDRESS, raw=True)): if data := cast(bytes, ad.get(AdvertisingData.PUBLIC_TARGET_ADDRESS, raw=True)):
+2 -2
View File
@@ -51,7 +51,7 @@ from pandora.l2cap_pb2 import ( # pytype: disable=pyi-error
WaitDisconnectionRequest, WaitDisconnectionRequest,
WaitDisconnectionResponse, WaitDisconnectionResponse,
) )
from typing import AsyncGenerator, Optional, Union from typing import AsyncGenerator, Dict, Optional, Union
from dataclasses import dataclass from dataclasses import dataclass
L2capChannel = Union[ClassicChannel, LeCreditBasedChannel] L2capChannel = Union[ClassicChannel, LeCreditBasedChannel]
@@ -70,7 +70,7 @@ class L2CAPService(L2CAPServicer):
) )
self.device = device self.device = device
self.config = config self.config = config
self.channels: dict[bytes, ChannelContext] = {} self.channels: Dict[bytes, ChannelContext] = {}
def register_event(self, l2cap_channel: L2capChannel) -> ChannelContext: def register_event(self, l2cap_channel: L2capChannel) -> ChannelContext:
close_future = asyncio.get_running_loop().create_future() close_future = asyncio.get_running_loop().create_future()
+6 -6
View File
@@ -57,7 +57,7 @@ from pandora.security_pb2 import (
WaitSecurityRequest, WaitSecurityRequest,
WaitSecurityResponse, WaitSecurityResponse,
) )
from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Union from typing import Any, AsyncGenerator, AsyncIterator, Callable, Dict, Optional, Union
class PairingDelegate(BasePairingDelegate): class PairingDelegate(BasePairingDelegate):
@@ -244,16 +244,16 @@ class SecurityService(SecurityServicer):
and connection.authenticated and connection.authenticated
and link_key_type and link_key_type
in ( in (
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192, hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256, hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
) )
) )
if level == LEVEL4: if level == LEVEL4:
return ( return (
connection.encryption == hci.HCI_Encryption_Change_Event.Enabled.AES_CCM connection.encryption == hci.HCI_Encryption_Change_Event.AES_CCM
and connection.authenticated and connection.authenticated
and link_key_type and link_key_type
== hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256 == hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE
) )
raise InvalidArgumentError(f"Unexpected level {level}") raise InvalidArgumentError(f"Unexpected level {level}")
@@ -457,7 +457,7 @@ class SecurityService(SecurityServicer):
if self.need_pairing(connection, level): if self.need_pairing(connection, level):
pair_task = asyncio.create_task(connection.pair()) pair_task = asyncio.create_task(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'),
'pairing_failure': set_failure('pairing_failure'), 'pairing_failure': set_failure('pairing_failure'),
'connection_authentication_failure': set_failure('authentication_failure'), 'connection_authentication_failure': set_failure('authentication_failure'),
+3 -3
View File
@@ -22,9 +22,9 @@ import logging
from bumble.device import Device from bumble.device import Device
from bumble.hci import Address, AddressType from bumble.hci import Address, AddressType
from google.protobuf.message import Message # pytype: disable=pyi-error from google.protobuf.message import Message # pytype: disable=pyi-error
from typing import Any, Generator, MutableMapping, Optional from typing import Any, Dict, Generator, MutableMapping, Optional, Tuple
ADDRESS_TYPES: dict[str, AddressType] = { ADDRESS_TYPES: Dict[str, AddressType] = {
"public": Address.PUBLIC_DEVICE_ADDRESS, "public": Address.PUBLIC_DEVICE_ADDRESS,
"random": Address.RANDOM_DEVICE_ADDRESS, "random": Address.RANDOM_DEVICE_ADDRESS,
"public_identity": Address.PUBLIC_IDENTITY_ADDRESS, "public_identity": Address.PUBLIC_IDENTITY_ADDRESS,
@@ -43,7 +43,7 @@ class BumbleServerLoggerAdapter(logging.LoggerAdapter): # type: ignore
def process( def process(
self, msg: str, kwargs: MutableMapping[str, Any] self, msg: str, kwargs: MutableMapping[str, Any]
) -> tuple[str, MutableMapping[str, Any]]: ) -> Tuple[str, MutableMapping[str, Any]]:
assert self.extra assert self.extra
service_name = self.extra['service_name'] service_name = self.extra['service_name']
assert isinstance(service_name, str) assert isinstance(service_name, str)
+5 -3
View File
@@ -198,7 +198,8 @@ class AudioInputControlPoint:
audio_input_state: AudioInputState audio_input_state: AudioInputState
gain_settings_properties: GainSettingsProperties gain_settings_properties: GainSettingsProperties
async def on_write(self, connection: Connection, value: bytes) -> None: async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
assert connection
opcode = AudioInputControlPointOpCode(value[0]) opcode = AudioInputControlPointOpCode(value[0])
@@ -319,10 +320,11 @@ class AudioInputDescription:
audio_input_description: str = "Bluetooth" audio_input_description: str = "Bluetooth"
attribute: Optional[Attribute] = None attribute: Optional[Attribute] = None
def on_read(self, _connection: Connection) -> str: def on_read(self, _connection: Optional[Connection]) -> str:
return self.audio_input_description return self.audio_input_description
async def on_write(self, connection: Connection, value: str) -> None: async def on_write(self, connection: Optional[Connection], value: str) -> None:
assert connection
assert self.attribute assert self.attribute
self.audio_input_description = value self.audio_input_description = value
+128 -129
View File
@@ -18,13 +18,10 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field
import enum import enum
import functools
import logging import logging
import struct import struct
from typing import Any, Optional, Union, TypeVar from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
from collections.abc import Sequence
from bumble import utils from bumble import utils
from bumble import colors from bumble import colors
@@ -51,11 +48,11 @@ class ASE_Operation:
See Audio Stream Control Service - 5 ASE Control operations. See Audio Stream Control Service - 5 ASE Control operations.
''' '''
classes: dict[int, type[ASE_Operation]] = {} classes: Dict[int, Type[ASE_Operation]] = {}
op_code: Opcode op_code: int
name: str name: str
fields: Optional[Sequence[Any]] = None fields: Optional[Sequence[Any]] = None
ase_id: Sequence[int] ase_id: List[int]
class Opcode(enum.IntEnum): class Opcode(enum.IntEnum):
# fmt: off # fmt: off
@@ -68,30 +65,51 @@ class ASE_Operation:
UPDATE_METADATA = 0x07 UPDATE_METADATA = 0x07
RELEASE = 0x08 RELEASE = 0x08
@classmethod @staticmethod
def from_bytes(cls, pdu: bytes) -> ASE_Operation: def from_bytes(pdu: bytes) -> ASE_Operation:
op_code = pdu[0] op_code = pdu[0]
clazz = ASE_Operation.classes[op_code] cls = ASE_Operation.classes.get(op_code)
return clazz( if cls is None:
**hci.HCI_Object.dict_from_bytes(pdu, offset=1, fields=clazz.fields) instance = ASE_Operation(pdu)
) instance.name = ASE_Operation.Opcode(op_code).name
instance.op_code = op_code
return instance
self = cls.__new__(cls)
ASE_Operation.__init__(self, pdu)
if self.fields is not None:
self.init_from_bytes(pdu, 1)
return self
_OP = TypeVar("_OP", bound="ASE_Operation") @staticmethod
def subclass(fields):
def inner(cls: Type[ASE_Operation]):
try:
operation = ASE_Operation.Opcode[cls.__name__[4:].upper()]
cls.name = operation.name
cls.op_code = operation
except:
raise KeyError(f'PDU name {cls.name} not found in Ase_Operation.Opcode')
cls.fields = fields
@classmethod # Register a factory for this class
def subclass(cls, clazz: type[_OP]) -> type[_OP]: ASE_Operation.classes[cls.op_code] = cls
clazz.name = f"ASE_{clazz.op_code.name.upper()}"
clazz.fields = hci.HCI_Object.fields_from_dataclass(clazz)
# Register a factory for this class
ASE_Operation.classes[clazz.op_code] = clazz
return clazz
@functools.cached_property return cls
def pdu(self) -> bytes:
return bytes([self.op_code]) + hci.HCI_Object.dict_to_bytes( return inner
self.__dict__, self.fields
) def __init__(self, pdu: Optional[bytes] = None, **kwargs) -> None:
if self.fields is not None and kwargs:
hci.HCI_Object.init_from_fields(self, self.fields, kwargs)
if pdu is None:
pdu = bytes([self.op_code]) + hci.HCI_Object.dict_to_bytes(
kwargs, self.fields
)
self.pdu = pdu
def init_from_bytes(self, pdu: bytes, offset: int):
return hci.HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
return self.pdu return self.pdu
@@ -106,128 +124,105 @@ class ASE_Operation:
return result return result
@ASE_Operation.subclass @ASE_Operation.subclass(
@dataclass [
[
('ase_id', 1),
('target_latency', 1),
('target_phy', 1),
('codec_id', hci.CodingFormat.parse_from_bytes),
('codec_specific_configuration', 'v'),
],
]
)
class ASE_Config_Codec(ASE_Operation): class ASE_Config_Codec(ASE_Operation):
''' '''
See Audio Stream Control Service 5.1 - Config Codec Operation See Audio Stream Control Service 5.1 - Config Codec Operation
''' '''
op_code = ASE_Operation.Opcode.CONFIG_CODEC target_latency: List[int]
target_phy: List[int]
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True)) codec_id: List[hci.CodingFormat]
target_latency: Sequence[int] = field(metadata=hci.metadata(1)) codec_specific_configuration: List[bytes]
target_phy: Sequence[int] = field(metadata=hci.metadata(1))
codec_id: Sequence[hci.CodingFormat] = field(
metadata=hci.metadata(hci.CodingFormat.parse_from_bytes)
)
codec_specific_configuration: Sequence[bytes] = field(
metadata=hci.metadata('v', list_end=True)
)
@ASE_Operation.subclass @ASE_Operation.subclass(
@dataclass [
[
('ase_id', 1),
('cig_id', 1),
('cis_id', 1),
('sdu_interval', 3),
('framing', 1),
('phy', 1),
('max_sdu', 2),
('retransmission_number', 1),
('max_transport_latency', 2),
('presentation_delay', 3),
],
]
)
class ASE_Config_QOS(ASE_Operation): class ASE_Config_QOS(ASE_Operation):
''' '''
See Audio Stream Control Service 5.2 - Config Qos Operation See Audio Stream Control Service 5.2 - Config Qos Operation
''' '''
op_code = ASE_Operation.Opcode.CONFIG_QOS cig_id: List[int]
cis_id: List[int]
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True)) sdu_interval: List[int]
cig_id: Sequence[int] = field(metadata=hci.metadata(1)) framing: List[int]
cis_id: Sequence[int] = field(metadata=hci.metadata(1)) phy: List[int]
sdu_interval: Sequence[int] = field(metadata=hci.metadata(3)) max_sdu: List[int]
framing: Sequence[int] = field(metadata=hci.metadata(1)) retransmission_number: List[int]
phy: Sequence[int] = field(metadata=hci.metadata(1)) max_transport_latency: List[int]
max_sdu: Sequence[int] = field(metadata=hci.metadata(2)) presentation_delay: List[int]
retransmission_number: Sequence[int] = field(metadata=hci.metadata(1))
max_transport_latency: Sequence[int] = field(metadata=hci.metadata(2))
presentation_delay: Sequence[int] = field(metadata=hci.metadata(3, list_end=True))
@ASE_Operation.subclass @ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
@dataclass
class ASE_Enable(ASE_Operation): class ASE_Enable(ASE_Operation):
''' '''
See Audio Stream Control Service 5.3 - Enable Operation See Audio Stream Control Service 5.3 - Enable Operation
''' '''
op_code = ASE_Operation.Opcode.ENABLE metadata: bytes
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
metadata: Sequence[bytes] = field(metadata=hci.metadata('v', list_end=True))
@ASE_Operation.subclass @ASE_Operation.subclass([[('ase_id', 1)]])
@dataclass
class ASE_Receiver_Start_Ready(ASE_Operation): class ASE_Receiver_Start_Ready(ASE_Operation):
''' '''
See Audio Stream Control Service 5.4 - Receiver Start Ready Operation See Audio Stream Control Service 5.4 - Receiver Start Ready Operation
''' '''
op_code = ASE_Operation.Opcode.RECEIVER_START_READY
ase_id: Sequence[int] = field( @ASE_Operation.subclass([[('ase_id', 1)]])
metadata=hci.metadata(1, list_begin=True, list_end=True)
)
@ASE_Operation.subclass
@dataclass
class ASE_Disable(ASE_Operation): class ASE_Disable(ASE_Operation):
''' '''
See Audio Stream Control Service 5.5 - Disable Operation See Audio Stream Control Service 5.5 - Disable Operation
''' '''
op_code = ASE_Operation.Opcode.DISABLE
ase_id: Sequence[int] = field( @ASE_Operation.subclass([[('ase_id', 1)]])
metadata=hci.metadata(1, list_begin=True, list_end=True)
)
@ASE_Operation.subclass
@dataclass
class ASE_Receiver_Stop_Ready(ASE_Operation): class ASE_Receiver_Stop_Ready(ASE_Operation):
''' '''
See Audio Stream Control Service 5.6 - Receiver Stop Ready Operation See Audio Stream Control Service 5.6 - Receiver Stop Ready Operation
''' '''
op_code = ASE_Operation.Opcode.RECEIVER_STOP_READY
ase_id: Sequence[int] = field( @ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
metadata=hci.metadata(1, list_begin=True, list_end=True)
)
@ASE_Operation.subclass
@dataclass
class ASE_Update_Metadata(ASE_Operation): class ASE_Update_Metadata(ASE_Operation):
''' '''
See Audio Stream Control Service 5.7 - Update Metadata Operation See Audio Stream Control Service 5.7 - Update Metadata Operation
''' '''
op_code = ASE_Operation.Opcode.UPDATE_METADATA metadata: List[bytes]
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
metadata: Sequence[bytes] = field(metadata=hci.metadata('v', list_end=True))
@ASE_Operation.subclass @ASE_Operation.subclass([[('ase_id', 1)]])
@dataclass
class ASE_Release(ASE_Operation): class ASE_Release(ASE_Operation):
''' '''
See Audio Stream Control Service 5.8 - Release Operation See Audio Stream Control Service 5.8 - Release Operation
''' '''
op_code = ASE_Operation.Opcode.RELEASE
ase_id: Sequence[int] = field(
metadata=hci.metadata(1, list_begin=True, list_end=True)
)
class AseResponseCode(enum.IntEnum): class AseResponseCode(enum.IntEnum):
# fmt: off # fmt: off
@@ -343,16 +338,22 @@ class AseStateMachine(gatt.Characteristic):
self.service.device.EVENT_CIS_ESTABLISHMENT, self.on_cis_establishment self.service.device.EVENT_CIS_ESTABLISHMENT, self.on_cis_establishment
) )
def on_cis_request(self, cis_link: device.CisLink) -> None: def on_cis_request(
self,
acl_connection: device.Connection,
cis_handle: int,
cig_id: int,
cis_id: int,
) -> None:
if ( if (
cis_link.cig_id == self.cig_id cig_id == self.cig_id
and cis_link.cis_id == self.cis_id and cis_id == self.cis_id
and self.state == self.State.ENABLING and self.state == self.State.ENABLING
): ):
utils.cancel_on_event( utils.cancel_on_event(
cis_link.acl_connection, acl_connection,
'flush', 'flush',
self.service.device.accept_cis_request(cis_link), self.service.device.accept_cis_request(cis_handle),
) )
def on_cis_establishment(self, cis_link: device.CisLink) -> None: def on_cis_establishment(self, cis_link: device.CisLink) -> None:
@@ -383,7 +384,7 @@ class AseStateMachine(gatt.Characteristic):
target_phy: int, target_phy: int,
codec_id: hci.CodingFormat, codec_id: hci.CodingFormat,
codec_specific_configuration: bytes, codec_specific_configuration: bytes,
) -> tuple[AseResponseCode, AseReasonCode]: ) -> Tuple[AseResponseCode, AseReasonCode]:
if self.state not in ( if self.state not in (
self.State.IDLE, self.State.IDLE,
self.State.CODEC_CONFIGURED, self.State.CODEC_CONFIGURED,
@@ -419,7 +420,7 @@ class AseStateMachine(gatt.Characteristic):
retransmission_number: int, retransmission_number: int,
max_transport_latency: int, max_transport_latency: int,
presentation_delay: int, presentation_delay: int,
) -> tuple[AseResponseCode, AseReasonCode]: ) -> Tuple[AseResponseCode, AseReasonCode]:
if self.state not in ( if self.state not in (
AseStateMachine.State.CODEC_CONFIGURED, AseStateMachine.State.CODEC_CONFIGURED,
AseStateMachine.State.QOS_CONFIGURED, AseStateMachine.State.QOS_CONFIGURED,
@@ -443,7 +444,7 @@ class AseStateMachine(gatt.Characteristic):
return (AseResponseCode.SUCCESS, AseReasonCode.NONE) return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
def on_enable(self, metadata: bytes) -> tuple[AseResponseCode, AseReasonCode]: def on_enable(self, metadata: bytes) -> Tuple[AseResponseCode, AseReasonCode]:
if self.state != AseStateMachine.State.QOS_CONFIGURED: if self.state != AseStateMachine.State.QOS_CONFIGURED:
return ( return (
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION, AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
@@ -455,7 +456,7 @@ class AseStateMachine(gatt.Characteristic):
return (AseResponseCode.SUCCESS, AseReasonCode.NONE) return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
def on_receiver_start_ready(self) -> tuple[AseResponseCode, AseReasonCode]: def on_receiver_start_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
if self.state != AseStateMachine.State.ENABLING: if self.state != AseStateMachine.State.ENABLING:
return ( return (
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION, AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
@@ -464,7 +465,7 @@ class AseStateMachine(gatt.Characteristic):
self.state = self.State.STREAMING self.state = self.State.STREAMING
return (AseResponseCode.SUCCESS, AseReasonCode.NONE) return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
def on_disable(self) -> tuple[AseResponseCode, AseReasonCode]: def on_disable(self) -> Tuple[AseResponseCode, AseReasonCode]:
if self.state not in ( if self.state not in (
AseStateMachine.State.ENABLING, AseStateMachine.State.ENABLING,
AseStateMachine.State.STREAMING, AseStateMachine.State.STREAMING,
@@ -479,7 +480,7 @@ class AseStateMachine(gatt.Characteristic):
self.state = self.State.DISABLING self.state = self.State.DISABLING
return (AseResponseCode.SUCCESS, AseReasonCode.NONE) return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
def on_receiver_stop_ready(self) -> tuple[AseResponseCode, AseReasonCode]: def on_receiver_stop_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
if ( if (
self.role != AudioRole.SOURCE self.role != AudioRole.SOURCE
or self.state != AseStateMachine.State.DISABLING or self.state != AseStateMachine.State.DISABLING
@@ -493,7 +494,7 @@ class AseStateMachine(gatt.Characteristic):
def on_update_metadata( def on_update_metadata(
self, metadata: bytes self, metadata: bytes
) -> tuple[AseResponseCode, AseReasonCode]: ) -> Tuple[AseResponseCode, AseReasonCode]:
if self.state not in ( if self.state not in (
AseStateMachine.State.ENABLING, AseStateMachine.State.ENABLING,
AseStateMachine.State.STREAMING, AseStateMachine.State.STREAMING,
@@ -505,7 +506,7 @@ class AseStateMachine(gatt.Characteristic):
self.metadata = le_audio.Metadata.from_bytes(metadata) self.metadata = le_audio.Metadata.from_bytes(metadata)
return (AseResponseCode.SUCCESS, AseReasonCode.NONE) return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
def on_release(self) -> tuple[AseResponseCode, AseReasonCode]: def on_release(self) -> Tuple[AseResponseCode, AseReasonCode]:
if self.state == AseStateMachine.State.IDLE: if self.state == AseStateMachine.State.IDLE:
return ( return (
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION, AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
@@ -515,7 +516,7 @@ class AseStateMachine(gatt.Characteristic):
async def remove_cis_async(): async def remove_cis_async():
if self.cis_link: if self.cis_link:
await self.cis_link.remove_data_path([self.role]) await self.cis_link.remove_data_path(self.role)
self.state = self.State.IDLE self.state = self.State.IDLE
await self.service.device.notify_subscribers(self, self.value) await self.service.device.notify_subscribers(self, self.value)
@@ -589,7 +590,7 @@ class AseStateMachine(gatt.Characteristic):
# Readonly. Do nothing in the setter. # Readonly. Do nothing in the setter.
pass pass
def on_read(self, _: device.Connection) -> bytes: def on_read(self, _: Optional[device.Connection]) -> bytes:
return self.value return self.value
def __str__(self) -> str: def __str__(self) -> str:
@@ -603,7 +604,7 @@ class AseStateMachine(gatt.Characteristic):
class AudioStreamControlService(gatt.TemplateService): class AudioStreamControlService(gatt.TemplateService):
UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE
ase_state_machines: dict[int, AseStateMachine] ase_state_machines: Dict[int, AseStateMachine]
ase_control_point: gatt.Characteristic[bytes] ase_control_point: gatt.Characteristic[bytes]
_active_client: Optional[device.Connection] = None _active_client: Optional[device.Connection] = None
@@ -648,9 +649,7 @@ class AudioStreamControlService(gatt.TemplateService):
ase.state = AseStateMachine.State.IDLE ase.state = AseStateMachine.State.IDLE
self._active_client = None self._active_client = None
def on_write_ase_control_point( def on_write_ase_control_point(self, connection, data):
self, connection: device.Connection, data: bytes
) -> None:
if not self._active_client and connection: if not self._active_client and connection:
self._active_client = connection self._active_client = connection
connection.once('disconnection', self._on_client_disconnected) connection.once('disconnection', self._on_client_disconnected)
@@ -659,7 +658,7 @@ class AudioStreamControlService(gatt.TemplateService):
responses = [] responses = []
logger.debug(f'*** ASCS Write {operation} ***') logger.debug(f'*** ASCS Write {operation} ***')
if isinstance(operation, ASE_Config_Codec): if operation.op_code == ASE_Operation.Opcode.CONFIG_CODEC:
for ase_id, *args in zip( for ase_id, *args in zip(
operation.ase_id, operation.ase_id,
operation.target_latency, operation.target_latency,
@@ -668,7 +667,7 @@ class AudioStreamControlService(gatt.TemplateService):
operation.codec_specific_configuration, operation.codec_specific_configuration,
): ):
responses.append(self.on_operation(operation.op_code, ase_id, args)) responses.append(self.on_operation(operation.op_code, ase_id, args))
elif isinstance(operation, ASE_Config_QOS): elif operation.op_code == ASE_Operation.Opcode.CONFIG_QOS:
for ase_id, *args in zip( for ase_id, *args in zip(
operation.ase_id, operation.ase_id,
operation.cig_id, operation.cig_id,
@@ -682,20 +681,20 @@ class AudioStreamControlService(gatt.TemplateService):
operation.presentation_delay, operation.presentation_delay,
): ):
responses.append(self.on_operation(operation.op_code, ase_id, args)) responses.append(self.on_operation(operation.op_code, ase_id, args))
elif isinstance(operation, (ASE_Enable, ASE_Update_Metadata)): elif operation.op_code in (
ASE_Operation.Opcode.ENABLE,
ASE_Operation.Opcode.UPDATE_METADATA,
):
for ase_id, *args in zip( for ase_id, *args in zip(
operation.ase_id, operation.ase_id,
operation.metadata, operation.metadata,
): ):
responses.append(self.on_operation(operation.op_code, ase_id, args)) responses.append(self.on_operation(operation.op_code, ase_id, args))
elif isinstance( elif operation.op_code in (
operation, ASE_Operation.Opcode.RECEIVER_START_READY,
( ASE_Operation.Opcode.DISABLE,
ASE_Receiver_Start_Ready, ASE_Operation.Opcode.RECEIVER_STOP_READY,
ASE_Disable, ASE_Operation.Opcode.RELEASE,
ASE_Receiver_Stop_Ready,
ASE_Release,
),
): ):
for ase_id in operation.ase_id: for ase_id in operation.ase_id:
responses.append(self.on_operation(operation.op_code, ase_id, [])) responses.append(self.on_operation(operation.op_code, ase_id, []))
@@ -724,8 +723,8 @@ class AudioStreamControlService(gatt.TemplateService):
class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy): class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy):
SERVICE_CLASS = AudioStreamControlService SERVICE_CLASS = AudioStreamControlService
sink_ase: list[gatt_client.CharacteristicProxy[bytes]] sink_ase: List[gatt_client.CharacteristicProxy[bytes]]
source_ase: list[gatt_client.CharacteristicProxy[bytes]] source_ase: List[gatt_client.CharacteristicProxy[bytes]]
ase_control_point: gatt_client.CharacteristicProxy[bytes] ase_control_point: gatt_client.CharacteristicProxy[bytes]
def __init__(self, service_proxy: gatt_client.ServiceProxy): def __init__(self, service_proxy: gatt_client.ServiceProxy):
+4 -4
View File
@@ -19,7 +19,7 @@
import enum import enum
import struct import struct
import logging import logging
from typing import Optional, Callable, Union, Any from typing import List, Optional, Callable, Union, Any
from bumble import l2cap from bumble import l2cap
from bumble import utils from bumble import utils
@@ -103,7 +103,7 @@ class AshaService(gatt.TemplateService):
def __init__( def __init__(
self, self,
capability: int, capability: int,
hisyncid: Union[list[int], bytes], hisyncid: Union[List[int], bytes],
device: Device, device: Device,
psm: int = 0, psm: int = 0,
audio_sink: Optional[Callable[[bytes], Any]] = None, audio_sink: Optional[Callable[[bytes], Any]] = None,
@@ -200,7 +200,7 @@ class AshaService(gatt.TemplateService):
# Handler for audio control commands # Handler for audio control commands
async def _on_audio_control_point_write( async def _on_audio_control_point_write(
self, connection: Connection, value: bytes self, connection: Optional[Connection], value: bytes
) -> None: ) -> None:
_logger.debug(f'--- AUDIO CONTROL POINT Write:{value.hex()}') _logger.debug(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
opcode = value[0] opcode = value[0]
@@ -247,7 +247,7 @@ class AshaService(gatt.TemplateService):
) )
# Handler for volume control # Handler for volume control
def _on_volume_write(self, connection: Connection, value: bytes) -> None: def _on_volume_write(self, connection: Optional[Connection], value: bytes) -> None:
_logger.debug(f'--- VOLUME Write:{value[0]}') _logger.debug(f'--- VOLUME Write:{value[0]}')
self.volume = value[0] self.volume = value[0]
self.emit(self.EVENT_VOLUME_CHANGED) self.emit(self.EVENT_VOLUME_CHANGED)
+4 -3
View File
@@ -24,6 +24,7 @@ import enum
import struct import struct
import functools import functools
import logging import logging
from typing import List
from typing_extensions import Self from typing_extensions import Self
from bumble import core from bumble import core
@@ -281,7 +282,7 @@ class UnicastServerAdvertisingData:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def bits_to_channel_counts(data: int) -> list[int]: def bits_to_channel_counts(data: int) -> List[int]:
pos = 0 pos = 0
counts = [] counts = []
while data != 0: while data != 0:
@@ -526,7 +527,7 @@ class BasicAudioAnnouncement:
codec_id: hci.CodingFormat codec_id: hci.CodingFormat
codec_specific_configuration: CodecSpecificConfiguration codec_specific_configuration: CodecSpecificConfiguration
metadata: le_audio.Metadata metadata: le_audio.Metadata
bis: list[BasicAudioAnnouncement.BIS] bis: List[BasicAudioAnnouncement.BIS]
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
metadata_bytes = bytes(self.metadata) metadata_bytes = bytes(self.metadata)
@@ -544,7 +545,7 @@ class BasicAudioAnnouncement:
) )
presentation_delay: int presentation_delay: int
subgroups: list[BasicAudioAnnouncement.Subgroup] subgroups: List[BasicAudioAnnouncement.Subgroup]
@classmethod @classmethod
def from_bytes(cls, data: bytes) -> Self: def from_bytes(cls, data: bytes) -> Self:
+5 -3
View File
@@ -19,7 +19,7 @@
from __future__ import annotations from __future__ import annotations
import enum import enum
import struct import struct
from typing import Optional from typing import Optional, Tuple
from bumble import core from bumble import core
from bumble import crypto from bumble import crypto
@@ -164,10 +164,12 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
super().__init__(characteristics) super().__init__(characteristics)
async def on_sirk_read(self, connection: device.Connection) -> bytes: async def on_sirk_read(self, connection: Optional[device.Connection]) -> bytes:
if self.set_identity_resolving_key_type == SirkType.PLAINTEXT: if self.set_identity_resolving_key_type == SirkType.PLAINTEXT:
sirk_bytes = self.set_identity_resolving_key sirk_bytes = self.set_identity_resolving_key
else: else:
assert connection
if connection.transport == core.PhysicalTransport.LE: if connection.transport == core.PhysicalTransport.LE:
key = await connection.device.get_long_term_key( key = await connection.device.get_long_term_key(
connection_handle=connection.handle, rand=b'', ediv=0 connection_handle=connection.handle, rand=b'', ediv=0
@@ -228,7 +230,7 @@ class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
): ):
self.set_member_rank = characteristics[0] self.set_member_rank = characteristics[0]
async def read_set_identity_resolving_key(self) -> tuple[SirkType, bytes]: async def read_set_identity_resolving_key(self) -> Tuple[SirkType, bytes]:
'''Reads SIRK and decrypts if encrypted.''' '''Reads SIRK and decrypts if encrypted.'''
response = await self.set_identity_resolving_key.read_value() response = await self.set_identity_resolving_key.read_value()
if len(response) != SET_IDENTITY_RESOLVING_KEY_LENGTH + 1: if len(response) != SET_IDENTITY_RESOLVING_KEY_LENGTH + 1:
@@ -17,7 +17,7 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import struct import struct
from typing import Optional from typing import Optional, Tuple
from bumble.gatt import ( from bumble.gatt import (
GATT_DEVICE_INFORMATION_SERVICE, GATT_DEVICE_INFORMATION_SERVICE,
@@ -60,7 +60,7 @@ class DeviceInformationService(TemplateService):
hardware_revision: Optional[str] = None, hardware_revision: Optional[str] = None,
firmware_revision: Optional[str] = None, firmware_revision: Optional[str] = None,
software_revision: Optional[str] = None, software_revision: Optional[str] = None,
system_id: Optional[tuple[int, int]] = None, # (OUI, Manufacturer ID) system_id: Optional[Tuple[int, int]] = None, # (OUI, Manufacturer ID)
ieee_regulatory_certification_data_list: Optional[bytes] = None, ieee_regulatory_certification_data_list: Optional[bytes] = None,
# TODO: pnp_id # TODO: pnp_id
): ):
+2 -2
View File
@@ -19,7 +19,7 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import logging import logging
import struct import struct
from typing import Optional, Union from typing import Optional, Tuple, Union
from bumble.core import Appearance from bumble.core import Appearance
from bumble.gatt import ( from bumble.gatt import (
@@ -54,7 +54,7 @@ class GenericAccessService(TemplateService):
appearance_characteristic: Characteristic[bytes] appearance_characteristic: Characteristic[bytes]
def __init__( def __init__(
self, device_name: str, appearance: Union[Appearance, tuple[int, int], int] = 0 self, device_name: str, appearance: Union[Appearance, Tuple[int, int], int] = 0
): ):
if isinstance(appearance, int): if isinstance(appearance, int):
appearance_int = appearance appearance_int = appearance
+3 -1
View File
@@ -127,7 +127,9 @@ class GenericAttributeProfileService(gatt.TemplateService):
return b'' return b''
def get_database_hash(self, connection: device.Connection) -> bytes: def get_database_hash(self, connection: device.Connection | None) -> bytes:
assert connection
m = b''.join( m = b''.join(
[ [
self.get_attribute_data(attribute) self.get_attribute_data(attribute)
+55 -39
View File
@@ -20,7 +20,7 @@ import asyncio
import functools import functools
from dataclasses import dataclass, field from dataclasses import dataclass, field
import logging import logging
from typing import Any, Optional, Union from typing import Any, Dict, List, Optional, Set, Union
from bumble import att, gatt, gatt_adapters, gatt_client from bumble import att, gatt, gatt_adapters, gatt_client
from bumble.core import InvalidArgumentError, InvalidStateError from bumble.core import InvalidArgumentError, InvalidStateError
@@ -228,25 +228,23 @@ class HearingAccessService(gatt.TemplateService):
hearing_aid_preset_control_point: gatt.Characteristic[bytes] hearing_aid_preset_control_point: gatt.Characteristic[bytes]
active_preset_index_characteristic: gatt.Characteristic[bytes] active_preset_index_characteristic: gatt.Characteristic[bytes]
active_preset_index: int active_preset_index: int
active_preset_index_per_device: dict[Address, int] active_preset_index_per_device: Dict[Address, int]
device: Device device: Device
server_features: HearingAidFeatures server_features: HearingAidFeatures
preset_records: dict[int, PresetRecord] # key is the preset index preset_records: Dict[int, PresetRecord] # key is the preset index
read_presets_request_in_progress: bool read_presets_request_in_progress: bool
other_server_in_binaural_set: Optional[HearingAccessService] = None preset_changed_operations_history_per_device: Dict[
Address, List[PresetChangedOperation]
preset_changed_operations_history_per_device: dict[
Address, list[PresetChangedOperation]
] ]
# Keep an updated list of connected client to send notification to # Keep an updated list of connected client to send notification to
currently_connected_clients: set[Connection] currently_connected_clients: Set[Connection]
def __init__( def __init__(
self, device: Device, features: HearingAidFeatures, presets: list[PresetRecord] self, device: Device, features: HearingAidFeatures, presets: List[PresetRecord]
) -> None: ) -> None:
self.active_preset_index_per_device = {} self.active_preset_index_per_device = {}
self.read_presets_request_in_progress = False self.read_presets_request_in_progress = False
@@ -335,10 +333,11 @@ class HearingAccessService(gatt.TemplateService):
# Update the active preset index if needed # Update the active preset index if needed
await self.notify_active_preset_for_connection(connection) await self.notify_active_preset_for_connection(connection)
connection.cancel_on_disconnection(on_connection_async()) utils.cancel_on_event(connection, 'disconnection', on_connection_async())
def _on_read_active_preset_index(self, connection: Connection) -> bytes: def _on_read_active_preset_index(
del connection # Unused self, __connection__: Optional[Connection]
) -> bytes:
return bytes([self.active_preset_index]) return bytes([self.active_preset_index])
# TODO this need to be triggered when device is unbonded # TODO this need to be triggered when device is unbonded
@@ -346,13 +345,18 @@ class HearingAccessService(gatt.TemplateService):
self.preset_changed_operations_history_per_device.pop(addr) self.preset_changed_operations_history_per_device.pop(addr)
async def _on_write_hearing_aid_preset_control_point( async def _on_write_hearing_aid_preset_control_point(
self, connection: Connection, value: bytes self, connection: Optional[Connection], value: bytes
): ):
assert connection
opcode = HearingAidPresetControlPointOpcode(value[0]) opcode = HearingAidPresetControlPointOpcode(value[0])
handler = getattr(self, '_on_' + opcode.name.lower()) handler = getattr(self, '_on_' + opcode.name.lower())
await handler(connection, value) await handler(connection, value)
async def _on_read_presets_request(self, connection: Connection, value: bytes): async def _on_read_presets_request(
self, connection: Optional[Connection], value: bytes
):
assert connection
if connection.att_mtu < 49: # 2.5. GATT sub-procedure requirements if connection.att_mtu < 49: # 2.5. GATT sub-procedure requirements
logging.warning(f'HAS require MTU >= 49: {connection}') logging.warning(f'HAS require MTU >= 49: {connection}')
@@ -381,7 +385,7 @@ class HearingAccessService(gatt.TemplateService):
utils.AsyncRunner.spawn(self._read_preset_response(connection, presets)) utils.AsyncRunner.spawn(self._read_preset_response(connection, presets))
async def _read_preset_response( async def _read_preset_response(
self, connection: Connection, presets: list[PresetRecord] self, connection: Connection, presets: List[PresetRecord]
): ):
# If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Read Presets Request operation aborted and shall not either continue or restart the operation when the client reconnects. # If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Read Presets Request operation aborted and shall not either continue or restart the operation when the client reconnects.
try: try:
@@ -467,7 +471,10 @@ class HearingAccessService(gatt.TemplateService):
for connection in self.currently_connected_clients: for connection in self.currently_connected_clients:
await self._preset_changed_operation(connection) await self._preset_changed_operation(connection)
async def _on_write_preset_name(self, connection: Connection, value: bytes): async def _on_write_preset_name(
self, connection: Optional[Connection], value: bytes
):
assert connection
if self.read_presets_request_in_progress: if self.read_presets_request_in_progress:
raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS) raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS)
@@ -515,7 +522,10 @@ class HearingAccessService(gatt.TemplateService):
for connection in self.currently_connected_clients: for connection in self.currently_connected_clients:
await self.notify_active_preset_for_connection(connection) await self.notify_active_preset_for_connection(connection)
async def set_active_preset(self, value: bytes) -> None: async def set_active_preset(
self, connection: Optional[Connection], value: bytes
) -> None:
assert connection
index = value[1] index = value[1]
preset = self.preset_records.get(index, None) preset = self.preset_records.get(index, None)
if ( if (
@@ -532,11 +542,16 @@ class HearingAccessService(gatt.TemplateService):
self.active_preset_index = index self.active_preset_index = index
await self.notify_active_preset() await self.notify_active_preset()
async def _on_set_active_preset(self, _: Connection, value: bytes): async def _on_set_active_preset(
await self.set_active_preset(value) self, connection: Optional[Connection], value: bytes
):
await self.set_active_preset(connection, value)
async def set_next_or_previous_preset(self, is_previous): async def set_next_or_previous_preset(
self, connection: Optional[Connection], is_previous
):
'''Set the next or the previous preset as active''' '''Set the next or the previous preset as active'''
assert connection
if self.active_preset_index == 0x00: if self.active_preset_index == 0x00:
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE) raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
@@ -565,47 +580,48 @@ class HearingAccessService(gatt.TemplateService):
self.active_preset_index = first_preset.index self.active_preset_index = first_preset.index
await self.notify_active_preset() await self.notify_active_preset()
async def _on_set_next_preset(self, _: Connection, __value__: bytes) -> None: async def _on_set_next_preset(
await self.set_next_or_previous_preset(False) self, connection: Optional[Connection], __value__: bytes
) -> None:
await self.set_next_or_previous_preset(connection, False)
async def _on_set_previous_preset(self, _: Connection, __value__: bytes) -> None: async def _on_set_previous_preset(
await self.set_next_or_previous_preset(True) self, connection: Optional[Connection], __value__: bytes
) -> None:
await self.set_next_or_previous_preset(connection, True)
async def _on_set_active_preset_synchronized_locally( async def _on_set_active_preset_synchronized_locally(
self, _: Connection, value: bytes self, connection: Optional[Connection], value: bytes
): ):
if ( if (
self.server_features.preset_synchronization_support self.server_features.preset_synchronization_support
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
): ):
raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED) raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
await self.set_active_preset(value) await self.set_active_preset(connection, value)
if self.other_server_in_binaural_set: # TODO (low priority) inform other server of the change
await self.other_server_in_binaural_set.set_active_preset(value)
async def _on_set_next_preset_synchronized_locally( async def _on_set_next_preset_synchronized_locally(
self, _: Connection, __value__: bytes self, connection: Optional[Connection], __value__: bytes
): ):
if ( if (
self.server_features.preset_synchronization_support self.server_features.preset_synchronization_support
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
): ):
raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED) raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
await self.set_next_or_previous_preset(False) await self.set_next_or_previous_preset(connection, False)
if self.other_server_in_binaural_set: # TODO (low priority) inform other server of the change
await self.other_server_in_binaural_set.set_next_or_previous_preset(False)
async def _on_set_previous_preset_synchronized_locally( async def _on_set_previous_preset_synchronized_locally(
self, _: Connection, __value__: bytes self, connection: Optional[Connection], __value__: bytes
): ):
if ( if (
self.server_features.preset_synchronization_support self.server_features.preset_synchronization_support
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
): ):
raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED) raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
await self.set_next_or_previous_preset(True) await self.set_next_or_previous_preset(connection, True)
if self.other_server_in_binaural_set: # TODO (low priority) inform other server of the change
await self.other_server_in_binaural_set.set_next_or_previous_preset(True)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
+4 -4
View File
@@ -19,7 +19,7 @@ from __future__ import annotations
import dataclasses import dataclasses
import enum import enum
import struct import struct
from typing import Any from typing import Any, List, Type
from typing_extensions import Self from typing_extensions import Self
from bumble.profiles import bap from bumble.profiles import bap
@@ -108,13 +108,13 @@ class Metadata:
return self.data return self.data
@classmethod @classmethod
def from_bytes(cls: type[Self], data: bytes) -> Self: def from_bytes(cls: Type[Self], data: bytes) -> Self:
return cls(tag=Metadata.Tag(data[0]), data=data[1:]) return cls(tag=Metadata.Tag(data[0]), data=data[1:])
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
return bytes([len(self.data) + 1, self.tag]) + self.data return bytes([len(self.data) + 1, self.tag]) + self.data
entries: list[Entry] = dataclasses.field(default_factory=list) entries: List[Entry] = dataclasses.field(default_factory=list)
def pretty_print(self, indent: str) -> str: def pretty_print(self, indent: str) -> str:
"""Convenience method to generate a string with one key-value pair per line.""" """Convenience method to generate a string with one key-value pair per line."""
@@ -140,7 +140,7 @@ class Metadata:
) )
@classmethod @classmethod
def from_bytes(cls: type[Self], data: bytes) -> Self: def from_bytes(cls: Type[Self], data: bytes) -> Self:
entries = [] entries = []
offset = 0 offset = 0
length = len(data) length = len(data)
+8 -5
View File
@@ -29,7 +29,7 @@ from bumble import gatt
from bumble import gatt_client from bumble import gatt_client
from bumble import utils from bumble import utils
from typing import Optional, ClassVar, TYPE_CHECKING from typing import Type, Optional, ClassVar, Dict, TYPE_CHECKING
from typing_extensions import Self from typing_extensions import Self
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -167,7 +167,7 @@ class ObjectId(int):
'''See Media Control Service 4.4.2. Object ID field.''' '''See Media Control Service 4.4.2. Object ID field.'''
@classmethod @classmethod
def create_from_bytes(cls: type[Self], data: bytes) -> Self: def create_from_bytes(cls: Type[Self], data: bytes) -> Self:
return cls(int.from_bytes(data, byteorder='little', signed=False)) return cls(int.from_bytes(data, byteorder='little', signed=False))
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
@@ -182,7 +182,7 @@ class GroupObjectType:
object_id: ObjectId object_id: ObjectId
@classmethod @classmethod
def from_bytes(cls: type[Self], data: bytes) -> Self: def from_bytes(cls: Type[Self], data: bytes) -> Self:
return cls( return cls(
object_type=ObjectType(data[0]), object_type=ObjectType(data[0]),
object_id=ObjectId.create_from_bytes(data[1:]), object_id=ObjectId.create_from_bytes(data[1:]),
@@ -287,8 +287,11 @@ class MediaControlService(gatt.TemplateService):
) )
async def on_media_control_point( async def on_media_control_point(
self, connection: device.Connection, data: bytes self, connection: Optional[device.Connection], data: bytes
) -> None: ) -> None:
if not connection:
raise core.InvalidStateError()
opcode = MediaControlPointOpcode(data[0]) opcode = MediaControlPointOpcode(data[0])
await connection.device.notify_subscriber( await connection.device.notify_subscriber(
@@ -310,7 +313,7 @@ class MediaControlServiceProxy(
): ):
SERVICE_CLASS = MediaControlService SERVICE_CLASS = MediaControlService
_CHARACTERISTICS: ClassVar[dict[str, core.UUID]] = { _CHARACTERISTICS: ClassVar[Dict[str, core.UUID]] = {
'media_player_name': gatt.GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC, 'media_player_name': gatt.GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC,
'media_player_icon_object_id': gatt.GATT_MEDIA_PLAYER_ICON_OBJECT_ID_CHARACTERISTIC, 'media_player_icon_object_id': gatt.GATT_MEDIA_PLAYER_ICON_OBJECT_ID_CHARACTERISTIC,
'media_player_icon_url': gatt.GATT_MEDIA_PLAYER_ICON_URL_CHARACTERISTIC, 'media_player_icon_url': gatt.GATT_MEDIA_PLAYER_ICON_URL_CHARACTERISTIC,
+9 -5
View File
@@ -20,7 +20,7 @@ from __future__ import annotations
import dataclasses import dataclasses
import enum import enum
from typing import Sequence from typing import Optional, Sequence
from bumble import att from bumble import att
from bumble import utils from bumble import utils
@@ -146,12 +146,14 @@ class VolumeControlService(gatt.TemplateService):
included_services=list(included_services), included_services=list(included_services),
) )
def _on_read_volume_state(self, _connection: device.Connection) -> bytes: def _on_read_volume_state(self, _connection: Optional[device.Connection]) -> bytes:
return bytes(VolumeState(self.volume_setting, self.muted, self.change_counter)) return bytes(VolumeState(self.volume_setting, self.muted, self.change_counter))
def _on_write_volume_control_point( def _on_write_volume_control_point(
self, connection: device.Connection, value: bytes self, connection: Optional[device.Connection], value: bytes
) -> None: ) -> None:
assert connection
opcode = VolumeControlPointOpcode(value[0]) opcode = VolumeControlPointOpcode(value[0])
change_counter = value[1] change_counter = value[1]
@@ -161,8 +163,10 @@ class VolumeControlService(gatt.TemplateService):
handler = getattr(self, '_on_' + opcode.name.lower()) handler = getattr(self, '_on_' + opcode.name.lower())
if handler(*value[2:]): if handler(*value[2:]):
self.change_counter = (self.change_counter + 1) % 256 self.change_counter = (self.change_counter + 1) % 256
connection.cancel_on_disconnection( utils.cancel_on_event(
connection.device.notify_subscribers(attribute=self.volume_state) connection,
'disconnection',
connection.device.notify_subscribers(attribute=self.volume_state),
) )
self.emit(self.EVENT_VOLUME_STATE_CHANGE) self.emit(self.EVENT_VOLUME_STATE_CHANGE)
+9 -6
View File
@@ -86,7 +86,7 @@ class VolumeOffsetState:
assert self.attribute is not None assert self.attribute is not None
await connection.device.notify_subscribers(attribute=self.attribute) await connection.device.notify_subscribers(attribute=self.attribute)
def on_read(self, _connection: Connection) -> bytes: def on_read(self, _connection: Optional[Connection]) -> bytes:
return bytes(self) return bytes(self)
@@ -103,10 +103,11 @@ class VocsAudioLocation:
audio_location = AudioLocation(struct.unpack('<I', data)[0]) audio_location = AudioLocation(struct.unpack('<I', data)[0])
return cls(audio_location) return cls(audio_location)
def on_read(self, _connection: Connection) -> bytes: def on_read(self, _connection: Optional[Connection]) -> bytes:
return bytes(self) return bytes(self)
async def on_write(self, connection: Connection, value: bytes) -> None: async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
assert connection
assert self.attribute assert self.attribute
self.audio_location = AudioLocation(int.from_bytes(value, 'little')) self.audio_location = AudioLocation(int.from_bytes(value, 'little'))
@@ -117,7 +118,8 @@ class VocsAudioLocation:
class VolumeOffsetControlPoint: 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: Optional[Connection], value: bytes) -> None:
assert connection
opcode = value[0] opcode = value[0]
if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET: if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET:
@@ -157,10 +159,11 @@ class AudioOutputDescription:
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
return self.audio_output_description.encode('utf-8') return self.audio_output_description.encode('utf-8')
def on_read(self, _connection: Connection) -> bytes: def on_read(self, _connection: Optional[Connection]) -> bytes:
return bytes(self) return bytes(self)
async def on_write(self, connection: Connection, value: bytes) -> None: async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
assert connection
assert self.attribute assert self.attribute
self.audio_output_description = value.decode('utf-8') self.audio_output_description = value.decode('utf-8')
+10 -10
View File
@@ -22,7 +22,7 @@ import asyncio
import collections import collections
import dataclasses import dataclasses
import enum import enum
from typing import Callable, Optional, Union, TYPE_CHECKING from typing import Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
from typing_extensions import Self from typing_extensions import Self
@@ -123,7 +123,7 @@ RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def make_service_sdp_records( def make_service_sdp_records(
service_record_handle: int, channel: int, uuid: Optional[UUID] = None service_record_handle: int, channel: int, uuid: Optional[UUID] = None
) -> list[sdp.ServiceAttribute]: ) -> List[sdp.ServiceAttribute]:
""" """
Create SDP records for an RFComm service given a channel number and an Create SDP records for an RFComm service given a channel number and an
optional UUID. A Service Class Attribute is included only if the UUID is not None. optional UUID. A Service Class Attribute is included only if the UUID is not None.
@@ -169,7 +169,7 @@ def make_service_sdp_records(
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def find_rfcomm_channels(connection: Connection) -> dict[int, list[UUID]]: async def find_rfcomm_channels(connection: Connection) -> Dict[int, List[UUID]]:
"""Searches all RFCOMM channels and their associated UUID from SDP service records. """Searches all RFCOMM channels and their associated UUID from SDP service records.
Args: Args:
@@ -188,7 +188,7 @@ async def find_rfcomm_channels(connection: Connection) -> dict[int, list[UUID]]:
], ],
) )
for attribute_lists in search_result: for attribute_lists in search_result:
service_classes: list[UUID] = [] service_classes: List[UUID] = []
channel: Optional[int] = None channel: Optional[int] = None
for attribute in attribute_lists: for attribute in attribute_lists:
# The layout is [[L2CAP_PROTOCOL], [RFCOMM_PROTOCOL, RFCOMM_CHANNEL]]. # The layout is [[L2CAP_PROTOCOL], [RFCOMM_PROTOCOL, RFCOMM_CHANNEL]].
@@ -275,7 +275,7 @@ class RFCOMM_Frame:
self.fcs = compute_fcs(bytes([self.address, self.control]) + self.length) self.fcs = compute_fcs(bytes([self.address, self.control]) + self.length)
@staticmethod @staticmethod
def parse_mcc(data) -> tuple[int, bool, bytes]: def parse_mcc(data) -> Tuple[int, bool, bytes]:
mcc_type = data[0] >> 2 mcc_type = data[0] >> 2
c_r = bool((data[0] >> 1) & 1) c_r = bool((data[0] >> 1) & 1)
length = data[1] length = data[1]
@@ -771,8 +771,8 @@ class Multiplexer(utils.EventEmitter):
connection_result: Optional[asyncio.Future] connection_result: Optional[asyncio.Future]
disconnection_result: Optional[asyncio.Future] disconnection_result: Optional[asyncio.Future]
open_result: Optional[asyncio.Future] open_result: Optional[asyncio.Future]
acceptor: Optional[Callable[[int], Optional[tuple[int, int]]]] acceptor: Optional[Callable[[int], Optional[Tuple[int, int]]]]
dlcs: dict[int, DLC] dlcs: Dict[int, DLC]
def __init__(self, l2cap_channel: l2cap.ClassicChannel, role: Role) -> None: def __init__(self, l2cap_channel: l2cap.ClassicChannel, role: Role) -> None:
super().__init__() super().__init__()
@@ -1088,8 +1088,8 @@ class Server(utils.EventEmitter):
) -> None: ) -> None:
super().__init__() super().__init__()
self.device = device self.device = device
self.acceptors: dict[int, Callable[[DLC], None]] = {} self.acceptors: Dict[int, Callable[[DLC], None]] = {}
self.dlc_configs: dict[int, tuple[int, int]] = {} self.dlc_configs: Dict[int, Tuple[int, int]] = {}
# Register ourselves with the L2CAP channel manager # Register ourselves with the L2CAP channel manager
self.l2cap_server = device.create_l2cap_server( self.l2cap_server = device.create_l2cap_server(
@@ -1144,7 +1144,7 @@ class Server(utils.EventEmitter):
# Notify # Notify
self.emit(self.EVENT_START, multiplexer) self.emit(self.EVENT_START, multiplexer)
def accept_dlc(self, channel_number: int) -> Optional[tuple[int, int]]: def accept_dlc(self, channel_number: int) -> Optional[Tuple[int, int]]:
return self.dlc_configs.get(channel_number) return self.dlc_configs.get(channel_number)
def on_dlc(self, dlc: DLC) -> None: def on_dlc(self, dlc: DLC) -> None:
+2 -1
View File
@@ -17,6 +17,7 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import struct import struct
from typing import List
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -59,7 +60,7 @@ class MediaPacket:
sequence_number: int, sequence_number: int,
timestamp: int, timestamp: int,
ssrc: int, ssrc: int,
csrc_list: list[int], csrc_list: List[int],
payload_type: int, payload_type: int,
payload: bytes, payload: bytes,
) -> None: ) -> None:
+2 -2
View File
@@ -19,7 +19,7 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
import struct import struct
from typing import Iterable, NewType, Optional, Union, Sequence, TYPE_CHECKING from typing import Iterable, NewType, Optional, Union, Sequence, Type, TYPE_CHECKING
from typing_extensions import Self from typing_extensions import Self
from bumble import core, l2cap from bumble import core, l2cap
@@ -547,7 +547,7 @@ class SDP_PDU:
SDP_SERVICE_ATTRIBUTE_REQUEST: SDP_SERVICE_ATTRIBUTE_RESPONSE, SDP_SERVICE_ATTRIBUTE_REQUEST: SDP_SERVICE_ATTRIBUTE_RESPONSE,
SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST: SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE, SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST: SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE,
} }
sdp_pdu_classes: dict[int, type[SDP_PDU]] = {} sdp_pdu_classes: dict[int, Type[SDP_PDU]] = {}
name = None name = None
pdu_id = 0 pdu_id = 0
+62 -58
View File
@@ -26,13 +26,18 @@ from __future__ import annotations
import logging import logging
import asyncio import asyncio
import enum import enum
import secrets
from dataclasses import dataclass from dataclasses import dataclass
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
Awaitable, Awaitable,
Callable, Callable,
Dict,
List,
Optional, Optional,
Tuple,
Type,
cast, cast,
) )
@@ -205,7 +210,7 @@ class SMP_Command:
See Bluetooth spec @ Vol 3, Part H - 3 SECURITY MANAGER PROTOCOL See Bluetooth spec @ Vol 3, Part H - 3 SECURITY MANAGER PROTOCOL
''' '''
smp_classes: dict[int, type[SMP_Command]] = {} smp_classes: Dict[int, Type[SMP_Command]] = {}
fields: Any fields: Any
code = 0 code = 0
name = '' name = ''
@@ -249,7 +254,7 @@ class SMP_Command:
@staticmethod @staticmethod
def key_distribution_str(value: int) -> str: def key_distribution_str(value: int) -> str:
key_types: list[str] = [] key_types: List[str] = []
if value & SMP_ENC_KEY_DISTRIBUTION_FLAG: if value & SMP_ENC_KEY_DISTRIBUTION_FLAG:
key_types.append('ENC') key_types.append('ENC')
if value & SMP_ID_KEY_DISTRIBUTION_FLAG: if value & SMP_ID_KEY_DISTRIBUTION_FLAG:
@@ -701,7 +706,7 @@ class Session:
self.peer_identity_resolving_key = None self.peer_identity_resolving_key = None
self.peer_bd_addr: Optional[Address] = None self.peer_bd_addr: Optional[Address] = None
self.peer_signature_key = None self.peer_signature_key = None
self.peer_expected_distributions: list[type[SMP_Command]] = [] self.peer_expected_distributions: List[Type[SMP_Command]] = []
self.dh_key = b'' self.dh_key = b''
self.confirm_value = None self.confirm_value = None
self.passkey: Optional[int] = None self.passkey: Optional[int] = None
@@ -762,9 +767,7 @@ class Session:
# OOB # OOB
self.oob_data_flag = ( self.oob_data_flag = (
1 1 if pairing_config.oob and pairing_config.oob.peer_data else 0
if pairing_config.oob and (not self.sc or pairing_config.oob.peer_data)
else 0
) )
# Set up addresses # Set up addresses
@@ -811,7 +814,7 @@ class Session:
self.tk = bytes(16) self.tk = bytes(16)
@property @property
def pkx(self) -> tuple[bytes, bytes]: def pkx(self) -> Tuple[bytes, bytes]:
return (self.ecc_key.x[::-1], self.peer_public_key_x) return (self.ecc_key.x[::-1], self.peer_public_key_x)
@property @property
@@ -823,7 +826,7 @@ class Session:
return self.pkx[0 if self.is_responder else 1] return self.pkx[0 if self.is_responder else 1]
@property @property
def nx(self) -> tuple[bytes, bytes]: def nx(self) -> Tuple[bytes, bytes]:
assert self.peer_random_value assert self.peer_random_value
return (self.r, self.peer_random_value) return (self.r, self.peer_random_value)
@@ -897,7 +900,7 @@ class Session:
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR) self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
self.connection.cancel_on_disconnection(prompt()) utils.cancel_on_event(self.connection, 'disconnection', prompt())
def prompt_user_for_numeric_comparison( def prompt_user_for_numeric_comparison(
self, code: int, next_steps: Callable[[], None] self, code: int, next_steps: Callable[[], None]
@@ -916,7 +919,7 @@ class Session:
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR) self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
self.connection.cancel_on_disconnection(prompt()) utils.cancel_on_event(self.connection, 'disconnection', prompt())
def prompt_user_for_number(self, next_steps: Callable[[int], None]) -> None: def prompt_user_for_number(self, next_steps: Callable[[int], None]) -> None:
async def prompt() -> None: async def prompt() -> None:
@@ -933,11 +936,12 @@ class Session:
logger.warning(f'exception while prompting: {error}') logger.warning(f'exception while prompting: {error}')
self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR) self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR)
self.connection.cancel_on_disconnection(prompt()) utils.cancel_on_event(self.connection, 'disconnection', prompt())
async def display_passkey(self) -> None: def display_passkey(self) -> None:
# Get the passkey value from the delegate # Generate random Passkey/PIN code
self.passkey = await self.pairing_config.delegate.generate_passkey() self.passkey = secrets.randbelow(1000000)
assert self.passkey is not None
logger.debug(f'Pairing PIN CODE: {self.passkey:06}') logger.debug(f'Pairing PIN CODE: {self.passkey:06}')
self.passkey_ready.set() self.passkey_ready.set()
@@ -946,7 +950,14 @@ class Session:
self.tk = self.passkey.to_bytes(16, byteorder='little') self.tk = self.passkey.to_bytes(16, byteorder='little')
logger.debug(f'TK from passkey = {self.tk.hex()}') logger.debug(f'TK from passkey = {self.tk.hex()}')
await self.pairing_config.delegate.display_number(self.passkey, digits=6) try:
utils.cancel_on_event(
self.connection,
'disconnection',
self.pairing_config.delegate.display_number(self.passkey, digits=6),
)
except Exception as error:
logger.warning(f'exception while displaying number: {error}')
def input_passkey(self, next_steps: Optional[Callable[[], None]] = None) -> None: def input_passkey(self, next_steps: Optional[Callable[[], None]] = None) -> None:
# Prompt the user for the passkey displayed on the peer # Prompt the user for the passkey displayed on the peer
@@ -968,16 +979,9 @@ class Session:
self, next_steps: Optional[Callable[[], None]] = None self, next_steps: Optional[Callable[[], None]] = None
) -> None: ) -> None:
if self.passkey_display: if self.passkey_display:
self.display_passkey()
async def display_passkey(): if next_steps is not None:
await self.display_passkey() next_steps()
if next_steps is not None:
next_steps()
try:
self.connection.cancel_on_disconnection(display_passkey())
except Exception as error:
logger.warning(f'exception while displaying passkey: {error}')
else: else:
self.input_passkey(next_steps) self.input_passkey(next_steps)
@@ -1047,7 +1051,7 @@ class Session:
) )
# Perform the next steps asynchronously in case we need to wait for input # Perform the next steps asynchronously in case we need to wait for input
self.connection.cancel_on_disconnection(next_steps()) utils.cancel_on_event(self.connection, 'disconnection', next_steps())
else: else:
confirm_value = crypto.c1( confirm_value = crypto.c1(
self.tk, self.tk,
@@ -1170,8 +1174,8 @@ class Session:
self.connection.transport == PhysicalTransport.BR_EDR self.connection.transport == PhysicalTransport.BR_EDR
and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
): ):
self.ctkd_task = self.connection.cancel_on_disconnection( self.ctkd_task = utils.cancel_on_event(
self.get_link_key_and_derive_ltk() self.connection, 'disconnection', self.get_link_key_and_derive_ltk()
) )
elif not self.sc: elif not self.sc:
# Distribute the LTK, EDIV and RAND # Distribute the LTK, EDIV and RAND
@@ -1209,8 +1213,8 @@ class Session:
self.connection.transport == PhysicalTransport.BR_EDR self.connection.transport == PhysicalTransport.BR_EDR
and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
): ):
self.ctkd_task = self.connection.cancel_on_disconnection( self.ctkd_task = utils.cancel_on_event(
self.get_link_key_and_derive_ltk() self.connection, 'disconnection', self.get_link_key_and_derive_ltk()
) )
# Distribute the LTK, EDIV and RAND # Distribute the LTK, EDIV and RAND
elif not self.sc: elif not self.sc:
@@ -1265,7 +1269,7 @@ class Session:
f'{[c.__name__ for c in self.peer_expected_distributions]}' f'{[c.__name__ for c in self.peer_expected_distributions]}'
) )
def check_key_distribution(self, command_class: type[SMP_Command]) -> None: def check_key_distribution(self, command_class: Type[SMP_Command]) -> None:
# First, check that the connection is encrypted # First, check that the connection is encrypted
if not self.connection.is_encrypted: if not self.connection.is_encrypted:
logger.warning( logger.warning(
@@ -1302,7 +1306,9 @@ class Session:
# Wait for the pairing process to finish # Wait for the pairing process to finish
assert self.pairing_result assert self.pairing_result
await self.connection.cancel_on_disconnection(self.pairing_result) await utils.cancel_on_event(
self.connection, 'disconnection', self.pairing_result
)
def on_disconnection(self, _: int) -> None: def on_disconnection(self, _: int) -> None:
self.connection.remove_listener( self.connection.remove_listener(
@@ -1323,7 +1329,7 @@ class Session:
if self.is_initiator: if self.is_initiator:
self.distribute_keys() self.distribute_keys()
self.connection.cancel_on_disconnection(self.on_pairing()) utils.cancel_on_event(self.connection, 'disconnection', self.on_pairing())
def on_connection_encryption_change(self) -> None: def on_connection_encryption_change(self) -> None:
if self.connection.is_encrypted and not self.completed: if self.connection.is_encrypted and not self.completed:
@@ -1434,8 +1440,10 @@ class Session:
def on_smp_pairing_request_command( def on_smp_pairing_request_command(
self, command: SMP_Pairing_Request_Command self, command: SMP_Pairing_Request_Command
) -> None: ) -> None:
self.connection.cancel_on_disconnection( utils.cancel_on_event(
self.on_smp_pairing_request_command_async(command) self.connection,
'disconnection',
self.on_smp_pairing_request_command_async(command),
) )
async def on_smp_pairing_request_command_async( async def on_smp_pairing_request_command_async(
@@ -1499,7 +1507,7 @@ class Session:
# Display a passkey if we need to # Display a passkey if we need to
if not self.sc: if not self.sc:
if self.pairing_method == PairingMethod.PASSKEY and self.passkey_display: if self.pairing_method == PairingMethod.PASSKEY and self.passkey_display:
await self.display_passkey() self.display_passkey()
# Respond # Respond
self.send_pairing_response_command() self.send_pairing_response_command()
@@ -1681,7 +1689,7 @@ class Session:
): ):
return return
elif self.pairing_method == PairingMethod.PASSKEY: elif self.pairing_method == PairingMethod.PASSKEY:
assert self.passkey is not None and self.confirm_value is not None assert self.passkey and self.confirm_value
# Check that the random value matches what was committed to earlier # Check that the random value matches what was committed to earlier
confirm_verifier = crypto.f4( confirm_verifier = crypto.f4(
self.pkb, self.pkb,
@@ -1710,7 +1718,7 @@ class Session:
): ):
self.send_pairing_random_command() self.send_pairing_random_command()
elif self.pairing_method == PairingMethod.PASSKEY: elif self.pairing_method == PairingMethod.PASSKEY:
assert self.passkey is not None and self.confirm_value is not None assert self.passkey and self.confirm_value
# Check that the random value matches what was committed to earlier # Check that the random value matches what was committed to earlier
confirm_verifier = crypto.f4( confirm_verifier = crypto.f4(
self.pka, self.pka,
@@ -1747,7 +1755,7 @@ class Session:
ra = bytes(16) ra = bytes(16)
rb = ra rb = ra
elif self.pairing_method == PairingMethod.PASSKEY: elif self.pairing_method == PairingMethod.PASSKEY:
assert self.passkey is not None assert self.passkey
ra = self.passkey.to_bytes(16, byteorder='little') ra = self.passkey.to_bytes(16, byteorder='little')
rb = ra rb = ra
elif self.pairing_method == PairingMethod.OOB: elif self.pairing_method == PairingMethod.OOB:
@@ -1846,23 +1854,19 @@ class Session:
elif self.pairing_method == PairingMethod.PASSKEY: elif self.pairing_method == PairingMethod.PASSKEY:
self.send_pairing_confirm_command() self.send_pairing_confirm_command()
else: else:
def next_steps() -> None:
# Send our public key back to the initiator
self.send_public_key_command()
if self.pairing_method in (
PairingMethod.JUST_WORKS,
PairingMethod.NUMERIC_COMPARISON,
PairingMethod.OOB,
):
# We can now send the confirmation value
self.send_pairing_confirm_command()
if self.pairing_method == PairingMethod.PASSKEY: if self.pairing_method == PairingMethod.PASSKEY:
self.display_or_input_passkey(next_steps) self.display_or_input_passkey()
else:
next_steps() # Send our public key back to the initiator
self.send_public_key_command()
if self.pairing_method in (
PairingMethod.JUST_WORKS,
PairingMethod.NUMERIC_COMPARISON,
PairingMethod.OOB,
):
# We can now send the confirmation value
self.send_pairing_confirm_command()
def on_smp_pairing_dhkey_check_command( def on_smp_pairing_dhkey_check_command(
self, command: SMP_Pairing_DHKey_Check_Command self, command: SMP_Pairing_DHKey_Check_Command
@@ -1884,7 +1888,7 @@ class Session:
self.wait_before_continuing = None self.wait_before_continuing = None
self.send_pairing_dhkey_check_command() self.send_pairing_dhkey_check_command()
self.connection.cancel_on_disconnection(next_steps()) utils.cancel_on_event(self.connection, 'disconnection', next_steps())
else: else:
self.send_pairing_dhkey_check_command() self.send_pairing_dhkey_check_command()
else: else:
@@ -1934,9 +1938,9 @@ class Manager(utils.EventEmitter):
''' '''
device: Device device: Device
sessions: dict[int, Session] sessions: Dict[int, Session]
pairing_config_factory: Callable[[Connection], PairingConfig] pairing_config_factory: Callable[[Connection], PairingConfig]
session_proxy: type[Session] session_proxy: Type[Session]
_ecc_key: Optional[crypto.EccKey] _ecc_key: Optional[crypto.EccKey]
def __init__( def __init__(
+16 -2
View File
@@ -20,9 +20,9 @@ import logging
import os import os
from typing import Optional from typing import Optional
from bumble import utils
from bumble.transport.common import ( from bumble.transport.common import (
Transport, Transport,
AsyncPipeSink,
SnoopingTransport, SnoopingTransport,
TransportSpecError, TransportSpecError,
) )
@@ -195,7 +195,6 @@ async def _open_transport(scheme: str, spec: Optional[str]) -> Transport:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@utils.deprecated("RemoteLink has been removed. Use open_transport instead.")
async def open_transport_or_link(name: str) -> Transport: async def open_transport_or_link(name: str) -> Transport:
""" """
Open a transport or a link relay. Open a transport or a link relay.
@@ -206,6 +205,21 @@ async def open_transport_or_link(name: str) -> Transport:
When the name starts with "link-relay:", open a link relay (see RemoteLink When the name starts with "link-relay:", open a link relay (see RemoteLink
for details on what the arguments are). for details on what the arguments are).
For other namespaces, see `open_transport`. For other namespaces, see `open_transport`.
""" """
if name.startswith('link-relay:'):
logger.warning('Link Relay has been deprecated.')
from bumble.controller import Controller
from bumble.link import RemoteLink # lazy import
link = RemoteLink(name[11:])
await link.wait_until_connected()
controller = Controller('remote', link=link) # type:ignore[arg-type]
class LinkTransport(Transport):
async def close(self):
link.close()
return _wrap_transport(LinkTransport(controller, AsyncPipeSink(controller)))
return await open_transport(name) return await open_transport(name)
+5 -5
View File
@@ -22,7 +22,7 @@ import os
import pathlib import pathlib
import platform import platform
import sys import sys
from typing import Optional from typing import Dict, Optional
import grpc.aio import grpc.aio
@@ -143,7 +143,7 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def open_android_netsim_controller_transport( async def open_android_netsim_controller_transport(
server_host: Optional[str], server_port: int, options: dict[str, str] server_host: Optional[str], server_port: int, options: Dict[str, str]
) -> Transport: ) -> Transport:
if not server_port: if not server_port:
raise TransportSpecError('invalid port') raise TransportSpecError('invalid port')
@@ -288,7 +288,7 @@ async def open_android_netsim_controller_transport(
async def open_android_netsim_host_transport_with_address( async def open_android_netsim_host_transport_with_address(
server_host: Optional[str], server_host: Optional[str],
server_port: int, server_port: int,
options: Optional[dict[str, str]] = None, options: Optional[Dict[str, str]] = None,
): ):
if server_host == '_' or not server_host: if server_host == '_' or not server_host:
server_host = 'localhost' server_host = 'localhost'
@@ -313,7 +313,7 @@ async def open_android_netsim_host_transport_with_address(
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def open_android_netsim_host_transport_with_channel( async def open_android_netsim_host_transport_with_channel(
channel, options: Optional[dict[str, str]] = None channel, options: Optional[Dict[str, str]] = None
): ):
# Wrapper for I/O operations # Wrapper for I/O operations
class HciDevice: class HciDevice:
@@ -451,7 +451,7 @@ async def open_android_netsim_transport(spec: Optional[str]) -> Transport:
port = 0 port = 0
params_offset = 0 params_offset = 0
options: dict[str, str] = {} options: Dict[str, str] = {}
for param in params[params_offset:]: for param in params[params_offset:]:
if '=' not in param: if '=' not in param:
raise TransportSpecError('invalid parameter, expected <name>=<value>') raise TransportSpecError('invalid parameter, expected <name>=<value>')
+4 -4
View File
@@ -21,7 +21,7 @@ import struct
import asyncio import asyncio
import logging import logging
import io import io
from typing import Any, ContextManager, Optional, Protocol from typing import Any, ContextManager, Tuple, Optional, Protocol, Dict
from bumble import core from bumble import core
from bumble import hci from bumble import hci
@@ -38,7 +38,7 @@ logger = logging.getLogger(__name__)
# Information needed to parse HCI packets with a generic parser: # Information needed to parse HCI packets with a generic parser:
# For each packet type, the info represents: # For each packet type, the info represents:
# (length-size, length-offset, unpack-type) # (length-size, length-offset, unpack-type)
HCI_PACKET_INFO: dict[int, tuple[int, int, str]] = { HCI_PACKET_INFO: Dict[int, Tuple[int, int, str]] = {
hci.HCI_COMMAND_PACKET: (1, 2, 'B'), hci.HCI_COMMAND_PACKET: (1, 2, 'B'),
hci.HCI_ACL_DATA_PACKET: (2, 2, 'H'), hci.HCI_ACL_DATA_PACKET: (2, 2, 'H'),
hci.HCI_SYNCHRONOUS_DATA_PACKET: (1, 2, 'B'), hci.HCI_SYNCHRONOUS_DATA_PACKET: (1, 2, 'B'),
@@ -108,8 +108,8 @@ class PacketParser:
NEED_BODY = 2 NEED_BODY = 2
sink: Optional[TransportSink] sink: Optional[TransportSink]
extended_packet_info: dict[int, tuple[int, int, str]] extended_packet_info: Dict[int, Tuple[int, int, str]]
packet_info: Optional[tuple[int, int, str]] = None packet_info: Optional[Tuple[int, int, str]] = None
def __init__(self, sink: Optional[TransportSink] = None) -> None: def __init__(self, sink: Optional[TransportSink] = None) -> None:
self.sink = sink self.sink = sink
+2 -2
View File
@@ -23,7 +23,7 @@ import time
import usb.core import usb.core
import usb.util import usb.util
from typing import Optional from typing import Optional, Set
from usb.core import Device as UsbDevice from usb.core import Device as UsbDevice
from usb.core import USBError from usb.core import USBError
from usb.util import CTRL_TYPE_CLASS, CTRL_RECIPIENT_OTHER from usb.util import CTRL_TYPE_CLASS, CTRL_RECIPIENT_OTHER
@@ -49,7 +49,7 @@ logger = logging.getLogger(__name__)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Global # Global
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
devices_in_use: set[int] = set() devices_in_use: Set[int] = set()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
+5 -2
View File
@@ -27,8 +27,11 @@ from typing import (
Any, Any,
Awaitable, Awaitable,
Callable, Callable,
List,
Optional, Optional,
Protocol, Protocol,
Set,
Tuple,
TypeVar, TypeVar,
Union, Union,
overload, overload,
@@ -153,7 +156,7 @@ class EventWatcher:
``` ```
''' '''
handlers: list[tuple[pyee.EventEmitter, str, Callable[..., Any]]] handlers: List[Tuple[pyee.EventEmitter, str, Callable[..., Any]]]
def __init__(self) -> None: def __init__(self) -> None:
self.handlers = [] self.handlers = []
@@ -326,7 +329,7 @@ class AsyncRunner:
default_queue = WorkQueue() default_queue = WorkQueue()
# Shared set of running tasks # Shared set of running tasks
running_tasks: set[Awaitable] = set() running_tasks: Set[Awaitable] = set()
@staticmethod @staticmethod
def run_in_task(queue=None): def run_in_task(queue=None):
+197 -115
View File
@@ -15,12 +15,21 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import dataclasses
from dataclasses import field
import struct import struct
from typing import Optional from typing import Dict, Optional, Type
from bumble import hci from bumble.hci import (
name_or_number,
hci_vendor_command_op_code,
Address,
HCI_Constant,
HCI_Object,
HCI_Command,
HCI_Event,
HCI_Extended_Event,
HCI_VENDOR_EVENT,
STATUS_SPEC,
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -32,28 +41,22 @@ from bumble import hci
# #
# pylint: disable-next=line-too-long # pylint: disable-next=line-too-long
# See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#chip-capabilities-and-configuration # See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#chip-capabilities-and-configuration
HCI_LE_GET_VENDOR_CAPABILITIES_COMMAND = hci.hci_vendor_command_op_code(0x153) HCI_LE_GET_VENDOR_CAPABILITIES_COMMAND = hci_vendor_command_op_code(0x153)
HCI_LE_APCF_COMMAND = hci.hci_vendor_command_op_code(0x157) HCI_LE_APCF_COMMAND = hci_vendor_command_op_code(0x157)
HCI_GET_CONTROLLER_ACTIVITY_ENERGY_INFO_COMMAND = hci.hci_vendor_command_op_code(0x159) HCI_GET_CONTROLLER_ACTIVITY_ENERGY_INFO_COMMAND = hci_vendor_command_op_code(0x159)
HCI_A2DP_HARDWARE_OFFLOAD_COMMAND = hci.hci_vendor_command_op_code(0x15D) HCI_A2DP_HARDWARE_OFFLOAD_COMMAND = hci_vendor_command_op_code(0x15D)
HCI_BLUETOOTH_QUALITY_REPORT_COMMAND = hci.hci_vendor_command_op_code(0x15E) HCI_BLUETOOTH_QUALITY_REPORT_COMMAND = hci_vendor_command_op_code(0x15E)
HCI_DYNAMIC_AUDIO_BUFFER_COMMAND = hci.hci_vendor_command_op_code(0x15F) HCI_DYNAMIC_AUDIO_BUFFER_COMMAND = hci_vendor_command_op_code(0x15F)
HCI_BLUETOOTH_QUALITY_REPORT_EVENT = 0x58 HCI_BLUETOOTH_QUALITY_REPORT_EVENT = 0x58
hci.HCI_Command.register_commands(globals()) HCI_Command.register_commands(globals())
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@hci.HCI_Command.command @HCI_Command.command(
@dataclasses.dataclass return_parameters_fields=[
class HCI_LE_Get_Vendor_Capabilities_Command(hci.HCI_Command): ('status', STATUS_SPEC),
# pylint: disable=line-too-long
'''
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), ('max_advt_instances', 1),
('offloaded_resolution_of_private_address', 1), ('offloaded_resolution_of_private_address', 1),
('total_scan_results_storage', 2), ('total_scan_results_storage', 2),
@@ -70,6 +73,12 @@ class HCI_LE_Get_Vendor_Capabilities_Command(hci.HCI_Command):
('bluetooth_quality_report_support', 1), ('bluetooth_quality_report_support', 1),
('dynamic_audio_buffer_support', 4), ('dynamic_audio_buffer_support', 4),
] ]
)
class HCI_LE_Get_Vendor_Capabilities_Command(HCI_Command):
# pylint: disable=line-too-long
'''
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#vendor-specific-capabilities
'''
@classmethod @classmethod
def parse_return_parameters(cls, parameters): def parse_return_parameters(cls, parameters):
@@ -77,13 +86,13 @@ class HCI_LE_Get_Vendor_Capabilities_Command(hci.HCI_Command):
# there are no more bytes to parse, and leave un-signal parameters set to # there are no more bytes to parse, and leave un-signal parameters set to
# None (older versions) # None (older versions)
nones = {field: None for field, _ in cls.return_parameters_fields} nones = {field: None for field, _ in cls.return_parameters_fields}
return_parameters = hci.HCI_Object(cls.return_parameters_fields, **nones) return_parameters = HCI_Object(cls.return_parameters_fields, **nones)
try: try:
offset = 0 offset = 0
for field in cls.return_parameters_fields: for field in cls.return_parameters_fields:
field_name, field_type = field field_name, field_type = field
field_value, field_size = hci.HCI_Object.parse_field( field_value, field_size = HCI_Object.parse_field(
parameters, offset, field_type parameters, offset, field_type
) )
setattr(return_parameters, field_name, field_value) setattr(return_parameters, field_name, field_value)
@@ -95,9 +104,30 @@ class HCI_LE_Get_Vendor_Capabilities_Command(hci.HCI_Command):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@hci.HCI_Command.command @HCI_Command.command(
@dataclasses.dataclass fields=[
class HCI_LE_APCF_Command(hci.HCI_Command): (
'opcode',
{
'size': 1,
'mapper': lambda x: HCI_LE_APCF_Command.opcode_name(x),
},
),
('payload', '*'),
],
return_parameters_fields=[
('status', STATUS_SPEC),
(
'opcode',
{
'size': 1,
'mapper': lambda x: HCI_LE_APCF_Command.opcode_name(x),
},
),
('payload', '*'),
],
)
class HCI_LE_APCF_Command(HCI_Command):
# pylint: disable=line-too-long # pylint: disable=line-too-long
''' '''
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_apcf_command See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_apcf_command
@@ -107,50 +137,80 @@ class HCI_LE_APCF_Command(hci.HCI_Command):
''' '''
# APCF Subcommands # APCF Subcommands
class Opcode(hci.SpecableEnum): # TODO: use the OpenIntEnum class (when upcoming PR is merged)
ENABLE = 0x00 APCF_ENABLE = 0x00
SET_FILTERING_PARAMETERS = 0x01 APCF_SET_FILTERING_PARAMETERS = 0x01
BROADCASTER_ADDRESS = 0x02 APCF_BROADCASTER_ADDRESS = 0x02
SERVICE_UUID = 0x03 APCF_SERVICE_UUID = 0x03
SERVICE_SOLICITATION_UUID = 0x04 APCF_SERVICE_SOLICITATION_UUID = 0x04
LOCAL_NAME = 0x05 APCF_LOCAL_NAME = 0x05
MANUFACTURER_DATA = 0x06 APCF_MANUFACTURER_DATA = 0x06
SERVICE_DATA = 0x07 APCF_SERVICE_DATA = 0x07
TRANSPORT_DISCOVERY_SERVICE = 0x08 APCF_TRANSPORT_DISCOVERY_SERVICE = 0x08
AD_TYPE_FILTER = 0x09 APCF_AD_TYPE_FILTER = 0x09
READ_EXTENDED_FEATURES = 0xFF APCF_READ_EXTENDED_FEATURES = 0xFF
opcode: int = dataclasses.field(metadata=Opcode.type_metadata(1)) OPCODE_NAMES = {
payload: bytes = dataclasses.field(metadata=hci.metadata("*")) APCF_ENABLE: 'APCF_ENABLE',
APCF_SET_FILTERING_PARAMETERS: 'APCF_SET_FILTERING_PARAMETERS',
APCF_BROADCASTER_ADDRESS: 'APCF_BROADCASTER_ADDRESS',
APCF_SERVICE_UUID: 'APCF_SERVICE_UUID',
APCF_SERVICE_SOLICITATION_UUID: 'APCF_SERVICE_SOLICITATION_UUID',
APCF_LOCAL_NAME: 'APCF_LOCAL_NAME',
APCF_MANUFACTURER_DATA: 'APCF_MANUFACTURER_DATA',
APCF_SERVICE_DATA: 'APCF_SERVICE_DATA',
APCF_TRANSPORT_DISCOVERY_SERVICE: 'APCF_TRANSPORT_DISCOVERY_SERVICE',
APCF_AD_TYPE_FILTER: 'APCF_AD_TYPE_FILTER',
APCF_READ_EXTENDED_FEATURES: 'APCF_READ_EXTENDED_FEATURES',
}
return_parameters_fields = [ @classmethod
('status', hci.STATUS_SPEC), def opcode_name(cls, opcode):
('opcode', Opcode.type_spec(1)), return name_or_number(cls.OPCODE_NAMES, opcode)
('payload', '*'),
]
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@hci.HCI_Command.command @HCI_Command.command(
@dataclasses.dataclass return_parameters_fields=[
class HCI_Get_Controller_Activity_Energy_Info_Command(hci.HCI_Command): ('status', STATUS_SPEC),
# pylint: disable=line-too-long
'''
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), ('total_tx_time_ms', 4),
('total_rx_time_ms', 4), ('total_rx_time_ms', 4),
('total_idle_time_ms', 4), ('total_idle_time_ms', 4),
('total_energy_used', 4), ('total_energy_used', 4),
] ],
)
class HCI_Get_Controller_Activity_Energy_Info_Command(HCI_Command):
# pylint: disable=line-too-long
'''
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_get_controller_activity_energy_info
'''
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@hci.HCI_Command.command @HCI_Command.command(
@dataclasses.dataclass fields=[
class HCI_A2DP_Hardware_Offload_Command(hci.HCI_Command): (
'opcode',
{
'size': 1,
'mapper': lambda x: HCI_A2DP_Hardware_Offload_Command.opcode_name(x),
},
),
('payload', '*'),
],
return_parameters_fields=[
('status', STATUS_SPEC),
(
'opcode',
{
'size': 1,
'mapper': lambda x: HCI_A2DP_Hardware_Offload_Command.opcode_name(x),
},
),
('payload', '*'),
],
)
class HCI_A2DP_Hardware_Offload_Command(HCI_Command):
# pylint: disable=line-too-long # pylint: disable=line-too-long
''' '''
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#a2dp-hardware-offload-support See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#a2dp-hardware-offload-support
@@ -160,24 +220,45 @@ class HCI_A2DP_Hardware_Offload_Command(hci.HCI_Command):
''' '''
# A2DP Hardware Offload Subcommands # A2DP Hardware Offload Subcommands
class Opcode(hci.SpecableEnum): # TODO: use the OpenIntEnum class (when upcoming PR is merged)
START_A2DP_OFFLOAD = 0x01 START_A2DP_OFFLOAD = 0x01
STOP_A2DP_OFFLOAD = 0x02 STOP_A2DP_OFFLOAD = 0x02
opcode: int = dataclasses.field(metadata=Opcode.type_metadata(1)) OPCODE_NAMES = {
payload: bytes = dataclasses.field(metadata=hci.metadata("*")) START_A2DP_OFFLOAD: 'START_A2DP_OFFLOAD',
STOP_A2DP_OFFLOAD: 'STOP_A2DP_OFFLOAD',
}
return_parameters_fields = [ @classmethod
('status', hci.STATUS_SPEC), def opcode_name(cls, opcode):
('opcode', Opcode.type_spec(1)), return name_or_number(cls.OPCODE_NAMES, opcode)
('payload', '*'),
]
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@hci.HCI_Command.command @HCI_Command.command(
@dataclasses.dataclass fields=[
class HCI_Dynamic_Audio_Buffer_Command(hci.HCI_Command): (
'opcode',
{
'size': 1,
'mapper': lambda x: HCI_Dynamic_Audio_Buffer_Command.opcode_name(x),
},
),
('payload', '*'),
],
return_parameters_fields=[
('status', STATUS_SPEC),
(
'opcode',
{
'size': 1,
'mapper': lambda x: HCI_Dynamic_Audio_Buffer_Command.opcode_name(x),
},
),
('payload', '*'),
],
)
class HCI_Dynamic_Audio_Buffer_Command(HCI_Command):
# pylint: disable=line-too-long # pylint: disable=line-too-long
''' '''
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#dynamic-audio-buffer-command See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#dynamic-audio-buffer-command
@@ -187,28 +268,27 @@ class HCI_Dynamic_Audio_Buffer_Command(hci.HCI_Command):
''' '''
# Dynamic Audio Buffer Subcommands # Dynamic Audio Buffer Subcommands
class Opcode(hci.SpecableEnum): # TODO: use the OpenIntEnum class (when upcoming PR is merged)
GET_AUDIO_BUFFER_TIME_CAPABILITY = 0x01 GET_AUDIO_BUFFER_TIME_CAPABILITY = 0x01
opcode: int = dataclasses.field(metadata=Opcode.type_metadata(1)) OPCODE_NAMES = {
payload: bytes = dataclasses.field(metadata=hci.metadata("*")) GET_AUDIO_BUFFER_TIME_CAPABILITY: 'GET_AUDIO_BUFFER_TIME_CAPABILITY',
}
return_parameters_fields = [ @classmethod
('status', hci.STATUS_SPEC), def opcode_name(cls, opcode):
('opcode', Opcode.type_spec(1)), return name_or_number(cls.OPCODE_NAMES, opcode)
('payload', '*'),
]
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class HCI_Android_Vendor_Event(hci.HCI_Extended_Event): class HCI_Android_Vendor_Event(HCI_Extended_Event):
event_code: int = hci.HCI_VENDOR_EVENT event_code: int = HCI_VENDOR_EVENT
subevent_classes: dict[int, type[hci.HCI_Extended_Event]] = {} subevent_classes: Dict[int, Type[HCI_Extended_Event]] = {}
@classmethod @classmethod
def subclass_from_parameters( def subclass_from_parameters(
cls, parameters: bytes cls, parameters: bytes
) -> Optional[hci.HCI_Extended_Event]: ) -> Optional[HCI_Extended_Event]:
subevent_code = parameters[0] subevent_code = parameters[0]
if subevent_code == HCI_BLUETOOTH_QUALITY_REPORT_EVENT: if subevent_code == HCI_BLUETOOTH_QUALITY_REPORT_EVENT:
quality_report_id = parameters[1] quality_report_id = parameters[1]
@@ -219,43 +299,45 @@ class HCI_Android_Vendor_Event(hci.HCI_Extended_Event):
HCI_Android_Vendor_Event.register_subevents(globals()) HCI_Android_Vendor_Event.register_subevents(globals())
hci.HCI_Event.add_vendor_factory(HCI_Android_Vendor_Event.subclass_from_parameters) HCI_Event.add_vendor_factory(HCI_Android_Vendor_Event.subclass_from_parameters)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@hci.HCI_Extended_Event.event @HCI_Extended_Event.event(
@dataclasses.dataclass fields=[
('quality_report_id', 1),
('packet_types', 1),
('connection_handle', 2),
('connection_role', {'size': 1, 'mapper': HCI_Constant.role_name}),
('tx_power_level', -1),
('rssi', -1),
('snr', 1),
('unused_afh_channel_count', 1),
('afh_select_unideal_channel_count', 1),
('lsto', 2),
('connection_piconet_clock', 4),
('retransmission_count', 4),
('no_rx_count', 4),
('nak_count', 4),
('last_tx_ack_timestamp', 4),
('flow_off_count', 4),
('last_flow_on_timestamp', 4),
('buffer_overflow_bytes', 4),
('buffer_underflow_bytes', 4),
('bdaddr', Address.parse_address),
('cal_failed_item_count', 1),
('tx_total_packets', 4),
('tx_unacked_packets', 4),
('tx_flushed_packets', 4),
('tx_last_subevent_packets', 4),
('crc_error_packets', 4),
('rx_duplicate_packets', 4),
('rx_unreceived_packets', 4),
('vendor_specific_parameters', '*'),
]
)
class HCI_Bluetooth_Quality_Report_Event(HCI_Android_Vendor_Event): class HCI_Bluetooth_Quality_Report_Event(HCI_Android_Vendor_Event):
# pylint: disable=line-too-long # pylint: disable=line-too-long
''' '''
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))
packet_types: int = field(metadata=hci.metadata(1))
connection_handle: int = field(metadata=hci.metadata(2))
connection_role: int = field(metadata=hci.Role.type_metadata(1))
tx_power_level: int = field(metadata=hci.metadata(-1))
rssi: int = field(metadata=hci.metadata(-1))
snr: int = field(metadata=hci.metadata(1))
unused_afh_channel_count: int = field(metadata=hci.metadata(1))
afh_select_unideal_channel_count: int = field(metadata=hci.metadata(1))
lsto: int = field(metadata=hci.metadata(2))
connection_piconet_clock: int = field(metadata=hci.metadata(4))
retransmission_count: int = field(metadata=hci.metadata(4))
no_rx_count: int = field(metadata=hci.metadata(4))
nak_count: int = field(metadata=hci.metadata(4))
last_tx_ack_timestamp: int = field(metadata=hci.metadata(4))
flow_off_count: int = field(metadata=hci.metadata(4))
last_flow_on_timestamp: int = field(metadata=hci.metadata(4))
buffer_overflow_bytes: int = field(metadata=hci.metadata(4))
buffer_underflow_bytes: int = field(metadata=hci.metadata(4))
bdaddr: hci.Address = field(metadata=hci.metadata(hci.Address.parse_address))
cal_failed_item_count: int = field(metadata=hci.metadata(1))
tx_total_packets: int = field(metadata=hci.metadata(4))
tx_unacked_packets: int = field(metadata=hci.metadata(4))
tx_flushed_packets: int = field(metadata=hci.metadata(4))
tx_last_subevent_packets: int = field(metadata=hci.metadata(4))
crc_error_packets: int = field(metadata=hci.metadata(4))
rx_duplicate_packets: int = field(metadata=hci.metadata(4))
rx_unreceived_packets: int = field(metadata=hci.metadata(4))
vendor_specific_parameters: bytes = field(metadata=hci.metadata('*'))
+28 -33
View File
@@ -15,9 +15,11 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import dataclasses from bumble.hci import (
hci_vendor_command_op_code,
from bumble import hci HCI_Command,
STATUS_SPEC,
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -29,10 +31,10 @@ from bumble import hci
# #
# pylint: disable-next=line-too-long # pylint: disable-next=line-too-long
# See https://github.com/zephyrproject-rtos/zephyr/blob/main/include/zephyr/bluetooth/hci_vs.h # See https://github.com/zephyrproject-rtos/zephyr/blob/main/include/zephyr/bluetooth/hci_vs.h
HCI_WRITE_TX_POWER_LEVEL_COMMAND = hci.hci_vendor_command_op_code(0x000E) HCI_WRITE_TX_POWER_LEVEL_COMMAND = hci_vendor_command_op_code(0x000E)
HCI_READ_TX_POWER_LEVEL_COMMAND = hci.hci_vendor_command_op_code(0x000F) HCI_READ_TX_POWER_LEVEL_COMMAND = hci_vendor_command_op_code(0x000F)
hci.HCI_Command.register_commands(globals()) HCI_Command.register_commands(globals())
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -47,9 +49,16 @@ class TX_Power_Level_Command:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@hci.HCI_Command.command @HCI_Command.command(
@dataclasses.dataclass fields=[('handle_type', 1), ('connection_handle', 2), ('tx_power_level', -1)],
class HCI_Write_Tx_Power_Level_Command(hci.HCI_Command, TX_Power_Level_Command): return_parameters_fields=[
('status', STATUS_SPEC),
('handle_type', 1),
('connection_handle', 2),
('selected_tx_power_level', -1),
],
)
class HCI_Write_Tx_Power_Level_Command(HCI_Command, TX_Power_Level_Command):
''' '''
Write TX power level. See BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL in Write TX power level. See BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL in
https://github.com/zephyrproject-rtos/zephyr/blob/main/include/zephyr/bluetooth/hci_vs.h https://github.com/zephyrproject-rtos/zephyr/blob/main/include/zephyr/bluetooth/hci_vs.h
@@ -58,22 +67,18 @@ class HCI_Write_Tx_Power_Level_Command(hci.HCI_Command, TX_Power_Level_Command):
TX_POWER_HANDLE_TYPE_SCAN should be zero. TX_POWER_HANDLE_TYPE_SCAN should be zero.
''' '''
handle_type: int = dataclasses.field(metadata=hci.metadata(1))
connection_handle: int = dataclasses.field(metadata=hci.metadata(2))
tx_power_level: int = dataclasses.field(metadata=hci.metadata(-1))
return_parameters_fields = [
('status', hci.STATUS_SPEC),
('handle_type', 1),
('connection_handle', 2),
('selected_tx_power_level', -1),
]
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@hci.HCI_Command.command @HCI_Command.command(
@dataclasses.dataclass fields=[('handle_type', 1), ('connection_handle', 2)],
class HCI_Read_Tx_Power_Level_Command(hci.HCI_Command, TX_Power_Level_Command): return_parameters_fields=[
('status', STATUS_SPEC),
('handle_type', 1),
('connection_handle', 2),
('tx_power_level', -1),
],
)
class HCI_Read_Tx_Power_Level_Command(HCI_Command, TX_Power_Level_Command):
''' '''
Read TX power level. See BT_HCI_OP_VS_READ_TX_POWER_LEVEL in Read TX power level. See BT_HCI_OP_VS_READ_TX_POWER_LEVEL in
https://github.com/zephyrproject-rtos/zephyr/blob/main/include/zephyr/bluetooth/hci_vs.h https://github.com/zephyrproject-rtos/zephyr/blob/main/include/zephyr/bluetooth/hci_vs.h
@@ -81,13 +86,3 @@ class HCI_Read_Tx_Power_Level_Command(hci.HCI_Command, TX_Power_Level_Command):
Power level is in dB. Connection handle for TX_POWER_HANDLE_TYPE_ADV and Power level is in dB. Connection handle for TX_POWER_HANDLE_TYPE_ADV and
TX_POWER_HANDLE_TYPE_SCAN should be zero. TX_POWER_HANDLE_TYPE_SCAN should be zero.
''' '''
handle_type: int = dataclasses.field(metadata=hci.metadata(1))
connection_handle: int = dataclasses.field(metadata=hci.metadata(2))
return_parameters_fields = [
('status', hci.STATUS_SPEC),
('handle_type', 1),
('connection_handle', 2),
('tx_power_level', -1),
]
+1
View File
@@ -57,6 +57,7 @@ nav:
- Pair: apps_and_tools/pair.md - Pair: apps_and_tools/pair.md
- Unbond: apps_and_tools/unbond.md - Unbond: apps_and_tools/unbond.md
- USB Probe: apps_and_tools/usb_probe.md - USB Probe: apps_and_tools/usb_probe.md
- Link Relay: apps_and_tools/link_relay.md
- Hardware: - Hardware:
- hardware/index.md - hardware/index.md
- Platforms: - Platforms:
+1
View File
@@ -13,3 +13,4 @@ These include:
* [Golden Gate Bridge](gg_bridge.md) - Bridge between GATT and UDP to use with the Golden Gate "stack tool". * [Golden Gate Bridge](gg_bridge.md) - Bridge between GATT and UDP to use with the Golden Gate "stack tool".
* [Show](show.md) - Parse a file with HCI packets and print the details of each packet in a human readable form. * [Show](show.md) - Parse a file with HCI packets and print the details of each packet in a human readable form.
* [Speaker](speaker.md) - Virtual Bluetooth speaker, with a command line and browser-based UI. * [Speaker](speaker.md) - Virtual Bluetooth speaker, with a command line and browser-based UI.
* [Link Relay](link_relay.md) - WebSocket relay for virtual RemoteLink instances to communicate with each other.
@@ -0,0 +1,33 @@
LINK RELAY TOOL
===============
The Link Relay is a WebSocket relay, which acts like an online chat system, where each "chat room" can be joined by multiple virtual controllers, which can then communicate with each other, as if connected with radio communication.
```
usage: python link_relay.py [-h] [--log-level LOG_LEVEL] [--log-config LOG_CONFIG] [--port PORT]
optional arguments:
-h, --help show this help message and exit
--log-level LOG_LEVEL
logger level
--log-config LOG_CONFIG
logger config file (YAML)
--port PORT Port to listen on
```
(the default port is `10723`)
When running, the link relay waits for connections on its listening port.
The WebSocket path used by a connecting client indicates which virtual "chat room" to join.
!!! tip "Connecting to the relay as a controller"
Most of the examples and tools that take a transport moniker as an argument also accept a link relay moniker, which is equivalent to a transport to a virtual controller that is connected to a relay.
The moniker syntax is: `link-relay:ws://<hostname>/<room>` where `<hostname>` is the hostname to connect to and `<room>` is the virtual "chat room" in a relay.
Example: `link-relay:ws://localhost:10723/test` will join the `test` "chat room"
!!! tip "Connecting to the relay as an observer"
It is possible to connect to a "chat room" in a relay as an observer, rather than a virtual controller. In this case, a text-based console can be used to observe what is going on in the "chat room". Tools like [`wscat`](https://github.com/websockets/wscat#readme) or [`websocat`](https://github.com/vi/websocat) can be used for that.
Example: `wscat --connect ws://localhost:10723/test`
+7
View File
@@ -56,6 +56,13 @@ Included in the project are two types of Link interface implementations:
The LocalLink implementation is a simple object used by an application that instantiates The LocalLink implementation is a simple object used by an application that instantiates
more than one Controller objects and connects them in-memory and in-process. more than one Controller objects and connects them in-memory and in-process.
#### Remote Link
The RemoteLink implementation communicates with other virtual controllers over a WebSocket.
Multiple instances of RemoteLink objects communicate with each other through a simple
WebSocket relay that can host any number of virtual 'rooms', where each 'room' is
a set of controllers that can communicate between themselves.
The `link_relay` app is where this relay is implemented.
## Host ## Host
The Host component connects to a controller over an HCI interface. It is responsible to sending commands and ACL data to the controller and receiving back events and ACL data. The Host component connects to a controller over an HCI interface. It is responsible to sending commands and ACL data to the controller and receiving back events and ACL data.
+2 -2
View File
@@ -24,7 +24,7 @@ import struct
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device from bumble.device import Device
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
from bumble.profiles.battery_service import BatteryService from bumble.profiles.battery_service import BatteryService
@@ -35,7 +35,7 @@ async def main() -> None:
print('example: python battery_server.py device1.json usb:0') print('example: python battery_server.py device1.json usb:0')
return return
async with await open_transport(sys.argv[2]) as hci_transport: async with await open_transport_or_link(sys.argv[2]) as hci_transport:
device = Device.from_config_file_with_hci( device = Device.from_config_file_with_hci(
sys.argv[1], hci_transport.source, hci_transport.sink sys.argv[1], hci_transport.source, hci_transport.sink
) )
+2 -2
View File
@@ -23,7 +23,7 @@ import struct
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device from bumble.device import Device
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
from bumble.profiles.device_information_service import DeviceInformationService from bumble.profiles.device_information_service import DeviceInformationService
@@ -34,7 +34,7 @@ async def main() -> None:
print('example: python device_info_server.py device1.json usb:0') print('example: python device_info_server.py device1.json usb:0')
return return
async with await open_transport(sys.argv[2]) as hci_transport: async with await open_transport_or_link(sys.argv[2]) as hci_transport:
device = Device.from_config_file_with_hci( device = Device.from_config_file_with_hci(
sys.argv[1], hci_transport.source, hci_transport.sink sys.argv[1], hci_transport.source, hci_transport.sink
) )
+2 -2
View File
@@ -26,7 +26,7 @@ import os
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device from bumble.device import Device
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
from bumble.profiles.device_information_service import DeviceInformationService from bumble.profiles.device_information_service import DeviceInformationService
from bumble.profiles.heart_rate_service import HeartRateService from bumble.profiles.heart_rate_service import HeartRateService
from bumble.utils import AsyncRunner from bumble.utils import AsyncRunner
@@ -39,7 +39,7 @@ async def main() -> None:
print('example: python heart_rate_server.py device1.json usb:0') print('example: python heart_rate_server.py device1.json usb:0')
return return
async with await open_transport(sys.argv[2]) as hci_transport: async with await open_transport_or_link(sys.argv[2]) as hci_transport:
device = Device.from_config_file_with_hci( device = Device.from_config_file_with_hci(
sys.argv[1], hci_transport.source, hci_transport.sink sys.argv[1], hci_transport.source, hci_transport.sink
) )
+2 -2
View File
@@ -27,7 +27,7 @@ from bumble.colors import color
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device, Connection, Peer from bumble.device import Device, Connection, Peer
from bumble.utils import AsyncRunner from bumble.utils import AsyncRunner
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
from bumble.gatt import ( from bumble.gatt import (
Descriptor, Descriptor,
Service, Service,
@@ -434,7 +434,7 @@ async def main() -> None:
) )
return return
async with await open_transport(sys.argv[2]) as hci_transport: async with await open_transport_or_link(sys.argv[2]) as hci_transport:
# Create a device to manage the host # Create a device to manage the host
device = Device.from_config_file_with_hci( device = Device.from_config_file_with_hci(
sys.argv[1], hci_transport.source, hci_transport.sink sys.argv[1], hci_transport.source, hci_transport.sink
+2 -2
View File
@@ -22,7 +22,7 @@ import logging
from bumble.colors import color from bumble.colors import color
from bumble.device import Device from bumble.device import Device
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
from bumble.core import ( from bumble.core import (
PhysicalTransport, PhysicalTransport,
BT_AVDTP_PROTOCOL_ID, BT_AVDTP_PROTOCOL_ID,
@@ -146,7 +146,7 @@ async def main() -> None:
return return
print('<<< connecting to HCI...') print('<<< connecting to HCI...')
async with await open_transport(sys.argv[2]) as hci_transport: async with await open_transport_or_link(sys.argv[2]) as hci_transport:
print('<<< connected') print('<<< connected')
# Create a device # Create a device
+4 -4
View File
@@ -19,10 +19,10 @@ import asyncio
import sys import sys
import os import os
import logging import logging
from typing import Any from typing import Any, Dict
from bumble.device import Device from bumble.device import Device
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
from bumble.core import PhysicalTransport from bumble.core import PhysicalTransport
from bumble.avdtp import ( from bumble.avdtp import (
AVDTP_AUDIO_MEDIA_TYPE, AVDTP_AUDIO_MEDIA_TYPE,
@@ -36,7 +36,7 @@ from bumble.a2dp import (
SbcMediaCodecInformation, SbcMediaCodecInformation,
) )
Context: dict[Any, Any] = {'output': None} Context: Dict[Any, Any] = {'output': None}
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -112,7 +112,7 @@ async def main() -> None:
return return
print('<<< connecting to HCI...') print('<<< connecting to HCI...')
async with await open_transport(sys.argv[2]) as hci_transport: async with await open_transport_or_link(sys.argv[2]) as hci_transport:
print('<<< connected') print('<<< connected')
with open(sys.argv[3], 'wb') as sbc_file: with open(sys.argv[3], 'wb') as sbc_file:
+152 -52
View File
@@ -16,28 +16,43 @@
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
import asyncio import asyncio
import sys
import os
import logging import logging
import os
import sys
from dataclasses import dataclass
from bumble.colors import color import ffmpeg
from bumble.device import Device
from bumble.transport import open_transport from bumble.a2dp import (
from bumble.core import PhysicalTransport A2DP_MPEG_2_4_AAC_CODEC_TYPE,
A2DP_SBC_CODEC_TYPE,
AacMediaCodecInformation,
AacPacketSource,
SbcMediaCodecInformation,
SbcPacketSource,
make_audio_source_service_sdp_records,
)
from bumble.avdtp import ( from bumble.avdtp import (
find_avdtp_service_with_connection,
AVDTP_AUDIO_MEDIA_TYPE, AVDTP_AUDIO_MEDIA_TYPE,
Listener,
MediaCodecCapabilities, MediaCodecCapabilities,
MediaPacketPump, MediaPacketPump,
Protocol, Protocol,
Listener, find_avdtp_service_with_connection,
)
from bumble.a2dp import (
make_audio_source_service_sdp_records,
A2DP_SBC_CODEC_TYPE,
SbcMediaCodecInformation,
SbcPacketSource,
) )
from bumble.colors import color
from bumble.core import PhysicalTransport
from bumble.device import Device
from bumble.transport import open_transport_or_link
from typing import Dict, Union
@dataclass
class CodecCapabilities:
name: str
sample_rate: str
number_of_channels: str
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -51,67 +66,147 @@ def sdp_records():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def codec_capabilities(): def on_avdtp_connection(
# NOTE: this shouldn't be hardcoded, but should be inferred from the input file read_function, protocol, codec_capabilities: MediaCodecCapabilities
# instead ):
return MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_SBC_CODEC_TYPE,
media_codec_information=SbcMediaCodecInformation(
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_44100,
channel_mode=SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
block_length=SbcMediaCodecInformation.BlockLength.BL_16,
subbands=SbcMediaCodecInformation.Subbands.S_8,
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
minimum_bitpool_value=2,
maximum_bitpool_value=53,
),
)
# -----------------------------------------------------------------------------
def on_avdtp_connection(read_function, protocol):
packet_source = SbcPacketSource(read_function, protocol.l2cap_channel.peer_mtu) packet_source = SbcPacketSource(read_function, protocol.l2cap_channel.peer_mtu)
packet_pump = MediaPacketPump(packet_source.packets) packet_pump = MediaPacketPump(packet_source.packets)
protocol.add_source(codec_capabilities(), packet_pump) protocol.add_source(codec_capabilities, packet_pump)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def stream_packets(read_function, protocol): async def stream_packets(
read_function, protocol, codec_capabilities: MediaCodecCapabilities
):
# Discover all endpoints on the remote device # Discover all endpoints on the remote device
endpoints = await protocol.discover_remote_endpoints() endpoints = await protocol.discover_remote_endpoints()
for endpoint in endpoints: for endpoint in endpoints:
print('@@@', endpoint) print('@@@', endpoint)
# Select a sink # Select a sink
assert codec_capabilities.media_codec_type in [
A2DP_SBC_CODEC_TYPE,
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
]
sink = protocol.find_remote_sink_by_codec( sink = protocol.find_remote_sink_by_codec(
AVDTP_AUDIO_MEDIA_TYPE, A2DP_SBC_CODEC_TYPE AVDTP_AUDIO_MEDIA_TYPE, codec_capabilities.media_codec_type
) )
if sink is None: if sink is None:
print(color('!!! no SBC sink found', 'red')) print(color('!!! no Sink found', 'red'))
return return
print(f'### Selected sink: {sink.seid}') print(f'### Selected sink: {sink.seid}')
# Stream the packets # Stream the packets
packet_source = SbcPacketSource(read_function, protocol.l2cap_channel.peer_mtu) packet_sources = {
packet_pump = MediaPacketPump(packet_source.packets) A2DP_SBC_CODEC_TYPE: SbcPacketSource(
source = protocol.add_source(codec_capabilities(), packet_pump) read_function, protocol.l2cap_channel.peer_mtu
),
A2DP_MPEG_2_4_AAC_CODEC_TYPE: AacPacketSource(
read_function, protocol.l2cap_channel.peer_mtu
),
}
packet_source = packet_sources[codec_capabilities.media_codec_type]
packet_pump = MediaPacketPump(packet_source.packets) # type: ignore
source = protocol.add_source(codec_capabilities, packet_pump)
stream = await protocol.create_stream(source, sink) stream = await protocol.create_stream(source, sink)
await stream.start() await stream.start()
await asyncio.sleep(5) await asyncio.sleep(60)
await stream.stop()
await asyncio.sleep(5)
await stream.start()
await asyncio.sleep(5)
await stream.stop() await stream.stop()
await stream.close() await stream.close()
# -----------------------------------------------------------------------------
def fetch_codec_informations(filepath) -> MediaCodecCapabilities:
probe = ffmpeg.probe(filepath)
assert 'streams' in probe
streams = probe['streams']
if not streams or len(streams) > 1:
print(streams)
print(color('!!! file not supported', 'red'))
exit()
audio_stream = streams[0]
media_codec_type = None
media_codec_information: Union[
SbcMediaCodecInformation, AacMediaCodecInformation, None
] = None
assert 'codec_name' in audio_stream
codec_name: str = audio_stream['codec_name']
if codec_name == "sbc":
media_codec_type = A2DP_SBC_CODEC_TYPE
sbc_sampling_frequency: Dict[
str, SbcMediaCodecInformation.SamplingFrequency
] = {
'16000': SbcMediaCodecInformation.SamplingFrequency.SF_16000,
'32000': SbcMediaCodecInformation.SamplingFrequency.SF_32000,
'44100': SbcMediaCodecInformation.SamplingFrequency.SF_44100,
'48000': SbcMediaCodecInformation.SamplingFrequency.SF_48000,
}
sbc_channel_mode: Dict[int, SbcMediaCodecInformation.ChannelMode] = {
1: SbcMediaCodecInformation.ChannelMode.MONO,
2: SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
}
assert 'sample_rate' in audio_stream
assert 'channels' in audio_stream
media_codec_information = SbcMediaCodecInformation(
sampling_frequency=sbc_sampling_frequency[audio_stream['sample_rate']],
channel_mode=sbc_channel_mode[audio_stream['channels']],
block_length=SbcMediaCodecInformation.BlockLength.BL_16,
subbands=SbcMediaCodecInformation.Subbands.S_8,
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
minimum_bitpool_value=2,
maximum_bitpool_value=53,
)
elif codec_name == "aac":
media_codec_type = A2DP_MPEG_2_4_AAC_CODEC_TYPE
object_type: Dict[str, AacMediaCodecInformation.ObjectType] = {
'LC': AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
'LTP': AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP,
'SSR': AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE,
}
aac_sampling_frequency: Dict[
str, AacMediaCodecInformation.SamplingFrequency
] = {
'44100': AacMediaCodecInformation.SamplingFrequency.SF_44100,
'48000': AacMediaCodecInformation.SamplingFrequency.SF_48000,
}
aac_channel_mode: Dict[int, AacMediaCodecInformation.Channels] = {
1: AacMediaCodecInformation.Channels.MONO,
2: AacMediaCodecInformation.Channels.STEREO,
}
assert 'profile' in audio_stream
assert 'sample_rate' in audio_stream
assert 'channels' in audio_stream
media_codec_information = AacMediaCodecInformation(
object_type=object_type[audio_stream['profile']],
sampling_frequency=aac_sampling_frequency[audio_stream['sample_rate']],
channels=aac_channel_mode[audio_stream['channels']],
vbr=1,
bitrate=128000,
)
else:
print(color('!!! codec not supported, only aac & sbc are supported', 'red'))
exit()
assert media_codec_type is not None
assert media_codec_information is not None
return MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=media_codec_type,
media_codec_information=media_codec_information,
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def main() -> None: async def main() -> None:
if len(sys.argv) < 4: if len(sys.argv) < 4:
print( print(
'Usage: run_a2dp_source.py <device-config> <transport-spec> <sbc-file> ' 'Usage: run_a2dp_source.py <device-config> <transport-spec> <audio-file> '
'[<bluetooth-address>]' '[<bluetooth-address>]'
) )
print( print(
@@ -120,7 +215,7 @@ async def main() -> None:
return return
print('<<< connecting to HCI...') print('<<< connecting to HCI...')
async with await open_transport(sys.argv[2]) as hci_transport: async with await open_transport_or_link(sys.argv[2]) as hci_transport:
print('<<< connected') print('<<< connected')
# Create a device # Create a device
@@ -135,11 +230,13 @@ async def main() -> None:
# Start # Start
await device.power_on() await device.power_on()
with open(sys.argv[3], 'rb') as sbc_file: with open(sys.argv[3], 'rb') as audio_file:
# NOTE: this should be using asyncio file reading, but blocking reads are # NOTE: this should be using asyncio file reading, but blocking reads are
# good enough for testing # good enough for testing
async def read(byte_count): async def read(byte_count):
return sbc_file.read(byte_count) return audio_file.read(byte_count)
codec_capabilities = fetch_codec_informations(sys.argv[3])
if len(sys.argv) > 4: if len(sys.argv) > 4:
# Connect to a peer # Connect to a peer
@@ -170,12 +267,15 @@ async def main() -> None:
protocol = await Protocol.connect(connection, avdtp_version) protocol = await Protocol.connect(connection, avdtp_version)
# Start streaming # Start streaming
await stream_packets(read, protocol) await stream_packets(read, protocol, codec_capabilities)
else: else:
# Create a listener to wait for AVDTP connections # Create a listener to wait for AVDTP connections
listener = Listener.for_device(device=device, version=(1, 2)) listener = Listener.for_device(device=device, version=(1, 2))
listener.on( listener.on(
'connection', lambda protocol: on_avdtp_connection(read, protocol) 'connection',
lambda protocol: on_avdtp_connection(
read, protocol, codec_capabilities
),
) )
# Become connectable and wait for a connection # Become connectable and wait for a connection
+2 -2
View File
@@ -24,7 +24,7 @@ import struct
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import AdvertisingType, Device from bumble.device import AdvertisingType, Device
from bumble.hci import Address from bumble.hci import Address
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -50,7 +50,7 @@ async def main() -> None:
target = None target = None
print('<<< connecting to HCI...') print('<<< connecting to HCI...')
async with await open_transport(sys.argv[2]) as hci_transport: async with await open_transport_or_link(sys.argv[2]) as hci_transport:
print('<<< connected') print('<<< connected')
device = Device.from_config_file_with_hci( device = Device.from_config_file_with_hci(
+1
View File
@@ -25,6 +25,7 @@ from bumble.device import Device, Peer
from bumble.transport import open_transport from bumble.transport import open_transport
from bumble.profiles.ancs import ( from bumble.profiles.ancs import (
AncsClient, AncsClient,
AppAttribute,
AppAttributeId, AppAttributeId,
EventFlags, EventFlags,
EventId, EventId,
+2 -2
View File
@@ -27,7 +27,7 @@ from bumble import decoder
from bumble import gatt from bumble import gatt
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device, AdvertisingParameters from bumble.device import Device, AdvertisingParameters
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
from bumble.profiles import asha from bumble.profiles import asha
ws_connection: Optional[websockets.WebSocketServerProtocol] = None ws_connection: Optional[websockets.WebSocketServerProtocol] = None
@@ -50,7 +50,7 @@ async def main() -> None:
print('example: python run_asha_sink.py device1.json usb:0') print('example: python run_asha_sink.py device1.json usb:0')
return return
async with await open_transport(sys.argv[2]) as hci_transport: async with await open_transport_or_link(sys.argv[2]) as hci_transport:
device = Device.from_config_file_with_hci( device = Device.from_config_file_with_hci(
sys.argv[1], hci_transport.source, hci_transport.sink sys.argv[1], hci_transport.source, hci_transport.sink
) )
+2 -2
View File
@@ -24,7 +24,7 @@ import logging
import websockets import websockets
from bumble.device import Device from bumble.device import Device
from bumble.transport import open_transport from bumble.transport import open_transport_or_link
from bumble.core import PhysicalTransport from bumble.core import PhysicalTransport
from bumble import avc from bumble import avc
from bumble import avrcp from bumble import avrcp
@@ -344,7 +344,7 @@ async def main() -> None:
return return
print('<<< connecting to HCI...') print('<<< connecting to HCI...')
async with await open_transport(sys.argv[2]) as hci_transport: async with await open_transport_or_link(sys.argv[2]) as hci_transport:
print('<<< connected') print('<<< connected')
# Create a device # Create a device

Some files were not shown because too many files have changed in this diff Show More