diff --git a/src/aare/gui/new_gui/README.md b/src/aare/gui/new_gui/README.md index ad88cbf6..31c6b5f8 100644 --- a/src/aare/gui/new_gui/README.md +++ b/src/aare/gui/new_gui/README.md @@ -26,8 +26,11 @@ panel — are **beam-path rails**: the completed length glows green and the acti station is a lavender focal bloom. **Manual sample:** if a sample is on the goniometer by hand (not in the DB), use -**⊕ Manual sample on gonio** in the changer footer — it unlocks the cockpit -without driving the robot (no `mount`/`unmount` calls). +**⊕ Manual sample on gonio** in the changer footer. It opens a dialog (custom +name + optional unit cell), then `POST /sample/manual` (`DAQWorker.sample_manual`) +so the backend creates a DB entry, assigns a `db_id` and sets it as the current +sample — mirroring the old GUI. No robot mount happens (the sample is already on +the gonio); the cockpit unlocks locally with the new sample. **Offline dev mode:** with no server (`base_url is None`, e.g. `BEAMLINE=SIMULATED` and no `-u`), `dev_seed.py` injects a few fake samples + a synthetic live status + diff --git a/src/aare/gui/new_gui/main_window.py b/src/aare/gui/new_gui/main_window.py index 77faf9c5..94cac82c 100644 --- a/src/aare/gui/new_gui/main_window.py +++ b/src/aare/gui/new_gui/main_window.py @@ -304,6 +304,8 @@ class MainWindow(QWidget): self.manual.xrf_scan_requested.connect(self._do_xrf) self.manual.raster_goto_requested.connect(self.daq.move_smargon) self.manual.bookmark_goto_requested.connect(self._on_bookmark_goto) + if hasattr(self.daq, "sample_manual"): + self.manual.manual_sample_requested.connect(self.daq.sample_manual) self.manual.status_message.connect(self._note) if hasattr(self.daq, "run_number_incremented"): self.daq.run_number_incremented.connect( diff --git a/src/aare/gui/new_gui/manual_view.py b/src/aare/gui/new_gui/manual_view.py index bcc25ca5..058b08e6 100644 --- a/src/aare/gui/new_gui/manual_view.py +++ b/src/aare/gui/new_gui/manual_view.py @@ -40,6 +40,7 @@ class ManualView(QWidget): xrf_scan_requested = Signal(object) # FluorescenceSpectrumParameterModel raster_goto_requested = Signal(object) # SmargonCoordinate (goto a grid) bookmark_goto_requested = Signal(object, float) # SmargonCoordinate, omega_deg + manual_sample_requested = Signal(object) # SampleShortInfo -> POST /sample/manual status_message = Signal(str) # transient note to surface in UI def __init__(self, state: AppState, palette: Palette, defaults: dict, parent=None): @@ -137,7 +138,7 @@ class ManualView(QWidget): # changer / mount CTAs self.changer.sample_selected.connect(s.mount_sample_obj) - self.changer.manual_mount_requested.connect(s.manual_mount) + self.changer.manual_mount_requested.connect(self._on_manual_mount) self.camera.mount_clicked.connect(s.open_picker) # pipeline interactions @@ -311,6 +312,23 @@ class ManualView(QWidget): else: self.status_message.emit("Grid has no stored position to go to.") + # -------------------------------------------------- manual sample + def _on_manual_mount(self) -> None: + from PySide6.QtWidgets import QDialog + from aare.gui.new_gui.widgets.manual_sample_dialog import ManualSampleDialog + pgroup = "p16371" + sess = getattr(self._last_status, "session", None) + if sess is not None: + pgroup = getattr(sess, "current_pgroup", None) or pgroup + dlg = ManualSampleDialog(self._p, pgroup, self) + if dlg.exec() != QDialog.Accepted: + return + sample = dlg.sample() + # Tell the backend to create the DB entry + set it as current sample, + # then unlock the cockpit locally with the new sample. + self.manual_sample_requested.emit(sample) + self._state.manual_mount(sample) + # ----------------------------------------------------- bookmarks def _refresh_bookmark_bar(self) -> None: self.pipeline.bookmarks.set_bookmarks([b["label"] for b in self._bookmarks]) diff --git a/src/aare/gui/new_gui/widgets/manual_sample_dialog.py b/src/aare/gui/new_gui/widgets/manual_sample_dialog.py new file mode 100644 index 00000000..a473d593 --- /dev/null +++ b/src/aare/gui/new_gui/widgets/manual_sample_dialog.py @@ -0,0 +1,107 @@ +"""Create a manual sample — one a user placed on the goniometer by hand. + +Mirrors the old GUI's Manual Sample panel: a custom name (+ optional unit cell). +On accept it yields a ``SampleShortInfo`` with ``db_id=-1``; the backend's +``POST /sample/manual`` creates the DB entry, assigns the real id and makes it the +current sample. No robot mount happens (the sample is already on the gonio). +""" + +from __future__ import annotations + +from PySide6.QtWidgets import ( + QCheckBox, + QDialog, + QDialogButtonBox, + QDoubleSpinBox, + QGridLayout, + QLabel, + QLineEdit, + QSpinBox, + QVBoxLayout, +) + +from aare.common.models import SampleShortInfo +from aare.gui.new_gui.theme import build_qss + + +class ManualSampleDialog(QDialog): + def __init__(self, palette, pgroup: str = "p16371", parent=None): + super().__init__(parent) + self._pgroup = pgroup or "p16371" + self.setWindowTitle("Manual sample on goniometer") + self.setModal(True) + self.setStyleSheet(build_qss(palette)) + self.setMinimumWidth(320) + + root = QVBoxLayout(self) + intro = QLabel("Register a sample you placed on the goniometer by hand.") + intro.setWordWrap(True) + intro.setStyleSheet(f"color:{palette.text_muted}; font-size:12px;") + root.addWidget(intro) + + grid = QGridLayout() + grid.setColumnStretch(1, 1) + grid.addWidget(QLabel("Name"), 0, 0) + self._name = QLineEdit("Lyso") + grid.addWidget(self._name, 0, 1, 1, 2) + + self._unit_cell = QCheckBox("Provide unit cell") + grid.addWidget(self._unit_cell, 1, 1, 1, 2) + + self._a = self._num(5, 1000, 39.0) + self._b = self._num(5, 1000, 78.0) + self._c = self._num(5, 1000, 78.0) + self._alpha = self._num(30, 330, 90.0) + self._beta = self._num(30, 330, 90.0) + self._gamma = self._num(30, 330, 90.0) + for row, (lbl, w, unit) in enumerate(( + ("a", self._a, "Å"), ("b", self._b, "Å"), ("c", self._c, "Å"), + ("α", self._alpha, "°"), ("β", self._beta, "°"), ("γ", self._gamma, "°"), + ), start=2): + grid.addWidget(QLabel(lbl), row, 0) + grid.addWidget(w, row, 1) + grid.addWidget(QLabel(unit), row, 2) + grid.addWidget(QLabel("SG"), 8, 0) + self._sg = QSpinBox() + self._sg.setRange(1, 250) + self._sg.setValue(1) + grid.addWidget(self._sg, 8, 1) + root.addLayout(grid) + + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttons.button(QDialogButtonBox.Ok).setText("Create + mount") + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + root.addWidget(buttons) + + self._unit_cell.toggled.connect(self._toggle_cell) + self._toggle_cell(False) + + def _num(self, lo: float, hi: float, val: float) -> QDoubleSpinBox: + s = QDoubleSpinBox() + s.setRange(lo, hi) + s.setDecimals(2) + s.setValue(val) + return s + + def _toggle_cell(self, on: bool) -> None: + for w in (self._a, self._b, self._c, self._alpha, self._beta, + self._gamma, self._sg): + w.setEnabled(on) + + def sample(self) -> SampleShortInfo: + params = None + if self._unit_cell.isChecked(): + from aareDB import DataCollectionParameters + params = DataCollectionParameters( + spacegroupnumber=int(self._sg.value()), + cellparameters=( + f"{self._a.value():.2f} {self._b.value():.2f} " + f"{self._c.value():.2f} {self._alpha.value():.2f} " + f"{self._beta.value():.2f} {self._gamma.value():.2f}"), + ) + return SampleShortInfo( + db_id=-1, puck_name="", dewar_name="", + sample_name=self._name.text().strip() or "Manual sample", + run_number=1, pin=1, aaredb_params=params, user=self._pgroup, + )