diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-04-13 15:22:56 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-04-13 15:22:56 -0600 |
| commit | 2ed9d37da7b27d25173535550fb92702225ac14e (patch) | |
| tree | d6ff360a656e577629d3cefdaa1ae02af7255864 | |
| parent | 8d6acf3a8ea4b37f86b321dbf430be5be01b1267 (diff) | |
Removed QtPy5 snippet and fixed directories
| -rw-r--r-- | api_layers/__init__.py (renamed from daq_system/daq_system/api_layers/__init__.py) | 0 | ||||
| -rw-r--r-- | api_layers/__pycache__/__init__.cpython-314.pyc | bin | 0 -> 324 bytes | |||
| -rw-r--r-- | api_layers/__pycache__/arduino_layer.cpython-314.pyc | bin | 0 -> 15070 bytes | |||
| -rw-r--r-- | api_layers/__pycache__/nidaqmx_layer.cpython-314.pyc | bin | 0 -> 10251 bytes | |||
| -rw-r--r-- | api_layers/arduino_layer.py (renamed from daq_system/daq_system/api_layers/arduino_layer.py) | 0 | ||||
| -rw-r--r-- | api_layers/nidaqmx_layer.py (renamed from daq_system/daq_system/api_layers/nidaqmx_layer.py) | 0 | ||||
| -rw-r--r-- | core/__init__.py (renamed from daq_system/daq_system/core/__init__.py) | 0 | ||||
| -rw-r--r-- | core/__pycache__/__init__.cpython-314.pyc | bin | 0 -> 277 bytes | |||
| -rw-r--r-- | core/__pycache__/acquisition.cpython-314.pyc | bin | 0 -> 14572 bytes | |||
| -rw-r--r-- | core/acquisition.py (renamed from daq_system/daq_system/core/acquisition.py) | 0 | ||||
| -rw-r--r-- | daq_system/core/__init__.py | 1 | ||||
| -rw-r--r-- | daq_system/core/__pycache__/__init__.cpython-312.pyc | bin | 161 -> 0 bytes | |||
| -rw-r--r-- | daq_system/core/__pycache__/acquisition.cpython-312.pyc | bin | 11381 -> 0 bytes | |||
| -rw-r--r-- | daq_system/core/acquisition.py | 214 | ||||
| -rw-r--r-- | daq_system/daq_system/README.md | 183 | ||||
| -rw-r--r-- | daq_system/devices/__init__.py | 1 | ||||
| -rw-r--r-- | daq_system/devices/__pycache__/__init__.cpython-312.pyc | bin | 164 -> 0 bytes | |||
| -rw-r--r-- | daq_system/devices/__pycache__/analog_input.cpython-312.pyc | bin | 9034 -> 0 bytes | |||
| -rw-r--r-- | daq_system/devices/__pycache__/base_device.cpython-312.pyc | bin | 6014 -> 0 bytes | |||
| -rw-r--r-- | daq_system/devices/__pycache__/device_registry.cpython-312.pyc | bin | 5219 -> 0 bytes | |||
| -rw-r--r-- | daq_system/devices/__pycache__/digital_io.cpython-312.pyc | bin | 6027 -> 0 bytes | |||
| -rw-r--r-- | daq_system/devices/__pycache__/serial_device.cpython-312.pyc | bin | 7405 -> 0 bytes | |||
| -rw-r--r-- | daq_system/devices/__pycache__/temperature.cpython-312.pyc | bin | 6707 -> 0 bytes | |||
| -rw-r--r-- | daq_system/devices/analog_input.py | 184 | ||||
| -rw-r--r-- | daq_system/devices/base_device.py | 127 | ||||
| -rw-r--r-- | daq_system/devices/device_registry.py | 87 | ||||
| -rw-r--r-- | daq_system/devices/digital_io.py | 95 | ||||
| -rw-r--r-- | daq_system/devices/serial_device.py | 136 | ||||
| -rw-r--r-- | daq_system/devices/temperature.py | 115 | ||||
| -rw-r--r-- | daq_system/main.py | 28 | ||||
| -rw-r--r-- | daq_system/ui/__pycache__/alarm_panel.cpython-312.pyc | bin | 5462 -> 0 bytes | |||
| -rw-r--r-- | daq_system/ui/__pycache__/config_dialog.cpython-312.pyc | bin | 9345 -> 0 bytes | |||
| -rw-r--r-- | daq_system/ui/__pycache__/device_panel.cpython-312.pyc | bin | 7943 -> 0 bytes | |||
| -rw-r--r-- | daq_system/ui/__pycache__/main_window.cpython-312.pyc | bin | 13127 -> 0 bytes | |||
| -rw-r--r-- | daq_system/ui/__pycache__/readout_panel.cpython-312.pyc | bin | 8378 -> 0 bytes | |||
| -rw-r--r-- | daq_system/ui/__pycache__/strip_chart.cpython-312.pyc | bin | 8328 -> 0 bytes | |||
| -rw-r--r-- | daq_system/ui/add_device_dialog.py | 7 | ||||
| -rw-r--r-- | daq_system/ui/alarm_panel.py | 85 | ||||
| -rw-r--r-- | daq_system/ui/config_dialog.py | 174 | ||||
| -rw-r--r-- | daq_system/ui/device_panel.py | 127 | ||||
| -rw-r--r-- | daq_system/ui/main_window.py | 228 | ||||
| -rw-r--r-- | daq_system/ui/readout_panel.py | 128 | ||||
| -rw-r--r-- | daq_system/ui/strip_chart.py | 140 | ||||
| -rw-r--r-- | devices/__init__.py (renamed from daq_system/daq_system/devices/__init__.py) | 0 | ||||
| -rw-r--r-- | devices/__pycache__/__init__.cpython-314.pyc | bin | 0 -> 398 bytes | |||
| -rw-r--r-- | devices/__pycache__/analog_input.cpython-314.pyc | bin | 0 -> 16238 bytes | |||
| -rw-r--r-- | devices/__pycache__/base_device.cpython-314.pyc | bin | 0 -> 7731 bytes | |||
| -rw-r--r-- | devices/__pycache__/device_registry.cpython-314.pyc | bin | 0 -> 5927 bytes | |||
| -rw-r--r-- | devices/__pycache__/digital_io.cpython-314.pyc | bin | 0 -> 19275 bytes | |||
| -rw-r--r-- | devices/__pycache__/serial_device.cpython-314.pyc | bin | 0 -> 11824 bytes | |||
| -rw-r--r-- | devices/analog_input.py (renamed from daq_system/daq_system/devices/analog_input.py) | 0 | ||||
| -rw-r--r-- | devices/base_device.py (renamed from daq_system/daq_system/devices/base_device.py) | 0 | ||||
| -rw-r--r-- | devices/device_registry.py (renamed from daq_system/daq_system/devices/device_registry.py) | 0 | ||||
| -rw-r--r-- | devices/digital_io.py (renamed from daq_system/daq_system/devices/digital_io.py) | 0 | ||||
| -rw-r--r-- | devices/serial_device.py (renamed from daq_system/daq_system/devices/serial_device.py) | 0 | ||||
| -rwxr-xr-x[-rw-r--r--] | main.py (renamed from daq_system/daq_system/main.py) | 6 | ||||
| -rw-r--r-- | requirements.txt (renamed from daq_system/daq_system/requirements.txt) | 0 | ||||
| -rw-r--r-- | ui/__init__.py (renamed from daq_system/daq_system/ui/__init__.py) | 0 | ||||
| -rw-r--r-- | ui/__pycache__/__init__.cpython-314.pyc | bin | 0 -> 153 bytes | |||
| -rw-r--r-- | ui/__pycache__/add_device_dialog.cpython-314.pyc | bin | 0 -> 5942 bytes | |||
| -rw-r--r-- | ui/__pycache__/alarm_panel.cpython-314.pyc | bin | 0 -> 6113 bytes | |||
| -rw-r--r-- | ui/__pycache__/config_dialog.cpython-314.pyc | bin | 0 -> 7331 bytes | |||
| -rw-r--r-- | ui/__pycache__/device_panel.cpython-314.pyc | bin | 0 -> 10002 bytes | |||
| -rw-r--r-- | ui/__pycache__/main_window.cpython-314.pyc | bin | 0 -> 15798 bytes | |||
| -rw-r--r-- | ui/__pycache__/readout_panel.cpython-314.pyc | bin | 0 -> 9028 bytes | |||
| -rw-r--r-- | ui/__pycache__/strip_chart.cpython-314.pyc | bin | 0 -> 11052 bytes | |||
| -rw-r--r-- | ui/add_device_dialog.py (renamed from daq_system/daq_system/ui/add_device_dialog.py) | 0 | ||||
| -rw-r--r-- | ui/alarm_panel.py (renamed from daq_system/daq_system/ui/alarm_panel.py) | 0 | ||||
| -rw-r--r-- | ui/config_dialog.py (renamed from daq_system/daq_system/ui/config_dialog.py) | 0 | ||||
| -rw-r--r-- | ui/device_panel.py (renamed from daq_system/daq_system/ui/device_panel.py) | 0 | ||||
| -rw-r--r-- | ui/main_window.py (renamed from daq_system/daq_system/ui/main_window.py) | 0 | ||||
| -rw-r--r-- | ui/readout_panel.py (renamed from daq_system/daq_system/ui/readout_panel.py) | 0 | ||||
| -rw-r--r-- | ui/strip_chart.py (renamed from daq_system/daq_system/ui/strip_chart.py) | 0 | ||||
| -rw-r--r-- | ui/style.qss (renamed from daq_system/daq_system/ui/style.qss) | 0 |
74 files changed, 3 insertions, 2063 deletions
diff --git a/daq_system/daq_system/api_layers/__init__.py b/api_layers/__init__.py index 242d6a9..242d6a9 100644 --- a/daq_system/daq_system/api_layers/__init__.py +++ b/api_layers/__init__.py diff --git a/api_layers/__pycache__/__init__.cpython-314.pyc b/api_layers/__pycache__/__init__.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..88ad51b --- /dev/null +++ b/api_layers/__pycache__/__init__.cpython-314.pyc diff --git a/api_layers/__pycache__/arduino_layer.cpython-314.pyc b/api_layers/__pycache__/arduino_layer.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..186b224 --- /dev/null +++ b/api_layers/__pycache__/arduino_layer.cpython-314.pyc diff --git a/api_layers/__pycache__/nidaqmx_layer.cpython-314.pyc b/api_layers/__pycache__/nidaqmx_layer.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..92f0ff3 --- /dev/null +++ b/api_layers/__pycache__/nidaqmx_layer.cpython-314.pyc diff --git a/daq_system/daq_system/api_layers/arduino_layer.py b/api_layers/arduino_layer.py index dea2783..dea2783 100644 --- a/daq_system/daq_system/api_layers/arduino_layer.py +++ b/api_layers/arduino_layer.py diff --git a/daq_system/daq_system/api_layers/nidaqmx_layer.py b/api_layers/nidaqmx_layer.py index ba40efb..ba40efb 100644 --- a/daq_system/daq_system/api_layers/nidaqmx_layer.py +++ b/api_layers/nidaqmx_layer.py diff --git a/daq_system/daq_system/core/__init__.py b/core/__init__.py index 06fabd5..06fabd5 100644 --- a/daq_system/daq_system/core/__init__.py +++ b/core/__init__.py diff --git a/core/__pycache__/__init__.cpython-314.pyc b/core/__pycache__/__init__.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..2a7cb7e --- /dev/null +++ b/core/__pycache__/__init__.cpython-314.pyc diff --git a/core/__pycache__/acquisition.cpython-314.pyc b/core/__pycache__/acquisition.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..d84b66b --- /dev/null +++ b/core/__pycache__/acquisition.cpython-314.pyc diff --git a/daq_system/daq_system/core/acquisition.py b/core/acquisition.py index 640e6d5..640e6d5 100644 --- a/daq_system/daq_system/core/acquisition.py +++ b/core/acquisition.py diff --git a/daq_system/core/__init__.py b/daq_system/core/__init__.py deleted file mode 100644 index 91db526..0000000 --- a/daq_system/core/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# core/__init__.py diff --git a/daq_system/core/__pycache__/__init__.cpython-312.pyc b/daq_system/core/__pycache__/__init__.cpython-312.pyc Binary files differdeleted file mode 100644 index 0ffb7a0..0000000 --- a/daq_system/core/__pycache__/__init__.cpython-312.pyc +++ /dev/null diff --git a/daq_system/core/__pycache__/acquisition.cpython-312.pyc b/daq_system/core/__pycache__/acquisition.cpython-312.pyc Binary files differdeleted file mode 100644 index 8e4bde7..0000000 --- a/daq_system/core/__pycache__/acquisition.cpython-312.pyc +++ /dev/null diff --git a/daq_system/core/acquisition.py b/daq_system/core/acquisition.py deleted file mode 100644 index 7e43529..0000000 --- a/daq_system/core/acquisition.py +++ /dev/null @@ -1,214 +0,0 @@ -""" -core/acquisition.py - -Background acquisition engine. Polls all connected devices at -their configured sample rates and emits data via Qt signals. -""" - -import time -import threading -import csv -import os -from collections import deque -from datetime import datetime -from typing import Callable, Dict, List, Optional, Tuple - -from PyQt6.QtCore import QObject, pyqtSignal - -from devices.base_device import BaseDevice, DeviceStatus - - -MAX_BUFFER = 10_000 # points per channel - - -class ChannelBuffer: - """Ring buffer for one channel's time-series data.""" - def __init__(self, maxlen: int = MAX_BUFFER): - self.times: deque = deque(maxlen=maxlen) - self.values: deque = deque(maxlen=maxlen) - - def append(self, t: float, v: float): - self.times.append(t) - self.values.append(v) - - def latest(self, n: int = 1) -> Tuple[List[float], List[float]]: - ts = list(self.times)[-n:] - vs = list(self.values)[-n:] - return ts, vs - - def all(self) -> Tuple[List[float], List[float]]: - return list(self.times), list(self.values) - - def clear(self): - self.times.clear() - self.values.clear() - - def __len__(self): - return len(self.times) - - -class AcquisitionEngine(QObject): - """ - Runs a background polling thread for all registered devices. - - Signals: - new_data(device_id, channel_id, timestamp, value) - alarm_triggered(device_id, channel_id, value, kind) kind = 'low'|'high' - device_status_changed(device_id, status_str) - log_written(filepath) - """ - - new_data = pyqtSignal(str, str, float, float) - alarm_triggered = pyqtSignal(str, str, float, str) - device_status_changed = pyqtSignal(str, str) - log_written = pyqtSignal(str) - - def __init__(self, poll_interval_ms: int = 100): - super().__init__() - self.poll_interval_s = poll_interval_ms / 1000.0 - self._devices: List[BaseDevice] = [] - self._buffers: Dict[str, Dict[str, ChannelBuffer]] = {} - self._running = False - self._thread: Optional[threading.Thread] = None - self._start_time = 0.0 - - # Logging - self._logging = False - self._log_file: Optional[str] = None - self._csv_writer = None - self._csv_handle = None - - # Alarm state (prevent repeated triggers) - self._alarm_active: Dict[str, bool] = {} - - # ------------------------------------------------------------------ # - # Device management # - # ------------------------------------------------------------------ # - - def add_device(self, device: BaseDevice) -> None: - self._devices.append(device) - self._buffers[device.info.device_id] = { - ch.channel_id: ChannelBuffer() - for ch in device.info.channels - } - - def remove_device(self, device_id: str) -> None: - self._devices = [d for d in self._devices if d.info.device_id != device_id] - self._buffers.pop(device_id, None) - - def get_buffer(self, device_id: str, channel_id: str) -> Optional[ChannelBuffer]: - return self._buffers.get(device_id, {}).get(channel_id) - - # ------------------------------------------------------------------ # - # Start / Stop # - # ------------------------------------------------------------------ # - - def start(self) -> None: - if self._running: - return - self._running = True - self._start_time = time.time() - self._thread = threading.Thread(target=self._loop, daemon=True) - self._thread.start() - - def stop(self) -> None: - self._running = False - if self._thread: - self._thread.join(timeout=2.0) - self.stop_logging() - - # ------------------------------------------------------------------ # - # Logging # - # ------------------------------------------------------------------ # - - def start_logging(self, filepath: Optional[str] = None) -> str: - if filepath is None: - ts = datetime.now().strftime("%Y%m%d_%H%M%S") - os.makedirs("logs", exist_ok=True) - filepath = f"logs/daq_{ts}.csv" - self._log_file = filepath - self._csv_handle = open(filepath, "w", newline="") - # Build header - headers = ["timestamp"] - for dev in self._devices: - for ch in dev.info.channels: - headers.append(f"{dev.info.device_id}.{ch.channel_id}") - self._csv_writer = csv.writer(self._csv_handle) - self._csv_writer.writerow(headers) - self._logging = True - return filepath - - def stop_logging(self) -> None: - self._logging = False - if self._csv_handle: - try: - self._csv_handle.close() - except Exception: - pass - self._csv_handle = None - self._csv_writer = None - - # ------------------------------------------------------------------ # - # Background loop # - # ------------------------------------------------------------------ # - - def _loop(self) -> None: - while self._running: - t0 = time.time() - timestamp = t0 - self._start_time - log_row = [f"{timestamp:.3f}"] - - for dev in self._devices: - if dev.status not in (DeviceStatus.CONNECTED, DeviceStatus.SIMULATED): - for ch in dev.info.channels: - log_row.append("") - continue - try: - readings = dev.read_channels() - except Exception as e: - print(f"[Acq] Error reading {dev.info.device_id}: {e}") - readings = {} - - for ch in dev.info.channels: - val = readings.get(ch.channel_id) - if val is None: - log_row.append("") - continue - # Buffer - buf = self._buffers.get(dev.info.device_id, {}).get(ch.channel_id) - if buf is not None: - buf.append(timestamp, val) - # Signal (emit on main thread via Qt queued connection) - self.new_data.emit(dev.info.device_id, ch.channel_id, timestamp, val) - log_row.append(f"{val:.4f}") - # Alarms - self._check_alarm(dev.info.device_id, ch, val) - - if self._logging and self._csv_writer: - try: - self._csv_writer.writerow(log_row) - except Exception: - pass - - elapsed = time.time() - t0 - sleep_t = self.poll_interval_s - elapsed - if sleep_t > 0: - time.sleep(sleep_t) - - def _check_alarm(self, device_id: str, ch, val: float) -> None: - key_lo = f"{device_id}.{ch.channel_id}.low" - key_hi = f"{device_id}.{ch.channel_id}.high" - - if ch.alarm_low is not None: - if val < ch.alarm_low and not self._alarm_active.get(key_lo): - self._alarm_active[key_lo] = True - self.alarm_triggered.emit(device_id, ch.channel_id, val, "low") - elif val >= ch.alarm_low: - self._alarm_active[key_lo] = False - - if ch.alarm_high is not None: - if val > ch.alarm_high and not self._alarm_active.get(key_hi): - self._alarm_active[key_hi] = True - self.alarm_triggered.emit(device_id, ch.channel_id, val, "high") - elif val <= ch.alarm_high: - self._alarm_active[key_hi] = False diff --git a/daq_system/daq_system/README.md b/daq_system/daq_system/README.md deleted file mode 100644 index 6ef85cc..0000000 --- a/daq_system/daq_system/README.md +++ /dev/null @@ -1,183 +0,0 @@ -# LabDAQ — Modular Python DAQ Frontend - -A production-grade, modular PyQt6 data acquisition UI supporting NI-DAQmx and Arduino backends with live strip-chart plotting, CSV logging, and per-device configuration. - ---- - -## Quick Start - -```bash -# 1. Install dependencies -pip install PyQt6 pyqtgraph numpy pyserial - -# 2. Run (simulation mode — no hardware required) -python main.py - -# 3. For real NI hardware -pip install nidaqmx # also install NI-DAQmx runtime from ni.com - -# 4. For real Arduino hardware -# Upload devices/arduino_firmware/labdaq.ino to your board -# Set port in device Configure dialog (e.g. COM3 / /dev/ttyUSB0) -``` - ---- - -## Project Structure - -``` -daq_system/ -├── main.py # Entry point -├── requirements.txt -│ -├── api_layers/ # Hardware abstraction layers -│ ├── nidaqmx_layer.py # NI-DAQmx wrapper + simulation -│ └── arduino_layer.py # Arduino serial wrapper + simulation -│ -├── devices/ # I/O device modules (auto-discovered) -│ ├── base_device.py # Abstract base class -│ ├── device_registry.py # Auto-discovery & instance manager -│ ├── analog_input.py # AI — NI or Arduino backend -│ ├── digital_io.py # DIO — NI or Arduino backend -│ └── serial_device.py # Generic serial / UART -│ -├── core/ -│ └── acquisition.py # Threaded polling engine, buffers, CSV log -│ -└── ui/ - ├── style.qss # Industrial dark theme - ├── main_window.py # Main window shell - ├── device_panel.py # Left sidebar — device cards - ├── strip_chart.py # Center — live pyqtgraph traces - ├── readout_panel.py # Right — numeric readouts - ├── alarm_panel.py # Right — alarm event log - ├── config_dialog.py # Per-device config dialog - └── add_device_dialog.py # Add device at runtime -``` - ---- - -## Backend Architecture - -### Swappable API Layers - -Each device module accepts a `backend` parameter: - -```python -# NI-DAQmx (real hardware) -dev = AnalogInputDevice( - device_id="ai_0", - backend="nidaqmx", - ni_device="Dev1", # NI device name - simulate=False, -) - -# Arduino (real hardware) -dev = AnalogInputDevice( - device_id="ard_0", - backend="arduino", - ard_port="COM3", # or "/dev/ttyUSB0" on Linux/Mac - ard_baud=115200, - simulate=False, -) - -# Simulation (no hardware) -dev = AnalogInputDevice(device_id="ai_sim", simulate=True) -``` - -You can hot-swap backends at runtime from the Configure dialog without restarting. - -### NI-DAQmx Layer (`api_layers/nidaqmx_layer.py`) - -- Auto-detects installed NI devices via `NidaqmxLayer.list_devices()` -- Falls back to simulation if `nidaqmx` package is not installed -- Configurable voltage range, sample rate, terminal configuration - -### Arduino Layer (`api_layers/arduino_layer.py`) - -- Serial protocol: `"A0:1.23,A1:4.56,D2:1\n"` (key:value CSV) -- Background read thread with latest-value cache -- Digital write: sends `"W:D13:1\n"` to Arduino -- Reference firmware included in `arduino_layer.py` as `ARDUINO_SKETCH` - ---- - -## Adding a New Device Module - -1. Create `devices/my_sensor.py` -2. Subclass `BaseDevice` -3. Implement: `connect()`, `disconnect()`, `read_channels()`, `write_channel()`, `get_config_widget()` -4. Drop the file in `devices/` — `DeviceRegistry` discovers it automatically - -```python -from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus - -class MySensor(BaseDevice): - def __init__(self, device_id="my_0", simulate=True): - channels = [ - ChannelConfig(channel_id="ch0", name="Pressure", unit="Pa", - min_value=0, max_value=1e5, color="#00d4ff"), - ] - info = DeviceInfo(device_id=device_id, name="My Sensor", - device_type="custom", channels=channels) - super().__init__(info) - self.simulate = simulate - - def connect(self): - self.status = DeviceStatus.SIMULATED if self.simulate else DeviceStatus.CONNECTED - return True - - def disconnect(self): - self.status = DeviceStatus.DISCONNECTED - - def read_channels(self): - import random - return {"ch0": random.uniform(0, 1e5)} - - def write_channel(self, channel_id, value): - return False - - def get_config_widget(self): - from PyQt6.QtWidgets import QLabel - return QLabel("No configuration needed.") -``` - ---- - -## Data Logging - -- Click **⬤ LOG** while acquisition is running -- CSV saved to `logs/daq_YYYYMMDD_HHMMSS.csv` -- Columns: `elapsed_s`, then one column per channel (`device_id/channel_id[unit]`) -- Stop logging with **⏹ LOGGING** — file is flushed and closed cleanly - ---- - -## Alarm System - -- Set per-channel `alarm_low` / `alarm_high` in the Configure → Channels & Alarms tab -- Alarms appear in the bottom-right panel with timestamp and direction (▲ HIGH / ▼ LOW) -- Alarm state is debounced — fires once on breach, resets when value recovers -- Channel readout highlights red while in alarm - ---- - -## Strip Chart - -- One plot row per device; all share a linked time axis -- Adjustable window (1 s → 3600 s) -- Auto or Fixed Y-scale -- Pause / Resume without stopping acquisition -- Powered by **pyqtgraph** for GPU-accelerated rendering - ---- - -## Dependencies - -| Package | Purpose | Required | -|-------------|----------------------------|----------| -| PyQt6 | UI framework | ✓ | -| pyqtgraph | Real-time strip chart | ✓ | -| numpy | Array math for chart data | ✓ | -| pyserial | Arduino serial comms | ✓ | -| nidaqmx | NI-DAQmx hardware access | Optional | diff --git a/daq_system/devices/__init__.py b/daq_system/devices/__init__.py deleted file mode 100644 index 9c98181..0000000 --- a/daq_system/devices/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# devices/__init__.py diff --git a/daq_system/devices/__pycache__/__init__.cpython-312.pyc b/daq_system/devices/__pycache__/__init__.cpython-312.pyc Binary files differdeleted file mode 100644 index af251ba..0000000 --- a/daq_system/devices/__pycache__/__init__.cpython-312.pyc +++ /dev/null diff --git a/daq_system/devices/__pycache__/analog_input.cpython-312.pyc b/daq_system/devices/__pycache__/analog_input.cpython-312.pyc Binary files differdeleted file mode 100644 index efe5752..0000000 --- a/daq_system/devices/__pycache__/analog_input.cpython-312.pyc +++ /dev/null diff --git a/daq_system/devices/__pycache__/base_device.cpython-312.pyc b/daq_system/devices/__pycache__/base_device.cpython-312.pyc Binary files differdeleted file mode 100644 index 4f08c29..0000000 --- a/daq_system/devices/__pycache__/base_device.cpython-312.pyc +++ /dev/null diff --git a/daq_system/devices/__pycache__/device_registry.cpython-312.pyc b/daq_system/devices/__pycache__/device_registry.cpython-312.pyc Binary files differdeleted file mode 100644 index 8d5ca55..0000000 --- a/daq_system/devices/__pycache__/device_registry.cpython-312.pyc +++ /dev/null diff --git a/daq_system/devices/__pycache__/digital_io.cpython-312.pyc b/daq_system/devices/__pycache__/digital_io.cpython-312.pyc Binary files differdeleted file mode 100644 index 9ca7bc7..0000000 --- a/daq_system/devices/__pycache__/digital_io.cpython-312.pyc +++ /dev/null diff --git a/daq_system/devices/__pycache__/serial_device.cpython-312.pyc b/daq_system/devices/__pycache__/serial_device.cpython-312.pyc Binary files differdeleted file mode 100644 index f4a2190..0000000 --- a/daq_system/devices/__pycache__/serial_device.cpython-312.pyc +++ /dev/null diff --git a/daq_system/devices/__pycache__/temperature.cpython-312.pyc b/daq_system/devices/__pycache__/temperature.cpython-312.pyc Binary files differdeleted file mode 100644 index ab2a2b9..0000000 --- a/daq_system/devices/__pycache__/temperature.cpython-312.pyc +++ /dev/null diff --git a/daq_system/devices/analog_input.py b/daq_system/devices/analog_input.py deleted file mode 100644 index 8aaf9db..0000000 --- a/daq_system/devices/analog_input.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -devices/analog_input.py - -Analog Input device module — reads voltage/current channels. -Includes a simulation mode (no hardware required) for development/demo. -""" - -import math -import random -import time -from typing import Any, Dict - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, - QDoubleSpinBox, QGroupBox, QCheckBox, QSpinBox, QFormLayout -) -from PyQt6.QtCore import Qt - -from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus - - -CHANNEL_COLORS = ["#00d4ff", "#ff6b35", "#7fff6e", "#ffcc00", "#c77dff", "#ff4d6d", "#4cc9f0", "#f72585"] - - -class AnalogInputDevice(BaseDevice): - """ - Analog voltage/current input module. - - Supports up to 16 channels. In simulation mode, generates - realistic waveforms (sine, ramp, noise) for each channel. - Real hardware: override read_channels() with your SDK calls. - """ - - DEVICE_TYPE = "analog_input" - ICON = "〜" - - def __init__(self, device_id: str = "ai_0", num_channels: int = 4, - simulate: bool = True, sample_rate_hz: float = 10.0): - channels = [ - ChannelConfig( - channel_id=f"ch{i}", - name=f"AI {i}", - unit="V", - min_value=-10.0, - max_value=10.0, - alarm_low=-8.0, - alarm_high=8.0, - color=CHANNEL_COLORS[i % len(CHANNEL_COLORS)], - ) - for i in range(num_channels) - ] - info = DeviceInfo( - device_id=device_id, - name="Analog Input", - device_type=self.DEVICE_TYPE, - description="Multi-channel analog voltage/current input", - manufacturer="Generic", - model="AI-16", - icon=self.ICON, - channels=channels, - ) - super().__init__(info) - self.simulate = simulate - self.sample_rate_hz = sample_rate_hz - self._start_time = 0.0 - # Sim parameters per channel - self._sim_params = [ - {"freq": 0.5 + i * 0.3, "amp": 5.0, "offset": 0.0, "noise": 0.05, "mode": "sine"} - for i in range(num_channels) - ] - - # ------------------------------------------------------------------ # - # BaseDevice interface # - # ------------------------------------------------------------------ # - - def connect(self) -> bool: - if self.simulate: - self._start_time = time.time() - self.status = DeviceStatus.SIMULATED - return True - # TODO: Replace with real hardware SDK init - # e.g. import nidaqmx; self._task = nidaqmx.Task(); ... - self.status = DeviceStatus.ERROR - return False - - def disconnect(self) -> None: - self.status = DeviceStatus.DISCONNECTED - - def read_channels(self) -> Dict[str, float]: - if self.simulate: - return self._simulate_read() - # TODO: Replace with real hardware read - return {} - - def write_channel(self, channel_id: str, value: Any) -> bool: - # Analog inputs don't support write — subclass for AO - return False - - def get_config_widget(self) -> QWidget: - return AnalogInputConfigWidget(self) - - # ------------------------------------------------------------------ # - # Simulation # - # ------------------------------------------------------------------ # - - def _simulate_read(self) -> Dict[str, float]: - t = time.time() - self._start_time - result = {} - for i, ch in enumerate(self.info.channels): - if not ch.enabled: - continue - p = self._sim_params[i] - if p["mode"] == "sine": - val = p["amp"] * math.sin(2 * math.pi * p["freq"] * t) + p["offset"] - elif p["mode"] == "ramp": - period = 1.0 / max(p["freq"], 0.01) - val = p["amp"] * ((t % period) / period) * 2 - p["amp"] + p["offset"] - elif p["mode"] == "square": - val = p["amp"] * math.copysign(1, math.sin(2 * math.pi * p["freq"] * t)) + p["offset"] - else: - val = p["offset"] - val += random.gauss(0, p["noise"] * p["amp"]) - val = max(ch.min_value, min(ch.max_value, val)) - result[ch.channel_id] = round(val, 4) - return result - - -# ------------------------------------------------------------------ # -# Config Widget # -# ------------------------------------------------------------------ # - -class AnalogInputConfigWidget(QWidget): - def __init__(self, device: AnalogInputDevice): - super().__init__() - self.device = device - self._build_ui() - - def _build_ui(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - - # Global settings - global_group = QGroupBox("Device Settings") - form = QFormLayout(global_group) - - self.sim_check = QCheckBox("Simulation Mode") - self.sim_check.setChecked(self.device.simulate) - form.addRow(self.sim_check) - - self.rate_spin = QDoubleSpinBox() - self.rate_spin.setRange(0.1, 1000.0) - self.rate_spin.setValue(self.device.sample_rate_hz) - self.rate_spin.setSuffix(" Hz") - form.addRow("Sample Rate:", self.rate_spin) - - layout.addWidget(global_group) - - # Per-channel - ch_group = QGroupBox("Channel Configuration") - ch_layout = QVBoxLayout(ch_group) - - for i, ch in enumerate(self.device.info.channels): - row = QHBoxLayout() - en = QCheckBox(ch.name) - en.setChecked(ch.enabled) - row.addWidget(en) - - mode_cb = QComboBox() - mode_cb.addItems(["sine", "ramp", "square", "dc"]) - mode_cb.setCurrentText(self.device._sim_params[i]["mode"]) - row.addWidget(QLabel("Mode:")) - row.addWidget(mode_cb) - - freq_sp = QDoubleSpinBox() - freq_sp.setRange(0.01, 100.0) - freq_sp.setValue(self.device._sim_params[i]["freq"]) - freq_sp.setSuffix(" Hz") - row.addWidget(QLabel("Freq:")) - row.addWidget(freq_sp) - - ch_layout.addLayout(row) - - layout.addWidget(ch_group) - layout.addStretch() diff --git a/daq_system/devices/base_device.py b/daq_system/devices/base_device.py deleted file mode 100644 index 4898a7a..0000000 --- a/daq_system/devices/base_device.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -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}>" diff --git a/daq_system/devices/device_registry.py b/daq_system/devices/device_registry.py deleted file mode 100644 index e407bc3..0000000 --- a/daq_system/devices/device_registry.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -devices/device_registry.py - -Discovers, instantiates, and manages all device modules. -Add new devices by dropping a .py file into the devices/ directory. -""" - -import importlib -import inspect -import pkgutil -from pathlib import Path -from typing import Dict, List, Optional, Type - -from devices.base_device import BaseDevice, DeviceInfo - - -class DeviceRegistry: - """Central registry for all DAQ device modules.""" - - def __init__(self): - self._device_classes: Dict[str, Type[BaseDevice]] = {} - self._instances: Dict[str, BaseDevice] = {} - self._auto_discover() - - # ------------------------------------------------------------------ # - # Discovery # - # ------------------------------------------------------------------ # - - def _auto_discover(self): - """Scan the devices/ package for BaseDevice subclasses.""" - devices_path = Path(__file__).parent - package = "devices" - - for _, module_name, _ in pkgutil.iter_modules([str(devices_path)]): - if module_name.startswith("_") or module_name in ("base_device", "device_registry"): - continue - try: - module = importlib.import_module(f"{package}.{module_name}") - for name, obj in inspect.getmembers(module, inspect.isclass): - if issubclass(obj, BaseDevice) and obj is not BaseDevice: - self._device_classes[name] = obj - except Exception as e: - print(f"[DeviceRegistry] Failed to load {module_name}: {e}") - - def register_class(self, cls: Type[BaseDevice]) -> None: - """Manually register a device class (for testing / runtime plugins).""" - self._device_classes[cls.__name__] = cls - - # ------------------------------------------------------------------ # - # Instance management # - # ------------------------------------------------------------------ # - - def create_device(self, class_name: str, device_id: str, **kwargs) -> Optional[BaseDevice]: - """Instantiate a device by class name with a unique device_id.""" - cls = self._device_classes.get(class_name) - if cls is None: - raise ValueError(f"Unknown device class: {class_name}") - instance = cls(device_id=device_id, **kwargs) - self._instances[device_id] = instance - return instance - - def add_instance(self, device: BaseDevice) -> None: - """Register a pre-built device instance.""" - self._instances[device.info.device_id] = device - - def remove_instance(self, device_id: str) -> None: - dev = self._instances.pop(device_id, None) - if dev: - try: - dev.disconnect() - except Exception: - pass - - def get_instance(self, device_id: str) -> Optional[BaseDevice]: - return self._instances.get(device_id) - - def all_instances(self) -> List[BaseDevice]: - return list(self._instances.values()) - - def available_classes(self) -> List[str]: - return list(self._device_classes.keys()) - - def get_class(self, class_name: str) -> Optional[Type[BaseDevice]]: - return self._device_classes.get(class_name) - - def __len__(self): - return len(self._instances) diff --git a/daq_system/devices/digital_io.py b/daq_system/devices/digital_io.py deleted file mode 100644 index b78b5cf..0000000 --- a/daq_system/devices/digital_io.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -devices/digital_io.py - -Digital I/O device module — reads and writes binary channels. -""" - -import random -import time -from typing import Any, Dict - -from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QCheckBox, QGroupBox, QPushButton, QLabel -from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus - - -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): - 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="#00d4ff" if i % 2 == 0 else "#4cc9f0" - )) - 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="#ff6b35" if i % 2 == 0 else "#ffcc00" - )) - info = DeviceInfo( - device_id=device_id, name="Digital I/O", - device_type=self.DEVICE_TYPE, - description="Digital input/output module", - icon=self.ICON, channels=channels - ) - super().__init__(info) - self.simulate = simulate - self._output_state: Dict[str, int] = {f"do{i}": 0 for i in range(num_outputs)} - self._toggle_counters = [0] * num_inputs - - def connect(self) -> bool: - self.status = DeviceStatus.SIMULATED if self.simulate else DeviceStatus.ERROR - return self.simulate - - def disconnect(self) -> None: - self.status = DeviceStatus.DISCONNECTED - - def read_channels(self) -> Dict[str, float]: - result = {} - # Simulate toggling inputs randomly - for i, ch in enumerate(self.info.channels): - if ch.channel_id.startswith("di"): - self._toggle_counters[i] += 1 - if self._toggle_counters[i] > random.randint(5, 30): - self._toggle_counters[i] = 0 - result[ch.channel_id] = float(random.randint(0, 1)) - else: - result[ch.channel_id] = result.get(ch.channel_id, 0.0) - elif ch.channel_id.startswith("do"): - 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: - if channel_id in self._output_state: - self._output_state[channel_id] = int(bool(value)) - return True - return False - - def get_config_widget(self) -> QWidget: - w = QWidget() - layout = QVBoxLayout(w) - grp = QGroupBox("Output Controls") - grp_layout = QVBoxLayout(grp) - for k in self._output_state: - row = QHBoxLayout() - lbl = QLabel(k.upper()) - btn = QPushButton("OFF") - btn.setCheckable(True) - btn.setChecked(bool(self._output_state[k])) - btn.setText("ON" if self._output_state[k] else "OFF") - channel_id = k - def on_toggle(checked, cid=channel_id, b=btn): - self.write_channel(cid, checked) - b.setText("ON" if checked else "OFF") - btn.toggled.connect(on_toggle) - row.addWidget(lbl) - row.addWidget(btn) - grp_layout.addLayout(row) - layout.addWidget(grp) - layout.addStretch() - return w diff --git a/daq_system/devices/serial_device.py b/daq_system/devices/serial_device.py deleted file mode 100644 index 842a401..0000000 --- a/daq_system/devices/serial_device.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -devices/serial_device.py - -Serial / UART device module — reads data from a serial port. -Parses CSV-format lines: "ch0,ch1,ch2,...\\n" -""" - -import random -import time -from typing import Any, Dict - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QFormLayout, QGroupBox, - QComboBox, QSpinBox, QLineEdit, QPushButton, QLabel -) -from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus - -SER_COLORS = ["#7fff6e", "#4cc9f0", "#f72585", "#00d4ff"] - - -class SerialDevice(BaseDevice): - DEVICE_TYPE = "serial" - ICON = "⇌" - - def __init__(self, device_id: str = "ser_0", port: str = "COM3", - baud_rate: int = 115200, num_channels: int = 4, - simulate: bool = True): - channels = [ - ChannelConfig( - channel_id=f"s{i}", name=f"Serial {i}", - unit="", min_value=0.0, max_value=1023.0, - color=SER_COLORS[i % len(SER_COLORS)] - ) - for i in range(num_channels) - ] - info = DeviceInfo( - device_id=device_id, name="Serial / UART", - device_type=self.DEVICE_TYPE, - description=f"Serial port {port} @ {baud_rate} baud", - icon=self.ICON, channels=channels - ) - super().__init__(info) - self.port = port - self.baud_rate = baud_rate - self.simulate = simulate - self._serial = None # Replace with serial.Serial() for real hardware - self._t0 = 0.0 - - def connect(self) -> bool: - if self.simulate: - self._t0 = time.time() - self.status = DeviceStatus.SIMULATED - return True - try: - import serial - self._serial = serial.Serial(self.port, self.baud_rate, timeout=0.1) - self.status = DeviceStatus.CONNECTED - return True - except Exception as e: - print(f"[SerialDevice] Connect failed: {e}") - self.status = DeviceStatus.ERROR - return False - - def disconnect(self) -> None: - if self._serial: - try: - self._serial.close() - except Exception: - pass - self.status = DeviceStatus.DISCONNECTED - - def read_channels(self) -> Dict[str, float]: - if self.simulate: - return self._simulate_read() - if not self._serial or not self._serial.is_open: - return {} - try: - line = self._serial.readline().decode("utf-8").strip() - if not line: - return {} - parts = line.split(",") - return { - ch.channel_id: float(parts[i]) - for i, ch in enumerate(self.info.channels) - if i < len(parts) - } - except Exception: - return {} - - def _simulate_read(self) -> Dict[str, float]: - t = time.time() - self._t0 - import math - return { - ch.channel_id: round(512 + 400 * math.sin(2 * 3.14159 * (0.2 + i * 0.15) * t) - + random.gauss(0, 5), 1) - for i, ch in enumerate(self.info.channels) - } - - def write_channel(self, channel_id: str, value: Any) -> bool: - if self._serial and self._serial.is_open: - try: - cmd = f"{channel_id}:{value}\n" - self._serial.write(cmd.encode()) - return True - except Exception: - return False - return False - - def get_config_widget(self) -> QWidget: - w = QWidget() - layout = QVBoxLayout(w) - grp = QGroupBox("Serial Port Settings") - form = QFormLayout(grp) - - self._port_edit = QLineEdit(self.port) - form.addRow("Port:", self._port_edit) - - self._baud_cb = QComboBox() - self._baud_cb.addItems(["9600", "19200", "38400", "57600", "115200", "230400", "460800"]) - self._baud_cb.setCurrentText(str(self.baud_rate)) - form.addRow("Baud Rate:", self._baud_cb) - - parity_cb = QComboBox() - parity_cb.addItems(["None", "Even", "Odd"]) - form.addRow("Parity:", parity_cb) - - bits_cb = QComboBox() - bits_cb.addItems(["8", "7"]) - form.addRow("Data Bits:", bits_cb) - - apply_btn = QPushButton("Apply & Reconnect") - form.addRow(apply_btn) - - layout.addWidget(grp) - layout.addStretch() - return w diff --git a/daq_system/devices/temperature.py b/daq_system/devices/temperature.py deleted file mode 100644 index 5427cf6..0000000 --- a/daq_system/devices/temperature.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -devices/temperature.py - -Temperature sensor module — thermocouple / RTD / thermistor inputs. -""" - -import math -import random -import time -from typing import Any, Dict - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QFormLayout, QGroupBox, - QComboBox, QDoubleSpinBox, QLabel, QCheckBox -) -from devices.base_device import BaseDevice, ChannelConfig, DeviceInfo, DeviceStatus - -TEMP_COLORS = ["#ff6b35", "#ffcc00", "#c77dff", "#ff4d6d"] - - -class TemperatureDevice(BaseDevice): - DEVICE_TYPE = "temperature" - ICON = "🌡" - - def __init__(self, device_id: str = "temp_0", num_channels: int = 4, - simulate: bool = True, sensor_type: str = "thermocouple"): - channels = [ - ChannelConfig( - channel_id=f"tc{i}", name=f"TC {i}", - unit="°C", min_value=-200.0, max_value=1200.0, - alarm_low=0.0, alarm_high=100.0, - color=TEMP_COLORS[i % len(TEMP_COLORS)] - ) - for i in range(num_channels) - ] - info = DeviceInfo( - device_id=device_id, name="Temperature", - device_type=self.DEVICE_TYPE, - description=f"{sensor_type.title()} temperature input", - icon=self.ICON, channels=channels - ) - super().__init__(info) - self.simulate = simulate - self.sensor_type = sensor_type - self._start = time.time() - # Simulate slow thermal drift - self._targets = [20.0 + i * 5 for i in range(num_channels)] - self._currents = [20.0 + i * 5 for i in range(num_channels)] - - def connect(self) -> bool: - self._start = time.time() - self.status = DeviceStatus.SIMULATED if self.simulate else DeviceStatus.ERROR - return self.simulate - - def disconnect(self) -> None: - self.status = DeviceStatus.DISCONNECTED - - def read_channels(self) -> Dict[str, float]: - result = {} - for i, ch in enumerate(self.info.channels): - if not ch.enabled: - continue - # Slow drift toward target with noise - diff = self._targets[i] - self._currents[i] - self._currents[i] += diff * 0.05 + random.gauss(0, 0.02) - # Occasionally shift target - if random.random() < 0.01: - self._targets[i] += random.gauss(0, 2.0) - self._targets[i] = max(10.0, min(200.0, self._targets[i])) - result[ch.channel_id] = round(self._currents[i], 2) - return result - - def write_channel(self, channel_id: str, value: Any) -> bool: - return False # Read-only - - def get_config_widget(self) -> QWidget: - w = QWidget() - layout = QVBoxLayout(w) - grp = QGroupBox("Sensor Configuration") - form = QFormLayout(grp) - - sensor_cb = QComboBox() - sensor_cb.addItems(["thermocouple", "rtd", "thermistor", "ic_sensor"]) - sensor_cb.setCurrentText(self.sensor_type) - form.addRow("Sensor Type:", sensor_cb) - - tc_type = QComboBox() - tc_type.addItems(["K", "J", "T", "E", "N", "R", "S", "B"]) - form.addRow("TC Type:", tc_type) - - unit_cb = QComboBox() - unit_cb.addItems(["°C", "°F", "K"]) - form.addRow("Units:", unit_cb) - - layout.addWidget(grp) - - # Alarm config per channel - alarm_grp = QGroupBox("Alarm Setpoints") - alarm_layout = QVBoxLayout(alarm_grp) - for ch in self.info.channels: - row_layout = QFormLayout() - lo = QDoubleSpinBox() - lo.setRange(-200, 1200) - lo.setValue(ch.alarm_low or 0.0) - lo.setSuffix(" °C") - hi = QDoubleSpinBox() - hi.setRange(-200, 1200) - hi.setValue(ch.alarm_high or 100.0) - hi.setSuffix(" °C") - row_layout.addRow(f"{ch.name} Low:", lo) - row_layout.addRow(f"{ch.name} High:", hi) - alarm_layout.addLayout(row_layout) - layout.addWidget(alarm_grp) - layout.addStretch() - return w diff --git a/daq_system/main.py b/daq_system/main.py deleted file mode 100644 index d4979b6..0000000 --- a/daq_system/main.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Lab DAQ System - Main Entry Point -Run this file to launch the application. -""" - -import sys -from PyQt6.QtWidgets import QApplication -from PyQt6.QtCore import Qt -from ui.main_window import MainWindow - - -def main(): - app = QApplication(sys.argv) - app.setApplicationName("LabDAQ") - app.setOrganizationName("Lab Instruments") - app.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps) - - # Load global stylesheet - with open("ui/style.qss", "r") as f: - app.setStyleSheet(f.read()) - - window = MainWindow() - window.show() - sys.exit(app.exec()) - - -if __name__ == "__main__": - main() diff --git a/daq_system/ui/__pycache__/alarm_panel.cpython-312.pyc b/daq_system/ui/__pycache__/alarm_panel.cpython-312.pyc Binary files differdeleted file mode 100644 index 7f2776c..0000000 --- a/daq_system/ui/__pycache__/alarm_panel.cpython-312.pyc +++ /dev/null diff --git a/daq_system/ui/__pycache__/config_dialog.cpython-312.pyc b/daq_system/ui/__pycache__/config_dialog.cpython-312.pyc Binary files differdeleted file mode 100644 index d62887f..0000000 --- a/daq_system/ui/__pycache__/config_dialog.cpython-312.pyc +++ /dev/null diff --git a/daq_system/ui/__pycache__/device_panel.cpython-312.pyc b/daq_system/ui/__pycache__/device_panel.cpython-312.pyc Binary files differdeleted file mode 100644 index 0616690..0000000 --- a/daq_system/ui/__pycache__/device_panel.cpython-312.pyc +++ /dev/null diff --git a/daq_system/ui/__pycache__/main_window.cpython-312.pyc b/daq_system/ui/__pycache__/main_window.cpython-312.pyc Binary files differdeleted file mode 100644 index d6af385..0000000 --- a/daq_system/ui/__pycache__/main_window.cpython-312.pyc +++ /dev/null diff --git a/daq_system/ui/__pycache__/readout_panel.cpython-312.pyc b/daq_system/ui/__pycache__/readout_panel.cpython-312.pyc Binary files differdeleted file mode 100644 index 47d94da..0000000 --- a/daq_system/ui/__pycache__/readout_panel.cpython-312.pyc +++ /dev/null diff --git a/daq_system/ui/__pycache__/strip_chart.cpython-312.pyc b/daq_system/ui/__pycache__/strip_chart.cpython-312.pyc Binary files differdeleted file mode 100644 index 95dc075..0000000 --- a/daq_system/ui/__pycache__/strip_chart.cpython-312.pyc +++ /dev/null diff --git a/daq_system/ui/add_device_dialog.py b/daq_system/ui/add_device_dialog.py deleted file mode 100644 index 8851c99..0000000 --- a/daq_system/ui/add_device_dialog.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -ui/add_device_dialog.py - -Re-exported from config_dialog for import convenience. -""" - -from ui.config_dialog import AddDeviceDialog # noqa: F401 diff --git a/daq_system/ui/alarm_panel.py b/daq_system/ui/alarm_panel.py deleted file mode 100644 index 590d785..0000000 --- a/daq_system/ui/alarm_panel.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -ui/alarm_panel.py — Alarm event log panel. -""" - -from datetime import datetime -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QScrollArea, QFrame, QPushButton -) -from PyQt6.QtCore import Qt - - -class AlarmEntry(QFrame): - def __init__(self, device_id, channel_id, value, kind, timestamp): - super().__init__() - self.setObjectName("alarmEntry") - color = "#f85149" if kind == "high" else "#d29922" - self.setStyleSheet(f"border-left: 3px solid {color};") - layout = QHBoxLayout(self) - layout.setContentsMargins(8, 4, 8, 4) - icon = "▲" if kind == "high" else "▼" - msg = QLabel(f"{icon} {device_id} / {channel_id} = {value:.3f} [{kind.upper()}]") - msg.setObjectName("alarmMsg") - ts_lbl = QLabel(timestamp) - ts_lbl.setObjectName("alarmTs") - layout.addWidget(msg) - layout.addStretch() - layout.addWidget(ts_lbl) - - -class AlarmPanel(QWidget): - def __init__(self): - super().__init__() - self._build_ui() - - def _build_ui(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - hdr_row = QHBoxLayout() - hdr = QLabel(" ALARMS") - hdr.setObjectName("panelHeader") - hdr_row.addWidget(hdr) - hdr_row.addStretch() - clr = QPushButton("Clear") - clr.setObjectName("clearAlarmsBtn") - clr.clicked.connect(self.clear_alarms) - hdr_row.addWidget(clr) - hdr_widget = QWidget() - hdr_widget.setLayout(hdr_row) - hdr_widget.setObjectName("alarmHeaderWidget") - layout.addWidget(hdr_widget) - - scroll = QScrollArea() - scroll.setWidgetResizable(True) - self._container = QWidget() - self._inner = QVBoxLayout(self._container) - self._inner.setContentsMargins(4, 4, 4, 4) - self._inner.setSpacing(3) - self._inner.addStretch() - scroll.setWidget(self._container) - layout.addWidget(scroll) - - self._scroll = scroll - self._count = 0 - - def add_alarm(self, device_id, channel_id, value, kind): - ts = datetime.now().strftime("%H:%M:%S") - entry = AlarmEntry(device_id, channel_id, value, kind, ts) - self._inner.insertWidget(0, entry) - self._count += 1 - # Keep last 200 - if self._count > 200: - item = self._inner.takeAt(self._inner.count() - 2) - if item and item.widget(): - item.widget().deleteLater() - self._count -= 1 - - def clear_alarms(self): - while self._inner.count() > 1: - item = self._inner.takeAt(0) - if item and item.widget(): - item.widget().deleteLater() - self._count = 0 diff --git a/daq_system/ui/config_dialog.py b/daq_system/ui/config_dialog.py deleted file mode 100644 index a7b0cdf..0000000 --- a/daq_system/ui/config_dialog.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -ui/config_dialog.py — Per-device configuration dialog. -""" - -from PyQt6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QTabWidget, QWidget, QFormLayout, - QLineEdit, QDoubleSpinBox, QCheckBox, QScrollArea -) -from PyQt6.QtCore import Qt - - -class DeviceConfigDialog(QDialog): - def __init__(self, device, parent=None): - super().__init__(parent) - self.device = device - self.setWindowTitle(f"Configure: {device.info.name} [{device.info.device_id}]") - self.setMinimumSize(500, 400) - self._build() - - def _build(self): - layout = QVBoxLayout(self) - - tabs = QTabWidget() - - # Tab 1: Device-specific config widget - dev_tab = QScrollArea() - dev_tab.setWidgetResizable(True) - dev_tab.setWidget(self.device.get_config_widget()) - tabs.addTab(dev_tab, "Device Settings") - - # Tab 2: Channel config - ch_tab = self._build_channel_tab() - tabs.addTab(ch_tab, "Channels") - - layout.addWidget(tabs) - - # Buttons - btn_row = QHBoxLayout() - btn_row.addStretch() - ok = QPushButton("Close") - ok.setDefault(True) - ok.clicked.connect(self.accept) - btn_row.addWidget(ok) - layout.addLayout(btn_row) - - def _build_channel_tab(self): - w = QWidget() - layout = QVBoxLayout(w) - for ch in self.device.info.channels: - grp_layout = QFormLayout() - name_edit = QLineEdit(ch.name) - unit_edit = QLineEdit(ch.unit) - en_check = QCheckBox() - en_check.setChecked(ch.enabled) - - lo_spin = QDoubleSpinBox() - lo_spin.setRange(-1e9, 1e9) - lo_spin.setValue(ch.alarm_low or 0.0) - - hi_spin = QDoubleSpinBox() - hi_spin.setRange(-1e9, 1e9) - hi_spin.setValue(ch.alarm_high or 100.0) - - grp_layout.addRow(f"[{ch.channel_id}] Name:", name_edit) - grp_layout.addRow("Unit:", unit_edit) - grp_layout.addRow("Enabled:", en_check) - grp_layout.addRow("Alarm Low:", lo_spin) - grp_layout.addRow("Alarm High:", hi_spin) - - def make_apply(c, ne, ue, ec, ls, hs): - def apply(): - c.name = ne.text() - c.unit = ue.text() - c.enabled = ec.isChecked() - c.alarm_low = ls.value() - c.alarm_high = hs.value() - return apply - - apply_btn = QPushButton("Apply") - apply_btn.clicked.connect(make_apply(ch, name_edit, unit_edit, en_check, lo_spin, hi_spin)) - grp_layout.addRow(apply_btn) - layout.addLayout(grp_layout) - - layout.addStretch() - return w - - -# ------------------------------------------------------------------ # -# Add Device Dialog # -# ------------------------------------------------------------------ # - -""" -ui/add_device_dialog.py — Dialog for adding a new device to the system. -""" - -from PyQt6.QtWidgets import ( - QDialog, QVBoxLayout, QFormLayout, QComboBox, - QLineEdit, QCheckBox, QSpinBox, QPushButton, QHBoxLayout, QLabel -) - -from devices.device_registry import DeviceRegistry -from devices.analog_input import AnalogInputDevice -from devices.digital_io import DigitalIODevice -from devices.temperature import TemperatureDevice -from devices.serial_device import SerialDevice - - -DEVICE_CONSTRUCTORS = { - "Analog Input": AnalogInputDevice, - "Digital I/O": DigitalIODevice, - "Temperature": TemperatureDevice, - "Serial / UART": SerialDevice, -} - - -class AddDeviceDialog(QDialog): - def __init__(self, registry: DeviceRegistry, parent=None): - super().__init__(parent) - self.registry = registry - self.created_device = None - self.setWindowTitle("Add Device") - self.setMinimumWidth(340) - self._build() - - def _build(self): - layout = QVBoxLayout(self) - form = QFormLayout() - - self.type_cb = QComboBox() - self.type_cb.addItems(list(DEVICE_CONSTRUCTORS.keys())) - form.addRow("Device Type:", self.type_cb) - - self.id_edit = QLineEdit() - self.id_edit.setPlaceholderText("e.g. ai_1") - form.addRow("Device ID:", self.id_edit) - - self.ch_spin = QSpinBox() - self.ch_spin.setRange(1, 16) - self.ch_spin.setValue(4) - form.addRow("# Channels:", self.ch_spin) - - self.sim_check = QCheckBox("Simulation Mode") - self.sim_check.setChecked(True) - form.addRow(self.sim_check) - - layout.addLayout(form) - - btns = QHBoxLayout() - btns.addStretch() - cancel = QPushButton("Cancel") - cancel.clicked.connect(self.reject) - add = QPushButton("Add") - add.setDefault(True) - add.clicked.connect(self._on_add) - btns.addWidget(cancel) - btns.addWidget(add) - layout.addLayout(btns) - - def _on_add(self): - dev_type = self.type_cb.currentText() - dev_id = self.id_edit.text().strip() or f"dev_{len(self.registry)}" - cls = DEVICE_CONSTRUCTORS[dev_type] - try: - dev = cls( - device_id=dev_id, - num_channels=self.ch_spin.value(), - simulate=self.sim_check.isChecked() - ) - self.created_device = dev - self.accept() - except Exception as e: - from PyQt6.QtWidgets import QMessageBox - QMessageBox.critical(self, "Error", str(e)) diff --git a/daq_system/ui/device_panel.py b/daq_system/ui/device_panel.py deleted file mode 100644 index 9b6c7e4..0000000 --- a/daq_system/ui/device_panel.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -ui/device_panel.py - -Left sidebar showing all registered devices with status indicators. -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QLabel, QPushButton, - QHBoxLayout, QScrollArea, QFrame -) -from PyQt6.QtCore import Qt, pyqtSignal - -from devices.base_device import DeviceStatus -from devices.device_registry import DeviceRegistry -from core.acquisition import AcquisitionEngine - - -class DeviceCard(QFrame): - config_clicked = pyqtSignal(str) - toggle_clicked = pyqtSignal(str, bool) - - STATUS_COLORS = { - DeviceStatus.CONNECTED: "#3fb950", - DeviceStatus.SIMULATED: "#58a6ff", - DeviceStatus.DISCONNECTED: "#8b949e", - DeviceStatus.CONNECTING: "#d29922", - DeviceStatus.ERROR: "#f85149", - } - - def __init__(self, device, parent=None): - super().__init__(parent) - self.device = device - self.setObjectName("deviceCard") - self._build() - - def _build(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(8, 8, 8, 8) - layout.setSpacing(4) - - # Header row - hdr = QHBoxLayout() - icon = QLabel(self.device.info.icon) - icon.setObjectName("deviceIcon") - hdr.addWidget(icon) - - name = QLabel(self.device.info.name) - name.setObjectName("deviceName") - hdr.addWidget(name) - hdr.addStretch() - - color = self.STATUS_COLORS.get(self.device.status, "#8b949e") - self.status_dot = QLabel("●") - self.status_dot.setStyleSheet(f"color: {color}; font-size: 10px;") - hdr.addWidget(self.status_dot) - - layout.addLayout(hdr) - - # ID + type - sub = QLabel(f"{self.device.info.device_id} · {self.device.info.device_type}") - sub.setObjectName("deviceSub") - layout.addWidget(sub) - - # Channel count - ch_count = sum(1 for ch in self.device.info.channels if ch.enabled) - ch_lbl = QLabel(f"{ch_count} channel{'s' if ch_count != 1 else ''}") - ch_lbl.setObjectName("deviceChannelCount") - layout.addWidget(ch_lbl) - - # Config button - cfg_btn = QPushButton("Configure") - cfg_btn.setObjectName("configButton") - cfg_btn.clicked.connect(lambda: self.config_clicked.emit(self.device.info.device_id)) - layout.addWidget(cfg_btn) - - def refresh_status(self): - color = self.STATUS_COLORS.get(self.device.status, "#8b949e") - self.status_dot.setStyleSheet(f"color: {color}; font-size: 10px;") - - -class DevicePanel(QWidget): - config_requested = pyqtSignal(str) - - def __init__(self, registry: DeviceRegistry, engine: AcquisitionEngine): - super().__init__() - self.registry = registry - self.engine = engine - self._cards = {} - self._build_ui() - self.refresh() - - def _build_ui(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - header = QLabel(" DEVICES") - header.setObjectName("panelHeader") - layout.addWidget(header) - - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - scroll.setObjectName("deviceScroll") - - self._container = QWidget() - self._inner = QVBoxLayout(self._container) - self._inner.setContentsMargins(8, 8, 8, 8) - self._inner.setSpacing(8) - self._inner.addStretch() - - scroll.setWidget(self._container) - layout.addWidget(scroll) - - def refresh(self): - # Remove old cards - for card in self._cards.values(): - self._inner.removeWidget(card) - card.deleteLater() - self._cards.clear() - - for dev in self.registry.all_instances(): - card = DeviceCard(dev) - card.config_clicked.connect(self.config_requested.emit) - # Insert before the stretch - self._inner.insertWidget(self._inner.count() - 1, card) - self._cards[dev.info.device_id] = card diff --git a/daq_system/ui/main_window.py b/daq_system/ui/main_window.py deleted file mode 100644 index 8b602d1..0000000 --- a/daq_system/ui/main_window.py +++ /dev/null @@ -1,228 +0,0 @@ -""" -ui/main_window.py - -Main application window. Hosts the left sidebar (device list), -center strip chart, right panel (numeric readouts + alarms). -""" - -from PyQt6.QtWidgets import ( - QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, - QSplitter, QStatusBar, QLabel, QPushButton, - QToolBar, QMessageBox, QFileDialog -) -from PyQt6.QtCore import Qt, QTimer, pyqtSlot -from PyQt6.QtGui import QAction, QIcon, QFont - -from devices.analog_input import AnalogInputDevice -from devices.digital_io import DigitalIODevice -from devices.temperature import TemperatureDevice -from devices.serial_device import SerialDevice -from devices.device_registry import DeviceRegistry -from core.acquisition import AcquisitionEngine - -from ui.device_panel import DevicePanel -from ui.strip_chart import StripChartWidget -from ui.readout_panel import ReadoutPanel -from ui.alarm_panel import AlarmPanel -from ui.config_dialog import DeviceConfigDialog - - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("LabDAQ — Data Acquisition System") - self.setMinimumSize(1280, 780) - - self.registry = DeviceRegistry() - self.engine = AcquisitionEngine(poll_interval_ms=100) - self._running = False - - self._init_default_devices() - self._build_ui() - self._connect_signals() - - # ------------------------------------------------------------------ # - # Default demo configuration # - # ------------------------------------------------------------------ # - - def _init_default_devices(self): - ai = AnalogInputDevice(device_id="ai_0", num_channels=4, simulate=True) - ai.connect() - temp = TemperatureDevice(device_id="temp_0", num_channels=3, simulate=True) - temp.connect() - dio = DigitalIODevice(device_id="dio_0", num_inputs=4, num_outputs=4, simulate=True) - dio.connect() - - for dev in [ai, temp, dio]: - self.registry.add_instance(dev) - self.engine.add_device(dev) - - # ------------------------------------------------------------------ # - # UI construction # - # ------------------------------------------------------------------ # - - def _build_ui(self): - # ---- Toolbar ---- - toolbar = QToolBar("Main") - toolbar.setMovable(False) - toolbar.setObjectName("mainToolbar") - self.addToolBar(toolbar) - - self.run_btn = QPushButton("▶ RUN") - self.run_btn.setObjectName("runButton") - self.run_btn.setCheckable(True) - self.run_btn.clicked.connect(self._toggle_run) - - self.log_btn = QPushButton("⬤ LOG") - self.log_btn.setObjectName("logButton") - self.log_btn.setCheckable(True) - self.log_btn.setEnabled(False) - self.log_btn.clicked.connect(self._toggle_log) - - add_dev_btn = QPushButton("+ Device") - add_dev_btn.setObjectName("addDeviceButton") - add_dev_btn.clicked.connect(self._add_device) - - toolbar.addWidget(self.run_btn) - toolbar.addSeparator() - toolbar.addWidget(self.log_btn) - toolbar.addSeparator() - toolbar.addWidget(add_dev_btn) - - # Spacer - spacer = QWidget() - spacer.setSizePolicy( - spacer.sizePolicy().horizontalPolicy(), - spacer.sizePolicy().verticalPolicy() - ) - from PyQt6.QtWidgets import QSizePolicy - spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) - toolbar.addWidget(spacer) - - self.time_label = QLabel("00:00:00") - self.time_label.setObjectName("timeLabel") - toolbar.addWidget(self.time_label) - - # ---- Central layout ---- - central = QWidget() - self.setCentralWidget(central) - root = QHBoxLayout(central) - root.setContentsMargins(0, 0, 0, 0) - root.setSpacing(0) - - splitter = QSplitter(Qt.Orientation.Horizontal) - splitter.setHandleWidth(4) - - # Left: device panel - self.device_panel = DevicePanel(self.registry, self.engine) - self.device_panel.setMinimumWidth(200) - splitter.addWidget(self.device_panel) - - # Center: strip chart - self.strip_chart = StripChartWidget(self.engine, self.registry) - splitter.addWidget(self.strip_chart) - - # Right: readouts + alarms - right_panel = QWidget() - right_layout = QVBoxLayout(right_panel) - right_layout.setContentsMargins(0, 0, 0, 0) - right_layout.setSpacing(0) - - self.readout_panel = ReadoutPanel(self.registry) - self.alarm_panel = AlarmPanel() - - right_splitter = QSplitter(Qt.Orientation.Vertical) - right_splitter.addWidget(self.readout_panel) - right_splitter.addWidget(self.alarm_panel) - right_layout.addWidget(right_splitter) - - right_panel.setMinimumWidth(220) - splitter.addWidget(right_panel) - - splitter.setSizes([220, 820, 240]) - root.addWidget(splitter) - - # ---- Status bar ---- - self.status_bar = QStatusBar() - self.setStatusBar(self.status_bar) - self._status_lbl = QLabel("Ready") - self.status_bar.addWidget(self._status_lbl) - - # Clock timer - self._elapsed = 0 - self._clock = QTimer() - self._clock.setInterval(1000) - self._clock.timeout.connect(self._tick_clock) - - def _connect_signals(self): - self.engine.alarm_triggered.connect(self._on_alarm) - self.engine.new_data.connect(self.readout_panel.on_new_data) - self.engine.new_data.connect(self.strip_chart.on_new_data) - self.device_panel.config_requested.connect(self._open_config) - - # ------------------------------------------------------------------ # - # Actions # - # ------------------------------------------------------------------ # - - def _toggle_run(self, checked: bool): - if checked: - self.engine.start() - self.run_btn.setText("⏹ STOP") - self.log_btn.setEnabled(True) - self._clock.start() - self._status_lbl.setText("Acquiring data…") - self._running = True - else: - self.engine.stop() - self.run_btn.setText("▶ RUN") - self.log_btn.setEnabled(False) - if self.log_btn.isChecked(): - self.log_btn.setChecked(False) - self._toggle_log(False) - self._clock.stop() - self._status_lbl.setText("Stopped") - self._running = False - - def _toggle_log(self, checked: bool): - if checked: - path = self.engine.start_logging() - self.log_btn.setText(f"⏹ LOGGING") - self._status_lbl.setText(f"Logging → {path}") - else: - self.engine.stop_logging() - self.log_btn.setText("⬤ LOG") - self._status_lbl.setText("Log saved.") - - def _add_device(self): - from ui.add_device_dialog import AddDeviceDialog - dlg = AddDeviceDialog(self.registry, self) - if dlg.exec(): - dev = dlg.created_device - if dev: - self.registry.add_instance(dev) - self.engine.add_device(dev) - dev.connect() - self.device_panel.refresh() - self.readout_panel.refresh() - self.strip_chart.refresh() - - def _open_config(self, device_id: str): - dev = self.registry.get_instance(device_id) - if dev: - dlg = DeviceConfigDialog(dev, self) - dlg.exec() - - @pyqtSlot(str, str, float, str) - def _on_alarm(self, device_id: str, channel_id: str, value: float, kind: str): - self.alarm_panel.add_alarm(device_id, channel_id, value, kind) - - def _tick_clock(self): - self._elapsed += 1 - h = self._elapsed // 3600 - m = (self._elapsed % 3600) // 60 - s = self._elapsed % 60 - self.time_label.setText(f"{h:02d}:{m:02d}:{s:02d}") - - def closeEvent(self, event): - self.engine.stop() - event.accept() diff --git a/daq_system/ui/readout_panel.py b/daq_system/ui/readout_panel.py deleted file mode 100644 index 6086c30..0000000 --- a/daq_system/ui/readout_panel.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -ui/readout_panel.py - -Right-side numeric readout panel. Shows current value for every channel -with a colored bar indicator and alarm highlighting. -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QScrollArea, QFrame, QProgressBar -) -from PyQt6.QtCore import Qt, pyqtSlot -from PyQt6.QtGui import QColor - -from devices.device_registry import DeviceRegistry - - -class ChannelReadout(QFrame): - def __init__(self, device_name: str, channel_config, parent=None): - super().__init__(parent) - self.ch = channel_config - self.device_name = device_name - self.setObjectName("channelReadout") - self._alarm_active = False - self._build() - - def _build(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(8, 6, 8, 6) - layout.setSpacing(2) - - top = QHBoxLayout() - self.name_lbl = QLabel(f"{self.ch.name}") - self.name_lbl.setObjectName("readoutName") - top.addWidget(self.name_lbl) - top.addStretch() - self.value_lbl = QLabel("---") - self.value_lbl.setObjectName("readoutValue") - self.value_lbl.setStyleSheet(f"color: {self.ch.color};") - top.addWidget(self.value_lbl) - unit = QLabel(self.ch.unit) - unit.setObjectName("readoutUnit") - top.addWidget(unit) - layout.addLayout(top) - - # Mini bar - self.bar = QProgressBar() - self.bar.setRange(0, 1000) - self.bar.setValue(0) - self.bar.setTextVisible(False) - self.bar.setMaximumHeight(4) - self.bar.setObjectName("readoutBar") - self.bar.setStyleSheet( - f"QProgressBar::chunk {{ background: {self.ch.color}; border-radius: 2px; }}" - ) - layout.addWidget(self.bar) - - def update_value(self, value: float): - self.value_lbl.setText(f"{value:.3f}") - rng = self.ch.max_value - self.ch.min_value - if rng > 0: - norm = int(((value - self.ch.min_value) / rng) * 1000) - self.bar.setValue(max(0, min(1000, norm))) - - def set_alarm(self, active: bool): - if active != self._alarm_active: - self._alarm_active = active - self.setProperty("alarm", active) - self.style().unpolish(self) - self.style().polish(self) - - -class ReadoutPanel(QWidget): - def __init__(self, registry: DeviceRegistry): - super().__init__() - self.registry = registry - self._readouts: dict = {} # (device_id, channel_id) -> ChannelReadout - self._build_ui() - self.refresh() - - def _build_ui(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - hdr = QLabel(" CHANNELS") - hdr.setObjectName("panelHeader") - layout.addWidget(hdr) - - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - - self._container = QWidget() - self._inner = QVBoxLayout(self._container) - self._inner.setContentsMargins(6, 6, 6, 6) - self._inner.setSpacing(4) - self._inner.addStretch() - - scroll.setWidget(self._container) - layout.addWidget(scroll) - - def refresh(self): - for w in self._readouts.values(): - self._inner.removeWidget(w) - w.deleteLater() - self._readouts.clear() - - for dev in self.registry.all_instances(): - for ch in dev.info.channels: - if not ch.enabled: - continue - ro = ChannelReadout(dev.info.name, ch) - self._inner.insertWidget(self._inner.count() - 1, ro) - self._readouts[(dev.info.device_id, ch.channel_id)] = ro - - @pyqtSlot(str, str, float, float) - def on_new_data(self, device_id: str, channel_id: str, timestamp: float, value: float): - ro = self._readouts.get((device_id, channel_id)) - if ro: - ro.update_value(value) - ch = ro.ch - alarm = False - if ch.alarm_low is not None and value < ch.alarm_low: - alarm = True - if ch.alarm_high is not None and value > ch.alarm_high: - alarm = True - ro.set_alarm(alarm) diff --git a/daq_system/ui/strip_chart.py b/daq_system/ui/strip_chart.py deleted file mode 100644 index 46d73fa..0000000 --- a/daq_system/ui/strip_chart.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -ui/strip_chart.py - -Live scrolling strip chart using pyqtgraph. -Each channel gets its own colored trace. Window duration is adjustable. -""" - -from typing import Dict, List -import numpy as np - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QComboBox, QDoubleSpinBox, QCheckBox, QScrollArea, - QPushButton, QSizePolicy -) -from PyQt6.QtCore import Qt, pyqtSlot - -try: - import pyqtgraph as pg - pg.setConfigOptions(antialias=True, background="#0d1117", foreground="#c9d1d9") - HAS_PG = True -except ImportError: - HAS_PG = False - -from devices.device_registry import DeviceRegistry -from core.acquisition import AcquisitionEngine - - -class StripChartWidget(QWidget): - def __init__(self, engine: AcquisitionEngine, registry: DeviceRegistry): - super().__init__() - self.engine = engine - self.registry = registry - self._plots: Dict[str, Dict] = {} # device_id -> {channel_id -> curve} - self._window_s = 30.0 - self._paused = False - self._build_ui() - self.refresh() - - def _build_ui(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(4, 4, 4, 4) - layout.setSpacing(4) - - # Control bar - ctrl = QHBoxLayout() - ctrl.addWidget(QLabel("Window:")) - - self.window_spin = QDoubleSpinBox() - self.window_spin.setRange(1, 600) - self.window_spin.setValue(self._window_s) - self.window_spin.setSuffix(" s") - self.window_spin.valueChanged.connect(lambda v: setattr(self, "_window_s", v)) - ctrl.addWidget(self.window_spin) - - ctrl.addStretch() - - self.pause_btn = QPushButton("⏸ Pause") - self.pause_btn.setCheckable(True) - self.pause_btn.setObjectName("pauseButton") - self.pause_btn.toggled.connect(lambda c: setattr(self, "_paused", c)) - self.pause_btn.toggled.connect(lambda c: self.pause_btn.setText("▶ Resume" if c else "⏸ Pause")) - ctrl.addWidget(self.pause_btn) - - layout.addLayout(ctrl) - - if HAS_PG: - self._build_pg_chart(layout) - else: - layout.addWidget(QLabel( - "pyqtgraph not installed.\nRun: pip install pyqtgraph\n\nData is still being acquired.", - alignment=Qt.AlignmentFlag.AlignCenter - )) - - def _build_pg_chart(self, parent_layout): - self.plot_widget = pg.GraphicsLayoutWidget() - self.plot_widget.setSizePolicy( - QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding - ) - parent_layout.addWidget(self.plot_widget) - - def refresh(self): - if not HAS_PG: - return - self.plot_widget.clear() - self._plots.clear() - - devices = self.registry.all_instances() - num = len(devices) - if num == 0: - return - - for row_idx, dev in enumerate(devices): - plot = self.plot_widget.addPlot(row=row_idx, col=0) - plot.setLabel("left", f"{dev.info.icon} {dev.info.name}") - plot.showGrid(x=True, y=True, alpha=0.15) - plot.getAxis("bottom").setStyle(showValues=(row_idx == num - 1)) - - if row_idx < num - 1: - plot.getAxis("bottom").setHeight(0) - else: - plot.setLabel("bottom", "Time (s)") - - self._plots[dev.info.device_id] = {} - for ch in dev.info.channels: - if not ch.enabled: - continue - color = ch.color - pen = pg.mkPen(color=color, width=1.5) - curve = plot.plot([], [], pen=pen, name=ch.name) - self._plots[dev.info.device_id][ch.channel_id] = { - "curve": curve, - "plot": plot, - } - - @pyqtSlot(str, str, float, float) - def on_new_data(self, device_id: str, channel_id: str, timestamp: float, value: float): - if self._paused or not HAS_PG: - return - dev_plots = self._plots.get(device_id, {}) - entry = dev_plots.get(channel_id) - if entry is None: - return - - buf = self.engine.get_buffer(device_id, channel_id) - if buf is None or len(buf) < 2: - return - - ts, vs = buf.all() - ts = np.array(ts, dtype=np.float64) - vs = np.array(vs, dtype=np.float64) - - t_max = ts[-1] - t_min = t_max - self._window_s - mask = ts >= t_min - ts = ts[mask] - vs = vs[mask] - - entry["curve"].setData(ts, vs) - entry["plot"].setXRange(t_min, t_max, padding=0) diff --git a/daq_system/daq_system/devices/__init__.py b/devices/__init__.py index f79b333..f79b333 100644 --- a/daq_system/daq_system/devices/__init__.py +++ b/devices/__init__.py diff --git a/devices/__pycache__/__init__.cpython-314.pyc b/devices/__pycache__/__init__.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..9ab29a6 --- /dev/null +++ b/devices/__pycache__/__init__.cpython-314.pyc diff --git a/devices/__pycache__/analog_input.cpython-314.pyc b/devices/__pycache__/analog_input.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..4c08631 --- /dev/null +++ b/devices/__pycache__/analog_input.cpython-314.pyc diff --git a/devices/__pycache__/base_device.cpython-314.pyc b/devices/__pycache__/base_device.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..ade2b20 --- /dev/null +++ b/devices/__pycache__/base_device.cpython-314.pyc diff --git a/devices/__pycache__/device_registry.cpython-314.pyc b/devices/__pycache__/device_registry.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..608c8ee --- /dev/null +++ b/devices/__pycache__/device_registry.cpython-314.pyc diff --git a/devices/__pycache__/digital_io.cpython-314.pyc b/devices/__pycache__/digital_io.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..b2bb27f --- /dev/null +++ b/devices/__pycache__/digital_io.cpython-314.pyc diff --git a/devices/__pycache__/serial_device.cpython-314.pyc b/devices/__pycache__/serial_device.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..0674606 --- /dev/null +++ b/devices/__pycache__/serial_device.cpython-314.pyc diff --git a/daq_system/daq_system/devices/analog_input.py b/devices/analog_input.py index 5bb5d28..5bb5d28 100644 --- a/daq_system/daq_system/devices/analog_input.py +++ b/devices/analog_input.py diff --git a/daq_system/daq_system/devices/base_device.py b/devices/base_device.py index 922a743..922a743 100644 --- a/daq_system/daq_system/devices/base_device.py +++ b/devices/base_device.py diff --git a/daq_system/daq_system/devices/device_registry.py b/devices/device_registry.py index 8ed6886..8ed6886 100644 --- a/daq_system/daq_system/devices/device_registry.py +++ b/devices/device_registry.py diff --git a/daq_system/daq_system/devices/digital_io.py b/devices/digital_io.py index 23a345f..23a345f 100644 --- a/daq_system/daq_system/devices/digital_io.py +++ b/devices/digital_io.py diff --git a/daq_system/daq_system/devices/serial_device.py b/devices/serial_device.py index 6bb7bf1..6bb7bf1 100644 --- a/daq_system/daq_system/devices/serial_device.py +++ b/devices/serial_device.py diff --git a/daq_system/daq_system/main.py b/main.py index 2f2d515..c039b66 100644..100755 --- a/daq_system/daq_system/main.py +++ b/main.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 """ main.py — LabDAQ entry point. @@ -17,7 +18,6 @@ import os sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from PyQt6.QtWidgets import QApplication -from PyQt6.QtCore import Qt from ui.main_window import MainWindow @@ -25,9 +25,9 @@ def main(): app = QApplication(sys.argv) app.setApplicationName("LabDAQ") app.setOrganizationName("Lab Instruments") - app.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps) + # Note: AA_UseHighDpiPixmaps was removed in PyQt6 — HiDPI is always on - qss_path = os.path.join(os.path.dirname(__file__), "ui", "style.qss") + qss_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ui", "style.qss") with open(qss_path, "r") as f: app.setStyleSheet(f.read()) diff --git a/daq_system/daq_system/requirements.txt b/requirements.txt index d5114b5..d5114b5 100644 --- a/daq_system/daq_system/requirements.txt +++ b/requirements.txt diff --git a/daq_system/daq_system/ui/__init__.py b/ui/__init__.py index dfde25c..dfde25c 100644 --- a/daq_system/daq_system/ui/__init__.py +++ b/ui/__init__.py diff --git a/ui/__pycache__/__init__.cpython-314.pyc b/ui/__pycache__/__init__.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..6e83590 --- /dev/null +++ b/ui/__pycache__/__init__.cpython-314.pyc diff --git a/ui/__pycache__/add_device_dialog.cpython-314.pyc b/ui/__pycache__/add_device_dialog.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..280fdbf --- /dev/null +++ b/ui/__pycache__/add_device_dialog.cpython-314.pyc diff --git a/ui/__pycache__/alarm_panel.cpython-314.pyc b/ui/__pycache__/alarm_panel.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..a1811f7 --- /dev/null +++ b/ui/__pycache__/alarm_panel.cpython-314.pyc diff --git a/ui/__pycache__/config_dialog.cpython-314.pyc b/ui/__pycache__/config_dialog.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..8876af3 --- /dev/null +++ b/ui/__pycache__/config_dialog.cpython-314.pyc diff --git a/ui/__pycache__/device_panel.cpython-314.pyc b/ui/__pycache__/device_panel.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..07eb750 --- /dev/null +++ b/ui/__pycache__/device_panel.cpython-314.pyc diff --git a/ui/__pycache__/main_window.cpython-314.pyc b/ui/__pycache__/main_window.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..27e4279 --- /dev/null +++ b/ui/__pycache__/main_window.cpython-314.pyc diff --git a/ui/__pycache__/readout_panel.cpython-314.pyc b/ui/__pycache__/readout_panel.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..9258b10 --- /dev/null +++ b/ui/__pycache__/readout_panel.cpython-314.pyc diff --git a/ui/__pycache__/strip_chart.cpython-314.pyc b/ui/__pycache__/strip_chart.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..4af6ff5 --- /dev/null +++ b/ui/__pycache__/strip_chart.cpython-314.pyc diff --git a/daq_system/daq_system/ui/add_device_dialog.py b/ui/add_device_dialog.py index 30d0016..30d0016 100644 --- a/daq_system/daq_system/ui/add_device_dialog.py +++ b/ui/add_device_dialog.py diff --git a/daq_system/daq_system/ui/alarm_panel.py b/ui/alarm_panel.py index 1270aff..1270aff 100644 --- a/daq_system/daq_system/ui/alarm_panel.py +++ b/ui/alarm_panel.py diff --git a/daq_system/daq_system/ui/config_dialog.py b/ui/config_dialog.py index 7d87772..7d87772 100644 --- a/daq_system/daq_system/ui/config_dialog.py +++ b/ui/config_dialog.py diff --git a/daq_system/daq_system/ui/device_panel.py b/ui/device_panel.py index 932f536..932f536 100644 --- a/daq_system/daq_system/ui/device_panel.py +++ b/ui/device_panel.py diff --git a/daq_system/daq_system/ui/main_window.py b/ui/main_window.py index d329571..d329571 100644 --- a/daq_system/daq_system/ui/main_window.py +++ b/ui/main_window.py diff --git a/daq_system/daq_system/ui/readout_panel.py b/ui/readout_panel.py index ef04e18..ef04e18 100644 --- a/daq_system/daq_system/ui/readout_panel.py +++ b/ui/readout_panel.py diff --git a/daq_system/daq_system/ui/strip_chart.py b/ui/strip_chart.py index afc7cf7..afc7cf7 100644 --- a/daq_system/daq_system/ui/strip_chart.py +++ b/ui/strip_chart.py diff --git a/daq_system/daq_system/ui/style.qss b/ui/style.qss index 60415fb..60415fb 100644 --- a/daq_system/daq_system/ui/style.qss +++ b/ui/style.qss |
