summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ui/control_panel.py40
-rw-r--r--ui/main_window.py18
-rw-r--r--ui/style_dark.qss2
-rw-r--r--ui/style_light.qss2
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 d0e0ac4..268a9d3 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -196,6 +196,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)
@@ -521,9 +524,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")
@@ -539,8 +545,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; }