Loading noctua/config/cameras.ini +8 −6 Original line number Diff line number Diff line Loading @@ -18,6 +18,9 @@ # half_frame(); omit to use the device's built-in default # small_frame — [px] xstart,ystart,xend,yend at binning 1, used by # small_frame(); omit to use the device's built-in default # background_estimate — [ADU] initial guess for fit_star()'s background # model; depends on sensor bias/bit-depth, tune on real # data. Omit to fall back to 900 (scicam1/STX level). # ######################################################### ############## Loading @@ -41,8 +44,6 @@ power_api = /scicam2/power fits_path = fits/scicam2.fits pixel_scale = 0.283 orientation = -60 # spettrografo long-slit: la finestra ritaglia solo in Y, tutta la # larghezza del sensore (KAI-11002, 4008x2672) resta leggibile. half_frame = 0, 668, 4007, 2004 small_frame = 0, 1500, 4007, 2000 Loading @@ -53,8 +54,6 @@ power_api = /telescope/power fits_path = fits/scicam3.fits pixel_scale = 0.283 orientation = -60 # stesso sensore KAI-11002 di scicam2 (4008x2672), ma qui centrato su # entrambi gli assi come STX: camera scientifica, non slit. half_frame = 1002, 668, 3006, 2004 small_frame = 1803, 1202, 2203, 1469 Loading @@ -71,6 +70,7 @@ device = tec1 has_loop = true pixel_scale = 0.283 orientation = -60 background_estimate = 900 [teccam2] role = tec Loading @@ -79,8 +79,9 @@ power_api = /telescope/power fits_path = fits/teccam2.fits device = tec2 has_loop = true pixel_scale = 0.283 pixel_scale = 0.160 orientation = -60 background_estimate = 10 [teccam3] role = tec Loading @@ -89,5 +90,6 @@ power_api = /telescope/power fits_path = fits/teccam3.fits device = tec3 has_loop = true pixel_scale = 0.283 pixel_scale = 0.160 orientation = -60 background_estimate = 10 noctua/config/constants.py +16 −0 Original line number Diff line number Diff line Loading @@ -46,6 +46,22 @@ def camera_optics(viewer_key): return scale, orientation def camera_star_estimate(viewer_key): """Return background_estimate [ADU] for a cam_id from cameras.ini. Initial guess for fit_star()'s background model; depends on sensor bias/bit-depth. Falls back to 900 (scicam1/STX level) if the key is absent or not configured. """ if not viewer_key: return 900.0 cfg = configparser.ConfigParser() cfg.read(_CAMERAS_INI) return cfg.getfloat(viewer_key, 'background_estimate', fallback=900.0) def camera_frame(viewer_key, name): """Return (x0, y0, x1, y1) unbinned px for 'half_frame'/'small_frame' from cameras.ini, or None if the key is absent or not configured. Loading noctua/guider.py +37 −17 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ # System modules import argparse import asyncio import time # Third-party modules import numpy as np Loading @@ -16,7 +17,8 @@ from astropy.wcs import WCS # Custom modules from noctua import devices from noctua.config.constants import alt, camera_optics, lat, lon, viewer_fits_path from noctua.config.constants import (alt, camera_optics, camera_star_estimate, dateobs, lat, lon, viewer_fits_path) from noctua.utils.analysis import fit_star from noctua.utils.logger import log Loading @@ -42,6 +44,7 @@ class Guider: self.last_correction = [0.0, 0.0] self._task = None self._stick_target = None self._last_frame_time = None self.ao_pos = [50.0, 50.0] self.ao_offload_threshold = 90 Loading Loading @@ -108,6 +111,7 @@ class Guider: self.active = True self.error = [] self._stick_target = None self._last_frame_time = None self._task = asyncio.get_running_loop().create_task(self._loop()) log.info(f"Guider started: camera={self.camera} actuator={self.actuator} target={self.target}") Loading Loading @@ -157,8 +161,9 @@ class Guider: x0, x1 = max(cx - half, 0), min(cx + half, nx) y0, y1 = max(cy - half, 0), min(cy + half, ny) background = camera_star_estimate(getattr(cam, "_viewer_key", None)) try: fitted = fit_star(data[y0:y1, x0:x1]) fitted = fit_star(data[y0:y1, x0:x1], background_estimation=background) except Exception as e: msg = f"fit_star: {e}" Loading Loading @@ -257,24 +262,39 @@ class Guider: self.ao_pos = [50.0, 50.0] def _acquire(self, cam): """Read the latest FITS frame (and header) for this camera from disk. """Read the next FITS frame (and header) for this camera from disk. The guider never drives acquisition itself — it only reads whatever the camera's own loop (or, for scicam, the last Expose) last wrote. Freshness is the caller's responsibility: guiding is only meaningful while something else keeps producing new frames. It blocks here (this runs in a worker thread, so a plain sleep is fine) until a frame with a new DATE-OBS shows up, so a correction is never computed twice off the same, still-uncorrected image. """ fits_path = viewer_fits_path(getattr(cam, "_viewer_key", None)) timeout = max(3.0, 3 * getattr(cam, "loop_exposure", 1.0)) deadline = time.monotonic() + timeout while True: try: data, header = fits.getdata(fits_path, header=True) return data.astype(np.float32), header except Exception as e: msg = f"acquire: {e}" log.error(f"Guider: {msg}") self.error.append(msg) return None, None frame_time = header.get(dateobs) if frame_time != self._last_frame_time: self._last_frame_time = frame_time return data.astype(np.float32), header if time.monotonic() > deadline: log.warning(f"Guider: no new frame from '{self.camera}' within {timeout:.1f}s") return None, None time.sleep(0.1) def _pixel_to_offset(self, cam, header, nx, ny, star_x, star_y, tgt_x, tgt_y): coordinates = devices.tel.coordinates Loading Loading
noctua/config/cameras.ini +8 −6 Original line number Diff line number Diff line Loading @@ -18,6 +18,9 @@ # half_frame(); omit to use the device's built-in default # small_frame — [px] xstart,ystart,xend,yend at binning 1, used by # small_frame(); omit to use the device's built-in default # background_estimate — [ADU] initial guess for fit_star()'s background # model; depends on sensor bias/bit-depth, tune on real # data. Omit to fall back to 900 (scicam1/STX level). # ######################################################### ############## Loading @@ -41,8 +44,6 @@ power_api = /scicam2/power fits_path = fits/scicam2.fits pixel_scale = 0.283 orientation = -60 # spettrografo long-slit: la finestra ritaglia solo in Y, tutta la # larghezza del sensore (KAI-11002, 4008x2672) resta leggibile. half_frame = 0, 668, 4007, 2004 small_frame = 0, 1500, 4007, 2000 Loading @@ -53,8 +54,6 @@ power_api = /telescope/power fits_path = fits/scicam3.fits pixel_scale = 0.283 orientation = -60 # stesso sensore KAI-11002 di scicam2 (4008x2672), ma qui centrato su # entrambi gli assi come STX: camera scientifica, non slit. half_frame = 1002, 668, 3006, 2004 small_frame = 1803, 1202, 2203, 1469 Loading @@ -71,6 +70,7 @@ device = tec1 has_loop = true pixel_scale = 0.283 orientation = -60 background_estimate = 900 [teccam2] role = tec Loading @@ -79,8 +79,9 @@ power_api = /telescope/power fits_path = fits/teccam2.fits device = tec2 has_loop = true pixel_scale = 0.283 pixel_scale = 0.160 orientation = -60 background_estimate = 10 [teccam3] role = tec Loading @@ -89,5 +90,6 @@ power_api = /telescope/power fits_path = fits/teccam3.fits device = tec3 has_loop = true pixel_scale = 0.283 pixel_scale = 0.160 orientation = -60 background_estimate = 10
noctua/config/constants.py +16 −0 Original line number Diff line number Diff line Loading @@ -46,6 +46,22 @@ def camera_optics(viewer_key): return scale, orientation def camera_star_estimate(viewer_key): """Return background_estimate [ADU] for a cam_id from cameras.ini. Initial guess for fit_star()'s background model; depends on sensor bias/bit-depth. Falls back to 900 (scicam1/STX level) if the key is absent or not configured. """ if not viewer_key: return 900.0 cfg = configparser.ConfigParser() cfg.read(_CAMERAS_INI) return cfg.getfloat(viewer_key, 'background_estimate', fallback=900.0) def camera_frame(viewer_key, name): """Return (x0, y0, x1, y1) unbinned px for 'half_frame'/'small_frame' from cameras.ini, or None if the key is absent or not configured. Loading
noctua/guider.py +37 −17 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ # System modules import argparse import asyncio import time # Third-party modules import numpy as np Loading @@ -16,7 +17,8 @@ from astropy.wcs import WCS # Custom modules from noctua import devices from noctua.config.constants import alt, camera_optics, lat, lon, viewer_fits_path from noctua.config.constants import (alt, camera_optics, camera_star_estimate, dateobs, lat, lon, viewer_fits_path) from noctua.utils.analysis import fit_star from noctua.utils.logger import log Loading @@ -42,6 +44,7 @@ class Guider: self.last_correction = [0.0, 0.0] self._task = None self._stick_target = None self._last_frame_time = None self.ao_pos = [50.0, 50.0] self.ao_offload_threshold = 90 Loading Loading @@ -108,6 +111,7 @@ class Guider: self.active = True self.error = [] self._stick_target = None self._last_frame_time = None self._task = asyncio.get_running_loop().create_task(self._loop()) log.info(f"Guider started: camera={self.camera} actuator={self.actuator} target={self.target}") Loading Loading @@ -157,8 +161,9 @@ class Guider: x0, x1 = max(cx - half, 0), min(cx + half, nx) y0, y1 = max(cy - half, 0), min(cy + half, ny) background = camera_star_estimate(getattr(cam, "_viewer_key", None)) try: fitted = fit_star(data[y0:y1, x0:x1]) fitted = fit_star(data[y0:y1, x0:x1], background_estimation=background) except Exception as e: msg = f"fit_star: {e}" Loading Loading @@ -257,24 +262,39 @@ class Guider: self.ao_pos = [50.0, 50.0] def _acquire(self, cam): """Read the latest FITS frame (and header) for this camera from disk. """Read the next FITS frame (and header) for this camera from disk. The guider never drives acquisition itself — it only reads whatever the camera's own loop (or, for scicam, the last Expose) last wrote. Freshness is the caller's responsibility: guiding is only meaningful while something else keeps producing new frames. It blocks here (this runs in a worker thread, so a plain sleep is fine) until a frame with a new DATE-OBS shows up, so a correction is never computed twice off the same, still-uncorrected image. """ fits_path = viewer_fits_path(getattr(cam, "_viewer_key", None)) timeout = max(3.0, 3 * getattr(cam, "loop_exposure", 1.0)) deadline = time.monotonic() + timeout while True: try: data, header = fits.getdata(fits_path, header=True) return data.astype(np.float32), header except Exception as e: msg = f"acquire: {e}" log.error(f"Guider: {msg}") self.error.append(msg) return None, None frame_time = header.get(dateobs) if frame_time != self._last_frame_time: self._last_frame_time = frame_time return data.astype(np.float32), header if time.monotonic() > deadline: log.warning(f"Guider: no new frame from '{self.camera}' within {timeout:.1f}s") return None, None time.sleep(0.1) def _pixel_to_offset(self, cam, header, nx, ny, star_x, star_y, tgt_x, tgt_y): coordinates = devices.tel.coordinates Loading