Commit 3b4866b7 authored by vertighel's avatar vertighel
Browse files

Mako: recover from a lost camera link without restarting noctua-app

A VmbError.InternalFault (link drop on the telescope LAN) left the dead
camera handle cached forever: every call failed on it, errors piled up
in device.error (2 entries per telemetry poll) and only a process
restart brought teccam2 back.

- mako.py: drop the handle on link-loss VmbC codes (InternalFault,
  BadHandle, DeviceNotOpen, ApiNotStarted) and reconnect on the next
  call; error list holds only the current problem; retry interval after
  a failed connect; recover() reopens the camera, then restarts the
  VmbSystem as a last resort; the loop reconnects on its own and
  reapplies exposure on every new connection; connection property,
  download() without frame, DATE-OBS on retries, binning read once per
  loop connection instead of once per frame.
- check.vmbpy_errors: record once via device.fail(), clear on success.
- api Loop.post: run off the event loop, re-arm the camera first.
- loop-toggle.js: switch follows response.running, not the HTTP status.
- guider.py: same structure for the two telescope offset blocks.
- Rename the teccam device classes stx.Guider/mako.Guider to Probe,
  with their routes in the new noctua.api.probe.

Co-Authored-By: default avatarClaude Opus 5.5 <noreply@anthropic.com>
parent 2b41eec5
Loading
Loading
Loading
Loading
Loading
+7 −7
Changes for noctua/api/__init__.py: 7 added lines, 7 removed lines.
Original line number Diff line number Diff line
@@ -55,13 +55,13 @@ def dynamic_import(url_path):

    This means a device's API module is shared by every device
    instance whose class has that same name — e.g. ``tec1``/``tec2``/
    ``tec3`` are all instances of ``guider.py``'s ``Guider`` class, so
    every teccam route resolves to ``noctua.api.guider``, not a
    per-teccam module. Where that module needs resource classes that
    live elsewhere (the teccam routes reuse ``camera.py``'s
    ``FrameBinning``/``Gain``/``Loop``/``SnapshotRaw``/
    ``SnapshotState``), it re-exports them with a plain import at the
    top of the file — see ``guider.py``'s import of those five names.
    ``tec3`` are all instances of a ``Probe`` class (``stx.Probe``,
    ``mako.Probe``), so every teccam route resolves to
    ``noctua.api.probe``, not a per-teccam module. Where that module
    needs resource classes that live elsewhere (the teccam routes reuse
    ``camera.py``'s ``FrameBinning``/``Gain``/``Loop``/``SnapshotRaw``/
    ``SnapshotState``), it re-exports them with a plain import — see
    ``probe.py``.
    """

    try:
+16 −3
Changes for noctua/api/camera.py: 16 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -239,6 +239,10 @@ class Loop(BaseResource):
        """Start the acquisition loop.

        Body: {"exptime": <float>, "binning": <int>, "gain": <float>}

        A camera left in error is re-armed first (devices that have
        ``recover()``, i.e. Mako), so turning the loop switch off and on
        is how the operator revives it.
        """

        body     = await self.get_payload()
@@ -246,9 +250,16 @@ class Loop(BaseResource):
        binning  = body.get('binning')
        gain     = body.get('gain')

        # Everything below talks to the camera (seconds, up to ~10 s for a
        # recovery): run it off the event loop.
        def action():
            stop_looping = getattr(self.dev, "stop_looping", None)
            if stop_looping and getattr(self.dev, "looping", False):
            await self.run_blocking(stop_looping)
                stop_looping()

            recover = getattr(self.dev, "recover", None)
            if recover:
                recover()

            if binning is not None:
                try:
@@ -265,11 +276,13 @@ class Loop(BaseResource):
            self.dev.loop_exposure = exposure
            self.dev.looping = True

        return self.make_response({'running': True, 'exposure': exposure})
        await self.run_blocking(action)

        return self.make_response({'running': self.dev.looping, 'exposure': exposure})

    async def delete(self):
        """Stop the acquisition loop."""

        self.dev.looping = False

        return self.make_response({'running': False})
        return self.make_response({'running': self.dev.looping})
+0 −1
Changes for noctua/api/guider.py: 0 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -9,7 +9,6 @@ from noctua.api.guider_instance import guider
from noctua.utils.logger import log

from .baseresource import BaseResource, expects
from .camera import FrameBinning, Gain, Loop, SnapshotRaw, SnapshotState  # noqa: F401 — re-exported for dynamic_import (tec1/2/3 class=Guider → module noctua.api.guider)

guider_api = Blueprint("guider", __name__)

noctua/api/probe.py

0 → 100644
+11 −0
Changes for noctua/api/probe.py: 11 added lines, 0 removed lines.
Original line number Diff line number Diff line
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""REST API for the technical (teccam) cameras, device class Probe."""

# Custom modules
from .camera import FrameBinning, Gain, Loop, SnapshotRaw, SnapshotState  # noqa: F401

# Nothing teccam-specific: dynamic_import resolves the module from the
# device class name (stx.Probe / mako.Probe -> noctua.api.probe), so the
# camera resources used by the /teccam* routes are re-exported here.
+3 −3
Changes for noctua/config/devices.ini: 3 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -28,21 +28,21 @@ depends-on = cam1

[tec1]
module = stx
class = Guider
class = Probe
node = STX
depends-on = cam1
viewer-key = teccam1

[tec2]
module = mako
class = Guider
class = Probe
node = MAKO111
depends-on = cab
viewer-key = teccam2

[tec3]
module = mako
class = Guider
class = Probe
node = MAKO115
depends-on = cab
viewer-key = teccam3
Loading