This guide covers contributing to HeadsetControl, adding new devices, and understanding the codebase architecture.
- C++20 compiler (GCC 10+, Clang 10+, MSVC 2019+)
- CMake 3.12+
- HIDAPI library
- clang-format 18 (for code formatting)
HeadsetControl/
├── lib/ # Core library
│ ├── devices/ # Device implementations
│ │ ├── hid_device.hpp # Base class for all devices
│ │ ├── protocols/ # Protocol templates (HID++, SteelSeries)
│ │ └── *.hpp # Device-specific implementations
│ ├── device.hpp # Capability enums and structs
│ ├── device_registry.hpp # Device lookup singleton
│ ├── result_types.hpp # Result<T> error handling
│ ├── headsetcontrol.hpp # Public C++ API
│ └── headsetcontrol_c.h # Public C API (for FFI)
├── cli/ # Command-line interface
│ ├── main.cpp # Entry point
│ ├── argument_parser.hpp # CLI argument parsing
│ ├── dev.cpp # Developer/debug mode
│ └── output/ # JSON/YAML/ENV serializers
├── tests/ # Unit and integration tests
└── docs/ # Documentation
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_CLANG_FORMAT=ON ..
makemake check # Build and run all tests
ctest --verbose # Run tests with output
./headsetcontrol_tests # Run unit tests directlyThe project uses WebKit style via clang-format. CI requires version 18:
# Install clang-format 18
brew install llvm@18 # macOS
apt install clang-format-18 # Debian/Ubuntu
# Format all code
make formatcmake -DENABLE_CLANG_TIDY=ON ..
make tidyAll devices are registered in lib/device_registry.cpp:
void DeviceRegistry::initialize() {
// Each device is created with make_unique
// Vendor/product IDs are defined inside each device class
registerDevice(std::make_unique<CorsairVoidRich>());
registerDevice(std::make_unique<LogitechG533>());
registerDevice(std::make_unique<SteelSeriesArctisNova7>());
// ...
}All devices inherit from HIDDevice (lib/devices/hid_device.hpp):
class MyDevice : public HIDDevice {
public:
std::string_view getDeviceName() const override { return "My Headset"; }
uint16_t getVendorId() const override { return 0x1234; }
int getCapabilities() const override { return B(CAP_SIDETONE) | B(CAP_BATTERY_STATUS); }
Result<BatteryResult> getBattery(hid_device* handle) override {
// Implementation
}
Result<SidetoneResult> setSidetone(hid_device* handle, uint8_t level) override {
// Implementation
}
};All device methods return Result<T> for proper error handling:
Result<BatteryResult> getBattery(hid_device* handle) override {
std::array<uint8_t, 64> buffer{};
buffer[0] = 0xC9;
auto result = writeHID(handle, buffer);
if (!result) {
return result.error(); // Propagate error
}
result = readHIDTimeout(handle, buffer, 5000);
if (!result) {
return DeviceError::timeout("Battery request timed out");
}
return BatteryResult{
.level_percent = buffer[2],
.status = BATTERY_AVAILABLE
};
}Error types:
DeviceError::timeout(msg)- HID read timeoutDeviceError::hidError(msg)- HID communication errorDeviceError::protocolError(msg)- Unexpected device responseDeviceError::notSupported(msg)- Feature not supported
Common protocols have reusable templates in lib/devices/protocols/:
- HID++ Protocol (
hidpp_protocol.hpp) - Logitech devices - SteelSeries Protocol (
steelseries_protocol.hpp) - SteelSeries devices
Example using a protocol template:
class LogitechG535 : public HIDPPDevice {
public:
// Protocol template provides common HID++ functionality
// Just override device-specific details
};Capabilities are defined in lib/device.hpp using an X-macro pattern:
// Single source of truth - add one line to add a capability
#define CAPABILITIES_XLIST \
X(CAP_SIDETONE, "sidetone", 's') \
X(CAP_BATTERY_STATUS, "battery", 'b') \
X(CAP_LIGHTS, "lights", 'l') \
// ... more capabilities
// Enum is auto-generated from CAPABILITIES_XLIST
enum capabilities {
#define X(id, name, short_char) id,
CAPABILITIES_XLIST
#undef X
NUM_CAPABILITIES
};
// Use B() macro for bitmask
int caps = B(CAP_SIDETONE) | B(CAP_BATTERY_STATUS);See ADDING_A_DEVICE.md for a complete step-by-step guide with code examples.
Quick overview:
- Find device IDs with
./headsetcontrol --dev -- --list - Capture USB traffic with Wireshark
- Create device class in
lib/devices/yourdevice.hpp - Register in
lib/device_registry.cpp - Test and generate docs with
./headsetcontrol --readme-helper
See ADDING_A_CAPABILITY.md for a complete step-by-step guide.
Quick overview:
- Add to
CAPABILITIES_XLISTinlib/device.hpp(enum + strings auto-generated) - Add descriptor in
lib/capability_descriptors.hpp - Add result type in
lib/result_types.hpp - Add virtual method in
lib/devices/hid_device.hpp - Register handler in
lib/feature_handlers.hpp - Add CLI argument in
cli/main.cpp - Implement in device classes
Use the test device for development:
./headsetcontrol --test-device -b
./headsetcontrol --test-device -o jsonThe test device (0xF00B:0xA00C) implements all capabilities with predictable values.
For low-level HID debugging:
# List all HID devices
./headsetcontrol --dev -- --list
# List specific device interfaces
./headsetcontrol --dev -- --list --device 0x1b1c:0x1b27
# Send raw data and receive response
./headsetcontrol --dev -- --device 0x1b1c:0x1b27 \
--send "0xC9, 0x64" --receive --timeout 100
# Send feature report
./headsetcontrol --dev -- --device 0x1b1c:0x1b27 \
--send-feature "0x05, 0x00, 0x01"
# Repeat command every 2 seconds
./headsetcontrol --dev -- --device 0x1b1c:0x1b27 \
--send "0xC9" --receive --repeat 2Windows HID implementation differs from Linux/macOS:
-
Usage Page/ID Required: Windows needs exact HID usage page and usage ID, not just interface number:
constexpr capability_detail getCapabilityDetail(capabilities cap) const override { return { .usagepage = 0xFF00, .usageid = 0x0001 }; }
-
Exact Byte Count: Windows requires sending the exact expected packet size.
-
Common Errors:
- "Incorrect function" → Wrong HID endpoint (usage page/ID)
- "Incorrect parameter" → Wrong packet size
- C++20 with modern features (
std::format,std::span,std::optional) - RAII for resource management
[[nodiscard]]on error-returning functions- Designated initializers for structs
- No raw
new/delete- use smart pointers - Header-only device implementations
- Fork the repository
- Create a feature branch
- Run
make formatbefore committing - Ensure
make checkpasses - Update documentation if needed
- Submit a pull request
For major changes, open an issue first to discuss the approach.