Skip to content

feat(keychron): support the G3 Air on Launcher's 8K Nordic protocol - #145

Merged
snekxs merged 1 commit into
OpenMouse-Project:mainfrom
ydw1904:feat/keychron-g3-air
Sep 30, 2026
Merged

snekxs merged 1 commit into
OpenMouse-Project:mainfrom
ydw1904:feat/keychron-g3-air

Conversation

@ydw1904

@ydw1904 ydw1904 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Keychron8kNordicHidClient (src/drivers/keychron/mouse-8k-nordic-hid.ts) for the Keychron G3 Air, which Keychron Launcher drives with its "8k_nordic" protocol. It shares the 4K family's 0xff0a collection and framing (64-byte report 0, 0xa1 - sum checksum, 0x40 on byte 0 routes through the receiver) and uses Orbital's DMS v2 settings blocks, with Keychron's polling gear table added to the system block.

Read and write:

  • DPI stages: five, 100-30000 in steps of 50, active stage and stage count. Each stage keeps its X/Y pair; the edited stage gets X = Y
  • polling 125 Hz to 8000 Hz through the gear table, with the separate 2.4 GHz gears from firmware 1.6.0
  • lift-off 0.7 / 1 / 2 mm as Low / Medium / High
  • motion sync, angle snapping, ripple control, 20K FPS mode (as performanceMode), angle tuning (-90 to 90)
  • debounce 0-20 ms, sleep 1-240 minutes
  • five onboard profiles
  • button remap for Left, Right, Middle, Back and Forward (mouse buttons, DPI loop / + / -, disabled)
  • battery and firmware

When the mouse does not answer (asleep behind the receiver), readStatus returns the name with settingsReady: false.

Devices

PID Device Source
0xd077 G3 Air, wired Launcher config sets "protocol": "8k_nordic"; Keychron's product API describes it as the 54L protocol
0xd05b Ultra-Link 8K receiver product API, chip listed as 54LM20A
0xd078 Ultra-Link 8K receiver product API, listed next to the G3 Air

The receiver PIDs are the weakest part: Launcher never names them, it recognises the protocol only by the handshake. So the driver sends that handshake unrouted first and refuses to go on unless bytes 6-7 carry Keychron's (0x3434) or Lemokey's (0x362d) vendor ID; bytes 8-9 then name the paired mouse. Keychron4kHidClient now leaves these three ids alone, and its 8K Nordic error includes the product ID so an unknown receiver can be reported and added.

Evidence

  • Launcher main.be11320b2a72b61b.js, module 20706: the 8k_nordic command classes (sensor and system blocks, buttons, profile switch, save, factory reset, version, receiver status)
  • module 45710: the 4-byte button codes those commands use (mouse buttons 01 00 f0-f4 00, the same table the GearHub driver confirmed on hardware)
  • the gear logic in Launcher's own assistant (resolvePollingRateLevel) and the Us gear sorter
  • the lazy settings chunks for ranges: debounce 0-20, sleep 1-240 minutes stored as seconds, angle -90 to 90, DPI step 50
  • launcher.keychron.com/static/device/875876471/json/v3.json: DPI 100-30000, rates up to 8000, LOD indexes 0/1/2 = 0.7/1/2 mm, buttons at indexes 0-4

Assumptions and unknowns

  • no capture exists; the tests run against a fake that implements Launcher's byte layout
  • status byte 4 (link state) is read by Launcher's live monitor but not used here
  • quick response, wheel reverse, wake sources and the sensor bytes Launcher never edits (full speed, lift-down, glass) are written back as read. No setters, because the panel has no controls for them
  • Launcher's own sensor writes collapse a separate Y DPI into X; this driver keeps each stage's pair instead
  • macros and keyboard or media assignments show as "Macro" / "Custom"

Tested

  • npm run check passes: 1862 tests, including the registry overlap matrix
  • OpenMouse npm run check (241 tests) with this build linked locally, and the dev server with a simulated G3 Air behind a stubbed navigator.hid: it connected as "Keychron G3 Air" (wired, 1000 Hz, 76%) with every card rendered, and applying 8000 Hz, High lift-off, angle snapping, Forward to DPI Loop, 4 ms debounce, 30 minute sleep and profile 4 sent the expected packets and read back

Not tested on a real G3 Air yet.

Related

The G3 Air's Launcher config (static/device/875876471/json/v3.json)
forces the "8k_nordic" protocol: the 4K family's 0xff0a collection and
framing with Orbital's DMS v2 settings blocks. Decoded from Keychron
Launcher (main.be11320b2a72b61b.js, webpack module 20706, plus the
lazy settings chunks for ranges):

- sensor block 04/81/01, written as 04/bc/02: processing toggles,
  20K FPS mode, lift-off (0.7, 1 and 2 mm), a signed angle, five X/Y
  DPI stages and their colours
- system block 04/83/03, written as 04/98/04: sleep in seconds,
  debounce, and a six-gear polling table whose active byte is a gear
  index. From firmware 1.6.0 a second gear set for 2.4 GHz follows and
  the write grows to 04/a0/04
- buttons 03/81/01 and 03/85/04 with 4-byte codes (01 00 f0-f4 00 for
  mouse buttons, 07 00 0x 00 for DPI), profiles 02/82/02, and a save
  (0a/81/01) after every write, as Launcher does

A polling change picks the gear that already holds the rate, else puts
the rate in the active gear, which is what Launcher's assistant does.
Fields Launcher never edits (full speed, lift-down, glass, quick
response, wake sources, wheel reverse) are written back as read.

The driver claims the wired G3 Air (0xd077) and the Ultra-Link 8K
receivers 0xd05b and 0xd078 from Keychron's product list. A receiver
has to answer the unrouted handshake with Keychron's or Lemokey's
vendor ID in bytes 6-7 (Launcher's test for this protocol) before
anything is written; bytes 8-9 name the paired mouse. When the mouse
does not answer, readStatus returns the name alone. The 4K driver now
leaves these ids alone and names the product ID when it meets an
8K Nordic receiver it does not know.

Not yet tested on hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.21.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants