# Klubhaus Doorbell — Hardware Spec Sheet

Derived from source code analysis of [klubhaus-doorbell](https://git.notsosm.art/david/klubhaus-doorbell.git) 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](https://www.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](https://www.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_PSRAM` is 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 `.crushmemory` file 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.sh` files 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.h` defines `GT911_ADDR 0x5D` but `LovyanPins.h` configures the touch at address `0x14`. 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.h` defines 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 in `DisplayDriverGFX.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](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_mainline` the 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=5m` for 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.
