mirror of
https://github.com/google/bumble.git
synced 2026-04-16 00:25:31 +00:00
add iso support to bench app
This commit is contained in:
@@ -128,7 +128,7 @@ class BroadcastScanner(bumble.utils.EventEmitter):
|
|||||||
broadcast_audio_announcement: Optional[bap.BroadcastAudioAnnouncement] = None
|
broadcast_audio_announcement: Optional[bap.BroadcastAudioAnnouncement] = None
|
||||||
basic_audio_announcement: Optional[bap.BasicAudioAnnouncement] = None
|
basic_audio_announcement: Optional[bap.BasicAudioAnnouncement] = None
|
||||||
appearance: Optional[core.Appearance] = None
|
appearance: Optional[core.Appearance] = None
|
||||||
biginfo: Optional[bumble.device.BIGInfoAdvertisement] = None
|
biginfo: Optional[bumble.device.BigInfoAdvertisement] = None
|
||||||
manufacturer_data: Optional[tuple[str, bytes]] = None
|
manufacturer_data: Optional[tuple[str, bytes]] = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
@@ -255,8 +255,10 @@ class BroadcastScanner(bumble.utils.EventEmitter):
|
|||||||
print(color(' SDU Interval: ', 'magenta'), self.biginfo.sdu_interval)
|
print(color(' SDU Interval: ', 'magenta'), self.biginfo.sdu_interval)
|
||||||
print(color(' Max SDU: ', 'magenta'), self.biginfo.max_sdu)
|
print(color(' Max SDU: ', 'magenta'), self.biginfo.max_sdu)
|
||||||
print(color(' PHY: ', 'magenta'), self.biginfo.phy.name)
|
print(color(' PHY: ', 'magenta'), self.biginfo.phy.name)
|
||||||
print(color(' Framed: ', 'magenta'), self.biginfo.framed)
|
print(color(' Framing: ', 'magenta'), self.biginfo.framing.name)
|
||||||
print(color(' Encrypted: ', 'magenta'), self.biginfo.encrypted)
|
print(
|
||||||
|
color(' Encryption: ', 'magenta'), self.biginfo.encryption.name
|
||||||
|
)
|
||||||
|
|
||||||
def on_sync_establishment(self) -> None:
|
def on_sync_establishment(self) -> None:
|
||||||
self.emit('sync_establishment')
|
self.emit('sync_establishment')
|
||||||
@@ -286,7 +288,7 @@ class BroadcastScanner(bumble.utils.EventEmitter):
|
|||||||
self.emit('change')
|
self.emit('change')
|
||||||
|
|
||||||
def on_biginfo_advertisement(
|
def on_biginfo_advertisement(
|
||||||
self, advertisement: bumble.device.BIGInfoAdvertisement
|
self, advertisement: bumble.device.BigInfoAdvertisement
|
||||||
) -> None:
|
) -> None:
|
||||||
self.biginfo = advertisement
|
self.biginfo = advertisement
|
||||||
self.emit('change')
|
self.emit('change')
|
||||||
|
|||||||
367
apps/bench.py
367
apps/bench.py
@@ -36,7 +36,15 @@ from bumble.core import (
|
|||||||
CommandTimeoutError,
|
CommandTimeoutError,
|
||||||
)
|
)
|
||||||
from bumble.colors import color
|
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.gatt import Characteristic, CharacteristicValue, Service
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
HCI_LE_1M_PHY,
|
HCI_LE_1M_PHY,
|
||||||
@@ -46,6 +54,7 @@ from bumble.hci import (
|
|||||||
HCI_Constant,
|
HCI_Constant,
|
||||||
HCI_Error,
|
HCI_Error,
|
||||||
HCI_StatusError,
|
HCI_StatusError,
|
||||||
|
HCI_IsoDataPacket,
|
||||||
)
|
)
|
||||||
from bumble.sdp import (
|
from bumble.sdp import (
|
||||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||||
@@ -83,11 +92,21 @@ SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53'
|
|||||||
SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D'
|
SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D'
|
||||||
|
|
||||||
DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'
|
DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'
|
||||||
|
|
||||||
DEFAULT_L2CAP_PSM = 128
|
DEFAULT_L2CAP_PSM = 128
|
||||||
DEFAULT_L2CAP_MAX_CREDITS = 128
|
DEFAULT_L2CAP_MAX_CREDITS = 128
|
||||||
DEFAULT_L2CAP_MTU = 1024
|
DEFAULT_L2CAP_MTU = 1024
|
||||||
DEFAULT_L2CAP_MPS = 1024
|
DEFAULT_L2CAP_MPS = 1024
|
||||||
|
|
||||||
|
DEFAULT_ISO_MAX_SDU_C_TO_P = 251
|
||||||
|
DEFAULT_ISO_MAX_SDU_P_TO_C = 251
|
||||||
|
DEFAULT_ISO_SDU_INTERVAL_C_TO_P = 10000
|
||||||
|
DEFAULT_ISO_SDU_INTERVAL_P_TO_C = 10000
|
||||||
|
DEFAULT_ISO_MAX_TRANSPORT_LATENCY_C_TO_P = 35
|
||||||
|
DEFAULT_ISO_MAX_TRANSPORT_LATENCY_P_TO_C = 35
|
||||||
|
DEFAULT_ISO_RTN_C_TO_P = 3
|
||||||
|
DEFAULT_ISO_RTN_P_TO_C = 3
|
||||||
|
|
||||||
DEFAULT_LINGER_TIME = 1.0
|
DEFAULT_LINGER_TIME = 1.0
|
||||||
DEFAULT_POST_CONNECTION_WAIT_TIME = 1.0
|
DEFAULT_POST_CONNECTION_WAIT_TIME = 1.0
|
||||||
|
|
||||||
@@ -104,14 +123,14 @@ def le_phy_name(phy_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def print_connection_phy(phy):
|
def print_connection_phy(phy: ConnectionPHY) -> None:
|
||||||
logging.info(
|
logging.info(
|
||||||
color('@@@ PHY: ', 'yellow') + f'TX:{le_phy_name(phy.tx_phy)}/'
|
color('@@@ PHY: ', 'yellow') + f'TX:{le_phy_name(phy.tx_phy)}/'
|
||||||
f'RX:{le_phy_name(phy.rx_phy)}'
|
f'RX:{le_phy_name(phy.rx_phy)}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def print_connection(connection):
|
def print_connection(connection: Connection) -> None:
|
||||||
params = []
|
params = []
|
||||||
if connection.transport == PhysicalTransport.LE:
|
if connection.transport == PhysicalTransport.LE:
|
||||||
params.append(
|
params.append(
|
||||||
@@ -136,6 +155,34 @@ def print_connection(connection):
|
|||||||
logging.info(color('@@@ Connection: ', 'yellow') + ' '.join(params))
|
logging.info(color('@@@ Connection: ', 'yellow') + ' '.join(params))
|
||||||
|
|
||||||
|
|
||||||
|
def print_cis_link(cis_link: CisLink) -> None:
|
||||||
|
logging.info(color("@@@ CIS established", "green"))
|
||||||
|
logging.info(color('@@@ ISO interval: ', 'green') + f"{cis_link.iso_interval}ms")
|
||||||
|
logging.info(color('@@@ NSE: ', 'green') + f"{cis_link.nse}")
|
||||||
|
logging.info(color('@@@ Central->Peripheral:', 'green'))
|
||||||
|
if cis_link.phy_c_to_p is not None:
|
||||||
|
logging.info(
|
||||||
|
color('@@@ PHY: ', 'green') + f"{cis_link.phy_c_to_p.name}"
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
color('@@@ Latency: ', 'green') + f"{cis_link.transport_latency_c_to_p}µs"
|
||||||
|
)
|
||||||
|
logging.info(color('@@@ BN: ', 'green') + f"{cis_link.bn_c_to_p}")
|
||||||
|
logging.info(color('@@@ FT: ', 'green') + f"{cis_link.ft_c_to_p}")
|
||||||
|
logging.info(color('@@@ Max PDU: ', 'green') + f"{cis_link.max_pdu_c_to_p}")
|
||||||
|
logging.info(color('@@@ Peripheral->Central:', 'green'))
|
||||||
|
if cis_link.phy_p_to_c is not None:
|
||||||
|
logging.info(
|
||||||
|
color('@@@ PHY: ', 'green') + f"{cis_link.phy_p_to_c.name}"
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
color('@@@ Latency: ', 'green') + f"{cis_link.transport_latency_p_to_c}µs"
|
||||||
|
)
|
||||||
|
logging.info(color('@@@ BN: ', 'green') + f"{cis_link.bn_p_to_c}")
|
||||||
|
logging.info(color('@@@ FT: ', 'green') + f"{cis_link.ft_p_to_c}")
|
||||||
|
logging.info(color('@@@ Max PDU: ', 'green') + f"{cis_link.max_pdu_p_to_c}")
|
||||||
|
|
||||||
|
|
||||||
def make_sdp_records(channel):
|
def make_sdp_records(channel):
|
||||||
return {
|
return {
|
||||||
0x00010001: [
|
0x00010001: [
|
||||||
@@ -461,6 +508,7 @@ class Sender:
|
|||||||
self.bytes_sent += len(packet)
|
self.bytes_sent += len(packet)
|
||||||
await self.packet_io.send_packet(packet)
|
await self.packet_io.send_packet(packet)
|
||||||
|
|
||||||
|
if self.packet_io.can_receive():
|
||||||
await self.done.wait()
|
await self.done.wait()
|
||||||
|
|
||||||
run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
|
run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
|
||||||
@@ -491,6 +539,9 @@ class Sender:
|
|||||||
)
|
)
|
||||||
self.done.set()
|
self.done.set()
|
||||||
|
|
||||||
|
def is_sender(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Receiver
|
# Receiver
|
||||||
@@ -538,7 +589,8 @@ class Receiver:
|
|||||||
logging.info(
|
logging.info(
|
||||||
color(
|
color(
|
||||||
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
||||||
f'but received {packet.sequence}'
|
f'but received {packet.sequence}',
|
||||||
|
'red',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -581,6 +633,9 @@ class Receiver:
|
|||||||
await self.done.wait()
|
await self.done.wait()
|
||||||
logging.info(color('=== Done!', 'magenta'))
|
logging.info(color('=== Done!', 'magenta'))
|
||||||
|
|
||||||
|
def is_sender(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Ping
|
# Ping
|
||||||
@@ -716,7 +771,8 @@ class Ping:
|
|||||||
color(
|
color(
|
||||||
f'!!! Unexpected packet, '
|
f'!!! Unexpected packet, '
|
||||||
f'expected {self.next_expected_packet_index} '
|
f'expected {self.next_expected_packet_index} '
|
||||||
f'but received {packet.sequence}'
|
f'but received {packet.sequence}',
|
||||||
|
'red',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -724,6 +780,9 @@ class Ping:
|
|||||||
self.done.set()
|
self.done.set()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def is_sender(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Pong
|
# Pong
|
||||||
@@ -768,7 +827,8 @@ class Pong:
|
|||||||
logging.info(
|
logging.info(
|
||||||
color(
|
color(
|
||||||
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
||||||
f'but received {packet.sequence}'
|
f'but received {packet.sequence}',
|
||||||
|
'red',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -790,6 +850,9 @@ class Pong:
|
|||||||
await self.done.wait()
|
await self.done.wait()
|
||||||
logging.info(color('=== Done!', 'magenta'))
|
logging.info(color('=== Done!', 'magenta'))
|
||||||
|
|
||||||
|
def is_sender(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# GattClient
|
# GattClient
|
||||||
@@ -953,6 +1016,9 @@ class StreamedPacketIO:
|
|||||||
# pylint: disable-next=not-callable
|
# pylint: disable-next=not-callable
|
||||||
self.io_sink(struct.pack('>H', len(packet)) + packet)
|
self.io_sink(struct.pack('>H', len(packet)) + packet)
|
||||||
|
|
||||||
|
def can_receive(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# L2capClient
|
# L2capClient
|
||||||
@@ -1224,6 +1290,96 @@ class RfcommServer(StreamedPacketIO):
|
|||||||
await self.dlc.drain()
|
await self.dlc.drain()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# IsoClient
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class IsoClient(StreamedPacketIO):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
device: Device,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.device = device
|
||||||
|
self.ready = asyncio.Event()
|
||||||
|
self.cis_link: Optional[CisLink] = None
|
||||||
|
|
||||||
|
async def on_connection(
|
||||||
|
self, connection: Connection, cis_link: CisLink, sender: bool
|
||||||
|
) -> None:
|
||||||
|
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
|
||||||
|
self.cis_link = cis_link
|
||||||
|
self.io_sink = cis_link.write
|
||||||
|
await cis_link.setup_data_path(
|
||||||
|
cis_link.Direction.HOST_TO_CONTROLLER
|
||||||
|
if sender
|
||||||
|
else cis_link.Direction.CONTROLLER_TO_HOST
|
||||||
|
)
|
||||||
|
cis_link.sink = self.on_iso_packet
|
||||||
|
self.ready.set()
|
||||||
|
|
||||||
|
def on_iso_packet(self, iso_packet: HCI_IsoDataPacket) -> None:
|
||||||
|
self.on_packet(iso_packet.iso_sdu_fragment)
|
||||||
|
|
||||||
|
def on_disconnection(self, _):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def drain(self):
|
||||||
|
if self.cis_link is None:
|
||||||
|
return
|
||||||
|
await self.cis_link.drain()
|
||||||
|
|
||||||
|
def can_receive(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# IsoServer
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class IsoServer(StreamedPacketIO):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
device: Device,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.device = device
|
||||||
|
self.cis_link: Optional[CisLink] = None
|
||||||
|
self.ready = asyncio.Event()
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
color(
|
||||||
|
'### Listening for ISO connection',
|
||||||
|
'yellow',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_connection(
|
||||||
|
self, connection: Connection, cis_link: CisLink, sender: bool
|
||||||
|
) -> None:
|
||||||
|
connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
|
||||||
|
self.io_sink = cis_link.write
|
||||||
|
await cis_link.setup_data_path(
|
||||||
|
cis_link.Direction.HOST_TO_CONTROLLER
|
||||||
|
if sender
|
||||||
|
else cis_link.Direction.CONTROLLER_TO_HOST
|
||||||
|
)
|
||||||
|
cis_link.sink = self.on_iso_packet
|
||||||
|
self.ready.set()
|
||||||
|
|
||||||
|
def on_iso_packet(self, iso_packet: HCI_IsoDataPacket) -> None:
|
||||||
|
self.on_packet(iso_packet.iso_sdu_fragment)
|
||||||
|
|
||||||
|
def on_disconnection(self, _):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def drain(self):
|
||||||
|
if self.cis_link is None:
|
||||||
|
return
|
||||||
|
await self.cis_link.drain()
|
||||||
|
|
||||||
|
def can_receive(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Central
|
# Central
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -1232,13 +1388,22 @@ class Central(Connection.Listener):
|
|||||||
self,
|
self,
|
||||||
transport,
|
transport,
|
||||||
peripheral_address,
|
peripheral_address,
|
||||||
classic,
|
|
||||||
scenario_factory,
|
scenario_factory,
|
||||||
mode_factory,
|
mode_factory,
|
||||||
connection_interval,
|
connection_interval,
|
||||||
phy,
|
phy,
|
||||||
authenticate,
|
authenticate,
|
||||||
encrypt,
|
encrypt,
|
||||||
|
iso,
|
||||||
|
iso_sdu_interval_c_to_p,
|
||||||
|
iso_sdu_interval_p_to_c,
|
||||||
|
iso_max_sdu_c_to_p,
|
||||||
|
iso_max_sdu_p_to_c,
|
||||||
|
iso_max_transport_latency_c_to_p,
|
||||||
|
iso_max_transport_latency_p_to_c,
|
||||||
|
iso_rtn_c_to_p,
|
||||||
|
iso_rtn_p_to_c,
|
||||||
|
classic,
|
||||||
extended_data_length,
|
extended_data_length,
|
||||||
role_switch,
|
role_switch,
|
||||||
le_scan,
|
le_scan,
|
||||||
@@ -1250,6 +1415,15 @@ class Central(Connection.Listener):
|
|||||||
self.transport = transport
|
self.transport = transport
|
||||||
self.peripheral_address = peripheral_address
|
self.peripheral_address = peripheral_address
|
||||||
self.classic = classic
|
self.classic = classic
|
||||||
|
self.iso = iso
|
||||||
|
self.iso_sdu_interval_c_to_p = iso_sdu_interval_c_to_p
|
||||||
|
self.iso_sdu_interval_p_to_c = iso_sdu_interval_p_to_c
|
||||||
|
self.iso_max_sdu_c_to_p = iso_max_sdu_c_to_p
|
||||||
|
self.iso_max_sdu_p_to_c = iso_max_sdu_p_to_c
|
||||||
|
self.iso_max_transport_latency_c_to_p = iso_max_transport_latency_c_to_p
|
||||||
|
self.iso_max_transport_latency_p_to_c = iso_max_transport_latency_p_to_c
|
||||||
|
self.iso_rtn_c_to_p = iso_rtn_c_to_p
|
||||||
|
self.iso_rtn_p_to_c = iso_rtn_p_to_c
|
||||||
self.scenario_factory = scenario_factory
|
self.scenario_factory = scenario_factory
|
||||||
self.mode_factory = mode_factory
|
self.mode_factory = mode_factory
|
||||||
self.authenticate = authenticate
|
self.authenticate = authenticate
|
||||||
@@ -1308,6 +1482,13 @@ class Central(Connection.Listener):
|
|||||||
)
|
)
|
||||||
mode = self.mode_factory(self.device)
|
mode = self.mode_factory(self.device)
|
||||||
scenario = self.scenario_factory(mode)
|
scenario = self.scenario_factory(mode)
|
||||||
|
self.device.classic_enabled = self.classic
|
||||||
|
self.device.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 pre_power_on(self.device, self.classic)
|
||||||
await self.device.power_on()
|
await self.device.power_on()
|
||||||
@@ -1392,6 +1573,71 @@ class Central(Connection.Listener):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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 mode.on_connection(self.connection)
|
||||||
|
|
||||||
await scenario.run()
|
await scenario.run()
|
||||||
@@ -1428,6 +1674,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|||||||
scenario_factory,
|
scenario_factory,
|
||||||
mode_factory,
|
mode_factory,
|
||||||
classic,
|
classic,
|
||||||
|
iso,
|
||||||
extended_data_length,
|
extended_data_length,
|
||||||
role_switch,
|
role_switch,
|
||||||
le_scan,
|
le_scan,
|
||||||
@@ -1437,6 +1684,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|||||||
):
|
):
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
self.classic = classic
|
self.classic = classic
|
||||||
|
self.iso = iso
|
||||||
self.scenario_factory = scenario_factory
|
self.scenario_factory = scenario_factory
|
||||||
self.mode_factory = mode_factory
|
self.mode_factory = mode_factory
|
||||||
self.extended_data_length = extended_data_length
|
self.extended_data_length = extended_data_length
|
||||||
@@ -1470,6 +1718,13 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|||||||
self.device.listener = self
|
self.device.listener = self
|
||||||
self.mode = self.mode_factory(self.device)
|
self.mode = self.mode_factory(self.device)
|
||||||
self.scenario = self.scenario_factory(self.mode)
|
self.scenario = self.scenario_factory(self.mode)
|
||||||
|
self.device.classic_enabled = self.classic
|
||||||
|
self.device.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 pre_power_on(self.device, self.classic)
|
||||||
await self.device.power_on()
|
await self.device.power_on()
|
||||||
@@ -1501,7 +1756,21 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|||||||
logging.info(color('### Connected', 'cyan'))
|
logging.info(color('### Connected', 'cyan'))
|
||||||
print_connection(self.connection)
|
print_connection(self.connection)
|
||||||
|
|
||||||
|
if self.iso:
|
||||||
|
|
||||||
|
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.mode.on_connection(self.connection)
|
||||||
|
|
||||||
await self.scenario.run()
|
await self.scenario.run()
|
||||||
await asyncio.sleep(DEFAULT_LINGER_TIME)
|
await asyncio.sleep(DEFAULT_LINGER_TIME)
|
||||||
|
|
||||||
@@ -1613,6 +1882,12 @@ def create_mode_factory(ctx, default_mode):
|
|||||||
credits_threshold=ctx.obj['rfcomm_credits_threshold'],
|
credits_threshold=ctx.obj['rfcomm_credits_threshold'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if mode == 'iso-server':
|
||||||
|
return IsoServer(device)
|
||||||
|
|
||||||
|
if mode == 'iso-client':
|
||||||
|
return IsoClient(device)
|
||||||
|
|
||||||
raise ValueError('invalid mode')
|
raise ValueError('invalid mode')
|
||||||
|
|
||||||
return create_mode
|
return create_mode
|
||||||
@@ -1640,6 +1915,9 @@ def create_scenario_factory(ctx, default_scenario):
|
|||||||
return Receiver(packet_io, ctx.obj['linger'])
|
return Receiver(packet_io, ctx.obj['linger'])
|
||||||
|
|
||||||
if scenario == 'ping':
|
if scenario == 'ping':
|
||||||
|
if isinstance(packet_io, (IsoClient, IsoServer)):
|
||||||
|
raise ValueError('ping not supported with ISO')
|
||||||
|
|
||||||
return Ping(
|
return Ping(
|
||||||
packet_io,
|
packet_io,
|
||||||
start_delay=ctx.obj['start_delay'],
|
start_delay=ctx.obj['start_delay'],
|
||||||
@@ -1651,6 +1929,9 @@ def create_scenario_factory(ctx, default_scenario):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if scenario == 'pong':
|
if scenario == 'pong':
|
||||||
|
if isinstance(packet_io, (IsoClient, IsoServer)):
|
||||||
|
raise ValueError('pong not supported with ISO')
|
||||||
|
|
||||||
return Pong(packet_io, ctx.obj['linger'])
|
return Pong(packet_io, ctx.obj['linger'])
|
||||||
|
|
||||||
raise ValueError('invalid scenario')
|
raise ValueError('invalid scenario')
|
||||||
@@ -1674,6 +1955,8 @@ def create_scenario_factory(ctx, default_scenario):
|
|||||||
'l2cap-server',
|
'l2cap-server',
|
||||||
'rfcomm-client',
|
'rfcomm-client',
|
||||||
'rfcomm-server',
|
'rfcomm-server',
|
||||||
|
'iso-client',
|
||||||
|
'iso-server',
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1896,6 +2179,7 @@ def bench(
|
|||||||
ctx.obj['classic_page_scan'] = classic_page_scan
|
ctx.obj['classic_page_scan'] = classic_page_scan
|
||||||
ctx.obj['classic_inquiry_scan'] = classic_inquiry_scan
|
ctx.obj['classic_inquiry_scan'] = classic_inquiry_scan
|
||||||
ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
|
ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
|
||||||
|
ctx.obj['iso'] = mode in ('iso-client', 'iso-server')
|
||||||
|
|
||||||
|
|
||||||
@bench.command()
|
@bench.command()
|
||||||
@@ -1917,26 +2201,88 @@ def bench(
|
|||||||
@click.option('--phy', type=click.Choice(['1m', '2m', 'coded']), help='PHY to use')
|
@click.option('--phy', type=click.Choice(['1m', '2m', 'coded']), help='PHY to use')
|
||||||
@click.option('--authenticate', is_flag=True, help='Authenticate (RFComm only)')
|
@click.option('--authenticate', is_flag=True, help='Authenticate (RFComm only)')
|
||||||
@click.option('--encrypt', is_flag=True, help='Encrypt the connection (RFComm only)')
|
@click.option('--encrypt', is_flag=True, help='Encrypt the connection (RFComm only)')
|
||||||
|
@click.option(
|
||||||
|
'--iso-sdu-interval-c-to-p',
|
||||||
|
type=int,
|
||||||
|
help='ISO SDU central -> peripheral (microseconds)',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--iso-sdu-interval-p-to-c',
|
||||||
|
type=int,
|
||||||
|
help='ISO SDU interval peripheral -> central (microseconds)',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--iso-max-sdu-c-to-p',
|
||||||
|
type=int,
|
||||||
|
help='ISO max SDU central -> peripheral',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--iso-max-sdu-p-to-c',
|
||||||
|
type=int,
|
||||||
|
help='ISO max SDU peripheral -> central',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--iso-max-transport-latency-c-to-p',
|
||||||
|
type=int,
|
||||||
|
help='ISO max transport latency central -> peripheral (milliseconds)',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--iso-max-transport-latency-p-to-c',
|
||||||
|
type=int,
|
||||||
|
help='ISO max transport latency peripheral -> central (milliseconds)',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--iso-rtn-c-to-p',
|
||||||
|
type=int,
|
||||||
|
help='ISO RTN central -> peripheral (integer count)',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--iso-rtn-p-to-c',
|
||||||
|
type=int,
|
||||||
|
help='ISO RTN peripheral -> central (integer count)',
|
||||||
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def central(
|
def central(
|
||||||
ctx, 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)"""
|
"""Run as a central (initiates the connection)"""
|
||||||
scenario_factory = create_scenario_factory(ctx, 'send')
|
scenario_factory = create_scenario_factory(ctx, 'send')
|
||||||
mode_factory = create_mode_factory(ctx, 'gatt-client')
|
mode_factory = create_mode_factory(ctx, 'gatt-client')
|
||||||
classic = ctx.obj['classic']
|
|
||||||
|
|
||||||
async def run_central():
|
async def run_central():
|
||||||
await Central(
|
await Central(
|
||||||
transport,
|
transport,
|
||||||
peripheral_address,
|
peripheral_address,
|
||||||
classic,
|
|
||||||
scenario_factory,
|
scenario_factory,
|
||||||
mode_factory,
|
mode_factory,
|
||||||
connection_interval,
|
connection_interval,
|
||||||
phy,
|
phy,
|
||||||
authenticate,
|
authenticate,
|
||||||
encrypt or authenticate,
|
encrypt or authenticate,
|
||||||
|
ctx.obj['iso'],
|
||||||
|
iso_sdu_interval_c_to_p,
|
||||||
|
iso_sdu_interval_p_to_c,
|
||||||
|
iso_max_sdu_c_to_p,
|
||||||
|
iso_max_sdu_p_to_c,
|
||||||
|
iso_max_transport_latency_c_to_p,
|
||||||
|
iso_max_transport_latency_p_to_c,
|
||||||
|
iso_rtn_c_to_p,
|
||||||
|
iso_rtn_p_to_c,
|
||||||
|
ctx.obj['classic'],
|
||||||
ctx.obj['extended_data_length'],
|
ctx.obj['extended_data_length'],
|
||||||
ctx.obj['role_switch'],
|
ctx.obj['role_switch'],
|
||||||
ctx.obj['le_scan'],
|
ctx.obj['le_scan'],
|
||||||
@@ -1962,6 +2308,7 @@ def peripheral(ctx, transport):
|
|||||||
scenario_factory,
|
scenario_factory,
|
||||||
mode_factory,
|
mode_factory,
|
||||||
ctx.obj['classic'],
|
ctx.obj['classic'],
|
||||||
|
ctx.obj['iso'],
|
||||||
ctx.obj['extended_data_length'],
|
ctx.obj['extended_data_length'],
|
||||||
ctx.obj['role_switch'],
|
ctx.obj['role_switch'],
|
||||||
ctx.obj['le_scan'],
|
ctx.obj['le_scan'],
|
||||||
|
|||||||
@@ -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...')
|
print('<<< connecting to HCI...')
|
||||||
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
||||||
print('<<< connected')
|
print('<<< connected')
|
||||||
@@ -251,19 +253,32 @@ async def async_main(latency_probes, transport):
|
|||||||
await host.reset()
|
await host.reset()
|
||||||
|
|
||||||
# Measure the latency if requested
|
# Measure the latency if requested
|
||||||
|
# (we add an extra probe at the start, that we ignore, just to ensure that
|
||||||
|
# the transport is primed)
|
||||||
latencies = []
|
latencies = []
|
||||||
if latency_probes:
|
if latency_probes:
|
||||||
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()
|
start = time.time()
|
||||||
await host.send_command(HCI_Read_Local_Version_Information_Command())
|
await host.send_command(probe_hci_command)
|
||||||
|
if iteration:
|
||||||
latencies.append(1000 * (time.time() - start))
|
latencies.append(1000 * (time.time() - start))
|
||||||
print(
|
print(
|
||||||
color('HCI Command Latency:', 'yellow'),
|
color('HCI Command Latency:', 'yellow'),
|
||||||
(
|
(
|
||||||
f'min={min(latencies):.2f}, '
|
f'min={min(latencies):.2f}, '
|
||||||
f'max={max(latencies):.2f}, '
|
f'max={max(latencies):.2f}, '
|
||||||
f'average={sum(latencies)/len(latencies):.2f}'
|
f'average={sum(latencies)/len(latencies):.2f},'
|
||||||
),
|
),
|
||||||
|
[f'{latency:.4}' for latency in latencies],
|
||||||
'\n',
|
'\n',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -311,10 +326,32 @@ async def async_main(latency_probes, transport):
|
|||||||
type=int,
|
type=int,
|
||||||
help='Send N commands to measure HCI transport latency statistics',
|
help='Send N commands to measure HCI transport latency statistics',
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'--latency-probe-interval',
|
||||||
|
metavar='INTERVAL',
|
||||||
|
type=int,
|
||||||
|
help='Interval between latency probes (milliseconds)',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--latency-probe-command',
|
||||||
|
metavar='COMMAND_HEX',
|
||||||
|
help=(
|
||||||
|
'Probe command (HCI Command packet bytes, in hex. Use 0177FC00 for'
|
||||||
|
' a loopback test with the HCI remote proxy app)'
|
||||||
|
),
|
||||||
|
)
|
||||||
@click.argument('transport')
|
@click.argument('transport')
|
||||||
def main(latency_probes, transport):
|
def main(latency_probes, latency_probe_interval, latency_probe_command, transport):
|
||||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
logging.basicConfig(
|
||||||
asyncio.run(async_main(latency_probes, transport))
|
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')
|
@click.argument('transport')
|
||||||
def main(packet_size, packet_count, 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)
|
loopback = Loopback(packet_size, packet_count, transport)
|
||||||
asyncio.run(loopback.run())
|
asyncio.run(loopback.run())
|
||||||
|
|||||||
@@ -337,7 +337,12 @@ class Speaker:
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
AdvertisingData.FLAGS,
|
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,
|
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||||
|
|||||||
@@ -618,8 +618,8 @@ class Controller:
|
|||||||
cis_sync_delay=0,
|
cis_sync_delay=0,
|
||||||
transport_latency_c_to_p=0,
|
transport_latency_c_to_p=0,
|
||||||
transport_latency_p_to_c=0,
|
transport_latency_p_to_c=0,
|
||||||
phy_c_to_p=0,
|
phy_c_to_p=1,
|
||||||
phy_p_to_c=0,
|
phy_p_to_c=1,
|
||||||
nse=0,
|
nse=0,
|
||||||
bn_c_to_p=0,
|
bn_c_to_p=0,
|
||||||
bn_p_to_c=0,
|
bn_p_to_c=0,
|
||||||
|
|||||||
300
bumble/device.py
300
bumble/device.py
@@ -139,6 +139,9 @@ DEVICE_DEFAULT_ADVERTISING_TX_POWER = (
|
|||||||
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
|
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
|
||||||
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT = 5.0
|
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT = 5.0
|
||||||
DEVICE_DEFAULT_LE_RPA_TIMEOUT = 15 * 60 # 15 minutes (in seconds)
|
DEVICE_DEFAULT_LE_RPA_TIMEOUT = 15 * 60 # 15 minutes (in seconds)
|
||||||
|
DEVICE_DEFAULT_ISO_CIS_MAX_SDU = 251
|
||||||
|
DEVICE_DEFAULT_ISO_CIS_RTN = 10
|
||||||
|
DEVICE_DEFAULT_ISO_CIS_MAX_TRANSPORT_LATENCY = 100
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
# pylint: enable=line-too-long
|
# pylint: enable=line-too-long
|
||||||
@@ -489,7 +492,18 @@ class PeriodicAdvertisement:
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@dataclass
|
@dataclass
|
||||||
class BIGInfoAdvertisement:
|
class BigInfoAdvertisement:
|
||||||
|
class Framing(utils.OpenIntEnum):
|
||||||
|
# fmt: off
|
||||||
|
UNFRAMED = 0X00
|
||||||
|
FRAMED_SEGMENTABLE_MODE = 0X01
|
||||||
|
FRAMED_UNSEGMENTED_MODE = 0X02
|
||||||
|
|
||||||
|
class Encryption(utils.OpenIntEnum):
|
||||||
|
# fmt: off
|
||||||
|
UNENCRYPTED = 0x00
|
||||||
|
ENCRYPTED = 0x01
|
||||||
|
|
||||||
address: hci.Address
|
address: hci.Address
|
||||||
sid: int
|
sid: int
|
||||||
num_bis: int
|
num_bis: int
|
||||||
@@ -502,8 +516,8 @@ class BIGInfoAdvertisement:
|
|||||||
sdu_interval: int
|
sdu_interval: int
|
||||||
max_sdu: int
|
max_sdu: int
|
||||||
phy: hci.Phy
|
phy: hci.Phy
|
||||||
framed: bool
|
framing: Framing
|
||||||
encrypted: bool
|
encryption: Encryption
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_report(cls, address: hci.Address, sid: int, report) -> Self:
|
def from_report(cls, address: hci.Address, sid: int, report) -> Self:
|
||||||
@@ -520,8 +534,8 @@ class BIGInfoAdvertisement:
|
|||||||
report.sdu_interval,
|
report.sdu_interval,
|
||||||
report.max_sdu,
|
report.max_sdu,
|
||||||
hci.Phy(report.phy),
|
hci.Phy(report.phy),
|
||||||
report.framing != 0,
|
cls.Framing(report.framing),
|
||||||
report.encryption != 0,
|
cls.Encryption(report.encryption),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1013,7 +1027,7 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
|
|||||||
def on_biginfo_advertising_report(self, report) -> None:
|
def on_biginfo_advertising_report(self, report) -> None:
|
||||||
self.emit(
|
self.emit(
|
||||||
self.EVENT_BIGINFO_ADVERTISEMENT,
|
self.EVENT_BIGINFO_ADVERTISEMENT,
|
||||||
BIGInfoAdvertisement.from_report(self.advertiser_address, self.sid, report),
|
BigInfoAdvertisement.from_report(self.advertiser_address, self.sid, report),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -1031,14 +1045,24 @@ class PeriodicAdvertisingSync(utils.EventEmitter):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@dataclass
|
@dataclass
|
||||||
class BigParameters:
|
class BigParameters:
|
||||||
|
class Packing(utils.OpenIntEnum):
|
||||||
|
# fmt: off
|
||||||
|
SEQUENTIAL = 0x00
|
||||||
|
INTERLEAVED = 0x01
|
||||||
|
|
||||||
|
class Framing(utils.OpenIntEnum):
|
||||||
|
# fmt: off
|
||||||
|
UNFRAMED = 0x00
|
||||||
|
FRAMED = 0x01
|
||||||
|
|
||||||
num_bis: int
|
num_bis: int
|
||||||
sdu_interval: int
|
sdu_interval: int # SDU interval, in microseconds
|
||||||
max_sdu: int
|
max_sdu: int
|
||||||
max_transport_latency: int
|
max_transport_latency: int # Max transport latency, in milliseconds
|
||||||
rtn: int
|
rtn: int
|
||||||
phy: hci.PhyBit = hci.PhyBit.LE_2M
|
phy: hci.PhyBit = hci.PhyBit.LE_2M
|
||||||
packing: int = 0
|
packing: Packing = Packing.SEQUENTIAL
|
||||||
framing: int = 0
|
framing: Framing = Framing.UNFRAMED
|
||||||
broadcast_code: bytes | None = None
|
broadcast_code: bytes | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -1061,15 +1085,15 @@ class Big(utils.EventEmitter):
|
|||||||
state: State = State.PENDING
|
state: State = State.PENDING
|
||||||
|
|
||||||
# Attributes provided by BIG Create Complete event
|
# Attributes provided by BIG Create Complete event
|
||||||
big_sync_delay: int = 0
|
big_sync_delay: int = 0 # Sync delay, in microseconds
|
||||||
transport_latency_big: int = 0
|
transport_latency_big: int = 0 # Transport latency, in microseconds
|
||||||
phy: int = 0
|
phy: hci.Phy = hci.Phy.LE_1M
|
||||||
nse: int = 0
|
nse: int = 0
|
||||||
bn: int = 0
|
bn: int = 0
|
||||||
pto: int = 0
|
pto: int = 0
|
||||||
irc: int = 0
|
irc: int = 0
|
||||||
max_pdu: int = 0
|
max_pdu: int = 0
|
||||||
iso_interval: float = 0.0
|
iso_interval: float = 0.0 # ISO interval, in milliseconds
|
||||||
bis_links: Sequence[BisLink] = ()
|
bis_links: Sequence[BisLink] = ()
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
@@ -1499,10 +1523,74 @@ class _IsoLink:
|
|||||||
"""Write an ISO SDU."""
|
"""Write an ISO SDU."""
|
||||||
self.device.host.send_iso_sdu(connection_handle=self.handle, sdu=sdu)
|
self.device.host.send_iso_sdu(connection_handle=self.handle, sdu=sdu)
|
||||||
|
|
||||||
|
async def get_tx_time_stamp(self) -> tuple[int, int, int]:
|
||||||
|
response = await self.device.host.send_command(
|
||||||
|
hci.HCI_LE_Read_ISO_TX_Sync_Command(connection_handle=self.handle),
|
||||||
|
check_result=True,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
response.return_parameters.packet_sequence_number,
|
||||||
|
response.return_parameters.tx_time_stamp,
|
||||||
|
response.return_parameters.time_offset,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data_packet_queue(self) -> DataPacketQueue | None:
|
def data_packet_queue(self) -> DataPacketQueue | None:
|
||||||
return self.device.host.get_data_packet_queue(self.handle)
|
return self.device.host.get_data_packet_queue(self.handle)
|
||||||
|
|
||||||
|
async def drain(self) -> None:
|
||||||
|
if data_packet_queue := self.data_packet_queue:
|
||||||
|
await data_packet_queue.drain(self.handle)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@dataclass
|
||||||
|
class CigParameters:
|
||||||
|
class WorstCaseSca(utils.OpenIntEnum):
|
||||||
|
# fmt: off
|
||||||
|
SCA_251_TO_500_PPM = 0x00
|
||||||
|
SCA_151_TO_250_PPM = 0x01
|
||||||
|
SCA_101_TO_150_PPM = 0x02
|
||||||
|
SCA_76_TO_100_PPM = 0x03
|
||||||
|
SCA_51_TO_75_PPM = 0x04
|
||||||
|
SCA_31_TO_50_PPM = 0x05
|
||||||
|
SCA_21_TO_30_PPM = 0x06
|
||||||
|
SCA_0_TO_20_PPM = 0x07
|
||||||
|
|
||||||
|
class Packing(utils.OpenIntEnum):
|
||||||
|
# fmt: off
|
||||||
|
SEQUENTIAL = 0x00
|
||||||
|
INTERLEAVED = 0x01
|
||||||
|
|
||||||
|
class Framing(utils.OpenIntEnum):
|
||||||
|
# fmt: off
|
||||||
|
UNFRAMED = 0x00
|
||||||
|
FRAMED = 0x01
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CisParameters:
|
||||||
|
cis_id: int
|
||||||
|
max_sdu_c_to_p: int = DEVICE_DEFAULT_ISO_CIS_MAX_SDU
|
||||||
|
max_sdu_p_to_c: int = DEVICE_DEFAULT_ISO_CIS_MAX_SDU
|
||||||
|
phy_c_to_p: hci.PhyBit = hci.PhyBit.LE_2M
|
||||||
|
phy_p_to_c: hci.PhyBit = hci.PhyBit.LE_2M
|
||||||
|
rtn_c_to_p: int = DEVICE_DEFAULT_ISO_CIS_RTN # Number of C->P retransmissions
|
||||||
|
rtn_p_to_c: int = DEVICE_DEFAULT_ISO_CIS_RTN # Number of P->C retransmissions
|
||||||
|
|
||||||
|
cig_id: int
|
||||||
|
cis_parameters: list[CisParameters]
|
||||||
|
sdu_interval_c_to_p: int # C->P SDU interval, in microseconds
|
||||||
|
sdu_interval_p_to_c: int # P->C SDU interval, in microseconds
|
||||||
|
worst_case_sca: WorstCaseSca = WorstCaseSca.SCA_251_TO_500_PPM
|
||||||
|
packing: Packing = Packing.SEQUENTIAL
|
||||||
|
framing: Framing = Framing.UNFRAMED
|
||||||
|
max_transport_latency_c_to_p: int = (
|
||||||
|
DEVICE_DEFAULT_ISO_CIS_MAX_TRANSPORT_LATENCY # Max C->P transport latency, in milliseconds
|
||||||
|
)
|
||||||
|
max_transport_latency_p_to_c: int = (
|
||||||
|
DEVICE_DEFAULT_ISO_CIS_MAX_TRANSPORT_LATENCY # Max C->P transport latency, in milliseconds
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -1516,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)
|
handle: int # CIS handle assigned by Controller (in LE_Set_CIG_Parameters Complete or LE_CIS_Request events)
|
||||||
cis_id: int # CIS ID assigned by Central device
|
cis_id: int # CIS ID assigned by Central device
|
||||||
cig_id: int # CIG ID assigned by Central device
|
cig_id: int # CIG ID assigned by Central device
|
||||||
|
cig_sync_delay: int = 0 # CIG sync delay, in microseconds
|
||||||
|
cis_sync_delay: int = 0 # CIS sync delay, in microseconds
|
||||||
|
transport_latency_c_to_p: int = 0 # C->P transport latency, in microseconds
|
||||||
|
transport_latency_p_to_c: int = 0 # P->C transport latency, in microseconds
|
||||||
|
phy_c_to_p: Optional[hci.Phy] = None
|
||||||
|
phy_p_to_c: Optional[hci.Phy] = None
|
||||||
|
nse: int = 0
|
||||||
|
bn_c_to_p: int = 0
|
||||||
|
bn_p_to_c: int = 0
|
||||||
|
ft_c_to_p: int = 0
|
||||||
|
ft_p_to_c: int = 0
|
||||||
|
max_pdu_c_to_p: int = 0
|
||||||
|
max_pdu_p_to_c: int = 0
|
||||||
|
iso_interval: float = 0.0 # ISO interval, in milliseconds
|
||||||
state: State = State.PENDING
|
state: State = State.PENDING
|
||||||
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
|
sink: Callable[[hci.HCI_IsoDataPacket], Any] | None = None
|
||||||
|
|
||||||
@@ -1598,6 +1700,7 @@ class Connection(utils.CompositeEventEmitter):
|
|||||||
peer_resolvable_address: Optional[hci.Address]
|
peer_resolvable_address: Optional[hci.Address]
|
||||||
peer_le_features: Optional[hci.LeFeatureMask]
|
peer_le_features: Optional[hci.LeFeatureMask]
|
||||||
role: hci.Role
|
role: hci.Role
|
||||||
|
parameters: Parameters
|
||||||
encryption: int
|
encryption: int
|
||||||
encryption_key_size: int
|
encryption_key_size: int
|
||||||
authenticated: bool
|
authenticated: bool
|
||||||
@@ -1642,6 +1745,9 @@ class Connection(utils.CompositeEventEmitter):
|
|||||||
EVENT_PAIRING_FAILURE = "pairing_failure"
|
EVENT_PAIRING_FAILURE = "pairing_failure"
|
||||||
EVENT_SECURITY_REQUEST = "security_request"
|
EVENT_SECURITY_REQUEST = "security_request"
|
||||||
EVENT_LINK_KEY = "link_key"
|
EVENT_LINK_KEY = "link_key"
|
||||||
|
EVENT_CIS_REQUEST = "cis_request"
|
||||||
|
EVENT_CIS_ESTABLISHMENT = "cis_establishment"
|
||||||
|
EVENT_CIS_ESTABLISHMENT_FAILURE = "cis_establishment_failure"
|
||||||
|
|
||||||
@utils.composite_listener
|
@utils.composite_listener
|
||||||
class Listener:
|
class Listener:
|
||||||
@@ -4569,48 +4675,39 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
@utils.experimental('Only for testing.')
|
@utils.experimental('Only for testing.')
|
||||||
async def setup_cig(
|
async def setup_cig(
|
||||||
self,
|
self,
|
||||||
cig_id: int,
|
parameters: CigParameters,
|
||||||
cis_id: Sequence[int],
|
|
||||||
sdu_interval: tuple[int, int],
|
|
||||||
framing: int,
|
|
||||||
max_sdu: tuple[int, int],
|
|
||||||
retransmission_number: int,
|
|
||||||
max_transport_latency: tuple[int, int],
|
|
||||||
) -> list[int]:
|
) -> list[int]:
|
||||||
"""Sends hci.HCI_LE_Set_CIG_Parameters_Command.
|
"""Sends hci.HCI_LE_Set_CIG_Parameters_Command.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cig_id: CIG_ID.
|
parameters: CIG parameters.
|
||||||
cis_id: CID ID list.
|
|
||||||
sdu_interval: SDU intervals of (Central->Peripheral, Peripheral->Cental).
|
|
||||||
framing: Un-framing(0) or Framing(1).
|
|
||||||
max_sdu: Max SDU counts of (Central->Peripheral, Peripheral->Cental).
|
|
||||||
retransmission_number: retransmission_number.
|
|
||||||
max_transport_latency: Max transport latencies of
|
|
||||||
(Central->Peripheral, Peripheral->Cental).
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of created CIS handles corresponding to the same order of [cid_id].
|
List of created CIS handles corresponding to the same order of [cid_id].
|
||||||
"""
|
"""
|
||||||
num_cis = len(cis_id)
|
num_cis = len(parameters.cis_parameters)
|
||||||
|
|
||||||
response = await self.send_command(
|
response = await self.send_command(
|
||||||
hci.HCI_LE_Set_CIG_Parameters_Command(
|
hci.HCI_LE_Set_CIG_Parameters_Command(
|
||||||
cig_id=cig_id,
|
cig_id=parameters.cig_id,
|
||||||
sdu_interval_c_to_p=sdu_interval[0],
|
sdu_interval_c_to_p=parameters.sdu_interval_c_to_p,
|
||||||
sdu_interval_p_to_c=sdu_interval[1],
|
sdu_interval_p_to_c=parameters.sdu_interval_p_to_c,
|
||||||
worst_case_sca=0x00, # 251-500 ppm
|
worst_case_sca=parameters.worst_case_sca,
|
||||||
packing=0x00, # Sequential
|
packing=int(parameters.packing),
|
||||||
framing=framing,
|
framing=int(parameters.framing),
|
||||||
max_transport_latency_c_to_p=max_transport_latency[0],
|
max_transport_latency_c_to_p=parameters.max_transport_latency_c_to_p,
|
||||||
max_transport_latency_p_to_c=max_transport_latency[1],
|
max_transport_latency_p_to_c=parameters.max_transport_latency_p_to_c,
|
||||||
cis_id=cis_id,
|
cis_id=[cis.cis_id for cis in parameters.cis_parameters],
|
||||||
max_sdu_c_to_p=[max_sdu[0]] * num_cis,
|
max_sdu_c_to_p=[
|
||||||
max_sdu_p_to_c=[max_sdu[1]] * num_cis,
|
cis.max_sdu_c_to_p for cis in parameters.cis_parameters
|
||||||
phy_c_to_p=[hci.HCI_LE_2M_PHY] * num_cis,
|
],
|
||||||
phy_p_to_c=[hci.HCI_LE_2M_PHY] * num_cis,
|
max_sdu_p_to_c=[
|
||||||
rtn_c_to_p=[retransmission_number] * num_cis,
|
cis.max_sdu_p_to_c for cis in parameters.cis_parameters
|
||||||
rtn_p_to_c=[retransmission_number] * num_cis,
|
],
|
||||||
|
phy_c_to_p=[cis.phy_c_to_p for cis in parameters.cis_parameters],
|
||||||
|
phy_p_to_c=[cis.phy_p_to_c for cis in parameters.cis_parameters],
|
||||||
|
rtn_c_to_p=[cis.rtn_c_to_p for cis in parameters.cis_parameters],
|
||||||
|
rtn_p_to_c=[cis.rtn_p_to_c for cis in parameters.cis_parameters],
|
||||||
),
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
@@ -4618,19 +4715,17 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
# Ideally, we should manage CIG lifecycle, but they are not useful for Unicast
|
# Ideally, we should manage CIG lifecycle, but they are not useful for Unicast
|
||||||
# Server, so here it only provides a basic functionality for testing.
|
# Server, so here it only provides a basic functionality for testing.
|
||||||
cis_handles = response.return_parameters.connection_handle[:]
|
cis_handles = response.return_parameters.connection_handle[:]
|
||||||
for id, cis_handle in zip(cis_id, cis_handles):
|
for cis, cis_handle in zip(parameters.cis_parameters, cis_handles):
|
||||||
self._pending_cis[cis_handle] = (id, cig_id)
|
self._pending_cis[cis_handle] = (cis.cis_id, parameters.cig_id)
|
||||||
|
|
||||||
return cis_handles
|
return cis_handles
|
||||||
|
|
||||||
# [LE only]
|
# [LE only]
|
||||||
@utils.experimental('Only for testing.')
|
@utils.experimental('Only for testing.')
|
||||||
async def create_cis(
|
async def create_cis(
|
||||||
self, cis_acl_pairs: Sequence[tuple[int, int]]
|
self, cis_acl_pairs: Sequence[tuple[int, Connection]]
|
||||||
) -> list[CisLink]:
|
) -> list[CisLink]:
|
||||||
for cis_handle, acl_handle in cis_acl_pairs:
|
for cis_handle, acl_connection in cis_acl_pairs:
|
||||||
acl_connection = self.lookup_connection(acl_handle)
|
|
||||||
assert acl_connection
|
|
||||||
cis_id, cig_id = self._pending_cis.pop(cis_handle)
|
cis_id, cig_id = self._pending_cis.pop(cis_handle)
|
||||||
self.cis_links[cis_handle] = CisLink(
|
self.cis_links[cis_handle] = CisLink(
|
||||||
device=self,
|
device=self,
|
||||||
@@ -4650,8 +4745,8 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
if pending_future := pending_cis_establishments.get(cis_link.handle):
|
if pending_future := pending_cis_establishments.get(cis_link.handle):
|
||||||
pending_future.set_result(cis_link)
|
pending_future.set_result(cis_link)
|
||||||
|
|
||||||
def on_cis_establishment_failure(cis_handle: int, status: int) -> None:
|
def on_cis_establishment_failure(cis_link: CisLink, status: int) -> None:
|
||||||
if pending_future := pending_cis_establishments.get(cis_handle):
|
if pending_future := pending_cis_establishments.get(cis_link.handle):
|
||||||
pending_future.set_exception(hci.HCI_Error(status))
|
pending_future.set_exception(hci.HCI_Error(status))
|
||||||
|
|
||||||
watcher.on(self, self.EVENT_CIS_ESTABLISHMENT, on_cis_establishment)
|
watcher.on(self, self.EVENT_CIS_ESTABLISHMENT, on_cis_establishment)
|
||||||
@@ -4661,7 +4756,7 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
await self.send_command(
|
await self.send_command(
|
||||||
hci.HCI_LE_Create_CIS_Command(
|
hci.HCI_LE_Create_CIS_Command(
|
||||||
cis_connection_handle=[p[0] for p in cis_acl_pairs],
|
cis_connection_handle=[p[0] for p in cis_acl_pairs],
|
||||||
acl_connection_handle=[p[1] for p in cis_acl_pairs],
|
acl_connection_handle=[p[1].handle for p in cis_acl_pairs],
|
||||||
),
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
@@ -4670,26 +4765,21 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
|
|
||||||
# [LE only]
|
# [LE only]
|
||||||
@utils.experimental('Only for testing.')
|
@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.
|
"""[LE Only] Accepts an incoming CIS request.
|
||||||
|
|
||||||
When the specified CIS handle is already created, this method returns the
|
This method returns when the CIS is established, or raises an exception if
|
||||||
existed CIS link object immediately.
|
the CIS establishment fails.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
handle: CIS handle to accept.
|
handle: CIS handle to accept.
|
||||||
|
|
||||||
Returns:
|
|
||||||
CIS link object on the given handle.
|
|
||||||
"""
|
"""
|
||||||
if not (cis_link := self.cis_links.get(handle)):
|
|
||||||
raise InvalidStateError(f'No pending CIS request of handle {handle}')
|
|
||||||
|
|
||||||
# There might be multiple ASE sharing a CIS channel.
|
# There might be multiple ASE sharing a CIS channel.
|
||||||
# If one of them has accepted the request, the others should just leverage it.
|
# If one of them has accepted the request, the others should just leverage it.
|
||||||
async with self._cis_lock:
|
async with self._cis_lock:
|
||||||
if cis_link.state == CisLink.State.ESTABLISHED:
|
if cis_link.state == CisLink.State.ESTABLISHED:
|
||||||
return cis_link
|
return
|
||||||
|
|
||||||
with closing(utils.EventWatcher()) as watcher:
|
with closing(utils.EventWatcher()) as watcher:
|
||||||
pending_establishment = asyncio.get_running_loop().create_future()
|
pending_establishment = asyncio.get_running_loop().create_future()
|
||||||
@@ -4708,26 +4798,24 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
await self.send_command(
|
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,
|
check_result=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
await pending_establishment
|
await pending_establishment
|
||||||
return cis_link
|
|
||||||
|
|
||||||
# Mypy believes this is reachable when context is an ExitStack.
|
|
||||||
raise UnreachableError()
|
|
||||||
|
|
||||||
# [LE only]
|
# [LE only]
|
||||||
@utils.experimental('Only for testing.')
|
@utils.experimental('Only for testing.')
|
||||||
async def reject_cis_request(
|
async def reject_cis_request(
|
||||||
self,
|
self,
|
||||||
handle: int,
|
cis_link: CisLink,
|
||||||
reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
reason: int = hci.HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||||
) -> None:
|
) -> None:
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
hci.HCI_LE_Reject_CIS_Request_Command(
|
hci.HCI_LE_Reject_CIS_Request_Command(
|
||||||
connection_handle=handle, reason=reason
|
connection_handle=cis_link.handle, reason=reason
|
||||||
),
|
),
|
||||||
check_result=True,
|
check_result=True,
|
||||||
)
|
)
|
||||||
@@ -5265,7 +5353,7 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
big.bis_links = [BisLink(handle=handle, big=big) for handle in bis_handles]
|
big.bis_links = [BisLink(handle=handle, big=big) for handle in bis_handles]
|
||||||
big.big_sync_delay = big_sync_delay
|
big.big_sync_delay = big_sync_delay
|
||||||
big.transport_latency_big = transport_latency_big
|
big.transport_latency_big = transport_latency_big
|
||||||
big.phy = phy
|
big.phy = hci.Phy(phy)
|
||||||
big.nse = nse
|
big.nse = nse
|
||||||
big.bn = bn
|
big.bn = bn
|
||||||
big.pto = pto
|
big.pto = pto
|
||||||
@@ -5929,24 +6017,63 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
f'cis_id=[0x{cis_id:02X}] ***'
|
f'cis_id=[0x{cis_id:02X}] ***'
|
||||||
)
|
)
|
||||||
# LE_CIS_Established event doesn't provide info, so we must store them here.
|
# LE_CIS_Established event doesn't provide info, so we must store them here.
|
||||||
self.cis_links[cis_handle] = CisLink(
|
cis_link = CisLink(
|
||||||
device=self,
|
device=self,
|
||||||
acl_connection=acl_connection,
|
acl_connection=acl_connection,
|
||||||
handle=cis_handle,
|
handle=cis_handle,
|
||||||
cig_id=cig_id,
|
cig_id=cig_id,
|
||||||
cis_id=cis_id,
|
cis_id=cis_id,
|
||||||
)
|
)
|
||||||
self.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]
|
# [LE only]
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@utils.experimental('Only for testing')
|
@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 = self.cis_links[cis_handle]
|
||||||
cis_link.state = CisLink.State.ESTABLISHED
|
cis_link.state = CisLink.State.ESTABLISHED
|
||||||
|
|
||||||
assert cis_link.acl_connection
|
assert cis_link.acl_connection
|
||||||
|
|
||||||
|
# Update the CIS
|
||||||
|
cis_link.cig_sync_delay = cig_sync_delay
|
||||||
|
cis_link.cis_sync_delay = cis_sync_delay
|
||||||
|
cis_link.transport_latency_c_to_p = transport_latency_c_to_p
|
||||||
|
cis_link.transport_latency_p_to_c = transport_latency_p_to_c
|
||||||
|
cis_link.phy_c_to_p = hci.Phy(phy_c_to_p)
|
||||||
|
cis_link.phy_p_to_c = hci.Phy(phy_p_to_c)
|
||||||
|
cis_link.nse = nse
|
||||||
|
cis_link.bn_c_to_p = bn_c_to_p
|
||||||
|
cis_link.bn_p_to_c = bn_p_to_c
|
||||||
|
cis_link.ft_c_to_p = ft_c_to_p
|
||||||
|
cis_link.ft_p_to_c = ft_p_to_c
|
||||||
|
cis_link.max_pdu_c_to_p = max_pdu_c_to_p
|
||||||
|
cis_link.max_pdu_p_to_c = max_pdu_p_to_c
|
||||||
|
cis_link.iso_interval = iso_interval * 1.25
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** CIS Establishment '
|
f'*** CIS Establishment '
|
||||||
f'{cis_link.acl_connection.peer_address}, '
|
f'{cis_link.acl_connection.peer_address}, '
|
||||||
@@ -5956,16 +6083,27 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
cis_link.emit(cis_link.EVENT_ESTABLISHMENT)
|
cis_link.emit(cis_link.EVENT_ESTABLISHMENT)
|
||||||
|
cis_link.acl_connection.emit(
|
||||||
|
cis_link.acl_connection.EVENT_CIS_ESTABLISHMENT, cis_link
|
||||||
|
)
|
||||||
self.emit(self.EVENT_CIS_ESTABLISHMENT, cis_link)
|
self.emit(self.EVENT_CIS_ESTABLISHMENT, cis_link)
|
||||||
|
|
||||||
# [LE only]
|
# [LE only]
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@utils.experimental('Only for testing')
|
@utils.experimental('Only for testing')
|
||||||
def on_cis_establishment_failure(self, cis_handle: int, status: int) -> None:
|
def on_cis_establishment_failure(self, cis_handle: int, status: int) -> None:
|
||||||
|
if (cis_link := self.cis_links.pop(cis_handle, None)) is None:
|
||||||
|
logger.warning("CIS link not found")
|
||||||
|
return
|
||||||
|
|
||||||
logger.debug(f'*** CIS Establishment Failure: cis=[0x{cis_handle:04X}] ***')
|
logger.debug(f'*** CIS Establishment Failure: cis=[0x{cis_handle:04X}] ***')
|
||||||
if cis_link := self.cis_links.pop(cis_handle):
|
|
||||||
cis_link.emit(cis_link.EVENT_ESTABLISHMENT_FAILURE, status)
|
cis_link.emit(cis_link.EVENT_ESTABLISHMENT_FAILURE, status)
|
||||||
self.emit(self.EVENT_CIS_ESTABLISHMENT_FAILURE, cis_handle, 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]
|
# [LE only]
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@@ -6026,13 +6164,19 @@ class Device(utils.CompositeEventEmitter):
|
|||||||
|
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@with_connection_from_handle
|
@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(
|
logger.debug(
|
||||||
f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
|
f'*** Connection Parameters Update: [0x{connection.handle:04X}] '
|
||||||
f'{connection.peer_address} as {connection.role_name}, '
|
f'{connection.peer_address} as {connection.role_name}, '
|
||||||
f'{connection_parameters}'
|
f'{connection_parameters}'
|
||||||
)
|
)
|
||||||
connection.parameters = connection_parameters
|
connection.parameters = Connection.Parameters(
|
||||||
|
connection_parameters.connection_interval * 1.25,
|
||||||
|
connection_parameters.peripheral_latency,
|
||||||
|
connection_parameters.supervision_timeout * 10.0,
|
||||||
|
)
|
||||||
connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
|
connection.emit(connection.EVENT_CONNECTION_PARAMETERS_UPDATE)
|
||||||
|
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
|
|||||||
@@ -5089,6 +5089,25 @@ class HCI_LE_Set_Default_Periodic_Advertising_Sync_Transfer_Parameters_Command(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class HCI_LE_Read_ISO_TX_Sync_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.8.96 LE Read ISO TX Sync command
|
||||||
|
'''
|
||||||
|
|
||||||
|
connection_handle: int = field(metadata=metadata(2))
|
||||||
|
|
||||||
|
return_parameters_fields = [
|
||||||
|
('status', STATUS_SPEC),
|
||||||
|
('connection_handle', 2),
|
||||||
|
('packet_sequence_number', 2),
|
||||||
|
('tx_time_stamp', 4),
|
||||||
|
('time_offset', 3),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command
|
@HCI_Command.command
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
@@ -7381,6 +7400,9 @@ class HCI_IsoDataPacket(HCI_Packet):
|
|||||||
iso_sdu_length: Optional[int] = None
|
iso_sdu_length: Optional[int] = None
|
||||||
packet_status_flag: Optional[int] = None
|
packet_status_flag: Optional[int] = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self.ts_flag = self.time_stamp is not None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_bytes(packet: bytes) -> HCI_IsoDataPacket:
|
def from_bytes(packet: bytes) -> HCI_IsoDataPacket:
|
||||||
time_stamp: Optional[int] = None
|
time_stamp: Optional[int] = None
|
||||||
@@ -7446,15 +7468,26 @@ class HCI_IsoDataPacket(HCI_Packet):
|
|||||||
return struct.pack(fmt, *args) + self.iso_sdu_fragment
|
return struct.pack(fmt, *args) + self.iso_sdu_fragment
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return (
|
result = (
|
||||||
f'{color("ISO", "blue")}: '
|
f'{color("ISO", "blue")}: '
|
||||||
f'handle=0x{self.connection_handle:04x}, '
|
f'handle=0x{self.connection_handle:04x}, '
|
||||||
f'pb={self.pb_flag}, '
|
f'pb={self.pb_flag}, '
|
||||||
|
f'data_total_length={self.data_total_length}'
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.ts_flag:
|
||||||
|
result += f', time_stamp={self.time_stamp}'
|
||||||
|
|
||||||
|
if self.pb_flag in (0b00, 0b10):
|
||||||
|
result += (
|
||||||
|
', '
|
||||||
|
f'packet_sequence_number={self.packet_sequence_number}, '
|
||||||
f'ps={self.packet_status_flag}, '
|
f'ps={self.packet_status_flag}, '
|
||||||
f'data_total_length={self.data_total_length}, '
|
|
||||||
f'sdu_fragment={self.iso_sdu_fragment.hex()}'
|
f'sdu_fragment={self.iso_sdu_fragment.hex()}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class HCI_AclDataPacketAssembler:
|
class HCI_AclDataPacketAssembler:
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ from bumble.snoop import Snooper
|
|||||||
from bumble import drivers
|
from bumble import drivers
|
||||||
from bumble import hci
|
from bumble import hci
|
||||||
from bumble.core import (
|
from bumble.core import (
|
||||||
PhysicalTransport,
|
|
||||||
PhysicalTransport,
|
PhysicalTransport,
|
||||||
ConnectionPHY,
|
ConnectionPHY,
|
||||||
ConnectionParameters,
|
ConnectionParameters,
|
||||||
@@ -72,6 +71,11 @@ class DataPacketQueue(utils.EventEmitter):
|
|||||||
|
|
||||||
max_packet_size: int
|
max_packet_size: int
|
||||||
|
|
||||||
|
class PerConnectionState:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.in_flight = 0
|
||||||
|
self.drained = asyncio.Event()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
max_packet_size: int,
|
max_packet_size: int,
|
||||||
@@ -82,9 +86,12 @@ class DataPacketQueue(utils.EventEmitter):
|
|||||||
self.max_packet_size = max_packet_size
|
self.max_packet_size = max_packet_size
|
||||||
self.max_in_flight = max_in_flight
|
self.max_in_flight = max_in_flight
|
||||||
self._in_flight = 0 # Total number of packets in flight across all connections
|
self._in_flight = 0 # Total number of packets in flight across all connections
|
||||||
self._in_flight_per_connection: dict[int, int] = collections.defaultdict(
|
self._connection_state: dict[int, DataPacketQueue.PerConnectionState] = (
|
||||||
int
|
collections.defaultdict(DataPacketQueue.PerConnectionState)
|
||||||
) # Number of packets in flight per connection
|
)
|
||||||
|
self._drained_per_connection: dict[int, asyncio.Event] = (
|
||||||
|
collections.defaultdict(asyncio.Event)
|
||||||
|
)
|
||||||
self._send = send
|
self._send = send
|
||||||
self._packets: collections.deque[tuple[hci.HCI_Packet, int]] = (
|
self._packets: collections.deque[tuple[hci.HCI_Packet, int]] = (
|
||||||
collections.deque()
|
collections.deque()
|
||||||
@@ -136,36 +143,40 @@ class DataPacketQueue(utils.EventEmitter):
|
|||||||
self._completed += flushed_count
|
self._completed += flushed_count
|
||||||
self._packets = collections.deque(packets_to_keep)
|
self._packets = collections.deque(packets_to_keep)
|
||||||
|
|
||||||
if connection_handle in self._in_flight_per_connection:
|
if connection_state := self._connection_state.pop(connection_handle, None):
|
||||||
in_flight = self._in_flight_per_connection[connection_handle]
|
in_flight = connection_state.in_flight
|
||||||
self._completed += in_flight
|
self._completed += in_flight
|
||||||
self._in_flight -= in_flight
|
self._in_flight -= in_flight
|
||||||
del self._in_flight_per_connection[connection_handle]
|
connection_state.drained.set()
|
||||||
|
|
||||||
def _check_queue(self) -> None:
|
def _check_queue(self) -> None:
|
||||||
while self._packets and self._in_flight < self.max_in_flight:
|
while self._packets and self._in_flight < self.max_in_flight:
|
||||||
packet, connection_handle = self._packets.pop()
|
packet, connection_handle = self._packets.pop()
|
||||||
self._send(packet)
|
self._send(packet)
|
||||||
self._in_flight += 1
|
self._in_flight += 1
|
||||||
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:
|
def on_packets_completed(self, packet_count: int, connection_handle: int) -> None:
|
||||||
"""Mark one or more packets associated with a connection as completed."""
|
"""Mark one or more packets associated with a connection as completed."""
|
||||||
if connection_handle not in self._in_flight_per_connection:
|
if connection_handle not in self._connection_state:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'received completion for unknown connection {connection_handle}'
|
f'received completion for unknown connection {connection_handle}'
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
in_flight_for_connection = self._in_flight_per_connection[connection_handle]
|
connection_state = self._connection_state[connection_handle]
|
||||||
if packet_count <= in_flight_for_connection:
|
if packet_count <= connection_state.in_flight:
|
||||||
self._in_flight_per_connection[connection_handle] -= packet_count
|
connection_state.in_flight -= packet_count
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'{packet_count} completed for {connection_handle} '
|
f'{packet_count} completed for {connection_handle} '
|
||||||
f'but only {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:
|
if packet_count <= self._in_flight:
|
||||||
self._in_flight -= packet_count
|
self._in_flight -= packet_count
|
||||||
@@ -180,6 +191,13 @@ class DataPacketQueue(utils.EventEmitter):
|
|||||||
self._check_queue()
|
self._check_queue()
|
||||||
self.emit('flow')
|
self.emit('flow')
|
||||||
|
|
||||||
|
async def drain(self, connection_handle: int) -> None:
|
||||||
|
"""Wait until there are no pending packets for a connection."""
|
||||||
|
if not (connection_state := self._connection_state.get(connection_handle)):
|
||||||
|
raise ValueError('no such connection')
|
||||||
|
|
||||||
|
await connection_state.drained.wait()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Connection:
|
class Connection:
|
||||||
@@ -1269,7 +1287,24 @@ class Host(utils.EventEmitter):
|
|||||||
self.cis_links[event.connection_handle] = IsoLink(
|
self.cis_links[event.connection_handle] = IsoLink(
|
||||||
handle=event.connection_handle, packet_queue=self.iso_packet_queue
|
handle=event.connection_handle, packet_queue=self.iso_packet_queue
|
||||||
)
|
)
|
||||||
self.emit('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:
|
else:
|
||||||
self.emit(
|
self.emit(
|
||||||
'cis_establishment_failure', event.connection_handle, event.status
|
'cis_establishment_failure', event.connection_handle, event.status
|
||||||
@@ -1372,6 +1407,10 @@ class Host(utils.EventEmitter):
|
|||||||
self.emit('role_change_failure', event.bd_addr, event.status)
|
self.emit('role_change_failure', event.bd_addr, event.status)
|
||||||
|
|
||||||
def on_hci_le_data_length_change_event(self, event):
|
def on_hci_le_data_length_change_event(self, event):
|
||||||
|
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||||
|
logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
|
||||||
|
return
|
||||||
|
|
||||||
self.emit(
|
self.emit(
|
||||||
'connection_data_length_change',
|
'connection_data_length_change',
|
||||||
event.connection_handle,
|
event.connection_handle,
|
||||||
|
|||||||
@@ -343,22 +343,16 @@ class AseStateMachine(gatt.Characteristic):
|
|||||||
self.service.device.EVENT_CIS_ESTABLISHMENT, self.on_cis_establishment
|
self.service.device.EVENT_CIS_ESTABLISHMENT, self.on_cis_establishment
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_cis_request(
|
def on_cis_request(self, cis_link: device.CisLink) -> None:
|
||||||
self,
|
|
||||||
acl_connection: device.Connection,
|
|
||||||
cis_handle: int,
|
|
||||||
cig_id: int,
|
|
||||||
cis_id: int,
|
|
||||||
) -> None:
|
|
||||||
if (
|
if (
|
||||||
cig_id == self.cig_id
|
cis_link.cig_id == self.cig_id
|
||||||
and cis_id == self.cis_id
|
and cis_link.cis_id == self.cis_id
|
||||||
and self.state == self.State.ENABLING
|
and self.state == self.State.ENABLING
|
||||||
):
|
):
|
||||||
utils.cancel_on_event(
|
utils.cancel_on_event(
|
||||||
acl_connection,
|
cis_link.acl_connection,
|
||||||
'flush',
|
'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:
|
def on_cis_establishment(self, cis_link: device.CisLink) -> None:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from bumble import utils
|
from bumble import utils
|
||||||
from bumble.device import Device, Connection
|
from bumble.device import Device, CigParameters, CisLink, Connection
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
OwnAddressType,
|
OwnAddressType,
|
||||||
)
|
)
|
||||||
@@ -61,27 +61,39 @@ async def main() -> None:
|
|||||||
devices[0].random_address, own_address_type=OwnAddressType.RANDOM
|
devices[0].random_address, own_address_type=OwnAddressType.RANDOM
|
||||||
)
|
)
|
||||||
|
|
||||||
cid_ids = [2, 3]
|
|
||||||
cis_handles = await devices[1].setup_cig(
|
cis_handles = await devices[1].setup_cig(
|
||||||
|
CigParameters(
|
||||||
cig_id=1,
|
cig_id=1,
|
||||||
cis_id=cid_ids,
|
cis_parameters=[
|
||||||
sdu_interval=(10000, 255),
|
CigParameters.CisParameters(
|
||||||
framing=0,
|
cis_id=2,
|
||||||
max_sdu=(120, 0),
|
max_sdu_c_to_p=120,
|
||||||
retransmission_number=13,
|
max_sdu_p_to_c=0,
|
||||||
max_transport_latency=(100, 5),
|
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(
|
def on_cis_request(connection: Connection, cis_link: CisLink):
|
||||||
connection: Connection, cis_handle: int, _cig_id: int, _cis_id: int
|
connection.cancel_on_disconnection(devices[0].accept_cis_request(cis_link))
|
||||||
):
|
|
||||||
connection.cancel_on_disconnection(devices[0].accept_cis_request(cis_handle))
|
|
||||||
|
|
||||||
devices[0].on('cis_request', on_cis_request)
|
devices[0].on('cis_request', on_cis_request)
|
||||||
|
|
||||||
cis_links = await devices[1].create_cis(
|
cis_links = await devices[1].create_cis([(cis, connection) for cis in cis_handles])
|
||||||
[(cis, connection.handle) for cis in cis_handles]
|
|
||||||
)
|
|
||||||
|
|
||||||
for cis_link in cis_links:
|
for cis_link in cis_links:
|
||||||
await cis_link.disconnect()
|
await cis_link.disconnect()
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ file_outputs: dict[AseStateMachine, io.BufferedWriter] = {}
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
if len(sys.argv) < 3:
|
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
|
return
|
||||||
|
|
||||||
print('<<< connecting to HCI...')
|
print('<<< connecting to HCI...')
|
||||||
@@ -149,12 +149,9 @@ async def main() -> None:
|
|||||||
sdu += pdu.iso_sdu_fragment
|
sdu += pdu.iso_sdu_fragment
|
||||||
file_outputs[ase].write(sdu)
|
file_outputs[ase].write(sdu)
|
||||||
|
|
||||||
def on_ase_state_change(
|
def on_ase_state_change(ase: AseStateMachine) -> None:
|
||||||
state: AseStateMachine.State,
|
if ase.state != AseStateMachine.State.STREAMING:
|
||||||
ase: AseStateMachine,
|
if file_output := file_outputs.pop(ase, None):
|
||||||
) -> None:
|
|
||||||
if state != AseStateMachine.State.STREAMING:
|
|
||||||
if file_output := file_outputs.pop(ase):
|
|
||||||
file_output.close()
|
file_output.close()
|
||||||
else:
|
else:
|
||||||
file_output = open(f'{datetime.datetime.now().isoformat()}.lc3', 'wb')
|
file_output = open(f'{datetime.datetime.now().isoformat()}.lc3', 'wb')
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.github.google.bumble.remotehci;
|
package com.github.google.bumble.remotehci;
|
||||||
|
|
||||||
|
import static com.github.google.bumble.remotehci.HciPacket.Type.COMMAND;
|
||||||
|
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
@@ -16,6 +18,10 @@ public class HciProxy {
|
|||||||
private int mAclPacketsSent;
|
private int mAclPacketsSent;
|
||||||
private int mScoPacketsSent;
|
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 {
|
HciProxy(int port, Listener listener) throws HalException {
|
||||||
this.mListener = listener;
|
this.mListener = listener;
|
||||||
|
|
||||||
@@ -84,6 +90,14 @@ public class HciProxy {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPacket(HciPacket.Type type, byte[] packet) {
|
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));
|
Log.d(TAG, String.format("HOST->CONTROLLER: type=%s, size=%d", type, packet.length));
|
||||||
hciHal.sendPacket(type, packet);
|
hciHal.sendPacket(type, packet);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import java.net.Socket;
|
|||||||
|
|
||||||
public class HciServer {
|
public class HciServer {
|
||||||
private static final String TAG = "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 int mPort;
|
||||||
private final Listener mListener;
|
private final Listener mListener;
|
||||||
private OutputStream mOutputStream;
|
private OutputStream mOutputStream;
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ from bumble.core import (
|
|||||||
from bumble.device import (
|
from bumble.device import (
|
||||||
AdvertisingEventProperties,
|
AdvertisingEventProperties,
|
||||||
AdvertisingParameters,
|
AdvertisingParameters,
|
||||||
|
CigParameters,
|
||||||
|
CisLink,
|
||||||
Connection,
|
Connection,
|
||||||
Device,
|
Device,
|
||||||
PeriodicAdvertisingParameters,
|
PeriodicAdvertisingParameters,
|
||||||
@@ -480,16 +482,13 @@ async def test_cis():
|
|||||||
|
|
||||||
peripheral_cis_futures = {}
|
peripheral_cis_futures = {}
|
||||||
|
|
||||||
def on_cis_request(
|
def on_cis_request(cis_link: CisLink):
|
||||||
acl_connection: Connection,
|
cis_link.acl_connection.cancel_on_disconnection(
|
||||||
cis_handle: int,
|
devices[1].accept_cis_request(cis_link),
|
||||||
_cig_id: int,
|
)
|
||||||
_cis_id: int,
|
peripheral_cis_futures[cis_link.handle] = (
|
||||||
):
|
asyncio.get_running_loop().create_future()
|
||||||
acl_connection.cancel_on_disconnection(
|
|
||||||
devices[1].accept_cis_request(cis_handle)
|
|
||||||
)
|
)
|
||||||
peripheral_cis_futures[cis_handle] = asyncio.get_running_loop().create_future()
|
|
||||||
|
|
||||||
devices[1].on('cis_request', on_cis_request)
|
devices[1].on('cis_request', on_cis_request)
|
||||||
devices[1].on(
|
devices[1].on(
|
||||||
@@ -498,19 +497,21 @@ async def test_cis():
|
|||||||
)
|
)
|
||||||
|
|
||||||
cis_handles = await devices[0].setup_cig(
|
cis_handles = await devices[0].setup_cig(
|
||||||
|
CigParameters(
|
||||||
cig_id=1,
|
cig_id=1,
|
||||||
cis_id=[2, 3],
|
cis_parameters=[
|
||||||
sdu_interval=(0, 0),
|
CigParameters.CisParameters(cis_id=2),
|
||||||
framing=0,
|
CigParameters.CisParameters(cis_id=3),
|
||||||
max_sdu=(0, 0),
|
],
|
||||||
retransmission_number=0,
|
sdu_interval_c_to_p=0,
|
||||||
max_transport_latency=(0, 0),
|
sdu_interval_p_to_c=0,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
assert len(cis_handles) == 2
|
assert len(cis_handles) == 2
|
||||||
cis_links = await devices[0].create_cis(
|
cis_links = await devices[0].create_cis(
|
||||||
[
|
[
|
||||||
(cis_handles[0], devices.connections[0].handle),
|
(cis_handles[0], devices.connections[0]),
|
||||||
(cis_handles[1], devices.connections[0].handle),
|
(cis_handles[1], devices.connections[0]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
await asyncio.gather(*peripheral_cis_futures.values())
|
await asyncio.gather(*peripheral_cis_futures.values())
|
||||||
@@ -528,32 +529,27 @@ async def test_cis_setup_failure():
|
|||||||
|
|
||||||
cis_requests = asyncio.Queue()
|
cis_requests = asyncio.Queue()
|
||||||
|
|
||||||
def on_cis_request(
|
def on_cis_request(cis_link: CisLink):
|
||||||
acl_connection: Connection,
|
cis_requests.put_nowait(cis_link)
|
||||||
cis_handle: int,
|
|
||||||
cig_id: int,
|
|
||||||
cis_id: int,
|
|
||||||
):
|
|
||||||
del acl_connection, cig_id, cis_id
|
|
||||||
cis_requests.put_nowait(cis_handle)
|
|
||||||
|
|
||||||
devices[1].on('cis_request', on_cis_request)
|
devices[1].on('cis_request', on_cis_request)
|
||||||
|
|
||||||
cis_handles = await devices[0].setup_cig(
|
cis_handles = await devices[0].setup_cig(
|
||||||
|
CigParameters(
|
||||||
cig_id=1,
|
cig_id=1,
|
||||||
cis_id=[2],
|
cis_parameters=[
|
||||||
sdu_interval=(0, 0),
|
CigParameters.CisParameters(cis_id=2),
|
||||||
framing=0,
|
],
|
||||||
max_sdu=(0, 0),
|
sdu_interval_c_to_p=0,
|
||||||
retransmission_number=0,
|
sdu_interval_p_to_c=0,
|
||||||
max_transport_latency=(0, 0),
|
),
|
||||||
)
|
)
|
||||||
assert len(cis_handles) == 1
|
assert len(cis_handles) == 1
|
||||||
|
|
||||||
cis_create_task = asyncio.create_task(
|
cis_create_task = asyncio.create_task(
|
||||||
devices[0].create_cis(
|
devices[0].create_cis(
|
||||||
[
|
[
|
||||||
(cis_handles[0], devices.connections[0].handle),
|
(cis_handles[0], devices.connections[0]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user