Compare commits

..

21 Commits

Author SHA1 Message Date
Jonas Diemer
7b5a63d220
Option to short-press power button. (#56)
Some checks are pending
CI / build (push) Waiting to run
Adresses #53 

Please check if we still need the code to "Give the user up to 1000ms to
start holding the power button, and must hold for
SETTINGS.getPowerButtonDuration()" - the power button should be pressed
already when waking up...

Also, decided to return before the delay to wait more to make the
behavior more immediate.

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-19 23:37:34 +11:00
IFAKA
c1d5f5d562
Add NULL checks after fopen() in ZipFile (#68)
## Problem
Three `fopen()` calls in ZipFile.cpp did not check for NULL before using
the file handle. If files cannot be opened, `fseek`/`fread`/`fclose`
receive NULL and crash.

## Fix
Added NULL checks with appropriate error logging and early returns for
all three locations:
- `getDataOffset()`
- `readFileToMemory()`
- `readFileToStream()`

## Testing
- Builds successfully with `pio run`
- Affects: `lib/ZipFile/ZipFile.cpp`
2025-12-19 23:28:43 +11:00
IFAKA
adfeee063f
Handle empty spine in getBookSize() and calculateProgress() (#67)
## Problem
- `getBookSize()` calls `getCumulativeSpineItemSize(getSpineItemsCount()
- 1)` which passes -1 when spine is empty
- `calculateProgress()` then divides by zero when book size is 0

## Fix
- Return 0 from `getBookSize()` if spine is empty
- Return 0 from `calculateProgress()` if book size is 0

## Testing
- Builds successfully with `pio run`
- Affects: `lib/Epub/Epub.cpp`
2025-12-19 23:28:36 +11:00
IFAKA
2d3928ed81
Validate file handle when reading progress.bin (#66)
## Problem
Reading progress.bin used `SD.exists()` then `SD.open()` without
checking if open succeeded. Race conditions or SD errors could cause
file handle to be invalid.

## Fix
- Removed redundant `SD.exists()` check
- Check if file opened successfully before reading
- Verify correct number of bytes were read

## Testing
- Builds successfully with `pio run`
- Affects: `src/activities/reader/EpubReaderActivity.cpp`
2025-12-19 23:27:08 +11:00
IFAKA
48249fbd1e
Check SD card initialization and show error on failure (#65)
## Problem
`SD.begin()` return value was ignored. If the SD card fails to
initialize, the device continues and crashes when trying to load
settings/state.

## Fix
Check return value and display "SD card error" message instead of
proceeding with undefined state.

## Testing
- Builds successfully with `pio run`
- Affects: `src/main.cpp`
2025-12-19 23:24:25 +11:00
IFAKA
1a53dccebd
Fix title truncation crash for short titles (#63)
## Problem
The status bar title truncation loop crashes when the chapter title is
shorter than 8 characters.

```cpp
// title.length() - 8 underflows when length < 8 (size_t is unsigned)
title = title.substr(0, title.length() - 8) + "...";
```

## Fix
Added a length guard to skip truncation for titles that are too short to
truncate safely.

## Testing
- Builds successfully with `pio run`
- Affects: `src/activities/reader/EpubReaderActivity.cpp`
2025-12-19 23:23:43 +11:00
IFAKA
3e28724b62
Add bounds checking for TOC/spine array access (#64)
## Problem
`getSpineIndexForTocIndex()` and `getTocIndexForSpineIndex()` access
`toc[tocIndex]` and `spine[spineIndex]` without validating indices are
within bounds. Malformed EPUBs or edge cases could trigger out-of-bounds
access.

## Fix
Added bounds validation at the start of both functions before accessing
the arrays.

## Testing
- Builds successfully with `pio run`
- Affects: `lib/Epub/Epub.cpp`
2025-12-19 23:23:23 +11:00
Jonas Diemer
d86b3fe134
Bugfix/word spacing indented (#59)
Some checks are pending
CI / build (push) Waiting to run
Simplified the indentation to fix having too large gaps between words
(original calculation was inaccurate).
2025-12-19 08:45:20 +11:00
Dave Allie
1a3d6b125d
Custom sleep screen support with BMP reading (#57)
## Summary

* Builds on top of
https://github.com/daveallie/crosspoint-reader/pull/16 - adresses
https://github.com/daveallie/crosspoint-reader/discussions/14
* This PR adds the ability for the user to supply a custom `sleep.bmp`
image at the root of the SD card that will be shown instead of the
default sleep screen if present.
* Supports:
  * Different BPPs:
    * 1bit
    * 2bit
    * 8bit
    * 24bit
    * 32bit (with alpha-channel ignored)
  * Grayscale rendering

---------

Co-authored-by: Sam Davis <sam@sjd.co>
2025-12-19 08:45:14 +11:00
Dave Allie
b2020f5512
Skip pagebreak blocks when parsing epub file (#58)
Some checks are pending
CI / build (push) Waiting to run
## Summary

* Skip pagebreak blocks when parsing epub file
* These blocks break the flow and often contain the page number in them
which should not interrupt the flow of the content
- Attributes sourced from:
  - https://www.w3.org/TR/epub-ssv-11/#pagebreak
  - https://www.w3.org/TR/dpub-aria-1.1/#doc-pagebreak
2025-12-19 01:11:03 +11:00
Dave Allie
70dc0f018e
Cut release 0.7.0 2025-12-19 00:44:22 +11:00
Jonas Diemer
424594488f
Caching of spine item sizes for faster book loading (saves 1-4 seconds). (#54)
Some checks are pending
CI / build (push) Waiting to run
As discussed in
https://github.com/daveallie/crosspoint-reader/pull/38#issuecomment-3665142427,
#38
2025-12-18 22:49:14 +11:00
Dave Allie
57fdb1c0fb
Rendering "Indexing..." on white screen to avoid partial update 2025-12-18 22:13:24 +11:00
Dave Allie
5e1694748c
Fix font readability by expanding blacks and trimming whites (#55)
## Summary

* Previously, only pure black pixels in the font were marked as black,
this expands the black range, and makes the lightest pixels white

## Additional Context

* Noticed personally it was kind of "thin" and washed out a bit, this
massively helps, should also address concerns raised here:
https://github.com/daveallie/crosspoint-reader/discussions/39
2025-12-18 21:39:13 +11:00
Jonas Diemer
063a1df851
Bugfix for #46: don't look at previous chapters if in chapter 0. (#48)
Some checks are pending
CI / build (push) Waiting to run
Fixes #46
2025-12-18 06:28:06 +11:00
Dave Allie
d429966dd4
Rename Screens to Activities and restructure files (#44)
## Summary

* This PR drastically reshapes the structure of the codebase, moving
from the concept of "Screens" to "Activities", restructing the files and
setting up the concept of subactivities.
* This should help with keep the main file clean and containing all
functional logic in the relevant activity.
* CrossPointState is now also a global singleton which should help with
accessing it from within activities.

## Additional Context

* This is probably going to be a bit disruptive for people with open
PRs, sorry 😞
2025-12-17 23:32:18 +11:00
Jonas Diemer
c78f2a9840
Calculate the progress in the book by file sizes of each chapter. (#38)
## Summary

Addresses #35.

Maybe it could be wise to do some caching of the spine sizes (but
performance isn't too bad).
2025-12-17 23:05:24 +11:00
Dave Allie
11f01d3a41
Add home screen (#42)
Some checks are pending
CI / build (push) Waiting to run
## Summary

* Add home screen
* Sits as new root screen, allows for navigation to settings or file
list
2025-12-17 20:47:43 +11:00
Arthur Tazhitdinov
973d372521
TOC location fix (#25)
## Summary

* Rely on media-type="application/x-dtbncx+xml" to find TOC instead of
hardcoded values

## Additional Context

* Most of my epubs don't have id==ncx for toc file location. I think
this media-type is EPUB standard

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-17 18:49:45 +11:00
Dave Allie
67da8139b3
Use 6x8kB chunks instead of 1x48kB chunk for secondary display buffer (#36)
Some checks are pending
CI / build (push) Waiting to run
## Summary

- When allocating the `bwBuffer` required to restore the RED RAM in the
EPD, we were previously allocating the whole frame buffer in one
contiguous memory chunk (48kB)
- Depending on the state of memory fragmentation at the time of this
call, it may not be possible to allocate all that memory
- Instead, we now allocate 6 blocks of 8kB instead of the full 48kB,
this should mean the display updates are more resilient to different
memory conditions

## Additional Context
2025-12-17 01:39:22 +11:00
Dave Allie
c287aa03a4
Use single buffer mode for EInkDisplay (#34)
## Summary

* Frees up 48kB of statically allocated RAM in exchange for 48kB just
when grayscale rendering is needed

## Additional Context

* Upstream changes:
https://github.com/open-x4-epaper/community-sdk/pull/7
2025-12-17 00:17:49 +11:00
55 changed files with 19192 additions and 19651 deletions

View File

@ -1,505 +0,0 @@
/**
* generated by fontconvert.py
* name: babyblue
* size: 8
* mode: 1-bit
*/
#pragma once
#include "EpdFontData.h"
static const uint8_t babyblueBitmaps[3140] = {
0xFF, 0xFF, 0x30, 0xFF, 0xFF, 0xF0, 0x36, 0x1B, 0x0D, 0x9F, 0xFF, 0xF9, 0xB3, 0xFF, 0xFF, 0x36, 0x1B, 0x0D, 0x80,
0x18, 0x18, 0x7E, 0xFF, 0xDB, 0xD8, 0xFE, 0x7F, 0x9B, 0xDB, 0xFF, 0x7E, 0x18, 0x00, 0x71, 0x9F, 0x73, 0x6C, 0x6F,
0x8F, 0xE0, 0xFF, 0x83, 0xF8, 0xFB, 0x1B, 0x63, 0x7C, 0xC7, 0x00, 0x38, 0x3E, 0x1B, 0x0D, 0x87, 0xC3, 0xEF, 0xBF,
0x8E, 0xCF, 0x7F, 0x1E, 0xC0, 0xFF, 0x37, 0xEC, 0xCC, 0xCC, 0xCC, 0xCC, 0x67, 0x20, 0xCE, 0x73, 0x33, 0x33, 0x33,
0x33, 0x6C, 0x80, 0x32, 0xFF, 0xDE, 0xFE, 0x30, 0x18, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0xBF, 0x00, 0xFF,
0xC0, 0x30, 0x0C, 0x31, 0xC6, 0x18, 0xE3, 0x1C, 0x63, 0x8C, 0x00, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3,
0xC7, 0x7E, 0x3C, 0x19, 0xDF, 0xFD, 0x8C, 0x63, 0x18, 0xC6, 0x3C, 0x7E, 0xE7, 0x83, 0x07, 0x0E, 0x1C, 0x38, 0x70,
0xFE, 0xFF, 0x7E, 0xFF, 0xC3, 0x07, 0x3E, 0x3E, 0x07, 0x03, 0x83, 0xFF, 0x7E, 0x0E, 0x1E, 0x3E, 0x76, 0xE6, 0xFF,
0xFF, 0x06, 0x06, 0x06, 0x06, 0x7F, 0xFF, 0x06, 0x0F, 0xDF, 0xC1, 0x83, 0x87, 0xFD, 0xF0, 0x3E, 0x7F, 0xE3, 0xC0,
0xFC, 0xFE, 0xE7, 0xC3, 0xC7, 0x7E, 0x3C, 0xFF, 0xFF, 0xC0, 0xE0, 0xE0, 0x60, 0x70, 0x30, 0x18, 0x1C, 0x0C, 0x06,
0x00, 0x3C, 0x7E, 0x66, 0x66, 0x7E, 0x7E, 0xE7, 0xC3, 0xC7, 0x7E, 0x3C, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7F,
0x3F, 0x07, 0x3E, 0x7C, 0xB0, 0x03, 0xB0, 0x03, 0xF8, 0x06, 0x3D, 0xF7, 0x8F, 0x0F, 0x87, 0x03, 0xFF, 0xFF, 0x00,
0xFF, 0xFF, 0x81, 0xE1, 0xF0, 0xF1, 0xEF, 0xBC, 0x40, 0x38, 0xFB, 0xBE, 0x30, 0xE3, 0x8E, 0x18, 0x30, 0x00, 0xC0,
0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x9D, 0xE7, 0xEF, 0xCF, 0x73, 0x3D, 0x8C, 0xF6, 0x33, 0xD8, 0xDB, 0x3F, 0xC6, 0x7E,
0x0C, 0x03, 0x18, 0x18, 0x7F, 0xE0, 0xFF, 0x00, 0x38, 0xFB, 0xBE, 0x3C, 0x7F, 0xFF, 0xE3, 0xC7, 0x8C, 0xFC, 0xFE,
0xC7, 0xC7, 0xFE, 0xFE, 0xC7, 0xC7, 0xFE, 0xFC, 0x3F, 0x3F, 0xF8, 0x78, 0x0C, 0x06, 0x03, 0x01, 0x83, 0x7F, 0x9F,
0x80, 0xFC, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0xFE, 0xFC, 0xFF, 0xFF, 0x06, 0x0F, 0xDF, 0xB0, 0x60, 0xFD,
0xFC, 0xFF, 0xFF, 0x06, 0x0F, 0xDF, 0xB0, 0x60, 0xC1, 0x80, 0x3F, 0x3F, 0xF8, 0x78, 0x0C, 0x7E, 0x3F, 0x07, 0x83,
0x7F, 0x9F, 0x80, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xF0, 0x0C, 0x30, 0xC3,
0x0C, 0x38, 0xF3, 0xFD, 0xE0, 0xC3, 0xC7, 0xCE, 0xDC, 0xF8, 0xF8, 0xDC, 0xCC, 0xC6, 0xC3, 0xC1, 0x83, 0x06, 0x0C,
0x18, 0x30, 0x60, 0xFD, 0xFC, 0xE1, 0xF8, 0x7F, 0x3F, 0xCF, 0xFF, 0xF7, 0xBD, 0xEF, 0x7B, 0xCC, 0xF3, 0x30, 0xC3,
0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x3E, 0x3F, 0xB8, 0xF8, 0x3C, 0x1E, 0x0F, 0x07, 0x87, 0x7F,
0x1F, 0x00, 0xFE, 0xFF, 0xC3, 0xC3, 0xFF, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0x3E, 0x3F, 0xB8, 0xF8, 0x3C, 0x1E, 0x0F,
0x37, 0x9F, 0x7F, 0x1F, 0xC0, 0xFE, 0xFF, 0xC3, 0xC7, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0x7D, 0xFF, 0x1F, 0x87,
0xC3, 0xC1, 0xC3, 0xFE, 0xF8, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xC3, 0xC3, 0xC3, 0xC3,
0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0xC3, 0xC3, 0xC3, 0xE7, 0x66, 0x7E, 0x3C, 0x3C, 0x3C, 0x18, 0x83, 0x0F, 0x0C,
0x3C, 0x30, 0xF1, 0xE7, 0x67, 0x99, 0xBF, 0xE3, 0xCF, 0x0F, 0x3C, 0x3C, 0xF0, 0x61, 0x80, 0x80, 0xF8, 0x77, 0x38,
0xFC, 0x1E, 0x07, 0x83, 0xF1, 0xCE, 0xE1, 0xB0, 0x30, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18,
0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xFE, 0xFF, 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xFE, 0x81, 0xC1,
0x83, 0x83, 0x83, 0x06, 0x06, 0x0C, 0x0C, 0xFF, 0x33, 0x33, 0x33, 0x33, 0x33, 0xFE, 0x18, 0x3C, 0x7E, 0x66, 0xE7,
0xC3, 0x83, 0xFF, 0xFF, 0x9D, 0x80, 0x7D, 0xFE, 0x1B, 0xFF, 0xF8, 0xFF, 0xBF, 0xC1, 0x83, 0xE7, 0xEC, 0xF8, 0xF1,
0xE7, 0xFD, 0xF0, 0x3C, 0xFF, 0x9E, 0x0C, 0x18, 0x9F, 0x9E, 0x06, 0x0C, 0xFB, 0xFE, 0x78, 0xF1, 0xE3, 0x7E, 0x7C,
0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x3B, 0xD9, 0xFF, 0xB1, 0x8C, 0x63, 0x18, 0x3E, 0xFF, 0x9E, 0x3C,
0x78, 0xDF, 0x9F, 0x06, 0x0D, 0xF3, 0xC0, 0xC3, 0x0F, 0xBF, 0xEF, 0x3C, 0xF3, 0xCF, 0x30, 0xFB, 0xFF, 0xF0, 0x6D,
0x36, 0xDB, 0x6D, 0xBD, 0x00, 0xC3, 0x0C, 0xF7, 0xFB, 0xCE, 0x3C, 0xDB, 0x30, 0xFF, 0xFF, 0xF0, 0x7F, 0xBF, 0xFC,
0xCF, 0x33, 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0x7B, 0xFC, 0xF3, 0xCF, 0x3C, 0xF3, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7,
0x7E, 0x3C, 0x79, 0xFB, 0x3E, 0x3C, 0x79, 0xFF, 0x7C, 0xC1, 0x82, 0x00, 0x3C, 0xFF, 0x9E, 0x3C, 0x78, 0xDF, 0x9F,
0x06, 0x0C, 0x10, 0x77, 0xF7, 0x8C, 0x63, 0x18, 0x7D, 0xFF, 0x1F, 0xE7, 0xF0, 0xFF, 0xBE, 0x63, 0x3D, 0xE6, 0x31,
0x8C, 0x71, 0xC0, 0x8F, 0x3C, 0xF3, 0xCF, 0x3F, 0xDE, 0x83, 0xC3, 0xC7, 0x66, 0x66, 0x6E, 0x3C, 0x18, 0x80, 0xF3,
0x3D, 0xFD, 0xFE, 0x7F, 0x9F, 0xE3, 0x30, 0xCC, 0x83, 0xC7, 0x6E, 0x3C, 0x38, 0x7C, 0xE6, 0xC3, 0x83, 0xC7, 0x66,
0x6E, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x70, 0x60, 0xFF, 0xFC, 0x71, 0xC7, 0x1C, 0x3F, 0x7F, 0x19, 0xCC, 0x63, 0x3B,
0x98, 0x61, 0x8C, 0x63, 0x1C, 0x40, 0xFF, 0xFF, 0xFF, 0xF8, 0x83, 0x87, 0x0C, 0x30, 0xE1, 0xC7, 0x38, 0xC3, 0x0C,
0x63, 0x08, 0x00, 0x79, 0xFF, 0xE3, 0xC0, 0xF2, 0xFF, 0xFE, 0x18, 0x30, 0xF3, 0xFF, 0xFB, 0x36, 0x6E, 0x7E, 0x78,
0x60, 0xC1, 0x00, 0x3C, 0x7E, 0x66, 0x60, 0xFC, 0xFC, 0x30, 0x72, 0xFF, 0xFE, 0x83, 0xFF, 0x7E, 0x66, 0x66, 0x7E,
0xFE, 0x83, 0x83, 0xE7, 0x7E, 0x3C, 0xFF, 0xFF, 0xFF, 0xFF, 0x18, 0x18, 0xFF, 0xFC, 0xBF, 0xF8, 0x3C, 0x7E, 0x66,
0x7E, 0x7C, 0xEE, 0xC7, 0xC3, 0x77, 0x3E, 0x0C, 0x66, 0x66, 0x7C, 0x38, 0x9E, 0xE6, 0x3F, 0x1F, 0xEF, 0xFF, 0xFF,
0xF3, 0xFC, 0x3F, 0x3F, 0xFF, 0xDF, 0xDF, 0xE3, 0xF0, 0x77, 0xFF, 0xFF, 0xBC, 0x36, 0xFF, 0xF6, 0xCD, 0xCD, 0x8D,
0x80, 0xFF, 0xFF, 0x03, 0x03, 0x03, 0x3F, 0x1F, 0xEF, 0xFF, 0xFB, 0xF6, 0xFF, 0xBF, 0xEF, 0xDB, 0xF7, 0xDF, 0xE3,
0xF0, 0xFF, 0xFF, 0x6F, 0xFF, 0x60, 0x18, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0xFE, 0xFF, 0x77, 0xE6, 0x77, 0x73,
0xFF, 0x77, 0xEF, 0x7F, 0xB8, 0x7F, 0x00, 0xCF, 0x3C, 0xF3, 0xCF, 0x3F, 0xFE, 0xC3, 0x0C, 0x20, 0x7F, 0xFF, 0xFE,
0xFE, 0xFE, 0x7E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x16, 0xB0, 0x63, 0xEC, 0x37, 0xFB, 0x33, 0x30, 0x7B,
0xFC, 0xF3, 0xFD, 0xE0, 0x99, 0xF9, 0xF9, 0xB7, 0xFF, 0xB6, 0x00, 0x30, 0x07, 0x03, 0xF0, 0x7B, 0x0E, 0x31, 0xC3,
0x38, 0x37, 0x60, 0xEE, 0x1D, 0xE3, 0xBF, 0x33, 0xF6, 0x06, 0x30, 0x67, 0x0E, 0xF1, 0xCB, 0x38, 0x37, 0x03, 0xEF,
0x1D, 0xF3, 0x9F, 0x70, 0xEE, 0x0E, 0xC1, 0xF0, 0x70, 0x6F, 0x8E, 0xB9, 0xCB, 0xB8, 0xFF, 0x07, 0xE6, 0x1C, 0xE3,
0x9E, 0x73, 0xFE, 0x3F, 0xC0, 0x60, 0x30, 0x60, 0xC1, 0x87, 0x1C, 0x30, 0x60, 0xC6, 0xD9, 0xE1, 0x80, 0x30, 0x70,
0x61, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x18, 0x70, 0xC1, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF,
0x1E, 0x3C, 0x60, 0x38, 0xF9, 0xB1, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x3C, 0xF9, 0xE1, 0xC7,
0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x6C, 0xD8, 0xE3, 0xEE, 0xF8, 0xF1, 0xFF, 0xFF, 0x8F, 0x1E, 0x30,
0x38, 0xF9, 0xF1, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x0F, 0xF8, 0x7F, 0xC7, 0xC0, 0x36, 0x03,
0xB0, 0x19, 0xF9, 0xFF, 0xCF, 0xE0, 0xE3, 0x06, 0x1F, 0xB0, 0xFE, 0x3F, 0x3F, 0xF8, 0x78, 0x0C, 0x06, 0x03, 0x01,
0x83, 0x7F, 0x9F, 0x86, 0x01, 0x83, 0x81, 0x80, 0x30, 0x70, 0x67, 0xFF, 0xF8, 0x30, 0x7E, 0xFD, 0x83, 0x07, 0xEF,
0xE0, 0x0C, 0x38, 0x67, 0xFF, 0xF8, 0x30, 0x7E, 0xFD, 0x83, 0x07, 0xEF, 0xE0, 0x18, 0x78, 0xF7, 0xFF, 0xF8, 0x30,
0x7E, 0xFD, 0x83, 0x07, 0xEF, 0xE0, 0x3C, 0x7B, 0xFF, 0xFC, 0x18, 0x3F, 0x7E, 0xC1, 0x83, 0xF7, 0xF0, 0x9D, 0x36,
0xDB, 0x6D, 0xB6, 0x7A, 0x6D, 0xB6, 0xDB, 0x6C, 0x6F, 0xB6, 0x66, 0x66, 0x66, 0x66, 0x60, 0xBB, 0x66, 0x66, 0x66,
0x66, 0x66, 0x7E, 0x3F, 0x98, 0xEC, 0x3F, 0xDF, 0xED, 0x86, 0xC7, 0x7F, 0x3F, 0x00, 0x1E, 0x3E, 0x3C, 0xC3, 0xE3,
0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x18, 0x0E, 0x03, 0x07, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0,
0xF0, 0xEF, 0xE3, 0xE0, 0x0C, 0x0E, 0x06, 0x07, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0xEF, 0xE3, 0xE0,
0x1C, 0x1F, 0x0D, 0x87, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0xEF, 0xE3, 0xE0, 0x1E, 0x1F, 0x0F, 0x07,
0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0xEF, 0xE3, 0xE0, 0x36, 0x1B, 0x0F, 0x8F, 0xEE, 0x3E, 0x0F, 0x07,
0x83, 0xC1, 0xE1, 0xDF, 0xC7, 0xC0, 0x8F, 0xF7, 0x9E, 0xFF, 0x30, 0x1F, 0xCF, 0xF7, 0x3D, 0x9F, 0x6E, 0xDF, 0x37,
0x8D, 0xC7, 0xFF, 0xB7, 0xC0, 0x30, 0x38, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x0C,
0x1C, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x18, 0x3C, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3,
0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x3C, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x0C,
0x1C, 0x18, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0xC0, 0xFC, 0xFE, 0xC7, 0xC3, 0xC7, 0xFE,
0xFC, 0xC0, 0xC0, 0x38, 0xFB, 0xB6, 0x6D, 0xDB, 0x37, 0x67, 0xE7, 0xFF, 0x70, 0x30, 0x70, 0x63, 0xEF, 0xF0, 0xDF,
0xFF, 0xC7, 0xFD, 0xF8, 0x0C, 0x38, 0x63, 0xEF, 0xF0, 0xDF, 0xFF, 0xC7, 0xFD, 0xF8, 0x18, 0x78, 0xF3, 0xEF, 0xF0,
0xDF, 0xFF, 0xC7, 0xFD, 0xF8, 0x3C, 0xF9, 0xE3, 0xEF, 0xF0, 0xDF, 0xFF, 0xC7, 0xFD, 0xF8, 0x3C, 0x79, 0xF7, 0xF8,
0x6F, 0xFF, 0xE3, 0xFE, 0xFC, 0x18, 0x78, 0xF0, 0xC7, 0xDF, 0xE1, 0xBF, 0xFF, 0x8F, 0xFB, 0xF0, 0x7F, 0xEF, 0xFF,
0x86, 0x37, 0xFF, 0xFF, 0xFC, 0x61, 0xFF, 0xF7, 0xFE, 0x3C, 0xFF, 0x9E, 0x0C, 0x18, 0x9F, 0x9E, 0x18, 0x18, 0xE1,
0x80, 0x30, 0x38, 0x18, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x0C, 0x1C, 0x18, 0x3C, 0x7E, 0xE7, 0xFF,
0xFF, 0xC0, 0x7E, 0x3F, 0x18, 0x3C, 0x3C, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x3C, 0x3C, 0x3C, 0x7E,
0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x9D, 0xA6, 0xDB, 0x6D, 0x80, 0x7F, 0x4D, 0xB6, 0xDB, 0x00, 0x6F, 0xB4, 0x66,
0x66, 0x66, 0x60, 0xBB, 0x46, 0x66, 0x66, 0x66, 0x3E, 0x3E, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C,
0x3D, 0xF7, 0x9E, 0xFF, 0x3C, 0xF3, 0xCF, 0x3C, 0xC0, 0x30, 0x38, 0x18, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E,
0x3C, 0x0C, 0x1C, 0x18, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x18, 0x3C, 0x3C, 0x3C, 0x7E, 0xE7, 0xC3,
0xC3, 0xC7, 0x7E, 0x3C, 0x1E, 0x3E, 0x3C, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x3C, 0x3C, 0x3C, 0x7E,
0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x18, 0x18, 0xFF, 0xFF, 0x10, 0x18, 0x3F, 0x7F, 0xEF, 0xDF, 0xFB, 0xF7, 0xFE,
0xFC, 0x61, 0xC3, 0x23, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x18, 0xE3, 0x23, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80,
0x31, 0xE7, 0xA3, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x79, 0xE8, 0xF3, 0xCF, 0x3C, 0xF3, 0xFD, 0xE0, 0x0C, 0x1C,
0x18, 0x83, 0xC7, 0x66, 0x6E, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x70, 0x60, 0xC1, 0x83, 0xE7, 0xEE, 0xF8, 0xF1, 0xE7,
0xFD, 0xF3, 0x06, 0x08, 0x00, 0x3C, 0x3C, 0x83, 0xC7, 0x66, 0x6E, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x70, 0x60, 0x7C,
0xF8, 0xE3, 0xEE, 0xF8, 0xF1, 0xFF, 0xFF, 0x8F, 0x1E, 0x30, 0x7C, 0xF9, 0xF7, 0xF8, 0x6F, 0xFF, 0xE3, 0xFE, 0xFC,
0x6C, 0xF8, 0xE1, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x6C, 0xF8, 0xE3, 0xEF, 0xF0, 0xDF, 0xFF,
0xC7, 0xFD, 0xF8, 0x38, 0x7C, 0xEE, 0xC6, 0xC6, 0xFE, 0xFE, 0xC6, 0xC6, 0xC6, 0x0C, 0x0C, 0x0F, 0x07, 0x7C, 0xFE,
0x86, 0x7E, 0xFE, 0xC6, 0xFE, 0x7E, 0x06, 0x0C, 0x0F, 0x0F, 0x0C, 0x0E, 0x06, 0x07, 0xE7, 0xFF, 0x0F, 0x01, 0x80,
0xC0, 0x60, 0x30, 0x6F, 0xF3, 0xF0, 0x0C, 0x38, 0x61, 0xE7, 0xFC, 0xF0, 0x60, 0xC4, 0xFC, 0xF0, 0x0C, 0x0F, 0x07,
0x87, 0xE7, 0xFF, 0x0F, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x6F, 0xF3, 0xF0, 0x18, 0x78, 0xF1, 0xE7, 0xFC, 0xF0, 0x60,
0xC4, 0xFC, 0xF0, 0x0C, 0x06, 0x0F, 0xCF, 0xFE, 0x1E, 0x03, 0x01, 0x80, 0xC0, 0x60, 0xDF, 0xE7, 0xE0, 0x18, 0x30,
0xF3, 0xFE, 0x78, 0x30, 0x62, 0x7E, 0x78, 0x1E, 0x0F, 0x03, 0x07, 0xE7, 0xFF, 0x0F, 0x01, 0x80, 0xC0, 0x60, 0x30,
0x6F, 0xF3, 0xF0, 0x3C, 0x78, 0x61, 0xE7, 0xFC, 0xF0, 0x60, 0xC4, 0xFC, 0xF0, 0x78, 0x78, 0x30, 0xFC, 0xFE, 0xC7,
0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0xFE, 0xFC, 0x01, 0x83, 0xC1, 0xE7, 0xC7, 0xE7, 0x33, 0x19, 0x8C, 0xC6, 0x3F, 0x0F,
0x80, 0x7E, 0x3F, 0x98, 0xEC, 0x3F, 0xDF, 0xED, 0x86, 0xC7, 0x7F, 0x3F, 0x00, 0x06, 0x1F, 0x1F, 0x3E, 0x7E, 0xE6,
0xC6, 0xC6, 0xC6, 0x7E, 0x3E, 0x3C, 0x7B, 0xFF, 0xFC, 0x18, 0x3F, 0x7E, 0xC1, 0x83, 0xF7, 0xF0, 0x3C, 0x3C, 0x3C,
0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x66, 0xFC, 0xF7, 0xFF, 0xF8, 0x30, 0x7E, 0xFD, 0x83, 0x07, 0xEF, 0xE0,
0x66, 0x7E, 0x3C, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x18, 0x33, 0xFF, 0xFC, 0x18, 0x3F, 0x7E, 0xC1,
0x83, 0xF7, 0xF0, 0x18, 0x18, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0xFE, 0xFE, 0xC0, 0xC0, 0xFC, 0xFC,
0xC0, 0xC0, 0xFC, 0xFE, 0x06, 0x0C, 0x0F, 0x0F, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x06, 0x0C, 0x0F,
0x07, 0x3C, 0x78, 0x67, 0xFF, 0xF8, 0x30, 0x7E, 0xFD, 0x83, 0x07, 0xEF, 0xE0, 0x3C, 0x3C, 0x18, 0x3C, 0x7E, 0xE7,
0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x0C, 0x0F, 0x07, 0x87, 0xE7, 0xFF, 0x0F, 0x01, 0x8F, 0xC7, 0xE0, 0xF0, 0x6F, 0xF3,
0xF0, 0x18, 0x78, 0xF1, 0xF7, 0xFC, 0xF1, 0xE3, 0xC6, 0xFC, 0xF8, 0x30, 0x6F, 0x9E, 0x00, 0x36, 0x1F, 0x07, 0x07,
0xE7, 0xFF, 0x0F, 0x01, 0x8F, 0xC7, 0xE0, 0xF0, 0x6F, 0xF3, 0xF0, 0x36, 0x7C, 0x71, 0xF7, 0xFC, 0xF1, 0xE3, 0xC6,
0xFC, 0xF8, 0x30, 0x6F, 0x9E, 0x00, 0x0C, 0x06, 0x0F, 0xCF, 0xFE, 0x1E, 0x03, 0x1F, 0x8F, 0xC1, 0xE0, 0xDF, 0xE7,
0xE0, 0x18, 0x30, 0xFB, 0xFE, 0x78, 0xF1, 0xE3, 0x7E, 0x7C, 0x18, 0x37, 0xCF, 0x00, 0x3F, 0x3F, 0xF8, 0x78, 0x0C,
0x7E, 0x3F, 0x07, 0x83, 0x7F, 0x9F, 0x83, 0x00, 0xC1, 0xC0, 0xE0, 0x18, 0x30, 0x61, 0xF7, 0xFC, 0xF1, 0xE3, 0xC6,
0xFC, 0xF8, 0x30, 0x6F, 0x9E, 0x00, 0x18, 0x3C, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3,
0x31, 0xE7, 0xB0, 0xC3, 0xEF, 0xFB, 0xCF, 0x3C, 0xF3, 0xCC, 0x61, 0xBF, 0xFF, 0xFD, 0x86, 0x7F, 0x9F, 0xE6, 0x19,
0x86, 0x61, 0x98, 0x60, 0x61, 0xF3, 0xE3, 0xE7, 0xEE, 0xD9, 0xB3, 0x66, 0xCD, 0x98, 0x7F, 0xEC, 0xC6, 0x31, 0x8C,
0x63, 0x18, 0xC6, 0x00, 0x7F, 0xEC, 0x86, 0x31, 0x8C, 0x63, 0x18, 0xFF, 0x66, 0x66, 0x66, 0x66, 0x66, 0xFF, 0x46,
0x66, 0x66, 0x66, 0x9F, 0xDC, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, 0x9F, 0xDC, 0x86, 0x31, 0x8C, 0x63, 0x18,
0x66, 0x66, 0x66, 0x66, 0x66, 0x6C, 0xF6, 0x66, 0x46, 0x66, 0x66, 0x66, 0x6C, 0xF6, 0xEF, 0xFF, 0xFF, 0xBF, 0xFF,
0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE3, 0xF3, 0xFF, 0xDE, 0xDE, 0xE7, 0xBD, 0xEF, 0x7B, 0xDE, 0xC6, 0x33, 0x10,
0x0C, 0x3C, 0x78, 0x60, 0xC1, 0x83, 0x06, 0x0D, 0x1B, 0x37, 0xE7, 0x80, 0x6F, 0xB4, 0x66, 0x66, 0x66, 0x66, 0x6C,
0x80, 0xC3, 0xC7, 0xCE, 0xDC, 0xF8, 0xF8, 0xDC, 0xCC, 0xC6, 0xC3, 0x38, 0x1C, 0x38, 0x30, 0xC3, 0x0C, 0xF7, 0xFB,
0xCE, 0x3C, 0xDB, 0x37, 0x0E, 0x71, 0x80, 0x8F, 0x7F, 0xBC, 0xE3, 0xCD, 0xB3, 0x30, 0xE1, 0x86, 0x0C, 0x18, 0x30,
0x60, 0xC1, 0x83, 0x07, 0xEF, 0xE0, 0x7A, 0x6D, 0xB6, 0xDB, 0x6C, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xFD,
0xFD, 0xC1, 0xC7, 0x0C, 0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0xEF, 0xEC, 0x0D, 0x9B, 0x36, 0x6C, 0x18, 0x30, 0x60,
0xC1, 0xFB, 0xF8, 0x3F, 0xFF, 0xCC, 0xCC, 0xCC, 0xC0, 0xC1, 0x83, 0x06, 0x0C, 0xD9, 0xB0, 0x60, 0xFD, 0xFC, 0xCC,
0xCC, 0xFF, 0xCC, 0xCC, 0x60, 0x60, 0x78, 0x78, 0xF0, 0xE0, 0x60, 0x60, 0x7E, 0x7F, 0x66, 0x67, 0xFE, 0x66, 0x66,
0x0C, 0x1C, 0x18, 0xC3, 0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x18, 0x63, 0x8C, 0x7B, 0xFC, 0xF3,
0xCF, 0x3C, 0xF3, 0xC3, 0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x38, 0x1C, 0x38, 0x30, 0x7B, 0xFC,
0xF3, 0xCF, 0x3C, 0xF3, 0x70, 0xE7, 0x18, 0x3C, 0x3C, 0x18, 0xC3, 0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7,
0xC3, 0x79, 0xE3, 0x1E, 0xFF, 0x3C, 0xF3, 0xCF, 0x3C, 0xC0, 0x81, 0x83, 0x05, 0xE7, 0xEC, 0xD9, 0xB3, 0x66, 0xCD,
0x98, 0xC3, 0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x03, 0x03, 0x03, 0x7B, 0xFC, 0xF3, 0xCF, 0x3C,
0xF3, 0x0C, 0x31, 0x84, 0x3E, 0x1F, 0x0F, 0x8F, 0xEE, 0x3E, 0x0F, 0x07, 0x83, 0xC1, 0xE1, 0xDF, 0xC7, 0xC0, 0x3C,
0x3C, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x36, 0x1F, 0x07, 0x07, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1,
0xE0, 0xF0, 0xEF, 0xE3, 0xE0, 0x36, 0x3E, 0x1C, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x1B, 0x1F, 0x8D,
0x87, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0xEF, 0xE3, 0xE0, 0x1E, 0x3E, 0x3C, 0x3C, 0x7E, 0xE7, 0xC3,
0xC3, 0xC7, 0x7E, 0x3C, 0x3F, 0xFB, 0xFF, 0xF9, 0xC1, 0x86, 0x0C, 0x3F, 0x61, 0xFB, 0x0C, 0x18, 0x60, 0xC7, 0x03,
0xFF, 0x0F, 0xFC, 0x3D, 0xE3, 0xFF, 0xB9, 0xCF, 0x87, 0xFC, 0x3F, 0xE3, 0x85, 0xFF, 0xE7, 0xBE, 0x18, 0x38, 0x30,
0xFE, 0xFF, 0xC3, 0xC7, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0x33, 0x98, 0xEF, 0xEF, 0x18, 0xC6, 0x30, 0xFE, 0xFF,
0xC3, 0xC7, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0x38, 0x1C, 0x38, 0x30, 0x77, 0xF7, 0x8C, 0x63, 0x18, 0xE3, 0xB9,
0x80, 0x3C, 0x3C, 0x18, 0xFE, 0xFF, 0xC3, 0xC7, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0xF7, 0x98, 0xEF, 0xEF, 0x18,
0xC6, 0x30, 0x18, 0x70, 0xC3, 0xEF, 0xF8, 0xFC, 0x3E, 0x1E, 0x0E, 0x1F, 0xF7, 0xC0, 0x18, 0x70, 0xC3, 0xEF, 0xF8,
0xFF, 0x3F, 0x87, 0xFD, 0xF0, 0x18, 0x78, 0xF3, 0xEF, 0xF8, 0xFC, 0x3E, 0x1E, 0x0E, 0x1F, 0xF7, 0xC0, 0x18, 0x78,
0xF3, 0xEF, 0xF8, 0xFF, 0x3F, 0x87, 0xFD, 0xF0, 0x7D, 0xFF, 0x1F, 0x87, 0xC3, 0xC1, 0xC3, 0xFE, 0xF8, 0xC0, 0xC7,
0x0C, 0x00, 0x7D, 0xFF, 0x1F, 0xE7, 0xF0, 0xFF, 0xBE, 0x18, 0x18, 0xE1, 0x80, 0x3C, 0x78, 0x63, 0xEF, 0xF8, 0xFC,
0x3E, 0x1E, 0x0E, 0x1F, 0xF7, 0xC0, 0x3C, 0x78, 0x63, 0xEF, 0xF8, 0xFF, 0x3F, 0x87, 0xFD, 0xF0, 0xFF, 0xFF, 0xC6,
0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x01, 0x83, 0x81, 0x80, 0x63, 0x3D, 0xE6, 0x31, 0x8C, 0x71,
0xCC, 0x37, 0x30, 0x3C, 0x3C, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x0D, 0xB6, 0xFF,
0xF1, 0x86, 0x18, 0x61, 0xC3, 0x80, 0xFF, 0xFF, 0x18, 0x18, 0x7E, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x63, 0x3D, 0xE6,
0x73, 0xCC, 0x71, 0xC0, 0x1E, 0x3E, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x3D, 0xF7,
0xA3, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x3C, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C,
0x79, 0xE8, 0xF3, 0xCF, 0x3C, 0xF3, 0xFD, 0xE0, 0x36, 0x3E, 0x1C, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7,
0x7E, 0x3C, 0x6D, 0xF3, 0xA3, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x18, 0x3C, 0x3C, 0xDB, 0xC3, 0xC3, 0xC3, 0xC3,
0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x31, 0xE7, 0xAF, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x1E, 0x3E, 0x3C, 0xC3, 0xC3,
0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x3D, 0xF7, 0xA3, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0xC3, 0xC3,
0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x18, 0x30, 0x3C, 0x1C, 0x8D, 0x9B, 0x36, 0x6C, 0xD9, 0xBF, 0x3E,
0x0C, 0x30, 0x78, 0x70, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x20, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x79, 0xD9, 0xE6, 0x6F,
0xF8, 0xF3, 0xC3, 0xCF, 0x0F, 0x3C, 0x18, 0x60, 0x0C, 0x07, 0x81, 0xE2, 0x03, 0xCC, 0xF7, 0xF7, 0xF9, 0xFE, 0x7F,
0x8C, 0xC3, 0x30, 0x18, 0x3C, 0x3C, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x3C,
0x83, 0xC7, 0x66, 0x6E, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x70, 0x60, 0x3C, 0x3C, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, 0x18,
0x18, 0x18, 0x18, 0x18, 0x0C, 0x1C, 0x18, 0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xFE, 0xFF, 0x0C, 0x38,
0x67, 0xFF, 0xE3, 0x8E, 0x38, 0xE1, 0xFB, 0xF8, 0x0C, 0x0C, 0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xFE,
0xFF, 0x18, 0x33, 0xFF, 0xF1, 0xC7, 0x1C, 0x70, 0xFD, 0xFC, 0x3C, 0x3C, 0x18, 0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38,
0x70, 0xE0, 0xFE, 0xFF, 0x3C, 0x78, 0x67, 0xFF, 0xE3, 0x8E, 0x38, 0xE1, 0xFB, 0xF8, 0x1E, 0x3F, 0x73, 0xFE, 0xFE,
0xFE, 0xFE, 0x62, 0x3F, 0x1E,
};
static const EpdGlyph babyblueGlyphs[] = {
{0, 0, 5, 0, 0, 0, 0}, //
{2, 10, 3, 1, 10, 3, 0}, // !
{4, 5, 5, 1, 11, 3, 3}, // "
{9, 11, 10, 0, 11, 13, 6}, // #
{8, 14, 9, 0, 12, 14, 19}, // $
{11, 11, 13, 1, 11, 16, 33}, // %
{9, 11, 11, 1, 11, 13, 49}, // &
{2, 4, 3, 1, 11, 1, 62}, // '
{4, 15, 5, 1, 11, 8, 63}, // (
{4, 15, 5, 1, 11, 8, 71}, // )
{6, 6, 6, 0, 11, 5, 79}, // *
{8, 8, 9, 0, 9, 8, 84}, // +
{2, 5, 3, 1, 3, 2, 92}, // ,
{5, 2, 5, 0, 5, 2, 94}, // -
{2, 2, 3, 1, 2, 1, 96}, // .
{6, 11, 6, 0, 11, 9, 97}, // /
{8, 11, 9, 0, 11, 11, 106}, // 0
{5, 11, 6, 1, 11, 7, 117}, // 1
{8, 11, 9, 0, 11, 11, 124}, // 2
{8, 11, 10, 1, 11, 11, 135}, // 3
{8, 11, 9, 0, 11, 11, 146}, // 4
{7, 11, 9, 1, 11, 10, 157}, // 5
{8, 11, 9, 0, 11, 11, 167}, // 6
{9, 11, 10, 0, 11, 13, 178}, // 7
{8, 11, 9, 0, 11, 11, 191}, // 8
{8, 11, 9, 0, 11, 11, 202}, // 9
{2, 8, 3, 1, 8, 2, 213}, // :
{2, 11, 3, 1, 8, 3, 215}, // ;
{7, 8, 9, 1, 9, 7, 218}, // <
{8, 5, 9, 0, 8, 5, 225}, // =
{7, 8, 7, 0, 9, 7, 230}, // >
{7, 11, 9, 1, 11, 10, 237}, // ?
{14, 15, 15, 0, 11, 27, 247}, // @
{7, 10, 9, 1, 10, 9, 274}, // A
{8, 10, 10, 1, 10, 10, 283}, // B
{9, 10, 11, 1, 10, 12, 293}, // C
{8, 10, 10, 1, 10, 10, 305}, // D
{7, 10, 9, 1, 10, 9, 315}, // E
{7, 10, 9, 1, 10, 9, 324}, // F
{9, 10, 11, 1, 10, 12, 333}, // G
{8, 10, 10, 1, 10, 10, 345}, // H
{2, 10, 3, 1, 10, 3, 355}, // I
{6, 10, 6, 0, 10, 8, 358}, // J
{8, 10, 10, 1, 10, 10, 366}, // K
{7, 10, 9, 1, 10, 9, 376}, // L
{10, 10, 12, 1, 10, 13, 385}, // M
{8, 10, 10, 1, 10, 10, 398}, // N
{9, 10, 11, 1, 10, 12, 408}, // O
{8, 10, 10, 1, 10, 10, 420}, // P
{9, 10, 11, 1, 10, 12, 430}, // Q
{8, 10, 10, 1, 10, 10, 442}, // R
{7, 10, 9, 1, 10, 9, 452}, // S
{8, 10, 9, 0, 10, 10, 461}, // T
{8, 10, 10, 1, 10, 10, 471}, // U
{8, 10, 10, 1, 10, 10, 481}, // V
{14, 10, 15, 0, 10, 18, 491}, // W
{10, 10, 11, 0, 10, 13, 509}, // X
{8, 10, 10, 1, 10, 10, 522}, // Y
{8, 10, 9, 0, 10, 10, 532}, // Z
{4, 14, 5, 1, 10, 7, 542}, // [
{7, 10, 7, 0, 10, 9, 549}, // <backslash>
{4, 14, 4, 0, 10, 7, 558}, // ]
{8, 7, 9, 0, 11, 7, 565}, // ^
{8, 2, 9, 0, -2, 2, 572}, // _
{3, 3, 3, 0, 11, 2, 574}, // `
{7, 8, 7, 0, 8, 7, 576}, // a
{7, 10, 9, 1, 10, 9, 583}, // b
{7, 8, 7, 0, 8, 7, 592}, // c
{7, 10, 7, 0, 10, 9, 599}, // d
{8, 8, 9, 0, 8, 8, 608}, // e
{5, 11, 5, 0, 11, 7, 616}, // f
{7, 12, 7, 0, 8, 11, 623}, // g
{6, 10, 7, 1, 10, 8, 634}, // h
{2, 10, 3, 1, 10, 3, 642}, // i
{3, 14, 3, 0, 10, 6, 645}, // j
{6, 10, 7, 1, 10, 8, 651}, // k
{2, 10, 3, 1, 10, 3, 659}, // l
{10, 8, 12, 1, 8, 10, 662}, // m
{6, 8, 7, 1, 8, 6, 672}, // n
{8, 8, 9, 0, 8, 8, 678}, // o
{7, 11, 9, 1, 8, 10, 686}, // p
{7, 11, 7, 0, 8, 10, 696}, // q
{5, 8, 6, 1, 8, 5, 706}, // r
{7, 8, 7, 0, 8, 7, 711}, // s
{5, 10, 5, 0, 10, 7, 718}, // t
{6, 8, 7, 1, 8, 6, 725}, // u
{8, 8, 9, 0, 8, 8, 731}, // v
{10, 8, 11, 0, 8, 10, 739}, // w
{8, 8, 9, 0, 8, 8, 749}, // x
{8, 11, 9, 0, 8, 11, 757}, // y
{7, 8, 7, 0, 8, 7, 768}, // z
{5, 15, 5, 0, 11, 10, 775}, // {
{2, 15, 3, 1, 11, 4, 785}, // |
{6, 15, 6, 0, 11, 12, 789}, // }
{9, 3, 10, 0, 7, 4, 801}, // ~
{2, 12, 4, 2, 8, 3, 805}, // ¡
{7, 13, 7, 0, 10, 12, 808}, // ¢
{8, 10, 9, 0, 10, 10, 820}, // £
{8, 8, 9, 0, 9, 8, 830}, // ¤
{8, 10, 9, 0, 10, 10, 838}, // ¥
{2, 15, 3, 1, 11, 4, 848}, // ¦
{8, 15, 9, 0, 11, 15, 852}, // §
{5, 3, 5, 0, 11, 2, 867}, // ¨
{10, 11, 11, 0, 11, 14, 869}, // ©
{5, 6, 5, 0, 11, 4, 883}, // ª
{7, 7, 9, 1, 8, 7, 887}, // «
{8, 5, 9, 0, 8, 5, 894}, // ¬
{10, 11, 11, 0, 11, 14, 899}, // ®
{8, 2, 9, 0, 12, 2, 913}, // ¯
{4, 5, 5, 1, 11, 3, 915}, // °
{8, 9, 9, 0, 9, 9, 918}, // ±
{5, 8, 5, 0, 11, 5, 927}, // ²
{5, 6, 5, 0, 11, 4, 932}, // ³
{3, 3, 4, 1, 11, 2, 936}, // ´
{6, 12, 9, 2, 8, 9, 938}, // µ
{8, 14, 9, 0, 10, 14, 947}, // ¶
{2, 2, 3, 1, 6, 1, 961}, // ·
{4, 4, 4, 0, 0, 2, 962}, // ¸
{4, 7, 4, 0, 12, 4, 964}, // ¹
{6, 6, 6, 0, 11, 5, 968}, // º
{7, 7, 9, 1, 8, 7, 973}, // »
{12, 12, 13, 0, 12, 18, 980}, // ¼
{12, 11, 13, 0, 11, 17, 998}, // ½
{12, 11, 13, 0, 11, 17, 1015}, // ¾
{7, 12, 9, 1, 8, 11, 1032}, // ¿
{7, 13, 9, 1, 13, 12, 1043}, // À
{7, 13, 9, 1, 13, 12, 1055}, // Á
{7, 13, 9, 1, 13, 12, 1067}, // Â
{7, 13, 9, 1, 13, 12, 1079}, // Ã
{7, 12, 9, 1, 12, 11, 1091}, // Ä
{7, 13, 9, 1, 13, 12, 1102}, // Å
{13, 11, 14, 0, 11, 18, 1114}, // Æ
{9, 14, 11, 1, 10, 16, 1132}, // Ç
{7, 13, 9, 1, 13, 12, 1148}, // È
{7, 13, 9, 1, 13, 12, 1160}, // É
{7, 13, 9, 1, 13, 12, 1172}, // Ê
{7, 12, 9, 1, 12, 11, 1184}, // Ë
{3, 13, 3, 0, 13, 5, 1195}, // Ì
{3, 13, 4, 1, 13, 5, 1200}, // Í
{4, 13, 4, 0, 13, 7, 1205}, // Î
{4, 12, 4, 0, 12, 6, 1212}, // Ï
{9, 10, 10, 0, 10, 12, 1218}, // Ð
{8, 13, 10, 1, 13, 13, 1230}, // Ñ
{9, 13, 11, 1, 13, 15, 1243}, // Ò
{9, 13, 11, 1, 13, 15, 1258}, // Ó
{9, 13, 11, 1, 13, 15, 1273}, // Ô
{9, 13, 11, 1, 13, 15, 1288}, // Õ
{9, 12, 11, 1, 12, 14, 1303}, // Ö
{6, 6, 7, 1, 8, 5, 1317}, // ×
{10, 10, 11, 0, 10, 13, 1322}, // Ø
{8, 13, 10, 1, 13, 13, 1335}, // Ù
{8, 13, 10, 1, 13, 13, 1348}, // Ú
{8, 13, 10, 1, 13, 13, 1361}, // Û
{8, 12, 10, 1, 12, 12, 1374}, // Ü
{8, 13, 10, 1, 13, 13, 1386}, // Ý
{8, 10, 10, 1, 10, 10, 1399}, // Þ
{7, 11, 9, 1, 11, 10, 1409}, // ß
{7, 11, 7, 0, 11, 10, 1419}, // à
{7, 11, 7, 0, 11, 10, 1429}, // á
{7, 11, 7, 0, 11, 10, 1439}, // â
{7, 11, 7, 0, 11, 10, 1449}, // ã
{7, 10, 7, 0, 10, 9, 1459}, // ä
{7, 12, 7, 0, 12, 11, 1468}, // å
{12, 8, 13, 0, 8, 12, 1479}, // æ
{7, 12, 7, 0, 8, 11, 1491}, // ç
{8, 11, 9, 0, 11, 11, 1502}, // è
{8, 11, 9, 0, 11, 11, 1513}, // é
{8, 11, 9, 0, 11, 11, 1524}, // ê
{8, 10, 9, 0, 10, 10, 1535}, // ë
{3, 11, 3, 0, 11, 5, 1545}, // ì
{3, 11, 4, 1, 11, 5, 1550}, // í
{4, 11, 4, 0, 11, 6, 1555}, // î
{4, 10, 4, 0, 10, 5, 1561}, // ï
{8, 11, 9, 0, 11, 11, 1566}, // ð
{6, 11, 7, 1, 11, 9, 1577}, // ñ
{8, 11, 9, 0, 11, 11, 1586}, // ò
{8, 11, 9, 0, 11, 11, 1597}, // ó
{8, 11, 9, 0, 11, 11, 1608}, // ô
{8, 11, 9, 0, 11, 11, 1619}, // õ
{8, 10, 9, 0, 10, 10, 1630}, // ö
{8, 6, 9, 0, 8, 6, 1640}, // ÷
{8, 8, 9, 0, 8, 8, 1646}, // ø
{6, 11, 7, 1, 11, 9, 1654}, // ù
{6, 11, 7, 1, 11, 9, 1663}, // ú
{6, 11, 7, 1, 11, 9, 1672}, // û
{6, 10, 7, 1, 10, 8, 1681}, // ü
{8, 14, 9, 0, 11, 14, 1689}, // ý
{7, 13, 9, 1, 10, 12, 1703}, // þ
{8, 13, 9, 0, 10, 13, 1715}, // ÿ
{7, 12, 9, 1, 12, 11, 1728}, // Ā
{7, 10, 7, 0, 10, 9, 1739}, // ā
{7, 13, 9, 1, 13, 12, 1748}, // Ă
{7, 11, 7, 0, 11, 10, 1760}, // ă
{8, 14, 10, 1, 10, 14, 1770}, // Ą
{8, 12, 9, 0, 8, 12, 1784}, // ą
{9, 13, 11, 1, 13, 15, 1796}, // Ć
{7, 11, 7, 0, 11, 10, 1811}, // ć
{9, 13, 11, 1, 13, 15, 1821}, // Ĉ
{7, 11, 7, 0, 11, 10, 1836}, // ĉ
{9, 12, 11, 1, 12, 14, 1846}, // Ċ
{7, 10, 7, 0, 10, 9, 1860}, // ċ
{9, 13, 11, 1, 13, 15, 1869}, // Č
{7, 11, 7, 0, 11, 10, 1884}, // č
{8, 13, 10, 1, 13, 13, 1894}, // Ď
{9, 11, 10, 0, 11, 13, 1907}, // ď
{9, 10, 10, 0, 10, 12, 1920}, // Đ
{8, 11, 9, 0, 11, 11, 1932}, // đ
{7, 12, 9, 1, 12, 11, 1943}, // Ē
{8, 10, 9, 0, 10, 10, 1954}, // ē
{7, 13, 9, 1, 13, 12, 1964}, // Ĕ
{8, 11, 9, 0, 11, 11, 1976}, // ĕ
{7, 12, 9, 1, 12, 11, 1987}, // Ė
{8, 10, 9, 0, 10, 10, 1998}, // ė
{8, 14, 10, 1, 10, 14, 2008}, // Ę
{8, 12, 9, 0, 8, 12, 2022}, // ę
{7, 13, 9, 1, 13, 12, 2034}, // Ě
{8, 11, 9, 0, 11, 11, 2046}, // ě
{9, 13, 11, 1, 13, 15, 2057}, // Ĝ
{7, 15, 7, 0, 11, 14, 2072}, // ĝ
{9, 13, 11, 1, 13, 15, 2086}, // Ğ
{7, 15, 7, 0, 11, 14, 2101}, // ğ
{9, 12, 11, 1, 12, 14, 2115}, // Ġ
{7, 14, 7, 0, 10, 13, 2129}, // ġ
{9, 14, 11, 1, 10, 16, 2142}, // Ģ
{7, 15, 7, 0, 11, 14, 2158}, // ģ
{8, 13, 10, 1, 13, 13, 2172}, // Ĥ
{6, 13, 7, 1, 13, 10, 2185}, // ĥ
{10, 10, 11, 0, 10, 13, 2195}, // Ħ
{7, 11, 7, 0, 11, 10, 2208}, // ħ
{5, 13, 5, 0, 13, 9, 2218}, // Ĩ
{5, 11, 5, 0, 11, 7, 2227}, // ĩ
{4, 12, 4, 0, 12, 6, 2234}, // Ī
{4, 10, 4, 0, 10, 5, 2240}, // ī
{5, 13, 5, 0, 13, 9, 2245}, // Ĭ
{5, 11, 5, 0, 11, 7, 2254}, // ĭ
{4, 14, 4, 0, 10, 7, 2261}, // Į
{4, 14, 4, 0, 10, 7, 2268}, // į
{2, 12, 3, 1, 12, 3, 2275}, // İ
{2, 8, 3, 1, 8, 2, 2278}, // ı
{8, 10, 10, 1, 10, 10, 2280}, // IJ
{5, 14, 6, 1, 10, 9, 2290}, // ij
{7, 13, 7, 0, 13, 12, 2299}, // Ĵ
{4, 15, 4, 0, 11, 8, 2311}, // ĵ
{8, 14, 10, 1, 10, 14, 2319}, // Ķ
{6, 14, 7, 1, 10, 11, 2333}, // ķ
{6, 8, 7, 1, 8, 6, 2344}, // ĸ
{7, 13, 9, 1, 13, 12, 2350}, // Ĺ
{3, 13, 4, 1, 13, 5, 2362}, // ĺ
{7, 14, 9, 1, 10, 13, 2367}, // Ļ
{4, 14, 4, 0, 10, 7, 2380}, // ļ
{7, 11, 9, 1, 11, 10, 2387}, // Ľ
{4, 11, 5, 1, 11, 6, 2397}, // ľ
{7, 10, 9, 1, 10, 9, 2403}, // Ŀ
{4, 10, 5, 1, 10, 5, 2412}, // ŀ
{8, 10, 9, 0, 10, 10, 2417}, // Ł
{4, 10, 4, 0, 10, 5, 2427}, // ł
{8, 13, 10, 1, 13, 13, 2432}, // Ń
{6, 12, 7, 1, 12, 9, 2445}, // ń
{8, 14, 10, 1, 10, 14, 2454}, // Ņ
{6, 12, 7, 1, 8, 9, 2468}, // ņ
{8, 13, 10, 1, 13, 13, 2477}, // Ň
{6, 11, 7, 1, 11, 9, 2490}, // ň
{7, 11, 7, 0, 11, 10, 2499}, // ʼn
{8, 13, 10, 1, 10, 13, 2509}, // Ŋ
{6, 12, 7, 1, 8, 9, 2522}, // ŋ
{9, 12, 11, 1, 12, 14, 2531}, // Ō
{8, 10, 9, 0, 10, 10, 2545}, // ō
{9, 13, 11, 1, 13, 15, 2555}, // Ŏ
{8, 11, 9, 0, 11, 11, 2570}, // ŏ
{9, 13, 11, 1, 13, 15, 2581}, // Ő
{8, 11, 9, 0, 11, 11, 2596}, // ő
{13, 11, 15, 1, 11, 18, 2607}, // Œ
{13, 8, 14, 0, 8, 13, 2625}, // œ
{8, 13, 10, 1, 13, 13, 2638}, // Ŕ
{5, 11, 6, 1, 11, 7, 2651}, // ŕ
{8, 14, 10, 1, 10, 14, 2658}, // Ŗ
{5, 12, 6, 1, 8, 8, 2672}, // ŗ
{8, 13, 10, 1, 13, 13, 2680}, // Ř
{5, 11, 6, 1, 11, 7, 2693}, // ř
{7, 13, 9, 1, 13, 12, 2700}, // Ś
{7, 11, 7, 0, 11, 10, 2712}, // ś
{7, 13, 9, 1, 13, 12, 2722}, // Ŝ
{7, 11, 7, 0, 11, 10, 2734}, // ŝ
{7, 14, 9, 1, 10, 13, 2744}, // Ş
{7, 12, 7, 0, 8, 11, 2757}, // ş
{7, 13, 9, 1, 13, 12, 2768}, // Š
{7, 11, 7, 0, 11, 10, 2780}, // š
{9, 14, 10, 0, 10, 16, 2790}, // Ţ
{5, 14, 5, 0, 10, 9, 2806}, // ţ
{8, 13, 9, 0, 13, 13, 2815}, // Ť
{6, 11, 6, 0, 11, 9, 2828}, // ť
{8, 10, 9, 0, 10, 10, 2837}, // Ŧ
{5, 10, 5, 0, 10, 7, 2847}, // ŧ
{8, 13, 10, 1, 13, 13, 2854}, // Ũ
{6, 11, 7, 1, 11, 9, 2867}, // ũ
{8, 12, 10, 1, 12, 12, 2876}, // Ū
{6, 10, 7, 1, 10, 8, 2888}, // ū
{8, 13, 10, 1, 13, 13, 2896}, // Ŭ
{6, 11, 7, 1, 11, 9, 2909}, // ŭ
{8, 13, 10, 1, 13, 13, 2918}, // Ů
{6, 11, 7, 1, 11, 9, 2931}, // ů
{8, 13, 10, 1, 13, 13, 2940}, // Ű
{6, 11, 7, 1, 11, 9, 2953}, // ű
{8, 14, 10, 1, 10, 14, 2962}, // Ų
{7, 12, 9, 1, 8, 11, 2976}, // ų
{14, 13, 15, 0, 13, 23, 2987}, // Ŵ
{10, 11, 11, 0, 11, 14, 3010}, // ŵ
{8, 13, 10, 1, 13, 13, 3024}, // Ŷ
{8, 14, 9, 0, 11, 14, 3037}, // ŷ
{8, 12, 10, 1, 12, 12, 3051}, // Ÿ
{8, 13, 9, 0, 13, 13, 3063}, // Ź
{7, 11, 7, 0, 11, 10, 3076}, // ź
{8, 12, 9, 0, 12, 12, 3086}, // Ż
{7, 10, 7, 0, 10, 9, 3098}, // ż
{8, 13, 9, 0, 13, 13, 3107}, // Ž
{7, 11, 7, 0, 11, 10, 3120}, // ž
{8, 10, 9, 0, 10, 10, 3130}, // €
};
static const EpdUnicodeInterval babyblueIntervals[] = {
{0x20, 0x7E, 0x0}, {0xA1, 0xAC, 0x5F}, {0xAE, 0xFF, 0x6B}, {0x100, 0x17E, 0xBD}, {0x20AC, 0x20AC, 0x13C},
};
static const EpdFontData babyblue = {
babyblueBitmaps, babyblueGlyphs, babyblueIntervals, 5, 17, 13, -4, false,
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -176,7 +176,7 @@ for i_start, i_end in intervals:
px = 0 px = 0
if is2Bit: if is2Bit:
# 0 = white, 15 black, 8+ dark grey, 7- light grey # 0-3 white, 4-7 light grey, 8-11 dark grey, 12-15 black
# Downsample to 2-bit bitmap # Downsample to 2-bit bitmap
pixels2b = [] pixels2b = []
px = 0 px = 0
@ -187,11 +187,11 @@ for i_start, i_end in intervals:
bm = pixels4g[y * pitch + (x // 2)] bm = pixels4g[y * pitch + (x // 2)]
bm = (bm >> ((x % 2) * 4)) & 0xF bm = (bm >> ((x % 2) * 4)) & 0xF
if bm == 15: if bm >= 12:
px += 3 px += 3
elif bm >= 8: elif bm >= 8:
px += 2 px += 2
elif bm > 0: elif bm >= 4:
px += 1 px += 1
if (y * bitmap.width + x) % 4 == 3: if (y * bitmap.width + x) % 4 == 3:
@ -211,7 +211,7 @@ for i_start, i_end in intervals:
# print(line) # print(line)
# print('') # print('')
else: else:
# Downsample to 1-bit bitmap - treat any non-zero as black # Downsample to 1-bit bitmap - treat any 2+ as black
pixelsbw = [] pixelsbw = []
px = 0 px = 0
pitch = (bitmap.width // 2) + (bitmap.width % 2) pitch = (bitmap.width // 2) + (bitmap.width % 2)
@ -219,7 +219,7 @@ for i_start, i_end in intervals:
for x in range(bitmap.width): for x in range(bitmap.width):
px = px << 1 px = px << 1
bm = pixels4g[y * pitch + (x // 2)] bm = pixels4g[y * pitch + (x // 2)]
px += 1 if ((x & 1) == 0 and bm & 0xF > 0) or ((x & 1) == 1 and bm & 0xF0 > 0) else 0 px += 1 if ((x & 1) == 0 and bm & 0xE > 0) or ((x & 1) == 1 and bm & 0xE0 > 0) else 0
if (y * bitmap.width + x) % 8 == 7: if (y * bitmap.width + x) % 8 == 7:
pixelsbw.append(px) pixelsbw.append(px)

View File

@ -73,10 +73,8 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
coverImageItem = opfParser.items.at(opfParser.coverItemId); coverImageItem = opfParser.items.at(opfParser.coverItemId);
} }
if (opfParser.items.count("ncx")) { if (!opfParser.tocNcxPath.empty()) {
tocNcxItem = opfParser.items.at("ncx"); tocNcxItem = opfParser.tocNcxPath;
} else if (opfParser.items.count("ncxtoc")) {
tocNcxItem = opfParser.items.at("ncxtoc");
} }
for (auto& spineRef : opfParser.spineRefs) { for (auto& spineRef : opfParser.spineRefs) {
@ -150,11 +148,54 @@ bool Epub::load() {
return false; return false;
} }
initializeSpineItemSizes();
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str()); Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
return true; return true;
} }
void Epub::initializeSpineItemSizes() {
setupCacheDir();
size_t spineItemsCount = getSpineItemsCount();
size_t cumSpineItemSize = 0;
if (SD.exists((getCachePath() + "/spine_size.bin").c_str())) {
File f = SD.open((getCachePath() + "/spine_size.bin").c_str());
uint8_t data[4];
for (size_t i = 0; i < spineItemsCount; i++) {
f.read(data, 4);
cumSpineItemSize = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
cumulativeSpineItemSize.emplace_back(cumSpineItemSize);
// Serial.printf("[%lu] [EBP] Loading item %d size %u to %u %u\n", millis(),
// i, cumSpineItemSize, data[1], data[0]);
}
f.close();
} else {
File f = SD.open((getCachePath() + "/spine_size.bin").c_str(), FILE_WRITE);
uint8_t data[4];
// determine size of spine items
for (size_t i = 0; i < spineItemsCount; i++) {
std::string spineItem = getSpineItem(i);
size_t s = 0;
getItemSize(spineItem, &s);
cumSpineItemSize += s;
cumulativeSpineItemSize.emplace_back(cumSpineItemSize);
// and persist to cache
data[0] = cumSpineItemSize & 0xFF;
data[1] = (cumSpineItemSize >> 8) & 0xFF;
data[2] = (cumSpineItemSize >> 16) & 0xFF;
data[3] = (cumSpineItemSize >> 24) & 0xFF;
// Serial.printf("[%lu] [EBP] Persisting item %d size %u to %u %u\n", millis(),
// i, cumSpineItemSize, data[1], data[0]);
f.write(data, 4);
}
f.close();
}
Serial.printf("[%lu] [EBP] Book size: %lu\n", millis(), cumSpineItemSize);
}
bool Epub::clearCache() const { bool Epub::clearCache() const {
if (!SD.exists(cachePath.c_str())) { if (!SD.exists(cachePath.c_str())) {
Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis()); Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis());
@ -257,6 +298,8 @@ bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
int Epub::getSpineItemsCount() const { return spine.size(); } int Epub::getSpineItemsCount() const { return spine.size(); }
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return cumulativeSpineItemSize.at(spineIndex); }
std::string& Epub::getSpineItem(const int spineIndex) { std::string& Epub::getSpineItem(const int spineIndex) {
if (spineIndex < 0 || spineIndex >= spine.size()) { if (spineIndex < 0 || spineIndex >= spine.size()) {
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex); Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
@ -279,6 +322,11 @@ int Epub::getTocItemsCount() const { return toc.size(); }
// work out the section index for a toc index // work out the section index for a toc index
int Epub::getSpineIndexForTocIndex(const int tocIndex) const { int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
if (tocIndex < 0 || tocIndex >= toc.size()) {
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex: tocIndex %d out of range\n", millis(), tocIndex);
return 0;
}
// the toc entry should have an href that matches the spine item // the toc entry should have an href that matches the spine item
// so we can find the spine index by looking for the href // so we can find the spine index by looking for the href
for (int i = 0; i < spine.size(); i++) { for (int i = 0; i < spine.size(); i++) {
@ -293,6 +341,11 @@ int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
} }
int Epub::getTocIndexForSpineIndex(const int spineIndex) const { int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
if (spineIndex < 0 || spineIndex >= spine.size()) {
Serial.printf("[%lu] [EBP] getTocIndexForSpineIndex: spineIndex %d out of range\n", millis(), spineIndex);
return -1;
}
// the toc entry should have an href that matches the spine item // the toc entry should have an href that matches the spine item
// so we can find the toc index by looking for the href // so we can find the toc index by looking for the href
for (int i = 0; i < toc.size(); i++) { for (int i = 0; i < toc.size(); i++) {
@ -304,3 +357,22 @@ int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
Serial.printf("[%lu] [EBP] TOC item not found\n", millis()); Serial.printf("[%lu] [EBP] TOC item not found\n", millis());
return -1; return -1;
} }
size_t Epub::getBookSize() const {
if (spine.empty()) {
return 0;
}
return getCumulativeSpineItemSize(getSpineItemsCount() - 1);
}
// Calculate progress in book
uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) {
size_t bookSize = getBookSize();
if (bookSize == 0) {
return 0;
}
size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
size_t sectionProgSize = currentSpineRead * curChapterSize;
return round(static_cast<float>(prevChapterSize + sectionProgSize) / bookSize * 100.0);
}

View File

@ -20,6 +20,8 @@ class Epub {
std::string filepath; std::string filepath;
// the spine of the EPUB file // the spine of the EPUB file
std::vector<std::pair<std::string, std::string>> spine; std::vector<std::pair<std::string, std::string>> spine;
// the file size of the spine items (proxy to book progress)
std::vector<size_t> cumulativeSpineItemSize;
// the toc of the EPUB file // the toc of the EPUB file
std::vector<EpubTocEntry> toc; std::vector<EpubTocEntry> toc;
// the base path for items in the EPUB file // the base path for items in the EPUB file
@ -30,6 +32,7 @@ class Epub {
bool findContentOpfFile(std::string* contentOpfFile) const; bool findContentOpfFile(std::string* contentOpfFile) const;
bool parseContentOpf(const std::string& contentOpfFilePath); bool parseContentOpf(const std::string& contentOpfFilePath);
bool parseTocNcxFile(); bool parseTocNcxFile();
void initializeSpineItemSizes();
public: public:
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) { explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
@ -51,8 +54,12 @@ class Epub {
bool getItemSize(const std::string& itemHref, size_t* size) const; bool getItemSize(const std::string& itemHref, size_t* size) const;
std::string& getSpineItem(int spineIndex); std::string& getSpineItem(int spineIndex);
int getSpineItemsCount() const; int getSpineItemsCount() const;
EpubTocEntry& getTocItem(int tocTndex); size_t getCumulativeSpineItemSize(const int spineIndex) const;
EpubTocEntry& getTocItem(int tocIndex);
int getTocItemsCount() const; int getTocItemsCount() const;
int getSpineIndexForTocIndex(int tocIndex) const; int getSpineIndexForTocIndex(int tocIndex) const;
int getTocIndexForSpineIndex(int spineIndex) const; int getTocIndexForSpineIndex(int spineIndex) const;
size_t getBookSize() const;
uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead);
}; };

View File

@ -3,7 +3,9 @@
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <Serialization.h> #include <Serialization.h>
namespace {
constexpr uint8_t PAGE_FILE_VERSION = 3; constexpr uint8_t PAGE_FILE_VERSION = 3;
}
void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); } void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); }

View File

@ -27,12 +27,16 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
const size_t totalWordCount = words.size(); const size_t totalWordCount = words.size();
const int pageWidth = renderer.getScreenWidth() - horizontalMargin; const int pageWidth = renderer.getScreenWidth() - horizontalMargin;
const int spaceWidth = renderer.getSpaceWidth(fontId); const int spaceWidth = renderer.getSpaceWidth(fontId);
// width of 1em to indent first line of paragraph if Extra Spacing is enabled
const int indentWidth = (!extraParagraphSpacing) ? 1 * renderer.getTextWidth(fontId, "m", REGULAR) : 0;
std::vector<uint16_t> wordWidths; std::vector<uint16_t> wordWidths;
wordWidths.reserve(totalWordCount); wordWidths.reserve(totalWordCount);
// add em-space at the beginning of first word in paragraph to indent
if (!extraParagraphSpacing) {
std::string& first_word = words.front();
first_word.insert(0, "\xe2\x80\x83");
}
auto wordsIt = words.begin(); auto wordsIt = words.begin();
auto wordStylesIt = wordStyles.begin(); auto wordStylesIt = wordStyles.begin();
@ -53,7 +57,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
ans[totalWordCount - 1] = totalWordCount - 1; ans[totalWordCount - 1] = totalWordCount - 1;
for (int i = totalWordCount - 2; i >= 0; --i) { for (int i = totalWordCount - 2; i >= 0; --i) {
int currlen = -spaceWidth + indentWidth; int currlen = -spaceWidth;
dp[i] = MAX_COST; dp[i] = MAX_COST;
for (size_t j = i; j < totalWordCount; ++j) { for (size_t j = i; j < totalWordCount; ++j) {
@ -125,9 +129,6 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
// Calculate spacing // Calculate spacing
int spareSpace = pageWidth - lineWordWidthSum; int spareSpace = pageWidth - lineWordWidthSum;
if (wordWidthIndex == 0) {
spareSpace -= indentWidth;
}
int spacing = spaceWidth; int spacing = spaceWidth;
const bool isLastLine = lineBreak == totalWordCount; const bool isLastLine = lineBreak == totalWordCount;
@ -137,8 +138,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
} }
// Calculate initial x position // Calculate initial x position
uint16_t xpos = (wordWidthIndex == 0) ? indentWidth : 0; uint16_t xpos = 0;
if (style == TextBlock::RIGHT_ALIGN) { if (style == TextBlock::RIGHT_ALIGN) {
xpos = spareSpace - (lineWordCount - 1) * spaceWidth; xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
} else if (style == TextBlock::CENTER_ALIGN) { } else if (style == TextBlock::CENTER_ALIGN) {

View File

@ -9,7 +9,9 @@
#include "Page.h" #include "Page.h"
#include "parsers/ChapterHtmlSlimParser.h" #include "parsers/ChapterHtmlSlimParser.h"
namespace {
constexpr uint8_t SECTION_FILE_VERSION = 5; constexpr uint8_t SECTION_FILE_VERSION = 5;
}
void Section::onPageComplete(std::unique_ptr<Page> page) { void Section::onPageComplete(std::unique_ptr<Page> page) {
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin"; const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";

View File

@ -75,6 +75,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
return; return;
} }
// Skip blocks with role="doc-pagebreak" and epub:type="pagebreak"
if (atts != nullptr) {
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "role") == 0 && strcmp(atts[i + 1], "doc-pagebreak") == 0 ||
strcmp(atts[i], "epub:type") == 0 && strcmp(atts[i + 1], "pagebreak") == 0) {
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
}
}
}
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) { if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
self->startNewTextBlock(TextBlock::CENTER_ALIGN); self->startNewTextBlock(TextBlock::CENTER_ALIGN);
self->boldUntilDepth = min(self->boldUntilDepth, self->depth); self->boldUntilDepth = min(self->boldUntilDepth, self->depth);

View File

@ -3,6 +3,10 @@
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <ZipFile.h> #include <ZipFile.h>
namespace {
constexpr const char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
}
bool ContentOpfParser::setup() { bool ContentOpfParser::setup() {
parser = XML_ParserCreate(nullptr); parser = XML_ParserCreate(nullptr);
if (!parser) { if (!parser) {
@ -111,16 +115,28 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
if (self->state == IN_MANIFEST && (strcmp(name, "item") == 0 || strcmp(name, "opf:item") == 0)) { if (self->state == IN_MANIFEST && (strcmp(name, "item") == 0 || strcmp(name, "opf:item") == 0)) {
std::string itemId; std::string itemId;
std::string href; std::string href;
std::string mediaType;
for (int i = 0; atts[i]; i += 2) { for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "id") == 0) { if (strcmp(atts[i], "id") == 0) {
itemId = atts[i + 1]; itemId = atts[i + 1];
} else if (strcmp(atts[i], "href") == 0) { } else if (strcmp(atts[i], "href") == 0) {
href = self->baseContentPath + atts[i + 1]; href = self->baseContentPath + atts[i + 1];
} else if (strcmp(atts[i], "media-type") == 0) {
mediaType = atts[i + 1];
} }
} }
self->items[itemId] = href; self->items[itemId] = href;
if (mediaType == MEDIA_TYPE_NCX) {
if (self->tocNcxPath.empty()) {
self->tocNcxPath = href;
} else {
Serial.printf("[%lu] [COF] Warning: Multiple NCX files found in manifest. Ignoring duplicate: %s\n", millis(),
href.c_str());
}
}
return; return;
} }

189
lib/GfxRenderer/Bitmap.cpp Normal file
View File

@ -0,0 +1,189 @@
#include "Bitmap.h"
#include <cstdlib>
#include <cstring>
uint16_t Bitmap::readLE16(File& f) {
const int c0 = f.read();
const int c1 = f.read();
const auto b0 = static_cast<uint8_t>(c0 < 0 ? 0 : c0);
const auto b1 = static_cast<uint8_t>(c1 < 0 ? 0 : c1);
return static_cast<uint16_t>(b0) | (static_cast<uint16_t>(b1) << 8);
}
uint32_t Bitmap::readLE32(File& f) {
const int c0 = f.read();
const int c1 = f.read();
const int c2 = f.read();
const int c3 = f.read();
const auto b0 = static_cast<uint8_t>(c0 < 0 ? 0 : c0);
const auto b1 = static_cast<uint8_t>(c1 < 0 ? 0 : c1);
const auto b2 = static_cast<uint8_t>(c2 < 0 ? 0 : c2);
const auto b3 = static_cast<uint8_t>(c3 < 0 ? 0 : c3);
return static_cast<uint32_t>(b0) | (static_cast<uint32_t>(b1) << 8) | (static_cast<uint32_t>(b2) << 16) |
(static_cast<uint32_t>(b3) << 24);
}
const char* Bitmap::errorToString(BmpReaderError err) {
switch (err) {
case BmpReaderError::Ok:
return "Ok";
case BmpReaderError::FileInvalid:
return "FileInvalid";
case BmpReaderError::SeekStartFailed:
return "SeekStartFailed";
case BmpReaderError::NotBMP:
return "NotBMP (missing 'BM')";
case BmpReaderError::DIBTooSmall:
return "DIBTooSmall (<40 bytes)";
case BmpReaderError::BadPlanes:
return "BadPlanes (!= 1)";
case BmpReaderError::UnsupportedBpp:
return "UnsupportedBpp (expected 1, 2, 8, 24, or 32)";
case BmpReaderError::UnsupportedCompression:
return "UnsupportedCompression (expected BI_RGB or BI_BITFIELDS for 32bpp)";
case BmpReaderError::BadDimensions:
return "BadDimensions";
case BmpReaderError::PaletteTooLarge:
return "PaletteTooLarge";
case BmpReaderError::SeekPixelDataFailed:
return "SeekPixelDataFailed";
case BmpReaderError::BufferTooSmall:
return "BufferTooSmall";
case BmpReaderError::OomRowBuffer:
return "OomRowBuffer";
case BmpReaderError::ShortReadRow:
return "ShortReadRow";
}
return "Unknown";
}
BmpReaderError Bitmap::parseHeaders() {
if (!file) return BmpReaderError::FileInvalid;
if (!file.seek(0)) return BmpReaderError::SeekStartFailed;
// --- BMP FILE HEADER ---
const uint16_t bfType = readLE16(file);
if (bfType != 0x4D42) return BmpReaderError::NotBMP;
file.seek(8, SeekCur);
bfOffBits = readLE32(file);
// --- DIB HEADER ---
const uint32_t biSize = readLE32(file);
if (biSize < 40) return BmpReaderError::DIBTooSmall;
width = static_cast<int32_t>(readLE32(file));
const auto rawHeight = static_cast<int32_t>(readLE32(file));
topDown = rawHeight < 0;
height = topDown ? -rawHeight : rawHeight;
const uint16_t planes = readLE16(file);
bpp = readLE16(file);
const uint32_t comp = readLE32(file);
const bool validBpp = bpp == 1 || bpp == 2 || bpp == 8 || bpp == 24 || bpp == 32;
if (planes != 1) return BmpReaderError::BadPlanes;
if (!validBpp) return BmpReaderError::UnsupportedBpp;
// Allow BI_RGB (0) for all, and BI_BITFIELDS (3) for 32bpp which is common for BGRA masks.
if (!(comp == 0 || (bpp == 32 && comp == 3))) return BmpReaderError::UnsupportedCompression;
file.seek(12, SeekCur); // biSizeImage, biXPelsPerMeter, biYPelsPerMeter
const uint32_t colorsUsed = readLE32(file);
if (colorsUsed > 256u) return BmpReaderError::PaletteTooLarge;
file.seek(4, SeekCur); // biClrImportant
if (width <= 0 || height <= 0) return BmpReaderError::BadDimensions;
// Pre-calculate Row Bytes to avoid doing this every row
rowBytes = (width * bpp + 31) / 32 * 4;
for (int i = 0; i < 256; i++) paletteLum[i] = static_cast<uint8_t>(i);
if (colorsUsed > 0) {
for (uint32_t i = 0; i < colorsUsed; i++) {
uint8_t rgb[4];
file.read(rgb, 4); // Read B, G, R, Reserved in one go
paletteLum[i] = (77u * rgb[2] + 150u * rgb[1] + 29u * rgb[0]) >> 8;
}
}
if (!file.seek(bfOffBits)) {
return BmpReaderError::SeekPixelDataFailed;
}
return BmpReaderError::Ok;
}
// packed 2bpp output, 0 = black, 1 = dark gray, 2 = light gray, 3 = white
BmpReaderError Bitmap::readRow(uint8_t* data, uint8_t* rowBuffer) const {
// Note: rowBuffer should be pre-allocated by the caller to size 'rowBytes'
if (file.read(rowBuffer, rowBytes) != rowBytes) return BmpReaderError::ShortReadRow;
uint8_t* outPtr = data;
uint8_t currentOutByte = 0;
int bitShift = 6;
// Helper lambda to pack 2bpp color into the output stream
auto packPixel = [&](uint8_t lum) {
uint8_t color = (lum >> 6); // Simple 2-bit reduction: 0-255 -> 0-3
currentOutByte |= (color << bitShift);
if (bitShift == 0) {
*outPtr++ = currentOutByte;
currentOutByte = 0;
bitShift = 6;
} else {
bitShift -= 2;
}
};
switch (bpp) {
case 8: {
for (int x = 0; x < width; x++) {
packPixel(paletteLum[rowBuffer[x]]);
}
break;
}
case 24: {
const uint8_t* p = rowBuffer;
for (int x = 0; x < width; x++) {
uint8_t lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
packPixel(lum);
p += 3;
}
break;
}
case 1: {
for (int x = 0; x < width; x++) {
uint8_t lum = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 0xFF : 0x00;
packPixel(lum);
}
break;
}
case 32: {
const uint8_t* p = rowBuffer;
for (int x = 0; x < width; x++) {
uint8_t lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
packPixel(lum);
p += 4;
}
break;
}
}
// Flush remaining bits if width is not a multiple of 4
if (bitShift != 6) *outPtr = currentOutByte;
return BmpReaderError::Ok;
}
BmpReaderError Bitmap::rewindToData() const {
if (!file.seek(bfOffBits)) {
return BmpReaderError::SeekPixelDataFailed;
}
return BmpReaderError::Ok;
}

52
lib/GfxRenderer/Bitmap.h Normal file
View File

@ -0,0 +1,52 @@
#pragma once
#include <FS.h>
enum class BmpReaderError : uint8_t {
Ok = 0,
FileInvalid,
SeekStartFailed,
NotBMP,
DIBTooSmall,
BadPlanes,
UnsupportedBpp,
UnsupportedCompression,
BadDimensions,
PaletteTooLarge,
SeekPixelDataFailed,
BufferTooSmall,
OomRowBuffer,
ShortReadRow,
};
class Bitmap {
public:
static const char* errorToString(BmpReaderError err);
explicit Bitmap(File& file) : file(file) {}
BmpReaderError parseHeaders();
BmpReaderError readRow(uint8_t* data, uint8_t* rowBuffer) const;
BmpReaderError rewindToData() const;
int getWidth() const { return width; }
int getHeight() const { return height; }
bool isTopDown() const { return topDown; }
bool hasGreyscale() const { return bpp > 1; }
int getRowBytes() const { return rowBytes; }
private:
static uint16_t readLE16(File& f);
static uint32_t readLE32(File& f);
File& file;
int width = 0;
int height = 0;
bool topDown = false;
uint32_t bfOffBits = 0;
uint16_t bpp = 0;
int rowBytes = 0;
uint8_t paletteLum[256] = {};
};

View File

@ -119,6 +119,66 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
einkDisplay.drawImage(bitmap, y, x, height, width); einkDisplay.drawImage(bitmap, y, x, height, width);
} }
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth,
const int maxHeight) const {
float scale = 1.0f;
bool isScaled = false;
if (maxWidth > 0 && bitmap.getWidth() > maxWidth) {
scale = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
isScaled = true;
}
if (maxHeight > 0 && bitmap.getHeight() > maxHeight) {
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight()));
isScaled = true;
}
const uint8_t outputRowSize = (bitmap.getWidth() + 3) / 4;
auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize));
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
// The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative).
// Screen's (0, 0) is the top-left corner.
int screenY = y + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY);
if (isScaled) {
screenY = std::floor(screenY * scale);
}
if (screenY >= getScreenHeight()) {
break;
}
if (bitmap.readRow(outputRow, rowBytes) != BmpReaderError::Ok) {
Serial.printf("[%lu] [GFX] Failed to read row %d from bitmap\n", millis(), bmpY);
free(outputRow);
free(rowBytes);
return;
}
for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) {
int screenX = x + bmpX;
if (isScaled) {
screenX = std::floor(screenX * scale);
}
if (screenX >= getScreenWidth()) {
break;
}
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
if (renderMode == BW && val < 3) {
drawPixel(screenX, screenY);
} else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) {
drawPixel(screenX, screenY, false);
} else if (renderMode == GRAYSCALE_LSB && val == 1) {
drawPixel(screenX, screenY, false);
}
}
}
free(outputRow);
free(rowBytes);
}
void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); } void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); }
void GfxRenderer::invertScreen() const { void GfxRenderer::invertScreen() const {
@ -132,13 +192,19 @@ void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) cons
einkDisplay.displayBuffer(refreshMode); einkDisplay.displayBuffer(refreshMode);
} }
// TODO: Support partial window update void GfxRenderer::displayWindow(const int x, const int y, const int width, const int height) const {
// void GfxRenderer::flushArea(const int x, const int y, const int width, const int height) const { // Rotate coordinates from portrait (480x800) to landscape (800x480)
// const int rotatedX = y; // Rotation: 90 degrees clockwise
// const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x; // Portrait coordinates: (x, y) with dimensions (width, height)
// // Landscape coordinates: (rotatedX, rotatedY) with dimensions (rotatedWidth, rotatedHeight)
// einkDisplay.displayBuffer(EInkDisplay::FAST_REFRESH, rotatedX, rotatedY, height, width);
// } const int rotatedX = y;
const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x - width + 1;
const int rotatedWidth = height;
const int rotatedHeight = width;
einkDisplay.displayWindow(rotatedX, rotatedY, rotatedWidth, rotatedHeight);
}
// Note: Internal driver treats screen in command orientation, this library treats in portrait orientation // Note: Internal driver treats screen in command orientation, this library treats in portrait orientation
int GfxRenderer::getScreenWidth() { return EInkDisplay::DISPLAY_HEIGHT; } int GfxRenderer::getScreenWidth() { return EInkDisplay::DISPLAY_HEIGHT; }
@ -164,7 +230,7 @@ int GfxRenderer::getLineHeight(const int fontId) const {
uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); } uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
void GfxRenderer::swapBuffers() const { einkDisplay.swapBuffers(); } size_t GfxRenderer::getBufferSize() { return EInkDisplay::BUFFER_SIZE; }
void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); } void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); }
@ -174,6 +240,90 @@ void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsb
void GfxRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); } void GfxRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); }
void GfxRenderer::freeBwBufferChunks() {
for (auto& bwBufferChunk : bwBufferChunks) {
if (bwBufferChunk) {
free(bwBufferChunk);
bwBufferChunk = nullptr;
}
}
}
/**
* This should be called before grayscale buffers are populated.
* A `restoreBwBuffer` call should always follow the grayscale render if this method was called.
* Uses chunked allocation to avoid needing 48KB of contiguous memory.
*/
void GfxRenderer::storeBwBuffer() {
const uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
// Allocate and copy each chunk
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
// Check if any chunks are already allocated
if (bwBufferChunks[i]) {
Serial.printf("[%lu] [GFX] !! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk\n",
millis(), i);
free(bwBufferChunks[i]);
bwBufferChunks[i] = nullptr;
}
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(BW_BUFFER_CHUNK_SIZE));
if (!bwBufferChunks[i]) {
Serial.printf("[%lu] [GFX] !! Failed to allocate BW buffer chunk %zu (%zu bytes)\n", millis(), i,
BW_BUFFER_CHUNK_SIZE);
// Free previously allocated chunks
freeBwBufferChunks();
return;
}
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
}
Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), BW_BUFFER_NUM_CHUNKS,
BW_BUFFER_CHUNK_SIZE);
}
/**
* This can only be called if `storeBwBuffer` was called prior to the grayscale render.
* It should be called to restore the BW buffer state after grayscale rendering is complete.
* Uses chunked restoration to match chunked storage.
*/
void GfxRenderer::restoreBwBuffer() {
// Check if any all chunks are allocated
bool missingChunks = false;
for (const auto& bwBufferChunk : bwBufferChunks) {
if (!bwBufferChunk) {
missingChunks = true;
break;
}
}
if (missingChunks) {
freeBwBufferChunks();
return;
}
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
// Check if chunk is missing
if (!bwBufferChunks[i]) {
Serial.printf("[%lu] [GFX] !! BW buffer chunks not stored - this is likely a bug\n", millis());
freeBwBufferChunks();
return;
}
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE);
}
einkDisplay.cleanupGrayscaleBuffers(frameBuffer);
freeBwBufferChunks();
Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis());
}
void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, int* x, const int* y, void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, int* x, const int* y,
const bool pixelState, const EpdFontStyle style) const { const bool pixelState, const EpdFontStyle style) const {
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style); const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);

View File

@ -2,19 +2,29 @@
#include <EInkDisplay.h> #include <EInkDisplay.h>
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <FS.h>
#include <map> #include <map>
#include "Bitmap.h"
class GfxRenderer { class GfxRenderer {
public: public:
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB }; enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
private: private:
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory
static constexpr size_t BW_BUFFER_NUM_CHUNKS = EInkDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_SIZE,
"BW buffer chunking does not line up with display buffer size");
EInkDisplay& einkDisplay; EInkDisplay& einkDisplay;
RenderMode renderMode; RenderMode renderMode;
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
std::map<int, EpdFontFamily> fontMap; std::map<int, EpdFontFamily> fontMap;
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState, void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
EpdFontStyle style) const; EpdFontStyle style) const;
void freeBwBufferChunks();
public: public:
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW) {} explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW) {}
@ -27,6 +37,8 @@ class GfxRenderer {
static int getScreenWidth(); static int getScreenWidth();
static int getScreenHeight(); static int getScreenHeight();
void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const; void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const;
// EXPERIMENTAL: Windowed update - display only a rectangular region (portrait coordinates)
void displayWindow(int x, int y, int width, int height) const;
void invertScreen() const; void invertScreen() const;
void clearScreen(uint8_t color = 0xFF) const; void clearScreen(uint8_t color = 0xFF) const;
@ -36,6 +48,7 @@ class GfxRenderer {
void drawRect(int x, int y, int width, int height, bool state = true) const; void drawRect(int x, int y, int width, int height, bool state = true) const;
void fillRect(int x, int y, int width, int height, bool state = true) const; void fillRect(int x, int y, int width, int height, bool state = true) const;
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const; void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
// Text // Text
int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const; int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const;
@ -49,9 +62,11 @@ class GfxRenderer {
void copyGrayscaleLsbBuffers() const; void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const; void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const; void displayGrayBuffer() const;
void storeBwBuffer();
void restoreBwBuffer();
// Low level functions // Low level functions
uint8_t* getFrameBuffer() const; uint8_t* getFrameBuffer() const;
void swapBuffers() const; static size_t getBufferSize();
void grayscaleRevert() const; void grayscaleRevert() const;
}; };

View File

@ -62,6 +62,10 @@ long ZipFile::getDataOffset(const mz_zip_archive_file_stat& fileStat) const {
const uint64_t fileOffset = fileStat.m_local_header_ofs; const uint64_t fileOffset = fileStat.m_local_header_ofs;
FILE* file = fopen(filePath.c_str(), "r"); FILE* file = fopen(filePath.c_str(), "r");
if (!file) {
Serial.printf("[%lu] [ZIP] Failed to open file for reading local header\n", millis());
return -1;
}
fseek(file, fileOffset, SEEK_SET); fseek(file, fileOffset, SEEK_SET);
const size_t read = fread(pLocalHeader, 1, localHeaderSize, file); const size_t read = fread(pLocalHeader, 1, localHeaderSize, file);
fclose(file); fclose(file);
@ -104,6 +108,10 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
} }
FILE* file = fopen(filePath.c_str(), "rb"); FILE* file = fopen(filePath.c_str(), "rb");
if (!file) {
Serial.printf("[%lu] [ZIP] Failed to open file for reading\n", millis());
return nullptr;
}
fseek(file, fileOffset, SEEK_SET); fseek(file, fileOffset, SEEK_SET);
const auto deflatedDataSize = static_cast<size_t>(fileStat.m_comp_size); const auto deflatedDataSize = static_cast<size_t>(fileStat.m_comp_size);
@ -175,6 +183,10 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
} }
FILE* file = fopen(filePath.c_str(), "rb"); FILE* file = fopen(filePath.c_str(), "rb");
if (!file) {
Serial.printf("[%lu] [ZIP] Failed to open file for streaming\n", millis());
return false;
}
fseek(file, fileOffset, SEEK_SET); fseek(file, fileOffset, SEEK_SET);
const auto deflatedDataSize = static_cast<size_t>(fileStat.m_comp_size); const auto deflatedDataSize = static_cast<size_t>(fileStat.m_comp_size);

@ -1 +1 @@
Subproject commit 4d0dcd5ff87fcd86eb2966a123e85b03284a03db Subproject commit 98a5aa1f8969ccd317c9b45bf0fa84b6c82e167f

View File

@ -1,5 +1,5 @@
[platformio] [platformio]
crosspoint_version = 0.6.0 crosspoint_version = 0.7.0
default_envs = default default_envs = default
[base] [base]
@ -20,6 +20,7 @@ build_flags =
-DARDUINO_USB_MODE=1 -DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_CDC_ON_BOOT=1
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1 -DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1
-DEINK_DISPLAY_SINGLE_BUFFER_MODE=1
# https://libexpat.github.io/doc/api/latest/#XML_GE # https://libexpat.github.io/doc/api/latest/#XML_GE
-DXML_GE=0 -DXML_GE=0
-DXML_CONTEXT_BYTES=1024 -DXML_CONTEXT_BYTES=1024

View File

@ -10,9 +10,11 @@
// Initialize the static instance // Initialize the static instance
CrossPointSettings CrossPointSettings::instance; CrossPointSettings CrossPointSettings::instance;
namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr uint8_t SETTINGS_FILE_VERSION = 1;
constexpr uint8_t SETTINGS_COUNT = 2; constexpr uint8_t SETTINGS_COUNT = 3;
constexpr char SETTINGS_FILE[] = "/sd/.crosspoint/settings.bin"; constexpr char SETTINGS_FILE[] = "/sd/.crosspoint/settings.bin";
} // namespace
bool CrossPointSettings::saveToFile() const { bool CrossPointSettings::saveToFile() const {
// Make sure the directory exists // Make sure the directory exists
@ -23,6 +25,7 @@ bool CrossPointSettings::saveToFile() const {
serialization::writePod(outputFile, SETTINGS_COUNT); serialization::writePod(outputFile, SETTINGS_COUNT);
serialization::writePod(outputFile, whiteSleepScreen); serialization::writePod(outputFile, whiteSleepScreen);
serialization::writePod(outputFile, extraParagraphSpacing); serialization::writePod(outputFile, extraParagraphSpacing);
serialization::writePod(outputFile, shortPwrBtn);
outputFile.close(); outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -49,15 +52,15 @@ bool CrossPointSettings::loadFromFile() {
serialization::readPod(inputFile, fileSettingsCount); serialization::readPod(inputFile, fileSettingsCount);
// load settings that exist // load settings that exist
switch (fileSettingsCount) { uint8_t settingsRead = 0;
case 1: do {
serialization::readPod(inputFile, whiteSleepScreen);
break;
case 2:
serialization::readPod(inputFile, whiteSleepScreen); serialization::readPod(inputFile, whiteSleepScreen);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, extraParagraphSpacing); serialization::readPod(inputFile, extraParagraphSpacing);
break; if (++settingsRead >= fileSettingsCount) break;
} serialization::readPod(inputFile, shortPwrBtn);
if (++settingsRead >= fileSettingsCount) break;
} while (false);
inputFile.close(); inputFile.close();
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis()); Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());

View File

@ -17,15 +17,18 @@ class CrossPointSettings {
// Sleep screen settings // Sleep screen settings
uint8_t whiteSleepScreen = 0; uint8_t whiteSleepScreen = 0;
// Text rendering settings // Text rendering settings
uint8_t extraParagraphSpacing = 1; uint8_t extraParagraphSpacing = 1;
// Duration of the power button press
uint8_t shortPwrBtn = 0;
~CrossPointSettings() = default; ~CrossPointSettings() = default;
// Get singleton instance // Get singleton instance
static CrossPointSettings& getInstance() { return instance; } static CrossPointSettings& getInstance() { return instance; }
uint16_t getPowerButtonDuration() const { return shortPwrBtn ? 10 : 500; }
bool saveToFile() const; bool saveToFile() const;
bool loadFromFile(); bool loadFromFile();
}; };

View File

@ -6,8 +6,12 @@
#include <fstream> #include <fstream>
namespace {
constexpr uint8_t STATE_FILE_VERSION = 1; constexpr uint8_t STATE_FILE_VERSION = 1;
constexpr char STATE_FILE[] = "/sd/.crosspoint/state.bin"; constexpr char STATE_FILE[] = "/sd/.crosspoint/state.bin";
} // namespace
CrossPointState CrossPointState::instance;
bool CrossPointState::saveToFile() const { bool CrossPointState::saveToFile() const {
std::ofstream outputFile(STATE_FILE); std::ofstream outputFile(STATE_FILE);

View File

@ -3,11 +3,20 @@
#include <string> #include <string>
class CrossPointState { class CrossPointState {
// Static instance
static CrossPointState instance;
public: public:
std::string openEpubPath; std::string openEpubPath;
~CrossPointState() = default; ~CrossPointState() = default;
// Get singleton instance
static CrossPointState& getInstance() { return instance; }
bool saveToFile() const; bool saveToFile() const;
bool loadFromFile(); bool loadFromFile();
}; };
// Helper macro to access settings
#define APP_STATE CrossPointState::getInstance()

18
src/activities/Activity.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include <InputManager.h>
class GfxRenderer;
class Activity {
protected:
GfxRenderer& renderer;
InputManager& inputManager;
public:
explicit Activity(GfxRenderer& renderer, InputManager& inputManager)
: renderer(renderer), inputManager(inputManager) {}
virtual ~Activity() = default;
virtual void onEnter() {}
virtual void onExit() {}
virtual void loop() {}
};

View File

@ -0,0 +1,21 @@
#include "ActivityWithSubactivity.h"
void ActivityWithSubactivity::exitActivity() {
if (subActivity) {
subActivity->onExit();
subActivity.reset();
}
}
void ActivityWithSubactivity::enterNewActivity(Activity* activity) {
subActivity.reset(activity);
subActivity->onEnter();
}
void ActivityWithSubactivity::loop() {
if (subActivity) {
subActivity->loop();
}
}
void ActivityWithSubactivity::onExit() { exitActivity(); }

View File

@ -0,0 +1,17 @@
#pragma once
#include <memory>
#include "Activity.h"
class ActivityWithSubactivity : public Activity {
protected:
std::unique_ptr<Activity> subActivity = nullptr;
void exitActivity();
void enterNewActivity(Activity* activity);
public:
explicit ActivityWithSubactivity(GfxRenderer& renderer, InputManager& inputManager)
: Activity(renderer, inputManager) {}
void loop() override;
void onExit() override;
};

View File

@ -1,11 +1,11 @@
#include "BootLogoScreen.h" #include "BootActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include "config.h" #include "config.h"
#include "images/CrossLarge.h" #include "images/CrossLarge.h"
void BootLogoScreen::onEnter() { void BootActivity::onEnter() {
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();

View File

@ -0,0 +1,8 @@
#pragma once
#include "../Activity.h"
class BootActivity final : public Activity {
public:
explicit BootActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity(renderer, inputManager) {}
void onEnter() override;
};

View File

@ -0,0 +1,87 @@
#include "SleepActivity.h"
#include <GfxRenderer.h>
#include "CrossPointSettings.h"
#include "SD.h"
#include "config.h"
#include "images/CrossLarge.h"
void SleepActivity::onEnter() {
// Look for sleep.bmp on the root of the sd card to determine if we should
// render a custom sleep screen instead of the default.
auto file = SD.open("/sleep.bmp");
if (file) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
renderCustomSleepScreen(bitmap);
return;
}
}
renderDefaultSleepScreen();
}
void SleepActivity::renderDefaultSleepScreen() const {
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
renderer.clearScreen();
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128);
renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING");
// Apply white screen if enabled in settings
if (!SETTINGS.whiteSleepScreen) {
renderer.invertScreen();
}
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
}
void SleepActivity::renderCustomSleepScreen(const Bitmap& bitmap) const {
int x, y;
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) {
// image will scale, make sure placement is right
const float ratio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
if (ratio > screenRatio) {
// image wider than viewport ratio, scaled down image needs to be centered vertically
x = 0;
y = (pageHeight - pageWidth / ratio) / 2;
} else {
// image taller than viewport ratio, scaled down image needs to be centered horizontally
x = (pageWidth - pageHeight * ratio) / 2;
y = 0;
}
} else {
// center the image
x = (pageWidth - bitmap.getWidth()) / 2;
y = (pageHeight - bitmap.getHeight()) / 2;
}
renderer.clearScreen();
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight);
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
if (bitmap.hasGreyscale()) {
bitmap.rewindToData();
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight);
renderer.copyGrayscaleLsbBuffers();
bitmap.rewindToData();
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight);
renderer.copyGrayscaleMsbBuffers();
renderer.displayGrayBuffer();
renderer.setRenderMode(GfxRenderer::BW);
}
}

View File

@ -0,0 +1,14 @@
#pragma once
#include "../Activity.h"
class Bitmap;
class SleepActivity final : public Activity {
public:
explicit SleepActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity(renderer, inputManager) {}
void onEnter() override;
private:
void renderDefaultSleepScreen() const;
void renderCustomSleepScreen(const Bitmap& bitmap) const;
};

View File

@ -0,0 +1,103 @@
#include "HomeActivity.h"
#include <GfxRenderer.h>
#include <SD.h>
#include "config.h"
namespace {
constexpr int menuItemCount = 2;
}
void HomeActivity::taskTrampoline(void* param) {
auto* self = static_cast<HomeActivity*>(param);
self->displayTaskLoop();
}
void HomeActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
selectorIndex = 0;
// Trigger first update
updateRequired = true;
xTaskCreate(&HomeActivity::taskTrampoline, "HomeActivityTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void HomeActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void HomeActivity::loop() {
const bool prevPressed =
inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
const bool nextPressed =
inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT);
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (selectorIndex == 0) {
onReaderOpen();
} else if (selectorIndex == 1) {
onSettingsOpen();
}
} else if (prevPressed) {
selectorIndex = (selectorIndex + menuItemCount - 1) % menuItemCount;
updateRequired = true;
} else if (nextPressed) {
selectorIndex = (selectorIndex + 1) % menuItemCount;
updateRequired = true;
}
}
void HomeActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void HomeActivity::render() const {
renderer.clearScreen();
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD);
// Draw selection
renderer.fillRect(0, 60 + selectorIndex * 30 + 2, pageWidth - 1, 30);
renderer.drawText(UI_FONT_ID, 20, 60, "Read", selectorIndex != 0);
renderer.drawText(UI_FONT_ID, 20, 90, "Settings", selectorIndex != 1);
renderer.drawRect(25, pageHeight - 40, 106, 40);
renderer.drawText(UI_FONT_ID, 25 + (105 - renderer.getTextWidth(UI_FONT_ID, "Back")) / 2, pageHeight - 35, "Back");
renderer.drawRect(130, pageHeight - 40, 106, 40);
renderer.drawText(UI_FONT_ID, 130 + (105 - renderer.getTextWidth(UI_FONT_ID, "Confirm")) / 2, pageHeight - 35,
"Confirm");
renderer.drawRect(245, pageHeight - 40, 106, 40);
renderer.drawText(UI_FONT_ID, 245 + (105 - renderer.getTextWidth(UI_FONT_ID, "Left")) / 2, pageHeight - 35, "Left");
renderer.drawRect(350, pageHeight - 40, 106, 40);
renderer.drawText(UI_FONT_ID, 350 + (105 - renderer.getTextWidth(UI_FONT_ID, "Right")) / 2, pageHeight - 35, "Right");
renderer.displayBuffer();
}

View File

@ -0,0 +1,29 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include "../Activity.h"
class HomeActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int selectorIndex = 0;
bool updateRequired = false;
const std::function<void()> onReaderOpen;
const std::function<void()> onSettingsOpen;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
public:
explicit HomeActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onReaderOpen,
const std::function<void()>& onSettingsOpen)
: Activity(renderer, inputManager), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen) {}
void onEnter() override;
void onExit() override;
void loop() override;
};

View File

@ -1,4 +1,4 @@
#include "EpubReaderScreen.h" #include "EpubReaderActivity.h"
#include <Epub/Page.h> #include <Epub/Page.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
@ -6,23 +6,25 @@
#include "Battery.h" #include "Battery.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "EpubReaderChapterSelectionScreen.h" #include "EpubReaderChapterSelectionActivity.h"
#include "config.h" #include "config.h"
constexpr int PAGES_PER_REFRESH = 15; namespace {
constexpr unsigned long SKIP_CHAPTER_MS = 700; constexpr int pagesPerRefresh = 15;
constexpr unsigned long skipChapterMs = 700;
constexpr float lineCompression = 0.95f; constexpr float lineCompression = 0.95f;
constexpr int marginTop = 8; constexpr int marginTop = 8;
constexpr int marginRight = 10; constexpr int marginRight = 10;
constexpr int marginBottom = 22; constexpr int marginBottom = 22;
constexpr int marginLeft = 10; constexpr int marginLeft = 10;
} // namespace
void EpubReaderScreen::taskTrampoline(void* param) { void EpubReaderActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderScreen*>(param); auto* self = static_cast<EpubReaderActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
} }
void EpubReaderScreen::onEnter() { void EpubReaderActivity::onEnter() {
if (!epub) { if (!epub) {
return; return;
} }
@ -31,21 +33,21 @@ void EpubReaderScreen::onEnter() {
epub->setupCacheDir(); epub->setupCacheDir();
// TODO: Move this to a state object
if (SD.exists((epub->getCachePath() + "/progress.bin").c_str())) {
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str()); File f = SD.open((epub->getCachePath() + "/progress.bin").c_str());
if (f) {
uint8_t data[4]; uint8_t data[4];
f.read(data, 4); if (f.read(data, 4) == 4) {
currentSpineIndex = data[0] + (data[1] << 8); currentSpineIndex = data[0] + (data[1] << 8);
nextPageNumber = data[2] + (data[3] << 8); nextPageNumber = data[2] + (data[3] << 8);
Serial.printf("[%lu] [ERS] Loaded cache: %d, %d\n", millis(), currentSpineIndex, nextPageNumber); Serial.printf("[%lu] [ERS] Loaded cache: %d, %d\n", millis(), currentSpineIndex, nextPageNumber);
}
f.close(); f.close();
} }
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
xTaskCreate(&EpubReaderScreen::taskTrampoline, "EpubReaderScreenTask", xTaskCreate(&EpubReaderActivity::taskTrampoline, "EpubReaderActivityTask",
8192, // Stack size 8192, // Stack size
this, // Parameters this, // Parameters
1, // Priority 1, // Priority
@ -53,7 +55,7 @@ void EpubReaderScreen::onEnter() {
); );
} }
void EpubReaderScreen::onExit() { void EpubReaderActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -66,22 +68,22 @@ void EpubReaderScreen::onExit() {
epub.reset(); epub.reset();
} }
void EpubReaderScreen::handleInput() { void EpubReaderActivity::loop() {
// Pass input responsibility to sub screen if exists // Pass input responsibility to sub activity if exists
if (subScreen) { if (subAcitivity) {
subScreen->handleInput(); subAcitivity->loop();
return; return;
} }
// Enter chapter selection screen // Enter chapter selection activity
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
// Don't start screen transition while rendering // Don't start activity transition while rendering
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
subScreen.reset(new EpubReaderChapterSelectionScreen( subAcitivity.reset(new EpubReaderChapterSelectionActivity(
this->renderer, this->inputManager, epub, currentSpineIndex, this->renderer, this->inputManager, epub, currentSpineIndex,
[this] { [this] {
subScreen->onExit(); subAcitivity->onExit();
subScreen.reset(); subAcitivity.reset();
updateRequired = true; updateRequired = true;
}, },
[this](const int newSpineIndex) { [this](const int newSpineIndex) {
@ -90,16 +92,16 @@ void EpubReaderScreen::handleInput() {
nextPageNumber = 0; nextPageNumber = 0;
section.reset(); section.reset();
} }
subScreen->onExit(); subAcitivity->onExit();
subScreen.reset(); subAcitivity.reset();
updateRequired = true; updateRequired = true;
})); }));
subScreen->onEnter(); subAcitivity->onEnter();
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} }
if (inputManager.wasPressed(InputManager::BTN_BACK)) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onGoHome(); onGoBack();
return; return;
} }
@ -120,7 +122,7 @@ void EpubReaderScreen::handleInput() {
return; return;
} }
const bool skipChapter = inputManager.getHeldTime() > SKIP_CHAPTER_MS; const bool skipChapter = inputManager.getHeldTime() > skipChapterMs;
if (skipChapter) { if (skipChapter) {
// We don't want to delete the section mid-render, so grab the semaphore // We don't want to delete the section mid-render, so grab the semaphore
@ -166,7 +168,7 @@ void EpubReaderScreen::handleInput() {
} }
} }
void EpubReaderScreen::displayTaskLoop() { void EpubReaderActivity::displayTaskLoop() {
while (true) { while (true) {
if (updateRequired) { if (updateRequired) {
updateRequired = false; updateRequired = false;
@ -179,7 +181,7 @@ void EpubReaderScreen::displayTaskLoop() {
} }
// TODO: Failure handling // TODO: Failure handling
void EpubReaderScreen::renderScreen() { void EpubReaderActivity::renderScreen() {
if (!epub) { if (!epub) {
return; return;
} }
@ -212,15 +214,12 @@ void EpubReaderScreen::renderScreen() {
{ {
const int textWidth = renderer.getTextWidth(READER_FONT_ID, "Indexing..."); const int textWidth = renderer.getTextWidth(READER_FONT_ID, "Indexing...");
constexpr int margin = 20; constexpr int margin = 20;
const int x = (GfxRenderer::getScreenWidth() - textWidth - margin * 2) / 2; // Round all coordinates to 8 pixel boundaries
constexpr int y = 50; const int x = ((GfxRenderer::getScreenWidth() - textWidth - margin * 2) / 2 + 7) / 8 * 8;
const int w = textWidth + margin * 2; constexpr int y = 56;
const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2; const int w = (textWidth + margin * 2 + 7) / 8 * 8;
renderer.grayscaleRevert(); const int h = (renderer.getLineHeight(READER_FONT_ID) + margin * 2 + 7) / 8 * 8;
uint8_t* fb1 = renderer.getFrameBuffer(); renderer.clearScreen();
renderer.swapBuffers();
memcpy(fb1, renderer.getFrameBuffer(), EInkDisplay::BUFFER_SIZE);
renderer.fillRect(x, y, w, h, 0);
renderer.drawText(READER_FONT_ID, x + margin, y + margin, "Indexing..."); renderer.drawText(READER_FONT_ID, x + margin, y + margin, "Indexing...");
renderer.drawRect(x + 5, y + 5, w - 10, h - 10); renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
renderer.displayBuffer(); renderer.displayBuffer();
@ -286,17 +285,20 @@ void EpubReaderScreen::renderScreen() {
f.close(); f.close();
} }
void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) { void EpubReaderActivity::renderContents(std::unique_ptr<Page> page) {
page->render(renderer, READER_FONT_ID); page->render(renderer, READER_FONT_ID);
renderStatusBar(); renderStatusBar();
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
pagesUntilFullRefresh = PAGES_PER_REFRESH; pagesUntilFullRefresh = pagesPerRefresh;
} else { } else {
renderer.displayBuffer(); renderer.displayBuffer();
pagesUntilFullRefresh--; pagesUntilFullRefresh--;
} }
// Save bw buffer to reset buffer state after grayscale data sync
renderer.storeBwBuffer();
// grayscale rendering // grayscale rendering
// TODO: Only do this if font supports it // TODO: Only do this if font supports it
{ {
@ -315,12 +317,21 @@ void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) {
renderer.displayGrayBuffer(); renderer.displayGrayBuffer();
renderer.setRenderMode(GfxRenderer::BW); renderer.setRenderMode(GfxRenderer::BW);
} }
// restore the bw data
renderer.restoreBwBuffer();
} }
void EpubReaderScreen::renderStatusBar() const { void EpubReaderActivity::renderStatusBar() const {
constexpr auto textY = 776; constexpr auto textY = 776;
// Calculate progress in book
float sectionChapterProg = static_cast<float>(section->currentPage) / section->pageCount;
uint8_t bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg);
// Right aligned text for progress counter // Right aligned text for progress counter
const std::string progress = std::to_string(section->currentPage + 1) + " / " + std::to_string(section->pageCount); const std::string progress = std::to_string(section->currentPage + 1) + "/" + std::to_string(section->pageCount) +
" " + std::to_string(bookProgress) + "%";
const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str()); const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str());
renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY, renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY,
progress.c_str()); progress.c_str());
@ -372,7 +383,7 @@ void EpubReaderScreen::renderStatusBar() const {
const auto tocItem = epub->getTocItem(tocIndex); const auto tocItem = epub->getTocItem(tocIndex);
title = tocItem.title; title = tocItem.title;
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
while (titleWidth > availableTextWidth) { while (titleWidth > availableTextWidth && title.length() > 11) {
title = title.substr(0, title.length() - 8) + "..."; title = title.substr(0, title.length() - 8) + "...";
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
} }

View File

@ -5,19 +5,19 @@
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <freertos/task.h> #include <freertos/task.h>
#include "Screen.h" #include "../Activity.h"
class EpubReaderScreen final : public Screen { class EpubReaderActivity final : public Activity {
std::shared_ptr<Epub> epub; std::shared_ptr<Epub> epub;
std::unique_ptr<Section> section = nullptr; std::unique_ptr<Section> section = nullptr;
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
std::unique_ptr<Screen> subScreen = nullptr; std::unique_ptr<Activity> subAcitivity = nullptr;
int currentSpineIndex = 0; int currentSpineIndex = 0;
int nextPageNumber = 0; int nextPageNumber = 0;
int pagesUntilFullRefresh = 0; int pagesUntilFullRefresh = 0;
bool updateRequired = false; bool updateRequired = false;
const std::function<void()> onGoHome; const std::function<void()> onGoBack;
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
@ -26,10 +26,10 @@ class EpubReaderScreen final : public Screen {
void renderStatusBar() const; void renderStatusBar() const;
public: public:
explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub, explicit EpubReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub,
const std::function<void()>& onGoHome) const std::function<void()>& onGoBack)
: Screen(renderer, inputManager), epub(std::move(epub)), onGoHome(onGoHome) {} : Activity(renderer, inputManager), epub(std::move(epub)), onGoBack(onGoBack) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput() override; void loop() override;
}; };

View File

@ -1,4 +1,4 @@
#include "EpubReaderChapterSelectionScreen.h" #include "EpubReaderChapterSelectionActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <SD.h> #include <SD.h>
@ -8,12 +8,12 @@
constexpr int PAGE_ITEMS = 24; constexpr int PAGE_ITEMS = 24;
constexpr int SKIP_PAGE_MS = 700; constexpr int SKIP_PAGE_MS = 700;
void EpubReaderChapterSelectionScreen::taskTrampoline(void* param) { void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderChapterSelectionScreen*>(param); auto* self = static_cast<EpubReaderChapterSelectionActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
} }
void EpubReaderChapterSelectionScreen::onEnter() { void EpubReaderChapterSelectionActivity::onEnter() {
if (!epub) { if (!epub) {
return; return;
} }
@ -23,7 +23,7 @@ void EpubReaderChapterSelectionScreen::onEnter() {
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
xTaskCreate(&EpubReaderChapterSelectionScreen::taskTrampoline, "EpubReaderChapterSelectionScreenTask", xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask",
2048, // Stack size 2048, // Stack size
this, // Parameters this, // Parameters
1, // Priority 1, // Priority
@ -31,7 +31,7 @@ void EpubReaderChapterSelectionScreen::onEnter() {
); );
} }
void EpubReaderChapterSelectionScreen::onExit() { void EpubReaderChapterSelectionActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -42,7 +42,7 @@ void EpubReaderChapterSelectionScreen::onExit() {
renderingMutex = nullptr; renderingMutex = nullptr;
} }
void EpubReaderChapterSelectionScreen::handleInput() { void EpubReaderChapterSelectionActivity::loop() {
const bool prevReleased = const bool prevReleased =
inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT); inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
const bool nextReleased = const bool nextReleased =
@ -72,7 +72,7 @@ void EpubReaderChapterSelectionScreen::handleInput() {
} }
} }
void EpubReaderChapterSelectionScreen::displayTaskLoop() { void EpubReaderChapterSelectionActivity::displayTaskLoop() {
while (true) { while (true) {
if (updateRequired) { if (updateRequired) {
updateRequired = false; updateRequired = false;
@ -84,7 +84,7 @@ void EpubReaderChapterSelectionScreen::displayTaskLoop() {
} }
} }
void EpubReaderChapterSelectionScreen::renderScreen() { void EpubReaderChapterSelectionActivity::renderScreen() {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();

View File

@ -6,9 +6,9 @@
#include <memory> #include <memory>
#include "Screen.h" #include "../Activity.h"
class EpubReaderChapterSelectionScreen final : public Screen { class EpubReaderChapterSelectionActivity final : public Activity {
std::shared_ptr<Epub> epub; std::shared_ptr<Epub> epub;
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
@ -23,16 +23,16 @@ class EpubReaderChapterSelectionScreen final : public Screen {
void renderScreen(); void renderScreen();
public: public:
explicit EpubReaderChapterSelectionScreen(GfxRenderer& renderer, InputManager& inputManager, explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::shared_ptr<Epub>& epub, const int currentSpineIndex, const std::shared_ptr<Epub>& epub, const int currentSpineIndex,
const std::function<void()>& onGoBack, const std::function<void()>& onGoBack,
const std::function<void(int newSpineIndex)>& onSelectSpineIndex) const std::function<void(int newSpineIndex)>& onSelectSpineIndex)
: Screen(renderer, inputManager), : Activity(renderer, inputManager),
epub(epub), epub(epub),
currentSpineIndex(currentSpineIndex), currentSpineIndex(currentSpineIndex),
onGoBack(onGoBack), onGoBack(onGoBack),
onSelectSpineIndex(onSelectSpineIndex) {} onSelectSpineIndex(onSelectSpineIndex) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput() override; void loop() override;
}; };

View File

@ -1,4 +1,4 @@
#include "FileSelectionScreen.h" #include "FileSelectionActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <SD.h> #include <SD.h>
@ -15,12 +15,12 @@ void sortFileList(std::vector<std::string>& strs) {
}); });
} }
void FileSelectionScreen::taskTrampoline(void* param) { void FileSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<FileSelectionScreen*>(param); auto* self = static_cast<FileSelectionActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
} }
void FileSelectionScreen::loadFiles() { void FileSelectionActivity::loadFiles() {
files.clear(); files.clear();
selectorIndex = 0; selectorIndex = 0;
auto root = SD.open(basepath.c_str()); auto root = SD.open(basepath.c_str());
@ -42,7 +42,7 @@ void FileSelectionScreen::loadFiles() {
sortFileList(files); sortFileList(files);
} }
void FileSelectionScreen::onEnter() { void FileSelectionActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
basepath = "/"; basepath = "/";
@ -52,7 +52,7 @@ void FileSelectionScreen::onEnter() {
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask", xTaskCreate(&FileSelectionActivity::taskTrampoline, "FileSelectionActivityTask",
2048, // Stack size 2048, // Stack size
this, // Parameters this, // Parameters
1, // Priority 1, // Priority
@ -60,7 +60,7 @@ void FileSelectionScreen::onEnter() {
); );
} }
void FileSelectionScreen::onExit() { void FileSelectionActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -72,7 +72,7 @@ void FileSelectionScreen::onExit() {
files.clear(); files.clear();
} }
void FileSelectionScreen::handleInput() { void FileSelectionActivity::loop() {
const bool prevPressed = const bool prevPressed =
inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT); inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
const bool nextPressed = const bool nextPressed =
@ -98,8 +98,8 @@ void FileSelectionScreen::handleInput() {
loadFiles(); loadFiles();
updateRequired = true; updateRequired = true;
} else { } else {
// At root level, go to settings // At root level, go back home
onSettingsOpen(); onGoHome();
} }
} else if (prevPressed) { } else if (prevPressed) {
selectorIndex = (selectorIndex + files.size() - 1) % files.size(); selectorIndex = (selectorIndex + files.size() - 1) % files.size();
@ -110,7 +110,7 @@ void FileSelectionScreen::handleInput() {
} }
} }
void FileSelectionScreen::displayTaskLoop() { void FileSelectionActivity::displayTaskLoop() {
while (true) { while (true) {
if (updateRequired) { if (updateRequired) {
updateRequired = false; updateRequired = false;
@ -122,14 +122,14 @@ void FileSelectionScreen::displayTaskLoop() {
} }
} }
void FileSelectionScreen::render() const { void FileSelectionActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD); renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD);
// Help text // Help text
renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, "Press BACK for Settings"); renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, "Press BACK for Home");
if (files.empty()) { if (files.empty()) {
renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found"); renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found");

View File

@ -7,9 +7,9 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "Screen.h" #include "../Activity.h"
class FileSelectionScreen final : public Screen { class FileSelectionActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
std::string basepath = "/"; std::string basepath = "/";
@ -17,7 +17,7 @@ class FileSelectionScreen final : public Screen {
int selectorIndex = 0; int selectorIndex = 0;
bool updateRequired = false; bool updateRequired = false;
const std::function<void(const std::string&)> onSelect; const std::function<void(const std::string&)> onSelect;
const std::function<void()> onSettingsOpen; const std::function<void()> onGoHome;
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
@ -25,11 +25,11 @@ class FileSelectionScreen final : public Screen {
void loadFiles(); void loadFiles();
public: public:
explicit FileSelectionScreen(GfxRenderer& renderer, InputManager& inputManager, explicit FileSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void(const std::string&)>& onSelect, const std::function<void(const std::string&)>& onSelect,
const std::function<void()>& onSettingsOpen) const std::function<void()>& onGoHome)
: Screen(renderer, inputManager), onSelect(onSelect), onSettingsOpen(onSettingsOpen) {} : Activity(renderer, inputManager), onSelect(onSelect), onGoHome(onGoHome) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput() override; void loop() override;
}; };

View File

@ -0,0 +1,68 @@
#include "ReaderActivity.h"
#include <SD.h>
#include "CrossPointState.h"
#include "Epub.h"
#include "EpubReaderActivity.h"
#include "FileSelectionActivity.h"
#include "activities/util/FullScreenMessageActivity.h"
std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
if (!SD.exists(path.c_str())) {
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
return nullptr;
}
auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
if (epub->load()) {
return epub;
}
Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
return nullptr;
}
void ReaderActivity::onSelectEpubFile(const std::string& path) {
exitActivity();
enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Loading..."));
auto epub = loadEpub(path);
if (epub) {
APP_STATE.openEpubPath = path;
APP_STATE.saveToFile();
onGoToEpubReader(std::move(epub));
} else {
exitActivity();
enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Failed to load epub", REGULAR,
EInkDisplay::HALF_REFRESH));
delay(2000);
onGoToFileSelection();
}
}
void ReaderActivity::onGoToFileSelection() {
exitActivity();
enterNewActivity(new FileSelectionActivity(
renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack));
}
void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
exitActivity();
enterNewActivity(new EpubReaderActivity(renderer, inputManager, std::move(epub), [this] { onGoToFileSelection(); }));
}
void ReaderActivity::onEnter() {
if (initialEpubPath.empty()) {
onGoToFileSelection();
return;
}
auto epub = loadEpub(initialEpubPath);
if (!epub) {
onGoBack();
return;
}
onGoToEpubReader(std::move(epub));
}

View File

@ -0,0 +1,24 @@
#pragma once
#include <memory>
#include "../ActivityWithSubactivity.h"
class Epub;
class ReaderActivity final : public ActivityWithSubactivity {
std::string initialEpubPath;
const std::function<void()> onGoBack;
static std::unique_ptr<Epub> loadEpub(const std::string& path);
void onSelectEpubFile(const std::string& path);
void onGoToFileSelection();
void onGoToEpubReader(std::unique_ptr<Epub> epub);
public:
explicit ReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::string initialEpubPath,
const std::function<void()>& onGoBack)
: ActivityWithSubactivity(renderer, inputManager),
initialEpubPath(std::move(initialEpubPath)),
onGoBack(onGoBack) {}
void onEnter() override;
};

View File

@ -1,4 +1,4 @@
#include "SettingsScreen.h" #include "SettingsActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
@ -7,16 +7,17 @@
// Define the static settings list // Define the static settings list
const SettingInfo SettingsScreen::settingsList[SettingsScreen::settingsCount] = { const SettingInfo SettingsActivity::settingsList[settingsCount] = {
{"White Sleep Screen", &CrossPointSettings::whiteSleepScreen}, {"White Sleep Screen", &CrossPointSettings::whiteSleepScreen},
{"Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing}}; {"Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing},
{"Short Power Button Click", &CrossPointSettings::shortPwrBtn}};
void SettingsScreen::taskTrampoline(void* param) { void SettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<SettingsScreen*>(param); auto* self = static_cast<SettingsActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
} }
void SettingsScreen::onEnter() { void SettingsActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
// Reset selection to first item // Reset selection to first item
@ -25,7 +26,7 @@ void SettingsScreen::onEnter() {
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
xTaskCreate(&SettingsScreen::taskTrampoline, "SettingsScreenTask", xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask",
2048, // Stack size 2048, // Stack size
this, // Parameters this, // Parameters
1, // Priority 1, // Priority
@ -33,7 +34,7 @@ void SettingsScreen::onEnter() {
); );
} }
void SettingsScreen::onExit() { void SettingsActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -44,7 +45,7 @@ void SettingsScreen::onExit() {
renderingMutex = nullptr; renderingMutex = nullptr;
} }
void SettingsScreen::handleInput() { void SettingsActivity::loop() {
// Handle actions with early return // Handle actions with early return
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
toggleCurrentSetting(); toggleCurrentSetting();
@ -70,7 +71,7 @@ void SettingsScreen::handleInput() {
} }
} }
void SettingsScreen::toggleCurrentSetting() { void SettingsActivity::toggleCurrentSetting() {
// Validate index // Validate index
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
return; return;
@ -84,7 +85,7 @@ void SettingsScreen::toggleCurrentSetting() {
SETTINGS.saveToFile(); SETTINGS.saveToFile();
} }
void SettingsScreen::displayTaskLoop() { void SettingsActivity::displayTaskLoop() {
while (true) { while (true) {
if (updateRequired) { if (updateRequired) {
updateRequired = false; updateRequired = false;
@ -96,7 +97,7 @@ void SettingsScreen::displayTaskLoop() {
} }
} }
void SettingsScreen::render() const { void SettingsActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();

View File

@ -7,7 +7,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "Screen.h" #include "../Activity.h"
class CrossPointSettings; class CrossPointSettings;
@ -17,7 +17,7 @@ struct SettingInfo {
uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings
}; };
class SettingsScreen final : public Screen { class SettingsActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false; bool updateRequired = false;
@ -25,7 +25,7 @@ class SettingsScreen final : public Screen {
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
// Static settings list // Static settings list
static constexpr int settingsCount = 2; // Number of settings static constexpr int settingsCount = 3; // Number of settings
static const SettingInfo settingsList[settingsCount]; static const SettingInfo settingsList[settingsCount];
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
@ -34,9 +34,9 @@ class SettingsScreen final : public Screen {
void toggleCurrentSetting(); void toggleCurrentSetting();
public: public:
explicit SettingsScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome) explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
: Screen(renderer, inputManager), onGoHome(onGoHome) {} : Activity(renderer, inputManager), onGoHome(onGoHome) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput() override; void loop() override;
}; };

View File

@ -1,10 +1,10 @@
#include "FullScreenMessageScreen.h" #include "FullScreenMessageActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include "config.h" #include "config.h"
void FullScreenMessageScreen::onEnter() { void FullScreenMessageActivity::onEnter() {
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (GfxRenderer::getScreenHeight() - height) / 2; const auto top = (GfxRenderer::getScreenHeight() - height) / 2;

View File

@ -0,0 +1,21 @@
#pragma once
#include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <string>
#include <utility>
#include "../Activity.h"
class FullScreenMessageActivity final : public Activity {
std::string text;
EpdFontStyle style;
EInkDisplay::RefreshMode refreshMode;
public:
explicit FullScreenMessageActivity(GfxRenderer& renderer, InputManager& inputManager, std::string text,
const EpdFontStyle style = REGULAR,
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
: Activity(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {}
void onEnter() override;
};

View File

@ -9,7 +9,7 @@
* "./lib/EpdFont/builtinFonts/bookerly_italic_2b.h", * "./lib/EpdFont/builtinFonts/bookerly_italic_2b.h",
* ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)' * ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
*/ */
#define READER_FONT_ID 828106571 #define READER_FONT_ID 1818981670
/** /**
* Generated with: * Generated with:
@ -18,7 +18,7 @@
* "./lib/EpdFont/builtinFonts/ubuntu_bold_10.h", * "./lib/EpdFont/builtinFonts/ubuntu_bold_10.h",
* ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)' * ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
*/ */
#define UI_FONT_ID -56235187 #define UI_FONT_ID (-1619831379)
/** /**
* Generated with: * Generated with:
@ -26,4 +26,4 @@
* "./lib/EpdFont/builtinFonts/pixelarial14.h", * "./lib/EpdFont/builtinFonts/pixelarial14.h",
* ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)' * ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
*/ */
#define SMALL_FONT_ID -1952330053 #define SMALL_FONT_ID (-139796914)

View File

@ -16,13 +16,13 @@
#include "Battery.h" #include "Battery.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "activities/boot_sleep/BootActivity.h"
#include "activities/boot_sleep/SleepActivity.h"
#include "activities/home/HomeActivity.h"
#include "activities/reader/ReaderActivity.h"
#include "activities/settings/SettingsActivity.h"
#include "activities/util/FullScreenMessageActivity.h"
#include "config.h" #include "config.h"
#include "screens/BootLogoScreen.h"
#include "screens/EpubReaderScreen.h"
#include "screens/FileSelectionScreen.h"
#include "screens/FullScreenMessageScreen.h"
#include "screens/SettingsScreen.h"
#include "screens/SleepScreen.h"
#define SPI_FQ 40000000 #define SPI_FQ 40000000
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
@ -41,8 +41,7 @@
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
InputManager inputManager; InputManager inputManager;
GfxRenderer renderer(einkDisplay); GfxRenderer renderer(einkDisplay);
Screen* currentScreen; Activity* currentActivity;
CrossPointState appState;
// Fonts // Fonts
EpdFont bookerlyFont(&bookerly_2b); EpdFont bookerlyFont(&bookerly_2b);
@ -58,60 +57,41 @@ EpdFont ubuntu10Font(&ubuntu_10);
EpdFont ubuntuBold10Font(&ubuntu_bold_10); EpdFont ubuntuBold10Font(&ubuntu_bold_10);
EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ubuntuBold10Font); EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ubuntuBold10Font);
// Power button timing
// Time required to confirm boot from sleep
constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 500;
// Time required to enter sleep mode
constexpr unsigned long POWER_BUTTON_SLEEP_MS = 500;
// Auto-sleep timeout (10 minutes of inactivity) // Auto-sleep timeout (10 minutes of inactivity)
constexpr unsigned long AUTO_SLEEP_TIMEOUT_MS = 10 * 60 * 1000; constexpr unsigned long AUTO_SLEEP_TIMEOUT_MS = 10 * 60 * 1000;
std::unique_ptr<Epub> loadEpub(const std::string& path) { void exitActivity() {
if (!SD.exists(path.c_str())) { if (currentActivity) {
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str()); currentActivity->onExit();
return nullptr; delete currentActivity;
}
auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
if (epub->load()) {
return epub;
}
Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
return nullptr;
}
void exitScreen() {
if (currentScreen) {
currentScreen->onExit();
delete currentScreen;
} }
} }
void enterNewScreen(Screen* screen) { void enterNewActivity(Activity* activity) {
currentScreen = screen; currentActivity = activity;
currentScreen->onEnter(); currentActivity->onEnter();
} }
// Verify long press on wake-up from deep sleep // Verify long press on wake-up from deep sleep
void verifyWakeupLongPress() { void verifyWakeupLongPress() {
// Give the user up to 1000ms to start holding the power button, and must hold for POWER_BUTTON_WAKEUP_MS // Give the user up to 1000ms to start holding the power button, and must hold for SETTINGS.getPowerButtonDuration()
const auto start = millis(); const auto start = millis();
bool abort = false; bool abort = false;
Serial.printf("[%lu] [ ] Verifying power button press\n", millis());
inputManager.update(); inputManager.update();
// Verify the user has actually pressed
while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) { while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) {
delay(50); delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration.
inputManager.update(); inputManager.update();
} }
if (inputManager.isPressed(InputManager::BTN_POWER)) { if (inputManager.isPressed(InputManager::BTN_POWER)) {
do { do {
delay(50); delay(10);
inputManager.update(); inputManager.update();
} while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < POWER_BUTTON_WAKEUP_MS); } while (inputManager.isPressed(InputManager::BTN_POWER) &&
abort = inputManager.getHeldTime() < POWER_BUTTON_WAKEUP_MS; inputManager.getHeldTime() < SETTINGS.getPowerButtonDuration());
abort = inputManager.getHeldTime() < SETTINGS.getPowerButtonDuration();
} else { } else {
abort = true; abort = true;
} }
@ -134,10 +114,10 @@ void waitForPowerRelease() {
// Enter deep sleep mode // Enter deep sleep mode
void enterDeepSleep() { void enterDeepSleep() {
exitScreen(); exitActivity();
enterNewScreen(new SleepScreen(renderer, inputManager)); enterNewActivity(new SleepActivity(renderer, inputManager));
Serial.printf("[%lu] [ ] Power button released after a long press. Entering deep sleep.\n", millis()); Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis());
delay(1000); // Allow Serial buffer to empty and display to update delay(1000); // Allow Serial buffer to empty and display to update
// Enable Wakeup on LOW (button press) // Enable Wakeup on LOW (button press)
@ -150,33 +130,20 @@ void enterDeepSleep() {
} }
void onGoHome(); void onGoHome();
void onSelectEpubFile(const std::string& path) { void onGoToReader(const std::string& initialEpubPath) {
exitScreen(); exitActivity();
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading...")); enterNewActivity(new ReaderActivity(renderer, inputManager, initialEpubPath, onGoHome));
auto epub = loadEpub(path);
if (epub) {
appState.openEpubPath = path;
appState.saveToFile();
exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
} else {
exitScreen();
enterNewScreen(
new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, EInkDisplay::HALF_REFRESH));
delay(2000);
onGoHome();
}
} }
void onGoToReaderHome() { onGoToReader(std::string()); }
void onGoToSettings() { void onGoToSettings() {
exitScreen(); exitActivity();
enterNewScreen(new SettingsScreen(renderer, inputManager, onGoHome)); enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome));
} }
void onGoHome() { void onGoHome() {
exitScreen(); exitActivity();
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings)); enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings));
} }
void setup() { void setup() {
@ -185,14 +152,25 @@ void setup() {
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis()); Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
inputManager.begin(); inputManager.begin();
verifyWakeupLongPress();
// Initialize pins // Initialize pins
pinMode(BAT_GPIO0, INPUT); pinMode(BAT_GPIO0, INPUT);
// Initialize SPI with custom pins // Initialize SPI with custom pins
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS); SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
// SD Card Initialization
if (!SD.begin(SD_SPI_CS, SPI, SPI_FQ)) {
Serial.printf("[%lu] [ ] SD card initialization failed\n", millis());
exitActivity();
enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "SD card error", BOLD));
return;
}
SETTINGS.loadFromFile();
// verify power button press duration after we've read settings.
verifyWakeupLongPress();
// Initialize display // Initialize display
einkDisplay.begin(); einkDisplay.begin();
Serial.printf("[%lu] [ ] Display initialized\n", millis()); Serial.printf("[%lu] [ ] Display initialized\n", millis());
@ -202,27 +180,15 @@ void setup() {
renderer.insertFont(SMALL_FONT_ID, smallFontFamily); renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
Serial.printf("[%lu] [ ] Fonts setup\n", millis()); Serial.printf("[%lu] [ ] Fonts setup\n", millis());
exitScreen(); exitActivity();
enterNewScreen(new BootLogoScreen(renderer, inputManager)); enterNewActivity(new BootActivity(renderer, inputManager));
// SD Card Initialization APP_STATE.loadFromFile();
SD.begin(SD_SPI_CS, SPI, SPI_FQ); if (APP_STATE.openEpubPath.empty()) {
onGoHome();
SETTINGS.loadFromFile(); } else {
appState.loadFromFile(); onGoToReader(APP_STATE.openEpubPath);
if (!appState.openEpubPath.empty()) {
auto epub = loadEpub(appState.openEpubPath);
if (epub) {
exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
// Ensure we're not still holding the power button before leaving setup
waitForPowerRelease();
return;
} }
}
exitScreen();
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings));
// Ensure we're not still holding the power button before leaving setup // Ensure we're not still holding the power button before leaving setup
waitForPowerRelease(); waitForPowerRelease();
@ -253,13 +219,14 @@ void loop() {
return; return;
} }
if (inputManager.wasReleased(InputManager::BTN_POWER) && inputManager.getHeldTime() > POWER_BUTTON_WAKEUP_MS) { if (inputManager.wasReleased(InputManager::BTN_POWER) &&
inputManager.getHeldTime() > SETTINGS.getPowerButtonDuration()) {
enterDeepSleep(); enterDeepSleep();
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
return; return;
} }
if (currentScreen) { if (currentActivity) {
currentScreen->handleInput(); currentActivity->loop();
} }
} }

View File

@ -1,8 +0,0 @@
#pragma once
#include "Screen.h"
class BootLogoScreen final : public Screen {
public:
explicit BootLogoScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
void onEnter() override;
};

View File

@ -1,21 +0,0 @@
#pragma once
#include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <string>
#include <utility>
#include "Screen.h"
class FullScreenMessageScreen final : public Screen {
std::string text;
EpdFontStyle style;
EInkDisplay::RefreshMode refreshMode;
public:
explicit FullScreenMessageScreen(GfxRenderer& renderer, InputManager& inputManager, std::string text,
const EpdFontStyle style = REGULAR,
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
: Screen(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {}
void onEnter() override;
};

View File

@ -1,17 +0,0 @@
#pragma once
#include <InputManager.h>
class GfxRenderer;
class Screen {
protected:
GfxRenderer& renderer;
InputManager& inputManager;
public:
explicit Screen(GfxRenderer& renderer, InputManager& inputManager) : renderer(renderer), inputManager(inputManager) {}
virtual ~Screen() = default;
virtual void onEnter() {}
virtual void onExit() {}
virtual void handleInput() {}
};

View File

@ -1,24 +0,0 @@
#include "SleepScreen.h"
#include <GfxRenderer.h>
#include "CrossPointSettings.h"
#include "config.h"
#include "images/CrossLarge.h"
void SleepScreen::onEnter() {
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();
renderer.clearScreen();
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128);
renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING");
// Apply white screen if enabled in settings
if (!SETTINGS.whiteSleepScreen) {
renderer.invertScreen();
}
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
}

View File

@ -1,8 +0,0 @@
#pragma once
#include "Screen.h"
class SleepScreen final : public Screen {
public:
explicit SleepScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
void onEnter() override;
};