akp02
Linux library for the Ajazz AKP02 9.2" (1920x462) USB secondary display. The AKP02 officially ships with Windows-only software; this project is the result of reverse engineering its USB HID protocol so the panel can be driven natively from Linux.
Confirmed on real hardware: full-screen frames, partial (region) updates including correct color rendering, brightness, screen on/off, clear, firmware version query, boot/display orientation, and sustained keepalive operation.
Install
Arch Linux
Available in the AUR as
python-akp02:
paru -S python-akp02 # or yay, or makepkg -si
The package installs the udev rule for you. Replug the panel afterwards and you're done -- skip the rest of this section.
Other distributions
pip install akp02
Then install the udev rule so the device is usable without root. The rule ships inside the package, so there's no need to clone the repo:
sudo cp "$(python -c 'from importlib.resources import files; print(files("akp02") / "udev" / "99-akp02.rules")')" /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger
Replug the device after installing the rule -- udev only applies it on the next connect.
From a checkout
git clone https://github.com/deonspengler/akp02
cd akp02
pip install .
sudo cp udev/99-akp02.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger
Verify
With the panel connected:
from akp02 import AKP02
with AKP02() as panel:
panel.set_brightness(30)
The backlight should visibly dim. A permissions error here almost always
means the udev rule isn't active yet -- confirm the file is in
/etc/udev/rules.d/ and that you replugged the device.
Note on hidapi
Most Linux users get a prebuilt hidapi wheel and need nothing extra.
If pip falls back to building it from source -- unusual architecture, or
a Python version newer than the available wheels -- install the headers
first:
sudo apt install libhidapi-dev libudev-dev # Debian/Ubuntu
sudo dnf install hidapi-devel systemd-devel # Fedora
sudo pacman -S hidapi # Arch
Library
from akp02 import AKP02
with AKP02() as panel:
panel.start_keepalive() # device sleeps without heartbeats
panel.set_brightness(80)
panel.set_boot_orientation("vertical") # live + persists across power cycles
panel.show(pil_image) # full screen, letterboxed if needed
panel.show(widget, at=(1600, 16)) # partial update, rest preserved
All public methods are thread-safe: a single lock is held across whole multi-report image transfers so the keepalive thread can never interleave with or corrupt a frame. Image composition and JPEG encoding happen outside the lock, so the keepalive thread isn't blocked by them.
A region update sent immediately after a full-frame draw needs a brief settling delay first, or the full frame can silently fail to render at all -- this is tracked and applied automatically; callers don't need to do anything.
Region placement is also corrected automatically: the device renders a
region with the wrong color unless its position satisfies a specific,
confirmed alignment rule (see Protocol notes below). show() nudges
the position by a few pixels when needed and warns when it does, so
at=(x, y) always renders correctly without the caller having to know
about this.
Tests
pip install -e ".[test]"
pytest
105 tests, 100% line and branch coverage, run entirely against a fake
HID device -- no physical hardware or hidapi installation required.
Covers protocol byte-exactness pinned against real captures, the
region color-alignment correction with real hardware-confirmed data
points, and concurrency behavior: dead-thread recovery after a
disconnect, bounded shutdown against a wedged device, and
lock-interleaving prevention verified under real contention.
For linting and type checking as well:
pip install -e ".[dev]"
ruff check .
mypy
Protocol notes (reverse engineered)
Plain USB HID, no encryption. VID:PID 0300:3017. Output reports are
1024 bytes (EP1 OUT), input reports 512 bytes (EP2 IN).
Commands are one zero-padded report:
"CRT" + 00 00 + <mnemonic> + 00 00 + <params>
| Mnemonic | Action |
|---|---|
HAN |
screen off |
DIS |
screen on |
LIG |
brightness (1 param byte, 0-100) |
CONNECT |
heartbeat (device sleeps without periodic heartbeats) |
STP |
commit / render buffered image data |
SET |
boot/display orientation (see below) |
Layout exceptions: CLE (clear) uses a 3-byte gap plus a literal 0xFF
trailer instead of the usual 2-byte gap; VER (firmware version) has a
leading 0x00 device-context byte before CRT, no gap after the
mnemonic, and returns its answer via a synchronous GET_REPORT on the
IN endpoint. Gap sizes are not universal across commands -- verify
per command when adding new ones.
Image transfers: a 32-byte CRT..DRA header (big-endian length =
payload + 0x20, then width/height/x/y as big-endian uint16, all zero
for a full-panel draw) followed immediately by JPEG bytes, chunked into
1024-byte reports, then STP. The JPEG is 462x1920 portrait --
landscape content rotated 90 degrees clockwise (confirmed on
hardware; rotating the other way renders the panel upside down).
Non-zero header coordinates draw a partial region (portrait space);
content outside it is preserved.
Region color alignment (confirmed empirically on real hardware): a
region update renders with the wrong color -- not a placement shift --
unless the position that actually lands in the header
(portrait_x = 462 - y - height, in landscape terms) satisfies
portrait_x % 8 == 2. Root cause understood, not just observed: 462
(the axis this applies to) doesn't divide evenly into 8- or 16-pixel
JPEG blocks the way 1920 (the other axis, which shows no equivalent
sensitivity) does, so the device's firmware evidently pads its buffer
on that axis with a fixed internal offset. The library corrects this
automatically rather than requiring callers to pick special
coordinates.
Settling delay: a region update sent immediately after a full-frame draw can fail to render the full frame at all unless a brief delay (confirmed: 4ms is sufficient, 0ms fails; the library uses 20ms for margin) separates them. Region-after-region and full-after-full both need no delay. Likely cause: a full-frame draw is a clean buffer replace, but a region draw is a read-modify-write against the current framebuffer; if that read starts before the previous commit has actually finished settling internally, the in-flight commit can apparently be corrupted or aborted.
Boot/display orientation (confirmed on real hardware):
"CRT" + 00,00 + "SET" + 00,00 + 0x00 + <orientation byte>, where the
orientation byte is 0x00 for horizontal or 0x01 for vertical.
Found by comparing two real captures of the same action with different
outcomes -- the first capture only ever showed the default value
(indistinguishable from "no parameter"), a second capture with actual
mode switches revealed the real byte. Confirmed to both apply
immediately to the live display and persist as the boot-time
default: verified by physically power-cycling the device after setting
each value and observing the boot splash orientation match.
hidapi note: write() needs a leading 0x00 report-ID placeholder per
report; the kernel strips it before the wire (captures show no
report-ID byte on the wire itself).
License
MPL-2.0
Metadata
Release files for akp02 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| akp02-1.0.0.tar.gz | 32.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| akp02-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 53.5 kB
Release files / akp02-1.0.0.tar.gz
| Download URL | akp02-1.0.0.tar.gz |
|---|---|
| Size | 32.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cf828a10db2832c55811bfcc8feb93afe6c95709fb2ea475c2b7263d99ca14f5
|
|
BLAKE2b-256 checksum How to use checksums |
77500b850863d33800a6af1d6d5c57e142541550d645bb6faf171a15cbbd2bf4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.
Transparency logRelease files / akp02-1.0.0-py3-none-any.whl
| Download URL | akp02-1.0.0-py3-none-any.whl |
|---|---|
| Size | 21.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4fafc4ed149f7187d119b51ca152a8183124b0e245fdd5407efc74acdcf4b8c6
|
|
BLAKE2b-256 checksum How to use checksums |
0c04548c533c99d3b2ae9bf5a693024742a755414419ab093429259b18807f83
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.
Transparency log