new_gui: manual sample creates a real DB entry via /sample/manual

Replace the GUI-local placeholder with the old GUI's flow: a dialog (custom name
+ optional unit cell) builds a SampleShortInfo(db_id=-1, ...) and posts it via
DAQWorker.sample_manual -> POST /sample/manual, so the backend creates the DB
entry, assigns the db_id and sets it as current sample. Cockpit then unlocks
locally with the new sample (no robot mount; sample already on the gonio).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
appleb_m
2026-06-25 16:41:11 +02:00
co-authored by Claude Opus 4.8
parent 02f33bd3fe
commit bb55811781
4 changed files with 133 additions and 3 deletions
+5 -2
View File
@@ -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 +
+2
View File
@@ -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(
+19 -1
View File
@@ -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])
@@ -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,
)