From ad7f1c39ef473a327ca3b467b579be0099377f55 Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Wed, 29 Jul 2026 13:17:00 -0600 Subject: Make Run the master stop switch; add Log button disabled/recording styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run/Stop previously only paused acquisition — control outputs (motor speed, switches, PWM duty) kept whatever value was last written, so stopping the run loop didn't stop a running motor. Adds ControlWidget.safe_stop() (per-widget override, default zeros the output) and ControlPanel.safe_stop_all(), called from _toggle_run's Stop branch before engine.stop(). Per-widget behavior is deliberately not uniform: - OnOffSwitch/MotorControl/PwmControl have a latched running/enabled state, so safe_stop() drives their own toggle handler (consistent UI + write in one path) and, for Motor/PWM, zeroes the slider too. - SetpointControl/AnalogOutputControl only write on an explicit user action and have no universally safe forced value (e.g. 0 isn't necessarily "off" for an arbitrary process setpoint or analog output) — Stop leaves them untouched rather than guessing. Log button: added a :disabled QSS rule so "can't log yet" reads as clearly inert rather than a duller version of the enabled look, and a 600ms blink (toggling a "recording" dynamic property the QSS keys off) while a recording is active, so it reads as live/recording rather than a static pressed button. Master Stop now calls _toggle_log(False) explicitly when forcing the button off, since QPushButton.setChecked() doesn't emit clicked — without this the blink would keep running after a master Stop even though logging itself already halted via engine.stop()'s internal stop_logging() call. Co-Authored-By: Claude Sonnet 5 --- ui/control_panel.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'ui/control_panel.py') diff --git a/ui/control_panel.py b/ui/control_panel.py index 2b936c4..4db18e3 100644 --- a/ui/control_panel.py +++ b/ui/control_panel.py @@ -154,6 +154,21 @@ class ControlWidget(QFrame): except Exception as e: print(f"[Control '{self.title}'] script error: {e}") + def safe_stop(self): + """ + Called on every control when the master Stop is pressed. + + Default: zero the output. Widgets with a latched running/enabled + state (OnOffSwitch, MotorControl, PwmControl) override this to go + through their own toggle handler, so UI state and the write stay + consistent. Widgets that only write on an explicit user action + (SetpointControl, AnalogOutputControl) override with a no-op — + there's no universally "safe" value to force onto an arbitrary + process setpoint or analog output, so Stop leaves them alone + rather than guessing. + """ + self._write(0.0) + # ══════════════════════════════════════════════════════════════════════════════ # On/Off Switch @@ -207,6 +222,9 @@ class OnOffSwitch(ControlWidget): w.style().unpolish(w); w.style().polish(w) self._write(self._logic_level(checked)) + def safe_stop(self): + self._btn.setChecked(False) # routes through _on_toggle: updates UI + writes off + # ══════════════════════════════════════════════════════════════════════════════ # Motor Control @@ -294,6 +312,10 @@ class MotorControl(ControlWidget): else: self._on_speed(self._slider.value()) + def safe_stop(self): + self._run_btn.setChecked(False) # routes through _on_run: stops + writes 0 + self._slider.setValue(0) + # ══════════════════════════════════════════════════════════════════════════════ # Setpoint Control @@ -386,6 +408,9 @@ class SetpointControl(ControlWidget): def _decrement(self): self._sp_spin.setValue(self._sp_spin.value() - self.step) + def safe_stop(self): + pass # no safe universal value for an arbitrary process setpoint — leave it + # ══════════════════════════════════════════════════════════════════════════════ # PWM Control @@ -455,6 +480,10 @@ class PwmControl(ControlWidget): self._en_btn.style().polish(self._en_btn) self._write(float(self._dc_slider.value()) if en else 0.0) + def safe_stop(self): + self._en_btn.setChecked(False) # routes through _on_enable: disables + writes 0 + self._dc_slider.setValue(0) + # ══════════════════════════════════════════════════════════════════════════════ # Generic Analog Output @@ -509,6 +538,9 @@ class AnalogOutputControl(ControlWidget): self._slider.setValue(max(0, min(1000, norm))) self._slider.blockSignals(False) + def safe_stop(self): + pass # only writes on explicit SET click — no safe universal value to force + # ══════════════════════════════════════════════════════════════════════════════ # Control Panel container @@ -566,6 +598,14 @@ class ControlPanel(QWidget): # ── Widget management ───────────────────────────────────────────────────── + def safe_stop_all(self): + """Master Stop — tell every control widget to go to a safe state.""" + for w in self._widgets: + try: + w.safe_stop() + except Exception as e: + print(f"[Control '{w.title}'] safe_stop failed: {e}") + def _make_wrapper(self, widget: ControlWidget, spec) -> QFrame: """Wrap a ControlWidget with Edit / Remove / reorder buttons.""" wrapper = QFrame(); wrapper.setObjectName("controlWidgetWrapper") -- cgit v1.2.3