# LabUI — 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/labui.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 |