new_gui Phase 3: staff Tools dialog (beam/sessions/recovery/local-contact/dev-help)
Staff-only Tools button -> StaffToolsDialog with 5 tabs: - Beam/detector: beam centre/size/mark, ABR GMX/GMY/GMZ tweak + save/goto, mono pitch scan, change energy - Sessions: p-group selector + open-GUI-session admin (request close / force remove, force/end session) - Recovery: confirmation-code actions + maintenance/init/resync ops - Local contact: simulation toggles, device restart, BEC macros/devices, links - Dev help: error-code registry + last error payload Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5b833bf9eb
commit
e4dc3d505c
@@ -45,6 +45,14 @@ Backend contract is documented in the `new-gui-backend-paths` memory.
|
||||
readout (`focus_measure`). Camera controls (⚙ gain/exposure → `samcam_settings`,
|
||||
AF → `autofocus`, ◉ → `send_screenshot_db`) live in the tab strip.
|
||||
- **Gonio / Beamline tabs** ← axis IP video via `VideoThread` (`frame_ready`).
|
||||
- **Staff Tools** (⚙ Tools in the top bar, staff only) — a tabbed dialog
|
||||
(`widgets/staff_tools.py`): **Beam/detector** (beam centre/size/mark, ABR
|
||||
GMX/GMY/GMZ tweak + save/goto, mono pitch scan, energy), **Sessions**
|
||||
(p-group selector + open-GUI-session admin: request-close/force-remove,
|
||||
force/end session), **Recovery** (confirmation-code free/take-over/recover/
|
||||
unmount + anneal/park&dry/TELL/init/pedestal/resync), **Local contact**
|
||||
(simulation toggles, device restart, BEC macros/devices, web links), and
|
||||
**Dev help** (error-code registry + last error payload).
|
||||
- **Preconditions**: data-collection actions are gated by a ring/shutter/hutch
|
||||
warning (1-hour snooze); mounting is hard-blocked by the hutch PSS;
|
||||
`sample_missing` shows a modal (suppressed during automation).
|
||||
|
||||
@@ -93,8 +93,10 @@ focus), B (gonio/beamline video), I (alert banner + precondition dialogs +
|
||||
sample-missing). Still deferred from A: smargon bookmarks (Ctrl-click), helical
|
||||
markers.
|
||||
|
||||
**Phase 3 — staff tooling:** G (beam/detector physics), J (pgroups + sessions),
|
||||
H (recovery + local-contact admin), developer help.
|
||||
**Phase 3 — staff tooling:** ✅ DONE (single staff Tools dialog). G (beam/detector
|
||||
physics), J (pgroups + sessions), H (recovery + local-contact admin), developer
|
||||
help. Deferred: full device-state status widget, BEC reinit-method picker,
|
||||
tracebacks/error-log live tabs.
|
||||
|
||||
**Phase 4 — polish:** L (file builder), F (face detection), M (analysis panels),
|
||||
N (shortcuts, layout persistence, tutorials, portrait/compact).
|
||||
|
||||
@@ -175,6 +175,7 @@ class MainWindow(QWidget):
|
||||
self.top_bar.request_baton.connect(self.daq.request_baton)
|
||||
self.top_bar.release_baton.connect(self.daq.release_baton)
|
||||
self.top_bar.cancel_baton_request.connect(self._on_cancel_baton)
|
||||
self.top_bar.tools_clicked.connect(self._open_staff_tools)
|
||||
self.daq.baton_request_result.connect(self._on_baton_request_result)
|
||||
self.daq.baton_response_result.connect(self._on_baton_response_result)
|
||||
if hasattr(self.daq, "baton_incoming_request"):
|
||||
@@ -340,6 +341,15 @@ class MainWindow(QWidget):
|
||||
return
|
||||
self.daq.mount(sample, reference)
|
||||
|
||||
def _open_staff_tools(self) -> None:
|
||||
from aare.gui.new_gui.widgets.staff_tools import StaffToolsDialog
|
||||
|
||||
if getattr(self, "_staff_tools", None) is None:
|
||||
self._staff_tools = StaffToolsDialog(
|
||||
self.daq, self._palette, lambda: self.manual.last_status, self)
|
||||
self._staff_tools.show()
|
||||
self._staff_tools.raise_()
|
||||
|
||||
def _on_sample_missing(self, msg: str) -> None:
|
||||
self.alert.show_message(msg, "error")
|
||||
if not self._auto_running: # don't interrupt automation with a modal
|
||||
|
||||
@@ -51,6 +51,7 @@ class TopBar(QWidget):
|
||||
request_baton = Signal()
|
||||
release_baton = Signal()
|
||||
cancel_baton_request = Signal()
|
||||
tools_clicked = Signal()
|
||||
|
||||
def __init__(self, palette: Palette, beamline_label: str, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -110,6 +111,18 @@ class TopBar(QWidget):
|
||||
self._baton_btn.clicked.connect(self._on_baton_btn)
|
||||
lay.addWidget(self._baton_btn)
|
||||
|
||||
# staff tools (hidden unless staff)
|
||||
self._tools_btn = QPushButton("⚙ Tools")
|
||||
self._tools_btn.setCursor(Qt.PointingHandCursor)
|
||||
self._tools_btn.setStyleSheet(
|
||||
f"QPushButton {{ background:{p.surface}; border:1px solid {p.border_control};"
|
||||
f" border-radius:8px; padding:6px 12px; font-size:12px;"
|
||||
f" color:{p.text_secondary}; }}"
|
||||
)
|
||||
self._tools_btn.clicked.connect(self.tools_clicked)
|
||||
self._tools_btn.setVisible(False)
|
||||
lay.addWidget(self._tools_btn)
|
||||
|
||||
# avatar
|
||||
self._avatar = QLabel("--")
|
||||
self._avatar.setObjectName("Avatar")
|
||||
@@ -129,6 +142,7 @@ class TopBar(QWidget):
|
||||
self.toggle.set_mode(mode)
|
||||
|
||||
def set_staff(self, is_staff: bool) -> None:
|
||||
self._tools_btn.setVisible(is_staff)
|
||||
if is_staff != self._is_staff:
|
||||
self._is_staff = is_staff
|
||||
if self._last_baton is not None:
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
"""Staff-only Tools dialog: Beam · Sessions · Recovery · Local Contact · Dev help.
|
||||
|
||||
A tabbed admin surface wired to the existing DAQWorker. Each tab reads live
|
||||
values via the ``status_provider`` callable and triggers the matching DAQWorker
|
||||
methods. Recovery actions use the confirmation-code flow (password prompt).
|
||||
Everything is staff-gated by the caller (the Tools button only shows for staff).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QDialog,
|
||||
QDoubleSpinBox,
|
||||
QFormLayout,
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QInputDialog,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QPushButton,
|
||||
QSpinBox,
|
||||
QTabWidget,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from aare.common.coordinate import AerotechCoordinate, Coordinate
|
||||
from aare.gui.new_gui.theme import build_qss
|
||||
|
||||
|
||||
def _btn(text: str) -> QPushButton:
|
||||
b = QPushButton(text)
|
||||
b.setCursor(Qt.PointingHandCursor)
|
||||
return b
|
||||
|
||||
|
||||
class StaffToolsDialog(QDialog):
|
||||
def __init__(self, daq, palette, status_provider, parent=None):
|
||||
super().__init__(parent)
|
||||
self._daq = daq
|
||||
self._p = palette
|
||||
self._status = status_provider
|
||||
self.setWindowTitle("Staff tools")
|
||||
self.setModal(False)
|
||||
self.resize(820, 640)
|
||||
self.setStyleSheet(build_qss(palette))
|
||||
|
||||
lay = QVBoxLayout(self)
|
||||
self.tabs = QTabWidget()
|
||||
lay.addWidget(self.tabs)
|
||||
self.tabs.addTab(self._build_beam_tab(), "Beam / detector")
|
||||
self.tabs.addTab(self._build_sessions_tab(), "Sessions")
|
||||
self.tabs.addTab(self._build_recovery_tab(), "Recovery")
|
||||
self.tabs.addTab(self._build_local_contact_tab(), "Local contact")
|
||||
self.tabs.addTab(self._build_devhelp_tab(), "Dev help")
|
||||
|
||||
self._connect_signals()
|
||||
|
||||
# ====================================================== signals / loads
|
||||
def _connect_signals(self) -> None:
|
||||
d = self._daq
|
||||
self._safe_connect(d, "staff_pgroups_loaded", self._on_pgroups)
|
||||
self._safe_connect(d, "gui_sessions_loaded", self._on_sessions)
|
||||
self._safe_connect(d, "recovery_action_completed", self._on_recovery_done)
|
||||
self._safe_connect(d, "sample_resync_completed", self._on_recovery_done)
|
||||
self._safe_connect(d, "detector_metadata_resync_completed", self._on_recovery_done)
|
||||
self._safe_connect(d, "local_contact_simulation_state_loaded", self._on_sim_state)
|
||||
self._safe_connect(d, "local_contact_links_loaded", self._on_links)
|
||||
self._safe_connect(d, "bec_user_macros_loaded",
|
||||
lambda v: self._becout.setPlainText("\n".join(v or [])))
|
||||
self._safe_connect(d, "bec_devices_loaded",
|
||||
lambda v: self._becout.setPlainText("\n".join(v or [])))
|
||||
self._safe_connect(d, "error_codes_loaded", self._on_error_codes)
|
||||
self._safe_connect(d, "last_error_payload_changed", self._on_err_payload)
|
||||
|
||||
@staticmethod
|
||||
def _safe_connect(obj, signal_name, slot):
|
||||
sig = getattr(obj, signal_name, None)
|
||||
if sig is not None:
|
||||
try:
|
||||
sig.connect(slot)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _call(self, method: str, *args) -> None:
|
||||
fn = getattr(self._daq, method, None)
|
||||
if callable(fn):
|
||||
fn(*args)
|
||||
|
||||
def showEvent(self, event): # noqa: N802
|
||||
super().showEvent(event)
|
||||
self._refresh_beam()
|
||||
self._call("get_all_pgroups")
|
||||
self._call("load_gui_sessions")
|
||||
self._call("get_error_codes")
|
||||
self._call("load_local_contact_simulation_state")
|
||||
self._call("load_local_contact_links")
|
||||
self._refresh_pgroup_label()
|
||||
|
||||
# ====================================================== Beam / detector
|
||||
def _build_beam_tab(self) -> QWidget:
|
||||
w = QWidget()
|
||||
lay = QVBoxLayout(w)
|
||||
|
||||
# Beam centre
|
||||
form = QFormLayout()
|
||||
self._bc_x = QDoubleSpinBox(); self._bc_x.setRange(-4000, 4000)
|
||||
self._bc_y = QDoubleSpinBox(); self._bc_y.setRange(-4000, 4000)
|
||||
bc_btn = _btn("Set beam centre")
|
||||
bc_btn.clicked.connect(
|
||||
lambda: self._call("beam_center", self._bc_x.value(), self._bc_y.value()))
|
||||
row = QHBoxLayout(); row.addWidget(self._bc_x); row.addWidget(self._bc_y); row.addWidget(bc_btn)
|
||||
form.addRow("Beam centre (px)", self._wrap(row))
|
||||
|
||||
# Beam size (µm)
|
||||
self._bs_x = QDoubleSpinBox(); self._bs_x.setRange(1, 400)
|
||||
self._bs_y = QDoubleSpinBox(); self._bs_y.setRange(1, 400)
|
||||
bs_btn = _btn("Set beam size")
|
||||
bs_btn.clicked.connect(
|
||||
lambda: self._call("beam_size_mm", self._bs_x.value() / 1000.0,
|
||||
self._bs_y.value() / 1000.0))
|
||||
row2 = QHBoxLayout(); row2.addWidget(self._bs_x); row2.addWidget(self._bs_y); row2.addWidget(bs_btn)
|
||||
form.addRow("Beam size (µm)", self._wrap(row2))
|
||||
|
||||
# Beam mark
|
||||
self._bm_label = QLabel("—")
|
||||
bm_add = _btn("Mark current beam")
|
||||
bm_add.clicked.connect(self._beam_mark_add)
|
||||
bm_clear = _btn("Clear marks")
|
||||
bm_clear.clicked.connect(lambda: self._call("beam_mark_clear"))
|
||||
row3 = QHBoxLayout(); row3.addWidget(self._bm_label, 1); row3.addWidget(bm_add); row3.addWidget(bm_clear)
|
||||
form.addRow("Beam mark (px)", self._wrap(row3))
|
||||
lay.addLayout(form)
|
||||
|
||||
# ABR tweak
|
||||
lay.addWidget(self._hsep())
|
||||
lay.addWidget(QLabel("<b>ABR measurement position</b>"))
|
||||
self._abr_step = QDoubleSpinBox(); self._abr_step.setRange(1, 1000); self._abr_step.setValue(10)
|
||||
self._abr_step.setSuffix(" µm")
|
||||
grid = QGridLayout()
|
||||
self._abr_labels = {}
|
||||
for r, axis in enumerate(("x", "y", "z")):
|
||||
grid.addWidget(QLabel(f"GM{axis.upper()}"), r, 0)
|
||||
minus = _btn("−"); minus.setFixedWidth(36)
|
||||
plus = _btn("+"); plus.setFixedWidth(36)
|
||||
minus.clicked.connect(lambda _=False, a=axis: self._abr_tweak(a, -1))
|
||||
plus.clicked.connect(lambda _=False, a=axis: self._abr_tweak(a, +1))
|
||||
val = QLabel("—")
|
||||
self._abr_labels[axis] = val
|
||||
grid.addWidget(minus, r, 1); grid.addWidget(plus, r, 2); grid.addWidget(val, r, 3)
|
||||
steprow = QHBoxLayout(); steprow.addWidget(QLabel("Step")); steprow.addWidget(self._abr_step); steprow.addStretch(1)
|
||||
save = _btn("Save meas. pos"); save.clicked.connect(lambda: self._call("abr_save"))
|
||||
goto = _btn("Goto meas. pos"); goto.clicked.connect(lambda: self._call("abr_goto_meas"))
|
||||
steprow.addWidget(save); steprow.addWidget(goto)
|
||||
lay.addLayout(steprow)
|
||||
lay.addLayout(grid)
|
||||
|
||||
# Monochromator
|
||||
lay.addWidget(self._hsep())
|
||||
lay.addWidget(QLabel("<b>Monochromator</b>"))
|
||||
mono = QHBoxLayout()
|
||||
pitch = _btn("Mono pitch scan"); pitch.clicked.connect(lambda: self._call("mono_pitch_scan"))
|
||||
self._energy = QDoubleSpinBox(); self._energy.setRange(1000, 30000); self._energy.setSingleStep(100)
|
||||
self._energy.setSuffix(" eV"); self._energy.setValue(12400)
|
||||
ch = _btn("Change energy")
|
||||
ch.clicked.connect(lambda: self._call("change_energy", self._energy.value()))
|
||||
mono.addWidget(pitch); mono.addStretch(1); mono.addWidget(self._energy); mono.addWidget(ch)
|
||||
lay.addLayout(mono)
|
||||
|
||||
refresh = _btn("Refresh readouts"); refresh.clicked.connect(self._refresh_beam)
|
||||
lay.addWidget(refresh)
|
||||
lay.addStretch(1)
|
||||
return w
|
||||
|
||||
def _beam_mark_add(self) -> None:
|
||||
geom = getattr(self._status(), "geom", None)
|
||||
loc = getattr(geom, "beam_location_pxl", None) if geom else None
|
||||
if loc is not None:
|
||||
self._call("beam_mark_add", loc.x, loc.y)
|
||||
|
||||
def _abr_tweak(self, axis: str, sign: int) -> None:
|
||||
step_mm = self._abr_step.value() / 1000.0 * sign
|
||||
kw = {"x": 0.0, "y": 0.0, "z": 0.0}
|
||||
kw[axis] = step_mm
|
||||
self._call("abr_tweak",
|
||||
AerotechCoordinate(at_mm=Coordinate(**kw), omega_deg=None))
|
||||
|
||||
def _refresh_beam(self) -> None:
|
||||
s = self._status()
|
||||
geom = getattr(s, "geom", None)
|
||||
diff = getattr(s, "diffraction", None)
|
||||
bc = getattr(diff, "beam_center_pxl", None) if diff else None
|
||||
if bc and len(bc) >= 2:
|
||||
self._bc_x.setValue(float(bc[0])); self._bc_y.setValue(float(bc[1]))
|
||||
bs = getattr(geom, "beam_size_mm", None) if geom else None
|
||||
if bs is not None:
|
||||
self._bs_x.setValue(float(bs.x) * 1000.0); self._bs_y.setValue(float(bs.y) * 1000.0)
|
||||
loc = getattr(geom, "beam_location_pxl", None) if geom else None
|
||||
if loc is not None:
|
||||
self._bm_label.setText(f"{loc.x:.0f}, {loc.y:.0f}")
|
||||
meas = getattr(geom, "aerotech_meas", None) if geom else None
|
||||
if meas is not None:
|
||||
for axis in ("x", "y", "z"):
|
||||
self._abr_labels[axis].setText(f"{getattr(meas, axis):.4f} mm")
|
||||
|
||||
# ====================================================== Sessions / pgroup
|
||||
def _build_sessions_tab(self) -> QWidget:
|
||||
w = QWidget()
|
||||
lay = QVBoxLayout(w)
|
||||
# pgroup
|
||||
pg = QHBoxLayout()
|
||||
pg.addWidget(QLabel("Current p-group:"))
|
||||
self._pgroup_label = QLabel("—")
|
||||
pg.addWidget(self._pgroup_label)
|
||||
pg.addStretch(1)
|
||||
from PySide6.QtWidgets import QComboBox
|
||||
self._pgroup_combo = QComboBox(); self._pgroup_combo.setEditable(True)
|
||||
self._pgroup_combo.setMinimumWidth(160)
|
||||
set_pg = _btn("Set p-group")
|
||||
set_pg.clicked.connect(
|
||||
lambda: self._call("set_pgroup", self._pgroup_combo.currentText().strip()))
|
||||
pg.addWidget(self._pgroup_combo); pg.addWidget(set_pg)
|
||||
lay.addLayout(pg)
|
||||
|
||||
lay.addWidget(self._hsep())
|
||||
lay.addWidget(QLabel("<b>Open GUI sessions</b>"))
|
||||
self._sess_table = QTableWidget(0, 6)
|
||||
self._sess_table.setHorizontalHeaderLabels(
|
||||
["Session", "User", "Staff", "Baton", "Last seen", "Close req."])
|
||||
self._sess_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
self._sess_table.verticalHeader().setVisible(False)
|
||||
lay.addWidget(self._sess_table, 1)
|
||||
|
||||
actions = QHBoxLayout()
|
||||
refresh = _btn("Refresh"); refresh.clicked.connect(lambda: self._call("load_gui_sessions"))
|
||||
self._grace = QSpinBox(); self._grace.setRange(5, 3600); self._grace.setValue(60); self._grace.setSuffix(" s")
|
||||
req = _btn("Request close"); req.clicked.connect(self._request_close)
|
||||
force = _btn("Force remove"); force.clicked.connect(self._force_remove)
|
||||
actions.addWidget(refresh); actions.addStretch(1)
|
||||
actions.addWidget(self._grace); actions.addWidget(req); actions.addWidget(force)
|
||||
lay.addLayout(actions)
|
||||
|
||||
sess = QHBoxLayout()
|
||||
fs = _btn("Force my session"); fs.clicked.connect(lambda: self._call("force_session"))
|
||||
es = _btn("End my session"); es.clicked.connect(lambda: self._call("end_session"))
|
||||
sess.addStretch(1); sess.addWidget(fs); sess.addWidget(es)
|
||||
lay.addLayout(sess)
|
||||
return w
|
||||
|
||||
def _selected_session_id(self):
|
||||
row = self._sess_table.currentRow()
|
||||
if row < 0:
|
||||
return None
|
||||
item = self._sess_table.item(row, 0)
|
||||
return int(item.text()) if item else None
|
||||
|
||||
def _request_close(self) -> None:
|
||||
sid = self._selected_session_id()
|
||||
if sid is not None:
|
||||
self._call("request_gui_close", sid, self._grace.value())
|
||||
|
||||
def _force_remove(self) -> None:
|
||||
sid = self._selected_session_id()
|
||||
if sid is None:
|
||||
return
|
||||
if QMessageBox.question(self, "Force remove",
|
||||
f"Force-remove session {sid}?") == QMessageBox.Yes:
|
||||
self._call("force_remove_gui_session", sid)
|
||||
|
||||
def _on_pgroups(self, pgroups) -> None:
|
||||
self._pgroup_combo.clear()
|
||||
self._pgroup_combo.addItems([str(p) for p in (pgroups or [])])
|
||||
|
||||
def _refresh_pgroup_label(self) -> None:
|
||||
sess = getattr(self._status(), "session", None)
|
||||
cur = getattr(sess, "current_pgroup", None) if sess else None
|
||||
self._pgroup_label.setText(cur or "—")
|
||||
|
||||
def _on_sessions(self, sessions) -> None:
|
||||
self._sess_table.setRowCount(0)
|
||||
for info in sessions or []:
|
||||
r = self._sess_table.rowCount()
|
||||
self._sess_table.insertRow(r)
|
||||
vals = [
|
||||
str(getattr(info, "session", "")),
|
||||
str(getattr(info, "username", "")),
|
||||
"✓" if getattr(info, "staff", False) else "",
|
||||
"●" if getattr(info, "holds_baton", False) else "",
|
||||
self._age(getattr(info, "last_seen_ts", None)),
|
||||
"yes" if getattr(info, "close_requested", False) else "",
|
||||
]
|
||||
for c, v in enumerate(vals):
|
||||
self._sess_table.setItem(r, c, QTableWidgetItem(v))
|
||||
|
||||
@staticmethod
|
||||
def _age(ts) -> str:
|
||||
if not ts:
|
||||
return "—"
|
||||
import time
|
||||
secs = max(0, int(time.time() - ts))
|
||||
if secs < 60:
|
||||
return f"{secs}s ago"
|
||||
m, s = divmod(secs, 60)
|
||||
if m < 60:
|
||||
return f"{m}m {s}s ago"
|
||||
h, m = divmod(m, 60)
|
||||
return f"{h}h {m}m ago"
|
||||
|
||||
# ====================================================== Recovery
|
||||
def _build_recovery_tab(self) -> QWidget:
|
||||
w = QWidget()
|
||||
lay = QVBoxLayout(w)
|
||||
warn = QLabel("⚠ Recovery actions can disrupt an active session. "
|
||||
"Confirmation-code actions require a staff code.")
|
||||
warn.setWordWrap(True)
|
||||
warn.setStyleSheet(f"color:{self._p.breakpoint};")
|
||||
lay.addWidget(warn)
|
||||
self._recovery_status = QLabel("")
|
||||
self._recovery_status.setStyleSheet(f"color:{self._p.text_muted};")
|
||||
lay.addWidget(self._recovery_status)
|
||||
|
||||
grid = QGridLayout()
|
||||
coded = [
|
||||
("Take over beamline", "take_over_beamline"),
|
||||
("Free beamline", "free_beamline"),
|
||||
("Recover beamline", "recover_beamline"),
|
||||
("Recovery unmount", "recovery_unmount_sample"),
|
||||
]
|
||||
for i, (label, method) in enumerate(coded):
|
||||
b = _btn(label)
|
||||
b.clicked.connect(lambda _=False, m=method, lbl=label: self._coded_action(lbl, m))
|
||||
grid.addWidget(b, i // 2, i % 2)
|
||||
lay.addLayout(grid)
|
||||
|
||||
lay.addWidget(self._hsep())
|
||||
lay.addWidget(QLabel("<b>Maintenance</b>"))
|
||||
grid2 = QGridLayout()
|
||||
plain = [
|
||||
("Anneal…", self._anneal),
|
||||
("Park && dry", lambda: self._call("park_and_dry")),
|
||||
("TELL dry", lambda: self._call("tell_dry")),
|
||||
("Toggle blower", lambda: self._call("tell_toggle_blower")),
|
||||
("Resync sample", lambda: self._call("resync_sample")),
|
||||
("Resync detector meta", lambda: self._call("resync_local_contact_detector_metadata")),
|
||||
("Init smargon", lambda: self._call("initialise_smargon")),
|
||||
("Init aerotech", lambda: self._call("initialise_aerotech")),
|
||||
("Init detector", lambda: self._call("initialise_detector")),
|
||||
("Take pedestal", lambda: self._call("detector_take_pedestal")),
|
||||
("Cancel current op", lambda: self._call("cancel")),
|
||||
]
|
||||
for i, (label, slot) in enumerate(plain):
|
||||
b = _btn(label); b.clicked.connect(lambda _=False, s=slot: s())
|
||||
grid2.addWidget(b, i // 3, i % 3)
|
||||
lay.addLayout(grid2)
|
||||
lay.addStretch(1)
|
||||
return w
|
||||
|
||||
def _coded_action(self, label: str, method: str) -> None:
|
||||
if QMessageBox.question(self, label, f"{label}: are you sure?") != QMessageBox.Yes:
|
||||
return
|
||||
code, ok = QInputDialog.getText(self, label, "Confirmation code:",
|
||||
QLineEdit.Password)
|
||||
if ok and code.strip():
|
||||
self._call(method, code.strip())
|
||||
|
||||
def _anneal(self) -> None:
|
||||
secs, ok = QInputDialog.getDouble(self, "Anneal", "Time (s):", 1.0, 0.1, 60.0, 1)
|
||||
if ok:
|
||||
self._call("anneal", secs)
|
||||
|
||||
def _on_recovery_done(self, message: str) -> None:
|
||||
self._recovery_status.setText(str(message))
|
||||
|
||||
# ====================================================== Local contact
|
||||
def _build_local_contact_tab(self) -> QWidget:
|
||||
w = QWidget()
|
||||
lay = QVBoxLayout(w)
|
||||
|
||||
lay.addWidget(QLabel("<b>Simulation</b>"))
|
||||
self._sim_checks = {}
|
||||
simrow = QHBoxLayout()
|
||||
for dev in ("tell", "bec", "detector", "aerotech", "smargon"):
|
||||
cb = QCheckBox(dev)
|
||||
cb.toggled.connect(
|
||||
lambda on, d=dev: self._call("set_local_contact_simulation", d, on))
|
||||
self._sim_checks[dev] = cb
|
||||
simrow.addWidget(cb)
|
||||
simrow.addStretch(1)
|
||||
lay.addLayout(simrow)
|
||||
|
||||
lay.addWidget(QLabel("<b>Restart / initialise backend</b>"))
|
||||
rrow = QGridLayout()
|
||||
for i, dev in enumerate(("tell", "bec", "detector", "smargon", "aerotech")):
|
||||
b = _btn(f"Restart {dev}")
|
||||
b.clicked.connect(lambda _=False, d=dev: self._call("restart_local_contact_device", d))
|
||||
rrow.addWidget(b, i // 3, i % 3)
|
||||
lay.addLayout(rrow)
|
||||
|
||||
lay.addWidget(QLabel("<b>BEC</b>"))
|
||||
becrow = QHBoxLayout()
|
||||
for label, method in (
|
||||
("Load macros", "bec_load_user_macros"),
|
||||
("List macros", "bec_list_all_user_macros"),
|
||||
("List devices", "bec_list_all_devices"),
|
||||
("Reinit planner", "bec_reinitialise_planner_and_position_devices"),
|
||||
):
|
||||
b = _btn(label); b.clicked.connect(lambda _=False, m=method: self._call(m))
|
||||
becrow.addWidget(b)
|
||||
lay.addLayout(becrow)
|
||||
self._becout = QPlainTextEdit(); self._becout.setReadOnly(True); self._becout.setMaximumHeight(120)
|
||||
lay.addWidget(self._becout)
|
||||
|
||||
lay.addWidget(QLabel("<b>Web panels</b>"))
|
||||
self._links_label = QLabel("—"); self._links_label.setOpenExternalLinks(True)
|
||||
self._links_label.setWordWrap(True)
|
||||
lay.addWidget(self._links_label)
|
||||
lay.addStretch(1)
|
||||
return w
|
||||
|
||||
def _on_sim_state(self, state: dict) -> None:
|
||||
for dev, cb in self._sim_checks.items():
|
||||
if dev in (state or {}):
|
||||
cb.blockSignals(True)
|
||||
cb.setChecked(bool(state[dev]))
|
||||
cb.blockSignals(False)
|
||||
|
||||
def _on_links(self, links: dict) -> None:
|
||||
parts = []
|
||||
for k, v in (links or {}).items():
|
||||
if isinstance(v, str) and v.startswith("http"):
|
||||
parts.append(f'<a href="{v}">{k}</a>')
|
||||
else:
|
||||
parts.append(f"{k}: {v}")
|
||||
self._links_label.setText(" · ".join(parts) if parts else "—")
|
||||
|
||||
# ====================================================== Dev help
|
||||
def _build_devhelp_tab(self) -> QWidget:
|
||||
w = QWidget()
|
||||
lay = QVBoxLayout(w)
|
||||
self._code_filter = QLineEdit(); self._code_filter.setPlaceholderText("Filter error codes…")
|
||||
self._code_filter.textChanged.connect(self._filter_codes)
|
||||
lay.addWidget(self._code_filter)
|
||||
self._codes_table = QTableWidget(0, 1)
|
||||
self._codes_table.setHorizontalHeaderLabels(["Error code"])
|
||||
self._codes_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
self._codes_table.verticalHeader().setVisible(False)
|
||||
lay.addWidget(self._codes_table, 1)
|
||||
self._all_codes: dict = {}
|
||||
|
||||
lay.addWidget(QLabel("<b>Last error payload</b>"))
|
||||
self._err_payload = QPlainTextEdit(); self._err_payload.setReadOnly(True)
|
||||
self._err_payload.setMaximumHeight(180)
|
||||
lay.addWidget(self._err_payload)
|
||||
return w
|
||||
|
||||
def _on_error_codes(self, codes: dict) -> None:
|
||||
self._all_codes = dict(codes or {})
|
||||
self._filter_codes(self._code_filter.text())
|
||||
|
||||
def _filter_codes(self, text: str) -> None:
|
||||
text = (text or "").lower()
|
||||
self._codes_table.setRowCount(0)
|
||||
for name in sorted(self._all_codes):
|
||||
if text and text not in name.lower():
|
||||
continue
|
||||
r = self._codes_table.rowCount()
|
||||
self._codes_table.insertRow(r)
|
||||
self._codes_table.setItem(r, 0, QTableWidgetItem(name))
|
||||
|
||||
def _on_err_payload(self, payload: dict) -> None:
|
||||
try:
|
||||
self._err_payload.setPlainText(json.dumps(payload, indent=2, default=str))
|
||||
except Exception:
|
||||
self._err_payload.setPlainText(str(payload))
|
||||
|
||||
# ====================================================== helpers
|
||||
@staticmethod
|
||||
def _wrap(layout) -> QWidget:
|
||||
wid = QWidget()
|
||||
wid.setLayout(layout)
|
||||
return wid
|
||||
|
||||
def _hsep(self) -> QLabel:
|
||||
line = QLabel()
|
||||
line.setFixedHeight(1)
|
||||
line.setStyleSheet(f"background:{self._p.border_panel};")
|
||||
return line
|
||||
Reference in New Issue
Block a user