{"id":51466638,"url":"https://github.com/gottz/open-picpak","last_synced_at":"2026-07-06T12:01:03.405Z","repository":{"id":366788983,"uuid":"1277505566","full_name":"GottZ/open-PicPak","owner":"GottZ","description":"The open firmware, tooling and documentation the PicPak e-ink frame should have shipped with — no cloud, no lock-in, fully in your hands.","archived":false,"fork":false,"pushed_at":"2026-06-30T19:12:55.000Z","size":891,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"root","last_synced_at":"2026-06-30T19:14:23.035Z","etag":null,"topics":["esp32-c3","firmware","open-source","picpak","reverse-engineering"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/GottZ.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-23T00:40:37.000Z","updated_at":"2026-06-30T19:12:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/GottZ/open-PicPak","commit_stats":null,"previous_names":["gottz/open-picpak"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/GottZ/open-PicPak","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GottZ%2Fopen-PicPak","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GottZ%2Fopen-PicPak/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GottZ%2Fopen-PicPak/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GottZ%2Fopen-PicPak/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/GottZ","download_url":"https://codeload.github.com/GottZ/open-PicPak/tar.gz/refs/heads/root","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GottZ%2Fopen-PicPak/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35189713,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-06T02:00:07.184Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["esp32-c3","firmware","open-source","picpak","reverse-engineering"],"created_at":"2026-07-06T12:01:02.254Z","updated_at":"2026-07-06T12:01:03.376Z","avatar_url":"https://github.com/GottZ.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# open-picpak\n\n\u003e The open firmware, tooling and documentation the PicPak e-ink frame should have\n\u003e shipped with — no cloud, no lock-in, fully in your hands.\n\n**open-picpak** is a clean-room, vendor-independent project around the PicPak 4.2″\ne-ink photo frame: a reverse-engineering reference, runnable tools, a full custom\nfirmware with features the stock product never had, and a self-hostable backend.\nEverything is developed in public.\n\n**Status**\n\n| Component | State |\n|---|---|\n| `documentation/` | **published** |\n| `firmware/` | **in progress** — networked run-cycle + recovery guard + Berry render/config engine + HOTP-authed C2 command channel |\n| `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) |\n| `tools/` | **started** — picpak-ops operator TUI + air-gap gate (on-device validation pending) |\n| `web-usb/` | not public yet |\n\n---\n\n## Contents\n\n- [Motivation](#motivation)\n- [What this is](#what-this-is)\n- [Status](#status) · [what they did](#what-they-did--the-stock-product) · [what I did](#what-i-did) · [what I published](#what-i-published) · [what I planned](#what-i-planned)\n- [Repository layout](#repository-layout)\n- [Documentation](#documentation)\n- [Hardware in one line](#hardware-in-one-line)\n- [Disclaimer](#disclaimer)\n- [Author \u0026 license](#author--license)\n\n---\n\n## Motivation\n\nI backed the Kickstarter because the campaign imagery showed **greens and blues** — I\nassumed I was getting a mature color e-ink panel at a fair price.\n\nWhat shipped was **400 × 300 BWRY**: four colors — black, white, red, yellow. No\ngreen, no blue, no grey. That hope was gone on first refresh.\n\nWorse, the stock firmware does the bare minimum. It is a thin, cloud-tethered image\npusher: the phone app pulls photos and firmware from the vendor cloud and shoves them\nover BLE. None of what I hoped to do with the device is possible out of the box — and\nthe vendor had far longer to build a proper firmware than the **single day** it took\nme to stand up a fully working Home Assistant WiFi pull-frame on my own firmware.\n\nSo I'm building the firmware (and the tools, and the backend) the device deserved —\n**in public**, so the community gets to use it too. I won't stop before every planned\nfeature below is real.\n\n---\n\n## What this is\n\nA full suite, built clean:\n\n- a **documentation / reverse-engineering reference** (what the device actually is),\n- a **custom ESP-IDF firmware** with features the stock never had,\n- a **self-hostable backend** (Docker) that serves per-device content,\n- and the **tools** to flash, configure and feed the device — without any cloud.\n\n\u003e **Air-gapped, no secrets.** This repository carries no real device identifiers — no\n\u003e serial numbers, MAC addresses, RF-calibration blobs, BLE keys or firmware hashes\n\u003e from any physical unit. Every such value is a format-preserving placeholder. Bring\n\u003e your own device. See the stub conventions at the top of\n\u003e [`documentation/device.md`](documentation/device.md).\n\n---\n\n## Status\n\n### What they did — the stock product\n\n- 4.2″ **400 × 300 BWRY** e-ink (black/white/red/yellow only; no green/blue/grey),\n  full-refresh only (~19 s).\n- **ESP32-C3**, **BLE-only** firmware — no WiFi linked at all.\n- The **phone app is a mandatory cloud gateway**: it pulls photos and firmware from\n  the vendor cloud and pushes them to the device over BLE.\n- 700 image slots, no on-device configuration, no scripting, no network of its own.\n\n### What I did\n\n- **Reverse-engineered the device end-to-end:** hardware \u0026 pin map, flash / NVS /\n  eFuse layout, MAC \u0026 serial identity, case-color encoding, the radio/brownout\n  envelope, the full BLE GATT protocol (`0xFF01/02/03` incl. OTA-over-BLE), and the\n  exact RGB→panel image pipeline (BT.601 nearest-color + Atkinson).\n- **Built a fully working Home Assistant WiFi pull-frame in a day** on custom ESP-IDF\n  firmware: wake (timer/button) → WiFi → fetch a server-rendered image → display →\n  deep sleep, with **SHA-256-verified OTA over WiFi** and brownout-hardened transfers.\n  It has since matured into a real run-cycle: refresh **only on a changed frame** (the\n  panel's ~19 s refresh is skipped otherwise), WLAN held across cycles on USB power, a\n  multi-SSID store, and a smart low-battery gate.\n- **Built a recovery guard** — bootloop detection → USB-reachable safe-mode (WiFi/EPD off,\n  esptool + console stay up); reset-reason-agnostic, fresh-app reset, NVS-tunable\n  threshold/stable-uptime. Host-tested + on-device validated.\n- **Built an on-device Berry engine** — *config is a script, not a data file*: one VM does\n  config *and* procedural graphics (text, shapes, QR, live device variables) and renders the\n  frame on-device. RAM feasibility validated on the C3 (VM ~3 KB, coexists with WiFi +\n  framebuffer, no PSRAM); a littlefs key-value store gives scripts persistence.\n- **Built a C2 command channel** — the device **long-polls** a self-hosted backend over HTTPS (a\n  single poll per wake on battery), fetches a **Berry command script and runs it** on a deliberately\n  safe surface (config/NVS writes, read-only queries, store, and reboot/refresh/sleep intents — no\n  drawing, no OTA trigger). Per-device **ECDSA bond + session HOTP** auth (the backend stores only the\n  public key), HTTPS-only because the payload is code, and the ack is persisted before the action so\n  a reboot can't loop.\n\n### What I published\n\n- [x] [`documentation/device.md`](documentation/device.md) — hardware, memory layout,\n      identity, colors, radio/brownout, BLE overview, image format\n- [x] [`documentation/ble-protocol.md`](documentation/ble-protocol.md) — full stock\n      BLE GATT / wire-frame / OTA spec\n- [x] [`documentation/image-pipeline.md`](documentation/image-pipeline.md) —\n      RGB→palette (BT.601 + Atkinson) with a runnable JS reference\n- [x] [`documentation/config-engine.md`](documentation/config-engine.md) — why the config\n      *is* a Berry script (mechanism = pinned C stdlib / policy = script) + the on-device result\n- [x] MPL-2.0 license\n- [x] [`firmware/`](firmware/) — custom ESP-IDF firmware: the **recovery guard** (bootloop →\n      USB-reachable safe-mode; host-tested + on-device validated), a matured **WiFi run-cycle**\n      (content-change gate, WLAN-hold, multi-SSID, smart low-battery gate, SHA-256 OTA), an\n      integrated **Berry render/config engine** (a script renders the frame on-device — text +\n      live device variables + shapes + QR + a Pacman; \"config is a script, not a data file\"), and\n      a **HOTP-authenticated C2 command channel** (poll a backend, run a returned Berry command\n      script over HTTPS). Validates Berry on the C3 (no PSRAM, coexists with WiFi); host-previewable.\n      Battery uses the stock divider/curve recovered by disassembly.\n- [x] [`backend/`](backend/) — self-hostable Docker backend: TimescaleDB + Grafana + the\n      telemetry/OTA/log schema + legacy ingest, the **C2 command channel** (per-device Berry\n      command queue + per-device HOTP auth), and **OTA serving + rollout management** — a single\n      authoritative resolver (per-serial pin ▸ channel fleet ▸ default), firmware register with\n      server-side re-hash, and a download-ticket-authorized `firmware.bin` serve (default-off until\n      the field firmware is measured on-device), plus **device-log reassembly** — the WiFi log\n      ride-along becomes a gapless per-device stream (offset-ack / gap / suspect / monotone seq,\n      idempotent byte-exact fragments) the operator can query and live-tail\n- [x] **operator control plane** (in [`backend/`](backend/)) — a bearer-authenticated admin API in a\n      process and address space **separate from the public ingest parser**: device CRUD + RCE-capable\n      command enqueue, and an **encrypted secrets KV** (AES-256-GCM sealed; the master key is never on\n      disk in the public image and the service fails closed without it). It serves an embedded\n      **operator web UI** — a Svelte 5 SPA (`//go:embed`, one binary, no CORS): key login, a live device\n      roster over server-sent events, and read-only degradation for non-admin keys. The **log viewer**\n      (keyset-paged history, a live SSE tail, gap/suspect markers, and gapless stream reconstruct) and the\n      **telemetry dashboard** (per-device health verdicts, running version / battery / last-seen, a live\n      SSE telemetry tail, and light sparklines — Grafana stays for deep time-series) ship now, as does the\n      **Berry command editor** — a capability-aware CodeMirror editor over the on-device C2 safe subset\n      (autocomplete + client-side lint that flags the render/policy-phase and severing calls, single-device\n      or typed-`'*'`-confirmed fleet enqueue, and an honest cursor-advance feedback model: *delivered +\n      attempted*, never *succeeded*, with a deep-link to the device log for the real outcome). The\n      remaining feature pages — OTA rollout, FaaS editor, Web-USB onboarding — mount into this shell in\n      later waves.\n- [x] [`tools/`](tools/) — host-side tooling: the **picpak-ops** operator TUI\n      (build / flash / console / OTA / telemetry / logs) and the air-gap gate\n\n### What I planned\n\nThe whole point. Re-implementing everything I already have — step by step, so it stays\nclean and gets refined on the way — then going far past what the stock firmware does.\n\n**Documentation**\n- [ ] flashing \u0026 backup guide (esptool params, bootloader mode, stock restore)\n- [ ] reverse-engineering methodology (ESP-image → ELF, radare2, Ghidra, blutter)\n- [ ] custom-firmware notes (adaptive TX, OTA block transfer, streaming-header pitfall)\n\n**`firmware/` — custom ESP-IDF firmware (C)**\n- [~] **config-/logic-engine (Berry, not TOML)** — config *is* a Berry script fetched per wake\n      (rotation, intervals, OTA URL, triggers/actions); the device exposes a small pinned C stdlib,\n      the script decides. Feasibility validated on-device; seed in\n      [`firmware/`](firmware/). WiFi/IP stubbed off until configured.\n- [~] **control actions** — landed as the **C2 command surface**: `reboot` / `refresh` / `sleep`\n      intents plus config/NVS writes as Berry calls, served from the backend. A button-trigger in\n      the config tool is still pending.\n- [~] **embedded fonts + simple shapes** — Adafruit-GFX-style 1-bit glyph blitter (text, lines,\n      rects, discs, triangles, QR codes), ~0 extra RAM; the Berry graphics stdlib in\n      [`firmware/`](firmware/)\n- [~] **self-debug screen** — serial, MACs, chip, uptime, reset, battery; rendered on-device\n      (the `firmware/` scene already shows these)\n- [ ] **playlist \u0026 rotation** — rotate stored images, configurable cycle interval,\n      remote **sync playlist** (poll for updates on an interval — not yet implemented),\n      single-frame remote (Home-Assistant-style backend), OTA URL\n- [ ] **image store** — stored frames, exposed to the config tool as editable\n      color-indexed PNGs\n- [ ] **BLE / WiFi, swappable** — never both at once (RAM budget = the larger, not the\n      sum); configurable swap (BLE for provisioning/OTA ↔ WiFi run-cycle), NimBLE\n- [ ] **BLE-stack re-implementation** — keep the original PicPak app able to manage\n      stored images (on-flash storage format TBD)\n- [ ] **use the upper flash region** — make the unaddressed 16→32 MB usable for bulk\n      image storage *(research — data only, not code)*\n**`backend/` — Docker backend**\n- [~] **scaffold** — TimescaleDB + Grafana + telemetry/OTA/log schema + legacy ingest + the\n      HOTP-authed **C2 command channel** (append-only queue + per-device cursor)\n- [ ] **serial-number differentiation** — serve separate frames per device\n- [ ] single-frame remote backend, generalized from the Home Assistant PoC\n- [ ] serves the `web-usb/` tool as static assets\n\n**`web-usb/` — browser config \u0026 flashing tool (served by the backend)**\n\u003e Replaces a USB mass-storage interface — impossible on the ESP32-C3 (no USB-OTG) —\n\u003e with the same file-like UX over two transports: **Web-USB** (WebSerial, on the\n\u003e cable) and **Web-WiFi** (HTTP, on the network).\n- [ ] flashing \u0026 stock backup / restore (esptool-js — no native install)\n- [ ] config form (writes the device TOML), control-action buttons, image gallery\n- [ ] image editor / uploader (built on the `image-pipeline.md` JS reference)\n\n**`tools/` — host-side tooling**\n- [~] **picpak-ops** — operator deployment TUI (build, flash, console, OTA, telemetry, logs); on-device validation pending\n- [ ] BLE client (push / manage images without the phone app)\n- [ ] dev \u0026 automation helpers\n\n---\n\n## Repository layout\n\n```\ndocumentation/      reference \u0026 reverse-engineering docs         (published)\nfirmware/           custom ESP-IDF firmware: recovery guard + Berry engine + C2  (in progress)\nbackend/            self-hostable Docker backend (TimescaleDB+Grafana+ingest) (in progress)\ntools/              host-side tooling: picpak-ops TUI + air-gap gate (in progress)\nweb-usb/            browser config + flashing tool               (planned)\n```\n\n`web-usb/` and `backend/` are coupled: the backend ships the browser tool as static\nassets, so flashing, uploading and configuring all work from one self-hosted URL.\n\n---\n\n## Documentation\n\n| Doc | What it covers |\n|---|---|\n| [device.md](documentation/device.md) | The hardware: ESP32-C3, the BWRY panel, full pin map, flash/NVS/eFuse layout, identity, colors, radio/brownout, BLE overview |\n| [ble-protocol.md](documentation/ble-protocol.md) | The stock BLE protocol byte-for-byte: GATT layout, wire frame, dispatchers, image upload, OTA-over-BLE |\n| [image-pipeline.md](documentation/image-pipeline.md) | Exact RGB→panel conversion (BT.601 nearest-color + unclamped Atkinson) with a Web-USB-ready JS reference |\n| [config-engine.md](documentation/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/` |\n\n---\n\n## Hardware in one line\n\nESP32-C3 (400 KB SRAM, 16 MB addressed flash) · 4.2″ 400×300 BWRY e-ink (BLE only on\nstock). Full breakdown in [`documentation/device.md`](documentation/device.md).\n\n---\n\n## Disclaimer\n\nopen-picpak is an independent, community-run project. It is **not affiliated with,\nauthorized by, or endorsed by AUTOHEART TECH CO., LTD**, the maker of PicPak.\n\"PicPak\" / \"PICPAK\" and any related names or marks are the property of AUTOHEART TECH\nCO., LTD and are used here only to identify the hardware this project targets.\n\nThis is **clean-room, interoperability** work: it documents and reimplements the\ndevice's behavior through reverse engineering. **No vendor source code, firmware\nbinaries, assets, keys or secrets are included or redistributed** (see the air-gap\nnote above). Studying and testing how a program you own behaves — and reverse\nengineering for interoperability — are expressly permitted under EU law (Directive\n2009/24/EC, Arts. 5–6; in Germany § 69e UrhG), and computer programs \"as such\" are\nnot patentable in the EU (Art. 52 EPC). The author lives in Germany (EU) and is not a\nUS citizen. Comparable open-source projects stand on the same footing despite pressure\nfrom original vendors — e.g. VideoLAN/VLC.\n\nNone of this is legal advice.\n\n---\n\n## Author \u0026 license\n\nCreated and maintained by **Jan-Stefan Janetzky (GottZ)** — [git@gottz.de](mailto:git@gottz.de).\n\nLicensed under the [Mozilla Public License 2.0](LICENSE). Copyright © 2026 Jan-Stefan Janetzky.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgottz%2Fopen-picpak","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgottz%2Fopen-picpak","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgottz%2Fopen-picpak/lists"}