Skip to content

Gen 3 keyboards: stop sending the lighting opcode that silences their keys - #16

Merged
fbnlrz merged 3 commits into
mainfrom
dev/1.0.14
Aug 5, 2026
Merged

Gen 3 keyboards: stop sending the lighting opcode that silences their keys#16
fbnlrz merged 3 commits into
mainfrom
dev/1.0.14

Conversation

@fbnlrz

@fbnlrz fbnlrz commented Aug 5, 2026

Copy link
Copy Markdown
Owner

An Apex Pro Gen 3 (1038:1640, firmware 4.15.3) is the first wired Gen 3
board to be measured rather than inherited from OpenRGB and apex-tux, and
it refutes one of the entries it was given.

The bug

0x40 is not per-key lighting on that board. It suspends key reporting for
as long as frames keep arriving — which, with an animated effect at 30 Hz,
is permanently. The keyboard stops typing while Inari runs, and the reporter
described it as "it works briefly, then goes off, then on again, in an
endless loop".

Counting real input events off the board's own /dev/input nodes while
replaying Inari's writes in phases:

sent keys/s
nothing 4.9
OLED 0x61 @ 2 Hz 4.4
direct 0x40 @ 5 Hz 0.2
direct 0x40 @ 30 Hz 0.1

It is the opcode, not the write load and not the payload: a bare 0x40
carrying an explicit zero key count and 641 zero bytes takes the board to
0.0 keys/s just as reliably, and it recovers within a tick of the frames
stopping.

The reason this shipped is worth recording. Every write is acknowledged at
a flat 1.2 ms throughout
— no EPIPE, no timeout, nothing in the log. The
board answers endpoint 0 from an interrupt while whatever 0x40 starts
starves its matrix scan, so the failure is invisible to the writer and shows
up only as a dead keyboard. Two plausible theories were checked and
discarded on the way: the kernel log proves the device never re-enumerated,
and the flat write latency rules out overloading it.

The fix

Model::direct_dialect separates "has per-key LEDs" from "Inari knows the
opcode for them" — the same distinction switch_dialect already draws for
the firmware commands, and for the same reason. direct_packet() returns
None where the dialect is a guess, the draw loop hands those boards to
their onboard profile instead, and has_per_key hides every Inari-rendered
effect in the UI rather than offering controls that would paint nothing.

Set false for 0x1640 and 0x1642 only. The OLED and the
firmware-rendered effects (Reactive, Colour shift) were unaffected on the
measured board and are left working. 0x61 was tried as a replacement: it
is harmless to key reporting but paints the wrong thing, so the wired Gen 3
per-key dialect stays unknown rather than trading one guess for another.
The likely missing piece is the start_driver_mode (0x6C) that the vendor
specification wraps around software control on these boards; that was not
tried.

Three documentation pages promised per-key RGB for these boards and have
been corrected.

Verification

475 Rust tests, 168 frontend tests, clippy -D warnings and tsc clean.

The regression test asserts at the point where the packet would be born
rather than at the writer, and it was checked by reverting the fix and
watching it go red. The fix itself was verified against the hardware: with
the built binary and the exact configuration that caused the report, the
keyboard reports 1.4 keys/s where it previously reported 0.1, and the
installed binary is hash-identical to the one that was measured.

fbnlrz added 3 commits August 1, 2026 22:02
An Apex Pro Gen 3 (1038:1640, firmware 4.15.3) is the first wired Gen 3
board to be measured rather than inherited from OpenRGB and apex-tux, and
it refutes one of the entries it was given.

0x40 is not per-key lighting there. It suspends key reporting for as long
as frames keep arriving, which with an animated effect is permanently:
the board becomes unusable as a keyboard while Inari runs. Counting real
input events off its own event nodes, per second:

  nothing sent          4.9
  OLED 0x61 @ 2 Hz      4.4
  direct 0x40 @ 5 Hz    0.2
  direct 0x40 @ 30 Hz   0.1

It is the opcode, not the load and not the payload — a bare 0x40 with a
zero key count does it just as reliably, and the board recovers within a
tick of the frames stopping. Every write is ACKed at a flat 1.2 ms
throughout, so the failure never reaches the software: the board answers
endpoint 0 from an interrupt while whatever 0x40 starts starves the
matrix scan. That is why this shipped at all.

Model::direct_dialect now separates "has per-key LEDs" from "Inari knows
the opcode for them", the same distinction switch_dialect already draws
for the firmware commands. direct_packet() returns None where the dialect
is a guess, the draw loop hands those boards to their onboard profile,
and has_per_key hides every Inari-rendered effect in the UI rather than
offering controls that would paint nothing.

Set false for 0x1640 and 0x1642 only. The OLED and the firmware-rendered
effects were unaffected on the measured board and are left alone. 0x61
was tried as a replacement: harmless to key reporting, but it paints the
wrong thing, so the wired Gen 3 dialect stays unknown rather than trading
one guess for another.

The docs claimed per-key RGB for these boards; hardware, protocols and
keyboard pages corrected.
`npm audit --audit-level=high` went red on advisories published after the
last green run, neither of them ours to fix in source: brace-expansion
(high, DoS via unbounded intermediate arrays, bypassing the CVE-2026-14257
mitigation) and postcss (moderate, arbitrary .map reads via an
attacker-controlled sourceMappingURL).

Lockfile only, both semver-compatible: brace-expansion 5.0.8 -> 5.0.9 and
postcss 8.5.19 -> 8.5.25. The root version field came along, having been
left at 1.0.8 while package.json moved to 1.0.14.

Deliberately a separate commit from the keyboard fix it unblocks, and
`npm audit fix` was run with --package-lock-only so it could not rewrite
package.json behind the change.
@fbnlrz
fbnlrz merged commit 7da4596 into main Aug 5, 2026
6 checks passed
@fbnlrz
fbnlrz deleted the dev/1.0.14 branch August 5, 2026 14:43
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