block entry while queue or tomo heartbeat
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user