1 Commits

Author SHA1 Message Date
Dave Allie
567fa6e1e2 feat: Overhaul font format into CrossPoint font 2026-01-28 23:59:22 +11:00
598 changed files with 284959 additions and 405938 deletions

View File

@@ -1,171 +0,0 @@
---
name: TTF Font Investigation
overview: Investigate replacing compile-time bitmap fonts with runtime TTF rendering using stb_truetype (the core of lvgl-ttf-esp32), integrated into the existing custom GfxRenderer pipeline for the ESP32-C3 e-ink reader.
todos:
- id: poc-stb
content: "Phase 1: Add stb_truetype.h and build a minimal proof-of-concept that loads a TTF from SD, rasterizes glyphs, and draws them via GfxRenderer"
status: pending
- id: measure-ram
content: "Phase 1: Measure actual RAM consumption and render performance of stb_truetype on ESP32-C3"
status: pending
- id: spiffs-mmap
content: "Phase 3: Test SPIFFS memory-mapping of TTF files using esp_partition_mmap() to avoid loading into RAM"
status: pending
- id: font-provider
content: "Phase 2: Create FontProvider abstraction layer and TtfFontProvider with glyph caching"
status: pending
- id: renderer-refactor
content: "Phase 2: Refactor GfxRenderer to use FontProvider interface instead of direct EpdFontFamily"
status: pending
- id: settings-integration
content: "Phase 4: Update settings to support arbitrary font sizes and custom font selection"
status: pending
- id: remove-bitmap-fonts
content: "Phase 5: Remove compiled bitmap reader fonts, keep only small UI bitmap fonts"
status: pending
isProject: false
---
# TTF Font Rendering Investigation
## Current State
The project uses **no LVGL** -- it has a custom `GfxRenderer` that draws directly into an e-ink framebuffer. Fonts are pre-rasterized offline (TTF -> Python FreeType script -> C header bitmaps) and embedded at compile time.
**Cost of current approach:**
- **~2.7 MB flash** (mod build, Bookerly + NotoSans + Ubuntu) up to **~7 MB** (full build with OpenDyslexic)
- **Only 4 discrete sizes** per family (12/14/16/18 pt) -- no runtime scaling
- Each size x style (regular/bold/italic/bold-italic) is a separate ~80-200 KB bitmap blob
- App partition is only **6.25 MB** -- fonts consume 43-100%+ of available space
## Why lvgl-ttf-esp32 Is Relevant (and What Isn't)
The [lvgl-ttf-esp32](https://github.com/huming2207/lvgl-ttf-esp32) repo wraps **stb_truetype** (a single-header C library) with an LVGL font driver. Since this project does not use LVGL, the wrapper is irrelevant, but the **stb_truetype library itself** is exactly what's needed -- a lightweight, zero-dependency TTF rasterizer that runs on ESP32.
## Proposed Architecture
```mermaid
flowchart TD
subgraph current [Current Pipeline]
TTF_Offline["TTF files (offline)"] --> fontconvert["fontconvert.py (FreeType)"]
fontconvert --> headers["56 .h files (~2.7-7 MB flash)"]
headers --> EpdFont["EpdFont / EpdFontFamily"]
EpdFont --> GfxRenderer["GfxRenderer::renderChar()"]
end
subgraph proposed [Proposed Pipeline]
TTF_SD["TTF files on SD card (~100-500 KB each)"] --> stb["stb_truetype.h (runtime)"]
stb --> cache["Glyph cache (RAM + SD)"]
cache --> TtfFont["TtfFont (new class)"]
TtfFont --> FontProvider["FontProvider interface"]
FontProvider --> GfxRenderer2["GfxRenderer::renderChar()"]
end
```
### Core Idea
1. **stb_truetype.h** -- add as a single header file in `lib/`. It rasterizes individual glyphs from TTF data on demand.
2. **TTF files on SD card** -- load at runtime from `.crosspoint/fonts/`. A typical TTF family (4 styles) is ~400-800 KB total vs 2.7 MB+ as bitmaps.
3. **Glyph cache** -- since e-ink pages are static, cache rasterized glyphs in RAM (LRU, ~20-50 KB) and optionally persist to SD card to avoid re-rasterizing across page turns.
4. `**FontProvider` abstraction** -- interface over both `EpdFont` (bitmap, for UI fonts) and new `TtfFont` (runtime, for reader fonts), so both can coexist.
## Integration Points
These are the key files/interfaces that would need changes:
| Component | File | Change |
| ----------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Font abstraction | New `lib/FontProvider/` | `FontProvider` interface with `getGlyph()`, `getMetrics()` |
| TTF renderer | New `lib/TtfFont/` | Wraps stb_truetype, manages TTF loading + glyph cache |
| GfxRenderer | [lib/GfxRenderer/GfxRenderer.h](lib/GfxRenderer/GfxRenderer.h) | Change `fontMap` from `EpdFontFamily` to `FontProvider*`; update `renderChar`, `getTextWidth`, `getSpaceWidth` |
| Font registration | [src/main.cpp](src/main.cpp) | Register TTF fonts from SD instead of (or alongside) bitmap fonts |
| Settings | [src/CrossPointSettings.cpp](src/CrossPointSettings.cpp) | `getReaderFontId()` supports arbitrary sizes, not just 4 discrete ones |
| PlaceholderCover | [lib/PlaceholderCover/PlaceholderCoverGenerator.cpp](lib/PlaceholderCover/PlaceholderCoverGenerator.cpp) | Uses own `renderGlyph()` -- needs similar adaptation |
| Text layout | [lib/Epub/Epub/ParsedText.cpp](lib/Epub/Epub/ParsedText.cpp) | Uses `getTextWidth()` / `getSpaceWidth()` for line breaking -- works unchanged if FontProvider is transparent |
## Feasibility Analysis
### Memory (ESP32-C3, ~380 KB RAM)
- **stb_truetype itself**: ~15-20 KB code in flash, minimal RAM overhead
- **TTF file in memory**: requires the full TTF loaded into RAM for glyph access. Options:
- **Memory-mapped from flash (SPIFFS)**: store TTF in SPIFFS (3.4 MB available, currently unused), memory-map via `mmap()` on ESP-IDF -- zero RAM cost
- **Partial loading from SD**: read only needed tables on demand (stb_truetype supports custom `stbtt_read()` but the default API expects full file in memory)
- **Load into PSRAM**: ESP32-C3 has no PSRAM, so this is not an option
- **Glyph cache**: ~50 bytes metadata + bitmap per glyph. At 18pt, a glyph bitmap is ~20x25 pixels = ~63 bytes (1-bit). Caching 256 glyphs = ~30 KB RAM.
- **Rasterization temp buffer**: stb_truetype allocates ~10-20 KB temporarily per glyph render (uses `malloc`)
**Verdict**: The biggest constraint is holding the TTF file in RAM. A typical Bookerly-Regular.ttf is ~150 KB. With 4 styles loaded, that's ~600 KB -- **too much for 380 KB RAM**. The viable path is using **SPIFFS** to store TTFs and memory-map them, or implementing a chunked reader that loads TTF table data on demand from SD.
### Flash Savings
- **Remove**: 2.7-7 MB of bitmap font headers from firmware
- **Add**: ~40 KB for stb_truetype + TtfFont code
- **Net savings**: **2.6-6.9 MB flash freed**
- TTF files move to SD card or SPIFFS (not in firmware)
### Performance
- stb_truetype rasterizes a glyph in **~0.5-2 ms** on ESP32 (160 MHz)
- A typical page has ~~200-300 glyphs, but with caching, only unique glyphs need rasterizing (~~60-80 per page)
- **First page render**: ~60-160 ms extra for cache warmup
- **Subsequent pages**: mostly cache hits, negligible overhead
- E-ink refresh takes ~300-1000 ms anyway, so TTF rasterization cost is acceptable
### Anti-aliasing for E-ink
stb_truetype produces 8-bit alpha bitmaps (256 levels). The current system uses 1-bit or 2-bit glyphs. The adapter would:
- **1-bit mode**: threshold the alpha (e.g., alpha > 128 = black)
- **2-bit mode**: quantize to 4 levels (0, 85, 170, 255) for e-ink grayscale
This should actually produce **better quality** than the offline FreeType conversion since stb_truetype does sub-pixel hinting.
## Recommended Implementation Phases
### Phase 1: Proof of Concept (stb_truetype standalone)
- Add stb_truetype.h to the project
- Write a minimal test that loads a TTF from SD, rasterizes a few glyphs, and draws them via `GfxRenderer::drawPixel()`
- Measure RAM usage and render time
- Validate glyph quality on e-ink
### Phase 2: FontProvider Abstraction
- Create `FontProvider` interface matching `EpdFontFamily`'s public API
- Wrap existing `EpdFontFamily` in a `BitmapFontProvider`
- Create `TtfFontProvider` backed by stb_truetype + glyph cache
- Refactor `GfxRenderer::fontMap` to use `FontProvider*`
### Phase 3: TTF Storage Strategy
- Evaluate SPIFFS memory mapping vs. SD-card chunked loading
- Implement the chosen strategy
- Handle font discovery (scan SD card for `.ttf` files)
### Phase 4: Settings and UI Integration
- Replace discrete font-size enum with a continuous size setting (or finer granularity)
- Add "Custom Font" option in settings
- Update section cache invalidation when font/size changes
### Phase 5: Remove Bitmap Reader Fonts
- Keep bitmap fonts only for UI (Ubuntu 10/12, NotoSans 8) which are small (~62 KB)
- Remove Bookerly, NotoSans, OpenDyslexic bitmap headers
- Ship TTF files on SD card (or downloadable)
## Key Risk: TTF-in-RAM on ESP32-C3
The critical question is whether TTF file data can be accessed without loading the full file into RAM. Three mitigation strategies:
1. **SPIFFS + mmap**: Store TTFs in the 3.4 MB SPIFFS partition and use ESP-IDF's `esp_partition_mmap()` to map them into the address space. Zero RAM cost, but SPIFFS is read-only after flashing (unless written at runtime).
2. **SD card + custom I/O**: Implement `stbtt_GetFontOffsetForIndex` and glyph extraction using buffered SD reads. stb_truetype's API assumes a contiguous byte array, so this would require a patched or wrapper approach.
3. **Load one style at a time**: Only keep the active style's TTF in RAM (~150 KB). Switch styles by unloading/reloading. Feasible but adds latency on style changes (bold/italic).
Strategy 1 (SPIFFS mmap) is the most promising since the SPIFFS partition is already allocated but unused.

View File

@@ -1 +0,0 @@
../../.skills/SKILL.md

View File

@@ -1,61 +1,10 @@
name: CI (build)
on:
name: CI
'on':
push:
branches: [master]
pull_request:
permissions:
contents: read
jobs:
clang-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install clang-format-21
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 21
sudo apt-get update
sudo apt-get install -y clang-format-21
- name: Run clang-format
run: |
PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix
git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)
cppcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
build:
runs-on: ubuntu-latest
steps:
@@ -67,55 +16,22 @@ jobs:
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
run: pip install --upgrade platformio
- name: Install clang-format-21
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 21
sudo apt-get update
sudo apt-get install -y clang-format-21
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
- name: Run clang-format
run: PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)
- name: Build CrossPoint
run: |
set -euo pipefail
pio run | tee pio.log
- name: Extract firmware stats
run: |
set -euo pipefail
ram_line="$(grep -E "RAM:\\s" -m1 pio.log || true)"
flash_line="$(grep -E "Flash:\\s" -m1 pio.log || true)"
echo "ram_line=${ram_line}" >> "$GITHUB_OUTPUT"
echo "flash_line=${flash_line}" >> "$GITHUB_OUTPUT"
{
echo "## Firmware build stats"
if [ -n "$ram_line" ]; then echo "- ${ram_line}"; else echo "- RAM: not found"; fi
if [ -n "$flash_line" ]; then echo "- ${flash_line}"; else echo "- Flash: not found"; fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload firmware.bin artifact
uses: actions/upload-artifact@v6
with:
name: firmware.bin
path: .pio/build/default/firmware.bin
if-no-files-found: error
# This job is used as the PR required actions check, allows for changes to other steps in the future without breaking
# PR requirements.
test-status:
name: Test Status
needs:
- build
- clang-format
- cppcheck
if: always()
runs-on: ubuntu-latest
steps:
- name: Fail because needed jobs failed
# Fail if any job failed or was cancelled (skipped jobs are ok)
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: exit 1
- name: Success
run: exit 0
run: pio run

View File

@@ -1,7 +1,6 @@
name: "PR Formatting"
on:
pull_request:
pull_request_target:
types:
- opened

View File

@@ -12,18 +12,19 @@ jobs:
with:
submodules: recursive
- uses: actions/cache@v5
with:
path: |
~/.cache/pip
~/.platformio/.cache
key: ${{ runner.os }}-pio
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
run: pip install --upgrade platformio
- name: Build CrossPoint
run: pio run -e gh_release

View File

@@ -1,47 +0,0 @@
name: Compile Release Candidate
on:
workflow_dispatch:
jobs:
build-release-candidate:
if: startsWith(github.ref_name, 'release/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install PlatformIO Core
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
- name: Extract env
run: |
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
echo "BRANCH_SUFFIX=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV
- name: Build CrossPoint Release Candidate
env:
CROSSPOINT_RC_HASH: ${{ env.SHORT_SHA }}
run: pio run -e gh_release_rc
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: CrossPoint-RC-${{ env.BRANCH_SUFFIX }}
path: |
.pio/build/gh_release_rc/bootloader.bin
.pio/build/gh_release_rc/firmware.bin
.pio/build/gh_release_rc/firmware.elf
.pio/build/gh_release_rc/firmware.map
.pio/build/gh_release_rc/partitions.bin

6
.gitignore vendored
View File

@@ -3,15 +3,9 @@
.DS_Store
.vscode
lib/EpdFont/fontsrc
lib/I18n/I18nKeys.h
lib/I18n/I18nStrings.h
lib/I18n/I18nStrings.cpp
*.generated.h
.vs
build
**/__pycache__/
/compile_commands.json
/.cache
.history/
/.venv
*.local*

View File

@@ -1,872 +0,0 @@
# CrossPoint Reader Development Guide
Project: Open-source e-reader firmware for Xteink X4 (ESP32-C3)
Mission: Provide a lightweight, high-performance reading experience focused on EPUB rendering on constrained hardware.
## AI Agent Identity and Cognitive Rules
* Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized).
* Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable.
* Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification.
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the open-x4-sdk or official docs first.
* No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage).
* Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected.
* Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file).
---
## Development Environment Awareness
**CRITICAL**: Detect the host platform at session start to choose appropriate tools and commands.
### Platform Detection
```bash
# Detect platform (run once per session)
uname -s
# Returns: MINGW64_NT-* (Windows Git Bash), Linux, Darwin (macOS)
```
**Detection Required**: Run `uname -s` at session start to determine platform
### Platform-Specific Behaviors
- **Windows (Git Bash)**: Unix commands, `C:\` paths in Windows but `/` in bash, limited glob (use `find`+`xargs`)
- **Linux/WSL**: Full bash, Unix paths, native glob support
**Cross-Platform Code Formatting**:
```bash
find src -name "*.cpp" -o -name "*.h" | xargs clang-format -i
```
---
## Platform and Hardware Constraints
### Hardware Specs
* MCU: ESP32-C3 (Single-core RISC-V @ 160MHz)
* RAM: ~380KB usable (VERY LIMITED - primary project constraint)
* **NO PSRAM**: ESP32-C3 has no PSRAM capability (unlike ESP32-S3)
* **Single Buffer Mode**: Only ONE 48KB framebuffer (not double-buffered)
* Flash: 16MB (Instruction storage and static data)
* Display: 800x480 E-Ink (Slow refresh, monochrome, 1-2s full update)
* Framebuffer: 48,000 bytes (800 × 480 ÷ 8)
* Storage: SD Card (Used for books and aggressive caching)
### The Resource Protocol
1. Stack Safety: Limit local function variables to < 256 bytes. The ESP32-C3 default stack is small; use std::unique_ptr or static pools for larger buffers.
2. Heap Fragmentation: Avoid repeated new/delete in loops. Allocate buffers once during onEnter() and reuse them.
3. Flash Persistence: Large constant data (UI strings, lookup tables) MUST be marked static const to stay in Flash (Instruction Bus), freeing DRAM.
4. String Policy: Prohibit std::string and Arduino String in hot paths. Use std::string_view for read-only access and snprintf with fixed char[] buffers for construction.
5. UI Strings: All user-facing text must use the `tr()` macro (e.g., `tr(STR_LOADING)`) for i18n support. Never hardcode UI strings directly. For the avoidance of doubt, logging messages (LOG_DBG/LOG_ERR) can be hardcoded, but user-facing text must use `tr()`.
6. `constexpr` First: Compile-time constants and lookup tables must be `constexpr`, not just `static const`. This moves computation to compile time, enables dead-branch elimination, and guarantees flash placement. Use `static constexpr` for class-level constants.
7. `std::vector` Pre-allocation: Always call `.reserve(N)` before any `push_back()` loop. Each growth event allocates a new block (2×), copies all elements, then frees the old one — three heap operations that fragment DRAM. When the final size is unknown, estimate conservatively.
8. SPIFFS Write Throttling: Never write a settings file on every user interaction. Guard all writes with a value-change check (`if (newVal == _current) return;`). Progress saves during reading must be debounced — write on activity exit or every N page turns, not on every turn. SPIFFS sectors have a finite erase cycle limit.
---
## Project Architecture
### Build System: PlatformIO
**PlatformIO is BOTH a VS Code extension AND a CLI tool**:
1. **VS Code Extension** (Recommended):
* Extension ID: `platformio.platformio-ide` (see `.vscode/extensions.json`)
* Provides: Toolbar buttons, IntelliSense, integrated build/upload/monitor
* Configuration: `.vscode/c_cpp_properties.json`, `.vscode/tasks.json`
* Usage: Click Build (✓), Upload (→), or Monitor (🔌) buttons
2. **CLI Tool** (`pio` command):
* **Installation**: Python package (typically `pip install platformio`)
* **Windows Location**: `C:\Users\<user>\AppData\Local\Programs\Python\Python3xx\Scripts\pio.exe`
* **Verify**: `which pio` (Git Bash) or `where.exe pio` (cmd)
* **Usage**: `pio run`, `pio run -t upload`, etc.
**Configuration Files**:
* `platformio.ini`: Main build configuration (committed to git)
* `platformio.local.ini`: Local overrides (gitignored, create if needed)
* `partitions.csv`: ESP32 flash partition layout
### Build Environment
* **Standard**: C++20 (`-std=c++2a`). No Exceptions, No RTTI.
* **Logging**: ALWAYS use `LOG_INF`, `LOG_DBG`, or `LOG_ERR` from `Logging.h`. Raw Serial output is deprecated.
* **Environments** (in `platformio.ini`):
* `default`: Development (LOG_LEVEL=2, serial enabled)
* `gh_release`: Production (LOG_LEVEL=0)
* `gh_release_rc`: Release candidate (LOG_LEVEL=1)
* `slim`: Minimal build (no serial logging)
### Critical Build Flags
These flags in `platformio.ini` fundamentally affect firmware behavior:
```cpp
-DEINK_DISPLAY_SINGLE_BUFFER_MODE=1 // Single framebuffer (saves 48KB RAM!)
-DARDUINO_USB_MODE=1 // Enable USB CDC
-DARDUINO_USB_CDC_ON_BOOT=1 // Serial available immediately at boot
-DXML_CONTEXT_BYTES=1024 // XML parser memory limit (EPUB parsing)
-DUSE_UTF8_LONG_NAMES=1 // SD card long filename support
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1 // Avoid zlib name conflicts
-DXML_GE=0 // Disable XML general entities (security)
```
**SINGLE_BUFFER_MODE implications**:
- Only ONE framebuffer exists (not double-buffered)
- Grayscale rendering requires temporary buffer allocation (`renderer.storeBwBuffer()`)
- Must call `renderer.restoreBwBuffer()` to free temporary buffers
- See [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp) for malloc usage
### Directory Structure
* lib/: Internal libraries (Epub engine, GfxRenderer, UITheme, I18n)
* lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage)
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
* src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit)
* open-x4-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
* .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections
### Hardware Abstraction Layer (HAL)
**CRITICAL**: Always use HAL classes, NOT SDK classes directly.
| HAL Class | Wraps SDK Class | Purpose | Singleton Macro |
|-----------|----------------|---------|-----------------|
| `HalDisplay` | `EInkDisplay` | E-ink display control | *(none)* |
| `HalGPIO` | `InputManager` | Button input handling | *(none)* |
| `HalStorage` | `SDCardManager` | SD card file I/O | `Storage` |
**Location**: [lib/hal/](../lib/hal/)
**Why HAL?**
- Provides consistent error logging per module
- Abstracts SDK implementation details
- Centralizes resource management
**Example - HalStorage**:
```cpp
#include <HalStorage.h>
// Use Storage singleton (defined via macro)
FsFile file;
if (Storage.openFileForRead("MODULE", "/path/to/file.bin", file)) {
// Read from file
file.close(); // Explicit close required
}
```
**Usage**: See example above. Uses `FsFile` (SdFat), NOT Arduino `File`.
---
## Coding Standards
### Naming Conventions
* Classes: PascalCase (e.g., EpubReaderActivity)
* Methods/Variables: camelCase (e.g., renderPage())
* Constants: UPPER_SNAKE_CASE (e.g., MAX_BUFFER_SIZE)
* Private Members: memberVariable (no prefix)
* File Names: Match Class names (e.g., EpubReaderActivity.cpp)
### Header Guards
* Use #pragma once for all header files.
### Memory Safety and RAII
* Smart Pointers: Prefer std::unique_ptr. Avoid std::shared_ptr (unnecessary atomic overhead for a single-core RISC-V).
* RAII: Use destructors for cleanup, but call file.close() or vTaskDelete() explicitly for deterministic resource release.
### ESP32-C3 Platform Pitfalls
#### `std::string_view` and Null Termination
`string_view` is *not* null-terminated. Passing `.data()` to any C-style API (`drawText`, `snprintf`, `strcmp`, SdFat file paths) is undefined behaviour when the view is a substring or a view of a non-null-terminated buffer.
**Rule**: `string_view` is safe only when passing to C++ APIs that accept `string_view`. For any C API boundary, convert explicitly:
```cpp
// WRONG - undefined behaviour if view is a substring:
renderer.drawText(font, x, y, myView.data(), true);
// CORRECT - guaranteed null-terminated:
renderer.drawText(font, x, y, std::string(myView).c_str(), true);
// CORRECT - for short strings, use a stack buffer:
char buf[64];
snprintf(buf, sizeof(buf), "%.*s", (int)myView.size(), myView.data());
```
#### `IRAM_ATTR` and Flash Cache Safety
All code runs from flash via the instruction cache. During SPI flash operations (OTA write, SPIFFS commit, NVS update) the cache is briefly suspended. Any code that can execute during this window — ISRs in particular — must reside in IRAM or it will crash silently.
```cpp
// ISR handler: must be in IRAM
void IRAM_ATTR gpioISR() { ... }
// Data accessed from IRAM_ATTR code: must be in DRAM, never a flash const
static DRAM_ATTR uint32_t isrEventFlags = 0;
```
**Rules**:
- All ISR handlers: `IRAM_ATTR`
- Data read by `IRAM_ATTR` code: `DRAM_ATTR` (a flash-resident `static const` will fault)
- Normal task code does **not** need `IRAM_ATTR`
#### ISR vs Task Shared State
`xSemaphoreTake()` (mutex) **cannot** be called from ISR context — it will crash. Use the correct primitive for each communication direction:
| Direction | Correct primitive |
|---|---|
| ISR → task (data) | `xQueueSendFromISR()` + `portYIELD_FROM_ISR()` |
| ISR → task (signal) | `xSemaphoreGiveFromISR()` + `portYIELD_FROM_ISR()` |
| Task → task | `xSemaphoreTake()` / mutex |
| Simple flag (single writer ISR) | `volatile bool` + `portENTER_CRITICAL_ISR()` |
#### RISC-V Alignment
ESP32-C3 faults on unaligned multi-byte loads. Never cast a `uint8_t*` buffer to a wider pointer type and dereference it directly. Use `memcpy` for any unaligned read:
```cpp
// WRONG — faults if buf is not 4-byte aligned:
uint32_t val = *reinterpret_cast<const uint32_t*>(buf);
// CORRECT:
uint32_t val;
memcpy(&val, buf, sizeof(val));
```
This applies to all cache deserialization code and any raw buffer-to-struct casting. `__attribute__((packed))` structs have the same hazard when accessed via member reference.
#### Template and `std::function` Bloat
Each template instantiation generates a separate binary copy. `std::function<void()>` adds ~24 KB per unique signature and heap-allocates its closure. Avoid both in library code and any path called from the render loop:
```cpp
// Avoid — heap-allocating, large binary footprint:
std::function<void()> callback;
// Prefer — zero overhead:
void (*callback)() = nullptr;
// For member function + context (common activity callback pattern):
struct Callback { void* ctx; void (*fn)(void*); };
```
When a template is necessary, limit instantiations: use explicit template instantiation in a `.cpp` file to prevent the compiler from generating duplicates across translation units.
---
### Error Handling Philosophy
**Source**: [src/main.cpp:132-143](../src/main.cpp), [lib/GfxRenderer/GfxRenderer.cpp:10](../lib/GfxRenderer/GfxRenderer.cpp)
**Pattern Hierarchy**:
1. **LOG_ERR + return false** (90%): `LOG_ERR("MOD", "Failed: %s", reason); return false;`
2. **LOG_ERR + fallback**: `LOG_ERR("MOD", "Unavailable"); useDefault();`
3. **assert(false)**: Only for fatal "impossible" states (framebuffer missing)
4. **ESP.restart()**: Only for recovery (OTA complete)
**Rules**: NO exceptions, NO abort(), ALWAYS log before error return
### Acceptable malloc/free Patterns
**Source**: [src/activities/home/HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
Despite "prefer stack allocation," malloc is acceptable for:
1. **Large temporary buffers** (> 256 bytes, won't fit on stack)
2. **One-time allocations** during activity initialization
3. **Bitmap rendering buffers** (variable size, used briefly)
**Pattern**:
```cpp
// Allocate
auto* buffer = static_cast<uint8_t*>(malloc(bufferSize));
if (!buffer) {
LOG_ERR("MODULE", "malloc failed: %d bytes", bufferSize);
return false; // Handle allocation failure
}
// Use buffer
processData(buffer, bufferSize);
// Free immediately after use
free(buffer);
buffer = nullptr;
```
**Rules**:
- **ALWAYS check for nullptr** after malloc
- **Free immediately** after use (don't hold across multiple operations)
- **Set to nullptr** after free (avoid use-after-free)
- **Document size**: Comment why stack allocation was rejected
**Examples in codebase**:
- Cover image buffers: [HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp)
- Text chunk buffers: [TxtReaderActivity.cpp:259](../src/activities/reader/TxtReaderActivity.cpp)
- Bitmap rendering: [GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp)
- OTA update buffer: [OtaUpdater.cpp:40](../src/network/OtaUpdater.cpp)
---
## UI and Orientation Guidelines
### Orientation-Aware Logic
* No Hardcoding: Never assume 800 or 480. Use renderer.getScreenWidth() and renderer.getScreenHeight().
* Viewable Area: Use renderer.getOrientedViewableTRBL() to stay within physical bezel margins.
### Logical Button Mapping
**Source**: [src/MappedInputManager.cpp:20-55](../src/MappedInputManager.cpp)
Constraint: Physical button positions are fixed on hardware, but their logical functions change based on user settings and screen orientation.
**Button Categories**:
1. **Physical Fixed** (Up/Down side buttons):
- `Button::Up` → Always `HalGPIO::BTN_UP`
- `Button::Down` → Always `HalGPIO::BTN_DOWN`
2. **User Remappable** (Front buttons):
- `Button::Back` → Maps to `SETTINGS.frontButtonBack` (hardware index)
- `Button::Confirm` → Maps to `SETTINGS.frontButtonConfirm`
- `Button::Left` → Maps to `SETTINGS.frontButtonLeft`
- `Button::Right` → Maps to `SETTINGS.frontButtonRight`
3. **Reader-Specific** (Page navigation with optional swap):
- `Button::PageBack` → Uses side button (swappable via `SETTINGS.sideButtonLayout`)
- `Button::PageForward` → Uses side button (swappable)
**Implementation**:
- Activities use **logical buttons** (e.g., `Button::Confirm`)
- `MappedInputManager` translates to **physical hardware buttons**
- User can remap front buttons in settings
- Orientation changes handled separately by renderer coordinate transforms
**Rule**: Always use `MappedInputManager::Button::*` enums, never raw `HalGPIO::BTN_*` indices (except in ButtonRemapActivity).
### UITheme (The GUI Macro)
* Rule: All UI rendering must go through the GUI macro (UITheme).
* Do not hardcode fonts, colors, or positioning. This ensures orientation-aware layout consistency.
---
## Common Patterns
### Singleton Access
**Available Singletons**:
```cpp
#define SETTINGS CrossPointSettings::getInstance() // User settings
#define APP_STATE CrossPointState::getInstance() // Runtime state
#define GUI UITheme::getInstance() // Current theme
#define Storage HalStorage::getInstance() // SD card I/O
#define I18N I18n::getInstance() // Internationalization
```
### Activity Lifecycle and Memory Management
**Source**: [src/main.cpp:132-143](../src/main.cpp)
**CRITICAL**: Activities are **heap-allocated** and **deleted on exit**.
```cpp
// main.cpp navigation pattern
void exitActivity() {
if (currentActivity) {
currentActivity->onExit();
delete currentActivity; // Activity deleted here!
currentActivity = nullptr;
}
}
void enterNewActivity(Activity* activity) {
currentActivity = activity; // Heap-allocated activity
currentActivity->onEnter();
}
```
**Memory Implications**:
- Activity navigation = `delete` old activity + `new` create next activity
- Any memory allocated in `onEnter()` MUST be freed in `onExit()`
- FreeRTOS tasks MUST be deleted in `onExit()` before activity destruction
- File handles MUST be closed in `onExit()`
**Activity Pattern**:
```cpp
void onEnter() { Activity::onEnter(); /* alloc: buffer, tasks */ render(); }
void loop() { mappedInput.update(); /* handle input */ }
void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::onExit(); }
```
**Critical**: Free resources in reverse order. Delete tasks BEFORE activity destruction.
### FreeRTOS Task Guidelines
**Source**: [src/activities/util/KeyboardEntryActivity.cpp:45-50](../src/activities/util/KeyboardEntryActivity.cpp)
**Pattern**: See Activity Lifecycle above. `xTaskCreate(&taskTrampoline, "Name", stackSize, this, 1, &handle)`
**Stack Sizing** (in BYTES, not words):
- **2048**: Simple rendering (most activities)
- **4096**: Network, EPUB parsing
- Monitor: `uxTaskGetStackHighWaterMark()` if crashes
**Rules**: Always `vTaskDelete()` in `onExit()` before destruction. Use mutex if shared state.
### Global Font Loading
**Source**: [src/main.cpp:40-115](../src/main.cpp)
**All fonts are loaded as global static objects** at firmware startup:
- Bookerly: 12, 14, 16, 18pt (4 styles each: regular, bold, italic, bold-italic)
- Noto Sans: 12, 14, 16, 18pt (4 styles each)
- OpenDyslexic: 8, 10, 12, 14pt (4 styles each)
- Ubuntu UI fonts: 10, 12pt (2 styles)
**Total**: ~80+ global `EpdFont` and `EpdFontFamily` objects
**Compilation Flag**:
```cpp
#ifndef OMIT_FONTS
// Most fonts loaded here
#endif
```
**Implications**:
- Fonts stored in **Flash** (marked as `static const` in `lib/EpdFont/builtinFonts/`)
- Font rendering data cached in **DRAM** when first used
- `OMIT_FONTS` can reduce binary size for minimal builds
- Font IDs defined in [src/fontIds.h](../src/fontIds.h)
**Usage**:
```cpp
#include "fontIds.h"
renderer.insertFont(FONT_UI_MEDIUM, ui12FontFamily);
renderer.drawText(FONT_UI_MEDIUM, x, y, "Hello", true);
```
---
## Testing and Debugging
### Build Commands
**Via CLI**:
```bash
# Build firmware (default environment)
pio run
# Build and upload to device
pio run -t upload
# Build specific environment
pio run -e gh_release
# Clean build artifacts
pio run -t clean
# Upload filesystem data (if using SPIFFS/LittleFS)
pio run -t uploadfs
```
**Via VS Code**:
* Use PlatformIO toolbar: Build (✓), Upload (→), Clean (🗑️)
* Or Command Palette: `PlatformIO: Build`, `PlatformIO: Upload`, etc.
### Monitoring and Debugging
```bash
# Enhanced monitor with color/logging (recommended)
python3 scripts/debugging_monitor.py
# Standard PlatformIO monitor
pio device monitor
# Combined upload + monitor
pio run -t upload && pio device monitor
```
**Via VS Code**: Click Monitor (🔌) button in PlatformIO toolbar
### Code Quality
```bash
# Static analysis (cppcheck)
pio check
# Format code (clang-format) - Windows Git Bash
find src -name "*.cpp" -o -name "*.h" | xargs clang-format -i
# Format code (clang-format) - Linux
clang-format -i src/**/*.cpp src/**/*.h
```
### Debugging Crashes
**Common Crash Causes**:
1. **Out of Memory** (Most common):
```cpp
LOG_DBG("MEM", "Free heap: %d bytes", ESP.getFreeHeap());
```
- Monitor heap usage throughout activity lifecycle
- Check if large allocations (>10KB) occur before crash
- Verify buffers are freed in `onExit()`
2. **Stack Overflow**:
```cpp
LOG_DBG("TASK", "Stack high water: %d", uxTaskGetStackHighWaterMark(taskHandle));
```
- Occurs during deep recursion or large local variables
- Increase task stack size in `xTaskCreate()` (2048 → 4096)
- Move large buffers to heap with malloc
3. **Use-After-Free**:
- Activity deleted but task still running
- Always `vTaskDelete()` in `onExit()` BEFORE activity destruction
- Set pointers to `nullptr` after `free()`
4. **Corrupt Cache Files**:
- Delete `.crosspoint/` directory on SD card
- Forces clean re-parse of all EPUBs
- Check file format versions in [docs/file-formats.md](../docs/file-formats.md)
5. **Watchdog Timeout**:
- Loop/task blocked for >5 seconds
- Add `vTaskDelay(1)` in tight loops
- Check for blocking I/O operations
**Verification Steps**:
1. Check serial output for stack traces
2. Monitor heap with `ESP.getFreeHeap()` before/after operations
3. Verify task deletion with task list (`vTaskList()`)
4. Test with `LOG_LEVEL=2` (debug logging enabled)
---
## Git Workflow and Repository Awareness
### Repository Detection Protocol
**CRITICAL**: ALWAYS verify repository context before git operations. This could be:
- A **fork** with `origin` pointing to personal repo, `upstream` to main repo
- A **direct clone** with `origin` pointing to main repo
- Multiple collaborator remotes
**Verification Commands** (run at session start):
```bash
# Check current branch
git branch --show-current
# Check all remotes
git remote -v
# Identify main branch name (could be 'main' or 'master')
git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@'
# Check working tree status
git status --short
```
**Example Output** (forked repository):
```text
origin https://github.com/<your-username>/crosspoint-reader.git (fetch/push)
upstream https://github.com/crosspoint-reader/crosspoint-reader.git (fetch/push)
```
### Git Operation Rules
1. **Never assume branch names**:
```bash
# Bad: git push origin main
# Good: git push origin $(git branch --show-current)
```
2. **Never assume remote names or write permissions**:
- **Forked repos**: Push to `origin` (your fork), submit PR to `upstream`
- **Direct contributors**: May push feature branches to `upstream`
- **Always ask**: "Should I push to origin or create a PR?"
3. **Check for upstream changes before starting work**:
```bash
# Sync fork with upstream (if applicable)
git fetch upstream
git merge upstream/main # or upstream/master
```
4. **Use explicit remote and branch names**:
```bash
# Check remotes first
git remote -v
# Use explicit syntax
git push <remote> <branch>
```
### Branch Naming Convention
**For feature/fix branches**:
```text
feature/<short-description> # New features
fix/<issue-number>-<description> # Bug fixes
refactor/<component-name> # Code refactoring
docs/<topic> # Documentation updates
```
**Examples**:
- `feature/sd-download-progress`
- `fix/123-orientation-crash`
- `refactor/hal-storage`
### Commit Message Format
**Pattern**:
```text
<type>: <short summary (50 chars max)>
<optional detailed description>
```
**Types**: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`
**Example**:
```text
feat: add real-time SD download progress bar
Implements progress tracking for book downloads using
UITheme progress bar component with heap-safe updates.
Tested in all 4 orientations with 5MB+ files.
```
### When to Commit
**DO commit when**:
- User explicitly requests: "commit these changes"
- Feature is complete and tested on device
- Bug fix is verified working
- Refactoring preserves all functionality
- All tests pass (`pio run` succeeds)
**DO NOT commit when**:
- Changes are untested on actual hardware
- Build fails or has warnings
- Experimenting or debugging in progress
- User hasn't explicitly requested commit
- Files excluded by `.gitignore` would be included — always run `git status` and cross-check against `.gitignore` before staging (e.g., `*.generated.h`, `.pio/`, `compile_commands.json`, `platformio.local.ini`)
**Rule**: **If uncertain, ASK before committing.**
---
## Generated Files and Build Artifacts
### Files Generated by Build Scripts
**NEVER manually edit these files** - they are regenerated automatically:
1. **HTML Headers** (generated by `scripts/build_html.py`):
- `src/network/html/*.generated.h`
- **Source**: HTML templates in `data/html/` directory
- **Triggered**: During PlatformIO `pre:` build step
- **To modify**: Edit source HTML in `data/html/`, not generated headers
2. **I18n Headers** (generated by `scripts/gen_i18n.py`):
- `lib/I18n/I18nKeys.h`, `lib/I18n/I18nStrings.h`, `lib/I18n/I18nStrings.cpp`
- **Source**: YAML translation files in `lib/I18n/translations/` (one per language)
- **To modify**: Edit source YAML files, then run `python scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
- **Commit**: Source YAML files + `I18nKeys.h` and `I18nStrings.h` (needed for IDE symbol resolution), but NOT `I18nStrings.cpp`
3. **Build Artifacts** (in `.gitignore`):
- `.pio/` - PlatformIO build output
- `build/` - Compiled binaries
- `*.generated.h` - Any auto-generated headers
- `compile_commands.json` - LSP/IDE metadata
### Modifying Generated Content Workflow
**To change HTML pages**:
1. Edit source: `data/html/<pagename>.html`
2. Build: `pio run` (auto-triggers `scripts/build_html.py`)
3. Generated headers update: `src/network/html/<pagename>Html.generated.h`
4. **Commit ONLY** source HTML, NOT generated `.generated.h` files
**To add/modify translations (i18n)**:
1. Edit or add YAML file: `lib/I18n/translations/<language>.yaml`
- Each file must contain: `_language_name`, `_language_code`, `_order`, and `STR_*` keys
- English (`english.yaml`) is the reference; missing keys in other languages fall back to English
2. Run generator: `python scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
3. Generated files update: `I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`
4. **Commit** source YAML files + `I18nKeys.h` and `I18nStrings.h` (IDE needs these for symbol resolution), but NOT `I18nStrings.cpp`
**To use translated strings in code**:
```cpp
#include <I18n.h>
// Use tr() macro with StrId enum (defined in generated I18nKeys.h)
renderer.drawText(FONT_UI, x, y, tr(STR_LOADING), true);
```
**To add custom fonts**:
1. Place source fonts in `lib/EpdFont/fontsrc/` (gitignored)
2. Run conversion script (see `lib/EpdFont/README`)
3. Update global font objects in `src/main.cpp:40-115`
4. Add font ID constant to `src/fontIds.h`
---
## Local Development Configuration
### platformio.local.ini (Personal Overrides)
**Purpose**: Personal development settings that should NEVER be committed.
**Use Cases**:
- Serial port configuration (varies by machine)
- Debug flags for specific testing
- Local build optimizations
- Developer-specific paths
**Example** `platformio.local.ini`:
```ini
# platformio.local.ini (gitignored)
[env:default]
upload_port = COM7 # Windows: COMx, Linux: /dev/ttyUSBx
monitor_port = COM7
build_flags =
${base.build_flags}
-DMY_DEBUG_FLAG=1 # Personal debug flags
-DTEST_FEATURE_ENABLED=1
```
**Configuration Hierarchy**:
1. `platformio.ini` - **Committed**, shared project settings
2. `platformio.local.ini` - **Gitignored**, personal overrides
3. Local file extends/overrides base config
**Rules**:
- **NEVER commit** `platformio.local.ini`
- **NEVER put** personal info (serial ports, credentials) in main `platformio.ini`
- Use `${base.build_flags}` to extend (not replace) base flags
---
## Testing and Verification Workflow
### Testing Checklist
**AI agent scope** (what you CAN verify):
1. ✅ **Build**: `pio run -t clean && pio run` (0 errors/warnings)
2. ✅ **Quality**: `pio check` + `find src -name "*.cpp" -o -name "*.h" | xargs clang-format -i`
3. ✅ **Format**: Commit messages (`feat:`/`fix:`), no `.gitignore`-excluded files staged (e.g., `*.generated.h`, `.pio/`, `platformio.local.ini`)
4. ✅ **CI**: Fix GitHub Actions failures before review
5. ✅ **Code review**: Ensure orientation-aware logic is correct in all 4 modes by inspecting switch/case coverage
**Human tester scope** (flag these for the user):
6. 🔲 **Device**: Test on hardware
7. 🔲 **Orientations**: Verify all 4 modes (Portrait/Inverted/Landscape CW/CCW)
8. 🔲 **Heap**: `ESP.getFreeHeap()` > 50KB, no leaks
9. 🔲 **Cache**: If EPUB modified, delete `.crosspoint/` and verify re-parse
### CI/CD Pipeline Awareness
**GitHub Actions** run automatically on pull requests:
| Workflow | File | Purpose |
|----------|------|---------|
| Build Check | `.github/workflows/ci.yml` | Verifies code compiles |
| Format Check | `.github/workflows/pr-formatting-check.yml` | Validates clang-format |
| Release Build | `.github/workflows/release.yml` | Production releases |
| RC Build | `.github/workflows/release_candidate.yml` | Release candidates |
**Rules**:
- **Fix CI failures BEFORE** requesting review
- CI runs on: Push to PR, PR updates
- Format check fails → Run clang-format locally
- Build check fails → Fix compile errors
---
## Serial Monitoring and Live Debugging
### Serial Monitor Options
1. **Enhanced**: `python3 scripts/debugging_monitor.py` (color-coded, recommended)
2. **Standard**: `pio device monitor` (basic, no colors)
3. **VS Code**: Monitor (🔌) button (IDE-integrated)
### Live Debugging Patterns
**Heap**: `LOG_DBG("MEM", "Free: %d", ESP.getFreeHeap());` (every 5s in loop)
**Stack**: `uxTaskGetStackHighWaterMark(nullptr)` (< 512 bytes → increase stack)
**Flush**: `logSerial.flush();` (force output before crash)
**Port Detection**: Windows: `mode` | Linux: `ls /dev/ttyUSB* /dev/ttyACM*` or `dmesg | grep tty`
---
## Cache Management and Invalidation
### Cache Structure on SD Card
**Location**: `.crosspoint/` directory on SD card root
**Structure**: `.crosspoint/epub_<hash>/{book.bin, progress.bin, cover.bmp, sections/*.bin}`
**Hash**: `std::hash<std::string>{}(filepath)` → Moving/renaming file = new hash = lost progress
### Cache Invalidation Rules
**Cache is automatically invalidated when**:
1. **File format version changes** (see `docs/file-formats.md`)
- `book.bin` version number incremented
- `section.bin` version number incremented
2. **Render settings change**:
- Font family or size (`SETTINGS.fontFamily`, `SETTINGS.fontSize`)
- Line spacing (`SETTINGS.lineSpacing`)
- Paragraph spacing (`SETTINGS.extraParagraphSpacing`)
- Screen margins (`SETTINGS.screenMargin`)
3. **Viewport dimensions change**:
- Screen orientation change
- Display resolution change
4. **Book file modified**:
- Moved, renamed, or content changed (new hash)
**Manual Cache Clear** (safe operations):
```bash
# Delete ALL caches (forces full regeneration)
rm -rf /path/to/sd/.crosspoint/
# Delete specific book cache
rm -rf /path/to/sd/.crosspoint/epub_<hash>/
# Keep progress, delete only rendered sections
rm -rf /path/to/sd/.crosspoint/epub_<hash>/sections/
```
**When to Clear Cache**:
- EPUB parsing errors after code changes to `lib/Epub/`
- Corrupt rendering (missing text, wrong layout)
- Testing cache generation logic
- After modifying:
- `lib/Epub/Epub/Section.cpp`
- `lib/Epub/Epub/BookMetadataCache.cpp`
- Render settings in `CrossPointSettings`
### Cache File Format Versioning
**Source**: `lib/Epub/Epub/Section.cpp`, `lib/Epub/Epub/BookMetadataCache.cpp`
**Current Versions** (as of docs/file-formats.md):
- `book.bin`: **Version 5** (metadata structure)
- `section.bin`: **Version 12** (layout structure)
**Version Increment Rules**:
1. **ALWAYS increment version** BEFORE changing binary structure
2. Version mismatch → Cache auto-invalidated and regenerated
3. Document format changes in `docs/file-formats.md`
**Example** (incrementing section format version):
```cpp
// lib/Epub/Epub/Section.cpp
static constexpr uint8_t SECTION_FILE_VERSION = 13; // Was 12, now 13
// Add new field to structure
struct PageLine {
// ... existing fields ...
uint16_t newField; // New field added
};
```
---
Philosophy: We are building a dedicated e-reader, not a Swiss Army knife. If a feature adds RAM pressure without significantly improving the reading experience, it is Out of Scope.

View File

@@ -1 +0,0 @@
.skills/SKILL.md

View File

@@ -1,38 +0,0 @@
# Project Governance & Community Principles
CrossPoint Reader is a community-driven, open-source project. Our goal is to provide a high-quality, open-source
firmware alternative for the Xteink X4 hardware. To keep this project productive and welcoming as we grow, we ask all
contributors to follow these principles.
### 1. The "Human First" Rule
Technical discussions can get heated, but they should never be personal.
- **Assume good intent:** We are all volunteers working on this in our free time. If a comment seems abrasive, assume
its a language barrier or a misunderstanding before taking offense.
- **Focus on the code, not the person:** Critique the implementation, the performance, or the UX. Never the intelligence
or character of the contributor.
- **Inflammatory language:** Personal attacks, trolling, or exclusionary language (based on race, gender, background,
etc.) are not welcome here and will be moderated.
### 2. A "Do-ocracy" with Guidance
CrossPoint thrives because people step up to build what they want to see.
- If you want a feature, the best way to get it is to start an
[Idea Discussion](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas) or open a PR.
- If you want to report a bug, check for duplicates and create an
[Issue](https://github.com/crosspoint-reader/crosspoint-reader/issues).
- While we encourage experimentation, the maintainers reserve the right to guide the projects technical direction to
ensure stability on the ESP32-C3s constrained hardware.
- For more guidance on the scope of the project, see the [SCOPE.md](SCOPE.md) document.
### 3. Transparent Communication
To keep the project healthy, we keep our "work" in the open.
- **Public by Default:** All technical decisions and project management discussions happen in GitHub Issues, Pull
Requests, or the public Discussions tab.
- **Clarity in Writing:** Because we have a global community with different levels of English proficiency, please be as
explicit and clear as possible in your PR descriptions and bug reports.
### 4. Moderation & Safety
The maintainers are responsible for keeping the community a safe place to contribute.
- We reserve the right to hide comments, lock threads, or block users who repeatedly violate these principles or engage
in harassment.
- **Reporting:** If you feel you are being harassed or see behavior that is damaging the community, please reach out
privately to @daveallie.

View File

@@ -26,7 +26,7 @@ This project is **not affiliated with Xteink**; it's built as a community projec
## Features & Usage
- [x] EPUB parsing and rendering (EPUB 2 and EPUB 3)
- [x] Image support within EPUB
- [ ] Image support within EPUB
- [x] Saved reading position
- [x] File explorer with file picker
- [x] Basic EPUB picker from root directory
@@ -36,7 +36,6 @@ This project is **not affiliated with Xteink**; it's built as a community projec
- [x] Cover sleep screen
- [x] Wifi book upload
- [x] Wifi OTA updates
- [x] KOReader Sync integration for cross-device reading progress
- [x] Configurable font, layout, and display options
- [ ] User provided fonts
- [ ] Full UTF support
@@ -44,16 +43,13 @@ This project is **not affiliated with Xteink**; it's built as a community projec
Multi-language support: Read EPUBs in various languages, including English, Spanish, French, German, Italian, Portuguese, Russian, Ukrainian, Polish, Swedish, Norwegian, [and more](./USER_GUIDE.md#supported-languages).
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint, including the
[KOReader Sync quick setup](./USER_GUIDE.md#365-koreader-sync-quick-setup).
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
## Installing
### Web (latest firmware)
1. Connect your Xteink X4 to your computer via USB-C and wake/unlock the device
1. Connect your Xteink X4 to your computer via USB-C
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
@@ -62,7 +58,7 @@ back to the other partition using the "Swap boot partition" button here https://
### Web (specific firmware version)
1. Connect your Xteink X4 to your computer via USB-C
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases)
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/daveallie/crosspoint-reader/releases)
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
@@ -86,7 +82,7 @@ See [Development](#development) below.
CrossPoint uses PlatformIO for building and flashing the firmware. To get started, clone the repository:
```
git clone --recursive https://github.com/crosspoint-reader/crosspoint-reader
git clone --recursive https://github.com/daveallie/crosspoint-reader
# Or, if you've already cloned without --recursive:
git submodule update --init --recursive
@@ -99,25 +95,6 @@ Connect your Xteink X4 to your computer via USB-C and run the following command.
```sh
pio run --target upload
```
### Debugging
After flashing the new features, its recommended to capture detailed logs from the serial port.
First, make sure all required Python packages are installed:
```python
python3 -m pip install pyserial colorama matplotlib
```
after that run the script:
```sh
# For Linux
# This was tested on Debian and should work on most Linux systems.
python3 scripts/debugging_monitor.py
# For macOS
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
```
Minor adjustments may be required for Windows.
## Internals
@@ -156,14 +133,9 @@ For more details on the internal file structures, see the [file formats document
Contributions are very welcome!
If you are new to the codebase, start with the [contributing docs](./docs/contributing/README.md).
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas).
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/daveallie/crosspoint-reader/discussions/categories/ideas).
If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort.
Everyone here is a volunteer, so please be respectful and patient. For more details on our goverance and community
principles, please see [GOVERNANCE.md](GOVERNANCE.md).
### To submit a contribution:
1. Fork the repo

View File

@@ -1,58 +0,0 @@
# Project Vision & Scope: CrossPoint Reader
The goal of CrossPoint Reader is to create an efficient, open-source reading experience for the Xteink X4. We believe a
dedicated e-reader should do one thing exceptionally well: **facilitate focused reading.**
## 1. Core Mission
To provide a lightweight, high-performance firmware that maximizes the potential of the X4, prioritizing legibility and
usability over "swiss-army-knife" functionality.
## 2. Scope
### In-Scope
*These are features that directly improve the primary purpose of the device.*
* **User Experience:** E.g. User-friendly interfaces, and interactions, both inside the reader and navigating the
firmware. This includes things like button mapping, book loading, and book navigation like bookmarks.
* **Document Rendering:** E.g. Support for rendering documents (primarily EPUB) and improvements to the rendering
engine.
* **Format Optimization:** E.g. Efficiently parsing EPUB (CSS/Images) and other documents within the device's
capabilities.
* **Typography & Legibility:** E.g. Custom font support, hyphenation engines, and adjustable line spacing.
* **E-Ink Driver Refinement:** E.g. Reducing full-screen flashes (ghosting management) and improving general rendering.
* **Library Management:** E.g. Simple, intuitive ways to organize and navigate a collection of books.
* **Local Transfer:** E.g. Simple, "pull" based book loading via a basic web-server or public and widely-used standards.
* **Language Support:** E.g. Support for multiple languages both in the reader and in the interfaces.
* **Reference Tools:** E.g. Local dictionary lookup. Providing quick, offline definitions to enhance comprehension
without breaking focus.
### Out-of-Scope
*These items are rejected because they compromise the device's stability or mission.*
* **Interactive Apps:** No Notepads, Calculators, or Games. This is a reader, not a PDA.
* **Active Connectivity:** No RSS readers, News aggregators, or Web browsers. Background Wi-Fi tasks drain the battery
and complicate the single-core CPU's execution.
* **Media Playback:** No Audio players or Audio-books.
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
capabilities and more powerful chips.
### In-scope — Technically Unsupported
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
* **Clock Display:** The ESP32-C3's RTC drifts significantly during deep sleep; making the clock untrustworthy after any sleep cycle. NTP sync could help, but CrossPoint doesn't connect to the internet on every boot.
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text — resulting in constant panning and zooming that makes for a poor reading experience on e-ink.
## 3. Idea Evaluation
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be
a lightweight, reliable, and performant e-reader. Things which distract or compromise the device's core mission will not
be accepted. As a guiding question, consider if your idea improve the "core reading experience" for the average user,
and, critically, not distract from that reading experience.
> **Note to Contributors:** If you are unsure if your idea fits the scope, please open a **Discussion** before you start
> coding!

View File

@@ -10,31 +10,22 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [First Launch](#first-launch)
- [3. Screens](#3-screens)
- [3.1 Home Screen](#31-home-screen)
- [3.2 Reading Mode](#32-reading-mode)
- [3.3 Browse Files Screen](#33-browse-files-screen)
- [3.4 Recent Books Screen](#34-recent-books-screen)
- [3.5 File Transfer Screen](#35-file-transfer-screen)
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
- [3.6 Settings](#36-settings)
- [3.6.1 Display](#361-display)
- [3.6.2 Reader](#362-reader)
- [3.6.3 Controls](#363-controls)
- [3.6.4 System](#364-system)
- [3.6.5 KOReader Sync Quick Setup](#365-koreader-sync-quick-setup)
- [3.7 Sleep Screen](#37-sleep-screen)
- [3.2 Book Selection](#32-book-selection)
- [3.3 Reading Mode](#33-reading-mode)
- [3.4 File Upload Screen](#34-file-upload-screen)
- [3.5 Settings](#35-settings)
- [3.6 Sleep Screen](#36-sleep-screen)
- [4. Reading Mode](#4-reading-mode)
- [Page Turning](#page-turning)
- [Chapter Navigation](#chapter-navigation)
- [System Navigation](#system-navigation)
- [Supported Languages](#supported-languages)
- [5. Chapter Selection Screen](#5-chapter-selection-screen)
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
- [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
## 1. Hardware Overview
The device utilises the standard buttons on the Xteink X4 (in the same layout as the manufacturer firmware, by default):
The device utilises the standard buttons on the Xtink X4 (in the same layout as the manufacturer firmware, by default):
### Button Layout
| Location | Buttons |
@@ -42,12 +33,7 @@ The device utilises the standard buttons on the Xteink X4 (in the same layout as
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
| **Right Side** | **Power**, **Volume Up**, **Volume Down**, **Reset** |
Button layout can be customized in the **[Controls Settings](#363-controls)**.
### Taking a Screenshot
When the Power Button and Volume Down button are pressed at the same time, it will take a screenshot and save it in the folder `screenshots/`.
Alternatively, while reading a book, press the **Confirm** button to open the reader menu and select **Take screenshot**.
Button layout can be customized in **[Settings](#35-settings)**.
---
@@ -56,9 +42,9 @@ Alternatively, while reading a book, press the **Confirm** button to open the re
### Power On / Off
To turn the device on or off, **press and hold the Power button for approximately half a second**.
In the **[Controls Settings](#363-controls)** you can configure the power button to turn the device off with a short press instead of a long one.
In **[Settings](#35-settings)** you can configure the power button to turn the device off with a short press instead of a long one.
To reboot the device (for example after a firmware update or if it's frozen), press and release the Reset button, and then quickly press and hold the Power button for a few seconds.
To reboot the device (for example if it's frozen, or after a firmware update), press and release the Reset button, and then quickly press and hold the Power button for a few seconds.
### First Launch
@@ -73,34 +59,29 @@ Upon turning the device on for the first time, you will be placed on the **[Home
### 3.1 Home Screen
The Home screen is the main entry point to the firmware. From here you can navigate to **[Reading Mode](#4-reading-mode)** with the most recently read book, the **[Browse Files](#33-browse-files-screen)** screen, the **[Recent Books](#34-recent-books-screen)** screen, the **[File Transfer](#35-file-transfer-screen)** screen, or **[Settings](#36-settings)**.
The Home Screen is the main entry point to the firmware. From here you can navigate to **[Reading Mode](#4-reading-mode)** with the most recently read book, **[Book Selection](#32-book-selection)**, **[Settings](#35-settings)**, or the **[File Upload](#34-file-upload-screen)** screen.
### 3.2 Reading Mode
### 3.2 Book Selection
The Book Selection acts as a folder and file browser.
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up and down through folders and books. You can also long-press these buttons to scroll a full page up or down.
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
### 3.3 Reading Mode
See [Reading Mode](#4-reading-mode) below for more information.
### 3.3 Browse Files Screen
### 3.4 File Upload Screen
The Browse Files screen acts as a file and folder browser.
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up and down through folders and books. You can also long-press these buttons to scroll a full page up or down.
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
* **Delete Files:** Hold and release **Confirm** to delete the selected file. You will be given an option to either confirm or cancel deletion. Folder deletion is not supported.
### 3.4 Recent Books Screen
The Recent Books screen lists the most recently opened books in a chronological view, displaying title and author.
### 3.5 File Transfer Screen
The File Transfer screen allows you to upload new e-books to the device. When you enter the screen, you'll be prompted with a WiFi selection dialog and then your X4 will start hosting a web server.
The File Upload screen allows you to upload new e-books to the device. When you enter the screen, you'll be prompted with a WiFi selection dialog and then your X4 will start hosting a web server.
See the [webserver docs](./docs/webserver.md) for more information on how to connect to the web server and upload files.
> [!TIP]
> Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details.
### 3.5.1 Calibre Wireless Transfers
### 3.4.1 Calibre Wireless Transfers
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
@@ -112,226 +93,73 @@ CrossPoint supports sending books from Calibre using the CrossPoint Reader devic
3. Make sure your computer is on the same WiFi network.
4. In Calibre, click "Send to device" to transfer books.
### 3.6 Settings
### 3.5 Settings
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
#### 3.6.1 Display
- **Sleep Screen**: Which sleep screen to display when the device sleeps:
- "Dark" (default) - The default dark Crosspoint logo sleep screen
- "Light" - The same default sleep screen, on a white background
- "Custom" - Custom images from the SD card; see [Sleep Screen](#37-sleep-screen) below for more information
- "Custom" - Custom images from the SD card; see [Sleep Screen](#36-sleep-screen) below for more information
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
- "None" - A blank screen
- "Cover + Custom" - The book cover image, falls back to "Custom" behavior
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
- "Crop" - Scale the image down and crop as necessary to try to fill the screen (Note: this is experimental and may not work as expected)
- **Sleep Screen Cover Filter**: What filter will be applied to the book cover when "Cover" sleep screen is selected:
- "Crop" - Scale the image down and crop as necessary to try to to fill the screen (Note: this is experimental and may not work as expected)
- **Sleep Screen Cover Filter**: What filter will be applied to the book cover when "Cover" sleep screen is selected
- "None" (default) - The cover image will be converted to a grayscale image and displayed as it is
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion
- "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion
- "Inverted" - The image will be inverted as in white&black and will be displayed without grayscale conversion
- **Status Bar**: Configure the status bar displayed while reading:
- "None" - No status bar
- "No Progress" - Show status bar without reading progress
- "Full w/ Percentage" - Show status bar with book progress (as percentage)
- "Full w/ Book Bar" - Show status bar with book progress (as bar)
- "Book Bar Only" - Show book progress (as bar)
- "Full w/ Chapter Bar" - Show status bar with chapter progress (as bar)
- **Hide Battery %**: Configure where to suppress the battery percentage display in the status bar; the battery icon will still be shown:
- "Never" (default) - Always show battery percentage
- "Full" - Show status bar with reading progress
- **Hide Battery %**: Configure where to suppress the battery pecentage display in the status bar; the battery icon will still be shown:
- "Never" - Always show battery percentage (default)
- "In Reader" - Show battery percentage everywhere except in reading mode
- "Always" - Always hide battery percentage
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting; options are every 1, 5, 10, 15, or 30 pages.
- **UI Theme**: Set which UI theme to use:
- "Classic" - The original Crosspoint theme
- "Lyra" - The new theme for Crosspoint featuring rounded elements and menu icons
- "Lyra Extended" - Lyra, but displays 3 books instead of 1 on the **[Home Screen](#31-home-screen)**
- **Sunlight Fading Fix**: Configure whether to enable a software-fix for the issue where white X4 models may fade when used in direct sunlight:
- "OFF" (default) - Disable the fix
- "ON" - Enable the fix
#### 3.6.2 Reader
- **Reader Font Family**: Choose the font used for reading:
- "Bookerly" (default) - Amazon's reading font
- "Noto Sans" - Google's sans-serif font
- "Open Dyslexic" - Font designed for readers with dyslexia
- **Reader Font Size**: Adjust the text size for reading; options are "Small", "Medium" (default), "Large", or "X Large".
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal" (default), or "Wide".
- **Reader Screen Margin**: Controls the screen margins in Reading Mode between 5 and 40 pixels in 5-pixel increments.
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
- **Embedded Style**: Whether to use the EPUB file's embedded HTML and CSS stylisation and formatting; options are "ON" or "OFF".
- **Hyphenation**: Whether to hyphenate text in Reading Mode; options are "ON" or "OFF".
- **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book. If disabled, paragraphs will not have vertical space between them, but will have first-line indentation.
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
- **Short Power Button Click**: Controls the effect of a short click of the power button:
- "Ignore" - Require a long press to turn off the device
- "Sleep" - A short press powers the device off
- "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off
- **Reading Orientation**: Set the screen orientation for reading EPUB files:
- "Portrait" (default) - Standard portrait orientation
- "Landscape CW" - Landscape, rotated clockwise
- "Inverted" - Portrait, upside down
- "Landscape CCW" - Landscape, rotated counter-clockwise
- **Extra Paragraph Spacing**: Set how to handle paragraph breaks:
- "ON" - Vertical space will be added between paragraphs in Reading Mode
- "OFF" - Paragraphs will not have vertical space added, but will have first-line indentation
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
#### 3.6.3 Controls
- **Remap Front Buttons**: A menu for customising the function of each bottom edge button.
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from "Prev/Next" (default) to "Next/Prev". This change is only in effect when reading.
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skips to the next/previous chapter:
- **Front Button Layout**: Configure the order of the bottom edge buttons:
- Back, Confirm, Left, Right (default)
- Left, Right, Back, Confirm
- Left, Back, Confirm, Right
- Back, Confirm, Right, Left
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter.
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
- "Page Scroll" - Long-pressing scrolls a page up/down
- **Short Power Button Click**: Controls the effect of a short click of the power button:
- "Ignore" (default) - Require a long press to turn off the device
- "Sleep" - A short press puts the device into sleep mode
- "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off
#### 3.6.4 System
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep; options are 1, 5, 10 (default), 15 or 30 minutes.
- **WiFi Networks**: Connect to WiFi networks for file transfers and firmware updates.
- **KOReader Sync**: Options for setting up KOReader for syncing book progress.
- Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Reader Font Family**: Choose the font used for reading:
- "Bookerly" (default) - Amazon's reading font
- "Noto Sans" - Google's sans-serif font
- "Open Dyslexic" - Font designed for readers with dyslexia
- **Reader Font Size**: Adjust the text size for reading; options are "Small", "Medium", "Large", or "X Large".
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal", or "Wide".
- **Reader Screen Margin**: Controls the screen margins in reader mode between 5 and 40 pixels in 5 pixel increments.
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep.
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting.
- **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication.
- **Clear Reading Cache**: Clear the internal SD card cache.
- **Check for updates**: Check for Crosspoint firmware updates over WiFi.
- **Language**: Set the system language (see **[Supported Languages](#supported-languages)** for more information).
- **Check for updates**: Check for firmware updates over WiFi.
#### 3.6.5 KOReader Sync Quick Setup
### 3.6 Sleep Screen
CrossPoint can sync reading progress with KOReader-compatible sync servers.
It also interoperates with KOReader apps/devices when they use the same server and credentials.
You can customize the sleep screen by placing custom images in specific locations on the SD card:
##### Option A: Free Public Server (`sync.koreader.rocks`)
1. Register a user once (only if needed):
```bash
USERNAME="user"
PASSWORD="pass"
PASSWORD_MD5="$(printf '%s' "$PASSWORD" | openssl md5 | awk '{print $2}')"
curl -i "https://sync.koreader.rocks/users/create" \
-H "Accept: application/vnd.koreader.v1+json" \
-H "Content-Type: application/json" \
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
```
Already have KOReader Sync credentials? Skip registration; basic sync only requires using the same existing username/password on all devices.
When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account.
2. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
- Set **Sync Server URL** to `https://sync.koreader.rocks`, or leave it empty (both use the same default KOReader sync server).
- Run **Authenticate**.
3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
- Choose **Apply Remote** to jump to remote progress.
- Choose **Upload Local** to push current progress.
##### Option B: Self-Hosted Server (Docker Compose)
1. Start a sync server:
```bash
mkdir -p kosync-quickstart
cd kosync-quickstart
cat > compose.yaml <<'YAML'
services:
kosync:
image: koreader/kosync:latest
ports:
- "7200:7200"
- "17200:17200"
volumes:
- ./data/redis:/var/lib/redis
environment:
- ENABLE_USER_REGISTRATION=true
restart: unless-stopped
YAML
# Docker
docker compose up -d
# Podman (alternative)
podman compose up -d
```
- **Single Image:** Place a file named `sleep.bmp` in the root directory.
- **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images inside. If images are found in this directory, they will take priority over the `sleep.bmp` file, and one will be randomly selected each time the device sleeps.
> [!NOTE]
> `ENABLE_USER_REGISTRATION=true` is convenient for first setup. After creating your users, set it to `false` (or remove it) to avoid unexpected registrations.
2. Verify the server:
```bash
curl -H "Accept: application/vnd.koreader.v1+json" "http://<server-ip>:17200/healthcheck"
# Expected: {"state":"OK"}
```
3. Register a user once.
CrossPoint authenticates against KOReader Sync (`koreader/kosync`) using an MD5 key, so register using the MD5 of your password:
> [!WARNING]
> Sending a reusable MD5-derived password over plain HTTP is insecure.
> Create unique sync-only credentials and do not reuse main account passwords.
> Prefer `https://<server-ip>:7200` whenever traffic leaves a fully trusted LAN or when using untrusted networks.
> Use `curl -k` only for self-signed certificate testing.
```bash
USERNAME="user"
PASSWORD="pass"
PASSWORD_MD5="$(printf '%s' "$PASSWORD" | openssl md5 | awk '{print $2}')"
curl -i "http://<server-ip>:17200/users/create" \
-H "Accept: application/vnd.koreader.v1+json" \
-H "Content-Type: application/json" \
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
```
If this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, the account already exists.
4. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
- Set **Sync Server URL** to `http://<server-ip>:17200`.
- Run **Authenticate**.
If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing).
5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
- Choose **Apply Remote** to jump to remote progress.
- Choose **Upload Local** to push current progress.
### 3.7 Sleep Screen
The **Sleep Screen** setting controls what is displayed when the device goes to sleep:
| Mode | Behavior |
|------|----------|
| **Dark** (default) | The CrossPoint logo on a dark background. |
| **Light** | The CrossPoint logo on a white background. |
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
| **Cover + Custom** | The cover of the currently open book. Falls back to **Custom** behavior if no book is open. |
| **None** | A blank screen. |
#### Cover settings
When using **Cover** or **Cover + Custom**, two additional settings apply:
- **Sleep Screen Cover Mode**: **Fit** (scale to fit, white borders) or **Crop** (scale and crop to fill the screen).
- **Sleep Screen Cover Filter**: **None** (grayscale), **Contrast** (black & white), or **Inverted** (inverted black & white).
#### Custom images
To use custom sleep images, set the sleep screen mode to **Custom** or **Cover + Custom**, then place images on the SD card:
- **Multiple Images (recommended):** Create a `.sleep` directory in the root of the SD card and place any number of `.bmp` images inside. One will be randomly selected each time the device sleeps. (A directory named `sleep` is also accepted as a fallback.)
- **Single Image:** Place a file named `sleep.bmp` in the root directory. This is used as a fallback if no valid images are found in the `.sleep`/`sleep` directory.
> You'll need to set the **Sleep Screen** setting to **Custom** in order to use these images.
> [!TIP]
> For best results:
@@ -350,7 +178,7 @@ Once you have opened a book, the button layout changes to facilitate reading.
| **Previous Page** | Press **Left** _or_ **Volume Up** |
| **Next Page** | Press **Right** _or_ **Volume Down** |
The role of the volume (side) buttons can be swapped in the **[Controls Settings](#363-controls)**.
The role of the volume (side) buttons can be swapped in **[Settings](#35-settings)**.
If the **Short Power Button Click** setting is set to "Page Turn", you can also turn to the next page by briefly pressing the Power button.
@@ -358,13 +186,13 @@ If the **Short Power Button Click** setting is set to "Page Turn", you can also
* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release.
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
This feature can be disabled in the **[Controls Settings](#363-controls)** to help avoid changing chapters by mistake.
This feature can be disabled in **[Settings](#35-settings)** to help avoid changing chapters by mistake.
### System Navigation
* **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
* **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen.
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)** screen.
* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen.
* **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)**.
### Supported Languages
@@ -373,7 +201,7 @@ CrossPoint renders text using the following Unicode character blocks, enabling s
* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others.
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic, Greek and Farsi.
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic and Farsi.
---
@@ -392,18 +220,3 @@ Accessible by pressing **Confirm** while inside a book.
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
* **Images:** Embedded images in e-books will not render.
* **Cover Images:** Large cover images embedded into EPUB require several seconds (~10s for ~2000 pixel tall image) to convert for sleep screen and home screen thumbnail. Consider optimizing the EPUB with e.g. https://github.com/bigbag/epub-to-xtc-converter to speed this up.
---
## 7. Troubleshooting Issues & Escaping Bootloop
If an issue or crash is encountered while using Crosspoint, feel free to raise an issue ticket and attach the serial monitor logs. The logs can be obtained by connecting the device to a computer and starting a serial monitor. Either [Serial Monitor](https://www.serialmonitor.org/) or the following command can be used:
```
pio device monitor
```
If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen.
There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.bin`, `state.bin`, or `epub_*` cache directories in the `.crosspoint/` folder).

View File

@@ -1,33 +1,10 @@
#!/usr/bin/env bash
# Check if clang-format is available and pick the preferred binary.
if command -v clang-format-21 >/dev/null 2>&1; then
CLANG_FORMAT_BIN="clang-format-21"
elif command -v clang-format >/dev/null 2>&1; then
CLANG_FORMAT_BIN="clang-format"
else
printf "'clang-format' not found in current environment\n"
printf "Install clang-format-21 (recommended), clang, clang-tools, or clang-format depending on your distro/os and tooling requirements\n"
exit 1
fi
set -euo pipefail
#!/bin/bash
GIT_LS_FILES_FLAGS=""
if [[ "${1:-}" == "-g" ]]; then
if [[ "$1" == "-g" ]]; then
GIT_LS_FILES_FLAGS="--modified"
fi
CLANG_FORMAT_VERSION_RAW="$(${CLANG_FORMAT_BIN} --version)"
CLANG_FORMAT_MAJOR="$(printf '%s\n' "${CLANG_FORMAT_VERSION_RAW}" | grep -oE '[0-9]+' | head -n1)"
if [[ -z "${CLANG_FORMAT_MAJOR}" || "${CLANG_FORMAT_MAJOR}" -lt 21 ]]; then
echo "Error: ${CLANG_FORMAT_BIN} is too old: ${CLANG_FORMAT_VERSION_RAW}"
echo "This repository's .clang-format requires clang-format 21 or newer."
echo "Install clang-format-21 and rerun ./bin/clang-format-fix"
exit 1
fi
# --- Main Logic ---
# Format all files (or only modified files if -g is passed)
@@ -36,10 +13,8 @@ fi
# --modified: files tracked by git that have been modified (staged or unstaged)
# --exclude-standard: ignores files in .gitignore
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
# Also exclude files in 'lib/Epub/Epub/hyphenation/generated/' as they are script-generated.
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
| grep -E '\.(c|cpp|h|hpp)$' \
| grep -v -E '^lib/EpdFont/builtinFonts/' \
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
| grep -v -E '^lib/uzlib/' \
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
| grep -v -E '^lib/CrossPointFont/builtinFonts/' \
| grep -v -E '^lib/CrossPointFont/Group5' \
| xargs -r clang-format -style=file -i

View File

@@ -1,21 +0,0 @@
# Project Documentation for CrossPoint Reader Firmware
**Date:** 2026-02-09
## Task
Create three documentation files in `/mod/docs/` covering the Xteink X4 hardware capabilities, project file structure, and CI/build/code style for the CrossPoint Reader firmware.
## Changes Made
### New Files
1. **`mod/docs/hardware.md`** -- Device hardware capabilities documentation covering CPU (ESP32-C3), 16MB flash with partition layout, 480x800 eink display (2-bit grayscale, 3 refresh modes), 7 physical buttons, SD card storage, battery monitoring, WiFi networking, USB-C, and power management (deep sleep with GPIO wakeup).
2. **`mod/docs/file-structure.md`** -- Complete project file structure map documenting `src/` (main app with Activity-based UI system), `lib/` (16 libraries including EPUB engine, HAL, fonts, XML/ZIP/JPEG support), `open-x4-sdk/` (hardware driver submodule), `scripts/` (build helpers), `test/` (hyphenation tests), `docs/` (upstream docs), and `.github/` (CI/templates).
3. **`mod/docs/ci-build-and-code-style.md`** -- CI pipeline documentation covering 4 GitHub Actions workflows (CI with format/cppcheck/build/gate jobs, release, RC, PR title check), PlatformIO build system with 3 environments, clang-format 21 rules (2-space indent, 120-col limit, K&R braces), cppcheck static analysis config, and contribution guidelines (semantic PR titles, AI disclosure, scope alignment).
## Follow-up Items
- None. All three documents are complete and ready for review.

View File

@@ -1,59 +0,0 @@
# Sleep Screen Tweaks Implementation
## Task Description
Implemented the two "Sleep Screen tweaks" from the plan:
1. **Gradient fill for letterboxed areas** - When a sleep screen image doesn't match the display's aspect ratio, the void (letterbox) areas are now filled with a dithered gradient sampled from the nearest ~20 pixels of the image's edge, fading toward white.
2. **Fix "Fit" mode for small images** - Images smaller than the 480x800 display are now scaled up (nearest-neighbor) to fit while preserving aspect ratio, instead of being displayed at native size with wasted screen space.
## Changes Made
### `lib/GfxRenderer/GfxRenderer.cpp`
- Modified `drawBitmap()` scaling logic: when both `maxWidth` and `maxHeight` are provided, always computes an aspect-ratio-preserving scale factor (supports both upscaling and downscaling)
- Modified `drawBitmap()` rendering loop: uses block-fill approach where each source pixel maps to a screen rectangle (handles both upscaling blocks and 1:1/downscaling single pixels via a unified loop)
- Applied same changes to `drawBitmap1Bit()` for 1-bit bitmap consistency
- Added `drawPixelGray()` method: draws a pixel using its 2-bit grayscale value, dispatching correctly based on the current render mode (BW, GRAYSCALE_LSB, GRAYSCALE_MSB)
### `lib/GfxRenderer/GfxRenderer.h`
- Added `drawPixelGray(int x, int y, uint8_t val2bit)` declaration
### `lib/GfxRenderer/BitmapHelpers.cpp`
- Added `quantizeNoiseDither()`: hash-based noise dithering that always uses noise (unlike `quantize()` which is controlled by a compile-time flag), used for smooth gradient rendering on the 4-level display
### `lib/GfxRenderer/BitmapHelpers.h`
- Added `quantizeNoiseDither()` declaration
### `src/activities/boot_sleep/SleepActivity.cpp`
- Removed the `if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight)` gate in `renderBitmapSleepScreen()` — position/scale is now always computed regardless of whether the image is larger or smaller than the screen
- Added anonymous namespace with:
- `LetterboxGradientData` struct for edge sample storage
- `sampleBitmapEdges()`: reads bitmap row-by-row to sample per-column (or per-row) average gray values from the first/last 20 pixels of the image edges
- `drawLetterboxGradients()`: draws dithered gradients in letterbox areas using sampled edge colors, interpolating toward white
- Integrated gradient rendering into the sleep screen flow: edge sampling (first pass), then gradient + bitmap rendering in BW pass, and again in each grayscale pass (LSB, MSB)
## Follow-up: Letterbox Fill Settings
Added three letterbox fill options and two new persisted settings:
### `src/CrossPointSettings.h`
- Added `SLEEP_SCREEN_LETTERBOX_FILL` enum: `LETTERBOX_NONE` (plain white), `LETTERBOX_GRADIENT` (default), `LETTERBOX_SOLID`
- Added `SLEEP_SCREEN_GRADIENT_DIR` enum: `GRADIENT_TO_WHITE` (default), `GRADIENT_TO_BLACK`
- Added `sleepScreenLetterboxFill` and `sleepScreenGradientDir` member fields
### `src/CrossPointSettings.cpp`
- Incremented `SETTINGS_COUNT` to 32
- Added serialization (read/write) for the two new fields at the end for backward compatibility
### `src/SettingsList.h`
- Added "Letterbox Fill" menu entry (None / Gradient / Solid) in Display category
- Added "Gradient Direction" menu entry (To White / To Black) in Display category
### `src/activities/boot_sleep/SleepActivity.cpp`
- Renamed `drawLetterboxGradients``drawLetterboxFill` with added `solidFill` and `targetColor` parameters
- Solid mode: uses edge color directly (no distance-based interpolation), quantized with noise dithering
- Gradient direction: interpolates from edge color toward `targetColor` (255 for white, 0 for black)
- `renderBitmapSleepScreen` reads the settings and skips edge sampling entirely when fill mode is "None"
## Follow-up Items
- Test with various cover image sizes and aspect ratios on actual hardware
- Test custom images from `/sleep/` directory (1-bit and multi-bit)
- Monitor RAM usage via serial during gradient rendering

View File

@@ -1,25 +0,0 @@
# Edge Data Caching for Sleep Screen Letterbox Fill
## Task
Cache the letterbox edge-sampling calculations so they are only computed once per cover image (on first sleep) and reused from a binary cache file on subsequent sleeps.
## Changes Made
### `src/activities/boot_sleep/SleepActivity.h`
- Added `#include <string>` and updated `renderBitmapSleepScreen` signature to accept an optional `edgeCachePath` parameter (defaults to empty string for no caching).
### `src/activities/boot_sleep/SleepActivity.cpp`
- Added `#include <Serialization.h>` for binary read/write helpers.
- Added `loadEdgeCache()` function in the anonymous namespace: loads edge data from a binary cache file, validates the cache version and screen dimensions against current values to detect stale data.
- Added `saveEdgeCache()` function: writes edge data (edgeA, edgeB arrays + metadata) to a compact binary file (~10 bytes header + 2 * edgeCount bytes data, typically under 1KB).
- Updated `renderBitmapSleepScreen()`: tries loading from cache before sampling. On cache miss, samples edges from bitmap and saves to cache. On cache hit, skips the bitmap sampling pass entirely.
- Updated `renderCoverSleepScreen()`: derives the edge cache path from the cover BMP path (e.g. `cover.bmp` -> `cover_edges.bin`) and passes it to `renderBitmapSleepScreen`. The cache is stored alongside the cover BMP in the book's `.crosspoint` directory.
- Custom sleep images (`renderCustomSleepScreen`) do not use caching since the image may change randomly each sleep.
### Cache Design
- **Format**: Binary file with version byte, screen dimensions, horizontal flag, edge count, letterbox sizes, and two edge color arrays.
- **Invalidation**: Validated by cache version and screen dimensions. Naturally scoped per-book (lives in `/.crosspoint/epub_<hash>/`). Different cover modes (FIT vs CROP) produce different BMP files and thus different cache paths.
- **Size**: ~970 bytes for a 480-wide image.
## Follow-up Items
- None

View File

@@ -1,26 +0,0 @@
# Letterbox Fill: 4-Mode Restructure (Solid, Blended, Gradient, None)
## Task
Restructure the letterbox fill modes from 3 (None, Gradient, Solid) to 4 distinct modes with clearer semantics.
## New Modes
1. **Solid** (new) - Picks the dominant (average) shade from the edge and fills the entire letterbox area with that single dithered color.
2. **Blended** (renamed from old "Solid") - Uses per-pixel sampled edge colors with noise dithering, no distance-based interpolation.
3. **Gradient** - Existing gradient behavior (interpolates per-pixel edge color toward a target color).
4. **None** - No fill.
## Changes Made
### `src/CrossPointSettings.h`
- Updated `SLEEP_SCREEN_LETTERBOX_FILL` enum: `LETTERBOX_NONE=0`, `LETTERBOX_SOLID=1`, `LETTERBOX_BLENDED=2`, `LETTERBOX_GRADIENT=3`.
- Note: enum values changed from the old 3-value layout. Existing saved settings may need reconfiguring.
### `src/SettingsList.h`
- Updated "Letterbox Fill" option labels to: "None", "Solid", "Blended", "Gradient".
### `src/activities/boot_sleep/SleepActivity.cpp`
- `drawLetterboxFill()`: Changed signature from `bool solidFill` to `uint8_t fillMode`. Added dominant shade computation for SOLID mode (averages all edge samples into one value per side). BLENDED and SOLID both skip gradient interpolation; SOLID additionally skips per-pixel edge lookups.
- `renderBitmapSleepScreen()`: Removed `solidFill` bool, passes `fillMode` directly to `drawLetterboxFill`. Updated log message to show the mode name.
## Follow-up Items
- None

View File

@@ -1,46 +0,0 @@
# Dictionary Feature Bug Fixes
**Date:** 2026-02-12
**Branch:** mod/add-dictionary
## Task
Fix three bugs reported after initial implementation of PR #857 dictionary word lookup feature.
## Changes Made
### 1. Fix: Lookup fails after first successful lookup (Dictionary.cpp)
**Root cause:** `cleanWord()` lowercases the search term, but the dictionary index is sorted case-sensitively (uppercase entries sort before lowercase). The binary search lands in the wrong segment on the second pass because the index is already loaded and the sparse offset table was built for a case-sensitive sort order.
**Fix:** Extracted the binary-search + linear-scan logic into a new `searchIndex()` private helper. The `lookup()` method now performs a **two-pass search**: first with the lowercased word, then with the first-letter-capitalized variant. This handles dictionaries that store headwords as "Hello" instead of "hello". Also removed `stripHtml` from the header since HTML is now rendered, not stripped.
**Files:** `src/util/Dictionary.h`, `src/util/Dictionary.cpp`
### 2. Fix: Raw HTML displayed in definitions (DictionaryDefinitionActivity)
**Root cause:** Dictionary uses `sametypesequence=h` (HTML format). The original activity rendered definitions as plain text.
**Fix:** Complete rewrite of the definition activity to **render HTML with styled text**:
- New `parseHtml()` method tokenizes HTML into `TextAtom` structs (word + style + newline directives)
- Supports bold (`<b>`, `<strong>`, headings), italic (`<i>`, `<em>`), and mixed bold-italic via `EpdFontFamily::Style`
- Handles `<ol>` (numbered with digit/alpha support), `<ul>` (bullet points), nested lists with indentation
- Decodes HTML entities (named + numeric/hex → UTF-8)
- Skips `<svg>` content, treats `</html>` as section separators
- `wrapText()` flows styled atoms into positioned line segments (`Segment` struct with x-offset and style)
- `renderScreen()` draws each segment with correct position and style via `renderer.drawText(fontId, x, y, text, true, style)`
**Files:** `src/activities/reader/DictionaryDefinitionActivity.h`, `src/activities/reader/DictionaryDefinitionActivity.cpp`
### 3. Fix: No button hints on word selection screen (DictionaryWordSelectActivity)
**Fix:** Added `GUI.drawButtonHints()` call at the end of `renderScreen()` with labels: "« Back", "✓ Lookup", "↕ Row", "↔ Word".
**Files:** `src/activities/reader/DictionaryWordSelectActivity.cpp`
## Follow-up Items
- If the reader font doesn't include bold/italic variants, styled text gracefully falls back to regular style
- Nested list indentation uses 15px per level after the first
- Alpha list numbering (`list-style-type: lower-alpha`) is supported; other custom list styles fall back to numeric
- Button hint labels may need tuning once tested on device (especially in landscape orientation)

View File

@@ -1,53 +0,0 @@
# Dictionary Feature Bug Fixes (Round 2)
**Date:** 2026-02-12
**Branch:** mod/add-dictionary
## Task
Fix three remaining bugs with the dictionary word lookup feature after initial implementation and first round of fixes.
## Changes Made
### 1. Fix: Lookup only works for first word searched (Dictionary.cpp)
**Root cause:** The binary search used C++ `operator<=` (case-sensitive, pure `strcmp` order) for comparing index entries, but StarDict `.idx` files are sorted using `stardict_strcmp` — a two-level comparison that sorts case-*insensitively* first, then uses case-sensitive `strcmp` as a tiebreaker. This means entries like "Simple" and "simple" are adjacent, and uppercase/lowercase entries for the same letter are interleaved (e.g., "Silver", "silver", "Simple", "simple", "Simpson", "simpson").
With the wrong sort order, the binary search overshoots for many words: uppercase entries like "Simpson" are case-sensitively < "simple" (because 'S' < 's'), so `lo` moves past the segment actually containing "simple". The linear scan then starts too late and doesn't find the word. By coincidence, some words (like "professor") happen to land in correct segments while others (like "simple") don't.
**Fix:**
- Added `stardictCmp()` (case-insensitive first, then case-sensitive tiebreaker) and `asciiCaseCmp()` helper functions
- Binary search now uses `stardictCmp(key, word) <= 0` instead of `key <= word`
- Linear scan early termination now uses `stardictCmp(key, word) > 0` instead of `key > word`
- Exact match now uses `asciiCaseCmp(key, word) == 0` (case-insensitive) since `cleanWord` lowercases the search term but the dictionary may store entries in any case
- Removed the two-pass search approach (no longer needed — single pass handles all casing)
**Files:** `src/util/Dictionary.cpp`
### 2. Fix: Unrendered glyphs in pronunciation guide (DictionaryDefinitionActivity.cpp)
**Root cause:** The dictionary stores IPA pronunciation as raw text between entries, e.g., `/əmˈsɛlvz/` appearing between `</html>` and the next `<p>` tag. These IPA Extension Unicode characters (U+0250U+02FF) are not in the e-ink display's bitmap font, rendering as empty boxes.
**Fix:** Added IPA detection in `parseHtml()`: when encountering `/` or `[` delimiters, the parser looks ahead for a matching close delimiter within 80 characters. If the enclosed content contains any non-ASCII byte (> 0x7F), the entire section (including delimiters) is skipped. This removes IPA transcriptions like `/ˈsɪmpəl/` and `[hɜː(ɹ)b]` while preserving legitimate ASCII bracket content like "[citation needed]" or "and/or".
**Files:** `src/activities/reader/DictionaryDefinitionActivity.cpp`
### 3. Fix: Thinner button hints with overlap detection (DictionaryWordSelectActivity)
**Root cause:** Button hints used `GUI.drawButtonHints()` which draws 40px-tall buttons, taking excessive space over the book page content. No overlap detection meant hints could obscure the selected word at the bottom of the screen.
**Fix:**
- Replaced `GUI.drawButtonHints()` with a custom `drawHints()` method
- Draws thinner hints (22px instead of 40px) using `drawRect` + small text
- Converts the selected word's bounding box from the current orientation to portrait coordinates (handles portrait, inverted, landscape CW/CCW)
- Checks vertical and horizontal overlap between each of the 4 button hint areas and the selected word (including hyphenation continuations)
- Individual hints that overlap the cursor are hidden (white area cleared, no button drawn)
- Uses the theme's button positions `[58, 146, 254, 342]` to match the physical button layout
**Files:** `src/activities/reader/DictionaryWordSelectActivity.h`, `src/activities/reader/DictionaryWordSelectActivity.cpp`
## Follow-up Items
- The landscape coordinate conversions for overlap detection are best-guess transforms; if they're wrong for the specific device rotation mapping, they may need tuning after testing
- The IPA skip heuristic is conservative (only skips content with non-ASCII in `/`/`[` delimiters); some edge-case IPA content outside these delimiters would still show
- `SMALL_FONT_ID` is now included via `fontIds.h` in the word select activity

View File

@@ -1,47 +0,0 @@
# Dictionary Feature Bug Fixes (Round 3)
**Date:** 2026-02-12
**Branch:** mod/add-dictionary
## Task
Fix three issues reported after round 2 of dictionary fixes.
## Changes Made
### 1. Fix: Definitions truncated for some words (Dictionary.cpp)
**Root cause:** The `asciiCaseCmp` case-insensitive match introduced in round 2 returns the *first* case variant found in the index. In StarDict order, "Professor" (capitalized) sorts before "professor" (lowercase). If the dictionary has separate entries for each — e.g., "Professor" as a title (short definition) and "professor" as the common noun (full multi-page definition) — the shorter entry is returned.
**Fix:** The linear scan in `searchIndex` now remembers the first case-insensitive match as a fallback, but continues scanning adjacent entries (case variants are always adjacent in StarDict order). If an exact case-sensitive match is found, it's used immediately. Otherwise, the first case-insensitive match is used. This ensures `cleanWord("professor")``"professor"` finds the full lowercase entry, not the shorter capitalized one.
**Files:** `src/util/Dictionary.cpp`
### 2. Fix: Non-renderable foreign script characters in definitions (DictionaryDefinitionActivity)
**Root cause:** Dictionary definitions include text from other languages (Chinese, Greek, Arabic, Cyrillic, etc.) as etymological references or examples. These characters aren't in the e-ink bitmap font and render as empty boxes. This is the same class of issue as the IPA pronunciation fix from round 2, but affecting inline content within definitions.
**Fix:**
- Added `isRenderableCodepoint(uint32_t cp)` static helper that whitelists character ranges the e-ink font supports:
- U+0000U+024F: Basic Latin through Latin Extended-B (ASCII + accented chars)
- U+0300U+036F: Combining Diacritical Marks
- U+2000U+206F: General Punctuation (dashes, quotes, bullets, ellipsis)
- U+20A0U+20CF: Currency Symbols
- U+2100U+214F: Letterlike Symbols
- U+2190U+21FF: Arrows
- Replaced the byte-by-byte character append in `parseHtml()` with a UTF-8-aware decoder that reads multi-byte sequences, decodes the codepoint, and only appends renderable characters. Invalid or non-renderable characters are silently skipped.
**Files:** `src/activities/reader/DictionaryDefinitionActivity.h`, `src/activities/reader/DictionaryDefinitionActivity.cpp`
### 3. Fix: Revert to standard-height hints, keep overlap hiding (DictionaryWordSelectActivity)
**What changed:** Reverted from 22px thin custom hints back to the standard 40px theme-style buttons (rounded corners with `cornerRadius=6`, `SMALL_FONT_ID` text, matching `LyraTheme::drawButtonHints` exactly). The overlap detection is preserved.
**Key design choice:** Instead of calling `GUI.drawButtonHints()` (which always clears all 4 button areas, erasing page content even for hidden buttons), the method draws each button individually in portrait mode. Hidden buttons are skipped entirely (`continue`), so the page content and word highlight underneath remain visible. Non-hidden buttons get the full theme treatment: white fill + rounded rect border + centered text.
**Files:** `src/activities/reader/DictionaryWordSelectActivity.cpp`
## Follow-up Items
- The `isRenderableCodepoint` whitelist is conservative — if the font gains additional glyph coverage (e.g., Greek letters for math), the whitelist can be extended
- Entity-decoded characters bypass the codepoint filter since they're appended as raw bytes; this is fine for the current entity set (all produce ASCII or General Punctuation characters)

View File

@@ -1,40 +0,0 @@
# Bookmark Feature Implementation
## Task
Implement bookmark functionality for the e-reader, replacing existing "Coming soon" stubs with full add/remove bookmark, visual page indicator, and bookmark navigation features.
## Changes Made
### New Files Created
- **`src/util/BookmarkStore.h`** / **`src/util/BookmarkStore.cpp`** - Bookmark persistence utility. Stores bookmarks as binary data (`bookmarks.bin`) per-book in the epub cache directory on SD card. Each bookmark is a (spineIndex, pageNumber) pair (4 bytes). Provides static methods: `load`, `save`, `addBookmark`, `removeBookmark`, `hasBookmark`.
- **`src/activities/reader/EpubReaderBookmarkSelectionActivity.h`** / **`.cpp`** - New activity for the "Go to Bookmark" list UI, modeled on the existing chapter selection activity. Shows bookmark entries as "Chapter Title - Page N" with ButtonNavigator for scrolling. Selecting a bookmark navigates to that spine/page.
### Edited Files
- **`src/activities/reader/EpubReaderMenuActivity.h`** - Added `REMOVE_BOOKMARK` to `MenuAction` enum. Changed `buildMenuItems()` to accept `isBookmarked` parameter; dynamically shows "Remove Bookmark" or "Add Bookmark" as the first menu item.
- **`src/activities/reader/EpubReaderActivity.cpp`** - Main integration point:
- Added includes for `BookmarkStore.h` and `EpubReaderBookmarkSelectionActivity.h`
- Menu creation now computes `isBookmarked` state and passes it through
- `ADD_BOOKMARK` handler: calls `BookmarkStore::addBookmark()`, shows "Bookmark added" popup
- `REMOVE_BOOKMARK` handler (new): calls `BookmarkStore::removeBookmark()`, shows "Bookmark removed" popup
- `GO_TO_BOOKMARK` handler: loads bookmarks, opens `EpubReaderBookmarkSelectionActivity` if any exist, falls back to Table of Contents if no bookmarks but TOC exists, otherwise returns to reader
- `renderContents()`: draws a small bookmark ribbon (fillPolygon, 5-point shape) in the top-right corner when the current page is bookmarked
## Follow-up Changes (same session)
### Force half refresh on menu exit
- `onReaderMenuBack()`: sets `pagesUntilFullRefresh = 1` so the next render uses `HALF_REFRESH` to clear menu/popup ghosting artifacts from the e-ink display.
- `ADD_BOOKMARK` / `REMOVE_BOOKMARK` handlers: also set `pagesUntilFullRefresh = 1` after their popups.
### Bookmark snippet (first sentence)
- `Bookmark` struct now includes a `snippet` string field storing the first sentence from the bookmarked page.
- `BookmarkStore` binary format upgraded to v2: version marker byte (0xFF) + count + entries with variable-length snippet. Backward-compatible: reads v1 files (no snippets) gracefully.
- `addBookmark()` now accepts an optional `snippet` parameter (max 120 chars).
- `EpubReaderActivity::onReaderMenuConfirm(ADD_BOOKMARK)`: extracts the first sentence from the page by iterating PageLine elements and their TextBlock words, stopping at sentence-ending punctuation (.!?:).
- `EpubReaderBookmarkSelectionActivity::getBookmarkLabel()`: displays bookmark as "Chapter Title - First sentence here - Page N".
## Follow-up Items
- Test on device to verify bookmark ribbon sizing/positioning looks good across orientations
- Consider caching bookmark state in memory to avoid SD reads on every page render (currently `hasBookmark` reads from SD each time in `renderContents`)
- The bookmark selection list could potentially support deleting bookmarks directly from the list in a future iteration

View File

@@ -1,32 +0,0 @@
# Implement Dictionary Word Lookup Feature (PR #857)
## Task
Ported upstream PR #857 (dictionary word lookup feature) into the local codebase on `mod/add-dictionary` branch. Two adaptations were made:
1. **Vector compatibility**: PR #857 was written against upstream `master` which used `std::list` for word storage. Our codebase already has PR #802 applied (list-to-vector). The `TextBlock.h` getters added by PR #857 were changed to return `const std::vector<...>&` instead of `const std::list<...>&`.
2. **Dictionary path tweak**: Changed dictionary file lookup from SD card root (`/dictionary.idx`, `/dictionary.dict`) to a `/.dictionary/` subfolder (`/.dictionary/dictionary.idx`, `/.dictionary/dictionary.dict`).
## Changes Made
### New files (10)
- `src/util/Dictionary.h` / `.cpp` -- StarDict 3 format dictionary lookup (sparse index + binary search)
- `src/util/LookupHistory.h` / `.cpp` -- Per-book lookup history stored in book cache dir
- `src/activities/reader/DictionaryDefinitionActivity.h` / `.cpp` -- Definition display with pagination
- `src/activities/reader/DictionaryWordSelectActivity.h` / `.cpp` -- Word selection from current page with orientation-aware navigation
- `src/activities/reader/LookedUpWordsActivity.h` / `.cpp` -- Lookup history browser with long-press delete
### Modified files (5)
- `lib/Epub/Epub/blocks/TextBlock.h` -- Added `getWords()`, `getWordXpos()`, `getWordStyles()` accessors (returning `std::vector`)
- `lib/Epub/Epub/Page.h` -- Added `getBlock()` accessor to `PageLine`
- `src/activities/reader/EpubReaderMenuActivity.h` -- Added `LOOKUP`/`LOOKED_UP_WORDS` enum values, `hasDictionary` constructor param, dynamic `buildMenuItems()`
- `src/activities/reader/EpubReaderActivity.h` -- Added includes for new activity headers
- `src/activities/reader/EpubReaderActivity.cpp` -- Added includes, `Dictionary::exists()` check, `LOOKUP` and `LOOKED_UP_WORDS` case handling
## Follow-up Items
- Dictionary files (`dictionary.idx`, `dictionary.dict`, `dictionary.ifo`) must be placed in `/.dictionary/` folder on the SD card root
- Menu items "Lookup" and "Lookup History" only appear when dictionary files are detected

View File

@@ -1,56 +0,0 @@
# Dictionary Feature Polish & Menu Reorganization
**Date:** 2026-02-12
## Task Description
Continued polishing the dictionary word lookup feature across multiple iterations: fixing side button hint placement and orientation, adding CCW text rotation, fixing pronunciation line rendering, adding index caching, and reorganizing the reader menu.
## Changes Made
### Side button hint fixes (`DictionaryWordSelectActivity.cpp`)
- Moved `drawSideButtonHints` call inside `drawHints()` where renderer is in portrait mode (fixes wrong placement)
- Made all button hint labels orientation-aware (portrait, inverted, landscape CW/CCW each get correct labels matching their button-to-action mapping)
- Replaced `GUI.drawSideButtonHints()` with custom drawing: solid background, overlap/cursor hiding, text truncation, and orientation-aware rotation
- Changed word navigation labels from "Prev Word"/"Next Word" to "« Word"/"Word »"
### GfxRenderer CCW text rotation (`lib/GfxRenderer/`)
- Added `drawTextRotated90CCW()` to `GfxRenderer.h` and `GfxRenderer.cpp`
- Mirrors the existing CW rotation: text reads top-to-bottom instead of bottom-to-top
- Used for side button hints in landscape CCW orientation
### Definition screen fixes (`DictionaryDefinitionActivity.cpp/.h`)
- Fixed pronunciation commas: definitions start with `/ˈsɪm.pəl/, /ˈsɪmpəl/` before `<p>` — now skips all content before first `<` tag in `parseHtml()`
- Added side button hints with proper CCW rotation and solid backgrounds
- Updated bottom button labels: "« Back", "" (hidden stub), "« Page", "Page »"
- Added half refresh on initial screen entry (`firstRender` flag)
### Dictionary index caching (`Dictionary.h/.cpp`)
- New `loadCachedIndex()`: reads `/.dictionary/dictionary.cache` — validates magic + idx file size, loads sparse offsets directly (~7KB binary read vs 17MB scan)
- New `saveCachedIndex()`: persists after first full scan
- Cache format: `[magic 4B][idxFileSize 4B][totalWords 4B][count 4B][offsets N×4B]`
- Auto-invalidates when `.idx` file size changes
- New public methods: `cacheExists()`, `deleteCache()`
### Reader menu reorganization (`EpubReaderMenuActivity.h`, `EpubReaderActivity.cpp`)
- New `MenuAction` enum values: `ADD_BOOKMARK`, `GO_TO_BOOKMARK`, `DELETE_DICT_CACHE`
- Reordered menu: Add Bookmark, Lookup Word, Lookup Word History, Reading Orientation, Table of Contents, Go to Bookmark, Go to %, Close Book, Sync Progress, Delete Book Cache, Delete Dictionary Cache
- Renamed: "Go to Chapter" → "Table of Contents", "Go Home" → "Close Book", "Lookup" → "Lookup Word"
- Bookmark stubs show "Coming soon" popup
- Delete Dictionary Cache checks existence and clears in-memory state
## Files Modified
- `lib/GfxRenderer/GfxRenderer.h` — added `drawTextRotated90CCW` declaration
- `lib/GfxRenderer/GfxRenderer.cpp` — added `drawTextRotated90CCW` implementation
- `src/util/Dictionary.h` — added `cacheExists()`, `deleteCache()`, `loadCachedIndex()`, `saveCachedIndex()`
- `src/util/Dictionary.cpp` — cache load/save implementation, delete cache
- `src/activities/reader/DictionaryWordSelectActivity.cpp` — orientation-aware hints, custom side button drawing
- `src/activities/reader/DictionaryDefinitionActivity.h` — added `firstRender` flag
- `src/activities/reader/DictionaryDefinitionActivity.cpp` — pronunciation fix, side hints, half refresh, label changes
- `src/activities/reader/EpubReaderMenuActivity.h` — new enum values, reordered menu
- `src/activities/reader/EpubReaderActivity.cpp` — handlers for new menu actions
## Follow-up Items
- Bookmark feature implementation (stubs are in place)
- Test CCW text rotation rendering on device
- Verify cache invalidation works when dictionary files are replaced

View File

@@ -1,28 +0,0 @@
# Implement Letterbox Edge Row Copy for MATCHED Mode
## Task
Implement the "FrameBuffer Edge Row Copy" plan for the MATCHED letterbox fill mode. Instead of computing letterbox colors from sampled edge data, the new approach copies the cover's rendered edge row directly from the frameBuffer into the letterbox area after each `drawBitmap` call.
## Changes Made
### `src/activities/boot_sleep/SleepActivity.cpp`
- **Added `#include <cstring>`** for `memcpy` usage.
- **Added `copyEdgeRowsToLetterbox()` helper** (anonymous namespace): Copies physical columns (horizontal letterbox) or physical rows (vertical letterbox) in the frameBuffer. For horizontal letterbox, iterates per-bit across 480 physical rows. For vertical letterbox, uses `memcpy` of 100-byte physical rows.
- **Updated `renderBitmapSleepScreen()`**:
- Added `scaledWidth`/`scaledHeight` computation matching `drawBitmap`'s floor logic.
- Added `isMatched` flag.
- MATCHED mode now skips edge sampling entirely (`sampleBitmapEdges` / cache load).
- After each `drawBitmap` call (BW, LSB, MSB passes), calls `copyEdgeRowsToLetterbox` for MATCHED mode.
- **Cleaned up dead code**:
- Removed the entire MATCHED case from `drawLetterboxFill()` (no longer called for MATCHED).
- Removed `grayToVal2bit` helper (was only used by the removed MATCHED case).
- Removed `skipFillInGreyscale` flag (no longer needed — the edge copy participates in all passes naturally).
## Build
Successfully compiled with `pio run` (0 errors, 0 warnings relevant to changes).
## Follow-up
- Needs on-device testing to verify:
1. The letterbox blends seamlessly with the cover edge (pixel-perfect 1:1 match).
2. No scan coupling corruption (the scattered pixel distribution from dithering should cause less coupling than uniform blocks).
3. If corruption is still unacceptable, the fallback is the previous flat-fill + greyscale-skip approach (revert this change).

View File

@@ -1,41 +0,0 @@
# Merge master into mod/master
**Date:** 2026-02-12
## Task
Compare upstream `master` (14 new commits) with `mod/master` (2 mod commits) since their common ancestor (`6202bfd` — release/1.0.0 merge), assess merge risk, and perform the merge.
## Branch Summary
### Upstream (`master`) — 14 commits, 47 files, ~6000 lines
- Unified navigation handling with ButtonNavigator utility
- Italian hyphenation support
- Natural sort in file browser
- Auto WiFi reconnect to last network
- Extended Python debugging monitor
- More power saving on idle
- OPDS fixes (absolute URLs, prevent sleep during download)
- Uniform debug message formatting (millis() timestamps)
- File browser Back/Home label fix, GPIO trigger fix
- USER_GUIDE.md updates
### Mod (`mod/master`) — 2 commits, 10 files, ~588 lines
- `.gitignore` tweaks for mod fork
- Sleep screen letterbox fill and image upscaling feature
## Conflict Resolution
Single conflict in `src/activities/boot_sleep/SleepActivity.cpp`:
- **Upstream** changed `Serial.println``Serial.printf("[%lu] [SLP] ...\n", millis())` for uniform debug logging
- **Mod** had already adopted this format in new code, but the original lines it modified were the old format
- **Resolution:** Kept mod's `renderBitmapSleepScreen(bitmap, edgeCachePath)` call with upstream's `millis()` log format
## Result
Merge commit: `182c236`
## Follow-up
- Test sleep screen behavior end-to-end (letterbox fill + upstream idle power saving changes)
- Verify new upstream features (navigation, WiFi auto-connect) work alongside mod changes

View File

@@ -1,23 +0,0 @@
# Add `env:mod` with version + git hash
## Task
Add a PlatformIO environment that flashes firmware with a `-mod+<git_hash>` version suffix (e.g. `1.0.0-mod+a3f7c21`).
## Changes
### New file: `scripts/inject_mod_version.py`
- PlatformIO pre-build script
- Reads `version` from the `[crosspoint]` section of `platformio.ini`
- Runs `git rev-parse --short HEAD` to get the current commit hash
- Injects `-DCROSSPOINT_VERSION="{version}-mod+{hash}"` into build flags
### Modified: `platformio.ini`
- Added `[env:mod]` section (lines 58-64) that extends `base`, includes the new script via `extra_scripts`, and inherits base build flags
## Usage
```
pio run -e mod -t upload
```
## Follow-up
- None

View File

@@ -1,39 +0,0 @@
# Prerender Book Covers/Thumbnails on First Open
**Date:** 2026-02-12
**Branch:** `mod/prerender-book-covers`
## Task
Implement todo item: "Process/render all covers/thumbs when opening book for first time" with a progress indicator so the reader doesn't appear frozen.
## Changes Made
### `src/components/UITheme.h`
- Added `PRERENDER_THUMB_HEIGHTS[]` and `PRERENDER_THUMB_HEIGHTS_COUNT` constants (226, 400) representing all known theme `homeCoverHeight` values (Lyra and Base). This ensures thumbnails are pre-generated for all themes.
### `src/activities/reader/EpubReaderActivity.cpp`
- Added prerender block in `onEnter()` after `setupCacheDir()` and before `addBook()`.
- Checks whether `cover.bmp`, `cover_crop.bmp`, `thumb_226.bmp`, and `thumb_400.bmp` exist.
- If any are missing, shows "Preparing book..." popup with a progress bar that updates after each generation step.
- On subsequent opens, all files already exist and the popup is skipped entirely.
### `src/activities/reader/XtcReaderActivity.cpp`
- Added same prerender pattern in `onEnter()`.
- Generates `cover.bmp`, `thumb_226.bmp`, and `thumb_400.bmp` (XTC has no cropped cover variant).
### `src/activities/reader/TxtReaderActivity.cpp`
- Added prerender for `cover.bmp` only (TXT has no thumbnail support).
- Shows "Preparing book..." popup if the cover needs generating.
## Design Decisions
- **Letterbox edge data not prerendered:** The sleep screen's letterbox gradient fill (`cover_edges.bin`) depends on runtime settings (crop mode, screen dimensions) and is already efficiently cached after first sleep. The expensive part (JPEG-to-BMP conversion) is what this change addresses.
- **TXT `addBook()` cover path unchanged:** The `coverBmpPath` field in `RecentBook` is used for home screen thumbnails, not sleep covers. Since TXT has no thumbnail support, passing `""` remains correct.
- **HomeActivity::loadRecentCovers() kept as fallback:** Books opened before this change will still have thumbnails generated lazily on the Home screen. No code removal needed.
## Follow-up Items
- Mark todo item as complete in `mod/docs/todo.md`
- Test on device with each book format (EPUB, XTC/XTCH, TXT)
- If a new theme is added with a different `homeCoverHeight`, update `PRERENDER_THUMB_HEIGHTS`

View File

@@ -1,33 +0,0 @@
# Letterbox Fill: Hash-Based Block Dithering Fix & Cleanup
**Date:** 2026-02-13
## Task Description
Resolved an e-ink display crosstalk issue where a specific book cover ("The World in a Grain") became completely washed out and invisible when using "Dithered" letterbox fill mode. The root cause was pixel-level Bayer dithering creating a high-frequency checkerboard pattern in the BW pass for gray values in the 171-254 range (level-2/level-3 boundary), which caused display crosstalk during HALF_REFRESH.
## Solution
Hash-based block dithering with 2x2 pixel blocks for gray values in the problematic BW-boundary range (171-254). Each 2x2 block gets a uniform level (2 or 3) determined by a spatial hash, with the proportion approximating the target gray. Standard Bayer dithering is used for all other gray ranges.
## Changes Made
### Modified Files
- **`src/activities/boot_sleep/SleepActivity.cpp`** — Added `bayerCrossesBwBoundary()` and `hashBlockDither()` functions; `drawLetterboxFill()` uses hash-based block dithering for BW-boundary gray values, standard Bayer for everything else. Removed all debug instrumentation (H1-H20 logs, frame buffer checksums, edge histograms, rewind verification).
- **`src/CrossPointSettings.h`** — Reordered letterbox fill enum to `DITHERED=0, SOLID=1, NONE=2` with `DITHERED` as default.
- **`src/SettingsList.h`** — Updated settings UI labels to match new enum order.
- **`lib/GfxRenderer/GfxRenderer.h`** — Removed `getRenderMode()` getter (was only needed by debug instrumentation).
### Deleted Files
- 16 debug log files from `.cursor/` directory
## Key Findings
- Single-pixel alternation (block=1) causes display crosstalk regardless of pattern regularity (Bayer or hash).
- 2x2 minimum pixel runs are sufficient to avoid crosstalk on this e-ink display.
- The irregular hash pattern is less visually noticeable than a regular Bayer grid at the same block size.
- The issue only affects gray values 171-254 where Bayer produces a level-2/level-3 mix (the BW pass boundary).
## Follow-up Items
- None. Feature is stable and working for all tested book covers.

View File

@@ -1,49 +0,0 @@
# Letterbox Fill: Replace Dithering with Edge Replication ("Extend Edges")
**Date:** 2026-02-13
## Task Description
After implementing Bayer ordered dithering for the "Blended" letterbox fill mode (replacing earlier noise dithering), the user reported that the problematic cover (*The World in a Grain* by Vince Beiser) looked even worse. Investigation revealed that **any dithering technique** is fundamentally incompatible with the e-ink multi-pass rendering pipeline for large uniform fill areas:
- The BW HALF_REFRESH pass creates a visible dark/white pattern in the letterbox
- The subsequent greyscale correction can't cleanly handle mixed-level patterns in large uniform areas
- This causes crosstalk artifacts that can affect adjacent cover rendering
- Uniform fills (all same level, like Solid mode) work fine because all pixels go through identical correction
The user's key insight: the display already renders the cover's grayscale correctly (including Atkinson-dithered mixed levels). Instead of re-dithering an averaged color, replicate the cover's actual boundary pixels into the letterbox -- letting the display render them the same way it renders the cover itself.
## Changes Made
### `src/CrossPointSettings.h`
- Renamed `LETTERBOX_BLENDED` (value 2) → `LETTERBOX_EXTENDED`
- Updated comment to reflect "None / Solid / Extend Edges"
### `src/SettingsList.h`
- Changed UI label from `"Blended"``"Extend Edges"`
### `src/activities/boot_sleep/SleepActivity.cpp`
- **Removed**: `BAYER_4X4` matrix, `quantizeBayerDither()` function (all Bayer dithering code)
- **Added**: `getPackedPixel()`, `setPackedPixel()` helpers for packed 2-bit array access
- **Rewrote** `LetterboxFillData` struct:
- Added `edgeA`/`edgeB` (dynamically allocated packed 2-bit arrays) for per-pixel edge data
- Added `edgePixelCount`, `scale` for coordinate mapping
- Added `freeEdgeData()` cleanup method
- **Renamed** `computeEdgeAverages()``computeEdgeData()`:
- New `captureEdgePixels` parameter controls whether to allocate and fill edge arrays
- For horizontal letterboxing: captures first/last visible BMP rows
- For vertical letterboxing: captures leftmost/rightmost pixel per visible row
- Still computes averages for SOLID mode in the same pass
- **Rewrote** `drawLetterboxFill()`:
- SOLID mode: unchanged (uniform fill with snapped level)
- EXTENDED mode: maps screen coordinates → BMP coordinates via scale factor, looks up stored 2-bit pixel values from the cover's boundary row/column
- **Updated** `renderBitmapSleepScreen()`: new log messages, `freeEdgeData()` call at end
## Backward Compatibility
- Enum value 2 is unchanged (was `LETTERBOX_BLENDED`, now `LETTERBOX_EXTENDED`)
- Serialized settings files continue to work without migration
## Follow-up Items
- Test "Extend Edges" mode with both covers (Power Broker and World in a Grain)
- Test vertical letterboxing (left/right) if applicable covers are available
- Verify SOLID mode still works as expected

View File

@@ -1,42 +0,0 @@
# Letterbox Fill Redesign
**Date:** 2026-02-13
**Task:** Strip out the 5-mode letterbox edge fill system and replace with a simplified 3-mode design
## Changes Made
### Problem
The existing letterbox fill feature had 5 modes (None, Solid, Blended, Gradient, Matched) with ~300 lines of complex code including per-pixel edge arrays, malloc'd buffers, binary edge caching, framebuffer-level column/row copying, and a gradient direction sub-setting. Several modes introduced visual corruption that couldn't be resolved.
### New Design
Simplified to 3 modes:
- **None** -- no fill
- **Solid** -- computes dominant edge color, snaps to nearest of 4 e-ink levels (black/dark gray/light gray/white), fills uniformly
- **Blended** -- computes dominant edge color, fills with exact gray value using noise dithering for smooth approximation
### Files Changed
1. **`src/CrossPointSettings.h`** -- Removed `LETTERBOX_GRADIENT`, `LETTERBOX_MATCHED` enum values; removed `SLEEP_SCREEN_GRADIENT_DIR` enum and `sleepScreenGradientDir` member; changed default to `LETTERBOX_NONE`
2. **`src/SettingsList.h`** -- Trimmed Letterbox Fill options to `{None, Solid, Blended}`; removed Gradient Direction setting entry
3. **`src/CrossPointSettings.cpp`** -- Removed `sleepScreenGradientDir` from write path; added dummy read for backward compatibility with old settings files; decremented `SETTINGS_COUNT` from 32 to 31
4. **`src/activities/boot_sleep/SleepActivity.cpp`** -- Major rewrite:
- Removed: `LetterboxGradientData` struct, `loadEdgeCache()`/`saveEdgeCache()`, `sampleBitmapEdges()`, `copyEdgeRowsToLetterbox()`, old `drawLetterboxFill()`
- Added: `LetterboxFillData` struct (2 bytes vs arrays), `snapToEinkLevel()`, `computeEdgeAverages()` (running sums only, no malloc), simplified `drawLetterboxFill()`
- Cleaned `renderBitmapSleepScreen()`: removed matched/gradient logic, edge cache paths, unused `scaledWidth`/`scaledHeight`
- Cleaned `renderCoverSleepScreen()`: removed edge cache path derivation
- Removed unused includes (`<Serialization.h>`, `<cstring>`), added `<cmath>`
5. **`src/activities/boot_sleep/SleepActivity.h`** -- Removed `edgeCachePath` parameter from `renderBitmapSleepScreen()` signature; removed unused `<string>` include
### Backward Compatibility
- Enum values 0/1/2 (None/Solid/Blended) unchanged -- existing settings preserved
- Old Gradient (3) or Matched (4) values rejected by `readAndValidate`, falling back to default (None)
- Old `sleepScreenGradientDir` byte consumed via dummy read during settings load
- Orphaned `_edges.bin` cache files on SD cards are harmless
## Follow-up Items
- Test all 3 fill modes on device with various cover aspect ratios
- Consider cleaning up orphaned `_edges.bin` files (optional, low priority)

View File

@@ -1,41 +0,0 @@
# Merge upstream/master into mod/master
**Date:** 2026-02-13
**Branch:** `mod/merge-upstream` (from `mod/master`)
**Commit:** `82bfbd8`
## Task
Merged 3 new upstream/master commits into the mod fork:
1. `7a385d7` feat: Allow screenshot retrieval from device (#820)
2. `cb24947` feat: Add central logging pragma (#843) — replaces Serial.printf with LOG_* macros across ~50 files, adds Logging library
3. `6e51afb` fix: Account for nbsp character as non-breaking space (#757)
## Conflicts Resolved
### src/main.cpp (1 conflict region)
- Kept mod's `HalPowerManager` (deep sleep, power saving) + upstream's `Logging.h` and `LOG_*` macros + screenshot serial handler (`logSerial`)
### src/activities/boot_sleep/SleepActivity.cpp (3 conflict regions)
- Kept mod's entire letterbox fill rework (~330 lines of dithering, edge caching, etc.)
- Replaced upstream's reverted positioning logic (size-gated) with mod's always-compute-scale approach
- Applied upstream's `LOG_*` pattern to all mod `Serial.printf` calls
## Additional Changes (beyond conflict resolution)
- **lib/GfxRenderer/GfxRenderer.cpp** — Fixed one `Serial.printf` that auto-merge missed converting to `LOG_ERR` (caused linker error with the new Logging library's `#define Serial` macro)
- **lib/hal/HalPowerManager.cpp** — Converted 4 `Serial.printf` calls to `LOG_DBG`/`LOG_ERR`, added `#include <Logging.h>`
- **src/util/BookSettings.cpp** — Converted 3 `Serial.printf` calls to `LOG_DBG`/`LOG_ERR`, replaced `#include <HardwareSerial.h>` with `#include <Logging.h>`
- **src/util/BookmarkStore.cpp** — Converted 2 `Serial.printf` calls to `LOG_ERR`/`LOG_DBG`, added `#include <Logging.h>`
- **platformio.ini** — Added `-DENABLE_SERIAL_LOG` and `-DLOG_LEVEL=2` to `[env:mod]` build flags (was missing, other envs all have these)
## Build Verification
PlatformIO build (`pio run -e mod`) succeeded:
- RAM: 31.0% (101724/327680 bytes)
- Flash: 96.8% (6342796/6553600 bytes)
## Follow-up
- The merge is on branch `mod/merge-upstream` — fast-forward `mod/master` when ready
- The `TxtReaderActivity.cpp` has a pre-existing `[[nodiscard]]` warning for `generateCoverBmp()` (not introduced by this merge)

View File

@@ -1,34 +0,0 @@
# Per-book Letterbox Fill Override
**Date:** 2026-02-13
**Branch:** mod/fix-edge-fills
## Task
Add the ability to override the sleep cover "letterbox fill mode" on a per-book basis (EPUB only for the menu UI; all book types respected at sleep time).
## Changes Made
### New files
- `src/util/BookSettings.h` / `src/util/BookSettings.cpp` — Lightweight per-book settings utility. Stores a `letterboxFillOverride` field (0xFF = use global default) in `{cachePath}/book_settings.bin`. Versioned binary format with field count for forward compatibility, matching the pattern used by BookmarkStore and CrossPointSettings.
### Modified files
- `src/activities/reader/EpubReaderMenuActivity.h` — Added `LETTERBOX_FILL` to the `MenuAction` enum. Added `bookCachePath`, `pendingLetterboxFill`, letterbox fill labels, and helper methods (`letterboxFillToIndex`, `indexToLetterboxFill`, `saveLetterboxFill`). Constructor now accepts a `bookCachePath` parameter and loads the current per-book settings.
- `src/activities/reader/EpubReaderMenuActivity.cpp` — Handle `LETTERBOX_FILL` action: cycles through Default/Dithered/Solid/None on Confirm (handled locally like `ROTATE_SCREEN`), saves immediately. Renders the current value on the right side of the menu item.
- `src/activities/reader/EpubReaderActivity.cpp` — Passes `epub->getCachePath()` to the menu activity constructor. Added `ROTATE_SCREEN` and `LETTERBOX_FILL` to the `onReaderMenuConfirm` switch as no-ops to prevent compiler warnings.
- `src/activities/boot_sleep/SleepActivity.h` — Added `fillModeOverride` parameter to `renderBitmapSleepScreen()`.
- `src/activities/boot_sleep/SleepActivity.cpp``renderCoverSleepScreen()` now loads `BookSettings` from the book's cache path after determining the book type. Passes the per-book override to `renderBitmapSleepScreen()`. `renderBitmapSleepScreen()` uses the override if valid, otherwise falls back to the global `SETTINGS.sleepScreenLetterboxFill`.
## How It Works
1. User opens the EPUB reader menu (Confirm button while reading).
2. "Letterbox Fill" appears between "Reading Orientation" and "Table of Contents".
3. Pressing Confirm cycles: Default → Dithered → Solid → None → Default...
4. The selection is persisted immediately to `book_settings.bin` in the book's cache directory.
5. When the device enters sleep with the cover screen, the per-book override is loaded and used instead of the global setting (if set).
6. XTC and TXT books also have their per-book override checked at sleep time, but can only be configured for EPUB via the reader menu (XTC/TXT lack general menus).
## Follow-up Items
- Consider adding the letterbox fill override to XTC/TXT reader menus if those get general menus in the future.
- The `BookSettings` struct is extensible — other per-book overrides can be added by appending fields and incrementing `BOOK_SETTINGS_COUNT`.

View File

@@ -1,36 +0,0 @@
# Revert Letterbox Fill to Dithered / Solid / None (with edge cache)
**Date:** 2026-02-13
## Task
Reverted letterbox fill modes from None/Solid/Extend Edges back to Dithered (default)/Solid/None per user request. Restored Bayer ordered dithering for "Dithered" mode and re-introduced edge average caching to avoid recomputing on every sleep.
## Changes Made
### `src/CrossPointSettings.h`
- Reordered enum: `LETTERBOX_DITHERED=0`, `LETTERBOX_SOLID=1`, `LETTERBOX_NONE=2`
- Changed default from `LETTERBOX_NONE` to `LETTERBOX_DITHERED`
### `src/SettingsList.h`
- Updated UI labels from `{"None", "Solid", "Extend Edges"}` to `{"Dithered", "Solid", "None"}`
### `src/activities/boot_sleep/SleepActivity.h`
- Added `#include <string>`
- Restored `edgeCachePath` parameter to `renderBitmapSleepScreen()`
### `src/activities/boot_sleep/SleepActivity.cpp`
- **Removed**: `getPackedPixel()`, `setPackedPixel()`, all EXTENDED-mode logic, `freeEdgeData()`, per-pixel edge arrays
- **Simplified** `LetterboxFillData` to just `avgA`, `avgB`, `letterboxA`, `letterboxB`, `horizontal`, `valid`
- **Restored** `BAYER_4X4[4][4]` matrix and `quantizeBayerDither()` function
- **Renamed** `computeEdgeData()``computeEdgeAverages()` (averages-only, no edge pixel capture)
- **Added** edge average cache: `loadEdgeCache()` / `saveEdgeCache()` (~12 byte binary file per cover)
- **Updated** `drawLetterboxFill()`: DITHERED uses Bayer dithering, SOLID uses snap-to-level
- **Updated** `renderBitmapSleepScreen()`: accepts `edgeCachePath`, tries cache before computing
- **Updated** `renderCoverSleepScreen()`: derives `edgeCachePath` from cover BMP path (`_edges.bin`)
## Build
Compilation succeeds (ESP32-C3 target, PlatformIO).
## Follow-up
- The specific "The World in a Grain" cover still has rendering issues with dithered mode — to be investigated separately
- Custom sleep BMPs (`/sleep/` directory, `/sleep.bmp`) intentionally skip caching since the selected BMP can change each sleep

View File

@@ -1,50 +0,0 @@
# Placeholder Cover Generation for Books Without Covers
## Task
Implement placeholder cover BMP generation for books that have no embedded cover image (or have covers in unsupported formats). Previously, these books showed empty rectangles on the home screen and fell back to the default sleep screen.
## Root Cause
Cover generation failed silently in three cases:
- **EPUB**: No `coverItemHref` in metadata, or cover is non-JPG format (PNG/SVG/GIF)
- **TXT**: No matching image file on the SD card
- **XTC**: First-page render failure (rare)
When `generateCoverBmp()` returned `false`, no BMP was created, and no fallback was attempted.
## Changes Made
### New Files
- `lib/PlaceholderCover/PlaceholderCoverGenerator.h` - Header for the placeholder generator
- `lib/PlaceholderCover/PlaceholderCoverGenerator.cpp` - Implementation: allocates a 1-bit pixel buffer, renders title/author text using EpdFont glyph data (ported from GfxRenderer::renderChar), writes a 1-bit BMP file with word-wrapped centered text, border, and separator line
### Modified Files
- `src/activities/reader/EpubReaderActivity.cpp` - Added placeholder fallback after `generateCoverBmp()` and `generateThumbBmp()` fail during first-open prerender
- `src/activities/reader/TxtReaderActivity.cpp` - Added placeholder fallback, added thumbnail generation (previously TXT had none), now passes thumb path to RECENT_BOOKS.addBook() instead of empty string
- `src/activities/reader/XtcReaderActivity.cpp` - Added placeholder fallback after cover/thumb generation fail (rare case)
- `src/activities/boot_sleep/SleepActivity.cpp` - Added placeholder generation before falling back to default sleep screen (handles books opened before this feature was added)
- `lib/Txt/Txt.h` - Added `getThumbBmpPath()` and `getThumbBmpPath(int height)` methods
- `lib/Txt/Txt.cpp` - Implemented the new thumb path methods
### Architecture
- Option B approach: shared `PlaceholderCoverGenerator` utility called from reader activities
- Generator is independent of `GfxRenderer` (no display framebuffer dependency)
- Includes Ubuntu 10 Regular and Ubuntu 12 Bold font data directly for self-contained rendering
- Memory: 48KB for full cover (480x800), 4-12KB for thumbnails; allocated and freed per call
## Flash Impact
- Before: 96.4% flash usage (6,317,250 bytes)
- After: 97.3% flash usage (6,374,546 bytes)
- Delta: ~57KB (mostly from duplicate font bitmap data included in the generator)
## Layout Revision (traditional book cover style)
- Border: moved inward with proportional edge padding (~10px at full size) and thickened to ~5px, keeping it visible within the device bezel
- Title: 2x integer-scaled Ubuntu 12 Bold (effectively ~24pt) for full-size covers, positioned in the top 2/3 zone, max 5 lines
- Author: positioned in the bottom 1/3 zone, max 3 lines with word wrapping
- Book icon: 48x48 1-bit bitmap (generated by `scripts/generate_book_icon.py`), displayed at 2x for full covers, 1x for medium thumbnails, omitted for small thumbnails
- Separator line between title and author zones
- All dimensions scale proportionally for thumbnails
## Follow-up Items
- Books opened before this feature was added won't get home screen thumbnails until re-opened (SleepActivity handles the sleep cover case by generating on demand)
- Font data duplication adds ~57KB flash; could be reduced by exposing shared font references instead of including headers directly
- Preview scripts in `scripts/` can regenerate the icon and layout previews: `generate_book_icon.py`, `preview_placeholder_cover.py`

View File

@@ -1,59 +0,0 @@
# PR #857 Full Feature Update Integration
**Date:** 2026-02-14
## Task Description
Implemented the full feature update from PR #857 ("feat: Add dictionary word lookup feature") into our fork, following the detailed plan in `pr_857_update_integration_190041ae.plan.md`. This covered dictionary intelligence features (stemming, edit distance, fuzzy matching), the `ActivityWithSubactivity` refactor for inline definition display, en-dash/em-dash splitting, cross-page hyphenation, reverse-chronological lookup history, and a new "Did you mean?" suggestions activity.
## Changes Made
### New Files (2)
- **`src/activities/reader/DictionarySuggestionsActivity.h`** — New "Did you mean?" activity header. Adapted from PR with `orientation` parameter for our `DictionaryDefinitionActivity` constructor.
- **`src/activities/reader/DictionarySuggestionsActivity.cpp`** — Suggestions list UI with `UITheme`-aware layout, sub-activity management for definition display.
### Modified Files (9)
1. **`src/util/Dictionary.h`** — Added `getStemVariants()`, `findSimilar()` (public) and `editDistance()` (private) declarations.
2. **`src/util/Dictionary.cpp`** — Added ~250 lines: morphological stemming (`getStemVariants`), Levenshtein distance (`editDistance`), and fuzzy index scan (`findSimilar`). Preserved fork's `/.dictionary/` paths, `stardictCmp`/`asciiCaseCmp`, `cacheExists()`/`deleteCache()`.
3. **`src/activities/reader/DictionaryDefinitionActivity.h`** — Added optional `onDone` callback parameter and member. Enables "Done" button to exit all the way back to the reader.
4. **`src/activities/reader/DictionaryDefinitionActivity.cpp`** — Split Confirm handler: calls `onDone()` if set, else `onBack()`. Button hint shows "Done" when callback provided. Preserved all HTML parsing, styled rendering, side button hints.
5. **`src/activities/reader/DictionaryWordSelectActivity.h`** — Changed base class to `ActivityWithSubactivity`. Replaced `onLookup` callback with `nextPageFirstWord` string. Added `pendingBackFromDef`/`pendingExitToReader` state.
6. **`src/activities/reader/DictionaryWordSelectActivity.cpp`** — Major update:
- En-dash/em-dash splitting in `extractWords()` (splits on U+2013/U+2014)
- Cross-page hyphenation in `mergeHyphenatedWords()` using `nextPageFirstWord`
- Cascading lookup flow: exact → stem variants → similar suggestions → "Not found"
- Sub-activity delegation in `loop()` for definition/suggestions screens
- Preserved custom `drawHints()` with overlap detection and `PageForward`/`PageBack` support
7. **`src/activities/reader/LookedUpWordsActivity.h`** — Replaced `onSelectWord` with `onDone` callback. Added `readerFontId`, `orientation`, `pendingBackFromDef`/`pendingExitToReader`, `getPageItems()`.
8. **`src/activities/reader/LookedUpWordsActivity.cpp`** — Major rewrite:
- Reverse-chronological word display
- Inline cascading lookup flow (same as word select)
- `UITheme`-aware layout with `GUI.drawHeader()`/`GUI.drawList()`
- `onNextRelease`/`onPreviousRelease`/`onNextContinuous`/`onPreviousContinuous` navigation
- Sub-activity management for definition/suggestions
- Preserved delete confirmation mode
9. **`src/activities/reader/EpubReaderActivity.cpp`** — Simplified LOOKUP handler (removed `onLookup` callback, added `nextPageFirstWord` extraction). Simplified LOOKED_UP_WORDS handler (removed inline lookup, passes `readerFontId` and `orientation`). Removed unused `LookupHistory.h` include.
### Cleanup
- Removed unused `DictionaryDefinitionActivity.h` include from `EpubReaderActivity.h`
- Removed unused `util/LookupHistory.h` include from `EpubReaderActivity.cpp`
## Architectural Summary
**Before:** `EpubReaderActivity` orchestrated definition display via callbacks — word select and history both called back to the reader to create definition activities.
**After:** `DictionaryWordSelectActivity` and `LookedUpWordsActivity` manage their own sub-activity chains (definition, suggestions) using `ActivityWithSubactivity`. This enables the cascading lookup flow: exact match → stem variants → similar suggestions → "Not found".
## What Was Preserved (Fork Advantages)
- Full HTML parsing in `DictionaryDefinitionActivity`
- Custom `drawHints()` with overlap detection in `DictionaryWordSelectActivity`
- `PageForward`/`PageBack` button support in word selection
- `DELETE_DICT_CACHE` menu item and `cacheExists()`/`deleteCache()`
- `stardictCmp`/`asciiCaseCmp` for proper StarDict index comparison
- `/.dictionary/` path prefix
## Follow-up Items
- Test the full lookup flow on device (exact → stems → suggestions → not found)
- Verify cross-page hyphenation with a book that has page-spanning hyphenated words
- Verify en-dash/em-dash splitting with books using those characters
- Confirm reverse-chronological history order is intuitive for users

View File

@@ -1,29 +0,0 @@
# Placeholder Cover Visual Refinements & Home Screen Integration
## Task
Refined the placeholder cover layout to match a mockup, and integrated placeholder generation into the home screen's thumbnail loading.
## Changes Made
### Layout Refinements (`PlaceholderCoverGenerator.cpp`, `preview_placeholder_cover.py`)
- **Icon position**: Moved from above-title to side-by-side (icon left, title right)
- **Author scale**: Increased from 1x to 2x on full-size covers for larger author text
- **Line spacing**: Reduced to 75% of advanceY so 2-3 title lines fit within icon height
- **Vertical centering**: Title text centers against icon when 1-2 lines; top-aligns with overflow for 3+ lines. Uses `ascender`-based visual height instead of `advanceY`-based for accurate centering
- **Horizontal centering**: The icon+gap+text block is now centered as a unit based on actual rendered text width, not the full available text area
### Home Screen Integration (`HomeActivity.cpp`)
- Added `PlaceholderCoverGenerator` fallback in `loadRecentCovers()` — when format-specific `generateThumbBmp()` fails (or for TXT which had no handler), a placeholder thumbnail is generated instead of clearing `coverBmpPath` and showing a blank rectangle
- This covers the case where a book was previously opened, cache was cleared, and the home screen needs to regenerate thumbnails
## Files Changed
- `lib/PlaceholderCover/PlaceholderCoverGenerator.cpp` — layout logic updates
- `scripts/preview_placeholder_cover.py` — matching preview updates
- `src/activities/home/HomeActivity.cpp` — placeholder fallback in loadRecentCovers
## Commit
`632b76c` on `mod/generate-placeholder-covers`
## Follow-up Items
- Test on actual device to verify C++ bitmap font rendering matches preview expectations
- The preview script uses Helvetica (different metrics than ubuntu_12_bold), so on-device appearance will differ slightly from previews

View File

@@ -1,55 +0,0 @@
# Flash Size Optimization: Per-Family Font and Per-Language Hyphenation Flags
**Date**: 2026-02-15
## Task Description
Investigated ESP32-C3 flash usage (97.3% full at 6.375 MB / 6.5 MB app partition) and implemented build flags to selectively exclude font families and hyphenation language tries. Fonts alone consumed 65.6% of flash (3.99 MB).
## Changes Made
### 1. `lib/EpdFont/builtinFonts/all.h`
- Wrapped Bookerly includes (16 files) in `#ifndef OMIT_BOOKERLY`
- Wrapped Noto Sans 12-18pt includes (16 files) in `#ifndef OMIT_NOTOSANS` (kept `notosans_8_regular.h` outside guard for UI small font)
- Wrapped OpenDyslexic includes (16 files) in `#ifndef OMIT_OPENDYSLEXIC`
### 2. `src/main.cpp`
- Wrapped Bookerly 14pt font objects (previously always included) in `#ifndef OMIT_BOOKERLY`
- Added per-family `#ifndef OMIT_*` guards inside the existing `#ifndef OMIT_FONTS` block for other sizes
- Added matching guards to font registration in `setupDisplayAndFonts()`
### 3. `src/SettingsList.h`
- Added `FontFamilyMapping` struct and `kFontFamilyMappings[]` compile-time table with per-family `#ifndef` guards
- Switched Font Family setting from `SettingInfo::Enum` to `SettingInfo::DynamicEnum` with getter/setter that maps between list indices and fixed enum values (BOOKERLY=0, NOTOSANS=1, OPENDYSLEXIC=2)
- Added `static_assert` ensuring at least one font family is available
### 4. `src/activities/settings/SettingsActivity.cpp`
- Added DynamicEnum toggle support in `toggleCurrentSetting()` (cycles through options via `valueGetter`/`valueSetter`)
- Added DynamicEnum display support in `render()` display lambda
### 5. `src/CrossPointSettings.cpp`
- Guarded `getReaderFontId()` switch cases with `#ifndef OMIT_*`, added `default:` fallback to first available font
- Guarded `getReaderLineCompression()` switch cases with `#ifndef OMIT_*`, added `default:` fallback
- Added `#error` directive if all font families are omitted
### 6. `lib/Epub/Epub/hyphenation/LanguageRegistry.cpp`
- Added per-language `#ifndef OMIT_HYPH_xx` guards around includes, `LanguageHyphenator` objects, and entries
- Switched from `std::array<LanguageEntry, 6>` to `std::vector<LanguageEntry>` for variable entry count
- Languages: DE (201 KB), EN (27 KB), ES (13 KB), FR (7 KB), IT (2 KB), RU (33 KB)
### 7. `platformio.ini`
- Added to `[env:mod]`: `-DOMIT_OPENDYSLEXIC`, `-DOMIT_HYPH_DE`, `-DOMIT_HYPH_EN`, `-DOMIT_HYPH_ES`, `-DOMIT_HYPH_FR`, `-DOMIT_HYPH_IT`, `-DOMIT_HYPH_RU`
## Design Decisions
- Enum values stay fixed (BOOKERLY=0, NOTOSANS=1, OPENDYSLEXIC=2) for settings file compatibility
- Existing `OMIT_FONTS` flag left untouched; per-family flags nest inside it
- DynamicEnum used for Font Family to handle index-to-value mapping when fonts are removed from the middle of the options list
## Estimated Savings (mod build)
- OpenDyslexic fonts: ~1,052 KB
- All hyphenation tries: ~282 KB
- **Total: ~1,334 KB (~1.30 MB)** -- from 97.3% down to ~76.9%
## Follow-up Items
- Build and verify the `mod` environment compiles cleanly and flash size reduction matches estimates
- Other available flags for future use: `OMIT_BOOKERLY`, `OMIT_NOTOSANS` (individual language OMIT_HYPH_xx flags can also be used selectively)

View File

@@ -1,30 +0,0 @@
# Table Rendering Fixes: &nbsp; Entities and Colspan Support
## Task Description
Fix two issues with the newly implemented EPUB table rendering:
1. Stray `&nbsp;` entities appearing as literal text in table cells instead of whitespace
2. Cells with `colspan` attributes (e.g., section headers like "Anders Celsius", "Scientific career") rendering as narrow single-column cells instead of spanning the full table width
## Changes Made
### 1. `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` — `flushPartWordBuffer()`
- Added detection and replacement of literal `&nbsp;` strings in the word buffer before flushing to `ParsedText`
- This handles double-encoded `&amp;nbsp;` entities common in Wikipedia and other generated EPUBs, where XML parsing converts `&amp;` to `&` leaving literal `&nbsp;` in the character data
### 2. `lib/Epub/Epub/TableData.h` — `TableCell` struct
- Added `int colspan = 1` field to store the HTML `colspan` attribute value
### 3. `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` — `startElement()`
- Added parsing of the `colspan` attribute from `<td>` and `<th>` tags
- Stores the parsed value (minimum 1) in the `TableCell::colspan` field
### 4. `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` — `processTable()`
- **Column count**: Changed from `max(row.cells.size())` to sum of `cell.colspan` per row, correctly determining logical column count
- **Natural width measurement**: Only non-spanning cells (colspan=1) contribute to per-column width calculations; spanning cells use combined width
- **Layout**: Added `spanContentWidth()` and `spanFullCellWidth()` lambdas to compute the combined content width and full cell width for cells spanning multiple columns
- **Cell mapping**: Each `PageTableCellData` now maps to an actual cell (not a logical column), with correct x-offset and combined column width for spanning cells
- **Fill logic**: Empty cells are appended only for unused logical columns after all actual cells are placed
## Follow-up Items
- Rowspan support is not yet implemented (uncommon in typical EPUB content)
- The `&nbsp;` fix only handles the most common double-encoded entity; other double-encoded entities (e.g., `&amp;mdash;`) could be handled similarly if needed

View File

@@ -1,49 +0,0 @@
# Table Rendering Tweaks: Centering, Line Breaks, Padding
## Task Description
Three visual tweaks to the EPUB table rendering based on comparison with a Wikipedia article (Anders Celsius):
1. Full-width spanning rows (like "Anders Celsius", "Scientific career", "Signature") should be center-aligned
2. `<br>` tags and block elements within table cells should create actual line breaks (e.g., "(aged 42)" then "Uppsala, Sweden" on the next line)
3. Cell padding was too tight — text too close to grid borders
## Changes Made
### 1. Forced Line Breaks in Table Cells
**`lib/Epub/Epub/ParsedText.h`**:
- Added `std::vector<bool> forceBreakAfter` member to track mandatory line break positions
- Added `void addLineBreak()` public method
**`lib/Epub/Epub/ParsedText.cpp`**:
- `addWord()`: grows `forceBreakAfter` vector alongside other word vectors
- `addLineBreak()`: sets `forceBreakAfter.back() = true` on the last word
- `computeLineBreaks()` (DP algorithm): respects forced breaks — cannot extend a line past a forced break point, and forced breaks override continuation groups
- `computeHyphenatedLineBreaks()` (greedy): same — stops line at forced break, won't backtrack past one
- `hyphenateWordAtIndex()`: when splitting a word, transfers the forced break flag to the remainder (last part)
**`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`** — `startNewTextBlock()`:
- When `inTable`, instead of being a no-op, now: flushes the word buffer, calls `addLineBreak()` on the current ParsedText, and resets `nextWordContinues`
- This means `<br>`, `<p>`, `<div>` etc. within table cells now produce real visual line breaks
### 2. Center-Aligned Full-Width Spanning Cells
**`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`** — `processTable()`:
- Before laying out a cell's content, checks if `cell.colspan >= numCols` (spans full table width)
- If so, sets the cell's BlockStyle alignment to `CssTextAlign::Center`
- This correctly centers section headers and title rows in Wikipedia infobox-style tables
### 3. Increased Cell Padding
**`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`**:
- `TABLE_CELL_PAD_X`: 2 → 4 pixels (horizontal padding)
- Added `TABLE_CELL_PAD_Y = 2` pixels (vertical padding)
- Row height now includes `2 * TABLE_CELL_PAD_Y` for top/bottom padding
**`lib/Epub/Epub/Page.cpp`**:
- `TABLE_CELL_PADDING_X`: 2 → 4 pixels (matches parser constant)
- Added `TABLE_CELL_PADDING_Y = 2` pixels
- Cell text Y position now accounts for vertical padding: `baseY + 1 + TABLE_CELL_PADDING_Y`
## Follow-up Items
- The padding constants are duplicated between `ChapterHtmlSlimParser.cpp` and `Page.cpp` — could be unified into a shared header
- Vertical centering within cells (when a cell has fewer lines than the tallest cell) is not implemented

View File

@@ -1,32 +0,0 @@
# Table Width Hints: HTML Attributes + CSS Width Support
## Task Description
Add support for author-specified column widths from HTML `width` attributes and CSS `width` property on `<table>`, `<col>`, `<td>`, and `<th>` elements, using them as hints for column sizing in `processTable()`.
## Changes Made
### 1. CSS Layer (`lib/Epub/Epub/css/CssStyle.h`, `lib/Epub/Epub/css/CssParser.cpp`)
- Added `width` bit to `CssPropertyFlags`
- Added `CssLength width` field to `CssStyle`
- Added `hasWidth()` convenience method
- Updated `applyOver()`, `reset()`, `clearAll()`, `anySet()` to include `width`
- Added `else if (propName == "width")` case to `CssParser::parseDeclarations()` using `interpretLength()`
### 2. Table Data (`lib/Epub/Epub/TableData.h`)
- Added `CssLength widthHint` and `bool hasWidthHint` to `TableCell`
- Added `std::vector<CssLength> colWidthHints` to `TableData` (from `<col>` tags)
- Added `#include "css/CssStyle.h"` for `CssLength`
### 3. Parser (`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`)
- Removed `"col"` from `TABLE_SKIP_TAGS` (was being skipped entirely)
- Added `parseHtmlWidthAttr()` helper: parses HTML `width="200"` (pixels) and `width="50%"` (percent) into `CssLength`
- Added `<col>` handling in `startElement`: parses `width` HTML attribute and `style` CSS, pushes to `tableData->colWidthHints`
- Updated `<td>`/`<th>` handling: now parses `width` HTML attribute, `style` attribute, and stylesheet CSS width (via `resolveStyle`). CSS takes priority over HTML attribute. Stored in `TableCell::widthHint`
### 4. Layout (`processTable()`)
- Added step 3a: resolves width hints per column. Priority: `<col>` hints > max cell hint (colspan=1 only). Percentages resolve relative to available content width.
- Modified step 3b: hinted columns get their resolved pixel width (clamped to min col width). If all hinted widths exceed available space, they're scaled down proportionally. Unhinted columns use the existing two-pass fair-share algorithm on the remaining space.
## Follow-up Items
- The `<colgroup>` tag is still skipped entirely; `<col>` tags within `<colgroup>` won't be reached. Could un-skip `<colgroup>` (make it transparent like `<thead>`/`<tbody>`) if needed.
- `rowspan` is not yet supported.

View File

@@ -1,41 +0,0 @@
# Cherry-pick Image Support from pablohc/crosspoint-reader@2d8cbcf (PR #556)
## Task
Merge EPUB embedded image support (JPEG/PNG) from pablohc's fork into the mod branch, based on upstream PR #556.
## Changes Made
### New Files (11)
- `lib/Epub/Epub/blocks/ImageBlock.h` / `.cpp` - Image block type for page layout
- `lib/Epub/Epub/converters/DitherUtils.h` - 4x4 Bayer dithering for 4-level grayscale
- `lib/Epub/Epub/converters/ImageDecoderFactory.h` / `.cpp` - Format-based decoder selection
- `lib/Epub/Epub/converters/ImageToFramebufferDecoder.h` / `.cpp` - Base decoder interface
- `lib/Epub/Epub/converters/JpegToFramebufferConverter.h` / `.cpp` - JPEG decoder (picojpeg)
- `lib/Epub/Epub/converters/PngToFramebufferConverter.h` / `.cpp` - PNG decoder (PNGdec)
- `lib/Epub/Epub/converters/PixelCache.h` - 2-bit pixel cache for fast re-render
- `scripts/generate_test_epub.py` - Test EPUB generator
### Modified Files (13)
- `lib/Epub/Epub/blocks/Block.h` - Removed unused `layout()` virtual
- `lib/Epub/Epub/blocks/TextBlock.h` - Removed unused `layout()` override
- `lib/Epub/Epub/Page.h` / `.cpp` - Added `PageImage` class, `TAG_PageImage=3`, `hasImages()`, `getImageBoundingBox()`
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h` / `.cpp` - Image extraction/decoding from EPUB, new constructor params
- `lib/Epub/Epub/Section.cpp` - Derive content base/image paths, bumped version 12→13
- `lib/GfxRenderer/GfxRenderer.h` / `.cpp` - Added `getRenderMode()`, implemented `displayWindow()`
- `lib/hal/HalDisplay.h` / `.cpp` - Added `displayWindow()` for partial refresh
- `src/activities/reader/EpubReaderActivity.cpp` - Image-aware refresh with double FAST_REFRESH optimization
- `platformio.ini` - Added `PNGdec` dependency, `PNG_MAX_BUFFERED_PIXELS=6402` build flag
## Key Conflict Resolutions
- `TAG_PageImage = 3` (not 2) to avoid collision with mod's `TAG_PageTableRow = 2`
- Preserved mod's bookmark ribbon rendering in `renderContents`
- Preserved mod's table rendering (`PageTableRow`) alongside new `PageImage`
- Section file version bumped to invalidate cached sections
## Build Result
- `mod` environment: SUCCESS (RAM 31.0%, Flash 77.5%)
## Follow-up Items
- Test on device with JPEG/PNG EPUBs
- Run `scripts/generate_test_epub.py` to create test EPUBs
- Consider whether `displayWindow()` experimental path should be enabled

View File

@@ -1,52 +0,0 @@
# EPUB Table Rendering Implementation
## Task
Replace the `[Table omitted]` placeholder in the EPUB reader with full column-aligned table rendering, including grid lines, proportional column widths, and proper serialization.
## Changes Made
### New file
- **`lib/Epub/Epub/TableData.h`** -- Lightweight structs (`TableCell`, `TableRow`, `TableData`) for buffering table content during SAX parsing.
### Modified files
- **`lib/Epub/Epub/ParsedText.h` / `.cpp`**
- Added `getNaturalWidth()` public method to measure the single-line content width of a ParsedText. Used by column width calculation.
- **`lib/Epub/Epub/Page.h` / `.cpp`**
- Added `TAG_PageTableRow = 2` to `PageElementTag` enum.
- Added `getTag()` pure virtual method to `PageElement` base class for tag-based serialization.
- Added `PageTableCellData` struct (cell lines, column width, x-offset).
- Added `PageTableRow` class with render (grid lines + cell text), serialize, and deserialize support.
- Updated `Page::serialize()` to use `el->getTag()` instead of hardcoded tag.
- Updated `Page::deserialize()` to handle `TAG_PageTableRow`.
- **`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h`**
- Added `#include "../TableData.h"`.
- Added table state fields: `bool inTable`, `std::unique_ptr<TableData> tableData`.
- Added `processTable()` and `addTableRowToPage()` method declarations.
- **`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`**
- Added table-related tag arrays (`TABLE_TRANSPARENT_TAGS`, `TABLE_SKIP_TAGS`).
- Replaced `[Table omitted]` placeholder with full table buffering logic in `startElement`.
- Modified `startNewTextBlock` to be a no-op when inside a table (cell content stays in one ParsedText).
- Added table close handling in `endElement` for `</td>`, `</th>`, and `</table>`.
- Disabled the 750-word early split when inside a table.
- Implemented `processTable()`: column width calculation (natural + proportional distribution), per-cell layout via `layoutAndExtractLines`, `PageTableRow` creation.
- Implemented `addTableRowToPage()`: page-break handling for table rows.
## Design Decisions
- Tables are buffered entirely during parsing, then processed on `</table>` close (two-pass: measure then layout).
- Column widths are proportional to natural content width, with equal distribution of extra space when content fits.
- Grid lines (1px) drawn around every cell; 2px horizontal cell padding.
- Nested tables are skipped (v1 limitation).
- `<caption>`, `<colgroup>`, `<col>` are skipped; `<thead>`, `<tbody>`, `<tfoot>` are transparent.
- `<th>` cells get bold text. Cell text is left-aligned with no paragraph indent.
- Serialization is backward-compatible: old firmware encountering the new tag will re-parse the section.
## Follow-up Items
- Nested table support (currently skipped)
- `colspan` / `rowspan` support
- `<caption>` rendering as centered text above the table
- CSS border detection (currently always draws grid lines)
- Consider CSS-based cell alignment

View File

@@ -1,39 +0,0 @@
# Adjust Low Power Mode: Fix Processing Bug and Sync with PR #852
**Date:** 2026-02-15
**Branch:** mod/adjust-low-power-mode
## Task
Fix a bug where the device enters low-power mode (10MHz CPU) during first-time book opening and chapter indexing, causing significant slowdown. Also sync minor logging differences with upstream PR #852.
## Root Causes (three issues combined)
1. **Missing delegation in ActivityWithSubactivity**: `main.cpp` calls `preventAutoSleep()` on `ReaderActivity` (the top-level activity). `ReaderActivity` creates `EpubReaderActivity` as a subactivity, but `ActivityWithSubactivity` never delegated `preventAutoSleep()` or `skipLoopDelay()` to the active subactivity.
2. **Stale check across activity transitions**: The `preventAutoSleep()` check at the top of the main loop runs before `loop()`. When an activity transitions mid-loop (HomeActivity -> ReaderActivity), the pre-loop check is stale but the post-loop power-saving decision fires.
3. **Section object vs section file**: `!section` alone was insufficient as a condition. The Section object is created early in the `!section` block, making `section` non-null, but `createSectionFile()` (the slow operation) runs afterward. A separate `loadingSection` flag is needed to cover the full duration.
## Changes Made
1. **ActivityWithSubactivity** (`src/activities/ActivityWithSubactivity.h`)
- Added `preventAutoSleep()` override that delegates to `subActivity->preventAutoSleep()`
- Added `skipLoopDelay()` override with same delegation pattern
2. **main.cpp** (`src/main.cpp`)
- Added a second `preventAutoSleep()` re-check after `currentActivity->loop()` returns, before the power-saving block
3. **EpubReaderActivity** (`src/activities/reader/EpubReaderActivity.h`, `.cpp`)
- Added `volatile bool loadingSection` flag
- `preventAutoSleep()` returns `!section || loadingSection`
- `!section` covers the pre-Section-object period (including cover prerendering in onEnter)
- `loadingSection` covers the full `!section` block in `renderScreen()` where `createSectionFile()` runs
- Flag is also cleared on the error path
4. **TxtReaderActivity** (`src/activities/reader/TxtReaderActivity.h`)
- `preventAutoSleep()` returns `!initialized`
- Covers cover prerendering and page index building
5. **HalPowerManager.cpp** (`lib/hal/HalPowerManager.cpp`)
- Synced log messages with upstream PR: `LOG_ERR` -> `LOG_DBG` with frequency values (matching commit `ff89fb1`)

View File

@@ -1,28 +0,0 @@
# Fix: Cover/Thumbnail Pipeline on Home Screen
**Date:** 2026-02-15
## Task Description
Multiple issues with book cover thumbnails on the home screen:
1. After clearing a book's cache, the home screen showed a placeholder instead of the real cover.
2. Books without covers showed blank rectangles instead of generated placeholder covers.
## Root Cause
`Epub::generateThumbBmp()` wrote an empty 0-byte BMP file as a "don't retry" sentinel when a book had no cover. This empty file:
- Blocked the placeholder fallback in `EpubReaderActivity::onEnter()` (file exists check passes)
- Tricked the home screen into thinking a valid thumbnail exists (skips regeneration)
- Failed to parse in `LyraTheme::drawRecentBookCover()` resulting in a blank gray rectangle
## Changes Made
- **`lib/Epub/Epub.cpp`**: Removed the empty sentinel file write from `generateThumbBmp()`. Now it simply returns `false` when there's no cover, letting callers generate valid placeholder BMPs that serve the same "don't retry" purpose.
- **`src/activities/home/HomeActivity.cpp`**: Changed placeholder fallback in `loadRecentCovers()` from `if (!success && !Storage.exists(coverPath))` to `if (!success)` as defense-in-depth for edge cases like global cache clear.
- **`src/RecentBooksStore.h`**: Added `removeBook(const std::string& path)` method declaration.
- **`src/RecentBooksStore.cpp`**: Implemented `removeBook()` — finds and erases the book by path, then persists the updated list.
- **`src/activities/reader/EpubReaderActivity.cpp`**: After clearing cache in the `DELETE_CACHE` handler, calls `RECENT_BOOKS.removeBook(epub->getPath())` so the book is cleanly removed from recents when its cache is wiped.
## Follow-up Items
- None.

View File

@@ -1,42 +0,0 @@
# Merge upstream master (CSS perf #779) into mod/master-img
**Date**: 2026-02-15
## Task
Merge the latest changes from `master` (upstream) into `mod/master-img`.
## Changes Merged
One upstream commit: `46c2109 perf: Improve large CSS files handling (#779)`
This commit significantly refactored the CSS subsystem:
- Streaming CSS parser with `StackBuffer` for zero-heap parsing
- Extracted `parseDeclarationIntoStyle()` from inline logic
- Rule limits and selector validation
- `CssParser` now owns its `cachePath` and manages caching internally
- CSS loading skipped when "Book's Embedded Style" is off
## Conflicts Resolved
### Section.cpp (2 regions)
- Combined mod's image support variables (`contentBase`, `imageBasePath`) with master's new CSS parser loading pattern (`cssParser->loadFromCache()`)
- Merged constructor call: kept mod's `epub`, `contentBase`, `imageBasePath` params while adopting master's `cssParser` local variable pattern
- Added master's `cssParser->clear()` calls on error/success paths
### CssParser.cpp (1 region)
- Accepted master's complete rewrite of the CSS parser
- Ported mod's `width` CSS property handler into the new `parseDeclarationIntoStyle()` function
## Auto-merged Files Verified
- `CssStyle.h`: `width` property and supporting code preserved
- `platformio.ini`: `PNGdec` library and `PNG_MAX_BUFFERED_PIXELS` preserved; `[env:mod]` section intact; master's `gnu++2a` and `build_unflags` applied
- `ChapterHtmlSlimParser.h`: image/table support members preserved
- `RecentBooksStore.cpp`: `removeBook()` method preserved
- `Epub.cpp`, `Epub.h`, `CssParser.h`, `ReaderActivity.cpp`: auto-merged cleanly
## Build Result
- `pio run -e mod` succeeded with zero errors
## Commit
- `744d616 Merge branch 'master' into mod/master-img`

View File

@@ -1,51 +0,0 @@
# Port PR #838 and PR #907 into fork
## Task
Cherry-pick / manually port two upstream PRs into the fork:
- **PR #907**: Cover image outlines to improve legibility
- **PR #838**: Fallback logic for epub cover extraction
## Changes Made
### PR #907 (cover outlines) — `src/components/themes/lyra/LyraTheme.cpp`
- Always draw a rectangle outline around cover tiles before drawing the bitmap on top
- Removed `hasCover` flag — simplified logic so outline is always present, preventing low-contrast covers from blending into the background
### PR #838 (cover fallback logic) — 3 files
#### `lib/Epub/Epub.h`
- Added declarations for 4 new methods: `generateInvalidFormatCoverBmp`, `generateInvalidFormatThumbBmp`, `isValidThumbnailBmp` (static), `getCoverCandidates` (private)
- Added doc comments on existing `generateCoverBmp` and `generateThumbBmp`
#### `lib/Epub/Epub.cpp`
- Added `#include <HalDisplay.h>` and `#include <algorithm>`
- **`generateCoverBmp`**: Added invalid BMP detection/retry, cover fallback candidate probing (`getCoverCandidates`), case-insensitive extension checking. Returns `false` on failure (no internal X-pattern fallback) so callers control the fallback chain
- **`generateThumbBmp`**: Same changes as `generateCoverBmp` — invalid BMP detection, fallback probing, case-insensitive check. Returns `false` on failure (no internal X-pattern fallback) so callers control the fallback chain
- **`generateInvalidFormatThumbBmp`** (new): Creates 1-bit BMP with X pattern as thumbnail marker
- **`generateInvalidFormatCoverBmp`** (new): Creates 1-bit BMP with X pattern as cover marker, using display dimensions
- **`isValidThumbnailBmp`** (new, static): Validates BMP by checking file size > 0 and 'BM' header
- **`getCoverCandidates`** (new, private): Returns list of common cover filenames to probe
#### `src/activities/home/HomeActivity.cpp`
- Replaced `Storage.exists()` check with `Epub::isValidThumbnailBmp()` to catch corrupt/empty thumbnails
- Added epub.load retry logic (cache-only first, then full build)
- After successful thumbnail generation, update `RECENT_BOOKS` with the new thumb path
- Thumbnail fallback chain: Real Cover → PlaceholderCoverGenerator → X-Pattern marker (epub only; xtc/other formats fall back to placeholder only)
## Adaptations from upstream PR
- All `Serial.printf` calls converted to `LOG_DBG`/`LOG_ERR` macros (fork convention)
- No `#include <HardwareSerial.h>` (not needed with Logging.h)
- Retained fork's `bookMetadataCache` null-check guards
- HomeActivity changes manually adapted to fork's `loadRecentCovers()` structure (upstream has inline code in a different method)
- Fork's `PlaceholderCoverGenerator` is the preferred fallback; X-pattern marker is last resort only
#### `src/activities/boot_sleep/SleepActivity.cpp`
- EPUB sleep screen cover now follows Real Cover → Placeholder → X-Pattern fallback chain
- Upgraded `Storage.exists()` check to `Epub::isValidThumbnailBmp()` for the epub cover path
#### `src/activities/reader/EpubReaderActivity.cpp`
- Cover and thumbnail pre-rendering now follows Real Cover → Placeholder → X-Pattern fallback chain
- Upgraded all `Storage.exists()` checks to `Epub::isValidThumbnailBmp()` for cover/thumb paths
## Follow-up Items
- Build and test on device to verify cover generation pipeline works end-to-end

View File

@@ -1,29 +0,0 @@
# Created CrossPoint Reader Development Skill
**Date**: 2026-02-16
**Task**: Create a Cursor agent skill for CrossPoint Reader firmware development guidance.
## Changes Made
Created project-level skill at `.cursor/skills/crosspoint-reader-dev/` with 6 files:
| File | Lines | Purpose |
|------|-------|---------|
| `SKILL.md` | 202 | Core rules: agent identity, hardware constraints, resource protocol, architecture overview, HAL usage, coding standards, error handling, activity lifecycle, UI rules |
| `architecture.md` | 138 | Build system (PlatformIO CLI + VS Code), build flags, environments, generated files, local config, platform detection |
| `coding-patterns.md` | 135 | FreeRTOS tasks, malloc patterns, global font loading, button mapping, UI rendering rules |
| `debugging-and-testing.md` | 148 | Build commands, serial monitoring, crash debugging (OOM, stack overflow, use-after-free, watchdog), testing checklist, CI/CD pipeline |
| `git-workflow.md` | 98 | Repository detection, branch naming, commit messages, when to commit |
| `cache-management.md` | 100 | SD card cache structure, invalidation rules, file format versioning |
## Design Decisions
- **Progressive disclosure**: SKILL.md kept to 202 lines (well under 500 limit) with always-needed info; detailed references in separate files one level deep
- **Project-level storage**: `.cursor/skills/` so it's shared with anyone using the repo
- **Description** includes broad trigger terms: ESP32-C3, PlatformIO, EPUB, e-ink, HAL, activity lifecycle, FreeRTOS, SD card, embedded C++
## Follow-up Items
- Consider adding the skill directory to `.gitignore` if this should remain personal, or commit if sharing with collaborators
- Update cache file format version numbers if they've changed since the guide was written
- Skill will auto-activate when the agent detects firmware/embedded development context

View File

@@ -1,57 +0,0 @@
# Reader Menu Improvements
## Task
Overhaul the EPUB reader menu: consolidate dictionary actions behind long-press, add portrait/landscape toggle with preferred-orientation settings, add font size cycling, and rename several menu labels.
## Changes Made
### 1. Consolidated Dictionary Menu Items
- Removed "Lookup Word History" and "Delete Dictionary Cache" from the reader menu
- Long-pressing Confirm on "Lookup Word" now opens the Lookup History screen
- "Delete Dictionary Cache" moved to a sentinel entry at the bottom of the Lookup History word list
**Files:** `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `LookedUpWordsActivity.h`, `LookedUpWordsActivity.cpp`, `EpubReaderActivity.cpp`
### 2. Toggle Portrait/Landscape
- Renamed "Reading Orientation" to "Toggle Portrait/Landscape" in the reader menu (kept the original name in global Settings)
- Short-press now toggles between preferred portrait and preferred landscape orientations
- Long-press opens a popup sub-menu with all 4 orientation options
- Added `preferredPortrait` and `preferredLandscape` settings to `CrossPointSettings` (serialized at end for backward-compat)
- Added corresponding settings to `SettingsList.h` using `DynamicEnum` (maps non-sequential enum values correctly)
**Files:** `CrossPointSettings.h`, `CrossPointSettings.cpp`, `SettingsList.h`, `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`
### 3. Toggle Font Size
- Added new `TOGGLE_FONT_SIZE` menu action
- Cycles through Small → Medium → Large → Extra Large → Small on each press
- Shows current size value next to the label (like orientation)
- Applied on menu exit via extended `onBack` callback `(uint8_t orientation, uint8_t fontSize)`
- Added `applyFontSize()` to `EpubReaderActivity` (saves to settings, resets section for re-layout)
**Files:** `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `EpubReaderActivity.h`, `EpubReaderActivity.cpp`
### 4. Label Renames
- "Letterbox Fill" → "Override Letterbox Fill" (reader menu only; global setting keeps original name)
- "Sync Progress" → "Sync Reading Progress"
**Files:** `english.yaml` (I18n source), regenerated `I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`
### 5. Long-Press Safety
- Added `ignoreNextConfirmRelease` flag to `EpubReaderMenuActivity` to prevent stale releases
- Added `initialSkipRelease` constructor parameter to `LookedUpWordsActivity`
- Extended `ignoreNextConfirmRelease` guard to cover the word-selection path (not just delete-confirm mode)
- Orientation sub-menu also uses `ignoreNextConfirmRelease` to avoid selecting on long-press release
### 6. New I18n Strings
- `STR_TOGGLE_ORIENTATION`: "Toggle Portrait/Landscape"
- `STR_TOGGLE_FONT_SIZE`: "Toggle Font Size"
- `STR_OVERRIDE_LETTERBOX_FILL`: "Override Letterbox Fill"
- `STR_PREFERRED_PORTRAIT`: "Preferred Portrait"
- `STR_PREFERRED_LANDSCAPE`: "Preferred Landscape"
## Build Status
Successfully compiled (default environment). RAM: 31.1%, Flash: 99.4%.
## Follow-up Items
- Translations: New strings fall back to English for all non-English languages. Translators can add entries to their respective YAML files.
- The `SettingInfo::Enum` approach doesn't work for non-sequential enum values (portrait=0, inverted=2). Used `DynamicEnum` with getter/setter lambdas instead.

View File

@@ -1,31 +0,0 @@
# Merge Assessment & Cherry-pick: master -> mod/master
## Task
Assess and merge new commits from `master` into `mod/master`.
## Analysis
- 19 commits on `master` not on `mod/master`, but 16 were already cherry-picked or manually ported (different hashes, same content)
- A full `git merge master` produced 30+ conflicts due to duplicate cherry-picks with different patch IDs
- Identified 3 genuinely new commits
## Changes Made
### 1. Cherry-pick `97c3314` (#932) - `f21720d`
- perf: Skip constructing unnecessary `std::string` in TextBlock.cpp
- 1-line change, applied cleanly
### 2. Cherry-pick `2a32d8a` (#926) - `424e332`
- chore: Improve Russian language support
- Renamed `russia.yaml` -> `russian.yaml`, updated `i18n.md`, fixed translation strings
- Applied cleanly
### 3. Cherry-pick `0bc6747` (#827) - `61fb11c`
- feat: Add PNG cover image support for EPUB books
- Added `PngToBmpConverter` library (new files, 858 lines)
- Resolved 2 conflicts:
- `Epub.cpp`: Discarded incoming JPG/PNG block (used old variable names), added PNG thumbnail support to mod's existing structure using `effectiveCoverImageHref` with case-insensitive checks. Fixed `generateCoverBmp()` PNG block to also use `effectiveCoverImageHref`. Added `.png` to `getCoverCandidates()`.
- `ImageToFramebufferDecoder.cpp`: Took upstream `LOG_ERR` version over mod's `Serial.printf`
## Follow-up Items
- Build and test PNG cover rendering on device
- `mod/master` is now fully caught up with `master`

View File

@@ -1,66 +0,0 @@
# Improve Home Screen
## Task Description
Enhance the Lyra theme home screen with six improvements: empty-state placeholder, adaptive recent book cards, 2-line title wrapping with author, adjusted button positioning, optional clock display, and a manual Set Time activity.
## Changes Made
### i18n Strings (8 YAML files + auto-generated C++ files)
- Added `STR_CHOOSE_SOMETHING`, `STR_HOME_SCREEN_CLOCK`, `STR_CLOCK_AMPM`, `STR_CLOCK_24H`, `STR_SET_TIME` to all 8 translation YAML files with localized text
- Regenerated `lib/I18n/I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp` via `gen_i18n.py`
### Settings: Clock Format (`CrossPointSettings.h`, `.cpp`, `SettingsList.h`)
- Added `CLOCK_FORMAT` enum (`CLOCK_OFF`, `CLOCK_AMPM`, `CLOCK_24H`) and `homeScreenClock` member (default OFF)
- Added persistence in `writeSettings()` and `loadFromFile()` (appended at end for backward compatibility)
- Added "Home Screen Clock" setting under Display category in `SettingsList.h`
### Lyra Theme: Empty State Placeholder (`LyraTheme.cpp`)
- When `recentBooks` is empty, draws centered "Choose something to read" text instead of blank area
### Lyra Theme: 1-Book Horizontal Layout (`LyraTheme.cpp`)
- When 1 recent book: cover on the left (natural aspect ratio), title + author on the right
- Title uses `UI_12_FONT_ID` with generous wrapping (up to 5 lines, no truncation unless very long)
- Author in `UI_10_FONT_ID` below title with 4px gap
### Lyra Theme: Multi-Book Tile Layout (`LyraTheme.cpp`)
- 2-3 books: tile-based layout with cover centered within tile (no stretching)
- Cover bitmap rendering preserves aspect ratio: crops if wider than slot, centers if narrower
- Title wraps to 2 lines with ellipsis, author in `SMALL_FONT_ID` below
### Lyra Theme: Selection Background Fix (`LyraTheme.cpp`)
- Bottom section of selection highlight now uses full remaining height below cover
- Prevents author text from clipping outside the selection area
### Lyra Theme: Shared Helpers (`LyraTheme.cpp`)
- Extracted `wrapText` lambda for reusable word-wrap logic (parameterized by font, maxLines, maxWidth)
- Extracted `renderCoverBitmap` lambda for aspect-ratio-preserving cover rendering
### Lyra Metrics (`LyraTheme.h`)
- Increased `homeCoverTileHeight` from 287 to 310 to accommodate expanded text area
- Menu buttons shift down automatically since they're positioned relative to this metric
### Home Screen Clock (`HomeActivity.cpp`)
- Added clock rendering in the header area (top-left) after `drawHeader()`
- Respects `homeScreenClock` setting (OFF / AM/PM / 24H)
- Skips rendering if system time is unset (year <= 2000)
### Set Time Activity (NEW: `SetTimeActivity.h`, `SetTimeActivity.cpp`)
- New sub-activity for manual time entry: displays HH:MM with field selection
- Left/Right switches between hours and minutes, Up/Down adjusts values
- Confirm saves via `settimeofday()`, Back discards
- Wired into Settings > Display as an action item
### Settings Activity Wiring (`SettingsActivity.h`, `SettingsActivity.cpp`)
- Added `SetTime` to `SettingAction` enum
- Added include and switch case for `SetTimeActivity`
- Added "Set Time" action to display settings category
## Build Verification
- `pio run -e default` succeeded
- RAM: 31.1% (101,772 / 327,680 bytes)
- Flash: 99.5%
## Follow-up Items
- Test on hardware: verify clock display, card layout with 0/1/2/3 books, Set Time activity
- Fine-tune `homeCoverTileHeight` value if text area feels too tight or loose visually
- Consider NTP auto-sync when WiFi is available (currently only during KOReader sync)

View File

@@ -1,63 +0,0 @@
# Upstream Sync: `upstream/master` into `mod/master`
**Date:** 2026-02-16
**Task:** Synchronize recent upstream changes into the mod fork while preserving all mod-specific features.
## Strategy
- **Cherry-pick** approach (not full merge) for granular control
- 13 upstream commits cherry-picked across 3 phases; 4 skipped (mod already had enhanced implementations)
- Working branch `mod/sync-upstream` used, then fast-forwarded into `mod/master`
## Phases & Commits
### Phase 1 — Low-risk (6 commits)
| PR | Description | Notes |
|----|-------------|-------|
| #689 | Hyphenation optimization | Naming conflict resolved (mod's `OMIT_HYPH_*` guards preserved) |
| #832 | Settings size auto-calc | Mod's `sleepScreenLetterboxFill` added to new `SettingsWriter` |
| #840 | clang-format-fix shebang | Clean |
| #917 | SCOPE.md dictionary docs | Clean |
| #856 | Multiple author display | Clean |
| #858 | Miniz compilation warning | Clean |
### Phase 2 — Major refactors (3 commits)
| PR | Description | Notes |
|----|-------------|-------|
| #774 | Activity render() refactor | ~15 conflicts. Mod's 5 custom activities (Bookmarks, Dictionary) adapted to new `render(RenderLock&&)` pattern. Deadlock in `EpubReaderActivity.cpp` identified and fixed (redundant mutex around `enterNewActivity()`). |
| #916 | RAII RenderLock | Clean cherry-pick + audit of mod code for manual mutex usage |
| #728 | I18n system | ~15-20 conflicts. 16 new `StrId` keys added for mod strings. `DynamicEnum` font handling preserved. `SettingsList.h` and `SettingsActivity.cpp` adapted. |
### Phase 3 — Post-I18n fixes (4 commits)
| PR | Description | Notes |
|----|-------------|-------|
| #884 | Empty button icon fix | Clean |
| #906 | Webserver docs update | Clean |
| #792 | Translators doc | Clean |
| #796 | Battery icon alignment | Clean |
### Mod adaptation commits (3)
- `mod: adapt mod activities to #774 render() pattern` — Systematic refactor of 5 activity pairs
- `mod: convert remaining manual render locks to RAII RenderLock` — Audit & cleanup
- `mod: remove duplicate I18n.h include in HomeActivity.cpp` — Cleanup
## Skipped Upstream Commits (4)
- #676, #700 (image/cover improvements) — Mod already has enhanced pipeline
- #668 (JPEG support) — Already in mod
- #780 (cover fallback) — Already cherry-picked into mod
## Files Changed
111 files changed, ~23,700 insertions, ~17,700 deletions across hyphenation tries, I18n system, activity refactors, and documentation.
## Key Decisions
- **Image pipeline:** Kept mod's versions entirely; upstream's cover fixes were already ported
- **Deadlock fix:** Removed redundant `xSemaphoreTake`/`xSemaphoreGive` around `enterNewActivity()` in `EpubReaderActivity.cpp` — the new `RenderLock` inside `enterNewActivity()` would deadlock with the outer manual lock
- **I18n integration:** Added mod-specific `StrId` keys rather than keeping hardcoded strings
## Verification
- All mod features (bookmarks, dictionary, letterbox fill, placeholder covers, table rendering, embedded images) verified for code-path integrity post-sync
- No broken references or missing dependencies found
## Follow-up Items
- Full PlatformIO build on hardware to confirm compilation
- Runtime testing of all mod features on device

View File

@@ -1,38 +0,0 @@
# Clock Persistence, Size Setting, and Timezone Support
## Task Description
Implemented the plan from `clock_settings_and_timezone_fd0bf03f.plan.md` covering three features:
1. Fix homeScreenClock setting not persisting across reboots
2. Add clock size setting (Small/Medium/Large)
3. Add timezone selection with North American presets and custom UTC offset
## Changes Made
### 1. Fix Persistence Bug
- **`src/CrossPointSettings.cpp`**: Removed stale legacy `sleepScreenGradientDir` read (lines 270-271) from `loadFromFile()` that was causing a one-byte deserialization offset, corrupting `preferredPortrait`, `preferredLandscape`, and `homeScreenClock`.
### 2. Clock Size Setting
- **`src/CrossPointSettings.h`**: Added `CLOCK_SIZE` enum (SMALL/MEDIUM/LARGE) and `uint8_t clockSize` field.
- **`src/CrossPointSettings.cpp`**: Added `clockSize` to `writeSettings()` and `loadFromFile()` serialization.
- **`src/SettingsList.h`**: Added clock size enum setting in Display category.
- **`lib/I18n/I18nKeys.h`**: Added `STR_CLOCK_SIZE`, `STR_CLOCK_SIZE_SMALL`, `STR_CLOCK_SIZE_MEDIUM`, `STR_CLOCK_SIZE_LARGE`.
- **All 8 YAML translation files**: Added clock size strings.
- **`src/components/themes/BaseTheme.cpp`** and **`src/components/themes/lyra/LyraTheme.cpp`**: Updated `drawHeader()` to select font (SMALL_FONT_ID / UI_10_FONT_ID / UI_12_FONT_ID) based on `clockSize` setting.
### 3. Timezone Support
- **`src/CrossPointSettings.h`**: Added `TIMEZONE` enum (UTC, Eastern, Central, Mountain, Pacific, Alaska, Hawaii, Custom), `uint8_t timezone` and `int8_t timezoneOffsetHours` fields, and `getTimezonePosixStr()` declaration.
- **`src/CrossPointSettings.cpp`**: Added timezone/offset serialization, validation (-12 to +14), and `getTimezonePosixStr()` implementation returning POSIX TZ strings (including DST rules for NA timezones).
- **`lib/I18n/I18nKeys.h`** + **all YAML files**: Added timezone strings and "Set UTC Offset" label.
- **`src/SettingsList.h`**: Added timezone enum setting in Display category.
- **`src/activities/settings/SetTimezoneOffsetActivity.h/.cpp`** (new files): UTC offset picker activity (-12 to +14), using same UI pattern as `SetTimeActivity`.
- **`src/activities/settings/SettingsActivity.h`**: Added `SetTimezoneOffset` to `SettingAction` enum.
- **`src/activities/settings/SettingsActivity.cpp`**: Added include, action entry, handler for SetTimezoneOffset, and `setenv`/`tzset` call after every settings save.
- **`src/main.cpp`**: Apply saved timezone via `setenv`/`tzset` on boot after `SETTINGS.loadFromFile()`.
- **`src/util/TimeSync.cpp`**: Apply timezone before starting NTP sync so time displays correctly.
## Build Status
Firmware builds successfully (99.5% flash usage).
## Follow-up Items
- Test on device: verify clock persistence, size changes, timezone selection, and custom UTC offset picker.
- Timezone strings use English fallbacks for non-English languages.

View File

@@ -1,33 +0,0 @@
# Move Clock Settings to Own Category
## Task Description
Moved all clock-related settings into a dedicated "Clock" tab in the settings screen, renamed "Home Screen Clock" to "Clock" (since it's now shown globally), and made "Set UTC Offset" conditionally visible only when timezone is set to "Custom".
## Changes Made
### 1. Rename "Home Screen Clock" -> "Clock"
- **`lib/I18n/I18nKeys.h`**: Renamed `STR_HOME_SCREEN_CLOCK` to `STR_CLOCK`.
- **All 8 YAML translation files**: Updated key name and shortened labels (e.g., "Home Screen Clock" -> "Clock", "Hodiny na domovske obrazovce" -> "Hodiny").
- **`src/CrossPointSettings.h`**: Renamed field `homeScreenClock` to `clockFormat`.
- **`src/CrossPointSettings.cpp`**: Updated all references (`writeSettings`, `loadFromFile`).
- **`src/SettingsList.h`**: Updated setting entry and JSON API key.
- **`src/main.cpp`**, **`src/components/themes/BaseTheme.cpp`**, **`src/components/themes/lyra/LyraTheme.cpp`**: Updated all `SETTINGS.homeScreenClock` references to `SETTINGS.clockFormat`.
### 2. New "Clock" settings category
- **`lib/I18n/I18nKeys.h`**: Added `STR_CAT_CLOCK`.
- **All 8 YAML translation files**: Added localized "Clock" category label.
- **`src/SettingsList.h`**: Changed category of Clock, Clock Size, and Timezone from `STR_CAT_DISPLAY` to `STR_CAT_CLOCK`.
### 3. Wire up in SettingsActivity
- **`src/activities/settings/SettingsActivity.h`**: Added `clockSettings` vector, `rebuildClockActions()` helper, bumped `categoryCount` to 5.
- **`src/activities/settings/SettingsActivity.cpp`**:
- Added `STR_CAT_CLOCK` to `categoryNames` (index 1, between Display and Reader).
- Added routing for `STR_CAT_CLOCK` in the category-sorting loop.
- Implemented `rebuildClockActions()`: always adds "Set Time", conditionally adds "Set UTC Offset" only when `timezone == TZ_CUSTOM`. Called on `onEnter()` and after every `toggleCurrentSetting()`.
- Updated category switch indices (Display=0, Clock=1, Reader=2, Controls=3, System=4).
## Build Status
Firmware builds successfully.
## Follow-up Items
- Test on device: verify the new Clock tab, setting visibility toggle, and tab bar layout with 5 tabs.

View File

@@ -1,50 +0,0 @@
# Clock Bug Fix, NTP Auto-Sync, and Sleep Persistence
**Date**: 2026-02-17
## Task Description
Three clock-related improvements:
1. Fix SetTimeActivity immediately dismissing when opened
2. Add automatic NTP time sync on WiFi connection
3. Verify time persistence across deep sleep modes
## Changes Made
### 1. Bug Fix: SetTimeActivity immediate dismiss (`src/activities/settings/SetTimeActivity.cpp`)
- **Root cause**: `loop()` used `wasReleased()` for Back, Confirm, Left, and Right buttons. The parent `SettingsActivity` enters this subactivity on a `wasPressed()` event, so the button release from the original press immediately triggered the exit path.
- **Fix**: Changed all four `wasReleased()` calls to `wasPressed()`, matching the pattern used by all other subactivities (e.g., `LanguageSelectActivity`).
### 2. Shared NTP Utility (`src/util/TimeSync.h`, `src/util/TimeSync.cpp`)
- Created `TimeSync` namespace with three functions:
- `startNtpSync()` -- non-blocking: configures and starts SNTP service
- `waitForNtpSync(int timeoutMs = 5000)` -- blocking: starts SNTP and polls until sync completes or timeout
- `stopNtpSync()` -- stops the SNTP service
### 3. NTP on WiFi Connection (`src/activities/network/WifiSelectionActivity.cpp`)
- Added `#include "util/TimeSync.h"` and call to `TimeSync::startNtpSync()` in `checkConnectionStatus()` right after `WL_CONNECTED` is detected. This is non-blocking so it doesn't delay the UI flow.
### 4. KOReaderSync refactor (`src/activities/reader/KOReaderSyncActivity.cpp`)
- Removed the local `syncTimeWithNTP()` anonymous namespace function and `#include <esp_sntp.h>`
- Replaced both call sites with `TimeSync::waitForNtpSync()` (blocking, since KOReader needs accurate time for API requests)
- Added `#include "util/TimeSync.h"`
### 5. Boot time debug log (`src/main.cpp`)
- Added `#include <ctime>` and a debug log in `setup()` that prints the current RTC time on boot (or "not set" if epoch). This helps verify time persistence across deep sleep during testing.
## Sleep Persistence Notes
- ESP32-C3's default ESP-IDF config uses both RTC and high-resolution timers for timekeeping
- The RTC timer continues during deep sleep, so `time()` / `gettimeofday()` return correct wall-clock time after wake (with drift from the internal ~150kHz RC oscillator)
- Time does NOT survive a full power-on reset (RTC timer resets)
- NTP auto-sync on WiFi connection handles drift correction
## Build Verification
- `pio run -e mod` -- SUCCESS (RAM: 31.0%, Flash: 78.8%)
## Follow-up Items
- Test on hardware: set time manually, sleep, wake, verify time in serial log
- Test NTP: connect to WiFi from Settings, verify time updates automatically
- Consider adding `TimeSync::stopNtpSync()` call when WiFi is disconnected (currently SNTP just stops getting responses, which is harmless)

View File

@@ -1,47 +0,0 @@
# Clock UI: Symmetry, Auto-Update, and System-Wide Header Clock
**Date**: 2026-02-17
## Task Description
Three improvements to clock display:
1. Make clock positioning symmetric with battery icon in headers
2. Auto-update the clock without requiring a button press
3. Show the clock everywhere the battery appears in system UI headers
## Changes Made
### 1. Moved clock rendering into `drawHeader` (BaseTheme + LyraTheme)
Previously the clock was drawn only in `HomeActivity::render()` with ad-hoc positioning (`contentSidePadding`, `topPadding`). Now it's rendered inside `drawHeader()` in both theme implementations, using the same positioning pattern as the battery:
- **Battery** (right): icon at `rect.x + rect.width - 12 - batteryWidth`, text at `rect.y + 5`
- **Clock** (left): text at `rect.x + 12`, `rect.y + 5`
Both use `SMALL_FONT_ID`, so the font matches. The 12px margin from the edge is now symmetric on both sides.
**Files changed:**
- `src/components/themes/BaseTheme.cpp` -- added clock block in `drawHeader()`, added `#include <ctime>` and `#include "CrossPointSettings.h"`
- `src/components/themes/lyra/LyraTheme.cpp` -- same changes for Lyra theme's `drawHeader()`
### 2. Clock now appears on all header screens automatically
Since `drawHeader()` is called by: HomeActivity, MyLibraryActivity, RecentBooksActivity, SettingsActivity, LookedUpWordsActivity, DictionarySuggestionsActivity -- the clock now appears on all of these screens when enabled. No per-activity code needed.
### 3. Removed standalone clock code from HomeActivity
- `src/activities/home/HomeActivity.cpp` -- removed the 15-line clock rendering block that was separate from `drawHeader()`
### 4. Added auto-update (once per minute) on home screen
- `src/activities/home/HomeActivity.h` -- added `lastRenderedMinute` field to track the currently displayed minute
- `src/activities/home/HomeActivity.cpp` -- added minute-change detection in `loop()` that calls `requestUpdate()` when the minute rolls over, triggering a screen refresh
## Build Verification
- `pio run -e mod` -- SUCCESS (RAM: 31.0%, Flash: 78.8%)
## Follow-up Items
- The auto-update only applies to HomeActivity. Other screens (Settings, Library, etc.) will show the current time when they render but won't auto-refresh purely for clock updates, which is appropriate for e-ink.
- The DictionarySuggestionsActivity and LookedUpWordsActivity pass a non-zero `contentX` offset in their header rect, so the clock position adjusts correctly via `rect.x + 12`.

View File

@@ -1,24 +0,0 @@
# Home Screen Book Card Highlight Fixes
## Task
Fix visual issues with the selection highlight on home screen recent book cards:
1. Bottom padding too tight against author text descenders
2. Rounded corners not uniform across all four corners
## Changes Made
### LyraTheme.h
- Increased `homeCoverTileHeight` from 310 to 318 to provide bottom padding below author text
### LyraTheme.cpp
- Reduced `cornerRadius` from 6 to 4 to prevent radius clamping in the 8px-tall top strip (which was clamping `min(6, w/2, 4) = 4` while the bottom section used the full 6)
### GfxRenderer.cpp
- No net changes. Two approaches were tried and reverted:
1. **Dither origin** (global shift): misaligned the highlight's dot pattern with surrounding UI
2. **Arc-relative dither** (per-corner relative coords): created visible seam artifacts at 3 of 4 corners where the arc's relative dither met the adjacent rectangle's absolute dither with inverted parity
## Key Decisions
- **Bottom padding**: Originally tried shrinking the highlight (subtracting extra padding from bottom section height), but this cut off the author text. Correct approach: increase the tile height so the highlight naturally has more room.
- **Corner radius**: Reducing from 6 to 4 ensures all corners (both the thin top strip and taller bottom section) use the same `maxRadius` through `fillRoundedRect`'s clamping formula.
- **Dither approaches (both reverted)**: Two dither fixes were tried: (1) global dither origin shift misaligned with surrounding UI, (2) arc-relative dither created visible seam artifacts at corners where parity mismatched. The absolute dither pattern in `fillArc` is the least-bad option — the minor L/R asymmetry at radius 4 is much less noticeable than seam artifacts.

View File

@@ -1,37 +0,0 @@
# Long-Press Confirm to Open Table of Contents
**Date**: 2026-02-17
**Branch**: mod/improve-home-screen
## Task
Add long-press detection on the Confirm button while reading an EPUB to directly open the Table of Contents (chapter selection), bypassing the reader menu. Short press retains existing behavior (opens menu).
## Changes Made
### `src/activities/reader/EpubReaderActivity.h`
- Added `bool ignoreNextConfirmRelease` member to suppress short-press after a long-press Confirm
- Added `void openChapterSelection()` private method declaration
### `src/activities/reader/EpubReaderActivity.cpp`
- Added `constexpr unsigned long longPressConfirmMs = 700` threshold constant
- Extracted `openChapterSelection()` helper method from the duplicated `EpubReaderChapterSelectionActivity` construction code
- Added long-press Confirm detection in `loop()` (before the existing short-press check): opens TOC directly if `epub->getTocItemsCount() > 0`
- Refactored `onReaderMenuConfirm(SELECT_CHAPTER)` to use the new helper (was ~35 lines of inline construction)
- Refactored `onReaderMenuConfirm(GO_TO_BOOKMARK)` fallback (no bookmarks + TOC available) to use the same helper
- Reset `ignoreNextConfirmRelease` when `skipNextButtonCheck` clears, to avoid stale state across subactivity transitions
### `src/activities/reader/EpubReaderChapterSelectionActivity.h`
- Added `bool ignoreNextConfirmRelease` member
- Added `initialSkipRelease` constructor parameter (default `false`) to consume stale Confirm release when opened via long-press
### `src/activities/reader/EpubReaderChapterSelectionActivity.cpp`
- Added guard in `loop()` to skip the first Confirm release when `ignoreNextConfirmRelease` is true
## Pattern Used
Follows the existing Back button short/long-press pattern: `isPressed() + getHeldTime() >= threshold` for long press, `wasReleased()` for short press, with `ignoreNextConfirmRelease` flag (same pattern as `EpubReaderMenuActivity`, `LookedUpWordsActivity`, and other activities).
## Follow-up Items
- None identified

View File

@@ -1,17 +0,0 @@
# Cherry-pick upstream PR #939 — dangling pointer fix
## Task
Cherry-pick commit `b47e1f6` from upstream PR [#939](https://github.com/crosspoint-reader/crosspoint-reader/pull/939) into `mod/master`.
## Changes
- **File**: `src/activities/home/MyLibraryActivity.cpp` (lines 199-200)
- **Fix**: Changed `folderName` from `auto` (deduced as `const char*` pointing to a temporary) to `std::string`, and called `.c_str()` at the point of use instead. This eliminates a dangling pointer caused by `.c_str()` on a temporary `std::string` from `basepath.substr(...)`.
## Method
- Fetched PR ref via `git fetch upstream pull/939/head:pr-939`
- Cherry-picked `b47e1f6` — applied cleanly with no conflicts
- Build verified: SUCCESS (PlatformIO, 68s)
- Cleaned up temporary `pr-939` branch ref
## Follow-up
- None required. The commit preserves original authorship (Uri Tauber).

View File

@@ -1,42 +0,0 @@
# Fix Indexing Display Issues
**Date:** 2026-02-18
**Branch:** `mod/merge-upstream-pr-979`
## Task
Fixed three issues with the indexing display implementation that was added as part of merging PR #979 (silent pre-indexing for next chapter):
1. **Restore original popup for direct chapter jumps** — The conditional logic that showed a full-screen "Indexing..." message when a status bar display mode was selected was removed. Direct chapter jumps now always show the original small popup overlay, regardless of the Indexing Display setting.
2. **Clear status bar indicator after silent indexing** — Added `preIndexedNextSpine` tracking member to prevent the `silentIndexingActive` flag from being re-set on re-renders after indexing completes. Changed `silentIndexNextChapterIfNeeded` to return `bool` and added `requestUpdate()` call to trigger a clean re-render that clears the indicator.
3. **Handle single-page chapters** — Updated the pre-indexing condition to trigger on the sole page of a 1-page chapter (not just the penultimate page of multi-page chapters).
## Files Changed
- `src/activities/reader/EpubReaderActivity.h` — Added `preIndexedNextSpine` member, changed `silentIndexNextChapterIfNeeded` return type to `bool`
- `src/activities/reader/EpubReaderActivity.cpp` — All three fixes applied
## Build
PlatformIO build succeeded (RAM: 31.1%, Flash: 99.6%).
## Follow-up fix: False "Indexing" indicator + image flash on e-ink
Two related issues: (1) paging backwards to a penultimate page of an already-indexed chapter showed "Indexing" in the status bar, and (2) the `requestUpdate()` that cleared the indicator caused images to flash on e-ink.
Root cause: `silentIndexingActive` was set optimistically based on page position alone, before checking whether the next chapter's cache actually exists. The subsequent `requestUpdate()` to clear the indicator triggered a full re-render causing image artifacts.
Fix — replaced optimistic flag with a pre-check:
- Before rendering, probes the next chapter's section file via `Section::loadSectionFile`. If cached, sets `preIndexedNextSpine` and leaves `silentIndexingActive = false`. Only sets `true` when indexing is genuinely needed.
- Removed `requestUpdate()` entirely — the indicator clears naturally on the next page turn.
- Added early-out in `silentIndexNextChapterIfNeeded` for `preIndexedNextSpine` match to avoid redundant Section construction.
The pre-check cost (one `loadSectionFile` call) only happens once per chapter due to `preIndexedNextSpine` caching.
Silent indexing is only performed on text-only penultimate pages (`!p->hasImages()`). On image pages, silent indexing is skipped entirely — the normal popup handles indexing on the next chapter transition. This avoids conflicts with the grayscale rendering pipeline (`displayWindow` after `displayGrayBuffer` triggers `grayscaleRevert`, causing image inversion/ghosting).
For text-only pages: after `renderContents` returns, the indicator is cleared via `displayWindow(FAST_REFRESH)` on just the status bar strip. This is safe because text-only pages use simple BW rendering without the grayscale pipeline.
Build succeeded (RAM: 31.1%, Flash: 99.6%).

View File

@@ -1,39 +0,0 @@
# Merge PR #979: Silent Pre-Indexing + Indexing Display Setting
**Date:** 2026-02-18
**Branch:** `mod/merge-upstream-pr-979`
## Task Description
Merged upstream PR #979 (silent pre-indexing for the next chapter) and implemented both "Possible Improvements" from the PR:
1. A user setting to choose between Popup, Status Bar Text, or Status Bar Icon for indexing feedback.
2. Status bar indicator rendering during silent pre-indexing.
## Changes Made
### PR #979 Cherry-Pick (2 files)
- **`src/activities/reader/EpubReaderActivity.h`** -- Added `silentIndexNextChapterIfNeeded()` method declaration and `silentIndexingActive` flag.
- **`src/activities/reader/EpubReaderActivity.cpp`** -- Moved viewport dimension calculations before the `!section` block. Added `silentIndexNextChapterIfNeeded()` implementation that pre-indexes the next chapter when the penultimate page is displayed.
### New "Indexing Display" Setting (5 files)
- **`src/CrossPointSettings.h`** -- Added `INDEXING_DISPLAY` enum (POPUP, STATUS_TEXT, STATUS_ICON) and `indexingDisplay` field.
- **`src/CrossPointSettings.cpp`** -- Added persistence (write/read) for the new setting at the end of the settings chain.
- **`src/SettingsList.h`** -- Registered the new Enum setting in the Display category.
- **`lib/I18n/I18nKeys.h`** -- Added 4 new string IDs: `STR_INDEXING_DISPLAY`, `STR_INDEXING_POPUP`, `STR_INDEXING_STATUS_TEXT`, `STR_INDEXING_STATUS_ICON`.
- **`lib/I18n/translations/*.yaml`** (8 files) -- Added translations for all 8 languages.
### Status Bar Indicator Implementation
- **`EpubReaderActivity.cpp`** -- Conditional popup logic: only shows popup when setting is POPUP; for STATUS_TEXT/STATUS_ICON, shows centered "Indexing..." text on blank screen during non-silent indexing. For silent pre-indexing, sets `silentIndexingActive` flag before rendering so `renderStatusBar()` draws the indicator (text or 8x8 hourglass icon) in the status bar.
- Defined an 8x8 1-bit hourglass bitmap icon (`kIndexingIcon`) for the icon mode.
## Build Verification
- PlatformIO build: SUCCESS
- RAM: 31.1% (101,820 / 327,680 bytes)
- Flash: 99.6% (6,525,028 / 6,553,600 bytes)
## Follow-Up Items
- Test on device: verify silent indexing triggers on penultimate page, verify all three display modes work correctly.
- The status bar indicator shows optimistically on penultimate pages even if the next chapter is already cached (false positive clears immediately after `loadSectionFile` check).
- Flash usage is at 99.6% -- monitor for future additions.

View File

@@ -1,59 +0,0 @@
# Merge Upstream PRs #965, #939, #852, #972, #971, #977, #975
**Date:** 2026-02-18
**Branch:** mod/merge-upstream-1
## Task
Port 7 upstream PRs from crosspoint-reader/crosspoint-reader into the mod branch.
## Status per PR
| PR | Description | Result |
|---|---|---|
| #939 | Fix dangling pointer in MyLibraryActivity | Already ported, no changes needed |
| #852 | HalPowerManager idle CPU freq scaling | Completed partial port (Lock RAII, WiFi check, skipLoopDelay, render locks) |
| #965 | Fix paragraph formatting inside list items | Fully ported |
| #972 | Micro-optimizations to eliminate value copies | Ported (fontMap move, getDataFromBook const ref) |
| #971 | Remove redundant hasPrintableChars pass | Fully ported |
| #977 | Skip unsupported image formats during parsing | Fully ported |
| #975 | Fix UITheme memory leak on theme reload | Fully ported |
## Changes Made
### PR #852 (partial port completion)
- `lib/hal/HalPowerManager.h` -- Added `LockMode` enum, `currentLockMode`/`modeMutex` members, nested `Lock` RAII class (non-copyable/non-movable), `extern powerManager` declaration
- `lib/hal/HalPowerManager.cpp` -- Added mutex init in `begin()`, WiFi.getMode() check in `setPowerSaving()`, Lock constructor/destructor, LockMode guard
- `src/activities/settings/ClearCacheActivity.h` -- Added `skipLoopDelay()` override
- `src/activities/settings/OtaUpdateActivity.h` -- Added `skipLoopDelay()` override
- `src/activities/Activity.cpp` -- Added `HalPowerManager::Lock` in `renderTaskLoop()`
- `src/activities/ActivityWithSubactivity.cpp` -- Added `HalPowerManager::Lock` in `renderTaskLoop()`
### PR #965
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h` -- Added `listItemUntilDepth` member
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` -- Modified `startElement` to handle `<p>` inside `<li>` without line break; added depth reset in `endElement`
### PR #972
- `lib/GfxRenderer/GfxRenderer.cpp` -- `std::move(font)` in `insertFont`
- `src/RecentBooksStore.h` / `.cpp` -- `getDataFromBook` now takes `const std::string&`
### PR #971
- `lib/EpdFont/EpdFont.h` / `.cpp` -- Removed `hasPrintableChars` method
- `lib/EpdFont/EpdFontFamily.h` / `.cpp` -- Removed `hasPrintableChars` method
- `lib/GfxRenderer/GfxRenderer.cpp` -- Removed 3 early-return guards calling `hasPrintableChars`
### PR #977
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` -- Added `PARSE_BUFFER_SIZE` constant, `isFormatSupported` guard before image extraction, timing instrumentation
### PR #975
- `src/components/UITheme.h` -- Changed `currentTheme` to `std::unique_ptr<const BaseTheme>`
- `src/components/UITheme.cpp` -- Changed allocations to `std::make_unique`
## Build Result
Build succeeded: RAM 31.1%, Flash 99.5%
## Follow-up Items
- PR #972 LyraTheme loop variable change not applicable (our code uses index-based loops)
- Test on device to verify all changes work as expected

View File

@@ -1,49 +0,0 @@
# Sync mod/master with upstream 1.1.0-RC
## Task Description
Integrated 19 missing upstream PRs from `master` (1.1.0-RC) into `mod/master` via phased cherry-picking, preserving all existing mod enhancements.
## Changes Made
### Branch Setup
- Created safety branch `mod/backup-pre-sync` from `mod/master`
- Created integration branch `mod/sync-upstream-1.1.0` (19 cherry-picked commits + 1 cleanup)
### Phase 1: Low-Risk PRs (8 PRs)
- **#646** Ukrainian hyphenation support (resolved conflict: merged with mod's `OMIT_HYPH_*` conditional compilation guards)
- **#732** Lyra screens (resolved 5 conflicts: preserved mod's clock, 3-cover layout, cover rendering; added upstream's drawSubHeader, popup, keyboard/text field methods)
- **#725** Lyra Icons (resolved conflicts: added icon infrastructure, iconForName(), Lyra icon assets; removed stale Lyra3CoversTheme.cpp)
- **#768** Tweak Lyra popup UI (resolved conflicts: preserved mod's epub loading logic, added upstream popup constants)
- **#880** Flash objects listing script (clean)
- **#897** Keyboard font size increase (clean)
- **#927** Translators list update (clean)
- **#935** Missing up/down button labels (resolved conflicts: kept GUI.drawList() over old manual rendering)
### Phase 2: Medium-Risk PRs (8 PRs - all applied cleanly)
- **#783** KOSync repositioning fix
- **#923** Bresenham line drawing
- **#933** Font map lookup performance
- **#944** 4-bit BMP support
- **#952** Skip large CSS files to prevent crashes
- **#963** Word width and space calculation fix
- **#964** Scale cover images up
- **#970** Fix prev-page teleport to end of book
### Phase 3: Large PR
- **#831** Compressed fonts (30.7% flash reduction, 73 files). Resolved 2 conflicts: merged font decompressor init in main.cpp, merged clearFontCache with mod's silent indexing in EpubReaderActivity.
### Phase 4: Overlap Assessment
- **#556** (JPEG/PNG image support): Confirmed mod already has complete coverage. All converter files identical. No action needed.
- **#980** (Basic table support): Confirmed mod's column-aligned table rendering is a strict superset. No action needed.
### Post-Sync
- Removed stale `Lyra3CoversTheme.h` (3-cover support merged into LyraTheme)
- Fixed `UITheme.cpp` to use `LyraTheme` for LYRA_3_COVERS variant
- Updated `open-x4-sdk` submodule to `91e7e2b` (drawImageTransparent support for Lyra Icons)
- Ran clang-format on all source files
- Build verified: RAM 31.4%, Flash 57.0%
## Follow-Up Items
- Merge `mod/sync-upstream-1.1.0` into `mod/master` when ready
- On-device smoke testing (book loading, images, tables, bookmarks, dictionary, sleep screen, clock, home screen)
- Safety branch `mod/backup-pre-sync` available for rollback if needed

View File

@@ -1,33 +0,0 @@
# Port Upstream PRs #997, #1003, #1005, #1010
## Task
Cherry-pick / port four upstream PRs from crosspoint-reader/crosspoint-reader into the mod fork.
## Changes Made
### PR #997 -- Already Ported (no changes)
Glyph null-safety in `getSpaceWidth`/`getTextAdvanceX` was already present via commit `c1b8e53`.
### PR #1010 -- Fix Dangling Pointer
- **src/main.cpp**: `onGoToReader()` now copies the `initialEpubPath` string before calling `exitActivity()`, preventing a dangling reference when the owning activity is destroyed.
### PR #1005 -- Use HalPowerManager for Battery Percentage
- **lib/hal/HalPowerManager.h**: Changed `getBatteryPercentage()` return type from `int` to `uint16_t`.
- **lib/hal/HalPowerManager.cpp**: Same return type change.
- **src/Battery.h**: Emptied contents (was `static BatteryMonitor battery(BAT_GPIO0)`).
- **src/main.cpp**: Removed `#include "Battery.h"`.
- **src/activities/home/HomeActivity.cpp**: Removed `#include "Battery.h"`.
- **src/components/themes/BaseTheme.cpp**: Replaced `Battery.h` include with `HalPowerManager.h`, replaced `battery.readPercentage()` with `powerManager.getBatteryPercentage()` (2 occurrences).
- **src/components/themes/lyra/LyraTheme.cpp**: Same replacements (2 occurrences).
### PR #1003 -- Render Image Placeholders While Waiting for Decode
- **lib/Epub/Epub/blocks/ImageBlock.h/.cpp**: Added `isCached()` method that checks if the `.pxc` cache file exists.
- **lib/Epub/Epub/Page.h/.cpp**: Added `PageImage::isCached()`, `PageImage::renderPlaceholder()`, `Page::renderTextOnly()`, `Page::countUncachedImages()`, `Page::renderImagePlaceholders()`. Added `#include <GfxRenderer.h>` to Page.h.
- **src/activities/reader/EpubReaderActivity.cpp**: Modified `renderContents()` to check for uncached images and display text + placeholder rectangles immediately (Phase 1 with HALF_REFRESH), then proceed with full image decode and display (Phase 2 with fast refresh). Existing mod-specific double FAST_REFRESH logic for anti-aliased image pages is preserved for the cached-image path.
## Build Result
SUCCESS -- RAM: 31.5%, Flash: 70.4%. No linter errors.
## Follow-up Items
- PR #1003 is still open upstream (not merged); may need to rebase if upstream changes before merge.
- The phased rendering for uncached images skips the mod's double FAST_REFRESH technique (relies on Phase 1's HALF_REFRESH instead). If grayscale quality on first-decode image pages is suboptimal, this could be revisited.

View File

@@ -1,33 +0,0 @@
# Port Upstream PR #978: Improve Font Drawing Performance
**Date:** 2026-02-19
**Branch:** `mod/more-upstream-patches-for-1.1.0`
**Commit:** `3a06418`
## Task
Port upstream PR #978 (perf: Improve font drawing performance) which optimizes glyph rendering by 15-23% on device.
## Changes Made
### Cherry-picked from upstream (commit `07d715e`)
- **`lib/GfxRenderer/GfxRenderer.cpp`**: Introduced `TextRotation` enum and `renderCharImpl<TextRotation>` template function that consolidates normal and 90-CW-rotated rendering paths. Hoists the `is2Bit` conditional above pixel loops (eliminating per-pixel branch). Uses `if constexpr` for compile-time rotation path selection. Fixes operator precedence bug in `bmpVal` calculation.
- **`lib/GfxRenderer/GfxRenderer.h`**: Changed `renderChar` signature (`const int* y` -> `int* y`). Moved `getGlyphBitmap` from private to public (needed by the free-function template).
### Mod-specific extension
- Extended `TextRotation` enum with `Rotated90CCW` and refactored `drawTextRotated90CCW` to delegate to the template. This fixed two bugs in the mod's CCW code:
1. Operator precedence bug in `bmpVal`: `3 - (byte >> bit_index) & 0x3` -> `3 - ((byte >> bit_index) & 0x3)`
2. Missing compressed font support: was using raw `bitmap[offset]` instead of `getGlyphBitmap()`
## Recovery of Discarded Changes
During plan-mode dry-run cherry-pick reset, `git checkout -- .` inadvertently discarded unstaged working tree changes in 12 files (from PRs #1005, #1010, #1003). These were recovered by:
- Cherry-picking upstream commits `cabbfcf` (#1005) and `63b2643` (#1010) with conflict resolution (kept mod's `CrossPointSettings.h` include)
- Fetching unmerged PR #1003 branch and manually applying the diff (ImageBlock::isCached, Page placeholder rendering, EpubReaderActivity phased rendering)
- Committed as `18be265`
## Build
PlatformIO build succeeded. RAM: 31.5%, Flash: 70.4%.

View File

@@ -1,37 +0,0 @@
# Port Upstream 1.1.0-RC Fixes
**Date:** 2026-02-19
**Task:** Port 3 commits from upstream crosspoint-reader PR #992 (1.1.0-rc branch) into mod/master.
## Commits Ported
### 1. chore: Bump version to 1.1.0 (402e887) — Skipped
- mod/master already has `version = 1.1.1-rc`, which is ahead of upstream's 1.1.0.
### 2. fix: Crash on unsupported bold/italic glyphs (3e2c518)
- **File:** `lib/GfxRenderer/GfxRenderer.cpp`
- Added null-safety checks to `getSpaceWidth()` and `getTextAdvanceX()`.
- `getSpaceWidth()`: returns 0 if the space glyph is missing in the styled font variant.
- `getTextAdvanceX()`: falls back to `REPLACEMENT_GLYPH` (U+FFFD) if a glyph is missing, and treats as zero-width if the replacement is also unavailable.
- Prevents a RISC-V Load access fault (nullptr dereference) when indexing chapters with characters unsupported by bold/italic font variants.
### 3. fix: Increase PNGdec buffer for wide images (b8e743e)
- **Files:** `platformio.ini`, `lib/Epub/Epub/converters/PngToFramebufferConverter.cpp`
- Bumped `PNG_MAX_BUFFERED_PIXELS` from 6402 to 16416 (supports up to 2048px wide RGBA images).
- Added `bytesPerPixelFromType()` and `requiredPngInternalBufferBytes()` helper functions.
- Added a pre-decode safety check that aborts with a log error if the PNG scanline buffer would overflow PNGdec's internal buffer.
## Verification
- Build (`pio run -e default`) succeeded cleanly with no errors or warnings.
- RAM: 31.5%, Flash: 70.4%.
## 4. CSS-aware image sizing (PR #1002, commit c8ba4fe)
- **Files:** `lib/Epub/Epub/css/CssStyle.h`, `lib/Epub/Epub/css/CssParser.cpp`, `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`
- Added `imageHeight` field to `CssPropertyFlags` and `CssStyle` (our existing `width` field maps to upstream's `imageWidth`).
- Added CSS `height` property parsing into `imageHeight`.
- Added `imageHeight` and `width` to cache serialization; bumped `CSS_CACHE_VERSION` 2->3.
- Replaced viewport-fit-only image scaling in `ChapterHtmlSlimParser` with CSS-aware sizing: resolves CSS height/width (including inline styles), preserves aspect ratio, clamps to viewport, includes divide-by-zero guards.
- `platformio.ini` changes excluded from commit per user request (PNG buffer bump was already committed separately).
## Follow-up
- None required. Changes are straightforward upstream ports.

View File

@@ -1,34 +0,0 @@
# Sleep Cover: Double FAST_REFRESH for Dithered Letterbox
**Date:** 2026-02-19
## Task
Apply the "double FAST_REFRESH" technique (proven for inline EPUB images via PR #556) to sleep cover rendering when dithered letterboxing is enabled. This replaces the corruption-prone HALF_REFRESH with two FAST_REFRESH passes through a white intermediate state.
Also reverted the hash-based block dithering workaround back to standard Bayer dithering for all gray ranges, confirming the root cause was HALF_REFRESH rather than the dithering pattern.
## Changes Made
### `src/activities/boot_sleep/SleepActivity.cpp`
- **Added `USE_SLEEP_DOUBLE_FAST_REFRESH` define** (set to 1): compile-time toggle for easy A/B testing.
- **Removed `bayerCrossesBwBoundary()` and `hashBlockDither()`**: These were the HALF_REFRESH crosstalk workaround (2x2 hash blocks for gray 171-254). Removed entirely since double FAST_REFRESH addresses the root cause.
- **Simplified `drawLetterboxFill()`**: Dithered mode now always uses `quantizeBayerDither()` for all gray ranges -- same algorithm used for non-boundary grays. No more hash/Bayer branching.
- **Modified `renderBitmapSleepScreen()` BW pass**: When dithered letterbox is active and `USE_SLEEP_DOUBLE_FAST_REFRESH` is enabled:
1. Clear screen to white + FAST_REFRESH (establishes clean baseline)
2. Render letterbox fill + cover + FAST_REFRESH (shows actual content)
- **Letterbox fill included in all greyscale passes**: Initially skipped to avoid scan coupling, but re-enabled after testing confirmed the double FAST_REFRESH baseline prevents corruption. This ensures the letterbox color matches the cover edge after the greyscale LUT is applied.
- **Standard HALF_REFRESH path preserved** for letterbox=none, letterbox=solid, or when define is disabled.
## Build
Successfully compiled with `pio run` (0 errors, 0 warnings).
## Testing
Confirmed working on-device -- dithered letterbox renders cleanly without corruption, and letterbox color matches the cover edge including after greyscale layer application.
## Commit
`0fda903` on branch `mod/sleep-screen-tweaks-on-1.1.0-rc-double-fast`

View File

@@ -1,36 +0,0 @@
# Port Upstream PRs #1038, #1045, #1037, #1019
## Task
Manually port 4 open upstream PRs into the `mod/sync-upstream-PRs` branch, pull in Romanian translations from `upstream/master`, and create a MERGED.md tracking document.
## Changes Made
### PR #1038 (partial -- incremental fixes)
- `lib/Epub/Epub/ParsedText.cpp`: Added `.erase()` calls in `layoutAndExtractLines` to remove consumed words after line extraction (fixes redundant early flush bug). Also fixed `wordContinues` flag handling in `hyphenateWordAtIndex` to preserve prefix's attach-to-previous flag.
### PR #1045 (direct string changes)
- Updated `STR_FORGET_BUTTON` in all 9 translation yaml files to shorter labels.
- Pulled `romanian.yaml` from `upstream/master` (merged upstream PR #987).
### PR #1037 (4-file manual port)
- `lib/Utf8/Utf8.h`: Added `utf8IsCombiningMark()` utility function.
- `lib/EpdFont/EpdFont.cpp`: Added combining mark positioning in `getTextBounds`.
- `lib/Epub/Epub/hyphenation/HyphenationCommon.cpp`: Added NFC-like precomposition for common Western European diacritics in `collectCodepoints()`.
- `lib/GfxRenderer/GfxRenderer.cpp`: Added combining mark rendering in `drawText`, `drawTextRotated90CW`, `drawTextRotated90CCW` (mod-only), and `getTextAdvanceX`. Also wrapped cursor advance in `renderCharImpl` to skip combining marks.
### PR #1019 (1-file manual port)
- `src/activities/home/MyLibraryActivity.cpp`: Added `getFileExtension()` helper and updated `drawList` call to show file extensions in the File Browser.
### Tracking
- `mod/prs/MERGED.md`: Created tracking document with details for all 4 PRs.
## Build Verification
`pio run -e mod` succeeded (24.49s, Flash: 57.2%, RAM: 31.4%).
## Follow-up Items
- All 4 upstream PRs are still open; monitor for any review feedback or changes before they merge.
- Romanian yaml is missing mod-specific string translations (using English fallbacks).
- PR #1019 open question: how file extensions look on long filenames.

View File

@@ -1,23 +0,0 @@
# Expandable Selected Row for Long Filenames
## Task
Implement a mod enhancement to PR #1019 (Display file extensions in File Browser). When the selected row's filename is too long, expand that row to 2 lines with character-level text wrapping. The file extension moves to the bottom-right of the expanded area. Non-selected rows retain single-line truncation behavior.
## Changes Made
### `src/components/themes/BaseTheme.cpp`
- Added `wrapTextToLines` static helper in the anonymous namespace: character-level UTF-8-aware text wrapping with "..." truncation on the final line.
- Modified `drawList`: pre-loop expansion detection for the selected item, pagination adjustment (`pageItems - 1`), expanded selection highlight (`2 * rowHeight`), 2-line title rendering via `wrapTextToLines`, extension repositioned to bottom-right of expanded area, `yPos` tracking for subsequent items.
### `src/components/themes/lyra/LyraTheme.cpp`
- Added identical `wrapTextToLines` helper.
- Modified `drawList` with analogous expansion logic, adapted for Lyra-specific styling (rounded-rect selection highlight, icon support, scroll bar-aware content width, preliminary text width computation for expansion check).
### `mod/prs/MERGED.md`
- Updated PR #1019 section to document the mod enhancement, files modified, and design decisions.
## Follow-up Items
- Visual testing on device to verify text positioning and edge cases (very long single-word filenames, last item on page expanding, single-item lists).
- The page-up/page-down navigation in `MyLibraryActivity::loop()` uses the base `pageItems` from `getNumberOfItemsPerPage` which doesn't account for expansion. This causes a minor cosmetic mismatch (page jump size differs by 1 from visual page when expansion is active) but is functionally correct.

View File

@@ -1,29 +0,0 @@
# Fix Text Wrapping and Spacing for Expanded Selected Row
## Task
Improve the expandable selected row feature (from PR #1019 enhancement). Two issues were identified from device testing:
1. Character-level wrapping broke words mid-character (e.g., "Preside / nt"), resulting in unnatural line breaks.
2. Poor vertical spacing -- text lines clustered near the top of the expanded highlight area with large empty space at the bottom.
## Changes Made
### `src/components/themes/BaseTheme.cpp`
- Rewrote `wrapTextToLines` with 3-tier break logic:
1. Preferred delimiters: " -- ", " - ", en-dash, em-dash (breaks at last occurrence to maximize line 1)
2. Word boundaries: last space or hyphen that fits
3. Character-level fallback for long unbroken tokens
- Extracted `truncateWithEllipsis` helper to reduce duplication
- Fixed expanded row rendering: text lines vertically centered in 2x row height area, extension baseline-aligned with last text line
### `src/components/themes/lyra/LyraTheme.cpp`
- Same `wrapTextToLines` rewrite and `truncateWithEllipsis` helper
- Same vertical centering for expanded row text lines
- Icon also vertically centered in expanded area
- Extension baseline-aligned with last text line instead of fixed offset
### `mod/prs/MERGED.md`
- Updated PR #1019 mod enhancement section to reflect the new wrapping strategy and spacing improvements
## Follow-up Items
- Device testing to verify improved wrapping and spacing visually

View File

@@ -1,51 +0,0 @@
# Port Upstream 1.1.0 RC Commits to mod/master
## Task Description
Audited all 11 commits from upstream PR #992 (1.1.0 Release Candidate) and ported the remaining unported/partially-ported ones to `mod/master`.
## Commit Audit Results
### Already Fully Ported (no changes needed)
- **402e887** - Bump version to 1.1.0 (skip, not relevant)
- **3e2c518** (#997) - Glyph null-safety (ported in c1b8e53)
- **b8e743e** (#995) - PNGdec buffer size (ported in c1b8e53)
- **588984e** (#1010) - Dangling pointer (ported in 18be265)
- **87d9d1d** (#978) - Font drawing performance (ported in 3a06418)
- **2cc497c** (#957) - Double FAST_REFRESH (ported in ad282ca)
### Already Effectively Present
- **8db3542** (#1017) - Cover outlines for Lyra themes (mod's LyraTheme.cpp already draws outlines unconditionally; Lyra3CoversTheme.cpp doesn't exist in mod)
### Ported in This Session
#### 1. Aligned #1002 + Ported #1018 (CSS cache invalidation)
**Files:** `CssParser.h`, `CssParser.cpp`, `Epub.cpp`, `ChapterHtmlSlimParser.cpp`
- Added `tryInterpretLength()` (bool return + out-param) to properly skip non-length CSS values like `auto`, `inherit`
- Added `deleteCache()` method to CssParser
- Moved `CSS_CACHE_VERSION` to static class member
- Added stale cache file removal in `loadFromCache()` on version mismatch
- Added "both CSS width and height set" branch in image sizing logic
- Refactored `parseCssFiles()` to early-return when cache exists
- Refactored `load()` to call `loadFromCache()` and invalidate sections on stale cache
#### 2. Ported #1014 (Strip unused CSS rules)
**File:** `CssParser.cpp`
- Added selector filtering in `processRuleBlockWithStyle` to skip `+`, `>`, `[`, `:`, `#`, `~`, `*`, and whitespace selectors
- Fixed `normalized()` trailing whitespace to use `while` loop and also strip `\n`
- Added TODO comments for multi-class selector support
#### 3. Ported #990 (Continue reading card classic theme)
**Files:** `BaseTheme.h`, `BaseTheme.cpp`
- Changed `homeTopPadding` from 20 to 40
- Computed `bookWidth` from cover BMP aspect ratio (clamped to 90% screen width, fallback to half-width)
- Fixed centering: added `rect.x` offset to `bookX`, `boxX`, and `continueBoxX`
- Simplified cover drawing (removed scaling/centering math since bookWidth now matches aspect ratio)
## Build Status
All changes compile successfully (0 errors, 0 warnings in modified files).
## Follow-up Items
- Test on device to verify CSS cache invalidation works correctly (books with stale caches should auto-rebuild)
- Test classic theme continue reading card with various cover aspect ratios
- Test image sizing with EPUBs that specify both CSS width and height on images

View File

@@ -1,27 +0,0 @@
# NTP Sync Clock Feature
## Task
Add a "Sync Clock" action to the Clock settings category that connects to WiFi and performs NTP time synchronization.
## Changes Made
### New Files
- **src/activities/settings/NtpSyncActivity.h** - Activity class extending `ActivityWithSubactivity` with states: WIFI_SELECTION, SYNCING, SUCCESS, FAILED
- **src/activities/settings/NtpSyncActivity.cpp** - Full implementation:
- Launches `WifiSelectionActivity` with auto-connect enabled
- On WiFi connect: blocking `waitForNtpSync(8000ms)`
- Shows synced time on success; auto-dismisses after 5 seconds
- Failed state requires manual Back press
- Clean WiFi teardown in `onExit()`
### Modified Files
- **src/activities/settings/SettingsActivity.h** - Added `SyncClock` to `SettingAction` enum
- **src/activities/settings/SettingsActivity.cpp** - Added include, switch case handler, and action in `rebuildClockActions()`
- **lib/I18n/translations/*.yaml** (all 9 files) - Added `STR_SYNC_CLOCK` and `STR_TIME_SYNCED` string keys
- **lib/I18n/I18nKeys.h, I18nStrings.h, I18nStrings.cpp** - Regenerated from YAML
## Build Result
SUCCESS - RAM: 32.7%, Flash: 70.7%
## Follow-up Items
- Non-English translations use English fallback values; translators can update later

View File

@@ -1,52 +0,0 @@
# Manage Books Feature - Implementation
## Task Description
Implemented the full Manage Books feature plan across 10 commits on the `mod/manage-books` branch. The feature adds Archive, Delete, Delete Cache, and Reindex book management actions, an interactive End of Book menu, and a bugfix for Clear Reading Cache.
## Changes Made (10 commits)
### COMMIT 1: `feat(hal): expose rename() on HalStorage`
- `lib/hal/HalStorage.h` / `.cpp` — Added `rename(path, newPath)` forwarding to SDCardManager for file/directory move operations.
### COMMIT 2: `feat(i18n): add string keys for book management feature`
- `lib/I18n/translations/english.yaml` — Added 15 new string keys (STR_MANAGE_BOOK, STR_ARCHIVE_BOOK, STR_UNARCHIVE_BOOK, etc.)
- `lib/I18n/I18nKeys.h` — Regenerated via gen_i18n.py
### COMMIT 3: `feat: add BookManager utility and RecentBooksStore::clear()`
- **New:** `src/util/BookManager.h` / `.cpp` — Static utility namespace with `archiveBook`, `unarchiveBook`, `deleteBook`, `deleteBookCache`, `reindexBook`, `isArchived`, `getBookCachePath`. Archive mirrors directory structure under `/.archive/` and renames cache dirs to match new path hashes.
- `src/RecentBooksStore.h` / `.cpp` — Added `clear()` method.
### COMMIT 4: `feat: add BookManageMenuActivity popup sub-activity`
- **New:** `src/activities/home/BookManageMenuActivity.h` / `.cpp` — Contextual popup menu with Archive/Unarchive, Delete, Delete Cache Only, Reindex Book. Supports long-press on Reindex for REINDEX_FULL (includes cover/thumb regeneration).
### COMMIT 5: `refactor: change browse activities to ActivityWithSubactivity`
- `HomeActivity`, `MyLibraryActivity`, `RecentBooksActivity` — Changed base class from `Activity` to `ActivityWithSubactivity`. Added `subActivity` guard at top of `loop()`.
### COMMIT 6: `feat: add long-press Confirm for book management in file browser and recents`
- `MyLibraryActivity` / `RecentBooksActivity` — Long-press Confirm on a book opens BookManageMenuActivity. Actions executed via BookManager, file list refreshed afterward.
### COMMIT 7: `feat: add long-press on HomeActivity for book management and archive browsing`
- `HomeActivity` — Long-press Confirm on recent book opens manage menu. Long-press on Browse Files navigates to `/.archive/`.
- `main.cpp` — Wired `onMyLibraryOpenWithPath` callback through to HomeActivity constructor.
### COMMIT 8: `feat: replace Delete Book Cache with Manage Book in reader menu`
- `EpubReaderMenuActivity` — Replaced DELETE_CACHE menu item with MANAGE_BOOK. Opens BookManageMenuActivity as sub-activity. Added ARCHIVE_BOOK, DELETE_BOOK, REINDEX_BOOK, REINDEX_BOOK_FULL action types.
- `EpubReaderActivity` — Handles new actions via BookManager.
### COMMIT 9: `feat: add EndOfBookMenuActivity replacing static end-of-book text`
- **New:** `src/activities/reader/EndOfBookMenuActivity.h` / `.cpp` — Interactive popup with Archive, Delete, Back to Beginning, Close Book, Close Menu options.
- `EpubReaderActivity` / `XtcReaderActivity` — Replaced static "End of Book" text with EndOfBookMenuActivity.
- `TxtReaderActivity` — Added end-of-book detection (advance past last page triggers menu).
### COMMIT 10: `fix: ClearCacheActivity now clears txt_* caches and recents list`
- `ClearCacheActivity.cpp` — Added `txt_*` to directory prefix check. Calls `RECENT_BOOKS.clear()` after cache wipe.
## New Files
- `src/util/BookManager.h` / `.cpp`
- `src/activities/home/BookManageMenuActivity.h` / `.cpp`
- `src/activities/reader/EndOfBookMenuActivity.h` / `.cpp`
## Follow-up Items
- Test on device: verify all menu interactions, archive/unarchive flow, end-of-book menu behavior
- Verify cache rename works correctly across different book formats
- Consider adding translations for new strings in non-English language files

View File

@@ -1,24 +0,0 @@
# Port Upstream PR #1027: ParsedText Word-Width Cache and Hyphenation Early Exit
## Task
Ported upstream PR #1027 (jpirnay) into the mod. The PR reduces `ParsedText::layoutAndExtractLines` CPU time by 59% via two independent optimizations.
## Changes Made
**`lib/Epub/Epub/ParsedText.cpp`** (single file):
1. Added `#include <cstring>` for `memcpy`/`memcmp`
2. Added 128-entry direct-mapped word-width cache in the anonymous namespace (`WordWidthCacheEntry`, FNV-1a hash, `cachedMeasureWordWidth`). 4 KB in BSS, zero heap allocation.
3. Switched `calculateWordWidths` to use `cachedMeasureWordWidth` instead of `measureWordWidth`
4. Added `lineBreakIndices.reserve(totalWordCount / 8 + 1)` in `computeLineBreaks`
5. In `hyphenateWordAtIndex`: added reusable `prefix` string buffer (avoids per-iteration `substr` allocations) and early exit `break` when prefix exceeds available width (ascending byte-offset order means all subsequent candidates will also be too wide)
**`mod/prs/MERGED.md`**: Added PR #1027 entry (TOC link + full section with context, changes, differences, discussion).
## Key Adaptation
The upstream PR targets `std::list`-based code, but our mod already uses `std::vector` (from PR #1038). List-specific optimizations (splice in `extractLine`, `std::next` vs `std::advance`, `continuesVec` pointer sync) were not applicable. Only the algorithmic improvements were ported.
## Follow-up Items
- None. Port is complete.

View File

@@ -1,24 +0,0 @@
# Port PR #1068: Correct hyphenation of URLs
## Task
Port upstream PR [#1068](https://github.com/crosspoint-reader/crosspoint-reader/pull/1068) by Uri-Tauber into the mod fork. The PR was not yet merged upstream, so it was manually patched in.
## Changes made
1. **`lib/Epub/Epub/hyphenation/HyphenationCommon.cpp`**: Added `case '/':` to `isExplicitHyphen` switch, treating `/` as an explicit hyphen delimiter for URL path segments.
2. **`lib/Epub/Epub/hyphenation/Hyphenator.cpp`**: Replaced the single combined filter in `buildExplicitBreakInfos` with a two-stage check:
- First checks `isExplicitHyphen(cp)`
- Then skips repeated separators (e.g., `//` in `http://`, `--`)
- Then applies strict alphabetic-surround rule only for non-URL separators (`cp != '/' && cp != '-'`)
3. **`mod/prs/MERGED.md`**: Added PR #1068 entry with full documentation.
## Mod enhancements over upstream PR
- Included coderabbit's nitpick suggestion (not yet addressed in upstream) to prevent breaks between consecutive identical separators like `//` and `--`.
## Follow-up items
- None. Port is complete.

View File

@@ -1,26 +0,0 @@
# Implement BmpViewer Activity (upstream PR #887)
## Task
Port the BmpViewer feature from upstream crosspoint-reader PR #887 to the mod/master fork, enabling .bmp file viewing from the file browser.
## Changes Made
### New Files
- **src/activities/util/BmpViewerActivity.h** -- Activity subclass declaration with file path, onGoBack callback, and loadFailed state
- **src/activities/util/BmpViewerActivity.cpp** -- BMP loading/rendering implementation: shows loading indicator, opens file via Storage HAL, parses BMP headers, computes centered position with aspect-ratio-preserving layout, renders via `renderer.drawBitmap()`, draws localized back button hint, handles back button input in loop
### Modified Files
- **src/activities/reader/ReaderActivity.h** -- Added `isBmpFile()` and `onGoToBmpViewer()` private declarations
- **src/activities/reader/ReaderActivity.cpp** -- Added BmpViewerActivity include, `isBmpFile()` implementation, `onGoToBmpViewer()` implementation (sets currentBookPath, exits, enters BmpViewerActivity with goToLibrary callback), BMP routing in `onEnter()` before XTC/TXT checks
- **src/activities/home/MyLibraryActivity.cpp** -- Added `.bmp` to the file extension filter in `loadFiles()`
- **src/components/themes/lyra/LyraTheme.cpp** -- Changed `fillRect` to `fillRoundedRect` in `drawButtonHints()` for both FULL and SMALL button sizes to prevent white rectangles overflowing rounded button borders
## Build Result
- PlatformIO build SUCCESS (default env)
- RAM: 32.7% (107,308 / 327,680 bytes)
- Flash: 71.1% (4,657,244 / 6,553,600 bytes)
## Follow-up Items
- Test on hardware with various BMP files (different sizes, bit depths)
- Consider adding image scaling for oversized BMPs (currently `drawBitmap` handles scaling)
- Future enhancements mentioned in upstream PR: next/prev image navigation, "display and sleep" button

View File

@@ -1,27 +0,0 @@
# Port PR #1055: Byte-level framebuffer writes
**Date:** 2026-02-21
**Task:** Port upstream PR #1055 (jpirnay) into the mod, including coderabbit review nitpick.
## Changes made
### `lib/GfxRenderer/GfxRenderer.h`
- Added two private method declarations: `fillPhysicalHSpanByte` (patterned byte-level span writer) and `fillPhysicalHSpan` (solid-fill wrapper).
### `lib/GfxRenderer/GfxRenderer.cpp`
- Added `#include <cstring>` for `memset`.
- Implemented `fillPhysicalHSpanByte`: bounds-clamped byte-level span writer with MSB-first bit packing, partial-byte masking at edges, and `memset` for aligned middle.
- Implemented `fillPhysicalHSpan`: thin wrapper mapping `state` bool to `0x00`/`0xFF` pattern byte.
- **`drawLine`**: Replaced per-pixel loops for axis-aligned lines with orientation-aware `fillPhysicalHSpan` calls (vertical lines in Portrait, horizontal lines in Landscape).
- **`fillRect`**: Replaced per-row `drawLine` loop with orientation-specific byte-level fast paths for all 4 orientations.
- **`fillRectDither`**: Replaced per-pixel `drawPixelDither` loops for DarkGray/LightGray with orientation-aware `fillPhysicalHSpanByte` using pre-computed byte patterns.
- **`fillPolygon`** (coderabbit nitpick): Added `fillPhysicalHSpan` fast path for Landscape orientations in the scanline inner loop.
### `mod/prs/MERGED.md`
- Appended PR #1055 entry with full documentation.
## Differences from upstream
- `fillPolygon` landscape optimization (from coderabbit review nitpick) was applied as a mod enhancement.
## Follow-up items
- None. Build verified successful.

View File

@@ -1,28 +0,0 @@
# README.md Update for Mod Branch
**Date:** 2026-02-21
## Task
Updated `README.md` to distinguish this mod fork from upstream CrossPoint Reader, documenting all mod-exclusive features, updated feature status, upstream PR ports, and upstream compatibility notes.
## Changes Made
**File:** `README.md`
1. **Header/intro** — Renamed to "CrossPoint Reader (Mod)" with a link to the upstream repo and a summary of what the mod provides. Added blockquote linking to upstream.
2. **Motivation** — Added a paragraph explaining the mod's purpose (faster iteration on features and fixes). Fixed "truely" typo.
3. **Features & Usage checklist** — Updated to reflect current mod capabilities:
- Checked: image support (JPEG/PNG), table rendering, bookmarks, dictionary, book management, clock, letterbox fill, placeholder covers, screen rotation (4 orientations), end-of-book menu, silent pre-indexing
- Added sub-items: file extensions, expandable rows
- Still unchecked: user provided fonts, full UTF, EPUB picker with cover art
4. **New section: "What This Mod Adds"** — Organized by category (Reading Enhancements, Home Screen & Navigation, Book Management, Reader Menu, Display & Rendering, Performance). Documents bookmarks, dictionary, tables, end-of-book menu, clock/NTP, adaptive home screen, file browser improvements, archive system, manage book menu, long-press actions, letterbox fill, landscape CCW, silent pre-indexing, placeholder covers, and 5 ported upstream performance PRs.
5. **New section: "Upstream Compatibility"** — Documents what's missing from the mod (BmpViewer, Catalan translations), build differences (font/hyphenation omissions, version string), and links to `mod/prs/MERGED.md`.
6. **Installing** — Replaced web flasher instructions with `pio run -e mod --target upload`. Noted `env:default` alternative.
7. **Development** — Updated clone command to use `-b mod/master`. Added build environments table (`mod` vs `default`). Updated flash command.
8. **Contributing** — Simplified to note this is a personal mod fork with a link to the upstream repo for contributions.
## Follow-up Items
- `USER_GUIDE.md` is out of date and does not document mod features (bookmarks, dictionary, clock, book management, etc.)
- The data caching section in Internals could be expanded to mention mod-specific cache files (`bookmarks.bin`, `book_settings.bin`, `dictionary.cache`, image `.pxc` files)

View File

@@ -1,28 +0,0 @@
# Boot NTP Auto-Sync Feature
## Task
Add a "Auto Sync on Boot" toggle in Clock Settings that silently syncs time via WiFi/NTP during boot, with no user interaction required and graceful failure handling.
## Changes Made
### New files
- `src/util/BootNtpSync.h` / `src/util/BootNtpSync.cpp` -- Background FreeRTOS task that scans WiFi, connects to a saved network, runs NTP sync, then tears down WiFi. Provides `start()`, `cancel()`, and `isRunning()` API.
### Modified files
- `src/CrossPointSettings.h` -- Added `uint8_t autoNtpSync = 0` field
- `src/CrossPointSettings.cpp` -- Added persistence (write/read) for the new field
- `src/SettingsList.h` -- Added Toggle entry under Clock category
- `lib/I18n/translations/*.yaml` (all 9 languages) -- Added `STR_AUTO_NTP_SYNC` string
- `lib/I18n/I18nKeys.h` / `lib/I18n/I18nStrings.cpp` -- Regenerated via `gen_i18n.py`
- `src/main.cpp` -- Calls `BootNtpSync::start()` during setup (non-blocking)
- `src/activities/settings/NtpSyncActivity.cpp` -- Added `BootNtpSync::cancel()` guard
- `src/activities/network/WifiSelectionActivity.cpp` -- Added cancel guard
- `src/activities/settings/OtaUpdateActivity.cpp` -- Added cancel guard
- `src/activities/settings/KOReaderAuthActivity.cpp` -- Added cancel guard
- `src/activities/reader/KOReaderSyncActivity.cpp` -- Added cancel guard
- `src/activities/network/CrossPointWebServerActivity.cpp` -- Added cancel guard
## Follow-up Items
- Translations: all non-English languages currently use English fallback for the new string
- The FreeRTOS task uses 4096 bytes of stack; monitor for stack overflow if WiFi scan behavior changes
- WiFi adds ~50-60KB RAM pressure during the sync window (temporary)

View File

@@ -1,43 +0,0 @@
# Port Upstream PRs #1207 and #1209
## Task
Ported two upstream PRs from crosspoint-reader/crosspoint-reader to the fork:
- **PR #1207**: `fix: use HTTPClient::writeToStream for downloading files from OPDS`
- **PR #1209**: `feat: Support for multiple OPDS servers`
Cherry-picking was not possible due to significant divergences (WiFiClient vs NetworkClient naming, i18n changes, binary vs JSON settings, missing JsonSettingsIO/ObfuscationUtils).
## Changes Made
### PR #1207 (2 files modified)
- `src/network/HttpDownloader.cpp` — Added `FileWriteStream` class, replaced manual chunked download loop with `HTTPClient::writeToStream`, improved Content-Length handling and post-download validation
- `src/activities/browser/OpdsBookBrowserActivity.cpp` — Truncated download status text to fit screen width
### PR #1209 (6 files created, ~15 files modified, 2 files deleted)
**New files:**
- `src/OpdsServerStore.h` / `src/OpdsServerStore.cpp` — Singleton store for up to 8 OPDS servers with MAC-based XOR+base64 password obfuscation and JSON persistence (self-contained — fork lacks JsonSettingsIO/ObfuscationUtils, so persistence was inlined)
- `src/activities/settings/OpdsServerListActivity.h` / `.cpp` — Dual-mode activity: settings list (add/edit/delete) and server picker
- `src/activities/settings/OpdsSettingsActivity.h` / `.cpp` — Individual server editor (name, URL, username, password, delete)
**Modified files:**
- `src/network/HttpDownloader.h` / `.cpp` — Added per-call username/password parameters (default empty), removed global SETTINGS dependency
- `src/activities/browser/OpdsBookBrowserActivity.h` / `.cpp` — Constructor accepts `OpdsServer`, uses server-specific credentials and URL, shows server name in header
- `src/activities/home/HomeActivity.h` / `.cpp``hasOpdsUrl``hasOpdsServers`, uses `OPDS_STORE.hasServers()`
- `src/main.cpp` — Added OPDS_STORE loading on boot, server picker when multiple servers configured
- `src/activities/settings/SettingsActivity.cpp` — Replaced CalibreSettingsActivity with OpdsServerListActivity
- `src/network/CrossPointWebServer.h` / `.cpp` — Added REST endpoints: GET/POST /api/opds, POST /api/opds/delete
- `src/network/html/SettingsPage.html` — Added OPDS server management UI (CSS + JS)
- `src/SettingsList.h` — Removed legacy OPDS entries (opdsServerUrl, opdsUsername, opdsPassword)
- 9 translation YAML files — Added STR_ADD_SERVER, STR_SERVER_NAME, STR_NO_SERVERS, STR_DELETE_SERVER, STR_DELETE_CONFIRM, STR_OPDS_SERVERS
**Deleted files:**
- `src/activities/settings/CalibreSettingsActivity.h` / `.cpp`
## Key Adaptation Decisions
- Kept `WiFiClient`/`WiFiClientSecure` naming (fork hasn't adopted `NetworkClient` rename)
- Inlined JSON persistence and MAC-based obfuscation directly into `OpdsServerStore.cpp` (fork lacks `JsonSettingsIO` and `ObfuscationUtils` libraries)
- Legacy single-server settings auto-migrate to new `opds.json` on first boot
## Build Status
Compilation verified: SUCCESS (RAM: 32.8%, Flash: 71.4%)

View File

@@ -1,33 +0,0 @@
# OPDS Post-Download Prompt Screen
## Task
After an OPDS download completes, instead of immediately returning to the catalog listing, show a prompt screen with two options: "Back to Listing" and "Open Book". A 5-second auto-timer executes the default action. A per-server setting controls which option is default (back to listing by default for backward compatibility).
## Changes Made
### New i18n strings
- `lib/I18n/translations/english.yaml` — Added `STR_DOWNLOAD_COMPLETE`, `STR_OPEN_BOOK`, `STR_BACK_TO_LISTING`, `STR_AFTER_DOWNLOAD`
- Regenerated `lib/I18n/I18nKeys.h`, `lib/I18n/I18nStrings.cpp` via `gen_i18n.py`
### Per-server setting
- `src/OpdsServerStore.h` — Added `afterDownloadAction` field to `OpdsServer` (0 = back to listing, 1 = open book)
- `src/OpdsServerStore.cpp` — Serialized/deserialized `after_download` in JSON
### Browser activity
- `src/activities/browser/OpdsBookBrowserActivity.h` — Added `DOWNLOAD_COMPLETE` state, `onGoToReader` callback, `downloadedFilePath`, `downloadCompleteTime`, `promptSelection` members, `executePromptAction()` method
- `src/activities/browser/OpdsBookBrowserActivity.cpp` — On download success: transition to `DOWNLOAD_COMPLETE` state instead of `BROWSING`. Added loop handling (Left/Right to toggle selection, Confirm to execute, Back to go to listing, 5s auto-timer). Added render for prompt screen with bracketed selection UI. Added `executePromptAction()` helper.
### Callback threading
- `src/main.cpp` — Passed `onGoToReader` callback to `OpdsBookBrowserActivity` constructor in both single-server and multi-server code paths
### Settings UI
- `src/activities/settings/OpdsSettingsActivity.cpp` — Incremented `BASE_ITEMS` from 6 to 7. Added "After Download" field at index 6 (toggles between "Back to Listing" and "Open Book"). Shifted Delete to index 7.
### Countdown refinements (follow-up)
- Added live countdown display using `FAST_REFRESH` -- shows "(5s)", "(4s)", etc., updating each second
- Any button press (Left/Right to change selection, Back, Confirm) cancels the countdown entirely instead of resetting it
- Added `countdownActive` bool and `lastCountdownSecond` int to track state without redundant redraws
## Follow-up Items
- Translations for the 4 new strings in non-English languages
- On-device testing of the full flow (download → prompt → open book / back to listing)

View File

@@ -1,18 +0,0 @@
# OPDS Per-Server Default Save Directory
## Task
Add a configurable default download path per OPDS server. The directory picker now starts at the server's saved path instead of always at root. New servers default to "/".
## Changes Made
### Modified files
- **`src/OpdsServerStore.h`** -- Added `downloadPath` field (default `"/"`) to `OpdsServer` struct.
- **`src/OpdsServerStore.cpp`** -- Serialize/deserialize `download_path` in JSON. Existing servers without the field default to `"/"`.
- **`src/activities/settings/OpdsSettingsActivity.cpp`** -- Added "Download Path" as field index 4 (shifted Delete to 5). Uses `DirectoryPickerActivity` as subactivity for folder selection. Displays current path in row value.
- **`src/activities/util/DirectoryPickerActivity.h`** / **`.cpp`** -- Added `initialPath` constructor parameter (default `"/"`). `onEnter()` validates the path exists on disk and falls back to `"/"` if not.
- **`src/activities/browser/OpdsBookBrowserActivity.cpp`** -- Passes `server.downloadPath` as `initialPath` when launching the directory picker.
- **`lib/I18n/translations/*.yaml`** (all 9 languages) -- Added `STR_DOWNLOAD_PATH` with translations.
- **`lib/I18n/I18nKeys.h`**, **`I18nStrings.h`**, **`I18nStrings.cpp`** -- Regenerated.
## Follow-up Items
- Test on device: verify settings UI shows/saves path, picker opens at saved path, fallback works when directory is removed

View File

@@ -1,46 +0,0 @@
# OPDS Server Reordering
## Task
Add the ability to reorder OPDS servers via a `sortOrder` field, editable on-device and with up/down buttons in the web UI.
## Changes Made
### Data Model (`src/OpdsServerStore.h`)
- Added `int sortOrder = 0` field to `OpdsServer` struct
### Store Logic (`src/OpdsServerStore.cpp`)
- `saveToFile()`: persists `sort_order` to JSON
- `loadFromFile()`: reads `sort_order`, assigns sequential defaults to servers missing it, sorts after load
- `migrateFromSettings()`: assigns `sortOrder = 1` to migrated server
- `addServer()`: auto-assigns `sortOrder = max(existing) + 1` when left at 0
- `updateServer()`: re-sorts after update
- Added `sortServers()` private helper: sorts by sortOrder ascending, ties broken case-insensitively by name (falling back to URL)
- Added `moveServer(index, direction)`: swaps sortOrder with adjacent server, re-sorts, saves
### On-Device UI (`src/activities/settings/OpdsSettingsActivity.cpp`)
- Added "Position" as first menu item (index 0), shifting all others by 1
- Uses `NumericStepperActivity` (new) for position editing: numeric stepper with Up/Down and PageForward/PageBack to increment/decrement
- `saveServer()` now re-locates the server by name+url after sort to keep `serverIndex` valid
### NumericStepperActivity (new: `src/activities/util/NumericStepperActivity.{h,cpp}`)
- Reusable numeric stepper modeled after `SetTimezoneOffsetActivity`
- Displays value with highlight rect and up/down arrow indicators
- Up/PageForward increment, Down/PageBack decrement (clamped to min/max)
- Confirm saves, Back cancels
- Side button hints show +/-
### Web API (`src/network/CrossPointWebServer.{h,cpp}`)
- `GET /api/opds`: now includes `sortOrder` in response
- `POST /api/opds`: preserves `downloadPath` and `sortOrder` on update
- New `POST /api/opds/reorder`: accepts `{index, direction: "up"|"down"}`, calls `moveServer()`
### Web UI (`src/network/html/SettingsPage.html`)
- Added up/down arrow buttons to each OPDS server card (hidden at boundaries)
- Added `reorderOpdsServer()` JS function calling the new API endpoint
### i18n
- Added `STR_POSITION` to all 9 translation YAML files
- Regenerated `I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`
## Follow-up Items
- None identified; build passes cleanly.

View File

@@ -1,35 +0,0 @@
# Port Upstream KOReader Sync PRs
## Task
Port three unmerged upstream PRs into the fork:
- PR #1185: Cache KOReader document hash
- PR #1217: Proper KOReader XPath synchronisation
- PR #1090: Push progress and sleep (with silent failure adaptation)
## Changes Made
### PR #1185 — Cache KOReader Document Hash
- **lib/KOReaderSync/KOReaderDocumentId.h**: Added private static helpers `getCacheFilePath`, `loadCachedHash`, `saveCachedHash`
- **lib/KOReaderSync/KOReaderDocumentId.cpp**: Cache lookup before hash computation, persist after; uses mtime fingerprint + file size for validation
### PR #1217 — Proper KOReader XPath Synchronisation
- **lib/KOReaderSync/ChapterXPathIndexer.h/.cpp**: New files — Expat-based on-demand XHTML parsing for bidirectional XPath/progress mapping
- **lib/KOReaderSync/ProgressMapper.h/.cpp**: XPath-first mapping in both directions, percentage fallback, DocFragment 1-based indexing fix, `std::clamp` sanitization
- **docs/contributing/koreader-sync-xpath-mapping.md**: Design doc
### PR #1090 — Push Progress & Sleep (adapted)
Adapted to fork's `ActivityWithSubactivity` + callback architecture (upstream uses `Activity` + `startActivityForResult`).
- **lib/I18n/translations/english.yaml** + auto-generated **I18nKeys.h**: Added `STR_PUSH_AND_SLEEP`
- **src/activities/reader/EpubReaderMenuActivity.h**: Added `PUSH_AND_SLEEP` to `MenuAction` enum and `buildMenuItems()`
- **src/activities/reader/KOReaderSyncActivity.h/.cpp**: Added `SyncMode::PUSH_ONLY`, `deferFinish()` mechanism, PUSH_ONLY paths in `performSync`/`performUpload`/`loop`
- **src/activities/reader/EpubReaderActivity.h/.cpp**: Added `pendingSleep` flag, `extern void enterDeepSleep()`, `PUSH_AND_SLEEP` case in `onReaderMenuConfirm`
**Silent failure**: Both `onCancel` and `onSyncComplete` callbacks set `pendingSleep = true`, so the device sleeps regardless of sync success/failure. No credentials also triggers sleep directly.
## Build
Compiles cleanly on `default` environment (ESP32-C3). RAM: 32.8%, Flash: 71.7%.
## Follow-up Items
- Changes are unstaged — commit when ready
- Other language YAML files will auto-fallback to English for `STR_PUSH_AND_SLEEP`

View File

@@ -1,50 +0,0 @@
# Fresh Replay: Sync mod/master with upstream/master (continued)
## Task
Continue the "Fresh Replay" synchronization of mod/master with upstream/master (HEAD: 170cc25). This session picked up from Phase 2c (GfxRenderer/theme modifications) and completed through Phase 4 (verification).
## Changes Made
### Phase 2c-e: GfxRenderer, themes, SleepActivity, SettingsActivity, platformio
- Added `drawPixelGray` to GfxRenderer for letterbox fill rendering
- Added `PRERENDER_THUMB_HEIGHTS` to UITheme for placeholder cover generation
- Added `[env:mod]` build environment to platformio.ini
- Implemented sleep screen letterbox fill (solid/dithered) with edge caching in SleepActivity
- Added placeholder cover fallback (PlaceholderCoverGenerator) for XTC/TXT/EPUB sleep screens
- Added Clock settings category to SettingsActivity with timezone, NTP sync, set-time actions
- Replaced CalibreSettingsActivity with OpdsServerListActivity for OPDS server management
- Added DynamicEnum rendering support for settings
- Added long-press book management to RecentBooksActivity
### Phase 3: Re-port unmerged upstream PRs
- **#1055** (byte-level framebuffer writes): fillPhysicalHSpan*, optimized fillRect/drawLine/fillRectDither/fillPolygon
- **#1027** (word-width cache): 128-entry FNV-1a cache, hyphenation early exit (7-9% layout speedup)
- **#1068** (URL hyphenation): Already present in upstream
- **#1019** (file extensions in browser): Already present in upstream
- **#1090/#1185/#1217** (KOReader sync): Binary credential store, document hash caching, ChapterXPathIndexer
- **#1209** (OPDS multi-server): OpdsBookBrowserActivity accepts OpdsServer, directory picker, download-complete prompt
- **#857** (Dictionary): Activities already ported in Phase 1/2
- **#1003** (Placeholder covers): Already integrated in Phase 2
### Fixes
- Added `STR_OFF` i18n string for clock format setting
- Fixed include paths (ActivityResult.h from subdirectories)
- Replaced `Epub::isValidThumbnailBmp` with `Storage.exists()` (method doesn't exist in upstream)
- Replaced `StringUtils::checkFileExtension` with `FsHelpers` equivalents
### Image pipeline decision
- Kept upstream's JPEGDEC implementation — mod's picojpeg was a workaround for the older codebase
- No mod-specific image pipeline changes needed
## Branch Status
- **mod/master-resync**: 6 commits ahead of upstream/master (170cc25)
- **mod/backup-pre-sync-2026-03-07**: Safety snapshot of original mod/master
- 189 files changed, ~114,566 insertions, ~379 deletions vs upstream
## Follow-up Items
- Run full PlatformIO build on hardware to verify compilation
- Run clang-format on all modified files
- Test on device: clock display, sleep screen letterbox fill, dictionary, OPDS browsing
- KOReaderSyncActivity PUSH_ONLY mode (from PR #1090) not yet re-added to activity
- Consider adding `StringUtils.h` if other mod code needs `checkFileExtension`
- Update mod version string

View File

@@ -1,42 +0,0 @@
# KOReaderSyncActivity PUSH_ONLY Mode Re-addition
**Date**: 2026-03-07
**Branch**: `mod/master-resync`
## Task
Re-add the `PUSH_ONLY` sync mode to `KOReaderSyncActivity` that was lost during the upstream resync/ActivityManager migration (originally from PR #1090). This mode allows the reader to silently push local progress to the KOReader sync server and then enter deep sleep — no interactive UI.
## Changes
### `src/activities/reader/KOReaderSyncActivity.h`
- Added `enum class SyncMode { INTERACTIVE, PUSH_ONLY }` to the class
- Added `syncMode` constructor parameter (defaults to `INTERACTIVE`)
- Added `deferFinish(bool success)`, `pendingFinish`, `pendingFinishSuccess` for safe async finish from blocking calls
### `src/activities/reader/KOReaderSyncActivity.cpp`
- Implemented `deferFinish()` — sets a flag that `loop()` picks up to call `setResult()`/`finish()`
- `onEnter()`: In PUSH_ONLY mode, silently finish if no credentials (no error UI)
- `performSync()`: In PUSH_ONLY mode, skip remote fetch entirely and go straight to `performUpload()`
- `performUpload()`: In PUSH_ONLY mode, use `deferFinish()` instead of setting UI state on success/failure
- `loop()`: Check `pendingFinish` first and perform deferred finish if set
### `src/activities/ActivityManager.h`
- Added `requestSleep()` / `isSleepRequested()` — allows activities to signal that the device should enter deep sleep. Checked by the main loop.
### `src/main.cpp`
- Added `activityManager.isSleepRequested()` check in the main loop, before the auto-sleep timeout check
### `src/activities/reader/EpubReaderMenuActivity.h` / `.cpp`
- Added `PUSH_AND_SLEEP` to the `MenuAction` enum
- Added menu item `{PUSH_AND_SLEEP, STR_PUSH_AND_SLEEP}` between SYNC and CLOSE_BOOK
### `src/activities/reader/EpubReaderActivity.cpp`
- Added `#include "activities/ActivityManager.h"`
- Added `PUSH_AND_SLEEP` case in `onReaderMenuConfirm`: launches `KOReaderSyncActivity` in `PUSH_ONLY` mode, then calls `activityManager.requestSleep()` on completion (regardless of success/failure)
### `lib/I18n/translations/english.yaml` / `lib/I18n/I18nKeys.h`
- Added `STR_PUSH_AND_SLEEP: "Push & Sleep"` and regenerated I18n keys
## Follow-up Items
- None

View File

@@ -1,61 +0,0 @@
# Missing Mod Features Audit — Implementation
**Date**: 2026-03-07
**Branch**: `mod/master-resync`
## Task
Comprehensive audit of `mod/master-resync` vs `mod/backup-pre-sync-2026-03-07` identified 4 mod features lost during the upstream resync. All 4 have been re-implemented.
## Changes
### 1. EndOfBookMenuActivity wired into EpubReaderActivity (HIGH)
**Files**: `EpubReaderActivity.h`, `EpubReaderActivity.cpp`
- Added `pendingEndOfBookMenu` and `endOfBookMenuOpened` flags
- In `render()`: when reaching end-of-book, sets `pendingEndOfBookMenu = true` (deferred to avoid render-lock deadlock)
- In `loop()`: checks flag and launches `EndOfBookMenuActivity` via `startActivityForResult`
- Result handler covers all 6 actions: ARCHIVE (→ goHome), DELETE (→ goHome), TABLE_OF_CONTENTS (→ last chapter), BACK_TO_BEGINNING (→ spine 0), CLOSE_BOOK (→ goHome), CLOSE_MENU (→ stay at end)
- Added `#include "EndOfBookMenuActivity.h"` and `#include "util/BookManager.h"`
### 2. Book management from reader menu (MEDIUM)
**Files**: `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `EpubReaderActivity.cpp`
- Added `ARCHIVE_BOOK`, `DELETE_BOOK`, `REINDEX_BOOK` to `MenuAction` enum
- Added corresponding menu items between CLOSE_BOOK and DELETE_CACHE
- Added handlers in `onReaderMenuConfirm`: each calls `BookManager::archiveBook/deleteBook/reindexBook` then `activityManager.goHome()`
### 3. Silent next-chapter pre-indexing (MEDIUM)
**Files**: `EpubReaderActivity.h`, `EpubReaderActivity.cpp`
- Added `preIndexedNextSpine` field and `silentIndexNextChapterIfNeeded()` method
- Triggers when user is on second-to-last or last page of a chapter
- Creates section file for `currentSpineIndex + 1` in advance
- Called after every page turn in `loop()`
- ~35 lines of self-contained implementation
### 4. Letterbox fill toggle in reader menu (LOW)
**Files**: `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `EpubReaderActivity.cpp`
- Added `LETTERBOX_FILL` to `MenuAction` enum
- Added `bookCachePath` constructor parameter (with default `""` for backward compat)
- Added per-book `pendingLetterboxFill`, `letterboxFillLabels`, `letterboxFillToIndex()`, `indexToLetterboxFill()`, `saveLetterboxFill()`
- Cycles Default → Dithered → Solid → None → Default on Confirm
- Renders current value on right edge of menu item
- Loads/saves per-book setting via `BookSettings`
- Updated call site in `EpubReaderActivity` to pass `epub->getCachePath()`
## Audit False Positives (confirmed NOT gaps)
- GfxRenderer kerning/ligatures/wrappedText — present on resync
- HttpDownloader auth fallback — present with OPDS settings fallback
- Lyra3CoversTheme — exists on resync
- ActivityWithSubactivity → Activity migration — intentional upstream change
- EndOfBookMenuActivity callbacks → setResult/finish — correctly migrated
## Follow-up Items
- None

View File

@@ -1,23 +0,0 @@
# Fix mod build environment compilation errors
## Task
Fix compilation errors in the `mod` PlatformIO build environment after the upstream resync. The `default` environment was also verified.
## Changes Made
### Include path fixes (11 files)
- `src/activities/{reader,settings,util}/*.cpp`: Changed bare `#include "ActivityResult.h"` to `#include "activities/ActivityResult.h"` (10 files)
- `src/activities/reader/DictionarySuggestionsActivity.cpp`: Changed `#include "RenderLock.h"` to `#include "activities/RenderLock.h"`
### API compatibility fixes
- `src/util/Dictionary.h`: Replaced invalid `class FsFile;` forward declaration with `#include <HalStorage.h>` (`FsFile` is now a `using` alias)
- `lib/Epub/Epub/blocks/TextBlock.h`: Added `getWordXpos()` public accessor
- `lib/GfxRenderer/GfxRenderer.{h,cpp}`: Re-added `drawTextRotated90CCW()` with `Rotated90CCW` enum value and coordinate mapping, adapted to new fixed-point rendering
- `src/activities/reader/{DictionarySuggestionsActivity,DictionaryWordSelectActivity,LookedUpWordsActivity}.cpp`: Fixed `setResult()` rvalue ref binding (6 lambdas)
- `src/activities/reader/EpubReaderActivity.cpp`: Fixed `std::max(uint8_t, int)` type mismatch
- `src/util/StringUtils.{h,cpp}`: Re-added `checkFileExtension()` and `sortFileList()` functions
- `src/RecentBooksStore.{h,cpp}`: Added missing `removeBook()` method
## Follow-up
- Both `mod` and `default` environments build successfully at 95.2% flash usage
- No functional testing performed yet (on-device verification needed)

View File

@@ -1,42 +0,0 @@
# Restore Lost Mod Features Post-Upstream-Sync
## Task
Implemented the full plan to restore 8 mod-specific features lost during the upstream sync, organized into 5 phased commits.
## Changes Made
### Phase 0 — Commit Pending Changes (`4627ec9`)
- Staged and committed 22 modified files from previous sessions (ADC fix, logging guard, include path fixes, etc.) as a clean baseline.
### Phase 1 — UI Rendering Fixes (`f44657a`)
- **Clock display**: Added clock rendering to `BaseTheme::drawHeader()` and `LyraTheme::drawHeader()` supporting OFF/AM-PM/24H formats and Small/Medium/Large sizes.
- **Placeholder covers**: Fixed `splitWords()` in `PlaceholderCoverGenerator.cpp` to treat `\n`, `\r`, `\t` as whitespace (not just spaces). Removed `drawBorder()` call since the UI already draws frames around book cards.
### Phase 2 — Reader Menu Restructure (`0493f30`)
- **Manage Book submenu**: Replaced 4 individual top-level menu items (Archive, Delete, Reindex, Delete Cache) with single "Manage Book" entry that launches `BookManageMenuActivity`.
- **Dictionary submenu**: Created new `DictionaryMenuActivity` and replaced 3 scattered dictionary items with single "Dictionary" entry.
- **Long-press TOC**: Added 700ms long-press detection on Confirm button to open Table of Contents directly, bypassing the menu.
- Added `STR_DICTIONARY` i18n key and regenerated I18nKeys.h/I18nStrings.h.
### Phase 3 — Settings and Indexing (`22c1892`)
- **Letterbox Fill position**: Moved from bottom of settings list to immediately after Sleep Screen Cover Filter.
- **Indexing Display setting**: Added to Display section with Popup/Status Bar Text/Status Bar Icon modes.
- **Silent indexing**: Restored proactive next-chapter indexing logic and status bar indicator (text or hourglass icon) in `EpubReaderActivity`.
### Phase 4 — Book Preparation (`a5ca15d`)
- **Prerender on first open**: Restored the cover/thumbnail prerender block in `EpubReaderActivity::onEnter()` with "Preparing book..." popup and progress bar.
- Added `isValidThumbnailBmp()`, `generateInvalidFormatCoverBmp()`, and `generateInvalidFormatThumbBmp()` methods to `Epub` class.
## Files Modified
- `src/components/themes/BaseTheme.cpp`, `src/components/themes/lyra/LyraTheme.cpp` — clock display
- `lib/PlaceholderCover/PlaceholderCoverGenerator.cpp` — whitespace splitting, border removal
- `src/activities/reader/EpubReaderMenuActivity.h/.cpp` — menu restructure
- `src/activities/reader/DictionaryMenuActivity.h/.cpp` — new submenu (created)
- `src/activities/reader/EpubReaderActivity.h/.cpp` — long-press TOC, MANAGE_BOOK/DICTIONARY handlers, silent indexing, book prerender
- `src/SettingsList.h` — settings reordering, indexing display entry
- `lib/Epub/Epub.h`, `lib/Epub/Epub.cpp` — BMP validation and fallback generation methods
- `lib/I18n/translations/english.yaml`, `lib/I18n/I18nKeys.h`, `lib/I18n/I18nStrings.h` — STR_DICTIONARY key
## Follow-up Items
- On-device verification of all 8 features by user
- Optional: verify `pio run -e default` still builds cleanly

View File

@@ -1,38 +0,0 @@
# Fix Reader Bugs: Covers, Indexing, TOC, Cache Deletion
**Date:** 2026-03-08
**Commit:** `022f519` on `mod/master-resync`
## Task Description
Fixed four bugs reported after the upstream sync feature restoration:
1. **Placeholder cover text rendering** — only first letter of each word visible
2. **Silent indexing indicator** — wrong timing (shown on first page, not during actual indexing) and icon positioned above the status bar
3. **Long-press confirm for TOC** — immediately selected the first chapter upon button release
4. **Book cache deletion from home screen** — showed "no open books" and required double confirm press
## Changes Made
### lib/PlaceholderCover/PlaceholderCoverGenerator.cpp
- Added `#include <EpdFontData.h>` for `fp4::toPixel()`
- Fixed `renderGlyph()`: `glyph->advanceX` is 12.4 fixed-point, not pixels — was advancing cursor ~16x too far, causing only the first character of each word to be visible
- Fixed `getCharAdvance()`: same fixed-point conversion needed for space width calculation in word wrapping
### src/activities/reader/EpubReaderActivity.cpp
- Removed `silentIndexNextChapterIfNeeded()` call from `loop()` (line 385) — this blocked the UI before render, preventing the indicator from ever showing. The backup branch only called it from `render()`, after the page was drawn.
- Fixed indexing icon Y position: changed `textY - kIndexingIconSize + 2` to `textY + 2` to align within the status bar alongside battery/text
- Passed `consumeFirstRelease` flag when creating chapter selection activity from long-press path
### src/activities/reader/EpubReaderChapterSelectionActivity.h/.cpp
- Added `ignoreNextConfirmRelease` member and `consumeFirstRelease` constructor parameter
- In `loop()`, consumes the first Confirm release when opened via long-press, preventing immediate selection of the first TOC item
### src/activities/home/HomeActivity.cpp
- In `openManageMenu()` callback: reset `ignoreNextConfirmRelease = false` to fix double-press bug
- Replaced `recentBooks.clear()` with `loadRecentBooks()` to reload remaining books from persistent store after deletion
## Follow-up Items
- Test all four fixes on device to verify correct behavior
- Verify placeholder covers render full title/author text at both thumbnail and full-cover sizes

View File

@@ -1,62 +0,0 @@
# Port 5 Upstream PRs (#1329, #1143, #1172, #1320, #1325)
**Date:** 2026-03-08
**Task:** Port unmerged upstream PRs to the mod across 4 feature branches
## Summary
Ported 5 upstream PRs from `crosspoint-reader/crosspoint-reader` to the mod codebase, organized across 4 phased feature branches:
### Phase 1: PR #1329 — ReaderUtils refactor
**Branch:** `port/1329-reader-utils`
- Created `src/activities/reader/ReaderUtils.h` with shared reader utilities: `GO_HOME_MS`, `applyOrientation()`, `detectPageTurn()`, `displayWithRefreshCycle()`, `renderAntiAliased()`
- Refactored `EpubReaderActivity.cpp` and `TxtReaderActivity.cpp` to use ReaderUtils
- Applied CodeRabbit's `storeBwBuffer()` null-check suggestion
### Phase 2: PRs #1143 + #1172 — TOC fragment navigation + multi-spine TOC
**Branch:** `port/1143-1172-toc-navigation`
- Extended `Section.h/.cpp` with TOC boundary tracking (`tocBoundaries`, `buildTocBoundaries()`, page lookup methods)
- Added TOC anchor page breaks to `ChapterHtmlSlimParser` (chapters start on fresh pages)
- Added TOC-aware navigation to `EpubReaderActivity` (long-press walks TOC entries, status bar shows subchapter title)
- Updated `EpubReaderChapterSelectionActivity` to pass and accept `tocIndex`
- Added multi-spine chapter caching (`cacheMultiSpineChapter()`)
- Incremented `SECTION_FILE_VERSION` from 18 to 19
- Preserved mod's footnote support, image rendering options, and Activity base class
### Phase 3: PR #1320 — JPEG resource cleanup
**Branch:** `port/1320-jpeg-cleanup`
- Added `ScopedCleanup` RAII struct to `JpegToBmpConverter.cpp`
- Removed scattered `free()`/`delete` calls
- Changed `rowCount` from `uint16_t*` to `uint32_t*` to prevent overflow
### Phase 4: PR #1325 — Settings tab label
**Branch:** `port/1325-settings-label`
- Dynamic confirm label in `SettingsActivity.cpp` shows next category name when tab bar is focused
## Files changed
| File | Phase | Change type |
|------|-------|-------------|
| `src/activities/reader/ReaderUtils.h` | 1 | New file |
| `src/activities/reader/EpubReaderActivity.cpp` | 1, 2 | Refactored |
| `src/activities/reader/EpubReaderActivity.h` | 2 | Extended |
| `src/activities/reader/TxtReaderActivity.cpp` | 1 | Refactored |
| `lib/Epub/Epub/Section.h` | 2 | Extended |
| `lib/Epub/Epub/Section.cpp` | 2 | Extended |
| `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h` | 2 | Extended |
| `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` | 2 | Extended |
| `src/activities/ActivityResult.h` | 2 | Extended |
| `src/activities/reader/EpubReaderChapterSelectionActivity.h` | 2 | Extended |
| `src/activities/reader/EpubReaderChapterSelectionActivity.cpp` | 2 | Extended |
| `lib/JpegToBmpConverter/JpegToBmpConverter.cpp` | 3 | Refactored |
| `src/activities/settings/SettingsActivity.cpp` | 4 | Modified |
| `mod/prs/MERGED.md` | Housekeeping | Updated |
| `mod/docs/upstream-sync.md` | Housekeeping | Updated |
## Follow-up items
- All 4 branches need hardware testing before merging to `mod/master`
- Test TOC navigation with multi-chapter spine EPUBs (short story collections, academic texts)
- Test JPEG resource cleanup with large image-heavy EPUBs
- Verify `SECTION_FILE_VERSION` bump invalidates old caches properly (delete `.crosspoint/` on SD card)
- When upstream merges these PRs, these ports should be dropped during the next sync

View File

@@ -1,31 +0,0 @@
# Fix Three Reader Bugs on mod/master
**Date**: 2026-03-08
**Branch**: mod/master
**Commit**: 422cad7
## Task
Fixed three bugs reported after merging the upstream PR ports:
## Changes
### Bug 1: Confirm button ignored after TOC navigation
- **File**: `src/activities/reader/EpubReaderActivity.cpp`
- **Root cause**: `ignoreNextConfirmRelease` was set to `true` on long-press but never cleared after being transferred to the child `EpubReaderChapterSelectionActivity`. The parent's flag remained `true`, causing the next Confirm press to be silently consumed.
- **Fix**: Added `ignoreNextConfirmRelease = false;` immediately after capturing the value into `consumeRelease`.
### Bug 2: Footnotes inaccessible from reader menu
- **File**: `src/activities/reader/EpubReaderMenuActivity.cpp`
- **Root cause**: `buildMenuItems()` accepted a `hasFootnotes` parameter but never used it to add the `MenuAction::FOOTNOTES` entry.
- **Fix**: Added conditional `FOOTNOTES` menu item after DICTIONARY when `hasFootnotes` is true.
### Bug 3: Phantom screen re-render at idle
- **File**: `src/main.cpp`
- **Root cause**: The clock update logic in `loop()` called `activityManager.requestUpdate()` every minute when the displayed minute changed. Reader activities don't show the clock, but still received full page re-renders, causing visible e-ink flashes while idle.
- **Fix**: Guarded the clock block with `!activityManager.isReaderActivity()`.
## Follow-up
- Hardware testing needed for all three fixes
- Bug 3 fix means the clock won't update while reading; it resumes updating when returning to non-reader activities

View File

@@ -1,28 +0,0 @@
# Restore Preferred Orientation Settings and Long-Press Sub-Menu
**Date**: 2026-03-08
**Branch**: mod/master
**Commit**: 0d8a3fd
## Task
Restore two orientation preference features lost during the upstream PR resync:
1. Settings UI entries for preferred portrait/landscape modes
2. Long-press sub-menu on the reader menu's orientation toggle
## Changes
### Settings UI and Persistence
- **`src/SettingsList.h`**: Added `DynamicEnum` entries for `preferredPortrait` (Portrait/Inverted) and `preferredLandscape` (Landscape CW/Landscape CCW) in the Reader settings category. Uses getter/setter lambdas to map between sequential indices and non-sequential orientation enum values.
- **`src/JsonSettingsIO.cpp`**: Added manual JSON save/load for both fields with validation (rejects invalid orientation values, falls back to defaults).
### Long-Press Orientation Sub-Menu
- **`src/activities/reader/EpubReaderMenuActivity.h`**: Added `orientationSubMenuOpen`, `orientationSubMenuIndex`, and `ignoreNextConfirmRelease` state flags.
- **`src/activities/reader/EpubReaderMenuActivity.cpp`**:
- `loop()`: Long-press (700ms) on Confirm when the orientation item is selected opens the sub-menu. Sub-menu handles its own Up/Down/Confirm/Back input. Added `ignoreNextConfirmRelease` guard to prevent the long-press release from immediately selecting.
- `render()`: When sub-menu is open, renders a centered list of all 4 orientations with the current one marked with `*`. Uses the same gutter/hint layout as the main menu.
## Follow-up
- Hardware testing needed for both features
- Translations for `STR_PREFERRED_PORTRAIT` and `STR_PREFERRED_LANDSCAPE` only exist in English; other languages fall back automatically

View File

@@ -1,43 +0,0 @@
# Port Upstream PRs #1311, #1322 + Verify #1329
**Date:** 2026-03-08
**Task:** Port two upstream PRs and verify alignment of a previously-ported PR that was recently merged.
## Changes Made
### PR #1311 -- Fix inter-word spacing rounding error (UNMERGED, ported as mod feature)
Replaced `getSpaceKernAdjust()` with `getSpaceAdvance()` which combines space glyph advance and flanking kern values into a single fixed-point sum before pixel snapping, fixing +/-1 px rounding drift in inter-word spacing.
**Files modified:**
- `lib/GfxRenderer/GfxRenderer.h` -- replaced declaration
- `lib/GfxRenderer/GfxRenderer.cpp` -- replaced implementation (single-snap pattern)
- `lib/Epub/Epub/ParsedText.h` -- removed `spaceWidth` parameter from 3 internal functions
- `lib/Epub/Epub/ParsedText.cpp` -- updated all 4 call sites to use `getSpaceAdvance()`
### PR #1322 -- Early exit on fillUncompressedSizes (MERGED, ported for immediate use)
Added `targetCount` variable and early `break` when all ZIP central-directory targets are matched.
**Files modified:**
- `lib/ZipFile/ZipFile.cpp` -- 5-line addition
### PR #1329 -- Reader utils refactor (MERGED, verification only)
Confirmed our existing port matches the upstream merged version (commit `cd508d2`) line-for-line. No code changes needed.
### Tracking documentation updated
- `mod/docs/upstream-sync.md` -- added #1311, #1322; updated #1329 status to MERGED
- `mod/prs/MERGED.md` -- added detailed entries for #1311 and #1322; updated #1329 author and status
## Build Result
SUCCESS -- zero compiler errors/warnings from our changes. Only pre-existing i18n translation warnings.
## Follow-up Items
- #1311: Will be dropped during next sync if/when merged upstream
- #1322: Will be dropped during next sync (already merged upstream)
- #1329: Will be dropped during next sync (already merged upstream)
- Hardware testing recommended: verify text layout rendering after spacing fix (#1311)

View File

@@ -1,52 +0,0 @@
# Port PR #1342: Book Info, Metadata, Serialization Safety
**Date:** 2026-03-08
**Task:** Port upstream PR #1342 with mod-specific adaptations (ManageBook menu in file browser, confirmation guards on all destructive actions)
## Changes Made
### Part A: EPUB Metadata Expansion (4 files)
- `lib/Epub/Epub/parsers/ContentOpfParser.h/.cpp`: Added `series`, `seriesIndex`, `description` fields and parser states. Parses `dc:description`, `calibre:series/calibre:series_index` (OPF2), and EPUB3 `belongs-to-collection/group-position`. Added `stripHtml()` and `trim()` helpers.
- `lib/Epub/Epub/BookMetadataCache.h/.cpp`: Added 3 new string fields to `BookMetadata`. Bumped `BOOK_CACHE_VERSION` 5→6. Updated binary serialization.
- `lib/Epub/Epub.h/.cpp`: Added `getSeries()`, `getSeriesIndex()`, `getDescription()` getters. Propagated new fields from parser.
### Part B: Serialization Safety (10 files, 37 call sites)
- `lib/Serialization/Serialization.h`: `readString` returns `bool` with `MAX_STRING_LENGTH=4096` guard.
- Updated all call sites in: `ContentOpfParser.cpp`, `BookMetadataCache.cpp`, `ImageBlock.cpp`, `TextBlock.cpp`, `Section.cpp`, `CrossPointSettings.cpp`, `CrossPointState.cpp`, `RecentBooksStore.cpp`, `WifiCredentialStore.cpp`, `KOReaderCredentialStore.cpp`.
### Part C: RecentBooksStore Series Field (5 files)
- `src/RecentBooksStore.h/.cpp`: Added `series` field to `RecentBook`. Updated `addBook()`/`updateBook()` signatures.
- `src/JsonSettingsIO.cpp`: Added `series` to JSON serialization.
- Updated call sites in `EpubReaderActivity.cpp`, `TxtReaderActivity.cpp`, `XtcReaderActivity.cpp`, `HomeActivity.cpp`.
### Part D: BookInfoActivity (2 new files)
- `src/activities/home/BookInfoActivity.h/.cpp`: ActivityManager-compliant info screen showing title, author, series, language, file size, description. Scrollable content, synchronous metadata loading.
### Part E: FileBrowser Controls (2 files)
- `src/activities/home/FileBrowserActivity.h/.cpp`: Replaced long-press Confirm=delete with Left=ManageBook menu, Right=BookInfo. Added contextual button hints. Added `handleManageResult()` with confirmation guards.
### Part F: ConfirmationActivity Input Gating (2 files)
- `src/activities/util/ConfirmationActivity.h/.cpp`: Added `inputArmed` mechanism preventing accidental confirm from the press that opened the dialog.
### Part G: Confirmation Guards (3 files, 10 sites)
- `src/activities/home/HomeActivity.h/.cpp`: Extracted `executeManageAction()`, wrapped DELETE/ARCHIVE in ConfirmationActivity chain.
- `src/activities/home/RecentBooksActivity.h/.cpp`: Same pattern as HomeActivity.
- `src/activities/reader/EpubReaderActivity.cpp`: Wrapped all 6 DELETE/ARCHIVE sites (EndOfBookMenu, ReaderMenu, ManageBook) in ConfirmationActivity chains.
### Part H: i18n + Theme (2 files)
- `lib/I18n/translations/english.yaml`: Added `STR_BOOK_INFO`, `STR_AUTHOR`, `STR_SERIES`, `STR_FILE_SIZE`, `STR_DESCRIPTION`, `STR_MANAGE`, `STR_INFO`.
- `src/activities/home/RecentBooksActivity.cpp`: Updated list subtitle to show "Author • Series" when available.
### Documentation
- `mod/prs/MERGED.md`: Added PR #1342 entry with full diff analysis.
## Build Verification
- `pio run`: SUCCESS (0 errors, 0 warnings)
- RAM: 30.3% (99,428 / 327,680 bytes)
- Flash: 95.6% (6,266,793 / 6,553,600 bytes)
- `clang-format`: Applied to all source files
## Follow-up Items
- Hardware testing needed: all 4 orientations, Book Info screen, ManageBook menu from file browser, confirmation dialogs
- Series rendering in Home screen "Continue Reading" card (deferred — data flows through but no visual change yet)
- Delete `.crosspoint/` on device to force cache regeneration (version bump 5→6)

View File

@@ -1,54 +0,0 @@
# Parse and Display All Available EPUB Metadata Fields + Cleanup Refactor
## Task
1. Extend the OPF parser, metadata cache, Epub accessors, i18n, and BookInfo screen to parse and display all standard Dublin Core metadata fields plus Calibre rating.
2. Refactor for code quality: consolidate duplicate static blank strings, add `getMetadata()` accessor, and simplify `buildLayout` to accept `BookMetadata` struct.
## Changes Made
### 1. ContentOpfParser (`lib/Epub/Epub/parsers/ContentOpfParser.h/.cpp`)
- Added 6 new `ParserState` entries: `IN_BOOK_PUBLISHER`, `IN_BOOK_DATE`, `IN_BOOK_SUBJECT`, `IN_BOOK_RIGHTS`, `IN_BOOK_CONTRIBUTOR`, `IN_BOOK_IDENTIFIER`
- Added 7 new public string members: `publisher`, `date`, `subjects`, `rights`, `contributor`, `identifier`, `rating`
- Added `identifierIsIsbn` flag for preferring ISBN identifiers over generic ones
- `startElement`: handles `dc:publisher`, `dc:date`, `dc:subject` (multi-tag, comma-separated), `dc:rights`, `dc:contributor` (multi-tag, comma-separated), `dc:identifier` (prefers `opf:scheme="ISBN"`), and `calibre:rating` meta tag
- `characterData`: appends text for all new states
- `endElement`: transitions back to `IN_METADATA` for all new `dc:*` elements
### 2. BookMetadataCache (`lib/Epub/Epub/BookMetadataCache.h/.cpp`)
- Added 7 new fields to `BookMetadata` struct
- Bumped `BOOK_CACHE_VERSION` from 6 to 7
- Updated `metadataSize` calculation (8 → 15 string fields)
- Added `writeString`/`readString` calls for all new fields in serialization/deserialization
### 3. Epub Accessors (`lib/Epub/Epub.h/.cpp`)
- Added 7 new accessor methods: `getPublisher()`, `getDate()`, `getSubjects()`, `getRights()`, `getContributor()`, `getIdentifier()`, `getRating()`
- Added `getMetadata()` returning full `BookMetadataCache::BookMetadata` const ref
- Consolidated 13 duplicate `static std::string blank` locals into single file-scope `kBlank` (saves ~384 bytes DRAM)
- Propagated new fields from `opfParser` to `bookMetadata` in `parseContentOpf()`
### 4. I18n (`lib/I18n/translations/english.yaml`, `lib/I18n/I18nKeys.h`)
- Added 7 new translation keys: `STR_PUBLISHER`, `STR_DATE`, `STR_SUBJECTS`, `STR_RATING`, `STR_ISBN`, `STR_RIGHTS`, `STR_CONTRIBUTOR`
- Regenerated I18n headers
### 5. BookInfoActivity (`src/activities/home/BookInfoActivity.h/.cpp`)
- Refactored `buildLayout()` from 14 individual parameters to single `BookMetadataCache::BookMetadata` struct + `fileSize`
- `onEnter()` EPUB path uses `epub.getMetadata()` directly; XTC path builds a local `BookMetadata`
- Display order: Title, Author, Series, Publisher, Date, Subjects, Rating (N/5), Language, ISBN, Contributor, File Size, Rights, Description
- Rating displayed as `N / 5` (Calibre stores 0-10, divided by 2)
### 6. BookInfoActivity UI Integration (`src/activities/home/BookInfoActivity.cpp`)
- Added `GUI.drawHeader()` call to show the standard header bar (clock, battery, "Book Info" title)
- Replaced hardcoded `MARGIN = 20` with theme metrics (`contentSidePadding`, `topPadding`, `headerHeight`, etc.)
- Content now starts below the header bar using `metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing`
- Y-culling stops content above button hints using `pageH - metrics.buttonHintsHeight - metrics.verticalSpacing`
- `contentHeight` includes bottom padding for button hints so scrolling accounts for the reserved hint area
## Commits
- `8025e6f``feat: parse and display all available EPUB metadata fields`
- `efa727e``refactor: consolidate Epub blank strings, simplify BookInfo buildLayout`
- (pending) — `fix: add header bar and fix bottom spacing in BookInfo`
## Follow-up
- Existing book caches will auto-invalidate (version 6 → 7) and regenerate on next load
- Users must delete `.crosspoint/` or let it regenerate to see new metadata for previously cached books
- Hardware testing needed to verify rendering of all new fields in all orientations

View File

@@ -1,38 +0,0 @@
# BookInfoActivity: Performance Fix and Cover Image
## Task
Fix BookInfoActivity sluggishness (slow open, unresponsive scrolling) and add book cover display.
## Root Cause (from device debug log)
1. **No Y-culling in render()**: All text lines drawn even when off-screen. Content extending ~1162px on 800px screen caused hundreds of thousands of `LOG_ERR("Outside range")` calls per frame, each doing serial I/O. Render times: 7-13 seconds per frame.
2. **Description text contained literal newlines**: `stripHtml()` and `trim()` in ContentOpfParser don't replace interior `\n` characters. These got passed to `drawText()`, triggering "No glyph for codepoint 10" errors.
3. **`wrappedText()` recomputed every frame**: Original render called it for every field on every scroll -- now pre-computed once.
4. **No cover image**: Activity never loaded or displayed any cover.
## Changes Made
### Committed first: PR #1342 port (commit 4cf395a)
- Staged and committed all prior working state before making further changes
### BookInfoActivity refactor (2 files)
**`src/activities/home/BookInfoActivity.h`**:
- Replaced individual metadata string members with `InfoField` struct + `std::vector<InfoField> fields`
- Added `coverBmpPath`, `coverDisplayHeight`, `coverDisplayWidth` members
- Added `buildLayout()` method for pre-computation
**`src/activities/home/BookInfoActivity.cpp`**:
- **Y-culling**: `render()` skips draw calls for items entirely above or below the visible screen area (`y + height > 0 && y < pageH`); breaks out of field loop when `y >= pageH`
- **Newline normalization**: Added `normalizeWhitespace()` helper that collapses `\n`, `\r`, `\t` sequences into single spaces; applied to description text before word-wrapping
- **Cover height clamping**: `drawBitmap1Bit` maxHeight capped to `std::min(coverDisplayHeight, pageH - y)` to prevent drawing beyond screen
- **Pre-computed layout**: All `wrappedText()` calls moved to `onEnter()` via `buildLayout()`; `render()` only iterates pre-computed lines
- Cover thumbnail generated via `epub.generateThumbBmp()` / `xtc.generateThumbBmp()`; fallback to `PlaceholderCoverGenerator`
- Cover rendered centered at top using `renderer.drawBitmap1Bit()`
## Build Verification
- `pio run` SUCCESS (19s incremental, RAM 30.3%, Flash 95.7%)
## Follow-up Items
- Hardware test: verify render times dropped from 7-13s to <100ms with Y-culling
- Hardware test: verify cover image renders correctly
- Hardware test: verify scroll responsiveness on device

View File

@@ -1,51 +0,0 @@
# BookInfo Button Mapping, Fallback Load, ManageBook Integration, Cover Regen Fix
**Date**: 2026-03-09
**Task**: Implement fixes 1-5 from the BookInfo Buttons and Load plan
## Changes Made
### BookInfoActivity.cpp — Fixes 1-3
1. **Button mapping** (`loop()`): Removed `Confirm` as exit trigger. Added `Left` and `Right` front buttons as scroll-down/scroll-up handlers alongside existing side-button `Down`/`Up` and `PageForward`/`PageBack`.
2. **Button hints** (`render()`): Updated `mapLabels` to show `STR_DIR_DOWN` on Left (btn3) and `STR_DIR_UP` on Right (btn4), with separate variables for each direction instead of a single combined hint.
3. **Fallback load** (`onEnter()`): Changed `epub.load(false, true)` pattern to try `epub.load(true, true)` on failure, ensuring cache is built for books that were never opened. XTC branch already builds unconditionally via `xtc.load()`.
### BookManageMenuActivity — Fix 4
- Added `BOOK_INFO` to `Action` enum (first entry)
- Added `STR_BOOK_INFO` menu item as first entry in `buildMenuItems()`
- Handled `BOOK_INFO` in all 4 result sites:
- `HomeActivity::openManageMenu()` — launches `BookInfoActivity` via `startActivityForResult`
- `RecentBooksActivity::openManageMenu()` — same pattern
- `FileBrowserActivity::handleManageResult()` — same pattern
- `EpubReaderActivity` inline switch — same pattern
- Added `BOOK_INFO` no-op case to `executeManageAction()` in Home/Recent to prevent compiler warnings
### HomeActivity cover regeneration — Fix 5
- After `generateThumbBmp()` fails or produces invalid BMP, now calls `generateCoverBmp(false)` to extract the full cover from the EPUB before retrying thumbnail generation
- Added `Epub::isValidThumbnailBmp()` validation after each `generateThumbBmp()` call
- Applied same pattern to XTC branch using `xtc.generateCoverBmp()` + validation
- This aligns the HomeActivity pipeline with the EpubReaderActivity's multi-tier fallback approach
## Files Changed
- `src/activities/home/BookInfoActivity.cpp` — button mapping, hints, fallback load
- `src/activities/home/BookManageMenuActivity.h` — BOOK_INFO enum entry
- `src/activities/home/BookManageMenuActivity.cpp` — BOOK_INFO menu item
- `src/activities/home/HomeActivity.cpp` — BOOK_INFO handler + cover regen fix
- `src/activities/home/RecentBooksActivity.cpp` — BOOK_INFO handler
- `src/activities/home/FileBrowserActivity.cpp` — BOOK_INFO handler
- `src/activities/reader/EpubReaderActivity.cpp` — BOOK_INFO handler
## Build Result
Build succeeded — 0 errors, 0 code warnings. RAM: 30.3%, Flash: 95.7%.
## Follow-up Items
- Test on device: verify all 4 BookInfo entry points work (Home manage, Recent manage, FileBrowser manage, Reader manage)
- Test cover regeneration after DELETE_CACHE from reader menu
- Verify button mapping on BookInfo screen (Left=down, Right=up, side buttons=scroll)
- Verify Book Info shows for books that have never been opened

View File

@@ -1,23 +0,0 @@
# BookInfo Loading Popup for Unopened Books
**Date**: 2026-03-09
**Task**: Show a progress popup when BookInfo needs to parse an unopened book
## Changes Made
### BookInfoActivity.cpp
Added a "Loading..." progress popup with progress bar to `onEnter()` when the book cache doesn't exist and a full parse is required:
- **EPUB branch**: `epub.load(false, true)` is tried first (fast, cache-only). If it fails, `GUI.drawPopup(tr(STR_LOADING))` is shown at 10%, `epub.load(true, true)` runs (slow full parse), progress updates to 50%, thumbnail generation runs, then progress hits 100%.
- **XTC branch**: Checks `Storage.exists(xtc.getCachePath())` before `xtc.load()`. If cache directory is missing, shows the same popup pattern around load + thumbnail generation.
- For books with existing cache, no popup is shown — the fast path completes silently.
## Build Result
Build succeeded — 0 errors, 0 linter warnings. RAM: 30.3%, Flash: 95.7%.
## Follow-up
- Test on device: open BookInfo for a book that has never been opened, verify popup appears
- Verify no popup appears for books that have been previously opened/cached

View File

@@ -1,19 +0,0 @@
# BookInfo Header Overlap Fix
**Date**: 2026-03-09
**Task**: Fix BookInfo header not properly overlaying scrolled content (cover image and text fields bleeding into the header bar area).
## Problem
The BookInfo screen's header (clock, battery, "Book Info" title) was being drawn *before* content, so the book cover thumbnail and text fields could render on top of or overlap with the header when scrolling. Additionally, the initial attempt to clip the cover by reducing `maxHeight` in `drawBitmap1Bit` caused the cover to *shrink* rather than scroll behind the header, because `maxHeight` controls bitmap scaling, not clipping.
## Changes Made
### `src/activities/home/BookInfoActivity.cpp`
- **Moved header rendering after content**: `GUI.drawHeader()` is now called *after* all content (cover + fields) is drawn, not before. A `renderer.fillRect(0, 0, pageW, contentTop, false)` wipe of the header zone precedes the header draw, ensuring any content that scrolled into the header region is cleared before the header paints on top.
- **Removed incorrect cover clipping**: The cover bitmap is now drawn at its natural scrolled `y` position with full `coverDisplayWidth` and `coverDisplayHeight` (no clamping of `maxHeight`). The header fill+draw handles visual clipping. This gives the correct "scroll behind the header" behavior instead of the cover appearing to shrink.
## Follow-up
None -- visual behavior confirmed acceptable by user.

Some files were not shown because too many files have changed in this diff Show More