Loading noctua/devices/atik2.py +41 −33 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ from AtikSDK import (ARTEMIS_OK, ARTEMIS_PROPERTIES_CAMERAFLAGS_HAS_SHUTTER, # Custom modules from .basedevice import BaseDevice from ..config.constants import camera_frame from ..utils.check import timed_lock from ..utils.image import make_png from ..utils.logger import log Loading Loading @@ -82,7 +83,8 @@ class Camera(BaseDevice): True if connected (already, or just now), False otherwise. """ with self._lock: try: with timed_lock(self._lock, "Atik"): if self._cam.is_connected(): return True try: Loading @@ -107,6 +109,12 @@ class Camera(BaseDevice): return False return True except TimeoutError as e: msg = str(e) if msg not in self.error: self.error.append(msg) log.error(msg) return False def __del__(self): """Safely disconnect from the camera.""" Loading @@ -126,7 +134,7 @@ class Camera(BaseDevice): if not self._check_connection(): return None try: with self._lock: with timed_lock(self._lock, "Atik"): return getattr(self._cam, method)(*args) except Exception as e: msg = f"Atik get({method}) failed: {e}" Loading @@ -141,7 +149,7 @@ class Camera(BaseDevice): if not self._check_connection(): return None try: with self._lock: with timed_lock(self._lock, "Atik"): return getattr(self._cam, method)(*args) except Exception as e: msg = f"Atik put({method}) failed: {e}" Loading @@ -158,7 +166,7 @@ class Camera(BaseDevice): if not self._check_connection(): return None try: with self._lock: with timed_lock(self._lock, "Atik"): return self._get_image() except Exception as e: msg = f"Atik get(get_image) failed: {e}" Loading Loading @@ -282,7 +290,7 @@ class Camera(BaseDevice): f"Cannot start exposure, camera is not idle. State: {self.state}") self.error.append("Camera not idle") return with self._lock: with timed_lock(self._lock, "Atik"): self._last_exptime = duration self._last_imagetype = imagetype self._last_datetime = datetime Loading Loading @@ -339,7 +347,7 @@ class Camera(BaseDevice): if not self._check_connection(): return None with self._lock: with timed_lock(self._lock, "Atik"): last_exptime = self._last_exptime last_imagetype = self._last_imagetype last_datetime = self._last_datetime Loading Loading @@ -394,7 +402,7 @@ class Camera(BaseDevice): hdr['SET-TEMP'] = (self._setpoint if self._setpoint is not None else 0.0, "[C] CCD setpoint temperature") with self._lock: with timed_lock(self._lock, "Atik"): subframe = self._subframe hdr['XPIXSZ'] = (self._props.get('PixelMicronsX', 0), "[um] Pixel X size") Loading Loading @@ -519,7 +527,7 @@ class Camera(BaseDevice): self.put('set_subframe', x, y, w, h) if len(self.error) > errors_before: return False with self._lock: with timed_lock(self._lock, "Atik"): self._subframe = [x, y, w, h] return True Loading Loading @@ -699,7 +707,7 @@ class Camera(BaseDevice): def xystart(self): """list of int : Sub-frame [X, Y] origin in binned pixels.""" with self._lock: with timed_lock(self._lock, "Atik"): x, y, w, h = self._subframe bx, by = self.binning bx = bx or 1 Loading @@ -711,7 +719,7 @@ class Camera(BaseDevice): def xyend(self): """list of int : Sub-frame [X, Y] end corner in binned pixels.""" with self._lock: with timed_lock(self._lock, "Atik"): x, y, w, h = self._subframe bx, by = self.binning bx = bx or 1 Loading Loading @@ -768,7 +776,7 @@ class Camera(BaseDevice): def xrange(self): """list of int : Sub-frame X extent [start, end] in binned pixels.""" with self._lock: with timed_lock(self._lock, "Atik"): x, y, w, h = self._subframe bx, by = self.binning bx = bx or 1 Loading @@ -779,7 +787,7 @@ class Camera(BaseDevice): def yrange(self): """list of int : Sub-frame Y extent [start, end] in binned pixels.""" with self._lock: with timed_lock(self._lock, "Atik"): x, y, w, h = self._subframe bx, by = self.binning by = by or 1 Loading Loading @@ -811,7 +819,7 @@ class Camera(BaseDevice): fan_power = round((level / 255.0) * 100) if maxlvl > 0 else 0 b_x, b_y = self.get('get_binning') or (1, 1) with self._lock: with timed_lock(self._lock, "Atik"): x_sf, y_sf, w_sf, h_sf = self._subframe nx, ny = self._props.get('nPixelsX', 0), self._props.get('nPixelsY', 0) x_start = x_sf // b_x if b_x else 0 Loading noctua/devices/mako.py +41 −27 Original line number Diff line number Diff line Loading @@ -79,7 +79,15 @@ class Mako(BaseDevice): """ if self._cam is None: with Mako._connect_lock: try: # Bounded: vmb.__enter__()/get_camera_by_id()/cam.__enter__() # below are direct blocking SDK calls with no timeout of # their own — if the lock itself is free but one of them # wedges, this still waits forever without the acquire # timeout. That part isn't fixable from here (see # timed_lock's docstring); this only bounds how long other # threads queue up behind a connect that's already stuck. with check.timed_lock(Mako._connect_lock, "Mako connect"): if self._cam is None: # re-check: another thread may have connected while we waited self.vmb.__enter__() try: Loading Loading @@ -108,6 +116,12 @@ class Mako(BaseDevice): self.error.append(msg) log.error(msg) raise ConnectionError(msg) except TimeoutError as e: msg = str(e) if msg not in self.error: self.error.append(msg) log.error(msg) raise ConnectionError(msg) return self._cam Loading noctua/devices/stl.py +1 −1 Original line number Diff line number Diff line Loading @@ -685,7 +685,7 @@ class Camera(STL): b = self._binning left, top = start_x // b, start_y // b width, height = sub_width // b, sub_height // b with self._lock: with check.timed_lock(self._lock, "STL"): self._last_matrix = self._read_frame(left, top, width, height) return self._last_matrix Loading noctua/utils/check.py +27 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ specific types of functions. # System modules import socket from contextlib import contextmanager # Third-party modules import requests Loading @@ -16,6 +17,32 @@ from pyvantagepro.device import NoDeviceException # Other templates from ..utils.logger import log LOCK_TIMEOUT = 5.0 @contextmanager def timed_lock(lock, name, timeout=LOCK_TIMEOUT): '''Acquire *lock* with a bound instead of blocking forever. Several device SDKs (Atik, SBIG/STL, Mako) are wrapped in a lock but make blocking native calls with no timeout of their own — if one call ever wedges inside the SDK, an unbounded ``with lock:`` leaves every other thread waiting on it (including telemetry polling) hung forever too, with no error anywhere. This raises TimeoutError instead, so the caller degrades to a visible error on that one device instead of silently freezing everything that touches it. It does not free the thread stuck inside the original call — that one is still lost — but it stops the hang from cascading to every other caller. ''' if not lock.acquire(timeout=timeout): raise TimeoutError( f"{name}: lock busy for >{timeout}s — a thread is likely stuck inside the SDK") try: yield finally: lock.release() # class ManualRetry(Exception): # ''' # Manually raise an exception, so it is possible to handle a retry Loading Loading
noctua/devices/atik2.py +41 −33 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ from AtikSDK import (ARTEMIS_OK, ARTEMIS_PROPERTIES_CAMERAFLAGS_HAS_SHUTTER, # Custom modules from .basedevice import BaseDevice from ..config.constants import camera_frame from ..utils.check import timed_lock from ..utils.image import make_png from ..utils.logger import log Loading Loading @@ -82,7 +83,8 @@ class Camera(BaseDevice): True if connected (already, or just now), False otherwise. """ with self._lock: try: with timed_lock(self._lock, "Atik"): if self._cam.is_connected(): return True try: Loading @@ -107,6 +109,12 @@ class Camera(BaseDevice): return False return True except TimeoutError as e: msg = str(e) if msg not in self.error: self.error.append(msg) log.error(msg) return False def __del__(self): """Safely disconnect from the camera.""" Loading @@ -126,7 +134,7 @@ class Camera(BaseDevice): if not self._check_connection(): return None try: with self._lock: with timed_lock(self._lock, "Atik"): return getattr(self._cam, method)(*args) except Exception as e: msg = f"Atik get({method}) failed: {e}" Loading @@ -141,7 +149,7 @@ class Camera(BaseDevice): if not self._check_connection(): return None try: with self._lock: with timed_lock(self._lock, "Atik"): return getattr(self._cam, method)(*args) except Exception as e: msg = f"Atik put({method}) failed: {e}" Loading @@ -158,7 +166,7 @@ class Camera(BaseDevice): if not self._check_connection(): return None try: with self._lock: with timed_lock(self._lock, "Atik"): return self._get_image() except Exception as e: msg = f"Atik get(get_image) failed: {e}" Loading Loading @@ -282,7 +290,7 @@ class Camera(BaseDevice): f"Cannot start exposure, camera is not idle. State: {self.state}") self.error.append("Camera not idle") return with self._lock: with timed_lock(self._lock, "Atik"): self._last_exptime = duration self._last_imagetype = imagetype self._last_datetime = datetime Loading Loading @@ -339,7 +347,7 @@ class Camera(BaseDevice): if not self._check_connection(): return None with self._lock: with timed_lock(self._lock, "Atik"): last_exptime = self._last_exptime last_imagetype = self._last_imagetype last_datetime = self._last_datetime Loading Loading @@ -394,7 +402,7 @@ class Camera(BaseDevice): hdr['SET-TEMP'] = (self._setpoint if self._setpoint is not None else 0.0, "[C] CCD setpoint temperature") with self._lock: with timed_lock(self._lock, "Atik"): subframe = self._subframe hdr['XPIXSZ'] = (self._props.get('PixelMicronsX', 0), "[um] Pixel X size") Loading Loading @@ -519,7 +527,7 @@ class Camera(BaseDevice): self.put('set_subframe', x, y, w, h) if len(self.error) > errors_before: return False with self._lock: with timed_lock(self._lock, "Atik"): self._subframe = [x, y, w, h] return True Loading Loading @@ -699,7 +707,7 @@ class Camera(BaseDevice): def xystart(self): """list of int : Sub-frame [X, Y] origin in binned pixels.""" with self._lock: with timed_lock(self._lock, "Atik"): x, y, w, h = self._subframe bx, by = self.binning bx = bx or 1 Loading @@ -711,7 +719,7 @@ class Camera(BaseDevice): def xyend(self): """list of int : Sub-frame [X, Y] end corner in binned pixels.""" with self._lock: with timed_lock(self._lock, "Atik"): x, y, w, h = self._subframe bx, by = self.binning bx = bx or 1 Loading Loading @@ -768,7 +776,7 @@ class Camera(BaseDevice): def xrange(self): """list of int : Sub-frame X extent [start, end] in binned pixels.""" with self._lock: with timed_lock(self._lock, "Atik"): x, y, w, h = self._subframe bx, by = self.binning bx = bx or 1 Loading @@ -779,7 +787,7 @@ class Camera(BaseDevice): def yrange(self): """list of int : Sub-frame Y extent [start, end] in binned pixels.""" with self._lock: with timed_lock(self._lock, "Atik"): x, y, w, h = self._subframe bx, by = self.binning by = by or 1 Loading Loading @@ -811,7 +819,7 @@ class Camera(BaseDevice): fan_power = round((level / 255.0) * 100) if maxlvl > 0 else 0 b_x, b_y = self.get('get_binning') or (1, 1) with self._lock: with timed_lock(self._lock, "Atik"): x_sf, y_sf, w_sf, h_sf = self._subframe nx, ny = self._props.get('nPixelsX', 0), self._props.get('nPixelsY', 0) x_start = x_sf // b_x if b_x else 0 Loading
noctua/devices/mako.py +41 −27 Original line number Diff line number Diff line Loading @@ -79,7 +79,15 @@ class Mako(BaseDevice): """ if self._cam is None: with Mako._connect_lock: try: # Bounded: vmb.__enter__()/get_camera_by_id()/cam.__enter__() # below are direct blocking SDK calls with no timeout of # their own — if the lock itself is free but one of them # wedges, this still waits forever without the acquire # timeout. That part isn't fixable from here (see # timed_lock's docstring); this only bounds how long other # threads queue up behind a connect that's already stuck. with check.timed_lock(Mako._connect_lock, "Mako connect"): if self._cam is None: # re-check: another thread may have connected while we waited self.vmb.__enter__() try: Loading Loading @@ -108,6 +116,12 @@ class Mako(BaseDevice): self.error.append(msg) log.error(msg) raise ConnectionError(msg) except TimeoutError as e: msg = str(e) if msg not in self.error: self.error.append(msg) log.error(msg) raise ConnectionError(msg) return self._cam Loading
noctua/devices/stl.py +1 −1 Original line number Diff line number Diff line Loading @@ -685,7 +685,7 @@ class Camera(STL): b = self._binning left, top = start_x // b, start_y // b width, height = sub_width // b, sub_height // b with self._lock: with check.timed_lock(self._lock, "STL"): self._last_matrix = self._read_frame(left, top, width, height) return self._last_matrix Loading
noctua/utils/check.py +27 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ specific types of functions. # System modules import socket from contextlib import contextmanager # Third-party modules import requests Loading @@ -16,6 +17,32 @@ from pyvantagepro.device import NoDeviceException # Other templates from ..utils.logger import log LOCK_TIMEOUT = 5.0 @contextmanager def timed_lock(lock, name, timeout=LOCK_TIMEOUT): '''Acquire *lock* with a bound instead of blocking forever. Several device SDKs (Atik, SBIG/STL, Mako) are wrapped in a lock but make blocking native calls with no timeout of their own — if one call ever wedges inside the SDK, an unbounded ``with lock:`` leaves every other thread waiting on it (including telemetry polling) hung forever too, with no error anywhere. This raises TimeoutError instead, so the caller degrades to a visible error on that one device instead of silently freezing everything that touches it. It does not free the thread stuck inside the original call — that one is still lost — but it stops the hang from cascading to every other caller. ''' if not lock.acquire(timeout=timeout): raise TimeoutError( f"{name}: lock busy for >{timeout}s — a thread is likely stuck inside the SDK") try: yield finally: lock.release() # class ManualRetry(Exception): # ''' # Manually raise an exception, so it is possible to handle a retry Loading