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) <noreply@anthropic.com>
This commit is contained in:
appleb_m
2026-06-25 10:20:01 +02:00
co-authored by Claude Opus 4.8
parent c09c2e31e3
commit 963af28cf4
3 changed files with 142 additions and 0 deletions
+6
View File
@@ -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 |
+121
View File
@@ -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()
+15
View File
@@ -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()