diff options
| author | Christian Kolset <ckolset@colostate.edu> | 2026-07-29 14:22:46 -0600 |
|---|---|---|
| committer | Christian Kolset <ckolset@colostate.edu> | 2026-07-29 14:22:46 -0600 |
| commit | 34c8b0950398c07e84777479f3534615907f4f58 (patch) | |
| tree | 5050896bbd5c2a4fab0239c6456637ca22306156 | |
| parent | c98885f503a8a2eb4b691b41404231da8d2aeda5 (diff) | |
| parent | ad7f1c39ef473a327ca3b467b579be0099377f55 (diff) | |
Merge branch 'fix/run-stop-log-master-switch'
| -rw-r--r-- | ui/control_panel.py | 40 | ||||
| -rw-r--r-- | ui/main_window.py | 18 | ||||
| -rw-r--r-- | ui/style_dark.qss | 2 | ||||
| -rw-r--r-- | ui/style_light.qss | 2 |
4 files changed, 61 insertions, 1 deletions
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") diff --git a/ui/main_window.py b/ui/main_window.py index d585a83..90294ac 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -203,6 +203,9 @@ class MainWindow(QMainWindow): self._clock = QTimer(self); self._clock.setInterval(1000) self._clock.timeout.connect(self._tick) + self._rec_blink = QTimer(self); self._rec_blink.setInterval(600) + self._rec_blink.timeout.connect(self._tick_rec_blink) + def _connect_signals(self): self.engine.new_data.connect(self.processor.on_raw_data) self.processor.processed_data.connect(self._chart.on_new_data) @@ -595,9 +598,12 @@ class MainWindow(QMainWindow): self._run_btn.setText("⏹ STOP"); self._log_btn.setEnabled(True) self._clock.start(); self._status.setText("Acquiring…") else: + self._ctrl.safe_stop_all() # master switch — stop outputs before halting acquisition self.engine.stop() self._run_btn.setText("▶ RUN") - if self._log_btn.isChecked(): self._log_btn.setChecked(False) + if self._log_btn.isChecked(): + self._log_btn.setChecked(False) + self._toggle_log(False) # setChecked() alone won't fire clicked — stop blink/logging explicitly self._log_btn.setEnabled(False); self._clock.stop() self._status.setText("Stopped") @@ -613,8 +619,18 @@ class MainWindow(QMainWindow): os.path.join(self._settings.get("log_dir", "logs"), "")) self._log_btn.setText("⏹ LOGGING") self._status.setText(f"Logging → {p}") + self._rec_blink.start() else: self.engine.stop_logging(); self._log_btn.setText("⬤ LOG") + self._rec_blink.stop() + self._log_btn.setProperty("recording", False) + self._log_btn.style().unpolish(self._log_btn); self._log_btn.style().polish(self._log_btn) + + def _tick_rec_blink(self): + """Pulse the Log button's background while a recording is active.""" + on = not self._log_btn.property("recording") + self._log_btn.setProperty("recording", on) + self._log_btn.style().unpolish(self._log_btn); self._log_btn.style().polish(self._log_btn) # ── Theme / settings ────────────────────────────────────────────────── diff --git a/ui/style_dark.qss b/ui/style_dark.qss index e562365..9fd7198 100644 --- a/ui/style_dark.qss +++ b/ui/style_dark.qss @@ -63,7 +63,9 @@ QPushButton#logButton { min-width: 80px; } QPushButton#logButton:enabled { color: #e2e8f0; border-color: #3b82f6; } +QPushButton#logButton:disabled { background-color: #12172a; color: #3d4a6b; border: 1px solid #1e2740; } QPushButton#logButton:checked { background-color: #7c2d12; border-color: #ef4444; color: #fee2e2; } +QPushButton#logButton[recording="true"] { background-color: #ef4444; border-color: #fca5a5; color: #ffffff; } QPushButton#addDeviceButton { background-color: #1e3a5f; diff --git a/ui/style_light.qss b/ui/style_light.qss index e0a1987..265894d 100644 --- a/ui/style_light.qss +++ b/ui/style_light.qss @@ -6,7 +6,9 @@ QPushButton#runButton { background:#166534; color:#dcfce7; border:1px solid #22c QPushButton#runButton:checked { background:#991b1b; border-color:#ef4444; color:#fee2e2; } QPushButton#logButton { background:#f1f5f9; color:#64748b; border:1px solid #cbd5e1; border-radius:4px; padding:5px 14px; font-family:"IBM Plex Mono",monospace; min-width:80px; } QPushButton#logButton:enabled { color:#1e293b; border-color:#3b82f6; } +QPushButton#logButton:disabled { background:#f8fafc; color:#94a3b8; border:1px solid #e2e8f0; } QPushButton#logButton:checked { background:#fef2f2; border-color:#ef4444; color:#991b1b; } +QPushButton#logButton[recording="true"] { background:#ef4444; border-color:#fca5a5; color:#ffffff; } QPushButton#toolbarSectionBtn { background:#f1f5f9; color:#475569; border:1px solid #cbd5e1; border-radius:4px; padding:5px 14px; font-weight:600; } QPushButton#toolbarSectionBtn:hover { background:#e2e8f0; color:#1e293b; } QPushButton#toolbarSectionBtn:checked { background:#dbeafe; color:#1d4ed8; border-color:#3b82f6; } |
