summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/__pycache__/acquisition.cpython-312.pycbin11068 -> 11417 bytes
-rw-r--r--core/__pycache__/profile.cpython-312.pycbin11779 -> 14026 bytes
-rw-r--r--core/acquisition.py5
-rw-r--r--core/profile.py56
-rw-r--r--devices/__pycache__/analog_input.cpython-312.pycbin24176 -> 24808 bytes
-rw-r--r--devices/__pycache__/digital_io.cpython-312.pycbin21962 -> 22589 bytes
-rw-r--r--devices/__pycache__/serial_device.cpython-312.pycbin14669 -> 15511 bytes
-rw-r--r--devices/analog_input.py27
-rw-r--r--devices/digital_io.py25
-rw-r--r--devices/serial_device.py13
-rw-r--r--ui/__pycache__/add_device_dialog.cpython-312.pycbin29737 -> 29737 bytes
-rw-r--r--ui/__pycache__/control_editor.cpython-312.pycbin20099 -> 20704 bytes
-rw-r--r--ui/__pycache__/control_panel.cpython-312.pycbin42162 -> 42676 bytes
-rw-r--r--ui/__pycache__/main_window.cpython-312.pycbin25329 -> 26317 bytes
-rw-r--r--ui/add_device_dialog.py2
-rw-r--r--ui/control_editor.py7
-rw-r--r--ui/control_panel.py14
-rw-r--r--ui/main_window.py14
-rw-r--r--ui/plot_builder.py4
19 files changed, 151 insertions, 16 deletions
diff --git a/core/__pycache__/acquisition.cpython-312.pyc b/core/__pycache__/acquisition.cpython-312.pyc
index 999e174..c593432 100644
--- a/core/__pycache__/acquisition.cpython-312.pyc
+++ b/core/__pycache__/acquisition.cpython-312.pyc
Binary files differ
diff --git a/core/__pycache__/profile.cpython-312.pyc b/core/__pycache__/profile.cpython-312.pyc
index c910e62..0fee9d9 100644
--- a/core/__pycache__/profile.cpython-312.pyc
+++ b/core/__pycache__/profile.cpython-312.pyc
Binary files differ
diff --git a/core/acquisition.py b/core/acquisition.py
index afccb56..57fda7e 100644
--- a/core/acquisition.py
+++ b/core/acquisition.py
@@ -102,6 +102,11 @@ class AcquisitionEngine(QObject):
def get_buffer(self, device_id: str, channel_id: str) -> Optional[ChannelBuffer]:
return self._buffers.get(device_id, {}).get(channel_id)
+ def clear_history(self):
+ for ch_map in self._buffers.values():
+ for buf in ch_map.values():
+ buf.clear()
+
# ── Start / stop ─────────────────────────────────────────────────────
def start(self):
diff --git a/core/profile.py b/core/profile.py
index b2d25b4..2a70370 100644
--- a/core/profile.py
+++ b/core/profile.py
@@ -69,6 +69,7 @@ class ProfileDerived:
class Profile:
name: str = "Untitled Profile"
version: str = "1.0"
+ devices: List[Dict] = field(default_factory=list)
controls: List[Dict] = field(default_factory=list)
channels: List[Dict] = field(default_factory=list)
pipelines: List[Dict] = field(default_factory=list)
@@ -85,6 +86,7 @@ class Profile:
return Profile(
name = d.get("name", ""),
version = d.get("version", "1.0"),
+ devices = d.get("devices", []),
controls = d.get("controls", []),
channels = d.get("channels", []),
pipelines = d.get("pipelines", []),
@@ -136,6 +138,11 @@ class ProfileManager:
) -> Profile:
p = Profile(name=profile_name)
+ # Devices
+ for dev in registry.all_instances():
+ if hasattr(dev, "get_save_config"):
+ p.devices.append(dev.get_save_config())
+
# Controls
p.controls = [spec.to_dict() for spec in control_specs]
@@ -197,6 +204,7 @@ class ProfileManager:
processor,
control_panel,
settings_ref: dict,
+ engine=None,
):
"""Apply a loaded profile to the live application state."""
from ui.control_editor import ControlSpec
@@ -205,6 +213,54 @@ class ProfileManager:
filter_from_dict,
)
+ # ── Devices ──────────────────────────────────────────────────────
+ _DEVICE_FACTORIES = {}
+ try:
+ from devices.analog_input import AnalogInputDevice
+ _DEVICE_FACTORIES["analog_input"] = AnalogInputDevice
+ except Exception:
+ pass
+ try:
+ from devices.digital_io import DigitalIODevice
+ _DEVICE_FACTORIES["digital_io"] = DigitalIODevice
+ except Exception:
+ pass
+ try:
+ from devices.serial_device import SerialDevice
+ _DEVICE_FACTORIES["serial"] = SerialDevice
+ except Exception:
+ pass
+
+ for dev_cfg in profile.devices:
+ dev_type = dev_cfg.get("device_type")
+ dev_id = dev_cfg.get("device_id")
+ if not dev_id:
+ continue
+ factory = _DEVICE_FACTORIES.get(dev_type)
+ if not factory:
+ print(f"[Profile] Unknown device type: {dev_type}")
+ continue
+ try:
+ # Replace existing device so saved config takes effect
+ existing = registry.get_instance(dev_id)
+ if existing is not None:
+ try:
+ existing.disconnect()
+ except Exception:
+ pass
+ registry.remove_instance(dev_id)
+ if engine is not None:
+ engine.remove_device(dev_id)
+
+ kwargs = {k: v for k, v in dev_cfg.items() if k != "device_type"}
+ dev = factory(**kwargs)
+ registry.add_instance(dev)
+ dev.connect()
+ if engine is not None:
+ engine.add_device(dev)
+ except Exception as e:
+ print(f"[Profile] Could not restore device {dev_id}: {e}")
+
# ── Channel overrides ────────────────────────────────────────────
for ch_data in profile.channels:
dev = registry.get_instance(ch_data["device_id"])
diff --git a/devices/__pycache__/analog_input.cpython-312.pyc b/devices/__pycache__/analog_input.cpython-312.pyc
index d285167..f22ffd6 100644
--- a/devices/__pycache__/analog_input.cpython-312.pyc
+++ b/devices/__pycache__/analog_input.cpython-312.pyc
Binary files differ
diff --git a/devices/__pycache__/digital_io.cpython-312.pyc b/devices/__pycache__/digital_io.cpython-312.pyc
index 5cc1faf..1907cb3 100644
--- a/devices/__pycache__/digital_io.cpython-312.pyc
+++ b/devices/__pycache__/digital_io.cpython-312.pyc
Binary files differ
diff --git a/devices/__pycache__/serial_device.cpython-312.pyc b/devices/__pycache__/serial_device.cpython-312.pyc
index 03a29f7..53ba302 100644
--- a/devices/__pycache__/serial_device.cpython-312.pyc
+++ b/devices/__pycache__/serial_device.cpython-312.pyc
Binary files differ
diff --git a/devices/analog_input.py b/devices/analog_input.py
index e1b8b09..c9e5d2a 100644
--- a/devices/analog_input.py
+++ b/devices/analog_input.py
@@ -73,12 +73,13 @@ class AnalogInputDevice(BaseDevice):
)
super().__init__(info)
- self._ni_device = ni_device
- self._ni_min_v = ni_min_v
- self._ni_max_v = ni_max_v
- self._ard_port = ard_port
- self._ard_baud = ard_baud
- self._last_error = ""
+ self._ni_device = ni_device
+ self._ni_min_v = ni_min_v
+ self._ni_max_v = ni_max_v
+ self._ard_port = ard_port
+ self._ard_baud = ard_baud
+ self._num_channels = num_channels
+ self._last_error = ""
self._layer = self._make_layer(
backend, simulate, ni_device, ni_min_v, ni_max_v,
@@ -170,6 +171,20 @@ class AnalogInputDevice(BaseDevice):
# Named parameter / setpoint
return self._layer.set_parameter(ch, value)
+ def get_save_config(self) -> dict:
+ return {
+ "device_type": self.DEVICE_TYPE,
+ "device_id": self.info.device_id,
+ "num_channels": self._num_channels,
+ "simulate": self.simulate,
+ "backend": self.backend,
+ "ni_device": self._ni_device,
+ "ni_min_v": self._ni_min_v,
+ "ni_max_v": self._ni_max_v,
+ "ard_port": self._ard_port,
+ "ard_baud": self._ard_baud,
+ }
+
def get_config_widget(self) -> QWidget:
return AnalogInputConfigWidget(self)
diff --git a/devices/digital_io.py b/devices/digital_io.py
index 3d24192..96c6ca5 100644
--- a/devices/digital_io.py
+++ b/devices/digital_io.py
@@ -40,11 +40,13 @@ class DigitalIODevice(BaseDevice):
ard_port: str = "COM3",
ard_baud: int = 115200,
):
- self.simulate = simulate
- self.backend = backend
- self._ni_device = ni_device
- self._ard_port = ard_port
- self._ard_baud = ard_baud
+ self.simulate = simulate
+ self.backend = backend
+ self._ni_device = ni_device
+ self._ard_port = ard_port
+ self._ard_baud = ard_baud
+ self._num_inputs = num_inputs
+ self._num_outputs = num_outputs
channels = []
for i in range(num_inputs):
@@ -252,6 +254,19 @@ class DigitalIODevice(BaseDevice):
if was_running:
self.connect()
+ def get_save_config(self) -> dict:
+ return {
+ "device_type": self.DEVICE_TYPE,
+ "device_id": self.info.device_id,
+ "num_inputs": self._num_inputs,
+ "num_outputs": self._num_outputs,
+ "simulate": self.simulate,
+ "backend": self.backend,
+ "ni_device": self._ni_device,
+ "ard_port": self._ard_port,
+ "ard_baud": self._ard_baud,
+ }
+
def get_config_widget(self) -> QWidget:
return DigitalIOConfigWidget(self)
diff --git a/devices/serial_device.py b/devices/serial_device.py
index a5bf08c..6f54cb5 100644
--- a/devices/serial_device.py
+++ b/devices/serial_device.py
@@ -109,6 +109,19 @@ class SerialDevice(BaseDevice):
def write_channel(self, channel_id: str, value: Any) -> bool:
return self._layer.write(channel_id, int(value))
+ def get_save_config(self) -> dict:
+ return {
+ "device_type": self.DEVICE_TYPE,
+ "device_id": self.info.device_id,
+ "port": self._port,
+ "baud_rate": self._baud,
+ "num_channels": len(self.info.channels),
+ "channel_names": [c.channel_id for c in self.info.channels],
+ "units": [c.unit for c in self.info.channels],
+ "parse_format": self._parse_format,
+ "simulate": self.simulate,
+ }
+
def get_config_widget(self) -> QWidget:
return SerialConfigWidget(self)
diff --git a/ui/__pycache__/add_device_dialog.cpython-312.pyc b/ui/__pycache__/add_device_dialog.cpython-312.pyc
index e9aab22..2da95a6 100644
--- a/ui/__pycache__/add_device_dialog.cpython-312.pyc
+++ b/ui/__pycache__/add_device_dialog.cpython-312.pyc
Binary files differ
diff --git a/ui/__pycache__/control_editor.cpython-312.pyc b/ui/__pycache__/control_editor.cpython-312.pyc
index 42855d5..ccecd3e 100644
--- a/ui/__pycache__/control_editor.cpython-312.pyc
+++ b/ui/__pycache__/control_editor.cpython-312.pyc
Binary files differ
diff --git a/ui/__pycache__/control_panel.cpython-312.pyc b/ui/__pycache__/control_panel.cpython-312.pyc
index 8761466..1c101a8 100644
--- a/ui/__pycache__/control_panel.cpython-312.pyc
+++ b/ui/__pycache__/control_panel.cpython-312.pyc
Binary files differ
diff --git a/ui/__pycache__/main_window.cpython-312.pyc b/ui/__pycache__/main_window.cpython-312.pyc
index a290571..d89b2e6 100644
--- a/ui/__pycache__/main_window.cpython-312.pyc
+++ b/ui/__pycache__/main_window.cpython-312.pyc
Binary files differ
diff --git a/ui/add_device_dialog.py b/ui/add_device_dialog.py
index 50f234d..7e8e935 100644
--- a/ui/add_device_dialog.py
+++ b/ui/add_device_dialog.py
@@ -401,7 +401,7 @@ class AddDeviceDialog(QDialog):
self.created_device = None
self.setWindowTitle("Add Device")
self.setMinimumSize(480, 460)
- self.resize(500, 520)
+ self.resize(500, 600)
self._build()
def _build(self):
diff --git a/ui/control_editor.py b/ui/control_editor.py
index 4d18aef..901d6f8 100644
--- a/ui/control_editor.py
+++ b/ui/control_editor.py
@@ -51,6 +51,7 @@ class ControlSpec:
icon: str = "⏻"
device_id: str = ""
channel_id: str = ""
+ active_low: bool = False
# Type-specific params
unit: str = ""
min_val: float = 0.0
@@ -206,6 +207,9 @@ class ControlEditorDialog(QDialog):
self._ch_cb.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
hw_form.addRow("Channel / Pin:", self._ch_cb)
+ self._active_low_chk = QCheckBox("Active-low output (ON sends LOW)")
+ hw_form.addRow("Polarity:", self._active_low_chk)
+
self._ch_hint = QLabel("")
self._ch_hint.setObjectName("traceSource")
self._ch_hint.setWordWrap(True)
@@ -255,6 +259,7 @@ class ControlEditorDialog(QDialog):
# Channel (populated after device selection)
self._on_dev_changed()
self._ch_cb.setCurrentText(self.spec.channel_id)
+ self._active_low_chk.setChecked(bool(self.spec.active_low))
# Params
for name, panel in self._param_panels.items():
@@ -263,6 +268,7 @@ class ControlEditorDialog(QDialog):
def _on_type_changed(self, type_name: str):
idx = list(_PARAM_PANELS.keys()).index(type_name)
self._stack.setCurrentIndex(idx)
+ self._active_low_chk.setEnabled(type_name == "On/Off Switch")
# Auto-set title if still default
if not self._title_edit.text() or \
self._title_edit.text() in CONTROL_TYPES:
@@ -309,6 +315,7 @@ class ControlEditorDialog(QDialog):
icon=CONTROL_ICONS.get(ctype, "⚙"),
device_id=self._dev_cb.currentData() or "",
channel_id=channel_id,
+ active_low=self._active_low_chk.isChecked(),
)
# Save type-specific params
panel = self._param_panels.get(ctype)
diff --git a/ui/control_panel.py b/ui/control_panel.py
index efd4f17..7a3e416 100644
--- a/ui/control_panel.py
+++ b/ui/control_panel.py
@@ -43,16 +43,24 @@ class ControlWidget(QFrame):
def __init__(self, title: str, icon: str = "⚙",
device_id: str = "", channel_id: str = "",
- registry: Optional[DeviceRegistry] = None):
+ registry: Optional[DeviceRegistry] = None,
+ active_low: bool = False):
super().__init__()
self.title = title
self.icon = icon
self.device_id = device_id
self.channel_id = channel_id
self.registry = registry
+ self.active_low = active_low
self.setObjectName("controlWidget")
self._build_frame()
+ def _logic_level(self, enabled: bool) -> float:
+ """Map logical ON/OFF to electrical level, respecting active-low outputs."""
+ if self.active_low:
+ return 0.0 if enabled else 1.0
+ return 1.0 if enabled else 0.0
+
def _build_frame(self):
"""Build the common outer frame; subclasses fill self._body."""
outer = QVBoxLayout(self)
@@ -155,7 +163,7 @@ class OnOffSwitch(ControlWidget):
# Re-polish so QSS picks up new objectName
for w in (self._btn, self._indicator, self._state_lbl):
w.style().unpolish(w); w.style().polish(w)
- self._write(1.0 if checked else 0.0)
+ self._write(self._logic_level(checked))
# ══════════════════════════════════════════════════════════════════════════════
@@ -568,6 +576,7 @@ class ControlPanel(QWidget):
spec = ControlSpec(
title=widget.title, icon=widget.icon,
device_id=widget.device_id, channel_id=widget.channel_id,
+ active_low=getattr(widget, "active_low", False),
)
wrapper = self._make_wrapper(widget, spec)
self._widgets.append(widget)
@@ -673,6 +682,7 @@ def _build_widget_from_spec(spec, registry: DeviceRegistry) -> Optional[ControlW
device_id=spec.device_id,
channel_id=spec.channel_id,
registry=registry,
+ active_low=getattr(spec, "active_low", False),
)
t = spec.control_type
ttl = spec.title
diff --git a/ui/main_window.py b/ui/main_window.py
index 9df7ef7..d4bb203 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -92,6 +92,12 @@ class MainWindow(QMainWindow):
self._log_btn.setEnabled(False); self._log_btn.clicked.connect(self._toggle_log)
tb.addWidget(self._log_btn)
+ self._clear_btn = QPushButton("⌫ Clear")
+ self._clear_btn.setObjectName("toolbarSectionBtn")
+ self._clear_btn.setToolTip("Clear plot history")
+ self._clear_btn.clicked.connect(self._clear_history)
+ tb.addWidget(self._clear_btn)
+
tb.addWidget(_sep())
dev_btn = QPushButton("⊞ Devices")
@@ -265,12 +271,15 @@ class MainWindow(QMainWindow):
processor=self.processor,
control_panel=self._ctrl,
settings_ref=self._settings,
+ engine=self.engine,
)
if plot_cfg:
self._chart.apply_layout(plot_cfg)
else:
self._chart.refresh()
# Refresh open windows
+ if self._win_devices:
+ self._win_devices.refresh()
if self._win_plot:
self._win_plot.refresh_channels()
self._status.setText(f"Profile loaded: {profile.name}")
@@ -309,6 +318,11 @@ class MainWindow(QMainWindow):
self._log_btn.setEnabled(False); self._clock.stop()
self._status.setText("Stopped")
+ def _clear_history(self):
+ self.engine.clear_history()
+ self._chart.refresh()
+ self._status.setText("History cleared.")
+
def _toggle_log(self, c: bool):
if c:
p = self.engine.start_logging(
diff --git a/ui/plot_builder.py b/ui/plot_builder.py
index beda116..ad6d231 100644
--- a/ui/plot_builder.py
+++ b/ui/plot_builder.py
@@ -356,8 +356,8 @@ class PlotBuilderWindow(QWidget):
else build_default_layout(registry, processor)
self.setWindowTitle("Plot Builder")
- self.setMinimumSize(700, 560)
- self.resize(800, 680)
+ self.setMinimumSize(700, 780)
+ self.resize(800, 780)
self._blocks: List[PlotBlock] = []
self._build()
self._populate()