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:
co-authored by
Claude Opus 4.8
parent
c09c2e31e3
commit
963af28cf4
@@ -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 |
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user