block entry while queue or tomo heartbeat

This commit is contained in:
x12sa
2026-07-07 10:41:40 +02:00
committed by holler
co-authored by holler
parent daebaa0efc
commit 723070794c
@@ -14,6 +14,22 @@ sync with changes made in a parallel CLI session. The parameter panel is
refreshed on demand (when the user opens it or submits changes) rather than
on every poll, to avoid clobbering in-progress edits.
Beamline-busy detection
------------------------
Two independent "something is running" signals gate parameter submission and
drive an advisory banner:
* ``tomo_progress["heartbeat"]`` — set by ``_tomo_scan_at_angle()`` at every
projection, whether the scan was queue- or CLI-started.
* the BEC scan queue — catches a single ptycho projection (or any other scan)
run manually outside a tomo run, which writes no heartbeat. The busy logic
mirrors bec_widgets' ``BECQueue`` widget so it stays consistent with the
queue display.
The banner is advisory (soft-warn): editing while busy is still allowed —
property-backed params such as counting time take effect on the next
projection — but Submit asks for confirmation.
Queue execution
---------------
``tomo_queue_execute()`` is a blocking Flomni CLI method that cannot be
@@ -28,6 +44,7 @@ import datetime
from typing import Any, Optional
from bec_lib import bec_logger
from bec_lib.endpoints import MessageEndpoints
from bec_widgets import BECWidget, SafeSlot
from qtpy.QtCore import Qt, QTimer
from qtpy.QtWidgets import (
@@ -70,6 +87,10 @@ STATUS_COLORS = {
"done": "#4CAF50",
}
# Scan-queue item statuses that mean "not occupying hardware".
# Mirrors bec_widgets' BECQueue.update_queue busy logic verbatim.
_QUEUE_IDLE_STATUSES = ("STOPPED", "COMPLETED", "IDLE")
# Exact tuple from Flomni._TOMO_QUEUE_PARAM_NAMES (source of truth in flomni.py)
QUEUE_PARAM_NAMES = (
"tomo_countingtime",
@@ -132,6 +153,8 @@ class TomoParamsWidget(BECWidget, QWidget):
Layout
------
Top:
- Advisory beamline-busy banner (hidden unless a scan/queue is active)
Top half (scrollable):
- Read-only sample-name header
- Tomo-type dropdown (always visible)
@@ -148,13 +171,7 @@ class TomoParamsWidget(BECWidget, QWidget):
PLUGIN = True
USER_ACCESS = [
"refresh",
"enter_edit_mode",
"cancel_edit",
"submit_params",
"add_to_queue",
]
USER_ACCESS = ["refresh", "enter_edit_mode", "cancel_edit", "submit_params", "add_to_queue"]
_POLL_INTERVAL_MS = 2000
@@ -164,6 +181,7 @@ class TomoParamsWidget(BECWidget, QWidget):
self._edit_mode = False
self._pw: dict[str, QWidget] = {} # key -> input widget
self._queue_dlg: Optional["TomoQueueDialog"] = None
self._busy_banner: Optional[QLabel] = None
# check whether the FlOMNI setup is active before building the UI
self._flomni_available = self._check_flomni_available()
self._build_ui()
@@ -173,6 +191,15 @@ class TomoParamsWidget(BECWidget, QWidget):
if self._flomni_available:
self._poll_timer.start()
self.refresh()
# live beamline-busy banner: react to scan-queue changes as they
# happen (the tomo heartbeat has no event and is covered by _on_poll)
try:
self.bec_dispatcher.connect_slot(
self._on_queue_status, MessageEndpoints.scan_queue_status()
)
except Exception as exc:
logger.warning(f"TomoParamsWidget: could not subscribe to scan queue: {exc}")
self._refresh_busy_banner()
# ── setup detection ──────────────────────────────────────────────────────
@@ -226,9 +253,26 @@ class TomoParamsWidget(BECWidget, QWidget):
root = QVBoxLayout(self)
root.setSpacing(6)
# advisory busy banner sits at the very top, present regardless of
# setup so it can warn before the user starts editing (hidden default)
self._busy_banner = QLabel("")
self._busy_banner.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._busy_banner.setWordWrap(True)
self._busy_banner.setStyleSheet(
"QLabel {"
" background-color: #FF9800;"
" color: black;"
" font-weight: bold;"
" border-radius: 4px;"
" padding: 6px;"
"}"
)
self._busy_banner.setVisible(False)
root.addWidget(self._busy_banner)
if not self._flomni_available:
lbl = QLabel(
"FlOMNI setup not detected in this BEC session.\n\n"
" FlOMNI setup not detected in this BEC session.\n\n"
"This widget is only meaningful when the FlOMNI\n"
"BEC session is active (fsamroy device present)."
)
@@ -367,11 +411,7 @@ class TomoParamsWidget(BECWidget, QWidget):
form.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
self._add_int(
form,
"_numprj_type3",
"Projections per sub-tomo (type 3)",
min_=1,
max_=10000,
form, "_numprj_type3", "Projections per sub-tomo (type 3)", min_=1, max_=10000
)
self._add_int(
form, "golden_ratio_bunch_size", "Bunch size (type 2 only)", min_=1, max_=10000
@@ -624,6 +664,62 @@ class TomoParamsWidget(BECWidget, QWidget):
"""Periodic refresh: params (guarded against edit mode) + sample name."""
self._refresh_params()
self._refresh_sample_name()
self._refresh_busy_banner()
# ── beamline-busy detection ───────────────────────────────────────────────
def _is_scan_queue_busy(self) -> bool:
"""
Return True if BEC's primary scan queue is actively running or paused
mid-scan.
Catches a single ptycho projection run manually (outside a tomo scan):
it writes no ``tomo_progress`` heartbeat but still occupies the
beamline. Mirrors the busy logic in bec_widgets' BECQueue widget so
it stays consistent with what the queue display shows.
"""
try:
msg = self.client.connector.get(MessageEndpoints.scan_queue_status())
if msg is None:
return False # no scan has run yet this session
primary = msg.content.get("queue", {}).get("primary")
if not primary:
return False
queue_info = getattr(primary, "info", None)
if not queue_info:
return False
return not all(item.status in _QUEUE_IDLE_STATUSES for item in queue_info)
except Exception as exc:
logger.warning(f"TomoParamsWidget: scan-queue busy check failed: {exc}")
return False
def _beamline_busy_reason(self) -> Optional[str]:
"""Return a short human reason if the beamline is busy, else None."""
if self._is_tomo_running():
return "tomo scan running"
if self._is_scan_queue_busy():
return "scan queue active (manual projection or other scan)"
return None
@SafeSlot(dict, dict)
def _on_queue_status(self, _content: dict, _meta: dict) -> None:
"""Dispatcher slot: scan-queue status changed -> refresh the banner."""
self._refresh_busy_banner()
def _refresh_busy_banner(self) -> None:
"""Show/hide the advisory busy banner based on current beamline state."""
banner = getattr(self, "_busy_banner", None)
if banner is None:
return
reason = self._beamline_busy_reason()
if reason:
banner.setText(
f"\u26a0 Beamline busy — {reason}.\n"
"Editing is allowed, but submitting changes will ask for confirmation."
)
banner.setVisible(True)
else:
banner.setVisible(False)
# ── public refresh API ────────────────────────────────────────────────────
@@ -723,15 +819,16 @@ class TomoParamsWidget(BECWidget, QWidget):
def submit_params(self) -> None:
"""Validate, write to global vars, and re-lock fields."""
if self._is_tomo_running():
busy_reason = self._beamline_busy_reason()
if busy_reason:
reply = QMessageBox.warning(
self,
"Scan in progress",
"A tomo scan appears to be running.\n\n"
f"A scan appears to be running ({busy_reason}).\n\n"
"Parameters read via properties (e.g. counting time) take effect "
"immediately on the next projection. Parameters used to build the "
"scan trajectory (FOV, angle step) are already fixed for this run "
"but will affect any subsequent queue job.\n\n"
"scan trajectory (FOV, angle step) are already fixed for the "
"current run but will affect any subsequent queue job.\n\n"
"Apply changes now?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
)