|
1 | | -# CrossPoint Reader — Consolidated Project Context |
| 1 | +# CrossPoint Reader — Durable Context |
2 | 2 |
|
3 | | -Any simulator patches should be made to the `crosspoint-simulator` project itself. If it is not co-located with this project, inform the user of all changes that should be made to the simulator project and request that they open a pull request for them. |
| 3 | +Keep this file focused on repo-specific gotchas that are worth reusing in future sessions. |
4 | 4 |
|
5 | | -## Known Simulator Limitations |
| 5 | +## Simulator |
6 | 6 |
|
7 | | -- **No image rendering**: `lib_ignore = hal, PNGdec, JPEGDEC` in `platformio.ini`. `ImageDecoderFactory::getDecoder()` returns null → images silently skipped. Would need `stb_image` to fix. |
8 | | -- **JPEGDEC stub**: Always returns failure (`open=0`, `getLastError=-1`). Log message `JPEGDEC fallback: open failed (err=-1)` in simulator is expected and harmless. |
9 | | -- **Deep sleep is a no-op**: `esp_deep_sleep_start()` returns immediately in simulator. `lastActivityTime = millis()` is reset after `enterDeepSleep()` calls in `src/main.cpp` to prevent infinite re-trigger loop. |
10 | | -- **HalStorage**: Uses POSIX `::open()` with `./fs_` prefix; allows multiple readers unlike real hardware (SdFat with mutex, one reader per file at a time). |
| 7 | +- Simulator patches belong in the adjacent `crosspoint-simulator` repo. |
| 8 | +- The valid local simulator env in this repo is `simulator`, and `pio run -e simulator` currently builds cleanly. |
| 9 | +- The simulator `PNGdec` stub in `crosspoint-simulator/src/PNGdec.h` needs to mirror the real API shape used by app code, including `hasAlpha()` and `getTransparentColor()`, even though decode still fails intentionally. |
| 10 | +- Known simulator limits: |
| 11 | + - No image rendering: `platformio.ini` ignores `hal`, `PNGdec`, and `JPEGDEC`, so image decoders are intentionally absent. |
| 12 | + - JPEGDEC stub always fails; `JPEGDEC fallback: open failed (err=-1)` is expected in simulator. |
| 13 | + - `esp_deep_sleep_start()` is a no-op in simulator. |
| 14 | + - `HalStorage` uses POSIX file access under `./fs_` and allows multiple readers, unlike real hardware. |
11 | 15 |
|
12 | | ---- |
| 16 | +## Real Hardware / Storage |
13 | 17 |
|
14 | | -## SdFat: One Reader Per File |
| 18 | +- SdFat on hardware allows only one open reader per file path at a time. If a fallback needs to reopen the same file, close the first handle before reopening. |
15 | 19 |
|
16 | | -On real hardware, SdFat cannot open a second read handle while the first is still open on the same path. Pattern when a fallback needs to reopen a file: |
| 20 | +## Rendering / Reader Pipeline |
17 | 21 |
|
18 | | -```cpp |
19 | | -jpegFile.close(); // Release handle before fallback reopens it |
20 | | -return fallbackFunction(filePath, ...); |
21 | | -``` |
| 22 | +- `lib/Epub/Epub/Page.cpp`: images must render only in `GfxRenderer::BW`; grayscale passes are text anti-aliasing passes only. |
| 23 | +- Kindle EPUBs may contain paired high-res and old-Kindle fallback images. `ChapterHtmlSlimParser` should skip `<img>` nodes with `data-AmznRemoved-M8` to avoid duplicate stacked images. |
| 24 | +- After image/layout pipeline changes that affect cached EPUB output, clear the affected `.crosspoint/epub_<hash>/` cache if behavior looks stale. |
22 | 25 |
|
23 | | -See: `lib/JpegToBmpConverter/JpegToBmpConverter.cpp` — `jpegFileToBmpStreamInternal()`. |
| 26 | +## Misc Repo Gotchas |
24 | 27 |
|
25 | | ---- |
26 | | - |
27 | | -## Image Rendering Pipeline |
28 | | - |
29 | | -### PageImage::render — Grayscale Skip |
30 | | - |
31 | | -`lib/Epub/Epub/Page.cpp:28-31` — Images must be skipped in GRAYSCALE_LSB/MSB passes: |
32 | | - |
33 | | -```cpp |
34 | | -void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { |
35 | | - // Images are only rendered in BW mode; grayscale passes are for text anti-aliasing only |
36 | | - if (renderer.getRenderMode() != GfxRenderer::BW) return; |
37 | | - imageBlock->render(renderer, xPos + xOffset, yPos + yOffset); |
38 | | -} |
39 | | -``` |
40 | | -
|
41 | | -**Why**: Dithered image pixels with values 1 or 2 would otherwise be picked up by the grayscale LUT and given gray waveforms, causing subtle ghosting around image edges. The LUT's `0b00` entry (what all image pixels become when skipped) = "no waveform" = pixel unchanged. |
42 | | -
|
43 | | -Note: `Page.cpp` must `#include <GfxRenderer.h>` explicitly — `Block.h` only forward-declares `GfxRenderer`, which is insufficient for member access. |
44 | | -
|
45 | | -### Kindle Dual-Image Pattern |
46 | | -
|
47 | | -Some Kindle-format EPUBs include two `<img>` tags for the same image — one high-res, one low-res fallback for old Kindle M8 hardware: |
48 | | -
|
49 | | -```html |
50 | | -<img src="high-res.jpeg" class="high-res" data-AmznRemoved="mobi7" /> |
51 | | -<img src="low-res.jpeg" class="low-res" width="120" height="120" data-AmznRemoved-M8="true" /> |
52 | | -``` |
53 | | - |
54 | | -Kindle/Calibre hides one via CSS `@media` query. `ChapterHtmlSlimParser` does not evaluate media queries → both render → two stacked images of different sizes. |
55 | | - |
56 | | -**Fix** (`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`): During `<img>` attribute scan, skip any image with `data-AmznRemoved-M8` attribute: |
57 | | - |
58 | | -```cpp |
59 | | -} else if (strncmp(atts[i], "data-AmznRemoved-M8", 19) == 0) { |
60 | | - amznM8Removed = true; |
61 | | -} |
62 | | -// After loop: |
63 | | -if (amznM8Removed) { |
64 | | - LOG_DBG("EHP", "Skipping Kindle M8 low-res fallback image"); |
65 | | - return; |
66 | | -} |
67 | | -``` |
68 | | - |
69 | | -**Cache note**: After this fix, delete `.crosspoint/epub_<hash>/` for affected books — stale `.pxc` files for the skipped image may linger. |
70 | | - |
71 | | ---- |
72 | | - |
73 | | -## Display / Grayscale Pipeline |
74 | | - |
75 | | -### GfxRenderer Pixel Convention |
76 | | - |
77 | | -- `drawPixel(x, y, true)` → CLEARS bit → 0 = **black** |
78 | | -- `drawPixel(x, y, false)` → SETS bit → 1 = **white** |
79 | | -- `clearScreen(0xFF)` = all white (default page clear) |
80 | | -- `clearScreen(0x00)` = all black (used before grayscale passes) |
81 | | - |
82 | | -### lut_grayscale entries (SSD1677) |
83 | | - |
84 | | -Indexed by 2-bit value (RED_bit=MSB, BW_bit=LSB): |
85 | | - |
86 | | -- `0b00`: no waveform → pixel **unchanged** |
87 | | -- `0b01`: light gray waveform |
88 | | -- `0b10`: gray waveform |
89 | | -- `0b11`: dark gray waveform |
90 | | - |
91 | | -### imagePageWithAA Render Sequence |
92 | | - |
93 | | -Triggered when `page->hasImages() && SETTINGS.textAntiAliasing`: |
94 | | - |
95 | | -1. BW render (image drawn to framebuffer) |
96 | | -2. `fillRect` blanks image area → `FAST_REFRESH` (shows text only, image area white) |
97 | | -3. Re-render with image → `FAST_REFRESH` (shows text + image) |
98 | | -4. `storeBwBuffer()` → `clearScreen(0x00)` → GRAYSCALE_LSB render → `copyGrayscaleLsbBuffers()` |
99 | | -5. `clearScreen(0x00)` → GRAYSCALE_MSB render → `copyGrayscaleMsbBuffers()` |
100 | | -6. `displayGrayBuffer()` (custom LUT, 61ms) → `restoreBwBuffer()` + `cleanupGrayscaleBuffers()` |
101 | | - |
102 | | -### Single Buffer Mode Post-Refresh |
103 | | - |
104 | | -After every `displayBuffer(FAST_REFRESH)`, RED RAM is synced with current frameBuffer. After `restoreBwBuffer()`, `cleanupGrayscaleBuffers(frameBuffer)` also syncs RED RAM with the restored BW content — required for correct differential fast refreshes on subsequent page turns. |
105 | | - |
106 | | ---- |
107 | | - |
108 | | -## POSIX TZ Offset Convention |
109 | | - |
110 | | -POSIX TZ sign is **inverted** from ISO 8601. `"UTC-1"` = 1 hour EAST (UTC+1). |
111 | | - |
112 | | -Formula used in `TimeStore::applyTimezone()`: |
113 | | - |
114 | | -```cpp |
115 | | -const int posixOffset = 12 - static_cast<int>(SETTINGS.timezoneIndex); |
116 | | -// timezoneIndex: 0=UTC-12, 12=UTC+0, 26=UTC+14 |
117 | | -``` |
118 | | - |
119 | | ---- |
120 | | - |
121 | | -## LyraTheme Overrides BaseTheme |
122 | | - |
123 | | -`LyraTheme::drawHeader()` overrides `BaseTheme::drawHeader()` and does **not** call super. Any rendering added to `BaseTheme::drawHeader()` will not appear with the Lyra theme unless explicitly duplicated in `LyraTheme::drawHeader()`. |
| 28 | +- POSIX TZ signs are inverted from ISO 8601 in `TimeStore::applyTimezone()`: `"UTC-1"` means UTC+1. |
| 29 | +- `LyraTheme::drawHeader()` does not call `BaseTheme::drawHeader()`, so header changes in the base theme must be duplicated in Lyra if needed. |
0 commit comments