Repository navigation
Releases: svumo/DrunkDeer-Control
Release list
v2.5.1 — G60/G65 Escape key fix
@
Fix
G60 / G65: the Esc key was typing backtick (`) instead of Escape.
On the G60 and G65 (and their m1/m2/m3 profile variants), the app was sending the wrong default keymap for the top-left key. These 60%/65% boards have no F-row, so their top-left key is Escape — but the app treated it as the grave/backtick key and overwrote Escape with ` on every profile sync. The web driver was unaffected, which is why the key worked there but not here.
This release ships the correct default (Escape, HID 0x29) for slot 21 on the G60 and G65 families. The A75 family and X60 are unaffected (they have a dedicated Esc in their F-row).
Thanks to the user who reported this with a debug log — that made it a quick find.
@
v2.5.0 — Last Win fixed, RGB lighting, explicit Sync
First non-beta release. Brings together everything from beta.22 through beta.42 plus the 2026-05-27 work session.
Headline fixes
Last Win (snap-tap) now actually works on gen-1. USBPcap of the official drunkdeer.keybord.net.cn driver doing a real A↔D save revealed we'd been sending a fc 01 LW-pair packet that the official driver never emits, plus a wrong rtModeFlag on the Type-4 keymap entries. Together these caused the "D stuck pressed" / "trailing key" symptoms after fast typing. Now matches the official wire format. Verified on A75 Pro firmware 0x17.
Explicit Sync model. Property changes (sliders, toggles, pair edits) no longer auto-push to the keyboard — they save to disk and the Sync button turns amber. Click "Push changes" to commit. Matches the official web driver's Save UX and eliminates the race window where the firmware was still mid-sync when you tried to test. Three button states:
- Idle: purple "Sync to keyboard"
- Unsynced: amber "● Push changes"
- Just-synced: 2 s green "✓ Synced", then back to idle
Per-profile state with Discard. Each profile tracks its own unsynced flag — switching to a clean profile no longer leaves the button amber. When you try to switch profiles with unsynced changes, you now get a dialog: [Push and switch] / [Discard and switch] / [Stay on this profile]. Discard reverts to the last-pushed snapshot.
RGB lighting (first time shipping)
The Lighting tab is now enabled. Preset effects (Off, AlwaysOn, Breath, Wave Spectrum, Ripple, Marquee, Neon, and 12 more from the official 19-effect catalog) are hardware-validated on A75 Pro 1st gen. Per-key custom painting is available with a one-time first-sync confirmation — wire format byte-matches the official driver but the per-key path is not yet formally hardware-verified.
Brightness is hard-clamped to 0–9 at every layer (UI, ViewModel, packet builder). Recovery procedure for the edge-case soft-brick is in docs/rgb-protocol.md and the Lights-off button does exactly that.
Hotkey safety
Profile hotkeys now require a modifier (Ctrl, Alt, or Shift). Plain keys like [ or R were swallowed system-wide while the app was running. Existing single-key bindings are not migrated — if you had one, re-bind with a modifier.
Bundled presets on first run
Fresh installs land on a Typing preset (default) and Valorant preset instead of an empty profile list. You can delete them.
Gen-2 A75 Pro OEM (VID 0x19F5)
Detection + sync path is unchanged from beta.42 — still echo-verified but observable-unverified. The new explicit-Sync UX applies, which should make any partial behaviour easier to test. If you have an OEM gen-2 unit, treat this as WIP and capture a USBPcap if anything goes wrong.
Other
- A75 Pro firmware 0x17 verified working in production
- Many UI polish fixes (lavender → purple accent, glass-card popups, compact tab strip, Viewbox-fitted keyboard)
- Profile auto-update flow (
%LocalAppData%\DrunkDeer Control\bin\) preserved - Single-instance enforcement preserved
Download
DrunkDeer-Control.exe below is a self-contained single-file build. No .NET installer needed.
🤖 Built with Claude Code
v2.4.1-beta.41 — gen-2 LW diagnostic + launch fixes
Prerelease — gen-2 OEM LW activation diagnostic build. Strips the KeyTrigger full-region write before LW commits (suspect #1 per the beta.40 decision tree). Trade-off: per-key AP/RT slider tweaks won't land on gen-2 OEM hardware while this is in place. Bundled launch/UX fixes: graceful HID release on takeover (no more replug after update), silent redirect when re-downloading the same version, no stray APP_DIR for numbered duplicates, LW auto-seed pairs are now A↔D + W↔S only.
v2.4.1-beta.40 — gen-2 LW targeted writes (mirror official driver)
Beta.38's bulk user-keymap write produced byte-identical packet content to the official driver's captures but froze every LW-paired key on tester B's keyboard. beta.40 swaps to targeted single-slot writes — matching the official driver's 9-packet pattern from usb5.pcapng exactly.
Theory
The official driver only writes the 2 user-keymap slots that actually change when toggling LW. Our bulk write touched every slot. Even though the byte content was correct, the firmware's SOCD state machine appears to be sensitive to extra writes (or their timing) — paired keys stop firing.
Fix
- Targeted single-slot 0x09 writes — one per pair direction
- Stale-slot tracking: any slot armed last sync but not this sync gets a 0x09 restore write to its exact factory entry (from the slot-map readback)
- Also includes RDT (type 0x95) and per-key remap (type 0x10 + target HID) via the same single-slot path
- Stale tracking resets on disconnect
Tester ask
- Factory reset the keyboard (last time, beta.40 self-cleans after this)
- Install beta.40
- Test LW with A↔D: hold A → "aaa", press D → "ddd", release D → "aaa"
- Add W↔S + ←↔→, sync, test all
- Remove one pair, sync — confirm it doesn't come back as a ghost pair
- Try a key remap (Caps → Esc) — sync, confirm in Notepad
- Send the debug log either way
What this beta does NOT change
- Full KeyTrigger region write (actuation sync) still fires every sync
- FuncBlock LW master-bit R-M-W still fires every sync
If LW still doesn't fire after this, those are next on the chopping block.
Safety
Gen-1 keyboards untouched. Beta.40 self-cleans any stale state from prior betas on the first sync.
v2.4.1-beta.38 — bulk user-keymap write fixes stale LW entries
Tester B's beta.37 log revealed a real bug: when he removed the ↑↔↓ LW pair, the firmware still showed it as a "ghost pair" in the official driver's readback, and none of his LW-armed keys responded.
What was broken
Beta.37 wrote per-slot user-keymap entries only for the slots in the NEW pair list. When a pair got removed, the slots from the old pair kept their [0x94, pair_idx, partner] entries — firmware still saw them as SOCD-armed with invalid pair indices pointing to nothing in the pair table, so:
- Browser readback showed a ghost pair
- Every LW-armed key locked up (firmware in confused SOCD state)
- Required a factory reset to recover
The fix
Bulk-write the full 384-byte user-keymap region every sync, matching what the OEM driver does (setUserKeys). Build from the slot map's defaults, overlay LW entries for currently-paired slots, send via 7 chunked writes. Stale entries can't survive — any slot LW-armed before but not now is reset to its standard-HID default as part of the same atomic write.
Tester ask
- Factory reset the keyboard (one last time — beta.38 will self-clean after this)
- Install beta.38
- Try multiple LW pair configurations:
- Set 3 pairs (A↔D, W↔S, ←↔→), sync, test
- Remove one pair, sync, test — old pair should NOT ghost
- Add ↑↔↓ pair, sync, test
- Toggle LW off, sync, test — all keys should work normally
- Send the debug log
RDT
Still waiting on a USBPcap of the official driver enabling RDT to find the FuncBlock master bit. Wire format is otherwise identical (type byte 0x93 instead of 0x94).
Safety
Gen-1 keyboards untouched. If slot map read fails, bulk write skipped (degrades to "pair table cleared only" — safe).
v2.4.1-beta.37 — gen-2 LW for any pair + actuation slot fix
Tester B's diagnostic log from beta.36 gave us everything needed for the proper fix. Two bugs corrected:
1. The "WASD paired with 8" bug
Beta.34's 0x09 packet had byte 2 = 0x14 thinking it was a threshold. The JS bundle showed the entry is actually {type, pair_index, partner_slot}. 0x14 = 20 = the firmware slot of the "8" key on the A75 Pro OEM — which is exactly why every LW-armed key got paired with "8". Byte 2 is now the partner key's firmware slot, looked up via the live slot map.
2. The actuation slot shift (silently broken since beta.21)
beta.27..beta.34 wrote Profile.Keys_Array[i] at firmware slot i — but our visual KeyIndex (A=64) doesn't match the firmware slot (A=37). User AP edits have been landing on the wrong physical keys across all 12 gen-2 OEM betas. The probes in beta.36's log showed every probed slot still held the firmware factory default, confirming no user edit had ever stuck. Fixed by routing via the live HID→slot map.
How beta.37 works
- Reads the firmware's default keymap on each connect (sub-cmd 0x07, 10 chunked reads, parsed into a slot map)
- Per-key actuation sync: iterate firmware slots, look up HID, find our KeyIndex for that HID, write the user's AP at the firmware's slot for that key
- LW writes: dynamic — works for any pair with HID codes in the firmware's keymap. A↔D, W↔S, arrows, modifiers, anything
Tester ask
- Factory reset the keyboard
- Install beta.37
- Try A↔D: hold A → "aaa", press D → "ddd", release D → "aaa"
- Try W↔S
- Try arrows: ←↔→ or ↑↔↓
- Send the debug log either way
What's NOT in this beta
- RDT (Release Dual Trigger) still needs a separate USBPcap of the official driver enabling RDT to find its FuncBlock master bit. Same code path otherwise — type byte is
0x93instead of0x94.
Safety
If the slot-map read fails (rare), LW activation is skipped rather than written to guessed slots. Empty pair table is still written which clears any prior state safely. Actuation sync falls back to direct-KeyIndex (= beta.27 behaviour). Gen-1 keyboards untouched.
v2.4.1-beta.36 — diagnostic beta with full slot-map dump
Diagnostic-only beta (supersedes beta.35). Behaviour identical to beta.34 — same writes, same hardcoded slots for A↔D and W↔S, arrow pairs still skipped. The only change vs beta.35 is the slot-map dump now lands in the normal log (not verbose-only), so we get every key's firmware slot without needing verbose mode enabled.
This is the data we need to fix LW, RDT, and any future per-key feature in one pass.
Tester ask
- Factory reset the keyboard from the official site first (clean baseline)
- Unplug + replug
- Install beta.36
- Keep Last Win OFF (don't enable it)
- Hit Sync once
- Send the debug log
The log will contain one line per real slot:
slot 026: type=0x10 code1=0x00 code2=0x1A [W]
slot 037: type=0x10 code1=0x00 code2=0x04 [A]
…
That single dump covers every key — LW, RDT, anything that addresses by slot. Beta.37 will use this to route per-pair writes correctly.
Don't enable LW in this beta
The hardcoded slots from beta.34 are wrong for your keyboard (WASD got paired with the "8" key — HID 0x25 = decimal 37, exactly the slot number I wrote). Don't enable LW until beta.37 ships with corrected routing.
v2.4.1-beta.35 — diagnostic beta (slot-map readback, no behaviour change)
Diagnostic-only beta. Behaviour is identical to beta.34 — same writes, same hardcoded slots for A↔D and W↔S, arrow pairs still skipped. The new additions are passive reads that log diagnostic data so we can verify the firmware's slot indexing before flipping to dynamic routing in beta.36.
What was found
Decoded the OEM driver's JS bundle (drunkdeer.keybord.net.cn): the LW writes use formulas addr = 3 × slot (0x09) and addr = 8 × slot + 1024 × profile (0xA1). The "slot" is the firmware's internal key slot index — read via sub-cmd 0x07.
What this beta logs
- Live slot map for every gen-2 OEM sync (10 chunked reads, 512 bytes total)
- Per-key probe: slot map says A → slot N, captured value was 37, our KeyIndex was 64 — match? Y/N
- Arrow slot positions (Left/Down/Right/Up)
- KeyTrigger sample readback at slots {37, 39, 38, 26} AND {64, 66, 65, 44}: settles whether tester B's per-key AP edits were landing on the captured slots, our KeyIndex slots, or neither
- Full 128-slot raw dump in verbose log
What this beta does NOT change
- LW pair activation: still hardcoded slots for A↔D and W↔S (same as beta.34)
- Per-key actuation sync: still uses Profile.Keys_Array index 0..127 (same as beta.27+)
- Arrow pairs: still skipped (await beta.36)
- Gen-1 keyboards: untouched
Tester ask
Trigger a sync (toggle anything in the app), grab the debug log, send back. Looking for:
- Does the slot map readback succeed?
- Do the probe lines show A=37, D=39, S=38, W=26 (matching captures)?
- What slot does the firmware report for arrows?
- KeyTrigger sample probes — do slot 37 / 64 hold your tuned APs, or defaults?
v2.4.1-beta.34 — gen-2 LW activation via verified 0x09 + 0xA1 writes
Tester B — please try this on your freshly-reset keyboard. Should activate SOCD on A↔D and W↔S pairs.
What's in
- Per-pair RTP (
0x55 0x09) + commit (0x55 0xA1) packets re-introduced, this time with hardware-verified per-pair addresses from your usb5 (A↔D) and usb6 (A↔D + W↔S) captures. - Beta.28's stride-based assumption (
0x086F + dir×6) was wrong: fresh-state addresses live in page0x00xx, and each pair has its own unique address pair — not a base+stride formula. - Pair table writes go ahead unchanged; only A↔D and W↔S currently have verified activation recipes. Arrow pairs (←↔→, ↑↔↓) need their own captures before they fire — those pairs will still appear in the pair table but won't trigger SOCD until then.
What to test
Toggle LW on, hit Sync. On A↔D and W↔S you should see SOCD: hold one, tap the other → only the second key registers; release the second → first resumes.
If A↔D works but you also want arrows working, send me a capture of the official driver saving arrow pairs from fresh-reset state.
v2.4.1-beta.33 — Kun-switch slider display fix (0.05 was showing as 0.1)
Beta.29 lowered the DS/US slider floor to 0.01mm for OldHighPrec/NewHighPrec dialects but a Kun-switch user reported the slider still appeared capped at 0.1mm. His log confirmed the runtime floor IS 0.01mm — the bug was in the display formatter.
C# format string 0.0 rounds to one decimal place: 0.05 → 0.1. So dragging the slider to 0.05mm made the screen read "0.1 mm", looking like the floor hadn't moved.
Switched all AP/DS/US value displays (ActuationDrawer + RdtPairDrawer) from 0.0 / F1 to 0.0# — always one decimal, optional second decimal when non-zero. 0.5 stays 0.5, 2.0 stays 2.0, but 0.05 now shows as 0.05.
Legacy (G65/G60) users see no visible change — their step is 0.1mm so the second decimal is always zero and gets dropped.