Skip to content

Instantly share code, notes, and snippets.

@casebeer
Last active February 20, 2026 00:48
Show Gist options
  • Select an option

  • Save casebeer/e6bda474efec24e32224a046d85bddf6 to your computer and use it in GitHub Desktop.

Select an option

Save casebeer/e6bda474efec24e32224a046d85bddf6 to your computer and use it in GitHub Desktop.

GICISKY ePaper Display BLE Protocol

Documentation based on the ATC1441 BLE E-Paper Uploader (source on Github) and testing with a 2.9" Black/White/Red epaper display.

BLE characteristics

  • Primary Service
    • Short UUID 0xfef0
    • Long UUID 0000fef0-0000-1000-8000-00805f9b34fb
    • Characteristics
      • "ext control" Characteristic
        • Short UUID 0xfef1
        • Long UUID 0000fef1-0000-1000-8000-00805f9b34fb
        • Write/Notify
        • BLE client and display use the extControl characteristic to send bi-directional commands using writes (client → display) and notifies (display → client).
      • "write block" Characteristic
        • Short UUID 0xfef2
        • Long UUID 0000fef2-0000-1000-8000-00805f9b34fb
        • Write only
        • BLE client writes data part messages to the writeBlock characteristic as instructed by the display. The display will send instructions via notifications to the extControl characteristic (see below).

Control protocol

Image upload transactions are managed via bi-directional commands sent via writes to and notifications from the extControl characteristic (0xfef1). All commands are binary data, with hex representations shown below.

Control protocol sequence

BLE Client
(computer/phone)
action to take
message direction, command, and command hex representation BLE Server
(ePaper display)
action to take
→ requestDataMessageSize()
01
respond with dataMessageSize*
store dataMessageSize ← setDataMessageSize(dataMessageSize)
01 <uint16le dataMessageSize>
→ setImageSize(imageSize)
02 <uint32le imageSize> 00 00 00
store imageSize and ack
← ackImageSize()
02
→ initiateDataTransfer()
03
Begin requesting data segments. Repeat until all data uploaded, then send data transfer complete confirmation.
send requested part number via data transfer protocol (see below) ← requestDataPart(partNumber)
05 00 <uint32le partNumber>
end transaction and disconnect ← confirmDataTransferComplete()
05 08

(*) The dataMessageSize is typically 244d (f4 00 as a uint16le). Other values seen include 101d (uint16le 65 00). dataMessageSize may vary across different clients for the same display. See atc1441 upload page pull request

Data transfer protocol

Transfer data by writing to the writeBlock characteristic (0xfef2) as requested by the display. Image data should be in the correct image format for your display.

The display will send a command requesting the transfer of a particular data part number (via notification from the extControl characteristic).

The client should respond by writing at most dataMessageSize bytes to the writeBlock characteristic. dataMessageSize should have previously been provided by the display in response to the 01 control protocol command.

Data part messages consist of:

  • Four header bytes holding the uint32le encoded data part number
  • Raw binary data bytes

All messages except the message holding the final data part should be exactly dataMessageSize bytes long. The final data part's message should include all remaining image data, and should thus be between 5 bytes and dataMessageSize bytes long, depending on the amount of data remaining.

Data message format

start byte offset field length in bytes description
0 4 uint32le encoded part number
4 dataMessageSize - 4 bytes for all except the final part.

imageSize - partNumber * (dataMessageSize - 4) bytes for the final part.
Raw binary data of image data part specified.

Since all data part messages except the message for the final part must be exactly dataMessageSize bytes, the part number requested by the display specifies the exact byte offset within the image data of the data part to be sent:

dataPartByteOffset(partNumber) = partNumber * (dataMessageSize - 4)
dataPartSize(partNumber) = min(dataMessageSize - 4, imageSize - dataPartByteOffset(partNumber))

Gicisky ePaper Image Format

Documentation based on the ATC1441 BLE E-Paper Uploader (source on Github) and testing with a 2.9" Black/White/Red epaper display.

Background

Gicisky ePaper displays accept image data as a sequence of concatenated binary bitmaps.

BLE Clients send this binary data to the writeBlock BLE characteristic according to the BLE protocol.

Overall format

A Gicisky ePaper image consists of one or more packed binary bitmaps, one bit per pixel, all concatenated together. There are neither headers nor footers.

Bitmaps and colors

The default color for each pixel is black. Each additional color the display supports gets its own bitmap.

The first bitmap in any image will be the white pixel bitmap. Any bits which are true in this bitmap will be white. The second bitmap, if any, will be the red pixel bitmap.

Bitmaps store pixels as one bit per pixel boolean values in normal 2D array row-wise order.

[ white row data ][ white row data ] ... [ red row data ][ red row data ] ...

Display orientation

Note that the displays are normally in "portrait" orientation, making the rows shorter than the columns.

Pixel data starts at the top left corner, proceeds to the top right corner, and then returns to the left-most pixel on the second line from the top, continuing to the right and down until the entire image is complete.

Example

A 296 x 128 pixel Black/White/Red image (for a 2.9" BWR display) has 296 × 128 = 37,888 pixels. Note that this display is in "portrait" orientation, so each row is 128 pixels wide, and there are 296 rows.

Packed into one-bit-per-pixel bitmaps, these 37,888 pixels fit in 4,736 bytes per bitmap.

Since there are two colors in addition to black (i.e. white and red), the overall image will be 2 × 4,736 = 9,472 bytes long.

The layout will be:

[ white pixels (4,736 bytes) ][ red pixels (4,736 bytes) ]

Alternate image format with headers

a.k.a. "compressed" on the atc1441 Web Bluetooth flasher page.

The alternate image format uses the same concatenated per-color bitmap data as the normal format, but adds a four byte overall image length header preceding the image data, and a seven byte row header preceding each row's pixel data. There is no additional header (beyond the following row header) for the individual bitmaps.

[ uint32le image data length ] [ row header ] [ row data ] [ row header ] [ row data ] ...

Image length header format

The image length header, which starts the entire image data block, consists of the length in bytes of the overall image data (including all headers), encoded as a little-endian 32-bit unsigned integer.

start byte offset field length in bytes description example
(binary data displayed as hex)
0 4 uint32le total image data length in bytes, including all headers 34 35 00 00 (= 13,620d, for 296x128 BWR)

n.b. the alternate format image length for the 296 x 128 BWR image above increases from 9,472 bytes in the normal format to 13,620 bytes in the alternate format because of the addition of:

  • 4 bytes overall image data length header
  • 7 byte per row header × 296 rows × 2 per-color bitmaps = 4,144 bytes of row headers

9,472 + 4 + 4,144 = 13,620 bytes of image data with headers

Row header format

Each row header consists of seven bytes:

start byte offset field length in bytes description example
(binary data displayed as hex)
0 1 fixed 0x75 row header indicator 75
1 1 uint8 byte length of row data + header 17 (= 23d = 7d header bytes + 16d row pixel bytes)
2 1 uint8 byte length of row data excluding header 10 (= 16d bytes, for a 128 pixel wide row with 8 pixels per byte)
3 4 four fixed zero bytes 00 00 00 00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment