Skip to content

usb transport packet splitter#936

Open
barbibulle wants to merge 2 commits into
mainfrom
gbg/usb-transport-packet-splitter
Open

usb transport packet splitter#936
barbibulle wants to merge 2 commits into
mainfrom
gbg/usb-transport-packet-splitter

Conversation

@barbibulle
Copy link
Copy Markdown
Collaborator

This PR adds packet splitters for all incoming HCI data streams. Even though in most cases we observe that each incoming transfer only contains a single packet, in some cases (as reported in #925) there may be more than one packet (or even partial packets) in transfer buffers. The splitters accumulate data until a complete packet has been receive, then emit complete packets only.
Also included in this PR is the use of multiple transfers for isochronous endpoints (currently set at 2, which should be enough for a simple double-buffering mechanism), which helps avoid data drops for SCO packets (I was able to get HFP audio streams without audio glitches now, which were caused by data being dropped when data arrived before a new transfer was submitted, at the end of a completed transfer).

Copy link
Copy Markdown
Collaborator

@zxzxwu zxzxwu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! I did a review and found a potential bug in PacketSplitter.feed.

Bug in PacketSplitter.feed

If the incoming data chunk ends exactly at the header boundary, and the packet has a 0-length payload, the packet will not be emitted immediately. It will be stuck until the next feed call.

Scenario:

  1. feed is called with data that exactly matches the header (e.g. 4 bytes for ACL header).
  2. self.packet accumulates the header. data becomes empty.
  3. The code hits continue at the end of the header accumulation block:
    if (bytes_needed := self.header_size - len(self.packet)) > 0:
        self.packet += data[:bytes_needed]
        data = data[bytes_needed:]
        continue
  4. The loop terminates because data is empty.
  5. The code below the if block (which calculates packet_length and emits the packet if complete) is never reached.

If the payload length is 0, the packet is already complete but not emitted.

Suggested Fix:

Only continue if we don't have a complete header yet:

@@ -413,7 +413,8 @@
             if (bytes_needed := self.header_size - len(self.packet)) > 0:
                 self.packet += data[:bytes_needed]
                 data = data[bytes_needed:]
-                continue
+                if len(self.packet) < self.header_size:
+                    continue

Unit Tests

I recommend adding unit tests for PacketSplitter to verify this. Here are some tests I wrote that catch this issue:

import pytest
from bumble import hci
from bumble.transport import usb

def test_packet_splitter_complete():
    emitted = []
    splitter = usb.AclPacketSplitter(emitted.append)
    packet = bytes([0x01, 0x00, 0x04, 0x00, 0x11, 0x22, 0x33, 0x44])
    splitter.feed(packet)
    assert emitted == [packet]

def test_packet_splitter_chunks():
    emitted = []
    splitter = usb.AclPacketSplitter(emitted.append)
    packet = bytes([0x01, 0x00, 0x04, 0x00, 0x11, 0x22, 0x33, 0x44])
    splitter.feed(packet[:4])
    assert emitted == []
    splitter.feed(packet[4:])
    assert emitted == [packet]

def test_packet_splitter_multiple():
    emitted = []
    splitter = usb.AclPacketSplitter(emitted.append)
    packet1 = bytes([0x01, 0x00, 0x04, 0x00, 0x11, 0x22, 0x33, 0x44])
    packet2 = bytes([0x02, 0x00, 0x02, 0x00, 0x55, 0x66])
    splitter.feed(packet1 + packet2)
    assert emitted == [packet1, packet2]

def test_packet_splitter_partial():
    emitted = []
    splitter = usb.AclPacketSplitter(emitted.append)
    packet1 = bytes([0x01, 0x00, 0x04, 0x00, 0x11, 0x22, 0x33, 0x44])
    packet2 = bytes([0x02, 0x00, 0x02, 0x00, 0x55, 0x66])
    splitter.feed(packet1 + packet2[:4])
    assert emitted == [packet1]
    splitter.feed(packet2[4:])
    assert emitted == [packet1, packet2]

def test_packet_splitter_empty_payload():
    emitted = []
    splitter = usb.AclPacketSplitter(emitted.append)
    packet = bytes([0x01, 0x00, 0x00, 0x00])
    splitter.feed(packet)
    assert emitted == [packet]

def test_sco_packet_splitter():
    emitted = []
    splitter = usb.ScoPacketSplitter(emitted.append)
    packet = bytes([0x01, 0x00, 0x03, 0x11, 0x22, 0x33])
    splitter.feed(packet)
    assert emitted == [packet]

def test_event_packet_splitter():
    emitted = []
    splitter = usb.EventPacketSplitter(emitted.append)
    packet = bytes([0x04, 0x02, 0x11, 0x22])
    splitter.feed(packet)
    assert emitted == [packet]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants