Skip to content

feat(keychron): drive every Launcher "8k" and "1k" mouse - #148

Merged
snekxs merged 4 commits into
OpenMouse-Project:mainfrom
ydw1904:feat/keychron-launcher-mice
Sep 30, 2026
Merged

snekxs merged 4 commits into
OpenMouse-Project:mainfrom
ydw1904:feat/keychron-launcher-mice

Conversation

@ydw1904

@ydw1904 ydw1904 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Keychron Launcher picks a mouse's protocol from the HID collection it exposes, not from the model: 0xffc1 is its "8k" protocol (the one the M6 speaks), usage page 0x8c is its "1k" protocol, and 0xff0a is the 4K family. This PR drives both "8k" and "1k" fully, which covers every remaining Keychron mouse Launcher configures.

  • Keychron8kHidClient (mouse-8k-hid.ts, replaces m6-hid.ts / KeychronM6HidClient) claims any Keychron device with the 0xffc1 interface, family keychron-8k. On top of what the M6 driver did, it reads Launcher's feature flags (status bytes 26/53/60, or from protocol 6 the 0x02 answer) and acts on them:
    • DPI ceiling and step from the mouse (status 40-42), floor from the model
    • rewritable polling gears: a rate the gear table lacks goes into the active gear, up to the model's ceiling
    • 20K FPS switch (performanceMode) and angle tuning, -90 to 90 like Launcher's slider
    • separate X/Y DPI through 0x48/0x49 (the edited stage gets X = Y, other stages keep their pairs)
    • split USB and 2.4 GHz polling tables through 0x4a/0x4b
    • lift-off levels (status 61, write byte 11) as the lift-off slider when there are more than three heights
    • new: button remapping (0x62 read, 0x52 write on 0xb3) and lighting (0x23/0x24)
    • behind a receiver whose own ID is not a mouse, the 0x03 list names the connected mouse
    • identity-only fallback (settingsReady: false) when the mouse stops answering
  • Keychron1kHidClient (mouse-1k-hid.ts, family keychron-1k) speaks the same command set in feature reports: 20 bytes on 0x51 (answers fetched with a feature read of the same report, after 200 ms behind a receiver) and 64 bytes on 0x52 for buttons. Identity is 0x06, status 0x07 (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/0x18 read, 0x22/0x23/0x27/0x28 write), and Back/Forward codes swapped relative to 8k.
  • launcher-mouse.ts holds what both share: status bytes 1-17, the 0x40/0x42/0x43 packets, 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.
  • Picker and registry: KEYCHRON_LAUNCHER_HID_FILTERS filters on vendor and collection instead of the M6's two product IDs, and the registry checks 0xffc1, then 0x8c, then 0xff0a, which is Launcher's order.

Devices

Names come from Launcher's product API (launcher.keychron.com/vapi/v2/product/<vpid>), limits from each model's static/device/<vpid>/json/v3.json, scanned over 0x3434:0xD000-0xD0FF:

  • M-series 1K: M1 (0xd035, 0xd059), M2 (0xd03b), M2 Mini (0xd03d), M3 (0xd033), M3 Mini (0xd036, 0xd04f), M4 (0xd043), M5 (0xd068), M6 (0xd060, 0xd03f, 0xd064), M6 SE (0xd067), M7 (0xd044, 0xd069), M3 KM (0xd04a), M3 Combo (0xd04c, 0xd04e), M2 Mini Combo (0xd04d), M5 SE (0xd073)
  • M-series 8K: M1 8K (0xd054), M2 8K (0xd052), M2 Mini 8K (0xd055), M3 8K (0xd050), M3 Mini 8K (0xd051), M4 8K (0xd053), M5 8K (0xd048), M6 8K (0xd049), M7 8K (0xd056), M3 V2 (0xd076), M8 (0xd075), M6 HE 8K (0xd08a), M8 HE (0xd091)
  • G series: G3 (0xd06e), G3 HE (0xd074, 0xd083), G4 (0xd06d), G4 HE (0xd080), G5 (0xd06f), G5 HE (0xd087), G6 HE 8K (0xd086, 0xd09d), G9 HE 8K (0xd092), G10 HE (0xd08c)
  • LM7 (0xd06b), LM7 Ultra (0xd06c), budget line: BM22 (0xd058), BM24-26 (0xd061-0xd063), BM27 (0xd070), BM28 (0xd079), BM28 8K (0xd082), T1 HE (0xd084)
  • receivers named in the connection detail: 0xd024, 0xd026-0xd031, 0xd05a

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

  • Launcher 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)
  • module 61892: the transceivers, which is where "1k" turns out to be feature reports (sendFeatureReport then receiveFeatureReport of the same ID, with the receiver variant waiting first)
  • modules 75994 (8k) and 8596 (1k): the button enums, identical except Back and Forward
  • module 66103: which dispatcher each panel action uses, and getBaseInfo's flag handling
  • the lazy settings chunks: angle -90..90, lighting sliders 0-255, the lift-off panel (lodLevelSupport writes the level byte with the code at 0)
  • createByMouse / getDeviceInfo: the collection-to-protocol map and its order

Assumptions and unknowns

  • Launcher does not say which models use which protocol; the collection decides at runtime, so both drivers filter on it and the model table is shared.
  • Only the M6 (firmware 1.0.3, USB and Link-KM) has been on hardware. Its verified reads and writes keep the same bytes. One intended change: the sensor packet now sends 0 in byte 11 unless the firmware flags lift-off levels, as Launcher does, where the old driver echoed status byte 61.
  • The M6's config lists 1 and 2 mm; code 3 (0.7 mm) stays because it round-tripped on the mouse.
  • Launcher's own reads return type 0 ("default") for every button until something is remapped, which matches what the M6 did in earlier testing; button names and defaults come from the config.
  • HE trigger tuning (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".
  • Lighting effects 5 (one-colour flow) and 6 (flash) have no panel equivalent and read as unknown.
  • Overlap with feat(keychron): support the G3 Air on Launcher's 8K Nordic protocol #145 (G3 Air): both PRs touch registry.ts and src/keychron/index.ts. Once both are in, the G3 Air's product IDs should also be excluded from Keychron8kHidClient.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 build and npm test: 1885 tests pass, including the registry probe matrix (usage page 0x8c and report 0x51 added) and the overlap check
  • new tests: 32 for the 8k driver (all the M6 cases kept), 11 for the 1k driver, 6 for the shared codec
  • OpenMouse npm run check (tsc, vite build, 241 tests) with this build linked, and the dev server with simulated mice behind a stubbed navigator.hid:
    • an M3 on 0xffc1 connected 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.
    • an M3 KM on 0x8c connected as "Keychron M3 KM" with no profile or sleep card; 1000 Hz and motion sync went out as 41 02 02 00 01 02 and 42 01 02 01 01 00 01 on feature report 0x51.

Not tested on real hardware beyond the M6's existing paths.

CI note: the build job currently fails at npm audit --audit-level=high, on new advisories for the undici, ip-address and brace-expansion copies bundled inside npm@11.19.1 (a transitive dev dependency in the lockfile). This PR does not touch package.json or the lockfile, and #145 passed the same step yesterday, so main will fail it too until the lockfile moves to a patched npm.

Related

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>
ydw1904 and others added 3 commits September 30, 2026 18:12
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
@snekxs
snekxs merged commit beef2df into OpenMouse-Project:main Sep 30, 2026
5 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.22.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