The open firmware, tooling and documentation the PicPak e-ink frame should have shipped with — no cloud, no lock-in, fully in your hands.
open-picpak is a clean-room, vendor-independent project around the PicPak 4.2″ e-ink photo frame: a reverse-engineering reference, runnable tools, a full custom firmware with features the stock product never had, and a self-hostable backend. Everything is developed in public.
Status
| Component | State |
|---|---|
documentation/ |
published |
firmware/ |
in progress — networked run-cycle + recovery guard + Berry render/config engine + HOTP-authed C2 command channel |
backend/ |
in progress — TimescaleDB + Grafana + telemetry/OTA/log schema + ingest + HOTP-authed C2 command channel, plus a bearer-authenticated operator control plane (admin API, encrypted secrets KV, embedded Svelte operator web UI) |
tools/ |
started — picpak-ops operator TUI + air-gap gate (on-device validation pending) |
web-usb/ |
not public yet |
- Motivation
- What this is
- Status · what they did · what I did · what I published · what I planned
- Repository layout
- Documentation
- Hardware in one line
- Disclaimer
- Author & license
I backed the Kickstarter because the campaign imagery showed greens and blues — I assumed I was getting a mature color e-ink panel at a fair price.
What shipped was 400 × 300 BWRY: four colors — black, white, red, yellow. No green, no blue, no grey. That hope was gone on first refresh.
Worse, the stock firmware does the bare minimum. It is a thin, cloud-tethered image pusher: the phone app pulls photos and firmware from the vendor cloud and shoves them over BLE. None of what I hoped to do with the device is possible out of the box — and the vendor had far longer to build a proper firmware than the single day it took me to stand up a fully working Home Assistant WiFi pull-frame on my own firmware.
So I'm building the firmware (and the tools, and the backend) the device deserved — in public, so the community gets to use it too. I won't stop before every planned feature below is real.
A full suite, built clean:
- a documentation / reverse-engineering reference (what the device actually is),
- a custom ESP-IDF firmware with features the stock never had,
- a self-hostable backend (Docker) that serves per-device content,
- and the tools to flash, configure and feed the device — without any cloud.
Air-gapped, no secrets. This repository carries no real device identifiers — no serial numbers, MAC addresses, RF-calibration blobs, BLE keys or firmware hashes from any physical unit. Every such value is a format-preserving placeholder. Bring your own device. See the stub conventions at the top of
documentation/device.md.
- 4.2″ 400 × 300 BWRY e-ink (black/white/red/yellow only; no green/blue/grey), full-refresh only (~19 s).
- ESP32-C3, BLE-only firmware — no WiFi linked at all.
- The phone app is a mandatory cloud gateway: it pulls photos and firmware from the vendor cloud and pushes them to the device over BLE.
- 700 image slots, no on-device configuration, no scripting, no network of its own.
- Reverse-engineered the device end-to-end: hardware & pin map, flash / NVS /
eFuse layout, MAC & serial identity, case-color encoding, the radio/brownout
envelope, the full BLE GATT protocol (
0xFF01/02/03incl. OTA-over-BLE), and the exact RGB→panel image pipeline (BT.601 nearest-color + Atkinson). - Built a fully working Home Assistant WiFi pull-frame in a day on custom ESP-IDF firmware: wake (timer/button) → WiFi → fetch a server-rendered image → display → deep sleep, with SHA-256-verified OTA over WiFi and brownout-hardened transfers. It has since matured into a real run-cycle: refresh only on a changed frame (the panel's ~19 s refresh is skipped otherwise), WLAN held across cycles on USB power, a multi-SSID store, and a smart low-battery gate.
- Built a recovery guard — bootloop detection → USB-reachable safe-mode (WiFi/EPD off, esptool + console stay up); reset-reason-agnostic, fresh-app reset, NVS-tunable threshold/stable-uptime. Host-tested + on-device validated.
- Built an on-device Berry engine — config is a script, not a data file: one VM does config and procedural graphics (text, shapes, QR, live device variables) and renders the frame on-device. RAM feasibility validated on the C3 (VM ~3 KB, coexists with WiFi + framebuffer, no PSRAM); a littlefs key-value store gives scripts persistence.
- Built a C2 command channel — the device long-polls a self-hosted backend over HTTPS (a single poll per wake on battery), fetches a Berry command script and runs it on a deliberately safe surface (config/NVS writes, read-only queries, store, and reboot/refresh/sleep intents — no drawing, no OTA trigger). Per-device ECDSA bond + session HOTP auth (the backend stores only the public key), HTTPS-only because the payload is code, and the ack is persisted before the action so a reboot can't loop.
-
documentation/device.md— hardware, memory layout, identity, colors, radio/brownout, BLE overview, image format -
documentation/ble-protocol.md— full stock BLE GATT / wire-frame / OTA spec -
documentation/image-pipeline.md— RGB→palette (BT.601 + Atkinson) with a runnable JS reference -
documentation/config-engine.md— why the config is a Berry script (mechanism = pinned C stdlib / policy = script) + the on-device result - MPL-2.0 license
-
firmware/— custom ESP-IDF firmware: the recovery guard (bootloop → USB-reachable safe-mode; host-tested + on-device validated), a matured WiFi run-cycle (content-change gate, WLAN-hold, multi-SSID, smart low-battery gate, SHA-256 OTA), an integrated Berry render/config engine (a script renders the frame on-device — text + live device variables + shapes + QR + a Pacman; "config is a script, not a data file"), and a HOTP-authenticated C2 command channel (poll a backend, run a returned Berry command script over HTTPS). Validates Berry on the C3 (no PSRAM, coexists with WiFi); host-previewable. Battery uses the stock divider/curve recovered by disassembly. -
backend/— self-hostable Docker backend: TimescaleDB + Grafana + the telemetry/OTA/log schema + legacy ingest, the C2 command channel (per-device Berry command queue + per-device HOTP auth), and OTA serving + rollout management — a single authoritative resolver (per-serial pin ▸ channel fleet ▸ default), firmware register with server-side re-hash, and a download-ticket-authorizedfirmware.binserve (default-off until the field firmware is measured on-device), plus device-log reassembly — the WiFi log ride-along becomes a gapless per-device stream (offset-ack / gap / suspect / monotone seq, idempotent byte-exact fragments) the operator can query and live-tail - operator control plane (in
backend/) — a bearer-authenticated admin API in a process and address space separate from the public ingest parser: device CRUD + RCE-capable command enqueue, and an encrypted secrets KV (AES-256-GCM sealed; the master key is never on disk in the public image and the service fails closed without it). It serves an embedded operator web UI — a Svelte 5 SPA (//go:embed, one binary, no CORS): key login, a live device roster over server-sent events, and read-only degradation for non-admin keys. The log viewer (keyset-paged history, a live SSE tail, gap/suspect markers, and gapless stream reconstruct) and the telemetry dashboard (per-device health verdicts, running version / battery / last-seen, a live SSE telemetry tail, and light sparklines — Grafana stays for deep time-series) ship now, as does the Berry command editor — a capability-aware CodeMirror editor over the on-device C2 safe subset (autocomplete + client-side lint that flags the render/policy-phase and severing calls, single-device or typed-'*'-confirmed fleet enqueue, and an honest cursor-advance feedback model: delivered + attempted, never succeeded, with a deep-link to the device log for the real outcome). The remaining feature pages — OTA rollout, FaaS editor, Web-USB onboarding — mount into this shell in later waves. -
tools/— host-side tooling: the picpak-ops operator TUI (build / flash / console / OTA / telemetry / logs) and the air-gap gate
The whole point. Re-implementing everything I already have — step by step, so it stays clean and gets refined on the way — then going far past what the stock firmware does.
Documentation
- flashing & backup guide (esptool params, bootloader mode, stock restore)
- reverse-engineering methodology (ESP-image → ELF, radare2, Ghidra, blutter)
- custom-firmware notes (adaptive TX, OTA block transfer, streaming-header pitfall)
firmware/ — custom ESP-IDF firmware (C)
- [~] config-/logic-engine (Berry, not TOML) — config is a Berry script fetched per wake
(rotation, intervals, OTA URL, triggers/actions); the device exposes a small pinned C stdlib,
the script decides. Feasibility validated on-device; seed in
firmware/. WiFi/IP stubbed off until configured. - [~] control actions — landed as the C2 command surface:
reboot/refresh/sleepintents plus config/NVS writes as Berry calls, served from the backend. A button-trigger in the config tool is still pending. - [~] embedded fonts + simple shapes — Adafruit-GFX-style 1-bit glyph blitter (text, lines,
rects, discs, triangles, QR codes), ~0 extra RAM; the Berry graphics stdlib in
firmware/ - [~] self-debug screen — serial, MACs, chip, uptime, reset, battery; rendered on-device
(the
firmware/scene already shows these) - playlist & rotation — rotate stored images, configurable cycle interval, remote sync playlist (poll for updates on an interval — not yet implemented), single-frame remote (Home-Assistant-style backend), OTA URL
- image store — stored frames, exposed to the config tool as editable color-indexed PNGs
- BLE / WiFi, swappable — never both at once (RAM budget = the larger, not the sum); configurable swap (BLE for provisioning/OTA ↔ WiFi run-cycle), NimBLE
- BLE-stack re-implementation — keep the original PicPak app able to manage stored images (on-flash storage format TBD)
- use the upper flash region — make the unaddressed 16→32 MB usable for bulk
image storage (research — data only, not code)
backend/— Docker backend - [~] scaffold — TimescaleDB + Grafana + telemetry/OTA/log schema + legacy ingest + the HOTP-authed C2 command channel (append-only queue + per-device cursor)
- serial-number differentiation — serve separate frames per device
- single-frame remote backend, generalized from the Home Assistant PoC
- serves the
web-usb/tool as static assets
web-usb/ — browser config & flashing tool (served by the backend)
Replaces a USB mass-storage interface — impossible on the ESP32-C3 (no USB-OTG) — with the same file-like UX over two transports: Web-USB (WebSerial, on the cable) and Web-WiFi (HTTP, on the network).
- flashing & stock backup / restore (esptool-js — no native install)
- config form (writes the device TOML), control-action buttons, image gallery
- image editor / uploader (built on the
image-pipeline.mdJS reference)
tools/ — host-side tooling
- [~] picpak-ops — operator deployment TUI (build, flash, console, OTA, telemetry, logs); on-device validation pending
- BLE client (push / manage images without the phone app)
- dev & automation helpers
documentation/ reference & reverse-engineering docs (published)
firmware/ custom ESP-IDF firmware: recovery guard + Berry engine + C2 (in progress)
backend/ self-hostable Docker backend (TimescaleDB+Grafana+ingest) (in progress)
tools/ host-side tooling: picpak-ops TUI + air-gap gate (in progress)
web-usb/ browser config + flashing tool (planned)
web-usb/ and backend/ are coupled: the backend ships the browser tool as static
assets, so flashing, uploading and configuring all work from one self-hosted URL.
| Doc | What it covers |
|---|---|
| device.md | The hardware: ESP32-C3, the BWRY panel, full pin map, flash/NVS/eFuse layout, identity, colors, radio/brownout, BLE overview |
| ble-protocol.md | The stock BLE protocol byte-for-byte: GATT layout, wire frame, dispatchers, image upload, OTA-over-BLE |
| image-pipeline.md | Exact RGB→panel conversion (BT.601 nearest-color + unclamped Atkinson) with a Web-USB-ready JS reference |
| config-engine.md | Why the config is a Berry script (mechanism = pinned C stdlib / policy = script), the on-device feasibility result, and the stdlib seeded in firmware/ |
ESP32-C3 (400 KB SRAM, 16 MB addressed flash) · 4.2″ 400×300 BWRY e-ink (BLE only on
stock). Full breakdown in documentation/device.md.
open-picpak is an independent, community-run project. It is not affiliated with, authorized by, or endorsed by AUTOHEART TECH CO., LTD, the maker of PicPak. "PicPak" / "PICPAK" and any related names or marks are the property of AUTOHEART TECH CO., LTD and are used here only to identify the hardware this project targets.
This is clean-room, interoperability work: it documents and reimplements the device's behavior through reverse engineering. No vendor source code, firmware binaries, assets, keys or secrets are included or redistributed (see the air-gap note above). Studying and testing how a program you own behaves — and reverse engineering for interoperability — are expressly permitted under EU law (Directive 2009/24/EC, Arts. 5–6; in Germany § 69e UrhG), and computer programs "as such" are not patentable in the EU (Art. 52 EPC). The author lives in Germany (EU) and is not a US citizen. Comparable open-source projects stand on the same footing despite pressure from original vendors — e.g. VideoLAN/VLC.
None of this is legal advice.
Created and maintained by Jan-Stefan Janetzky (GottZ) — git@gottz.de.
Licensed under the Mozilla Public License 2.0. Copyright © 2026 Jan-Stefan Janetzky.