forked from auracaster/bumble_mirror
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43a8cc37f8 | |||
| 1c1b947455 | |||
| d7ddffd275 | |||
| 3cb97d2373 | |||
| bad037b010 | |||
| 88777710a4 | |||
| 0ab5b6c49a | |||
| 22ff0d5e32 | |||
| 2f5de37d76 | |||
| 799d730f88 | |||
| 1a05eebfdb | |||
| ebaa720e74 | |||
| a505badffc | |||
| 45d938c901 | |||
| a0498af626 | |||
| bf027cf38f | |||
| f2d7faa9af | |||
| a0248a1cdf | |||
| 630243e243 | |||
| 60e31884c8 |
+6
-4
@@ -128,7 +128,7 @@ class BroadcastScanner(bumble.utils.EventEmitter):
|
||||
broadcast_audio_announcement: Optional[bap.BroadcastAudioAnnouncement] = None
|
||||
basic_audio_announcement: Optional[bap.BasicAudioAnnouncement] = 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
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -255,8 +255,10 @@ class BroadcastScanner(bumble.utils.EventEmitter):
|
||||
print(color(' SDU Interval: ', 'magenta'), self.biginfo.sdu_interval)
|
||||
print(color(' Max SDU: ', 'magenta'), self.biginfo.max_sdu)
|
||||
print(color(' PHY: ', 'magenta'), self.biginfo.phy.name)
|
||||
print(color(' Framed: ', 'magenta'), self.biginfo.framed)
|
||||
print(color(' Encrypted: ', 'magenta'), self.biginfo.encrypted)
|
||||
print(color(' Framing: ', 'magenta'), self.biginfo.framing.name)
|
||||
print(
|
||||
color(' Encryption: ', 'magenta'), self.biginfo.encryption.name
|
||||
)
|
||||
|
||||
def on_sync_establishment(self) -> None:
|
||||
self.emit('sync_establishment')
|
||||
@@ -286,7 +288,7 @@ class BroadcastScanner(bumble.utils.EventEmitter):
|
||||
self.emit('change')
|
||||
|
||||
def on_biginfo_advertisement(
|
||||
self, advertisement: bumble.device.BIGInfoAdvertisement
|
||||
self, advertisement: bumble.device.BigInfoAdvertisement
|
||||
) -> None:
|
||||
self.biginfo = advertisement
|
||||
self.emit('change')
|
||||
|
||||
+360
-13
@@ -36,7 +36,15 @@ from bumble.core import (
|
||||
CommandTimeoutError,
|
||||
)
|
||||
from bumble.colors import color
|
||||
from bumble.device import Connection, ConnectionParametersPreferences, Device, Peer
|
||||
from bumble.core import ConnectionPHY
|
||||
from bumble.device import (
|
||||
CigParameters,
|
||||
CisLink,
|
||||
Connection,
|
||||
ConnectionParametersPreferences,
|
||||
Device,
|
||||
Peer,
|
||||
)
|
||||
from bumble.gatt import Characteristic, CharacteristicValue, Service
|
||||
from bumble.hci import (
|
||||
HCI_LE_1M_PHY,
|
||||
@@ -46,6 +54,7 @@ from bumble.hci import (
|
||||
HCI_Constant,
|
||||
HCI_Error,
|
||||
HCI_StatusError,
|
||||
HCI_IsoDataPacket,
|
||||
)
|
||||
from bumble.sdp import (
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
@@ -83,11 +92,21 @@ SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53'
|
||||
SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D'
|
||||
|
||||
DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'
|
||||
|
||||
DEFAULT_L2CAP_PSM = 128
|
||||
DEFAULT_L2CAP_MAX_CREDITS = 128
|
||||
DEFAULT_L2CAP_MTU = 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_POST_CONNECTION_WAIT_TIME = 1.0
|
||||
|
||||
@@ -104,14 +123,14 @@ def le_phy_name(phy_id):
|
||||
)
|
||||
|
||||
|
||||
def print_connection_phy(phy):
|
||||
def print_connection_phy(phy: ConnectionPHY) -> None:
|
||||
logging.info(
|
||||
color('@@@ PHY: ', 'yellow') + f'TX:{le_phy_name(phy.tx_phy)}/'
|
||||
f'RX:{le_phy_name(phy.rx_phy)}'
|
||||
)
|
||||
|
||||
|
||||
def print_connection(connection):
|
||||
def print_connection(connection: Connection) -> None:
|
||||
params = []
|
||||
if connection.transport == PhysicalTransport.LE:
|
||||
params.append(
|
||||
@@ -136,6 +155,34 @@ def print_connection(connection):
|
||||
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):
|
||||
return {
|
||||
0x00010001: [
|
||||
@@ -461,7 +508,8 @@ class Sender:
|
||||
self.bytes_sent += len(packet)
|
||||
await self.packet_io.send_packet(packet)
|
||||
|
||||
await self.done.wait()
|
||||
if self.packet_io.can_receive():
|
||||
await self.done.wait()
|
||||
|
||||
run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
|
||||
logging.info(color(f'=== {run_counter} Done!', 'magenta'))
|
||||
@@ -491,6 +539,9 @@ class Sender:
|
||||
)
|
||||
self.done.set()
|
||||
|
||||
def is_sender(self):
|
||||
return True
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Receiver
|
||||
@@ -538,7 +589,8 @@ class Receiver:
|
||||
logging.info(
|
||||
color(
|
||||
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
||||
f'but received {packet.sequence}'
|
||||
f'but received {packet.sequence}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
|
||||
@@ -581,6 +633,9 @@ class Receiver:
|
||||
await self.done.wait()
|
||||
logging.info(color('=== Done!', 'magenta'))
|
||||
|
||||
def is_sender(self):
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Ping
|
||||
@@ -716,7 +771,8 @@ class Ping:
|
||||
color(
|
||||
f'!!! Unexpected packet, '
|
||||
f'expected {self.next_expected_packet_index} '
|
||||
f'but received {packet.sequence}'
|
||||
f'but received {packet.sequence}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
|
||||
@@ -724,6 +780,9 @@ class Ping:
|
||||
self.done.set()
|
||||
return
|
||||
|
||||
def is_sender(self):
|
||||
return True
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Pong
|
||||
@@ -768,7 +827,8 @@ class Pong:
|
||||
logging.info(
|
||||
color(
|
||||
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
||||
f'but received {packet.sequence}'
|
||||
f'but received {packet.sequence}',
|
||||
'red',
|
||||
)
|
||||
)
|
||||
|
||||
@@ -790,6 +850,9 @@ class Pong:
|
||||
await self.done.wait()
|
||||
logging.info(color('=== Done!', 'magenta'))
|
||||
|
||||
def is_sender(self):
|
||||
return False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GattClient
|
||||
@@ -953,6 +1016,9 @@ class StreamedPacketIO:
|
||||
# pylint: disable-next=not-callable
|
||||
self.io_sink(struct.pack('>H', len(packet)) + packet)
|
||||
|
||||
def can_receive(self):
|
||||
return True
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# L2capClient
|
||||
@@ -1224,6 +1290,96 @@ class RfcommServer(StreamedPacketIO):
|
||||
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
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1232,13 +1388,22 @@ class Central(Connection.Listener):
|
||||
self,
|
||||
transport,
|
||||
peripheral_address,
|
||||
classic,
|
||||
scenario_factory,
|
||||
mode_factory,
|
||||
connection_interval,
|
||||
phy,
|
||||
authenticate,
|
||||
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,
|
||||
role_switch,
|
||||
le_scan,
|
||||
@@ -1250,6 +1415,15 @@ class Central(Connection.Listener):
|
||||
self.transport = transport
|
||||
self.peripheral_address = peripheral_address
|
||||
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.mode_factory = mode_factory
|
||||
self.authenticate = authenticate
|
||||
@@ -1308,6 +1482,13 @@ class Central(Connection.Listener):
|
||||
)
|
||||
mode = self.mode_factory(self.device)
|
||||
scenario = self.scenario_factory(mode)
|
||||
self.device.classic_enabled = self.classic
|
||||
self.device.cis_enabled = self.iso
|
||||
|
||||
# Set up a pairing config factory with minimal requirements.
|
||||
self.device.pairing_config_factory = lambda _: PairingConfig(
|
||||
sc=False, mitm=False, bonding=False
|
||||
)
|
||||
|
||||
await pre_power_on(self.device, self.classic)
|
||||
await self.device.power_on()
|
||||
@@ -1392,7 +1573,72 @@ class Central(Connection.Listener):
|
||||
)
|
||||
)
|
||||
|
||||
await mode.on_connection(self.connection)
|
||||
# Setup ISO streams.
|
||||
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 asyncio.sleep(DEFAULT_LINGER_TIME)
|
||||
@@ -1428,6 +1674,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
scenario_factory,
|
||||
mode_factory,
|
||||
classic,
|
||||
iso,
|
||||
extended_data_length,
|
||||
role_switch,
|
||||
le_scan,
|
||||
@@ -1437,6 +1684,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
):
|
||||
self.transport = transport
|
||||
self.classic = classic
|
||||
self.iso = iso
|
||||
self.scenario_factory = scenario_factory
|
||||
self.mode_factory = mode_factory
|
||||
self.extended_data_length = extended_data_length
|
||||
@@ -1470,6 +1718,13 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
self.device.listener = self
|
||||
self.mode = self.mode_factory(self.device)
|
||||
self.scenario = self.scenario_factory(self.mode)
|
||||
self.device.classic_enabled = self.classic
|
||||
self.device.cis_enabled = self.iso
|
||||
|
||||
# Set up a pairing config factory with minimal requirements.
|
||||
self.device.pairing_config_factory = lambda _: PairingConfig(
|
||||
sc=False, mitm=False, bonding=False
|
||||
)
|
||||
|
||||
await pre_power_on(self.device, self.classic)
|
||||
await self.device.power_on()
|
||||
@@ -1501,7 +1756,21 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
logging.info(color('### Connected', 'cyan'))
|
||||
print_connection(self.connection)
|
||||
|
||||
await self.mode.on_connection(self.connection)
|
||||
if self.iso:
|
||||
|
||||
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 asyncio.sleep(DEFAULT_LINGER_TIME)
|
||||
|
||||
@@ -1613,6 +1882,12 @@ def create_mode_factory(ctx, default_mode):
|
||||
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')
|
||||
|
||||
return create_mode
|
||||
@@ -1640,6 +1915,9 @@ def create_scenario_factory(ctx, default_scenario):
|
||||
return Receiver(packet_io, ctx.obj['linger'])
|
||||
|
||||
if scenario == 'ping':
|
||||
if isinstance(packet_io, (IsoClient, IsoServer)):
|
||||
raise ValueError('ping not supported with ISO')
|
||||
|
||||
return Ping(
|
||||
packet_io,
|
||||
start_delay=ctx.obj['start_delay'],
|
||||
@@ -1651,6 +1929,9 @@ def create_scenario_factory(ctx, default_scenario):
|
||||
)
|
||||
|
||||
if scenario == 'pong':
|
||||
if isinstance(packet_io, (IsoClient, IsoServer)):
|
||||
raise ValueError('pong not supported with ISO')
|
||||
|
||||
return Pong(packet_io, ctx.obj['linger'])
|
||||
|
||||
raise ValueError('invalid scenario')
|
||||
@@ -1674,6 +1955,8 @@ def create_scenario_factory(ctx, default_scenario):
|
||||
'l2cap-server',
|
||||
'rfcomm-client',
|
||||
'rfcomm-server',
|
||||
'iso-client',
|
||||
'iso-server',
|
||||
]
|
||||
),
|
||||
)
|
||||
@@ -1896,6 +2179,7 @@ def bench(
|
||||
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['iso'] = mode in ('iso-client', 'iso-server')
|
||||
|
||||
|
||||
@bench.command()
|
||||
@@ -1917,26 +2201,88 @@ def bench(
|
||||
@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('--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
|
||||
def central(
|
||||
ctx, transport, peripheral_address, connection_interval, phy, authenticate, encrypt
|
||||
ctx,
|
||||
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)"""
|
||||
scenario_factory = create_scenario_factory(ctx, 'send')
|
||||
mode_factory = create_mode_factory(ctx, 'gatt-client')
|
||||
classic = ctx.obj['classic']
|
||||
|
||||
async def run_central():
|
||||
await Central(
|
||||
transport,
|
||||
peripheral_address,
|
||||
classic,
|
||||
scenario_factory,
|
||||
mode_factory,
|
||||
connection_interval,
|
||||
phy,
|
||||
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['role_switch'],
|
||||
ctx.obj['le_scan'],
|
||||
@@ -1962,6 +2308,7 @@ def peripheral(ctx, transport):
|
||||
scenario_factory,
|
||||
mode_factory,
|
||||
ctx.obj['classic'],
|
||||
ctx.obj['iso'],
|
||||
ctx.obj['extended_data_length'],
|
||||
ctx.obj['role_switch'],
|
||||
ctx.obj['le_scan'],
|
||||
|
||||
+45
-8
@@ -242,7 +242,9 @@ async def get_codecs_info(host: Host) -> None:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main(latency_probes, transport):
|
||||
async def async_main(
|
||||
latency_probes, latency_probe_interval, latency_probe_command, transport
|
||||
):
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
@@ -251,19 +253,32 @@ async def async_main(latency_probes, transport):
|
||||
await host.reset()
|
||||
|
||||
# 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 = []
|
||||
if latency_probes:
|
||||
for _ in range(latency_probes):
|
||||
if latency_probe_command:
|
||||
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()
|
||||
await host.send_command(HCI_Read_Local_Version_Information_Command())
|
||||
latencies.append(1000 * (time.time() - start))
|
||||
await host.send_command(probe_hci_command)
|
||||
if iteration:
|
||||
latencies.append(1000 * (time.time() - start))
|
||||
print(
|
||||
color('HCI Command Latency:', 'yellow'),
|
||||
(
|
||||
f'min={min(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',
|
||||
)
|
||||
|
||||
@@ -311,10 +326,32 @@ async def async_main(latency_probes, transport):
|
||||
type=int,
|
||||
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')
|
||||
def main(latency_probes, transport):
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
asyncio.run(async_main(latency_probes, transport))
|
||||
def main(latency_probes, latency_probe_interval, latency_probe_command, transport):
|
||||
logging.basicConfig(
|
||||
level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -194,7 +194,11 @@ class Loopback:
|
||||
)
|
||||
@click.argument('transport')
|
||||
def main(packet_size, packet_count, transport):
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||
logging.basicConfig(
|
||||
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)
|
||||
asyncio.run(loopback.run())
|
||||
|
||||
@@ -337,7 +337,12 @@ class Speaker:
|
||||
),
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes([AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG]),
|
||||
bytes(
|
||||
[
|
||||
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
|
||||
| AdvertisingData.BR_EDR_NOT_SUPPORTED_FLAG
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
|
||||
+17
-9
@@ -402,14 +402,19 @@ async def pair(
|
||||
# Create an OOB context if needed
|
||||
if oob:
|
||||
our_oob_context = OobContext()
|
||||
shared_data = (
|
||||
None
|
||||
if oob == '-'
|
||||
else OobData.from_ad(
|
||||
if oob == '-':
|
||||
shared_data = None
|
||||
legacy_context = OobLegacyContext()
|
||||
else:
|
||||
oob_data = OobData.from_ad(
|
||||
AdvertisingData.from_bytes(bytes.fromhex(oob))
|
||||
).shared_data
|
||||
)
|
||||
legacy_context = OobLegacyContext()
|
||||
)
|
||||
shared_data = oob_data.shared_data
|
||||
legacy_context = oob_data.legacy_context
|
||||
if legacy_context is None and not sc:
|
||||
print(color('OOB pairing in legacy mode requires TK', 'red'))
|
||||
return
|
||||
|
||||
oob_contexts = PairingConfig.OobConfig(
|
||||
our_context=our_oob_context,
|
||||
peer_data=shared_data,
|
||||
@@ -419,7 +424,9 @@ async def pair(
|
||||
print(color('@@@ OOB Data:', 'yellow'))
|
||||
if shared_data is None:
|
||||
oob_data = OobData(
|
||||
address=device.random_address, shared_data=our_oob_context.share()
|
||||
address=device.random_address,
|
||||
shared_data=our_oob_context.share(),
|
||||
legacy_context=(None if sc else legacy_context),
|
||||
)
|
||||
print(
|
||||
color(
|
||||
@@ -427,7 +434,8 @@ async def pair(
|
||||
'yellow',
|
||||
)
|
||||
)
|
||||
print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
|
||||
if legacy_context:
|
||||
print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
|
||||
print(color('@@@-----------------------------------', 'yellow'))
|
||||
else:
|
||||
oob_contexts = None
|
||||
|
||||
+18
-27
@@ -50,19 +50,10 @@ from bumble.sdp import (
|
||||
ServiceAttribute,
|
||||
)
|
||||
from bumble import utils
|
||||
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 core
|
||||
from bumble import l2cap
|
||||
from bumble import avc
|
||||
from bumble import avctp
|
||||
from bumble import utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -102,8 +93,8 @@ def make_controller_service_sdp_records(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.uuid(BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE),
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE),
|
||||
]
|
||||
),
|
||||
),
|
||||
@@ -113,13 +104,13 @@ def make_controller_service_sdp_records(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
|
||||
]
|
||||
),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AVCTP_PROTOCOL_ID),
|
||||
DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp_version_int),
|
||||
]
|
||||
),
|
||||
@@ -132,7 +123,7 @@ def make_controller_service_sdp_records(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.unsigned_integer_16(avrcp_version_int),
|
||||
]
|
||||
),
|
||||
@@ -170,7 +161,7 @@ def make_target_service_sdp_records(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AV_REMOTE_CONTROL_TARGET_SERVICE),
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_TARGET_SERVICE),
|
||||
]
|
||||
),
|
||||
),
|
||||
@@ -180,13 +171,13 @@ def make_target_service_sdp_records(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
|
||||
]
|
||||
),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AVCTP_PROTOCOL_ID),
|
||||
DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_16(avctp_version_int),
|
||||
]
|
||||
),
|
||||
@@ -199,7 +190,7 @@ def make_target_service_sdp_records(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
|
||||
DataElement.unsigned_integer_16(avrcp_version_int),
|
||||
]
|
||||
),
|
||||
@@ -1208,7 +1199,7 @@ class Protocol(utils.EventEmitter):
|
||||
def _delegate_command(
|
||||
self, transaction_label: int, command: Command, method: Awaitable
|
||||
) -> None:
|
||||
async def call():
|
||||
async def call() -> None:
|
||||
try:
|
||||
await method
|
||||
except Delegate.Error as error:
|
||||
@@ -1412,7 +1403,7 @@ class Protocol(utils.EventEmitter):
|
||||
def notify_track_changed(self, identifier: bytes) -> None:
|
||||
"""Notify the connected peer of a Track change."""
|
||||
if len(identifier) != 8:
|
||||
raise InvalidArgumentError("identifier must be 8 bytes")
|
||||
raise core.InvalidArgumentError("identifier must be 8 bytes")
|
||||
self.notify_event(TrackChangedEvent(identifier))
|
||||
|
||||
def notify_playback_position_changed(self, position: int) -> None:
|
||||
@@ -1679,7 +1670,7 @@ class Protocol(utils.EventEmitter):
|
||||
else:
|
||||
logger.debug("unexpected PDU ID")
|
||||
pending_command.response.set_exception(
|
||||
ProtocolError(
|
||||
core.ProtocolError(
|
||||
error_code=None,
|
||||
error_namespace="avrcp",
|
||||
details="unexpected PDU ID",
|
||||
@@ -1688,7 +1679,7 @@ class Protocol(utils.EventEmitter):
|
||||
else:
|
||||
logger.debug("unexpected response code")
|
||||
pending_command.response.set_exception(
|
||||
ProtocolError(
|
||||
core.ProtocolError(
|
||||
error_code=None,
|
||||
error_namespace="avrcp",
|
||||
details="unexpected response code",
|
||||
@@ -1866,12 +1857,12 @@ class Protocol(utils.EventEmitter):
|
||||
) -> None:
|
||||
logger.debug(f"<<< AVRCP command PDU: {command}")
|
||||
|
||||
async def get_supported_events():
|
||||
async def get_supported_events() -> None:
|
||||
if (
|
||||
command.capability_id
|
||||
!= GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED
|
||||
):
|
||||
raise Protocol.InvalidParameterError
|
||||
raise core.InvalidArgumentError()
|
||||
|
||||
supported_events = await self.delegate.get_supported_events()
|
||||
self.send_avrcp_response(
|
||||
@@ -1887,7 +1878,7 @@ class Protocol(utils.EventEmitter):
|
||||
) -> None:
|
||||
logger.debug(f"<<< AVRCP command PDU: {command}")
|
||||
|
||||
async def set_absolute_volume():
|
||||
async def set_absolute_volume() -> None:
|
||||
await self.delegate.set_absolute_volume(command.volume)
|
||||
effective_volume = await self.delegate.get_absolute_volume()
|
||||
self.send_avrcp_response(
|
||||
@@ -1903,7 +1894,7 @@ class Protocol(utils.EventEmitter):
|
||||
) -> None:
|
||||
logger.debug(f"<<< AVRCP command PDU: {command}")
|
||||
|
||||
async def register_notification():
|
||||
async def register_notification() -> None:
|
||||
# Check if the event is supported.
|
||||
supported_events = await self.delegate.get_supported_events()
|
||||
if command.event_id not in supported_events:
|
||||
|
||||
+15
-15
@@ -108,7 +108,9 @@ class Connection:
|
||||
def on_hci_acl_data_packet(self, packet):
|
||||
self.assembler.feed_packet(packet)
|
||||
self.controller.send_hci_packet(
|
||||
HCI_Number_Of_Completed_Packets_Event([(self.handle, 1)])
|
||||
HCI_Number_Of_Completed_Packets_Event(
|
||||
connection_handles=[self.handle], num_completed_packets=[1]
|
||||
)
|
||||
)
|
||||
|
||||
def on_acl_pdu(self, data):
|
||||
@@ -392,7 +394,7 @@ class Controller:
|
||||
peer_address=peer_address,
|
||||
link=self.link,
|
||||
transport=PhysicalTransport.LE,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
self.peripheral_connections[peer_address] = connection
|
||||
logger.debug(f'New PERIPHERAL connection handle: 0x{connection_handle:04X}')
|
||||
@@ -452,7 +454,7 @@ class Controller:
|
||||
peer_address=peer_address,
|
||||
link=self.link,
|
||||
transport=PhysicalTransport.LE,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
self.central_connections[peer_address] = connection
|
||||
logger.debug(
|
||||
@@ -542,15 +544,14 @@ class Controller:
|
||||
acl_packet = HCI_AclDataPacket(connection.handle, 2, 0, len(data), data)
|
||||
self.send_hci_packet(acl_packet)
|
||||
|
||||
def on_link_advertising_data(self, sender_address, data):
|
||||
def on_link_advertising_data(self, sender_address: Address, data: bytes):
|
||||
# Ignore if we're not scanning
|
||||
if self.le_scan_enable == 0:
|
||||
return
|
||||
|
||||
# Send a scan report
|
||||
report = HCI_LE_Advertising_Report_Event.Report(
|
||||
HCI_LE_Advertising_Report_Event.Report.FIELDS,
|
||||
event_type=HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||
event_type=HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
|
||||
address_type=sender_address.address_type,
|
||||
address=sender_address,
|
||||
data=data,
|
||||
@@ -560,8 +561,7 @@ class Controller:
|
||||
|
||||
# Simulate a scan response
|
||||
report = HCI_LE_Advertising_Report_Event.Report(
|
||||
HCI_LE_Advertising_Report_Event.Report.FIELDS,
|
||||
event_type=HCI_LE_Advertising_Report_Event.SCAN_RSP,
|
||||
event_type=HCI_LE_Advertising_Report_Event.EventType.SCAN_RSP,
|
||||
address_type=sender_address.address_type,
|
||||
address=sender_address,
|
||||
data=data,
|
||||
@@ -618,8 +618,8 @@ class Controller:
|
||||
cis_sync_delay=0,
|
||||
transport_latency_c_to_p=0,
|
||||
transport_latency_p_to_c=0,
|
||||
phy_c_to_p=0,
|
||||
phy_p_to_c=0,
|
||||
phy_c_to_p=1,
|
||||
phy_p_to_c=1,
|
||||
nse=0,
|
||||
bn_c_to_p=0,
|
||||
bn_p_to_c=0,
|
||||
@@ -695,7 +695,7 @@ class Controller:
|
||||
peer_address=peer_address,
|
||||
link=self.link,
|
||||
transport=PhysicalTransport.BR_EDR,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
self.classic_connections[peer_address] = connection
|
||||
logger.debug(
|
||||
@@ -709,7 +709,7 @@ class Controller:
|
||||
connection_handle=connection_handle,
|
||||
bd_addr=peer_address,
|
||||
encryption_enabled=False,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -720,7 +720,7 @@ class Controller:
|
||||
connection_handle=0,
|
||||
bd_addr=peer_address,
|
||||
encryption_enabled=False,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -945,7 +945,7 @@ class Controller:
|
||||
)
|
||||
)
|
||||
self.link.classic_sco_connect(
|
||||
self, connection.peer_address, HCI_Connection_Complete_Event.ESCO_LINK_TYPE
|
||||
self, connection.peer_address, HCI_Connection_Complete_Event.LinkType.ESCO
|
||||
)
|
||||
|
||||
def on_hci_enhanced_accept_synchronous_connection_request_command(self, command):
|
||||
@@ -974,7 +974,7 @@ class Controller:
|
||||
)
|
||||
)
|
||||
self.link.classic_accept_sco_connection(
|
||||
self, connection.peer_address, HCI_Connection_Complete_Event.ESCO_LINK_TYPE
|
||||
self, connection.peer_address, HCI_Connection_Complete_Event.LinkType.ESCO
|
||||
)
|
||||
|
||||
def on_hci_switch_role_command(self, command):
|
||||
|
||||
+288
-132
@@ -139,6 +139,9 @@ DEVICE_DEFAULT_ADVERTISING_TX_POWER = (
|
||||
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
|
||||
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT = 5.0
|
||||
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
|
||||
# pylint: enable=line-too-long
|
||||
@@ -201,25 +204,35 @@ class Advertisement:
|
||||
# -----------------------------------------------------------------------------
|
||||
class LegacyAdvertisement(Advertisement):
|
||||
@classmethod
|
||||
def from_advertising_report(cls, report):
|
||||
def from_advertising_report(
|
||||
cls, report: hci.HCI_LE_Advertising_Report_Event.Report
|
||||
) -> Self:
|
||||
return cls(
|
||||
address=report.address,
|
||||
rssi=report.rssi,
|
||||
is_legacy=True,
|
||||
is_connectable=report.event_type
|
||||
in (
|
||||
hci.HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||
hci.HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
|
||||
is_connectable=(
|
||||
report.event_type
|
||||
in (
|
||||
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
|
||||
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND,
|
||||
)
|
||||
),
|
||||
is_directed=report.event_type
|
||||
== hci.HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
|
||||
is_scannable=report.event_type
|
||||
in (
|
||||
hci.HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||
hci.HCI_LE_Advertising_Report_Event.ADV_SCAN_IND,
|
||||
is_directed=(
|
||||
report.event_type
|
||||
== hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND
|
||||
),
|
||||
is_scannable=(
|
||||
report.event_type
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -227,18 +240,20 @@ class LegacyAdvertisement(Advertisement):
|
||||
# -----------------------------------------------------------------------------
|
||||
class ExtendedAdvertisement(Advertisement):
|
||||
@classmethod
|
||||
def from_advertising_report(cls, report):
|
||||
def from_advertising_report(
|
||||
cls, report: hci.HCI_LE_Extended_Advertising_Report_Event.Report
|
||||
) -> Self:
|
||||
# fmt: off
|
||||
# pylint: disable=line-too-long
|
||||
return cls(
|
||||
address = report.address,
|
||||
rssi = report.rssi,
|
||||
is_legacy = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.LEGACY_ADVERTISING_PDU_USED) != 0,
|
||||
is_legacy = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.LEGACY_ADVERTISING_PDU_USED) != 0,
|
||||
is_anonymous = report.address.address_type == hci.HCI_LE_Extended_Advertising_Report_Event.ANONYMOUS_ADDRESS_TYPE,
|
||||
is_connectable = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.CONNECTABLE_ADVERTISING) != 0,
|
||||
is_directed = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.DIRECTED_ADVERTISING) != 0,
|
||||
is_scannable = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.SCANNABLE_ADVERTISING) != 0,
|
||||
is_scan_response = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.SCAN_RESPONSE) != 0,
|
||||
is_connectable = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.CONNECTABLE_ADVERTISING) != 0,
|
||||
is_directed = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.DIRECTED_ADVERTISING) != 0,
|
||||
is_scannable = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.SCANNABLE_ADVERTISING) != 0,
|
||||
is_scan_response = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.SCAN_RESPONSE) != 0,
|
||||
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,
|
||||
primary_phy = report.primary_phy,
|
||||
@@ -477,7 +492,18 @@ class PeriodicAdvertisement:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@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
|
||||
sid: int
|
||||
num_bis: int
|
||||
@@ -490,8 +516,8 @@ class BIGInfoAdvertisement:
|
||||
sdu_interval: int
|
||||
max_sdu: int
|
||||
phy: hci.Phy
|
||||
framed: bool
|
||||
encrypted: bool
|
||||
framing: Framing
|
||||
encryption: Encryption
|
||||
|
||||
@classmethod
|
||||
def from_report(cls, address: hci.Address, sid: int, report) -> Self:
|
||||
@@ -508,8 +534,8 @@ class BIGInfoAdvertisement:
|
||||
report.sdu_interval,
|
||||
report.max_sdu,
|
||||
hci.Phy(report.phy),
|
||||
report.framing != 0,
|
||||
report.encryption != 0,
|
||||
cls.Framing(report.framing),
|
||||
cls.Encryption(report.encryption),
|
||||
)
|
||||
|
||||
|
||||
@@ -1001,7 +1027,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
|
||||
def on_biginfo_advertising_report(self, report) -> None:
|
||||
self.emit(
|
||||
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:
|
||||
@@ -1019,14 +1045,24 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclass
|
||||
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
|
||||
sdu_interval: int
|
||||
sdu_interval: int # SDU interval, in microseconds
|
||||
max_sdu: int
|
||||
max_transport_latency: int
|
||||
max_transport_latency: int # Max transport latency, in milliseconds
|
||||
rtn: int
|
||||
phy: hci.PhyBit = hci.PhyBit.LE_2M
|
||||
packing: int = 0
|
||||
framing: int = 0
|
||||
packing: Packing = Packing.SEQUENTIAL
|
||||
framing: Framing = Framing.UNFRAMED
|
||||
broadcast_code: bytes | None = None
|
||||
|
||||
|
||||
@@ -1049,15 +1085,15 @@ class Big(utils.EventEmitter):
|
||||
state: State = State.PENDING
|
||||
|
||||
# Attributes provided by BIG Create Complete event
|
||||
big_sync_delay: int = 0
|
||||
transport_latency_big: int = 0
|
||||
phy: int = 0
|
||||
big_sync_delay: int = 0 # Sync delay, in microseconds
|
||||
transport_latency_big: int = 0 # Transport latency, in microseconds
|
||||
phy: hci.Phy = hci.Phy.LE_1M
|
||||
nse: int = 0
|
||||
bn: int = 0
|
||||
pto: int = 0
|
||||
irc: int = 0
|
||||
max_pdu: int = 0
|
||||
iso_interval: float = 0.0
|
||||
iso_interval: float = 0.0 # ISO interval, in milliseconds
|
||||
bis_links: Sequence[BisLink] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -1487,10 +1523,74 @@ class _IsoLink:
|
||||
"""Write an ISO 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
|
||||
def data_packet_queue(self) -> DataPacketQueue | None:
|
||||
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
|
||||
@@ -1504,6 +1604,20 @@ class CisLink(utils.EventEmitter, _IsoLink):
|
||||
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
|
||||
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
|
||||
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
|
||||
|
||||
@@ -1586,6 +1700,7 @@ class Connection(utils.CompositeEventEmitter):
|
||||
peer_resolvable_address: Optional[hci.Address]
|
||||
peer_le_features: Optional[hci.LeFeatureMask]
|
||||
role: hci.Role
|
||||
parameters: Parameters
|
||||
encryption: int
|
||||
encryption_key_size: int
|
||||
authenticated: bool
|
||||
@@ -1630,6 +1745,9 @@ class Connection(utils.CompositeEventEmitter):
|
||||
EVENT_PAIRING_FAILURE = "pairing_failure"
|
||||
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
|
||||
class Listener:
|
||||
@@ -4557,48 +4675,39 @@ class Device(utils.CompositeEventEmitter):
|
||||
@utils.experimental('Only for testing.')
|
||||
async def setup_cig(
|
||||
self,
|
||||
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],
|
||||
parameters: CigParameters,
|
||||
) -> list[int]:
|
||||
"""Sends hci.HCI_LE_Set_CIG_Parameters_Command.
|
||||
|
||||
Args:
|
||||
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).
|
||||
parameters: CIG parameters.
|
||||
|
||||
Returns:
|
||||
List of created CIS handles corresponding to the same order of [cid_id].
|
||||
"""
|
||||
num_cis = len(cis_id)
|
||||
num_cis = len(parameters.cis_parameters)
|
||||
|
||||
response = await self.send_command(
|
||||
hci.HCI_LE_Set_CIG_Parameters_Command(
|
||||
cig_id=cig_id,
|
||||
sdu_interval_c_to_p=sdu_interval[0],
|
||||
sdu_interval_p_to_c=sdu_interval[1],
|
||||
worst_case_sca=0x00, # 251-500 ppm
|
||||
packing=0x00, # Sequential
|
||||
framing=framing,
|
||||
max_transport_latency_c_to_p=max_transport_latency[0],
|
||||
max_transport_latency_p_to_c=max_transport_latency[1],
|
||||
cis_id=cis_id,
|
||||
max_sdu_c_to_p=[max_sdu[0]] * num_cis,
|
||||
max_sdu_p_to_c=[max_sdu[1]] * num_cis,
|
||||
phy_c_to_p=[hci.HCI_LE_2M_PHY] * num_cis,
|
||||
phy_p_to_c=[hci.HCI_LE_2M_PHY] * num_cis,
|
||||
rtn_c_to_p=[retransmission_number] * num_cis,
|
||||
rtn_p_to_c=[retransmission_number] * num_cis,
|
||||
cig_id=parameters.cig_id,
|
||||
sdu_interval_c_to_p=parameters.sdu_interval_c_to_p,
|
||||
sdu_interval_p_to_c=parameters.sdu_interval_p_to_c,
|
||||
worst_case_sca=parameters.worst_case_sca,
|
||||
packing=int(parameters.packing),
|
||||
framing=int(parameters.framing),
|
||||
max_transport_latency_c_to_p=parameters.max_transport_latency_c_to_p,
|
||||
max_transport_latency_p_to_c=parameters.max_transport_latency_p_to_c,
|
||||
cis_id=[cis.cis_id for cis in parameters.cis_parameters],
|
||||
max_sdu_c_to_p=[
|
||||
cis.max_sdu_c_to_p for cis in parameters.cis_parameters
|
||||
],
|
||||
max_sdu_p_to_c=[
|
||||
cis.max_sdu_p_to_c for cis in parameters.cis_parameters
|
||||
],
|
||||
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,
|
||||
)
|
||||
@@ -4606,19 +4715,17 @@ class Device(utils.CompositeEventEmitter):
|
||||
# Ideally, we should manage CIG lifecycle, but they are not useful for Unicast
|
||||
# Server, so here it only provides a basic functionality for testing.
|
||||
cis_handles = response.return_parameters.connection_handle[:]
|
||||
for id, cis_handle in zip(cis_id, cis_handles):
|
||||
self._pending_cis[cis_handle] = (id, cig_id)
|
||||
for cis, cis_handle in zip(parameters.cis_parameters, cis_handles):
|
||||
self._pending_cis[cis_handle] = (cis.cis_id, parameters.cig_id)
|
||||
|
||||
return cis_handles
|
||||
|
||||
# [LE only]
|
||||
@utils.experimental('Only for testing.')
|
||||
async def create_cis(
|
||||
self, cis_acl_pairs: Sequence[tuple[int, int]]
|
||||
self, cis_acl_pairs: Sequence[tuple[int, Connection]]
|
||||
) -> list[CisLink]:
|
||||
for cis_handle, acl_handle in cis_acl_pairs:
|
||||
acl_connection = self.lookup_connection(acl_handle)
|
||||
assert acl_connection
|
||||
for cis_handle, acl_connection in cis_acl_pairs:
|
||||
cis_id, cig_id = self._pending_cis.pop(cis_handle)
|
||||
self.cis_links[cis_handle] = CisLink(
|
||||
device=self,
|
||||
@@ -4638,8 +4745,8 @@ class Device(utils.CompositeEventEmitter):
|
||||
if pending_future := pending_cis_establishments.get(cis_link.handle):
|
||||
pending_future.set_result(cis_link)
|
||||
|
||||
def on_cis_establishment_failure(cis_handle: int, status: int) -> None:
|
||||
if pending_future := pending_cis_establishments.get(cis_handle):
|
||||
def on_cis_establishment_failure(cis_link: CisLink, status: int) -> None:
|
||||
if pending_future := pending_cis_establishments.get(cis_link.handle):
|
||||
pending_future.set_exception(hci.HCI_Error(status))
|
||||
|
||||
watcher.on(self, self.EVENT_CIS_ESTABLISHMENT, on_cis_establishment)
|
||||
@@ -4649,7 +4756,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
await self.send_command(
|
||||
hci.HCI_LE_Create_CIS_Command(
|
||||
cis_connection_handle=[p[0] for p in cis_acl_pairs],
|
||||
acl_connection_handle=[p[1] for p in cis_acl_pairs],
|
||||
acl_connection_handle=[p[1].handle for p in cis_acl_pairs],
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
@@ -4658,26 +4765,21 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
# [LE only]
|
||||
@utils.experimental('Only for testing.')
|
||||
async def accept_cis_request(self, handle: int) -> CisLink:
|
||||
async def accept_cis_request(self, cis_link: CisLink) -> None:
|
||||
"""[LE Only] Accepts an incoming CIS request.
|
||||
|
||||
When the specified CIS handle is already created, this method returns the
|
||||
existed CIS link object immediately.
|
||||
This method returns when the CIS is established, or raises an exception if
|
||||
the CIS establishment fails.
|
||||
|
||||
Args:
|
||||
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.
|
||||
# If one of them has accepted the request, the others should just leverage it.
|
||||
async with self._cis_lock:
|
||||
if cis_link.state == CisLink.State.ESTABLISHED:
|
||||
return cis_link
|
||||
return
|
||||
|
||||
with closing(utils.EventWatcher()) as watcher:
|
||||
pending_establishment = asyncio.get_running_loop().create_future()
|
||||
@@ -4696,26 +4798,24 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
|
||||
await self.send_command(
|
||||
hci.HCI_LE_Accept_CIS_Request_Command(connection_handle=handle),
|
||||
hci.HCI_LE_Accept_CIS_Request_Command(
|
||||
connection_handle=cis_link.handle
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
await pending_establishment
|
||||
return cis_link
|
||||
|
||||
# Mypy believes this is reachable when context is an ExitStack.
|
||||
raise UnreachableError()
|
||||
|
||||
# [LE only]
|
||||
@utils.experimental('Only for testing.')
|
||||
async def reject_cis_request(
|
||||
self,
|
||||
handle: int,
|
||||
cis_link: CisLink,
|
||||
reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||
) -> None:
|
||||
await self.send_command(
|
||||
hci.HCI_LE_Reject_CIS_Request_Command(
|
||||
connection_handle=handle, reason=reason
|
||||
connection_handle=cis_link.handle, reason=reason
|
||||
),
|
||||
check_result=True,
|
||||
)
|
||||
@@ -5072,8 +5172,8 @@ class Device(utils.CompositeEventEmitter):
|
||||
# Store the keys in the key store
|
||||
if self.keystore:
|
||||
authenticated = key_type in (
|
||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
|
||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
|
||||
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192,
|
||||
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256,
|
||||
)
|
||||
pairing_keys = PairingKeys(
|
||||
link_key=PairingKeys.Key(value=link_key, authenticated=authenticated),
|
||||
@@ -5253,7 +5353,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
big.bis_links = [BisLink(handle=handle, big=big) for handle in bis_handles]
|
||||
big.big_sync_delay = big_sync_delay
|
||||
big.transport_latency_big = transport_latency_big
|
||||
big.phy = phy
|
||||
big.phy = hci.Phy(phy)
|
||||
big.nse = nse
|
||||
big.bn = bn
|
||||
big.pto = pto
|
||||
@@ -5520,8 +5620,8 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
# Handle SCO request.
|
||||
if link_type in (
|
||||
hci.HCI_Connection_Complete_Event.SCO_LINK_TYPE,
|
||||
hci.HCI_Connection_Complete_Event.ESCO_LINK_TYPE,
|
||||
hci.HCI_Connection_Complete_Event.LinkType.SCO,
|
||||
hci.HCI_Connection_Complete_Event.LinkType.ESCO,
|
||||
):
|
||||
if connection := self.find_connection_by_bd_addr(
|
||||
bd_addr, transport=PhysicalTransport.BR_EDR
|
||||
@@ -5629,7 +5729,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_authentication_io_capability_request(self, connection):
|
||||
def on_authentication_io_capability_request(self, connection: Connection):
|
||||
# Ask what the pairing config should be for this connection
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
@@ -5637,13 +5737,13 @@ class Device(utils.CompositeEventEmitter):
|
||||
authentication_requirements = (
|
||||
# No Bonding
|
||||
(
|
||||
hci.HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||
hci.HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||
hci.AuthenticationRequirements.MITM_NOT_REQUIRED_NO_BONDING,
|
||||
hci.AuthenticationRequirements.MITM_REQUIRED_NO_BONDING,
|
||||
),
|
||||
# General Bonding
|
||||
(
|
||||
hci.HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||
hci.HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||
hci.AuthenticationRequirements.MITM_NOT_REQUIRED_GENERAL_BONDING,
|
||||
hci.AuthenticationRequirements.MITM_REQUIRED_GENERAL_BONDING,
|
||||
),
|
||||
)[1 if pairing_config.bonding else 0][1 if pairing_config.mitm else 0]
|
||||
|
||||
@@ -5698,30 +5798,30 @@ class Device(utils.CompositeEventEmitter):
|
||||
raise UnreachableError()
|
||||
|
||||
# See Bluetooth spec @ Vol 3, Part C 5.2.2.6
|
||||
methods = {
|
||||
hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: {
|
||||
hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: display_auto_confirm,
|
||||
hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: display_confirm,
|
||||
hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
|
||||
hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
|
||||
methods: dict[int, dict[int, Callable[[], Awaitable[bool]]]] = {
|
||||
hci.IoCapability.DISPLAY_ONLY: {
|
||||
hci.IoCapability.DISPLAY_ONLY: display_auto_confirm,
|
||||
hci.IoCapability.DISPLAY_YES_NO: display_confirm,
|
||||
hci.IoCapability.KEYBOARD_ONLY: na,
|
||||
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
|
||||
},
|
||||
hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: {
|
||||
hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: display_auto_confirm,
|
||||
hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: display_confirm,
|
||||
hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
|
||||
hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
|
||||
hci.IoCapability.DISPLAY_YES_NO: {
|
||||
hci.IoCapability.DISPLAY_ONLY: display_auto_confirm,
|
||||
hci.IoCapability.DISPLAY_YES_NO: display_confirm,
|
||||
hci.IoCapability.KEYBOARD_ONLY: na,
|
||||
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
|
||||
},
|
||||
hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: {
|
||||
hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: na,
|
||||
hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: na,
|
||||
hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: na,
|
||||
hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
|
||||
hci.IoCapability.KEYBOARD_ONLY: {
|
||||
hci.IoCapability.DISPLAY_ONLY: na,
|
||||
hci.IoCapability.DISPLAY_YES_NO: na,
|
||||
hci.IoCapability.KEYBOARD_ONLY: na,
|
||||
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
|
||||
},
|
||||
hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: {
|
||||
hci.HCI_DISPLAY_ONLY_IO_CAPABILITY: confirm,
|
||||
hci.HCI_DISPLAY_YES_NO_IO_CAPABILITY: confirm,
|
||||
hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY: auto_confirm,
|
||||
hci.HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: auto_confirm,
|
||||
hci.IoCapability.NO_INPUT_NO_OUTPUT: {
|
||||
hci.IoCapability.DISPLAY_ONLY: confirm,
|
||||
hci.IoCapability.DISPLAY_YES_NO: confirm,
|
||||
hci.IoCapability.KEYBOARD_ONLY: auto_confirm,
|
||||
hci.IoCapability.NO_INPUT_NO_OUTPUT: auto_confirm,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5787,7 +5887,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
io_capability = pairing_config.delegate.classic_io_capability
|
||||
|
||||
# Respond
|
||||
if io_capability == hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY:
|
||||
if io_capability == hci.IoCapability.KEYBOARD_ONLY:
|
||||
# Ask the user to enter a string
|
||||
async def get_pin_code():
|
||||
pin_code = await connection.cancel_on_disconnection(
|
||||
@@ -5917,24 +6017,63 @@ class Device(utils.CompositeEventEmitter):
|
||||
f'cis_id=[0x{cis_id:02X}] ***'
|
||||
)
|
||||
# LE_CIS_Established event doesn't provide info, so we must store them here.
|
||||
self.cis_links[cis_handle] = CisLink(
|
||||
cis_link = CisLink(
|
||||
device=self,
|
||||
acl_connection=acl_connection,
|
||||
handle=cis_handle,
|
||||
cig_id=cig_id,
|
||||
cis_id=cis_id,
|
||||
)
|
||||
self.emit(self.EVENT_CIS_REQUEST, acl_connection, cis_handle, cig_id, cis_id)
|
||||
self.cis_links[cis_handle] = cis_link
|
||||
acl_connection.emit(acl_connection.EVENT_CIS_REQUEST, cis_link)
|
||||
self.emit(self.EVENT_CIS_REQUEST, cis_link)
|
||||
|
||||
# [LE only]
|
||||
@host_event_handler
|
||||
@utils.experimental('Only for testing')
|
||||
def on_cis_establishment(self, cis_handle: int) -> None:
|
||||
def on_cis_establishment(
|
||||
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.state = CisLink.State.ESTABLISHED
|
||||
|
||||
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(
|
||||
f'*** CIS Establishment '
|
||||
f'{cis_link.acl_connection.peer_address}, '
|
||||
@@ -5944,16 +6083,27 @@ class Device(utils.CompositeEventEmitter):
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
# [LE only]
|
||||
@host_event_handler
|
||||
@utils.experimental('Only for testing')
|
||||
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}] ***')
|
||||
if cis_link := self.cis_links.pop(cis_handle):
|
||||
cis_link.emit(cis_link.EVENT_ESTABLISHMENT_FAILURE, status)
|
||||
self.emit(self.EVENT_CIS_ESTABLISHMENT_FAILURE, cis_handle, status)
|
||||
cis_link.emit(cis_link.EVENT_ESTABLISHMENT_FAILURE, status)
|
||||
cis_link.acl_connection.emit(
|
||||
cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT_FAILURE,
|
||||
cis_link,
|
||||
status,
|
||||
)
|
||||
self.emit(self.EVENT_CIS_ESTABLISHMENT_FAILURE, cis_link, status)
|
||||
|
||||
# [LE only]
|
||||
@host_event_handler
|
||||
@@ -5967,7 +6117,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_encryption_change(
|
||||
self, connection, encryption, encryption_key_size
|
||||
self, connection: Connection, encryption: int, encryption_key_size: int
|
||||
):
|
||||
logger.debug(
|
||||
f'*** Connection Encryption Change: [0x{connection.handle:04X}] '
|
||||
@@ -5980,14 +6130,14 @@ class Device(utils.CompositeEventEmitter):
|
||||
if (
|
||||
not connection.authenticated
|
||||
and connection.transport == PhysicalTransport.BR_EDR
|
||||
and encryption == hci.HCI_Encryption_Change_Event.AES_CCM
|
||||
and encryption == hci.HCI_Encryption_Change_Event.Enabled.AES_CCM
|
||||
):
|
||||
connection.authenticated = True
|
||||
connection.sc = True
|
||||
if (
|
||||
not connection.authenticated
|
||||
and connection.transport == PhysicalTransport.LE
|
||||
and encryption == hci.HCI_Encryption_Change_Event.E0_OR_AES_CCM
|
||||
and encryption == hci.HCI_Encryption_Change_Event.Enabled.E0_OR_AES_CCM
|
||||
):
|
||||
connection.authenticated = True
|
||||
connection.sc = True
|
||||
@@ -6014,13 +6164,19 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_connection_parameters_update(self, connection, connection_parameters):
|
||||
def on_connection_parameters_update(
|
||||
self, connection: Connection, connection_parameters: core.ConnectionParameters
|
||||
):
|
||||
logger.debug(
|
||||
f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address} as {connection.role_name}, '
|
||||
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)
|
||||
|
||||
@host_event_handler
|
||||
|
||||
+33
-36
@@ -90,54 +90,51 @@ HCI_INTEL_WRITE_BOOT_PARAMS_COMMAND = hci.hci_vendor_command_op_code(0x000E)
|
||||
hci.HCI_Command.register_commands(globals())
|
||||
|
||||
|
||||
@hci.HCI_Command.command(
|
||||
fields=[
|
||||
("param0", 1),
|
||||
],
|
||||
return_parameters_fields=[
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_Intel_Read_Version_Command(hci.HCI_Command):
|
||||
param0: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
|
||||
return_parameters_fields = [
|
||||
("status", hci.STATUS_SPEC),
|
||||
("tlv", "*"),
|
||||
],
|
||||
)
|
||||
class HCI_Intel_Read_Version_Command(hci.HCI_Command):
|
||||
pass
|
||||
]
|
||||
|
||||
|
||||
@hci.HCI_Command.command(
|
||||
fields=[("data_type", 1), ("data", "*")],
|
||||
return_parameters_fields=[
|
||||
("status", 1),
|
||||
],
|
||||
)
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class Hci_Intel_Secure_Send_Command(hci.HCI_Command):
|
||||
pass
|
||||
data_type: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
data: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
return_parameters_fields = [
|
||||
("status", 1),
|
||||
]
|
||||
|
||||
|
||||
@hci.HCI_Command.command(
|
||||
fields=[
|
||||
("reset_type", 1),
|
||||
("patch_enable", 1),
|
||||
("ddc_reload", 1),
|
||||
("boot_option", 1),
|
||||
("boot_address", 4),
|
||||
],
|
||||
return_parameters_fields=[
|
||||
("data", "*"),
|
||||
],
|
||||
)
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_Intel_Reset_Command(hci.HCI_Command):
|
||||
pass
|
||||
reset_type: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
patch_enable: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
ddc_reload: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
boot_option: int = dataclasses.field(metadata=hci.metadata(1))
|
||||
boot_address: int = dataclasses.field(metadata=hci.metadata(4))
|
||||
|
||||
return_parameters_fields = [
|
||||
("data", "*"),
|
||||
]
|
||||
|
||||
|
||||
@hci.HCI_Command.command(
|
||||
fields=[("data", "*")],
|
||||
return_parameters_fields=[
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class Hci_Intel_Write_Device_Config_Command(hci.HCI_Command):
|
||||
data: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
return_parameters_fields = [
|
||||
("status", hci.STATUS_SPEC),
|
||||
("params", "*"),
|
||||
],
|
||||
)
|
||||
class Hci_Intel_Write_Device_Config_Command(hci.HCI_Command):
|
||||
pass
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
+27
-33
@@ -20,7 +20,7 @@ Based on various online bits of information, including the Linux kernel.
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
@@ -33,14 +33,7 @@ import weakref
|
||||
|
||||
|
||||
from bumble import core
|
||||
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 import hci
|
||||
from bumble.drivers import common
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -182,27 +175,29 @@ RTK_USB_PRODUCTS = {
|
||||
# -----------------------------------------------------------------------------
|
||||
# HCI Commands
|
||||
# -----------------------------------------------------------------------------
|
||||
HCI_RTK_READ_ROM_VERSION_COMMAND = hci_vendor_command_op_code(0x6D)
|
||||
HCI_RTK_DOWNLOAD_COMMAND = hci_vendor_command_op_code(0x20)
|
||||
HCI_RTK_DROP_FIRMWARE_COMMAND = hci_vendor_command_op_code(0x66)
|
||||
HCI_Command.register_commands(globals())
|
||||
HCI_RTK_READ_ROM_VERSION_COMMAND = hci.hci_vendor_command_op_code(0x6D)
|
||||
HCI_RTK_DOWNLOAD_COMMAND = hci.hci_vendor_command_op_code(0x20)
|
||||
HCI_RTK_DROP_FIRMWARE_COMMAND = hci.hci_vendor_command_op_code(0x66)
|
||||
hci.HCI_Command.register_commands(globals())
|
||||
|
||||
|
||||
@HCI_Command.command(return_parameters_fields=[("status", STATUS_SPEC), ("version", 1)])
|
||||
class HCI_RTK_Read_ROM_Version_Command(HCI_Command):
|
||||
pass
|
||||
@hci.HCI_Command.command
|
||||
@dataclass
|
||||
class HCI_RTK_Read_ROM_Version_Command(hci.HCI_Command):
|
||||
return_parameters_fields = [("status", hci.STATUS_SPEC), ("version", 1)]
|
||||
|
||||
|
||||
@HCI_Command.command(
|
||||
fields=[("index", 1), ("payload", RTK_FRAGMENT_LENGTH)],
|
||||
return_parameters_fields=[("status", STATUS_SPEC), ("index", 1)],
|
||||
)
|
||||
class HCI_RTK_Download_Command(HCI_Command):
|
||||
pass
|
||||
@hci.HCI_Command.command
|
||||
@dataclass
|
||||
class HCI_RTK_Download_Command(hci.HCI_Command):
|
||||
index: int = field(metadata=hci.metadata(1))
|
||||
payload: bytes = field(metadata=hci.metadata(RTK_FRAGMENT_LENGTH))
|
||||
return_parameters_fields = [("status", hci.STATUS_SPEC), ("index", 1)]
|
||||
|
||||
|
||||
@HCI_Command.command()
|
||||
class HCI_RTK_Drop_Firmware_Command(HCI_Command):
|
||||
@hci.HCI_Command.command
|
||||
@dataclass
|
||||
class HCI_RTK_Drop_Firmware_Command(hci.HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
@@ -498,17 +493,17 @@ class Driver(common.Driver):
|
||||
async def driver_info_for_host(cls, host):
|
||||
try:
|
||||
await host.send_command(
|
||||
HCI_Reset_Command(),
|
||||
hci.HCI_Reset_Command(),
|
||||
check_result=True,
|
||||
response_timeout=cls.POST_RESET_DELAY,
|
||||
)
|
||||
host.ready = True # Needed to let the host know the controller is ready.
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
logger.warning("timeout waiting for hci reset, retrying")
|
||||
await host.send_command(HCI_Reset_Command(), check_result=True)
|
||||
await host.send_command(hci.HCI_Reset_Command(), check_result=True)
|
||||
host.ready = True
|
||||
|
||||
command = HCI_Read_Local_Version_Information_Command()
|
||||
command = hci.HCI_Read_Local_Version_Information_Command()
|
||||
response = await host.send_command(command, check_result=True)
|
||||
if response.command_opcode != command.op_code:
|
||||
logger.error("failed to probe local version information")
|
||||
@@ -595,7 +590,7 @@ class Driver(common.Driver):
|
||||
response = await self.host.send_command(
|
||||
HCI_RTK_Read_ROM_Version_Command(), check_result=True
|
||||
)
|
||||
if response.return_parameters.status != HCI_SUCCESS:
|
||||
if response.return_parameters.status != hci.HCI_SUCCESS:
|
||||
logger.warning("can't get ROM version")
|
||||
return
|
||||
rom_version = response.return_parameters.version
|
||||
@@ -633,9 +628,8 @@ class Driver(common.Driver):
|
||||
fragment = payload[fragment_offset : fragment_offset + RTK_FRAGMENT_LENGTH]
|
||||
logger.debug(f"downloading fragment {fragment_index}")
|
||||
await self.host.send_command(
|
||||
HCI_RTK_Download_Command(
|
||||
index=download_index, payload=fragment, check_result=True
|
||||
)
|
||||
HCI_RTK_Download_Command(index=download_index, payload=fragment),
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
logger.debug("download complete!")
|
||||
@@ -644,7 +638,7 @@ class Driver(common.Driver):
|
||||
response = await self.host.send_command(
|
||||
HCI_RTK_Read_ROM_Version_Command(), check_result=True
|
||||
)
|
||||
if response.return_parameters.status != HCI_SUCCESS:
|
||||
if response.return_parameters.status != hci.HCI_SUCCESS:
|
||||
logger.warning("can't get ROM version")
|
||||
else:
|
||||
rom_version = response.return_parameters.version
|
||||
@@ -667,7 +661,7 @@ class Driver(common.Driver):
|
||||
|
||||
async def init_controller(self):
|
||||
await self.download_firmware()
|
||||
await self.host.send_command(HCI_Reset_Command(), check_result=True)
|
||||
await self.host.send_command(hci.HCI_Reset_Command(), check_result=True)
|
||||
logger.info(f"loaded FW image {self.driver_info.fw_name}")
|
||||
|
||||
|
||||
|
||||
+2478
-2855
File diff suppressed because it is too large
Load Diff
+76
-25
@@ -38,7 +38,6 @@ from bumble.snoop import Snooper
|
||||
from bumble import drivers
|
||||
from bumble import hci
|
||||
from bumble.core import (
|
||||
PhysicalTransport,
|
||||
PhysicalTransport,
|
||||
ConnectionPHY,
|
||||
ConnectionParameters,
|
||||
@@ -72,6 +71,11 @@ class DataPacketQueue(utils.EventEmitter):
|
||||
|
||||
max_packet_size: int
|
||||
|
||||
class PerConnectionState:
|
||||
def __init__(self) -> None:
|
||||
self.in_flight = 0
|
||||
self.drained = asyncio.Event()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
max_packet_size: int,
|
||||
@@ -82,9 +86,12 @@ class DataPacketQueue(utils.EventEmitter):
|
||||
self.max_packet_size = max_packet_size
|
||||
self.max_in_flight = max_in_flight
|
||||
self._in_flight = 0 # Total number of packets in flight across all connections
|
||||
self._in_flight_per_connection: dict[int, int] = collections.defaultdict(
|
||||
int
|
||||
) # Number of packets in flight per connection
|
||||
self._connection_state: dict[int, DataPacketQueue.PerConnectionState] = (
|
||||
collections.defaultdict(DataPacketQueue.PerConnectionState)
|
||||
)
|
||||
self._drained_per_connection: dict[int, asyncio.Event] = (
|
||||
collections.defaultdict(asyncio.Event)
|
||||
)
|
||||
self._send = send
|
||||
self._packets: collections.deque[tuple[hci.HCI_Packet, int]] = (
|
||||
collections.deque()
|
||||
@@ -136,36 +143,40 @@ class DataPacketQueue(utils.EventEmitter):
|
||||
self._completed += flushed_count
|
||||
self._packets = collections.deque(packets_to_keep)
|
||||
|
||||
if connection_handle in self._in_flight_per_connection:
|
||||
in_flight = self._in_flight_per_connection[connection_handle]
|
||||
if connection_state := self._connection_state.pop(connection_handle, None):
|
||||
in_flight = connection_state.in_flight
|
||||
self._completed += in_flight
|
||||
self._in_flight -= in_flight
|
||||
del self._in_flight_per_connection[connection_handle]
|
||||
connection_state.drained.set()
|
||||
|
||||
def _check_queue(self) -> None:
|
||||
while self._packets and self._in_flight < self.max_in_flight:
|
||||
packet, connection_handle = self._packets.pop()
|
||||
self._send(packet)
|
||||
self._in_flight += 1
|
||||
self._in_flight_per_connection[connection_handle] += 1
|
||||
connection_state = self._connection_state[connection_handle]
|
||||
connection_state.in_flight += 1
|
||||
connection_state.drained.clear()
|
||||
|
||||
def on_packets_completed(self, packet_count: int, connection_handle: int) -> None:
|
||||
"""Mark one or more packets associated with a connection as completed."""
|
||||
if connection_handle not in self._in_flight_per_connection:
|
||||
if connection_handle not in self._connection_state:
|
||||
logger.warning(
|
||||
f'received completion for unknown connection {connection_handle}'
|
||||
)
|
||||
return
|
||||
|
||||
in_flight_for_connection = self._in_flight_per_connection[connection_handle]
|
||||
if packet_count <= in_flight_for_connection:
|
||||
self._in_flight_per_connection[connection_handle] -= packet_count
|
||||
connection_state = self._connection_state[connection_handle]
|
||||
if packet_count <= connection_state.in_flight:
|
||||
connection_state.in_flight -= packet_count
|
||||
else:
|
||||
logger.warning(
|
||||
f'{packet_count} completed for {connection_handle} '
|
||||
f'but only {in_flight_for_connection} in flight'
|
||||
f'but only {connection_state.in_flight} in flight'
|
||||
)
|
||||
self._in_flight_per_connection[connection_handle] = 0
|
||||
connection_state.in_flight = 0
|
||||
if connection_state.in_flight == 0:
|
||||
connection_state.drained.set()
|
||||
|
||||
if packet_count <= self._in_flight:
|
||||
self._in_flight -= packet_count
|
||||
@@ -180,6 +191,13 @@ class DataPacketQueue(utils.EventEmitter):
|
||||
self._check_queue()
|
||||
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:
|
||||
@@ -835,8 +853,8 @@ class Host(utils.EventEmitter):
|
||||
def on_packet(self, packet: bytes) -> None:
|
||||
try:
|
||||
hci_packet = hci.HCI_Packet.from_bytes(packet)
|
||||
except Exception as error:
|
||||
logger.warning(f'!!! error parsing packet from bytes: {error}')
|
||||
except Exception:
|
||||
logger.exception('!!! error parsing packet from bytes')
|
||||
return
|
||||
|
||||
if self.ready or (
|
||||
@@ -1126,11 +1144,19 @@ class Host(utils.EventEmitter):
|
||||
else:
|
||||
self.emit('connection_phy_update_failure', connection.handle, event.status)
|
||||
|
||||
def on_hci_le_advertising_report_event(self, event):
|
||||
def on_hci_le_advertising_report_event(
|
||||
self,
|
||||
event: (
|
||||
hci.HCI_LE_Advertising_Report_Event
|
||||
| hci.HCI_LE_Extended_Advertising_Report_Event
|
||||
),
|
||||
):
|
||||
for report in event.reports:
|
||||
self.emit('advertising_report', report)
|
||||
|
||||
def on_hci_le_extended_advertising_report_event(self, event):
|
||||
def on_hci_le_extended_advertising_report_event(
|
||||
self, event: hci.HCI_LE_Extended_Advertising_Report_Event
|
||||
):
|
||||
self.on_hci_le_advertising_report_event(event)
|
||||
|
||||
def on_hci_le_advertising_set_terminated_event(self, event):
|
||||
@@ -1261,7 +1287,24 @@ class Host(utils.EventEmitter):
|
||||
self.cis_links[event.connection_handle] = IsoLink(
|
||||
handle=event.connection_handle, packet_queue=self.iso_packet_queue
|
||||
)
|
||||
self.emit('cis_establishment', event.connection_handle)
|
||||
self.emit(
|
||||
'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:
|
||||
self.emit(
|
||||
'cis_establishment_failure', event.connection_handle, event.status
|
||||
@@ -1364,6 +1407,10 @@ class Host(utils.EventEmitter):
|
||||
self.emit('role_change_failure', event.bd_addr, event.status)
|
||||
|
||||
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(
|
||||
'connection_data_length_change',
|
||||
event.connection_handle,
|
||||
@@ -1384,7 +1431,7 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_encryption_change_event(self, event):
|
||||
def on_hci_encryption_change_event(self, event: hci.HCI_Encryption_Change_Event):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
@@ -1398,7 +1445,9 @@ class Host(utils.EventEmitter):
|
||||
'connection_encryption_failure', event.connection_handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_encryption_change_v2_event(self, event):
|
||||
def on_hci_encryption_change_v2_event(
|
||||
self, event: hci.HCI_Encryption_Change_V2_Event
|
||||
):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
@@ -1519,13 +1568,15 @@ class Host(utils.EventEmitter):
|
||||
self.emit('inquiry_complete')
|
||||
|
||||
def on_hci_inquiry_result_with_rssi_event(self, event):
|
||||
for response in event.responses:
|
||||
for bd_addr, class_of_device, rssi in zip(
|
||||
event.bd_addr, event.class_of_device, event.rssi
|
||||
):
|
||||
self.emit(
|
||||
'inquiry_result',
|
||||
response.bd_addr,
|
||||
response.class_of_device,
|
||||
bd_addr,
|
||||
class_of_device,
|
||||
b'',
|
||||
response.rssi,
|
||||
rssi,
|
||||
)
|
||||
|
||||
def on_hci_extended_inquiry_result_event(self, event):
|
||||
|
||||
+1
-1
@@ -274,7 +274,7 @@ class LocalLink:
|
||||
|
||||
responder_controller.on_classic_connection_request(
|
||||
initiator_controller.public_address,
|
||||
HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
|
||||
def classic_accept_connection(
|
||||
|
||||
+11
-17
@@ -21,13 +21,7 @@ from dataclasses import dataclass
|
||||
import secrets
|
||||
from typing import Optional
|
||||
|
||||
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 import hci
|
||||
from bumble.smp import (
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
@@ -50,7 +44,7 @@ from bumble.core import AdvertisingData, LeRole
|
||||
class OobData:
|
||||
"""OOB data that can be sent from one device to another."""
|
||||
|
||||
address: Optional[Address] = None
|
||||
address: Optional[hci.Address] = None
|
||||
role: Optional[LeRole] = None
|
||||
shared_data: Optional[OobSharedData] = None
|
||||
legacy_context: Optional[OobLegacyContext] = None
|
||||
@@ -62,7 +56,7 @@ class OobData:
|
||||
shared_data_r: Optional[bytes] = None
|
||||
for ad_type, ad_data in ad.ad_structures:
|
||||
if ad_type == AdvertisingData.LE_BLUETOOTH_DEVICE_ADDRESS:
|
||||
instance.address = Address(ad_data)
|
||||
instance.address = hci.Address(ad_data)
|
||||
elif ad_type == AdvertisingData.LE_ROLE:
|
||||
instance.role = LeRole(ad_data[0])
|
||||
elif ad_type == AdvertisingData.LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE:
|
||||
@@ -130,11 +124,11 @@ class PairingDelegate:
|
||||
# Default mapping from abstract to Classic I/O capabilities.
|
||||
# Subclasses may override this if they prefer a different mapping.
|
||||
CLASSIC_IO_CAPABILITIES_MAP = {
|
||||
NO_OUTPUT_NO_INPUT: HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
KEYBOARD_INPUT_ONLY: HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||
DISPLAY_OUTPUT_ONLY: HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
DISPLAY_OUTPUT_AND_YES_NO_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
NO_OUTPUT_NO_INPUT: hci.IoCapability.NO_INPUT_NO_OUTPUT,
|
||||
KEYBOARD_INPUT_ONLY: hci.IoCapability.KEYBOARD_ONLY,
|
||||
DISPLAY_OUTPUT_ONLY: hci.IoCapability.DISPLAY_ONLY,
|
||||
DISPLAY_OUTPUT_AND_YES_NO_INPUT: hci.IoCapability.DISPLAY_YES_NO,
|
||||
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT: hci.IoCapability.DISPLAY_YES_NO,
|
||||
}
|
||||
|
||||
io_capability: IoCapability
|
||||
@@ -160,7 +154,7 @@ class PairingDelegate:
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
return self.CLASSIC_IO_CAPABILITIES_MAP.get(
|
||||
self.io_capability, HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
|
||||
self.io_capability, hci.IoCapability.NO_INPUT_NO_OUTPUT
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -237,8 +231,8 @@ class PairingConfig:
|
||||
"""Configuration for the Pairing protocol."""
|
||||
|
||||
class AddressType(enum.IntEnum):
|
||||
PUBLIC = Address.PUBLIC_DEVICE_ADDRESS
|
||||
RANDOM = Address.RANDOM_DEVICE_ADDRESS
|
||||
PUBLIC = hci.Address.PUBLIC_DEVICE_ADDRESS
|
||||
RANDOM = hci.Address.RANDOM_DEVICE_ADDRESS
|
||||
|
||||
@dataclass
|
||||
class OobConfig:
|
||||
|
||||
@@ -244,16 +244,16 @@ class SecurityService(SecurityServicer):
|
||||
and connection.authenticated
|
||||
and link_key_type
|
||||
in (
|
||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
|
||||
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
|
||||
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192,
|
||||
hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256,
|
||||
)
|
||||
)
|
||||
if level == LEVEL4:
|
||||
return (
|
||||
connection.encryption == hci.HCI_Encryption_Change_Event.AES_CCM
|
||||
connection.encryption == hci.HCI_Encryption_Change_Event.Enabled.AES_CCM
|
||||
and connection.authenticated
|
||||
and link_key_type
|
||||
== hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE
|
||||
== hci.LinkKeyType.AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256
|
||||
)
|
||||
raise InvalidArgumentError(f"Unexpected level {level}")
|
||||
|
||||
|
||||
+5
-11
@@ -343,22 +343,16 @@ class AseStateMachine(gatt.Characteristic):
|
||||
self.service.device.EVENT_CIS_ESTABLISHMENT, self.on_cis_establishment
|
||||
)
|
||||
|
||||
def on_cis_request(
|
||||
self,
|
||||
acl_connection: device.Connection,
|
||||
cis_handle: int,
|
||||
cig_id: int,
|
||||
cis_id: int,
|
||||
) -> None:
|
||||
def on_cis_request(self, cis_link: device.CisLink) -> None:
|
||||
if (
|
||||
cig_id == self.cig_id
|
||||
and cis_id == self.cis_id
|
||||
cis_link.cig_id == self.cig_id
|
||||
and cis_link.cis_id == self.cis_id
|
||||
and self.state == self.State.ENABLING
|
||||
):
|
||||
utils.cancel_on_event(
|
||||
acl_connection,
|
||||
cis_link.acl_connection,
|
||||
'flush',
|
||||
self.service.device.accept_cis_request(cis_handle),
|
||||
self.service.device.accept_cis_request(cis_link),
|
||||
)
|
||||
|
||||
def on_cis_establishment(self, cis_link: device.CisLink) -> None:
|
||||
|
||||
+3
-1
@@ -762,7 +762,9 @@ class Session:
|
||||
|
||||
# OOB
|
||||
self.oob_data_flag = (
|
||||
1 if pairing_config.oob and pairing_config.oob.peer_data else 0
|
||||
1
|
||||
if pairing_config.oob and (not self.sc or pairing_config.oob.peer_data)
|
||||
else 0
|
||||
)
|
||||
|
||||
# Set up addresses
|
||||
|
||||
Vendored
+114
-196
@@ -15,21 +15,12 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import dataclasses
|
||||
from dataclasses import field
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
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,
|
||||
)
|
||||
from bumble import hci
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -41,22 +32,28 @@ from bumble.hci import (
|
||||
#
|
||||
# pylint: disable-next=line-too-long
|
||||
# See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#chip-capabilities-and-configuration
|
||||
HCI_LE_GET_VENDOR_CAPABILITIES_COMMAND = hci_vendor_command_op_code(0x153)
|
||||
HCI_LE_APCF_COMMAND = hci_vendor_command_op_code(0x157)
|
||||
HCI_GET_CONTROLLER_ACTIVITY_ENERGY_INFO_COMMAND = hci_vendor_command_op_code(0x159)
|
||||
HCI_A2DP_HARDWARE_OFFLOAD_COMMAND = hci_vendor_command_op_code(0x15D)
|
||||
HCI_BLUETOOTH_QUALITY_REPORT_COMMAND = hci_vendor_command_op_code(0x15E)
|
||||
HCI_DYNAMIC_AUDIO_BUFFER_COMMAND = hci_vendor_command_op_code(0x15F)
|
||||
HCI_LE_GET_VENDOR_CAPABILITIES_COMMAND = hci.hci_vendor_command_op_code(0x153)
|
||||
HCI_LE_APCF_COMMAND = hci.hci_vendor_command_op_code(0x157)
|
||||
HCI_GET_CONTROLLER_ACTIVITY_ENERGY_INFO_COMMAND = hci.hci_vendor_command_op_code(0x159)
|
||||
HCI_A2DP_HARDWARE_OFFLOAD_COMMAND = hci.hci_vendor_command_op_code(0x15D)
|
||||
HCI_BLUETOOTH_QUALITY_REPORT_COMMAND = hci.hci_vendor_command_op_code(0x15E)
|
||||
HCI_DYNAMIC_AUDIO_BUFFER_COMMAND = hci.hci_vendor_command_op_code(0x15F)
|
||||
|
||||
HCI_BLUETOOTH_QUALITY_REPORT_EVENT = 0x58
|
||||
|
||||
HCI_Command.register_commands(globals())
|
||||
hci.HCI_Command.register_commands(globals())
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_LE_Get_Vendor_Capabilities_Command(hci.HCI_Command):
|
||||
# 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),
|
||||
('offloaded_resolution_of_private_address', 1),
|
||||
('total_scan_results_storage', 2),
|
||||
@@ -73,12 +70,6 @@ HCI_Command.register_commands(globals())
|
||||
('bluetooth_quality_report_support', 1),
|
||||
('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
|
||||
def parse_return_parameters(cls, parameters):
|
||||
@@ -86,13 +77,13 @@ class HCI_LE_Get_Vendor_Capabilities_Command(HCI_Command):
|
||||
# there are no more bytes to parse, and leave un-signal parameters set to
|
||||
# None (older versions)
|
||||
nones = {field: None for field, _ in cls.return_parameters_fields}
|
||||
return_parameters = HCI_Object(cls.return_parameters_fields, **nones)
|
||||
return_parameters = hci.HCI_Object(cls.return_parameters_fields, **nones)
|
||||
|
||||
try:
|
||||
offset = 0
|
||||
for field in cls.return_parameters_fields:
|
||||
field_name, field_type = field
|
||||
field_value, field_size = HCI_Object.parse_field(
|
||||
field_value, field_size = hci.HCI_Object.parse_field(
|
||||
parameters, offset, field_type
|
||||
)
|
||||
setattr(return_parameters, field_name, field_value)
|
||||
@@ -104,30 +95,9 @@ class HCI_LE_Get_Vendor_Capabilities_Command(HCI_Command):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[
|
||||
(
|
||||
'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):
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_LE_APCF_Command(hci.HCI_Command):
|
||||
# pylint: disable=line-too-long
|
||||
'''
|
||||
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_apcf_command
|
||||
@@ -137,80 +107,50 @@ class HCI_LE_APCF_Command(HCI_Command):
|
||||
'''
|
||||
|
||||
# APCF Subcommands
|
||||
# TODO: use the OpenIntEnum class (when upcoming PR is merged)
|
||||
APCF_ENABLE = 0x00
|
||||
APCF_SET_FILTERING_PARAMETERS = 0x01
|
||||
APCF_BROADCASTER_ADDRESS = 0x02
|
||||
APCF_SERVICE_UUID = 0x03
|
||||
APCF_SERVICE_SOLICITATION_UUID = 0x04
|
||||
APCF_LOCAL_NAME = 0x05
|
||||
APCF_MANUFACTURER_DATA = 0x06
|
||||
APCF_SERVICE_DATA = 0x07
|
||||
APCF_TRANSPORT_DISCOVERY_SERVICE = 0x08
|
||||
APCF_AD_TYPE_FILTER = 0x09
|
||||
APCF_READ_EXTENDED_FEATURES = 0xFF
|
||||
class Opcode(hci.SpecableEnum):
|
||||
ENABLE = 0x00
|
||||
SET_FILTERING_PARAMETERS = 0x01
|
||||
BROADCASTER_ADDRESS = 0x02
|
||||
SERVICE_UUID = 0x03
|
||||
SERVICE_SOLICITATION_UUID = 0x04
|
||||
LOCAL_NAME = 0x05
|
||||
MANUFACTURER_DATA = 0x06
|
||||
SERVICE_DATA = 0x07
|
||||
TRANSPORT_DISCOVERY_SERVICE = 0x08
|
||||
AD_TYPE_FILTER = 0x09
|
||||
READ_EXTENDED_FEATURES = 0xFF
|
||||
|
||||
OPCODE_NAMES = {
|
||||
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',
|
||||
}
|
||||
opcode: int = dataclasses.field(metadata=Opcode.type_metadata(1))
|
||||
payload: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
@classmethod
|
||||
def opcode_name(cls, opcode):
|
||||
return name_or_number(cls.OPCODE_NAMES, opcode)
|
||||
return_parameters_fields = [
|
||||
('status', hci.STATUS_SPEC),
|
||||
('opcode', Opcode.type_spec(1)),
|
||||
('payload', '*'),
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('total_tx_time_ms', 4),
|
||||
('total_rx_time_ms', 4),
|
||||
('total_idle_time_ms', 4),
|
||||
('total_energy_used', 4),
|
||||
],
|
||||
)
|
||||
class HCI_Get_Controller_Activity_Energy_Info_Command(HCI_Command):
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_Get_Controller_Activity_Energy_Info_Command(hci.HCI_Command):
|
||||
# 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_rx_time_ms', 4),
|
||||
('total_idle_time_ms', 4),
|
||||
('total_energy_used', 4),
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[
|
||||
(
|
||||
'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):
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_A2DP_Hardware_Offload_Command(hci.HCI_Command):
|
||||
# pylint: disable=line-too-long
|
||||
'''
|
||||
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#a2dp-hardware-offload-support
|
||||
@@ -220,45 +160,24 @@ class HCI_A2DP_Hardware_Offload_Command(HCI_Command):
|
||||
'''
|
||||
|
||||
# A2DP Hardware Offload Subcommands
|
||||
# TODO: use the OpenIntEnum class (when upcoming PR is merged)
|
||||
START_A2DP_OFFLOAD = 0x01
|
||||
STOP_A2DP_OFFLOAD = 0x02
|
||||
class Opcode(hci.SpecableEnum):
|
||||
START_A2DP_OFFLOAD = 0x01
|
||||
STOP_A2DP_OFFLOAD = 0x02
|
||||
|
||||
OPCODE_NAMES = {
|
||||
START_A2DP_OFFLOAD: 'START_A2DP_OFFLOAD',
|
||||
STOP_A2DP_OFFLOAD: 'STOP_A2DP_OFFLOAD',
|
||||
}
|
||||
opcode: int = dataclasses.field(metadata=Opcode.type_metadata(1))
|
||||
payload: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
@classmethod
|
||||
def opcode_name(cls, opcode):
|
||||
return name_or_number(cls.OPCODE_NAMES, opcode)
|
||||
return_parameters_fields = [
|
||||
('status', hci.STATUS_SPEC),
|
||||
('opcode', Opcode.type_spec(1)),
|
||||
('payload', '*'),
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[
|
||||
(
|
||||
'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):
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_Dynamic_Audio_Buffer_Command(hci.HCI_Command):
|
||||
# pylint: disable=line-too-long
|
||||
'''
|
||||
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#dynamic-audio-buffer-command
|
||||
@@ -268,27 +187,28 @@ class HCI_Dynamic_Audio_Buffer_Command(HCI_Command):
|
||||
'''
|
||||
|
||||
# Dynamic Audio Buffer Subcommands
|
||||
# TODO: use the OpenIntEnum class (when upcoming PR is merged)
|
||||
GET_AUDIO_BUFFER_TIME_CAPABILITY = 0x01
|
||||
class Opcode(hci.SpecableEnum):
|
||||
GET_AUDIO_BUFFER_TIME_CAPABILITY = 0x01
|
||||
|
||||
OPCODE_NAMES = {
|
||||
GET_AUDIO_BUFFER_TIME_CAPABILITY: 'GET_AUDIO_BUFFER_TIME_CAPABILITY',
|
||||
}
|
||||
opcode: int = dataclasses.field(metadata=Opcode.type_metadata(1))
|
||||
payload: bytes = dataclasses.field(metadata=hci.metadata("*"))
|
||||
|
||||
@classmethod
|
||||
def opcode_name(cls, opcode):
|
||||
return name_or_number(cls.OPCODE_NAMES, opcode)
|
||||
return_parameters_fields = [
|
||||
('status', hci.STATUS_SPEC),
|
||||
('opcode', Opcode.type_spec(1)),
|
||||
('payload', '*'),
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class HCI_Android_Vendor_Event(HCI_Extended_Event):
|
||||
event_code: int = HCI_VENDOR_EVENT
|
||||
subevent_classes: dict[int, type[HCI_Extended_Event]] = {}
|
||||
class HCI_Android_Vendor_Event(hci.HCI_Extended_Event):
|
||||
event_code: int = hci.HCI_VENDOR_EVENT
|
||||
subevent_classes: dict[int, type[hci.HCI_Extended_Event]] = {}
|
||||
|
||||
@classmethod
|
||||
def subclass_from_parameters(
|
||||
cls, parameters: bytes
|
||||
) -> Optional[HCI_Extended_Event]:
|
||||
) -> Optional[hci.HCI_Extended_Event]:
|
||||
subevent_code = parameters[0]
|
||||
if subevent_code == HCI_BLUETOOTH_QUALITY_REPORT_EVENT:
|
||||
quality_report_id = parameters[1]
|
||||
@@ -299,45 +219,43 @@ class HCI_Android_Vendor_Event(HCI_Extended_Event):
|
||||
|
||||
|
||||
HCI_Android_Vendor_Event.register_subevents(globals())
|
||||
HCI_Event.add_vendor_factory(HCI_Android_Vendor_Event.subclass_from_parameters)
|
||||
hci.HCI_Event.add_vendor_factory(HCI_Android_Vendor_Event.subclass_from_parameters)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Extended_Event.event(
|
||||
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', '*'),
|
||||
]
|
||||
)
|
||||
@hci.HCI_Extended_Event.event
|
||||
@dataclasses.dataclass
|
||||
class HCI_Bluetooth_Quality_Report_Event(HCI_Android_Vendor_Event):
|
||||
# pylint: disable=line-too-long
|
||||
'''
|
||||
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('*'))
|
||||
|
||||
Vendored
+32
-27
@@ -15,11 +15,9 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from bumble.hci import (
|
||||
hci_vendor_command_op_code,
|
||||
HCI_Command,
|
||||
STATUS_SPEC,
|
||||
)
|
||||
import dataclasses
|
||||
|
||||
from bumble import hci
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -31,10 +29,10 @@ from bumble.hci import (
|
||||
#
|
||||
# pylint: disable-next=line-too-long
|
||||
# See https://github.com/zephyrproject-rtos/zephyr/blob/main/include/zephyr/bluetooth/hci_vs.h
|
||||
HCI_WRITE_TX_POWER_LEVEL_COMMAND = hci_vendor_command_op_code(0x000E)
|
||||
HCI_READ_TX_POWER_LEVEL_COMMAND = hci_vendor_command_op_code(0x000F)
|
||||
HCI_WRITE_TX_POWER_LEVEL_COMMAND = hci.hci_vendor_command_op_code(0x000E)
|
||||
HCI_READ_TX_POWER_LEVEL_COMMAND = hci.hci_vendor_command_op_code(0x000F)
|
||||
|
||||
HCI_Command.register_commands(globals())
|
||||
hci.HCI_Command.register_commands(globals())
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -49,16 +47,9 @@ class TX_Power_Level_Command:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[('handle_type', 1), ('connection_handle', 2), ('tx_power_level', -1)],
|
||||
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):
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_Write_Tx_Power_Level_Command(hci.HCI_Command, TX_Power_Level_Command):
|
||||
'''
|
||||
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
|
||||
@@ -67,18 +58,22 @@ class HCI_Write_Tx_Power_Level_Command(HCI_Command, TX_Power_Level_Command):
|
||||
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))
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[('handle_type', 1), ('connection_handle', 2)],
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
return_parameters_fields = [
|
||||
('status', hci.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):
|
||||
('selected_tx_power_level', -1),
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@hci.HCI_Command.command
|
||||
@dataclasses.dataclass
|
||||
class HCI_Read_Tx_Power_Level_Command(hci.HCI_Command, TX_Power_Level_Command):
|
||||
'''
|
||||
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
|
||||
@@ -86,3 +81,13 @@ class HCI_Read_Tx_Power_Level_Command(HCI_Command, TX_Power_Level_Command):
|
||||
Power level is in dB. Connection handle for TX_POWER_HANDLE_TYPE_ADV and
|
||||
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),
|
||||
]
|
||||
|
||||
+28
-16
@@ -20,7 +20,7 @@ import logging
|
||||
import sys
|
||||
import os
|
||||
from bumble import utils
|
||||
from bumble.device import Device, Connection
|
||||
from bumble.device import Device, CigParameters, CisLink, Connection
|
||||
from bumble.hci import (
|
||||
OwnAddressType,
|
||||
)
|
||||
@@ -61,27 +61,39 @@ async def main() -> None:
|
||||
devices[0].random_address, own_address_type=OwnAddressType.RANDOM
|
||||
)
|
||||
|
||||
cid_ids = [2, 3]
|
||||
cis_handles = await devices[1].setup_cig(
|
||||
cig_id=1,
|
||||
cis_id=cid_ids,
|
||||
sdu_interval=(10000, 255),
|
||||
framing=0,
|
||||
max_sdu=(120, 0),
|
||||
retransmission_number=13,
|
||||
max_transport_latency=(100, 5),
|
||||
CigParameters(
|
||||
cig_id=1,
|
||||
cis_parameters=[
|
||||
CigParameters.CisParameters(
|
||||
cis_id=2,
|
||||
max_sdu_c_to_p=120,
|
||||
max_sdu_p_to_c=0,
|
||||
rtn_c_to_p=13,
|
||||
rtn_p_to_c=13,
|
||||
),
|
||||
CigParameters.CisParameters(
|
||||
cis_id=3,
|
||||
max_sdu_c_to_p=120,
|
||||
max_sdu_p_to_c=0,
|
||||
rtn_c_to_p=13,
|
||||
rtn_p_to_c=13,
|
||||
),
|
||||
],
|
||||
sdu_interval_c_to_p=10000,
|
||||
sdu_interval_p_to_c=255,
|
||||
framing=CigParameters.Framing.UNFRAMED,
|
||||
max_transport_latency_c_to_p=100,
|
||||
max_transport_latency_p_to_c=5,
|
||||
),
|
||||
)
|
||||
|
||||
def on_cis_request(
|
||||
connection: Connection, cis_handle: int, _cig_id: int, _cis_id: int
|
||||
):
|
||||
connection.cancel_on_disconnection(devices[0].accept_cis_request(cis_handle))
|
||||
def on_cis_request(connection: Connection, cis_link: CisLink):
|
||||
connection.cancel_on_disconnection(devices[0].accept_cis_request(cis_link))
|
||||
|
||||
devices[0].on('cis_request', on_cis_request)
|
||||
|
||||
cis_links = await devices[1].create_cis(
|
||||
[(cis, connection.handle) for cis in cis_handles]
|
||||
)
|
||||
cis_links = await devices[1].create_cis([(cis, connection) for cis in cis_handles])
|
||||
|
||||
for cis_link in cis_links:
|
||||
await cis_link.disconnect()
|
||||
|
||||
@@ -46,7 +46,7 @@ def on_dlc(dlc: rfcomm.DLC, configuration: hfp.HfConfiguration):
|
||||
|
||||
def on_sco_request(connection: Connection, link_type: int, protocol: HfProtocol):
|
||||
if connection == protocol.dlc.multiplexer.l2cap_channel.connection:
|
||||
if link_type == hci.HCI_Connection_Complete_Event.SCO_LINK_TYPE:
|
||||
if link_type == hci.HCI_Connection_Complete_Event.LinkType.SCO:
|
||||
esco_parameters = hfp.ESCO_PARAMETERS[
|
||||
hfp.DefaultCodecParameters.SCO_CVSD_D1
|
||||
]
|
||||
|
||||
@@ -77,7 +77,7 @@ file_outputs: dict[AseStateMachine, io.BufferedWriter] = {}
|
||||
# -----------------------------------------------------------------------------
|
||||
async def main() -> None:
|
||||
if len(sys.argv) < 3:
|
||||
print('Usage: run_cig_setup.py <config-file>' '<transport-spec-for-device>')
|
||||
print('Usage: run_cig_setup.py <config-file> <transport-spec-for-device>')
|
||||
return
|
||||
|
||||
print('<<< connecting to HCI...')
|
||||
@@ -149,12 +149,9 @@ async def main() -> None:
|
||||
sdu += pdu.iso_sdu_fragment
|
||||
file_outputs[ase].write(sdu)
|
||||
|
||||
def on_ase_state_change(
|
||||
state: AseStateMachine.State,
|
||||
ase: AseStateMachine,
|
||||
) -> None:
|
||||
if state != AseStateMachine.State.STREAMING:
|
||||
if file_output := file_outputs.pop(ase):
|
||||
def on_ase_state_change(ase: AseStateMachine) -> None:
|
||||
if ase.state != AseStateMachine.State.STREAMING:
|
||||
if file_output := file_outputs.pop(ase, None):
|
||||
file_output.close()
|
||||
else:
|
||||
file_output = open(f'{datetime.datetime.now().isoformat()}.lc3', 'wb')
|
||||
|
||||
+14
@@ -1,5 +1,7 @@
|
||||
package com.github.google.bumble.remotehci;
|
||||
|
||||
import static com.github.google.bumble.remotehci.HciPacket.Type.COMMAND;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -16,6 +18,10 @@ public class HciProxy {
|
||||
private int mAclPacketsSent;
|
||||
private int mScoPacketsSent;
|
||||
|
||||
private static final byte[] LOOPBACK_COMMAND_COMPLETE_EVENT = {
|
||||
0x0E, 0x04, 0x01, 0x77, (byte)0xFC, 0x00
|
||||
};
|
||||
|
||||
HciProxy(int port, Listener listener) throws HalException {
|
||||
this.mListener = listener;
|
||||
|
||||
@@ -84,6 +90,14 @@ public class HciProxy {
|
||||
|
||||
@Override
|
||||
public void onPacket(HciPacket.Type type, byte[] packet) {
|
||||
// Short-circuit a local response when a special latency-testing packet
|
||||
// is received.
|
||||
if (type == COMMAND && packet[0] == (byte)0x77 && packet[1] == (byte)0xFC) {
|
||||
Log.d(TAG, "LOOPBACK");
|
||||
mServer.sendPacket(HciPacket.Type.EVENT, LOOPBACK_COMMAND_COMPLETE_EVENT);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(TAG, String.format("HOST->CONTROLLER: type=%s, size=%d", type, packet.length));
|
||||
hciHal.sendPacket(type, packet);
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import java.net.Socket;
|
||||
|
||||
public class HciServer {
|
||||
private static final String TAG = "HciServer";
|
||||
private static final int BUFFER_SIZE = 1024;
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
private final int mPort;
|
||||
private final Listener mListener;
|
||||
private OutputStream mOutputStream;
|
||||
|
||||
@@ -127,7 +127,7 @@ async fn main() -> PyResult<()> {
|
||||
} else {
|
||||
matching
|
||||
.iter()
|
||||
.map(|t| format!("{}", t))
|
||||
.map(|t| format!("{t}"))
|
||||
.join(" / ")
|
||||
.blue()
|
||||
.to_string()
|
||||
@@ -148,7 +148,7 @@ async fn main() -> PyResult<()> {
|
||||
.next()
|
||||
.unwrap_or_else(|| format!("0x{}", hex::encode_upper(&data)));
|
||||
|
||||
println!(" [{}]: {}", code_str, data_str)
|
||||
println!(" [{code_str}]: {data_str}")
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
||||
+9
-9
@@ -163,7 +163,7 @@ impl CommonDataType {
|
||||
/// Apply type-specific human-oriented formatting to data, if any is applicable
|
||||
pub fn format_data(&self, data: &[u8]) -> Option<String> {
|
||||
match self {
|
||||
Self::Flags => Some(Flags::matching(data).map(|f| format!("{:?}", f)).join(",")),
|
||||
Self::Flags => Some(Flags::matching(data).map(|f| format!("{f:?}")).join(",")),
|
||||
Self::CompleteListOf16BitServiceClassUuids
|
||||
| Self::IncompleteListOf16BitServiceClassUuids
|
||||
| Self::ListOf16BitServiceSolicitationUuids => {
|
||||
@@ -174,8 +174,8 @@ impl CommonDataType {
|
||||
.map(|uuid| {
|
||||
SERVICE_IDS
|
||||
.get(&uuid)
|
||||
.map(|name| format!("{:?} ({name})", uuid))
|
||||
.unwrap_or_else(|| format!("{:?}", uuid))
|
||||
.map(|name| format!("{uuid:?} ({name})"))
|
||||
.unwrap_or_else(|| format!("{uuid:?}"))
|
||||
})
|
||||
.join(", ")
|
||||
})
|
||||
@@ -185,14 +185,14 @@ impl CommonDataType {
|
||||
| Self::IncompleteListOf32BitServiceClassUuids
|
||||
| Self::ListOf32BitServiceSolicitationUuids => {
|
||||
combinator::complete(multi::many0(Uuid32::parse))(data)
|
||||
.map(|(_res, uuids)| uuids.into_iter().map(|u| format!("{:?}", u)).join(", "))
|
||||
.map(|(_res, uuids)| uuids.into_iter().map(|u| format!("{u:?}")).join(", "))
|
||||
.ok()
|
||||
}
|
||||
Self::CompleteListOf128BitServiceClassUuids
|
||||
| Self::IncompleteListOf128BitServiceClassUuids
|
||||
| Self::ListOf128BitServiceSolicitationUuids => {
|
||||
combinator::complete(multi::many0(Uuid128::parse_le))(data)
|
||||
.map(|(_res, uuids)| uuids.into_iter().map(|u| format!("{:?}", u)).join(", "))
|
||||
.map(|(_res, uuids)| uuids.into_iter().map(|u| format!("{u:?}")).join(", "))
|
||||
.ok()
|
||||
}
|
||||
Self::ServiceData16BitUuid => Uuid16::parse_le(data)
|
||||
@@ -201,8 +201,8 @@ impl CommonDataType {
|
||||
"service={:?}, data={}",
|
||||
SERVICE_IDS
|
||||
.get(&uuid)
|
||||
.map(|name| format!("{:?} ({name})", uuid))
|
||||
.unwrap_or_else(|| format!("{:?}", uuid)),
|
||||
.map(|name| format!("{uuid:?} ({name})"))
|
||||
.unwrap_or_else(|| format!("{uuid:?}")),
|
||||
hex::encode_upper(rem)
|
||||
)
|
||||
})
|
||||
@@ -214,7 +214,7 @@ impl CommonDataType {
|
||||
.map(|(rem, uuid)| format!("service={:?}, data={}", uuid, hex::encode_upper(rem)))
|
||||
.ok(),
|
||||
Self::ShortenedLocalName | Self::CompleteLocalName => {
|
||||
std::str::from_utf8(data).ok().map(|s| format!("\"{}\"", s))
|
||||
std::str::from_utf8(data).ok().map(|s| format!("\"{s}\""))
|
||||
}
|
||||
Self::TxPowerLevel => {
|
||||
let (_, tx) =
|
||||
@@ -230,7 +230,7 @@ impl CommonDataType {
|
||||
COMPANY_IDS
|
||||
.get(&id)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| format!("{:?}", id)),
|
||||
.unwrap_or_else(|| format!("{id:?}")),
|
||||
hex::encode_upper(rem)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ pub async fn start(args: &Args, device: &mut Device) -> PyResult<()> {
|
||||
ble_connection.on_disconnection(|_py, reason| {
|
||||
let disconnection_info = match HciConstant::error_name(reason) {
|
||||
Ok(info_string) => info_string,
|
||||
Err(py_err) => format!("failed to get disconnection error name ({})", py_err),
|
||||
Err(py_err) => format!("failed to get disconnection error name ({py_err})"),
|
||||
};
|
||||
println!(
|
||||
"{} {}",
|
||||
@@ -114,10 +114,10 @@ async fn proxy_data_between_tcp_and_l2cap(
|
||||
mtu: Option<u16>,
|
||||
mps: Option<u16>,
|
||||
) -> PyResult<()> {
|
||||
println!("{}", format!("<<< TCP connection from {}", addr).magenta());
|
||||
println!("{}", format!("<<< TCP connection from {addr}").magenta());
|
||||
println!(
|
||||
"{}",
|
||||
format!(">>> Opening L2CAP channel on PSM = {}", psm).yellow()
|
||||
format!(">>> Opening L2CAP channel on PSM = {psm}").yellow()
|
||||
);
|
||||
|
||||
let mut l2cap_channel = match ble_connection
|
||||
|
||||
@@ -158,7 +158,7 @@ async fn proxy_tcp_rx_to_l2cap_tx(
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("!!! TCP connection lost: {}", e).red());
|
||||
println!("{}", format!("!!! TCP connection lost: {e}").red());
|
||||
if let Some(mut channel) = l2cap_channel.lock().await.take() {
|
||||
let _ = channel.disconnect().await.map_err(|e| {
|
||||
eprintln!("Failed to call disconnect on l2cap channel: {e}");
|
||||
|
||||
@@ -83,7 +83,7 @@ pub async fn start(args: &Args, device: &mut Device) -> PyResult<()> {
|
||||
connection.on_disconnection(|_py, reason| {
|
||||
let disconnection_info = match HciConstant::error_name(reason) {
|
||||
Ok(info_string) => info_string,
|
||||
Err(py_err) => format!("failed to get disconnection error name ({})", py_err),
|
||||
Err(py_err) => format!("failed to get disconnection error name ({py_err})"),
|
||||
};
|
||||
println!(
|
||||
"{} {}",
|
||||
|
||||
@@ -100,7 +100,7 @@ pub(crate) fn probe(verbose: bool) -> anyhow::Result<()> {
|
||||
.map(|serials| serials.contains(s))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
transport_names.push(format!("{}/{}", basic_transport_name, s))
|
||||
transport_names.push(format!("{basic_transport_name}/{s}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ impl ClassInfo {
|
||||
self.sub_class,
|
||||
self.protocol,
|
||||
self.protocol_name()
|
||||
.map(|s| format!(" [{}]", s))
|
||||
.map(|s| format!(" [{s}]"))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
}
|
||||
|
||||
+106
-40
@@ -19,6 +19,7 @@ import asyncio
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
from unittest import mock
|
||||
import pytest
|
||||
|
||||
from bumble.core import (
|
||||
@@ -28,11 +29,15 @@ from bumble.core import (
|
||||
from bumble.device import (
|
||||
AdvertisingEventProperties,
|
||||
AdvertisingParameters,
|
||||
CigParameters,
|
||||
CisLink,
|
||||
Connection,
|
||||
Device,
|
||||
PeriodicAdvertisingParameters,
|
||||
)
|
||||
from bumble.host import DataPacketQueue, Host
|
||||
from bumble import device
|
||||
from bumble import hci
|
||||
from bumble.hci import (
|
||||
HCI_ACCEPT_CONNECTION_REQUEST_COMMAND,
|
||||
HCI_COMMAND_STATUS_PENDING,
|
||||
@@ -122,7 +127,7 @@ async def test_device_connect_parallel():
|
||||
HCI_Connection_Request_Event(
|
||||
bd_addr=d0.public_address,
|
||||
class_of_device=0,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -142,7 +147,7 @@ async def test_device_connect_parallel():
|
||||
HCI_Connection_Request_Event(
|
||||
bd_addr=d0.public_address,
|
||||
class_of_device=0,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -165,7 +170,7 @@ async def test_device_connect_parallel():
|
||||
status=HCI_SUCCESS,
|
||||
connection_handle=0x100,
|
||||
bd_addr=d0.public_address,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
encryption_enabled=True,
|
||||
)
|
||||
)
|
||||
@@ -175,7 +180,7 @@ async def test_device_connect_parallel():
|
||||
status=HCI_SUCCESS,
|
||||
connection_handle=0x100,
|
||||
bd_addr=d1.public_address,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
encryption_enabled=True,
|
||||
)
|
||||
)
|
||||
@@ -199,7 +204,7 @@ async def test_device_connect_parallel():
|
||||
status=HCI_SUCCESS,
|
||||
connection_handle=0x101,
|
||||
bd_addr=d0.public_address,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
encryption_enabled=True,
|
||||
)
|
||||
)
|
||||
@@ -209,7 +214,7 @@ async def test_device_connect_parallel():
|
||||
status=HCI_SUCCESS,
|
||||
connection_handle=0x101,
|
||||
bd_addr=d2.public_address,
|
||||
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||
link_type=HCI_Connection_Complete_Event.LinkType.ACL,
|
||||
encryption_enabled=True,
|
||||
)
|
||||
)
|
||||
@@ -477,16 +482,13 @@ async def test_cis():
|
||||
|
||||
peripheral_cis_futures = {}
|
||||
|
||||
def on_cis_request(
|
||||
acl_connection: Connection,
|
||||
cis_handle: int,
|
||||
_cig_id: int,
|
||||
_cis_id: int,
|
||||
):
|
||||
acl_connection.cancel_on_disconnection(
|
||||
devices[1].accept_cis_request(cis_handle)
|
||||
def on_cis_request(cis_link: CisLink):
|
||||
cis_link.acl_connection.cancel_on_disconnection(
|
||||
devices[1].accept_cis_request(cis_link),
|
||||
)
|
||||
peripheral_cis_futures[cis_link.handle] = (
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
peripheral_cis_futures[cis_handle] = asyncio.get_running_loop().create_future()
|
||||
|
||||
devices[1].on('cis_request', on_cis_request)
|
||||
devices[1].on(
|
||||
@@ -495,19 +497,21 @@ async def test_cis():
|
||||
)
|
||||
|
||||
cis_handles = await devices[0].setup_cig(
|
||||
cig_id=1,
|
||||
cis_id=[2, 3],
|
||||
sdu_interval=(0, 0),
|
||||
framing=0,
|
||||
max_sdu=(0, 0),
|
||||
retransmission_number=0,
|
||||
max_transport_latency=(0, 0),
|
||||
CigParameters(
|
||||
cig_id=1,
|
||||
cis_parameters=[
|
||||
CigParameters.CisParameters(cis_id=2),
|
||||
CigParameters.CisParameters(cis_id=3),
|
||||
],
|
||||
sdu_interval_c_to_p=0,
|
||||
sdu_interval_p_to_c=0,
|
||||
),
|
||||
)
|
||||
assert len(cis_handles) == 2
|
||||
cis_links = await devices[0].create_cis(
|
||||
[
|
||||
(cis_handles[0], devices.connections[0].handle),
|
||||
(cis_handles[1], devices.connections[0].handle),
|
||||
(cis_handles[0], devices.connections[0]),
|
||||
(cis_handles[1], devices.connections[0]),
|
||||
]
|
||||
)
|
||||
await asyncio.gather(*peripheral_cis_futures.values())
|
||||
@@ -525,32 +529,27 @@ async def test_cis_setup_failure():
|
||||
|
||||
cis_requests = asyncio.Queue()
|
||||
|
||||
def on_cis_request(
|
||||
acl_connection: Connection,
|
||||
cis_handle: int,
|
||||
cig_id: int,
|
||||
cis_id: int,
|
||||
):
|
||||
del acl_connection, cig_id, cis_id
|
||||
cis_requests.put_nowait(cis_handle)
|
||||
def on_cis_request(cis_link: CisLink):
|
||||
cis_requests.put_nowait(cis_link)
|
||||
|
||||
devices[1].on('cis_request', on_cis_request)
|
||||
|
||||
cis_handles = await devices[0].setup_cig(
|
||||
cig_id=1,
|
||||
cis_id=[2],
|
||||
sdu_interval=(0, 0),
|
||||
framing=0,
|
||||
max_sdu=(0, 0),
|
||||
retransmission_number=0,
|
||||
max_transport_latency=(0, 0),
|
||||
CigParameters(
|
||||
cig_id=1,
|
||||
cis_parameters=[
|
||||
CigParameters.CisParameters(cis_id=2),
|
||||
],
|
||||
sdu_interval_c_to_p=0,
|
||||
sdu_interval_p_to_c=0,
|
||||
),
|
||||
)
|
||||
assert len(cis_handles) == 1
|
||||
|
||||
cis_create_task = asyncio.create_task(
|
||||
devices[0].create_cis(
|
||||
[
|
||||
(cis_handles[0], devices.connections[0].handle),
|
||||
(cis_handles[0], devices.connections[0]),
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -630,6 +629,73 @@ def test_gatt_services_with_gas_and_gatt():
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_inquiry_result():
|
||||
d = (await TwoDevices.create_with_connection())[0]
|
||||
m = mock.Mock()
|
||||
d.on(d.EVENT_INQUIRY_RESULT, m)
|
||||
d.host.on_packet(
|
||||
bytes(
|
||||
hci.HCI_Extended_Inquiry_Result_Event(
|
||||
num_responses=1,
|
||||
bd_addr=hci.Address("00:11:22:33:44:55/P"),
|
||||
page_scan_repetition_mode=2,
|
||||
reserved=0,
|
||||
class_of_device=3,
|
||||
clock_offset=4,
|
||||
rssi=5,
|
||||
extended_inquiry_response=b"6789",
|
||||
)
|
||||
)
|
||||
)
|
||||
m.assert_called_with(hci.Address("00:11:22:33:44:55/P"), 3, mock.ANY, 5)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_extended_inquiry_result():
|
||||
d = (await TwoDevices.create_with_connection())[0]
|
||||
m = mock.Mock()
|
||||
d.on(d.EVENT_INQUIRY_RESULT, m)
|
||||
d.host.on_packet(
|
||||
bytes(
|
||||
hci.HCI_Extended_Inquiry_Result_Event(
|
||||
num_responses=1,
|
||||
bd_addr=hci.Address("00:11:22:33:44:55/P"),
|
||||
page_scan_repetition_mode=2,
|
||||
reserved=0,
|
||||
class_of_device=3,
|
||||
clock_offset=4,
|
||||
rssi=5,
|
||||
extended_inquiry_response=b"6789",
|
||||
)
|
||||
)
|
||||
)
|
||||
m.assert_called_with(hci.Address("00:11:22:33:44:55/P"), 3, mock.ANY, 5)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_inquiry_result_with_rssi():
|
||||
d = (await TwoDevices.create_with_connection())[0]
|
||||
m = mock.Mock()
|
||||
d.on(d.EVENT_INQUIRY_RESULT, m)
|
||||
d.host.on_packet(
|
||||
bytes(
|
||||
hci.HCI_Inquiry_Result_With_RSSI_Event(
|
||||
bd_addr=[hci.Address("00:11:22:33:44:55/P")],
|
||||
page_scan_repetition_mode=[2],
|
||||
reserved=[0],
|
||||
class_of_device=[3],
|
||||
clock_offset=[4],
|
||||
rssi=[5],
|
||||
)
|
||||
)
|
||||
)
|
||||
m.assert_called_with(hci.Address("00:11:22:33:44:55/P"), 3, mock.ANY, 5)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run_test_device():
|
||||
await test_device_connect_parallel()
|
||||
|
||||
+222
-178
@@ -15,62 +15,12 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import inspect
|
||||
import struct
|
||||
|
||||
from bumble.hci import (
|
||||
HCI_DISCONNECT_COMMAND,
|
||||
HCI_LE_1M_PHY_BIT,
|
||||
HCI_LE_CODED_PHY_BIT,
|
||||
HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_RESET_COMMAND,
|
||||
HCI_VENDOR_EVENT,
|
||||
HCI_SUCCESS,
|
||||
HCI_LE_CONNECTION_COMPLETE_EVENT,
|
||||
HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
|
||||
Address,
|
||||
CodingFormat,
|
||||
CodecID,
|
||||
HCI_Command,
|
||||
HCI_Command_Complete_Event,
|
||||
HCI_Command_Status_Event,
|
||||
HCI_CustomPacket,
|
||||
HCI_Disconnect_Command,
|
||||
HCI_Event,
|
||||
HCI_IsoDataPacket,
|
||||
HCI_LE_Add_Device_To_Filter_Accept_List_Command,
|
||||
HCI_LE_Advertising_Report_Event,
|
||||
HCI_LE_Channel_Selection_Algorithm_Event,
|
||||
HCI_LE_Connection_Complete_Event,
|
||||
HCI_LE_Connection_Update_Command,
|
||||
HCI_LE_Connection_Update_Complete_Event,
|
||||
HCI_LE_Create_Connection_Command,
|
||||
HCI_LE_Extended_Create_Connection_Command,
|
||||
HCI_LE_Read_Buffer_Size_Command,
|
||||
HCI_LE_Read_Remote_Features_Command,
|
||||
HCI_LE_Read_Remote_Features_Complete_Event,
|
||||
HCI_LE_Remove_Device_From_Filter_Accept_List_Command,
|
||||
HCI_LE_Set_Advertising_Data_Command,
|
||||
HCI_LE_Set_Advertising_Parameters_Command,
|
||||
HCI_LE_Set_Default_PHY_Command,
|
||||
HCI_LE_Set_Event_Mask_Command,
|
||||
HCI_LE_Set_Extended_Advertising_Enable_Command,
|
||||
HCI_LE_Set_Extended_Scan_Parameters_Command,
|
||||
HCI_LE_Set_Random_Address_Command,
|
||||
HCI_LE_Set_Scan_Enable_Command,
|
||||
HCI_LE_Set_Scan_Parameters_Command,
|
||||
HCI_LE_Setup_ISO_Data_Path_Command,
|
||||
HCI_Number_Of_Completed_Packets_Event,
|
||||
HCI_Packet,
|
||||
HCI_PIN_Code_Request_Reply_Command,
|
||||
HCI_Read_Local_Supported_Codecs_Command,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command,
|
||||
HCI_Read_Local_Supported_Commands_Command,
|
||||
HCI_Read_Local_Supported_Features_Command,
|
||||
HCI_Read_Local_Version_Information_Command,
|
||||
HCI_Reset_Command,
|
||||
HCI_Set_Event_Mask_Command,
|
||||
HCI_Vendor_Event,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from bumble import hci
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -80,7 +30,7 @@ from bumble.hci import (
|
||||
def basic_check(x):
|
||||
packet = bytes(x)
|
||||
print(packet.hex())
|
||||
parsed = HCI_Packet.from_bytes(packet)
|
||||
parsed = hci.HCI_Packet.from_bytes(packet)
|
||||
x_str = str(x)
|
||||
parsed_str = str(parsed)
|
||||
print(x_str)
|
||||
@@ -91,18 +41,18 @@ def basic_check(x):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Event():
|
||||
event = HCI_Event(0xF9)
|
||||
event = hci.HCI_Event(event_code=0xF9)
|
||||
basic_check(event)
|
||||
|
||||
event = HCI_Event(0xF8, bytes.fromhex('AABBCC'))
|
||||
event = hci.HCI_Event(event_code=0xF8, parameters=bytes.fromhex('AABBCC'))
|
||||
basic_check(event)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Connection_Complete_Event():
|
||||
address = Address('00:11:22:33:44:55')
|
||||
event = HCI_LE_Connection_Complete_Event(
|
||||
status=HCI_SUCCESS,
|
||||
address = hci.Address('00:11:22:33:44:55')
|
||||
event = hci.HCI_LE_Connection_Complete_Event(
|
||||
status=hci.HCI_SUCCESS,
|
||||
connection_handle=1,
|
||||
role=1,
|
||||
peer_address_type=1,
|
||||
@@ -117,25 +67,47 @@ def test_HCI_LE_Connection_Complete_Event():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Advertising_Report_Event():
|
||||
address = Address('00:11:22:33:44:55/P')
|
||||
report = HCI_LE_Advertising_Report_Event.Report(
|
||||
HCI_LE_Advertising_Report_Event.Report.FIELDS,
|
||||
event_type=HCI_LE_Advertising_Report_Event.ADV_IND,
|
||||
address_type=Address.PUBLIC_DEVICE_ADDRESS,
|
||||
address = hci.Address('00:11:22:33:44:55/P')
|
||||
report = hci.HCI_LE_Advertising_Report_Event.Report(
|
||||
event_type=hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
|
||||
address_type=hci.Address.PUBLIC_DEVICE_ADDRESS,
|
||||
address=address,
|
||||
data=bytes.fromhex(
|
||||
'0201061106ba5689a6fabfa2bd01467d6e00fbabad08160a181604659b03'
|
||||
),
|
||||
rssi=100,
|
||||
)
|
||||
event = HCI_LE_Advertising_Report_Event([report])
|
||||
event = hci.HCI_LE_Advertising_Report_Event([report])
|
||||
basic_check(event)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Extended_Advertising_Report_Event():
|
||||
address = hci.Address('00:11:22:33:44:55/P')
|
||||
report = hci.HCI_LE_Extended_Advertising_Report_Event.Report(
|
||||
event_type=hci.HCI_LE_Extended_Advertising_Report_Event.EventType.CONNECTABLE_ADVERTISING,
|
||||
address_type=hci.Address.PUBLIC_DEVICE_ADDRESS,
|
||||
address=address,
|
||||
data=bytes.fromhex(
|
||||
'0201061106ba5689a6fabfa2bd01467d6e00fbabad08160a181604659b03'
|
||||
),
|
||||
rssi=100,
|
||||
primary_phy=hci.HCI_LE_1M_PHY,
|
||||
secondary_phy=hci.HCI_LE_CODED_PHY,
|
||||
advertising_sid=0,
|
||||
tx_power=10,
|
||||
periodic_advertising_interval=2,
|
||||
direct_address=hci.Address('00:11:22:33:44:55/P'),
|
||||
direct_address_type=hci.Address.PUBLIC_DEVICE_ADDRESS,
|
||||
)
|
||||
event = hci.HCI_LE_Extended_Advertising_Report_Event([report])
|
||||
basic_check(event)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Read_Remote_Features_Complete_Event():
|
||||
event = HCI_LE_Read_Remote_Features_Complete_Event(
|
||||
status=HCI_SUCCESS,
|
||||
event = hci.HCI_LE_Read_Remote_Features_Complete_Event(
|
||||
status=hci.HCI_SUCCESS,
|
||||
connection_handle=0x007,
|
||||
le_features=bytes.fromhex('0011223344556677'),
|
||||
)
|
||||
@@ -144,8 +116,8 @@ def test_HCI_LE_Read_Remote_Features_Complete_Event():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Connection_Update_Complete_Event():
|
||||
event = HCI_LE_Connection_Update_Complete_Event(
|
||||
status=HCI_SUCCESS,
|
||||
event = hci.HCI_LE_Connection_Update_Complete_Event(
|
||||
status=hci.HCI_SUCCESS,
|
||||
connection_handle=0x007,
|
||||
connection_interval=10,
|
||||
peripheral_latency=3,
|
||||
@@ -156,7 +128,7 @@ def test_HCI_LE_Connection_Update_Complete_Event():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Channel_Selection_Algorithm_Event():
|
||||
event = HCI_LE_Channel_Selection_Algorithm_Event(
|
||||
event = hci.HCI_LE_Channel_Selection_Algorithm_Event(
|
||||
connection_handle=7, channel_selection_algorithm=1
|
||||
)
|
||||
basic_check(event)
|
||||
@@ -165,10 +137,10 @@ def test_HCI_LE_Channel_Selection_Algorithm_Event():
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Command_Complete_Event():
|
||||
# With a serializable object
|
||||
event = HCI_Command_Complete_Event(
|
||||
event = hci.HCI_Command_Complete_Event(
|
||||
num_hci_command_packets=34,
|
||||
command_opcode=HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
return_parameters=HCI_LE_Read_Buffer_Size_Command.create_return_parameters(
|
||||
command_opcode=hci.HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
return_parameters=hci.HCI_LE_Read_Buffer_Size_Command.create_return_parameters(
|
||||
status=0,
|
||||
le_acl_data_packet_length=1234,
|
||||
total_num_le_acl_data_packets=56,
|
||||
@@ -177,26 +149,28 @@ def test_HCI_Command_Complete_Event():
|
||||
basic_check(event)
|
||||
|
||||
# With an arbitrary byte array
|
||||
event = HCI_Command_Complete_Event(
|
||||
event = hci.HCI_Command_Complete_Event(
|
||||
num_hci_command_packets=1,
|
||||
command_opcode=HCI_RESET_COMMAND,
|
||||
command_opcode=hci.HCI_RESET_COMMAND,
|
||||
return_parameters=bytes([1, 2, 3, 4]),
|
||||
)
|
||||
basic_check(event)
|
||||
|
||||
# With a simple status as a 1-byte array
|
||||
event = HCI_Command_Complete_Event(
|
||||
event = hci.HCI_Command_Complete_Event(
|
||||
num_hci_command_packets=1,
|
||||
command_opcode=HCI_RESET_COMMAND,
|
||||
command_opcode=hci.HCI_RESET_COMMAND,
|
||||
return_parameters=bytes([7]),
|
||||
)
|
||||
basic_check(event)
|
||||
event = HCI_Packet.from_bytes(bytes(event))
|
||||
event = hci.HCI_Packet.from_bytes(bytes(event))
|
||||
assert event.return_parameters == 7
|
||||
|
||||
# With a simple status as an integer status
|
||||
event = HCI_Command_Complete_Event(
|
||||
num_hci_command_packets=1, command_opcode=HCI_RESET_COMMAND, return_parameters=9
|
||||
event = hci.HCI_Command_Complete_Event(
|
||||
num_hci_command_packets=1,
|
||||
command_opcode=hci.HCI_RESET_COMMAND,
|
||||
return_parameters=9,
|
||||
)
|
||||
basic_check(event)
|
||||
assert event.return_parameters == 9
|
||||
@@ -204,30 +178,35 @@ def test_HCI_Command_Complete_Event():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Command_Status_Event():
|
||||
event = HCI_Command_Status_Event(
|
||||
status=0, num_hci_command_packets=37, command_opcode=HCI_DISCONNECT_COMMAND
|
||||
event = hci.HCI_Command_Status_Event(
|
||||
status=0, num_hci_command_packets=37, command_opcode=hci.HCI_DISCONNECT_COMMAND
|
||||
)
|
||||
basic_check(event)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Number_Of_Completed_Packets_Event():
|
||||
event = HCI_Number_Of_Completed_Packets_Event([(1, 2), (3, 4)])
|
||||
event = hci.HCI_Number_Of_Completed_Packets_Event(
|
||||
connection_handles=(1, 2),
|
||||
num_completed_packets=(3, 4),
|
||||
)
|
||||
basic_check(event)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Vendor_Event():
|
||||
data = bytes.fromhex('01020304')
|
||||
event = HCI_Vendor_Event(data=data)
|
||||
event = hci.HCI_Vendor_Event(data=data)
|
||||
event_bytes = bytes(event)
|
||||
parsed = HCI_Packet.from_bytes(event_bytes)
|
||||
assert isinstance(parsed, HCI_Vendor_Event)
|
||||
parsed = hci.HCI_Packet.from_bytes(event_bytes)
|
||||
assert isinstance(parsed, hci.HCI_Vendor_Event)
|
||||
assert parsed.data == data
|
||||
|
||||
class HCI_Custom_Event(HCI_Event):
|
||||
class HCI_Custom_Event(hci.HCI_Event):
|
||||
def __init__(self, blabla):
|
||||
super().__init__(HCI_VENDOR_EVENT, parameters=struct.pack("<I", blabla))
|
||||
super().__init__(
|
||||
event_code=hci.HCI_VENDOR_EVENT, parameters=struct.pack("<I", blabla)
|
||||
)
|
||||
self.name = 'HCI_CUSTOM_EVENT'
|
||||
self.blabla = blabla
|
||||
|
||||
@@ -236,41 +215,91 @@ def test_HCI_Vendor_Event():
|
||||
return HCI_Custom_Event(blabla=struct.unpack('<I', payload)[0])
|
||||
return None
|
||||
|
||||
HCI_Event.add_vendor_factory(create_event)
|
||||
parsed = HCI_Packet.from_bytes(event_bytes)
|
||||
hci.HCI_Event.add_vendor_factory(create_event)
|
||||
parsed = hci.HCI_Packet.from_bytes(event_bytes)
|
||||
assert isinstance(parsed, HCI_Custom_Event)
|
||||
assert parsed.blabla == 0x04030201
|
||||
event_bytes2 = event_bytes[:3] + bytes([7]) + event_bytes[4:]
|
||||
parsed = HCI_Packet.from_bytes(event_bytes2)
|
||||
parsed = hci.HCI_Packet.from_bytes(event_bytes2)
|
||||
assert not isinstance(parsed, HCI_Custom_Event)
|
||||
assert isinstance(parsed, HCI_Vendor_Event)
|
||||
HCI_Event.remove_vendor_factory(create_event)
|
||||
assert isinstance(parsed, hci.HCI_Vendor_Event)
|
||||
hci.HCI_Event.remove_vendor_factory(create_event)
|
||||
|
||||
parsed = HCI_Packet.from_bytes(event_bytes)
|
||||
parsed = hci.HCI_Packet.from_bytes(event_bytes)
|
||||
assert not isinstance(parsed, HCI_Custom_Event)
|
||||
assert isinstance(parsed, HCI_Vendor_Event)
|
||||
assert isinstance(parsed, hci.HCI_Vendor_Event)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Command():
|
||||
command = HCI_Command(0x5566)
|
||||
command = hci.HCI_Command(op_code=0x5566)
|
||||
basic_check(command)
|
||||
|
||||
command = HCI_Command(0x5566, bytes.fromhex('AABBCC'))
|
||||
command = hci.HCI_Command(op_code=0x5566, parameters=bytes.fromhex('AABBCC'))
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize(
|
||||
"clazz,",
|
||||
[
|
||||
clazz[1]
|
||||
for clazz in inspect.getmembers(hci)
|
||||
if isinstance(clazz[1], type)
|
||||
and issubclass(clazz[1], hci.HCI_Command)
|
||||
and clazz[1] is not hci.HCI_Command
|
||||
],
|
||||
)
|
||||
def test_hci_command_subclasses_op_code(clazz: type[hci.HCI_Command]):
|
||||
assert clazz.op_code > 0
|
||||
assert isinstance(clazz.name, str)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize(
|
||||
"clazz,",
|
||||
[
|
||||
clazz[1]
|
||||
for clazz in inspect.getmembers(hci)
|
||||
if isinstance(clazz[1], type)
|
||||
and clazz[1] is not hci.HCI_Event
|
||||
and issubclass(clazz[1], hci.HCI_Event)
|
||||
and not issubclass(clazz[1], hci.HCI_Extended_Event)
|
||||
],
|
||||
)
|
||||
def test_hci_event_subclasses_event_code(clazz: type[hci.HCI_Event]):
|
||||
assert clazz.event_code > 0
|
||||
assert isinstance(clazz.name, str)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize(
|
||||
"clazz,",
|
||||
[
|
||||
clazz[1]
|
||||
for clazz in inspect.getmembers(hci)
|
||||
if isinstance(clazz[1], type)
|
||||
and issubclass(clazz[1], hci.HCI_Extended_Event)
|
||||
and clazz[1] not in (hci.HCI_Extended_Event, hci.HCI_LE_Meta_Event)
|
||||
],
|
||||
)
|
||||
def test_hci_extended_event_subclasses_event_code(clazz: type[hci.HCI_Extended_Event]):
|
||||
assert clazz.event_code > 0
|
||||
assert clazz.subevent_code > 0
|
||||
assert isinstance(clazz.name, str)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_PIN_Code_Request_Reply_Command():
|
||||
pin_code = b'1234'
|
||||
pin_code_length = len(pin_code)
|
||||
# here to make the test pass, we need to
|
||||
# pad pin_code, as HCI_Object.format_fields
|
||||
# pad pin_code, as hci.HCI_Object.format_fields
|
||||
# does not do it for us
|
||||
padded_pin_code = pin_code + bytes(16 - pin_code_length)
|
||||
command = HCI_PIN_Code_Request_Reply_Command(
|
||||
bd_addr=Address(
|
||||
'00:11:22:33:44:55', address_type=Address.PUBLIC_DEVICE_ADDRESS
|
||||
command = hci.HCI_PIN_Code_Request_Reply_Command(
|
||||
bd_addr=hci.Address(
|
||||
'00:11:22:33:44:55', address_type=hci.Address.PUBLIC_DEVICE_ADDRESS
|
||||
),
|
||||
pin_code_length=pin_code_length,
|
||||
pin_code=padded_pin_code,
|
||||
@@ -279,47 +308,49 @@ def test_HCI_PIN_Code_Request_Reply_Command():
|
||||
|
||||
|
||||
def test_HCI_Reset_Command():
|
||||
command = HCI_Reset_Command()
|
||||
command = hci.HCI_Reset_Command()
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Read_Local_Version_Information_Command():
|
||||
command = HCI_Read_Local_Version_Information_Command()
|
||||
command = hci.HCI_Read_Local_Version_Information_Command()
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Read_Local_Supported_Commands_Command():
|
||||
command = HCI_Read_Local_Supported_Commands_Command()
|
||||
command = hci.HCI_Read_Local_Supported_Commands_Command()
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Read_Local_Supported_Features_Command():
|
||||
command = HCI_Read_Local_Supported_Features_Command()
|
||||
command = hci.HCI_Read_Local_Supported_Features_Command()
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Disconnect_Command():
|
||||
command = HCI_Disconnect_Command(connection_handle=123, reason=0x11)
|
||||
command = hci.HCI_Disconnect_Command(connection_handle=123, reason=0x11)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Set_Event_Mask_Command():
|
||||
command = HCI_Set_Event_Mask_Command(event_mask=bytes.fromhex('0011223344556677'))
|
||||
command = hci.HCI_Set_Event_Mask_Command(
|
||||
event_mask=bytes.fromhex('0011223344556677')
|
||||
)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Event_Mask_Command():
|
||||
command = HCI_LE_Set_Event_Mask_Command(
|
||||
le_event_mask=HCI_LE_Set_Event_Mask_Command.mask(
|
||||
command = hci.HCI_LE_Set_Event_Mask_Command(
|
||||
le_event_mask=hci.HCI_LE_Set_Event_Mask_Command.mask(
|
||||
[
|
||||
HCI_LE_CONNECTION_COMPLETE_EVENT,
|
||||
HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
|
||||
hci.HCI_LE_CONNECTION_COMPLETE_EVENT,
|
||||
hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -329,21 +360,21 @@ def test_HCI_LE_Set_Event_Mask_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Random_Address_Command():
|
||||
command = HCI_LE_Set_Random_Address_Command(
|
||||
random_address=Address('00:11:22:33:44:55')
|
||||
command = hci.HCI_LE_Set_Random_Address_Command(
|
||||
random_address=hci.Address('00:11:22:33:44:55')
|
||||
)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Advertising_Parameters_Command():
|
||||
command = HCI_LE_Set_Advertising_Parameters_Command(
|
||||
command = hci.HCI_LE_Set_Advertising_Parameters_Command(
|
||||
advertising_interval_min=20,
|
||||
advertising_interval_max=30,
|
||||
advertising_type=HCI_LE_Set_Advertising_Parameters_Command.ADV_NONCONN_IND,
|
||||
own_address_type=Address.PUBLIC_DEVICE_ADDRESS,
|
||||
peer_address_type=Address.RANDOM_DEVICE_ADDRESS,
|
||||
peer_address=Address('00:11:22:33:44:55'),
|
||||
advertising_type=hci.HCI_LE_Set_Advertising_Parameters_Command.AdvertisingType.ADV_NONCONN_IND,
|
||||
own_address_type=hci.Address.PUBLIC_DEVICE_ADDRESS,
|
||||
peer_address_type=hci.Address.RANDOM_DEVICE_ADDRESS,
|
||||
peer_address=hci.Address('00:11:22:33:44:55'),
|
||||
advertising_channel_map=0x03,
|
||||
advertising_filter_policy=1,
|
||||
)
|
||||
@@ -352,7 +383,7 @@ def test_HCI_LE_Set_Advertising_Parameters_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Advertising_Data_Command():
|
||||
command = HCI_LE_Set_Advertising_Data_Command(
|
||||
command = hci.HCI_LE_Set_Advertising_Data_Command(
|
||||
advertising_data=bytes.fromhex('AABBCC')
|
||||
)
|
||||
basic_check(command)
|
||||
@@ -360,7 +391,7 @@ def test_HCI_LE_Set_Advertising_Data_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Scan_Parameters_Command():
|
||||
command = HCI_LE_Set_Scan_Parameters_Command(
|
||||
command = hci.HCI_LE_Set_Scan_Parameters_Command(
|
||||
le_scan_type=1,
|
||||
le_scan_interval=20,
|
||||
le_scan_window=10,
|
||||
@@ -372,18 +403,18 @@ def test_HCI_LE_Set_Scan_Parameters_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Scan_Enable_Command():
|
||||
command = HCI_LE_Set_Scan_Enable_Command(le_scan_enable=1, filter_duplicates=0)
|
||||
command = hci.HCI_LE_Set_Scan_Enable_Command(le_scan_enable=1, filter_duplicates=0)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Create_Connection_Command():
|
||||
command = HCI_LE_Create_Connection_Command(
|
||||
command = hci.HCI_LE_Create_Connection_Command(
|
||||
le_scan_interval=4,
|
||||
le_scan_window=5,
|
||||
initiator_filter_policy=1,
|
||||
peer_address_type=1,
|
||||
peer_address=Address('00:11:22:33:44:55'),
|
||||
peer_address=hci.Address('00:11:22:33:44:55'),
|
||||
own_address_type=2,
|
||||
connection_interval_min=7,
|
||||
connection_interval_max=8,
|
||||
@@ -397,11 +428,11 @@ def test_HCI_LE_Create_Connection_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Extended_Create_Connection_Command():
|
||||
command = HCI_LE_Extended_Create_Connection_Command(
|
||||
command = hci.HCI_LE_Extended_Create_Connection_Command(
|
||||
initiator_filter_policy=0,
|
||||
own_address_type=0,
|
||||
peer_address_type=1,
|
||||
peer_address=Address('00:11:22:33:44:55'),
|
||||
peer_address=hci.Address('00:11:22:33:44:55'),
|
||||
initiating_phys=3,
|
||||
scan_intervals=(10, 11),
|
||||
scan_windows=(12, 13),
|
||||
@@ -417,23 +448,23 @@ def test_HCI_LE_Extended_Create_Connection_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Add_Device_To_Filter_Accept_List_Command():
|
||||
command = HCI_LE_Add_Device_To_Filter_Accept_List_Command(
|
||||
address_type=1, address=Address('00:11:22:33:44:55')
|
||||
command = hci.HCI_LE_Add_Device_To_Filter_Accept_List_Command(
|
||||
address_type=1, address=hci.Address('00:11:22:33:44:55')
|
||||
)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Remove_Device_From_Filter_Accept_List_Command():
|
||||
command = HCI_LE_Remove_Device_From_Filter_Accept_List_Command(
|
||||
address_type=1, address=Address('00:11:22:33:44:55')
|
||||
command = hci.HCI_LE_Remove_Device_From_Filter_Accept_List_Command(
|
||||
address_type=1, address=hci.Address('00:11:22:33:44:55')
|
||||
)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Connection_Update_Command():
|
||||
command = HCI_LE_Connection_Update_Command(
|
||||
command = hci.HCI_LE_Connection_Update_Command(
|
||||
connection_handle=0x0002,
|
||||
connection_interval_min=10,
|
||||
connection_interval_max=20,
|
||||
@@ -447,27 +478,29 @@ def test_HCI_LE_Connection_Update_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Read_Remote_Features_Command():
|
||||
command = HCI_LE_Read_Remote_Features_Command(connection_handle=0x0002)
|
||||
command = hci.HCI_LE_Read_Remote_Features_Command(connection_handle=0x0002)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Default_PHY_Command():
|
||||
command = HCI_LE_Set_Default_PHY_Command(all_phys=0, tx_phys=1, rx_phys=1)
|
||||
command = hci.HCI_LE_Set_Default_PHY_Command(all_phys=0, tx_phys=1, rx_phys=1)
|
||||
basic_check(command)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Extended_Scan_Parameters_Command():
|
||||
command = HCI_LE_Set_Extended_Scan_Parameters_Command(
|
||||
own_address_type=Address.RANDOM_DEVICE_ADDRESS,
|
||||
command = hci.HCI_LE_Set_Extended_Scan_Parameters_Command(
|
||||
own_address_type=hci.Address.RANDOM_DEVICE_ADDRESS,
|
||||
# pylint: disable-next=line-too-long
|
||||
scanning_filter_policy=HCI_LE_Set_Extended_Scan_Parameters_Command.BASIC_FILTERED_POLICY,
|
||||
scanning_phys=(1 << HCI_LE_1M_PHY_BIT | 1 << HCI_LE_CODED_PHY_BIT | 1 << 4),
|
||||
scanning_filter_policy=hci.HCI_LE_Set_Extended_Scan_Parameters_Command.BASIC_FILTERED_POLICY,
|
||||
scanning_phys=(
|
||||
1 << hci.HCI_LE_1M_PHY_BIT | 1 << hci.HCI_LE_CODED_PHY_BIT | 1 << 4
|
||||
),
|
||||
scan_types=[
|
||||
HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
|
||||
HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
|
||||
HCI_LE_Set_Extended_Scan_Parameters_Command.PASSIVE_SCANNING,
|
||||
hci.HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
|
||||
hci.HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
|
||||
hci.HCI_LE_Set_Extended_Scan_Parameters_Command.PASSIVE_SCANNING,
|
||||
],
|
||||
scan_intervals=[1, 2, 3],
|
||||
scan_windows=[4, 5, 6],
|
||||
@@ -477,7 +510,7 @@ def test_HCI_LE_Set_Extended_Scan_Parameters_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Set_Extended_Advertising_Enable_Command():
|
||||
command = HCI_Packet.from_bytes(
|
||||
command = hci.HCI_Packet.from_bytes(
|
||||
bytes.fromhex('0139200e010301050008020600090307000a')
|
||||
)
|
||||
assert command.enable == 1
|
||||
@@ -485,7 +518,7 @@ def test_HCI_LE_Set_Extended_Advertising_Enable_Command():
|
||||
assert command.durations == [5, 6, 7]
|
||||
assert command.max_extended_advertising_events == [8, 9, 10]
|
||||
|
||||
command = HCI_LE_Set_Extended_Advertising_Enable_Command(
|
||||
command = hci.HCI_LE_Set_Extended_Advertising_Enable_Command(
|
||||
enable=1,
|
||||
advertising_handles=[1, 2, 3],
|
||||
durations=[5, 6, 7],
|
||||
@@ -496,20 +529,22 @@ def test_HCI_LE_Set_Extended_Advertising_Enable_Command():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_LE_Setup_ISO_Data_Path_Command():
|
||||
command = HCI_Packet.from_bytes(bytes.fromhex('016e200d60000001030000000000000000'))
|
||||
command = hci.HCI_Packet.from_bytes(
|
||||
bytes.fromhex('016e200d60000001030000000000000000')
|
||||
)
|
||||
|
||||
assert command.connection_handle == 0x0060
|
||||
assert command.data_path_direction == 0x00
|
||||
assert command.data_path_id == 0x01
|
||||
assert command.codec_id == CodingFormat(CodecID.TRANSPARENT)
|
||||
assert command.codec_id == hci.CodingFormat(hci.CodecID.TRANSPARENT)
|
||||
assert command.controller_delay == 0
|
||||
assert command.codec_configuration == b''
|
||||
|
||||
command = HCI_LE_Setup_ISO_Data_Path_Command(
|
||||
command = hci.HCI_LE_Setup_ISO_Data_Path_Command(
|
||||
connection_handle=0x0060,
|
||||
data_path_direction=0x00,
|
||||
data_path_id=0x01,
|
||||
codec_id=CodingFormat(CodecID.TRANSPARENT),
|
||||
codec_id=hci.CodingFormat(hci.CodecID.TRANSPARENT),
|
||||
controller_delay=0x00,
|
||||
codec_configuration=b'',
|
||||
)
|
||||
@@ -519,54 +554,63 @@ def test_HCI_LE_Setup_ISO_Data_Path_Command():
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Read_Local_Supported_Codecs_Command_Complete():
|
||||
returned_parameters = (
|
||||
HCI_Read_Local_Supported_Codecs_Command.parse_return_parameters(
|
||||
bytes([HCI_SUCCESS, 3, CodecID.A_LOG, CodecID.CVSD, CodecID.LINEAR_PCM, 0])
|
||||
)
|
||||
)
|
||||
assert returned_parameters.standard_codec_ids == [
|
||||
CodecID.A_LOG,
|
||||
CodecID.CVSD,
|
||||
CodecID.LINEAR_PCM,
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Read_Local_Supported_Codecs_V2_Command_Complete():
|
||||
returned_parameters = (
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.parse_return_parameters(
|
||||
hci.HCI_Read_Local_Supported_Codecs_Command.parse_return_parameters(
|
||||
bytes(
|
||||
[
|
||||
HCI_SUCCESS,
|
||||
hci.HCI_SUCCESS,
|
||||
3,
|
||||
CodecID.A_LOG,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_ACL,
|
||||
CodecID.CVSD,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_SCO,
|
||||
CodecID.LINEAR_PCM,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.LE_CIS,
|
||||
hci.CodecID.A_LOG,
|
||||
hci.CodecID.CVSD,
|
||||
hci.CodecID.LINEAR_PCM,
|
||||
0,
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
assert returned_parameters.standard_codec_ids == [
|
||||
CodecID.A_LOG,
|
||||
CodecID.CVSD,
|
||||
CodecID.LINEAR_PCM,
|
||||
hci.CodecID.A_LOG,
|
||||
hci.CodecID.CVSD,
|
||||
hci.CodecID.LINEAR_PCM,
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_HCI_Read_Local_Supported_Codecs_V2_Command_Complete():
|
||||
returned_parameters = (
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.parse_return_parameters(
|
||||
bytes(
|
||||
[
|
||||
hci.HCI_SUCCESS,
|
||||
3,
|
||||
hci.CodecID.A_LOG,
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_ACL,
|
||||
hci.CodecID.CVSD,
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_SCO,
|
||||
hci.CodecID.LINEAR_PCM,
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.LE_CIS,
|
||||
0,
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
assert returned_parameters.standard_codec_ids == [
|
||||
hci.CodecID.A_LOG,
|
||||
hci.CodecID.CVSD,
|
||||
hci.CodecID.LINEAR_PCM,
|
||||
]
|
||||
assert returned_parameters.standard_codec_transports == [
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_ACL,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_SCO,
|
||||
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.LE_CIS,
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_ACL,
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_SCO,
|
||||
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.LE_CIS,
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_address():
|
||||
a = Address('C4:F2:17:1A:1D:BB')
|
||||
a = hci.Address('C4:F2:17:1A:1D:BB')
|
||||
assert not a.is_public
|
||||
assert a.is_random
|
||||
assert a.address_type == Address.RANDOM_DEVICE_ADDRESS
|
||||
assert a.address_type == hci.Address.RANDOM_DEVICE_ADDRESS
|
||||
assert not a.is_resolvable
|
||||
assert not a.is_resolved
|
||||
assert a.is_static
|
||||
@@ -575,7 +619,7 @@ def test_address():
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_custom():
|
||||
data = bytes([0x77, 0x02, 0x01, 0x03])
|
||||
packet = HCI_CustomPacket(data)
|
||||
packet = hci.HCI_CustomPacket(data)
|
||||
assert packet.hci_packet_type == 0x77
|
||||
assert packet.payload == data
|
||||
|
||||
@@ -586,7 +630,7 @@ def test_iso_data_packet():
|
||||
'05616044002ac9f0a193003c00e83b477b00eba8d41dc018bf1a980f0290afe1e7c37652096697'
|
||||
'52b6a535a8df61e22931ef5a36281bc77ed6a3206d984bcdabee6be831c699cb50e2'
|
||||
)
|
||||
packet = HCI_IsoDataPacket.from_bytes(data)
|
||||
packet = hci.HCI_IsoDataPacket.from_bytes(data)
|
||||
assert packet.connection_handle == 0x0061
|
||||
assert packet.packet_status_flag == 0
|
||||
assert packet.pb_flag == 0x02
|
||||
|
||||
Reference in New Issue
Block a user