mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2025-12-18 15:17:42 +03:00
Compare commits
5 Commits
40ca305e7b
...
533234adc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
533234adc0 | ||
|
|
d429966dd4 | ||
|
|
c78f2a9840 | ||
|
|
11f01d3a41 | ||
|
|
03896d5215 |
@ -148,6 +148,18 @@ bool Epub::load() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// determine size of spine items
|
||||
size_t spineItemsCount = getSpineItemsCount();
|
||||
size_t spineItemsSize = 0;
|
||||
for (size_t i = 0; i < spineItemsCount; i++) {
|
||||
std::string spineItem = getSpineItem(i);
|
||||
size_t s = 0;
|
||||
getItemSize(spineItem, &s);
|
||||
spineItemsSize += s;
|
||||
cumulativeSpineItemSize.emplace_back(spineItemsSize);
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Book size: %u\n", millis(), spineItemsSize);
|
||||
|
||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||
|
||||
return true;
|
||||
@ -255,6 +267,8 @@ bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
|
||||
|
||||
int Epub::getSpineItemsCount() const { return spine.size(); }
|
||||
|
||||
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return cumulativeSpineItemSize.at(spineIndex); }
|
||||
|
||||
std::string& Epub::getSpineItem(const int spineIndex) {
|
||||
if (spineIndex < 0 || spineIndex >= spine.size()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
||||
@ -302,3 +316,14 @@ int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
|
||||
Serial.printf("[%lu] [EBP] TOC item not found\n", millis());
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t Epub::getBookSize() const { return getCumulativeSpineItemSize(getSpineItemsCount() - 1); }
|
||||
|
||||
// Calculate progress in book
|
||||
uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) {
|
||||
size_t prevChapterSize = getCumulativeSpineItemSize(currentSpineIndex - 1);
|
||||
size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
|
||||
size_t bookSize = getBookSize();
|
||||
size_t sectionProgSize = currentSpineRead * curChapterSize;
|
||||
return round(static_cast<float>(prevChapterSize + sectionProgSize) / bookSize * 100.0);
|
||||
}
|
||||
|
||||
@ -20,6 +20,8 @@ class Epub {
|
||||
std::string filepath;
|
||||
// the spine of the EPUB file
|
||||
std::vector<std::pair<std::string, std::string>> spine;
|
||||
// the file size of the spine items (proxy to book progress)
|
||||
std::vector<size_t> cumulativeSpineItemSize;
|
||||
// the toc of the EPUB file
|
||||
std::vector<EpubTocEntry> toc;
|
||||
// the base path for items in the EPUB file
|
||||
@ -51,8 +53,12 @@ class Epub {
|
||||
bool getItemSize(const std::string& itemHref, size_t* size) const;
|
||||
std::string& getSpineItem(int spineIndex);
|
||||
int getSpineItemsCount() const;
|
||||
EpubTocEntry& getTocItem(int tocTndex);
|
||||
size_t getCumulativeSpineItemSize(const int spineIndex) const;
|
||||
EpubTocEntry& getTocItem(int tocIndex);
|
||||
int getTocItemsCount() const;
|
||||
int getSpineIndexForTocIndex(int tocIndex) const;
|
||||
int getTocIndexForSpineIndex(int spineIndex) const;
|
||||
|
||||
size_t getBookSize() const;
|
||||
uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead);
|
||||
};
|
||||
|
||||
@ -3,7 +3,9 @@
|
||||
#include <HardwareSerial.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t PAGE_FILE_VERSION = 3;
|
||||
}
|
||||
|
||||
void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); }
|
||||
|
||||
|
||||
@ -9,7 +9,9 @@
|
||||
#include "Page.h"
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 5;
|
||||
}
|
||||
|
||||
void Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
|
||||
|
||||
244
lib/JpegToBmpConverter/JpegToBmpConverter.cpp
Normal file
244
lib/JpegToBmpConverter/JpegToBmpConverter.cpp
Normal file
@ -0,0 +1,244 @@
|
||||
#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;
|
||||
}
|
||||
15
lib/JpegToBmpConverter/JpegToBmpConverter.h
Normal file
15
lib/JpegToBmpConverter/JpegToBmpConverter.h
Normal file
@ -0,0 +1,15 @@
|
||||
#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);
|
||||
};
|
||||
2087
lib/picojpeg/picojpeg.c
Normal file
2087
lib/picojpeg/picojpeg.c
Normal file
File diff suppressed because it is too large
Load Diff
124
lib/picojpeg/picojpeg.h
Normal file
124
lib/picojpeg/picojpeg.h
Normal file
@ -0,0 +1,124 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 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
|
||||
@ -10,9 +10,11 @@
|
||||
// Initialize the static instance
|
||||
CrossPointSettings CrossPointSettings::instance;
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||
constexpr uint8_t SETTINGS_COUNT = 2;
|
||||
constexpr char SETTINGS_FILE[] = "/sd/.crosspoint/settings.bin";
|
||||
} // namespace
|
||||
|
||||
bool CrossPointSettings::saveToFile() const {
|
||||
// Make sure the directory exists
|
||||
|
||||
@ -6,8 +6,12 @@
|
||||
|
||||
#include <fstream>
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t STATE_FILE_VERSION = 1;
|
||||
constexpr char STATE_FILE[] = "/sd/.crosspoint/state.bin";
|
||||
} // namespace
|
||||
|
||||
CrossPointState CrossPointState::instance;
|
||||
|
||||
bool CrossPointState::saveToFile() const {
|
||||
std::ofstream outputFile(STATE_FILE);
|
||||
|
||||
@ -3,11 +3,20 @@
|
||||
#include <string>
|
||||
|
||||
class CrossPointState {
|
||||
// Static instance
|
||||
static CrossPointState instance;
|
||||
|
||||
public:
|
||||
std::string openEpubPath;
|
||||
~CrossPointState() = default;
|
||||
|
||||
// Get singleton instance
|
||||
static CrossPointState& getInstance() { return instance; }
|
||||
|
||||
bool saveToFile() const;
|
||||
|
||||
bool loadFromFile();
|
||||
};
|
||||
|
||||
// Helper macro to access settings
|
||||
#define APP_STATE CrossPointState::getInstance()
|
||||
|
||||
18
src/activities/Activity.h
Normal file
18
src/activities/Activity.h
Normal file
@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include <InputManager.h>
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
class Activity {
|
||||
protected:
|
||||
GfxRenderer& renderer;
|
||||
InputManager& inputManager;
|
||||
|
||||
public:
|
||||
explicit Activity(GfxRenderer& renderer, InputManager& inputManager)
|
||||
: renderer(renderer), inputManager(inputManager) {}
|
||||
virtual ~Activity() = default;
|
||||
virtual void onEnter() {}
|
||||
virtual void onExit() {}
|
||||
virtual void loop() {}
|
||||
};
|
||||
21
src/activities/ActivityWithSubactivity.cpp
Normal file
21
src/activities/ActivityWithSubactivity.cpp
Normal file
@ -0,0 +1,21 @@
|
||||
#include "ActivityWithSubactivity.h"
|
||||
|
||||
void ActivityWithSubactivity::exitActivity() {
|
||||
if (subActivity) {
|
||||
subActivity->onExit();
|
||||
subActivity.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWithSubactivity::enterNewActivity(Activity* activity) {
|
||||
subActivity.reset(activity);
|
||||
subActivity->onEnter();
|
||||
}
|
||||
|
||||
void ActivityWithSubactivity::loop() {
|
||||
if (subActivity) {
|
||||
subActivity->loop();
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWithSubactivity::onExit() { exitActivity(); }
|
||||
17
src/activities/ActivityWithSubactivity.h
Normal file
17
src/activities/ActivityWithSubactivity.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
|
||||
#include "Activity.h"
|
||||
|
||||
class ActivityWithSubactivity : public Activity {
|
||||
protected:
|
||||
std::unique_ptr<Activity> subActivity = nullptr;
|
||||
void exitActivity();
|
||||
void enterNewActivity(Activity* activity);
|
||||
|
||||
public:
|
||||
explicit ActivityWithSubactivity(GfxRenderer& renderer, InputManager& inputManager)
|
||||
: Activity(renderer, inputManager) {}
|
||||
void loop() override;
|
||||
void onExit() override;
|
||||
};
|
||||
@ -1,11 +1,11 @@
|
||||
#include "BootLogoScreen.h"
|
||||
#include "BootActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "images/CrossLarge.h"
|
||||
|
||||
void BootLogoScreen::onEnter() {
|
||||
void BootActivity::onEnter() {
|
||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||
|
||||
8
src/activities/boot_sleep/BootActivity.h
Normal file
8
src/activities/boot_sleep/BootActivity.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include "../Activity.h"
|
||||
|
||||
class BootActivity final : public Activity {
|
||||
public:
|
||||
explicit BootActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity(renderer, inputManager) {}
|
||||
void onEnter() override;
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
#include "SleepScreen.h"
|
||||
#include "SleepActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
#include "config.h"
|
||||
#include "images/CrossLarge.h"
|
||||
|
||||
void SleepScreen::onEnter() {
|
||||
void SleepActivity::onEnter() {
|
||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||
|
||||
8
src/activities/boot_sleep/SleepActivity.h
Normal file
8
src/activities/boot_sleep/SleepActivity.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include "../Activity.h"
|
||||
|
||||
class SleepActivity final : public Activity {
|
||||
public:
|
||||
explicit SleepActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity(renderer, inputManager) {}
|
||||
void onEnter() override;
|
||||
};
|
||||
103
src/activities/home/HomeActivity.cpp
Normal file
103
src/activities/home/HomeActivity.cpp
Normal file
@ -0,0 +1,103 @@
|
||||
#include "HomeActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
namespace {
|
||||
constexpr int menuItemCount = 2;
|
||||
}
|
||||
|
||||
void HomeActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<HomeActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void HomeActivity::onEnter() {
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
selectorIndex = 0;
|
||||
|
||||
// Trigger first update
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&HomeActivity::taskTrampoline, "HomeActivityTask",
|
||||
2048, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
}
|
||||
|
||||
void HomeActivity::onExit() {
|
||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
}
|
||||
|
||||
void HomeActivity::loop() {
|
||||
const bool prevPressed =
|
||||
inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
|
||||
const bool nextPressed =
|
||||
inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT);
|
||||
|
||||
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||
if (selectorIndex == 0) {
|
||||
onReaderOpen();
|
||||
} else if (selectorIndex == 1) {
|
||||
onSettingsOpen();
|
||||
}
|
||||
} else if (prevPressed) {
|
||||
selectorIndex = (selectorIndex + menuItemCount - 1) % menuItemCount;
|
||||
updateRequired = true;
|
||||
} else if (nextPressed) {
|
||||
selectorIndex = (selectorIndex + 1) % menuItemCount;
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
void HomeActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void HomeActivity::render() const {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||
renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD);
|
||||
|
||||
// Draw selection
|
||||
renderer.fillRect(0, 60 + selectorIndex * 30 + 2, pageWidth - 1, 30);
|
||||
renderer.drawText(UI_FONT_ID, 20, 60, "Read", selectorIndex != 0);
|
||||
renderer.drawText(UI_FONT_ID, 20, 90, "Settings", selectorIndex != 1);
|
||||
|
||||
renderer.drawRect(25, pageHeight - 40, 106, 40);
|
||||
renderer.drawText(UI_FONT_ID, 25 + (105 - renderer.getTextWidth(UI_FONT_ID, "Back")) / 2, pageHeight - 35, "Back");
|
||||
|
||||
renderer.drawRect(130, pageHeight - 40, 106, 40);
|
||||
renderer.drawText(UI_FONT_ID, 130 + (105 - renderer.getTextWidth(UI_FONT_ID, "Confirm")) / 2, pageHeight - 35,
|
||||
"Confirm");
|
||||
|
||||
renderer.drawRect(245, pageHeight - 40, 106, 40);
|
||||
renderer.drawText(UI_FONT_ID, 245 + (105 - renderer.getTextWidth(UI_FONT_ID, "Left")) / 2, pageHeight - 35, "Left");
|
||||
|
||||
renderer.drawRect(350, pageHeight - 40, 106, 40);
|
||||
renderer.drawText(UI_FONT_ID, 350 + (105 - renderer.getTextWidth(UI_FONT_ID, "Right")) / 2, pageHeight - 35, "Right");
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
29
src/activities/home/HomeActivity.h
Normal file
29
src/activities/home/HomeActivity.h
Normal file
@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "../Activity.h"
|
||||
|
||||
class HomeActivity final : public Activity {
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
int selectorIndex = 0;
|
||||
bool updateRequired = false;
|
||||
const std::function<void()> onReaderOpen;
|
||||
const std::function<void()> onSettingsOpen;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render() const;
|
||||
|
||||
public:
|
||||
explicit HomeActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onReaderOpen,
|
||||
const std::function<void()>& onSettingsOpen)
|
||||
: Activity(renderer, inputManager), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
#include "EpubReaderScreen.h"
|
||||
#include "EpubReaderActivity.h"
|
||||
|
||||
#include <Epub/Page.h>
|
||||
#include <GfxRenderer.h>
|
||||
@ -6,23 +6,25 @@
|
||||
|
||||
#include "Battery.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "EpubReaderChapterSelectionScreen.h"
|
||||
#include "EpubReaderChapterSelectionActivity.h"
|
||||
#include "config.h"
|
||||
|
||||
constexpr int PAGES_PER_REFRESH = 15;
|
||||
constexpr unsigned long SKIP_CHAPTER_MS = 700;
|
||||
namespace {
|
||||
constexpr int pagesPerRefresh = 15;
|
||||
constexpr unsigned long skipChapterMs = 700;
|
||||
constexpr float lineCompression = 0.95f;
|
||||
constexpr int marginTop = 8;
|
||||
constexpr int marginRight = 10;
|
||||
constexpr int marginBottom = 22;
|
||||
constexpr int marginLeft = 10;
|
||||
} // namespace
|
||||
|
||||
void EpubReaderScreen::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<EpubReaderScreen*>(param);
|
||||
void EpubReaderActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<EpubReaderActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void EpubReaderScreen::onEnter() {
|
||||
void EpubReaderActivity::onEnter() {
|
||||
if (!epub) {
|
||||
return;
|
||||
}
|
||||
@ -44,7 +46,7 @@ void EpubReaderScreen::onEnter() {
|
||||
// Trigger first update
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&EpubReaderScreen::taskTrampoline, "EpubReaderScreenTask",
|
||||
xTaskCreate(&EpubReaderActivity::taskTrampoline, "EpubReaderActivityTask",
|
||||
8192, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
@ -52,7 +54,7 @@ void EpubReaderScreen::onEnter() {
|
||||
);
|
||||
}
|
||||
|
||||
void EpubReaderScreen::onExit() {
|
||||
void EpubReaderActivity::onExit() {
|
||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
@ -65,22 +67,22 @@ void EpubReaderScreen::onExit() {
|
||||
epub.reset();
|
||||
}
|
||||
|
||||
void EpubReaderScreen::handleInput() {
|
||||
// Pass input responsibility to sub screen if exists
|
||||
if (subScreen) {
|
||||
subScreen->handleInput();
|
||||
void EpubReaderActivity::loop() {
|
||||
// Pass input responsibility to sub activity if exists
|
||||
if (subAcitivity) {
|
||||
subAcitivity->loop();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter chapter selection screen
|
||||
// Enter chapter selection activity
|
||||
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||
// Don't start screen transition while rendering
|
||||
// Don't start activity transition while rendering
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
subScreen.reset(new EpubReaderChapterSelectionScreen(
|
||||
subAcitivity.reset(new EpubReaderChapterSelectionActivity(
|
||||
this->renderer, this->inputManager, epub, currentSpineIndex,
|
||||
[this] {
|
||||
subScreen->onExit();
|
||||
subScreen.reset();
|
||||
subAcitivity->onExit();
|
||||
subAcitivity.reset();
|
||||
updateRequired = true;
|
||||
},
|
||||
[this](const int newSpineIndex) {
|
||||
@ -89,16 +91,16 @@ void EpubReaderScreen::handleInput() {
|
||||
nextPageNumber = 0;
|
||||
section.reset();
|
||||
}
|
||||
subScreen->onExit();
|
||||
subScreen.reset();
|
||||
subAcitivity->onExit();
|
||||
subAcitivity.reset();
|
||||
updateRequired = true;
|
||||
}));
|
||||
subScreen->onEnter();
|
||||
subAcitivity->onEnter();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
|
||||
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||
onGoHome();
|
||||
onGoBack();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -119,7 +121,7 @@ void EpubReaderScreen::handleInput() {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool skipChapter = inputManager.getHeldTime() > SKIP_CHAPTER_MS;
|
||||
const bool skipChapter = inputManager.getHeldTime() > skipChapterMs;
|
||||
|
||||
if (skipChapter) {
|
||||
// We don't want to delete the section mid-render, so grab the semaphore
|
||||
@ -165,7 +167,7 @@ void EpubReaderScreen::handleInput() {
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderScreen::displayTaskLoop() {
|
||||
void EpubReaderActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
@ -178,7 +180,7 @@ void EpubReaderScreen::displayTaskLoop() {
|
||||
}
|
||||
|
||||
// TODO: Failure handling
|
||||
void EpubReaderScreen::renderScreen() {
|
||||
void EpubReaderActivity::renderScreen() {
|
||||
if (!epub) {
|
||||
return;
|
||||
}
|
||||
@ -285,12 +287,12 @@ void EpubReaderScreen::renderScreen() {
|
||||
f.close();
|
||||
}
|
||||
|
||||
void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) {
|
||||
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page) {
|
||||
page->render(renderer, READER_FONT_ID);
|
||||
renderStatusBar();
|
||||
if (pagesUntilFullRefresh <= 1) {
|
||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||
pagesUntilFullRefresh = PAGES_PER_REFRESH;
|
||||
pagesUntilFullRefresh = pagesPerRefresh;
|
||||
} else {
|
||||
renderer.displayBuffer();
|
||||
pagesUntilFullRefresh--;
|
||||
@ -322,10 +324,16 @@ void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) {
|
||||
renderer.restoreBwBuffer();
|
||||
}
|
||||
|
||||
void EpubReaderScreen::renderStatusBar() const {
|
||||
void EpubReaderActivity::renderStatusBar() const {
|
||||
constexpr auto textY = 776;
|
||||
|
||||
// Calculate progress in book
|
||||
float sectionChapterProg = static_cast<float>(section->currentPage) / section->pageCount;
|
||||
uint8_t bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg);
|
||||
|
||||
// Right aligned text for progress counter
|
||||
const std::string progress = std::to_string(section->currentPage + 1) + " / " + std::to_string(section->pageCount);
|
||||
const std::string progress = std::to_string(section->currentPage + 1) + "/" + std::to_string(section->pageCount) +
|
||||
" " + std::to_string(bookProgress) + "%";
|
||||
const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str());
|
||||
renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY,
|
||||
progress.c_str());
|
||||
@ -5,19 +5,19 @@
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include "Screen.h"
|
||||
#include "../Activity.h"
|
||||
|
||||
class EpubReaderScreen final : public Screen {
|
||||
class EpubReaderActivity final : public Activity {
|
||||
std::shared_ptr<Epub> epub;
|
||||
std::unique_ptr<Section> section = nullptr;
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
std::unique_ptr<Screen> subScreen = nullptr;
|
||||
std::unique_ptr<Activity> subAcitivity = nullptr;
|
||||
int currentSpineIndex = 0;
|
||||
int nextPageNumber = 0;
|
||||
int pagesUntilFullRefresh = 0;
|
||||
bool updateRequired = false;
|
||||
const std::function<void()> onGoHome;
|
||||
const std::function<void()> onGoBack;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
@ -26,10 +26,10 @@ class EpubReaderScreen final : public Screen {
|
||||
void renderStatusBar() const;
|
||||
|
||||
public:
|
||||
explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub,
|
||||
const std::function<void()>& onGoHome)
|
||||
: Screen(renderer, inputManager), epub(std::move(epub)), onGoHome(onGoHome) {}
|
||||
explicit EpubReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub,
|
||||
const std::function<void()>& onGoBack)
|
||||
: Activity(renderer, inputManager), epub(std::move(epub)), onGoBack(onGoBack) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void handleInput() override;
|
||||
void loop() override;
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
#include "EpubReaderChapterSelectionScreen.h"
|
||||
#include "EpubReaderChapterSelectionActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <SD.h>
|
||||
@ -8,12 +8,12 @@
|
||||
constexpr int PAGE_ITEMS = 24;
|
||||
constexpr int SKIP_PAGE_MS = 700;
|
||||
|
||||
void EpubReaderChapterSelectionScreen::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<EpubReaderChapterSelectionScreen*>(param);
|
||||
void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<EpubReaderChapterSelectionActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void EpubReaderChapterSelectionScreen::onEnter() {
|
||||
void EpubReaderChapterSelectionActivity::onEnter() {
|
||||
if (!epub) {
|
||||
return;
|
||||
}
|
||||
@ -23,7 +23,7 @@ void EpubReaderChapterSelectionScreen::onEnter() {
|
||||
|
||||
// Trigger first update
|
||||
updateRequired = true;
|
||||
xTaskCreate(&EpubReaderChapterSelectionScreen::taskTrampoline, "EpubReaderChapterSelectionScreenTask",
|
||||
xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask",
|
||||
2048, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
@ -31,7 +31,7 @@ void EpubReaderChapterSelectionScreen::onEnter() {
|
||||
);
|
||||
}
|
||||
|
||||
void EpubReaderChapterSelectionScreen::onExit() {
|
||||
void EpubReaderChapterSelectionActivity::onExit() {
|
||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
@ -42,7 +42,7 @@ void EpubReaderChapterSelectionScreen::onExit() {
|
||||
renderingMutex = nullptr;
|
||||
}
|
||||
|
||||
void EpubReaderChapterSelectionScreen::handleInput() {
|
||||
void EpubReaderChapterSelectionActivity::loop() {
|
||||
const bool prevReleased =
|
||||
inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
|
||||
const bool nextReleased =
|
||||
@ -72,7 +72,7 @@ void EpubReaderChapterSelectionScreen::handleInput() {
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderChapterSelectionScreen::displayTaskLoop() {
|
||||
void EpubReaderChapterSelectionActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
@ -84,7 +84,7 @@ void EpubReaderChapterSelectionScreen::displayTaskLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderChapterSelectionScreen::renderScreen() {
|
||||
void EpubReaderChapterSelectionActivity::renderScreen() {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
@ -6,9 +6,9 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Screen.h"
|
||||
#include "../Activity.h"
|
||||
|
||||
class EpubReaderChapterSelectionScreen final : public Screen {
|
||||
class EpubReaderChapterSelectionActivity final : public Activity {
|
||||
std::shared_ptr<Epub> epub;
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
@ -23,16 +23,16 @@ class EpubReaderChapterSelectionScreen final : public Screen {
|
||||
void renderScreen();
|
||||
|
||||
public:
|
||||
explicit EpubReaderChapterSelectionScreen(GfxRenderer& renderer, InputManager& inputManager,
|
||||
const std::shared_ptr<Epub>& epub, const int currentSpineIndex,
|
||||
const std::function<void()>& onGoBack,
|
||||
const std::function<void(int newSpineIndex)>& onSelectSpineIndex)
|
||||
: Screen(renderer, inputManager),
|
||||
explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
|
||||
const std::shared_ptr<Epub>& epub, const int currentSpineIndex,
|
||||
const std::function<void()>& onGoBack,
|
||||
const std::function<void(int newSpineIndex)>& onSelectSpineIndex)
|
||||
: Activity(renderer, inputManager),
|
||||
epub(epub),
|
||||
currentSpineIndex(currentSpineIndex),
|
||||
onGoBack(onGoBack),
|
||||
onSelectSpineIndex(onSelectSpineIndex) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void handleInput() override;
|
||||
void loop() override;
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
#include "FileSelectionScreen.h"
|
||||
#include "FileSelectionActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <SD.h>
|
||||
@ -15,12 +15,12 @@ void sortFileList(std::vector<std::string>& strs) {
|
||||
});
|
||||
}
|
||||
|
||||
void FileSelectionScreen::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<FileSelectionScreen*>(param);
|
||||
void FileSelectionActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<FileSelectionActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void FileSelectionScreen::loadFiles() {
|
||||
void FileSelectionActivity::loadFiles() {
|
||||
files.clear();
|
||||
selectorIndex = 0;
|
||||
auto root = SD.open(basepath.c_str());
|
||||
@ -42,7 +42,7 @@ void FileSelectionScreen::loadFiles() {
|
||||
sortFileList(files);
|
||||
}
|
||||
|
||||
void FileSelectionScreen::onEnter() {
|
||||
void FileSelectionActivity::onEnter() {
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
basepath = "/";
|
||||
@ -52,7 +52,7 @@ void FileSelectionScreen::onEnter() {
|
||||
// Trigger first update
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask",
|
||||
xTaskCreate(&FileSelectionActivity::taskTrampoline, "FileSelectionActivityTask",
|
||||
2048, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
@ -60,7 +60,7 @@ void FileSelectionScreen::onEnter() {
|
||||
);
|
||||
}
|
||||
|
||||
void FileSelectionScreen::onExit() {
|
||||
void FileSelectionActivity::onExit() {
|
||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
@ -72,7 +72,7 @@ void FileSelectionScreen::onExit() {
|
||||
files.clear();
|
||||
}
|
||||
|
||||
void FileSelectionScreen::handleInput() {
|
||||
void FileSelectionActivity::loop() {
|
||||
const bool prevPressed =
|
||||
inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
|
||||
const bool nextPressed =
|
||||
@ -98,8 +98,8 @@ void FileSelectionScreen::handleInput() {
|
||||
loadFiles();
|
||||
updateRequired = true;
|
||||
} else {
|
||||
// At root level, go to settings
|
||||
onSettingsOpen();
|
||||
// At root level, go back home
|
||||
onGoHome();
|
||||
}
|
||||
} else if (prevPressed) {
|
||||
selectorIndex = (selectorIndex + files.size() - 1) % files.size();
|
||||
@ -110,7 +110,7 @@ void FileSelectionScreen::handleInput() {
|
||||
}
|
||||
}
|
||||
|
||||
void FileSelectionScreen::displayTaskLoop() {
|
||||
void FileSelectionActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
@ -122,14 +122,14 @@ void FileSelectionScreen::displayTaskLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
void FileSelectionScreen::render() const {
|
||||
void FileSelectionActivity::render() const {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||
renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD);
|
||||
|
||||
// Help text
|
||||
renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, "Press BACK for Settings");
|
||||
renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, "Press BACK for Home");
|
||||
|
||||
if (files.empty()) {
|
||||
renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found");
|
||||
@ -7,9 +7,9 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Screen.h"
|
||||
#include "../Activity.h"
|
||||
|
||||
class FileSelectionScreen final : public Screen {
|
||||
class FileSelectionActivity final : public Activity {
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
std::string basepath = "/";
|
||||
@ -17,7 +17,7 @@ class FileSelectionScreen final : public Screen {
|
||||
int selectorIndex = 0;
|
||||
bool updateRequired = false;
|
||||
const std::function<void(const std::string&)> onSelect;
|
||||
const std::function<void()> onSettingsOpen;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
@ -25,11 +25,11 @@ class FileSelectionScreen final : public Screen {
|
||||
void loadFiles();
|
||||
|
||||
public:
|
||||
explicit FileSelectionScreen(GfxRenderer& renderer, InputManager& inputManager,
|
||||
const std::function<void(const std::string&)>& onSelect,
|
||||
const std::function<void()>& onSettingsOpen)
|
||||
: Screen(renderer, inputManager), onSelect(onSelect), onSettingsOpen(onSettingsOpen) {}
|
||||
explicit FileSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
|
||||
const std::function<void(const std::string&)>& onSelect,
|
||||
const std::function<void()>& onGoHome)
|
||||
: Activity(renderer, inputManager), onSelect(onSelect), onGoHome(onGoHome) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void handleInput() override;
|
||||
void loop() override;
|
||||
};
|
||||
68
src/activities/reader/ReaderActivity.cpp
Normal file
68
src/activities/reader/ReaderActivity.cpp
Normal file
@ -0,0 +1,68 @@
|
||||
#include "ReaderActivity.h"
|
||||
|
||||
#include <SD.h>
|
||||
|
||||
#include "CrossPointState.h"
|
||||
#include "Epub.h"
|
||||
#include "EpubReaderActivity.h"
|
||||
#include "FileSelectionActivity.h"
|
||||
#include "activities/util/FullScreenMessageActivity.h"
|
||||
|
||||
std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
|
||||
if (!SD.exists(path.c_str())) {
|
||||
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
|
||||
if (epub->load()) {
|
||||
return epub;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ReaderActivity::onSelectEpubFile(const std::string& path) {
|
||||
exitActivity();
|
||||
enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Loading..."));
|
||||
|
||||
auto epub = loadEpub(path);
|
||||
if (epub) {
|
||||
APP_STATE.openEpubPath = path;
|
||||
APP_STATE.saveToFile();
|
||||
onGoToEpubReader(std::move(epub));
|
||||
} else {
|
||||
exitActivity();
|
||||
enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Failed to load epub", REGULAR,
|
||||
EInkDisplay::HALF_REFRESH));
|
||||
delay(2000);
|
||||
onGoToFileSelection();
|
||||
}
|
||||
}
|
||||
|
||||
void ReaderActivity::onGoToFileSelection() {
|
||||
exitActivity();
|
||||
enterNewActivity(new FileSelectionActivity(
|
||||
renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack));
|
||||
}
|
||||
|
||||
void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
|
||||
exitActivity();
|
||||
enterNewActivity(new EpubReaderActivity(renderer, inputManager, std::move(epub), [this] { onGoToFileSelection(); }));
|
||||
}
|
||||
|
||||
void ReaderActivity::onEnter() {
|
||||
if (initialEpubPath.empty()) {
|
||||
onGoToFileSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
auto epub = loadEpub(initialEpubPath);
|
||||
if (!epub) {
|
||||
onGoBack();
|
||||
return;
|
||||
}
|
||||
|
||||
onGoToEpubReader(std::move(epub));
|
||||
}
|
||||
24
src/activities/reader/ReaderActivity.h
Normal file
24
src/activities/reader/ReaderActivity.h
Normal file
@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
|
||||
#include "../ActivityWithSubactivity.h"
|
||||
|
||||
class Epub;
|
||||
|
||||
class ReaderActivity final : public ActivityWithSubactivity {
|
||||
std::string initialEpubPath;
|
||||
const std::function<void()> onGoBack;
|
||||
static std::unique_ptr<Epub> loadEpub(const std::string& path);
|
||||
|
||||
void onSelectEpubFile(const std::string& path);
|
||||
void onGoToFileSelection();
|
||||
void onGoToEpubReader(std::unique_ptr<Epub> epub);
|
||||
|
||||
public:
|
||||
explicit ReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::string initialEpubPath,
|
||||
const std::function<void()>& onGoBack)
|
||||
: ActivityWithSubactivity(renderer, inputManager),
|
||||
initialEpubPath(std::move(initialEpubPath)),
|
||||
onGoBack(onGoBack) {}
|
||||
void onEnter() override;
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
#include "SettingsScreen.h"
|
||||
#include "SettingsActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
@ -7,16 +7,16 @@
|
||||
|
||||
// Define the static settings list
|
||||
|
||||
const SettingInfo SettingsScreen::settingsList[SettingsScreen::settingsCount] = {
|
||||
const SettingInfo SettingsActivity::settingsList[settingsCount] = {
|
||||
{"White Sleep Screen", &CrossPointSettings::whiteSleepScreen},
|
||||
{"Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing}};
|
||||
|
||||
void SettingsScreen::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<SettingsScreen*>(param);
|
||||
void SettingsActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<SettingsActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void SettingsScreen::onEnter() {
|
||||
void SettingsActivity::onEnter() {
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
// Reset selection to first item
|
||||
@ -25,7 +25,7 @@ void SettingsScreen::onEnter() {
|
||||
// Trigger first update
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&SettingsScreen::taskTrampoline, "SettingsScreenTask",
|
||||
xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask",
|
||||
2048, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
@ -33,7 +33,7 @@ void SettingsScreen::onEnter() {
|
||||
);
|
||||
}
|
||||
|
||||
void SettingsScreen::onExit() {
|
||||
void SettingsActivity::onExit() {
|
||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
@ -44,7 +44,7 @@ void SettingsScreen::onExit() {
|
||||
renderingMutex = nullptr;
|
||||
}
|
||||
|
||||
void SettingsScreen::handleInput() {
|
||||
void SettingsActivity::loop() {
|
||||
// Handle actions with early return
|
||||
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||
toggleCurrentSetting();
|
||||
@ -70,7 +70,7 @@ void SettingsScreen::handleInput() {
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsScreen::toggleCurrentSetting() {
|
||||
void SettingsActivity::toggleCurrentSetting() {
|
||||
// Validate index
|
||||
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
|
||||
return;
|
||||
@ -84,7 +84,7 @@ void SettingsScreen::toggleCurrentSetting() {
|
||||
SETTINGS.saveToFile();
|
||||
}
|
||||
|
||||
void SettingsScreen::displayTaskLoop() {
|
||||
void SettingsActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
@ -96,7 +96,7 @@ void SettingsScreen::displayTaskLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsScreen::render() const {
|
||||
void SettingsActivity::render() const {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||
@ -7,7 +7,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Screen.h"
|
||||
#include "../Activity.h"
|
||||
|
||||
class CrossPointSettings;
|
||||
|
||||
@ -17,7 +17,7 @@ struct SettingInfo {
|
||||
uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings
|
||||
};
|
||||
|
||||
class SettingsScreen final : public Screen {
|
||||
class SettingsActivity final : public Activity {
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
bool updateRequired = false;
|
||||
@ -34,9 +34,9 @@ class SettingsScreen final : public Screen {
|
||||
void toggleCurrentSetting();
|
||||
|
||||
public:
|
||||
explicit SettingsScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
|
||||
: Screen(renderer, inputManager), onGoHome(onGoHome) {}
|
||||
explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
|
||||
: Activity(renderer, inputManager), onGoHome(onGoHome) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void handleInput() override;
|
||||
void loop() override;
|
||||
};
|
||||
@ -1,10 +1,10 @@
|
||||
#include "FullScreenMessageScreen.h"
|
||||
#include "FullScreenMessageActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
void FullScreenMessageScreen::onEnter() {
|
||||
void FullScreenMessageActivity::onEnter() {
|
||||
const auto height = renderer.getLineHeight(UI_FONT_ID);
|
||||
const auto top = (GfxRenderer::getScreenHeight() - height) / 2;
|
||||
|
||||
21
src/activities/util/FullScreenMessageActivity.h
Normal file
21
src/activities/util/FullScreenMessageActivity.h
Normal file
@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include <EInkDisplay.h>
|
||||
#include <EpdFontFamily.h>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "../Activity.h"
|
||||
|
||||
class FullScreenMessageActivity final : public Activity {
|
||||
std::string text;
|
||||
EpdFontStyle style;
|
||||
EInkDisplay::RefreshMode refreshMode;
|
||||
|
||||
public:
|
||||
explicit FullScreenMessageActivity(GfxRenderer& renderer, InputManager& inputManager, std::string text,
|
||||
const EpdFontStyle style = REGULAR,
|
||||
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
|
||||
: Activity(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {}
|
||||
void onEnter() override;
|
||||
};
|
||||
103
src/main.cpp
103
src/main.cpp
@ -16,13 +16,13 @@
|
||||
#include "Battery.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "activities/boot_sleep/BootActivity.h"
|
||||
#include "activities/boot_sleep/SleepActivity.h"
|
||||
#include "activities/home/HomeActivity.h"
|
||||
#include "activities/reader/ReaderActivity.h"
|
||||
#include "activities/settings/SettingsActivity.h"
|
||||
#include "activities/util/FullScreenMessageActivity.h"
|
||||
#include "config.h"
|
||||
#include "screens/BootLogoScreen.h"
|
||||
#include "screens/EpubReaderScreen.h"
|
||||
#include "screens/FileSelectionScreen.h"
|
||||
#include "screens/FullScreenMessageScreen.h"
|
||||
#include "screens/SettingsScreen.h"
|
||||
#include "screens/SleepScreen.h"
|
||||
|
||||
#define SPI_FQ 40000000
|
||||
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
|
||||
@ -41,8 +41,7 @@
|
||||
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
|
||||
InputManager inputManager;
|
||||
GfxRenderer renderer(einkDisplay);
|
||||
Screen* currentScreen;
|
||||
CrossPointState appState;
|
||||
Activity* currentActivity;
|
||||
|
||||
// Fonts
|
||||
EpdFont bookerlyFont(&bookerly_2b);
|
||||
@ -66,31 +65,16 @@ constexpr unsigned long POWER_BUTTON_SLEEP_MS = 500;
|
||||
// Auto-sleep timeout (10 minutes of inactivity)
|
||||
constexpr unsigned long AUTO_SLEEP_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
|
||||
std::unique_ptr<Epub> loadEpub(const std::string& path) {
|
||||
if (!SD.exists(path.c_str())) {
|
||||
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
|
||||
if (epub->load()) {
|
||||
return epub;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void exitScreen() {
|
||||
if (currentScreen) {
|
||||
currentScreen->onExit();
|
||||
delete currentScreen;
|
||||
void exitActivity() {
|
||||
if (currentActivity) {
|
||||
currentActivity->onExit();
|
||||
delete currentActivity;
|
||||
}
|
||||
}
|
||||
|
||||
void enterNewScreen(Screen* screen) {
|
||||
currentScreen = screen;
|
||||
currentScreen->onEnter();
|
||||
void enterNewActivity(Activity* activity) {
|
||||
currentActivity = activity;
|
||||
currentActivity->onEnter();
|
||||
}
|
||||
|
||||
// Verify long press on wake-up from deep sleep
|
||||
@ -134,8 +118,8 @@ void waitForPowerRelease() {
|
||||
|
||||
// Enter deep sleep mode
|
||||
void enterDeepSleep() {
|
||||
exitScreen();
|
||||
enterNewScreen(new SleepScreen(renderer, inputManager));
|
||||
exitActivity();
|
||||
enterNewActivity(new SleepActivity(renderer, inputManager));
|
||||
|
||||
Serial.printf("[%lu] [ ] Power button released after a long press. Entering deep sleep.\n", millis());
|
||||
delay(1000); // Allow Serial buffer to empty and display to update
|
||||
@ -150,33 +134,20 @@ void enterDeepSleep() {
|
||||
}
|
||||
|
||||
void onGoHome();
|
||||
void onSelectEpubFile(const std::string& path) {
|
||||
exitScreen();
|
||||
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading..."));
|
||||
|
||||
auto epub = loadEpub(path);
|
||||
if (epub) {
|
||||
appState.openEpubPath = path;
|
||||
appState.saveToFile();
|
||||
exitScreen();
|
||||
enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
|
||||
} else {
|
||||
exitScreen();
|
||||
enterNewScreen(
|
||||
new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, EInkDisplay::HALF_REFRESH));
|
||||
delay(2000);
|
||||
onGoHome();
|
||||
}
|
||||
void onGoToReader(const std::string& initialEpubPath) {
|
||||
exitActivity();
|
||||
enterNewActivity(new ReaderActivity(renderer, inputManager, initialEpubPath, onGoHome));
|
||||
}
|
||||
void onGoToReaderHome() { onGoToReader(std::string()); }
|
||||
|
||||
void onGoToSettings() {
|
||||
exitScreen();
|
||||
enterNewScreen(new SettingsScreen(renderer, inputManager, onGoHome));
|
||||
exitActivity();
|
||||
enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome));
|
||||
}
|
||||
|
||||
void onGoHome() {
|
||||
exitScreen();
|
||||
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings));
|
||||
exitActivity();
|
||||
enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings));
|
||||
}
|
||||
|
||||
void setup() {
|
||||
@ -202,28 +173,20 @@ void setup() {
|
||||
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
|
||||
Serial.printf("[%lu] [ ] Fonts setup\n", millis());
|
||||
|
||||
exitScreen();
|
||||
enterNewScreen(new BootLogoScreen(renderer, inputManager));
|
||||
exitActivity();
|
||||
enterNewActivity(new BootActivity(renderer, inputManager));
|
||||
|
||||
// SD Card Initialization
|
||||
SD.begin(SD_SPI_CS, SPI, SPI_FQ);
|
||||
|
||||
SETTINGS.loadFromFile();
|
||||
appState.loadFromFile();
|
||||
if (!appState.openEpubPath.empty()) {
|
||||
auto epub = loadEpub(appState.openEpubPath);
|
||||
if (epub) {
|
||||
exitScreen();
|
||||
enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
|
||||
// Ensure we're not still holding the power button before leaving setup
|
||||
waitForPowerRelease();
|
||||
return;
|
||||
}
|
||||
APP_STATE.loadFromFile();
|
||||
if (APP_STATE.openEpubPath.empty()) {
|
||||
onGoHome();
|
||||
} else {
|
||||
onGoToReader(APP_STATE.openEpubPath);
|
||||
}
|
||||
|
||||
exitScreen();
|
||||
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoToSettings));
|
||||
|
||||
// Ensure we're not still holding the power button before leaving setup
|
||||
waitForPowerRelease();
|
||||
}
|
||||
@ -259,7 +222,7 @@ void loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentScreen) {
|
||||
currentScreen->handleInput();
|
||||
if (currentActivity) {
|
||||
currentActivity->loop();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
#pragma once
|
||||
#include "Screen.h"
|
||||
|
||||
class BootLogoScreen final : public Screen {
|
||||
public:
|
||||
explicit BootLogoScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
|
||||
void onEnter() override;
|
||||
};
|
||||
@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
#include <EInkDisplay.h>
|
||||
#include <EpdFontFamily.h>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "Screen.h"
|
||||
|
||||
class FullScreenMessageScreen final : public Screen {
|
||||
std::string text;
|
||||
EpdFontStyle style;
|
||||
EInkDisplay::RefreshMode refreshMode;
|
||||
|
||||
public:
|
||||
explicit FullScreenMessageScreen(GfxRenderer& renderer, InputManager& inputManager, std::string text,
|
||||
const EpdFontStyle style = REGULAR,
|
||||
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
|
||||
: Screen(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {}
|
||||
void onEnter() override;
|
||||
};
|
||||
@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
#include <InputManager.h>
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
class Screen {
|
||||
protected:
|
||||
GfxRenderer& renderer;
|
||||
InputManager& inputManager;
|
||||
|
||||
public:
|
||||
explicit Screen(GfxRenderer& renderer, InputManager& inputManager) : renderer(renderer), inputManager(inputManager) {}
|
||||
virtual ~Screen() = default;
|
||||
virtual void onEnter() {}
|
||||
virtual void onExit() {}
|
||||
virtual void handleInput() {}
|
||||
};
|
||||
@ -1,8 +0,0 @@
|
||||
#pragma once
|
||||
#include "Screen.h"
|
||||
|
||||
class SleepScreen final : public Screen {
|
||||
public:
|
||||
explicit SleepScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
|
||||
void onEnter() override;
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user