Node.js utilities for uploading images and syncing time on GMK87 keyboard displays via HID protocol.
Stable but still improving.
The upload process now works more consistently, but it’s important to give the device time to "breathe" between uploads.
Current priorities:
- Fine-tuning pacing delays
- Better understanding command timing (especially
0x23) - Further cross-platform testing on macOS, Windows, and Linux
- Vendor ID:
0x320f - Product ID:
0x5055 - Display: 240x135 pixels, RGB565 encoding
- Slots: 2 image slots (0 and 1)
npm installnode-hid- HID device communicationjimp- Image processing- ImageMagick (
magickorconvertbinary in PATH) - forsendImageMagick.js
npm run sendimage # Upload image with ImageMagick preprocessing
npm run upload # Upload hardcoded test images
npm run timesync # Sync device time and configRecommended approach. Handles format conversion and resizing automatically.
npm run sendimage -- --file path/to/image.png --slot 0
npm run sendimage -- --file image.jpg --slot 1 --show=falseOptions:
--file(required) - Path to input image--slot(required) - Target slot:0or1--show(optional) - Display after upload (default:true)
npm run uploadHardcoded to upload nyan.bmp to slot 0 and encoded-rgb555.bmp to slot 1. Edit src/uploadImage.js to change sources.
npm run timesyncSyncs device clock and configures display settings. Sets the current system time on the keyboard's RTC and configures frame duration, shown image slot, and other display parameters.
Config frame fields:
- System time (seconds, minutes, hours, day, date, month, year)
- Frame duration (default: 1000ms)
- Shown image selector (0=time widget, 1=slot 0, 2=slot 1)
- Frame counts for each image slot
This command is also auto-run as part of the upload sequence. Can be run standalone to just update time without uploading images.
64-byte HID reports with 60-byte payloads:
[0] = 0x04 (report ID)
[1-2] = checksum (uint16 LE, sum of bytes 3-63)
[3] = command byte
[4-63] = payload (60 bytes)
payload[0] = 0x38 (56 data bytes flag)
payload[1-2] = pixel offset (uint16 LE)
payload[3] = image slot (0 or 1)
payload[4-59] = RGB565 pixel data (MSB first)
0x01 → init
0x06 (config frame) → time + display config
0x02 → commit config
0x23 → ???
0x01 → init again
<500ms delay>
0x21 (frames...) → pixel data
0x02 → final commit
- First row artifacts: White pixels or corruption at y=0. Workaround: 500-600ms delay before pixel upload.
- HID write failures: Transient EPIPE/EBUSY errors. Mitigated with retry logic and pacing delays.
- Offset calculation:
sendImageMagick.jsusesimageIndex * 0x28offset (matches C# reference).uploadImage.jsuses0x00. Both work inconsistently. - macOS open issues: Opening by VID/PID sometimes fails. Retry logic helps.
- Green keyboard flash: Keyboard backlighting changes to green when image upload succeeds. Side effect of the display switch command.
.
├── src/
│ ├── sendImageMagick.js # ImageMagick pipeline (recommended)
│ ├── uploadImage.js # Direct upload (hardcoded images)
│ └── timesync.js # Time sync + config frame
├── nyan.bmp # Test image
├── encoded-rgb555.bmp # Test image
└── package.json
Help wanted on:
- Reliable frame transmission (no drops, no corruption)
- Understanding the mystery command bytes (0x23, config frame fields)
- Proper offset calculation logic
- Cross-platform HID stability
Open an issue or PR if you've got ideas.
Huge thanks to @ikkentim for the original reverse engineering work:
https://github.com/ikkentim/gmk87-usb-reverse
This Node.js port is based on that C# implementation. All protocol knowledge comes from their research.
MIT