fix(tools/uploader): handle USB CDC reconnect race during reboot (#27419)

After sending reboot-to-bootloader, the PX4 USB CDC node briefly
disappears while the bootloader re-enumerates. Reopening the serial
port can land on a half-broken descriptor and the next tcdrain()
raises termios.error (5, 'Input/output error'). That bare OSError
escaped every retry layer and crashed the uploader, even though a
manual re-run would succeed once enumeration settled.

Convert OSError/SerialException from flush() and reset_buffers() into
the module's ConnectionError, matching how send()/recv() already
behave, and let the identify retry loops in _try_identify also catch
ConnectionError so a single transient I/O hiccup doesn't abort the
upload.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
This commit is contained in:
Jacob Dahl
2026-05-20 21:08:53 -04:00
committed by GitHub
parent f094358bf0
commit ee002b1db6

View File

@@ -520,14 +520,31 @@ class SerialTransport:
def flush(self) -> None:
"""Flush output buffer."""
if self._port is not None:
if self._port is None:
return
# tcdrain() can raise termios.error (a bare OSError) if the device
# disappeared mid-flush — common right after a reboot-to-bootloader
# when the USB CDC node is being torn down and re-enumerated.
try:
self._port.flush()
except (OSError, serial.SerialException) as e:
raise ConnectionError(
f"Flush failed: {e}", port=self.port_name, operation="flush"
)
def reset_buffers(self) -> None:
"""Reset input and output buffers."""
if self._port is not None:
if self._port is None:
return
try:
self._port.reset_input_buffer()
self._port.reset_output_buffer()
except (OSError, serial.SerialException) as e:
raise ConnectionError(
f"Buffer reset failed: {e}",
port=self.port_name,
operation="reset_buffers",
)
def set_baudrate(self, baudrate: int) -> None:
"""Change baud rate.
@@ -1737,7 +1754,7 @@ class Uploader:
port=transport.port_name,
)
return True
except (ProtocolError, TimeoutError):
except (ProtocolError, TimeoutError, ConnectionError):
pass
# Try rebooting at each baud rate
@@ -1802,8 +1819,10 @@ class Uploader:
port=transport.port_name,
)
return True
except (ProtocolError, TimeoutError):
# Board may still be rebooting, wait a bit and retry
except (ProtocolError, TimeoutError, ConnectionError):
# Board may still be rebooting, wait a bit and retry.
# ConnectionError covers the USB CDC node briefly going
# away as the bootloader re-enumerates.
time.sleep(0.3)
return False