Zum Inhalt

BTClock v4 architecture (UML)

Diagrams render natively on Forgejo via Mermaid. Each section maps to a real subsystem: file paths in the captions are the source of truth — if a diagram drifts from the code, trust the code.

Conventions: - Class boxes name the C++ class as it appears in source. - Owns = unique_ptr / optional / by-value member. - Refs = raw pointer / reference, lifetime managed elsewhere. - Calls = method invocation, no ownership.


1. Component overview

Top-level subsystems and the wires between them. AppCtx (main/app/app_ctx.hpp) is the runtime root — everything else hangs off it.

Diagram 1


2. AppCtx ownership (class diagram)

AppCtx is behaviour-free — a struct of subsystem handles populated by the init_* TUs in main/app/boot/. Composition arrows point from owner to owned.

Diagram 2


3. Data pipeline (class diagram)

Pure-virtual DataSource (components/data_core/include/data_core/source.hpp:23) is the contract; DataHub (hub.hpp) fan-ins reports under a mutex and fires an UpdateCallback.

Diagram 3


4. Mining-pool plugins (class diagram)

Every mining_pool_* component subclasses PoolDataSource (components/mining_pool_common/include/mining_pool_common/pool_base.hpp:41). Two pools (ViaBTC, Foundry) inherit through an intermediate KeyedGetPoolBase that swaps the auth header to X-API-KEY.

Diagram 4


5. ScreenManager + renderers (class diagram)

ScreenManager (main/app/screen_manager.hpp:51) owns the slot index, refresh policy, rotation timer, and last-rendered diff state. Renderers are free template functions in main/screens/ — there is no Screen base class.

Diagram 5


6. ControlServer + adapter interfaces (class diagram)

ControlServer (components/webserver/include/control_server.hpp) talks to main/ only through pure-virtual *Iface interfaces so the webserver component never has to include main/. main.cpp instantiates adapter structs in main/app/boot/adapters.hpp that forward to the real subsystems.

Diagram 6


7. IO controllers (class diagram)

main/io/ wraps the chip drivers in higher-level controllers the rest of the app talks to.

Diagram 7


8. Boot sequence

app_main (main/main.cpp:37) is straight-line wire-up. Each step populates AppCtx; the last call hands off to the event loop.

Diagram 8


9. Data update → render

Hot path from a data source receiving a frame to pixels on the EPD.

Diagram 9


10. Button press → screen change

Buttons live on an MCP23017; the reader polls in its own task and drops events into a queue the main task drains.

Diagram 10


11. HTTP API → screen change

A WebUI / curl client jumps to a specific slot via POST /api/show/screen?s=<idx>. Handlers run on the httpd worker task; state mutation is queued for the main task to keep ScreenManager single-threaded.

Diagram 11


12. WiFi provisioning (AP → STA)

Cold-boot path when no STA credentials are stored: device puts up a SoftAP, the user joins it, the captive portal collects the new SSID/PSK, the device retries against STA, and on success persists creds and restarts.

Diagram 12


13. OTA firmware upload

POST /upload/firmware streams the new app partition. The handler latches the OTA overlay on ScreenManager so the EPD shows progress and the main loop stays out of the renderer for the duration.

Diagram 13


14. WiFi state machine

Diagram 14


15. ScreenManager mode/overlay priority

ScreenManager has several latching overlays. Priority (highest first): OTA > Debug > Zap notify > Custom > rotation slot.

Diagram 15


16. Flash partition layout

The three partitions_*.csv files (partitions_4mb.csv, partitions_8mb.csv, partitions_16mb.csv) carve flash into the same six regions across every variant — only the sizes differ. NVS sits at the conventional 0x9000 offset; both OTA app slots are 64 KiB-aligned so MMU-page mapping is clean; LittleFS holds the WebUI bundle; the coredump partition catches panic backtraces. On Rev A the coredump partition exists but capture is disabled at the sdkconfig level (CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=n in sdkconfig.defaults.rev_a) — the 4 MB flash leaves the app partition with only ~3% headroom and the ~14 KiB capture path eats most of it. Rev B and V8 keep capture on and serve the dump over GET /api/coredump.

Partition Type / subtype Rev A (4 MB) Rev B (8 MB) V8 (16 MB) Notes
nvs data / nvs 0x9000, 20 KiB 0x9000, 20 KiB 0x9000, 20 KiB Settings + WiFi creds + Nostr keys + LED prefs
otadata data / ota 0xe000, 8 KiB 0xe000, 8 KiB 0xe000, 8 KiB Boot-slot pointer for app_update
app0 app / ota_0 0x10000, 1.6875 MiB 0x10000, 3.4375 MiB 0x10000, 6.9375 MiB Active firmware (per OTA)
app1 app / ota_1 0x1C0000, 1.6875 MiB 0x380000, 3.4375 MiB 0x700000, 6.9375 MiB OTA staging slot
storage data / littlefs 0x370000, 412 KiB 0x6F0000, 820 KiB 0xDF0000, 2 MiB WebUI bundle + per-pool logo cache
coredump data / coredump 0x3D7000, 64 KiB (unused) 0x7BD000, 64 KiB 0xFF0000, 64 KiB ELF panic dump; pull via GET /api/coredump, clear via DELETE /api/coredump
unused tail 0x3E7000, ~100 KiB 0x7CD000, ~200 KiB Reserved for future partition growth without re-flashing the table

Flash sizes are 4 MiB on Rev A (Lolin S3 Mini, no PSRAM constraint beyond the chip's 2 MiB), 8 MiB on Rev B (ESP32-S3-WROOM-1-N8R2, 2 MiB PSRAM), and 16 MiB on V8 (8 MiB PSRAM). The OTA-able image (btclock_v4.bin) lives in whichever app slot otadata points to; /upload/firmware and the auto-update path (components/ota/) write to the inactive slot, flip otadata, and reboot.


File index