From 3899675103d09b03048705f30280b4f96433d5ec Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Mon, 2 Feb 2026 14:52:47 +0300 Subject: [PATCH 1/5] feat: front button remapper --- src/CrossPointSettings.cpp | 45 +++- src/CrossPointSettings.h | 19 +- src/MappedInputManager.cpp | 81 ++++--- src/MappedInputManager.h | 2 + .../settings/ButtonRemapActivity.cpp | 225 ++++++++++++++++++ src/activities/settings/ButtonRemapActivity.h | 49 ++++ .../settings/CategorySettingsActivity.cpp | 12 +- src/activities/settings/SettingsActivity.cpp | 15 +- 8 files changed, 402 insertions(+), 46 deletions(-) create mode 100644 src/activities/settings/ButtonRemapActivity.cpp create mode 100644 src/activities/settings/ButtonRemapActivity.h diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 232c7c57..ebdc12a8 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -22,10 +22,31 @@ 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 = 27; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; +// Validate front button mapping to ensure each hardware button is unique. +// If duplicates are detected, reset to the default physical order to prevent invalid mappings. +void validateFrontButtonMapping(CrossPointSettings& settings) { + // Snapshot the logical->hardware mapping so we can compare for duplicates. + const uint8_t mapping[] = {settings.frontButtonBack, settings.frontButtonConfirm, settings.frontButtonLeft, + settings.frontButtonRight}; + for (size_t i = 0; i < 4; i++) { + for (size_t j = i + 1; j < 4; j++) { + if (mapping[i] == mapping[j]) { + // Duplicate detected: restore the default physical order (Back, Confirm, Left, Right). + settings.frontButtonBack = CrossPointSettings::FRONT_HW_BACK; + settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM; + settings.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT; + settings.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT; + return; + } + } + } +} } // namespace + + bool CrossPointSettings::saveToFile() const { // Make sure the directory exists SdMan.mkdir("/.crosspoint"); @@ -42,7 +63,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, shortPwrBtn); serialization::writePod(outputFile, statusBar); serialization::writePod(outputFile, orientation); - serialization::writePod(outputFile, frontButtonLayout); + serialization::writePod(outputFile, frontButtonLayout); // legacy serialization::writePod(outputFile, sideButtonLayout); serialization::writePod(outputFile, fontFamily); serialization::writePod(outputFile, fontSize); @@ -60,6 +81,10 @@ bool CrossPointSettings::saveToFile() const { serialization::writeString(outputFile, std::string(opdsUsername)); serialization::writeString(outputFile, std::string(opdsPassword)); serialization::writePod(outputFile, sleepScreenCoverFilter); + serialization::writePod(outputFile, frontButtonBack); + serialization::writePod(outputFile, frontButtonConfirm); + serialization::writePod(outputFile, frontButtonLeft); + serialization::writePod(outputFile, frontButtonRight); // New fields added at end for backward compatibility outputFile.close(); @@ -86,6 +111,8 @@ bool CrossPointSettings::loadFromFile() { // load settings that exist (support older files with fewer fields) uint8_t settingsRead = 0; + // Track whether remap fields were present in the settings file. + bool frontButtonMappingRead = false; do { readAndValidate(inputFile, sleepScreen, SLEEP_SCREEN_MODE_COUNT); if (++settingsRead >= fileSettingsCount) break; @@ -97,7 +124,7 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, orientation, ORIENTATION_COUNT); if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT); + readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT); // legacy if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, sideButtonLayout, SIDE_BUTTON_LAYOUT_COUNT); if (++settingsRead >= fileSettingsCount) break; @@ -148,9 +175,21 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT); if (++settingsRead >= fileSettingsCount) break; + readAndValidate(inputFile, frontButtonBack, FRONT_BUTTON_HARDWARE_COUNT); + if (++settingsRead >= fileSettingsCount) break; + readAndValidate(inputFile, frontButtonConfirm, FRONT_BUTTON_HARDWARE_COUNT); + if (++settingsRead >= fileSettingsCount) break; + readAndValidate(inputFile, frontButtonLeft, FRONT_BUTTON_HARDWARE_COUNT); + if (++settingsRead >= fileSettingsCount) break; + readAndValidate(inputFile, frontButtonRight, FRONT_BUTTON_HARDWARE_COUNT); + frontButtonMappingRead = true; // New fields added at end for backward compatibility } while (false); + if (frontButtonMappingRead) { + validateFrontButtonMapping(*this); + } + inputFile.close(); Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis()); return true; diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index c450d348..10b7ba07 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -42,7 +42,7 @@ class CrossPointSettings { ORIENTATION_COUNT }; - // Front button layout options + // Front button layout options (legacy) // Default: Back, Confirm, Left, Right // Swapped: Left, Right, Back, Confirm enum FRONT_BUTTON_LAYOUT { @@ -53,6 +53,15 @@ class CrossPointSettings { FRONT_BUTTON_LAYOUT_COUNT }; + // Front button hardware identifiers (for remapping) + enum FRONT_BUTTON_HARDWARE { + FRONT_HW_BACK = 0, + FRONT_HW_CONFIRM = 1, + FRONT_HW_LEFT = 2, + FRONT_HW_RIGHT = 3, + FRONT_BUTTON_HARDWARE_COUNT + }; + // Side button layout options // Default: Previous, Next // Swapped: Next, Previous @@ -113,9 +122,15 @@ class CrossPointSettings { // EPUB reading orientation settings // 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise uint8_t orientation = PORTRAIT; - // Button layouts + // Button layouts (front layout retained for migration only) uint8_t frontButtonLayout = BACK_CONFIRM_LEFT_RIGHT; uint8_t sideButtonLayout = PREV_NEXT; + // Front button remap (logical -> hardware) + // Used by MappedInputManager to translate logical buttons into physical front buttons. + uint8_t frontButtonBack = FRONT_HW_BACK; + uint8_t frontButtonConfirm = FRONT_HW_CONFIRM; + uint8_t frontButtonLeft = FRONT_HW_LEFT; + uint8_t frontButtonRight = FRONT_HW_RIGHT; // Reader font settings uint8_t fontFamily = BOOKERLY; uint8_t fontSize = MEDIUM; diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index e5423724..467c0f75 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -5,26 +5,11 @@ namespace { using ButtonIndex = uint8_t; -struct FrontLayoutMap { - ButtonIndex back; - ButtonIndex confirm; - ButtonIndex left; - ButtonIndex right; -}; - struct SideLayoutMap { ButtonIndex pageBack; ButtonIndex pageForward; }; -// Order matches CrossPointSettings::FRONT_BUTTON_LAYOUT. -constexpr FrontLayoutMap kFrontLayouts[] = { - {HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT}, - {HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT, HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM}, - {HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_BACK, HalGPIO::BTN_RIGHT}, - {HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_RIGHT, HalGPIO::BTN_LEFT}, -}; - // Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT. constexpr SideLayoutMap kSideLayouts[] = { {HalGPIO::BTN_UP, HalGPIO::BTN_DOWN}, @@ -33,29 +18,36 @@ constexpr SideLayoutMap kSideLayouts[] = { } // namespace bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const { - const auto frontLayout = static_cast(SETTINGS.frontButtonLayout); const auto sideLayout = static_cast(SETTINGS.sideButtonLayout); - const auto& front = kFrontLayouts[frontLayout]; const auto& side = kSideLayouts[sideLayout]; switch (button) { case Button::Back: - return (gpio.*fn)(front.back); + // Logical Back maps to user-configured front button. + return (gpio.*fn)(SETTINGS.frontButtonBack); case Button::Confirm: - return (gpio.*fn)(front.confirm); + // Logical Confirm maps to user-configured front button. + return (gpio.*fn)(SETTINGS.frontButtonConfirm); case Button::Left: - return (gpio.*fn)(front.left); + // Logical Left maps to user-configured front button. + return (gpio.*fn)(SETTINGS.frontButtonLeft); case Button::Right: - return (gpio.*fn)(front.right); + // Logical Right maps to user-configured front button. + return (gpio.*fn)(SETTINGS.frontButtonRight); case Button::Up: + // Side buttons remain fixed for Up/Down. return (gpio.*fn)(HalGPIO::BTN_UP); case Button::Down: + // Side buttons remain fixed for Up/Down. return (gpio.*fn)(HalGPIO::BTN_DOWN); case Button::Power: + // Power button bypasses remapping. return (gpio.*fn)(HalGPIO::BTN_POWER); case Button::PageBack: + // Reader page navigation uses side buttons and can be swapped via settings. return (gpio.*fn)(side.pageBack); case Button::PageForward: + // Reader page navigation uses side buttons and can be swapped via settings. return (gpio.*fn)(side.pageForward); } @@ -76,17 +68,42 @@ unsigned long MappedInputManager::getHeldTime() const { return gpio.getHeldTime( MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const { - const auto layout = static_cast(SETTINGS.frontButtonLayout); + // Build the label order based on the configured hardware mapping. + auto labelForHardware = [&](uint8_t hw) -> const char* { + // Compare against configured logical roles and return the matching label. + if (hw == SETTINGS.frontButtonBack) { + return back; + } + if (hw == SETTINGS.frontButtonConfirm) { + return confirm; + } + if (hw == SETTINGS.frontButtonLeft) { + return previous; + } + if (hw == SETTINGS.frontButtonRight) { + return next; + } + return ""; + }; - switch (layout) { - case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: - return {previous, next, back, confirm}; - case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: - return {previous, back, confirm, next}; - case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: - return {back, confirm, next, previous}; - case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: - default: - return {back, confirm, previous, next}; + return {labelForHardware(HalGPIO::BTN_BACK), labelForHardware(HalGPIO::BTN_CONFIRM), + labelForHardware(HalGPIO::BTN_LEFT), labelForHardware(HalGPIO::BTN_RIGHT)}; +} + +int MappedInputManager::getPressedFrontButton() const { + // Scan the raw front buttons in hardware order. + // This bypasses remapping so the remap activity can capture physical presses. + if (gpio.wasPressed(HalGPIO::BTN_BACK)) { + return HalGPIO::BTN_BACK; } + if (gpio.wasPressed(HalGPIO::BTN_CONFIRM)) { + return HalGPIO::BTN_CONFIRM; + } + if (gpio.wasPressed(HalGPIO::BTN_LEFT)) { + return HalGPIO::BTN_LEFT; + } + if (gpio.wasPressed(HalGPIO::BTN_RIGHT)) { + return HalGPIO::BTN_RIGHT; + } + return -1; } \ No newline at end of file diff --git a/src/MappedInputManager.h b/src/MappedInputManager.h index f507a928..bd594a25 100644 --- a/src/MappedInputManager.h +++ b/src/MappedInputManager.h @@ -22,6 +22,8 @@ class MappedInputManager { bool wasAnyReleased() const; unsigned long getHeldTime() const; Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const; + // Returns the raw front button index that was pressed this frame (or -1 if none). + int getPressedFrontButton() const; private: HalGPIO& gpio; diff --git a/src/activities/settings/ButtonRemapActivity.cpp b/src/activities/settings/ButtonRemapActivity.cpp new file mode 100644 index 00000000..48553530 --- /dev/null +++ b/src/activities/settings/ButtonRemapActivity.cpp @@ -0,0 +1,225 @@ +#include "ButtonRemapActivity.h" + +#include + +#include "CrossPointSettings.h" +#include "MappedInputManager.h" +#include "fontIds.h" + +namespace { +// UI steps correspond to logical roles in order: Back, Confirm, Left, Right. +constexpr uint8_t kRoleCount = 4; +// Marker used when a role has not been assigned yet. +constexpr uint8_t kUnassigned = 0xFF; +// Duration to show temporary error text when reassigning a button. +constexpr unsigned long kErrorDisplayMs = 1500; +} // namespace + +void ButtonRemapActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void ButtonRemapActivity::onEnter() { + Activity::onEnter(); + + renderingMutex = xSemaphoreCreateMutex(); + // Start with all roles unassigned to avoid duplicate blocking. + currentStep = 0; + tempMapping[0] = kUnassigned; + tempMapping[1] = kUnassigned; + tempMapping[2] = kUnassigned; + tempMapping[3] = kUnassigned; + errorMessage.clear(); + errorUntil = 0; + updateRequired = true; + + xTaskCreate(&ButtonRemapActivity::taskTrampoline, "ButtonRemapTask", 4096, this, 1, &displayTaskHandle); +} + +void ButtonRemapActivity::onExit() { + Activity::onExit(); + + // Ensure display task is stopped outside of active rendering. + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (displayTaskHandle) { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; + } + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; +} + +void ButtonRemapActivity::loop() { + // Side buttons: + // - Up: reset mapping to defaults and exit. + // - Down: cancel without saving. + if (mappedInput.wasPressed(MappedInputManager::Button::Up)) { + // Persist default mapping immediately so the user can recover quickly. + SETTINGS.frontButtonBack = CrossPointSettings::FRONT_HW_BACK; + SETTINGS.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM; + SETTINGS.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT; + SETTINGS.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT; + SETTINGS.saveToFile(); + onBack(); + return; + } + + if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { + // Exit without changing settings. + onBack(); + return; + } + + // Wait for the UI to refresh before accepting another assignment. + // This avoids rapid double-presses that can advance the step without a visible redraw. + if (updateRequired) { + return; + } + + // Wait for a front button press to assign to the current role. + const int pressedButton = mappedInput.getPressedFrontButton(); + if (pressedButton < 0) { + return; + } + + // Update temporary mapping and advance the remap step. + // Only accept the press if this hardware button isn't already assigned elsewhere. + if (!validateUnassigned(static_cast(pressedButton))) { + updateRequired = true; + return; + } + tempMapping[currentStep] = static_cast(pressedButton); + currentStep++; + + if (currentStep >= kRoleCount) { + // All roles assigned; save to settings and exit. + applyTempMapping(); + SETTINGS.saveToFile(); + onBack(); + return; + } + + updateRequired = true; +} + +[[noreturn]] void ButtonRemapActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + // Ensure render calls are serialized with UI thread changes. + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + updateRequired = false; + xSemaphoreGive(renderingMutex); + } + + // Clear any temporary warning after its timeout. + if (errorUntil > 0 && millis() > errorUntil) { + errorMessage.clear(); + errorUntil = 0; + updateRequired = true; + } + + vTaskDelay(50 / portTICK_PERIOD_MS); + } +} + +void ButtonRemapActivity::render() { + renderer.clearScreen(); + + const auto pageWidth = renderer.getScreenWidth(); + const auto labelForHardware = [&](uint8_t hardwareIndex) -> const char* { + for (uint8_t i = 0; i < kRoleCount; i++) { + if (tempMapping[i] == hardwareIndex) { + return getRoleName(i); + } + } + return "-"; + }; + + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Remap Front Buttons", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, 40, "Press a front button for each role"); + + for (uint8_t i = 0; i < kRoleCount; i++) { + const int y = 70 + i * 30; + const bool isSelected = (i == currentStep); + + // Highlight the role that is currently being assigned. + if (isSelected) { + renderer.fillRect(0, y - 2, pageWidth - 1, 30); + } + + const char* roleName = getRoleName(i); + renderer.drawText(UI_10_FONT_ID, 20, y, roleName, !isSelected); + + // Show currently assigned hardware button (or unassigned). + const char* assigned = (tempMapping[i] == kUnassigned) ? "Unassigned" : getHardwareName(tempMapping[i]); + const auto width = renderer.getTextWidth(UI_10_FONT_ID, assigned); + renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, y, assigned, !isSelected); + } + + // Temporary warning banner for duplicates. + if (!errorMessage.empty()) { + renderer.drawCenteredText(UI_10_FONT_ID, 210, errorMessage.c_str(), true); + } + + // Provide side button actions at the bottom of the screen. + renderer.drawCenteredText(SMALL_FONT_ID, 235, "Side button Up: Reset Side Button Down: Cancel", true); + + // Live preview of logical labels under front buttons. + // This mirrors the on-device front button order: Back, Confirm, Left, Right. + renderer.drawButtonHints(UI_10_FONT_ID, labelForHardware(CrossPointSettings::FRONT_HW_BACK), + labelForHardware(CrossPointSettings::FRONT_HW_CONFIRM), + labelForHardware(CrossPointSettings::FRONT_HW_LEFT), + labelForHardware(CrossPointSettings::FRONT_HW_RIGHT)); + renderer.displayBuffer(); +} + +void ButtonRemapActivity::applyTempMapping() { + // Commit temporary mapping into settings (logical role -> hardware). + SETTINGS.frontButtonBack = tempMapping[0]; + SETTINGS.frontButtonConfirm = tempMapping[1]; + SETTINGS.frontButtonLeft = tempMapping[2]; + SETTINGS.frontButtonRight = tempMapping[3]; +} + +bool ButtonRemapActivity::validateUnassigned(const uint8_t pressedButton) { + // Block reusing a hardware button already assigned to another role. + for (uint8_t i = 0; i < kRoleCount; i++) { + if (tempMapping[i] == pressedButton && i != currentStep) { + errorMessage = "Already assigned"; + errorUntil = millis() + kErrorDisplayMs; + return false; + } + } + return true; +} + +const char* ButtonRemapActivity::getRoleName(const uint8_t roleIndex) const { + switch (roleIndex) { + case 0: + return "Back"; + case 1: + return "Confirm"; + case 2: + return "Left"; + case 3: + default: + return "Right"; + } +} + +const char* ButtonRemapActivity::getHardwareName(const uint8_t buttonIndex) const { + switch (buttonIndex) { + case CrossPointSettings::FRONT_HW_BACK: + return "Back (1st button)"; + case CrossPointSettings::FRONT_HW_CONFIRM: + return "Confirm (2nd button)"; + case CrossPointSettings::FRONT_HW_LEFT: + return "Left (3rd button)"; + case CrossPointSettings::FRONT_HW_RIGHT: + return "Right (4th button)"; + default: + return "Unknown"; + } +} diff --git a/src/activities/settings/ButtonRemapActivity.h b/src/activities/settings/ButtonRemapActivity.h new file mode 100644 index 00000000..3d317672 --- /dev/null +++ b/src/activities/settings/ButtonRemapActivity.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include + +#include +#include + +#include "activities/Activity.h" + +class ButtonRemapActivity final : public Activity { + public: + explicit ButtonRemapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onBack) + : Activity("ButtonRemap", renderer, mappedInput), onBack(onBack) {} + + void onEnter() override; + void onExit() override; + void loop() override; + + private: + // Rendering task state. + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + bool updateRequired = false; + + // Callback used to exit the remap flow back to the settings list. + const std::function onBack; + // Index of the logical role currently awaiting input. + uint8_t currentStep = 0; + // Temporary mapping from logical role -> hardware button index. + uint8_t tempMapping[4] = {0xFF, 0xFF, 0xFF, 0xFF}; + // Error banner timing (used when reassigning duplicate buttons). + unsigned long errorUntil = 0; + std::string errorMessage; + + // FreeRTOS task helpers. + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render(); + + // Commit temporary mapping to settings. + void applyTempMapping(); + // Returns false if a hardware button is already assigned to a different role. + bool validateUnassigned(uint8_t pressedButton); + // Labels for UI display. + const char* getRoleName(uint8_t roleIndex) const; + const char* getHardwareName(uint8_t buttonIndex) const; +}; diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index 7fd5ef5f..8c8d01c1 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -5,6 +5,7 @@ #include +#include "ButtonRemapActivity.h" #include "CalibreSettingsActivity.h" #include "ClearCacheActivity.h" #include "CrossPointSettings.h" @@ -127,6 +128,15 @@ void CategorySettingsActivity::toggleCurrentSetting() { updateRequired = true; })); xSemaphoreGive(renderingMutex); + } else if (strcmp(setting.name, "Remap Front Buttons") == 0) { + // Start the button remap flow. + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new ButtonRemapActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); } } else { return; @@ -186,7 +196,7 @@ void CategorySettingsActivity::render() const { renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), pageHeight - 60, CROSSPOINT_VERSION); - const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", ""); + const auto labels = mappedInput.mapLabels("« Back", "Toggle", "Up", "Down"); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7316db05..b4048aad 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -40,13 +40,12 @@ const SettingInfo readerSettings[readerSettingsCount] = { constexpr int controlsSettingsCount = 4; const SettingInfo controlsSettings[controlsSettingsCount] = { - SettingInfo::Enum( - "Front Button Layout", &CrossPointSettings::frontButtonLayout, - {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght", "Bck, Cnfrm, Rght, Lft"}), - SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, - {"Prev, Next", "Next, Prev"}), - SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), - SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; + // Launches the remap wizard for front buttons. + SettingInfo::Action("Remap Front Buttons"), + SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, + {"Prev, Next", "Next, Prev"}), + SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), + SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; constexpr int systemSettingsCount = 5; const SettingInfo systemSettings[systemSettingsCount] = { @@ -199,7 +198,7 @@ void SettingsActivity::render() const { pageHeight - 60, CROSSPOINT_VERSION); // Draw help text - const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); + const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); // Always use standard refresh for settings screen From e77321c26d2ca4c9ee83d66a64264df512f09d62 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Mon, 2 Feb 2026 14:58:21 +0300 Subject: [PATCH 2/5] update hints --- src/activities/settings/ButtonRemapActivity.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/activities/settings/ButtonRemapActivity.cpp b/src/activities/settings/ButtonRemapActivity.cpp index 48553530..a1abb2bc 100644 --- a/src/activities/settings/ButtonRemapActivity.cpp +++ b/src/activities/settings/ButtonRemapActivity.cpp @@ -163,8 +163,9 @@ void ButtonRemapActivity::render() { renderer.drawCenteredText(UI_10_FONT_ID, 210, errorMessage.c_str(), true); } - // Provide side button actions at the bottom of the screen. - renderer.drawCenteredText(SMALL_FONT_ID, 235, "Side button Up: Reset Side Button Down: Cancel", true); + // Provide side button actions at the bottom of the screen (split across two lines). + renderer.drawCenteredText(SMALL_FONT_ID, 225, "Side button Up: Reset", true); + renderer.drawCenteredText(SMALL_FONT_ID, 242, "Side button Down: Cancel", true); // Live preview of logical labels under front buttons. // This mirrors the on-device front button order: Back, Confirm, Left, Right. From 623ea25d65c5a87d9e776fcba50331aa17b37a7c Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Mon, 2 Feb 2026 15:03:47 +0300 Subject: [PATCH 3/5] clang format fix --- src/CrossPointSettings.cpp | 6 ++---- src/activities/settings/ButtonRemapActivity.h | 20 +++++++++---------- src/activities/settings/SettingsActivity.cpp | 12 +++++------ 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index ebdc12a8..efdc5249 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -45,8 +45,6 @@ void validateFrontButtonMapping(CrossPointSettings& settings) { } } // namespace - - bool CrossPointSettings::saveToFile() const { // Make sure the directory exists SdMan.mkdir("/.crosspoint"); @@ -63,7 +61,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, shortPwrBtn); serialization::writePod(outputFile, statusBar); serialization::writePod(outputFile, orientation); - serialization::writePod(outputFile, frontButtonLayout); // legacy + serialization::writePod(outputFile, frontButtonLayout); // legacy serialization::writePod(outputFile, sideButtonLayout); serialization::writePod(outputFile, fontFamily); serialization::writePod(outputFile, fontSize); @@ -124,7 +122,7 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, orientation, ORIENTATION_COUNT); if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT); // legacy + readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT); // legacy if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, sideButtonLayout, SIDE_BUTTON_LAYOUT_COUNT); if (++settingsRead >= fileSettingsCount) break; diff --git a/src/activities/settings/ButtonRemapActivity.h b/src/activities/settings/ButtonRemapActivity.h index 3d317672..f87a66ea 100644 --- a/src/activities/settings/ButtonRemapActivity.h +++ b/src/activities/settings/ButtonRemapActivity.h @@ -19,31 +19,31 @@ class ButtonRemapActivity final : public Activity { void loop() override; private: - // Rendering task state. + // Rendering task state. TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; bool updateRequired = false; - // Callback used to exit the remap flow back to the settings list. + // Callback used to exit the remap flow back to the settings list. const std::function onBack; - // Index of the logical role currently awaiting input. + // Index of the logical role currently awaiting input. uint8_t currentStep = 0; - // Temporary mapping from logical role -> hardware button index. + // Temporary mapping from logical role -> hardware button index. uint8_t tempMapping[4] = {0xFF, 0xFF, 0xFF, 0xFF}; - // Error banner timing (used when reassigning duplicate buttons). + // Error banner timing (used when reassigning duplicate buttons). unsigned long errorUntil = 0; std::string errorMessage; - // FreeRTOS task helpers. + // FreeRTOS task helpers. static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void render(); - // Commit temporary mapping to settings. + // Commit temporary mapping to settings. void applyTempMapping(); - // Returns false if a hardware button is already assigned to a different role. - bool validateUnassigned(uint8_t pressedButton); - // Labels for UI display. + // Returns false if a hardware button is already assigned to a different role. + bool validateUnassigned(uint8_t pressedButton); + // Labels for UI display. const char* getRoleName(uint8_t roleIndex) const; const char* getHardwareName(uint8_t buttonIndex) const; }; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index b4048aad..d5c717d8 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -40,12 +40,12 @@ const SettingInfo readerSettings[readerSettingsCount] = { constexpr int controlsSettingsCount = 4; const SettingInfo controlsSettings[controlsSettingsCount] = { - // Launches the remap wizard for front buttons. - SettingInfo::Action("Remap Front Buttons"), - SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, - {"Prev, Next", "Next, Prev"}), - SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), - SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; + // Launches the remap wizard for front buttons. + SettingInfo::Action("Remap Front Buttons"), + SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, + {"Prev, Next", "Next, Prev"}), + SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), + SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; constexpr int systemSettingsCount = 5; const SettingInfo systemSettings[systemSettingsCount] = { From 6bdeb4888e65bdd880459eca5f3b3a62d50457f3 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Mon, 2 Feb 2026 15:22:51 +0300 Subject: [PATCH 4/5] move hints --- src/activities/settings/ButtonRemapActivity.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/activities/settings/ButtonRemapActivity.cpp b/src/activities/settings/ButtonRemapActivity.cpp index a1abb2bc..a286bc42 100644 --- a/src/activities/settings/ButtonRemapActivity.cpp +++ b/src/activities/settings/ButtonRemapActivity.cpp @@ -164,8 +164,8 @@ void ButtonRemapActivity::render() { } // Provide side button actions at the bottom of the screen (split across two lines). - renderer.drawCenteredText(SMALL_FONT_ID, 225, "Side button Up: Reset", true); - renderer.drawCenteredText(SMALL_FONT_ID, 242, "Side button Down: Cancel", true); + renderer.drawCenteredText(SMALL_FONT_ID, 250, "Side button Up: Reset", true); + renderer.drawCenteredText(SMALL_FONT_ID, 270, "Side button Down: Cancel", true); // Live preview of logical labels under front buttons. // This mirrors the on-device front button order: Back, Confirm, Left, Right. From 4dfa1d6384c496a4b790b21e7e5c5147513a3249 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Tue, 3 Feb 2026 12:38:11 +0300 Subject: [PATCH 5/5] migration --- src/CrossPointSettings.cpp | 34 +++++++++++++++++++ .../settings/ButtonRemapActivity.cpp | 4 +-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index efdc5249..320ce217 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -24,6 +24,7 @@ constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields constexpr uint8_t SETTINGS_COUNT = 27; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; + // Validate front button mapping to ensure each hardware button is unique. // If duplicates are detected, reset to the default physical order to prevent invalid mappings. void validateFrontButtonMapping(CrossPointSettings& settings) { @@ -43,6 +44,37 @@ void validateFrontButtonMapping(CrossPointSettings& settings) { } } } + +// Convert legacy front button layout into explicit logical->hardware mapping. +void applyLegacyFrontButtonLayout(CrossPointSettings& settings) { + switch (static_cast(settings.frontButtonLayout)) { + case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: + settings.frontButtonBack = CrossPointSettings::FRONT_HW_LEFT; + settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_RIGHT; + settings.frontButtonLeft = CrossPointSettings::FRONT_HW_BACK; + settings.frontButtonRight = CrossPointSettings::FRONT_HW_CONFIRM; + break; + case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: + settings.frontButtonBack = CrossPointSettings::FRONT_HW_CONFIRM; + settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_LEFT; + settings.frontButtonLeft = CrossPointSettings::FRONT_HW_BACK; + settings.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT; + break; + case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: + settings.frontButtonBack = CrossPointSettings::FRONT_HW_BACK; + settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM; + settings.frontButtonLeft = CrossPointSettings::FRONT_HW_RIGHT; + settings.frontButtonRight = CrossPointSettings::FRONT_HW_LEFT; + break; + case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: + default: + settings.frontButtonBack = CrossPointSettings::FRONT_HW_BACK; + settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM; + settings.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT; + settings.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT; + break; + } +} } // namespace bool CrossPointSettings::saveToFile() const { @@ -186,6 +218,8 @@ bool CrossPointSettings::loadFromFile() { if (frontButtonMappingRead) { validateFrontButtonMapping(*this); + } else { + applyLegacyFrontButtonLayout(*this); } inputFile.close(); diff --git a/src/activities/settings/ButtonRemapActivity.cpp b/src/activities/settings/ButtonRemapActivity.cpp index a286bc42..e0ee5bfa 100644 --- a/src/activities/settings/ButtonRemapActivity.cpp +++ b/src/activities/settings/ButtonRemapActivity.cpp @@ -164,8 +164,8 @@ void ButtonRemapActivity::render() { } // Provide side button actions at the bottom of the screen (split across two lines). - renderer.drawCenteredText(SMALL_FONT_ID, 250, "Side button Up: Reset", true); - renderer.drawCenteredText(SMALL_FONT_ID, 270, "Side button Down: Cancel", true); + renderer.drawCenteredText(SMALL_FONT_ID, 250, "Side button Up: Reset to default layout", true); + renderer.drawCenteredText(SMALL_FONT_ID, 280, "Side button Down: Cancel remapping", true); // Live preview of logical labels under front buttons. // This mirrors the on-device front button order: Back, Confirm, Left, Right.