Repository navigation
Add CAP_LIGHT_COLOR (--light-color RRGGBB) - #580
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate findings remain in API versioning and Virtuoso error and connection handling.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds CAP_LIGHT_COLOR with CLI, C/C++ APIs, serialization, documentation, tests, and Corsair Virtuoso XT/SE support.
Changes:
- Adds RGB parsing, capability plumbing, and public APIs.
- Implements color output across JSON, YAML, and ENV.
- Adds Corsair lighting control and test coverage.
Unresolved findings concern API versioning and Virtuoso error, cleanup, connectivity, and target-resolution handling.
File summaries
| File | Summary |
|---|---|
tests/test_utilities.cpp |
Tests RGB parsing |
tests/test_output_formats.cpp |
Tests color and value serialization |
tests/test_library_api.cpp |
Tests C/C++ APIs |
tests/test_cli_output.cpp |
Tests CLI output and ordering |
README.md |
Documents color support and device behavior |
lib/utility.hpp |
Declares the color parser |
lib/utility.cpp |
Implements color parsing |
lib/result_types.hpp |
Adds light color result types |
lib/headsetcontrol.hpp |
Adds the C++ color API |
lib/headsetcontrol.cpp |
Implements the C++ color API |
lib/headsetcontrol_c.h |
Adds the C API and capability enum |
lib/headsetcontrol_c.cpp |
Implements the C API and enum checks |
lib/feature_handlers.hpp |
Registers color handling |
lib/devices/hid_device.hpp |
Adds the device color method |
lib/devices/headsetcontrol_test.hpp |
Adds test-device support |
lib/devices/corsair_virtuoso_xt.hpp |
Implements Virtuoso lighting control |
lib/device.hpp |
Adds capability and RGB parameter plumbing |
lib/capability_descriptors.hpp |
Adds capability metadata |
docs/LIBRARY_USAGE.md |
Documents library APIs |
docs/ADDING_A_DEVICE.md |
Updates capability guidance |
docs/ADDING_A_CAPABILITY.md |
Documents capability implementation rules |
cli/output/output.cpp |
Emits structured color fields |
cli/output/output_data.hpp |
Adds color-aware value handling |
cli/main.cpp |
Parses and schedules color actions |
AGENTS.md |
Updates capability guidance |
Review details
Suppressed comments (3)
cli/output/output.cpp:103
- Adding
action.colorchanges the structured output schema, butAPI_VERSIONremains1.5(line 29).docs/LIBRARY_USAGE.md:97-104defines minor-version increments for additions, so clients cannot distinguish this schema from the old 1.5 format; bump the version and update the corresponding documentation/fixtures.
action.color = std::format("#{:02x}{:02x}{:02x}", c.r, c.g, c.b);
lib/devices/corsair_virtuoso_xt.hpp:133
- When the headset is charging, this new branch returns
BATTERY_CHARGING, but the CLI's--connectedimplementation incli/main.cpp:1209-1210only treatsBATTERY_AVAILABLEas connected. Consequentlyheadsetcontrol --connectedreportsfalsefor a connected Virtuoso while it is charging; the connectivity check should accept both available and charging statuses.
auto status = BATTERY_AVAILABLE;
if (auto charge_state = readProperty(device_handle, resolved->target, PROP_BATTERY_STATUS);
charge_state && *charge_state == CHARGE_STATE_CHARGING) {
status = BATTERY_CHARGING;
lib/devices/corsair_virtuoso_xt.hpp:453
resolveTargettreats every failedreadPropertyas evidence that the candidate was wrong and discards the error. If both probes hit an HID, permission, or protocol failure, the caller is incorrectly told that the headset is offline; only a silent timeout/offline result should trigger the alternate probe, while other errors should be propagated.
for (const uint8_t candidate : { hinted, alternate }) {
if (auto level = readProperty(
device_handle, candidate, PROP_BATTERY_LEVEL, TARGET_PROBE_TIMEOUT_MS)) {
return ResolvedTarget { .target = candidate, .battery_level = *level };
}
- Files reviewed: 25/25 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (auto result = writeProperty(device_handle, target, PROP_BRIGHTNESS, BRIGHTNESS_MAX); | ||
| !result) { | ||
| return result.error(); |
There was a problem hiding this comment.
Fixed in d79a316. SoftwareModeGuard gained dismiss(), and the color path now takes a guard like every other write, dismissing it only once the frame is written. A successful color stays in software mode, so the frame stays visible; a failed brightness or frame write hands the headset back to hardware mode.
test_corsair_virtuoso.cpp covers both halves with a mock: a clean write takes six requests and never restores hardware mode, and a failed frame write still closes the handle and then restores it. Dropping the guard fails the failure case, and dropping dismiss() fails the success case. On the XT, --light-color still leaves the headset in software mode.
| if (result.error().code == DeviceError::Code::Timeout) { | ||
| return DeviceError::deviceOffline("Headset not connected or powered off"); | ||
| } | ||
| return result.error(); |
There was a problem hiding this comment.
Fixed — in #570 (8feff12), since that's where this code comes from, merged in here. The previous review there settled for a comment; this replaces the comment with a fix.
Every request now goes through transact(), which throws away anything already queued (non-blocking reads, bounded) before writing. Nothing read before a request goes out can be its reply, so it only ever discards stale replies and the volume events readReply() skipped anyway. The handle commands for the lighting frame use it too. A battery read still takes about 0.2 s on the XT.
The test mock makes each report readable only after a given number of writes, which is what distinguishes a stale reply from a real one. With a stale charge-state reply queued before getBattery(), the level comes out right; without the discard, the stale 1 is read as the battery level and the test fails.
Two findings from Copilot's review of the stacked Sapd#580, both in code this PR introduces. A reply that arrives after its request timed out stayed queued, and replies carry no property or handle ID - only who sent them and the command they answer - so the next request of the same kind would take it for its own. The last review settled for a comment on this; it is fixed instead. Every request now goes through transact(), which throws away anything already queued before writing. Reports read before a request is sent cannot be its reply, so this only ever drops stale replies and the volume events readReply() skipped anyway. It is bounded, so a device that never stops sending cannot stall a request, and a battery read still takes about 0.2 s. resolveTarget() treated every failed probe as "wrong target" and ended with "offline", so a failed HID write or an unexpected status was reported as the headset being powered off. Only two failures now mean wrong target: silence, and "no such property", which is a receiver answering for itself with no headset behind it. Anything else is returned as is. tests/test_corsair_virtuoso.cpp adds a mock whose reports only become readable after a given number of writes, which is what distinguishes a stale reply from a real one. It covers battery over both targets, a stale reply queued before the request, a receiver with no headset (offline, not unsupported), and a HID write failure (propagated, and the other target is not probed). Removing the discard fails the stale-reply test; treating every probe failure as wrong target fails the HID error test; treating only silence as wrong target fails the receiver test. Checked on the XT over wireless: battery, sidetone, sleep timer and lights, each read back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BHCVMmAAeV5WGP7oYZsk3T
From Copilot's review. setLightColor() switched to software mode and then returned on a failed brightness or frame write without switching back, since the color path deliberately has no guard. A failed color left the headset in software mode with nothing worth keeping on its LEDs. SoftwareModeGuard now has dismiss(): the color path takes a guard like every other write and dismisses it only once the frame is written, so success stays in software mode and failure is handed back. A mock test covers both: a clean write takes six requests and never restores hardware mode, and a failed frame write still closes the handle and then restores it. Without the guard the failure case fails; without dismiss() the success case does. On the XT, --light-color still leaves the headset in software mode. API_VERSION goes to 1.6. Action entries gained "color", and 4.1.0 already shipped 1.5, so this cannot fold into 1.5 the way Sapd#560's sidetone field did before that release. The LIBRARY_USAGE.md example and the serializer fixture follow, as in Sapd#549. Also brings in Sapd#570's fixes for stale replies and probe errors, which the light color path picks up through transact(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BHCVMmAAeV5WGP7oYZsk3T
|
Addressing the three findings Copilot suppressed in its summary: API version — fixed, now 1.6 (d79a316). Action entries gained
|
Sapd
left a comment
There was a problem hiding this comment.
Plumbing is good - matches #572 point for point. Output is byte-identical to master for every non-color action across all formats, so the hasValue() change is safe. API bump follows what #549 did.
The per-entry static_asserts are worth keeping: they catch two entries having their values swapped, which a count check wouldn't. (Your description is wrong on one detail - deleting HSC_CAP_NOISE_FILTER fails with "undeclared identifier", not the sync message.)
I don't want to advertise light color on the Virtuoso yet, and it's worse than the ~60s revert:
- Any later command kills it.
setSidetone,setInactiveTimeandsetLightsall install aSoftwareModeGuard, and restoring hardware mode swaps the frame for the headset's own effect - your own finding. So--light-color ff0000then-s 50a minute later and the color is gone. setLightColorwritesPROP_BRIGHTNESS = BRIGHTNESS_MAXunconditionally, and brightness persists.-l 0today plus--light-colortomorrow leaves the stock effect at full brightness, with the earlier "lights off" silently undone.
The README table can't say "works for a minute unless you run another command". Please split the Virtuoso commit off - I'll take it once there's a hold or a hardware-mode store. If you'd rather keep it, at minimum read brightness and only raise it from 0.
My fault in #572 on this: "color implies on" assumed on was stateless. On this headset it's a persisted brightness value. Should have said raise it only if it reads 0, or leave it to the device.
-f can't be the hold either - processFeatureRequests only runs requests still marked FEATURE_NOT_PROCESSED and nothing resets that in the loop, so follow mode re-prints instead of re-querying. Opened separately, not your problem.
|
On So the condition becomes Send it separately, agreed it doesn't belong in this PR. It changes |
A separate capability rather than an extension of -l, as agreed in Sapd#572. LightColorSettings { r, g, b } joins the FeatureParam variant, since an int is only for a scalar in a range. The color applies to every zone; how many there are stays private to the device. 000000 is valid. CLI: --light-color RRGGBB, a leading # accepted. The request runs after -l, so with both on one command line the color wins. Library: HIDDevice::setLightColor(), Headset::setLightColor() and hsc_set_light_color(h, r, g, b). CAP_LIGHT_COLOR is appended after CAP_HEADSET_VOLUME in CAPABILITIES_XLIST and hsc_capability_t. Output: actions carry "color": "#rrggbb" in JSON, YAML and ENV, and "value" packs 0xRRGGBB. The rule for when an action reports its value now lives once in ActionData::hasValue() instead of three inline "value > 0" checks, so black is not dropped; other actions keep omitting 0. API_VERSION goes to 1.9. The test device implements it. Docs list the capability, and say that new capabilities are appended and mirrored in the C header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Split as you asked, and rebased onto
Unit suite green and clang-format 18 clean. |
|
All three points are addressed, and the asserts in master cover Checked here:
Merging. Title trimmed - it still advertised the Virtuoso support you parked. The |
Implements light color support as agreed in #572.
Changes made
Following your answers in #572, point by point:
CAP_LIGHT_COLOR, CLI--light-color RRGGBBwith a leading#accepted, appended at the end ofCAPABILITIES_XLISTand ofCAPABILITY_DESCRIPTORS.HSC_CAP_LIGHT_COLOR = 24is mirrored inhsc_capability_t, and master's per-capabilitystatic_asserts from 97e74b5 check it, so the version of those asserts this PR used to carry has been dropped.LightColorSettings { uint8_t r, g, b; }inFeatureParam;HIDDevice::setLightColor(hid_device*, const LightColorSettings&); descriptor min/maxnullopt, value hint<RRGGBB>. C API ishsc_set_light_color(h, r, g, b), C++ isHeadset::setLightColor(). The int-vs-struct rule is written down inADDING_A_CAPABILITY.md.000000is valid.LightsResult.modeas is.-lwith--light-color: the color request is last in the request table, so it runs after-land wins. Documented in--help, the README usage section, and at the request table.Output: actions carry
color: "#rrggbb"in JSON, YAML and ENV, andvaluepacks0xRRGGBB. Thevalue > 0guard wasn't only inoutput_data.hpp— YAML and ENV each had their own inline copy, so fixing the one line would still have lost black in two formats. The rule now lives once inActionData::hasValue(). It still omits0for other actions, since the equalizer returnssuccess(0)to mean "no value";-l 0output is unchanged.api_versiongoes to 1.9: the capability list grows by one, and actions gain thecolorfield.Also: test device implements it;
ADDING_A_CAPABILITY.mdsays new capabilities append only and must be mirrored in the C header;LIBRARY_USAGE.md,ADDING_A_DEVICE.mdandAGENTS.mdlist the capability.AGENTS.md's list had also fallen behind, so it now includes sidetone status, the ANC family, the two microphone statuses and headset volume; README table regenerated with--readme-helper.Corsair Virtuoso XT/SE
Split off as you asked. This PR is now only the plumbing plus the test device, and no hardware advertises
CAP_LIGHT_COLORyet. The Virtuoso implementation is parked ongdevenyi:virtuoso-light-coloruntil there's a hold or a hardware-mode store. When it comes back, brightness will only be raised if it reads 0.Testing
clang-format18 clean over every changed file.ActionData's value rule; the C and C++ API through the test device; CLI output in every format, including black and-lordering. RevertinghasValue()tovalue > 0fails theActionDatatest, and doing so only in the YAML writer fails the CLI test.Checklist
🤖 Generated with Claude Code