Skip to content

feat: add SteelSeries Arctis 9 support - #58

Open
spinside wants to merge 1 commit into
elegos:developfrom
spinside:feat/arctis-9-support
Open

feat: add SteelSeries Arctis 9 support#58
spinside wants to merge 1 commit into
elegos:developfrom
spinside:feat/arctis-9-support

Conversation

@spinside

Copy link
Copy Markdown

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.yaml

Feature Direction Notes
Battery level read byte 3, range 0x64–0x9a → 0–100%
Charging status read byte 4 (0x01 = charging)
ChatMix read bytes 9/10 (game/chat, 0–19 each)
Sidetone read/write byte 8, exponential curve (0xc0 off → 0xfd max)
Auto-shutoff (inactive time) write 1–90 min, encoded as minutes×60 16-bit big-endian seconds
Notification sound write on/off toggle

All writes are followed by a [0x90, 0x00] save command so settings persist to the headset's onboard memory.

CoreEngine fixes (core.py)

  1. Interface 0 kernel driver detach. The Arctis 9 uses USB interface 0 as its control interface, but CoreEngine skipped interface 0 entirely (correct for Nova devices, which use a vendor interface). It now detaches usbhid on interface 0 when command_interface_index[0] == 0x00. This is a no-op for Nova configs (the condition is false).
  2. post_update_sequence. After sending the main update_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 byte 0x20, the 0xaa response prefix, and the byte indices for battery/charging/sidetone/chatmix. I confirmed the live byte layout with a small pyusb probe on Linux (send 0x20 via SET_REPORT on 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 the linux-arctis-manager AUR package (v2.4.1) as the base.

USB / HID layer

  • lsusb confirms the device enumerates as 1038:12c2 + 1038:12c4.
  • A pyusb probe reproduces LAM's exact SET_REPORT path (interface 0, wIndex=0, 31-byte padded payload) and returns the status frame aa 01 01 94 00 00 00 00 <sidetone> <game> <chat> ….

Daemon

  • Before the kernel-detach fix: CoreEngine: Error sending command: [Errno 16] Resource busy on every SET_REPORT (13000+ log lines), because usbhid held interface 0.
  • After the fix: Kernel driver active on interface 0, detaching… / Claimed interface 0, zero Resource busy errors.
  • PulseAudio virtual sinks Arctis_Media / Arctis_Chat created successfully.

D-Bus interface (end-to-end via gdbus)

  • GetStatus returns: battery 88% (matches headsetcontrol -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's log2 mapping exactly.
  • SetSetting inactive_time <v>: 15 / 60 / 90 all return true, stored value persists, zero USB errors.
  • SetSetting notification_sound 0 / 1: both return true; an audible beep is heard on toggle (verified by ear).

Cross-check

  • headsetcontrol -b reports the same battery percentage as LAM's GetStatus (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:

  • Equalizer (10-band, persists to headset) — the one genuinely-missing feature. Requires a Windows Wireshark capture of SteelSeries Engine writing the EQ; I don't have that capture. The HID command bytes are unknown.
  • DTS Headphone:X v2 — PC-only software DSP, not a headset setting. Out of scope.
  • LED toggle — the Arctis 9 has a single status LED (battery-level color: green/yellow/red), not a controllable earcup LED. The Arctis 7's 0x06 0x35 command is silently ignored. Not implemented.
  • Voice prompts — not a documented Arctis 9 feature; the Arctis 7's 0x06 0x55 command has no effect. Not implemented.
  • Mic volume — Engine exposes a slider, but it controls the Windows audio mixer level, not a device HID command. Writing 0x06 0x37 had no effect on the headset. Not implemented.
  • "Never" auto-shutoff0 seconds 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

  • Device config follows the specs
  • New config fields are optional and backward-compatible
  • Tested on real hardware (Arctis 9, Arch Linux, LAM v2.4.1)
  • Battery reading cross-checked against headsetcontrol
  • Docs updated for the new config features
  • Equalizer support (needs Wireshark capture — left for a follow-up)
  • lam-cli udev write-rules picks 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.

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.
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.

1 participant