3 Commits

Author SHA1 Message Date
cottongin
6cf212d12a fix: prevent idle freeze by hardening NTP and clock power management
BootNtpSync now acquires a HalPowerManager::Lock for the entire WiFi/NTP
task lifecycle, keeping the CPU at full speed during scan, connect, sync,
and teardown. The clock refresh logic in the main loop now explicitly
restores CPU frequency and resets the activity timer before requesting a
render, preventing display SPI operations from running at 10 MHz.

Made-with: Cursor
2026-03-09 05:19:48 -04:00
cottongin
4851016c47 fix: adjust BookInfo landscape layout for side button hint gutters
In landscape CW/CCW the physical buttons are on the left/right side,
not the bottom. Reserve a horizontal gutter (sideButtonHintsWidth)
on the appropriate side and remove the bottom buttonHintsHeight
padding, following the established pattern from other activities.

Made-with: Cursor
2026-03-09 04:28:26 -04:00
cottongin
630fb56a11 feat: side-by-side cover layout for BookInfo in landscape
In landscape orientation the cover is pinned on the left panel
(filling the content height) while metadata fields scroll
independently on the right. Portrait layout is unchanged.

Made-with: Cursor
2026-03-09 04:00:41 -04:00
7 changed files with 154 additions and 24 deletions

View File

@@ -0,0 +1,26 @@
# BookInfo: Landscape Side-by-Side Layout
**Date**: 2026-03-09
**Task**: Implement a side-by-side layout for BookInfo in landscape orientation -- cover fixed on the left, scrollable metadata fields on the right. Portrait layout unchanged.
## Changes Made
### `src/activities/home/BookInfoActivity.h`
- Added `bool isLandscape` and `int coverPanelWidth` member variables.
### `src/activities/home/BookInfoActivity.cpp`
- **`onEnter()`**: Detects orientation via `renderer.getOrientation()`. In landscape, the cover thumbnail height fills the full content area (between header and button hints) instead of 2/5 of screen height.
- **`buildLayout()`**: Reads cover bitmap dimensions first. In landscape with a cover, computes `coverPanelWidth` (cover width + padding, capped at 40% of screen). Text fields are wrapped to the narrower right-panel width. Cover height is excluded from `contentHeight` (it sits beside, not above, the fields).
- **`render()`**: In landscape, draws the cover at a fixed position in the left panel (unaffected by scroll). Fields render in the right panel at `x = coverPanelWidth`. In portrait, existing behavior is preserved (cover above fields, both scroll together). The header fill + draw-on-top pattern continues to prevent content bleeding into the header zone.
## Behavior Summary
- **Portrait** (480x800): Cover on top, fields below, everything scrolls vertically (no change).
- **Landscape** (800x480): Cover pinned on left (centered vertically, fills content height), metadata fields scroll independently on the right.
- If no cover exists in landscape, the full screen width is used for fields (same as portrait but in landscape dimensions).
## Follow-up
None -- ready for hardware testing in all 4 orientations.

View File

@@ -0,0 +1,27 @@
# BookInfo: Landscape Button Hint Gutter Fix
**Date**: 2026-03-09
**Task**: Adjust BookInfo's content area and header placement in landscape orientations to account for button hints appearing on a side instead of the bottom.
## Problem
In landscape orientations the physical front buttons end up on a side of the screen (CW = left, CCW = right). BookInfo was reserving `buttonHintsHeight` at the bottom in all orientations, wasting vertical space in landscape and not accounting for the side gutter needed to avoid overlapping the side-drawn button hints.
## Changes Made
### `src/activities/home/BookInfoActivity.cpp`
- **`buildLayout()`**: Added `hintGutterWidth` computation (`metrics.sideButtonHintsWidth` in landscape, 0 in portrait). Text wrapping width (`contentW`) now subtracts `hintGutterWidth`. Bottom padding for `contentHeight` uses only `verticalSpacing` in landscape (no `buttonHintsHeight` since there are no bottom hints).
- **`render()`**: Added `isLandscapeCw`, `hintGutterWidth`, and `contentX` computations following the established codebase pattern. In CW, `contentX = hintGutterWidth` shifts all content right (hints on left). In CCW, `contentX = 0` and content width is reduced (hints on right). Cover X position offset by `contentX`. Field X offset by `contentX`. `contentBottom` no longer subtracts `buttonHintsHeight` in landscape. Header `fillRect` and `drawHeader` Rect both adjusted by `contentX` and `hintGutterWidth`.
## Pattern Followed
The established pattern from `LookedUpWordsActivity`, `DictionarySuggestionsActivity`, `EpubReaderMenuActivity`, and others:
```cpp
const int hintGutterWidth = isLandscape ? metrics.sideButtonHintsWidth : 0;
const int contentX = isLandscapeCw ? hintGutterWidth : 0;
```
## Follow-up
Ready for hardware testing in all 4 orientations.

View File

@@ -0,0 +1,28 @@
# Fix Idle Freeze: NTP Power Lock and Clock Refresh Hardening
**Date**: 2026-03-09
## Task Description
Fix device freeze during idle, where the device stops responding to button presses after idling on the Home screen and requires a hard reset. Root cause was the mod-specific clock refresh logic in the main loop triggering a display render while the CPU is at reduced frequency (10 MHz low-power mode), combined with the background BootNtpSync task running WiFi/NTP operations without holding a power lock.
## Root Cause
The freeze coincides with the clock minute boundary. The mod's clock refresh code detects the minute change and calls `activityManager.requestUpdate()`, triggering a Home screen render while the CPU is at 10 MHz. SPI display operations at reduced APB frequency can deadlock the display communication. Additionally, `BootNtpSync` runs WiFi/NTP on a background task with no power lock, risking instability during WiFi teardown when the main loop may enter low-power mode.
## Changes Made
### 1. `src/util/BootNtpSync.cpp`
- Added `#include <HalPowerManager.h>`
- Added `HalPowerManager::Lock powerLock;` at the top of `taskFunc()` to keep the CPU at full speed for the entire duration of WiFi scanning, connection, NTP sync, and teardown
### 2. `src/main.cpp` (clock refresh block, ~lines 408-428)
- In the `sawInvalidTime` branch: added `lastActivityTime = millis()` and `powerManager.setPowerSaving(false)` before calling `requestUpdate()`
- In the minute-change branch: added `lastActivityTime = millis()` and `powerManager.setPowerSaving(false)` before calling `requestUpdate()`
- This ensures the CPU is restored to 160 MHz before any render-related code executes and prevents immediate re-entry into low-power mode
## Follow-up Items
- Verify on device that the freeze no longer occurs after idling for extended periods
- Monitor heap usage to confirm the power lock doesn't introduce memory issues
- Test that NTP sync still completes successfully with the power lock in place

View File

@@ -43,6 +43,10 @@ std::string normalizeWhitespace(const std::string& s) {
void BookInfoActivity::onEnter() {
Activity::onEnter();
const auto orient = renderer.getOrientation();
isLandscape = orient == GfxRenderer::Orientation::LandscapeClockwise ||
orient == GfxRenderer::Orientation::LandscapeCounterClockwise;
std::string fileName = filePath;
const size_t lastSlash = filePath.rfind('/');
if (lastSlash != std::string::npos) {
@@ -58,6 +62,11 @@ void BookInfoActivity::onEnter() {
}
}
const auto& metrics = UITheme::getInstance().getMetrics();
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentBottom = renderer.getScreenHeight() - metrics.buttonHintsHeight - metrics.verticalSpacing;
const int coverH = isLandscape ? (contentBottom - contentTop) : (renderer.getScreenHeight() * 2 / 5);
BookMetadataCache::BookMetadata meta;
if (FsHelpers::hasEpubExtension(fileName)) {
@@ -74,7 +83,6 @@ void BookInfoActivity::onEnter() {
meta = epub.getMetadata();
meta.description = normalizeWhitespace(meta.description);
const int coverH = renderer.getScreenHeight() * 2 / 5;
if (epub.generateThumbBmp(coverH)) {
coverBmpPath = epub.getThumbBmpPath(coverH);
} else {
@@ -103,7 +111,6 @@ void BookInfoActivity::onEnter() {
meta.title = xtc.getTitle();
meta.author = xtc.getAuthor();
const int coverH = renderer.getScreenHeight() * 2 / 5;
if (xtc.generateThumbBmp(coverH)) {
coverBmpPath = xtc.getThumbBmpPath(coverH);
} else {
@@ -132,8 +139,32 @@ void BookInfoActivity::onExit() { Activity::onExit(); }
void BookInfoActivity::buildLayout(const BookMetadataCache::BookMetadata& meta, size_t fileSize) {
const auto& metrics = UITheme::getInstance().getMetrics();
const int pageW = renderer.getScreenWidth();
const int sidePad = metrics.contentSidePadding;
const int contentW = renderer.getScreenWidth() - sidePad * 2;
const int hintGutterWidth = isLandscape ? metrics.sideButtonHintsWidth : 0;
if (!coverBmpPath.empty()) {
FsFile file;
if (Storage.openFileForRead("BIF", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
coverDisplayHeight = bitmap.getHeight();
coverDisplayWidth = bitmap.getWidth();
}
file.close();
}
}
coverPanelWidth = 0;
if (isLandscape && coverDisplayWidth > 0) {
coverPanelWidth = std::min(coverDisplayWidth + sidePad * 2, pageW * 2 / 5);
}
const int availW = pageW - hintGutterWidth;
const int contentW = (isLandscape && coverPanelWidth > 0)
? availW - coverPanelWidth - sidePad
: availW - sidePad * 2;
fields.reserve(13);
auto addField = [&](const char* label, const std::string& text, bool bold, EpdFontFamily::Style style) {
@@ -185,19 +216,8 @@ void BookInfoActivity::buildLayout(const BookMetadataCache::BookMetadata& meta,
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
int h = contentTop;
if (!coverBmpPath.empty()) {
FsFile file;
if (Storage.openFileForRead("BIF", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
coverDisplayHeight = bitmap.getHeight();
coverDisplayWidth = bitmap.getWidth();
}
file.close();
}
if (coverDisplayHeight > 0) {
h += coverDisplayHeight + COVER_GAP;
}
if (!isLandscape && coverDisplayHeight > 0 && !coverBmpPath.empty()) {
h += coverDisplayHeight + COVER_GAP;
}
for (const auto& field : fields) {
@@ -207,7 +227,7 @@ void BookInfoActivity::buildLayout(const BookMetadataCache::BookMetadata& meta,
h += static_cast<int>(field.lines.size()) * lineH12;
h += SECTION_GAP;
}
h += metrics.buttonHintsHeight + metrics.verticalSpacing;
h += isLandscape ? metrics.verticalSpacing : (metrics.buttonHintsHeight + metrics.verticalSpacing);
contentHeight = h;
}
@@ -252,12 +272,32 @@ void BookInfoActivity::render(RenderLock&&) {
const int sidePad = metrics.contentSidePadding;
const int lineH10 = renderer.getLineHeight(UI_10_FONT_ID);
const int lineH12 = renderer.getLineHeight(UI_12_FONT_ID);
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentBottom = pageH - metrics.buttonHintsHeight - metrics.verticalSpacing;
const bool isLandscapeCw = renderer.getOrientation() == GfxRenderer::Orientation::LandscapeClockwise;
const int hintGutterWidth = isLandscape ? metrics.sideButtonHintsWidth : 0;
const int contentX = (isLandscape && isLandscapeCw) ? hintGutterWidth : 0;
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentBottom = isLandscape ? pageH - metrics.verticalSpacing
: pageH - metrics.buttonHintsHeight - metrics.verticalSpacing;
if (isLandscape && coverPanelWidth > 0 && !coverBmpPath.empty() && coverDisplayHeight > 0) {
FsFile file;
if (Storage.openFileForRead("BIF", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
const int availH = contentBottom - contentTop;
const int coverX = contentX + (coverPanelWidth - coverDisplayWidth) / 2;
renderer.drawBitmap1Bit(bitmap, coverX, contentTop, coverDisplayWidth, availH);
}
file.close();
}
}
const int fieldX = (isLandscape && coverPanelWidth > 0) ? contentX + coverPanelWidth : contentX + sidePad;
int y = contentTop - scrollOffset;
if (!coverBmpPath.empty() && coverDisplayHeight > 0) {
if (!isLandscape && !coverBmpPath.empty() && coverDisplayHeight > 0) {
if (y + coverDisplayHeight > 0 && y < contentBottom) {
FsFile file;
if (Storage.openFileForRead("BIF", coverBmpPath, file)) {
@@ -277,7 +317,7 @@ void BookInfoActivity::render(RenderLock&&) {
if (field.label) {
if (y + lineH10 > contentTop && y < contentBottom) {
renderer.drawText(UI_10_FONT_ID, sidePad, y, field.label, true, EpdFontFamily::BOLD);
renderer.drawText(UI_10_FONT_ID, fieldX, y, field.label, true, EpdFontFamily::BOLD);
}
y += lineH10 + LABEL_VALUE_GAP;
}
@@ -286,15 +326,16 @@ void BookInfoActivity::render(RenderLock&&) {
for (const auto& line : field.lines) {
if (y >= contentBottom) break;
if (y + lineH12 > contentTop) {
renderer.drawText(UI_12_FONT_ID, sidePad, y, line.c_str(), true, style);
renderer.drawText(UI_12_FONT_ID, fieldX, y, line.c_str(), true, style);
}
y += lineH12;
}
y += SECTION_GAP;
}
renderer.fillRect(0, 0, pageW, contentTop, false);
GUI.drawHeader(renderer, Rect(0, metrics.topPadding, pageW, metrics.headerHeight), tr(STR_BOOK_INFO));
renderer.fillRect(contentX, 0, pageW - hintGutterWidth, contentTop, false);
GUI.drawHeader(renderer, Rect(contentX, metrics.topPadding, pageW - hintGutterWidth, metrics.headerHeight),
tr(STR_BOOK_INFO));
const bool canScrollDown = scrollOffset + pageH < contentHeight;
const bool canScrollUp = scrollOffset > 0;

View File

@@ -32,6 +32,8 @@ class BookInfoActivity final : public Activity {
int contentHeight = 0;
int coverDisplayHeight = 0;
int coverDisplayWidth = 0;
int coverPanelWidth = 0;
bool isLandscape = false;
void buildLayout(const BookMetadataCache::BookMetadata& meta, size_t fileSize);
static std::string formatFileSize(size_t bytes);

View File

@@ -416,9 +416,13 @@ void loop() {
if (lastRenderedMinute < 0) {
lastRenderedMinute = currentMinute;
if (sawInvalidTime) {
lastActivityTime = millis();
powerManager.setPowerSaving(false);
activityManager.requestUpdate();
}
} else if (currentMinute != lastRenderedMinute) {
lastActivityTime = millis();
powerManager.setPowerSaving(false);
activityManager.requestUpdate();
lastRenderedMinute = currentMinute;
}

View File

@@ -10,6 +10,7 @@
#include "CrossPointSettings.h"
#include "WifiCredentialStore.h"
#include <HalPowerManager.h>
#include "util/TimeSync.h"
namespace BootNtpSync {
@@ -93,6 +94,7 @@ static bool tryConnectToSavedNetwork(const TaskParams& params) {
}
static void taskFunc(void* param) {
HalPowerManager::Lock powerLock;
auto* params = static_cast<TaskParams*>(param);
bool connected = tryConnectToSavedNetwork(*params);