Commit 1a687e75 authored by vertighel's avatar vertighel
Browse files

guider/loop: grande semplificazione — un solo switch Loop per camera



Invece di far coesistere loop e Expose/Guide con logica automatica di
priorità (stop_looping/abort/wait inseguiti nella sessione precedente),
si impedisce del tutto di premere Expose mentre il loop scicam è attivo
(disabled in UI + rifiuto lato backend) — l'operatore deve fermare il
loop a mano prima. Elimina la necessità di auto-stop e con essa tutto
il bus di sincronizzazione cross-widget.

- teccam_panel(panel_id, cam_id): riscritta con firma fissa (non più
  dipendente dal dropdown Camera del Guider), una chiamata per stazione
  (teccam1/2/3). Loop switch + Exptime + Binning, unico punto di
  controllo per l'acquisizione della teccam.
- Rimossi i controlli loop nativi dal viewer (viewer_panel.html,
  control.html, viewer.html) — il viewer torna puro display ovunque
  compaia; has_loop rimosso anche da web/__init__.py.
- guider.py: _acquire() collassata a un solo ramo (legge sempre da
  disco via viewer_fits_path, per ogni camera) — il guider non aziona
  più nessuna camera. Spariti PASSIVE_CAMERAS, trigger_mode,
  self.exptime/self.binning, il blocco stop_looping()/binning in
  Guider.start().
- start() su stx.py/atik.py/mako.py torna a rifiutarsi esplicitamente
  se la camera sta facendo loop, invece di auto-fermarlo e procedere.
  stop_looping() resta solo per Loop.post() (riavvio con nuovi
  parametri).
- control.js: lo switch Loop dello scicam disabilita Expose/Stop
  quando acceso (nuovo parametro onChange di wireLoopToggle); wiring
  Loop per tutte e 3 le stazioni (scicam + teccam).
- guider-panel.js: rimossa ogni logica exptime/binning/loop dal
  pannello Guider — resta solo scelta camera-da-cui-leggere, box,
  target, pick, guide/stop.
- sync-bus.js eliminato: un solo switch per camera in tutta la pagina,
  nessun cross-widget sync necessario.

Co-Authored-By: default avatarClaude Sonnet 5 <noreply@anthropic.com>
parent 65702067
Loading
Loading
Loading
Loading
+5 −2
Original line number Diff line number Diff line
@@ -212,7 +212,8 @@ class Camera(BaseDevice):
    def start(self, duration, imagetype, datetime=None):
        """Start a single exposure.

        Takes priority over a running acquisition loop: stops it first.
        Refuses with an error if a loop is active; stop it first
        (``looping = False`` or ``stop_looping()``).

        Parameters
        ----------
@@ -224,7 +225,9 @@ class Camera(BaseDevice):
            UTC ISO-8601 string stored for later FITS header use.
        """
        if self._looping:
            self.stop_looping()
            log.error("Camera is looping. Stop the loop first.")
            self.error.append("Camera is looping")
            return
        self._start(duration, imagetype, datetime)

        
+5 −2
Original line number Diff line number Diff line
@@ -214,7 +214,8 @@ class Guider(Mako):
    def start(self, exptime=None):
        """Acquire a single frame synchronously.

        Takes priority over a running acquisition loop: stops it first.
        Refuses with an error if a loop is active; stop it first
        (``looping = False`` or ``stop_looping()``).
        The result is stored in ``matrix``.

        Parameters
@@ -223,7 +224,9 @@ class Guider(Mako):
            Exposure time in seconds. If None, the current camera setting is used.
        """
        if self._looping:
            self.stop_looping()
            log.error("Mako: camera is looping. Stop the loop first.")
            self.error.append("Camera is looping")
            return
        self._start(exptime)

    def _start(self, exptime=None):
+5 −2
Original line number Diff line number Diff line
@@ -267,7 +267,8 @@ class Camera(STX):
    def start(self, duration, imagetype, datetime=None):
        """Start a single exposure.

        Takes priority over a running acquisition loop: stops it first.
        Refuses with an error if a loop is active; stop it first
        (``looping = False`` or ``stop_looping()``).

        Parameters
        ----------
@@ -280,7 +281,9 @@ class Camera(STX):
            Defaults to the current UTC time.
        """
        if self._looping:
            self.stop_looping()
            log.error("Camera is looping. Stop the loop first.")
            self.error.append("Camera is looping")
            return
        self._start(duration, imagetype, datetime)


+13 −56
Original line number Diff line number Diff line
@@ -6,7 +6,6 @@
# System modules
import argparse
import asyncio
import time

# Third-party modules
import numpy as np
@@ -17,17 +16,11 @@ from astropy.wcs import WCS

# Custom modules
from noctua import devices
from noctua.config.constants import (alt, camera_optics, frame_number, lat,
                                      lon, viewer_fits_path)
from noctua.config.constants import alt, camera_optics, lat, lon, viewer_fits_path
from noctua.utils.analysis import fit_star
from noctua.utils.logger import log


#: Camera devices that acquire passively (read the last FITS Expose wrote to
#: disk, no direct trigger) — the guider never drives their exposure/binning.
PASSIVE_CAMERAS = ("cam1", "cam2")


class Guider:
    """
    Secondary guiding loop that corrects telescope pointing using a guide star.
@@ -41,8 +34,6 @@ class Guider:
        self.active        = False
        self.camera        = "tec1"
        self.actuator      = "telescope"
        self.exptime       = 1.0
        self.binning       = 1
        self.box           = 300
        self.box_center    = None
        self.target        = "center"
@@ -78,12 +69,12 @@ class Guider:
        Parameters
        ----------
        params : dict
            Keys matching any of: camera, actuator, exptime, binning, box,
            box_center, target, ao_offload_threshold.
            Keys matching any of: camera, actuator, box, box_center, target,
            ao_offload_threshold.
        """

        for key in ("camera", "actuator", "exptime", "binning", "box",
                    "box_center", "target", "ao_offload_threshold", "ao_offload"):
        for key in ("camera", "actuator", "box", "box_center", "target",
                    "ao_offload_threshold", "ao_offload"):
            if key in params:
                setattr(self, key, params[key])
        if "target" in params:
@@ -104,18 +95,6 @@ class Guider:
        if self._task and not self._task.done():
            return

        cam = getattr(devices, self.camera, None)
        if cam is not None:
            stop_looping = getattr(cam, "stop_looping", None)
            if stop_looping and getattr(cam, "looping", False):
                # Blocking (up to a few seconds) — keep off the event loop.
                await asyncio.get_running_loop().run_in_executor(None, stop_looping)
            if self.camera not in PASSIVE_CAMERAS:
                try:
                    cam.binning = [self.binning, self.binning]
                except Exception as e:
                    log.warning(f"Guider: could not set binning: {e}")

        if self.actuator == "ao_x":
            ao = getattr(devices, "ao", None)
            if ao is None:
@@ -264,31 +243,17 @@ class Guider:
        self.ao_pos = [50.0, 50.0]

    def _acquire(self, cam):
        """Read the latest FITS frame 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.
        """

        if self.camera in PASSIVE_CAMERAS:
        fits_path = viewer_fits_path(getattr(cam, "_viewer_key", None))
        try:
            return fits.getdata(fits_path).astype(np.float32)
            except Exception as e:
                msg = f"acquire (passive): {e}"
                log.error(f"Guider: {msg}")
                self.error.append(msg)
                return None

        trigger_mode = getattr(cam, "acquisition", "stream") == "trigger"
        try:
            if trigger_mode:
                if int(cam.state) != 0:
                    cam.abort()
                    time.sleep(0.1)
                gps = Time(devices.tel.coordinates["utc"], format="unix")
                cam.start(self.exptime, frame_number["Light"], datetime=gps.isot)
                while int(cam.state) != 0:
                    time.sleep(0.1)
            data = cam.matrix
            if data is None:
                return None
            return data.astype(np.float32)
        except Exception as e:
            msg = f"acquire: {e}"
            log.error(f"Guider: {msg}")
@@ -348,8 +313,6 @@ class Guider:
            "active":        self.active,
            "camera":        self.camera,
            "actuator":      self.actuator,
            "exptime":       self.exptime,
            "binning":       self.binning,
            "box":           self.box,
            "box_center":    self.box_center,
            "target":        self.target,
@@ -392,16 +355,12 @@ def cli():
    parser.add_argument("--actuator", default="telescope",
                        choices=["telescope", "ao_x"],
                        help="Correction actuator")
    parser.add_argument("--exptime",  type=float, default=1.0,
                        help="Guide exposure time in seconds")
    parser.add_argument("--box",      type=int,   default=300,
                        help="Star search box size in pixels")
    parser.add_argument("--box-center", type=float, nargs=2, metavar=("X", "Y"),
                        help="Box centre in pixels (default: image centre)")
    parser.add_argument("--target",   default="center",
                        help="Guide target: 'center', 'stick', or 'X,Y' pixel coords")
    parser.add_argument("--binning",  type=int,   default=1,
                        help="Camera binning (applied once at start, teccam only)")
    parser.add_argument("--ao-offload-threshold", type=int, default=90,
                        help="AO stroke threshold (0-100) before offloading to telescope")
    parser.add_argument("--no-ao-offload", action="store_true",
@@ -420,8 +379,6 @@ def cli():
    params = {
        "camera":               args.camera,
        "actuator":             args.actuator,
        "exptime":              args.exptime,
        "binning":              args.binning,
        "box":                  args.box,
        "target":               target,
        "ao_offload_threshold": args.ao_offload_threshold,
+2 −3
Original line number Diff line number Diff line
@@ -43,7 +43,7 @@ def _camera_views():
    Build the viewer name → panel list map from cameras.ini.

    Returns a dict where each key is a view name ('scicam1', 'combo1', …)
    and each value is a list of panel dicts with 'id', 'cam_id', 'has_loop'.
    and each value is a list of panel dicts with 'id', 'cam_id'.
    """
    cfg = configparser.ConfigParser()
    cfg.read(Path(__file__).parent.parent / 'config' / 'cameras.ini')
@@ -52,9 +52,8 @@ def _camera_views():
    by_station = {}

    for cam_id in cfg.sections():
        has_loop = cfg.get(cam_id, 'device', fallback=None) is not None
        station  = cfg.getint(cam_id, 'station', fallback=None)
        panel    = {'id': cam_id, 'cam_id': cam_id, 'has_loop': has_loop}
        panel    = {'id': cam_id, 'cam_id': cam_id}
        views[cam_id] = [panel]
        if station is not None:
            by_station.setdefault(station, []).append(panel)
Loading