Skip to content

Conversation

@Sapd
Copy link
Owner

@Sapd Sapd commented Dec 14, 2025

Rewrite codebase from C to modern C++20.

A part of it was obviously done using AI Agents, it would have been impossible otherwise.
See docs/ for examples how now the modern library works.

Complete modernization introducing type safety, better error handling, and a cleaner architecture.

  • Language: C → C++20 (requires GCC 10+, Clang 10+, MSVC 2019+)
  • Structure: Reorganized into lib/, cli/, tests/
  • Error handling: Result type with rich error information
  • Device code: 50-70% reduction via protocol templates

  • High-level C++ API (headsetcontrol.hpp)
  • C API for FFI bindings (headsetcontrol_c.h)
  • Shared library support (-DBUILD_SHARED_LIBRARY=ON)
  • Protocol templates: HIDPPDevice, SteelSeriesNovaDevice
  • Data-driven capability system
  • Test suite

  • HIDDevice base class with virtual methods per capability
  • Device registry singleton for device lookup
  • Capability descriptors as single source of truth
  • Feature handler registry (replaces switch statements)

CLI interface unchanged - fully backwards compatible.

  • See docs/ADDING_A_DEVICE.md for adding devices
  • See docs/ADDING_A_CAPABILITY.md for adding features
  • See docs/LIBRARY_USAGE.md for library integration

Fixes #431

@Sapd Sapd mentioned this pull request Dec 14, 2025
@ChrisLauinger77
Copy link
Contributor

I can test it on linux - are they changes how to build it or still
cd build
cmake ..
make
sudo make install ?

@Sapd
Copy link
Owner Author

Sapd commented Dec 14, 2025

I can test it on linux - are they changes how to build it or still cd build cmake .. make sudo make install ?

@ChrisLauinger77 Thank you, yes they are exactly as before.

Readme is also updated: https://github.com/Sapd/HeadsetControl/blob/236d4f79044b9bc313c65c9c1527c31111024637/README.md

@ChrisLauinger77
Copy link
Contributor

christian@debian:/var/data/dev/HeadsetControl/build$ headsetcontrol -o json { "name": "HeadsetControl", "version": "continuous-3-g236d4f7", "api_version": "1.4", "hidapi_version": "0.14.0", "device_count": 1, "devices": [ { "status": "partial", "device": "SteelSeries Arctis 7+", "vendor": "", "product": "", "id_vendor": "0x1038", "id_product": "0x220e", "capabilities": [ "CAP_SIDETONE", "CAP_BATTERY_STATUS", "CAP_INACTIVE_TIME", "CAP_CHATMIX_STATUS", "CAP_EQUALIZER_PRESET", "CAP_EQUALIZER" ], "capabilities_str": [ "sidetone", "battery", "inactive time", "chatmix", "equalizer preset", "equalizer" ], "battery": { "status": "BATTERY_AVAILABLE", "level": 100 }, "errors": { "chatmix": "Feature not supported by this device" } } ] }

looks like it still works :)

Good job !

@ChrisLauinger77
Copy link
Contributor

ChrisLauinger77 commented Dec 14, 2025

Ah sorry I was too fast.
It says chatmix not supported - which is wrong.
Here is the output from master to compare:
christian@debian:/var/data/dev/HeadsetControl/build$ headsetcontrol -o json { "name": "HeadsetControl", "version": "continuous", "api_version": "1.3", "hidapi_version": "0.14.0", "device_count": 1, "devices": [ { "status": "success", "device": "SteelSeries Arctis 7+", "vendor": "SteelSeries", "product": "Arctis 7+", "id_vendor": "0x1038", "id_product": "0x220e", "capabilities": [ "CAP_SIDETONE", "CAP_BATTERY_STATUS", "CAP_INACTIVE_TIME", "CAP_CHATMIX_STATUS", "CAP_EQUALIZER_PRESET", "CAP_EQUALIZER" ], "capabilities_str": [ "sidetone", "battery", "inactive time", "chatmix", "equalizer preset", "equalizer" ], "battery": { "status": "BATTERY_AVAILABLE", "level": 100 }, "equalizer": { "bands": 10, "baseline": 0, "step": 0.5, "min": -12, "max": 12 }, "equalizer_presets_count": 4, "chatmix": 64 } ] }

Chatmix is here so is equalizer_presets_count (the preset names for the count 4 - never worked for this headset - not sure if they supposed to)

Rewrite codebase from C to modern C++20

Complete modernization introducing type safety, better error handling, and a cleaner architecture.

- **Language**: C → C++20 (requires GCC 10+, Clang 10+, MSVC 2019+)
- **Structure**: Reorganized into lib/, cli/, tests/
- **Error handling**: Result<T> type with rich error information
- **Device code**: 50-70% reduction via protocol templates

- High-level C++ API (headsetcontrol.hpp)
- C API for FFI bindings (headsetcontrol_c.h)
- Shared library support (-DBUILD_SHARED_LIBRARY=ON)
- Protocol templates: HIDPPDevice, SteelSeriesNovaDevice
- Data-driven capability system
- Test suite

- HIDDevice base class with virtual methods per capability
- Device registry singleton for device lookup
- Capability descriptors as single source of truth
- Feature handler registry (replaces switch statements)

CLI interface unchanged - fully backwards compatible.

- See docs/ADDING_A_DEVICE.md for adding devices
- See docs/ADDING_A_CAPABILITY.md for adding features
- See docs/LIBRARY_USAGE.md for library integration

CI: Use GCC 13 on Ubuntu, add verbose test output
@Sapd
Copy link
Owner Author

Sapd commented Dec 14, 2025

@ChrisLauinger77 Indeed I missed parts of chatmix and equalizer. Can you please test again?

@ChrisLauinger77
Copy link
Contributor

ChrisLauinger77 commented Dec 14, 2025

here we go:
{ "name": "HeadsetControl", "version": "continuous-1-g1a0011e", "api_version": "1.4", "hidapi_version": "0.14.0", "device_count": 1, "devices": [ { "status": "success", "device": "SteelSeries Arctis 7+", "vendor": "", "product": "", "id_vendor": "0x1038", "id_product": "0x220e", "capabilities": [ "CAP_SIDETONE", "CAP_BATTERY_STATUS", "CAP_INACTIVE_TIME", "CAP_CHATMIX_STATUS", "CAP_EQUALIZER_PRESET", "CAP_EQUALIZER" ], "capabilities_str": [ "sidetone", "battery", "inactive time", "chatmix", "equalizer preset", "equalizer" ], "battery": { "status": "BATTERY_AVAILABLE", "level": 100 }, "equalizer": { "bands": 10, "baseline": 0, "step": 0.5, "min": -12, "max": 12 }, "equalizer_presets_count": 4, "chatmix": 64 } ] }
now it looks like before - except vendor and product are also empty

@ChrisLauinger77
Copy link
Contributor

ChrisLauinger77 commented Dec 14, 2025

When I add --test-device I get:
{ "name": "HeadsetControl", "version": "continuous-1-g1a0011e", "api_version": "1.4", "hidapi_version": "0.14.0", "device_count": 2, "devices": [ { "status": "success", "device": "HeadsetControl Test device", "vendor": "", "product": "", "id_vendor": "0xf00b", "id_product": "0xa00c", "capabilities": [ "CAP_SIDETONE", "CAP_BATTERY_STATUS", "CAP_NOTIFICATION_SOUND", "CAP_LIGHTS", "CAP_INACTIVE_TIME", "CAP_CHATMIX_STATUS", "CAP_VOICE_PROMPTS", "CAP_ROTATE_TO_MUTE", "CAP_EQUALIZER_PRESET", "CAP_EQUALIZER", "CAP_PARAMETRIC_EQUALIZER", "CAP_MICROPHONE_MUTE_LED_BRIGHTNESS", "CAP_MICROPHONE_VOLUME", "CAP_VOLUME_LIMITER", "CAP_BT_WHEN_POWERED_ON", "CAP_BT_CALL_VOLUME" ], "capabilities_str": [ "sidetone", "battery", "notification sound", "lights", "inactive time", "chatmix", "voice prompts", "rotate to mute", "equalizer preset", "equalizer", "parametric equalizer", "microphone mute led brightness", "microphone volume", "volume limiter", "bluetooth when powered on", "bluetooth call volume" ], "battery": { "status": "BATTERY_AVAILABLE", "level": 42, "voltage_mv": 3650, "time_to_empty_min": 302 }, "equalizer": { "bands": 10, "baseline": 0, "step": 0.5, "min": -12, "max": 12 }, "equalizer_presets_count": 4, "chatmix": 64 }, { "status": "success", "device": "SteelSeries Arctis 7+", "vendor": "", "product": "", "id_vendor": "0x1038", "id_product": "0x220e", "capabilities": [ "CAP_SIDETONE", "CAP_BATTERY_STATUS", "CAP_INACTIVE_TIME", "CAP_CHATMIX_STATUS", "CAP_EQUALIZER_PRESET", "CAP_EQUALIZER" ], "capabilities_str": [ "sidetone", "battery", "inactive time", "chatmix", "equalizer preset", "equalizer" ], "battery": { "status": "BATTERY_AVAILABLE", "level": 100 }, "equalizer": { "bands": 10, "baseline": 0, "step": 0.5, "min": -12, "max": 12 }, "equalizer_presets_count": 4, "chatmix": 64 } ] }

going offline now - can continue tomorrow

@Sapd
Copy link
Owner Author

Sapd commented Dec 14, 2025

Ah yes thank you, they are now populated

@ChrisLauinger77
Copy link
Contributor

grafik Here is a Meld Compare - NOW it looks good :)

Here is the output with --test-device
{ "name": "HeadsetControl", "version": "continuous-2-g181ba21", "api_version": "1.4", "hidapi_version": "0.14.0", "device_count": 2, "devices": [ { "status": "success", "device": "HeadsetControl Test device", "vendor": "HeadsetControl", "product": "Test Device", "id_vendor": "0xf00b", "id_product": "0xa00c", "capabilities": [ "CAP_SIDETONE", "CAP_BATTERY_STATUS", "CAP_NOTIFICATION_SOUND", "CAP_LIGHTS", "CAP_INACTIVE_TIME", "CAP_CHATMIX_STATUS", "CAP_VOICE_PROMPTS", "CAP_ROTATE_TO_MUTE", "CAP_EQUALIZER_PRESET", "CAP_EQUALIZER", "CAP_PARAMETRIC_EQUALIZER", "CAP_MICROPHONE_MUTE_LED_BRIGHTNESS", "CAP_MICROPHONE_VOLUME", "CAP_VOLUME_LIMITER", "CAP_BT_WHEN_POWERED_ON", "CAP_BT_CALL_VOLUME" ], "capabilities_str": [ "sidetone", "battery", "notification sound", "lights", "inactive time", "chatmix", "voice prompts", "rotate to mute", "equalizer preset", "equalizer", "parametric equalizer", "microphone mute led brightness", "microphone volume", "volume limiter", "bluetooth when powered on", "bluetooth call volume" ], "battery": { "status": "BATTERY_AVAILABLE", "level": 42, "voltage_mv": 3650, "time_to_empty_min": 302 }, "equalizer": { "bands": 10, "baseline": 0, "step": 0.5, "min": -12, "max": 12 }, "equalizer_presets_count": 4, "chatmix": 64 }, { "status": "success", "device": "SteelSeries Arctis 7+", "vendor": "SteelSeries", "product": "Arctis 7+", "id_vendor": "0x1038", "id_product": "0x220e", "capabilities": [ "CAP_SIDETONE", "CAP_BATTERY_STATUS", "CAP_INACTIVE_TIME", "CAP_CHATMIX_STATUS", "CAP_EQUALIZER_PRESET", "CAP_EQUALIZER" ], "capabilities_str": [ "sidetone", "battery", "inactive time", "chatmix", "equalizer preset", "equalizer" ], "battery": { "status": "BATTERY_AVAILABLE", "level": 100 }, "equalizer": { "bands": 10, "baseline": 0, "step": 0.5, "min": -12, "max": 12 }, "equalizer_presets_count": 4, "chatmix": 64 } ] }

@ChrisLauinger77
Copy link
Contributor

ChrisLauinger77 commented Dec 15, 2025

I get this when no headset is connected:
{ "name": "HeadsetControl", "version": "continuous-2-g181ba21", "api_version": "1.4", "hidapi_version": "0.14.0", "device_count": 1, "devices": [ { "status": "partial", "device": "SteelSeries Arctis 7+", "vendor": "SteelSeries", "product": "Arctis 7+", "id_vendor": "0x1038", "id_product": "0x220e", "capabilities": [ "CAP_SIDETONE", "CAP_BATTERY_STATUS", "CAP_INACTIVE_TIME", "CAP_CHATMIX_STATUS", "CAP_EQUALIZER_PRESET", "CAP_EQUALIZER" ], "capabilities_str": [ "sidetone", "battery", "inactive time", "chatmix", "equalizer preset", "equalizer" ], "equalizer": { "bands": 10, "baseline": 0, "step": 0.5, "min": -12, "max": 12 }, "equalizer_presets_count": 4, "errors": { "battery": "Device is offline or not responding", "chatmix": "Device is offline or not responding" } } ] }
The errors are new (and I like them).

But
"battery": {
"status": "BATTERY_UNAVAILABLE",
"level": -1
},
AND
"chatmix": 64

Should be reported - I rely on them in my tools.
(My headset dongle ALWAYS reports a chatmix value.)

@Sapd
Copy link
Owner Author

Sapd commented Dec 15, 2025

Yep, fixed

@ChrisLauinger77
Copy link
Contributor

here we go:
{ "name": "HeadsetControl", "version": "continuous-3-g6fc0e7d", "api_version": "1.4", "hidapi_version": "0.14.0", "device_count": 1, "devices": [ { "status": "partial", "device": "SteelSeries Arctis 7+", "vendor": "SteelSeries", "product": "Arctis 7+", "id_vendor": "0x1038", "id_product": "0x220e", "capabilities": [ "CAP_SIDETONE", "CAP_BATTERY_STATUS", "CAP_INACTIVE_TIME", "CAP_CHATMIX_STATUS", "CAP_EQUALIZER_PRESET", "CAP_EQUALIZER" ], "capabilities_str": [ "sidetone", "battery", "inactive time", "chatmix", "equalizer preset", "equalizer" ], "battery": { "status": "BATTERY_UNAVAILABLE", "level": -1 }, "equalizer": { "bands": 10, "baseline": 0, "step": 0.5, "min": -12, "max": 12 }, "equalizer_presets_count": 4, "chatmix": 64, "errors": { "battery": "Device is offline or not responding" } } ] }

@Sapd Sapd mentioned this pull request Dec 18, 2025
@floraaubry
Copy link
Contributor

I cannot manage to build with MSVC. Mingw works fine.
This is a bit too complex for me, so sorry if the agent report is full of garbage

MSVC Compilation Failures Summary

Environment:

  • Compiler: MSVC 2026 (Visual Studio BuildTools)

Issues:

  1. Missing POSIX Headers & Functions
  • File: tests/test_cli_output.cpp:73
    • Missing: popen(), pclose()
  • Files: cli/argument_parser.hpp:15, cli/dev.cpp:14
    • Missing: getopt.h
  • File: lib/devices/logitech_g633_g933_935.hpp:7
    • Missing: unistd.h
  1. C++20 Preprocessor Compatibility
  • File: lib/headsetcontrol.cpp:224 (and multiple locations)
    • Error: VA_OPT requires /Zc:preprocessor flag in MSVC
    • Warning: C5109: VA_OPT use in macro requires '/Zc:preprocessor'
  1. Designated Initializers Parsing
  • File: lib/device.cpp:5-62 (multiple lines)
    • Error: C2760: syntax error: '=' was unexpected here; expected '{'

This suggests MSVC may not be parsing designated initializers correctly even with C++20 enabled.

@Sapd
Copy link
Owner Author

Sapd commented Dec 19, 2025

@floraaubry Yeah it was not really programmed with MSVC in mind. That said providing that support was not too hard any more.

To do so:

Prerequisites:

# Install VCPKG first if not yet
git clone https://github.com/microsoft/vcpkg C:\vcpkg
C:\vcpkg\bootstrap-vcpkg.bat

# Install getopt
C:\vcpkg\vcpkg install hidapi:x64-windows getopt:x64-windows

Build:

cd HeadsetControl
cmake -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build build

@floraaubry
Copy link
Contributor

Managed to build!

Small errors when building shared:

# Library dependencies
if(MSVC)
    target_link_libraries(headsetcontrol_lib PUBLIC ${HIDAPI_LIBRARIES})
else()
    target_link_libraries(headsetcontrol_lib PUBLIC m ${HIDAPI_LIBRARIES})
endif()

Conditional import for libm, but missing same conditional for shared

    # Optionally build shared library for FFI bindings
if(BUILD_SHARED_LIBRARY)
    add_library(headsetcontrol_shared SHARED ${LIBRARY_SOURCES} ${LIBRARY_HEADERS})

    set_target_properties(headsetcontrol_shared PROPERTIES
        OUTPUT_NAME "headsetcontrol"
        VERSION "${GIT_VERSION}"
        SOVERSION 1
    )

    target_include_directories(headsetcontrol_shared PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}/lib
        ${CMAKE_CURRENT_SOURCE_DIR}/lib/devices
        ${HIDAPI_INCLUDE_DIRS}
    )

    target_link_libraries(headsetcontrol_shared PUBLIC m ${HIDAPI_LIBRARIES}) # <--

    # Export symbols for the C API on Windows
    if(WIN32)
        target_compile_definitions(headsetcontrol_shared PRIVATE HSC_BUILDING_DLL)
    endif()
endif()

Another issue when building shared:
DLL import library overwrites static library

i manged to fix it by temporary renaming

# Set library properties
set_target_properties(headsetcontrol_lib PROPERTIES
    OUTPUT_NAME "headsetcontrol"
    POSITION_INDEPENDENT_CODE ON
)

to

# Set library properties
set_target_properties(headsetcontrol_lib PROPERTIES
    OUTPUT_NAME "headsetcontrol_static"
    POSITION_INDEPENDENT_CODE ON
)

@Sapd
Copy link
Owner Author

Sapd commented Dec 19, 2025

@floraaubry Can you try again please?

@floraaubry
Copy link
Contributor

floraaubry commented Dec 19, 2025

@Sapd Build is succesful with d0071f2

@floraaubry
Copy link
Contributor

floraaubry commented Dec 19, 2025

device.hpp

struct capability_detail {
    // Usage page, only used when usageid is not 0; HID Protocol specific
    uint16_t usagepage;
    // Used instead of interface when not 0, and only used on Windows currently;
    // HID Protocol specific
    uint16_t usageid;
    /// Interface ID - zero means first enumerated interface!
    int interface;
};

interface is treated as an MSVC reserved keyword.

Renaming to interface_id allowed me to compile my application

image

Everything (that i support) seems to be working so far :)

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
5.0% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@Sapd
Copy link
Owner Author

Sapd commented Dec 19, 2025

@floraaubry I also renamed it in code. You are linking the application right (not using json output or similar)?, so I guess that aspects works fully now

@floraaubry
Copy link
Contributor

Yep i diteched the executable in profit of the lib. All i can test is sidetone, lights, battery status and level, and everything is working flawlesly

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rewrite to C++

4 participants