Backup: Stable state with EPUB reader fixes (Freeze, OOM, Speed, State, Tooling)

This commit is contained in:
Antigravity Agent
2026-01-19 18:44:45 -05:00
parent 21277e03eb
commit 1237f01ac2
34 changed files with 1600 additions and 192 deletions

View File

@@ -14,7 +14,7 @@ CrossPointSettings CrossPointSettings::instance;
namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 18;
constexpr uint8_t SETTINGS_COUNT = 20;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace
@@ -48,6 +48,8 @@ bool CrossPointSettings::saveToFile() const {
serialization::writePod(outputFile, textAntiAliasing);
serialization::writePod(outputFile, hideBatteryPercentage);
serialization::writePod(outputFile, longPressChapterSkip);
serialization::writeString(outputFile, std::string(customFontFamily));
serialization::writePod(outputFile, customFontSize);
outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@@ -116,6 +118,15 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, longPressChapterSkip);
if (++settingsRead >= fileSettingsCount) break;
{
std::string fontStr;
serialization::readString(inputFile, fontStr);
strncpy(customFontFamily, fontStr.c_str(), sizeof(customFontFamily) - 1);
customFontFamily[sizeof(customFontFamily) - 1] = '\0';
}
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, customFontSize);
if (++settingsRead >= fileSettingsCount) break;
} while (false);
inputFile.close();
@@ -191,7 +202,33 @@ int CrossPointSettings::getRefreshFrequency() const {
}
}
#include <EpdFontLoader.h>
int CrossPointSettings::getReaderFontId() const {
if (fontFamily == FONT_CUSTOM) {
uint8_t targetSize = customFontSize;
if (targetSize == 0) {
switch (fontSize) {
case SMALL:
targetSize = 12;
break;
case MEDIUM:
default:
targetSize = 14;
break;
case LARGE:
targetSize = 16;
break;
case EXTRA_LARGE:
targetSize = 18;
break;
}
}
int id = EpdFontLoader::getBestFontId(customFontFamily, targetSize);
if (id != -1) return id;
// Fallback if custom font not found
}
switch (fontFamily) {
case BOOKERLY:
default:

View File

@@ -40,7 +40,7 @@ class CrossPointSettings {
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1 };
// Font family options
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2 };
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_CUSTOM = 3 };
// Font size options
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 };
@@ -77,7 +77,9 @@ class CrossPointSettings {
uint8_t sideButtonLayout = PREV_NEXT;
// Reader font settings
uint8_t fontFamily = BOOKERLY;
char customFontFamily[64] = "";
uint8_t fontSize = MEDIUM;
uint8_t customFontSize = 0; // 0 means use enum mapping
uint8_t lineSpacing = NORMAL;
uint8_t paragraphAlignment = JUSTIFIED;
// Auto-sleep timeout setting (default 10 minutes)

View File

@@ -38,7 +38,9 @@ bool CrossPointState::loadFromFile() {
return false;
}
Serial.printf("[%lu] [CPS] Reading OpenEpubPath\n", millis());
serialization::readString(inputFile, openEpubPath);
Serial.printf("[%lu] [CPS] Read OpenEpubPath: %s\n", millis(), openEpubPath.c_str());
if (version >= 2) {
serialization::readPod(inputFile, lastSleepImage);
} else {

View File

@@ -12,9 +12,15 @@ void BootActivity::onEnter() {
const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
Serial.println("[Boot] clearScreen done");
renderer.drawImage(CrossLarge, (pageWidth + 128) / 2, (pageHeight - 128) / 2, 128, 128);
Serial.println("[Boot] drawImage done");
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD);
Serial.println("[Boot] CrossPoint text done");
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "BOOTING");
Serial.println("[Boot] BOOTING text done");
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION);
Serial.println("[Boot] Version text done");
renderer.displayBuffer();
Serial.println("[Boot] displayBuffer done");
}

View File

@@ -59,7 +59,14 @@ void EpubReaderActivity::onEnter() {
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);
// Validation: If loaded index is invalid, reset to 0
if (currentSpineIndex >= epub->getSpineItemsCount()) {
Serial.printf("[%lu] [ERS] Loaded invalid spine index %d (max %d), resetting\n", millis(), currentSpineIndex,
epub->getSpineItemsCount());
currentSpineIndex = 0;
nextPageNumber = 0;
}
}
f.close();
}
@@ -69,8 +76,6 @@ void EpubReaderActivity::onEnter() {
int textSpineIndex = epub->getSpineIndexForTextReference();
if (textSpineIndex != 0) {
currentSpineIndex = textSpineIndex;
Serial.printf("[%lu] [ERS] Opened for first time, navigating to text reference at index %d\n", millis(),
textSpineIndex);
}
}
@@ -260,7 +265,7 @@ void EpubReaderActivity::renderScreen() {
if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
@@ -269,8 +274,6 @@ void EpubReaderActivity::renderScreen() {
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight)) {
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
// Progress bar dimensions
constexpr int barWidth = 200;
constexpr int barHeight = 10;
@@ -319,7 +322,6 @@ void EpubReaderActivity::renderScreen() {
return;
}
} else {
Serial.printf("[%lu] [ERS] Cache found, skipping build...\n", millis());
}
if (nextPageNumber == UINT16_MAX) {
@@ -353,11 +355,17 @@ void EpubReaderActivity::renderScreen() {
Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
section->clearCache();
section.reset();
return renderScreen();
// Prevent infinite recursion. If load fails, show error.
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Error loading page", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 330, "File system error or corruption", true);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer();
return;
}
const auto start = millis();
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
}
FsFile f;
@@ -386,11 +394,11 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
}
// Save bw buffer to reset buffer state after grayscale data sync
renderer.storeBwBuffer();
bool bufferStored = renderer.storeBwBuffer();
// grayscale rendering
// TODO: Only do this if font supports it
if (SETTINGS.textAntiAliasing) {
// Only do this if font supports it AND we successfully stored the backup buffer
if (SETTINGS.textAntiAliasing && bufferStored) {
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);

View File

@@ -75,7 +75,7 @@ void FileSelectionActivity::onEnter() {
updateRequired = true;
xTaskCreate(&FileSelectionActivity::taskTrampoline, "FileSelectionActivityTask",
2048, // Stack size
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle

View File

@@ -0,0 +1,139 @@
#include "FontSelectionActivity.h"
#include <EpdFontLoader.h>
#include <HardwareSerial.h>
#include "../../CrossPointSettings.h"
#include "../../fontIds.h"
#include "../../managers/FontManager.h"
FontSelectionActivity::FontSelectionActivity(GfxRenderer& renderer, MappedInputManager& inputManager,
std::function<void()> onClose)
: Activity("Font Selection", renderer, inputManager), onClose(onClose) {}
FontSelectionActivity::~FontSelectionActivity() {}
void FontSelectionActivity::onEnter() {
Serial.println("[FSA] onEnter start");
Activity::onEnter();
Serial.println("[FSA] Getting available families...");
fontFamilies = FontManager::getInstance().getAvailableFamilies();
Serial.printf("[FSA] Got %d families\n", fontFamilies.size());
std::string current = SETTINGS.customFontFamily;
Serial.printf("[FSA] Current setting: %s\n", current.c_str());
for (size_t i = 0; i < fontFamilies.size(); i++) {
if (fontFamilies[i] == current) {
selectedIndex = i;
Serial.printf("[FSA] Found current family at index %d\n", i);
// Adjust scroll
if (selectedIndex >= itemsPerPage) {
scrollOffset = selectedIndex - itemsPerPage / 2;
if (scrollOffset > (int)fontFamilies.size() - itemsPerPage) {
scrollOffset = std::max(0, (int)fontFamilies.size() - itemsPerPage);
}
}
break;
}
}
Serial.println("[FSA] Calling render()");
render();
Serial.println("[FSA] onEnter end");
}
void FontSelectionActivity::loop() {
bool update = false;
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onClose();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left) ||
mappedInput.wasPressed(MappedInputManager::Button::PageBack)) {
if (selectedIndex > 0) {
selectedIndex--;
if (selectedIndex < scrollOffset) {
scrollOffset = selectedIndex;
update = true;
} else {
update = true;
}
}
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right) ||
mappedInput.wasPressed(MappedInputManager::Button::PageForward)) {
if (selectedIndex < (int)fontFamilies.size() - 1) {
selectedIndex++;
if (selectedIndex >= scrollOffset + itemsPerPage) {
scrollOffset = selectedIndex - itemsPerPage + 1;
update = true;
} else {
update = true;
}
}
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
saveAndExit();
return;
}
if (update) {
render();
}
}
void FontSelectionActivity::saveAndExit() {
if (selectedIndex >= 0 && selectedIndex < (int)fontFamilies.size()) {
strncpy(SETTINGS.customFontFamily, fontFamilies[selectedIndex].c_str(), sizeof(SETTINGS.customFontFamily) - 1);
SETTINGS.customFontFamily[sizeof(SETTINGS.customFontFamily) - 1] = '\0';
SETTINGS.fontFamily = CrossPointSettings::FONT_CUSTOM;
SETTINGS.saveToFile();
// Reload fonts to make sure the newly selected font is loaded
EpdFontLoader::loadFontsFromSd(renderer);
}
onClose();
}
void FontSelectionActivity::render() const {
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Font", true, EpdFontFamily::BOLD);
int y = 50;
if (fontFamilies.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, 120, "No fonts found in /fonts", false);
renderer.drawCenteredText(UI_10_FONT_ID, 150, "Add .epdfont files to SD Card", false);
renderer.displayBuffer(); // ensure update
return;
}
for (int i = 0; i < itemsPerPage; i++) {
int idx = scrollOffset + i;
if (idx >= (int)fontFamilies.size()) break;
// Draw selection box
if (idx == selectedIndex) {
Serial.printf("[FSA] Drawing selected: %s at %d\n", fontFamilies[idx].c_str(), y);
renderer.fillRect(10, y - 2, 460, 24);
renderer.drawText(UI_10_FONT_ID, 20, y, fontFamilies[idx].c_str(), false); // false = white (on black box)
} else {
Serial.printf("[FSA] Drawing: %s at %d\n", fontFamilies[idx].c_str(), y);
renderer.drawText(UI_10_FONT_ID, 20, y, fontFamilies[idx].c_str(), true); // true = black (on white bg)
}
// Mark current active font
if (fontFamilies[idx] == SETTINGS.customFontFamily) {
renderer.drawText(UI_10_FONT_ID, 400, y, "*", idx != selectedIndex);
}
y += 30;
}
renderer.displayBuffer();
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include <GfxRenderer.h>
#include <MappedInputManager.h>
#include <functional>
#include <string>
#include <vector>
#include "../Activity.h"
class FontSelectionActivity : public Activity {
public:
FontSelectionActivity(GfxRenderer& renderer, MappedInputManager& inputManager, std::function<void()> onClose);
~FontSelectionActivity() override;
void onEnter() override;
void loop() override;
void render() const;
private:
std::function<void()> onClose;
std::vector<std::string> fontFamilies;
int selectedIndex = 0;
int scrollOffset = 0;
static constexpr int itemsPerPage = 8;
void saveAndExit();
};

View File

@@ -1,5 +1,6 @@
#include "SettingsActivity.h"
#include <EpdFontLoader.h>
#include <GfxRenderer.h>
#include <HardwareSerial.h>
@@ -7,13 +8,14 @@
#include "CalibreSettingsActivity.h"
#include "CrossPointSettings.h"
#include "FontSelectionActivity.h"
#include "MappedInputManager.h"
#include "OtaUpdateActivity.h"
#include "fontIds.h"
// Define the static settings list
namespace {
constexpr int settingsCount = 20;
constexpr int settingsCount = 21;
const SettingInfo settingsList[settingsCount] = {
// Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
@@ -31,7 +33,8 @@ const SettingInfo settingsList[settingsCount] = {
{"Prev, Next", "Next, Prev"}),
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
SettingInfo::Enum("Reader Font Family", &CrossPointSettings::fontFamily,
{"Bookerly", "Noto Sans", "Open Dyslexic"}),
{"Bookerly", "Noto Sans", "Open Dyslexic", "Custom"}),
SettingInfo::Action("Set Custom Font Family"),
SettingInfo::Enum("Reader Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
SettingInfo::Enum("Reader Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
SettingInfo::Value("Reader Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
@@ -112,6 +115,15 @@ void SettingsActivity::loop() {
selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0;
updateRequired = true;
}
if (updateRequired) {
// Ensure selected item is in view
if (selectedSettingIndex < scrollOffset) {
scrollOffset = selectedSettingIndex;
} else if (selectedSettingIndex >= scrollOffset + itemsPerPage) {
scrollOffset = selectedSettingIndex - itemsPerPage + 1;
}
}
}
void SettingsActivity::toggleCurrentSetting() {
@@ -121,6 +133,7 @@ void SettingsActivity::toggleCurrentSetting() {
}
const auto& setting = settingsList[selectedSettingIndex];
Serial.printf("[Settings] Toggling: '%s' (Type: %d)\n", setting.name, (int)setting.type);
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
// Toggle the boolean value using the member pointer
@@ -129,6 +142,12 @@ void SettingsActivity::toggleCurrentSetting() {
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
if (strcmp(setting.name, "Reader Font Family") == 0 || strcmp(setting.name, "Reader Font Size") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
EpdFontLoader::loadFontsFromSd(renderer);
xSemaphoreGive(renderingMutex);
}
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
// Decreasing would also be nice for large ranges I think but oh well can't have everything
const int8_t currentValue = SETTINGS.*(setting.valuePtr);
@@ -155,6 +174,17 @@ void SettingsActivity::toggleCurrentSetting() {
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
} else if (strcmp(setting.name, "Set Custom Font Family") == 0) {
Serial.println("[Settings] Launching FontSelectionActivity");
xSemaphoreTake(renderingMutex, portMAX_DELAY);
subActivity.reset(new FontSelectionActivity(renderer, mappedInput, [this] {
subActivity.reset();
updateRequired = true;
}));
subActivity->onEnter();
xSemaphoreGive(renderingMutex);
} else {
Serial.printf("[Settings] Unknown action: %s\n", setting.name);
}
} else {
// Only toggle if it's a toggle type and has a value pointer
@@ -187,28 +217,41 @@ void SettingsActivity::render() const {
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD);
// Draw selection
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
if (selectedSettingIndex >= scrollOffset && selectedSettingIndex < scrollOffset + itemsPerPage) {
renderer.fillRect(0, 60 + (selectedSettingIndex - scrollOffset) * 30 - 2, pageWidth - 1, 30);
}
// Draw visible settings
for (int i = 0; i < itemsPerPage; i++) {
int index = scrollOffset + i;
if (index >= settingsCount) break;
// Draw all settings
for (int i = 0; i < settingsCount; i++) {
const int settingY = 60 + i * 30; // 30 pixels between settings
// Draw setting name
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, i != selectedSettingIndex);
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[index].name, index != selectedSettingIndex);
// Draw value based on setting type
std::string valueText = "";
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
const bool value = SETTINGS.*(settingsList[i].valuePtr);
if (settingsList[index].type == SettingType::TOGGLE && settingsList[index].valuePtr != nullptr) {
const bool value = SETTINGS.*(settingsList[index].valuePtr);
valueText = value ? "ON" : "OFF";
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
valueText = settingsList[i].enumValues[value];
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
} else if (settingsList[index].type == SettingType::ENUM && settingsList[index].valuePtr != nullptr) {
const uint8_t value = SETTINGS.*(settingsList[index].valuePtr);
valueText = settingsList[index].enumValues[value];
} else if (settingsList[index].type == SettingType::VALUE && settingsList[index].valuePtr != nullptr) {
valueText = std::to_string(SETTINGS.*(settingsList[index].valuePtr));
} else if (settingsList[index].type == SettingType::ACTION &&
strcmp(settingsList[index].name, "Set Custom Font Family") == 0) {
if (SETTINGS.fontFamily == CrossPointSettings::FONT_CUSTOM) {
valueText = SETTINGS.customFontFamily;
}
}
if (!valueText.empty()) {
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(),
index != selectedSettingIndex);
}
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), i != selectedSettingIndex);
}
// Draw version text above button hints

View File

@@ -49,6 +49,8 @@ class SettingsActivity final : public ActivityWithSubactivity {
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
int selectedSettingIndex = 0; // Currently selected setting
int scrollOffset = 0; // Index of the first visible setting
static constexpr int itemsPerPage = 25;
const std::function<void()> onGoHome;
static void taskTrampoline(void* param);

View File

@@ -1,5 +1,6 @@
#include <Arduino.h>
#include <EInkDisplay.h>
#include <EpdFontLoader.h>
#include <Epub.h>
#include <GfxRenderer.h>
#include <InputManager.h>
@@ -263,15 +264,20 @@ void setupDisplayAndFonts() {
}
void setup() {
// force serial for debugging
Serial.begin(115200);
delay(500);
Serial.printf("[%lu] [DBG] setup() start - FIRMWARE DEBUG BUILD 001\n", millis());
Serial.flush();
t1 = millis();
// Only start serial if USB connected
pinMode(UART0_RXD, INPUT);
if (digitalRead(UART0_RXD) == HIGH) {
Serial.begin(115200);
}
inputManager.begin();
Serial.printf("[%lu] [DBG] inputManager initialized\n", millis());
// Initialize pins
pinMode(BAT_GPIO0, INPUT);
@@ -287,21 +293,37 @@ void setup() {
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD));
return;
}
Serial.printf("[%lu] [DBG] SdMan.begin() success\n", millis());
SETTINGS.loadFromFile();
Serial.printf("[%lu] [DBG] SETTINGS loaded\n", millis());
Serial.flush();
// verify power button press duration after we've read settings.
verifyWakeupLongPress();
// verifyWakeupLongPress(); // Disabled for debugging to prevent auto-shutdown
// Serial.printf("[%lu] [DBG] Wakeup long press verified\n", millis());
// First serial output only here to avoid timing inconsistencies for power button press duration verification
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
Serial.flush();
setupDisplayAndFonts();
Serial.printf("[%lu] [DBG] setupDisplayAndFonts done\n", millis());
Serial.flush();
EpdFontLoader::loadFontsFromSd(renderer);
Serial.printf("[%lu] [DBG] loadFontsFromSd done\n", millis());
Serial.flush();
exitActivity();
enterNewActivity(new BootActivity(renderer, mappedInputManager));
Serial.printf("[%lu] [DBG] BootActivity entered\n", millis());
Serial.flush();
APP_STATE.loadFromFile();
Serial.printf("[%lu] [DBG] APP_STATE loaded\n", millis());
if (APP_STATE.openEpubPath.empty()) {
onGoHome();
} else {
@@ -314,6 +336,7 @@ void setup() {
}
// Ensure we're not still holding the power button before leaving setup
Serial.printf("[%lu] [ ] Setup complete\n", millis());
waitForPowerRelease();
}

View File

@@ -0,0 +1,212 @@
#include "FontManager.h"
#include <GfxRenderer.h> // for EpdFontData usage validation if needed
#include <HardwareSerial.h>
#include <SDCardManager.h>
#include <algorithm>
#include "CustomEpdFont.h"
FontManager& FontManager::getInstance() {
static FontManager instance;
return instance;
}
FontManager::~FontManager() {
for (auto& familyPair : loadedFonts) {
for (auto& sizePair : familyPair.second) {
delete sizePair.second;
}
}
}
const std::vector<std::string>& FontManager::getAvailableFamilies() {
if (!scanned) {
scanFonts();
}
return availableFamilies;
}
void FontManager::scanFonts() {
Serial.println("[FM] Scanning fonts...");
availableFamilies.clear();
scanned = true;
FsFile fontDir;
if (!SdMan.openFileForRead("FontScan", "/fonts", fontDir)) {
Serial.println("[FM] Failed to open /fonts directory");
// Even if failed, we proceed to sort empty list to avoid crashes
return;
}
if (!fontDir.isDirectory()) {
Serial.println("[FM] /fonts is not a directory");
fontDir.close();
return;
}
Serial.println("[FM] /fonts opened. Iterating files...");
FsFile file;
while (file.openNext(&fontDir, O_READ)) {
if (!file.isDirectory()) {
char filename[128];
file.getName(filename, sizeof(filename));
Serial.printf("[FM] Checking: %s\n", filename);
String name = String(filename);
if (name.endsWith(".epdfont")) {
// Expected format: Family-Style-Size.epdfont
int firstDash = name.indexOf('-');
if (firstDash > 0) {
String family = name.substring(0, firstDash);
if (std::find(availableFamilies.begin(), availableFamilies.end(), family.c_str()) ==
availableFamilies.end()) {
availableFamilies.push_back(family.c_str());
Serial.printf("[FM] Added family: %s\n", family.c_str());
}
}
}
}
file.close();
}
fontDir.close();
std::sort(availableFamilies.begin(), availableFamilies.end());
Serial.printf("[FM] Scan complete. Found %d families\n", availableFamilies.size());
}
struct EpdfHeader {
char magic[4];
uint32_t intervalCount;
uint32_t totalGlyphCount;
uint8_t advanceY;
int32_t ascender;
int32_t descender;
uint8_t is2Bit;
};
// Helper to load a single font file
CustomEpdFont* loadFontFile(const String& path) {
Serial.printf("[FontMgr] Loading file: %s\n", path.c_str());
Serial.flush();
FsFile f;
if (!SdMan.openFileForRead("FontLoading", path.c_str(), f)) {
Serial.printf("[FontMgr] Failed to open: %s\n", path.c_str());
Serial.flush();
return nullptr;
}
// Read custom header format (detected from file dump)
// 0: Magic (4)
// 4: IntervalCount (4)
// 8: FileSize (4)
// 12: Height (4) -> advanceY
// 16: GlyphCount (4)
// 20: Ascender (4)
// 24: Unknown (4)
// 28: Descender (4)
// 32: Unknown (4)
// 36: OffsetIntervals (4)
// 40: OffsetGlyphs (4)
// 44: OffsetBitmaps (4)
uint32_t buf[12]; // 48 bytes
if (f.read(buf, 48) != 48) {
Serial.printf("[FontMgr] Header read failed for %s\n", path.c_str());
f.close();
return nullptr;
}
if (strncmp((char*)&buf[0], "EPDF", 4) != 0) {
Serial.printf("[FontMgr] Invalid magic for %s\n", path.c_str());
f.close();
return nullptr;
}
uint32_t intervalCount = buf[1];
uint32_t fileSize = buf[2];
uint32_t height = buf[3];
uint32_t glyphCount = buf[4];
int32_t ascender = (int32_t)buf[5];
int32_t descender = (int32_t)buf[7];
uint32_t offsetIntervals = buf[9];
uint32_t offsetGlyphs = buf[10];
uint32_t offsetBitmaps = buf[11];
Serial.printf("[FontMgr] parsed header: intv=%u, glyphs=%u, fileSz=%u, h=%u, asc=%d, desc=%d\n", intervalCount,
glyphCount, fileSize, height, ascender, descender);
Serial.printf("[FontMgr] offsets: intv=%u, gly=%u, bmp=%u\n", offsetIntervals, offsetGlyphs, offsetBitmaps);
// Validation
if (offsetIntervals >= fileSize || offsetGlyphs >= fileSize || offsetBitmaps >= fileSize) {
Serial.println("[FontMgr] Invalid offsets in header");
f.close();
return nullptr;
}
// We need to load intervals into RAM
EpdUnicodeInterval* intervals = new (std::nothrow) EpdUnicodeInterval[intervalCount];
if (!intervals) {
Serial.printf("[FontMgr] Failed to allocate intervals: %d\n", intervalCount);
f.close();
return nullptr;
}
if (!f.seekSet(offsetIntervals)) {
Serial.println("[FontMgr] Failed to seek to intervals");
delete[] intervals;
f.close();
return nullptr;
}
f.read((uint8_t*)intervals, intervalCount * sizeof(EpdUnicodeInterval));
f.close();
// Create EpdFontData
EpdFontData* fontData = new (std::nothrow) EpdFontData();
if (!fontData) {
Serial.println("[FontMgr] Failed to allocate EpdFontData! OOM.");
delete[] intervals;
return nullptr;
}
fontData->intervalCount = intervalCount;
fontData->intervals = intervals;
fontData->glyph = nullptr;
fontData->advanceY = (uint8_t)height;
fontData->ascender = ascender;
fontData->descender = descender;
fontData->descender = descender;
fontData->is2Bit = (buf[8] != 0);
fontData->bitmap = nullptr;
return new CustomEpdFont(path, fontData, offsetIntervals, offsetGlyphs, offsetBitmaps);
}
EpdFontFamily* FontManager::getCustomFontFamily(const std::string& familyName, int fontSize) {
if (loadedFonts[familyName][fontSize]) {
return loadedFonts[familyName][fontSize];
}
String basePath = "/fonts/" + String(familyName.c_str()) + "-";
String sizeStr = String(fontSize);
CustomEpdFont* regular = loadFontFile(basePath + "Regular-" + sizeStr + ".epdfont");
CustomEpdFont* bold = loadFontFile(basePath + "Bold-" + sizeStr + ".epdfont");
CustomEpdFont* italic = loadFontFile(basePath + "Italic-" + sizeStr + ".epdfont");
CustomEpdFont* boldItalic = loadFontFile(basePath + "BoldItalic-" + sizeStr + ".epdfont");
if (!regular) {
if (bold) regular = bold;
}
if (regular) {
EpdFontFamily* family = new EpdFontFamily(regular, bold, italic, boldItalic);
loadedFonts[familyName][fontSize] = family;
return family;
}
return nullptr;
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <map>
#include <string>
#include <vector>
#include "EpdFontFamily.h"
class FontManager {
public:
static FontManager& getInstance();
// Scan SD card for fonts
void scanFonts();
// Get list of available font family names
const std::vector<std::string>& getAvailableFamilies();
// Load a specific family and size (returns pointer to cached family or new one)
EpdFontFamily* getCustomFontFamily(const std::string& familyName, int fontSize);
private:
FontManager() = default;
~FontManager();
std::vector<std::string> availableFamilies;
bool scanned = false;
// Map: FamilyName -> Size -> EpdFontFamily*
std::map<std::string, std::map<int, EpdFontFamily*>> loadedFonts;
};