summaryrefslogtreecommitdiff
path: root/devices
diff options
context:
space:
mode:
Diffstat (limited to 'devices')
-rw-r--r--devices/__init__.py4
-rw-r--r--devices/analog_input.py461
-rw-r--r--devices/arduino_device.py136
-rw-r--r--devices/base_device.py22
-rw-r--r--devices/digital_io.py334
-rw-r--r--devices/nidaqmx_device.py44
-rw-r--r--devices/serial_device.py66
7 files changed, 153 insertions, 914 deletions
diff --git a/devices/__init__.py b/devices/__init__.py
index 7cee4ac..d175390 100644
--- a/devices/__init__.py
+++ b/devices/__init__.py
@@ -1,4 +1,4 @@
-# devices/__init__.py from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus
+# devices/__init__.py from devices.base_device import BaseDevice, SignalConfig, DeviceInfo, DeviceStatus
from devices.device_registry import DeviceRegistry
-__all__ = ["BaseDevice", "ChannelConfig", "DeviceInfo", "DeviceStatus", "DeviceRegistry"]
+__all__ = ["BaseDevice", "SignalConfig", "DeviceInfo", "DeviceStatus", "DeviceRegistry"]
diff --git a/devices/analog_input.py b/devices/analog_input.py
deleted file mode 100644
index 8938bf2..0000000
--- a/devices/analog_input.py
+++ /dev/null
@@ -1,461 +0,0 @@
-"""
-devices/analog_input.py
-
-Analog Input device. Backends: NI-DAQmx or Arduino.
-"""
-
-from typing import Any, Dict, List
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox,
- QComboBox, QDoubleSpinBox, QCheckBox, QLineEdit, QLabel,
- QPushButton, QListWidget, QListWidgetItem, QFrame, QTextEdit,
-)
-from PyQt6.QtCore import Qt, QThread, pyqtSignal
-from PyQt6.QtGui import QFont
-
-from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus
-from api_layers.nidaqmx_layer import NidaqmxLayer
-from api_layers.arduino_layer import ArduinoLayer
-
-_COLORS = [
- "#00d4ff", "#ff6b35", "#7fff6e", "#ffcc00",
- "#c77dff", "#ff4d6d", "#4cc9f0", "#f72585",
- "#38b000", "#e9c46a", "#a8dadc", "#e63946",
- "#90e0ef", "#fb8500", "#b5e48c", "#d62828",
-]
-
-
-class AnalogInputDevice(BaseDevice):
- DEVICE_TYPE = "analog_input"
- ICON = "〜"
-
- def __init__(
- self,
- device_id: str = "ai_0",
- num_channels: int = 4,
- simulate: bool = True,
- backend: str = "nidaqmx",
- ni_device: str = "Dev1",
- ni_min_v: float = -10.0,
- ni_max_v: float = 10.0,
- ard_port: str = "COM3",
- ard_baud: int = 115200,
- ):
- self.backend = backend
- self.simulate = simulate
-
- if backend == "arduino":
- pins = ArduinoLayer.DEFAULT_ANALOG_PINS[:num_channels]
- channels = [
- ChannelConfig(channel_id=p, name=p, unit="V",
- min_value=0.0, max_value=5.0,
- color=_COLORS[i % len(_COLORS)])
- for i, p in enumerate(pins)
- ]
- else:
- ni_pins = [f"ai{i}" for i in range(num_channels)]
- channels = [
- ChannelConfig(channel_id=p, name=p.upper(), unit="V",
- min_value=ni_min_v, max_value=ni_max_v,
- color=_COLORS[i % len(_COLORS)])
- for i, p in enumerate(ni_pins)
- ]
-
- info = DeviceInfo(
- device_id=device_id,
- name=f"Analog Input ({backend.upper()})",
- device_type=self.DEVICE_TYPE,
- description=f"Multi-channel analog input via {backend}",
- manufacturer="NI" if backend == "nidaqmx" else "Arduino",
- icon=self.ICON,
- channels=channels,
- )
- 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._num_channels = num_channels
- self._last_error = ""
-
- self._layer = self._make_layer(
- backend, simulate, ni_device, ni_min_v, ni_max_v,
- ard_port, ard_baud,
- [ch.channel_id for ch in channels],
- )
-
- def _make_layer(self, backend, simulate,
- ni_device, ni_min_v, ni_max_v,
- ard_port, ard_baud, channel_ids):
- if backend == "arduino":
- # Use shared layer so two devices on the same port don't conflict
- from api_layers.port_registry import port_registry
- return port_registry.get_layer(
- port=ard_port,
- baud=ard_baud,
- simulate=simulate,
- extra_pins=channel_ids,
- )
- else:
- return NidaqmxLayer(
- device_name=ni_device,
- channels=channel_ids,
- min_val=ni_min_v,
- max_val=ni_max_v,
- simulate=simulate,
- )
-
- # ── BaseDevice ──────────────────────────────────────────────────────────
-
- def connect(self) -> bool:
- self._last_error = ""
-
- # ArduinoLayer: use connect_if_needed so shared layers aren't
- # opened twice when multiple devices share the same port.
- # NidaqmxLayer: uses start()
- if hasattr(self._layer, "connect"):
- if self._layer.is_connected:
- ok = True # already open — shared with another device
- else:
- ok = self._layer.connect()
- else:
- ok = self._layer.start()
-
- if not ok and hasattr(self._layer, "last_error"):
- self._last_error = self._layer.last_error
-
- if self.simulate:
- self.status = DeviceStatus.SIMULATED
- else:
- self.status = DeviceStatus.CONNECTED if ok else DeviceStatus.ERROR
-
- return ok
-
- def disconnect(self) -> None:
- # For shared Arduino layers, release our ref count via port_registry.
- # The layer stays open until the last device using it disconnects.
- if self.backend == "arduino" and not self.simulate:
- from api_layers.port_registry import port_registry
- port_registry.release(self._ard_port, self._ard_baud)
- else:
- try:
- if hasattr(self._layer, "stop"):
- self._layer.stop()
- elif hasattr(self._layer, "disconnect"):
- self._layer.disconnect()
- except Exception:
- pass
- self.status = DeviceStatus.DISCONNECTED
-
- def read_channels(self) -> Dict[str, float]:
- return self._layer.read()
-
- def write_channel(self, channel_id: str, value: Any) -> bool:
- """
- Route output commands through the Arduino layer.
-
- channel_id examples:
- "D7" → digital write W:D7:1 or W:D7:0
- "D9" → PWM write P:D9:<duty> (if value is 0-255)
- "HEAT" → named param C:HEAT:1.0
- """
- if self.backend != "arduino" or not hasattr(self._layer, "digital_write"):
- return False
- ch = channel_id.strip()
- # Digital pin (D7, D13, etc.)
- if ch.upper().startswith("D") and ch[1:].isdigit():
- return self._layer.digital_write(ch, int(bool(value)))
- # 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,
- "name": self.info.name,
- "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)
-
- def switch_backend(self, backend: str, simulate: bool,
- ni_device: str, min_val: float, max_val: float,
- port: str, baud: int) -> None:
- was_running = self.status in (DeviceStatus.CONNECTED, DeviceStatus.SIMULATED)
- if was_running:
- self.disconnect()
-
- self.backend = backend
- self.simulate = simulate
- self._ni_device = ni_device
- self._ni_min_v = min_val
- self._ni_max_v = max_val
- self._ard_port = port
- self._ard_baud = baud
-
- self._layer = self._make_layer(
- backend, simulate, ni_device, min_val, max_val,
- port, baud,
- [ch.channel_id for ch in self.info.channels],
- )
-
- if was_running:
- self.connect()
-
-
-# ── Port scanner thread ───────────────────────────────────────────────────────
-
-class _PortScanThread(QThread):
- done = pyqtSignal(list)
-
- def run(self):
- ports = ArduinoLayer.list_ports() # returns [(device, desc), ...]
- self.done.emit(ports)
-
-
-# ── Config widget ─────────────────────────────────────────────────────────────
-
-class AnalogInputConfigWidget(QWidget):
- def __init__(self, device: AnalogInputDevice):
- super().__init__()
- self.device = device
- self._scanner = None
- self._build()
-
- def _build(self):
- root = QVBoxLayout(self)
- root.setContentsMargins(10, 14, 10, 10)
- root.setSpacing(12)
- self.setMinimumWidth(460)
-
- # ── Backend / Simulate ───────────────────────────────────────────
- be_grp = QGroupBox("Backend Selection")
- be_form = QFormLayout(be_grp)
- be_form.setContentsMargins(10, 16, 10, 10)
-
- self.backend_cb = QComboBox()
- self.backend_cb.addItems(["nidaqmx", "arduino"])
- self.backend_cb.setCurrentText(self.device.backend)
- be_form.addRow("Backend:", self.backend_cb)
-
- self.sim_check = QCheckBox("Simulation Mode (no hardware)")
- self.sim_check.setChecked(self.device.simulate)
- from core.app_settings import is_developer_mode
- self.sim_check.setVisible(is_developer_mode())
- be_form.addRow(self.sim_check)
- root.addWidget(be_grp)
-
- # ── NI-DAQmx ────────────────────────────────────────────────────
- self.ni_grp = QGroupBox("NI-DAQmx Settings")
- ni_form = QFormLayout(self.ni_grp)
- ni_form.setContentsMargins(10, 16, 10, 10)
-
- self.ni_dev_edit = QLineEdit(self.device._ni_device)
- ni_form.addRow("NI Device:", self.ni_dev_edit)
-
- self.ni_min_spin = QDoubleSpinBox()
- self.ni_min_spin.setRange(-100, 0)
- self.ni_min_spin.setValue(self.device._ni_min_v)
- self.ni_min_spin.setSuffix(" V")
- ni_form.addRow("Min Voltage:", self.ni_min_spin)
-
- self.ni_max_spin = QDoubleSpinBox()
- self.ni_max_spin.setRange(0, 100)
- self.ni_max_spin.setValue(self.device._ni_max_v)
- self.ni_max_spin.setSuffix(" V")
- ni_form.addRow("Max Voltage:", self.ni_max_spin)
-
- detect_btn = QPushButton("Detect NI Devices")
- detect_btn.setObjectName("configButton")
- detect_btn.clicked.connect(self._detect_ni)
- ni_form.addRow(detect_btn)
- self.ni_detect_lbl = QLabel("")
- self.ni_detect_lbl.setObjectName("traceSource")
- ni_form.addRow(self.ni_detect_lbl)
- root.addWidget(self.ni_grp)
-
- # ── Arduino ──────────────────────────────────────────────────────
- self.ard_grp = QGroupBox("Arduino Serial Settings")
- ard_lay = QVBoxLayout(self.ard_grp)
- ard_lay.setContentsMargins(10, 16, 10, 10)
- ard_lay.setSpacing(8)
-
- ard_form = QFormLayout()
- self.ard_port_edit = QLineEdit(self.device._ard_port)
- self.ard_port_edit.setPlaceholderText("e.g. /dev/ttyUSB0 or COM3")
- ard_form.addRow("Port:", self.ard_port_edit)
-
- self.ard_baud_cb = QComboBox()
- self.ard_baud_cb.addItems(["9600", "19200", "57600", "115200", "230400"])
- self.ard_baud_cb.setCurrentText(str(self.device._ard_baud))
- ard_form.addRow("Baud Rate:", self.ard_baud_cb)
- ard_lay.addLayout(ard_form)
-
- # Scan row
- scan_row = QHBoxLayout()
- self._scan_btn = QPushButton("🔍 Scan Ports")
- self._scan_btn.setObjectName("addTraceBtn")
- self._scan_btn.clicked.connect(self._scan_ports)
- self._scan_lbl = QLabel("")
- self._scan_lbl.setObjectName("traceSource")
- scan_row.addWidget(self._scan_btn)
- scan_row.addWidget(self._scan_lbl, 1)
- ard_lay.addLayout(scan_row)
-
- # Port list
- self._port_list = QListWidget()
- self._port_list.setObjectName("portList")
- self._port_list.setMaximumHeight(90)
- self._port_list.itemClicked.connect(self._on_port_selected)
- ard_lay.addWidget(self._port_list)
-
- hint = QLabel("↑ Click a port to fill the field above")
- hint.setObjectName("traceSource")
- ard_lay.addWidget(hint)
-
- # pyserial availability notice
- if not ArduinoLayer.is_pyserial_available():
- warn = QLabel("⚠ pyserial not installed — run: pip install pyserial")
- warn.setStyleSheet("color:#f59e0b; font-weight:600;")
- ard_lay.addWidget(warn)
-
- root.addWidget(self.ard_grp)
-
- # ── Diagnostics ───────────────────────────────────────────────────
- self.diag_grp = QGroupBox("Connection Diagnostics")
- diag_lay = QVBoxLayout(self.diag_grp)
- diag_lay.setContentsMargins(10, 16, 10, 10)
-
- self._diag_box = QTextEdit()
- self._diag_box.setObjectName("codeEditor")
- self._diag_box.setReadOnly(True)
- self._diag_box.setMaximumHeight(160)
- self._diag_box.setPlaceholderText("Connection log will appear here…")
- diag_lay.addWidget(self._diag_box)
-
- # Show any existing error
- if self.device._last_error:
- self._diag_box.setPlainText(f"Last error:\n{self.device._last_error}")
-
- root.addWidget(self.diag_grp)
-
- # ── Apply ─────────────────────────────────────────────────────────
- apply_btn = QPushButton("Apply & Reconnect")
- apply_btn.setObjectName("applyButton")
- apply_btn.clicked.connect(self._apply)
- root.addWidget(apply_btn)
- root.addStretch()
-
- self._update_visibility()
- self.backend_cb.currentTextChanged.connect(self._update_visibility)
- self._refresh_diag()
-
- # ── Helpers ──────────────────────────────────────────────────────────────
-
- def _update_visibility(self):
- be = self.backend_cb.currentText()
- self.ni_grp.setVisible(be == "nidaqmx")
- self.ard_grp.setVisible(be == "arduino")
-
- def _refresh_diag(self):
- lines = []
- lines.append(f"Backend: {self.device.backend}")
- lines.append(f"Simulate: {self.device.simulate}")
- lines.append(f"Status: {self.device.status.value}")
- if self.device.backend == "arduino":
- lines.append(f"Port: {self.device._ard_port}")
- lines.append(f"Baud: {self.device._ard_baud}")
- lines.append(f"pyserial: {'available' if ArduinoLayer.is_pyserial_available() else 'NOT INSTALLED'}")
- # Show last raw lines received from Arduino
- if hasattr(self.device._layer, "raw_lines"):
- raw = self.device._layer.raw_lines
- if raw:
- lines.append("\nLast lines from Arduino:")
- for r in raw:
- lines.append(f" {r}")
- if self.device._last_error:
- lines.append(f"\nError:\n{self.device._last_error}")
- self._diag_box.setPlainText("\n".join(lines))
-
- def _detect_ni(self):
- devs = NidaqmxLayer.list_devices()
- self.ni_detect_lbl.setText(
- ", ".join(devs) if devs else "No NI devices found (nidaqmx package required)"
- )
-
- def _scan_ports(self):
- self._scan_btn.setEnabled(False)
- self._scan_lbl.setText("Scanning…")
- self._port_list.clear()
- self._scanner = _PortScanThread()
- self._scanner.done.connect(self._on_scan_done)
- self._scanner.start()
-
- def _on_scan_done(self, ports):
- self._scan_btn.setEnabled(True)
- self._port_list.clear()
-
- if not ArduinoLayer.is_pyserial_available():
- self._scan_lbl.setText("pyserial not installed")
- item = QListWidgetItem(" Install pyserial: pip install pyserial")
- item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsSelectable)
- self._port_list.addItem(item)
- return
-
- if not ports:
- self._scan_lbl.setText("No ports detected")
- item = QListWidgetItem(
- " No serial ports found.\n"
- " • Check USB cable\n"
- " • On Linux: sudo usermod -aG dialout $USER then re-login\n"
- " • On Arch: sudo usermod -aG uucp $USER then re-login"
- )
- item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsSelectable)
- self._port_list.addItem(item)
- else:
- self._scan_lbl.setText(f"{len(ports)} port(s) found — click to select")
- for device, desc in ports:
- label = f" {device}"
- if desc and desc.strip() and desc.strip() != device:
- label += f" — {desc}"
- item = QListWidgetItem(label)
- item.setData(Qt.ItemDataRole.UserRole, device)
- self._port_list.addItem(item)
-
- def _on_port_selected(self, item: QListWidgetItem):
- port = item.data(Qt.ItemDataRole.UserRole)
- if port:
- self.ard_port_edit.setText(port)
-
- def _apply(self):
- backend = self.backend_cb.currentText()
- simulate = self.sim_check.isChecked()
- port = self.ard_port_edit.text().strip()
- baud = int(self.ard_baud_cb.currentText())
-
- self._diag_box.setPlainText(
- f"Connecting...\nBackend: {backend}\nSimulate: {simulate}\n"
- + (f"Port: {port}\nBaud: {baud}" if backend == "arduino" else "")
- )
-
- self.device.switch_backend(
- backend=backend,
- simulate=simulate,
- ni_device=self.ni_dev_edit.text().strip() or "Dev1",
- min_val=self.ni_min_spin.value(),
- max_val=self.ni_max_spin.value(),
- port=port,
- baud=baud,
- )
- self._refresh_diag()
diff --git a/devices/arduino_device.py b/devices/arduino_device.py
index 1641eaa..5c954c8 100644
--- a/devices/arduino_device.py
+++ b/devices/arduino_device.py
@@ -15,7 +15,7 @@ from PyQt6.QtWidgets import (
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
-from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus
+from devices.base_device import BaseDevice, SignalConfig, DeviceInfo, DeviceStatus
from api_layers.arduino_layer import ArduinoLayer
_ANALOG_COLORS = [
@@ -91,26 +91,11 @@ class ArduinoDevice(BaseDevice):
self._di_pins: List[str] = list(di_pins)
self._do_pins: List[str] = list(do_pins)
- channels = []
- for i, pin in enumerate(self._analog_pins):
- channels.append(ChannelConfig(
- channel_id=pin, name=pin, unit="V",
- min_value=0.0, max_value=5.0,
- color=_ANALOG_COLORS[i % len(_ANALOG_COLORS)],
- ))
- for i in range(len(self._di_pins)):
- channels.append(ChannelConfig(
- channel_id=f"di{i}", name=f"DI {i}", unit="",
- min_value=0.0, max_value=1.0,
- color=_DI_COLORS[i % len(_DI_COLORS)],
- ))
- for i in range(len(self._do_pins)):
- channels.append(ChannelConfig(
- channel_id=f"do{i}", name=f"DO {i}", unit="",
- min_value=0.0, max_value=1.0,
- writable=True,
- color=_DO_COLORS[i % len(_DO_COLORS)],
- ))
+ self._ai_map = {f"ai{i}": pin for i, pin in enumerate(self._analog_pins)}
+ self._di_map = {f"di{i}": pin for i, pin in enumerate(self._di_pins)}
+ self._do_map = {f"do{i}": pin for i, pin in enumerate(self._do_pins)}
+
+ channels = self._build_channels()
_desc = ArduinoLayer.describe_port(port) if not simulate else ""
if _desc:
@@ -121,22 +106,45 @@ class ArduinoDevice(BaseDevice):
device_id=device_id,
name=_name,
device_type=self.DEVICE_TYPE,
- description=(f"Arduino — {len(self._analog_pins)} analog, "
- f"{len(self._di_pins)} DI, {len(self._do_pins)} DO"),
+ description=self._description(),
manufacturer="Arduino",
icon=self.ICON,
- channels=channels,
+ signals=channels,
)
super().__init__(info)
self._output_state: Dict[str, int] = {f"do{i}": 0 for i in range(len(self._do_pins))}
- self._di_state: Dict[str, int] = {f"di{i}": 0 for i in range(len(self._di_pins))}
-
- self._di_map = {f"di{i}": pin for i, pin in enumerate(self._di_pins)}
- self._do_map = {f"do{i}": pin for i, pin in enumerate(self._do_pins)}
+ self._di_state: Dict[str, int] = {f"di{i}": 0 for i in range(len(self._di_pins))}
self._layer = self._make_layer()
+ def _description(self) -> str:
+ return (f"Arduino — {len(self._analog_pins)} analog, "
+ f"{len(self._di_pins)} DI, {len(self._do_pins)} DO")
+
+ def _build_channels(self) -> List[SignalConfig]:
+ channels = []
+ for i, pin in enumerate(self._analog_pins):
+ channels.append(SignalConfig(
+ signal_id=f"ai{i}", name=pin, unit="V",
+ min_value=0.0, max_value=5.0,
+ color=_ANALOG_COLORS[i % len(_ANALOG_COLORS)],
+ ))
+ for i in range(len(self._di_pins)):
+ channels.append(SignalConfig(
+ signal_id=f"di{i}", name=f"DI {i}", unit="",
+ min_value=0.0, max_value=1.0,
+ color=_DI_COLORS[i % len(_DI_COLORS)],
+ ))
+ for i in range(len(self._do_pins)):
+ channels.append(SignalConfig(
+ signal_id=f"do{i}", name=f"DO {i}", unit="",
+ min_value=0.0, max_value=1.0,
+ writable=True,
+ color=_DO_COLORS[i % len(_DO_COLORS)],
+ ))
+ return channels
+
def _make_layer(self):
from api_layers.port_registry import port_registry
return port_registry.get_layer(
@@ -177,10 +185,12 @@ class ArduinoDevice(BaseDevice):
def read_channels(self) -> Dict[str, float]:
raw = self._layer.read()
result = {}
- for ch in self.info.channels:
- cid = ch.channel_id
- if cid in raw:
- result[cid] = raw[cid]
+ for ch in self.info.signals:
+ cid = ch.signal_id
+ if cid.startswith("ai"):
+ pin = self._ai_map.get(cid)
+ if pin and pin in raw:
+ result[cid] = raw[pin]
elif cid.startswith("di"):
pin = self._di_map.get(cid)
if pin and pin in raw:
@@ -244,15 +254,25 @@ class ArduinoDevice(BaseDevice):
di_pins: List[str] = None,
do_pins: List[str] = None,
) -> None:
- """Update active pin mapping and send config commands if hardware-connected."""
+ """Update active pin mapping, rebuild channels, and send config
+ commands if hardware-connected. Caller is responsible for emitting
+ a `device_reconfigured` signal so the acquisition engine, profile,
+ and UI pick up the new channel list (see ui/windows/config_window.py)."""
if analog_pins is not None:
self._analog_pins = list(analog_pins)
+ self._ai_map = {f"ai{i}": p for i, p in enumerate(analog_pins)}
if di_pins is not None:
self._di_pins = list(di_pins)
self._di_map = {f"di{i}": p for i, p in enumerate(di_pins)}
if do_pins is not None:
self._do_pins = list(do_pins)
self._do_map = {f"do{i}": p for i, p in enumerate(do_pins)}
+
+ self.info.signals = self._build_channels()
+ self.info.description = self._description()
+ self._output_state = {f"do{i}": self._output_state.get(f"do{i}", 0) for i in range(len(self._do_pins))}
+ self._di_state = {f"di{i}": self._di_state.get(f"di{i}", 0) for i in range(len(self._di_pins))}
+
if self.status == DeviceStatus.CONNECTED:
self._layer.configure_pins(
di_pins=self._di_pins,
@@ -329,27 +349,40 @@ class ArduinoConfigWidget(QWidget):
# ── Pin Mapping ───────────────────────────────────────────────────
pin_grp = QGroupBox("Pin Mapping")
- pin_form = QFormLayout(pin_grp)
- pin_form.setContentsMargins(10, 16, 10, 10)
+ pin_outer = QVBoxLayout(pin_grp)
+ pin_outer.setContentsMargins(10, 16, 10, 10)
+ pin_outer.setSpacing(8)
+
+ pin_cols = QHBoxLayout()
+ pin_cols.setSpacing(12)
+ analog_grp = QGroupBox("Analog Pins")
+ analog_form = QFormLayout(analog_grp)
self.analog_pins_edit = QLineEdit(", ".join(p[1:] for p in self.device._analog_pins))
- self.analog_pins_edit.setPlaceholderText("e.g. 0, 1, 2, 3 (indices into A0–A5)")
- pin_form.addRow("Analog Input Pins:", self.analog_pins_edit)
+ self.analog_pins_edit.setPlaceholderText("e.g. 0, 1, 2, 3")
+ analog_form.addRow("Input:", self.analog_pins_edit)
+ pin_cols.addWidget(analog_grp)
+ digital_grp = QGroupBox("Digital Pins")
+ digital_form = QFormLayout(digital_grp)
self.di_pins_edit = QLineEdit(", ".join(p[1:] for p in self.device._di_pins))
self.di_pins_edit.setPlaceholderText("e.g. 2, 3, 8")
- pin_form.addRow("Digital Input Pins:", self.di_pins_edit)
-
+ digital_form.addRow("Input:", self.di_pins_edit)
self.do_pins_edit = QLineEdit(", ".join(p[1:] for p in self.device._do_pins))
- self.do_pins_edit.setPlaceholderText("e.g. 5, 6, 7, 9")
- pin_form.addRow("Digital Output Pins:", self.do_pins_edit)
+ self.do_pins_edit.setPlaceholderText("e.g. 5, 6, 7")
+ digital_form.addRow("Output:", self.do_pins_edit)
+ pin_cols.addWidget(digital_grp)
+
+ pin_outer.addLayout(pin_cols)
hint = QLabel("Changes take effect on Apply & Reconnect.\n"
"Analog: enter A-pin indices (0=A0, 1=A1, …). "
- "Digital: enter pin numbers.")
+ "Digital: enter pin numbers. "
+ "For analog-style output, drive a PWM-capable digital "
+ "output pin (e.g. D9) with a duty-cycle value.")
hint.setObjectName("traceSource")
hint.setWordWrap(True)
- pin_form.addRow(hint)
+ pin_outer.addWidget(hint)
root.addWidget(pin_grp)
@@ -417,19 +450,18 @@ class ArduinoConfigWidget(QWidget):
self.port_edit.setText(port)
def _apply(self):
- # Parse pin fields and update device before reconnect so connect() sends correct APIN/DPIN
+ # Parse pin fields and remap (rebuilds device.info.signals) before
+ # reconnect, so connect() sends correct APIN/DPIN and the acquisition
+ # engine picks up the new channel list once this dialog closes.
analog = _parse_analog_pin_edit(self.analog_pins_edit.text())
di = _parse_pin_edit(self.di_pins_edit.text())
do = _parse_pin_edit(self.do_pins_edit.text())
- if analog:
- self.device._analog_pins = analog
- if di:
- self.device._di_pins = di
- self.device._di_map = {f"di{i}": p for i, p in enumerate(di)}
- if do:
- self.device._do_pins = do
- self.device._do_map = {f"do{i}": p for i, p in enumerate(do)}
+ self.device.remap_pins(
+ analog_pins=analog or None,
+ di_pins=di or None,
+ do_pins=do or None,
+ )
self.device.switch_backend(
simulate=self.sim_chk.isChecked(),
port=self.port_edit.text().strip() or self.device._port,
diff --git a/devices/base_device.py b/devices/base_device.py
index 9803867..e3eed65 100644
--- a/devices/base_device.py
+++ b/devices/base_device.py
@@ -20,17 +20,19 @@ class DeviceStatus(Enum):
@dataclass
-class ChannelConfig:
- """Configuration for a single I/O channel."""
- channel_id: str
+class SignalConfig:
+ """Configuration for a single physical Signal — one I/O stream coming
+ off a Device (e.g. an Arduino analog pin, a digital line). Contrast
+ with a Channel (core/signal_processor.py), which takes one or more
+ Signals and applies math/filters to produce a new derived stream."""
+ signal_id: str
name: str
unit: str = ""
min_value: float = 0.0
max_value: float = 100.0
enabled: bool = True
- is_output: bool = False
color: str = "#00d4ff"
- writable: bool = False # True if this channel accepts write_channel() calls
+ writable: bool = False # True if this signal accepts write_channel() calls
extra: Dict[str, Any] = field(default_factory=dict)
@@ -39,13 +41,13 @@ class DeviceInfo:
"""Metadata describing a device module."""
device_id: str
name: str
- device_type: str # "analog_input" | "digital_io" | "serial" | "temperature"
+ device_type: str # "arduino" | "serial" | "nidaqmx" | ...
description: str = ""
manufacturer: str = ""
model: str = ""
version: str = "1.0.0"
icon: str = "⚙"
- channels: List[ChannelConfig] = field(default_factory=list)
+ signals: List[SignalConfig] = field(default_factory=list)
class BaseDevice(ABC):
@@ -75,7 +77,7 @@ class BaseDevice(ABC):
@abstractmethod
def read_channels(self) -> Dict[str, float]:
- """Return {channel_id: value} for all enabled channels."""
+ """Return {signal_id: value} for all enabled signals."""
@abstractmethod
def write_channel(self, channel_id: str, value: Any) -> bool:
@@ -97,8 +99,8 @@ class BaseDevice(ABC):
except Exception:
pass
- def get_channel(self, channel_id: str) -> Optional[ChannelConfig]:
- return next((c for c in self.info.channels if c.channel_id == channel_id), None)
+ def get_signal(self, signal_id: str) -> Optional[SignalConfig]:
+ return next((s for s in self.info.signals if s.signal_id == signal_id), None)
def __repr__(self):
return f"<{self.__class__.__name__} id={self.info.device_id} status={self.status.value}>"
diff --git a/devices/digital_io.py b/devices/digital_io.py
deleted file mode 100644
index 7cda61f..0000000
--- a/devices/digital_io.py
+++ /dev/null
@@ -1,334 +0,0 @@
-"""
-devices/digital_io.py
-
-Digital I/O device module. Backends: NI-DAQmx or Arduino.
-
-NI backend – uses nidaqmx digital line tasks (P0.0..P0.7)
-Arduino – uses ArduinoLayer digital pin reads; writes via W:Dxx:val
-"""
-
-import random
-import time
-from typing import Any, Dict
-
-from PyQt6.QtWidgets import (
- QWidget, QVBoxLayout, QHBoxLayout, QGroupBox,
- QCheckBox, QPushButton, QLabel, QFormLayout,
- QComboBox, QLineEdit,
-)
-
-from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus
-
-_IN_COLORS = ["#00d4ff", "#4cc9f0", "#90e0ef", "#caf0f8",
- "#0077b6", "#023e8a", "#48cae4", "#ade8f4"]
-_OUT_COLORS = ["#ff6b35", "#ffcc00", "#f77f00", "#fcbf49",
- "#d62828", "#e63946", "#fb8500", "#ffd166"]
-
-
-class DigitalIODevice(BaseDevice):
- DEVICE_TYPE = "digital_io"
- ICON = "⬛"
-
- def __init__(
- self,
- device_id: str = "dio_0",
- num_inputs: int = 8,
- num_outputs: int = 8,
- simulate: bool = True,
- backend: str = "nidaqmx", # "nidaqmx" | "arduino"
- ni_device: str = "Dev1",
- 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._num_inputs = num_inputs
- self._num_outputs = num_outputs
-
- channels = []
- for i in range(num_inputs):
- channels.append(ChannelConfig(
- channel_id=f"di{i}", name=f"DI {i}", unit="",
- min_value=0.0, max_value=1.0,
- color=_IN_COLORS[i % len(_IN_COLORS)],
- ))
- for i in range(num_outputs):
- channels.append(ChannelConfig(
- channel_id=f"do{i}", name=f"DO {i}", unit="",
- min_value=0.0, max_value=1.0,
- color=_OUT_COLORS[i % len(_OUT_COLORS)],
- writable=True,
- ))
-
- info = DeviceInfo(
- device_id=device_id,
- name=f"Digital I/O ({backend.upper()})",
- device_type=self.DEVICE_TYPE,
- description="Digital input/output module",
- icon=self.ICON,
- channels=channels,
- )
- super().__init__(info)
-
- self._output_state: Dict[str, int] = {
- f"do{i}": 0 for i in range(num_outputs)
- }
- self._sim_toggle: Dict[str, int] = {}
- self._sim_state: Dict[str, int] = {}
-
- # Hardware task placeholders
- self._ni_in_task = None
- self._ni_out_task = None
- self._ard_layer = None
-
- # ── BaseDevice ──────────────────────────────────────────────────────
-
- def connect(self) -> bool:
- if self.simulate:
- self.status = DeviceStatus.SIMULATED
- return True
-
- if self.backend == "nidaqmx":
- return self._ni_connect()
- else:
- return self._ard_connect()
-
- def _ni_connect(self) -> bool:
- try:
- import nidaqmx # type: ignore
- from nidaqmx.constants import LineGrouping # type: ignore
- n_in = sum(1 for c in self.info.channels if c.channel_id.startswith("di"))
- n_out = sum(1 for c in self.info.channels if c.channel_id.startswith("do"))
-
- if n_in:
- self._ni_in_task = nidaqmx.Task()
- for i in range(n_in):
- self._ni_in_task.di_channels.add_di_chan(
- f"{self._ni_device}/port0/line{i}",
- line_grouping=LineGrouping.CHAN_PER_LINE,
- )
- self._ni_in_task.start()
-
- if n_out:
- self._ni_out_task = nidaqmx.Task()
- for i in range(n_out):
- self._ni_out_task.do_channels.add_do_chan(
- f"{self._ni_device}/port1/line{i}",
- line_grouping=LineGrouping.CHAN_PER_LINE,
- )
- self._ni_out_task.start()
-
- self.status = DeviceStatus.CONNECTED
- return True
- except Exception as e:
- print(f"[DigitalIODevice] NI connect failed: {e}")
- self.status = DeviceStatus.ERROR
- return False
-
- def _ard_connect(self) -> bool:
- from api_layers.port_registry import port_registry
- # Get shared layer — won't open a second connection if AnalogInputDevice
- # is already connected on the same port
- self._ard_layer = port_registry.get_layer(
- port=self._ard_port,
- baud=self._ard_baud,
- simulate=False,
- )
- if self._ard_layer.is_connected:
- # Port already open — just register ourselves and return OK
- self.status = DeviceStatus.CONNECTED
- return True
- ok = self._ard_layer.connect()
- self.status = DeviceStatus.CONNECTED if ok else DeviceStatus.ERROR
- return ok
-
- def disconnect(self) -> None:
- if self._ni_in_task:
- try: self._ni_in_task.stop(); self._ni_in_task.close()
- except Exception: pass
- if self._ni_out_task:
- try: self._ni_out_task.stop(); self._ni_out_task.close()
- except Exception: pass
- if self._ard_layer and self.backend == "arduino" and not self.simulate:
- from api_layers.port_registry import port_registry
- port_registry.release(self._ard_port, self._ard_baud)
- self._ard_layer = None
- self.status = DeviceStatus.DISCONNECTED
-
- def read_channels(self) -> Dict[str, float]:
- if self.simulate:
- return self._sim_read()
- if self.backend == "nidaqmx":
- return self._ni_read()
- return self._ard_read()
-
- def _ni_read(self) -> Dict[str, float]:
- result = {}
- try:
- if self._ni_in_task:
- vals = self._ni_in_task.read()
- for i, ch in enumerate(c for c in self.info.channels if c.channel_id.startswith("di")):
- result[ch.channel_id] = float(vals[i] if isinstance(vals, list) else vals)
- except Exception as e:
- print(f"[DigitalIODevice] NI read failed: {e}")
- for ch in (c for c in self.info.channels if c.channel_id.startswith("do")):
- result[ch.channel_id] = float(self._output_state.get(ch.channel_id, 0))
- return result
-
- def _ard_read(self) -> Dict[str, float]:
- if not self._ard_layer:
- return {}
- raw = self._ard_layer.read()
- result = {}
- for ch in self.info.channels:
- if ch.channel_id in raw:
- result[ch.channel_id] = raw[ch.channel_id]
- elif ch.channel_id.startswith("do"):
- result[ch.channel_id] = float(self._output_state.get(ch.channel_id, 0))
- return result
-
- def _sim_read(self) -> Dict[str, float]:
- result = {}
- for ch in self.info.channels:
- if ch.channel_id.startswith("di"):
- cnt = self._sim_toggle.get(ch.channel_id, 0) + 1
- if cnt >= random.randint(8, 40):
- self._sim_state[ch.channel_id] = 1 - self._sim_state.get(ch.channel_id, 0)
- cnt = 0
- self._sim_toggle[ch.channel_id] = cnt
- result[ch.channel_id] = float(self._sim_state.get(ch.channel_id, 0))
- else:
- result[ch.channel_id] = float(self._output_state.get(ch.channel_id, 0))
- return result
-
- def write_channel(self, channel_id: str, value: Any) -> bool:
- self._output_state[channel_id] = int(bool(value))
- if not self.simulate:
- if self.backend == "nidaqmx" and self._ni_out_task:
- try:
- out_chs = [c for c in self.info.channels if c.channel_id.startswith("do")]
- idx = next((i for i, c in enumerate(out_chs) if c.channel_id == channel_id), None)
- if idx is not None:
- states = [self._output_state.get(c.channel_id, 0) for c in out_chs]
- self._ni_out_task.write(states)
- except Exception as e:
- print(f"[DigitalIODevice] NI write failed: {e}")
- elif self.backend == "arduino" and self._ard_layer:
- # Map channel ID to Arduino pin name:
- # do0→D5, do1→D6, do2→D7... (matches DIGITAL_OUT in firmware)
- # Or the channel_id itself if it already looks like D5
- pin = channel_id if channel_id.upper().startswith("D") else f"D{5 + int(channel_id.replace('do',''))}"
- self._ard_layer.digital_write(pin, int(bool(value)))
- return True
-
- def pwm_channel(self, channel_id: str, duty_pct: float) -> bool:
- """Send a PWM command to an Arduino output pin (0–100%)."""
- if self.backend == "arduino" and self._ard_layer and not self.simulate:
- pin = channel_id if channel_id.upper().startswith("D") else f"D{9 + int(channel_id.replace('do',''))}"
- return self._ard_layer.pwm_write_pct(pin, duty_pct)
- return False
-
- def set_parameter(self, name: str, value: Any) -> bool:
- """Send a named parameter/setpoint to the Arduino."""
- if self.backend == "arduino" and self._ard_layer and not self.simulate:
- return self._ard_layer.set_parameter(name, value)
- return False
-
- def switch_backend(self, backend: str, simulate: bool,
- ni_device: str, ard_port: str, ard_baud: int) -> None:
- was_running = self.status in (DeviceStatus.CONNECTED, DeviceStatus.SIMULATED)
- if was_running:
- self.disconnect()
-
- self.backend = backend
- self.simulate = simulate
- self._ni_device = ni_device
- self._ard_port = ard_port
- self._ard_baud = ard_baud
- self._ard_layer = None
- self._ni_in_task = None
- self._ni_out_task = None
-
- if was_running:
- self.connect()
-
- def get_save_config(self) -> dict:
- return {
- "device_type": self.DEVICE_TYPE,
- "device_id": self.info.device_id,
- "name": self.info.name,
- "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)
-
-
-class DigitalIOConfigWidget(QWidget):
- def __init__(self, device: DigitalIODevice):
- super().__init__()
- self.device = device
- self._build()
-
- def _build(self):
- root = QVBoxLayout(self)
- root.setContentsMargins(8, 8, 8, 8)
- self.setMinimumWidth(380)
-
- be_grp = QGroupBox("Backend")
- be_form = QFormLayout(be_grp)
- self.be_cb = QComboBox()
- self.be_cb.addItems(["nidaqmx", "arduino"])
- self.be_cb.setCurrentText(self.device.backend)
- be_form.addRow("Backend:", self.be_cb)
- self.sim_chk = QCheckBox("Simulate")
- self.sim_chk.setChecked(self.device.simulate)
- from core.app_settings import is_developer_mode
- self.sim_chk.setVisible(is_developer_mode())
- be_form.addRow(self.sim_chk)
-
- self.ni_dev_edit = QLineEdit(self.device._ni_device)
- be_form.addRow("NI Device:", self.ni_dev_edit)
-
- self.ard_port_edit = QLineEdit(self.device._ard_port)
- self.ard_port_edit.setPlaceholderText("e.g. /dev/ttyUSB0 or COM3")
- be_form.addRow("Arduino Port:", self.ard_port_edit)
-
- self.ard_baud_cb = QComboBox()
- self.ard_baud_cb.addItems(["9600", "19200", "57600", "115200", "230400"])
- self.ard_baud_cb.setCurrentText(str(self.device._ard_baud))
- be_form.addRow("Baud Rate:", self.ard_baud_cb)
-
- apply_btn = QPushButton("Apply & Reconnect")
- apply_btn.setObjectName("applyButton")
- apply_btn.clicked.connect(self._apply)
- be_form.addRow(apply_btn)
-
- self.be_cb.currentTextChanged.connect(self._update_visibility)
- root.addWidget(be_grp)
- self._update_visibility(self.device.backend)
- root.addStretch()
-
- def _update_visibility(self, backend: str = ""):
- backend = backend or self.be_cb.currentText()
- self.ni_dev_edit.setVisible(backend == "nidaqmx")
- self.ard_port_edit.setVisible(backend == "arduino")
- self.ard_baud_cb.setVisible(backend == "arduino")
-
- def _apply(self):
- self.device.switch_backend(
- backend=self.be_cb.currentText(),
- simulate=self.sim_chk.isChecked(),
- ni_device=self.ni_dev_edit.text().strip() or "Dev1",
- ard_port=self.ard_port_edit.text().strip(),
- ard_baud=int(self.ard_baud_cb.currentText()),
- )
diff --git a/devices/nidaqmx_device.py b/devices/nidaqmx_device.py
index 9ebec0f..652b7e2 100644
--- a/devices/nidaqmx_device.py
+++ b/devices/nidaqmx_device.py
@@ -13,7 +13,7 @@ from PyQt6.QtWidgets import (
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
-from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus
+from devices.base_device import BaseDevice, SignalConfig, DeviceInfo, DeviceStatus
from api_layers.nidaqmx_layer import NidaqmxLayer
_ANALOG_COLORS = [
@@ -53,20 +53,20 @@ class NidaqmxDevice(BaseDevice):
channels = []
for i in range(num_analog):
- channels.append(ChannelConfig(
- channel_id=f"ai{i}", name=f"AI{i}", unit="V",
+ channels.append(SignalConfig(
+ signal_id=f"ai{i}", name=f"AI{i}", unit="V",
min_value=min_v, max_value=max_v,
color=_ANALOG_COLORS[i % len(_ANALOG_COLORS)],
))
for i in range(num_di):
- channels.append(ChannelConfig(
- channel_id=f"di{i}", name=f"DI {i}", unit="",
+ channels.append(SignalConfig(
+ signal_id=f"di{i}", name=f"DI {i}", unit="",
min_value=0.0, max_value=1.0,
color=_DI_COLORS[i % len(_DI_COLORS)],
))
for i in range(num_do):
- channels.append(ChannelConfig(
- channel_id=f"do{i}", name=f"DO {i}", unit="",
+ channels.append(SignalConfig(
+ signal_id=f"do{i}", name=f"DO {i}", unit="",
min_value=0.0, max_value=1.0,
color=_DO_COLORS[i % len(_DO_COLORS)],
writable=True,
@@ -79,7 +79,7 @@ class NidaqmxDevice(BaseDevice):
description=f"NI-DAQmx — {num_analog} AI, {num_di} DI, {num_do} DO",
manufacturer="National Instruments",
icon=self.ICON,
- channels=channels,
+ signals=channels,
)
super().__init__(info)
@@ -99,8 +99,8 @@ class NidaqmxDevice(BaseDevice):
ok = True
try:
if self._num_analog > 0:
- ai_ids = [ch.channel_id for ch in self.info.channels
- if ch.channel_id.startswith("ai")]
+ ai_ids = [ch.signal_id for ch in self.info.signals
+ if ch.signal_id.startswith("ai")]
self._ai_layer = NidaqmxLayer(
device_name=self._ni_device,
channels=ai_ids,
@@ -179,14 +179,14 @@ class NidaqmxDevice(BaseDevice):
try:
if self._ni_in_task:
vals = self._ni_in_task.read()
- di_chs = [c for c in self.info.channels if c.channel_id.startswith("di")]
+ di_chs = [c for c in self.info.signals if c.signal_id.startswith("di")]
for i, ch in enumerate(di_chs):
- result[ch.channel_id] = float(vals[i] if isinstance(vals, list) else vals)
+ result[ch.signal_id] = float(vals[i] if isinstance(vals, list) else vals)
except Exception:
pass
- for ch in (c for c in self.info.channels if c.channel_id.startswith("do")):
- result[ch.channel_id] = float(self._output_state.get(ch.channel_id, 0))
+ for ch in (c for c in self.info.signals if c.signal_id.startswith("do")):
+ result[ch.signal_id] = float(self._output_state.get(ch.signal_id, 0))
return result
@@ -194,12 +194,12 @@ class NidaqmxDevice(BaseDevice):
import math, time
t = time.time()
result = {}
- for i, ch in enumerate([c for c in self.info.channels if c.channel_id.startswith("ai")]):
- result[ch.channel_id] = math.sin(t + i) * (self._max_v * 0.5)
- for ch in (c for c in self.info.channels if c.channel_id.startswith("di")):
- result[ch.channel_id] = 0.0
- for ch in (c for c in self.info.channels if c.channel_id.startswith("do")):
- result[ch.channel_id] = float(self._output_state.get(ch.channel_id, 0))
+ for i, ch in enumerate([c for c in self.info.signals if c.signal_id.startswith("ai")]):
+ result[ch.signal_id] = math.sin(t + i) * (self._max_v * 0.5)
+ for ch in (c for c in self.info.signals if c.signal_id.startswith("di")):
+ result[ch.signal_id] = 0.0
+ for ch in (c for c in self.info.signals if c.signal_id.startswith("do")):
+ result[ch.signal_id] = float(self._output_state.get(ch.signal_id, 0))
return result
def write_channel(self, channel_id: str, value: Any) -> bool:
@@ -208,8 +208,8 @@ class NidaqmxDevice(BaseDevice):
self._output_state[cid] = int(bool(value))
if not self.simulate and self._ni_out_task:
try:
- do_chs = [c for c in self.info.channels if c.channel_id.startswith("do")]
- states = [self._output_state.get(c.channel_id, 0) for c in do_chs]
+ do_chs = [c for c in self.info.signals if c.signal_id.startswith("do")]
+ states = [self._output_state.get(c.signal_id, 0) for c in do_chs]
self._ni_out_task.write(states)
except Exception as e:
print(f"[NidaqmxDevice] write failed: {e}")
diff --git a/devices/serial_device.py b/devices/serial_device.py
index d402f09..86d5e80 100644
--- a/devices/serial_device.py
+++ b/devices/serial_device.py
@@ -31,7 +31,7 @@ from PyQt6.QtWidgets import (
QStackedWidget, QTableWidget, QTableWidgetItem, QTextEdit, QVBoxLayout, QWidget,
)
-from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus
+from devices.base_device import BaseDevice, SignalConfig, DeviceInfo, DeviceStatus
from api_layers.arduino_layer import ArduinoLayer
_COLORS = ["#7fff6e", "#4cc9f0", "#f72585", "#00d4ff",
@@ -145,7 +145,7 @@ class SerialDevice(BaseDevice):
device_type=self.DEVICE_TYPE,
description=self._description(),
icon=self.ICON,
- channels=channels,
+ signals=channels,
)
super().__init__(info)
self._layer = self._make_layer()
@@ -175,21 +175,21 @@ class SerialDevice(BaseDevice):
# keep the channel's unit in sync so new plots/pickers/CSV log
# headers pick up the currently-selected unit instead of a fixed
# default. Does not relabel the axis of an already-open plot pane.
- force_ch = self.get_channel("force")
+ force_ch = self.get_signal("force")
if force_ch is not None:
force_ch.unit = self._layer.current_unit
if self._fmt not in _GENERIC_FORMATS:
- # Protocol layers already key by channel_id — pass through untouched.
+ # Protocol layers already key by signal_id — pass through untouched.
# (Write-only channels, e.g. CML "M1_VS", never appear in raw — correctly dropped.)
return raw
# Generic (ArduinoLayer) may use arbitrary names — remap by position.
mapped: Dict[str, float] = {}
raw_vals = list(raw.values())
- for i, ch in enumerate(self.info.channels):
- if ch.channel_id in raw:
- mapped[ch.channel_id] = raw[ch.channel_id]
+ for i, ch in enumerate(self.info.signals):
+ if ch.signal_id in raw:
+ mapped[ch.signal_id] = raw[ch.signal_id]
elif i < len(raw_vals):
- mapped[ch.channel_id] = raw_vals[i]
+ mapped[ch.signal_id] = raw_vals[i]
return mapped
def write_channel(self, channel_id: str, value: Any) -> bool:
@@ -217,9 +217,9 @@ class SerialDevice(BaseDevice):
cfg["port_serial"] = ArduinoLayer.port_serial_number(self._port)
if self._fmt in _GENERIC_FORMATS:
cfg.update({
- "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],
+ "num_channels": len(self.info.signals),
+ "channel_names": [c.signal_id for c in self.info.signals],
+ "units": [c.unit for c in self.info.signals],
})
elif self._fmt == "scpi":
cfg["scpi_channels"] = self._scpi_channels
@@ -274,7 +274,7 @@ class SerialDevice(BaseDevice):
self._mb_channels = modbus_channels or self._mb_channels
self._motors = motors or self._motors
- self.info.channels = self._build_channel_configs()
+ self.info.signals = self._build_channel_configs()
self.info.description = self._description()
self._layer = self._make_layer()
@@ -287,14 +287,14 @@ class SerialDevice(BaseDevice):
label = _LABEL_FOR.get(self._fmt, self._fmt)
return f"{self._port} @ {self._baud} [{label}]"
- def _build_channel_configs(self) -> List[ChannelConfig]:
+ def _build_channel_configs(self) -> List[SignalConfig]:
fmt = self._fmt
if fmt in _GENERIC_FORMATS:
names = self._chan_names or [f"sig{i}" for i in range(self._num_channels)]
_units = self._units or [""] * len(names)
return [
- ChannelConfig(
- channel_id=names[i], name=names[i], unit=_units[i],
+ SignalConfig(
+ signal_id=names[i], name=names[i], unit=_units[i],
min_value=0.0, max_value=1023.0,
color=_COLORS[i % len(_COLORS)],
)
@@ -302,8 +302,8 @@ class SerialDevice(BaseDevice):
]
elif fmt == "scpi":
return [
- ChannelConfig(
- channel_id=sc["channel_id"],
+ SignalConfig(
+ signal_id=sc["channel_id"],
name=sc.get("name", sc["channel_id"]),
unit=sc.get("unit", ""),
min_value=-1e9, max_value=1e9,
@@ -314,8 +314,8 @@ class SerialDevice(BaseDevice):
]
elif fmt == "modbus_rtu":
return [
- ChannelConfig(
- channel_id=mc["channel_id"],
+ SignalConfig(
+ signal_id=mc["channel_id"],
name=mc.get("name", mc["channel_id"]),
unit=mc.get("unit", ""),
min_value=-1e9, max_value=1e9,
@@ -328,15 +328,15 @@ class SerialDevice(BaseDevice):
elif fmt == "mark10":
from api_layers.protocols.mark10 import UNITS as _MARK10_UNITS
return [
- ChannelConfig("force", "Force", "N", -5000.0, 5000.0, color=_COLORS[0]),
- ChannelConfig("unit_code", "Unit Code", "", 0.0, float(len(_MARK10_UNITS) - 1), color=_COLORS[1]),
+ SignalConfig("force", "Force", "N", -5000.0, 5000.0, color=_COLORS[0]),
+ SignalConfig("unit_code", "Unit Code", "", 0.0, float(len(_MARK10_UNITS) - 1), color=_COLORS[1]),
# Write-only action channels (no reading — for control buttons)
- ChannelConfig("zero", "Zero Gauge", "", 0.0, 1.0, color=_COLORS[2], writable=True),
- ChannelConfig("cycle_units", "Cycle Units", "", 0.0, 1.0, color=_COLORS[3], writable=True),
+ SignalConfig("zero", "Zero Gauge", "", 0.0, 1.0, color=_COLORS[2], writable=True),
+ SignalConfig("cycle_units", "Cycle Units", "", 0.0, 1.0, color=_COLORS[3], writable=True),
]
elif fmt == "dscusb":
return [
- ChannelConfig("extension", "Extension", "", -50.0, 50.0, color=_COLORS[0]),
+ SignalConfig("extension", "Extension", "", -50.0, 50.0, color=_COLORS[0]),
]
elif fmt == "cml":
_CMD_UNITS = {"TP": "counts", "TV": "counts/s", "TC": "%×10", "TS": "flags"}
@@ -346,8 +346,8 @@ class SerialDevice(BaseDevice):
for motor in self._motors:
mid = motor.get("motor_id", "M1")
for cmd in motor.get("read_cmds", ["TP", "TV", "TC"]):
- channels.append(ChannelConfig(
- channel_id=f"{mid}_{cmd}",
+ channels.append(SignalConfig(
+ signal_id=f"{mid}_{cmd}",
name=f"{mid} {cmd}",
unit=_CMD_UNITS.get(cmd, ""),
min_value=-1e6, max_value=1e6,
@@ -356,8 +356,8 @@ class SerialDevice(BaseDevice):
color_idx += 1
# Write-only setpoint channels (no reading — for control widgets)
for cmd in ("VS", "MA"):
- channels.append(ChannelConfig(
- channel_id=f"{mid}_{cmd}",
+ channels.append(SignalConfig(
+ signal_id=f"{mid}_{cmd}",
name=f"{mid} {cmd}",
unit=_WRITE_UNITS.get(cmd, ""),
min_value=-1e6, max_value=1e6,
@@ -367,8 +367,8 @@ class SerialDevice(BaseDevice):
color_idx += 1
# Write-only action channels — motor must be enabled (ME) before VS/MA take effect
for cmd, label in (("ME", "Enable"), ("MD", "Disable"), ("ST", "Stop")):
- channels.append(ChannelConfig(
- channel_id=f"{mid}_{cmd}",
+ channels.append(SignalConfig(
+ signal_id=f"{mid}_{cmd}",
name=f"{mid} {label}",
unit="",
min_value=0.0, max_value=1.0,
@@ -376,13 +376,13 @@ class SerialDevice(BaseDevice):
writable=True,
))
color_idx += 1
- return channels or [ChannelConfig("M1_TP", "M1 TP", "counts", color=_COLORS[0])]
+ return channels or [SignalConfig("M1_TP", "M1 TP", "counts", color=_COLORS[0])]
return []
def _make_layer(self):
fmt = self._fmt
if fmt in _GENERIC_FORMATS:
- names = [ch.channel_id for ch in self.info.channels]
+ names = [ch.signal_id for ch in self.info.signals]
return ArduinoLayer(
port=self._port, baud=self._baud,
analog_pins=names, simulate=self.simulate,
@@ -632,7 +632,7 @@ class _GenericPanel(QWidget):
self._ch_spin = QSpinBox()
self._ch_spin.setRange(1, 32)
- self._ch_spin.setValue(len(device.info.channels) if device._fmt in _GENERIC_FORMATS else 4)
+ self._ch_spin.setValue(len(device.info.signals) if device._fmt in _GENERIC_FORMATS else 4)
lay.addRow("Signals:", self._ch_spin)
# ── Diagnostics ───────────────────────────────────────────────────