From 5941ecf767d0bab4aea63d0b58ba94783be34eaf Mon Sep 17 00:00:00 2001 From: AdrianKuriata Date: Wed, 29 Jul 2026 15:56:07 +0200 Subject: [PATCH 1/4] perf(cli): don't force info reads when the invocation sets something Structured output marks every info capability as requested, even when the command line only asked to set a value. On an Audeze Maxwell 2 that turns a 0.07 s write into 2.90 s, because the status read sends 21 packets at 60 ms intervals. Explicitly requested info still works, -b -s 20 -o json reports the battery. --- cli/main.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/cli/main.cpp b/cli/main.cpp index 7255980..61425a3 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -1031,10 +1031,26 @@ std::vector toLegacyDeviceList(std::vector& device return legacy; } +// Whether this invocation was asked to set something. +bool hasRequestedAction(const std::vector& devices) +{ + for (const auto& dev : devices) { + for (const auto& req : dev.feature_requests) { + if (req.type == CAPABILITYTYPE_ACTION && req.should_process) { + return true; + } + } + } + + return false; +} + // Enable info requests for extended output formats (JSON, YAML, ENV) +// Only for a pure query: the extra reads can cost far more than the action +// itself (Maxwell 2: -s 20 is 0.07 s, -s 20 -o json is 2.90 s). void enableExtendedInfoRequests(std::vector& devices, bool extended) { - if (!extended) + if (!extended || hasRequestedAction(devices)) return; for (auto& dev : devices) { From 2f3c5fa667467a7c4afdc3c5b88c8049f98641a6 Mon Sep 17 00:00:00 2001 From: AdrianKuriata Date: Fri, 14 Aug 2026 14:18:33 +0200 Subject: [PATCH 2/4] perf(cli): check for actions per device, after the multi-device guard Review feedback on three points. enableExtendedInfoRequests ran before handleMultiDeviceActions, so it judged action requests that were neutralized one line later. It now runs after, and the ordering is spelled out at the call site so it does not get swapped back. hasRequestedAction looked at every device at once, so an action on one headset switched off the info reads of another that never had one. It now takes a single device and the caller skips per device. The output shape changes, so API_VERSION goes to 1.5. Updated the sample response in docs/LIBRARY_USAGE.md to match. --- cli/main.cpp | 26 ++++++++++++++++---------- cli/output/output.cpp | 4 +++- docs/LIBRARY_USAGE.md | 2 +- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/cli/main.cpp b/cli/main.cpp index 61425a3..d0be489 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -1031,14 +1031,13 @@ std::vector toLegacyDeviceList(std::vector& device return legacy; } -// Whether this invocation was asked to set something. -bool hasRequestedAction(const std::vector& devices) +// Whether this device was asked to set something. Only counts requests that are +// still live, so it has to run after handleMultiDeviceActions has had its say. +bool hasRequestedAction(const DiscoveredDevice& device) { - for (const auto& dev : devices) { - for (const auto& req : dev.feature_requests) { - if (req.type == CAPABILITYTYPE_ACTION && req.should_process) { - return true; - } + for (const auto& req : device.feature_requests) { + if (req.type == CAPABILITYTYPE_ACTION && req.should_process) { + return true; } } @@ -1047,13 +1046,17 @@ bool hasRequestedAction(const std::vector& devices) // Enable info requests for extended output formats (JSON, YAML, ENV) // Only for a pure query: the extra reads can cost far more than the action -// itself (Maxwell 2: -s 20 is 0.07 s, -s 20 -o json is 2.90 s). +// itself (Maxwell 2: -s 20 is 0.07 s, -s 20 -o json is 2.90 s). Per device, so +// an action on one headset does not silence the info reads of another. void enableExtendedInfoRequests(std::vector& devices, bool extended) { - if (!extended || hasRequestedAction(devices)) + if (!extended) return; for (auto& dev : devices) { + if (hasRequestedAction(dev)) + continue; + for (auto& req : dev.feature_requests) { if (req.type == CAPABILITYTYPE_INFO && !req.should_process && dev.hasCapability(req.cap)) { req.should_process = true; @@ -1217,8 +1220,11 @@ int main(int argc, char* argv[]) // Initialize and configure feature requests initializeFeatureRequests(devices, opts); bool extended = opts.output_format == OUTPUT_YAML || opts.output_format == OUTPUT_JSON || opts.output_format == OUTPUT_ENV; - enableExtendedInfoRequests(devices, extended); + // Order matters: handleMultiDeviceActions neutralizes action requests, and + // enableExtendedInfoRequests has to see the result of that, not the requests + // as they were parsed. handleMultiDeviceActions(devices, opts); + enableExtendedInfoRequests(devices, extended); // Main loop do { diff --git a/cli/output/output.cpp b/cli/output/output.cpp index d193cf0..1ee16bd 100644 --- a/cli/output/output.cpp +++ b/cli/output/output.cpp @@ -23,7 +23,9 @@ using namespace headsetcontrol::serializers; // Constants // ============================================================================ -constexpr std::string_view API_VERSION = "1.4"; +// 1.5: an invocation that performs an action no longer reports info it was not +// explicitly asked for. +constexpr std::string_view API_VERSION = "1.5"; constexpr std::string_view APP_NAME = "HeadsetControl"; // ============================================================================ diff --git a/docs/LIBRARY_USAGE.md b/docs/LIBRARY_USAGE.md index cf8afc0..419dd80 100644 --- a/docs/LIBRARY_USAGE.md +++ b/docs/LIBRARY_USAGE.md @@ -28,7 +28,7 @@ headsetcontrol -o env { "name": "HeadsetControl", "version": "3.0.0", - "api_version": "1.4", + "api_version": "1.5", "device_count": 1, "devices": [ { From ab84bf1d688408b8a7167a985a0335997584b86a Mon Sep 17 00:00:00 2001 From: AdrianKuriata Date: Fri, 14 Aug 2026 14:25:05 +0200 Subject: [PATCH 3/4] style: satisfy clang-format 18 in the files this PR touches cpp-linter checks whole files, not changed lines, and cli/main.cpp and cli/output/output.cpp both carry formatting violations that predate this PR. They are not reachable from master's own CI, which only runs clang-format on pull requests, so they went unnoticed. clang-format 18.1.3 output, nothing hand-written. --- cli/main.cpp | 6 +++--- cli/output/output.cpp | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cli/main.cpp b/cli/main.cpp index d0be489..007cff8 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -37,8 +37,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -420,7 +420,7 @@ class HIDEnumeration { if (devices_) hid_free_enumeration(devices_); } - HIDEnumeration(const HIDEnumeration&) = delete; + HIDEnumeration(const HIDEnumeration&) = delete; HIDEnumeration& operator=(const HIDEnumeration&) = delete; hid_device_info* get() const { return devices_; } @@ -1003,7 +1003,7 @@ void setupSignalHandler() #ifdef _WIN32 signal(SIGINT, signalHandler); #else - struct sigaction act {}; + struct sigaction act { }; act.sa_handler = signalHandler; sigaction(SIGINT, &act, nullptr); #endif diff --git a/cli/output/output.cpp b/cli/output/output.cpp index 1ee16bd..68b2d56 100644 --- a/cli/output/output.cpp +++ b/cli/output/output.cpp @@ -181,8 +181,7 @@ void processFeatureRequest(const FeatureRequest& req, DeviceData& dev, std::stri for (const auto& preset : presets->presets) { preset_data.push_back(EqualizerPresetData { .name = preset.name, - .values = preset.values - }); + .values = preset.values }); } dev.equalizer_presets = std::move(preset_data); } From 9a438ac391fb201c4de7fb677c9891622cf21fa5 Mon Sep 17 00:00:00 2001 From: Denis Arnst Date: Tue, 18 Aug 2026 18:33:25 +0200 Subject: [PATCH 4/4] test(cli): cover action-only suppression of unrequested extended info No existing CLI-level test exercised the path this PR adds: every JSON test passes -b explicitly alongside any action flag, so should_process for battery was already true before enableExtendedInfoRequests ran, and the new suppression logic was never hit. Add three cases: action-only omits info, action + explicit info keeps only that info, and a pure query still returns everything. --- tests/test_cli_output.cpp | 53 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_cli_output.cpp b/tests/test_cli_output.cpp index 6ca6d16..8c109a3 100644 --- a/tests/test_cli_output.cpp +++ b/tests/test_cli_output.cpp @@ -297,6 +297,54 @@ void testCliEnvShellSafe() std::cout << " ✓ CLI ENV output is shell-safe" << std::endl; } +// ============================================================================ +// Action + Extended Output Tests +// ============================================================================ + +void testCliActionOnlyOmitsUnrequestedInfo() +{ + std::cout << " Testing action-only invocation omits info not explicitly requested..." << std::endl; + + // -s (sidetone) is an action; battery/chatmix were not asked for, so an + // extended-format invocation should not pay for reading them. + std::string output = exec(HEADSETCONTROL_EXE " --test-device -s 20 -o json 2>&1"); + + ASSERT_CONTAINS(output, "\"actions\":", "Should have actions array"); + ASSERT_NOT_CONTAINS(output, "\"battery\":", "Battery info should be omitted when not requested"); + ASSERT_NOT_CONTAINS(output, "\"chatmix\":", "Chatmix info should be omitted when not requested"); + + std::cout << " ✓ Action-only invocation omits unrequested info" << std::endl; +} + +void testCliActionWithExplicitInfoKeepsOnlyThat() +{ + std::cout << " Testing action + explicit info request keeps only that info..." << std::endl; + + // -b is explicitly requested alongside the -s action; chatmix was not + // asked for and should still be omitted. + std::string output = exec(HEADSETCONTROL_EXE " --test-device -b -s 20 -o json 2>&1"); + + ASSERT_CONTAINS(output, "\"actions\":", "Should have actions array"); + ASSERT_CONTAINS(output, "\"battery\":", "Explicitly requested battery info should be present"); + ASSERT_NOT_CONTAINS(output, "\"chatmix\":", "Chatmix info should be omitted when not requested"); + + std::cout << " ✓ Action with explicit info request keeps only that info" << std::endl; +} + +void testCliPureQueryStillReturnsAllInfo() +{ + std::cout << " Testing pure query (no action) still returns all extended info..." << std::endl; + + // No action requested, so the extended-format default of reading every + // supported info capability still applies. + std::string output = exec(HEADSETCONTROL_EXE " --test-device -o json 2>&1"); + + ASSERT_CONTAINS(output, "\"battery\":", "Pure query should still report battery"); + ASSERT_CONTAINS(output, "\"chatmix\":", "Pure query should still report chatmix"); + + std::cout << " ✓ Pure query still returns all extended info" << std::endl; +} + // ============================================================================ // Standard Output Tests // ============================================================================ @@ -444,6 +492,11 @@ void runAllCliOutputTests() runTest("ENV Output", testCliEnvOutput); runTest("ENV Shell-Safe", testCliEnvShellSafe); + std::cout << "\n=== Action + Extended Output Tests ===" << std::endl; + runTest("Action-Only Omits Unrequested Info", testCliActionOnlyOmitsUnrequestedInfo); + runTest("Action With Explicit Info", testCliActionWithExplicitInfoKeepsOnlyThat); + runTest("Pure Query Still Returns All Info", testCliPureQueryStillReturnsAllInfo); + std::cout << "\n=== Standard Output Tests ===" << std::endl; runTest("Standard Output", testCliStandardOutput); runTest("Standard Battery Details", testCliStandardBatteryDetails);