Compare commits

...

4 Commits

Author SHA1 Message Date
Jesse Vincent
54ef16928c
Merge aa2467de9e into 78d6e5931c 2026-02-04 09:18:12 +11:00
Jake Kenneally
78d6e5931c
fix: Correct debugging_monitor.py script instructions (#676)
Some checks are pending
CI / build (push) Waiting to run
## Summary

**What is the goal of this PR?**
- Minor correction to the `debugging_monitor.py` script instructions

**What changes are included?**
- `pyserial` should be installed, NOT `serial`, which is a [different
lib](https://pypi.org/project/serial/)
- Added macOS serial port

## Additional Context

- Just a minor docs update. I can confirm the debugging script is
working great on macOS

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< NO >**_
2026-02-04 00:33:20 +03:00
Luke Stein
dac11c3fdd
fix: Correct instruction text to match actual button text (#672)
## Summary

* Instruction text says "Press OK to scan again" but button label is
actually "Connect" (not OK)
* Corrects instruction text

---

### AI Usage

Did you use AI tools to help write this code? **No**
2026-02-04 00:32:52 +03:00
Jesse Vincent
aa2467de9e feat: Add inverse display setting with proper anti-aliasing
Adds an inverseDisplay setting that renders white text on a black
background in both EPUB and TXT readers. The grayscale anti-aliasing
passes swap the LSB gray level condition based on text direction
(pixelState/black parameter) so intermediate gray values render
correctly against the dark background.
2026-02-01 23:46:57 -08:00
12 changed files with 44 additions and 27 deletions

View File

@ -102,13 +102,18 @@ After flashing the new features, its recommended to capture detailed logs fro
First, make sure all required Python packages are installed:
```python
python3 -m pip install serial colorama matplotlib
python3 -m pip install pyserial colorama matplotlib
```
after that run the script:
```sh
# For Linux
# This was tested on Debian and should work on most Linux systems.
python3 scripts/debugging_monitor.py
# For macOS
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
```
This was tested on Debian and should work on most Linux systems. Minor adjustments may be required for Windows or macOS.
Minor adjustments may be required for Windows.
## Internals

View File

@ -3,8 +3,8 @@
#include <HardwareSerial.h>
#include <Serialization.h>
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset, const bool black) {
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset, black);
}
bool PageLine::serialize(FsFile& file) {
@ -25,9 +25,10 @@ std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
}
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset,
const bool black) const {
for (auto& element : elements) {
element->render(renderer, fontId, xOffset, yOffset);
element->render(renderer, fontId, xOffset, yOffset, black);
}
}

View File

@ -17,7 +17,7 @@ class PageElement {
int16_t yPos;
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
virtual ~PageElement() = default;
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool black = true) = 0;
virtual bool serialize(FsFile& file) = 0;
};
@ -28,7 +28,7 @@ class PageLine final : public PageElement {
public:
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
: PageElement(xPos, yPos), block(std::move(block)) {}
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool black = true) override;
bool serialize(FsFile& file) override;
static std::unique_ptr<PageLine> deserialize(FsFile& file);
};
@ -37,7 +37,7 @@ class Page {
public:
// the list of block index and line numbers on this page
std::vector<std::shared_ptr<PageElement>> elements;
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool black = true) const;
bool serialize(FsFile& file) const;
static std::unique_ptr<Page> deserialize(FsFile& file);
};

View File

@ -3,7 +3,8 @@
#include <GfxRenderer.h>
#include <Serialization.h>
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y,
const bool black) const {
// Validate iterator bounds before rendering
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
Serial.printf("[%lu] [TXB] Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
@ -16,7 +17,7 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
auto wordXposIt = wordXpos.begin();
for (size_t i = 0; i < words.size(); i++) {
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), black, *wordStylesIt);
std::advance(wordIt, 1);
std::advance(wordStylesIt, 1);

View File

@ -34,7 +34,7 @@ class TextBlock final : public Block {
bool isEmpty() override { return words.empty(); }
void layout(GfxRenderer& renderer) override {};
// given a renderer works out where to break the words into lines
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
void render(const GfxRenderer& renderer, int fontId, int x, int y, bool black = true) const;
BlockType getType() override { return TEXT_BLOCK; }
bool serialize(FsFile& file) const;
static std::unique_ptr<TextBlock> deserialize(FsFile& file);

View File

@ -638,7 +638,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
drawPixel(screenX, screenY, black);
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
drawPixel(screenX, screenY, false);
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
} else if (renderMode == GRAYSCALE_LSB && bmpVal == (black ? 1 : 2)) {
drawPixel(screenX, screenY, false);
}
} else {
@ -822,8 +822,8 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
// Light gray (also mark the MSB if it's going to be a dark gray too)
// We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update
drawPixel(screenX, screenY, false);
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
// Dark gray
} else if (renderMode == GRAYSCALE_LSB && bmpVal == (pixelState ? 1 : 2)) {
// Dark gray (swap gray level for inverse display)
drawPixel(screenX, screenY, false);
}
} else {

View File

@ -22,7 +22,7 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) {
namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 23;
constexpr uint8_t SETTINGS_COUNT = 24;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace
@ -60,6 +60,7 @@ bool CrossPointSettings::saveToFile() const {
serialization::writeString(outputFile, std::string(opdsUsername));
serialization::writeString(outputFile, std::string(opdsPassword));
serialization::writePod(outputFile, sleepScreenCoverFilter);
serialization::writePod(outputFile, inverseDisplay);
// New fields added at end for backward compatibility
outputFile.close();
@ -148,6 +149,8 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, inverseDisplay);
if (++settingsRead >= fileSettingsCount) break;
// New fields added at end for backward compatibility
} while (false);

View File

@ -137,6 +137,8 @@ class CrossPointSettings {
uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press chapter skip on side buttons
uint8_t longPressChapterSkip = 1;
// Inverse display (white text on black background)
uint8_t inverseDisplay = 0;
~CrossPointSettings() = default;

View File

@ -520,7 +520,7 @@ void WifiSelectionActivity::renderNetworkList() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
renderer.drawCenteredText(UI_10_FONT_ID, top, "No networks found");
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press OK to scan again");
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press Connect to scan again");
} else {
// Calculate how many networks we can display
constexpr int startY = 60;

View File

@ -400,11 +400,12 @@ void EpubReaderActivity::renderScreen() {
}
}
renderer.clearScreen();
const bool inverse = SETTINGS.inverseDisplay;
renderer.clearScreen(inverse ? 0x00 : 0xFF);
if (section->pageCount == 0) {
Serial.printf("[%lu] [ERS] No pages to render\n", millis());
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", !inverse, EpdFontFamily::BOLD);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer();
return;
@ -412,7 +413,7 @@ void EpubReaderActivity::renderScreen() {
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount);
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", !inverse, EpdFontFamily::BOLD);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer();
return;
@ -453,7 +454,8 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
const int orientedMarginRight, const int orientedMarginBottom,
const int orientedMarginLeft) {
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
const bool inverse = SETTINGS.inverseDisplay;
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop, !inverse);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
@ -471,13 +473,13 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
if (SETTINGS.textAntiAliasing) {
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop, !inverse);
renderer.copyGrayscaleLsbBuffers();
// Render and copy to MSB buffer
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop, !inverse);
renderer.copyGrayscaleMsbBuffers();
// display grayscale part

View File

@ -393,7 +393,7 @@ void TxtReaderActivity::renderScreen() {
currentPageLines.clear();
loadPageAtOffset(offset, currentPageLines, nextOffset);
renderer.clearScreen();
renderer.clearScreen(SETTINGS.inverseDisplay ? 0x00 : 0xFF);
renderPage();
// Save progress
@ -412,6 +412,8 @@ void TxtReaderActivity::renderPage() {
const int lineHeight = renderer.getLineHeight(cachedFontId);
const int contentWidth = viewportWidth;
const bool inverse = SETTINGS.inverseDisplay;
// Render text lines with alignment
auto renderLines = [&]() {
int y = orientedMarginTop;
@ -441,7 +443,7 @@ void TxtReaderActivity::renderPage() {
break;
}
renderer.drawText(cachedFontId, x, y, line.c_str());
renderer.drawText(cachedFontId, x, y, line.c_str(), !inverse);
}
y += lineHeight;
}

View File

@ -24,7 +24,7 @@ const SettingInfo displaySettings[displaySettingsCount] = {
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};
constexpr int readerSettingsCount = 9;
constexpr int readerSettingsCount = 10;
const SettingInfo readerSettings[readerSettingsCount] = {
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}),
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
@ -36,7 +36,8 @@ const SettingInfo readerSettings[readerSettingsCount] = {
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing),
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)};
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing),
SettingInfo::Toggle("Inverse Display", &CrossPointSettings::inverseDisplay)};
constexpr int controlsSettingsCount = 4;
const SettingInfo controlsSettings[controlsSettingsCount] = {