summaryrefslogtreecommitdiff
path: root/daq_system/devices/base_device.py
blob: 4898a7a698806bcfbc240df225523387c63c441d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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}>"