What it is
A physical Tom Riddle diary. V1: an ESP32-C3 makes its own WiFi AP, a captive portal opens a parchment page on any phone, and the reply types itself onto an OLED. V2 removes the phone and the buzzer: a 2.13″ e-paper book you write on with a stylus, where the answer stays on the page with the power off. Full build analysis of the firmware, the three-pass ink engine and the e-paper redesign, plus a byte-identical mirror.
Version history
Straight from the project’s own card data — what each version added.
ESP32-C3 + OLED + captive portal. Compiles and flashed — firmware.bin 1,026,592 B.
LilyGO T5 V2.4 2.13″ e-paper port in progress: GxEPD2, partial refresh, stylus overlay, no phone, no buzzer.
Files on the site
Full analysis
The one-line brief that deleted the first design
“We're not using phones, we're using the book itself only.” Everything in this project is downstream of that sentence. V1 needed a phone to be the keyboard, the display and the settings panel. A diary that needs a phone is a website with a costume on. V2 has to be a closed object: a board, a panel, a battery, and a hand.
What V1 actually is
An ESP32-C3 SuperMini with a 0.96″/2.42″ SSD1306-family OLED over I²C (SDA 10, SCL 7, address 0x3C — never GPIO2/8/9, which are strapping pins and produce a board that sometimes refuses to boot). It raises an access point called RiddlesDiary, and a captive portal forces a parchment page open on any phone that joins. You type; the device calls an OpenAI-compatible endpoint; the reply types itself onto the OLED and onto the page.
579 lines of C++, a 1,026,592-byte firmware.bin, and no filesystem image — the entire web UI lives in flash as a R"rawliteral(...)" block, so there is exactly one thing to flash.
A captive portal is seven shims, not a feature
This is the part nobody warns you about. Nine routes exist so that no operating system wins the argument: /generate_204 for Android, /hotspot-detect.html and /library/test/success.html for iOS, /connecttest.txt and /ncsi.txt for Windows, /canonical.html for Firefox, /redirect for Microsoft — plus a wildcard DNS server that answers every lookup with 192.168.4.1, plus onNotFound 302ing anything else back to the page. Take one route away and the portal silently fails on exactly one platform, which is the worst kind of bug: it works on your phone.
Firmware anatomy
loop() does three things — pump DNS, pump HTTP, animate the thinking dots every 300 ms. No scheduler, no tasks, no heap churn: the request and response JSON documents are statically sized (StaticJsonDocument<2048> / <1024>), so a diary left on for a week does not fragment itself to death.
Text layout matters more than it sounds: 128 px at the default font is 21 columns, so the renderer wraps on the last space or punctuation across six lines and overflows to “…”. The reply budget — 200 tokens, under 300 characters — is not an aesthetic choice, it is the size of the page. On the V2 panel it is the same number for the same reason.
And the API key is not in the firmware. The device accepts it over /settings and keeps it in RAM. Flash a diary for someone else and it ships with no credential inside it.
The ink engine
The browser twin is where the feel lives. Every stroke is three overlapping passes: a bleed pass 2.5 px wider at alpha 0.12, a body pass whose colour jitters per segment between rgb(26,15,5) and rgb(34,20,10), and a centre pass at 0.3× width that makes a thick stroke look pressed rather than painted. Width comes from stroke speed — 1.2 px when you move fast, 3.5 px when you slow down — smoothed with a quadratic curve through point midpoints. The canvas is DPR-scaled and re-blitted on resize, pointer capture keeps a stroke alive when your hand leaves the page, and the leaf clears itself after each answer.
The honest limit
The canvas is exported as a PNG data-URL and then thrown away. All the code does with those pixels is scan the alpha channel to answer “has the user written anything?”. The model never sees your handwriting — draw without typing and it receives a fixed instruction saying the writer has drawn something illegible, then asks you to say it in words. The handwriting is theatre, not OCR. Making it real means a vision model on the image or an on-device recogniser, and both are a different, much bigger project. That sentence belongs on the page rather than in a footnote, because it is the difference between a magic trick and a lie.
V2 — why paper, and what paper costs
The target is a LilyGO T5 V2.4: ESP32 dual-core LX6 at 240 MHz, 4 MB flash, no PSRAM, a 2.13″ 122×250 SPI e-paper panel, ~2 s full refresh, ~300 µA idle, panel power gated by GPIO12. It replaces six jumper wires with one PCB and trades about ¥45 for the privilege.
Three things follow from the panel, and none of them are negotiable:
- Refresh latency becomes characterisation. Two seconds is a disaster for a UI and perfect for a Horcrux: the page blanks, then the answer is simply there — closer to the film than a typewriter effect ever was.
- Ghosting is a feature you have to control. Partial refresh leaves remnants of the previous line, so the firmware has to alternate partial refreshes with an occasional full one or the page slowly accumulates every conversation.
- 122 px is not a writing surface. At roughly 130 dpi a 1.5 mm nib is 8 pixels wide — two or three legible words per line. So the panel is the answer surface, and the overlay captures one short line to send as text. Trying to render live handwriting on bistable e-paper would look like a fax from 1988 and refresh at the speed of one.
Power, roughly: 300 µA standby on a 500 mAh cell is about 69 days untouched; one exchange costs ~0.2 mAh (WiFi TX ~200 mA for ~3 s), so twenty a day is ~4 mAh. The idle floor dominates — deep sleep between writes matters more than anything the radio does. E-paper also pulls a current spike at refresh, so a tired cell browns out mid-page; gate the refresh on battery voltage or you ship half a sentence.
The input problem nobody warns you about
The T5 has no touch. With the phone banned, something on the book has to accept handwriting, and every option is imperfect. A 2.13″ 4-wire resistive overlay with an XPT2046 costs ¥11–17 and four GPIOs, works with any plastic stylus, and never registers a palm. Three buttons cost nothing and remove all the magic. And the 14-pin “2.4″ SPI touch” module is a trap: that pinout is almost always ILI9341 TFT plus XPT2046 — a glossy backlit LCD, the exact opposite of a paper page. Check that the listing says 墨水屏.
Buzzer and phone: deliberately cut
The buzzer went first. A silent book is creepier than a chirping one, and it removed a part, a pin and a state. The phone went second, and it removed the whole portal — which is most of the cleverness in V1 and none of the product.
What I'd change
- Write the constraint down before the BOM. “Book only” arrived after a working build, and it invalidated the architecture, not just the parts list.
- Treat the input device as part of the concept. The display was researched for a week; the stylus was an afterthought, and it is the harder problem.
- Do not build the portal twice. Nine routes of OS-compat shims exist to support a phone that V2 doesn't have.
- Say the limitation out loud in the artefact. The canvas-not-OCR fact is the single most useful sentence on the analysis page, and it started as something I would rather not mention.
What was verified, and what was not
Compiled and flashed: V1 (firmware.bin 1,026,592 B), the portal on iOS and Android probe paths, the no-key and bad-key error text. Live: the analysis page, the byte-identical mirror (sha256 b04e2f6a…9839b8, 20,363 B — the served copy is 757 B longer, and every one of those bytes is a deploy-injected tag: the site skin script plus Cloudflare's analytics beacon), the canned demo, and every card action, checked in headless Chromium with zero console errors. Not done: the V2 e-paper port has not been flashed. It says “in progress” on the card and on the page, because the board has not been in my hand yet.
Build spec
The project’s own build notes, rendered. Open the raw .md ↗
Show the spec
Build spec — Harry Potter AI Diary (Tom Riddle's diary, as hardware)
Rebuild brief for a one-input device: you write on the page, something writes back.
V1 — shipped (ESP32-C3 + OLED + phone portal)
- MCU: ESP32-C3 SuperMini (pre-soldered header), PlatformIO
esp32-c3-devkitm-1 - Display: 0.96″ / 2.42″ SSD1306-family OLED, I²C, addr
0x3C— SDA GPIO10, SCL GPIO7 - AP:
RiddlesDiary/12345678, static192.168.4.1 - Captive portal:
DNSServerwildcard*→ 192.168.4.1, plus explicit routes for - API:
POST /ask(text/plain in/out) →HTTPClient→ OpenAI-compatible endpoint; - UI: whole page in PROGMEM as
R"rawliteral(...)"— no filesystem image - AI params: 200 max tokens, temperature 0.9, 15 s timeout,
StaticJsonDocument<2048>req /<1024>resp - Persona: Tom Riddle Horcrux system prompt; 2–4 sentences, <300 chars, never break character
- States: splash → thinking (dots every 300 ms) → reply / “The ink fades…” + HTTP code
- Libraries: Adafruit SSD1306, Adafruit GFX, ArduinoJson
- Artifact:
firmware.bin1,026,592 B ·src/main.cpp579 lines / 20,320 B
(never GPIO2/8/9: C3 strapping pins)
/generate_204 (Android), /hotspot-detect.html + /library/test/success.html (iOS), /connecttest.txt + /ncsi.txt (Windows), /canonical.html (Firefox), /redirect (Microsoft), and onNotFound → 302 to the page
POST /settings (JSON {key,endpoint,model}), key held in RAM only
V1.5 — browser twin (the mirror)
- Canvas 560×360 (0.64 aspect), DPR-scaled, re-blit on resize
- Ink = three passes: bleed (+2.5 px, alpha .12) → body (per-segment colour jitter rgb(26–34,15–20,5–10))
setPointerCapture, touch-scroll suppressed, leaf cleared after each reply, typewriter 25–45 ms/char- Settings in
localStorage(diary_settings); key never in page source - Known limit: the canvas PNG is discarded — pixels only answer “has the user written anything?”.
- Mirror is byte-identical to
~/Desktop/harry-potter-diary.html
→ centre line (0.3× width); width from stroke speed, 1.2–3.5 px, quadratic smoothing through midpoints
Drawing alone sends a fixed “illegible handwriting” instruction. No OCR, no vision call.
(sha256 b04e2f6ac5c5c1e8cb7eb0d277c64a6d3ec20cc74da71e7b7138def3619839b8, 20,363 B)
V2 — e-paper book (in port, not flashed)
- Board: LilyGO T5 V2.4 — ESP32 dual-core LX6 240 MHz, 4 MB flash, no PSRAM
- Panel: 2.13″ SPI e-paper 122×250; DEPG0213BN (2-grey) or GDEM0213B74 (4-grey) — check which
- Refresh: ~2 s full, partial refresh supported; panel power gated by GPIO12; ~300 µA idle
- Driver: GxEPD2 (swap out Adafruit SSD1306); layout for 122 px wide ≈ 15 chars/line at readable size
- Ink behaviour: blank the page, partial-refresh the reply in, full refresh on page turn;
- Power: 500 mAh LiPo (502030), deep sleep between writes, voltage check before refresh (brown-out
- Input (open): 2.13″ 4-wire resistive overlay + XPT2046 (¥11–17, 4 GPIOs) — or three buttons.
- Cut on purpose: phone/captive portal, buzzer (silent book), per-char typewriter (e-paper is bistable:
- Keep: same persona prompt, same
POST /askshape, same JSON schema, same 300-char reply budget
periodically force a full refresh to clear accumulated ghosting
mid-page leaves half a sentence)
A 14-pin “2.4″ SPI touch” module is usually ILI9341 TFT + XPT2046, i.e. LCD, not 墨水屏 — verify.
the answer should simply be there)
Verification checklist
pio run -e esp32-c3-devkitm-1clean; notefirmware.binsize- Portal opens unprompted on iOS and Android (both probe paths)
POST /askwith no key → in-character refusal, no crash- Wrong key → HTTP 401 surfaced in the reply text
- V2: partial refresh leaves <1 full-refresh of ghosting after 20 replies
- V2: 24 h idle → no visible fade, battery delta logged