- Fix TerminalDisplay: add screen clear each frame (cursor home + erase down) - Fix CameraStage: use set_canvas_size instead of read-only viewport properties - Fix Glitch effect: preserve visible line lengths, remove cursor positioning - Fix Fade effect: return original line when fade=0 instead of empty string - Fix Noise effect: use input line length instead of terminal_width - Remove HUD effect from all presets (redundant with border FPS display) - Add regression tests for effect dimension stability - Add docs/ARCHITECTURE.md with Mermaid diagrams - Add mise tasks: diagram-ascii, diagram-validate, diagram-check - Move markdown docs to docs/ (ARCHITECTURE, Refactor, hardware specs) - Remove redundant requirements files (use pyproject.toml) - Add *.dot and *.png to .gitignore Closes #25
17 KiB
Klubhaus Doorbell — Hardware Spec Sheet
Derived from source code analysis of klubhaus-doorbell and manufacturer datasheets.
The project targets three ESP32-based development boards, each with an integrated TFT display and touch input. All three are all-in-one "ESP32 + screen" modules (not a bare Arduino with a separate breakout).
Board 1 — ESP32-32E (2.8″ ILI9341)
| Attribute | Value |
|---|---|
| Manufacturer | Hosyond (likely) |
| Module | ESP32-32E (ESP32-WROOM-32 family) |
| SoC | ESP32-D0WD-V3, Xtensa dual-core 32-bit LX6 |
| Max clock | 240 MHz |
| SRAM | 520 KB |
| ROM | 448 KB |
| Flash | 4 MB (external QSPI) — per FQBN FlashSize=4M |
| PSRAM | Flagged in build (-DBOARD_HAS_PSRAM) — likely present but unconfirmed capacity |
| WiFi | 2.4 GHz 802.11 b/g/n |
| Bluetooth | v4.2 BR/EDR + BLE |
| Display driver | ILI9341 |
| Display size | ~2.8″ (inferred from .crushmemory note "original 2.8″ ILI9341") |
| Resolution | 320 × 240 (landscape rotation 1) |
| Display interface | 4-line SPI |
| Color depth | 65K (RGB565) |
| Touch | XPT2046 resistive (SPI) |
| Backlight GPIO | 22 |
| SPI pins | MOSI 23, SCLK 18, CS 5, DC 27, RST 33 |
| Touch CS | GPIO 14 |
| SPI clock | 40 MHz (display), 20 MHz (read), 2.5 MHz (touch) |
| Serial baud | 115200 |
| USB | Type-C (programming / power) |
| Display library | TFT_eSPI (vendored) |
Board 2 — ESP32-32E-4″ (Hosyond 4.0″ ST7796)
| Attribute | Value |
|---|---|
| Manufacturer | Hosyond |
| Module | ESP32-32E |
| SoC | ESP32-D0WD-V3, Xtensa dual-core 32-bit LX6 |
| Max clock | 240 MHz |
| SRAM | 520 KB |
| ROM | 448 KB |
| Flash | 4 MB (external QSPI) — per FQBN FlashSize=4M |
| PSRAM | Flagged in build (-DBOARD_HAS_PSRAM) — likely present but unconfirmed capacity |
| WiFi | 2.4 GHz 802.11 b/g/n |
| Bluetooth | v4.2 BR/EDR + BLE |
| Display driver | ST7796S |
| Display size | 4.0″ |
| Resolution | 320 × 480 (landscape rotation 1) |
| Display interface | 4-line SPI |
| Color depth | 262K (RGB666) per manufacturer; firmware uses RGB565 |
| Touch | XPT2046 resistive (SPI) |
| Backlight GPIO | 27 (active HIGH) |
| SPI pins | MISO 12, MOSI 13, SCLK 14, CS 15, DC 2, RST tied to EN |
| Touch CS / IRQ | GPIO 33 / GPIO 36 |
| SPI clock | 40 MHz (display), 20 MHz (read), 2.5 MHz (touch) |
| Serial baud | 115200 |
| Physical size | 60.88 × 111.11 × 5.65 mm |
| USB | Type-C |
| Display library | TFT_eSPI (vendored) |
| Reference | lcdwiki.com/4.0inch_ESP32-32E_Display |
Board 3 — Waveshare ESP32-S3-Touch-LCD-4.3
| Attribute | Value |
|---|---|
| Manufacturer | Waveshare |
| Module | ESP32-S3-WROOM-1-N16R8 |
| SoC | ESP32-S3, Xtensa dual-core 32-bit LX7 |
| Max clock | 240 MHz |
| SRAM | 512 KB |
| ROM | 384 KB |
| PSRAM | 8 MB (onboard) |
| Flash | 16 MB — per FQBN FlashSize=16M |
| WiFi | 2.4 GHz 802.11 b/g/n |
| Bluetooth | v5.0 BLE |
| Antenna | Onboard PCB antenna |
| Display driver | RGB parallel (16-bit, 5-6-5 R/G/B channel split) |
| Display size | 4.3″ |
| Resolution | 800 × 480 |
| Color depth | 65K |
| Touch | GT911 capacitive, 5-point, I2C |
| Touch I2C | SDA 8, SCL 9, INT 4, addr 0x14 (runtime) / 0x5D (defined in config) |
| I/O expander | CH422G (I2C, shared bus with touch) — controls backlight, LCD reset, SD CS, etc. |
| Pixel clock | 14 MHz (LovyanGFX Bus_RGB) |
| USB | Type-C (UART via CH343P + native USB HW CDC on GPIO 19/20) |
| Peripheral interfaces | CAN, RS485, I2C, UART, TF card slot (SPI via CH422G EXIO4), ADC sensor header |
| Partition scheme | app3M_fat9M_16MB (3 MB app, 9 MB FAT) |
| Serial baud | 115200 |
| Display library | LovyanGFX (vendored) |
| Reference | waveshare.com/wiki/ESP32-S3-Touch-LCD-4.3 |
RGB bus pin map (Board 3)
| Signal | GPIOs |
|---|---|
| Red R0–R4 | 1, 2, 42, 41, 40 |
| Green G0–G5 | 39, 0, 45, 48, 47, 21 |
| Blue B0–B4 | 14, 38, 18, 17, 10 |
| DE / VSYNC / HSYNC / PCLK | 5, 3, 46, 7 |
Known unknowns
These are facts the code references or implies but does not pin down:
- PSRAM size on Boards 1 & 2. The build flag
-DBOARD_HAS_PSRAMis set for both ESP32-32E targets, but the capacity (typically 4 MB or 8 MB on ESP32-WROOM-32 variants) is never stated in the code or config. Hosyond product pages list some models with PSRAM and some without. - Exact screen panel size for Board 1. The
.crushmemoryfile calls it "original 2.8″ ILI9341," but the ILI9341 driver is also used on 2.4″ and 3.2″ panels. No board_config.h comment names the panel size explicitly. - Board 1 manufacturer. The code doesn't name Hosyond for the 2.8″ board the way it does for the 4″. It could be a generic ESP32-32E devkit from any number of vendors.
- Board 1 SPI MISO pin. Not defined in
tft_user_setup.h(only MOSI/SCLK/CS/DC/RST are set). This means SPI read-back from the display may not be wired or used. - Serial port path. All three
board-config.shfiles default to/dev/ttyUSB0, which is a Linux convention. The actual development machine appears to be macOS, so the real port (e.g./dev/cu.usbserial-*) is likely overridden at runtime. - GT911 I2C address discrepancy (Board 3).
board_config.hdefinesGT911_ADDR 0x5DbutLovyanPins.hconfigures the touch at address0x14. Both are valid GT911 addresses; the runtime address depends on the INT pin state at boot. The code comment says "IMPORTANT: Address 0x14, not 0x5D!" suggesting 0x5D was tried and didn't work. - CH422G expander pin mapping (Board 3).
LovyanPins.hdefines symbolic names (TP_RST=1,LCD_BL=2,LCD_RST=3,SD_CS=4,USB_SEL=5) but these are CH422G expander output indices, not ESP32 GPIOs. The I2C init sequence that drives these pins lives inDisplayDriverGFX.cpp, which was not fully inspected.
Unknown unknowns
Things that are plausibly relevant but entirely absent from the codebase:
- Power supply specs. No code references input voltage ranges, regulators, or battery charging circuits, though the Waveshare board has a PH2.0 LiPo header and the Hosyond boards support external lithium batteries with onboard charge management.
- Thermal limits / operating temperature range. Not mentioned anywhere.
- Hardware revision / PCB version. No version identifiers for any of the three physical boards.
- Antenna characteristics. The Waveshare board uses an onboard PCB antenna; the Hosyond boards likely do as well. Gain, radiation pattern, and any shielding considerations are unaddressed.
- Display viewing angle / brightness / contrast. The Hosyond 4″ is listed as TN type (narrower viewing angles); the Waveshare 4.3″ is likely IPS but not confirmed in code.
- ESD / EMC compliance. No mention of certifications (FCC, CE, etc.).
- Deep sleep / low-power modes. The firmware uses
millis()-based timing and a display-off state, but never enters ESP32 deep sleep. Whether the hardware supports wake-on-touch or wake-on-WiFi is not explored. - Audio hardware. The Hosyond boards support external speakers per their datasheets, and the codebase has no audio code. The Waveshare board does not appear to have onboard audio.
- SD card. The Waveshare board has a TF card slot (CS via CH422G EXIO4), and the Hosyond boards have TF card slots as well. The firmware does not use storage.
Questions for board owner
I'm looking at displaying mainline.py — a scrolling news/poetry consciousness stream — on one of these boards. The plan ("Mainline Renderer + ntfy Message Queue for ESP32") uses a server + thin client architecture: a Python server pre-renders headlines and serves them via HTTP; the ESP32 just displays pre-rendered bitmaps, applies gradient/glitch effects locally, and polls ntfy.sh/klubhaus_terminal_mainline for messages that interrupt the news scroll.
To build this I need the following from you:
1. Which board?
With a renderer server doing the heavy lifting, all three boards are viable — but the experience differs:
| Board 1 (2.8″) | Board 2 (4.0″) | Board 3 (4.3″) | |
|---|---|---|---|
| Resolution | 320 × 240 | 320 × 480 | 800 × 480 |
| Headline buffer (120 items) | ~77 KB | ~77 KB | ~192 KB |
| Firehose mode | no (too narrow) | no (SPI too slow) | yes |
| Smooth scroll at 20 FPS | yes (partial updates) | tight (partial updates mandatory) | yes (framebuffer) |
| Flash for caching | 4 MB (tight) | 4 MB (tight) | 16 MB (9 MB FAT partition) |
Board 3 is the most capable. Boards 1 & 2 work but lose firehose mode and need careful partial-update rendering. Which board do you want this on?
2. PSRAM on Boards 1 & 2
The build flags say -DBOARD_HAS_PSRAM but I can't confirm the capacity. Can you check?
// Add to setup() temporarily:
Serial.printf("PSRAM size: %d bytes\n", ESP.getPsramSize());
Serial.printf("Free PSRAM: %d bytes\n", ESP.getFreePsram());
If PSRAM is 0, the board can only buffer ~20 headlines in a ring buffer instead of the full ~120 set. (Board 3's 8 MB PSRAM is confirmed — this only matters if you pick Board 1 or 2.)
3. Network & server hosting
The renderer server (serve.py) needs Python 3 + Pillow, internet access (for RSS feeds), and network access to the ESP32.
- Where will the server run? Raspberry Pi, NAS, always-on desktop, cloud VM?
- Same LAN as the ESP32? If yes, the ESP32 can use plain HTTP (no TLS overhead). If remote, we'd need HTTPS (~40–50 KB RAM per connection).
- Server discovery: Hardcoded IP in
secrets.h, mDNS (mainline.local), or a DNS name? - WiFi credentials: Use the existing multi-network setup from the doorbell firmware, or a specific SSID?
4. ESP32 client repo
The ESP32 sketch reuses NetManager, IDisplayDriver, and vendored display libraries from klubhaus-doorbell. Two options:
- klubhaus-doorbell repo — natural fit as a new board target (
boards/esp32-mainline/). Requires push access or a PR. - mainline repo — under
firmware/esp32-mainline/with a vendored copy of KlubhausCore. Self-contained but duplicates shared code.
Which repo should host the ESP32 client?
5. Display features
With the renderer handling RSS fetching, translation, content filtering, and font rendering server-side, the remaining feature questions are about what the ESP32 displays locally:
- Left-to-right color gradient — the white-hot → green → black fade, applied per-pixel on the ESP32. Keep?
- Glitch / noise effects — random block characters and katakana rain between headlines. Keep?
- Glitch reactivity — mainline.py uses a microphone (none on these boards). Substitute with touch-reactive, time-of-day reactive, or just random?
- Firehose mode — dense data ticker at the bottom (only viable on Board 3). Want it?
- Boot sequence animation — typewriter-style status log during startup. Keep?
- Poetry mode — the server can serve Gutenberg text instead of RSS. Want both modes available, or just news?
6. ntfy message queue
The ESP32 polls ntfy.sh/klubhaus_terminal_mainline directly for messages that interrupt the news scroll.
- Is
klubhaus_terminal_mainlinethe right topic name? Or match the doorbell convention (ALERT_klubhaus_topic, etc.)? - Who sends messages? Just you (manual
curl), a bot, other people? - Display duration: How long before auto-dismiss? The doorbell uses 120s for alerts. 30s for terminal messages? Touch-to-dismiss regardless?
- Priority levels? ntfy supports 1–5. Should high-priority messages turn on the backlight if the display is OFF, or show in a different color?
- Message history on boot? Show recent messages from the topic, or only messages arriving while running? The doorbell uses
?since=20s. You might want?since=5mfor a message board feel.
7. Server-offline fallback
If the renderer server goes down, what should the ESP32 do?
- A. Show last cached headlines indefinitely (ntfy messages still work independently).
- B. Show a "no signal" screen after a timeout, keep polling.
- C. Fall back to ntfy messages + clock only.
8. Scroll and layout
- Vertical scroll (like mainline.py)? Or horizontal marquee?
- Speed: mainline.py uses ~3.75s per headline. Same pace, or different for ambient display?
- Font height: The server pre-renders at a configurable pixel height. On Board 3 (480px tall), 32–48px headlines would match mainline.py's feel. On Boards 1/2, 16–24px. What looks right to you?
9. Font and aesthetics
mainline.py uses CSBishopDrawn-Italic.otf. The server renders bitmaps with this font, so the ESP32 never needs the font file itself.
- Can you provide this font to whoever hosts the server? Or should it fall back to a freely available alternative?
- Any license concern with serving the rendered bitmaps (not the font file) over HTTP?
10. SD card
All three boards have TF card slots (unused by the doorbell firmware). An SD card would let the ESP32 cache headline bitmaps for instant boot and offline operation. Is there an SD card in the board you'd pick?
Recent mainline.py changes and port implications
Updated after merge of latest main — three new features affect the port calculus.
Local headline cache (--refresh)
mainline.py now caches fetched headlines to a JSON file (.mainline_cache_{MODE}.json) and skips network fetches on subsequent runs unless --refresh is passed. This is good news for an ESP32 port:
- Faster boot. The device could load cached headlines from flash/SD on power-up and start scrolling immediately, then refresh feeds in the background.
- Offline operation. If WiFi drops, the device can still display cached content.
- Storage question. A typical cache is ~200–500 headlines × ~100 bytes ≈ 20–50 KB of JSON. That fits comfortably in Board 3's 9 MB FAT partition or on SD. On Boards 1 & 2 (4 MB flash), it would need SPIFFS/LittleFS and would compete with app code for flash space.
Recommendation: If targeting Boards 1 or 2, confirm how much flash is left after the app partition. Board 3's app3M_fat9M_16MB partition scheme already has 9 MB of FAT storage — more than enough.
Firehose mode (--firehose)
mainline.py now has a --firehose flag that adds a 12-row dense data zone at the bottom of the viewport, cycling rapidly (every frame) through raw headline text, glitch noise, status lines, and headline fragments. This is the most demanding new feature for an ESP32 port:
- Frame rate. The firehose zone redraws completely every frame at 20 FPS. On SPI displays (Boards 1 & 2), rewriting 12 rows × 320 or 480 pixels at 40 MHz SPI would consume a significant fraction of each 50 ms frame budget. On Board 3's RGB framebuffer, this is trivial.
- Randomness. Each firehose line calls
random.choice()over the full item pool. On ESP32,esp_random()is hardware-backed and fast, but iterating the full pool each frame needs the pool in RAM. - Visual density. At 320px wide (Boards 1 & 2), 12 rows of dense data may be illegible. At 800px (Board 3), it works.
Recommendation: Firehose mode is only practical on Board 3. On Boards 1 & 2, consider either dropping it or replacing it with a single-line ticker.
Fixed 20 FPS frame timing
The scroll loop now uses time.monotonic() with a 50 ms frame budget (_FRAME_DT = 0.05) and a scroll accumulator instead of sleeping per scroll step. This is actually a better fit for ESP32 than the old approach — it maps cleanly to a millis()-based main loop:
// ESP32 equivalent pattern
void loop() {
uint32_t t0 = millis();
// ... render frame ...
uint32_t elapsed = millis() - t0;
if (elapsed < FRAME_DT_MS) delay(FRAME_DT_MS - elapsed);
}
Estimated per-frame budgets:
| Board 1 (320×240) | Board 2 (320×480) | Board 3 (800×480) | |
|---|---|---|---|
| Full-screen SPI flush | ~30 ms | ~60 ms | n/a (framebuffer) |
| Partial update (12 rows) | ~4 ms | ~4 ms | n/a |
| Remaining CPU budget (of 50 ms) | ~20 ms | ~0 ms (tight) | ~45 ms |
Board 2 at 20 FPS with a full-screen redraw each frame would have essentially zero headroom. Partial updates (dirty-rect tracking) would be mandatory.
Port-relevant implications
- Firehose mode is only practical on Board 3 (see §5 in Questions). On Boards 1 & 2, consider a single-line ticker instead.
- Headline caching maps to the ESP32 storing bitmap data from the server to flash/SD for instant boot and offline fallback (see §10 in Questions).
- 20 FPS frame timing maps cleanly to a
millis()-based ESP32 main loop. Board 2 would have zero headroom without partial updates.