summaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md183
1 files changed, 183 insertions, 0 deletions
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 |