Compare commits

...

11 Commits

Author SHA1 Message Date
Dave Allie
06f13d86de
Merge 551e57295f into 21277e03eb 2026-01-15 08:53:33 -05:00
Luke Stein
21277e03eb
docs: Update User Guide to reflect release 0.14.0 (#376)
Some checks failed
CI / build (push) Has been cancelled
2026-01-15 23:27:17 +11:00
Luke Stein
4eef2b5793
feat: Add MAC address display to WiFi Networks screen (#381)
## Summary

* Implements #380, allowing the user to see the device's MAC address in
order to register on wifi networks

## Additional Context

* Although @markatlnk suggested showing on the settings screen, I
implemented display at the bottom of the WiFi Networks selection screen
(accessed via "File Transfer" > "Join a Network") since I think it makes
more sense there.
* Tested on my own device


![IMG_2873](https://github.com/user-attachments/assets/b82a20dc-41a0-4b21-81f1-20876aa2c6b0)


---

### 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? _**YES**_

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-01-15 23:26:39 +11:00
Nathan James
5a55fa1c6e
fix: also apply longPressChapterSkip setting to xtc reader (#378)
## Summary

* This builds upon the helpful PR
https://github.com/crosspoint-reader/crosspoint-reader/pull/341, and
adds support for the setting to also apply to the XTC reader, which I
believed has just been missed and was not intentionally left out.
* XTC does not have chapter support yet, but it does skip 10 pages when
long-pressed, and so I think this is useful.

---

### AI Usage

Did you use AI tools to help write this code? No
2026-01-15 23:25:18 +11:00
Maeve Andrews
c98ba142e8
fix: draw button hints correctly if orientation is not portrait (#363)
~~Quick~~ fix for
https://github.com/daveallie/crosspoint-reader/issues/362

(this just applies to the chapter selection menu:)

~~If the orientation is portrait, hints as we know them make sense to
draw. If the orientation is inverted, we'd have to change the order of
the labels (along with everything's position), and if it's one of the
landscape choices, we'd have to render the text and buttons vertically.
All those other cases will be more complicated.~~

~~Punt on this for now by only rendering if portrait.~~

Update: this now draws the hints at the physical button position no
matter what the orientation is, by temporarily changing orientation to
portrait.

---------

Co-authored-by: Maeve Andrews <maeve@git.mail.maeveandrews.com>
2026-01-15 23:23:36 +11:00
Jonas Diemer
c1c94c0112
Feature: Show img alt text (#168)
Let's start small by showing the ALT text of IMG. This is rudimentary,
but avoids those otherwise completely blank chapters.

I feel we will need this even when we can render images if that
rendering takes >1s - I would then prefer rendering optional and showing
the ALT text first.
2026-01-15 23:21:46 +11:00
Dave Allie
eb84bcee7c
chore: Pin links2004/WebSockets version 2026-01-15 23:15:30 +11:00
efenner
d45f355e87
feat: Add EPUB table omitted placeholder (#372)
## Summary

* **What is the goal of this PR?**: Fix the bug I reported in
https://github.com/daveallie/crosspoint-reader/issues/292
* **What changes are included?**: Instead of silently dropping table
content in EPUBs., replace with an italicized '[Table omitted]' message
where tables appear.

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

---

### 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? _**PARTIALLY **_

---------

Co-authored-by: Evan Fenner <evan@evanfenner.com>
Co-authored-by: Warp <agent@warp.dev>
2026-01-15 23:14:59 +11:00
Dave Allie
551e57295f
Alloc/free inflator once per drawText call 2026-01-05 23:13:17 +11:00
Dave Allie
c9cfedf3d6
Do not include ZLIB header or checksum bytes in compressed glyphs 2026-01-05 22:58:42 +11:00
Dave Allie
cb3e08e73c
Basic glyph compression 2026-01-05 22:54:09 +11:00
64 changed files with 198701 additions and 249545 deletions

View File

@ -96,6 +96,10 @@ The Settings screen allows you to configure the device's behavior. There are a f
- Left, Right, Back, Confirm - Left, Right, Back, Confirm
- Left, Back, Confirm, Right - Left, Back, Confirm, Right
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading. - **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter.
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
- "Page Scroll" - Long-pressing scrolls a page up/down
- Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Reader Font Family**: Choose the font used for reading: - **Reader Font Family**: Choose the font used for reading:
- "Bookerly" (default) - Amazon's reading font - "Bookerly" (default) - Amazon's reading font
- "Noto Sans" - Google's sans-serif font - "Noto Sans" - Google's sans-serif font
@ -144,6 +148,9 @@ If the **Short Power Button Click** setting is set to "Page Turn", you can also
* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release. * **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release.
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release. * **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
This feature can be disabled in **[Settings](#35-settings)** to help avoid changing chapters by mistake.
### System Navigation ### System Navigation
* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen. * **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen.
* **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen. * **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.

View File

@ -5,6 +5,7 @@
#include <cstdint> #include <cstdint>
/// Font data stored PER GLYPH /// Font data stored PER GLYPH
#pragma pack(push, 1)
typedef struct { typedef struct {
uint8_t width; ///< Bitmap dimensions in pixels uint8_t width; ///< Bitmap dimensions in pixels
uint8_t height; ///< Bitmap dimensions in pixels uint8_t height; ///< Bitmap dimensions in pixels
@ -13,7 +14,9 @@ typedef struct {
int16_t top; ///< Y dist from cursor pos to UL corner int16_t top; ///< Y dist from cursor pos to UL corner
uint16_t dataLength; ///< Size of the font data. uint16_t dataLength; ///< Size of the font data.
uint32_t dataOffset; ///< Pointer into EpdFont->bitmap uint32_t dataOffset; ///< Pointer into EpdFont->bitmap
bool compressed;
} EpdGlyph; } EpdGlyph;
#pragma pack(pop)
/// Glyph interval structure /// Glyph interval structure
typedef struct { typedef struct {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@ parser.add_argument("--2bit", dest="is2Bit", action="store_true", help="generate
parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.") parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.")
args = parser.parse_args() args = parser.parse_args()
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"]) GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "compressed", "code_point"])
font_stack = [freetype.Face(f) for f in args.fontstack] font_stack = [freetype.Face(f) for f in args.fontstack]
is2Bit = args.is2Bit is2Bit = args.is2Bit
@ -124,7 +124,6 @@ def load_glyph(code_point):
face.load_glyph(glyph_index, freetype.FT_LOAD_RENDER) face.load_glyph(glyph_index, freetype.FT_LOAD_RENDER)
return face return face
face_index += 1 face_index += 1
print(f"code point {code_point} ({hex(code_point)}) not found in font stack!", file=sys.stderr)
return None return None
unmerged_intervals = sorted(intervals + add_ints) unmerged_intervals = sorted(intervals + add_ints)
@ -242,6 +241,13 @@ for i_start, i_end in intervals:
# Build output data # Build output data
packed = bytes(pixels) packed = bytes(pixels)
# DEFLATE compressed data without zlib header/footer
compressed = zlib.compress(packed, wbits=-15)
is_compressed = len(compressed) < len(packed) * 0.9
# Use compressed data only if it's at least 10% smaller
if is_compressed:
packed = compressed
glyph = GlyphProps( glyph = GlyphProps(
width = bitmap.width, width = bitmap.width,
height = bitmap.rows, height = bitmap.rows,
@ -250,6 +256,7 @@ for i_start, i_end in intervals:
top = face.glyph.bitmap_top, top = face.glyph.bitmap_top,
data_length = len(packed), data_length = len(packed),
data_offset = total_size, data_offset = total_size,
compressed = 'true' if is_compressed else 'false',
code_point = code_point, code_point = code_point,
) )
total_size += len(packed) total_size += len(packed)

View File

@ -25,7 +25,7 @@ constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]);
const char* IMAGE_TAGS[] = {"img"}; const char* IMAGE_TAGS[] = {"img"};
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]); constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
const char* SKIP_TAGS[] = {"head", "table"}; const char* SKIP_TAGS[] = {"head"};
constexpr int NUM_SKIP_TAGS = sizeof(SKIP_TAGS) / sizeof(SKIP_TAGS[0]); constexpr int NUM_SKIP_TAGS = sizeof(SKIP_TAGS) / sizeof(SKIP_TAGS[0]);
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; } bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
@ -63,13 +63,44 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
return; return;
} }
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) { // Special handling for tables - show placeholder text instead of dropping silently
// TODO: Start processing image tags if (strcmp(name, "table") == 0) {
// Add placeholder text
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
if (self->currentTextBlock) {
self->currentTextBlock->addWord("[Table omitted]", EpdFontFamily::ITALIC);
}
// Skip table contents
self->skipUntilDepth = self->depth; self->skipUntilDepth = self->depth;
self->depth += 1; self->depth += 1;
return; return;
} }
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
// TODO: Start processing image tags
std::string alt;
if (atts != nullptr) {
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "alt") == 0) {
alt = "[Image: " + std::string(atts[i + 1]) + "]";
}
}
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
self->depth += 1;
self->characterData(userData, alt.c_str(), alt.length());
} else {
// Skip for now
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
}
}
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) { if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
// start skip // start skip
self->skipUntilDepth = self->depth; self->skipUntilDepth = self->depth;

View File

@ -1,6 +1,31 @@
#include "GfxRenderer.h" #include "GfxRenderer.h"
#include <Utf8.h> #include <Utf8.h>
#include <miniz.h>
namespace {
tinfl_decompressor* inflator = nullptr;
bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) {
if (!inflator) {
Serial.printf("[%lu] [GFX] Inflator not initialized\n", millis());
return false;
}
size_t inBytes = deflatedSize;
size_t outBytes = inflatedSize;
tinfl_init(inflator);
const tinfl_status status = tinfl_decompress(inflator, inputBuf, &inBytes, nullptr, outputBuf, &outBytes,
TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
if (status != TINFL_STATUS_DONE) {
Serial.printf("[%lu] [GFX] tinfl_decompress() failed with status %d\n", millis(), status);
return false;
}
return true;
}
} // namespace
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); } void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
@ -104,10 +129,21 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
return; return;
} }
// Setup inflator
inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
if (!inflator) {
Serial.printf("[%lu] [GFX] Failed to allocate memory for inflator\n", millis());
return;
}
memset(inflator, 0, sizeof(tinfl_decompressor));
uint32_t cp; uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) { while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
renderChar(font, cp, &xpos, &yPos, black, style); renderChar(font, cp, &xpos, &yPos, black, style);
} }
free(inflator);
inflator = nullptr;
} }
void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const { void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const {
@ -468,7 +504,10 @@ int GfxRenderer::getLineHeight(const int fontId) const {
} }
void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char* btn2, const char* btn3, void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char* btn2, const char* btn3,
const char* btn4) const { const char* btn4) {
const Orientation orig_orientation = getOrientation();
setOrientation(Orientation::Portrait);
const int pageHeight = getScreenHeight(); const int pageHeight = getScreenHeight();
constexpr int buttonWidth = 106; constexpr int buttonWidth = 106;
constexpr int buttonHeight = 40; constexpr int buttonHeight = 40;
@ -481,12 +520,15 @@ void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char
// Only draw if the label is non-empty // Only draw if the label is non-empty
if (labels[i] != nullptr && labels[i][0] != '\0') { if (labels[i] != nullptr && labels[i][0] != '\0') {
const int x = buttonPositions[i]; const int x = buttonPositions[i];
fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false);
drawRect(x, pageHeight - buttonY, buttonWidth, buttonHeight); drawRect(x, pageHeight - buttonY, buttonWidth, buttonHeight);
const int textWidth = getTextWidth(fontId, labels[i]); const int textWidth = getTextWidth(fontId, labels[i]);
const int textX = x + (buttonWidth - 1 - textWidth) / 2; const int textX = x + (buttonWidth - 1 - textWidth) / 2;
drawText(fontId, textX, pageHeight - buttonY + textYOffset, labels[i]); drawText(fontId, textX, pageHeight - buttonY + textYOffset, labels[i]);
} }
} }
setOrientation(orig_orientation);
} }
void GfxRenderer::drawSideButtonHints(const int fontId, const char* topBtn, const char* bottomBtn) const { void GfxRenderer::drawSideButtonHints(const int fontId, const char* topBtn, const char* bottomBtn) const {
@ -765,15 +807,42 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
} }
const int is2Bit = fontFamily.getData(style)->is2Bit; const int is2Bit = fontFamily.getData(style)->is2Bit;
const uint32_t offset = glyph->dataOffset; // const uint32_t offset = glyph->dataOffset;
// const uint32_t dataLength = glyph->dataLength;
const uint8_t width = glyph->width; const uint8_t width = glyph->width;
const uint8_t height = glyph->height; const uint8_t height = glyph->height;
const int left = glyph->left; const int left = glyph->left;
const size_t outputDataSize = is2Bit ? ((width * height + 3) / 4) : ((width * height + 7) / 8);
const uint8_t* bitmap = nullptr; if (outputDataSize != glyph->dataLength && !glyph->compressed) {
bitmap = &fontFamily.getData(style)->bitmap[offset]; Serial.printf("[%lu] [GFX] Glyph bitmap size mismatch for codepoint %d (expected %zu, got %d)\n", millis(), cp,
outputDataSize, glyph->dataLength);
}
uint8_t* bitmapData = nullptr;
const uint8_t* bitmap;
if (glyph->compressed) {
bitmapData = static_cast<uint8_t*>(malloc(outputDataSize));
if (!bitmapData) {
Serial.printf("[%lu] [GFX] Failed to allocate memory for glyph bitmap for codepoint %d\n", millis(), cp);
return;
}
const auto success = inflateOneShot(&fontFamily.getData(style)->bitmap[glyph->dataOffset], glyph->dataLength,
bitmapData, outputDataSize);
if (!success) {
Serial.printf("[%lu] [GFX] Failed to inflate glyph bitmap for codepoint %d\n", millis(), cp);
free(bitmapData);
*x += glyph->advanceX;
return;
}
bitmap = bitmapData;
} else {
bitmap = &fontFamily.getData(style)->bitmap[glyph->dataOffset];
}
if (bitmap != nullptr) {
for (int glyphY = 0; glyphY < height; glyphY++) { for (int glyphY = 0; glyphY < height; glyphY++) {
const int screenY = *y - glyph->top + glyphY; const int screenY = *y - glyph->top + glyphY;
for (int glyphX = 0; glyphX < width; glyphX++) { for (int glyphX = 0; glyphX < width; glyphX++) {
@ -809,6 +878,9 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
} }
} }
} }
if (bitmapData) {
free(bitmapData);
} }
*x += glyph->advanceX; *x += glyph->advanceX;

View File

@ -84,7 +84,7 @@ class GfxRenderer {
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
// UI Components // UI Components
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4) const; void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4);
void drawSideButtonHints(int fontId, const char* topBtn, const char* bottomBtn) const; void drawSideButtonHints(int fontId, const char* topBtn, const char* bottomBtn) const;
private: private:

View File

@ -4,6 +4,7 @@
#include <SDCardManager.h> #include <SDCardManager.h>
#include <miniz.h> #include <miniz.h>
namespace {
bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) { bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) {
// Setup inflator // Setup inflator
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor))); const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
@ -27,6 +28,7 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
return true; return true;
} }
} // namespace
bool ZipFile::loadAllFileStatSlims() { bool ZipFile::loadAllFileStatSlims() {
const bool wasOpen = isOpen(); const bool wasOpen = isOpen();

View File

@ -45,9 +45,9 @@ lib_deps =
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager
ArduinoJson @ 7.4.2 bblanchon/ArduinoJson @ 7.4.2
QRCode @ 0.0.1 ricmoo/QRCode @ 0.0.1
links2004/WebSockets @ ^2.4.1 links2004/WebSockets @ 2.7.3
[env:default] [env:default]
extends = base extends = base

View File

@ -37,6 +37,14 @@ void WifiSelectionActivity::onEnter() {
savePromptSelection = 0; savePromptSelection = 0;
forgetPromptSelection = 0; forgetPromptSelection = 0;
// Cache MAC address for display
uint8_t mac[6];
WiFi.macAddress(mac);
char macStr[32];
snprintf(macStr, sizeof(macStr), "MAC address: %02x-%02x-%02x-%02x-%02x-%02x", mac[0], mac[1], mac[2], mac[3], mac[4],
mac[5]);
cachedMacAddress = std::string(macStr);
// Trigger first update to show scanning message // Trigger first update to show scanning message
updateRequired = true; updateRequired = true;
@ -572,6 +580,9 @@ void WifiSelectionActivity::renderNetworkList() const {
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr); renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr);
} }
// Show MAC address above the network count and legend
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str());
// Draw help text // Draw help text
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved"); renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved");
const auto labels = mappedInput.mapLabels("« Back", "Connect", "", ""); const auto labels = mappedInput.mapLabels("« Back", "Connect", "", "");

View File

@ -62,6 +62,9 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
// Password to potentially save (from keyboard or saved credentials) // Password to potentially save (from keyboard or saved credentials)
std::string enteredPassword; std::string enteredPassword;
// Cached MAC address string for display
std::string cachedMacAddress;
// Whether network was connected using a saved password (skip save prompt) // Whether network was connected using a saved password (skip save prompt)
bool usedSavedPassword = false; bool usedSavedPassword = false;

View File

@ -127,7 +127,7 @@ void XtcReaderActivity::loop() {
return; return;
} }
const bool skipPages = mappedInput.getHeldTime() > skipPageMs; const bool skipPages = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipPageMs;
const int skipAmount = skipPages ? 10 : 1; const int skipAmount = skipPages ? 10 : 1;
if (prevReleased) { if (prevReleased) {