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): 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('