- Assembly 74.2%
- Python 25.8%
| docs | ||
| research | ||
| tools | ||
| udev | ||
| .gitignore | ||
| LICENSE | ||
| README.md | ||
drunkdeer-linux
Native Linux tooling for DrunkDeer keyboards (A75 and friends).
The official updater is a Windows-only GUI that talks to the keyboard over raw
HID. This repo contains the reverse-engineered protocol
(docs/PROTOCOL.md) and a small, dependency-free Linux
flasher (tools/drunkdeer-flash) that speaks it
directly through /dev/hidraw.
Tested on a real A75 US: firmware 0x0017 → 0x0021
(see Verification status).
TL;DR
# 1. once: let the logged-in user open the HID nodes
pkexec install -m0644 udev/60-drunkdeer.rules /etc/udev/rules.d/
pkexec udevadm control --reload && pkexec udevadm trigger
# then re-plug the keyboard
# 2. get the vendor firmware images (not redistributed here)
curl -LO 'https://cdn.shopify.com/s/files/1/0671/4694/0719/files/DrunkdeerUpdaterV1.4.9.zip?v=1744874175'
unzip DrunkdeerUpdaterV1.4.9.zip -d updater
# 3. read the keyboard
tools/drunkdeer-flash info
# 4. rehearse: print every packet, send nothing
tools/drunkdeer-flash flash updater/DrunkdeerUpdaterV1.4.9 --dry-run
# 5. flash
tools/drunkdeer-flash flash updater/DrunkdeerUpdaterV1.4.9
# 6. confirm the new version
tools/drunkdeer-flash info
Requirements
| requirement | notes |
|---|---|
Linux with hidraw |
/dev/hidraw* must exist (CONFIG_HIDRAW); no libusb, no hidapi |
| Python ≥ 3.8 | stdlib only — the script uses bytes.hex(sep) (3.8) and required subparsers (3.7) |
| no third-party packages | argparse, configparser, os, select, struct, sys, time |
pkexec (or root) once |
to install the udev rule; flashing itself runs as your user |
Permissions
Raw HID access needs a udev rule. Install it once:
pkexec install -m0644 udev/60-drunkdeer.rules /etc/udev/rules.d/
pkexec udevadm control --reload
pkexec udevadm trigger
Then re-plug the keyboard (or re-login). The rule tags every 352d:* hidraw
node with uaccess, so the logged-in user gets read/write access to both the
application firmware (352d:2383) and the bootloader (352d:1101).
The web app's "keyboard firmware and driver does not match(2)" error on Linux is exactly this:
WebHID device.open()was rejected because the hidraw node was root-only. Installing the rule fixes it.
Getting the firmware images
The flasher does not ship any firmware. Download the official updater zip and point the flasher at the unpacked directory:
curl -LO 'https://cdn.shopify.com/s/files/1/0671/4694/0719/files/DrunkdeerUpdaterV1.4.9.zip?v=1744874175'
unzip DrunkdeerUpdaterV1.4.9.zip -d updater
The images live in updater/DrunkdeerUpdaterV1.4.9/<FOLDER>/*.enc, selected by
config/config.ini (VID/PID + identity) and described by each folder's
update_config.ini.
Supported devices
Everything below is derived from
DrunkdeerUpdaterV1.4.9/config/config.ini and the folders actually shipped in
the v1.4.9 zip. The flasher is identity-driven: it reads the keyboard's
identity triple, looks up the matching [DEVICEn] section, and flashes that
folder's .enc through the RYMicro bootloader path.
Shipped and usable (.enc / RYMicro path)
| config | VID:PID | identity | folder | image |
|---|---|---|---|---|
DEVICE0 |
352d:2383 |
0x0b,0x04,0x01 |
A75_ANSI_WIN |
usb_hid_app_v1.0.0_2B61B097.enc |
DEVICE1 |
05ac:024f |
0x0b,0x04,0x01 |
A75_ANSI_MAC |
usb_hid_app_v1.0.0_2B61B097.enc |
DEVICE2 |
352d:2383 |
0x0b,0x04,0x02 |
A75_ISO_WIN |
usb_hid_app_v1.0.0_F2D75F68.enc |
DEVICE3 |
05ac:024f |
0x0b,0x04,0x02 |
A75_ISO_MAC |
usb_hid_app_v1.0.0_F2D75F68.enc |
DEVICE4 |
352d:2383 |
0x0b,0x04,0x03 |
A75_Pro_ANSI_WIN |
usb_hid_app_v1.0.0_E15CF7C4.enc |
DEVICE5 |
05ac:024f |
0x0b,0x04,0x03 |
A75_Pro_ANSI_MAC |
usb_hid_app_v1.0.0_E15CF7C4.enc |
DEVICE8 |
352d:2386 |
0x0b,0x04,0x05 |
G75_ANSI |
usb_hid_app_v1.0.0_7FEDA629.enc |
DEVICE9 |
352d:2384 |
0x0b,0x03,0x01 |
G60_ANSI |
usb_hid_app_v1.0.0_44580D3F.enc |
DEVICE11 |
352d:2382 |
0x0b,0x02,0x01 |
G65_ANSI |
usb_hid_app_v1.0.0_F609860C.enc |
DEVICE13 |
352d:2391 |
0x0b,0x04,0x07 |
G75_JP |
usb_hid_app_v1.0.0_CDB25D98.enc |
Two more folders ship a usable .enc but their config.ini folder name does
not match the directory in the zip, so the flasher cannot find them on Linux
(case-sensitive filesystem):
| config | folder in config.ini |
directory in the zip | result |
|---|---|---|---|
DEVICE10 |
G60_iso |
G60_ISO |
error[7]: no update_config.ini |
DEVICE12 |
G65_iso |
G65-ISO |
error[7]: no update_config.ini |
G65-ISO is a mismatch even on Windows (underscore vs. hyphen), i.e. a bug in
the vendor package. You can still flash those images by pointing the flasher
at the file directly:
tools/drunkdeer-flash flash updater/DrunkdeerUpdaterV1.4.9/G65-ISO/usb_hid_app_v1.0.0_4FBE6B6D.enc
config.ini entries with no folder shipped
These sections exist in config.ini but the v1.4.9 zip ships no such
directory, so the flasher aborts with error[7]:
DEVICE6 A75_pro_iso_win, DEVICE7 A75_pro_iso_mac,
DEVICE14 KG645U_62_uk, DEVICE15 KG650U_68_us,
DEVICE16 KG650_69_uk.
A75 Ultra / A75 Master — not supported
DEVICE17 (A75_ultra, identity 0x0b,0x04,0x04, UpdateType=1) and
DEVICE18 (A75_master, identity 0x0b,0x05,0x04, UpdateType=2) use a
different update mechanism: their folders contain A75Ultra.bin /
A75Master.bin, DualBankBoot.bin and blhost.exe (NXP's bootloader host
tool) instead of an .enc + update_config.ini. The flasher implements only
the RYMicro .enc path and does not support them. It fails safely rather
than flashing the wrong thing:
flash <updater-dir> --identity 0x0b,0x04,0x04(or--folder A75_ultra) →error[7]: no update_config.ini in .../A75_ultra, exit7, nothing sent.flash .../A75_ultra/A75Ultra.bin→error[7]: image A75Ultra.bin does not match encryption_en=1 (expected a .enc file), exit7, nothing sent.
Use the vendor's Windows updater (or blhost) for those two models.
Autodetection caveat
find_app_device() only matches 352d:2383 on the input1 collection. For
any other application PID (2382, 2384, 2386, 2391) you must name the
node yourself:
tools/drunkdeer-flash --device /dev/hidraw7 info
The bootloader (352d:1101) is matched by VID:PID alone, so recovery works
regardless of model.
Only the A75 ANSI path was tested on hardware. The other models are supported by the same protocol and config-driven image selection, but have not been exercised on a real device.
Usage
Identify the keyboard
tools/drunkdeer-flash info
# device: /dev/hidraw7
# identity: 0x0b,0x04,0x01
# firmware: 0x0017 (23)
Dry run (recommended first)
Prints every packet that would be sent, without entering the bootloader or
sending any 0xA5/bootloader command. It still performs the read-only
identity query (A0 02) if the application interface is present, so it can
pick the right image:
tools/drunkdeer-flash flash updater/DrunkdeerUpdaterV1.4.9 --dry-run
Use --limit 0 to print all 232 write commands, --limit N to print only the
first N (default 2). --assume-boot makes the dry run rehearse the
recovery path instead of the normal one.
Flash
tools/drunkdeer-flash flash updater/DrunkdeerUpdaterV1.4.9
The image is chosen from config/config.ini by the keyboard's identity; the
tool refuses to flash when the identity does not match the image's config. You
can also point it at a single image file:
tools/drunkdeer-flash flash updater/DrunkdeerUpdaterV1.4.9/A75_ANSI_WIN/usb_hid_app_v1.0.0_2B61B097.enc
The .enc filename tag is validated against the XOR of the file's 32-bit
little-endian words before anything is sent.
Recovery from a stuck bootloader
If a flash is interrupted the keyboard stays in bootloader mode (352d:1101)
and there is no application interface to query. The flasher detects this and
skips the identity query and the 0xA5 enter-boot command, but it cannot know
which image to use — select it explicitly:
tools/drunkdeer-flash flash updater/DrunkdeerUpdaterV1.4.9 --folder A75_ANSI_WIN
# or
tools/drunkdeer-flash flash updater/DrunkdeerUpdaterV1.4.9 --identity 0x0b,0x04,0x01
It never guesses: without --folder/--identity it refuses to flash while the
identity is unknown.
How it works
The flasher is a byte-exact reimplementation of the DLL's update state machine.
Full packet layouts, DLL addresses and abort guards are in
docs/PROTOCOL.md.
sequenceDiagram
participant T as drunkdeer-flash
participant A as app 352d:2383
participant B as boot 352d:1101
T->>A: A0 02 identity query (report id 4)
A-->>T: identity 0x0b,0x04,0x01, fw 0x0017
T->>A: A5 01 enter bootloader (report id 4)
A--xT: device re-enumerates
B-->>T: 352d:1101 appears
Note over T: settle 1000 ms (program_type=USB)
T->>B: getv (65-byte frame, report id 0)
B-->>T: chip_lo/chip_hi/UID -> opcode set + ack byte
T->>B: erase 0x31 (size, chip id, first 32 B, ARM stub)
B-->>T: ack 0x34
loop 232 x 512 B
T->>B: write 0x32, chunk n
B-->>T: ack 0x34
end
T->>B: final 0x33 (checksum of data[0x20:], timestamp)
B-->>T: ack 0x34
Note over B: reboots into application firmware
B-->>T: 352d:2383 re-appears
Notes:
- The opcode set (
0x31/0x32/0x33vs.0x01/0x02/0x03) and the ack byte (0x34vs.0x04) are chosen from thegetvreply, not from the config. - The final checksum covers only the 512-byte write chunks (
data[0x20:]), not the whole file — the whole-file XOR is the.encfilename tag. - There is no reset command: the bootloader reboots itself after the final ack.
Using the web configurator on Linux
drunkdeer-antler.com is the browser-based configurator. It needs a Chromium-based browser with WebHID (Chrome, Chromium, Edge, Brave — Firefox does not implement WebHID).
- Install the udev rule above; that is what fixes the
"keyboard firmware and driver does not match(2)" error. Without it,
WebHID device.open()is rejected because the hidraw node is root-only. - The site's own in-browser firmware updater only covers the A75 Ultra and
A75 Master (the NXP/blhost models). For the
.enc/RYMicro models usetools/drunkdeer-flashinstead.
Troubleshooting
| symptom | cause / fix |
|---|---|
error[2]: cannot open /dev/hidrawN ([Errno 13] Permission denied); check that udev/60-drunkdeer.rules is installed and re-plug the keyboard |
udev rule missing or not applied. Install it, then re-plug. Check with getfacl /dev/hidrawN — you should see a user:<you>:rw- ACL entry. |
error: DrunkDeer application interface not found (exit 2) |
Keyboard not plugged in, or it is a non-2383 model (pass --device), or it is sitting in the bootloader. |
error[3]: bootloader did not appear |
The 0xA5 command was accepted but 352d:1101 never showed up within time_out (30 s). Re-plug and retry; check dmesg for USB errors. |
error[4]: <cmd>: bad ack (got <hex>, want resp[0]=0x33 resp[8]=0x34) where the reply contains 35 |
0x35 is the bootloader rejecting the final checksum (observed on real hardware). The image or its .enc tag is wrong — re-download the updater zip. |
error[4]: <cmd>: bad ack (...) with other bytes |
The bootloader rejected a command. The raw reply is printed; include it in a bug report. |
error[7]: no update_config.ini in ... |
The folder has no .enc update path (Ultra/Master, or a config entry with no shipped folder), or a G60_iso/G65_iso name mismatch. See Supported devices. |
error[7]: image X does not match encryption_en=1 (expected a .enc file) |
The selected image is not the type the config expects (e.g. an NXP .bin). |
error[7]: no 8-hex checksum tag in '...' |
The image filename has no 8-hex checksum tag (e.g. A75Ultra.bin). |
error[8]: image checksum 0x... does not match filename tag 0x... |
The .enc file is truncated or corrupted. |
Keyboard stuck in bootloader (352d:1101, no info reply) |
Expected after an interrupted flash — see Recovery. It is recoverable. |
| Keyboard did not re-appear after flashing | The flash itself succeeded; the tool prints warning: keyboard did not re-appear as 352d:2383. Re-plug and run info. |
For bug reports, capture the full packet trace:
tools/drunkdeer-flash --log /tmp/dd.log flash updater/DrunkdeerUpdaterV1.4.9
--log writes every frame written and every report read, timestamped and in
hex. The log contains vendor firmware bytes — do not attach it publicly
without checking.
Exit codes
| code | meaning |
|---|---|
0 |
ok |
1 |
usage error or unhandled OS error |
2 |
device not found / disappeared |
3 |
timed out waiting for the bootloader (or for an ack) |
4 |
bootloader rejected a command (bad ack, e.g. 0x35 checksum) |
5 |
HID write failed |
6 |
failed to enter bootloader / chip-id guard refused |
7 |
image or config problem |
8 |
firmware checksum mismatch |
9 |
internal state error |
Safety
--dry-runsends no0xA5and no bootloader command; it only reads the identity.- The flasher validates the image checksum and the identity before sending anything, and mirrors the official updater's chip-id guards.
- A failed flash leaves the keyboard in the bootloader, which is usually recoverable with the procedure above.
Repository layout
| path | tracked? | what |
|---|---|---|
tools/drunkdeer-flash |
yes | single-file Python 3 flasher (stdlib only) |
docs/PROTOCOL.md |
yes | packet-level protocol, with DLL addresses as evidence |
udev/60-drunkdeer.rules |
yes | grants the logged-in user access to the HID nodes |
research/enc-analysis.md |
yes | static analysis of the .enc images (ECB block cipher, per-file key) |
research/asm/*.s |
yes | disassembly dumps of the DLL routines cited in docs/PROTOCOL.md |
research/updater/ |
no | unpacked vendor updater (binaries + firmware) — gitignored, not redistributed |
research/web/ |
no | the configurator's JS bundle — gitignored |
research/*.log |
no | flash traffic traces (contain firmware bytes) — gitignored |
The gitignored material is reproducible from the vendor zip:
curl -LO 'https://cdn.shopify.com/s/files/1/0671/4694/0719/files/DrunkdeerUpdaterV1.4.9.zip?v=1744874175'
unzip DrunkdeerUpdaterV1.4.9.zip -d research/updater/x
License
GPL-3.0-or-later © 2026 fedes1to. Forks and redistributions must keep the copyright notice and stay under the GPL.
This covers the code and docs in this repo only. The DrunkDeer updater and firmware images are not included and are not covered.
Disclaimer
Use at your own risk. This is an unofficial, reverse-engineered tool. It is not affiliated with, endorsed by, or supported by DrunkDeer.
This software is provided "as is", without warranty of any kind. The author is not responsible for any damage caused by using it, including but not limited to a bricked or unusable keyboard, lost settings, voided warranty, or any other hardware or data loss. You alone are responsible for deciding to flash your keyboard and for the result. See sections 15 and 16 of the GPL-3.0 (Disclaimer of Warranty, Limitation of Liability).
A failed or interrupted flash normally leaves the keyboard in its bootloader, which this tool can usually recover (see Recovery), but recovery is not guaranteed.
No firmware is redistributed here; the images are downloaded from the vendor's CDN.
Verification status
Tested on a real DrunkDeer A75 US (352d:2383, identity 0x0b,0x04,0x01,
firmware 0x0017 = 23) with
A75_ANSI_WIN/usb_hid_app_v1.0.0_2B61B097.enc:
info— verified: reports identity0x0b,0x04,0x01, firmware0x0017.flash --dry-run— verified: prints the enter-boot,getv, erase, 232 write and final packets, and asserts the checksum consistency.flash(first attempt) — failed at the final command. The enter-boot,getv, erase and all 232 write commands were accepted (resp[8] == 0x34), but the final command was rejected withresp[8] == 0x35and the keyboard stayed in bootloader mode (352d:1101). Cause: the tool computed the final checksum over the whole image instead of only the write chunks (data[0x20:]), which is what the DLL accumulates. Trace:research/flash-1790427540.log(local, gitignored).flash --folder A75_ANSI_WIN(recovery from bootloader) — succeeded. The tool detected352d:1101, skipped the identity query and0xA5, and re-flashed with the corrected checksum0x84560d4f; the final ack was0x34. The keyboard re-enumerated as352d:2383andinfonow reports firmware0x0021(33) — i.e.0x0017 → 0x0021. Trace:research/recovery-1790427771.log(local, gitignored).
The normal path was not exercised end to end with the corrected checksum: its
steps are covered across the two runs — enter-boot, getv, erase and the 232
writes in the first run, and getv, erase, the 232 writes and the final command
in the recovery run. Failure handling and recovery from a keyboard stuck in the
bootloader are both verified.