From 85645ab6c52ab7a80e679c63b9e1af0e8ae9c466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 18 May 2026 11:31:22 -0400 Subject: [PATCH] Add CLAUDE.md for AI-assisted contributors Captures the project conventions that aren't obvious from the code or the README: camelCase style, the python3/explicit-test-file workflow, the PhysicalDevice/DeviceManager/NotificationCenter triad, the per-family base classes, the 7-step recipe for adding a new device (referencing labjackdevice.py), and the patterns to avoid (the half-finished 'commands' dict, the bloated oceaninsight.py, wildcard imports). The goal is to let an LLM-assisted contributor produce code that matches existing conventions without having to derive them from scattered evidence across the codebase. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..8ffa31da --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +PyHardwareLibrary controls scientific hardware (motion stages, spectrometers, lasers, DAQs, power meters, oscilloscopes, cameras) used in the Côté lab. Drivers live under `hardwarelibrary//.py`. + +This file is for AI-assisted contributors. Humans should read `README.md` and the numbered companion docs (`README-1-USB.md` through `README-7-Sutter-ROE-200.md`) first — they cover USB/RS-232 background, the new-device tutorial, and the existing device matrix. + +## Style + +- **camelCase everywhere** for methods, attributes, and parameters. +- **No docstrings on obvious code.** Only add one when the *why* is non-obvious (a constraint, a workaround, a surprising invariant). +- **No inline `# what this does` comments.** Well-named identifiers do that job. +- **No emojis** in code, commit messages, or markdown. +- **No wildcard imports in new code.** The existing `from X import *` everywhere is technical debt, not convention. Use explicit imports. +- **Descriptive parameter names.** `text_format` over `text` when the value is a format string. `displacement` over `d`. Brevity is not a goal. + +## Running things + +- Python is `python3`, not `python`. +- Tests: `python3 -m pytest hardwarelibrary/tests/.py -v`. +- Directory collection (`pytest hardwarelibrary/tests/`) currently returns **zero tests** because pytest's default `python_files` pattern is `test_*.py` and the files here are `testFoo.py`. Name files explicitly until `pyproject.toml` is updated. +- Tests for hardware-dependent code must `skipTest(...)` when no hardware is attached — they must not fail. Pattern established in PRs #65 and #66; the `DebugLabjackDevice` tests in PR on `labjack-rewrite` are the cleanest reference. + +## Architecture + +Core triad — all under `hardwarelibrary/`: + +- `physicaldevice.py:PhysicalDevice` — base for every device. Owns lifecycle (`initializeDevice` / `shutdownDevice`), state machine (`DeviceState`), port handle, and monitoring thread. +- `devicemanager.py:DeviceManager` — singleton. Discovers connected USB devices, dispatches add/remove notifications. +- `notificationcenter.py:NotificationCenter` — Cocoa-style pub/sub. Devices post notifications on state changes and measurements. + +Communication abstractions under `hardwarelibrary/communication/`: + +- `commands.py` — `Command` / `TextCommand` / `MultilineTextCommand` / `DataCommand` classes that bundle outgoing payload + reply parser. +- `debugport.py:DebugPort` — table-driven mock port. Used by the few devices that adopt the `commands` dict pattern. +- `serialport.py`, `usbport.py` — concrete port implementations. + +## Device families + +Each family has a protocol base class. Implement these methods when adding a device: + +| Family | Folder | Base class | Required `do*` methods | +|---|---|---|---| +| Linear motion | `motion/` | `LinearMotionDevice` | `doMoveTo`, `doMoveBy`, `doGetPosition`, `doHome` | +| Rotation | `motion/` | `RotationDevice` | `doMoveTo`, `doMoveBy`, `doGetOrientation`, `doHome` | +| Power meter | `powermeters/` | `PowerMeterDevice` | `doGetAbsolutePower`, `doGetCalibrationWavelength`, `doSetCalibrationWavelength` | +| Spectrometer | `spectrometers/` | `Spectrometer` (`base.py`) | `getSpectrum`, `setIntegrationTime`, `getIntegrationTime` | +| Oscilloscope | `oscilloscope/` | `OscilloscopeDevice` | per-instrument SCPI methods | +| Camera | `cameras/` | `CameraDevice` | `doCaptureFrame` | +| Laser source | `sources/` | `LaserSourceDevice` | `doTurnOn`, `doTurnOff`, `doSetPower`, `doGetPower` | +| DAQ | `daq/` | `AnalogIOProtocol` / `DigitalIOProtocol` (mixins) | `getAnalogVoltage`, `setAnalogVoltage`, `getDigitalValue`, `setDigitalValue` | + +## Adding a new device + +Reference implementation: `hardwarelibrary/daq/labjackdevice.py`. The pattern: + +1. Subclass the family base **and** `PhysicalDevice`. +2. Set class attributes `classIdVendor` and `classIdProduct` (USB VID/PID, or the equivalent for serial-only devices). +3. Implement `doInitializeDevice` and `doShutdownDevice`. Keep them minimal — open the port, close the port. +4. Implement the family-required `do*` methods. +5. Add a companion `DebugXxxDevice(XxxDevice)` in the same file. Stub the hardware, store state in dicts. Use `classIdVendor = 0xFFFF` and a unique `classIdProduct >= 0xFFF0`. +6. Export both from the family's `__init__.py`. +7. Add a test file `hardwarelibrary/tests/testXxx.py`. Include a `TestDebugXxxDevice` class (always runs) and a `TestXxxDevice` class that skips when hardware is absent. + +`README-4-New-device-coding-example.md` walks through this end-to-end for humans; the LabJack rewrite is the most current concrete example. + +## Patterns to avoid + +- **The `commands` dict on `PhysicalDevice` is half-finished.** Only `IntegraDevice` (`powermeters/integradevice.py`) and `EchoDevice` use it. For new devices, do not adopt this pattern — implement protocol bytes directly in your `do*` methods, the way `SutterDevice`, `CoboltDevice`, and `OISpectrometer` do. The `send-side-migration` branch was meant to complete the pattern; until it merges, treat it as experimental. +- **`spectrometers/oceaninsight.py`** is 967 lines and has known structural problems (duplicate exception classes, a `DebugSpectro` that doesn't inherit from `Spectrometer`, a duplicate `validateUSBBackend` whose macOS check is wrong). Do not copy from it. New spectrometers should subclass `spectrometers/base.py:Spectrometer` directly. +- **`from X import *` in new files.** Many existing modules do this; resist mirroring. It hides what's in scope and confuses static tooling. + +## Branch hygiene + +- The main branch is named `master`, not `main`. +- `send-side-migration` has been unmerged for months — do not assume its changes are present on `master`. On `master`, `TextCommand` uses `self.text`, not `self.text_format`. +- Open PRs against `master`. Prefer per-bug commits (each with a Problem/Solution body) over bundled commits — PR #74 is the template. + +## Test running cheatsheet + +``` +python3 -m pytest hardwarelibrary/tests/testCommandRecognition.py -v +python3 -m pytest hardwarelibrary/tests/testPhysicalDevice.py -v +python3 -m pytest hardwarelibrary/tests/testTableDrivenDebugPort.py -v +python3 -m pytest hardwarelibrary/tests/testLabjackU3.py -v +``` + +Single test: +``` +python3 -m pytest hardwarelibrary/tests/testLabjackU3.py::TestDebugLabjackDevice::testSetAndGetAnalogVoltage -v +```