Turn the little screen on the abandoned Noirgear GMK87 keyboard (also sold
as the Zuoya GMK87 — same board, USB 320F:5055) into something useful: a live price ticker, a Claude Code
session monitor, and a clock that never drifts — with a web manager and a
system-tray app to drive it all. Windows-native: talks through the stock HID
driver, no Zadig / libusb driver replacement needed.
- Price card (slot 1): BTC & ETH from Hyperliquid with 24h change, plus a USD forex rate (ECB via Frankfurter), re-rendered every minute.
- Claude Code card (slot 2): live status of every Claude Code session on
the machine — name, busy/idle, project, age — straight from
~/.claude/sessions/. - Auto-switch: the screen alternates between the two cards every minute.
- No clock drift, ever: every update rewrites the keyboard's BCD clock in the same config packet.
- Custom images/GIFs: drop any image into the web UI; it's cover-cropped to 240×135 and uploaded (GIFs resampled to ≤30 frames).
- Web manager at
http://127.0.0.1:8091+ a tray app with screen switching and session statuses in the menu.
Requires Python 3.10+ on Windows.
git clone <this repo> ; cd gmk87
pip install -r requirements.txt
.\install.ps1 # registers the server task + tray autostart, starts bothinstall.ps1 -Uninstall removes the scheduled task and Startup shortcut.
config.json (created next to the scripts on first change; all keys optional):
| key | default | meaning |
|---|---|---|
rotate |
true |
auto-switch between slot 1 and slot 2 |
slot2 |
"claude" |
what slot 2 holds: "claude" or "custom" |
interval |
60 |
update tick in seconds (see flash wear below) |
fx_to |
"IDR" |
forex quote currency on the price card |
Flash wear: the display images almost certainly live in SPI flash. At the
default 60 s tick with only the visible slot rewritten, that's ~720
writes/day/slot — fine for years on typical parts, but raise interval if you
want to be conservative.
| file | role |
|---|---|
gmk87_time.py |
HID protocol core + clock sync CLI + cross-process device lock |
gmk87_display.py |
price card renderer/uploader CLI (--preview, --show) |
gmk87_claude.py |
Claude Code session scanner + status card renderer |
ui.py / ui.html |
local web manager + the per-minute updater thread |
tray.pyw |
system-tray app (pystray), drives everything via the HTTP API |
All device access goes through a lockfile (.device.lock) so the tray, web
UI, and updater never interleave HID commands.
The Noirgear/Zuoya GMK87 is USB 320F:5055; commands go to the vendor HID interface (MI_03)
as 64-byte reports:
byte 0 report ID 0x04
bytes 1-2 checksum = sum(bytes 3..63), little-endian
byte 3 command ID
byte 4 payload length
bytes 5-7 position/offset, little-endian
bytes 8+ payload (≤56 bytes)
Each command is acked on the IN endpoint (echoes report ID + checksum).
Commands: 0x01 open, 0x02 commit (wait ~100 ms first), 0x05 read config,
0x06 write config, 0x23 begin image session (exactly one per upload),
0x21 image data.
The 48-byte config (read via 0x05, written via 0x06):
| offset | field |
|---|---|
| 28 | LED mode |
| 33 | screen mode: 0 clock · 1 image slot 1 · 2 slot 2 |
| 34 | slot 1 frame count |
| 35–41 | clock, BCD: sec min hour weekday(raw) day month yy |
| 43–44 | animation frame delay, ms, little-endian |
| 46 | slot 2 frame count |
Frames are 240×135 RGB565 big-endian, each padded to 0x10000 bytes;
slot 2 starts at frames1 × 0x10000. (Not 0x8000 — half-frame offsets show
erased flash as a white striped block.)
Protocol reverse-engineering by the community: jeisinger's gist, GMK87TimeSetterPy, gmk87-node.
MIT licensed.
