From 963af28cf4d39a96bdba831367e178b6d9b2f951 Mon Sep 17 00:00:00 2001 From: appleb_m Date: Thu, 25 Jun 2026 10:20:01 +0200 Subject: [PATCH] new_gui: offline dev-seed (fake samples + status) for SIMULATED standalone When there is no server (base_url is None), seed fake samples, a synthetic live status (incl. a real SampleGeometryModel so bookmark/click projections work), and a fake camera frame, so Manual mount -> pipeline/motors/bookmarks can be tested without a backend. Gated to no-server; never affects real runs. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/aare/gui/new_gui/README.md | 6 ++ src/aare/gui/new_gui/dev_seed.py | 121 ++++++++++++++++++++++++++++ src/aare/gui/new_gui/main_window.py | 15 ++++ 3 files changed, 142 insertions(+) create mode 100644 src/aare/gui/new_gui/dev_seed.py diff --git a/src/aare/gui/new_gui/README.md b/src/aare/gui/new_gui/README.md index fa558294..cbbeda50 100644 --- a/src/aare/gui/new_gui/README.md +++ b/src/aare/gui/new_gui/README.md @@ -16,6 +16,12 @@ uv run python -m aare.gui.new_gui.app -u https://host -c /path/to.crt -p tcp://h `BEAMLINE` (X06DA / X10SA / X06SA / unset→SIMULATED) drives all config via `cfg_get`, exactly like the existing GUI. +**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 + +a fake camera frame so the full Manual flow (mount → pipeline, motors, bookmarks) +is testable without a backend. It activates *only* when there's no server and +never affects real runs. + ## Layout | File | Responsibility | diff --git a/src/aare/gui/new_gui/dev_seed.py b/src/aare/gui/new_gui/dev_seed.py new file mode 100644 index 00000000..989be650 --- /dev/null +++ b/src/aare/gui/new_gui/dev_seed.py @@ -0,0 +1,121 @@ +"""Dev-only seed for SIMULATED standalone (no DAQ server). + +When the GUI runs with no backend (base_url is None) there is no /spreadsheet, +camera or /status, so the Sample Changer is empty and nothing can be mounted. +This feeds a handful of fake samples + a synthetic live status + a fake camera +frame so the whole Manual flow (mount → pipeline, motors, bookmarks) can be +exercised offline. It is created ONLY when base_url is None and never touches a +real run. +""" + +from __future__ import annotations + +import math +from types import SimpleNamespace + +from PySide6.QtCore import QObject, QTimer +from PySide6.QtGui import QColor, QPainter, QPixmap + +from aare.common.coordinate import Coordinate, SmargonCoordinate +from aare.common.models import DewarAddress, SampleShortInfo, SampleShortInfoList +from aare.common.sample_geometry import SampleGeometryModel + + +def _fake_samples() -> SampleShortInfoList: + samples = [] + for i in range(1, 6): + samples.append(SampleShortInfo( + db_id=i, puck_name="PSIMX006", dewar_name="X06DA-2", + sample_name=f"lyso_bromid_{i}", run_number=1, pin=i, + location=DewarAddress(segment="C", pos=i), + )) + for i in range(1, 4): + samples.append(SampleShortInfo( + db_id=100 + i, puck_name="PSIMX043", dewar_name="X06DA-2", + sample_name=f"thaum_nat_{i}", run_number=1, pin=i, + location=DewarAddress(segment="A", pos=i), + )) + return SampleShortInfoList(s=samples) + + +def _make_frame() -> QPixmap: + pm = QPixmap(640, 480) + pm.fill(QColor("#14130f")) + p = QPainter(pm) + p.setRenderHint(QPainter.Antialiasing) + p.setPen(QColor("#3a352c")) + p.setBrush(QColor("#241f18")) + p.drawEllipse(250, 150, 150, 180) # loop + p.setBrush(QColor("#3a342a")) + p.drawEllipse(285, 195, 80, 95) # inner + p.setBrush(QColor("#6b6150")) + p.drawEllipse(310, 225, 30, 34) # "crystal" + p.end() + return pm + + +class DevSeed(QObject): + def __init__(self, mw): + super().__init__(mw) + self._mw = mw + self._t = 0.0 + self._frame = _make_frame() + + # samples + control baton (you hold it, staff) + mw._on_spreadsheet(_fake_samples()) + baton = SimpleNamespace( + you_are_holder=True, you_have_pending_request=False, + incoming_request=False, + holder=SimpleNamespace(username="devuser", is_staff=True, pgroup="p16371")) + mw._on_baton_status(baton) + mw.top_bar.update_baton_status(baton) + + self._timer = QTimer(self) + self._timer.setInterval(500) + self._timer.timeout.connect(self._tick) + self._timer.start() + self._tick() + + def _tick(self) -> None: + self._t += 0.5 + # gentle sample-holder drift so successive bookmarks land apart + dx = 0.03 * math.sin(self._t / 3.0) + dy = 0.02 * math.cos(self._t / 4.0) + geom = SampleGeometryModel( + beam_location_pxl=Coordinate(x=320, y=240, z=0), + pixel_in_mm=0.001, + aerotech=Coordinate(x=0, y=0, z=0), + aerotech_meas=Coordinate(x=0.0, y=0.0, z=18.0), + smargon=SmargonCoordinate( + sh_mm=Coordinate(x=dx, y=dy, z=18.0), phi_deg=0.0, chi_deg=0.0), + omega_deg=0.0, + beam_size_mm=Coordinate(x=0.05, y=0.03, z=0.001), + ) + status = SimpleNamespace( + bl=SimpleNamespace( + flux_ph_s=4.0e11, ring_current_mA=401.0, cryojet_K=100.1, + shutter_open=False, exp_shutter_open=True, transmission=0.1, + zoom=2.0, front_light=80, back_light=45, + sample_camera=SimpleNamespace(gain=1.0, exposure=0.02), + pss_prohibited=True, pss_alarm=False), + geom=geom, + diffraction=SimpleNamespace( + wavelength_angstrom=1.0, energy_keV=12.4, beam_center_pxl=(960.0, 540.0)), + state=SimpleNamespace(display_name=lambda: "Sample alignment", + name="SampleAlignment"), + session=SimpleNamespace(staff=True, current_pgroup="p16371"), + sample=None, busy=False, tell_connected=True, + ) + for sink in (self._mw.status_bar.update_daq_status, + self._mw.manual.update_daq_status, + self._mw.staff.update_daq_status, + self._mw._on_status): + try: + sink(status) + except Exception: + pass + self._mw.manual.camera.update_pixmap(self._frame) + self._mw.staff.camera.update_pixmap(self._frame) + + def stop(self) -> None: + self._timer.stop() diff --git a/src/aare/gui/new_gui/main_window.py b/src/aare/gui/new_gui/main_window.py index 875acff4..b4a66aa3 100644 --- a/src/aare/gui/new_gui/main_window.py +++ b/src/aare/gui/new_gui/main_window.py @@ -117,6 +117,16 @@ class MainWindow(QWidget): self._build_shortcuts() self._restore_state() + # dev seed: no server -> fake samples + status so the UI is testable offline + self._dev_seed = None + if self._base_url is None: + try: + from aare.gui.new_gui.dev_seed import DevSeed + self._dev_seed = DevSeed(self) + logger.info("Dev seed active (no server): fake samples + status.") + except Exception as exc: + logger.warning("Dev seed failed: %s", exc) + # ------------------------------------------------------------- backend def _build_backend(self, pred_zmq_addr) -> None: from aare.gui.threads.daq_worker import DAQWorker @@ -798,6 +808,11 @@ class MainWindow(QWidget): self._save_state() except Exception: pass + if self._dev_seed is not None: + try: + self._dev_seed.stop() + except Exception: + pass try: if hasattr(self.daq, "cleanup"): self.daq.cleanup()