Technical Deep-Dive
Architecture
CrossPet runs on an ESP32-C3 with ~380KB of usable RAM and no PSRAM. Every design decision flows from that constraint β aggressive SD caching, single-buffer rendering, careful heap discipline, and a tight activity lifecycle.
Hardware Specifications
CrossPoint Core Architecture
The foundation inherited from CrossPoint Reader β the open-source firmware that makes everything possible.
Activity Lifecycle
CrossPet's UI is structured around Activities β heap-allocated objects each responsible for one screen or interaction. The activity manager holds a single pointer to the current activity and calls onEnter(), loop(), and onExit() in sequence.
Navigation is a delete-then-new pattern: the outgoing activity's onExit() frees all resources it allocated (buffers, FreeRTOS tasks, open file handles), then the object is deleted. The next activity is heap-allocated and its onEnter() runs immediately.
Because the ESP32-C3 has no garbage collector and no exceptions, every allocation in onEnter() must have a matching free in onExit() β in reverse order. FreeRTOS tasks are deleted before the activity is destroyed to prevent dangling-pointer callbacks.
SD-First Caching
With only 380KB of RAM β shared between the framebuffer (48KB), stack, heap, and FreeRTOS β there is no room to hold a parsed EPUB layout in memory. CrossPet offloads the result of every expensive operation to the SD card's .crosspoint/ directory.
Each book gets a directory keyed by a hash of its file path. Inside: book.bin (metadata and spine), progress.bin (reading position), cover.bmp (pre-scaled cover art), and a sections/ folder containing one binary per EPUB section. Sections store the complete rendered layout β character positions, line breaks, image coordinates β so that page turns require only a fast sequential read from SD, not a full re-parse.
Cache invalidation is automatic: a version number in each binary triggers full regeneration when the firmware's layout engine changes, or when the user adjusts font size, family, line spacing, or screen orientation.
E-Ink Rendering Pipeline
E-ink displays are slow to refresh and prone to ghosting. CrossPet uses a single framebuffer (48KB) and a three-phase rendering pipeline to produce clean grayscale anti-aliased text without double-buffering.
Phase 1 β BW Fast Refresh: A low-latency black-and-white waveform clears the previous content and renders a coarse version of the new page. This gives immediate visual feedback (~200ms).
Phase 2 β Grayscale LUT: A custom lookup-table waveform drives the display through multiple voltage levels, setting each pixel to one of 16 gray shades. The anti-aliased glyph data (computed during layout) maps sub-pixel coverage percentages to gray levels.
Full page refreshes are triggered on activity transitions to eliminate ghosting. Fast refreshes are used for page turns when anti-aliasing is disabled, giving a snappier feel at the cost of some ghosting accumulation.
Memory Safety
CrossPet compiles with no C++ exceptions and no RTTI. Error propagation uses the LOG_ERR + return false pattern throughout. This eliminates the exception unwinding tables from the binary β a meaningful saving on a 16MB flash budget β and keeps control flow explicit.
The string policy prohibits std::string and Arduino String in hot paths. Read-only string access uses std::string_view; construction uses snprintf into fixed-size stack buffers. Every std::vector calls .reserve(N) before any push_back loop to avoid heap fragmentation from growth reallocations.
Large temporary buffers (cover images, text chunks, OTA blocks) are mallocd, null-checked, used immediately, and freed β never held across multiple operations. Smart pointers use std::unique_ptr; std::shared_ptr is avoided because its atomic reference count is unnecessary overhead on a single-core MCU.
EPUB Pipeline
Processing an EPUB is a multi-stage pipeline. First, the ZIP archive is opened on SD and its OPF manifest parsed to extract the spine order and resource map. Each spine item's XHTML is parsed, CSS rules extracted, and inline styling resolved into a flat sequence of styled text runs.
The layout engine performs a word-wrap pass using the selected font's glyph metrics, inserting soft hyphens according to the language's hyphenation dictionary. Each output line is stored as a compact struct (font ID, x/y offset, glyph range) in the section cache binary.
Images are decoded from their base64 or binary representation, scaled to fit the viewport using a nearest-neighbour or bilinear filter, and stored as pre-scaled bitmaps in the cache. This means rendering a page is a pure cache-read + blit operation β no parsing, no scaling, no layout math at read time.
Build System & Environments
CrossPet uses PlatformIO with a C++20 toolchain targeting the ESP32-C3 RISC-V architecture. Four build environments cover the full development-to-release lifecycle:
defaultβ Development build. Full serial logging (LOG_LEVEL=2), debug assertions enabled.gh_releaseβ Production. Logging stripped (LOG_LEVEL=0), size-optimised.gh_release_rcβ Release candidate. Minimal logging (LOG_LEVEL=1) for field testing.slimβ Minimal build with no serial output, smallest possible binary.
I18n strings are generated from YAML translation files by scripts/gen_i18n.py into C++ headers. HTML pages served over WiFi are compiled into .generated.h headers by scripts/build_html.py at build time β neither file is hand-edited.
CrossPet Additions
Features built on top of CrossPoint β the reason this fork exists.
Virtual Pet System
A full tamagotchi-style virtual pet that lives on the sleep screen and in the Tools menu. The pet tracks reading sessions via the Reading Stats system β reading more causes the pet to evolve through 4 stages (egg β chick β hen β rooster). Pet mood reflects recent reading activity; long gaps make it listless.
Pet sprites are rendered via PetSpriteRenderer using 16Γ16 pixel art at 2Γ scale. The pet's name, type, and evolution state are persisted on SD card. Daily missions encourage consistent reading habits. The system is purely cosmetic β zero impact on reading performance.
Reading Stats & Gamification
A binary stats file (.crosspoint/reading_stats.bin, v2) tracks: today's reading time, all-time total, session count, books finished, current streak, and longest streak. Sessions are started in reader onEnter() and ended in onExit() with book title and progress.
A dedicated Reading Stats sleep screen renders a dashboard: today/all-time time, session grid, streak display, and a progress bar for the last book read. Stats feed into the pet evolution system to reward consistent reading.
Weather & Clock
Weather data from Open-Meteo HTTPS API, fetched silently on WiFi connect. Cached to .crosspoint/weather_cache.json on SD. Displayed in header bar, clock sleep screen, and dedicated Weather activity. NTP time sync via configTzTime() on WiFi connect.
Clock activity includes a monthly calendar grid with Left/Right month navigation, Vietnamese lunar calendar integration (showing lunar dates in cells and "Γm lα»ch" range in header), and 28 rotating daily literary quotes on the clock sleep screen.
Mini Games
Five games in the Tools menu: Chess (with AI opponent), Caro/Gomoku, Sudoku (4 difficulty levels with auto-save), Minesweeper, and 2048. All rendered on the 800Γ480 e-ink display using physical button controls.
Games are standard Activities β they only load when navigated to and are fully freed on exit. No background memory usage. Each game uses the shared GfxRenderer and MappedInputManager for consistent input/output across all screen orientations.
BLE Remote & Presenter
ESP32-C3 supports BLE 5.0 only (no Classic Bluetooth). Two BLE HID profiles: "CrossPoint" keyboard remote for page turning from a distance, and "CrossPet_Presenter" slide controller for presentations. Only one connection at a time (CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1).
BLE controller memory is preserved at boot via extern "C" bool btInUse(void) { return true; } to prevent Arduino from releasing it. Scan shows all BLE devices (no filter), connection validates HID service. Security uses NimBLE auto-negotiation with no-input-output capability.
Vietnamese i18n
Full Vietnamese support across all UI: games, tools, pet system, clock, pomodoro, sleep screens. Vietnamese glyphs in all 3 font families with NotoSans fallback for Ubuntu UI fonts. YAML source files in lib/I18n/translations/ auto-generate C++ headers via gen_i18n.py.
Deep sleep screen re-render on wake ensures dynamic content (clock, reading stats) stays current after the RTC elapsed-time correction. Lunar calendar uses an embedded algorithm β no network required.
SD Cache Structure
All persistent cache lives in .crosspoint/ at the SD card root. Deleting this directory forces a full regeneration on next boot β safe to do at any time.
.crosspoint/
βββ reading_stats.bin # Global reading stats (v2)
βββ epub_<path-hash>/ # One directory per book
βββ book.bin # Metadata + spine (v5)
βββ progress.bin # Reading position
βββ cover.bmp # Pre-scaled cover art
βββ sections/
βββ 000.bin # Rendered layout, section 0 (v12)
βββ 001.bin
βββ ...
Cache keys are std::hash<std::string> of the book's full SD path. Renaming or moving a file creates a new cache entry and loses reading progress for that book.