summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-04-13 15:22:56 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-04-13 15:22:56 -0600
commit2ed9d37da7b27d25173535550fb92702225ac14e (patch)
treed6ff360a656e577629d3cefdaa1ae02af7255864
parent8d6acf3a8ea4b37f86b321dbf430be5be01b1267 (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.pycbin0 -> 324 bytes
-rw-r--r--api_layers/__pycache__/arduino_layer.cpython-314.pycbin0 -> 15070 bytes
-rw-r--r--api_layers/__pycache__/nidaqmx_layer.cpython-314.pycbin0 -> 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.pycbin0 -> 277 bytes
-rw-r--r--core/__pycache__/acquisition.cpython-314.pycbin0 -> 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__.py1
-rw-r--r--daq_system/core/__pycache__/__init__.cpython-312.pycbin161 -> 0 bytes
-rw-r--r--daq_system/core/__pycache__/acquisition.cpython-312.pycbin11381 -> 0 bytes
-rw-r--r--daq_system/core/acquisition.py214
-rw-r--r--daq_system/daq_system/README.md183
-rw-r--r--daq_system/devices/__init__.py1
-rw-r--r--daq_system/devices/__pycache__/__init__.cpython-312.pycbin164 -> 0 bytes
-rw-r--r--daq_system/devices/__pycache__/analog_input.cpython-312.pycbin9034 -> 0 bytes
-rw-r--r--daq_system/devices/__pycache__/base_device.cpython-312.pycbin6014 -> 0 bytes
-rw-r--r--daq_system/devices/__pycache__/device_registry.cpython-312.pycbin5219 -> 0 bytes
-rw-r--r--daq_system/devices/__pycache__/digital_io.cpython-312.pycbin6027 -> 0 bytes
-rw-r--r--daq_system/devices/__pycache__/serial_device.cpython-312.pycbin7405 -> 0 bytes
-rw-r--r--daq_system/devices/__pycache__/temperature.cpython-312.pycbin6707 -> 0 bytes
-rw-r--r--daq_system/devices/analog_input.py184
-rw-r--r--daq_system/devices/base_device.py127
-rw-r--r--daq_system/devices/device_registry.py87
-rw-r--r--daq_system/devices/digital_io.py95
-rw-r--r--daq_system/devices/serial_device.py136
-rw-r--r--daq_system/devices/temperature.py115
-rw-r--r--daq_system/main.py28
-rw-r--r--daq_system/ui/__pycache__/alarm_panel.cpython-312.pycbin5462 -> 0 bytes
-rw-r--r--daq_system/ui/__pycache__/config_dialog.cpython-312.pycbin9345 -> 0 bytes
-rw-r--r--daq_system/ui/__pycache__/device_panel.cpython-312.pycbin7943 -> 0 bytes
-rw-r--r--daq_system/ui/__pycache__/main_window.cpython-312.pycbin13127 -> 0 bytes
-rw-r--r--daq_system/ui/__pycache__/readout_panel.cpython-312.pycbin8378 -> 0 bytes
-rw-r--r--daq_system/ui/__pycache__/strip_chart.cpython-312.pycbin8328 -> 0 bytes
-rw-r--r--daq_system/ui/add_device_dialog.py7
-rw-r--r--daq_system/ui/alarm_panel.py85
-rw-r--r--daq_system/ui/config_dialog.py174
-rw-r--r--daq_system/ui/device_panel.py127
-rw-r--r--daq_system/ui/main_window.py228
-rw-r--r--daq_system/ui/readout_panel.py128
-rw-r--r--daq_system/ui/strip_chart.py140
-rw-r--r--devices/__init__.py (renamed from daq_system/daq_system/devices/__init__.py)0
-rw-r--r--devices/__pycache__/__init__.cpython-314.pycbin0 -> 398 bytes
-rw-r--r--devices/__pycache__/analog_input.cpython-314.pycbin0 -> 16238 bytes
-rw-r--r--devices/__pycache__/base_device.cpython-314.pycbin0 -> 7731 bytes
-rw-r--r--devices/__pycache__/device_registry.cpython-314.pycbin0 -> 5927 bytes
-rw-r--r--devices/__pycache__/digital_io.cpython-314.pycbin0 -> 19275 bytes
-rw-r--r--devices/__pycache__/serial_device.cpython-314.pycbin0 -> 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.pycbin0 -> 153 bytes
-rw-r--r--ui/__pycache__/add_device_dialog.cpython-314.pycbin0 -> 5942 bytes
-rw-r--r--ui/__pycache__/alarm_panel.cpython-314.pycbin0 -> 6113 bytes
-rw-r--r--ui/__pycache__/config_dialog.cpython-314.pycbin0 -> 7331 bytes
-rw-r--r--ui/__pycache__/device_panel.cpython-314.pycbin0 -> 10002 bytes
-rw-r--r--ui/__pycache__/main_window.cpython-314.pycbin0 -> 15798 bytes
-rw-r--r--ui/__pycache__/readout_panel.cpython-314.pycbin0 -> 9028 bytes
-rw-r--r--ui/__pycache__/strip_chart.cpython-314.pycbin0 -> 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
new file mode 100644
index 0000000..88ad51b
--- /dev/null
+++ b/api_layers/__pycache__/__init__.cpython-314.pyc
Binary files differ
diff --git a/api_layers/__pycache__/arduino_layer.cpython-314.pyc b/api_layers/__pycache__/arduino_layer.cpython-314.pyc
new file mode 100644
index 0000000..186b224
--- /dev/null
+++ b/api_layers/__pycache__/arduino_layer.cpython-314.pyc
Binary files differ
diff --git a/api_layers/__pycache__/nidaqmx_layer.cpython-314.pyc b/api_layers/__pycache__/nidaqmx_layer.cpython-314.pyc
new file mode 100644
index 0000000..92f0ff3
--- /dev/null
+++ b/api_layers/__pycache__/nidaqmx_layer.cpython-314.pyc
Binary files differ
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
new file mode 100644
index 0000000..2a7cb7e
--- /dev/null
+++ b/core/__pycache__/__init__.cpython-314.pyc
Binary files differ
diff --git a/core/__pycache__/acquisition.cpython-314.pyc b/core/__pycache__/acquisition.cpython-314.pyc
new file mode 100644
index 0000000..d84b66b
--- /dev/null
+++ b/core/__pycache__/acquisition.cpython-314.pyc
Binary files differ
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
deleted file mode 100644
index 0ffb7a0..0000000
--- a/daq_system/core/__pycache__/__init__.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/core/__pycache__/acquisition.cpython-312.pyc b/daq_system/core/__pycache__/acquisition.cpython-312.pyc
deleted file mode 100644
index 8e4bde7..0000000
--- a/daq_system/core/__pycache__/acquisition.cpython-312.pyc
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index af251ba..0000000
--- a/daq_system/devices/__pycache__/__init__.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/devices/__pycache__/analog_input.cpython-312.pyc b/daq_system/devices/__pycache__/analog_input.cpython-312.pyc
deleted file mode 100644
index efe5752..0000000
--- a/daq_system/devices/__pycache__/analog_input.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/devices/__pycache__/base_device.cpython-312.pyc b/daq_system/devices/__pycache__/base_device.cpython-312.pyc
deleted file mode 100644
index 4f08c29..0000000
--- a/daq_system/devices/__pycache__/base_device.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/devices/__pycache__/device_registry.cpython-312.pyc b/daq_system/devices/__pycache__/device_registry.cpython-312.pyc
deleted file mode 100644
index 8d5ca55..0000000
--- a/daq_system/devices/__pycache__/device_registry.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/devices/__pycache__/digital_io.cpython-312.pyc b/daq_system/devices/__pycache__/digital_io.cpython-312.pyc
deleted file mode 100644
index 9ca7bc7..0000000
--- a/daq_system/devices/__pycache__/digital_io.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/devices/__pycache__/serial_device.cpython-312.pyc b/daq_system/devices/__pycache__/serial_device.cpython-312.pyc
deleted file mode 100644
index f4a2190..0000000
--- a/daq_system/devices/__pycache__/serial_device.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/devices/__pycache__/temperature.cpython-312.pyc b/daq_system/devices/__pycache__/temperature.cpython-312.pyc
deleted file mode 100644
index ab2a2b9..0000000
--- a/daq_system/devices/__pycache__/temperature.cpython-312.pyc
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index 7f2776c..0000000
--- a/daq_system/ui/__pycache__/alarm_panel.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/ui/__pycache__/config_dialog.cpython-312.pyc b/daq_system/ui/__pycache__/config_dialog.cpython-312.pyc
deleted file mode 100644
index d62887f..0000000
--- a/daq_system/ui/__pycache__/config_dialog.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/ui/__pycache__/device_panel.cpython-312.pyc b/daq_system/ui/__pycache__/device_panel.cpython-312.pyc
deleted file mode 100644
index 0616690..0000000
--- a/daq_system/ui/__pycache__/device_panel.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/ui/__pycache__/main_window.cpython-312.pyc b/daq_system/ui/__pycache__/main_window.cpython-312.pyc
deleted file mode 100644
index d6af385..0000000
--- a/daq_system/ui/__pycache__/main_window.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/ui/__pycache__/readout_panel.cpython-312.pyc b/daq_system/ui/__pycache__/readout_panel.cpython-312.pyc
deleted file mode 100644
index 47d94da..0000000
--- a/daq_system/ui/__pycache__/readout_panel.cpython-312.pyc
+++ /dev/null
Binary files differ
diff --git a/daq_system/ui/__pycache__/strip_chart.cpython-312.pyc b/daq_system/ui/__pycache__/strip_chart.cpython-312.pyc
deleted file mode 100644
index 95dc075..0000000
--- a/daq_system/ui/__pycache__/strip_chart.cpython-312.pyc
+++ /dev/null
Binary files differ
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
new file mode 100644
index 0000000..9ab29a6
--- /dev/null
+++ b/devices/__pycache__/__init__.cpython-314.pyc
Binary files differ
diff --git a/devices/__pycache__/analog_input.cpython-314.pyc b/devices/__pycache__/analog_input.cpython-314.pyc
new file mode 100644
index 0000000..4c08631
--- /dev/null
+++ b/devices/__pycache__/analog_input.cpython-314.pyc
Binary files differ
diff --git a/devices/__pycache__/base_device.cpython-314.pyc b/devices/__pycache__/base_device.cpython-314.pyc
new file mode 100644
index 0000000..ade2b20
--- /dev/null
+++ b/devices/__pycache__/base_device.cpython-314.pyc
Binary files differ
diff --git a/devices/__pycache__/device_registry.cpython-314.pyc b/devices/__pycache__/device_registry.cpython-314.pyc
new file mode 100644
index 0000000..608c8ee
--- /dev/null
+++ b/devices/__pycache__/device_registry.cpython-314.pyc
Binary files differ
diff --git a/devices/__pycache__/digital_io.cpython-314.pyc b/devices/__pycache__/digital_io.cpython-314.pyc
new file mode 100644
index 0000000..b2bb27f
--- /dev/null
+++ b/devices/__pycache__/digital_io.cpython-314.pyc
Binary files differ
diff --git a/devices/__pycache__/serial_device.cpython-314.pyc b/devices/__pycache__/serial_device.cpython-314.pyc
new file mode 100644
index 0000000..0674606
--- /dev/null
+++ b/devices/__pycache__/serial_device.cpython-314.pyc
Binary files differ
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
new file mode 100644
index 0000000..6e83590
--- /dev/null
+++ b/ui/__pycache__/__init__.cpython-314.pyc
Binary files differ
diff --git a/ui/__pycache__/add_device_dialog.cpython-314.pyc b/ui/__pycache__/add_device_dialog.cpython-314.pyc
new file mode 100644
index 0000000..280fdbf
--- /dev/null
+++ b/ui/__pycache__/add_device_dialog.cpython-314.pyc
Binary files differ
diff --git a/ui/__pycache__/alarm_panel.cpython-314.pyc b/ui/__pycache__/alarm_panel.cpython-314.pyc
new file mode 100644
index 0000000..a1811f7
--- /dev/null
+++ b/ui/__pycache__/alarm_panel.cpython-314.pyc
Binary files differ
diff --git a/ui/__pycache__/config_dialog.cpython-314.pyc b/ui/__pycache__/config_dialog.cpython-314.pyc
new file mode 100644
index 0000000..8876af3
--- /dev/null
+++ b/ui/__pycache__/config_dialog.cpython-314.pyc
Binary files differ
diff --git a/ui/__pycache__/device_panel.cpython-314.pyc b/ui/__pycache__/device_panel.cpython-314.pyc
new file mode 100644
index 0000000..07eb750
--- /dev/null
+++ b/ui/__pycache__/device_panel.cpython-314.pyc
Binary files differ
diff --git a/ui/__pycache__/main_window.cpython-314.pyc b/ui/__pycache__/main_window.cpython-314.pyc
new file mode 100644
index 0000000..27e4279
--- /dev/null
+++ b/ui/__pycache__/main_window.cpython-314.pyc
Binary files differ
diff --git a/ui/__pycache__/readout_panel.cpython-314.pyc b/ui/__pycache__/readout_panel.cpython-314.pyc
new file mode 100644
index 0000000..9258b10
--- /dev/null
+++ b/ui/__pycache__/readout_panel.cpython-314.pyc
Binary files differ
diff --git a/ui/__pycache__/strip_chart.cpython-314.pyc b/ui/__pycache__/strip_chart.cpython-314.pyc
new file mode 100644
index 0000000..4af6ff5
--- /dev/null
+++ b/ui/__pycache__/strip_chart.cpython-314.pyc
Binary files differ
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