diff options
19 files changed, 151 insertions, 16 deletions
diff --git a/core/__pycache__/acquisition.cpython-312.pyc b/core/__pycache__/acquisition.cpython-312.pyc Binary files differindex 999e174..c593432 100644 --- a/core/__pycache__/acquisition.cpython-312.pyc +++ b/core/__pycache__/acquisition.cpython-312.pyc diff --git a/core/__pycache__/profile.cpython-312.pyc b/core/__pycache__/profile.cpython-312.pyc Binary files differindex c910e62..0fee9d9 100644 --- a/core/__pycache__/profile.cpython-312.pyc +++ b/core/__pycache__/profile.cpython-312.pyc 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 Binary files differindex d285167..f22ffd6 100644 --- a/devices/__pycache__/analog_input.cpython-312.pyc +++ b/devices/__pycache__/analog_input.cpython-312.pyc diff --git a/devices/__pycache__/digital_io.cpython-312.pyc b/devices/__pycache__/digital_io.cpython-312.pyc Binary files differindex 5cc1faf..1907cb3 100644 --- a/devices/__pycache__/digital_io.cpython-312.pyc +++ b/devices/__pycache__/digital_io.cpython-312.pyc diff --git a/devices/__pycache__/serial_device.cpython-312.pyc b/devices/__pycache__/serial_device.cpython-312.pyc Binary files differindex 03a29f7..53ba302 100644 --- a/devices/__pycache__/serial_device.cpython-312.pyc +++ b/devices/__pycache__/serial_device.cpython-312.pyc 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 Binary files differindex e9aab22..2da95a6 100644 --- a/ui/__pycache__/add_device_dialog.cpython-312.pyc +++ b/ui/__pycache__/add_device_dialog.cpython-312.pyc diff --git a/ui/__pycache__/control_editor.cpython-312.pyc b/ui/__pycache__/control_editor.cpython-312.pyc Binary files differindex 42855d5..ccecd3e 100644 --- a/ui/__pycache__/control_editor.cpython-312.pyc +++ b/ui/__pycache__/control_editor.cpython-312.pyc diff --git a/ui/__pycache__/control_panel.cpython-312.pyc b/ui/__pycache__/control_panel.cpython-312.pyc Binary files differindex 8761466..1c101a8 100644 --- a/ui/__pycache__/control_panel.cpython-312.pyc +++ b/ui/__pycache__/control_panel.cpython-312.pyc diff --git a/ui/__pycache__/main_window.cpython-312.pyc b/ui/__pycache__/main_window.cpython-312.pyc Binary files differindex a290571..d89b2e6 100644 --- a/ui/__pycache__/main_window.cpython-312.pyc +++ b/ui/__pycache__/main_window.cpython-312.pyc 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() |
