add iso support to bench app

This commit is contained in:
Gilles Boccon-Gibod
2025-07-06 14:08:16 +02:00
parent 1c1b947455
commit 43a8cc37f8
15 changed files with 820 additions and 196 deletions

View File

@@ -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')

View File

@@ -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'],

View File

@@ -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
)
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -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())

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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:

View File

@@ -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,

View File

@@ -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:

View File

@@ -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()

View File

@@ -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')

View File

@@ -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);

View File

@@ -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;

View File

@@ -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]),
] ]
) )
) )