feat(keychron): drive every Launcher "8k" and "1k" mouse - #148
Merged
snekxs merged 4 commits intoSep 30, 2026
Merged
Conversation
Keychron Launcher picks a mouse's protocol from the HID collection it exposes: 0xffc1 is the "8k" protocol the M6 speaks, usage page 0x8c is the "1k" one, 0xff0a is the 4K family. Both "8k" and "1k" are now driven fully, decoded from Launcher (main.be11320b2a72b61b.js, webpack modules 20706, 61892, 75994 and 8596). - The M6 driver becomes Keychron8kHidClient (mouse-8k-hid.ts) and claims any Keychron device with the 0xffc1 interface. It adds Launcher's feature flags (status bytes 26/53/60, or the 0x02 answer from protocol 6): the mouse's own DPI ceiling and step, rewritable polling gears, 20K FPS, X/Y DPI through 0x48/0x49, split USB and 2.4 GHz polling through 0x4a/0x4b, and lift-off levels. Button remapping (0x61/0x62 read, 0x52 write) and lighting (0x23/0x24) are new; behind a receiver the 0x03 list names the paired mouse. - Keychron1kHidClient (mouse-1k-hid.ts) speaks the same command set in feature reports 0x51 and 0x52, with no sleep, profile or angle commands, a fixed 125/500/1000 Hz table and Back/Forward codes swapped. - launcher-mouse.ts holds what both share, and KEYCHRON_LAUNCHER_MICE lists 54 models from Launcher's per-model configs and product list: DPI range, polling ceiling, lift-off heights, buttons and lighting. - The picker filters on vendor and collection instead of the M6's two product IDs, and the registry checks 0xffc1, then 0x8c, then 0xff0a, as Launcher does. Only the M6 has been on hardware. Its verified paths keep their bytes; buttons, lighting, the flagged paths and the 1k driver still need an owner test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "1k" battery sits in the 0x06 identity answer, which the driver read once per connection, so the percentage never moved after connecting. Launcher re-reads 0x06 with every status read; so does this now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # src/drivers/registry.test.ts # src/drivers/registry.ts
|
🎉 This PR is included in version 0.22.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
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
Keychron Launcher picks a mouse's protocol from the HID collection it exposes, not from the model:
0xffc1is its "8k" protocol (the one the M6 speaks), usage page0x8cis its "1k" protocol, and0xff0ais the 4K family. This PR drives both "8k" and "1k" fully, which covers every remaining Keychron mouse Launcher configures.Keychron8kHidClient(mouse-8k-hid.ts, replacesm6-hid.ts/KeychronM6HidClient) claims any Keychron device with the0xffc1interface, familykeychron-8k. On top of what the M6 driver did, it reads Launcher's feature flags (status bytes 26/53/60, or from protocol 6 the0x02answer) and acts on them:performanceMode) and angle tuning, -90 to 90 like Launcher's slider0x48/0x49(the edited stage gets X = Y, other stages keep their pairs)0x4a/0x4b0x62read,0x52write on0xb3) and lighting (0x23/0x24)0x03list names the connected mousesettingsReady: false) when the mouse stops answeringKeychron1kHidClient(mouse-1k-hid.ts, familykeychron-1k) speaks the same command set in feature reports: 20 bytes on0x51(answers fetched with a feature read of the same report, after 200 ms behind a receiver) and 64 bytes on0x52for buttons. Identity is0x06, status0x07(bytes 1-17 match the 8k status), no sleep, profile, angle or 20K FPS commands, a fixed 125/500/1000 Hz table, Launcher's 1k lighting commands (0x12/0x18read,0x22/0x23/0x27/0x28write), and Back/Forward codes swapped relative to 8k.launcher-mouse.tsholds what both share: status bytes 1-17, the0x40/0x42/0x43packets, button codes, lift-off mapping and lighting conversion.KEYCHRON_LAUNCHER_MICE(src/keychron/index.ts) lists 54 models with their DPI range, polling ceiling, lift-off heights, buttons (index and default function, which also names the button) and lighting effects.KEYCHRON_LAUNCHER_HID_FILTERSfilters on vendor and collection instead of the M6's two product IDs, and the registry checks0xffc1, then0x8c, then0xff0a, which is Launcher's order.Devices
Names come from Launcher's product API (
launcher.keychron.com/vapi/v2/product/<vpid>), limits from each model'sstatic/device/<vpid>/json/v3.json, scanned over0x3434:0xD000-0xD0FF:Left out: the six 4K models and the G3 Air (their own drivers), the Nape (trackball driver), 0xd085 (no product entry), and two v3 configs whose product IDs Launcher's product list files as receivers (0xd030 "Lemokey G2", 0xd05a "G6 HE").
Evidence
main.be11320b2a72b61b.js, webpack module 20706: every command class for both protocols (status, version, DPI, X/Y DPI, polling, split polling, sensor, angle, debounce, sleep, profile, buttons, lighting, receiver state)sendFeatureReportthenreceiveFeatureReportof the same ID, with the receiver variant waiting first)getBaseInfo's flag handlinglodLevelSupportwrites the level byte with the code at 0)createByMouse/getDeviceInfo: the collection-to-protocol map and its orderAssumptions and unknowns
0x65/0x55), the scroll motor (0x69/0x59), macros, per-button debounce, wake sources, scroll speed and the pairing-key combo are not included. Macros and keyboard assignments show as "Macro" / "Custom".registry.tsandsrc/keychron/index.ts. Once both are in, the G3 Air's product IDs should also be excluded fromKeychron8kHidClient.isSupported(one line), because the registry overlap test's all-collections probe would otherwise match both drivers for 0xd077. I will rebase whichever lands second.Tested
npm run buildandnpm test: 1885 tests pass, including the registry probe matrix (usage page0x8cand report0x51added) and the overlap checknpm run check(tsc, vite build, 241 tests) with this build linked, and the dev server with simulated mice behind a stubbednavigator.hid:0xffc1connected as "Keychron M3" and showed the stage editor, Low/High lift-off, 125-1000 Hz, the processing toggles with 20K FPS and angle tune, lighting, the 8-button remapper, sleep and profiles. High lift-off + 500 Hz + 20K FPS, Breathing single at 50%, and Forward to DPI Loop each sent the expected packet and read back.0x8cconnected as "Keychron M3 KM" with no profile or sleep card; 1000 Hz and motion sync went out as41 02 02 00 01 02and42 01 02 01 01 00 01on feature report0x51.Not tested on real hardware beyond the M6's existing paths.
CI note: the
buildjob currently fails atnpm audit --audit-level=high, on new advisories for the undici, ip-address and brace-expansion copies bundled insidenpm@11.19.1(a transitive dev dependency in the lockfile). This PR does not touchpackage.jsonor the lockfile, and #145 passed the same step yesterday, so main will fail it too until the lockfile moves to a patchednpm.Related