summaryrefslogtreecommitdiff
path: root/ui/control_panel.py
diff options
context:
space:
mode:
Diffstat (limited to 'ui/control_panel.py')
-rw-r--r--ui/control_panel.py61
1 files changed, 55 insertions, 6 deletions
diff --git a/ui/control_panel.py b/ui/control_panel.py
index 61e1807..aef0f89 100644
--- a/ui/control_panel.py
+++ b/ui/control_panel.py
@@ -33,6 +33,7 @@ from PyQt6.QtCore import Qt, pyqtSignal, QTimer
from PyQt6.QtGui import QFont
from devices.device_registry import DeviceRegistry
+from devices.base_device import DeviceStatus
# ══════════════════════════════════════════════════════════════════════════════
@@ -119,13 +120,21 @@ class ControlWidget(QFrame):
if self.registry and self.device_id and self.channel_id:
dev = self.registry.get_instance(self.device_id)
if dev:
- ok = dev.write_channel(self.channel_id, value)
- if ok:
- written = True
+ ch = dev.get_channel(self.channel_id)
+ if ch is not None and not ch.enabled:
+ print(f"[Control] write_channel({self.channel_id}, {value}) skipped on "
+ f"{self.device_id} — channel is disabled")
+ elif dev.status not in (DeviceStatus.CONNECTED, DeviceStatus.SIMULATED):
+ print(f"[Control] write_channel({self.channel_id}, {value}) skipped on "
+ f"{self.device_id} — device status is {dev.status.value}, not connected")
else:
- print(f"[Control] write_channel({self.channel_id}, {value}) "
- f"returned False on {self.device_id} — "
- f"check device type and channel ID")
+ ok = dev.write_channel(self.channel_id, value)
+ if ok:
+ written = True
+ else:
+ print(f"[Control] write_channel({self.channel_id}, {value}) "
+ f"returned False on {self.device_id} — "
+ f"check device type and channel ID")
self.value_changed.emit(self.channel_id, value)
if self._on_action_fn is not None:
@@ -149,6 +158,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
@@ -202,6 +226,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
@@ -289,6 +316,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
@@ -381,6 +412,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
@@ -450,6 +484,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
@@ -504,6 +542,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
@@ -548,6 +589,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")