|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +This file provides guidance to AI coding assistants (Claude Code, Copilot, Cursor, Codex, etc.) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +HeadsetControl is a cross-platform C++20 application to control USB-connected headsets on Linux, macOS, and Windows. It enables controlling features like sidetone, battery status, LEDs, inactive time, equalizers, and more for various gaming headsets (Logitech, SteelSeries, Corsair, HyperX, Roccat, Audeze). |
| 8 | + |
| 9 | +## Build System |
| 10 | + |
| 11 | +The project uses **CMake** as its build system. |
| 12 | + |
| 13 | +### Standard build commands: |
| 14 | +```bash |
| 15 | +# Initial setup |
| 16 | +mkdir build && cd build |
| 17 | +cmake .. |
| 18 | +make |
| 19 | + |
| 20 | +# Install globally (includes udev rules on Linux) |
| 21 | +sudo make install |
| 22 | + |
| 23 | +# On Linux after install, reload udev rules: |
| 24 | +sudo udevadm control --reload-rules && sudo udevadm trigger |
| 25 | +``` |
| 26 | + |
| 27 | +### Testing: |
| 28 | +```bash |
| 29 | +# Enable + build + run unit tests |
| 30 | +cmake -DBUILD_UNIT_TESTS=ON .. |
| 31 | +make check # Builds dependencies and runs ctest --output-on-failure |
| 32 | +# Or just: |
| 33 | +ctest |
| 34 | +``` |
| 35 | + |
| 36 | +### Code formatting: |
| 37 | +**IMPORTANT:** The CI uses clang-format version 18. To avoid formatting conflicts, install the matching version: |
| 38 | +```bash |
| 39 | +# On macOS: |
| 40 | +brew install llvm@18 |
| 41 | + |
| 42 | +# On Linux (Ubuntu/Debian): |
| 43 | +sudo apt-get install clang-format-18 |
| 44 | + |
| 45 | +# Format all code |
| 46 | +cmake -DENABLE_CLANG_FORMAT=ON .. |
| 47 | +make format |
| 48 | +``` |
| 49 | + |
| 50 | +### Code analysis: |
| 51 | +```bash |
| 52 | +cmake -DENABLE_CLANG_TIDY=ON .. |
| 53 | +make tidy |
| 54 | +``` |
| 55 | +The `tidy` target prefers `clang-tidy-9` if installed, otherwise falls back to the system `clang-tidy` (typically the same version as your formatter). |
| 56 | + |
| 57 | +## Architecture |
| 58 | + |
| 59 | +### Project Structure |
| 60 | + |
| 61 | +``` |
| 62 | +HeadsetControl/ |
| 63 | +├── lib/ # Core library (libheadsetcontrol) |
| 64 | +│ ├── devices/ # Device implementations (header-only) |
| 65 | +│ │ ├── hid_device.hpp # Base class for all devices |
| 66 | +│ │ ├── protocols/ # CRTP protocol templates |
| 67 | +│ │ │ ├── hidpp_protocol.hpp |
| 68 | +│ │ │ ├── steelseries_protocol.hpp |
| 69 | +│ │ │ ├── logitech_centurion_protocol.hpp |
| 70 | +│ │ │ └── logitech_calibrations.hpp |
| 71 | +│ │ └── *.hpp # Device-specific implementations |
| 72 | +│ ├── device.hpp # Capability enums and structs |
| 73 | +│ ├── device_registry.hpp # Device lookup singleton |
| 74 | +│ ├── result_types.hpp # Result<T> error handling |
| 75 | +│ ├── capability_descriptors.hpp # CAPABILITY_DESCRIPTORS metadata array |
| 76 | +│ ├── feature_handlers.hpp # FeatureHandlerRegistry dispatch table |
| 77 | +│ ├── headsetcontrol.hpp # Public C++ API |
| 78 | +│ ├── headsetcontrol_c.h # Public C API |
| 79 | +│ ├── dev.hpp / dev.cpp # Dev-mode HID exploration helpers |
| 80 | +│ └── utility.hpp # Helper functions |
| 81 | +├── cli/ # Command-line interface |
| 82 | +│ ├── main.cpp # Entry point |
| 83 | +│ ├── argument_parser.hpp # CLI argument parsing |
| 84 | +│ ├── dev.cpp # Developer/debug mode (--dev) |
| 85 | +│ └── output/ # Serialization (JSON, YAML, ENV) |
| 86 | +├── tests/ # Unit and integration tests |
| 87 | +├── docs/ # Documentation (ADDING_A_DEVICE, ADDING_A_CAPABILITY, |
| 88 | +│ # ADDING_A_CORSAIR_DEVICE, DEVELOPMENT, LIBRARY_USAGE) |
| 89 | +├── cmake_modules/ # Findhidapi.cmake and related |
| 90 | +├── assets/ # Icons and Windows resource file |
| 91 | +├── .github/ # CI workflows, issue and PR templates |
| 92 | +├── CMakePresets.json # CMake presets |
| 93 | +└── vcpkg.json # vcpkg manifest (Windows builds) |
| 94 | +``` |
| 95 | + |
| 96 | +### Device Registration System |
| 97 | + |
| 98 | +The core architecture uses a **device registry pattern** with modern C++20: |
| 99 | + |
| 100 | +1. **Device Registry** (`lib/device_registry.hpp`): |
| 101 | + - Singleton pattern for device management |
| 102 | + - `DeviceRegistry::instance().initialize()` registers all devices |
| 103 | + - `getDevice(vendor_id, product_id)` looks up devices by USB IDs |
| 104 | + - `getAllDevices()` returns all registered implementations |
| 105 | + |
| 106 | +2. **HIDDevice Base Class** (`lib/devices/hid_device.hpp`): |
| 107 | + - Pure virtual interface for all headset devices |
| 108 | + - Virtual methods for each capability (e.g., `setSidetone()`, `getBattery()`) |
| 109 | + - Returns `Result<T>` types for proper error handling |
| 110 | + - Provides HID communication helpers (`writeHID()`, `readHIDTimeout()`) |
| 111 | + |
| 112 | +3. **Device Implementations** (`lib/devices/*.hpp`): |
| 113 | + - Each headset is a class inheriting from `HIDDevice` (or a CRTP protocol template) |
| 114 | + - Header-only implementations |
| 115 | + - CRTP protocol templates under `lib/devices/protocols/` reduce boilerplate (HID++, SteelSeries, Logitech Centurion). Inherit via e.g. `protocols::HIDPPDevice<MyDevice>`. |
| 116 | + |
| 117 | +### Result<T> Error Handling |
| 118 | + |
| 119 | +All device methods return `Result<T>` (similar to `std::expected`): |
| 120 | + |
| 121 | +```cpp |
| 122 | +Result<BatteryResult> getBattery(hid_device* handle) override { |
| 123 | + auto result = readHIDTimeout(handle, buffer, timeout); |
| 124 | + if (!result) { |
| 125 | + return result.error(); // Propagate error |
| 126 | + } |
| 127 | + return BatteryResult { .level_percent = 85, .status = BATTERY_AVAILABLE }; |
| 128 | +} |
| 129 | +``` |
| 130 | +
|
| 131 | +Error types: `DeviceError::timeout()`, `hidError()`, `protocolError()`, `notSupported()` |
| 132 | +
|
| 133 | +### Adding New Device Support |
| 134 | +
|
| 135 | +See `docs/ADDING_A_DEVICE.md` for detailed instructions. Quick overview: |
| 136 | +
|
| 137 | +1. Create `lib/devices/vendor_model.hpp` |
| 138 | +2. Inherit from `HIDDevice` or a protocol template |
| 139 | +3. Implement required virtual methods |
| 140 | +4. Register in `lib/device_registry.cpp` |
| 141 | +
|
| 142 | +### Using as a Library |
| 143 | +
|
| 144 | +See `docs/LIBRARY_USAGE.md` for integration guide. The library provides: |
| 145 | +
|
| 146 | +- `headsetcontrol_lib` static library target |
| 147 | +- `headsetcontrol_shared` shared library target (with `-DBUILD_SHARED_LIBRARY=ON`) |
| 148 | +- Public headers in `lib/` directory |
| 149 | +- C++ API (`headsetcontrol.hpp`) and C API (`headsetcontrol_c.h`) |
| 150 | +- Device discovery and control API |
| 151 | +
|
| 152 | +```bash |
| 153 | +# Build shared library for FFI (Python, Rust, etc.) |
| 154 | +cmake -DBUILD_SHARED_LIBRARY=ON .. |
| 155 | +make |
| 156 | +``` |
| 157 | + |
| 158 | +### Capability System |
| 159 | + |
| 160 | +Capabilities are enumerated in `lib/device.hpp` via the `CAPABILITIES_XLIST` macro. The full set: |
| 161 | + |
| 162 | +- `CAP_SIDETONE` — Microphone sidetone level |
| 163 | +- `CAP_BATTERY_STATUS` — Battery level and charging status |
| 164 | +- `CAP_NOTIFICATION_SOUND` — Play a notification tone |
| 165 | +- `CAP_LIGHTS` — LED on/off |
| 166 | +- `CAP_INACTIVE_TIME` — Auto-shutoff timer |
| 167 | +- `CAP_CHATMIX_STATUS` — Game/chat audio balance |
| 168 | +- `CAP_VOICE_PROMPTS` — Toggle spoken prompts |
| 169 | +- `CAP_ROTATE_TO_MUTE` — Rotate boom mic to mute |
| 170 | +- `CAP_EQUALIZER_PRESET` — Select preset EQ |
| 171 | +- `CAP_EQUALIZER` — Custom EQ bands |
| 172 | +- `CAP_PARAMETRIC_EQUALIZER` — Parametric EQ (gain, Q-factor, frequency) |
| 173 | +- `CAP_MICROPHONE_MUTE_LED_BRIGHTNESS` |
| 174 | +- `CAP_MICROPHONE_VOLUME` |
| 175 | +- `CAP_VOLUME_LIMITER` |
| 176 | +- `CAP_BT_WHEN_POWERED_ON` — Bluetooth-on-power-on behavior |
| 177 | +- `CAP_BT_CALL_VOLUME` |
| 178 | +- `CAP_NOISE_FILTER` |
| 179 | + |
| 180 | +When adding a capability, update both `CAPABILITIES_XLIST` and the descriptor/handler tables (see Data-Driven Feature System below). |
| 181 | + |
| 182 | +### Data-Driven Feature System |
| 183 | + |
| 184 | +The codebase uses a data-driven approach for feature handling: |
| 185 | + |
| 186 | +1. **Capability Descriptors** (`lib/capability_descriptors.hpp`): |
| 187 | + - Single source of truth for all capability metadata |
| 188 | + - CLI flag names, descriptions, value ranges |
| 189 | + - Used for validation and help text generation |
| 190 | + |
| 191 | +2. **Feature Handler Registry** (`lib/feature_handlers.hpp`): |
| 192 | + - Dispatch table for feature execution |
| 193 | + - Eliminates giant switch statements |
| 194 | + - One handler per capability |
| 195 | + |
| 196 | +```cpp |
| 197 | +// Adding a new capability only requires: |
| 198 | +// 1. Add descriptor to CAPABILITY_DESCRIPTORS array |
| 199 | +// 2. Register handler in FeatureHandlerRegistry::registerAllHandlers() |
| 200 | +``` |
| 201 | + |
| 202 | +See `docs/ADDING_A_CAPABILITY.md` for a step-by-step guide. |
| 203 | + |
| 204 | +## Code Style |
| 205 | + |
| 206 | +- **C++20 standard** with modern features |
| 207 | +- `.clang-format` uses WebKit base style |
| 208 | +- RAII for resource management |
| 209 | +- `Result<T>` is `[[nodiscard]]` at the class level — you do **not** need to mark virtual override methods returning `Result<T>` with `[[nodiscard]]`; the enforcement is on the type |
| 210 | +- `std::format` for string formatting |
| 211 | +- `std::optional`, `std::span`, `std::string_view` |
| 212 | +- Designated initializers for struct initialization |
| 213 | +- Device files are **header-only** (`.hpp`); the only `.cpp` in `lib/devices/` is `hid_device.cpp` |
| 214 | +- Naming: methods `camelCase`, member variables `snake_case_` (trailing underscore), constants `ALL_CAPS` |
| 215 | +- `using namespace std::string_view_literals;` at file scope is the convention in device headers |
| 216 | +- Use `[[maybe_unused]]` on unused parameters in virtual overrides |
| 217 | + |
| 218 | +## Dependencies |
| 219 | + |
| 220 | +- **HIDAPI** — USB HID communication library (required) |
| 221 | +- **CMake** — Build system (minimum 3.12) |
| 222 | +- **C++20 compiler** — GCC 13+, Clang 16+, Apple Clang 15+, MSVC 19.29+ (VS 2019 16.10+) as enforced in `CMakeLists.txt` |
| 223 | + |
| 224 | +## Platform-Specific Notes |
| 225 | + |
| 226 | +### Linux |
| 227 | +- Generates udev rules: `headsetcontrol -u > /etc/udev/rules.d/70-headset.rules` |
| 228 | +- Reload rules: `sudo udevadm control --reload-rules && sudo udevadm trigger` |
| 229 | + |
| 230 | +### macOS |
| 231 | +- No special permissions needed |
| 232 | +- Homebrew: `brew install sapd/headsetcontrol/headsetcontrol --HEAD` |
| 233 | + |
| 234 | +### Windows |
| 235 | +- Uses SetupAPI for HID access |
| 236 | +- Some devices require `usagepage`/`usageid` instead of `interface` |
| 237 | + |
| 238 | +## Testing |
| 239 | + |
| 240 | +- **Unit tests**: `tests/test_*.cpp` using a lightweight test framework |
| 241 | +- **Test device**: `HeadsetControlTest` (0xF00B:0xA00C) implements all capabilities |
| 242 | +- Run with: `./headsetcontrol --test-device -b` |
| 243 | +- **Dev mode**: `./headsetcontrol --dev -- --list` for HID exploration |
0 commit comments