Keyboard: SteelSeries Apex support (per-key RGB, OLED, 24 scenes) - #14
Merged
Conversation
Adds a Keyboard tab: per-key RGB with host-rendered effects, the 128x40
OLED, brightness and the settings the firmware exposes, for the Apex Pro,
7, 5 and 9 families across all three protocol generations.
Most of the protocol was measured on an Apex Pro TKL Wireless (2023)
(1038:1632, firmware 3.24.1) rather than taken on trust, because the
existing sources disagree and one of them is wrong for this device. Its
vendor interface declares three reports and probing confirmed each:
- Configuration is a 64-byte output report, written as 65 bytes with a
leading 0x00 report id. Zone colour, brightness, apply and the
queries all work this way.
- Query replies arrive one command behind, so the input queue has to be
drained before an answer can be trusted. 0x90 answers with bare ASCII
("3.24.1"); 0x92 answers 92 95, which is 95% as two BCD digits - the
same number the keyboard's own battery indicator was showing.
- Bulk payloads are 641-byte feature reports with the command byte
FIRST. Prefixing a 0x00 report id instead - what OpenRGB and hidapi's
convention produce - makes this device stall the control transfer,
and 640, 644 or 64 bytes are refused outright. Enough stalls in a row
make the keyboard re-enumerate, which is how this was found.
Per-key lighting is proven, not inferred: lighting exactly four scattered
keys (A, N, R, I) and leaving the other 108 dark came out on the board
exactly that way, which no zone-based control could produce. The OLED is
proven the same way - a full-panel test picture came back on the display.
Its transport is eight 641-byte chunks under command 0x0C carrying
row-major pixels; apex-tux and OmniLED use other combinations for other
models, so all of them are kept and the Display tab can send a test
picture with each until one lands.
Two behaviours are worth knowing because they look like bugs:
- Direct mode is sticky. The board holds the last frame indefinitely,
so the LEDs are handed back explicitly on quit and when the master
toggle goes off.
- The firmware composites its profile and battery over the top of the
panel whatever is sent, so every screen starts 10 px down, and it
repaints on its own events, so frames are re-sent twice a second.
Its screensaver never fires while Inari is pushing, so the blanking
timer lives on this side.
What does not work is documented as not working rather than shipped as if
it did: the actuation command 0x2D is accepted by the hardware and
changes nothing on firmware 3.24.1, and Rapid Trigger, Rapid Tap/SOCD and
Protection Mode have never been captured by anyone. The tab says so, and
carries a raw command probe so someone with hardware can help find them.
Also fixes a latent test bug this surfaced: the hidraw poll test built
its pipe without O_CLOEXEC, so the new tests that shell out (nvidia-smi,
playerctl) inherited its write end and kept the pipe alive past the close
the test was measuring. It failed only alongside those tests, which is
exactly the kind of failure that gets written off as flaky.
Framebuffer grows a runtime height so one set of drawing primitives
serves both the base station's 128x64 and the keyboard's 128x40.
432 Rust tests, 161 frontend tests, clippy -D warnings clean.
The effect list was the one every vendor ships - static, wave, breathing - which is a waste of a board that can address 112 keys individually. Adds a Scenes effect backed by twelve of them, each a small piece of shader-style maths over the key's normalised position and a phase: Tokyo Night, Osaka Neon, Sakura, Kanagawa, Foxfire, Rain, Fuji Sunrise, Ripple, Aurora, Lantern, Amanogawa, Slash. They are written as maths over (x, y, phase) rather than as per-layout frames, so the same scene animates correctly across a full-size board, a TKL and a mini without a table per model. Everything is pure and deterministic - same inputs, same colour - which is what lets the UI preview a scene with no keyboard attached and what makes them testable at all. The per-key randomness is a hash of the key rather than a PRNG stream, because a key that re-rolls its "random" brightness every frame strobes instead of twinkling. The details that make them read as scenes rather than as gradients are deliberate: Osaka's neon has hard dropouts because a constant shimmer just looks like noise; Kanagawa's crest curls with y because a straight front is not a wave; the blade in Slash has a gap between strikes, without which it is a wave with extra steps; and Sakura's petals sway, or they fall like rain. The key map was too small and too flat to show any of it. It now measures its container and scales the board to fill it, and the caps are drawn as dark plastic lit from underneath - glow scaled by how lit the key actually is, a bevel, a highlight - instead of as flat swatches. That is both what the hardware looks like and what makes a dim scene readable at a glance. 442 Rust tests, 161 frontend tests, clippy -D warnings clean.
…e found Doubles the scene list to 24: Hanabi, Torii, Shibuya, Neon Rain, Glitch, Vaporwave, Koi, Komorebi, Taiko, Inferno, Plasma and Kaminari. The new ones lean on the thing a per-key board can do that a zone board cannot - structure that sits still while something moves through it. Torii draws an actual gate in board space and lights what is behind it; Shibuya crosses two streams of long thin streaks; Hanabi puts four shells in flight on their own cycles, each a thin shock front with sparks lagging behind; Kaminari builds its bolt row by row, each row offsetting the last. Several of them are deliberately dark most of the time - Kaminari is a bolt and then nothing, Taiko is a hit and then silence - and that broke two tests, which is the interesting part. Both sampled a scene at one or two instants: one caught Kaminari mid-dark and called it flat, and the other compared Taiko at 0.4 and 2.9, which land on exactly the same beat because its period divides the gap. Sampling a whole cycle and asserting the property that actually matters - it lights up and varies at some point - fixes both without weakening either. The scene picker was hidden behind the "Scenes" effect chip, so nobody found it. It is its own always-visible card now, each tile carrying a strip of its own palette so the list is scannable by eye.
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.
Adds a Keyboard tab: per-key RGB, the 128x40 OLED, brightness and the settings
the firmware exposes, for the Apex Pro, 7, 5 and 9 families across all three
protocol generations, wired or over the dongle.
Measured, not assumed
Most of the protocol was worked out on an Apex Pro TKL Wireless (2023)
(
1038:1632, firmware 3.24.1) over/dev/hidraw, because the existing sourcesdisagree and one of them is wrong for this device. Its vendor interface declares
three reports and probing confirmed each:
leading
0x00report id.drained before an answer can be trusted.
0x90answers with bare ASCII(
3.24.1);0x92answers92 95, which is 95 % as two BCD digits — the samenumber the keyboard's own battery indicator was showing.
Prefixing a
0x00report id instead — what OpenRGB and hidapi's conventionproduce — makes this device stall the control transfer, and 640, 644 or 64
bytes are refused outright. Enough stalls in a row make the keyboard
re-enumerate, which is how this was found.
Per-key lighting is proven rather than inferred: lighting exactly four scattered
keys (A, N, R, I) and leaving the other 108 dark came out on the board exactly
that way, which no zone-based control could produce. The OLED is proven the same
way — a full-panel test picture came back on the display. Its transport is eight
641-byte chunks under command
0x0Ccarrying row-major pixels.What does not work is documented as not working
The actuation command
0x2Dis accepted by the hardware and changes nothing onfirmware 3.24.1. Rapid Trigger, Rapid Tap/SOCD and Protection Mode have never
been captured by anyone — not OpenRGB, apex-tux, OmniLED or steelseriesgg-rs.
The tab says so, and carries a raw command probe so someone with hardware can
help find them.
Two behaviours that look like bugs
LEDs are handed back explicitly on quit and when the master toggle goes off.
whatever is sent, so every screen starts 10 px down; it repaints on its own
events, so frames are re-sent twice a second. Its screensaver never fires while
Inari is pushing, so the blanking timer lives on this side.
Also in here
Rain, Fuji Sunrise, Ripple, Aurora, Lantern, Amanogawa, Slash, Hanabi, Torii,
Shibuya, Neon Rain, Glitch, Vaporwave, Koi, Komorebi, Taiko, Inferno, Plasma,
Kaminari — written as maths over (x, y, phase) so one scene animates correctly
across full-size, TKL and mini boards.
caps drawn as plastic lit from underneath.
whose packet shape is not yet confirmed, and a host-side screensaver.
without
O_CLOEXEC, so the new tests that shell out inherited its write endand kept the pipe alive past the close the test was measuring. It failed only
alongside those tests — the sort of failure that gets written off as flaky.
442 Rust tests, 161 frontend tests,
clippy -D warningsclean.