new gpr
This commit is contained in:
@@ -79,11 +79,21 @@ def decode_frame(frame: bytes) -> Packet:
|
||||
|
||||
|
||||
class FrameScanner:
|
||||
"""Incremental frame scanner for raw USB byte streams."""
|
||||
"""Incremental frame scanner for raw USB byte streams.
|
||||
|
||||
Tolerant of corruption: a ``0x5A`` that opens a frame which does not decode
|
||||
(bad CRC, unknown packet type, length mismatch) is treated as a false header.
|
||||
Such a byte is skipped one at a time and scanning resyncs on the next
|
||||
candidate header, so a malformed or partially-lost frame costs only a brief
|
||||
resync — ``feed`` never raises and never permanently desynchronizes the
|
||||
stream. ``discarded_byte_count`` exposes how many bytes were skipped this way
|
||||
for diagnostics.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize internal undecoded byte buffer."""
|
||||
"""Initialize internal undecoded byte buffer and resync counter."""
|
||||
self._buffer = bytearray()
|
||||
self.discarded_byte_count = 0
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Drop all buffered undecoded bytes."""
|
||||
@@ -109,15 +119,31 @@ class FrameScanner:
|
||||
|
||||
(length,) = struct.unpack_from("<H", self._buffer, 1)
|
||||
if length < _FRAME_OVERHEAD or length > _MAX_FRAME_LENGTH:
|
||||
logger.debug("Discarding byte due to invalid frame length=%d", length)
|
||||
del self._buffer[0]
|
||||
self._discard_false_header(f"invalid frame length={length}")
|
||||
continue
|
||||
|
||||
if len(self._buffer) < length:
|
||||
break
|
||||
|
||||
frame = bytes(self._buffer[:length])
|
||||
try:
|
||||
packet = decode_frame(frame)
|
||||
except (ParseError, CRCError) as exc:
|
||||
# The 0x5A was a false header — e.g. a byte inside a CRC-less
|
||||
# VNADatapoint payload, or a frame mangled by USB byte loss. Skip
|
||||
# one byte and resync on the next candidate header rather than
|
||||
# propagating (which would otherwise kill the whole transport).
|
||||
self._discard_false_header(str(exc))
|
||||
continue
|
||||
|
||||
del self._buffer[:length]
|
||||
decoded.append(decode_frame(frame))
|
||||
decoded.append(packet)
|
||||
|
||||
return decoded
|
||||
|
||||
def _discard_false_header(self, reason: str) -> None:
|
||||
"""Drop one buffered byte past a false frame header and count the resync."""
|
||||
self.discarded_byte_count += 1
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("Resyncing frame stream past false header: %s", reason)
|
||||
del self._buffer[0]
|
||||
|
||||
@@ -130,7 +130,7 @@ class USBTransport:
|
||||
raise DeviceDisconnectedError(f"Failed to prepare USB kernel driver state: {exc}") from exc
|
||||
|
||||
try:
|
||||
selected_handle.claimInterface(self.INTERFACE)
|
||||
self._claim_interface_with_busy_recovery(selected_handle)
|
||||
except usb1.USBError as exc:
|
||||
selected_handle.close()
|
||||
if self._ctx is not None:
|
||||
@@ -152,6 +152,29 @@ class USBTransport:
|
||||
self._rx_thread = threading.Thread(target=self._rx_loop, name="librevna-usb-rx", daemon=True)
|
||||
self._rx_thread.start()
|
||||
|
||||
def _claim_interface_with_busy_recovery(self, handle: "usb1.USBDeviceHandle") -> None: # type: ignore[name-defined]
|
||||
"""Claim the data interface, clearing a stale BUSY claim once if needed.
|
||||
|
||||
When a previous session's RX thread wedged inside libusb, ``disconnect()``
|
||||
deliberately leaks that handle with the interface still claimed to avoid a
|
||||
use-after-free. A fresh ``connect()`` then fails with
|
||||
``LIBUSB_ERROR_BUSY``. Resetting the device clears the orphaned claim so
|
||||
the retry succeeds, instead of the device being unusable until it is
|
||||
physically replugged. If the reset forces a re-enumeration the retry raises
|
||||
and the caller falls back to its normal reopen backoff.
|
||||
"""
|
||||
try:
|
||||
handle.claimInterface(self.INTERFACE)
|
||||
return
|
||||
except usb1.USBErrorBusy as exc:
|
||||
logger.warning(
|
||||
"USB interface %d busy on claim; resetting device to clear a stale claim: %s",
|
||||
self.INTERFACE,
|
||||
exc,
|
||||
)
|
||||
handle.resetDevice()
|
||||
handle.claimInterface(self.INTERFACE)
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Stop RX thread and close USB resources."""
|
||||
logger.debug("USB disconnect requested")
|
||||
@@ -234,8 +257,8 @@ class USBTransport:
|
||||
continue
|
||||
except usb1.USBErrorNoDevice as exc:
|
||||
if self._on_disconnect is not None:
|
||||
self._on_disconnect(DeviceDisconnectedError("USB device disconnected"))
|
||||
logger.warning("USB RX stopped: device disconnected")
|
||||
self._on_disconnect(DeviceDisconnectedError(f"USB device disconnected: {exc}"))
|
||||
logger.warning("USB RX stopped: device disconnected: %s", exc)
|
||||
return
|
||||
except usb1.USBError as exc:
|
||||
if self._stop_event.is_set():
|
||||
|
||||
Reference in New Issue
Block a user