# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Running the app ```bash python main.py ``` No build step. Runs in simulation mode by default (no hardware needed). ## Dependencies ```bash pip install PyQt6 pyqtgraph numpy pyserial pip install nidaqmx # optional — only for real NI hardware ``` ## Architecture ### Data flow ``` Hardware / Simulation ↓ api_layers/ ArduinoLayer, NidaqmxLayer — low-level I/O, background threads ↓ devices/ BaseDevice subclasses — wrap api_layers, expose read_channels() / write_channel() ↓ core/acquisition.py AcquisitionEngine — polls devices at 100 ms, fills ChannelBuffers, emits new_data signal ↓ core/signal_processor.py SignalProcessor — filters + derived channels, re-emits processed_data ↓ ui/strip_chart.py StripChartWidget — consumes processed_data, renders via pyqtgraph ``` Control widgets (left panel) go the other direction: UI → `ControlWidget._write()` → `DeviceRegistry.get_instance()` → `device.write_channel()` → api_layer. ### Key design patterns **Shared serial port** — `api_layers/port_registry.py` holds a module-level `port_registry` singleton. When multiple devices share one Arduino (e.g. `AnalogInputDevice` + `DigitalIODevice` on the same port), both call `port_registry.get_layer(port, baud)` and get the same `ArduinoLayer` instance. Never construct `ArduinoLayer` directly in device code. **Device auto-discovery** — `DeviceRegistry._discover()` scans `devices/` with `pkgutil`, imports every module, and registers any class that subclasses `BaseDevice`. Adding a new device type = drop a file in `devices/`, no registration needed. **Backend switching at runtime** — `AnalogInputDevice` and `DigitalIODevice` both have `switch_backend(backend, simulate, ...)` that disconnects, reconfigures, and reconnects without restarting. Called from "Apply & Reconnect" in the config dialog. **Profile persistence** — `core/profile.py` serialises the full operator state (controls, plot layout, signal pipelines, derived channels) to `.labdaq` JSON files. `ProfileManager` handles save/load. ### Directory map | Path | Purpose | |------|---------| | `api_layers/arduino_layer.py` | Serial protocol + simulation; `ARDUINO_FIRMWARE` string is the uploadable sketch | | `api_layers/nidaqmx_layer.py` | NI-DAQmx wrapper with simulation fallback | | `api_layers/port_registry.py` | Shared `ArduinoLayer` singleton per (port, baud) | | `devices/base_device.py` | `BaseDevice`, `ChannelConfig`, `DeviceInfo`, `DeviceStatus` | | `devices/analog_input.py` | Analog input — NI or Arduino backend | | `devices/digital_io.py` | Digital I/O — NI or Arduino backend | | `devices/serial_device.py` | Generic UART device | | `core/acquisition.py` | `AcquisitionEngine` + `ChannelBuffer` | | `core/signal_processor.py` | Filter chain + derived/virtual channels | | `core/profile.py` | `.labdaq` profile save/load | | `ui/main_window.py` | Top-level window, toolbar, demo device init | | `ui/control_panel.py` | Left panel output widgets (`OnOffSwitch`, `MotorControl`, etc.) | | `ui/strip_chart.py` | Live pyqtgraph chart, config-driven by `LayoutConfig` | | `ui/windows/` | Floating tool windows (Devices, Signals, Plot, Settings) | | `ui/style_dark.qss` / `style_light.qss` | Full app theme | ### Arduino firmware The firmware is embedded as `ARDUINO_FIRMWARE` in `api_layers/arduino_layer.py`. When editing it, set `N_DIG_OUT` to match the number of digital output pins in use (default was 0 — leaving pins in INPUT mode causes inverted write behaviour). Upload via Arduino IDE. Serial protocol: `A0:3.14,D2:1\n` stream from Arduino; `W:D7:1\n` / `P:D9:128\n` commands from PC. ### Adding a new device type 1. Subclass `BaseDevice` in a new file under `devices/` 2. Implement: `connect()`, `disconnect()`, `read_channels() → Dict[str, float]`, `write_channel(channel_id, value) → bool`, `get_config_widget() → QWidget` 3. Include `switch_backend()` if the device supports runtime reconfiguration 4. `DeviceRegistry` discovers it automatically on next run