Still images (picture page)¶
A still image is a flat, unbanked RGB565 upload to the picture page. To make the module
actually display it, the upload must be paced and committed via PK_ADD_PIC — see
Display commit (PK_*). This page covers the pixel format and byte-map.
TL;DR
- The panel is 96×160 = 30,720 bytes RGB565 big-endian, row-major (NOT 112×137).
- Data = 548 × 56-byte blocks + 1 × 32-byte tail block at offset
0x77E0. - The setup packet
A5 5A 0C <len>isPK_ADD_PIC(the commit), and display attributes are baked into pixels client-side — there is no brightness/saturation opcode.
📟 Panel geometry — 96×160¶
The display is 96 wide × 160 tall = 15,360 px = 30,720 bytes RGB565 big-endian, row-major, top-left origin.
Correction — it is NOT 112×137
The old "112×137 / 30,688 bytes / 548 blocks" figure was wrong. The first capture-analysis
script filtered out a final 32-byte data block (byte[3]=0x20), dropping the last 16
pixels, so every still image built was malformed and never rendered. The firmware length field
proves 96×160: the type-0x0C setup declares 0x7800 = 30,720 = 96×160.
Correct still-image data = 548 × 56-byte blocks + 1 × 32-byte tail block. The 548 blocks cover
offsets 0 … 30687; the tail sits at offset 30,688 (0x77E0); total = 30,720 (0x7800).
📦 Upload sequence¶
announce (0x40, type 0x10): 40 00 00 08 CF 02 00 A5 5A 10 00 01 C5 B1 01
setup (0x41, type 0x0C): 41 00 00 07 21 03 00 A5 5A 0C 78 00 C3 93 ; len 0x7800 = 30,720
data (0x41): 548 × 56-byte blocks + 1 × 32-byte tail (byte[3]=0x20 at 0x77E0)
finish (0x42): 42 00 00 38 7A
The setup packet IS the commit command
A5 5A 0C <len> is PK_ADD_PIC (0x0C) = commit-the-scratch-buffer-and-display, not just a
length declaration. For it to commit you MUST pace the front of the transfer (~300 ms after the
announce, ~30 ms after the setup). And do not send a trailing type 0x0D — that is
PK_TOGGLE_PIC, which advances past the frame you committed. See
Display commit (PK_*).
📦 Data block layout¶
41 [off:2 LE] 38 [cksum:2 LE] 00 <56-byte payload>
| Byte(s) | Field |
|---|---|
[1,2] |
little-endian destination byte-offset into the frame buffer; steps by 56 (0, 56, 112, …) |
[3] |
payload length: 0x38 (56) for the 548 blocks; 0x20 (32) for the final tail |
[4,5] |
16-bit LE additive checksum = (0x41 + offLo + offHi + len + Σpayload) & 0xFFFF (details) |
[6] |
0x00 reserved |
[7..62] |
56 bytes of RGB565 big-endian pixels, row-major from top-left |
0x41 is a generic block-write: the LCD is effectively a dumb framebuffer you push arbitrary
pixels to. A complete frame ends with the 32-byte tail block at 0x77E0 then a 0x42 finish.
🖼️ Pixel format — RGB565 big-endian¶
value = ((R>>3)<<11) | ((G>>2)<<5) | (B>>3)
emitted as [ value>>8, value & 0xFF ] // high byte first
| Color | RGB565 | Bytes |
|---|---|---|
| Red | 0xF800 |
F8 00 |
| Green | 0x07E0 |
07 E0 |
| Blue | 0x001F |
00 1F |
| White | 0xFFFF |
FF FF |
| Black | 0x0000 |
00 00 |
The vendor app resamples any uploaded image to native 96×160 before sending (canvas resize
with imageSmoothingEnabled=false). For pixel-perfect graphics, render directly at 96×160.
🎨 Display attributes are client-side (no device opcode)¶
Brightness, Chroma, Saturation, Grayscale, "Fuzzy", Sharpening emit zero HID traffic: they only update the app's canvas preview and are baked into the pixels on "Save to the device." There is no "set brightness/saturation" opcode to reverse; transform your own RGB565 buffer before sending. The one save-time exception is frame rate (GIF only). See Render a custom frame.