From 2b0834bcb84ca529ede50b2de3afcbd77325e93c Mon Sep 17 00:00:00 2001 From: Caroline Pascal Date: Wed, 17 Jun 2026 11:40:17 +0200 Subject: [PATCH] fix(cameras): snapshot stop_event in read loops to avoid None deref (#3812) * Do not set stop_event to None when stopping thread * fix(cameras): snapshot stop_event in read loops to avoid None deref The background read loops accessed self.stop_event repeatedly while _stop_read_thread() can reassign it to None after join(). Reading the attribute across the loop condition (and a mid-loop re-check) was a time-of-check/time-of-use race: stop_event could flip to None between the `is None` test and the `.is_set()` call, raising AttributeError on the worker thread. Snapshot self.stop_event into a local once, guard it, and loop on the local Event. The Event object is thread-safe and lives for the thread's lifetime; _stop_read_thread() always calls .set() before nulling the attribute, so the local observes the stop and exits cleanly. This also lets us drop the redundant pre-lock stop check. Applies to OpenCVCamera, RealSenseCamera, and ZMQ camera. --------- Co-authored-by: Anes Benmerzoug --- src/lerobot/cameras/opencv/camera_opencv.py | 5 +++-- src/lerobot/cameras/realsense/camera_realsense.py | 5 +++-- src/lerobot/cameras/zmq/camera_zmq.py | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/lerobot/cameras/opencv/camera_opencv.py b/src/lerobot/cameras/opencv/camera_opencv.py index 3e92eaf06..b3c20e8dd 100644 --- a/src/lerobot/cameras/opencv/camera_opencv.py +++ b/src/lerobot/cameras/opencv/camera_opencv.py @@ -442,11 +442,12 @@ class OpenCVCamera(Camera): Stops on DeviceNotConnectedError, logs other errors and continues. """ - if self.stop_event is None: + stop_event = self.stop_event + if stop_event is None: raise RuntimeError(f"{self}: stop_event is not initialized before starting read loop.") failure_count = 0 - while not self.stop_event.is_set(): + while not stop_event.is_set(): try: raw_frame = self._read_from_hardware() processed_frame = self._postprocess_image(raw_frame) diff --git a/src/lerobot/cameras/realsense/camera_realsense.py b/src/lerobot/cameras/realsense/camera_realsense.py index e156e6d14..80008e9f9 100644 --- a/src/lerobot/cameras/realsense/camera_realsense.py +++ b/src/lerobot/cameras/realsense/camera_realsense.py @@ -471,11 +471,12 @@ class RealSenseCamera(Camera): Stops on DeviceNotConnectedError, logs other errors and continues. """ - if self.stop_event is None: + stop_event = self.stop_event + if stop_event is None: raise RuntimeError(f"{self}: stop_event is not initialized before starting read loop.") failure_count = 0 - while not self.stop_event.is_set(): + while not stop_event.is_set(): try: frame = self._read_from_hardware() color_frame_raw = frame.get_color_frame() diff --git a/src/lerobot/cameras/zmq/camera_zmq.py b/src/lerobot/cameras/zmq/camera_zmq.py index 1b0be5de6..f3df17814 100644 --- a/src/lerobot/cameras/zmq/camera_zmq.py +++ b/src/lerobot/cameras/zmq/camera_zmq.py @@ -246,11 +246,12 @@ class ZMQCamera(Camera): """ Internal loop run by the background thread for asynchronous reading. """ - if self.stop_event is None: + stop_event = self.stop_event + if stop_event is None: raise RuntimeError(f"{self}: stop_event is not initialized.") failure_count = 0 - while not self.stop_event.is_set(): + while not stop_event.is_set(): try: frame = self._read_from_hardware() capture_time = time.perf_counter()