summaryrefslogtreecommitdiff
path: root/devices/base_device.py
blob: 980386790e1ae651849d02559f778f57550aec8b (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
"""
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
    enabled:     bool           = True
    is_output:   bool           = False
    color:       str            = "#00d4ff"
    writable:    bool           = False  # True if this channel 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            # "analog_input" | "digital_io" | "serial" | "temperature"
    description: str            = ""
    manufacturer: str           = ""
    model:       str            = ""
    version:     str            = "1.0.0"
    icon:        str            = "⚙"
    channels:    List[ChannelConfig] = 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 {channel_id: value} for all enabled channels."""

    @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_channel(self, channel_id: str) -> Optional[ChannelConfig]:
        return next((c for c in self.info.channels if c.channel_id == channel_id), None)

    def __repr__(self):
        return f"<{self.__class__.__name__} id={self.info.device_id} status={self.status.value}>"