Compare commits
3 Commits
2aba348070
...
6cf212d12a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cf212d12a
|
||
|
|
4851016c47
|
||
|
|
630fb56a11
|
26
chat-summaries/2026-03-09_04-30-summary.md
Normal file
26
chat-summaries/2026-03-09_04-30-summary.md
Normal 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.
|
||||
27
chat-summaries/2026-03-09_05-00-summary.md
Normal file
27
chat-summaries/2026-03-09_05-00-summary.md
Normal 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.
|
||||
28
chat-summaries/2026-03-09_06-00-summary.md
Normal file
28
chat-summaries/2026-03-09_06-00-summary.md
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user