Skip to content

aar-rafi/aks075-linux

Repository files navigation

aks075-linux

Upload images and GIFs to the AJAZZ AKS075 keyboard screen on Linux.

The AJAZZ AKS075 is a 75% mechanical keyboard with a 128x128 TFT screen. The official software is Windows-only. This tool lets you control the screen from Linux — upload static images, animated GIFs, and sync the clock.

Features

  • Static images — Upload PNG, JPG, BMP, or single-frame GIF to the screen
  • Animated GIFs — Upload multi-frame GIFs with proper frame timing (up to 255 frames)
  • Time sync — Sync the keyboard's clock to your system time
  • No dependencies on the Windows driver — Pure Python, talks directly to the HID device

Installation

git clone https://github.com/torr20/aks075-linux.git
cd aks075-linux
pip install -r requirements.txt

udev Rule (recommended)

Without a udev rule, you'll need sudo for every command:

sudo python -m aks075 setup-udev

This copies the udev rule to /etc/udev/rules.d/ and reloads. After this, unplug and replug the keyboard — you can then run without sudo.

Usage

# Upload a static image
python -m aks075 image photo.png

# Upload an animated GIF
python -m aks075 gif animation.gif

# Sync the keyboard clock
python -m aks075 time

Example GIFs are included in the examples/ directory:

python -m aks075 gif examples/keycaps.gif
python -m aks075 gif examples/complex-animation.gif

How It Works

The keyboard communicates over two vendor-specific HID interfaces: a control pipe (feature reports for commands) and a data pipe (4096-byte output reports for pixel data). Images are encoded as RGB565 little-endian and wrapped in a 256-byte GIF header before transmission.

For the full protocol specification, see docs/protocol.md.

The Reverse Engineering Story

This tool exists because the AJAZZ AKS075's screen protocol was completely undocumented. Getting it working required three rounds of reverse engineering, each solving a different layer of the problem.

Round 1: Open-Source References

Several community projects had partially reverse-engineered similar Sonix-based keyboards. The gohv/EPOMAKER-Ajazz-AK820-Pro Rust project had time sync and the basic image upload structure. Time sync worked immediately. Image upload partially worked — the image appeared briefly on screen but didn't persist, had wrong colors, and was misaligned.

Three problems remained unsolved:

  1. Persistence — Images appeared for ~1 second, then the built-in 132-frame GIF animation resumed
  2. Color encoding — Red and blue were swapped, grays appeared as pink/green
  3. Data corruption — Some images displayed correctly while others were garbled, with no clear pattern

Round 2: Ghidra Static Analysis

We decompiled DeviceDriver.exe (the Windows driver) using Ghidra's headless analyzer. The image upload function (FUN_00422b50) revealed:

  • A 256-byte GIF header must precede the pixel data (we were sending raw pixels)
  • The driver reads back an ACK from the device after every 4096-byte data chunk (we were just blasting writes with a sleep)
  • There is no FINISH command — the 0xF0 command we were sending actually restarts the GIF player, breaking persistence

Adding the GIF header and per-chunk ACK read-back fixed persistence. But colors were still wrong.

Round 3: QEMU USB Capture

To solve the remaining issues, we set up a Windows 10 VM in QEMU with USB passthrough for the keyboard, and captured the Windows driver's USB traffic with Wireshark.

The capture confirmed:

  • IMAGE_CFG byte 2 = 0x03 (we had been using 0x02 — guessed from other projects)
  • RGB565 little-endian pixel format (we had been using big-endian)

But even after matching every byte, our uploads still produced corrupted images. We captured our own USB traffic on Linux and compared the two captures side by side.

The Smoking Gun: Linux hidraw's Silent Byte Stripping

Comparing the Windows and Linux captures revealed that 84 out of 97 data chunks were 4095 bytes instead of 4096. Linux hidraw was silently stripping the first byte of each write when that byte happened to be 0x00, interpreting it as a HID report ID — even though the device's HID descriptor defines no report IDs.

The fix was a single line: prepend \x00 to every data write so the kernel always has a byte to strip:

os.write(fd, b'\x00' + data)  # kernel strips the 0x00, device gets exactly 4096 bytes

This is a known quirk of the Linux HID subsystem for devices with no report IDs. Chunks whose first data byte was non-zero (like the GIF header, or a row of non-black pixels) were sent correctly at 4096 bytes — which is why some images worked fine while others were corrupted. The bug was invisible without byte-level USB traffic comparison.

Compatibility

  • Keyboard: AJAZZ AKS075 (USB wired connection required; 2.4G not supported for screen control)
  • OS: Linux (tested on kernel 6.x)
  • Python: 3.10+
  • Dependencies: Pillow

Acknowledgments

This project builds on the work of several open-source reverse engineering efforts:

License

MIT

Releases

No releases published

Packages

 
 
 

Contributors