Compare commits

..

No commits in common. "77c655fcf5f3af9969f76718ed04f60dfd106474" and "0d32d21d756c5b45b5a7b3b3d0a29429763387ff" have entirely different histories.

45 changed files with 281 additions and 3792 deletions

View File

@ -59,32 +59,11 @@ See the [webserver docs](./docs/webserver.md) for more information on how to con
### 3.5 Settings ### 3.5 Settings
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust: The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
- **Sleep Screen**: Which sleep screen to display when the device sleeps, options are: - **White Sleep Screen**: Whether to use the white screen or black (inverted) default sleep screen
- "Dark" (default) - The default dark sleep screen
- "Light" - The same default sleep screen, on a white background
- "Custom" - Custom images from the SD card, see [3.6 Sleep Screen](#36-sleep-screen) below for more information
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
- **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book, if disabled, - **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book, if disabled,
paragraphs will not have vertical space between them, but will have first word indentation. paragraphs will not have vertical space between them, but will have first word indentation.
- **Short Power Button Click**: Whether to trigger the power button on a short press or a long press. - **Short Power Button Click**: Whether to trigger the power button on a short press or a long press.
### 3.6 Sleep Screen
You can customize the sleep screen by placing custom images in specific locations on the SD card:
- **Single Image:** Place a file named `sleep.bmp` in the root directory.
- **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images
inside. If images are found in this directory, they will take priority over the `sleep.png` file, and one will be
randomly selected each time the device sleeps.
> [!NOTE]
> You'll need to set the **Sleep Screen** setting to **Custom** in order to use these images.
> [!TIP]
> For best results:
> - Use uncompressed BMP files with 24-bit color depth
> - Use a resolution of 480x800 pixels to match the device's screen resolution.
--- ---
## 4. Reading Mode ## 4. Reading Mode

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
#include "Epub.h" #include "Epub.h"
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <JpegToBmpConverter.h>
#include <SD.h> #include <SD.h>
#include <ZipFile.h> #include <ZipFile.h>
@ -94,42 +93,24 @@ bool Epub::parseTocNcxFile() {
Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str()); Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str());
const auto tmpNcxPath = getCachePath() + "/toc.ncx"; size_t tocSize;
File tempNcxFile = SD.open(tmpNcxPath.c_str(), FILE_WRITE); if (!getItemSize(tocNcxItem, &tocSize)) {
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024); Serial.printf("[%lu] [EBP] Could not get size of toc ncx\n", millis());
tempNcxFile.close(); return false;
tempNcxFile = SD.open(tmpNcxPath.c_str(), FILE_READ); }
const auto ncxSize = tempNcxFile.size();
TocNcxParser ncxParser(contentBasePath, ncxSize); TocNcxParser ncxParser(contentBasePath, tocSize);
if (!ncxParser.setup()) { if (!ncxParser.setup()) {
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis()); Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
return false; return false;
} }
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024)); if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
if (!ncxBuffer) { Serial.printf("[%lu] [EBP] Could not read toc ncx stream\n", millis());
Serial.printf("[%lu] [EBP] Could not allocate memory for toc ncx parser\n", millis());
return false; return false;
} }
while (tempNcxFile.available()) {
const auto readSize = tempNcxFile.read(ncxBuffer, 1024);
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
if (processedSize != readSize) {
Serial.printf("[%lu] [EBP] Could not process all toc ncx data\n", millis());
free(ncxBuffer);
tempNcxFile.close();
return false;
}
}
free(ncxBuffer);
tempNcxFile.close();
SD.remove(tmpNcxPath.c_str());
this->toc = std::move(ncxParser.toc); this->toc = std::move(ncxParser.toc);
Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size()); Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size());
@ -219,45 +200,7 @@ const std::string& Epub::getPath() const { return filepath; }
const std::string& Epub::getTitle() const { return title; } const std::string& Epub::getTitle() const { return title; }
std::string Epub::getCoverBmpPath() const { return cachePath + "/cover.bmp"; } const std::string& Epub::getCoverImageItem() const { return coverImageItem; }
bool Epub::generateCoverBmp() const {
// Already generated, return true
if (SD.exists(getCoverBmpPath().c_str())) {
return true;
}
if (coverImageItem.empty()) {
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
return false;
}
if (coverImageItem.substr(coverImageItem.length() - 4) == ".jpg" ||
coverImageItem.substr(coverImageItem.length() - 5) == ".jpeg") {
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image\n", millis());
File coverJpg = SD.open((getCachePath() + "/.cover.jpg").c_str(), FILE_WRITE, true);
readItemContentsToStream(coverImageItem, coverJpg, 1024);
coverJpg.close();
coverJpg = SD.open((getCachePath() + "/.cover.jpg").c_str(), FILE_READ);
File coverBmp = SD.open(getCoverBmpPath().c_str(), FILE_WRITE, true);
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp);
coverJpg.close();
coverBmp.close();
SD.remove((getCachePath() + "/.cover.jpg").c_str());
if (!success) {
Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis());
SD.remove(getCoverBmpPath().c_str());
}
Serial.printf("[%lu] [EBP] Generated BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no");
return success;
} else {
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping\n", millis());
}
return false;
}
std::string normalisePath(const std::string& path) { std::string normalisePath(const std::string& path) {
std::vector<std::string> components; std::vector<std::string> components;
@ -350,7 +293,7 @@ std::string& Epub::getSpineItem(const int spineIndex) {
} }
EpubTocEntry& Epub::getTocItem(const int tocTndex) { EpubTocEntry& Epub::getTocItem(const int tocTndex) {
static EpubTocEntry emptyEntry = {}; static EpubTocEntry emptyEntry("", "", "", 0);
if (toc.empty()) { if (toc.empty()) {
Serial.printf("[%lu] [EBP] getTocItem called but toc is empty\n", millis()); Serial.printf("[%lu] [EBP] getTocItem called but toc is empty\n", millis());
return emptyEntry; return emptyEntry;

View File

@ -48,8 +48,7 @@ class Epub {
const std::string& getCachePath() const; const std::string& getCachePath() const;
const std::string& getPath() const; const std::string& getPath() const;
const std::string& getTitle() const; const std::string& getTitle() const;
std::string getCoverBmpPath() const; const std::string& getCoverImageItem() const;
bool generateCoverBmp() const;
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr, uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
bool trailingNullByte = false) const; bool trailingNullByte = false) const;
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const; bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;

View File

@ -2,9 +2,12 @@
#include <string> #include <string>
struct EpubTocEntry { class EpubTocEntry {
public:
std::string title; std::string title;
std::string href; std::string href;
std::string anchor; std::string anchor;
uint8_t level; int level;
EpubTocEntry(std::string title, std::string href, std::string anchor, const int level)
: title(std::move(title)), href(std::move(href)), anchor(std::move(anchor)), level(level) {}
}; };

View File

@ -144,7 +144,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
const int spareSpace = pageWidth - lineWordWidthSum; const int spareSpace = pageWidth - lineWordWidthSum;
int spacing = spaceWidth; int spacing = spaceWidth;
const bool isLastLine = breakIndex == lineBreakIndices.size() - 1; const bool isLastLine = lineBreak == words.size();
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) { if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) {
spacing = spareSpace / (lineWordCount - 1); spacing = spareSpace / (lineWordCount - 1);

View File

@ -155,7 +155,7 @@ void XMLCALL TocNcxParser::endElement(void* userData, const XML_Char* name) {
} }
// Push to vector // Push to vector
self->toc.push_back({std::move(self->currentLabel), std::move(href), std::move(anchor), self->currentDepth}); self->toc.emplace_back(self->currentLabel, href, anchor, self->currentDepth);
// Clear them so we don't re-add them if there are weird XML structures // Clear them so we don't re-add them if there are weird XML structures
self->currentLabel.clear(); self->currentLabel.clear();

View File

@ -17,7 +17,7 @@ class TocNcxParser final : public Print {
std::string currentLabel; std::string currentLabel;
std::string currentSrc; std::string currentSrc;
uint8_t currentDepth = 0; size_t currentDepth = 0;
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts); static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
static void characterData(void* userData, const XML_Char* s, int len); static void characterData(void* userData, const XML_Char* s, int len);

View File

@ -128,7 +128,7 @@ BmpReaderError Bitmap::readRow(uint8_t* data, uint8_t* rowBuffer) const {
int bitShift = 6; int bitShift = 6;
// Helper lambda to pack 2bpp color into the output stream // Helper lambda to pack 2bpp color into the output stream
auto packPixel = [&](const uint8_t lum) { auto packPixel = [&](uint8_t lum) {
uint8_t color = (lum >> 6); // Simple 2-bit reduction: 0-255 -> 0-3 uint8_t color = (lum >> 6); // Simple 2-bit reduction: 0-255 -> 0-3
currentOutByte |= (color << bitShift); currentOutByte |= (color << bitShift);
if (bitShift == 0) { if (bitShift == 0) {
@ -140,49 +140,38 @@ BmpReaderError Bitmap::readRow(uint8_t* data, uint8_t* rowBuffer) const {
} }
}; };
uint8_t lum;
switch (bpp) { switch (bpp) {
case 32: {
const uint8_t* p = rowBuffer;
for (int x = 0; x < width; x++) {
lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
packPixel(lum);
p += 4;
}
break;
}
case 24: {
const uint8_t* p = rowBuffer;
for (int x = 0; x < width; x++) {
lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
packPixel(lum);
p += 3;
}
break;
}
case 8: { case 8: {
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
packPixel(paletteLum[rowBuffer[x]]); packPixel(paletteLum[rowBuffer[x]]);
} }
break; break;
} }
case 2: { case 24: {
const uint8_t* p = rowBuffer;
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
lum = paletteLum[(rowBuffer[x >> 2] >> (6 - ((x & 3) * 2))) & 0x03]; uint8_t lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
packPixel(lum); packPixel(lum);
p += 3;
} }
break; break;
} }
case 1: { case 1: {
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
lum = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 0xFF : 0x00; uint8_t lum = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 0xFF : 0x00;
packPixel(lum); packPixel(lum);
} }
break; break;
} }
default: case 32: {
return BmpReaderError::UnsupportedBpp; 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 // Flush remaining bits if width is not a multiple of 4

View File

@ -1,244 +0,0 @@
#include "JpegToBmpConverter.h"
#include <picojpeg.h>
#include <cstdio>
#include <cstring>
// Context structure for picojpeg callback
struct JpegReadContext {
File& file;
uint8_t buffer[512];
size_t bufferPos;
size_t bufferFilled;
};
// Helper function: Convert 8-bit grayscale to 2-bit (0-3)
uint8_t JpegToBmpConverter::grayscaleTo2Bit(const uint8_t grayscale) {
// Simple threshold mapping:
// 0-63 -> 0 (black)
// 64-127 -> 1 (dark gray)
// 128-191 -> 2 (light gray)
// 192-255 -> 3 (white)
return grayscale >> 6;
}
inline void write16(Print& out, const uint16_t value) {
// out.write(reinterpret_cast<const uint8_t *>(&value), 2);
out.write(value & 0xFF);
out.write((value >> 8) & 0xFF);
}
inline void write32(Print& out, const uint32_t value) {
// out.write(reinterpret_cast<const uint8_t *>(&value), 4);
out.write(value & 0xFF);
out.write((value >> 8) & 0xFF);
out.write((value >> 16) & 0xFF);
out.write((value >> 24) & 0xFF);
}
inline void write32Signed(Print& out, const int32_t value) {
// out.write(reinterpret_cast<const uint8_t *>(&value), 4);
out.write(value & 0xFF);
out.write((value >> 8) & 0xFF);
out.write((value >> 16) & 0xFF);
out.write((value >> 24) & 0xFF);
}
// Helper function: Write BMP header with 2-bit color depth
void JpegToBmpConverter::writeBmpHeader(Print& bmpOut, const int width, const int height) {
// Calculate row padding (each row must be multiple of 4 bytes)
const int bytesPerRow = (width * 2 + 31) / 32 * 4; // 2 bits per pixel, round up
const int imageSize = bytesPerRow * height;
const uint32_t fileSize = 70 + imageSize; // 14 (file header) + 40 (DIB header) + 16 (palette) + image
// BMP File Header (14 bytes)
bmpOut.write('B');
bmpOut.write('M');
write32(bmpOut, fileSize); // File size
write32(bmpOut, 0); // Reserved
write32(bmpOut, 70); // Offset to pixel data
// DIB Header (BITMAPINFOHEADER - 40 bytes)
write32(bmpOut, 40);
write32Signed(bmpOut, width);
write32Signed(bmpOut, -height); // Negative height = top-down bitmap
write16(bmpOut, 1); // Color planes
write16(bmpOut, 2); // Bits per pixel (2 bits)
write32(bmpOut, 0); // BI_RGB (no compression)
write32(bmpOut, imageSize);
write32(bmpOut, 2835); // xPixelsPerMeter (72 DPI)
write32(bmpOut, 2835); // yPixelsPerMeter (72 DPI)
write32(bmpOut, 4); // colorsUsed
write32(bmpOut, 4); // colorsImportant
// Color Palette (4 colors x 4 bytes = 16 bytes)
// Format: Blue, Green, Red, Reserved (BGRA)
uint8_t palette[16] = {
0x00, 0x00, 0x00, 0x00, // Color 0: Black
0x55, 0x55, 0x55, 0x00, // Color 1: Dark gray (85)
0xAA, 0xAA, 0xAA, 0x00, // Color 2: Light gray (170)
0xFF, 0xFF, 0xFF, 0x00 // Color 3: White
};
for (const uint8_t i : palette) {
bmpOut.write(i);
}
}
// Callback function for picojpeg to read JPEG data
unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const unsigned char buf_size,
unsigned char* pBytes_actually_read, void* pCallback_data) {
auto* context = static_cast<JpegReadContext*>(pCallback_data);
if (!context || !context->file) {
return PJPG_STREAM_READ_ERROR;
}
// Check if we need to refill our context buffer
if (context->bufferPos >= context->bufferFilled) {
context->bufferFilled = context->file.read(context->buffer, sizeof(context->buffer));
context->bufferPos = 0;
if (context->bufferFilled == 0) {
// EOF or error
*pBytes_actually_read = 0;
return 0; // Success (EOF is normal)
}
}
// Copy available bytes to picojpeg's buffer
const size_t available = context->bufferFilled - context->bufferPos;
const size_t toRead = available < buf_size ? available : buf_size;
memcpy(pBuf, context->buffer + context->bufferPos, toRead);
context->bufferPos += toRead;
*pBytes_actually_read = static_cast<unsigned char>(toRead);
return 0; // Success
}
// Core function: Convert JPEG file to 2-bit BMP
bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
Serial.printf("[%lu] [JPG] Converting JPEG to BMP\n", millis());
// Setup context for picojpeg callback
JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0};
// Initialize picojpeg decoder
pjpeg_image_info_t imageInfo;
const unsigned char status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
if (status != 0) {
Serial.printf("[%lu] [JPG] JPEG decode init failed with error code: %d\n", millis(), status);
return false;
}
Serial.printf("[%lu] [JPG] JPEG dimensions: %dx%d, components: %d, MCUs: %dx%d\n", millis(), imageInfo.m_width,
imageInfo.m_height, imageInfo.m_comps, imageInfo.m_MCUSPerRow, imageInfo.m_MCUSPerCol);
// Write BMP header
writeBmpHeader(bmpOut, imageInfo.m_width, imageInfo.m_height);
// Calculate row parameters
const int bytesPerRow = (imageInfo.m_width * 2 + 31) / 32 * 4;
// Allocate row buffer for packed 2-bit pixels
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
if (!rowBuffer) {
Serial.printf("[%lu] [JPG] Failed to allocate row buffer\n", millis());
return false;
}
// Allocate a buffer for one MCU row worth of grayscale pixels
// This is the minimal memory needed for streaming conversion
const int mcuPixelHeight = imageInfo.m_MCUHeight;
const int mcuRowPixels = imageInfo.m_width * mcuPixelHeight;
auto* mcuRowBuffer = static_cast<uint8_t*>(malloc(mcuRowPixels));
if (!mcuRowBuffer) {
Serial.printf("[%lu] [JPG] Failed to allocate MCU row buffer\n", millis());
free(rowBuffer);
return false;
}
// Process MCUs row-by-row and write to BMP as we go (top-down)
const int mcuPixelWidth = imageInfo.m_MCUWidth;
for (int mcuY = 0; mcuY < imageInfo.m_MCUSPerCol; mcuY++) {
// Clear the MCU row buffer
memset(mcuRowBuffer, 0, mcuRowPixels);
// Decode one row of MCUs
for (int mcuX = 0; mcuX < imageInfo.m_MCUSPerRow; mcuX++) {
const unsigned char mcuStatus = pjpeg_decode_mcu();
if (mcuStatus != 0) {
if (mcuStatus == PJPG_NO_MORE_BLOCKS) {
Serial.printf("[%lu] [JPG] Unexpected end of blocks at MCU (%d, %d)\n", millis(), mcuX, mcuY);
} else {
Serial.printf("[%lu] [JPG] JPEG decode MCU failed at (%d, %d) with error code: %d\n", millis(), mcuX, mcuY,
mcuStatus);
}
free(mcuRowBuffer);
free(rowBuffer);
return false;
}
// Process MCU block into MCU row buffer
for (int blockY = 0; blockY < mcuPixelHeight; blockY++) {
for (int blockX = 0; blockX < mcuPixelWidth; blockX++) {
const int pixelX = mcuX * mcuPixelWidth + blockX;
// Skip pixels outside image width (can happen with MCU alignment)
if (pixelX >= imageInfo.m_width) {
continue;
}
// Get grayscale value
uint8_t gray;
if (imageInfo.m_comps == 1) {
// Grayscale image
gray = imageInfo.m_pMCUBufR[blockY * mcuPixelWidth + blockX];
} else {
// RGB image - convert to grayscale
const uint8_t r = imageInfo.m_pMCUBufR[blockY * mcuPixelWidth + blockX];
const uint8_t g = imageInfo.m_pMCUBufG[blockY * mcuPixelWidth + blockX];
const uint8_t b = imageInfo.m_pMCUBufB[blockY * mcuPixelWidth + blockX];
// Luminance formula: Y = 0.299*R + 0.587*G + 0.114*B
// Using integer approximation: (30*R + 59*G + 11*B) / 100
gray = (r * 30 + g * 59 + b * 11) / 100;
}
// Store grayscale value in MCU row buffer
mcuRowBuffer[blockY * imageInfo.m_width + pixelX] = gray;
}
}
}
// Write all pixel rows from this MCU row to BMP file
const int startRow = mcuY * mcuPixelHeight;
const int endRow = (mcuY + 1) * mcuPixelHeight;
for (int y = startRow; y < endRow && y < imageInfo.m_height; y++) {
memset(rowBuffer, 0, bytesPerRow);
// Pack 4 pixels per byte (2 bits each)
for (int x = 0; x < imageInfo.m_width; x++) {
const int bufferY = y - startRow;
const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x];
const uint8_t twoBit = grayscaleTo2Bit(gray);
const int byteIndex = (x * 2) / 8;
const int bitOffset = 6 - ((x * 2) % 8); // 6, 4, 2, 0
rowBuffer[byteIndex] |= (twoBit << bitOffset);
}
// Write row with padding
bmpOut.write(rowBuffer, bytesPerRow);
}
}
// Clean up
free(mcuRowBuffer);
free(rowBuffer);
Serial.printf("[%lu] [JPG] Successfully converted JPEG to BMP\n", millis());
return true;
}

View File

@ -1,15 +0,0 @@
#pragma once
#include <FS.h>
class ZipFile;
class JpegToBmpConverter {
static void writeBmpHeader(Print& bmpOut, int width, int height);
static uint8_t grayscaleTo2Bit(uint8_t grayscale);
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
unsigned char* pBytes_actually_read, void* pCallback_data);
public:
static bool jpegFileToBmpStream(File& jpegFile, Print& bmpOut);
};

File diff suppressed because it is too large Load Diff

View File

@ -1,124 +0,0 @@
//------------------------------------------------------------------------------
// picojpeg - Public domain, Rich Geldreich <richgel99@gmail.com>
//------------------------------------------------------------------------------
#ifndef PICOJPEG_H
#define PICOJPEG_H
#ifdef __cplusplus
extern "C" {
#endif
// Error codes
enum {
PJPG_NO_MORE_BLOCKS = 1,
PJPG_BAD_DHT_COUNTS,
PJPG_BAD_DHT_INDEX,
PJPG_BAD_DHT_MARKER,
PJPG_BAD_DQT_MARKER,
PJPG_BAD_DQT_TABLE,
PJPG_BAD_PRECISION,
PJPG_BAD_HEIGHT,
PJPG_BAD_WIDTH,
PJPG_TOO_MANY_COMPONENTS,
PJPG_BAD_SOF_LENGTH,
PJPG_BAD_VARIABLE_MARKER,
PJPG_BAD_DRI_LENGTH,
PJPG_BAD_SOS_LENGTH,
PJPG_BAD_SOS_COMP_ID,
PJPG_W_EXTRA_BYTES_BEFORE_MARKER,
PJPG_NO_ARITHMITIC_SUPPORT,
PJPG_UNEXPECTED_MARKER,
PJPG_NOT_JPEG,
PJPG_UNSUPPORTED_MARKER,
PJPG_BAD_DQT_LENGTH,
PJPG_TOO_MANY_BLOCKS,
PJPG_UNDEFINED_QUANT_TABLE,
PJPG_UNDEFINED_HUFF_TABLE,
PJPG_NOT_SINGLE_SCAN,
PJPG_UNSUPPORTED_COLORSPACE,
PJPG_UNSUPPORTED_SAMP_FACTORS,
PJPG_DECODE_ERROR,
PJPG_BAD_RESTART_MARKER,
PJPG_ASSERTION_ERROR,
PJPG_BAD_SOS_SPECTRAL,
PJPG_BAD_SOS_SUCCESSIVE,
PJPG_STREAM_READ_ERROR,
PJPG_NOTENOUGHMEM,
PJPG_UNSUPPORTED_COMP_IDENT,
PJPG_UNSUPPORTED_QUANT_TABLE,
PJPG_UNSUPPORTED_MODE, // picojpeg doesn't support progressive JPEG's
};
// Scan types
typedef enum { PJPG_GRAYSCALE, PJPG_YH1V1, PJPG_YH2V1, PJPG_YH1V2, PJPG_YH2V2 } pjpeg_scan_type_t;
typedef struct {
// Image resolution
int m_width;
int m_height;
// Number of components (1 or 3)
int m_comps;
// Total number of minimum coded units (MCU's) per row/col.
int m_MCUSPerRow;
int m_MCUSPerCol;
// Scan type
pjpeg_scan_type_t m_scanType;
// MCU width/height in pixels (each is either 8 or 16 depending on the scan type)
int m_MCUWidth;
int m_MCUHeight;
// m_pMCUBufR, m_pMCUBufG, and m_pMCUBufB are pointers to internal MCU Y or RGB pixel component buffers.
// Each time pjpegDecodeMCU() is called successfully these buffers will be filled with 8x8 pixel blocks of Y or RGB
// pixels. Each MCU consists of (m_MCUWidth/8)*(m_MCUHeight/8) Y/RGB blocks: 1 for greyscale/no subsampling, 2 for
// H1V2/H2V1, or 4 blocks for H2V2 sampling factors. Each block is a contiguous array of 64 (8x8) bytes of a single
// component: either Y for grayscale images, or R, G or B components for color images.
//
// The 8x8 pixel blocks are organized in these byte arrays like this:
//
// PJPG_GRAYSCALE: Each MCU is decoded to a single block of 8x8 grayscale pixels.
// Only the values in m_pMCUBufR are valid. Each 8 bytes is a row of pixels (raster order: left to right, top to
// bottom) from the 8x8 block.
//
// PJPG_H1V1: Each MCU contains is decoded to a single block of 8x8 RGB pixels.
//
// PJPG_YH2V1: Each MCU is decoded to 2 blocks, or 16x8 pixels.
// The 2 RGB blocks are at byte offsets: 0, 64
//
// PJPG_YH1V2: Each MCU is decoded to 2 blocks, or 8x16 pixels.
// The 2 RGB blocks are at byte offsets: 0,
// 128
//
// PJPG_YH2V2: Each MCU is decoded to 4 blocks, or 16x16 pixels.
// The 2x2 block array is organized at byte offsets: 0, 64,
// 128, 192
//
// It is up to the caller to copy or blit these pixels from these buffers into the destination bitmap.
unsigned char* m_pMCUBufR;
unsigned char* m_pMCUBufG;
unsigned char* m_pMCUBufB;
} pjpeg_image_info_t;
typedef unsigned char (*pjpeg_need_bytes_callback_t)(unsigned char* pBuf, unsigned char buf_size,
unsigned char* pBytes_actually_read, void* pCallback_data);
// Initializes the decompressor. Returns 0 on success, or one of the above error codes on failure.
// pNeed_bytes_callback will be called to fill the decompressor's internal input buffer.
// If reduce is 1, only the first pixel of each block will be decoded. This mode is much faster because it skips the AC
// dequantization, IDCT and chroma upsampling of every image pixel. Not thread safe.
unsigned char pjpeg_decode_init(pjpeg_image_info_t* pInfo, pjpeg_need_bytes_callback_t pNeed_bytes_callback,
void* pCallback_data, unsigned char reduce);
// Decompresses the file's next MCU. Returns 0 on success, PJPG_NO_MORE_BLOCKS if no more blocks are available, or an
// error code. Must be called a total of m_MCUSPerRow*m_MCUSPerCol times to completely decompress the image. Not thread
// safe.
unsigned char pjpeg_decode_mcu(void);
#ifdef __cplusplus
}
#endif
#endif // PICOJPEG_H

View File

@ -1,5 +1,5 @@
[platformio] [platformio]
crosspoint_version = 0.8.0 crosspoint_version = 0.7.0
default_envs = default default_envs = default
[base] [base]

View File

@ -23,7 +23,7 @@ bool CrossPointSettings::saveToFile() const {
std::ofstream outputFile(SETTINGS_FILE); std::ofstream outputFile(SETTINGS_FILE);
serialization::writePod(outputFile, SETTINGS_FILE_VERSION); serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
serialization::writePod(outputFile, SETTINGS_COUNT); serialization::writePod(outputFile, SETTINGS_COUNT);
serialization::writePod(outputFile, sleepScreen); serialization::writePod(outputFile, whiteSleepScreen);
serialization::writePod(outputFile, extraParagraphSpacing); serialization::writePod(outputFile, extraParagraphSpacing);
serialization::writePod(outputFile, shortPwrBtn); serialization::writePod(outputFile, shortPwrBtn);
outputFile.close(); outputFile.close();
@ -54,7 +54,7 @@ bool CrossPointSettings::loadFromFile() {
// load settings that exist // load settings that exist
uint8_t settingsRead = 0; uint8_t settingsRead = 0;
do { do {
serialization::readPod(inputFile, sleepScreen); serialization::readPod(inputFile, whiteSleepScreen);
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, extraParagraphSpacing); serialization::readPod(inputFile, extraParagraphSpacing);
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;

View File

@ -15,11 +15,8 @@ class CrossPointSettings {
CrossPointSettings(const CrossPointSettings&) = delete; CrossPointSettings(const CrossPointSettings&) = delete;
CrossPointSettings& operator=(const CrossPointSettings&) = delete; CrossPointSettings& operator=(const CrossPointSettings&) = delete;
// Should match with SettingsActivity text
enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3 };
// Sleep screen settings // Sleep screen settings
uint8_t sleepScreen = DARK; uint8_t whiteSleepScreen = 0;
// Text rendering settings // Text rendering settings
uint8_t extraParagraphSpacing = 1; uint8_t extraParagraphSpacing = 1;
// Duration of the power button press // Duration of the power button press

View File

@ -1,22 +1,19 @@
#pragma once #pragma once
#include <InputManager.h> #include <InputManager.h>
#include <utility>
class GfxRenderer; class GfxRenderer;
class Activity { class Activity {
protected: protected:
std::string name;
GfxRenderer& renderer; GfxRenderer& renderer;
InputManager& inputManager; InputManager& inputManager;
public: public:
explicit Activity(std::string name, GfxRenderer& renderer, InputManager& inputManager) explicit Activity(GfxRenderer& renderer, InputManager& inputManager)
: name(std::move(name)), renderer(renderer), inputManager(inputManager) {} : renderer(renderer), inputManager(inputManager) {}
virtual ~Activity() = default; virtual ~Activity() = default;
virtual void onEnter() { Serial.printf("[%lu] [ACT] Entering activity: %s\n", millis(), name.c_str()); } virtual void onEnter() {}
virtual void onExit() { Serial.printf("[%lu] [ACT] Exiting activity: %s\n", millis(), name.c_str()); } virtual void onExit() {}
virtual void loop() {} virtual void loop() {}
virtual bool skipLoopDelay() { return false; } virtual bool skipLoopDelay() { return false; }
}; };

View File

@ -18,7 +18,4 @@ void ActivityWithSubactivity::loop() {
} }
} }
void ActivityWithSubactivity::onExit() { void ActivityWithSubactivity::onExit() { exitActivity(); }
Activity::onExit();
exitActivity();
}

View File

@ -10,8 +10,8 @@ class ActivityWithSubactivity : public Activity {
void enterNewActivity(Activity* activity); void enterNewActivity(Activity* activity);
public: public:
explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, InputManager& inputManager) explicit ActivityWithSubactivity(GfxRenderer& renderer, InputManager& inputManager)
: Activity(std::move(name), renderer, inputManager) {} : Activity(renderer, inputManager) {}
void loop() override; void loop() override;
void onExit() override; void onExit() override;
}; };

View File

@ -6,8 +6,6 @@
#include "images/CrossLarge.h" #include "images/CrossLarge.h"
void BootActivity::onEnter() { void BootActivity::onEnter() {
Activity::onEnter();
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();

View File

@ -3,6 +3,6 @@
class BootActivity final : public Activity { class BootActivity final : public Activity {
public: public:
explicit BootActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity("Boot", renderer, inputManager) {} explicit BootActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity(renderer, inputManager) {}
void onEnter() override; void onEnter() override;
}; };

View File

@ -1,46 +1,16 @@
#include "SleepActivity.h" #include "SleepActivity.h"
#include <Epub.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <SD.h> #include <SD.h>
#include <vector> #include <vector>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "config.h" #include "config.h"
#include "images/CrossLarge.h" #include "images/CrossLarge.h"
void SleepActivity::onEnter() { void SleepActivity::onEnter() {
Activity::onEnter();
renderPopup("Entering Sleep..."); renderPopup("Entering Sleep...");
if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM) {
return renderCustomSleepScreen();
}
if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::COVER) {
return renderCoverSleepScreen();
}
renderDefaultSleepScreen();
}
void SleepActivity::renderPopup(const char* message) const {
const int textWidth = renderer.getTextWidth(READER_FONT_ID, message);
constexpr int margin = 20;
const int x = (renderer.getScreenWidth() - textWidth - margin * 2) / 2;
constexpr int y = 117;
const int w = textWidth + margin * 2;
const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2;
// renderer.clearScreen();
renderer.fillRect(x + 5, y + 5, w - 10, h - 10, false);
renderer.drawText(READER_FONT_ID, x + margin, y + margin, message);
renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
renderer.displayBuffer();
}
void SleepActivity::renderCustomSleepScreen() const {
// Check if we have a /sleep directory // Check if we have a /sleep directory
auto dir = SD.open("/sleep"); auto dir = SD.open("/sleep");
if (dir && dir.isDirectory()) { if (dir && dir.isDirectory()) {
@ -58,31 +28,31 @@ void SleepActivity::renderCustomSleepScreen() const {
} }
if (filename.substr(filename.length() - 4) != ".bmp") { if (filename.substr(filename.length() - 4) != ".bmp") {
Serial.printf("[%lu] [SLP] Skipping non-.bmp file name: %s\n", millis(), file.name()); Serial.printf("[%lu] [Slp] Skipping non-.bmp file name: %s\n", millis(), file.name());
file.close(); file.close();
continue; continue;
} }
Bitmap bitmap(file); Bitmap bitmap(file);
if (bitmap.parseHeaders() != BmpReaderError::Ok) { if (bitmap.parseHeaders() != BmpReaderError::Ok) {
Serial.printf("[%lu] [SLP] Skipping invalid BMP file: %s\n", millis(), file.name()); Serial.printf("[%lu] [Slp] Skipping invalid BMP file: %s\n", millis(), file.name());
file.close(); file.close();
continue; continue;
} }
files.emplace_back(filename); files.emplace_back(filename);
file.close(); file.close();
} }
const auto numFiles = files.size(); int numFiles = files.size();
if (numFiles > 0) { if (numFiles > 0) {
// Generate a random number between 1 and numFiles // Generate a random number between 1 and numFiles
const auto randomFileIndex = random(numFiles); int randomFileIndex = random(numFiles);
const auto filename = "/sleep/" + files[randomFileIndex]; auto filename = "/sleep/" + files[randomFileIndex];
auto file = SD.open(filename.c_str()); auto file = SD.open(filename.c_str());
if (file) { if (file) {
Serial.printf("[%lu] [SLP] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str()); Serial.printf("[%lu] [Slp] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str());
delay(100); delay(100);
Bitmap bitmap(file); Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) { if (bitmap.parseHeaders() == BmpReaderError::Ok) {
renderBitmapSleepScreen(bitmap); renderCustomSleepScreen(bitmap);
dir.close(); dir.close();
return; return;
} }
@ -97,8 +67,8 @@ void SleepActivity::renderCustomSleepScreen() const {
if (file) { if (file) {
Bitmap bitmap(file); Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) { if (bitmap.parseHeaders() == BmpReaderError::Ok) {
Serial.printf("[%lu] [SLP] Loading: /sleep.bmp\n", millis()); Serial.printf("[%lu] [Slp] Loading: /sleep.bmp\n", millis());
renderBitmapSleepScreen(bitmap); renderCustomSleepScreen(bitmap);
return; return;
} }
} }
@ -106,27 +76,41 @@ void SleepActivity::renderCustomSleepScreen() const {
renderDefaultSleepScreen(); renderDefaultSleepScreen();
} }
void SleepActivity::renderPopup(const char* message) const {
const int textWidth = renderer.getTextWidth(READER_FONT_ID, message);
constexpr int margin = 20;
const int x = (GfxRenderer::getScreenWidth() - textWidth - margin * 2) / 2;
constexpr int y = 117;
const int w = textWidth + margin * 2;
const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2;
// renderer.clearScreen();
renderer.fillRect(x + 5, y + 5, w - 10, h - 10, false);
renderer.drawText(READER_FONT_ID, x + margin, y + margin, message);
renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
renderer.displayBuffer();
}
void SleepActivity::renderDefaultSleepScreen() const { void SleepActivity::renderDefaultSleepScreen() const {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();
renderer.clearScreen(); renderer.clearScreen();
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128); renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128);
renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD); renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING"); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING");
// Make sleep screen dark unless light is selected in settings // Apply white screen if enabled in settings
if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::LIGHT) { if (!SETTINGS.whiteSleepScreen) {
renderer.invertScreen(); renderer.invertScreen();
} }
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
} }
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { void SleepActivity::renderCustomSleepScreen(const Bitmap& bitmap) const {
int x, y; int x, y;
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = GfxRenderer::getScreenHeight();
if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) { if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) {
// image will scale, make sure placement is right // image will scale, make sure placement is right
@ -169,31 +153,3 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
renderer.setRenderMode(GfxRenderer::BW); renderer.setRenderMode(GfxRenderer::BW);
} }
} }
void SleepActivity::renderCoverSleepScreen() const {
if (APP_STATE.openEpubPath.empty()) {
return renderDefaultSleepScreen();
}
Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint");
if (!lastEpub.load()) {
Serial.println("[SLP] Failed to load last epub");
return renderDefaultSleepScreen();
}
if (!lastEpub.generateCoverBmp()) {
Serial.println("[SLP] Failed to generate cover bmp");
return renderDefaultSleepScreen();
}
auto file = SD.open(lastEpub.getCoverBmpPath().c_str(), FILE_READ);
if (file) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
renderBitmapSleepScreen(bitmap);
return;
}
}
renderDefaultSleepScreen();
}

View File

@ -5,14 +5,11 @@ class Bitmap;
class SleepActivity final : public Activity { class SleepActivity final : public Activity {
public: public:
explicit SleepActivity(GfxRenderer& renderer, InputManager& inputManager) explicit SleepActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity(renderer, inputManager) {}
: Activity("Sleep", renderer, inputManager) {}
void onEnter() override; void onEnter() override;
private: private:
void renderPopup(const char* message) const;
void renderDefaultSleepScreen() const; void renderDefaultSleepScreen() const;
void renderCustomSleepScreen() const; void renderCustomSleepScreen(const Bitmap& bitmap) const;
void renderCoverSleepScreen() const; void renderPopup(const char* message) const;
void renderBitmapSleepScreen(const Bitmap& bitmap) const;
}; };

View File

@ -15,8 +15,6 @@ void HomeActivity::taskTrampoline(void* param) {
} }
void HomeActivity::onEnter() { void HomeActivity::onEnter() {
Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
selectorIndex = 0; selectorIndex = 0;
@ -33,8 +31,6 @@ void HomeActivity::onEnter() {
} }
void HomeActivity::onExit() { void HomeActivity::onExit() {
Activity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {

View File

@ -23,7 +23,7 @@ class HomeActivity final : public Activity {
public: public:
explicit HomeActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onReaderOpen, explicit HomeActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onReaderOpen,
const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen) const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen)
: Activity("Home", renderer, inputManager), : Activity(renderer, inputManager),
onReaderOpen(onReaderOpen), onReaderOpen(onReaderOpen),
onSettingsOpen(onSettingsOpen), onSettingsOpen(onSettingsOpen),
onFileTransferOpen(onFileTransferOpen) {} onFileTransferOpen(onFileTransferOpen) {}

View File

@ -11,8 +11,7 @@ void CrossPointWebServerActivity::taskTrampoline(void* param) {
} }
void CrossPointWebServerActivity::onEnter() { void CrossPointWebServerActivity::onEnter() {
ActivityWithSubactivity::onEnter(); Serial.printf("[%lu] [WEBACT] ========== CrossPointWebServerActivity onEnter ==========\n", millis());
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onEnter: %d bytes\n", millis(), ESP.getFreeHeap()); Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onEnter: %d bytes\n", millis(), ESP.getFreeHeap());
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
@ -37,13 +36,13 @@ void CrossPointWebServerActivity::onEnter() {
// Launch WiFi selection subactivity // Launch WiFi selection subactivity
Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis()); Serial.printf("[%lu] [WEBACT] Launching WifiSelectionActivity...\n", millis());
enterNewActivity(new WifiSelectionActivity(renderer, inputManager, wifiSelection.reset(new WifiSelectionActivity(renderer, inputManager,
[this](const bool connected) { onWifiSelectionComplete(connected); })); [this](bool connected) { onWifiSelectionComplete(connected); }));
wifiSelection->onEnter();
} }
void CrossPointWebServerActivity::onExit() { void CrossPointWebServerActivity::onExit() {
ActivityWithSubactivity::onExit(); Serial.printf("[%lu] [WEBACT] ========== CrossPointWebServerActivity onExit START ==========\n", millis());
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit start: %d bytes\n", millis(), ESP.getFreeHeap()); Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit start: %d bytes\n", millis(), ESP.getFreeHeap());
state = WebServerActivityState::SHUTTING_DOWN; state = WebServerActivityState::SHUTTING_DOWN;
@ -51,6 +50,14 @@ void CrossPointWebServerActivity::onExit() {
// Stop the web server first (before disconnecting WiFi) // Stop the web server first (before disconnecting WiFi)
stopWebServer(); stopWebServer();
// Exit WiFi selection subactivity if still active
if (wifiSelection) {
Serial.printf("[%lu] [WEBACT] Exiting WifiSelectionActivity...\n", millis());
wifiSelection->onExit();
wifiSelection.reset();
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity exited\n", millis());
}
// CRITICAL: Wait for LWIP stack to flush any pending packets // CRITICAL: Wait for LWIP stack to flush any pending packets
Serial.printf("[%lu] [WEBACT] Waiting 500ms for network stack to flush pending packets...\n", millis()); Serial.printf("[%lu] [WEBACT] Waiting 500ms for network stack to flush pending packets...\n", millis());
delay(500); delay(500);
@ -85,17 +92,20 @@ void CrossPointWebServerActivity::onExit() {
Serial.printf("[%lu] [WEBACT] Mutex deleted\n", millis()); Serial.printf("[%lu] [WEBACT] Mutex deleted\n", millis());
Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap()); Serial.printf("[%lu] [WEBACT] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap());
Serial.printf("[%lu] [WEBACT] ========== CrossPointWebServerActivity onExit COMPLETE ==========\n", millis());
} }
void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) { void CrossPointWebServerActivity::onWifiSelectionComplete(bool connected) {
Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected); Serial.printf("[%lu] [WEBACT] WifiSelectionActivity completed, connected=%d\n", millis(), connected);
if (connected) { if (connected) {
// Get connection info before exiting subactivity // Get connection info before exiting subactivity
connectedIP = static_cast<WifiSelectionActivity*>(subActivity.get())->getConnectedIP(); connectedIP = wifiSelection->getConnectedIP();
connectedSSID = WiFi.SSID().c_str(); connectedSSID = WiFi.SSID().c_str();
exitActivity(); // Exit the wifi selection subactivity
wifiSelection->onExit();
wifiSelection.reset();
// Start the web server // Start the web server
startWebServer(); startWebServer();
@ -140,40 +150,47 @@ void CrossPointWebServerActivity::stopWebServer() {
} }
void CrossPointWebServerActivity::loop() { void CrossPointWebServerActivity::loop() {
if (subActivity) {
// Forward loop to subactivity
subActivity->loop();
return;
}
// Handle different states // Handle different states
if (state == WebServerActivityState::SERVER_RUNNING) { switch (state) {
// Handle web server requests - call handleClient multiple times per loop case WebServerActivityState::WIFI_SELECTION:
// to improve responsiveness and upload throughput // Forward loop to WiFi selection subactivity
if (webServer && webServer->isRunning()) { if (wifiSelection) {
const unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime; wifiSelection->loop();
}
break;
// Log if there's a significant gap between handleClient calls (>100ms) case WebServerActivityState::SERVER_RUNNING:
if (lastHandleClientTime > 0 && timeSinceLastHandleClient > 100) { // Handle web server requests - call handleClient multiple times per loop
Serial.printf("[%lu] [WEBACT] WARNING: %lu ms gap since last handleClient\n", millis(), // to improve responsiveness and upload throughput
timeSinceLastHandleClient); if (webServer && webServer->isRunning()) {
unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime;
// Log if there's a significant gap between handleClient calls (>100ms)
if (lastHandleClientTime > 0 && timeSinceLastHandleClient > 100) {
Serial.printf("[%lu] [WEBACT] WARNING: %lu ms gap since last handleClient\n", millis(),
timeSinceLastHandleClient);
}
// Call handleClient multiple times to process pending requests faster
// This is critical for upload performance - HTTP file uploads send data
// in chunks and each handleClient() call processes incoming data
constexpr int HANDLE_CLIENT_ITERATIONS = 10;
for (int i = 0; i < HANDLE_CLIENT_ITERATIONS && webServer->isRunning(); i++) {
webServer->handleClient();
}
lastHandleClientTime = millis();
} }
// Call handleClient multiple times to process pending requests faster // Handle exit on Back button
// This is critical for upload performance - HTTP file uploads send data if (inputManager.wasPressed(InputManager::BTN_BACK)) {
// in chunks and each handleClient() call processes incoming data onGoBack();
constexpr int HANDLE_CLIENT_ITERATIONS = 10; return;
for (int i = 0; i < HANDLE_CLIENT_ITERATIONS && webServer->isRunning(); i++) {
webServer->handleClient();
} }
lastHandleClientTime = millis(); break;
}
// Handle exit on Back button case WebServerActivityState::SHUTTING_DOWN:
if (inputManager.wasPressed(InputManager::BTN_BACK)) { // Do nothing - waiting for cleanup
onGoBack(); break;
return;
}
} }
} }

View File

@ -9,7 +9,6 @@
#include "../Activity.h" #include "../Activity.h"
#include "WifiSelectionActivity.h" #include "WifiSelectionActivity.h"
#include "activities/ActivityWithSubactivity.h"
#include "server/CrossPointWebServer.h" #include "server/CrossPointWebServer.h"
// Web server activity states // Web server activity states
@ -27,13 +26,16 @@ enum class WebServerActivityState {
* - Handles client requests in its loop() function * - Handles client requests in its loop() function
* - Cleans up the server and shuts down WiFi on exit * - Cleans up the server and shuts down WiFi on exit
*/ */
class CrossPointWebServerActivity final : public ActivityWithSubactivity { class CrossPointWebServerActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false; bool updateRequired = false;
WebServerActivityState state = WebServerActivityState::WIFI_SELECTION; WebServerActivityState state = WebServerActivityState::WIFI_SELECTION;
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
// WiFi selection subactivity
std::unique_ptr<WifiSelectionActivity> wifiSelection;
// Web server - owned by this activity // Web server - owned by this activity
std::unique_ptr<CrossPointWebServer> webServer; std::unique_ptr<CrossPointWebServer> webServer;
@ -56,7 +58,7 @@ class CrossPointWebServerActivity final : public ActivityWithSubactivity {
public: public:
explicit CrossPointWebServerActivity(GfxRenderer& renderer, InputManager& inputManager, explicit CrossPointWebServerActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void()>& onGoBack) const std::function<void()>& onGoBack)
: ActivityWithSubactivity("CrossPointWebServer", renderer, inputManager), onGoBack(onGoBack) {} : Activity(renderer, inputManager), onGoBack(onGoBack) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;

View File

@ -14,8 +14,6 @@ void WifiSelectionActivity::taskTrampoline(void* param) {
} }
void WifiSelectionActivity::onEnter() { void WifiSelectionActivity::onEnter() {
Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
// Load saved WiFi credentials // Load saved WiFi credentials
@ -49,8 +47,7 @@ void WifiSelectionActivity::onEnter() {
} }
void WifiSelectionActivity::onExit() { void WifiSelectionActivity::onExit() {
Activity::onExit(); Serial.printf("[%lu] [WIFI] ========== WifiSelectionActivity onExit START ==========\n", millis());
Serial.printf("[%lu] [WIFI] [MEM] Free heap at onExit start: %d bytes\n", millis(), ESP.getFreeHeap()); Serial.printf("[%lu] [WIFI] [MEM] Free heap at onExit start: %d bytes\n", millis(), ESP.getFreeHeap());
// Stop any ongoing WiFi scan // Stop any ongoing WiFi scan
@ -81,6 +78,7 @@ void WifiSelectionActivity::onExit() {
Serial.printf("[%lu] [WIFI] Mutex deleted\n", millis()); Serial.printf("[%lu] [WIFI] Mutex deleted\n", millis());
Serial.printf("[%lu] [WIFI] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap()); Serial.printf("[%lu] [WIFI] [MEM] Free heap at onExit end: %d bytes\n", millis(), ESP.getFreeHeap());
Serial.printf("[%lu] [WIFI] ========== WifiSelectionActivity onExit COMPLETE ==========\n", millis());
} }
void WifiSelectionActivity::startWifiScan() { void WifiSelectionActivity::startWifiScan() {

View File

@ -98,7 +98,7 @@ class WifiSelectionActivity final : public Activity {
public: public:
explicit WifiSelectionActivity(GfxRenderer& renderer, InputManager& inputManager, explicit WifiSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void(bool connected)>& onComplete) const std::function<void(bool connected)>& onComplete)
: Activity("WifiSelection", renderer, inputManager), onComplete(onComplete) {} : Activity(renderer, inputManager), onComplete(onComplete) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;

View File

@ -6,7 +6,6 @@
#include "Battery.h" #include "Battery.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "EpubReaderChapterSelectionActivity.h" #include "EpubReaderChapterSelectionActivity.h"
#include "config.h" #include "config.h"
@ -26,8 +25,6 @@ void EpubReaderActivity::taskTrampoline(void* param) {
} }
void EpubReaderActivity::onEnter() { void EpubReaderActivity::onEnter() {
ActivityWithSubactivity::onEnter();
if (!epub) { if (!epub) {
return; return;
} }
@ -47,10 +44,6 @@ void EpubReaderActivity::onEnter() {
f.close(); f.close();
} }
// Save current epub as last opened epub
APP_STATE.openEpubPath = epub->getPath();
APP_STATE.saveToFile();
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;
@ -63,8 +56,6 @@ void EpubReaderActivity::onEnter() {
} }
void EpubReaderActivity::onExit() { void EpubReaderActivity::onExit() {
ActivityWithSubactivity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -79,8 +70,8 @@ void EpubReaderActivity::onExit() {
void EpubReaderActivity::loop() { void EpubReaderActivity::loop() {
// Pass input responsibility to sub activity if exists // Pass input responsibility to sub activity if exists
if (subActivity) { if (subAcitivity) {
subActivity->loop(); subAcitivity->loop();
return; return;
} }
@ -88,11 +79,11 @@ void EpubReaderActivity::loop() {
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
// Don't start activity transition while rendering // Don't start activity transition while rendering
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); subAcitivity.reset(new EpubReaderChapterSelectionActivity(
enterNewActivity(new EpubReaderChapterSelectionActivity(
this->renderer, this->inputManager, epub, currentSpineIndex, this->renderer, this->inputManager, epub, currentSpineIndex,
[this] { [this] {
exitActivity(); subAcitivity->onExit();
subAcitivity.reset();
updateRequired = true; updateRequired = true;
}, },
[this](const int newSpineIndex) { [this](const int newSpineIndex) {
@ -101,9 +92,11 @@ void EpubReaderActivity::loop() {
nextPageNumber = 0; nextPageNumber = 0;
section.reset(); section.reset();
} }
exitActivity(); subAcitivity->onExit();
subAcitivity.reset();
updateRequired = true; updateRequired = true;
})); }));
subAcitivity->onEnter();
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} }
@ -332,8 +325,8 @@ void EpubReaderActivity::renderStatusBar() const {
constexpr auto textY = 776; constexpr auto textY = 776;
// Calculate progress in book // Calculate progress in book
const float sectionChapterProg = static_cast<float>(section->currentPage) / section->pageCount; float sectionChapterProg = static_cast<float>(section->currentPage) / section->pageCount;
const uint8_t bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg); uint8_t bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg);
// Right aligned text for progress counter // Right aligned text for progress counter
const std::string progress = std::to_string(section->currentPage + 1) + "/" + std::to_string(section->pageCount) + const std::string progress = std::to_string(section->currentPage + 1) + "/" + std::to_string(section->pageCount) +

View File

@ -5,13 +5,14 @@
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <freertos/task.h> #include <freertos/task.h>
#include "activities/ActivityWithSubactivity.h" #include "../Activity.h"
class EpubReaderActivity final : public ActivityWithSubactivity { class EpubReaderActivity final : public Activity {
std::shared_ptr<Epub> epub; std::shared_ptr<Epub> epub;
std::unique_ptr<Section> section = nullptr; std::unique_ptr<Section> section = nullptr;
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
std::unique_ptr<Activity> subAcitivity = nullptr;
int currentSpineIndex = 0; int currentSpineIndex = 0;
int nextPageNumber = 0; int nextPageNumber = 0;
int pagesUntilFullRefresh = 0; int pagesUntilFullRefresh = 0;
@ -27,7 +28,7 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
public: public:
explicit EpubReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub, explicit EpubReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub,
const std::function<void()>& onGoBack) const std::function<void()>& onGoBack)
: ActivityWithSubactivity("EpubReader", renderer, inputManager), epub(std::move(epub)), onGoBack(onGoBack) {} : Activity(renderer, inputManager), epub(std::move(epub)), onGoBack(onGoBack) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;

View File

@ -5,10 +5,8 @@
#include "config.h" #include "config.h"
namespace {
constexpr int PAGE_ITEMS = 24; constexpr int PAGE_ITEMS = 24;
constexpr int SKIP_PAGE_MS = 700; constexpr int SKIP_PAGE_MS = 700;
} // namespace
void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) { void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderChapterSelectionActivity*>(param); auto* self = static_cast<EpubReaderChapterSelectionActivity*>(param);
@ -16,8 +14,6 @@ void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) {
} }
void EpubReaderChapterSelectionActivity::onEnter() { void EpubReaderChapterSelectionActivity::onEnter() {
Activity::onEnter();
if (!epub) { if (!epub) {
return; return;
} }
@ -36,8 +32,6 @@ void EpubReaderChapterSelectionActivity::onEnter() {
} }
void EpubReaderChapterSelectionActivity::onExit() { void EpubReaderChapterSelectionActivity::onExit() {
Activity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {

View File

@ -27,7 +27,7 @@ class EpubReaderChapterSelectionActivity final : public Activity {
const std::shared_ptr<Epub>& epub, const int currentSpineIndex, const std::shared_ptr<Epub>& epub, const int currentSpineIndex,
const std::function<void()>& onGoBack, const std::function<void()>& onGoBack,
const std::function<void(int newSpineIndex)>& onSelectSpineIndex) const std::function<void(int newSpineIndex)>& onSelectSpineIndex)
: Activity("EpubReaderChapterSelection", renderer, inputManager), : Activity(renderer, inputManager),
epub(epub), epub(epub),
currentSpineIndex(currentSpineIndex), currentSpineIndex(currentSpineIndex),
onGoBack(onGoBack), onGoBack(onGoBack),

View File

@ -5,11 +5,6 @@
#include "config.h" #include "config.h"
namespace {
constexpr int PAGE_ITEMS = 23;
constexpr int SKIP_PAGE_MS = 700;
} // namespace
void sortFileList(std::vector<std::string>& strs) { void sortFileList(std::vector<std::string>& strs) {
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
if (str1.back() == '/' && str2.back() != '/') return true; if (str1.back() == '/' && str2.back() != '/') return true;
@ -48,8 +43,6 @@ void FileSelectionActivity::loadFiles() {
} }
void FileSelectionActivity::onEnter() { void FileSelectionActivity::onEnter() {
Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
basepath = "/"; basepath = "/";
@ -68,8 +61,6 @@ void FileSelectionActivity::onEnter() {
} }
void FileSelectionActivity::onExit() { void FileSelectionActivity::onExit() {
Activity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -82,12 +73,10 @@ void FileSelectionActivity::onExit() {
} }
void FileSelectionActivity::loop() { void FileSelectionActivity::loop() {
const bool prevReleased = const bool prevPressed =
inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT); inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
const bool nextReleased = const bool nextPressed =
inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT); inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT);
const bool skipPage = inputManager.getHeldTime() > SKIP_PAGE_MS;
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (files.empty()) { if (files.empty()) {
@ -112,19 +101,11 @@ void FileSelectionActivity::loop() {
// At root level, go back home // At root level, go back home
onGoHome(); onGoHome();
} }
} else if (prevReleased) { } else if (prevPressed) {
if (skipPage) { selectorIndex = (selectorIndex + files.size() - 1) % files.size();
selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + files.size()) % files.size();
} else {
selectorIndex = (selectorIndex + files.size() - 1) % files.size();
}
updateRequired = true; updateRequired = true;
} else if (nextReleased) { } else if (nextPressed) {
if (skipPage) { selectorIndex = (selectorIndex + 1) % files.size();
selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % files.size();
} else {
selectorIndex = (selectorIndex + 1) % files.size();
}
updateRequired = true; updateRequired = true;
} }
} }
@ -145,27 +126,21 @@ void FileSelectionActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = GfxRenderer::getScreenWidth(); const auto pageWidth = GfxRenderer::getScreenWidth();
renderer.drawCenteredText(READER_FONT_ID, 10, "Books", true, BOLD); renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD);
// Help text // Help text
renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, "Press BACK for Home"); renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, "Press BACK for Home");
if (files.empty()) { if (files.empty()) {
renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found"); renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found");
renderer.displayBuffer(); } else {
return; // Draw selection
} renderer.fillRect(0, 60 + selectorIndex * 30 + 2, pageWidth - 1, 30);
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; for (size_t i = 0; i < files.size(); i++) {
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 + 2, pageWidth - 1, 30); const auto file = files[i];
for (int i = pageStartIndex; i < files.size() && i < pageStartIndex + PAGE_ITEMS; i++) { renderer.drawText(UI_FONT_ID, 20, 60 + i * 30, file.c_str(), i != selectorIndex);
auto item = files[i];
int itemWidth = renderer.getTextWidth(UI_FONT_ID, item.c_str());
while (itemWidth > renderer.getScreenWidth() - 40 && item.length() > 8) {
item.replace(item.length() - 5, 5, "...");
itemWidth = renderer.getTextWidth(UI_FONT_ID, item.c_str());
} }
renderer.drawText(UI_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex);
} }
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -28,7 +28,7 @@ class FileSelectionActivity final : public Activity {
explicit FileSelectionActivity(GfxRenderer& renderer, InputManager& inputManager, explicit FileSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void(const std::string&)>& onSelect, const std::function<void(const std::string&)>& onSelect,
const std::function<void()>& onGoHome) const std::function<void()>& onGoHome)
: Activity("FileSelection", renderer, inputManager), onSelect(onSelect), onGoHome(onGoHome) {} : Activity(renderer, inputManager), onSelect(onSelect), onGoHome(onGoHome) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;

View File

@ -2,6 +2,7 @@
#include <SD.h> #include <SD.h>
#include "CrossPointState.h"
#include "Epub.h" #include "Epub.h"
#include "EpubReaderActivity.h" #include "EpubReaderActivity.h"
#include "FileSelectionActivity.h" #include "FileSelectionActivity.h"
@ -28,6 +29,8 @@ void ReaderActivity::onSelectEpubFile(const std::string& path) {
auto epub = loadEpub(path); auto epub = loadEpub(path);
if (epub) { if (epub) {
APP_STATE.openEpubPath = path;
APP_STATE.saveToFile();
onGoToEpubReader(std::move(epub)); onGoToEpubReader(std::move(epub));
} else { } else {
exitActivity(); exitActivity();
@ -50,8 +53,6 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
} }
void ReaderActivity::onEnter() { void ReaderActivity::onEnter() {
ActivityWithSubactivity::onEnter();
if (initialEpubPath.empty()) { if (initialEpubPath.empty()) {
onGoToFileSelection(); onGoToFileSelection();
return; return;

View File

@ -17,7 +17,7 @@ class ReaderActivity final : public ActivityWithSubactivity {
public: public:
explicit ReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::string initialEpubPath, explicit ReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::string initialEpubPath,
const std::function<void()>& onGoBack) const std::function<void()>& onGoBack)
: ActivityWithSubactivity("Reader", renderer, inputManager), : ActivityWithSubactivity(renderer, inputManager),
initialEpubPath(std::move(initialEpubPath)), initialEpubPath(std::move(initialEpubPath)),
onGoBack(onGoBack) {} onGoBack(onGoBack) {}
void onEnter() override; void onEnter() override;

View File

@ -6,14 +6,11 @@
#include "config.h" #include "config.h"
// Define the static settings list // Define the static settings list
namespace {
constexpr int settingsCount = 3; const SettingInfo SettingsActivity::settingsList[settingsCount] = {
const SettingInfo settingsList[settingsCount] = { {"White Sleep Screen", SettingType::TOGGLE, &CrossPointSettings::whiteSleepScreen},
// Should match with SLEEP_SCREEN_MODE {"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing},
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}}, {"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn}};
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, {}},
{"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn, {}}};
} // namespace
void SettingsActivity::taskTrampoline(void* param) { void SettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<SettingsActivity*>(param); auto* self = static_cast<SettingsActivity*>(param);
@ -21,8 +18,6 @@ void SettingsActivity::taskTrampoline(void* param) {
} }
void SettingsActivity::onEnter() { void SettingsActivity::onEnter() {
Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
// Reset selection to first item // Reset selection to first item
@ -40,8 +35,6 @@ void SettingsActivity::onEnter() {
} }
void SettingsActivity::onExit() { void SettingsActivity::onExit() {
Activity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -80,7 +73,7 @@ void SettingsActivity::loop() {
} }
} }
void SettingsActivity::toggleCurrentSetting() const { void SettingsActivity::toggleCurrentSetting() {
// Validate index // Validate index
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) { if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
return; return;
@ -88,18 +81,15 @@ void SettingsActivity::toggleCurrentSetting() const {
const auto& setting = settingsList[selectedSettingIndex]; const auto& setting = settingsList[selectedSettingIndex];
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { // Only toggle if it's a toggle type and has a value pointer
// Toggle the boolean value using the member pointer if (setting.type != SettingType::TOGGLE || setting.valuePtr == nullptr) {
const bool currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = !currentValue;
} 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());
} else {
// Only toggle if it's a toggle type and has a value pointer
return; return;
} }
// Toggle the boolean value using the member pointer
bool currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = !currentValue;
// Save settings when they change // Save settings when they change
SETTINGS.saveToFile(); SETTINGS.saveToFile();
} }
@ -139,13 +129,8 @@ void SettingsActivity::render() const {
// Draw value based on setting type // Draw value based on setting type
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) { if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
const bool value = SETTINGS.*(settingsList[i].valuePtr); bool value = SETTINGS.*(settingsList[i].valuePtr);
renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF"); renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF");
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
auto valueText = settingsList[i].enumValues[value];
const auto width = renderer.getTextWidth(UI_FONT_ID, valueText.c_str());
renderer.drawText(UI_FONT_ID, pageWidth - 50 - width, settingY, valueText.c_str());
} }
} }

View File

@ -12,14 +12,13 @@
class CrossPointSettings; class CrossPointSettings;
enum class SettingType { TOGGLE, ENUM }; enum class SettingType { TOGGLE };
// Structure to hold setting information // Structure to hold setting information
struct SettingInfo { struct SettingInfo {
const char* name; // Display name of the setting const char* name; // Display name of the setting
SettingType type; // Type of setting SettingType type; // Type of setting
uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE/ENUM) uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE)
std::vector<std::string> enumValues;
}; };
class SettingsActivity final : public Activity { class SettingsActivity final : public Activity {
@ -29,14 +28,18 @@ class SettingsActivity final : public Activity {
int selectedSettingIndex = 0; // Currently selected setting int selectedSettingIndex = 0; // Currently selected setting
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
// Static settings list
static constexpr int settingsCount = 3; // Number of settings
static const SettingInfo settingsList[settingsCount];
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
void render() const; void render() const;
void toggleCurrentSetting() const; void toggleCurrentSetting();
public: public:
explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome) explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
: Activity("Settings", renderer, inputManager), onGoHome(onGoHome) {} : Activity(renderer, inputManager), onGoHome(onGoHome) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;

View File

@ -5,8 +5,6 @@
#include "config.h" #include "config.h"
void FullScreenMessageActivity::onEnter() { void FullScreenMessageActivity::onEnter() {
Activity::onEnter();
const auto height = renderer.getLineHeight(UI_FONT_ID); const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (GfxRenderer::getScreenHeight() - height) / 2; const auto top = (GfxRenderer::getScreenHeight() - height) / 2;

View File

@ -16,9 +16,6 @@ class FullScreenMessageActivity final : public Activity {
explicit FullScreenMessageActivity(GfxRenderer& renderer, InputManager& inputManager, std::string text, explicit FullScreenMessageActivity(GfxRenderer& renderer, InputManager& inputManager, std::string text,
const EpdFontStyle style = REGULAR, const EpdFontStyle style = REGULAR,
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
: Activity("FullScreenMessage", renderer, inputManager), : Activity(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {}
text(std::move(text)),
style(style),
refreshMode(refreshMode) {}
void onEnter() override; void onEnter() override;
}; };

View File

@ -12,6 +12,11 @@ const char* const KeyboardEntryActivity::keyboard[NUM_ROWS] = {
const char* const KeyboardEntryActivity::keyboardShift[NUM_ROWS] = {"~!@#$%^&*()_+", "QWERTYUIOP{}|", "ASDFGHJKL:\"", const char* const KeyboardEntryActivity::keyboardShift[NUM_ROWS] = {"~!@#$%^&*()_+", "QWERTYUIOP{}|", "ASDFGHJKL:\"",
"ZXCVBNM<>?", "^ _____<OK"}; "ZXCVBNM<>?", "^ _____<OK"};
KeyboardEntryActivity::KeyboardEntryActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::string& title, const std::string& initialText, size_t maxLength,
bool isPassword)
: Activity(renderer, inputManager), title(title), text(initialText), maxLength(maxLength), isPassword(isPassword) {}
void KeyboardEntryActivity::setText(const std::string& newText) { void KeyboardEntryActivity::setText(const std::string& newText) {
text = newText; text = newText;
if (maxLength > 0 && text.length() > maxLength) { if (maxLength > 0 && text.length() > maxLength) {
@ -32,13 +37,15 @@ void KeyboardEntryActivity::reset(const std::string& newTitle, const std::string
} }
void KeyboardEntryActivity::onEnter() { void KeyboardEntryActivity::onEnter() {
Activity::onEnter();
// Reset state when entering the activity // Reset state when entering the activity
complete = false; complete = false;
cancelled = false; cancelled = false;
} }
void KeyboardEntryActivity::onExit() {
// Clean up if needed
}
void KeyboardEntryActivity::loop() { void KeyboardEntryActivity::loop() {
handleInput(); handleInput();
render(10); render(10);

View File

@ -34,12 +34,7 @@ class KeyboardEntryActivity : public Activity {
* @param isPassword If true, display asterisks instead of actual characters * @param isPassword If true, display asterisks instead of actual characters
*/ */
KeyboardEntryActivity(GfxRenderer& renderer, InputManager& inputManager, const std::string& title = "Enter Text", KeyboardEntryActivity(GfxRenderer& renderer, InputManager& inputManager, const std::string& title = "Enter Text",
const std::string& initialText = "", const size_t maxLength = 0, const bool isPassword = false) const std::string& initialText = "", size_t maxLength = 0, bool isPassword = false);
: Activity("KeyboardEntry", renderer, inputManager),
title(title),
text(initialText),
maxLength(maxLength),
isPassword(isPassword) {}
/** /**
* Handle button input. Call this in your main loop. * Handle button input. Call this in your main loop.
@ -90,6 +85,7 @@ class KeyboardEntryActivity : public Activity {
// Activity overrides // Activity overrides
void onEnter() override; void onEnter() override;
void onExit() override;
void loop() override; void loop() override;
private: private:

View File

@ -26,4 +26,4 @@
* "./lib/EpdFont/builtinFonts/pixelarial14.h", * "./lib/EpdFont/builtinFonts/pixelarial14.h",
* ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)' * ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
*/ */
#define SMALL_FONT_ID 1482513144 #define SMALL_FONT_ID (-139796914)

View File

@ -119,12 +119,14 @@ void enterDeepSleep() {
exitActivity(); exitActivity();
enterNewActivity(new SleepActivity(renderer, inputManager)); enterNewActivity(new SleepActivity(renderer, inputManager));
Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis());
delay(1000); // Allow Serial buffer to empty and display to update
// Enable Wakeup on LOW (button press)
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
einkDisplay.deepSleep(); einkDisplay.deepSleep();
Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis());
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
waitForPowerRelease();
// Enter Deep Sleep // Enter Deep Sleep
esp_deep_sleep_start(); esp_deep_sleep_start();
} }
@ -192,11 +194,7 @@ void setup() {
if (APP_STATE.openEpubPath.empty()) { if (APP_STATE.openEpubPath.empty()) {
onGoHome(); onGoHome();
} else { } else {
// Clear app state to avoid getting into a boot loop if the epub doesn't load onGoToReader(APP_STATE.openEpubPath);
const auto path = APP_STATE.openEpubPath;
APP_STATE.openEpubPath = "";
APP_STATE.saveToFile();
onGoToReader(path);
} }
// Ensure we're not still holding the power button before leaving setup // Ensure we're not still holding the power button before leaving setup
@ -229,7 +227,7 @@ void loop() {
return; return;
} }
if (inputManager.isPressed(InputManager::BTN_POWER) && if (inputManager.wasReleased(InputManager::BTN_POWER) &&
inputManager.getHeldTime() > SETTINGS.getPowerButtonDuration()) { inputManager.getHeldTime() > SETTINGS.getPowerButtonDuration()) {
enterDeepSleep(); enterDeepSleep();
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start