Skip to content

Add sidetone status reading for Arctis Nova 7 Gen 2 - #560

Merged
Sapd merged 5 commits into
Sapd:masterfrom
flexusjan:feature/nova7-gen2-sidetone-status
Aug 18, 2026
Merged

Add sidetone status reading for Arctis Nova 7 Gen 2#560
Sapd merged 5 commits into
Sapd:masterfrom
flexusjan:feature/nova7-gen2-sidetone-status

Conversation

@flexusjan

@flexusjan flexusjan commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Changes made

Adds a generic read-only sidetone status capability and implements it for the verified SteelSeries Arctis Nova 7 Gen 2 (1038:227e).

Device protocol

  • Sends the 64-byte Nova settings request beginning with 00 20.
  • Accepts settings responses beginning with 20 and reads sidetone from byte 2.
  • Maps native Off/Low/Medium/High values 0/1/2/3 to HeadsetControl values 0/43/85/128 using an explicit table.
  • Validates response length, response type, and native value range.
  • Skips known asynchronous Nova status reports (b0) while waiting for the settings response.
  • Restricts sidetone reading to PID 0x227e; other Nova 7 PIDs do not advertise the capability.
  • Sends the confirmed 00 09 persistence command after setting sidetone only for PID 0x227e.

The protocol was captured from SteelSeries GG on Windows and is independently corroborated by the proposed Linux SteelSeries driver implementation:
https://patchew.org/linux/20260227235042.410062-1-srimanachanta%40gmail.com/20260227235042.410062-15-srimanachanta%40gmail.com/

Public interfaces

  • New internal query capability: CAP_SIDETONE_STATUS
  • CLI: headsetcontrol -s / headsetcontrol --sidetone queries; providing LEVEL continues to set sidetone
  • C++: Headset::getSidetone()
  • C: hsc_get_sidetone()
  • Text, short, JSON, YAML, and ENV output support
  • CLI output API version increased from 1.4 to 1.5 for the additive structured-output field

CLI examples:

headsetcontrol -s             # query sidetone
headsetcontrol -s 43          # set sidetone
headsetcontrol --sidetone     # query sidetone
headsetcontrol --sidetone 43  # set sidetone

Example text output:

Sidetone: Low (43; device level 1)

Example JSON field:

"sidetone": {
  "level": 43,
  "device_level": 1,
  "name": "Low"
}

Tests

  • Unit tests cover all four native levels.
  • Protocol tests cover short responses, unexpected response types, invalid values, asynchronous status reports, HID write/read errors, and timeouts.
  • Tests verify the Gen 2 save command and ensure other Nova 7 PIDs neither save nor advertise the read capability.
  • C++, C API, CLI, and structured-output tests cover the new public interfaces.
  • CLI tests cover short/long query and setter forms and verify that the separate --sidetone-status option is not exposed.
  • cmake --build build --parallel: passed
  • cmake --build build --target check: 2/2 tests passed
  • clang-format 18.1.8 dry-run: passed
  • git diff --check: passed

Real hardware acceptance test on 1038:227e:

SteelSeries GG: Low
HeadsetControl: Sidetone: Low (43; device level 1)

The optional-value CLI behavior was also verified on the same device with both -s and --sidetone.

Closes #559

Checklist

@Sapd

Sapd commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Thanks looks good so far, will do a more detailed review.

However im not sure if it should be --sidetone-status or just the existing -s giving a possible hint also in the CLI.

@flexusjan

Copy link
Copy Markdown
Contributor Author

Thanks! Reusing the existing -s / --sidetone option sounds better to me as well.

My preferred CLI behavior would be:

headsetcontrol -s 43          # set sidetone
headsetcontrol -s             # query sidetone
headsetcontrol --sidetone 43  # set sidetone
headsetcontrol --sidetone     # query sidetone

That keeps the existing setter syntax compatible and avoids adding a separate public --sidetone-status option. The help could describe it as -s, --sidetone [LEVEL] with “Get the current sidetone level, or set it to LEVEL (0-128)”.

Internally I would keep the read capability separate, since some devices can set sidetone but cannot query it. Calling -s without a value on such a device would then return a clear “reading sidetone is not supported” error.

I can update the PR accordingly after your detailed review, or right away if this is the direction you prefer.

@Sapd

Sapd commented Aug 14, 2026

Copy link
Copy Markdown
Owner

That keeps the existing setter syntax compatible and avoids adding a separate public --sidetone-status option. The help could describe it as -s, --sidetone [LEVEL] with “Get the current sidetone level, or set it to LEVEL (0-128)”.

Yes

Internally I would keep the read capability separate, since some devices can set sidetone but cannot query it. Calling -s without a value on such a device would then return a clear “reading sidetone is not supported” error.

Yes, sounds good keeping it separate.

Feel free to implement, I will review next week.

@flexusjan

Copy link
Copy Markdown
Contributor Author

Implemented in 2144ea9. The CLI now uses -s / --sidetone without a value to query, while the existing forms with a value continue to set sidetone. I added regression coverage for all four short/long query/set forms, updated help and docs, and verified the behavior on the real 1038:227e device. Build, 2/2 tests, clang-format 18, and whitespace checks pass locally.

Resolves the API_VERSION comment conflict with Sapd#549/Sapd#550 (both already
merged), which independently landed on the same "1.5" version string.
Extended the comment to also cover this PR's additive sidetone field.
@Sapd

Sapd commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Merged current master (af0ba47) to clear the CONFLICTING state — #549 and #550 both landed since this branched, and independently bumped API_VERSION to the same "1.5" you did here, so the only clash was over the explanatory comment text, not the version number itself. Extended that comment to also cover this PR's additive sidetone field. Rebuilt and ran the full suite (2/2, including all sidetone tests) on the merged branch, no issues. Shows as MERGEABLE now.

Sapd added 2 commits August 18, 2026 18:54
…heck

getSidetone() checked bytes_read < 4 before checking for an
asynchronous 0xb0 status report, so a short 0xb0 report was treated as
a fatal protocol error instead of being skipped like every other one.
Move the 0xb0 check first - reading response[0] is safe once
bytes_read > 0, since the buffer is zero-initialized and the read
fills from index 0.
CAP_SIDETONE's help entry hardcoded "[LEVEL]" instead of calling
getValueHint(CAP_SIDETONE) like its sibling entries, so the
descriptor's own value_hint ("<0-128>") was stale and unused. Updated
the descriptor to "[LEVEL]" to match the option's actual optional-get-
or-set syntax, and pull it via getValueHint() at the call site.
@Sapd

Sapd commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Pushed two more fixes from the review:

1ca9872 — getSidetone() checked the response length before checking for an async 0xb0 status report, so a short 0xb0 report was treated as a fatal protocol error instead of being skipped like every other one. Swapped the order and added a regression test (confirmed it fails without the fix, passes with it).

0f046a5 — the -s help entry hardcoded "[LEVEL]" instead of pulling from getCapabilityDescriptor(CAP_SIDETONE) like its sibling options do. Updated the descriptor's value_hint to "[LEVEL]" (it was still the stale "<0-128>" from before this PR) and wired the help entry to getValueHint(). Same rendered output, just no longer a stale, unused source of truth.

Full suite still green (2/2, all sidetone tests included) on top of the master merge from earlier.

@Sapd
Sapd merged commit 3019ba3 into Sapd:master Aug 18, 2026
5 of 6 checks passed
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.

Read sidetone status on SteelSeries Arctis Nova 7 Gen 2

2 participants