diff --git a/src/aare/gui/panels/smart_rotation_panel.py b/src/aare/gui/panels/smart_rotation_panel.py
index 625f9fd5..c031f6e2 100644
--- a/src/aare/gui/panels/smart_rotation_panel.py
+++ b/src/aare/gui/panels/smart_rotation_panel.py
@@ -55,6 +55,15 @@ DEFAULT_MIN_EXP_TIME_S = 0.0005
# there. Stated explicitly so it does not silently drop to 1.2 ms if
# EXP_TIME_STEP_S is ever made finer.
STRATEGY_MIN_EXP_TIME_S = {MXBeamline.X06DA: 0.002}
+# Entry limits for the visible-resolution field. Named because the clamp on
+# the DAQ value has to use the same numbers as the widget's validator.
+VISIBLE_RES_MIN_A = 0.8
+VISIBLE_RES_MAX_A = 10.0
+VISIBLE_RES_DEFAULT_A = 2.0
+# Two DAQ resolutions closer than this are the same raster result reported
+# again, not a new one.
+DAQ_RES_EPS_A = 0.001
+VISIBLE_RES_LABEL = "Visible resolution"
DTZ_STEP_MM = 10.0
EXP_TIME_STEP_S = 0.001
TRANSMISSION_SIG_DIGITS = 2
@@ -98,6 +107,10 @@ class SimpleRotationSettingsPanel(QWidget):
super().__init__(parent)
self._beamline = mx_beamline()
+ self._daq_best_res = None
+ # Distinguishes "no DAQ resolution yet" from "DAQ reports none", so the
+ # first status tick always paints the label.
+ self._daq_res_applied = False
self.n_images = 1
self.transmission = 1.0
self.delivered_dose_MGy = 0.0
@@ -126,10 +139,17 @@ class SimpleRotationSettingsPanel(QWidget):
m = self._layout.contentsMargins()
self._layout.setContentsMargins(m.left(), 0, m.right(), 3)
- # Visible resolution (entry)
- self._layout.addWidget(QLabel("Visible resolution", parent=self), 0, 0)
+ # Visible resolution (entry). The label carries the provenance of the
+ # value: silence means it came from the DAQ, otherwise it says so.
+ self.visible_res_label = QLabel(VISIBLE_RES_LABEL, parent=self)
+ self._layout.addWidget(self.visible_res_label, 0, 0)
self.visible_res_enter = NumberLineEdit(
- 0.8, 10.0, decimals=2, default=2.0, parent=self, track_pending=True
+ VISIBLE_RES_MIN_A,
+ VISIBLE_RES_MAX_A,
+ decimals=2,
+ default=VISIBLE_RES_DEFAULT_A,
+ parent=self,
+ track_pending=True,
)
self._layout.addWidget(self.visible_res_enter, 0, 1, 1, 3)
self._layout.addWidget(QLabel("Å", parent=self), 0, 4)
@@ -300,18 +320,10 @@ class SimpleRotationSettingsPanel(QWidget):
@Slot(DAQStatusModel)
def update_daq_status(self, s: DAQStatusModel):
self._d = s
- # TODO only update best_res after raster finished otherwise ask to update. or have toggle to overwrite with user value
# TODO only take best res from flat face scan
# TODO identify flat face!
- best_res = s.last_best_res
self._omega = s.geom.omega_deg
-
- if best_res is not None:
- # clamp to control limits and update field; this will also trigger recalculation
- lo, hi = 0.8, 10.0
- v = max(lo, min(hi, float(best_res)))
- self.visible_res_enter.update_value(v)
- self.set_visible_resolution(v)
+ self._apply_daq_resolution(s.last_best_res)
self._wilson_b = s.last_best_b_factor
@@ -337,6 +349,51 @@ class SimpleRotationSettingsPanel(QWidget):
self.wavelength_label.setText(f"{s.diffraction.wavelength_angstrom:.3f}")
self.update_calculated_labels()
+ def _apply_daq_resolution(self, best_res: float | None) -> None:
+ """Track ``DAQStatusModel.last_best_res`` in the resolution field.
+
+ Written on every *change* of the DAQ value rather than on every 2 Hz
+ tick, so a fresh raster result always lands while a repeat of the same
+ value leaves a manual override alone.
+
+ Two things this deliberately does not use:
+
+ * ``update_value`` compares against ``saved_value``, which
+ ``on_editing_finished`` never updates (it self-assigns), so it only
+ ever sees programmatic writes. Once the operator has typed over a
+ pushed value, the same DAQ value could never re-assert itself.
+ ``set_committed_value`` compares against what the field actually
+ shows, which is the question being asked here.
+ * A bare ``is not None`` guard. The DAQ clears ``last_best_res`` on
+ mount, and skipping the update then leaves the previous crystal's
+ resolution in the field with nothing to say it is stale.
+ """
+ if not self._daq_res_applied:
+ unchanged = False
+ elif best_res is None or self._daq_best_res is None:
+ unchanged = best_res is None and self._daq_best_res is None
+ else:
+ unchanged = abs(best_res - self._daq_best_res) <= DAQ_RES_EPS_A
+ if unchanged:
+ return
+
+ self._daq_res_applied = True
+ self._daq_best_res = best_res
+ if best_res is None:
+ # No raster result for this sample: either none has run yet, or the
+ # pgroup gate in the status endpoint stripped it. Either way the
+ # field is showing a default, and the strategy below is built on a
+ # number nothing measured -- say so instead of looking authoritative.
+ value = self.visible_res_enter.get_default()
+ self.visible_res_label.setText(
+ f"""{VISIBLE_RES_LABEL} (no raster)"""
+ )
+ else:
+ value = max(VISIBLE_RES_MIN_A, min(VISIBLE_RES_MAX_A, float(best_res)))
+ self.visible_res_label.setText(VISIBLE_RES_LABEL)
+ self.visible_res_enter.set_committed_value(value)
+ self.update_calculated_labels()
+
@Slot(float)
def set_visible_resolution(self, v: float):
self.update_calculated_labels()