mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
Compare commits
9 Commits
b6488f3739
...
871c702084
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
871c702084 | ||
|
|
e5c0ddc9fa | ||
|
|
b1dcb7733b | ||
|
|
0d82b03981 | ||
|
|
5a97334ace | ||
|
|
436dc9e593 | ||
|
|
85b7e8124d | ||
|
|
6d6559022c | ||
|
|
5671b05d04 |
14
README.md
14
README.md
@ -95,6 +95,20 @@ Connect your Xteink X4 to your computer via USB-C and run the following command.
|
||||
```sh
|
||||
pio run --target upload
|
||||
```
|
||||
### Debugging
|
||||
|
||||
After flashing the new features, it’s recommended to capture detailed logs from the serial port.
|
||||
|
||||
First, make sure all required Python packages are installed:
|
||||
|
||||
```python
|
||||
python3 -m pip install serial colorama matplotlib
|
||||
```
|
||||
after that run the script:
|
||||
```sh
|
||||
python3 scripts/debugging_monitor.py
|
||||
```
|
||||
This was tested on Debian and should work on most Linux systems. Minor adjustments may be required for Windows or macOS.
|
||||
|
||||
## Internals
|
||||
|
||||
|
||||
@ -428,28 +428,28 @@ bool Epub::generateCoverBmp(bool cropped) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; }
|
||||
std::string Epub::getCoverHomeBmpPath() const { return cachePath + "/cover_home.bmp"; }
|
||||
|
||||
bool Epub::generateThumbBmp() const {
|
||||
bool Epub::generateCoverHomeBmp() const {
|
||||
// Already generated, return true
|
||||
if (SdMan.exists(getThumbBmpPath().c_str())) {
|
||||
if (SdMan.exists(getCoverHomeBmpPath().c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] Cannot generate thumb BMP, cache not loaded\n", millis());
|
||||
Serial.printf("[%lu] [EBP] Cannot generate home BMP, cache not loaded\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
if (coverImageHref.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No known cover image for thumbnail\n", millis());
|
||||
Serial.printf("[%lu] [EBP] No known cover image for home screen\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||
Serial.printf("[%lu] [EBP] Generating thumb BMP from JPG cover image\n", millis());
|
||||
Serial.printf("[%lu] [EBP] Generating home BMP from JPG cover image\n", millis());
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
FsFile coverJpg;
|
||||
@ -463,30 +463,50 @@ bool Epub::generateThumbBmp() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile thumbBmp;
|
||||
if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) {
|
||||
FsFile homeBmp;
|
||||
if (!SdMan.openFileForWrite("EBP", getCoverHomeBmpPath(), homeBmp)) {
|
||||
coverJpg.close();
|
||||
return false;
|
||||
}
|
||||
// Use smaller target size for Continue Reading card (half of screen: 240x400)
|
||||
|
||||
// For home screen, use 400px height with proportional width for optimal performance
|
||||
// Generate 1-bit BMP for fast home screen rendering (no gray passes needed)
|
||||
constexpr int THUMB_TARGET_WIDTH = 240;
|
||||
constexpr int THUMB_TARGET_HEIGHT = 400;
|
||||
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH,
|
||||
THUMB_TARGET_HEIGHT);
|
||||
constexpr int HOME_TARGET_HEIGHT = 400;
|
||||
|
||||
FsFile tempJpg;
|
||||
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, tempJpg)) {
|
||||
coverJpg.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// First get JPEG dimensions to calculate proper width
|
||||
int jpegWidth, jpegHeight;
|
||||
if (!JpegToBmpConverter::getJpegDimensions(tempJpg, &jpegWidth, &jpegHeight)) {
|
||||
Serial.printf("[%lu] [EBP] Failed to get JPEG dimensions for home cover\n", millis());
|
||||
coverJpg.close();
|
||||
tempJpg.close();
|
||||
return false;
|
||||
}
|
||||
tempJpg.close();
|
||||
|
||||
// Calculate proportional width for 400px height
|
||||
const int targetWidth = (400 * jpegWidth) / jpegHeight;
|
||||
|
||||
const bool success =
|
||||
JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, homeBmp, targetWidth, HOME_TARGET_HEIGHT);
|
||||
coverJpg.close();
|
||||
thumbBmp.close();
|
||||
homeBmp.close();
|
||||
SdMan.remove(coverJpgTempPath.c_str());
|
||||
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [EBP] Failed to generate thumb BMP from JPG cover image\n", millis());
|
||||
SdMan.remove(getThumbBmpPath().c_str());
|
||||
Serial.printf("[%lu] [EBP] Failed to generate home BMP from JPG cover image\n", millis());
|
||||
SdMan.remove(getCoverHomeBmpPath().c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated thumb BMP from JPG cover image, success: %s\n", millis(),
|
||||
Serial.printf("[%lu] [EBP] Generated home 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 thumbnail\n", millis());
|
||||
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping home screen\n", millis());
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@ -47,8 +47,9 @@ class Epub {
|
||||
const std::string& getLanguage() const;
|
||||
std::string getCoverBmpPath(bool cropped = false) const;
|
||||
bool generateCoverBmp(bool cropped = false) const;
|
||||
std::string getThumbBmpPath() const;
|
||||
bool generateThumbBmp() const;
|
||||
// Home screen support (optimized 400px height covers for Continue Reading card)
|
||||
std::string getCoverHomeBmpPath() const;
|
||||
bool generateCoverHomeBmp() const;
|
||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||
bool trailingNullByte = false) const;
|
||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||
|
||||
@ -415,13 +415,21 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
|
||||
|
||||
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
||||
const EpdFontFamily::Style style) const {
|
||||
if (!text || maxWidth <= 0) return "";
|
||||
|
||||
std::string item = text;
|
||||
int itemWidth = getTextWidth(fontId, item.c_str(), style);
|
||||
while (itemWidth > maxWidth && item.length() > 8) {
|
||||
item.replace(item.length() - 5, 5, "...");
|
||||
itemWidth = getTextWidth(fontId, item.c_str(), style);
|
||||
const char* ellipsis = "...";
|
||||
int textWidth = getTextWidth(fontId, item.c_str(), style);
|
||||
if (textWidth <= maxWidth) {
|
||||
// Text fits, return as is
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
|
||||
while (!item.empty() && getTextWidth(fontId, (item + ellipsis).c_str(), style) >= maxWidth) {
|
||||
utf8RemoveLastChar(item);
|
||||
}
|
||||
|
||||
return item.empty() ? ellipsis : item + ellipsis;
|
||||
}
|
||||
|
||||
// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation
|
||||
|
||||
@ -569,3 +569,32 @@ bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print
|
||||
int targetMaxHeight) {
|
||||
return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true);
|
||||
}
|
||||
|
||||
// Get JPEG dimensions without full conversion
|
||||
bool JpegToBmpConverter::getJpegDimensions(FsFile& jpegFile, int* width, int* height) {
|
||||
// Reset file position to beginning
|
||||
if (!jpegFile.seek(0)) {
|
||||
Serial.printf("[%lu] [JPG] Failed to seek to beginning of JPEG file\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize JPEG decoder
|
||||
pjpeg_image_info_t imageInfo = {};
|
||||
JpegReadContext context{jpegFile, {}, 0, 0};
|
||||
|
||||
const int decodeStatus = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, false);
|
||||
if (decodeStatus != 0) {
|
||||
Serial.printf("[%lu] [JPG] pjpeg_decode_init failed with status %d\n", millis(), decodeStatus);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get dimensions from image info
|
||||
*width = imageInfo.m_width;
|
||||
*height = imageInfo.m_height;
|
||||
|
||||
Serial.printf("[%lu] [JPG] Read JPEG dimensions: %dx%d\n", millis(), *width, *height);
|
||||
|
||||
// Reset file position after reading
|
||||
jpegFile.seek(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -16,4 +16,6 @@ class JpegToBmpConverter {
|
||||
static bool jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
|
||||
// Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering
|
||||
static bool jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
|
||||
// Extract JPEG dimensions without loading full image
|
||||
static bool getJpegDimensions(FsFile& jpegFile, int* width, int* height);
|
||||
};
|
||||
|
||||
@ -29,3 +29,20 @@ uint32_t utf8NextCodepoint(const unsigned char** string) {
|
||||
|
||||
return cp;
|
||||
}
|
||||
|
||||
size_t utf8RemoveLastChar(std::string& str) {
|
||||
if (str.empty()) return 0;
|
||||
size_t pos = str.size() - 1;
|
||||
while (pos > 0 && (static_cast<unsigned char>(str[pos]) & 0xC0) == 0x80) {
|
||||
--pos;
|
||||
}
|
||||
str.resize(pos);
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Truncate string by removing N UTF-8 characters from the end
|
||||
void utf8TruncateChars(std::string& str, const size_t numChars) {
|
||||
for (size_t i = 0; i < numChars && !str.empty(); ++i) {
|
||||
utf8RemoveLastChar(str);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <string>
|
||||
#define REPLACEMENT_GLYPH 0xFFFD
|
||||
|
||||
uint32_t utf8NextCodepoint(const unsigned char** string);
|
||||
// Remove the last UTF-8 codepoint from a std::string and return the new size.
|
||||
size_t utf8RemoveLastChar(std::string& str);
|
||||
// Truncate string by removing N UTF-8 codepoints from the end.
|
||||
void utf8TruncateChars(std::string& str, size_t numChars);
|
||||
|
||||
@ -301,11 +301,11 @@ bool Xtc::generateCoverBmp() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Xtc::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; }
|
||||
std::string Xtc::getCoverHomeBmpPath() const { return cachePath + "/cover_home.bmp"; }
|
||||
|
||||
bool Xtc::generateThumbBmp() const {
|
||||
bool Xtc::generateCoverHomeBmp() const {
|
||||
// Already generated
|
||||
if (SdMan.exists(getThumbBmpPath().c_str())) {
|
||||
if (SdMan.exists(getCoverHomeBmpPath().c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -332,43 +332,18 @@ bool Xtc::generateThumbBmp() const {
|
||||
// Get bit depth
|
||||
const uint8_t bitDepth = parser->getBitDepth();
|
||||
|
||||
// Calculate target dimensions for thumbnail (fit within 240x400 Continue Reading card)
|
||||
constexpr int THUMB_TARGET_WIDTH = 240;
|
||||
constexpr int THUMB_TARGET_HEIGHT = 400;
|
||||
// For home screen, use 400px height with proportional width for optimal performance
|
||||
// Generate 1-bit BMP for fast home screen rendering (no gray passes needed)
|
||||
constexpr int HOME_TARGET_HEIGHT = 400;
|
||||
|
||||
// Calculate scale factor
|
||||
float scaleX = static_cast<float>(THUMB_TARGET_WIDTH) / pageInfo.width;
|
||||
float scaleY = static_cast<float>(THUMB_TARGET_HEIGHT) / pageInfo.height;
|
||||
float scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
// Calculate proportional width for 400px height
|
||||
const uint16_t targetWidth = static_cast<uint16_t>((HOME_TARGET_HEIGHT * pageInfo.width) / pageInfo.height);
|
||||
|
||||
// Only scale down, never up
|
||||
if (scale >= 1.0f) {
|
||||
// Page is already small enough, just use cover.bmp
|
||||
// Copy cover.bmp to thumb.bmp
|
||||
if (generateCoverBmp()) {
|
||||
FsFile src, dst;
|
||||
if (SdMan.openFileForRead("XTC", getCoverBmpPath(), src)) {
|
||||
if (SdMan.openFileForWrite("XTC", getThumbBmpPath(), dst)) {
|
||||
uint8_t buffer[512];
|
||||
while (src.available()) {
|
||||
size_t bytesRead = src.read(buffer, sizeof(buffer));
|
||||
dst.write(buffer, bytesRead);
|
||||
}
|
||||
dst.close();
|
||||
}
|
||||
src.close();
|
||||
}
|
||||
Serial.printf("[%lu] [XTC] Copied cover to thumb (no scaling needed)\n", millis());
|
||||
return SdMan.exists(getThumbBmpPath().c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t thumbWidth = static_cast<uint16_t>(pageInfo.width * scale);
|
||||
uint16_t thumbHeight = static_cast<uint16_t>(pageInfo.height * scale);
|
||||
Serial.printf("[%lu] [XTC] Generating home BMP: %dx%d -> %dx%d\n", millis(), pageInfo.width, pageInfo.height,
|
||||
targetWidth, HOME_TARGET_HEIGHT);
|
||||
|
||||
Serial.printf("[%lu] [XTC] Generating thumb BMP: %dx%d -> %dx%d (scale: %.3f)\n", millis(), pageInfo.width,
|
||||
pageInfo.height, thumbWidth, thumbHeight, scale);
|
||||
pageInfo.height, targetWidth, HOME_TARGET_HEIGHT);
|
||||
|
||||
// Allocate buffer for page data
|
||||
size_t bitmapSize;
|
||||
@ -393,15 +368,15 @@ bool Xtc::generateThumbBmp() const {
|
||||
|
||||
// Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes)
|
||||
FsFile thumbBmp;
|
||||
if (!SdMan.openFileForWrite("XTC", getThumbBmpPath(), thumbBmp)) {
|
||||
if (!SdMan.openFileForWrite("XTC", getCoverHomeBmpPath(), thumbBmp)) {
|
||||
Serial.printf("[%lu] [XTC] Failed to create thumb BMP file\n", millis());
|
||||
free(pageBuffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write 1-bit BMP header for fast home screen rendering
|
||||
const uint32_t rowSize = (thumbWidth + 31) / 32 * 4; // 1 bit per pixel, aligned to 4 bytes
|
||||
const uint32_t imageSize = rowSize * thumbHeight;
|
||||
const uint32_t rowSize = (targetWidth + 31) / 32 * 4; // 1 bit per pixel, aligned to 4 bytes
|
||||
const uint32_t imageSize = rowSize * HOME_TARGET_HEIGHT;
|
||||
const uint32_t fileSize = 14 + 40 + 8 + imageSize; // 8 bytes for 2-color palette
|
||||
|
||||
// File header
|
||||
@ -416,9 +391,9 @@ bool Xtc::generateThumbBmp() const {
|
||||
// DIB header
|
||||
uint32_t dibHeaderSize = 40;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&dibHeaderSize), 4);
|
||||
int32_t widthVal = thumbWidth;
|
||||
int32_t widthVal = targetWidth;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&widthVal), 4);
|
||||
int32_t heightVal = -static_cast<int32_t>(thumbHeight); // Negative for top-down
|
||||
int32_t heightVal = -static_cast<int32_t>(HOME_TARGET_HEIGHT); // Negative for top-down
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&heightVal), 4);
|
||||
uint16_t planes = 1;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
|
||||
@ -451,8 +426,8 @@ bool Xtc::generateThumbBmp() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fixed-point scale factor (16.16)
|
||||
uint32_t scaleInv_fp = static_cast<uint32_t>(65536.0f / scale);
|
||||
// Fixed-point scale factor (16.16) - scale to fit 400px height
|
||||
uint32_t scaleInv_fp = static_cast<uint32_t>(65536.0f * static_cast<float>(pageInfo.height) / HOME_TARGET_HEIGHT);
|
||||
|
||||
// Pre-calculate plane info for 2-bit mode
|
||||
const size_t planeSize = (bitDepth == 2) ? ((static_cast<size_t>(pageInfo.width) * pageInfo.height + 7) / 8) : 0;
|
||||
@ -461,7 +436,7 @@ bool Xtc::generateThumbBmp() const {
|
||||
const size_t colBytes = (bitDepth == 2) ? ((pageInfo.height + 7) / 8) : 0;
|
||||
const size_t srcRowBytes = (bitDepth == 1) ? ((pageInfo.width + 7) / 8) : 0;
|
||||
|
||||
for (uint16_t dstY = 0; dstY < thumbHeight; dstY++) {
|
||||
for (uint16_t dstY = 0; dstY < HOME_TARGET_HEIGHT; dstY++) {
|
||||
memset(rowBuffer, 0xFF, rowSize); // Start with all white (bit 1)
|
||||
|
||||
// Calculate source Y range with bounds checking
|
||||
@ -472,7 +447,7 @@ bool Xtc::generateThumbBmp() const {
|
||||
if (srcYEnd <= srcYStart) srcYEnd = srcYStart + 1;
|
||||
if (srcYEnd > pageInfo.height) srcYEnd = pageInfo.height;
|
||||
|
||||
for (uint16_t dstX = 0; dstX < thumbWidth; dstX++) {
|
||||
for (uint16_t dstX = 0; dstX < targetWidth; dstX++) {
|
||||
// Calculate source X range with bounds checking
|
||||
uint32_t srcXStart = (static_cast<uint32_t>(dstX) * scaleInv_fp) >> 16;
|
||||
uint32_t srcXEnd = (static_cast<uint32_t>(dstX + 1) * scaleInv_fp) >> 16;
|
||||
@ -557,8 +532,8 @@ bool Xtc::generateThumbBmp() const {
|
||||
thumbBmp.close();
|
||||
free(pageBuffer);
|
||||
|
||||
Serial.printf("[%lu] [XTC] Generated thumb BMP (%dx%d): %s\n", millis(), thumbWidth, thumbHeight,
|
||||
getThumbBmpPath().c_str());
|
||||
Serial.printf("[%lu] [XTC] Generated home BMP (%dx%d): %s\n", millis(), targetWidth, HOME_TARGET_HEIGHT,
|
||||
getCoverHomeBmpPath().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -63,9 +63,9 @@ class Xtc {
|
||||
// Cover image support (for sleep screen)
|
||||
std::string getCoverBmpPath() const;
|
||||
bool generateCoverBmp() const;
|
||||
// Thumbnail support (for Continue Reading card)
|
||||
std::string getThumbBmpPath() const;
|
||||
bool generateThumbBmp() const;
|
||||
// Home screen support (optimized 400px height covers for Continue Reading card)
|
||||
std::string getCoverHomeBmpPath() const;
|
||||
bool generateCoverHomeBmp() const;
|
||||
|
||||
// Page access
|
||||
uint32_t getPageCount() const;
|
||||
|
||||
214
scripts/debugging_monitor.py
Executable file
214
scripts/debugging_monitor.py
Executable file
@ -0,0 +1,214 @@
|
||||
import sys
|
||||
import argparse
|
||||
import re
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from collections import deque
|
||||
import time
|
||||
|
||||
# Try to import potentially missing packages
|
||||
try:
|
||||
import serial
|
||||
from colorama import init, Fore, Style
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.animation as animation
|
||||
except ImportError as e:
|
||||
missing_package = e.name
|
||||
print("\n" + "!" * 50)
|
||||
print(f" Error: The required package '{missing_package}' is not installed.")
|
||||
print("!" * 50)
|
||||
|
||||
print(f"\nTo fix this, please run the following command in your terminal:\n")
|
||||
|
||||
install_cmd = "pip install "
|
||||
packages = []
|
||||
if 'serial' in str(e): packages.append("pyserial")
|
||||
if 'colorama' in str(e): packages.append("colorama")
|
||||
if 'matplotlib' in str(e): packages.append("matplotlib")
|
||||
|
||||
print(f" {install_cmd}{' '.join(packages)}")
|
||||
|
||||
print("\nExiting...")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Global Variables for Data Sharing ---
|
||||
# Store last 50 data points
|
||||
MAX_POINTS = 50
|
||||
time_data = deque(maxlen=MAX_POINTS)
|
||||
free_mem_data = deque(maxlen=MAX_POINTS)
|
||||
total_mem_data = deque(maxlen=MAX_POINTS)
|
||||
data_lock = threading.Lock() # Prevent reading while writing
|
||||
|
||||
# Initialize colors
|
||||
init(autoreset=True)
|
||||
|
||||
def get_color_for_line(line):
|
||||
"""
|
||||
Classify log lines by type and assign appropriate colors.
|
||||
"""
|
||||
line_upper = line.upper()
|
||||
|
||||
if any(keyword in line_upper for keyword in ["ERROR", "[ERR]", "[SCT]", "FAILED", "WARNING"]):
|
||||
return Fore.RED
|
||||
if "[MEM]" in line_upper or "FREE:" in line_upper:
|
||||
return Fore.CYAN
|
||||
if any(keyword in line_upper for keyword in ["[GFX]", "[ERS]", "DISPLAY", "RAM WRITE", "RAM COMPLETE", "REFRESH", "POWERING ON", "FRAME BUFFER", "LUT"]):
|
||||
return Fore.MAGENTA
|
||||
if any(keyword in line_upper for keyword in ["[EBP]", "[BMC]", "[ZIP]", "[PARSER]", "[EHP]", "LOADING EPUB", "CACHE", "DECOMPRESSED", "PARSING"]):
|
||||
return Fore.GREEN
|
||||
if "[ACT]" in line_upper or "ENTERING ACTIVITY" in line_upper or "EXITING ACTIVITY" in line_upper:
|
||||
return Fore.YELLOW
|
||||
if any(keyword in line_upper for keyword in ["RENDERED PAGE", "[LOOP]", "DURATION", "WAIT COMPLETE"]):
|
||||
return Fore.BLUE
|
||||
if any(keyword in line_upper for keyword in ["[CPS]", "SETTINGS", "[CLEAR_CACHE]"]):
|
||||
return Fore.LIGHTYELLOW_EX
|
||||
if any(keyword in line_upper for keyword in ["ESP-ROM", "BUILD:", "RST:", "BOOT:", "SPIWP:", "MODE:", "LOAD:", "ENTRY", "[SD]", "STARTING CROSSPOINT", "VERSION"]):
|
||||
return Fore.LIGHTBLACK_EX
|
||||
if "[RBS]" in line_upper:
|
||||
return Fore.LIGHTCYAN_EX
|
||||
if "[KRS]" in line_upper:
|
||||
return Fore.LIGHTMAGENTA_EX
|
||||
if any(keyword in line_upper for keyword in ["EINKDISPLAY:", "STATIC FRAME", "INITIALIZING", "SPI INITIALIZED", "GPIO PINS", "RESETTING", "SSD1677", "E-INK"]):
|
||||
return Fore.LIGHTMAGENTA_EX
|
||||
if any(keyword in line_upper for keyword in ["[FNS]", "FOOTNOTE"]):
|
||||
return Fore.LIGHTGREEN_EX
|
||||
if any(keyword in line_upper for keyword in ["[CHAP]", "[OPDS]", "[COF]"]):
|
||||
return Fore.LIGHTYELLOW_EX
|
||||
|
||||
return Fore.WHITE
|
||||
|
||||
def parse_memory_line(line):
|
||||
"""
|
||||
Extracts Free and Total bytes from the specific log line.
|
||||
Format: [MEM] Free: 196344 bytes, Total: 226412 bytes, Min Free: 112620 bytes
|
||||
"""
|
||||
# Regex to find 'Free: <digits>' and 'Total: <digits>'
|
||||
match = re.search(r"Free:\s*(\d+).*Total:\s*(\d+)", line)
|
||||
if match:
|
||||
try:
|
||||
free_bytes = int(match.group(1))
|
||||
total_bytes = int(match.group(2))
|
||||
return free_bytes, total_bytes
|
||||
except ValueError:
|
||||
return None, None
|
||||
return None, None
|
||||
|
||||
def serial_worker(port, baud):
|
||||
"""
|
||||
Runs in a background thread. Handles reading serial, printing to console,
|
||||
and updating the data lists.
|
||||
"""
|
||||
print(f"{Fore.CYAN}--- Opening {port} at {baud} baud ---{Style.RESET_ALL}")
|
||||
|
||||
try:
|
||||
ser = serial.Serial(port, baud, timeout=0.1)
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
except serial.SerialException as e:
|
||||
print(f"{Fore.RED}Error opening port: {e}{Style.RESET_ALL}")
|
||||
return
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
raw_data = ser.readline().decode('utf-8', errors='replace')
|
||||
|
||||
if not raw_data:
|
||||
continue
|
||||
|
||||
clean_line = raw_data.strip()
|
||||
if not clean_line:
|
||||
continue
|
||||
|
||||
# Add PC timestamp
|
||||
pc_time = datetime.now().strftime("%H:%M:%S")
|
||||
formatted_line = re.sub(r"^\[\d+\]", f"[{pc_time}]", clean_line)
|
||||
|
||||
# Check for Memory Line
|
||||
if "[MEM]" in formatted_line:
|
||||
free_val, total_val = parse_memory_line(formatted_line)
|
||||
if free_val is not None:
|
||||
with data_lock:
|
||||
time_data.append(pc_time)
|
||||
free_mem_data.append(free_val / 1024) # Convert to KB
|
||||
total_mem_data.append(total_val / 1024) # Convert to KB
|
||||
|
||||
# Print to console
|
||||
line_color = get_color_for_line(formatted_line)
|
||||
print(f"{line_color}{formatted_line}")
|
||||
|
||||
except OSError:
|
||||
print(f"{Fore.RED}Device disconnected.{Style.RESET_ALL}")
|
||||
break
|
||||
except Exception as e:
|
||||
# If thread is killed violently (e.g. main exit), silence errors
|
||||
pass
|
||||
finally:
|
||||
if 'ser' in locals() and ser.is_open:
|
||||
ser.close()
|
||||
|
||||
def update_graph(frame):
|
||||
"""
|
||||
Called by Matplotlib animation to redraw the chart.
|
||||
"""
|
||||
with data_lock:
|
||||
if not time_data:
|
||||
return
|
||||
|
||||
# Convert deques to lists for plotting
|
||||
x = list(time_data)
|
||||
y_free = list(free_mem_data)
|
||||
y_total = list(total_mem_data)
|
||||
|
||||
plt.cla() # Clear axis
|
||||
|
||||
# Plot Total RAM
|
||||
plt.plot(x, y_total, label='Total RAM (KB)', color='red', linestyle='--')
|
||||
|
||||
# Plot Free RAM
|
||||
plt.plot(x, y_free, label='Free RAM (KB)', color='green', marker='o')
|
||||
|
||||
# Fill area under Free RAM
|
||||
plt.fill_between(x, y_free, color='green', alpha=0.1)
|
||||
|
||||
plt.title("ESP32 Memory Monitor")
|
||||
plt.ylabel("Memory (KB)")
|
||||
plt.xlabel("Time")
|
||||
plt.legend(loc='upper left')
|
||||
plt.grid(True, linestyle=':', alpha=0.6)
|
||||
|
||||
# Rotate date labels
|
||||
plt.xticks(rotation=45, ha='right')
|
||||
plt.tight_layout()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="ESP32 Monitor with Graph")
|
||||
parser.add_argument("port", nargs="?", default="/dev/ttyACM0", help="Serial port")
|
||||
parser.add_argument("--baud", type=int, default=115200, help="Baud rate")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 1. Start the Serial Reader in a separate thread
|
||||
# Daemon=True means this thread dies when the main program closes
|
||||
t = threading.Thread(target=serial_worker, args=(args.port, args.baud), daemon=True)
|
||||
t.start()
|
||||
|
||||
# 2. Set up the Graph (Main Thread)
|
||||
try:
|
||||
plt.style.use('light_background')
|
||||
except:
|
||||
pass
|
||||
|
||||
fig = plt.figure(figsize=(10, 6))
|
||||
|
||||
# Update graph every 1000ms
|
||||
ani = animation.FuncAnimation(fig, update_graph, interval=1000)
|
||||
|
||||
try:
|
||||
print(f"{Fore.YELLOW}Starting Graph Window... (Close window to exit){Style.RESET_ALL}")
|
||||
plt.show()
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Fore.YELLOW}Exiting...{Style.RESET_ALL}")
|
||||
plt.close('all') # Force close any lingering plot windows
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -4,6 +4,7 @@
|
||||
#include <Epub.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <SDCardManager.h>
|
||||
#include <Utf8.h>
|
||||
#include <Xtc.h>
|
||||
|
||||
#include <cstring>
|
||||
@ -59,8 +60,8 @@ void HomeActivity::onEnter() {
|
||||
lastBookAuthor = std::string(epub.getAuthor());
|
||||
}
|
||||
// Try to generate thumbnail image for Continue Reading card
|
||||
if (epub.generateThumbBmp()) {
|
||||
coverBmpPath = epub.getThumbBmpPath();
|
||||
if (epub.generateCoverHomeBmp()) {
|
||||
coverBmpPath = epub.getCoverHomeBmpPath();
|
||||
hasCoverImage = true;
|
||||
}
|
||||
} else if (StringUtils::checkFileExtension(lastBookTitle, ".xtch") ||
|
||||
@ -75,8 +76,8 @@ void HomeActivity::onEnter() {
|
||||
lastBookAuthor = std::string(xtc.getAuthor());
|
||||
}
|
||||
// Try to generate thumbnail image for Continue Reading card
|
||||
if (xtc.generateThumbBmp()) {
|
||||
coverBmpPath = xtc.getThumbBmpPath();
|
||||
if (xtc.generateCoverHomeBmp()) {
|
||||
coverBmpPath = xtc.getCoverHomeBmpPath();
|
||||
hasCoverImage = true;
|
||||
}
|
||||
}
|
||||
@ -223,10 +224,32 @@ void HomeActivity::render() {
|
||||
constexpr int bottomMargin = 60;
|
||||
|
||||
// --- Top "book" card for the current title (selectorIndex == 0) ---
|
||||
const int bookWidth = pageWidth / 2;
|
||||
const int bookHeight = pageHeight / 2;
|
||||
const int bookX = (pageWidth - bookWidth) / 2;
|
||||
// Load cover image to get its dimensions
|
||||
int coverWidth = 0;
|
||||
int coverHeight = 0;
|
||||
if (hasContinueReading && hasCoverImage && !coverBmpPath.empty()) {
|
||||
FsFile coverFile;
|
||||
if (SdMan.openFileForRead("HOME", coverBmpPath, coverFile)) {
|
||||
Bitmap testBitmap(coverFile);
|
||||
if (testBitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||
coverWidth = testBitmap.getWidth();
|
||||
coverHeight = testBitmap.getHeight();
|
||||
}
|
||||
coverFile.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate card dimensions based on cover image
|
||||
// Use 400px height as specified, with proportional width
|
||||
constexpr int CARD_HEIGHT = 400;
|
||||
const int cardWidth = (coverWidth > 0 && coverHeight > 0)
|
||||
? (CARD_HEIGHT * coverWidth) / coverHeight
|
||||
: 240; // Fallback to 240px width if no image (maintain aspect ratio)
|
||||
|
||||
const int bookX = (pageWidth - cardWidth) / 2;
|
||||
constexpr int bookY = 30;
|
||||
const int bookWidth = cardWidth;
|
||||
const int bookHeight = CARD_HEIGHT;
|
||||
const bool bookSelected = hasContinueReading && selectorIndex == 0;
|
||||
|
||||
// Bookmark dimensions (used in multiple places)
|
||||
@ -245,27 +268,9 @@ void HomeActivity::render() {
|
||||
if (SdMan.openFileForRead("HOME", coverBmpPath, file)) {
|
||||
Bitmap bitmap(file);
|
||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||
// Calculate position to center image within the book card
|
||||
int coverX, coverY;
|
||||
|
||||
if (bitmap.getWidth() > bookWidth || bitmap.getHeight() > bookHeight) {
|
||||
const float imgRatio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
||||
const float boxRatio = static_cast<float>(bookWidth) / static_cast<float>(bookHeight);
|
||||
|
||||
if (imgRatio > boxRatio) {
|
||||
coverX = bookX;
|
||||
coverY = bookY + (bookHeight - static_cast<int>(bookWidth / imgRatio)) / 2;
|
||||
} else {
|
||||
coverX = bookX + (bookWidth - static_cast<int>(bookHeight * imgRatio)) / 2;
|
||||
coverY = bookY;
|
||||
}
|
||||
} else {
|
||||
coverX = bookX + (bookWidth - bitmap.getWidth()) / 2;
|
||||
coverY = bookY + (bookHeight - bitmap.getHeight()) / 2;
|
||||
}
|
||||
|
||||
// Draw the cover image centered within the book card
|
||||
renderer.drawBitmap(bitmap, coverX, coverY, bookWidth, bookHeight);
|
||||
// Since the book card already has the exact same size as the image,
|
||||
// we can draw it at the same position with the same dimensions
|
||||
renderer.drawBitmap(bitmap, bookX, bookY, bookWidth, bookHeight);
|
||||
|
||||
// Draw border around the card
|
||||
renderer.drawRect(bookX, bookY, bookWidth, bookHeight);
|
||||
@ -366,7 +371,7 @@ void HomeActivity::render() {
|
||||
while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
|
||||
// Remove "..." first, then remove one UTF-8 char, then add "..." back
|
||||
lines.back().resize(lines.back().size() - 3); // Remove "..."
|
||||
StringUtils::utf8RemoveLastChar(lines.back());
|
||||
utf8RemoveLastChar(lines.back());
|
||||
lines.back().append("...");
|
||||
}
|
||||
break;
|
||||
@ -375,7 +380,7 @@ void HomeActivity::render() {
|
||||
int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str());
|
||||
while (wordWidth > maxLineWidth && !i.empty()) {
|
||||
// Word itself is too long, trim it (UTF-8 safe)
|
||||
StringUtils::utf8RemoveLastChar(i);
|
||||
utf8RemoveLastChar(i);
|
||||
// Check if we have room for ellipsis
|
||||
std::string withEllipsis = i + "...";
|
||||
wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str());
|
||||
@ -428,7 +433,7 @@ void HomeActivity::render() {
|
||||
if (!lastBookAuthor.empty()) {
|
||||
std::string trimmedAuthor = lastBookAuthor;
|
||||
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
|
||||
StringUtils::utf8RemoveLastChar(trimmedAuthor);
|
||||
utf8RemoveLastChar(trimmedAuthor);
|
||||
}
|
||||
if (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) <
|
||||
renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) {
|
||||
@ -462,14 +467,14 @@ void HomeActivity::render() {
|
||||
// Trim author if too long (UTF-8 safe)
|
||||
bool wasTrimmed = false;
|
||||
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
|
||||
StringUtils::utf8RemoveLastChar(trimmedAuthor);
|
||||
utf8RemoveLastChar(trimmedAuthor);
|
||||
wasTrimmed = true;
|
||||
}
|
||||
if (wasTrimmed && !trimmedAuthor.empty()) {
|
||||
// Make room for ellipsis
|
||||
while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth &&
|
||||
!trimmedAuthor.empty()) {
|
||||
StringUtils::utf8RemoveLastChar(trimmedAuthor);
|
||||
utf8RemoveLastChar(trimmedAuthor);
|
||||
}
|
||||
trimmedAuthor.append("...");
|
||||
}
|
||||
|
||||
@ -570,8 +570,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
|
||||
availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight;
|
||||
titleMarginLeftAdjusted = titleMarginLeft;
|
||||
}
|
||||
while (titleWidth > availableTitleSpace && title.length() > 11) {
|
||||
title.replace(title.length() - 8, 8, "...");
|
||||
if (titleWidth > availableTitleSpace) {
|
||||
title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTitleSpace);
|
||||
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@ -533,8 +533,8 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int
|
||||
|
||||
std::string title = txt->getTitle();
|
||||
int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||
while (titleWidth > availableTextWidth && title.length() > 11) {
|
||||
title.replace(title.length() - 8, 8, "...");
|
||||
if (titleWidth > availableTextWidth) {
|
||||
title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTextWidth);
|
||||
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||
}
|
||||
|
||||
|
||||
@ -61,23 +61,4 @@ bool checkFileExtension(const String& fileName, const char* extension) {
|
||||
return localFile.endsWith(localExtension);
|
||||
}
|
||||
|
||||
size_t utf8RemoveLastChar(std::string& str) {
|
||||
if (str.empty()) return 0;
|
||||
size_t pos = str.size() - 1;
|
||||
// Walk back to find the start of the last UTF-8 character
|
||||
// UTF-8 continuation bytes start with 10xxxxxx (0x80-0xBF)
|
||||
while (pos > 0 && (static_cast<unsigned char>(str[pos]) & 0xC0) == 0x80) {
|
||||
--pos;
|
||||
}
|
||||
str.resize(pos);
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Truncate string by removing N UTF-8 characters from the end
|
||||
void utf8TruncateChars(std::string& str, const size_t numChars) {
|
||||
for (size_t i = 0; i < numChars && !str.empty(); ++i) {
|
||||
utf8RemoveLastChar(str);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace StringUtils
|
||||
|
||||
@ -19,10 +19,4 @@ std::string sanitizeFilename(const std::string& name, size_t maxLength = 100);
|
||||
bool checkFileExtension(const std::string& fileName, const char* extension);
|
||||
bool checkFileExtension(const String& fileName, const char* extension);
|
||||
|
||||
// UTF-8 safe string truncation - removes one character from the end
|
||||
// Returns the new size after removing one UTF-8 character
|
||||
size_t utf8RemoveLastChar(std::string& str);
|
||||
|
||||
// Truncate string by removing N UTF-8 characters from the end
|
||||
void utf8TruncateChars(std::string& str, size_t numChars);
|
||||
} // namespace StringUtils
|
||||
|
||||
Loading…
Reference in New Issue
Block a user