""" devices/base_device.py Abstract base class for all DAQ I/O modules. Every device plugin must subclass BaseDevice and implement the required methods. """ from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any, Dict, List, Optional from enum import Enum class DeviceStatus(Enum): DISCONNECTED = "disconnected" CONNECTING = "connecting" CONNECTED = "connected" ERROR = "error" SIMULATED = "simulated" @dataclass 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 color: str = "#00d4ff" writable: bool = False # True if this signal accepts write_channel() calls extra: Dict[str, Any] = field(default_factory=dict) @dataclass class DeviceInfo: """Metadata describing a device module.""" device_id: str name: str device_type: str # "arduino" | "serial" | "nidaqmx" | ... description: str = "" manufacturer: str = "" model: str = "" version: str = "1.0.0" icon: str = "⚙" signals: List[SignalConfig] = field(default_factory=list) class BaseDevice(ABC): """ Abstract base for all DAQ device plugins. To create a new module: 1. Subclass BaseDevice 2. Implement all @abstractmethod methods 3. Drop the file in devices/ — DeviceRegistry auto-discovers it """ def __init__(self, device_info: DeviceInfo): self.info = device_info self.status = DeviceStatus.DISCONNECTED self._callbacks: List[Any] = [] # ── Required interface ────────────────────────────────────────────── @abstractmethod def connect(self) -> bool: """Open connection. Returns True on success.""" @abstractmethod def disconnect(self) -> None: """Close connection and release resources.""" @abstractmethod def read_channels(self) -> Dict[str, float]: """Return {signal_id: value} for all enabled signals.""" @abstractmethod def write_channel(self, channel_id: str, value: Any) -> bool: """Write value to an output channel. Returns True on success.""" @abstractmethod def get_config_widget(self): """Return a QWidget with device-specific configuration controls.""" # ── Shared helpers ────────────────────────────────────────────────── def add_data_callback(self, cb) -> None: self._callbacks.append(cb) def _emit(self, channel_id: str, value: float, timestamp: float) -> None: for cb in self._callbacks: try: cb(self.info.device_id, channel_id, value, timestamp) except Exception: pass 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}>"