From 8d6acf3a8ea4b37f86b321dbf430be5be01b1267 Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Mon, 13 Apr 2026 14:36:55 -0600 Subject: init --- README.md | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 README.md (limited to 'README.md') diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ef85cc --- /dev/null +++ b/README.md @@ -0,0 +1,183 @@ +# 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 | -- cgit v1.2.3