Commit 11162188 authored by vertighel's avatar vertighel
Browse files

STX: one loop thread at a time

Turning the loop off and on again (e.g. to change the binning) could
leave the old thread alive: stop_looping() only waited for state 0, and
the old thread found _looping True again and kept acquiring with the new
one. Frames with the previous binning then reached the viewer.

- stop_looping() joins the loop thread instead of waiting for state 0
- _run_loop() exits when a newer loop thread has replaced it

Same fix as 1beb766e for the Mako. Applies to Camera and Guider (tec1).

Co-Authored-By: default avatarClaude Opus 5.5 <noreply@anthropic.com>
parent 1beb766e
Loading
Loading
Loading
Loading
+20 −8
Changes for noctua/devices/stx.py: 20 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -269,7 +269,7 @@ class Camera(STX):
            log.info("STX: Loop started")

    def stop_looping(self):
        """Stop the acquisition loop and wait until the camera is actually idle.
        """Stop the acquisition loop and wait until the loop thread has ended.

        ``looping = False`` only tells the background thread not to
        retrigger — a frame already in flight keeps running. Abort it and
@@ -284,16 +284,28 @@ class Camera(STX):
        self.looping = False
        if self.state != 0:
            self.abort()
        deadline = time.time() + self.loop_exposure + 2.0
        while self.state != 0 and time.time() < deadline:
            time.sleep(0.1)
        # Wait for the loop thread itself to end, not just for state 0:
        # the camera can be idle while the thread is still downloading
        # the previous frame, which would then overwrite the new ones.
        thread = self._loop_thread
        if thread is not None:
            thread.join(timeout=self.loop_exposure + self.download_timeout + 5.0)

    def _is_current_loop(self):
        """bool : True if the calling thread is the active loop thread."""

        return self._looping and self._loop_thread is threading.current_thread()

    def _run_loop(self):
        while self._looping:
        # Also stop if a newer loop thread has replaced this one: if the
        # loop is turned off and on again (e.g. to change the binning)
        # while this thread is sleeping, it would otherwise find _looping
        # True again and keep acquiring together with the new one.
        while self._is_current_loop():
            self._start(self.loop_exposure, 1)
            while self._looping and self.ready != 1:
            while self._is_current_loop() and self.ready != 1:
                time.sleep(0.3)
            if self._looping:
            if self._is_current_loop():
                # Consume the buffer (HTTP readout) synchronously, in the same
                # loop that decides when to retrigger — otherwise the next
                # _start() can fire before the camera has left Reading state.
@@ -302,7 +314,7 @@ class Camera(STX):
                # actually left Exposing/Reading state yet (ready and state
                # are two independent HTTP polls) — wait for real idle
                # before retriggering, or _start() rejects the next frame.
                while self._looping and self.state != 0:
                while self._is_current_loop() and self.state != 0:
                    time.sleep(0.1)
        log.info("STX: Loop stopped")