webpage_generator: make tomo-queue and tomo-type formula setup-configurable
webpage_generator: report blocked on interlock even when scan is interrupted - Add HAS_TOMO_QUEUE and TOMO_TYPES capability flags to WebpageGeneratorBase, defaulting to flomni's current behavior (type1 grid-8, type2/3 golden-capped) so this is a no-op for flomni. - Gate the tomo_queue global-var read in _cycle() behind HAS_TOMO_QUEUE; unaffected: queue_status/queue_locks/beamline_states, which come from BEC's own primary scan queue and stay common to all setups. - _render_html() now conditionally emits the 'Tomo queue' card and its DEFAULT_ORDER slot, and drives the projection-count formula from a TOMO_TYPES JSON blob (generic calcProjections()) instead of a hardcoded per-instrument JS branch. - LamniWebpageGenerator: HAS_TOMO_QUEUE=False, TOMO_TYPES with 2-subtomo and 8-subtomo equally_spaced_grid entries (placeholder keys pending real tomo_type values from the LamNI producer side). A block interrupts the running scan, clearing active_request_block, so the old _derive_status() (which only returned 'blocked' inside the queue_has_active_scan branch) fell through to 'idle' with the blocked LED off, despite watched beamline states being out of spec. - _derive_status: return 'blocked' on beamline_blocking OR queue_locks, independent of active scan; only fresh-heartbeat 'scanning' outranks it. - _cycle: compute beamline_blocking = any(state mismatched), defined identically to the beamline-states card so pill and card can't disagree; also count it as activity so the idle clock restarts once cleared. - status-detail JS: describe blocking beamline states when no explicit queue lock is present. Update help text."
This commit is contained in:
@@ -284,21 +284,27 @@ def _derive_status(
|
||||
last_active_time,
|
||||
had_activity: bool,
|
||||
queue_locks: list | None = None,
|
||||
beamline_blocking: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Returns one of:
|
||||
scanning -- tomo heartbeat fresh (< _TOMO_HEARTBEAT_STALE_S), OR
|
||||
heartbeat recently seen AND queue still has active scan
|
||||
(handles long individual projections > heartbeat timeout)
|
||||
blocked -- queue has active scan, heartbeat stale (beyond the long-
|
||||
projection grace window), AND the primary queue has at
|
||||
least one lock applied (e.g. by BEC's ScanInterlockActor
|
||||
when a watched beamline state goes out of spec). Only
|
||||
reported when it is actually preventing a queued/active
|
||||
scan from progressing.
|
||||
blocked -- the experiment is being held up externally: EITHER a
|
||||
watched beamline state is out of its accepted range while
|
||||
the scan interlock would act on it (beamline_blocking),
|
||||
OR the primary queue has at least one explicit lock
|
||||
applied (queue_locks). Reported regardless of whether a
|
||||
scan is currently executing: when a block hits, the
|
||||
running scan is interrupted and its repeat waits for the
|
||||
block to clear, so active_request_block (and hence
|
||||
queue_has_active_scan) may already be cleared even though
|
||||
the beamline is still blocking. Only 'scanning' (fresh
|
||||
heartbeat = data still flowing) takes precedence.
|
||||
running -- queue has active scan but heartbeat has never been seen,
|
||||
and the queue is not locked
|
||||
idle -- not scanning, last_active_time known
|
||||
and nothing is blocking
|
||||
idle -- not scanning, not blocked, last_active_time known
|
||||
unknown -- no activity ever seen since generator started
|
||||
|
||||
'unknown' is ONLY returned before any scan activity has been observed.
|
||||
@@ -309,14 +315,19 @@ def _derive_status(
|
||||
hb_age = _heartbeat_age_s(progress.get("heartbeat"))
|
||||
if hb_age < _TOMO_HEARTBEAT_STALE_S:
|
||||
return "scanning"
|
||||
# External block takes precedence over the remaining scan/idle states.
|
||||
# A blocked scan is interrupted (active_request_block cleared) while its
|
||||
# repeat waits for the block to clear, so we must NOT require an active
|
||||
# scan here — that was the previous behaviour and it made a blocked-but-
|
||||
# interrupted experiment fall through to 'idle'.
|
||||
if beamline_blocking or queue_locks:
|
||||
return "blocked"
|
||||
# Heartbeat stale but queue still active and heartbeat was seen recently
|
||||
# enough (within 10× the stale window) — likely a long projection, not idle.
|
||||
# Report as 'scanning' so audio system does not trigger a false alarm.
|
||||
if queue_has_active_scan and hb_age < _TOMO_HEARTBEAT_STALE_S * 10:
|
||||
return "scanning"
|
||||
if queue_has_active_scan:
|
||||
if queue_locks:
|
||||
return "blocked"
|
||||
return "running"
|
||||
if last_active_time is not None or had_activity:
|
||||
return "idle"
|
||||
@@ -1003,8 +1014,13 @@ class WebpageGeneratorBase:
|
||||
queue_has_active_scan = False
|
||||
queue_locks = []
|
||||
|
||||
# ── Beamline states (diagnostic display only; not used for the
|
||||
# 'blocked' experiment_status decision — see _read_queue_locks) ──
|
||||
# ── Beamline states ──────────────────────────────────────────
|
||||
# Diagnostic display AND (via beamline_blocking below) one of the two
|
||||
# signals that drive the 'blocked' experiment_status; the other is
|
||||
# queue_locks. A watched state that is out of its accepted range
|
||||
# (mismatched) means the scan interlock is / would be holding the
|
||||
# queue, even if the running scan has already been interrupted and
|
||||
# active_request_block cleared.
|
||||
try:
|
||||
beamline_states_info = _read_beamline_states_info(self._bec)
|
||||
except Exception as exc:
|
||||
@@ -1012,6 +1028,14 @@ class WebpageGeneratorBase:
|
||||
f"beamline_states read error: {exc}", level="warning")
|
||||
beamline_states_info = {"enabled": None, "states": []}
|
||||
|
||||
# True if any watched state is out of spec. Defined identically to the
|
||||
# "N watched states currently blocking" count shown on the beamline
|
||||
# states card (renderBeamlineStates: states.filter(s => s.mismatched)),
|
||||
# so the top status pill and that card can never disagree.
|
||||
beamline_blocking = any(
|
||||
s.get("mismatched") for s in beamline_states_info.get("states", [])
|
||||
)
|
||||
|
||||
# Current scan number (the BEC scan in progress right now, e.g. for
|
||||
# comparison against ptycho reconstruction filenames like S06770).
|
||||
# Only meaningful while a scan is actually active; None otherwise.
|
||||
@@ -1040,14 +1064,15 @@ class WebpageGeneratorBase:
|
||||
)
|
||||
self._last_queue_id = latest_queue_id
|
||||
|
||||
if tomo_active or queue_has_active_scan or history_changed:
|
||||
if (tomo_active or queue_has_active_scan or history_changed
|
||||
or beamline_blocking):
|
||||
self._last_active_time = _epoch()
|
||||
self._had_activity = True
|
||||
|
||||
exp_status = _derive_status(
|
||||
progress, queue_has_active_scan,
|
||||
self._last_active_time, self._had_activity,
|
||||
queue_locks,
|
||||
queue_locks, beamline_blocking,
|
||||
)
|
||||
idle_for_s = (
|
||||
None if self._last_active_time is None
|
||||
@@ -2099,7 +2124,7 @@ def _render_html(phone_numbers: list, has_tomo_queue: bool = True, tomo_types: d
|
||||
<ul>
|
||||
<li><strong>Scanning</strong> — a tomography scan is actively running (a heartbeat from the scan loop has been seen recently).</li>
|
||||
<li><strong>Running</strong> — the scan queue has an active item, but it is not a tomo scan with a heartbeat (e.g. an alignment scan).</li>
|
||||
<li><strong>Blocked</strong> — a scan is queued or active, but the scan queue itself has been locked (for example by BEC's scan interlock when a watched beamline condition, such as the shutter or ring current, is out of spec). This is usually external and self-resolves once the condition clears.</li>
|
||||
<li><strong>Blocked</strong> — the experiment is being held up by a watched beamline condition that is out of spec (for example the shutter or ring current, see the Beamline states card), or by an explicit lock on the scan queue. A running scan is interrupted when this happens and its repeat waits for the condition to clear, so this shows even when no scan is momentarily executing. Usually external and self-resolves once the condition clears.</li>
|
||||
<li><strong>Idle</strong> — nothing is running or queued right now.</li>
|
||||
<li><strong>Unknown</strong> — shown only before any activity has been observed since the generator started.</li>
|
||||
</ul>
|
||||
@@ -2599,8 +2624,17 @@ const DETAILS={{
|
||||
scanning: d=>'Tomo scan in progress · projection '+(d.progress.projection||0)+' of '+(d.progress.total_projections||0)+' · '+(d.progress.tomo_type||''),
|
||||
running: d=>'Queue active · outside tomo heartbeat window',
|
||||
idle: d=>'Idle for <strong>'+d.idle_for_human+'</strong>',
|
||||
blocked: d=>'Queue locked'+((d.queue_locks&&d.queue_locks.length>1)?' by multiple locks':'')+': <strong>'
|
||||
+(d.queue_locks||[]).map(l=>esc(l.reason||l.identifier)).join('; ')+'</strong>',
|
||||
blocked: d=>{{
|
||||
const locks=d.queue_locks||[];
|
||||
if(locks.length>0)
|
||||
return 'Queue locked'+(locks.length>1?' by multiple locks':'')+': <strong>'
|
||||
+locks.map(l=>esc(l.reason||l.identifier)).join('; ')+'</strong>';
|
||||
const bad=(((d.beamline_states||{{}}).states)||[]).filter(s=>s.mismatched)
|
||||
.map(s=>esc(s.label||s.name));
|
||||
if(bad.length>0)
|
||||
return 'Beamline interlock blocking: <strong>'+bad.join('; ')+'</strong>';
|
||||
return 'Blocked';
|
||||
}},
|
||||
error: d=>'Queue stopped unexpectedly · idle for <strong>'+(d.idle_for_human||'?')+'</strong>',
|
||||
unknown: d=>'Waiting for first data\u2026',
|
||||
}};
|
||||
|
||||
Reference in New Issue
Block a user