feat: add SteelSeries Arctis 9 support - #58
Open
spinside wants to merge 1 commit into
Open
Conversation
Add device config and CoreEngine fixes to support the Arctis 9, which uses
the legacy 31-byte SteelSeries HID protocol (single USB interface 0, no OUT
endpoint) rather than the 64-byte Nova protocol.
New device config (arctis_9.yaml):
- Battery, charging status, ChatMix (read)
- Sidetone (read/write) with exponential curve matching HeadsetControl
- Auto-shutoff / inactive time (write, 1-90 min, 16-bit big-endian seconds)
- Notification sound (write, on/off)
- Save-to-flash after every write so settings persist across power cycles
CoreEngine (core.py):
- Detach usbhid on interface 0 when the device uses interface 0 as its
control interface (command_interface_index[0] == 0). Nova devices use a
vendor interface and are unaffected (no-op).
- Send optional post_update_sequence after the main write to persist settings.
Config engine (config.py):
- value_multiplier: scale the slider value before encoding (minutes -> seconds).
- 'value_hi' / 'value_lo' tokens: emit big-endian multi-byte payloads.
- value_transform: 'log2_sidetone' for the Arctis 9 exponential sidetone curve.
- post_update_sequence: optional second HID command after the main write.
Docs:
- device_configuration_file_specs.md: document the new settings fields.
- device_support.md: note on the Arctis 9 legacy protocol and how it was
reverse-engineered from HeadsetControl (no Wireshark capture required).
The protocol byte layout was cross-checked against HeadsetControl's
steelseries_arctis_9.hpp (GPL-3.0) and confirmed with a pyusb probe on Linux.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds support for the SteelSeries Arctis 9 (USB IDs
1038:12c2/1038:12c4), which uses the legacy 31-byte SteelSeries HID protocol — a single USB interface 0 with no OUT endpoint — rather than the 64-byte Nova protocol used by the currently-supported devices.This is my first contribution to this project. The device config and code changes were developed and verified on a real Arctis 9 on Arch Linux.
What's included
New device config:
arctis_9.yamlminutes×6016-bit big-endian secondsAll writes are followed by a
[0x90, 0x00]save command so settings persist to the headset's onboard memory.CoreEngine fixes (
core.py)CoreEngineskipped interface 0 entirely (correct for Nova devices, which use a vendor interface). It now detachesusbhidon interface 0 whencommand_interface_index[0] == 0x00. This is a no-op for Nova configs (the condition is false).post_update_sequence. After sending the mainupdate_sequence, the engine sends an optional second command to persist settings to flash.Config engine extensions (
config.py)All optional, all default to existing single-byte behavior — no existing device config is affected:
value_multiplier— scales the slider value before encoding (e.g. minutes → seconds).'value_hi'/'value_lo'tokens — emit big-endian multi-byte payloads.value_transform: log2_sidetone— reproduces the Arctis 9's exponential sidetone curve.post_update_sequence— optional save-to-flash command after the main write.Docs
device_configuration_file_specs.md— documents the four new settings fields with a worked example.device_support.md— a note on the Arctis 9's legacy protocol and how it differs from Nova devices.How it was reverse-engineered
I did not use a Windows Wireshark capture. The protocol was cross-checked against the HeadsetControl project's
steelseries_arctis_9.hpp(GPL-3.0), which documents the status request byte0x20, the0xaaresponse prefix, and the byte indices for battery/charging/sidetone/chatmix. I confirmed the live byte layout with a small pyusb probe on Linux (send0x20viaSET_REPORTon interface 0, read EP 0x81) and verified each writable setting by reading status back before/after.How it was tested
All testing was done on Arch Linux with a physical SteelSeries Arctis 9 (
1038:12c2) connected via its 2.4GHz wireless dongle, with thelinux-arctis-managerAUR package (v2.4.1) as the base.USB / HID layer
lsusbconfirms the device enumerates as1038:12c2+1038:12c4.SET_REPORTpath (interface 0,wIndex=0, 31-byte padded payload) and returns the status frameaa 01 01 94 00 00 00 00 <sidetone> <game> <chat> ….Daemon
CoreEngine: Error sending command: [Errno 16] Resource busyon everySET_REPORT(13000+ log lines), becauseusbhidheld interface 0.Kernel driver active on interface 0, detaching… / Claimed interface 0, zeroResource busyerrors.Arctis_Media/Arctis_Chatcreated successfully.D-Bus interface (end-to-end via
gdbus)GetStatusreturns: battery 88% (matchesheadsetcontrol -b), charging off, chatmix centered, sidetone reflecting the last write.SetSetting mic_side_tone <v>: slider 0 → 0% (off, byte 0xc0), 8 → 45%, 32 → 100%, 128 → 100% (byte 0xfd). Matches HeadsetControl'slog2mapping exactly.SetSetting inactive_time <v>: 15 / 60 / 90 all returntrue, stored value persists, zero USB errors.SetSetting notification_sound 0/1: both returntrue; an audible beep is heard on toggle (verified by ear).Cross-check
headsetcontrol -breports the same battery percentage as LAM'sGetStatus(88% at the time of testing), confirming the byte-index mapping is correct.What's NOT included (and why)
Based on the official Arctis 9 user manual and SteelSeries Engine 3 documentation:
0x06 0x35command is silently ignored. Not implemented.0x06 0x55command has no effect. Not implemented.0x06 0x37had no effect on the headset. Not implemented.0seconds means immediate power-off on the Arctis 9 (not "never"). HeadsetControl caps the field at 255 min and offers no disable. No known "never" sentinel; I didn't want to probe blind. The slider minimum is 1 minute.Honesty note
The device config and code changes in this PR were developed with the assistance of an AI coding agent (Claude, via the Pi harness). I directed the work, verified every feature physically on my hardware, and wrote this PR description myself. The protocol bytes come from HeadSetControl's GPL-3.0 source and my own USB probing, not from AI guesswork. I've noted above which features I could not verify and deliberately left out.
Checklist
headsetcontrollam-cli udev write-rulespicks up the new PID automatically (generated from device configs at runtime)Closes the gap for Arctis 9 owners who currently have no Linux management tool beyond the CLI-only HeadsetControl.