Compare commits

..

No commits in common. "master" and "0.6.0" have entirely different histories.

55 changed files with 19645 additions and 19186 deletions

View File

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

View File

@ -73,8 +73,10 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
coverImageItem = opfParser.items.at(opfParser.coverItemId);
}
if (!opfParser.tocNcxPath.empty()) {
tocNcxItem = opfParser.tocNcxPath;
if (opfParser.items.count("ncx")) {
tocNcxItem = opfParser.items.at("ncx");
} else if (opfParser.items.count("ncxtoc")) {
tocNcxItem = opfParser.items.at("ncxtoc");
}
for (auto& spineRef : opfParser.spineRefs) {
@ -148,54 +150,11 @@ bool Epub::load() {
return false;
}
initializeSpineItemSizes();
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
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 {
if (!SD.exists(cachePath.c_str())) {
Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis());
@ -298,8 +257,6 @@ bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
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) {
if (spineIndex < 0 || spineIndex >= spine.size()) {
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
@ -322,11 +279,6 @@ int Epub::getTocItemsCount() const { return toc.size(); }
// work out the section index for a toc index
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
// so we can find the spine index by looking for the href
for (int i = 0; i < spine.size(); i++) {
@ -341,11 +293,6 @@ int Epub::getSpineIndexForTocIndex(const int tocIndex) 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
// so we can find the toc index by looking for the href
for (int i = 0; i < toc.size(); i++) {
@ -357,22 +304,3 @@ int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
Serial.printf("[%lu] [EBP] TOC item not found\n", millis());
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,8 +20,6 @@ class Epub {
std::string filepath;
// the spine of the EPUB file
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
std::vector<EpubTocEntry> toc;
// the base path for items in the EPUB file
@ -32,7 +30,6 @@ class Epub {
bool findContentOpfFile(std::string* contentOpfFile) const;
bool parseContentOpf(const std::string& contentOpfFilePath);
bool parseTocNcxFile();
void initializeSpineItemSizes();
public:
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
@ -54,12 +51,8 @@ class Epub {
bool getItemSize(const std::string& itemHref, size_t* size) const;
std::string& getSpineItem(int spineIndex);
int getSpineItemsCount() const;
size_t getCumulativeSpineItemSize(const int spineIndex) const;
EpubTocEntry& getTocItem(int tocIndex);
EpubTocEntry& getTocItem(int tocTndex);
int getTocItemsCount() const;
int getSpineIndexForTocIndex(int tocIndex) const;
int getTocIndexForSpineIndex(int spineIndex) const;
size_t getBookSize() const;
uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead);
};

View File

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

View File

@ -27,16 +27,12 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
const size_t totalWordCount = words.size();
const int pageWidth = renderer.getScreenWidth() - horizontalMargin;
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;
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 wordStylesIt = wordStyles.begin();
@ -57,7 +53,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
ans[totalWordCount - 1] = totalWordCount - 1;
for (int i = totalWordCount - 2; i >= 0; --i) {
int currlen = -spaceWidth;
int currlen = -spaceWidth + indentWidth;
dp[i] = MAX_COST;
for (size_t j = i; j < totalWordCount; ++j) {
@ -129,6 +125,9 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
// Calculate spacing
int spareSpace = pageWidth - lineWordWidthSum;
if (wordWidthIndex == 0) {
spareSpace -= indentWidth;
}
int spacing = spaceWidth;
const bool isLastLine = lineBreak == totalWordCount;
@ -138,7 +137,8 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
}
// Calculate initial x position
uint16_t xpos = 0;
uint16_t xpos = (wordWidthIndex == 0) ? indentWidth : 0;
if (style == TextBlock::RIGHT_ALIGN) {
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
} else if (style == TextBlock::CENTER_ALIGN) {

View File

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

View File

@ -75,18 +75,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
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)) {
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
self->boldUntilDepth = min(self->boldUntilDepth, self->depth);

View File

@ -3,10 +3,6 @@
#include <HardwareSerial.h>
#include <ZipFile.h>
namespace {
constexpr const char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
}
bool ContentOpfParser::setup() {
parser = XML_ParserCreate(nullptr);
if (!parser) {
@ -115,28 +111,16 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
if (self->state == IN_MANIFEST && (strcmp(name, "item") == 0 || strcmp(name, "opf:item") == 0)) {
std::string itemId;
std::string href;
std::string mediaType;
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "id") == 0) {
itemId = atts[i + 1];
} else if (strcmp(atts[i], "href") == 0) {
href = self->baseContentPath + atts[i + 1];
} else if (strcmp(atts[i], "media-type") == 0) {
mediaType = atts[i + 1];
}
}
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;
}

View File

@ -1,189 +0,0 @@
#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;
}

View File

@ -1,52 +0,0 @@
#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,66 +119,6 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
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::invertScreen() const {
@ -192,19 +132,13 @@ void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) cons
einkDisplay.displayBuffer(refreshMode);
}
void GfxRenderer::displayWindow(const int x, const int y, const int width, const int height) const {
// Rotate coordinates from portrait (480x800) to landscape (800x480)
// Rotation: 90 degrees clockwise
// Portrait coordinates: (x, y) with dimensions (width, height)
// Landscape coordinates: (rotatedX, rotatedY) with dimensions (rotatedWidth, rotatedHeight)
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);
}
// TODO: Support partial window update
// void GfxRenderer::flushArea(const int x, const int y, const int width, const int height) const {
// const int rotatedX = y;
// const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
//
// einkDisplay.displayBuffer(EInkDisplay::FAST_REFRESH, rotatedX, rotatedY, height, width);
// }
// Note: Internal driver treats screen in command orientation, this library treats in portrait orientation
int GfxRenderer::getScreenWidth() { return EInkDisplay::DISPLAY_HEIGHT; }
@ -230,7 +164,7 @@ int GfxRenderer::getLineHeight(const int fontId) const {
uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
size_t GfxRenderer::getBufferSize() { return EInkDisplay::BUFFER_SIZE; }
void GfxRenderer::swapBuffers() const { einkDisplay.swapBuffers(); }
void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); }
@ -240,90 +174,6 @@ void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsb
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,
const bool pixelState, const EpdFontStyle style) const {
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);

View File

@ -2,29 +2,19 @@
#include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <FS.h>
#include <map>
#include "Bitmap.h"
class GfxRenderer {
public:
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
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;
RenderMode renderMode;
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
std::map<int, EpdFontFamily> fontMap;
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
EpdFontStyle style) const;
void freeBwBufferChunks();
public:
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW) {}
@ -37,8 +27,6 @@ class GfxRenderer {
static int getScreenWidth();
static int getScreenHeight();
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 clearScreen(uint8_t color = 0xFF) const;
@ -48,7 +36,6 @@ class GfxRenderer {
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 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
int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const;
@ -62,11 +49,9 @@ class GfxRenderer {
void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const;
void storeBwBuffer();
void restoreBwBuffer();
// Low level functions
uint8_t* getFrameBuffer() const;
static size_t getBufferSize();
void swapBuffers() const;
void grayscaleRevert() const;
};

View File

@ -62,10 +62,6 @@ long ZipFile::getDataOffset(const mz_zip_archive_file_stat& fileStat) const {
const uint64_t fileOffset = fileStat.m_local_header_ofs;
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);
const size_t read = fread(pLocalHeader, 1, localHeaderSize, file);
fclose(file);
@ -108,10 +104,6 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
}
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);
const auto deflatedDataSize = static_cast<size_t>(fileStat.m_comp_size);
@ -183,10 +175,6 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
}
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);
const auto deflatedDataSize = static_cast<size_t>(fileStat.m_comp_size);

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +0,0 @@
#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

@ -1,21 +0,0 @@
#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

@ -1,17 +0,0 @@
#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,8 +0,0 @@
#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

@ -1,87 +0,0 @@
#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

@ -1,14 +0,0 @@
#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

@ -1,103 +0,0 @@
#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

@ -1,29 +0,0 @@
#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,68 +0,0 @@
#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

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

View File

@ -16,13 +16,13 @@
#include "Battery.h"
#include "CrossPointSettings.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 "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
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
@ -41,7 +41,8 @@
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
InputManager inputManager;
GfxRenderer renderer(einkDisplay);
Activity* currentActivity;
Screen* currentScreen;
CrossPointState appState;
// Fonts
EpdFont bookerlyFont(&bookerly_2b);
@ -57,41 +58,60 @@ EpdFont ubuntu10Font(&ubuntu_10);
EpdFont ubuntuBold10Font(&ubuntu_bold_10);
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)
constexpr unsigned long AUTO_SLEEP_TIMEOUT_MS = 10 * 60 * 1000;
void exitActivity() {
if (currentActivity) {
currentActivity->onExit();
delete currentActivity;
std::unique_ptr<Epub> 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 exitScreen() {
if (currentScreen) {
currentScreen->onExit();
delete currentScreen;
}
}
void enterNewActivity(Activity* activity) {
currentActivity = activity;
currentActivity->onEnter();
void enterNewScreen(Screen* screen) {
currentScreen = screen;
currentScreen->onEnter();
}
// Verify long press on wake-up from deep sleep
void verifyWakeupLongPress() {
// Give the user up to 1000ms to start holding the power button, and must hold for SETTINGS.getPowerButtonDuration()
// Give the user up to 1000ms to start holding the power button, and must hold for POWER_BUTTON_WAKEUP_MS
const auto start = millis();
bool abort = false;
Serial.printf("[%lu] [ ] Verifying power button press\n", millis());
inputManager.update();
// Verify the user has actually pressed
while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) {
delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration.
delay(50);
inputManager.update();
}
if (inputManager.isPressed(InputManager::BTN_POWER)) {
do {
delay(10);
delay(50);
inputManager.update();
} while (inputManager.isPressed(InputManager::BTN_POWER) &&
inputManager.getHeldTime() < SETTINGS.getPowerButtonDuration());
abort = inputManager.getHeldTime() < SETTINGS.getPowerButtonDuration();
} while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < POWER_BUTTON_WAKEUP_MS);
abort = inputManager.getHeldTime() < POWER_BUTTON_WAKEUP_MS;
} else {
abort = true;
}
@ -114,10 +134,10 @@ void waitForPowerRelease() {
// Enter deep sleep mode
void enterDeepSleep() {
exitActivity();
enterNewActivity(new SleepActivity(renderer, inputManager));
exitScreen();
enterNewScreen(new SleepScreen(renderer, inputManager));
Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis());
Serial.printf("[%lu] [ ] Power button released after a long press. Entering deep sleep.\n", millis());
delay(1000); // Allow Serial buffer to empty and display to update
// Enable Wakeup on LOW (button press)
@ -130,20 +150,33 @@ void enterDeepSleep() {
}
void onGoHome();
void onGoToReader(const std::string& initialEpubPath) {
exitActivity();
enterNewActivity(new ReaderActivity(renderer, inputManager, initialEpubPath, onGoHome));
void onSelectEpubFile(const std::string& path) {
exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading..."));
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() {
exitActivity();
enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome));
exitScreen();
enterNewScreen(new SettingsScreen(renderer, inputManager, onGoHome));
}
void onGoHome() {
exitActivity();
enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings));
exitScreen();
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings));
}
void setup() {
@ -152,25 +185,14 @@ void setup() {
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
inputManager.begin();
verifyWakeupLongPress();
// Initialize pins
pinMode(BAT_GPIO0, INPUT);
// Initialize SPI with custom pins
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
einkDisplay.begin();
Serial.printf("[%lu] [ ] Display initialized\n", millis());
@ -180,16 +202,28 @@ void setup() {
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
Serial.printf("[%lu] [ ] Fonts setup\n", millis());
exitActivity();
enterNewActivity(new BootActivity(renderer, inputManager));
exitScreen();
enterNewScreen(new BootLogoScreen(renderer, inputManager));
APP_STATE.loadFromFile();
if (APP_STATE.openEpubPath.empty()) {
onGoHome();
} else {
onGoToReader(APP_STATE.openEpubPath);
// SD Card Initialization
SD.begin(SD_SPI_CS, SPI, SPI_FQ);
SETTINGS.loadFromFile();
appState.loadFromFile();
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
waitForPowerRelease();
}
@ -219,14 +253,13 @@ void loop() {
return;
}
if (inputManager.wasReleased(InputManager::BTN_POWER) &&
inputManager.getHeldTime() > SETTINGS.getPowerButtonDuration()) {
if (inputManager.wasReleased(InputManager::BTN_POWER) && inputManager.getHeldTime() > POWER_BUTTON_WAKEUP_MS) {
enterDeepSleep();
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
return;
}
if (currentActivity) {
currentActivity->loop();
if (currentScreen) {
currentScreen->handleInput();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
#include "FullScreenMessageActivity.h"
#include "FullScreenMessageScreen.h"
#include <GfxRenderer.h>
#include "config.h"
void FullScreenMessageActivity::onEnter() {
void FullScreenMessageScreen::onEnter() {
const auto height = renderer.getLineHeight(UI_FONT_ID);
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 "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;
};

17
src/screens/Screen.h Normal file
View File

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

View File

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

View File

@ -0,0 +1,24 @@
#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

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