From ee002b1db6407d8e36e790313f0393b2b7e025dc Mon Sep 17 00:00:00 2001 From: Jacob Dahl <37091262+dakejahl@users.noreply.github.com> Date: Wed, 20 May 2026 21:08:53 -0400 Subject: [PATCH] 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 --- Tools/px4_uploader.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/Tools/px4_uploader.py b/Tools/px4_uploader.py index a6eb3fa604..c4dcfc754f 100755 --- a/Tools/px4_uploader.py +++ b/Tools/px4_uploader.py @@ -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