From cab80c25ef44b3e8551f0654388f5f64416fcbf3 Mon Sep 17 00:00:00 2001 From: kwagyeman <5694981+kwagyeman@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:17:00 -0700 Subject: [PATCH 1/2] transport: Don't hang on an out-of-sequence NAK or on event storms. A NAK whose sequence number did not match the expected one was dropped by the header check, so a device that reset its protocol state (soft reboot) could never trigger a resync. Events also reset the receive deadline, so a device emitting stdout notifications every 50 ms kept the pending command waiting forever. Accept ACK/NAK packets with any sequence number and let events pass without extending the timeout. --- src/openmv/transport.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/openmv/transport.py b/src/openmv/transport.py index c0121f5..18f7f7a 100644 --- a/src/openmv/transport.py +++ b/src/openmv/transport.py @@ -79,9 +79,12 @@ def _check_crc(self, crc, buffer, crc_size=16): def _check_seq(self, sequence, expected_sequence, opcode, flags): """Check if sequence is valid or sequence checking is disabled""" + # ACK/NAK packets are accepted with any sequence number: after the + # device resets its protocol state (e.g. a soft reboot) its sequence + # restarts at 0, and the NAK it sends for our out-of-sequence command + # is the only way to learn that a resync is needed. return (not self.seq_enabled or - (flags & Flags.EVENT) or - (flags & Flags.RTX) or + (flags & (Flags.EVENT | Flags.RTX | Flags.ACK | Flags.NAK)) or sequence == expected_sequence or opcode == Opcode.PROTO_SYNC) @@ -211,11 +214,13 @@ def recv_packet(self, poll_events=False): else: self.send_packet(packet['opcode'], packet['channel'], Flags.ACK) - # Handle event packets + # Handle event packets. Events do not extend the receive timeout: + # a device that keeps emitting events (e.g. stdout notifications + # every 50 ms while its output is unread) must not keep a command + # waiting for its response forever. if packet['flags'] & Flags.EVENT: self.event_callback(packet['channel'], 0xFFFF if not packet['length'] else struct.unpack(' Date: Mon, 26 Oct 2026 12:58:38 -0700 Subject: [PATCH 2/2] camera: Resync after a soft reboot, like the IDE. Stopping a running script soft-reboots the device, which restarts its protocol sequence at 0, resets the negotiated capabilities and drops the dynamic channels. Commands sent before the host noticed were ACKed by the context being torn down (and their effect lost) or NAKed afterwards. On the SOFT_REBOOT event mark a resync as pending, reset the sequence for in-flight packets and schedule a channel list refresh; the next command resyncs first. Without events there is no notification, so stop() marks the resync itself. This matches the IDE's client. --- src/openmv/camera.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/openmv/camera.py b/src/openmv/camera.py index fd1aa20..dbc3c92 100644 --- a/src/openmv/camera.py +++ b/src/openmv/camera.py @@ -51,6 +51,7 @@ def __init__(self, port, baudrate=921600, self.verinfo = {} self.transport = None self.frame_event = False + self.resync_pending = False def __enter__(self): """Context manager entry""" @@ -82,6 +83,14 @@ def _send_cmd_wait_resp(self, opcode, channel=0, data=b''): if not self.is_connected(): raise OMVException("Not connected") + # The device announced a soft reboot: its protocol state (sequence, + # capabilities, dynamic channels) is reset once it is back. Start the + # next command with a resync so both sides agree again. + if self.resync_pending: + # Clear first: _resync() negotiates capabilities through this function. + self.resync_pending = False + self._resync() + # Special handling for reset commands - they never return if opcode in [Opcode.SYS_RESET, Opcode.SYS_BOOT]: self.transport.send_packet(opcode, channel, 0, data) @@ -109,6 +118,11 @@ def _handle_event(self, channel_id, event): # Handle system events if event == EventType.SOFT_REBOOT: logging.info("🔥 Soft Reboot triggered") + self.resync_pending = True + # In-flight packets from the rebooting device restart at 0. + self.transport.reset_sequence() + # The reboot drops every dynamic channel: refresh the list. + self.pending_channel_events += 1 elif event == EventType.CHANNEL_REGISTERED: self.pending_channel_events += 1 elif channel_id in self.channels_by_id: @@ -334,6 +348,10 @@ def stop(self): # Stop running script stdin_id = self.get_channel(name="stdin") self._channel_ioctl(stdin_id, ChannelIOCTL.STDIN_STOP) + # Stopping a running script soft-reboots the device. Without events + # there is no SOFT_REBOOT notification, so assume a resync is needed. + if not self.caps['events']: + self.resync_pending = True @retry_if_failed def exec(self, script):