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

MCU ESP32-C3 β€” Single-core RISC-V @ 160MHz
RAM ~380KB usable DRAM (no PSRAM)
Flash 16MB β€” instruction + static data storage
Display 800Γ—480 E-Ink, SSD1677 controller
Framebuffer 48,000 bytes (800 Γ— 480 Γ· 8, single buffer)
Storage SD Card β€” books + aggressive layout cache
WiFi 802.11 b/g/n (2.4GHz)
Bluetooth BLE 5.0 only β€” no Classic BT
Buttons 4 front + 4 side, all remappable
Build system PlatformIO, C++20, no exceptions / no RTTI

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.