Compare commits

...

15 Commits

Author SHA1 Message Date
Stanislav Khromov
d293185ab2
Merge 7089bf84c5 into 7185e5d287 2026-01-19 21:50:44 +10:00
Nathan James
7185e5d287
feat: Change keyboard "caps" to "shift" & Wrap Keyboard (#377)
## Summary

* This PR solves issue
https://github.com/crosspoint-reader/crosspoint-reader/issues/357 in the
first commit
* I then added an additional commit which means when you reach the end
of the keyboard, if you go 'beyond', you wrap back to the other side.
* This replaces existing behaviour, so if you would rather this be
removed, let me know and I'll just do the `caps` -> `shift` change

## Additional Context

### Screenshots for the new shift display

I thought it might not fit and need column size changes, but ended up
fitting fine, see screenshots showing this below:

<img width="573" height="366" alt="image"
src="https://github.com/user-attachments/assets/b8f6a4ec-94f5-4f5e-b9a6-06cc5f250ddb"
/>

<img width="570" height="308" alt="image"
src="https://github.com/user-attachments/assets/7d775518-4784-4120-a20a-a9dc67af8565"
/>


### Gif showing the wrap-around of the text



![IMG_7648](https://github.com/user-attachments/assets/7eec9066-e1cc-49a1-8b6b-a61556038d31)

---

### AI Usage

Did you use AI tools to help write this code? **PARTIALLY** - used to
double check the text wrapping had no edge-cases. (It did also suggest
rewriting the function, but I decided that was too big of a change for a
working part of the codebase, for now!)
2026-01-19 22:50:34 +11:00
Eunchurn Park
12940cc546
fix: XTC 1-bit thumb BMP polarity inversion (#373)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**

- Fix inverted colors in Continue Reading cover image for 1-bit XTC
files

## Additional Context

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

- Fix `grayValue = pixelBit ? 0 : 255` → `grayValue = pixelBit ? 255 :
0` in `lib/Xtc/Xtc.cpp`
- The thumb BMP generation had inverted polarity compared to cover BMP
generation
- bit=0 should be black, bit=1 should be white (matching the BMP palette
order)
- Update misleading comment about XTC polarity

---

### 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**_
2026-01-19 22:41:48 +11:00
Stanislav Khromov
7089bf84c5 format 2026-01-15 01:13:52 +01:00
Stanislav Khromov
c2f08a7422 Update FormatSDCardActivity.cpp 2026-01-15 01:08:58 +01:00
Stanislav Khromov
80b34bb395 Update open-x4-sdk 2026-01-15 01:00:15 +01:00
Stanislav Khromov
bd3dc2ecd2 Update main.cpp 2026-01-15 00:54:33 +01:00
Stanislav Khromov
306b38e356 Update main.cpp 2026-01-15 00:54:20 +01:00
Stanislav Khromov
1cb59eba90 Update FormatSDCardActivity.cpp 2026-01-15 00:47:23 +01:00
Stanislav Khromov
6a7b42cb03 Update main.cpp 2026-01-15 00:26:57 +01:00
Stanislav Khromov
f30b47ec69 Update main.cpp 2026-01-15 00:25:48 +01:00
Stanislav Khromov
8aecf5b73a Update SettingsActivity.cpp 2026-01-15 00:09:00 +01:00
Stanislav Khromov
1f8f4d8735 Merge remote-tracking branch 'upstream/master' into format-sd-card 2026-01-15 00:07:18 +01:00
Stanislav Khromov
3b5aadfc8a Update SettingsActivity.cpp 2026-01-15 00:06:55 +01:00
Stanislav Khromov
f4ced6ac7b feat: format sd card screen 2026-01-14 01:36:37 +01:00
7 changed files with 214 additions and 20 deletions

View File

@ -203,7 +203,7 @@ bool Xtc::generateCoverBmp() const {
coverBmp.write(reinterpret_cast<const uint8_t*>(&colorsImportant), 4);
// Color palette (2 colors for 1-bit)
// XTC uses inverted polarity: 0 = black, 1 = white
// XTC 1-bit polarity: 0 = black, 1 = white (standard BMP palette order)
// Color 0: Black (text/foreground in XTC)
uint8_t black[4] = {0x00, 0x00, 0x00, 0x00};
coverBmp.write(black, 4);
@ -506,8 +506,8 @@ bool Xtc::generateThumbBmp() const {
// Bounds check for buffer access
if (byteIdx < bitmapSize) {
const uint8_t pixelBit = (pageBuffer[byteIdx] >> bitIdx) & 1;
// XTC polarity: 1=black, 0=white
grayValue = pixelBit ? 0 : 255;
// XTC 1-bit polarity: 0=black, 1=white (same as BMP palette)
grayValue = pixelBit ? 255 : 0;
}
}

@ -1 +1 @@
Subproject commit bd4e6707503ab9c97d13ee0d8f8c69e9ff03cd12
Subproject commit 82e6f846bc427c130a3ca9ecf0b4d27214699d4e

View File

@ -0,0 +1,134 @@
#include "FormatSDCardActivity.h"
#include <GfxRenderer.h>
#include <SDCardManager.h>
#include <esp_system.h>
#include "MappedInputManager.h"
#include "fontIds.h"
void FormatSDCardActivity::taskTrampoline(void* param) {
auto* self = static_cast<FormatSDCardActivity*>(param);
self->displayTaskLoop();
}
void FormatSDCardActivity::onEnter() {
ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
updateRequired = true;
xTaskCreate(&FormatSDCardActivity::taskTrampoline, "FormatSDCardTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void FormatSDCardActivity::onExit() {
ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void FormatSDCardActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void FormatSDCardActivity::render() {
if (subActivity) return;
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Format SD Card", true, EpdFontFamily::BOLD);
if (state == WAITING_CONFIRMATION) {
renderer.drawCenteredText(UI_10_FONT_ID, 150, "WARNING!", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 200, "This will ERASE ALL DATA");
renderer.drawCenteredText(UI_10_FONT_ID, 230, "on the SD card including:");
renderer.drawCenteredText(UI_10_FONT_ID, 270, "- All books and documents");
renderer.drawCenteredText(UI_10_FONT_ID, 300, "- Reading progress");
renderer.drawCenteredText(UI_10_FONT_ID, 330, "- Cached data");
renderer.drawCenteredText(UI_10_FONT_ID, 380, "This action CANNOT be undone.");
const auto labels = mappedInput.mapLabels("Cancel", "FORMAT", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} else if (state == FORMATTING) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Formatting...", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 350, "Please wait, do not power off");
} else if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Format Complete!", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 330, "SD card has been formatted.");
renderer.drawCenteredText(UI_10_FONT_ID, 380, "Device will restart...");
} else if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Format Failed", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 350, "Please try again or check SD card.");
const auto labels = mappedInput.mapLabels("« Back", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}
renderer.displayBuffer();
}
void FormatSDCardActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
if (state == WAITING_CONFIRMATION) {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
state = FORMATTING;
render(); // Render synchronously to show "Formatting..." before we start
xSemaphoreGive(renderingMutex);
performFormat();
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
goBack();
}
return;
}
if (state == FAILED) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
goBack();
}
return;
}
if (state == SUCCESS) {
// Auto-restart after brief delay
vTaskDelay(2000 / portTICK_PERIOD_MS);
esp_restart();
}
}
void FormatSDCardActivity::performFormat() {
Serial.printf("[%lu] [FORMAT] Starting SD card format...\n", millis());
// Call the format method on SDCardManager
bool success = SdMan.format(&Serial);
xSemaphoreTake(renderingMutex, portMAX_DELAY);
state = success ? SUCCESS : FAILED;
xSemaphoreGive(renderingMutex);
updateRequired = true;
Serial.printf("[%lu] [FORMAT] Format %s\n", millis(), success ? "succeeded" : "failed");
}

View File

@ -0,0 +1,32 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include "activities/ActivityWithSubactivity.h"
class FormatSDCardActivity final : public ActivityWithSubactivity {
enum State { WAITING_CONFIRMATION, FORMATTING, SUCCESS, FAILED };
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
const std::function<void()> goBack;
State state = WAITING_CONFIRMATION;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render();
void performFormat();
public:
explicit FormatSDCardActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& goBack)
: ActivityWithSubactivity("FormatSDCard", renderer, mappedInput), goBack(goBack) {}
void onEnter() override;
void onExit() override;
void loop() override;
bool preventAutoSleep() override { return state == FORMATTING; }
};

View File

@ -7,13 +7,14 @@
#include "CalibreSettingsActivity.h"
#include "CrossPointSettings.h"
#include "FormatSDCardActivity.h"
#include "MappedInputManager.h"
#include "OtaUpdateActivity.h"
#include "fontIds.h"
// Define the static settings list
namespace {
constexpr int settingsCount = 20;
constexpr int settingsCount = 21;
const SettingInfo settingsList[settingsCount] = {
// Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
@ -42,7 +43,8 @@ const SettingInfo settingsList[settingsCount] = {
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
SettingInfo::Action("Calibre Settings"),
SettingInfo::Action("Check for updates")};
SettingInfo::Action("Check for updates"),
SettingInfo::Action("Format SD Card")};
} // namespace
void SettingsActivity::taskTrampoline(void* param) {
@ -155,6 +157,14 @@ void SettingsActivity::toggleCurrentSetting() {
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
} else if (strcmp(setting.name, "Format SD Card") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new FormatSDCardActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
}
} else {
// Only toggle if it's a toggle type and has a value pointer

View File

@ -73,7 +73,7 @@ int KeyboardEntryActivity::getRowLength(const int row) const {
case 3:
return 10; // zxcvbnm,./
case 4:
return 10; // caps (2 wide), space (5 wide), backspace (2 wide), OK
return 10; // shift (2 wide), space (5 wide), backspace (2 wide), OK
default:
return 0;
}
@ -145,6 +145,11 @@ void KeyboardEntryActivity::loop() {
// Clamp column to valid range for new row
const int maxCol = getRowLength(selectedRow) - 1;
if (selectedCol > maxCol) selectedCol = maxCol;
} else {
// Wrap to bottom row
selectedRow = NUM_ROWS - 1;
const int maxCol = getRowLength(selectedRow) - 1;
if (selectedCol > maxCol) selectedCol = maxCol;
}
updateRequired = true;
}
@ -154,16 +159,24 @@ void KeyboardEntryActivity::loop() {
selectedRow++;
const int maxCol = getRowLength(selectedRow) - 1;
if (selectedCol > maxCol) selectedCol = maxCol;
} else {
// Wrap to top row
selectedRow = 0;
const int maxCol = getRowLength(selectedRow) - 1;
if (selectedCol > maxCol) selectedCol = maxCol;
}
updateRequired = true;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
const int maxCol = getRowLength(selectedRow) - 1;
// Special bottom row case
if (selectedRow == SPECIAL_ROW) {
// Bottom row has special key widths
if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) {
// In shift key, do nothing
// In shift key, wrap to end of row
selectedCol = maxCol;
} else if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) {
// In space bar, move to shift
selectedCol = SHIFT_COL;
@ -180,10 +193,9 @@ void KeyboardEntryActivity::loop() {
if (selectedCol > 0) {
selectedCol--;
} else if (selectedRow > 0) {
// Wrap to previous row
selectedRow--;
selectedCol = getRowLength(selectedRow) - 1;
} else {
// Wrap to end of current row
selectedCol = maxCol;
}
updateRequired = true;
}
@ -204,7 +216,8 @@ void KeyboardEntryActivity::loop() {
// In backspace, move to done
selectedCol = DONE_COL;
} else if (selectedCol >= DONE_COL) {
// At done button, do nothing
// At done button, wrap to beginning of row
selectedCol = SHIFT_COL;
}
updateRequired = true;
return;
@ -212,9 +225,8 @@ void KeyboardEntryActivity::loop() {
if (selectedCol < maxCol) {
selectedCol++;
} else if (selectedRow < NUM_ROWS - 1) {
// Wrap to next row
selectedRow++;
} else {
// Wrap to beginning of current row
selectedCol = 0;
}
updateRequired = true;
@ -288,14 +300,14 @@ void KeyboardEntryActivity::render() const {
// Handle bottom row (row 4) specially with proper multi-column keys
if (row == 4) {
// Bottom row layout: CAPS (2 cols) | SPACE (5 cols) | <- (2 cols) | OK (2 cols)
// Bottom row layout: SHIFT (2 cols) | SPACE (5 cols) | <- (2 cols) | OK (2 cols)
// Total: 11 visual columns, but we use logical positions for selection
int currentX = startX;
// CAPS key (logical col 0, spans 2 key widths)
const bool capsSelected = (selectedRow == 4 && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL);
renderItemWithSelector(currentX + 2, rowY, shiftActive ? "CAPS" : "caps", capsSelected);
// SHIFT key (logical col 0, spans 2 key widths)
const bool shiftSelected = (selectedRow == 4 && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL);
renderItemWithSelector(currentX + 2, rowY, shiftActive ? "SHIFT" : "shift", shiftSelected);
currentX += 2 * (keyWidth + keySpacing);
// Space bar (logical cols 2-6, spans 5 key widths)

View File

@ -6,6 +6,7 @@
#include <SDCardManager.h>
#include <SPI.h>
#include <builtinFonts/all.h>
#include <esp_system.h>
#include <cstring>
@ -150,6 +151,11 @@ void enterNewActivity(Activity* activity) {
// Verify long press on wake-up from deep sleep
void verifyWakeupLongPress() {
// Skip verification for software resets (for example when calling esp_restart after sd card format)
if (esp_reset_reason() == ESP_RST_SW) {
return;
}
// Give the user up to 1000ms to start holding the power button, and must hold for SETTINGS.getPowerButtonDuration()
const auto start = millis();
bool abort = false;