Skip to content

Add SteelSeries Arctis Nova Pro Omni (battery + sidetone) - #584

Open
marctew wants to merge 1 commit into
Sapd:masterfrom
marctew:add-nova-pro-omni
Open

marctew wants to merge 1 commit into
Sapd:masterfrom
marctew:add-nova-pro-omni

Conversation

@marctew

@marctew marctew commented Sep 19, 2026

Copy link
Copy Markdown

Adds support for the SteelSeries Arctis Nova Pro Omni GameHub (VID 0x1038, PID 0x2290).

Capabilities

  • Battery status — 01 B0 status poll; reply[6] = headset battery %, reply[15] = status (0x01 offline, 0x02 cable charging, 0x08 online).
  • Sidetone — 01 38 <enable> <level>, level 0–10. Write-only: the hub exposes no readable sidetone state (verified against the status poll, the config feature report, and the Nova 7's 0x20 query), so no getSidetone.

Notes for reviewers

The Omni's GameHub is a close cousin of the Nova Pro Wireless base station, but its vendor HID interface differs in two ways, which is why it's a new device rather than an added PID:

  • Commands use report ID 0x01 (not 0x06) and 64-byte reports (not 31).
  • Interface 3 exposes two top-level collections; the command/response channel is the one on usage page 0xFFC0, report ID 1, selected via getCapabilityDetail (same pattern as the Nova 7 Gen 2).

It extends SteelSeriesNovaDevice (64-byte packets + usage-page selection) with battery/sidetone overrides for the Omni's report-ID and status-byte layout.

Testing

Built on Windows (MSYS2/MinGW, GCC 16) and tested against real hardware (firmware rev 0x0132):

  • headsetcontrol --list detects the device.
  • headsetcontrol -b returns a correct battery level (verified 93%, matching the OLED).
  • headsetcontrol -s 0/128 toggles sidetone audibly.

Reverse-engineered by USB capture (USBPcap) of SteelSeries GG traffic plus replay verification. The 1035-byte HID feature report (hardware EQ/preset store) is never written.

Adds the Nova Pro Omni GameHub (VID 0x1038, PID 0x2290).

The Omni's GameHub is a close cousin of the Nova Pro Wireless but its vendor
HID interface differs: commands use report ID 0x01 and 64-byte reports, and
interface 3 exposes two top-level collections, so the command/response channel
(usage page 0xFFC0, report ID 1) must be selected explicitly.

Verified empirically on firmware rev 0x0132:
- Battery: 01 B0 -> reply[6] battery %, reply[15] status (01 offline / 02
  charging / 08 online).
- Sidetone: 01 38 <enable> <level>, level 0-10. Write-only (no readable
  sidetone state exists on this hub), so no getSidetone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@Sapd Sapd left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good writeup in the header - the two-collection detail and why it's a new device rather than a PID are exactly what the next person needs.

Two things:

make format - clang-format 18 wants steelseries_arctis_nova_pro_omni.hpp before ..._wireless.hpp in device_registry.cpp. CI will fail on it.

The battery comment doesn't match the code. Header and inline both say the fuel gauge caps at 99 and you present that as full, but the code is if (level > 100) level = 100, which never maps 99 to 100. Either the comment is wrong or users never see 100%. Which is it on your hardware?

Otherwise builds clean here, tests pass, README matches --readme-helper.

@github-actions github-actions Bot added awaiting-author We replied - waiting on the author needs-rebase Branch conflicts with master labels Oct 20, 2026
@LDCanetti

Copy link
Copy Markdown

Tested this on Linux (CachyOS, hidraw backend) with an Omni GameHub (1038:2290). Detection and the 01 B0 reply layout work, but there are three things I hit:

1. Battery reads 0% on Linux: the hub interleaves event reports with the 01 B0 reply

While the headset is connected, the hub pushes unsolicited reports with report ID 0x07 on the same 0xFFC0 collection (e.g. 07 19 …, 07 27 …, 07 b7 …, 07 45 …). getBattery() takes whatever arrives first, so it parses an event as the status reply and reports Level: 0%. A raw capture on my unit:

07 19 00 00 ...
07 27 00 00 ...
01 b0 00 00 04 01 30 3c 08 01 00 0a 05 00 08 08 03 ...   <- the actual reply, 3rd report

Skipping anything that isn't 01 B0 fixes it (3/3 reads then match the raw value):

// The hub interleaves unsolicited event reports (report ID 0x07) on
// the same collection, so skip anything that isn't the 01 B0 reply.
std::vector<uint8_t> response(STATUS_BUF_SIZE);
bool got_status = false;
for (int attempt = 0; attempt < 8 && !got_status; attempt++) {
    auto read_result = this->readHIDTimeout(device_handle, response, hsc_device_timeout);
    if (!read_result) {
        return read_result.error();
    }
    got_status = *read_result >= 16 && response[0] == 0x01 && response[1] == 0xb0;
}
if (!got_status) {
    return DeviceError::protocolError("No status reply from hub");
}

2. Sidetone is readable

After writing 01 20 (64-byte output report), a HIDIOCGFEATURE read of report 1 (1037-byte buffer) returns a settings block that begins at the command byte (20 …). At offset 141 from the start of that buffer, +5 is the sidetone level (1–10) and +28 is the sidetone on/off state. I confirmed this by writing 01 38 01 07 and reading back level 7, then writing 01 38 01 05 and reading back 5. So getSidetone / CAP_SIDETONE_STATUS look possible. This is a read only, with no writes to the feature report.

3. reply[15] may not be the connection state

I think reply[14] is the 2.4 GHz link state (0x08 = paired and connected) and reply[15] is charging state. On my unit both read 0x08 while connected and not on the cable, so I can't tell them apart from this one sample. It's worth checking by powering the headset off and putting it on the charger before relying on [15] for offline detection.

Thanks for the PR, the two-collection notes saved a lot of time.

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

Labels

awaiting-author We replied - waiting on the author needs-rebase Branch conflicts with master

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants