Commit fa00c96a authored by vertighel's avatar vertighel
Browse files

Gain teccam passivo come exptime/binning; lock di connessione Mako



Il campo Gain sparava una PUT indipendente ad ogni cambio, in qualsiasi
momento — a differenza di exptime/binning per i teccam, che vengono
letti solo quando lo switch Loop passa a True e inviati nel body della
stessa POST di avvio. Quella PUT indipendente poteva sovrapporsi alla
sequenza di connessione VmbPy (propria o di un'altra Mako, dato che
VmbSystem.get_instance() è un singleton di processo condiviso tra
teccam2/teccam3), causando "Called 'get_camera_by_id()' outside of
'with' context" e simili.

- loop-toggle.js: wireLoopToggle() accetta un getGain, incluso nel body
  della POST di start loop solo se non nullo (stesso schema di binning).
- control.js: wireTeccamLoop passa il getter (null se l'input è assente
  o disabled, cioè teccam1/STX; 0 dB trattato come valore valido, non
  falsy); rimossa wireTeccamGain e la sua PUT indipendente.
- api/camera.py: Loop.post() legge gain dal body e lo applica prima di
  avviare il loop, stesso try/except di binning. Le rotte
  /teccam2|3/gain restano per uso diretto via script/ipython.
- mako.py: nuovo Mako._connect_lock (di classe, condiviso tra tutte le
  istanze — non basta un lock per-istanza dato il singleton VmbSystem),
  usato in _check_connection() con doppio controllo per serializzare i
  primi tentativi di connessione concorrenti tra camere diverse.

Co-Authored-By: default avatarClaude Sonnet 5 <noreply@anthropic.com>
parent 5f120aed
Loading
Loading
Loading
Loading
Loading
+8 −1
Original line number Diff line number Diff line
@@ -252,11 +252,12 @@ class Loop(BaseResource):
    async def post(self):
        """Start the acquisition loop.

        Body: {"exptime": <float>, "binning": <int>}
        Body: {"exptime": <float>, "binning": <int>, "gain": <float>}
        """
        body     = await self.get_payload()
        exposure = float(body.get('exptime', 1.0))
        binning  = body.get('binning')
        gain     = body.get('gain')

        stop_looping = getattr(self.dev, "stop_looping", None)
        if stop_looping and getattr(self.dev, "looping", False):
@@ -268,6 +269,12 @@ class Loop(BaseResource):
            except Exception as e:
                log.warning(f"Loop: could not set binning: {e}")

        if gain is not None:
            try:
                self.dev.gain = float(gain)
            except Exception as e:
                log.warning(f"Loop: could not set gain: {e}")

        self.dev.loop_exposure = exposure
        self.dev.looping = True
        return self.make_response({'running': True, 'exposure': exposure})
+37 −20
Original line number Diff line number Diff line
@@ -38,6 +38,17 @@ class Mako(BaseDevice):
    # which is plenty for the 1-10 fps this driver actually needs.
    throughput_limit_bps = 8_000_000

    # Shared across every Mako instance (teccam2, teccam3, ...): VmbSystem.
    # get_instance() returns one process-wide singleton whose "context
    # entered" flag is a single boolean, not per-camera/per-thread. Two
    # cameras (or two threads on the same camera) connecting for the first
    # time concurrently can race on it — one's failed/half-finished
    # VmbStartup can flip the shared flag back to False right as the other
    # is about to call get_camera_by_id(), raising "outside of 'with'
    # context" even though that camera's own __enter__() looked fine. This
    # lock serializes first-time connection attempts across all instances.
    _connect_lock = threading.Lock()

    def __init__(self, url):
        super().__init__(url)
        self.id = url
@@ -58,8 +69,14 @@ class Mako(BaseDevice):
                log.warning(f"Mako: could not set throughput limit: {e}")

    def _check_connection(self):
        """Open and cache the VmbSystem + Camera context."""
        """Open and cache the VmbSystem + Camera context.

        Serialized by ``_connect_lock`` (shared across all Mako instances,
        see its docstring) — first-time connects must not race each other.
        """
        if self._cam is None:
            with Mako._connect_lock:
                if self._cam is None:  # re-check: another thread may have connected while we waited
                    self.vmb.__enter__()
                    try:
                        self._cam = self.vmb.get_camera_by_id(self.id)
+1 −1
Original line number Diff line number Diff line
@@ -155,7 +155,7 @@
    <div class="input-group input-group-sm">
      <input type="number" class="form-control bg-black"
             id="teccam-gain-val-{{ panel_id }}"
             placeholder="1" step="1" min="0" max="24"
             value="1" step="1" min="0" max="24"
             data-load-from-storage="{{ cam_id }}-gain"
             {% if not has_gain %}
             disabled
+12 −24
Original line number Diff line number Diff line
@@ -229,6 +229,7 @@ document.addEventListener('DOMContentLoaded', () => {
            () => mode.camId,
            () => parseFloat(document.querySelector(`[name="exptime"][form="form-${mode.panel}"]`)?.value) || 1.0,
            () => parseInt(document.getElementById(`binning-sel-${mode.panel}`)?.value) || null,
            null,
            looping => {
                if (exposeBtn) exposeBtn.disabled = looping;
                if (stopBtn)   stopBtn.disabled   = looping;
@@ -236,6 +237,11 @@ document.addEventListener('DOMContentLoaded', () => {
        );
    }

    // Gain — absent from teccam1's panel (has_gain=False, STX guider has no
    // gain concept); the lookup below is then NaN and omitted from the body.
    // Passive like Exptime/Binning: read only at the moment the switch turns
    // on, never applied live (a live PUT racing the loop's own connection
    // setup caused intermittent VmbPy "outside of 'with' context" errors).
    function wireTeccamLoop(station) {
        const mode = MODES[station];
        wireLoopToggle(
@@ -243,36 +249,18 @@ document.addEventListener('DOMContentLoaded', () => {
            () => mode.teccamId,
            () => parseFloat(document.getElementById(`teccam-exptime-val-${mode.panel}`)?.value) || 1.0,
            () => parseInt(document.getElementById(`teccam-binning-sel-${mode.panel}`)?.value) || null,
        );
    }

    // Gain — absent from teccam1's panel (has_gain=False, STX guider has no
    // gain concept), so the lookup below is null there and this is a no-op.
    function wireTeccamGain(station) {
        const mode  = MODES[station];
            () => {
                const input = document.getElementById(`teccam-gain-val-${mode.panel}`);
        if (!input) return;

        input.addEventListener('change', async () => {
            const value = parseFloat(input.value);
            if (isNaN(value)) return;
            try {
                const res = await fetch(`/api/${mode.teccamId}/gain`, {
                    method: 'PUT',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(value),
                });
                if (!res.ok) throw new Error('Server error');
            } catch (err) {
                showToast(`Error: ${err.message}`, 'danger');
            }
        });
                if (!input || input.disabled) return null;   // teccam1/STX has no gain concept
                const v = parseFloat(input.value);
                return Number.isNaN(v) ? null : v;            // 0 dB is a valid gain — don't treat it as falsy
            },
        );
    }

    for (const station of Object.keys(MODES)) {
        wireScicamLoop(station);
        wireTeccamLoop(station);
        wireTeccamGain(station);
    }

    // -----------------------------------------------------------------------
+7 −1
Original line number Diff line number Diff line
@@ -11,6 +11,10 @@
 * @param {function(): number} [getExptime] - exposure seconds to send on start.
 * @param {function(): (number|null)} [getBinning] - binning to send on start;
 *   omitted from the request body if null/undefined.
 * @param {function(): (number|null)} [getGain] - gain (dB) to send on start;
 *   omitted from the request body if null/undefined. Passive like binning —
 *   read only at the moment the switch turns on, never applied live while
 *   looping (see mako.py's gain setter, which refuses mid-loop changes).
 * @param {function(boolean): void} [onChange] - called whenever the switch's
 *   state is set (on refresh and after a successful/failed toggle), so
 *   callers can react (e.g. disable Expose while the loop is on).
@@ -18,7 +22,7 @@
 */
const POLL_INTERVAL_MS = 5000;

export function wireLoopToggle(el, getCamId, getExptime, getBinning, onChange) {
export function wireLoopToggle(el, getCamId, getExptime, getBinning, getGain, onChange) {
    if (!el) return null;

    // While the switch is on, poll the backend periodically — a loop can die
@@ -68,6 +72,8 @@ export function wireLoopToggle(el, getCamId, getExptime, getBinning, onChange) {
            const body    = { exptime: getExptime?.() ?? 1.0 };
            const binning = getBinning?.();
            if (binning != null) body.binning = binning;
            const gain = getGain?.();
            if (gain != null) body.gain = gain;
            options.body = JSON.stringify(body);
        }
        try {