diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-04-13 14:36:55 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-04-13 14:36:55 -0600 |
| commit | 8d6acf3a8ea4b37f86b321dbf430be5be01b1267 (patch) | |
| tree | 923251b7ee88c7b5c89500883154bad32a2ecca5 /daq_system/devices/base_device.py | |
init
Diffstat (limited to 'daq_system/devices/base_device.py')
| -rw-r--r-- | daq_system/devices/base_device.py | 127 |
1 files changed, 127 insertions, 0 deletions
diff --git a/daq_system/devices/base_device.py b/daq_system/devices/base_device.py new file mode 100644 index 0000000..4898a7a --- /dev/null +++ b/daq_system/devices/base_device.py @@ -0,0 +1,127 @@ +""" +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 ChannelConfig: + """Configuration for a single I/O channel.""" + channel_id: str + name: str + unit: str = "" + min_value: float = 0.0 + max_value: float = 100.0 + alarm_low: Optional[float] = None + alarm_high: Optional[float] = None + enabled: bool = True + color: str = "#00d4ff" # For plotting + extra: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class DeviceInfo: + """Metadata describing a device module.""" + device_id: str + name: str + device_type: str # e.g. "analog_input", "digital_io", "serial", "temperature" + description: str = "" + manufacturer: str = "" + model: str = "" + version: str = "1.0.0" + icon: str = "⚙" # Unicode icon for UI display + channels: List[ChannelConfig] = field(default_factory=list) + + +class BaseDevice(ABC): + """ + Abstract base class for all DAQ device plugins. + + To create a new device module: + 1. Subclass BaseDevice + 2. Implement all @abstractmethod methods + 3. Place the file in the devices/ directory + 4. The DeviceRegistry will auto-discover it + """ + + def __init__(self, device_info: DeviceInfo): + self.info = device_info + self.status = DeviceStatus.DISCONNECTED + self._callbacks: List[callable] = [] + + # ------------------------------------------------------------------ # + # Abstract interface — every device must implement these # + # ------------------------------------------------------------------ # + + @abstractmethod + def connect(self) -> bool: + """ + Open connection to the physical device. + Returns True on success, False on failure. + Sets self.status appropriately. + """ + + @abstractmethod + def disconnect(self) -> None: + """Close the connection and release resources.""" + + @abstractmethod + def read_channels(self) -> Dict[str, float]: + """ + Read current values from all enabled channels. + Returns dict mapping channel_id -> float value. + Called repeatedly by the acquisition loop. + """ + + @abstractmethod + def write_channel(self, channel_id: str, value: Any) -> bool: + """ + Write a value to an output channel (if supported). + Returns True on success. + """ + + @abstractmethod + def get_config_widget(self): + """ + Return a QWidget with device-specific configuration controls. + This widget is embedded in the Device Config panel. + """ + + # ------------------------------------------------------------------ # + # Shared helpers # + # ------------------------------------------------------------------ # + + def add_data_callback(self, callback: callable) -> None: + """Register a callback: callback(device_id, channel_id, value, timestamp)""" + self._callbacks.append(callback) + + 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_channel(self, channel_id: str) -> Optional[ChannelConfig]: + for ch in self.info.channels: + if ch.channel_id == channel_id: + return ch + return None + + def __repr__(self): + return f"<{self.__class__.__name__} id={self.info.device_id} status={self.status.value}>" |
