new_gui: staff view + motor/camera UX edits + prediction-metrics
- Staff alignment view (3rd mode, staff only): beam location / beamstop / align CoR / flux measurement procedures; camera+spectrum + alignment motors - Motors: compact SMARGON with editable Chi/Phi + Focus(Z) jog; staff-only ABR (GMX/GMY/GMZ) moved under SMARGON; energy removed (now staff-tools only) - Inline camera Exp/Gain by the sample camera - Prediction-metrics dialog from the prediction stream - shared _wire_motors; guest overlay also covers the staff camera 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
7c22b24437
commit
49dfb40eff
@@ -35,6 +35,17 @@ Backend contract is documented in the `new-gui-backend-paths` memory.
|
||||
|
||||
## What is wired to the backend
|
||||
|
||||
- **Staff view** (3rd mode toggle, staff only) — alignment cockpit with procedures
|
||||
Beam location / Beamstop alignment / Align centre of rotation / Flux measurement
|
||||
(switch the beamline state where one exists; CoR uses Sample alignment + omega +
|
||||
smargon + ABR). Centre = camera (beam-centre marker) / XRF spectrum; right =
|
||||
full motors (omega/smargon/ABR/zoom/light).
|
||||
- **Motors panel**: compact SMARGON with **editable Chi/Phi** + **Focus (Z)** jog
|
||||
(new — smargon-z move), staff-only **ABR** (GMX/GMY/GMZ + save/goto) under
|
||||
SMARGON. Energy lives in the staff Tools dialog (not the motors panel).
|
||||
- **Camera exp/gain** are inline in the camera tab strip (Exp/Gain → `samcam_settings`).
|
||||
- **Prediction metrics** dialog (per-class counts, confidence, FPS) from the
|
||||
prediction stream (launched from the camera strip). (smargon-trace still deferred.)
|
||||
- **Polish (Phase 4)**: keyboard shortcuts (Ctrl+1/2 mode, Alt+1-4 camera tabs,
|
||||
Ctrl+T tools, F1 help, Ctrl+Q quit); window-geometry + mode persistence
|
||||
(QSettings); a **filename builder** (prefix + auto-incrementing run number) that
|
||||
|
||||
@@ -97,8 +97,11 @@ class MainWindow(QWidget):
|
||||
self.body = QStackedWidget()
|
||||
self.manual = ManualView(self.state, self._palette, self._defaults)
|
||||
self.automation = AutomationView(self.state, self._palette)
|
||||
from aare.gui.new_gui.staff_view import StaffView
|
||||
self.staff = StaffView(self._palette)
|
||||
self.body.addWidget(self.manual)
|
||||
self.body.addWidget(self.automation)
|
||||
self.body.addWidget(self.staff)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -213,6 +216,7 @@ class MainWindow(QWidget):
|
||||
cc.screenshot_requested.connect(self.daq.send_screenshot_db)
|
||||
cc.face_detect_clicked.connect(self._open_face_detection)
|
||||
cc.stability_clicked.connect(self._open_stability)
|
||||
cc.metrics_clicked.connect(self._open_metrics)
|
||||
if hasattr(self.daq, "raster_scan_completed"):
|
||||
self.daq.raster_scan_completed.connect(self.manual.on_raster_completed)
|
||||
|
||||
@@ -231,12 +235,24 @@ class MainWindow(QWidget):
|
||||
s.unmount_requested.connect(self.daq.unmount)
|
||||
|
||||
# motors -> DAQ
|
||||
self.manual.motors.omega_rel.connect(self.daq.set_omega_rel)
|
||||
self.manual.motors.zoom_changed.connect(self.daq.zoom)
|
||||
self.manual.motors.front_light_changed.connect(self.daq.front_light)
|
||||
self.manual.motors.back_light_changed.connect(self.daq.back_light)
|
||||
self.manual.motors.smargon_jog.connect(self._on_smargon_jog)
|
||||
self.manual.motors.energy_changed.connect(self.daq.change_energy)
|
||||
self._wire_motors(self.manual.motors)
|
||||
|
||||
# staff alignment view
|
||||
self.daq.update.connect(self.staff.update_daq_status)
|
||||
self.staff.state_requested.connect(self._on_state_change)
|
||||
if hasattr(self.daq, "beam_center"):
|
||||
self.staff.beam_center_changed.connect(self.daq.beam_center)
|
||||
if hasattr(self.daq, "beam_mark_clear"):
|
||||
self.staff.beam_mark_clear.connect(self.daq.beam_mark_clear)
|
||||
self._wire_motors(self.staff.motors)
|
||||
sp2 = self.staff.spectrum
|
||||
sp2.start_clicked.connect(self.daq.fluorimeter_start)
|
||||
sp2.stop_clicked.connect(self.daq.fluorimeter_stop)
|
||||
sp2.snapshot_clicked.connect(self.daq.fluorimeter_request_snapshot)
|
||||
if hasattr(self.daq, "fluorimeter_spectrum_update"):
|
||||
self.daq.fluorimeter_spectrum_update.connect(sp2.update_spectrum)
|
||||
if hasattr(self.daq, "fluorimeter_update"):
|
||||
self.daq.fluorimeter_update.connect(sp2.update_live)
|
||||
|
||||
# automation
|
||||
self.automation.start_requested.connect(self._start_automation)
|
||||
@@ -294,13 +310,34 @@ class MainWindow(QWidget):
|
||||
self.camera_thread.enable_focus_measurement(True)
|
||||
if hasattr(self.camera_thread, "update_daq_status"):
|
||||
self.daq.update.connect(self.camera_thread.update_daq_status)
|
||||
# staff-view camera mirrors the same stream
|
||||
scam = self.staff.camera
|
||||
self.camera_thread.image.connect(scam.update_pixmap)
|
||||
if hasattr(self.camera_thread, "fps_measure"):
|
||||
self.camera_thread.fps_measure.connect(scam.update_fps)
|
||||
if hasattr(self.camera_thread, "target_point"):
|
||||
self.camera_thread.target_point.connect(scam.update_target_point)
|
||||
|
||||
# ------------------------------------------------------------ handlers
|
||||
def _wire_motors(self, m) -> None:
|
||||
m.omega_rel.connect(self.daq.set_omega_rel)
|
||||
m.zoom_changed.connect(self.daq.zoom)
|
||||
m.front_light_changed.connect(self.daq.front_light)
|
||||
m.back_light_changed.connect(self.daq.back_light)
|
||||
m.smargon_jog.connect(self._on_smargon_jog)
|
||||
m.smargon_angles.connect(self._on_smargon_angles)
|
||||
if hasattr(self.daq, "abr_tweak"):
|
||||
m.abr_tweak.connect(self.daq.abr_tweak)
|
||||
if hasattr(self.daq, "abr_save"):
|
||||
m.abr_save.connect(self.daq.abr_save)
|
||||
if hasattr(self.daq, "abr_goto_meas"):
|
||||
m.abr_goto.connect(self.daq.abr_goto_meas)
|
||||
|
||||
@Slot(str)
|
||||
def _on_mode_changed(self, mode: str) -> None:
|
||||
self.body.setCurrentWidget(
|
||||
self.automation if mode == "automation" else self.manual
|
||||
)
|
||||
page = {"automation": self.automation, "staff": self.staff}.get(
|
||||
mode, self.manual)
|
||||
self.body.setCurrentWidget(page)
|
||||
self.top_bar.set_mode(mode)
|
||||
|
||||
@Slot(object)
|
||||
@@ -377,6 +414,18 @@ class MainWindow(QWidget):
|
||||
self._stability_dialog.show()
|
||||
self._stability_dialog.raise_()
|
||||
|
||||
def _open_metrics(self) -> None:
|
||||
from aare.gui.new_gui.widgets.prediction_metrics_dialog import (
|
||||
PredictionMetricsDialog,
|
||||
)
|
||||
if getattr(self, "_metrics_dialog", None) is None:
|
||||
self._metrics_dialog = PredictionMetricsDialog(self._palette, self)
|
||||
if self.camera_thread is not None and hasattr(self.camera_thread, "prediction"):
|
||||
self.camera_thread.prediction.connect(
|
||||
self._metrics_dialog.update_from_prediction)
|
||||
self._metrics_dialog.show()
|
||||
self._metrics_dialog.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
|
||||
@@ -417,18 +466,20 @@ class MainWindow(QWidget):
|
||||
timeout = getattr(req, "timeout_seconds", 30) if req else 30
|
||||
self._show_incoming_dialog(who, timeout)
|
||||
|
||||
def _set_control(self, in_control: bool, title: str = "GUEST MODE",
|
||||
sub: str = "") -> None:
|
||||
self.manual.set_control_state(in_control, title, sub)
|
||||
self.staff.camera.set_control_state(in_control, title, sub)
|
||||
|
||||
def _update_control(self) -> None:
|
||||
"""Grey the camera + show Guest/Vacant overlay unless we hold the baton."""
|
||||
b = self._baton
|
||||
if b is None:
|
||||
self.manual.set_control_state(True)
|
||||
return
|
||||
if getattr(b, "you_are_holder", False):
|
||||
self.manual.set_control_state(True)
|
||||
if b is None or getattr(b, "you_are_holder", False):
|
||||
self._set_control(True)
|
||||
return
|
||||
holder = getattr(b, "holder", None)
|
||||
if holder is None:
|
||||
self.manual.set_control_state(
|
||||
self._set_control(
|
||||
False, "VACANT", "Grab the baton to take control of the beamline.")
|
||||
return
|
||||
username = getattr(holder, "username", None)
|
||||
@@ -439,7 +490,7 @@ class MainWindow(QWidget):
|
||||
sub = "Staff has control of the beamline."
|
||||
else:
|
||||
sub = "Another user has control of the beamline."
|
||||
self.manual.set_control_state(False, "GUEST MODE", sub)
|
||||
self._set_control(False, "GUEST MODE", sub)
|
||||
|
||||
# ---- baton request / response flow ----
|
||||
@Slot(dict)
|
||||
@@ -542,14 +593,14 @@ class MainWindow(QWidget):
|
||||
else:
|
||||
self._note(f"No DAQ method for state '{key}'.", error=True)
|
||||
|
||||
def _on_smargon_jog(self, dx_mm: float, dy_mm: float) -> None:
|
||||
"""Relative sample-holder jog in beamline (screen) space.
|
||||
def _on_smargon_jog(self, dx_mm: float, dy_mm: float, dz_mm: float = 0.0) -> None:
|
||||
"""Relative sample-holder jog in beamline (screen) space (x/y) + focus (z).
|
||||
|
||||
The delta is a beamline-frame offset; ``beamline_to_smargon`` rotates it
|
||||
through the current omega/phi/chi into an absolute smargon sh_mm target,
|
||||
so the jog stays screen-intuitive at any goniometer angle. Arrow signs:
|
||||
←/→ = -x/+x, ↑/↓ = -y/+y (camera y points down). Flip in MotorsPanel if
|
||||
the beamline convention differs.
|
||||
through the current omega/phi/chi into an absolute smargon sh_mm target.
|
||||
z is the optical/focus axis. Arrow signs: ←/→ = -x/+x, ↑/↓ = -y/+y
|
||||
(camera y points down); focus +/- = +z/-z. Flip in MotorsPanel if the
|
||||
beamline convention differs.
|
||||
"""
|
||||
from aare.common.coordinate import Coordinate, SmargonCoordinate
|
||||
|
||||
@@ -558,12 +609,22 @@ class MainWindow(QWidget):
|
||||
self._note("No geometry yet; cannot jog.", error=True)
|
||||
return
|
||||
try:
|
||||
sh = geom.beamline_to_smargon(Coordinate(x=dx_mm, y=dy_mm, z=0.0))
|
||||
sh = geom.beamline_to_smargon(Coordinate(x=dx_mm, y=dy_mm, z=dz_mm))
|
||||
except Exception as exc:
|
||||
self._note(f"Could not compute jog target: {exc}", error=True)
|
||||
return
|
||||
self.daq.move_smargon(SmargonCoordinate(sh_mm=sh))
|
||||
|
||||
def _on_smargon_angles(self, chi_deg: float, phi_deg: float) -> None:
|
||||
"""Move to absolute chi/phi, preserving the current sample-holder position."""
|
||||
from aare.common.coordinate import SmargonCoordinate
|
||||
|
||||
geom = getattr(self.manual.last_status, "geom", None)
|
||||
smg = getattr(geom, "smargon", None) if geom else None
|
||||
sh = getattr(smg, "sh_mm", None) if smg else None
|
||||
self.daq.move_smargon(
|
||||
SmargonCoordinate(sh_mm=sh, phi_deg=phi_deg, chi_deg=chi_deg))
|
||||
|
||||
def _on_spectrum(self, model) -> None:
|
||||
self.manual.spectrum.update_spectrum(model)
|
||||
self.manual.show_spectrum()
|
||||
|
||||
@@ -345,6 +345,7 @@ class ManualView(QWidget):
|
||||
def update_daq_status(self, s) -> None:
|
||||
self._last_status = s
|
||||
self.motors.update_daq_status(s)
|
||||
self.camera_controls.update_daq_status(s)
|
||||
bl = getattr(s, "bl", None)
|
||||
if bl is not None and getattr(bl, "zoom", None) is not None:
|
||||
self.camera.set_zoom(bl.zoom)
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Staff alignment view — a Manual-style cockpit for beamline alignment.
|
||||
|
||||
Procedures: Beam location · Beamstop alignment · Align centre of rotation · Flux
|
||||
measurement. Each switches the relevant beamline state (where one exists) and the
|
||||
contextual controls. Centre-of-rotation has no backend state — it parks the
|
||||
beamline in Sample alignment and relies on omega + smargon + ABR (right panel).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QDoubleSpinBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QStackedWidget,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from aare.gui.new_gui.theme import CHANGER_W, Palette
|
||||
from aare.gui.new_gui.widgets.camera import CameraViewport
|
||||
from aare.gui.new_gui.widgets.common import micro_label, section_label
|
||||
from aare.gui.new_gui.widgets.motors_panel import MotorsPanel
|
||||
from aare.gui.new_gui.widgets.spectrum_view import SpectrumView
|
||||
|
||||
# (key, title, description, DAQWorker state method, centre page index)
|
||||
PROCEDURES = (
|
||||
("beam", "Beam location", "Mark the beam on the sample camera / detector.",
|
||||
"beam_location", 0),
|
||||
("beamstop", "Beamstop alignment", "Centre the beamstop (use Beamline camera).",
|
||||
"beamstop_alignment", 0),
|
||||
("cor", "Align centre of rotation",
|
||||
"Rotate ω and re-centre with smargon / ABR.", "sample_alignment", 0),
|
||||
("flux", "Flux measurement", "Measure flux / fluorescence spectrum.",
|
||||
"flux_measurement", 1),
|
||||
)
|
||||
|
||||
|
||||
class StaffView(QWidget):
|
||||
state_requested = Signal(str) # DAQWorker state method name
|
||||
beam_center_changed = Signal(float, float)
|
||||
beam_mark_clear = Signal()
|
||||
|
||||
def __init__(self, palette: Palette, parent=None):
|
||||
super().__init__(parent)
|
||||
self._p = palette
|
||||
self._last_status = None
|
||||
|
||||
root = QHBoxLayout(self)
|
||||
root.setContentsMargins(0, 0, 0, 0)
|
||||
root.setSpacing(0)
|
||||
|
||||
# --- left: procedure selector ---
|
||||
left = QWidget()
|
||||
left.setObjectName("SidePanel")
|
||||
left.setFixedWidth(CHANGER_W)
|
||||
ll = QVBoxLayout(left)
|
||||
ll.setContentsMargins(16, 16, 16, 16)
|
||||
ll.setSpacing(8)
|
||||
ll.addWidget(section_label("STAFF · ALIGNMENT"))
|
||||
ll.addSpacing(4)
|
||||
self._proc_btns: dict[str, QPushButton] = {}
|
||||
for key, title, desc, _method, _idx in PROCEDURES:
|
||||
btn = QPushButton(f"{title}\n{desc}")
|
||||
btn.setCheckable(True)
|
||||
btn.setCursor(Qt.PointingHandCursor)
|
||||
btn.setStyleSheet(self._proc_qss(False))
|
||||
btn.clicked.connect(lambda _=False, k=key: self._select(k))
|
||||
btn.toggled.connect(lambda on, b=btn: b.setStyleSheet(self._proc_qss(on)))
|
||||
self._proc_btns[key] = btn
|
||||
ll.addWidget(btn)
|
||||
ll.addStretch(1)
|
||||
root.addWidget(left)
|
||||
|
||||
# --- centre: camera / spectrum + contextual controls ---
|
||||
center = QWidget()
|
||||
cl = QVBoxLayout(center)
|
||||
cl.setContentsMargins(0, 0, 0, 0)
|
||||
cl.setSpacing(0)
|
||||
self.camera = CameraViewport(palette)
|
||||
self.camera.set_mode("plain")
|
||||
self.spectrum = SpectrumView(palette)
|
||||
self.center_stack = QStackedWidget()
|
||||
self.center_stack.addWidget(self.camera) # 0
|
||||
self.center_stack.addWidget(self.spectrum) # 1
|
||||
cl.addWidget(self.center_stack, 1)
|
||||
cl.addWidget(self._build_contextual())
|
||||
root.addWidget(center, 1)
|
||||
|
||||
# --- right: alignment motors (omega / smargon / ABR / zoom / light) ---
|
||||
self.motors = MotorsPanel(palette)
|
||||
self.motors.set_staff(True) # ABR always available in staff view
|
||||
root.addWidget(self.motors)
|
||||
|
||||
self._select("beam")
|
||||
|
||||
# ----- contextual bottom panel -----
|
||||
def _build_contextual(self) -> QWidget:
|
||||
host = QWidget()
|
||||
host.setObjectName("BottomPanel")
|
||||
hl = QVBoxLayout(host)
|
||||
hl.setContentsMargins(22, 12, 22, 12)
|
||||
self.ctx = QStackedWidget()
|
||||
hl.addWidget(self.ctx)
|
||||
|
||||
# beam location
|
||||
beam = QWidget()
|
||||
bl = QHBoxLayout(beam)
|
||||
bl.setContentsMargins(0, 0, 0, 0)
|
||||
bl.setSpacing(8)
|
||||
bl.addWidget(micro_label("BEAM CENTRE (px)"))
|
||||
self._bc_x = QDoubleSpinBox(); self._bc_x.setRange(-4000, 4000)
|
||||
self._bc_y = QDoubleSpinBox(); self._bc_y.setRange(-4000, 4000)
|
||||
bl.addWidget(self._bc_x); bl.addWidget(self._bc_y)
|
||||
setb = QPushButton("Set beam centre"); setb.setCursor(Qt.PointingHandCursor)
|
||||
setb.clicked.connect(
|
||||
lambda: self.beam_center_changed.emit(self._bc_x.value(), self._bc_y.value()))
|
||||
clr = QPushButton("Clear beam marks"); clr.setCursor(Qt.PointingHandCursor)
|
||||
clr.clicked.connect(self.beam_mark_clear)
|
||||
bl.addWidget(setb); bl.addWidget(clr); bl.addStretch(1)
|
||||
self.ctx.addWidget(beam) # 0
|
||||
|
||||
self.ctx.addWidget(self._note_panel( # 1
|
||||
"Switch to the Beamline camera and adjust the beamstop hardware until "
|
||||
"centred on the beam."))
|
||||
self.ctx.addWidget(self._note_panel( # 2
|
||||
"Rotate ω (right panel) and re-centre the sample with the smargon jog "
|
||||
"and ABR focus so it stays on the rotation axis at all angles."))
|
||||
|
||||
# flux — spectrum controls live in the SpectrumView itself
|
||||
self.ctx.addWidget(self._note_panel( # 3
|
||||
"Flux / fluorescence spectrum shown above. Use Start / Stop / Snapshot."))
|
||||
return host
|
||||
|
||||
def _note_panel(self, text: str) -> QWidget:
|
||||
w = QWidget()
|
||||
lay = QHBoxLayout(w)
|
||||
lay.setContentsMargins(0, 0, 0, 0)
|
||||
lbl = QLabel(text)
|
||||
lbl.setWordWrap(True)
|
||||
lbl.setStyleSheet(f"font-size:12px; color:{self._p.text_muted};")
|
||||
lay.addWidget(lbl)
|
||||
return w
|
||||
|
||||
def _proc_qss(self, on: bool) -> str:
|
||||
p = self._p
|
||||
if on:
|
||||
return (f"QPushButton {{ text-align:left; background:{p.accent_tint_bg};"
|
||||
f" border:1px solid {p.accent_tint_border}; border-radius:9px;"
|
||||
f" padding:10px 12px; font-size:12.5px; color:{p.text_primary}; }}")
|
||||
return (f"QPushButton {{ text-align:left; background:{p.surface};"
|
||||
f" border:1px solid {p.border_control}; border-radius:9px;"
|
||||
f" padding:10px 12px; font-size:12.5px; color:{p.text_secondary}; }}")
|
||||
|
||||
def _select(self, key: str) -> None:
|
||||
ctx_index = {"beam": 0, "beamstop": 1, "cor": 2, "flux": 3}[key]
|
||||
for k, btn in self._proc_btns.items():
|
||||
btn.blockSignals(True)
|
||||
btn.setChecked(k == key)
|
||||
btn.setStyleSheet(self._proc_qss(k == key))
|
||||
btn.blockSignals(False)
|
||||
for _key, _title, _desc, method, center_idx in PROCEDURES:
|
||||
if _key == key:
|
||||
self.center_stack.setCurrentIndex(center_idx)
|
||||
self.ctx.setCurrentIndex(ctx_index)
|
||||
self.state_requested.emit(method)
|
||||
break
|
||||
|
||||
# ----- live status -----
|
||||
def update_daq_status(self, s) -> None:
|
||||
self._last_status = s
|
||||
self.motors.update_daq_status(s)
|
||||
geom = getattr(s, "geom", None)
|
||||
beam = getattr(geom, "beam_location_pxl", None) if geom else None
|
||||
if beam is not None:
|
||||
self.camera.set_beam_center(beam.x, beam.y)
|
||||
bl = getattr(s, "bl", None)
|
||||
if bl is not None:
|
||||
self.camera.set_beam_state(getattr(bl, "shutter_open", False),
|
||||
getattr(s, "busy", False))
|
||||
diff = getattr(s, "diffraction", None)
|
||||
bc = getattr(diff, "beam_center_pxl", None) if diff else None
|
||||
if bc and len(bc) >= 2 and not self._bc_x.hasFocus() and not self._bc_y.hasFocus():
|
||||
self._bc_x.blockSignals(True); self._bc_y.blockSignals(True)
|
||||
self._bc_x.setValue(float(bc[0])); self._bc_y.setValue(float(bc[1]))
|
||||
self._bc_x.blockSignals(False); self._bc_y.blockSignals(False)
|
||||
@@ -99,7 +99,7 @@ class AppState(QObject):
|
||||
return self._mode
|
||||
|
||||
def set_mode(self, mode: str) -> None:
|
||||
if mode not in ("manual", "automation") or mode == self._mode:
|
||||
if mode not in ("manual", "automation", "staff") or mode == self._mode:
|
||||
return
|
||||
self._mode = mode
|
||||
self.mode_changed.emit(mode)
|
||||
|
||||
@@ -28,7 +28,8 @@ class ModeToggle(QWidget):
|
||||
self._group = QButtonGroup(self)
|
||||
self._group.setExclusive(True)
|
||||
self._buttons: dict[str, QPushButton] = {}
|
||||
for key, label in (("manual", "Manual"), ("automation", "Automation")):
|
||||
for key, label in (("manual", "Manual"), ("automation", "Automation"),
|
||||
("staff", "Staff")):
|
||||
btn = QPushButton(label)
|
||||
btn.setObjectName("ModeSeg")
|
||||
btn.setCheckable(True)
|
||||
@@ -38,11 +39,15 @@ class ModeToggle(QWidget):
|
||||
lay.addWidget(btn)
|
||||
self._buttons[key] = btn
|
||||
self._buttons["manual"].setChecked(True)
|
||||
self._buttons["staff"].setVisible(False) # staff only
|
||||
|
||||
def set_mode(self, mode: str) -> None:
|
||||
if mode in self._buttons:
|
||||
self._buttons[mode].setChecked(True)
|
||||
|
||||
def set_staff(self, is_staff: bool) -> None:
|
||||
self._buttons["staff"].setVisible(is_staff)
|
||||
|
||||
|
||||
class TopBar(QWidget):
|
||||
"""Top chrome strip. ``mode_changed`` is re-exposed from the toggle."""
|
||||
@@ -143,6 +148,7 @@ class TopBar(QWidget):
|
||||
|
||||
def set_staff(self, is_staff: bool) -> None:
|
||||
self._tools_btn.setVisible(is_staff)
|
||||
self.toggle.set_staff(is_staff)
|
||||
if is_staff != self._is_staff:
|
||||
self._is_staff = is_staff
|
||||
if self._last_baton is not None:
|
||||
|
||||
@@ -26,6 +26,7 @@ class CameraControls(QWidget):
|
||||
screenshot_requested = Signal(str, str) # filename, message
|
||||
face_detect_clicked = Signal()
|
||||
stability_clicked = Signal()
|
||||
metrics_clicked = Signal()
|
||||
|
||||
def __init__(self, palette: Palette, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -33,12 +34,34 @@ class CameraControls(QWidget):
|
||||
lay = QHBoxLayout(self)
|
||||
lay.setContentsMargins(0, 0, 0, 0)
|
||||
lay.setSpacing(6)
|
||||
|
||||
# Inline exposure + gain (always visible, by the camera).
|
||||
lay.addWidget(self._mini_label("Exp"))
|
||||
self._exposure = QDoubleSpinBox()
|
||||
self._exposure.setRange(0.0, 1.0)
|
||||
self._exposure.setDecimals(4)
|
||||
self._exposure.setSingleStep(0.001)
|
||||
self._exposure.setValue(0.02)
|
||||
self._exposure.setSuffix(" s")
|
||||
self._exposure.setFixedWidth(82)
|
||||
self._style_spin(self._exposure)
|
||||
self._exposure.editingFinished.connect(self._emit_samcam)
|
||||
lay.addWidget(self._exposure)
|
||||
lay.addWidget(self._mini_label("Gain"))
|
||||
self._gain = QDoubleSpinBox()
|
||||
self._gain.setRange(0, 1000)
|
||||
self._gain.setValue(1.0)
|
||||
self._gain.setFixedWidth(64)
|
||||
self._style_spin(self._gain)
|
||||
self._gain.editingFinished.connect(self._emit_samcam)
|
||||
lay.addWidget(self._gain)
|
||||
|
||||
for text, tip, slot in (
|
||||
("⚙", "Camera settings (gain / exposure)", self._open_settings),
|
||||
("AF", "Autofocus", self._on_autofocus),
|
||||
("◉", "Screenshot to database", self._open_screenshot),
|
||||
("⊙ Face", "Face detection", self.face_detect_clicked.emit),
|
||||
("∿ Stability", "Target stability", self.stability_clicked.emit),
|
||||
("◧ Metrics", "Prediction metrics", self.metrics_clicked.emit),
|
||||
):
|
||||
b = QPushButton(text)
|
||||
b.setCursor(Qt.PointingHandCursor)
|
||||
@@ -52,28 +75,35 @@ class CameraControls(QWidget):
|
||||
b.clicked.connect(slot)
|
||||
lay.addWidget(b)
|
||||
|
||||
def _open_settings(self) -> None:
|
||||
dlg = QDialog(self)
|
||||
dlg.setWindowTitle("Camera settings")
|
||||
form = QFormLayout(dlg)
|
||||
gain = QDoubleSpinBox()
|
||||
gain.setRange(0, 1000)
|
||||
gain.setValue(1.0)
|
||||
exposure = QDoubleSpinBox()
|
||||
exposure.setRange(0.0, 1.0)
|
||||
exposure.setDecimals(4)
|
||||
exposure.setSingleStep(0.001)
|
||||
exposure.setValue(0.02)
|
||||
form.addRow("Gain", gain)
|
||||
form.addRow("Exposure (s)", exposure)
|
||||
bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
form.addRow(bb)
|
||||
bb.accepted.connect(dlg.accept)
|
||||
bb.rejected.connect(dlg.reject)
|
||||
if dlg.exec() == QDialog.Accepted:
|
||||
self.samcam_changed.emit(
|
||||
SampleCameraSettings(gain=gain.value(), exposure=exposure.value())
|
||||
)
|
||||
def _mini_label(self, text: str):
|
||||
from PySide6.QtWidgets import QLabel
|
||||
lbl = QLabel(text)
|
||||
lbl.setStyleSheet(f"font-size:11px; color:{self._p.text_faint};")
|
||||
return lbl
|
||||
|
||||
def _style_spin(self, spin: QDoubleSpinBox) -> None:
|
||||
spin.setStyleSheet(
|
||||
f"QDoubleSpinBox {{ background:{self._p.surface};"
|
||||
f" border:1px solid {self._p.border_control}; border-radius:6px;"
|
||||
f" padding:2px 4px; font-size:12px; }}"
|
||||
)
|
||||
|
||||
def _emit_samcam(self) -> None:
|
||||
self.samcam_changed.emit(
|
||||
SampleCameraSettings(gain=self._gain.value(),
|
||||
exposure=self._exposure.value()))
|
||||
|
||||
def update_daq_status(self, s) -> None:
|
||||
bl = getattr(s, "bl", None)
|
||||
cam = getattr(bl, "sample_camera", None) if bl else None
|
||||
if cam is None:
|
||||
return
|
||||
for spin, attr in ((self._exposure, "exposure"), (self._gain, "gain")):
|
||||
v = getattr(cam, attr, None)
|
||||
if v is not None and not spin.hasFocus():
|
||||
spin.blockSignals(True)
|
||||
spin.setValue(float(v))
|
||||
spin.blockSignals(False)
|
||||
|
||||
def _on_autofocus(self) -> None:
|
||||
self.autofocus_requested.emit(
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Motors panel (Manual mode, right column): OMEGA, SMARGON, ZOOM, LIGHT.
|
||||
"""Motors panel (right column): OMEGA, SMARGON (+ABR for staff), ZOOM, LIGHT.
|
||||
|
||||
Readouts update from DAQStatusModel; controls emit intent signals the main
|
||||
window wires to DAQWorker (set_omega_rel, move_smargon, zoom, front/back light).
|
||||
window wires to DAQWorker (set_omega_rel, move_smargon, zoom, front/back light,
|
||||
abr_tweak/save/goto). Energy lives in the staff Tools dialog, not here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -14,11 +15,13 @@ from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QSlider,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from aare.common.coordinate import AerotechCoordinate, Coordinate
|
||||
from aare.gui.new_gui.theme import FONT_MONO_FALLBACK, MOTORS_W, Palette
|
||||
from aare.gui.new_gui.widgets.common import hline, section_label
|
||||
|
||||
@@ -28,12 +31,15 @@ JOG_STEPS_UM = (10, 50, 100, 500)
|
||||
|
||||
|
||||
class MotorsPanel(QWidget):
|
||||
omega_rel = Signal(float) # degrees, relative
|
||||
smargon_jog = Signal(float, float) # dx_mm, dy_mm
|
||||
omega_rel = Signal(float) # degrees, relative
|
||||
smargon_jog = Signal(float, float, float) # dx_mm, dy_mm, dz_mm (beamline frame)
|
||||
smargon_angles = Signal(float, float) # absolute chi_deg, phi_deg
|
||||
zoom_changed = Signal(float)
|
||||
front_light_changed = Signal(int)
|
||||
back_light_changed = Signal(int)
|
||||
energy_changed = Signal(float) # keV (staff only)
|
||||
abr_tweak = Signal(object) # AerotechCoordinate (staff)
|
||||
abr_save = Signal()
|
||||
abr_goto = Signal()
|
||||
|
||||
def __init__(self, palette: Palette, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -42,7 +48,6 @@ class MotorsPanel(QWidget):
|
||||
self._p = palette
|
||||
self._jog_idx = 2 # default 100 µm
|
||||
|
||||
from PySide6.QtWidgets import QScrollArea
|
||||
scroll = QScrollArea(self)
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QScrollArea.NoFrame)
|
||||
@@ -58,19 +63,18 @@ class MotorsPanel(QWidget):
|
||||
|
||||
# ---- OMEGA ----
|
||||
lay.addWidget(section_label("OMEGA"))
|
||||
lay.addSpacing(10)
|
||||
lay.addSpacing(8)
|
||||
row = QHBoxLayout()
|
||||
self._omega_val = QLabel("0.00°")
|
||||
self._omega_val.setStyleSheet(
|
||||
f"font-family:{FONT_MONO_FALLBACK}; font-size:22px; font-weight:600;"
|
||||
)
|
||||
f"font-family:{FONT_MONO_FALLBACK}; font-size:22px; font-weight:600;")
|
||||
row.addWidget(self._omega_val)
|
||||
row.addStretch(1)
|
||||
wsym = QLabel("ω")
|
||||
wsym.setStyleSheet(f"font-size:11px; color:{palette.text_faint};")
|
||||
row.addWidget(wsym)
|
||||
lay.addLayout(row)
|
||||
lay.addSpacing(10)
|
||||
lay.addSpacing(8)
|
||||
grid = QGridLayout()
|
||||
grid.setSpacing(6)
|
||||
for i, step in enumerate(OMEGA_STEPS):
|
||||
@@ -80,58 +84,109 @@ class MotorsPanel(QWidget):
|
||||
grid.addWidget(btn, i // 3, i % 3)
|
||||
lay.addLayout(grid)
|
||||
|
||||
lay.addSpacing(16)
|
||||
lay.addSpacing(14)
|
||||
lay.addWidget(hline(palette))
|
||||
lay.addSpacing(16)
|
||||
lay.addSpacing(14)
|
||||
|
||||
# ---- SMARGON ----
|
||||
# ---- SMARGON (compact) ----
|
||||
lay.addWidget(section_label("SMARGON"))
|
||||
lay.addSpacing(10)
|
||||
readouts = QHBoxLayout()
|
||||
self._chi_val = self._readout(readouts, "Chi", palette)
|
||||
self._phi_val = self._readout(readouts, "Phi", palette)
|
||||
lay.addLayout(readouts)
|
||||
lay.addSpacing(10)
|
||||
lay.addSpacing(8)
|
||||
|
||||
# editable chi / phi + Set
|
||||
ang = QHBoxLayout()
|
||||
ang.setSpacing(6)
|
||||
ang.addWidget(self._small_label("Chi"))
|
||||
self._chi = self._angle_spin(-90, 90)
|
||||
ang.addWidget(self._chi)
|
||||
ang.addWidget(self._small_label("Phi"))
|
||||
self._phi = self._angle_spin(-180, 180)
|
||||
ang.addWidget(self._phi)
|
||||
set_ang = QPushButton("Set")
|
||||
set_ang.setCursor(Qt.PointingHandCursor)
|
||||
set_ang.clicked.connect(
|
||||
lambda: self.smargon_angles.emit(self._chi.value(), self._phi.value()))
|
||||
ang.addWidget(set_ang)
|
||||
lay.addLayout(ang)
|
||||
lay.addSpacing(8)
|
||||
|
||||
# XY jog pad + focus (Z)
|
||||
padrow = QHBoxLayout()
|
||||
pad = QGridLayout()
|
||||
pad.setSpacing(6)
|
||||
up = self._jog_btn("↑")
|
||||
down = self._jog_btn("↓")
|
||||
left = self._jog_btn("←")
|
||||
right = self._jog_btn("→")
|
||||
self._center = QPushButton("100µm")
|
||||
self._center.setCursor(Qt.PointingHandCursor)
|
||||
self._center.setStyleSheet(
|
||||
f"QPushButton {{ background:{palette.surface};"
|
||||
f" border:1px solid {palette.border_control}; border-radius:6px;"
|
||||
f" font-size:10px; color:{palette.text_faint}; padding:7px 0; }}"
|
||||
)
|
||||
pad.setSpacing(5)
|
||||
up, down = self._jog_btn("↑"), self._jog_btn("↓")
|
||||
left, right = self._jog_btn("←"), self._jog_btn("→")
|
||||
self._center = self._jog_btn("100µm")
|
||||
self._center.clicked.connect(self._cycle_step)
|
||||
up.clicked.connect(lambda: self._jog(0, 1))
|
||||
down.clicked.connect(lambda: self._jog(0, -1))
|
||||
left.clicked.connect(lambda: self._jog(-1, 0))
|
||||
right.clicked.connect(lambda: self._jog(1, 0))
|
||||
up.clicked.connect(lambda: self._jog(0, 1, 0))
|
||||
down.clicked.connect(lambda: self._jog(0, -1, 0))
|
||||
left.clicked.connect(lambda: self._jog(-1, 0, 0))
|
||||
right.clicked.connect(lambda: self._jog(1, 0, 0))
|
||||
pad.addWidget(up, 0, 1)
|
||||
pad.addWidget(left, 1, 0)
|
||||
pad.addWidget(self._center, 1, 1)
|
||||
pad.addWidget(right, 1, 2)
|
||||
pad.addWidget(down, 2, 1)
|
||||
padw = QWidget()
|
||||
padw.setLayout(pad)
|
||||
padw.setFixedWidth(150)
|
||||
center_pad = QHBoxLayout()
|
||||
center_pad.addStretch(1)
|
||||
center_pad.addWidget(padw)
|
||||
center_pad.addStretch(1)
|
||||
lay.addLayout(center_pad)
|
||||
padw = QWidget(); padw.setLayout(pad); padw.setFixedWidth(132)
|
||||
padrow.addWidget(padw)
|
||||
# focus (Z) column
|
||||
foc = QVBoxLayout()
|
||||
foc.setSpacing(5)
|
||||
foc.addWidget(self._small_label("Focus (Z)"))
|
||||
fplus = self._jog_btn("+")
|
||||
fminus = self._jog_btn("-")
|
||||
fplus.clicked.connect(lambda: self._jog(0, 0, 1))
|
||||
fminus.clicked.connect(lambda: self._jog(0, 0, -1))
|
||||
foc.addWidget(fplus)
|
||||
foc.addWidget(fminus)
|
||||
foc.addStretch(1)
|
||||
padrow.addLayout(foc)
|
||||
lay.addLayout(padrow)
|
||||
|
||||
lay.addSpacing(16)
|
||||
# ---- ABR (staff only) ----
|
||||
self._abr_section = QWidget()
|
||||
ab = QVBoxLayout(self._abr_section)
|
||||
ab.setContentsMargins(0, 0, 0, 0)
|
||||
ab.setSpacing(0)
|
||||
ab.addSpacing(14)
|
||||
ab.addWidget(hline(palette))
|
||||
ab.addSpacing(14)
|
||||
ab.addWidget(section_label("ABR · meas. pos"))
|
||||
ab.addSpacing(6)
|
||||
steprow = QHBoxLayout()
|
||||
steprow.addWidget(self._small_label("Step"))
|
||||
self._abr_step = QDoubleSpinBox(); self._abr_step.setRange(1, 1000)
|
||||
self._abr_step.setValue(10); self._abr_step.setSuffix(" µm")
|
||||
steprow.addWidget(self._abr_step); steprow.addStretch(1)
|
||||
ab.addLayout(steprow)
|
||||
abr_grid = QGridLayout(); abr_grid.setSpacing(5)
|
||||
self._abr_labels = {}
|
||||
for r, axis in enumerate(("x", "y", "z")):
|
||||
abr_grid.addWidget(self._small_label(f"GM{axis.upper()}"), r, 0)
|
||||
m = self._jog_btn("−"); p = self._jog_btn("+")
|
||||
m.clicked.connect(lambda _=False, a=axis: self._abr(a, -1))
|
||||
p.clicked.connect(lambda _=False, a=axis: self._abr(a, +1))
|
||||
val = QLabel("—")
|
||||
val.setStyleSheet(f"font-family:{FONT_MONO_FALLBACK}; font-size:11px;")
|
||||
self._abr_labels[axis] = val
|
||||
abr_grid.addWidget(m, r, 1); abr_grid.addWidget(p, r, 2); abr_grid.addWidget(val, r, 3)
|
||||
ab.addLayout(abr_grid)
|
||||
abr_btns = QHBoxLayout()
|
||||
save = QPushButton("Save"); save.clicked.connect(self.abr_save)
|
||||
goto = QPushButton("Goto"); goto.clicked.connect(self.abr_goto)
|
||||
for b in (save, goto):
|
||||
b.setCursor(Qt.PointingHandCursor)
|
||||
abr_btns.addWidget(save); abr_btns.addWidget(goto)
|
||||
ab.addLayout(abr_btns)
|
||||
self._abr_section.setVisible(False)
|
||||
lay.addWidget(self._abr_section)
|
||||
|
||||
lay.addSpacing(14)
|
||||
lay.addWidget(hline(palette))
|
||||
lay.addSpacing(16)
|
||||
lay.addSpacing(14)
|
||||
|
||||
# ---- ZOOM ----
|
||||
lay.addWidget(section_label("ZOOM"))
|
||||
lay.addSpacing(10)
|
||||
lay.addSpacing(8)
|
||||
zoom_row = QHBoxLayout()
|
||||
zoom_row.setSpacing(6)
|
||||
self._zoom_group = QButtonGroup(self)
|
||||
@@ -150,88 +205,55 @@ class MotorsPanel(QWidget):
|
||||
zoom_row.addStretch(1)
|
||||
lay.addLayout(zoom_row)
|
||||
|
||||
lay.addSpacing(16)
|
||||
lay.addSpacing(14)
|
||||
lay.addWidget(hline(palette))
|
||||
lay.addSpacing(16)
|
||||
lay.addSpacing(14)
|
||||
|
||||
# ---- LIGHT ----
|
||||
lay.addWidget(section_label("LIGHT"))
|
||||
lay.addSpacing(8)
|
||||
self._front = self._light_slider(lay, "Front", palette, self.front_light_changed)
|
||||
lay.addSpacing(10)
|
||||
self._front = self._light_slider(lay, "Front", palette,
|
||||
self.front_light_changed)
|
||||
lay.addSpacing(12)
|
||||
self._back = self._light_slider(lay, "Back", palette,
|
||||
self.back_light_changed)
|
||||
|
||||
# ---- ENERGY (staff only) ----
|
||||
self._energy_section = QWidget()
|
||||
es = QVBoxLayout(self._energy_section)
|
||||
es.setContentsMargins(0, 0, 0, 0)
|
||||
es.setSpacing(0)
|
||||
es.addSpacing(16)
|
||||
es.addWidget(hline(palette))
|
||||
es.addSpacing(16)
|
||||
es.addWidget(section_label("ENERGY"))
|
||||
es.addSpacing(10)
|
||||
erow = QHBoxLayout()
|
||||
erow.setSpacing(6)
|
||||
self._energy_spin = QDoubleSpinBox()
|
||||
self._energy_spin.setDecimals(3)
|
||||
self._energy_spin.setRange(4.0, 25.0)
|
||||
self._energy_spin.setSingleStep(0.01)
|
||||
self._energy_spin.setSuffix(" keV")
|
||||
self._energy_spin.setValue(12.4)
|
||||
self._energy_spin.setStyleSheet(
|
||||
f"QDoubleSpinBox {{ font-family:{FONT_MONO_FALLBACK}; font-size:14px;"
|
||||
f" background:{palette.surface}; border:1px solid {palette.border_control};"
|
||||
f" border-radius:6px; padding:5px 8px; color:{palette.text_primary}; }}"
|
||||
)
|
||||
set_btn = QPushButton("Set")
|
||||
set_btn.setCursor(Qt.PointingHandCursor)
|
||||
set_btn.setStyleSheet(
|
||||
f"QPushButton {{ background:{palette.accent}; color:#fff; border:none;"
|
||||
f" border-radius:6px; padding:6px 14px; font-weight:600; }}"
|
||||
)
|
||||
set_btn.clicked.connect(
|
||||
lambda: self.energy_changed.emit(self._energy_spin.value())
|
||||
)
|
||||
erow.addWidget(self._energy_spin, 1)
|
||||
erow.addWidget(set_btn)
|
||||
es.addLayout(erow)
|
||||
self._energy_section.setVisible(False)
|
||||
lay.addWidget(self._energy_section)
|
||||
|
||||
self._back = self._light_slider(lay, "Back", palette, self.back_light_changed)
|
||||
lay.addStretch(1)
|
||||
|
||||
def set_staff(self, is_staff: bool) -> None:
|
||||
self._energy_section.setVisible(is_staff)
|
||||
self._abr_section.setVisible(is_staff)
|
||||
|
||||
# -------- helpers --------
|
||||
def _readout(self, lay: QHBoxLayout, name: str, p: Palette) -> QLabel:
|
||||
box = QVBoxLayout()
|
||||
box.setSpacing(0)
|
||||
lab = QLabel(name)
|
||||
lab.setStyleSheet(f"font-size:10.5px; color:{p.text_faint};")
|
||||
val = QLabel("0.0°")
|
||||
val.setStyleSheet(f"font-family:{FONT_MONO_FALLBACK}; font-size:15px;")
|
||||
box.addWidget(lab)
|
||||
box.addWidget(val)
|
||||
wrap = QWidget()
|
||||
wrap.setLayout(box)
|
||||
lay.addWidget(wrap, 1)
|
||||
return val
|
||||
def _small_label(self, text: str) -> QLabel:
|
||||
lbl = QLabel(text)
|
||||
lbl.setStyleSheet(f"font-size:10.5px; color:{self._p.text_faint};")
|
||||
return lbl
|
||||
|
||||
def _angle_spin(self, lo: float, hi: float) -> QDoubleSpinBox:
|
||||
s = QDoubleSpinBox()
|
||||
s.setRange(lo, hi)
|
||||
s.setDecimals(1)
|
||||
s.setSuffix("°")
|
||||
s.setFixedWidth(64)
|
||||
s.setStyleSheet(
|
||||
f"QDoubleSpinBox {{ font-family:{FONT_MONO_FALLBACK}; font-size:12px;"
|
||||
f" background:{self._p.surface}; border:1px solid {self._p.border_control};"
|
||||
f" border-radius:6px; padding:3px 4px; }}"
|
||||
)
|
||||
return s
|
||||
|
||||
def _jog_btn(self, glyph: str) -> QPushButton:
|
||||
b = QPushButton(glyph)
|
||||
b.setCursor(Qt.PointingHandCursor)
|
||||
b.setStyleSheet(
|
||||
f"QPushButton {{ background:{self._p.surface};"
|
||||
f" border:1px solid {self._p.border_control}; border-radius:6px;"
|
||||
f" font-size:11px; padding:6px 0; color:{self._p.text_secondary}; }}"
|
||||
)
|
||||
return b
|
||||
|
||||
def _light_slider(self, lay: QVBoxLayout, name: str, p: Palette,
|
||||
signal) -> QSlider:
|
||||
def _light_slider(self, lay: QVBoxLayout, name: str, p: Palette, signal) -> QSlider:
|
||||
lbl = QLabel(name)
|
||||
lbl.setStyleSheet(f"font-size:10.5px; color:{p.text_faint};")
|
||||
lay.addWidget(lbl)
|
||||
lay.addSpacing(5)
|
||||
lay.addSpacing(4)
|
||||
s = QSlider(Qt.Horizontal)
|
||||
s.setRange(0, 100)
|
||||
s.setStyleSheet(self._slider_qss(p))
|
||||
@@ -255,22 +277,26 @@ class MotorsPanel(QWidget):
|
||||
if b.isChecked():
|
||||
b.setStyleSheet(
|
||||
f"QPushButton {{ background:{p.accent}; color:#fff; border:none;"
|
||||
f" border-radius:6px; font-size:11px; font-weight:600; padding:5px 10px; }}"
|
||||
)
|
||||
f" border-radius:6px; font-size:11px; font-weight:600; padding:5px 10px; }}")
|
||||
else:
|
||||
b.setStyleSheet(
|
||||
f"QPushButton {{ background:{p.surface};"
|
||||
f" border:1px solid {p.border_control}; border-radius:6px;"
|
||||
f" font-size:11px; padding:5px 10px; }}"
|
||||
)
|
||||
f" font-size:11px; padding:5px 10px; }}")
|
||||
|
||||
def _cycle_step(self) -> None:
|
||||
self._jog_idx = (self._jog_idx + 1) % len(JOG_STEPS_UM)
|
||||
self._center.setText(f"{JOG_STEPS_UM[self._jog_idx]}µm")
|
||||
|
||||
def _jog(self, sx: int, sy: int) -> None:
|
||||
def _jog(self, sx: int, sy: int, sz: int) -> None:
|
||||
step_mm = JOG_STEPS_UM[self._jog_idx] / 1000.0
|
||||
self.smargon_jog.emit(sx * step_mm, sy * step_mm)
|
||||
self.smargon_jog.emit(sx * step_mm, sy * step_mm, sz * step_mm)
|
||||
|
||||
def _abr(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.abr_tweak.emit(AerotechCoordinate(at_mm=Coordinate(**kw), omega_deg=None))
|
||||
|
||||
# -------- live updates --------
|
||||
def update_daq_status(self, s) -> None:
|
||||
@@ -281,12 +307,14 @@ class MotorsPanel(QWidget):
|
||||
self._omega_val.setText(f"{omega:.2f}°")
|
||||
smg = getattr(geom, "smargon", None)
|
||||
if smg is not None:
|
||||
chi = getattr(smg, "chi_deg", None)
|
||||
phi = getattr(smg, "phi_deg", None)
|
||||
if chi is not None:
|
||||
self._chi_val.setText(f"{chi:.1f}°")
|
||||
if phi is not None:
|
||||
self._phi_val.setText(f"{phi:.1f}°")
|
||||
self._sync_spin(self._chi, getattr(smg, "chi_deg", None))
|
||||
self._sync_spin(self._phi, getattr(smg, "phi_deg", None))
|
||||
meas = getattr(geom, "aerotech_meas", None)
|
||||
if meas is not None:
|
||||
for axis in ("x", "y", "z"):
|
||||
v = getattr(meas, axis, None)
|
||||
if v is not None:
|
||||
self._abr_labels[axis].setText(f"{v:.4f}")
|
||||
bl = getattr(s, "bl", None)
|
||||
if bl is not None:
|
||||
zoom = getattr(bl, "zoom", None)
|
||||
@@ -294,15 +322,16 @@ class MotorsPanel(QWidget):
|
||||
self._sync_zoom(zoom)
|
||||
self._sync_slider(self._front, getattr(bl, "front_light", None))
|
||||
self._sync_slider(self._back, getattr(bl, "back_light", None))
|
||||
diff = getattr(s, "diffraction", None)
|
||||
energy = getattr(diff, "energy_keV", None) if diff else None
|
||||
if energy is not None and not self._energy_spin.hasFocus():
|
||||
self._energy_spin.blockSignals(True)
|
||||
self._energy_spin.setValue(float(energy))
|
||||
self._energy_spin.blockSignals(False)
|
||||
|
||||
@staticmethod
|
||||
def _sync_spin(spin: QDoubleSpinBox, value) -> None:
|
||||
if value is None or spin.hasFocus():
|
||||
return
|
||||
spin.blockSignals(True)
|
||||
spin.setValue(float(value))
|
||||
spin.blockSignals(False)
|
||||
|
||||
def _sync_zoom(self, zoom: float) -> None:
|
||||
# check the nearest level without re-emitting
|
||||
nearest = min(ZOOM_LEVELS, key=lambda z: abs(z - zoom))
|
||||
btn = self._zoom_btns.get(nearest)
|
||||
if btn and not btn.isChecked():
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Compact ML prediction-metrics dialog: per-class counts, confidence, FPS.
|
||||
|
||||
Fed by the PredictionSubscriber ``prediction`` stream (throttled). A lightweight
|
||||
version of the old prediction-metrics panel (no rolling chart).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from PySide6.QtCore import Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from aare.gui.new_gui.theme import build_qss
|
||||
|
||||
|
||||
class PredictionMetricsDialog(QDialog):
|
||||
def __init__(self, palette, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Prediction metrics")
|
||||
self.setModal(False)
|
||||
self.resize(360, 360)
|
||||
self.setStyleSheet(build_qss(palette))
|
||||
self._last = 0.0
|
||||
self._frame_ts: list[float] = []
|
||||
|
||||
lay = QVBoxLayout(self)
|
||||
self._summary = QLabel("Waiting for predictions…")
|
||||
self._summary.setStyleSheet(f"color:{palette.text_muted};")
|
||||
lay.addWidget(self._summary)
|
||||
self._table = QTableWidget(0, 2)
|
||||
self._table.setHorizontalHeaderLabels(["Class", "Count"])
|
||||
self._table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
self._table.verticalHeader().setVisible(False)
|
||||
lay.addWidget(self._table)
|
||||
|
||||
@Slot(dict)
|
||||
def update_from_prediction(self, payload: dict) -> None:
|
||||
if not self.isVisible() or not isinstance(payload, dict):
|
||||
return
|
||||
now = time.monotonic()
|
||||
if now - self._last < 0.1: # throttle to ~10 Hz
|
||||
return
|
||||
self._last = now
|
||||
self._frame_ts.append(now)
|
||||
self._frame_ts = [t for t in self._frame_ts if now - t <= 2.0]
|
||||
fps = len(self._frame_ts) / 2.0
|
||||
|
||||
boxes = payload.get("boxes", []) or []
|
||||
if isinstance(boxes, dict):
|
||||
boxes = list(boxes.values())
|
||||
counts: dict[str, int] = {}
|
||||
confs: list[float] = []
|
||||
for b in boxes:
|
||||
if not isinstance(b, dict):
|
||||
continue
|
||||
label = str(b.get("label", b.get("cls", "?")))
|
||||
counts[label] = counts.get(label, 0) + 1
|
||||
c = b.get("conf", b.get("confidence"))
|
||||
if isinstance(c, (int, float)):
|
||||
confs.append(float(c))
|
||||
mean_c = sum(confs) / len(confs) if confs else 0.0
|
||||
max_c = max(confs) if confs else 0.0
|
||||
self._summary.setText(
|
||||
f"{len(boxes)} detections · mean conf {mean_c:.2f} · "
|
||||
f"max {max_c:.2f} · {fps:.0f} fps")
|
||||
self._table.setRowCount(0)
|
||||
for name in sorted(counts):
|
||||
r = self._table.rowCount()
|
||||
self._table.insertRow(r)
|
||||
self._table.setItem(r, 0, QTableWidgetItem(name))
|
||||
self._table.setItem(r, 1, QTableWidgetItem(str(counts[name])))
|
||||
Reference in New Issue
Block a user