feat: live energy/wavelength readout in Beamline setup

New 'Current energy / lambda' row above the setpoint, fed per DAQ
tick from diffraction.energy_keV; the setpoint row is renamed
'Set Energy (keV)' so 'what is' and 'what to set' are distinct.
Guards energy 0.0 (server's detector-unavailable fallback) because
the wavelength property divides by it. The copied energy row in the
experiment configuration is renamed to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 15:34:58 +02:00
committed by duan_j
co-authored by Claude Fable 5
parent 900bc2f6f7
commit 2a82c598c0
3 changed files with 38 additions and 7 deletions
@@ -97,7 +97,7 @@ class DataCollectionSettings(QFrame):
energy_row = QWidget(self)
energy_layout = QHBoxLayout(energy_row)
energy_layout.setContentsMargins(0, 0, 0, 0)
energy_layout.addWidget(QLabel("Energy (keV)", parent=energy_row))
energy_layout.addWidget(QLabel("Set Energy (keV)", parent=energy_row))
energy_layout.addWidget(self.energy_spin)
energy_layout.addWidget(self.change_energy_button)
+21 -6
View File
@@ -26,22 +26,28 @@ class MonochromatorPanel(QWidget):
self.mono_pitch_scan_button.clicked.connect(self.mono_pitch_scan.emit)
grid_layout.addWidget(self.mono_pitch_scan_button, 1, 0, 1, 3)
# Live readout above the setpoint, so "what is" and "what to set"
# stop sharing one ambiguous Energy row. Fed per DAQ tick.
grid_layout.addWidget(QLabel("Current energy / λ", parent=self), 2, 0)
self.current_energy_label = QLabel("", parent=self)
grid_layout.addWidget(self.current_energy_label, 2, 1, 1, 2)
# One row (label | value | button) instead of three — vertical space.
# Display in keV; the DAQ API stays in eV (converted on emit).
# Unit lives in the label, not as a spinbox suffix — the suffix ate
# field width and sat between the value and the +/- arrow.
grid_layout.addWidget(QLabel("Energy (keV)", parent=self), 2, 0)
grid_layout.addWidget(QLabel("Set Energy (keV)", parent=self), 3, 0)
self.energy_spin = QDoubleSpinBox(parent=self)
self.energy_spin.setDecimals(3)
self.energy_spin.setRange(1.0, 30.0)
self.energy_spin.setSingleStep(0.1)
self.energy_spin.setValue(12.0)
grid_layout.addWidget(self.energy_spin, 2, 1)
grid_layout.addWidget(self.energy_spin, 3, 1)
self.change_energy_button = QPushButton("Change Energy", parent=self)
self.change_energy_button.clicked.connect(self._emit_change_energy)
grid_layout.addWidget(self.change_energy_button, 2, 2)
grid_layout.addWidget(self.change_energy_button, 3, 2)
# TODO(wire backend): no DAQ endpoint exists yet for moving the beam
# to the box center — shown disabled as WIP until the operation is
@@ -51,12 +57,21 @@ class MonochromatorPanel(QWidget):
self.move_beam_to_box_button.setToolTip("Coming soon — not functional yet.")
self.move_beam_to_box_button.setEnabled(False)
self.move_beam_to_box_button.clicked.connect(self.move_beam_to_box.emit)
grid_layout.addWidget(self.move_beam_to_box_button, 3, 0, 1, 3)
grid_layout.addWidget(self.move_beam_to_box_button, 4, 0, 1, 3)
@Slot()
def _emit_change_energy(self):
self.change_energy.emit(float(self.energy_spin.value()) * 1000.0)
@Slot(DAQStatusModel)
def update_daq_status(self, _status: DAQStatusModel):
pass
def update_daq_status(self, status: DAQStatusModel):
energy = status.diffraction.energy_keV
# 0.0 is the server's detector-unavailable fallback, and the
# wavelength property divides by it — guard before touching it.
if not energy:
text = "— / —"
else:
text = f"{energy:.3f} keV / {status.diffraction.wavelength_angstrom:.4f} Å"
# Guarded: runs per DAQ tick (2 Hz), skip the repaint when unchanged.
if self.current_energy_label.text() != text:
self.current_energy_label.setText(text)
@@ -0,0 +1,16 @@
from aare.gui.panels.monochromator_panel import MonochromatorPanel
def test_current_energy_readout(qtbot, daq_status_factory):
panel = MonochromatorPanel()
qtbot.addWidget(panel)
status = daq_status_factory()
panel.update_daq_status(status)
assert panel.current_energy_label.text() == "12.000 keV / 1.0332 Å"
# 0.0 is the server's detector-unavailable fallback; the wavelength
# property divides by energy, so the readout must not touch it.
status.diffraction = status.diffraction.model_copy(update={"energy_keV": 0.0})
panel.update_daq_status(status)
assert panel.current_energy_label.text() == "— / —"