Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions bumble/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ class Host(utils.EventEmitter):
bis_links: dict[int, IsoLink]
sco_links: dict[int, ScoLink]
bigs: dict[int, set[int]]
link_ts_flags: dict[int, int]
acl_packet_queue: DataPacketQueue | None = None
le_acl_packet_queue: DataPacketQueue | None = None
iso_packet_queue: DataPacketQueue | None = None
Expand All @@ -269,6 +270,7 @@ def __init__(
self.bis_links = {} # BIS links, by connection handle
self.sco_links = {} # SCO links, by connection handle
self.bigs = {} # BIG Handle to BIS Handles
self.link_ts_flags = {} # TS_Flag for ISO links, by handle
self.pending_command: hci.HCI_SyncCommand | hci.HCI_AsyncCommand | None = None
self.pending_response: (
asyncio.Future[
Expand Down Expand Up @@ -486,6 +488,7 @@ async def reset(self, driver_factory=drivers.get_driver_for_host) -> None:
hci.HCI_LE_PHY_UPDATE_COMPLETE_EVENT,
hci.HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT,
hci.HCI_LE_PERIODIC_ADVERTISING_SYNC_ESTABLISHED_EVENT,
hci.HCI_LE_PERIODIC_ADVERTISING_SYNC_ESTABLISHED_V2_EVENT,
hci.HCI_LE_PERIODIC_ADVERTISING_REPORT_EVENT,
hci.HCI_LE_PERIODIC_ADVERTISING_SYNC_LOST_EVENT,
hci.HCI_LE_SCAN_TIMEOUT_EVENT,
Expand Down Expand Up @@ -1026,6 +1029,82 @@ def on_hci_acl_data_packet(self, packet: hci.HCI_AclDataPacket) -> None:
# Look for the connection to which this data belongs
if connection := self.connections.get(packet.connection_handle):
connection.on_hci_acl_data_packet(packet)
return

# WORKAROUND: Some controllers (e.g. Intel BE200) send ISO data wrapped in ACL packets
# using the CIS handle.
is_cis = packet.connection_handle in self.cis_links
is_bis = packet.connection_handle in self.bis_links

if is_cis or is_bis:
logger.debug(
f"Received ISO data wrapped in ACL packet for handle 0x{packet.connection_handle:04X}"
)
payload = packet.data

ts_flag = self.link_ts_flags.get(packet.connection_handle)
if ts_flag is None:
# Learn TS flag from the first packet on this link
if is_bis:
# BIS packets always have Timestamp according to spec
ts_flag = 1
elif len(payload) < 8:
# Too short to have 8-byte header (TS), must be No TS
ts_flag = 0
else:
psn_no_ts = int.from_bytes(payload[0:2], 'little')
psn_has_ts = int.from_bytes(payload[4:6], 'little')
if psn_has_ts == 0:
ts_flag = 1
elif psn_no_ts == 0:
ts_flag = 0
else:
# Fallback heuristic
ts_flag = 1 if psn_has_ts < psn_no_ts else 0
self.link_ts_flags[packet.connection_handle] = ts_flag
logger.info(
f"Learned TS_Flag = {ts_flag} for handle 0x{packet.connection_handle:04X}"
)

if ts_flag:
header_size = 8
sdu_length_offset = 6
else:
header_size = 4
sdu_length_offset = 2

pb_flag = 0b10
if len(payload) >= header_size:
sdu_length = int.from_bytes(
payload[sdu_length_offset : sdu_length_offset + 2], 'little'
)
if sdu_length == len(payload) - header_size:
pb_flag = 0b10 # Complete SDU
else:
pb_flag = 0b00 # First fragment
else:
pb_flag = 0b01 # Continuation
ts_flag = 0

# Reconstruct the raw ISO packet (excluding packet indicator 0x05)
pdu_info = packet.connection_handle | (pb_flag << 12) | (ts_flag << 14)
header = bytes(
[
pdu_info & 0xFF,
(pdu_info >> 8) & 0xFF,
len(payload) & 0xFF,
(len(payload) >> 8) & 0xFF,
]
)
raw_iso_packet = header + payload

try:
iso_packet = hci.HCI_IsoDataPacket.from_bytes(
bytes([hci.HCI_ISO_DATA_PACKET]) + raw_iso_packet
)
self.on_hci_iso_data_packet(iso_packet)
except Exception as e:
logger.warning(f"Failed to reconstruct ISO packet from ACL: {e}")

def on_hci_sco_data_packet(self, packet: hci.HCI_SynchronousDataPacket) -> None:
# Experimental
Expand Down Expand Up @@ -1232,6 +1311,7 @@ def on_hci_disconnection_complete_event(
self.emit('disconnection', handle, event.reason)

# Remove the handle reference
self.link_ts_flags.pop(handle, None)
_ = (
self.connections.pop(handle, 0)
or self.cis_links.pop(handle, 0)
Expand Down Expand Up @@ -1352,6 +1432,20 @@ def on_hci_le_periodic_advertising_sync_established_event(
event.advertiser_clock_accuracy,
)

def on_hci_le_periodic_advertising_sync_established_v2_event(
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Established_V2_Event
):
self.emit(
'periodic_advertising_sync_establishment',
event.status,
event.sync_handle,
event.advertising_sid,
event.advertiser_address,
event.advertiser_phy,
event.periodic_advertising_interval,
event.advertiser_clock_accuracy,
)

def on_hci_le_periodic_advertising_sync_lost_event(
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Lost_Event
):
Expand Down
5 changes: 5 additions & 0 deletions bumble/transport/usb.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ async def process_queue(self):
self.acl_out, packet[1:], callback=self.transfer_callback
)
self.acl_out_transfer.submit()
elif packet_type == hci.HCI_ISO_DATA_PACKET:
self.acl_out_transfer.setBulk(
self.acl_out, packet[1:], callback=self.transfer_callback
)
self.acl_out_transfer.submit()
elif packet_type == hci.HCI_COMMAND_PACKET:
self.acl_out_transfer.setControl(
USB_RECIPIENT_DEVICE | USB_REQUEST_TYPE_CLASS,
Expand Down
Loading