A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: front button remapper (#664)

## Summary

* Custom remapper to create any variant of front button layout.

## Additional Context

* Included migration from previous frontlayout setting
* This will solve:
* https://github.com/crosspoint-reader/crosspoint-reader/issues/654
* https://github.com/crosspoint-reader/crosspoint-reader/issues/652
* https://github.com/crosspoint-reader/crosspoint-reader/issues/620
* https://github.com/crosspoint-reader/crosspoint-reader/issues/468

<img width="860" height="1147" alt="image"
src="https://github.com/user-attachments/assets/457356ed-7a7d-4e1c-8683-e187a1df47c0"
/>



---

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

authored by

Arthur Tazhitdinov and committed by
GitHub
c49a8199 bf87a7dc

+432 -43
+76 -5
src/CrossPointSettings.cpp
··· 22 22 namespace { 23 23 constexpr uint8_t SETTINGS_FILE_VERSION = 1; 24 24 // Increment this when adding new persisted settings fields 25 - constexpr uint8_t SETTINGS_COUNT = 24; 25 + constexpr uint8_t SETTINGS_COUNT = 28; 26 26 constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; 27 + 28 + // Validate front button mapping to ensure each hardware button is unique. 29 + // If duplicates are detected, reset to the default physical order to prevent invalid mappings. 30 + void validateFrontButtonMapping(CrossPointSettings& settings) { 31 + // Snapshot the logical->hardware mapping so we can compare for duplicates. 32 + const uint8_t mapping[] = {settings.frontButtonBack, settings.frontButtonConfirm, settings.frontButtonLeft, 33 + settings.frontButtonRight}; 34 + for (size_t i = 0; i < 4; i++) { 35 + for (size_t j = i + 1; j < 4; j++) { 36 + if (mapping[i] == mapping[j]) { 37 + // Duplicate detected: restore the default physical order (Back, Confirm, Left, Right). 38 + settings.frontButtonBack = CrossPointSettings::FRONT_HW_BACK; 39 + settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM; 40 + settings.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT; 41 + settings.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT; 42 + return; 43 + } 44 + } 45 + } 46 + } 47 + 48 + // Convert legacy front button layout into explicit logical->hardware mapping. 49 + void applyLegacyFrontButtonLayout(CrossPointSettings& settings) { 50 + switch (static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(settings.frontButtonLayout)) { 51 + case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: 52 + settings.frontButtonBack = CrossPointSettings::FRONT_HW_LEFT; 53 + settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_RIGHT; 54 + settings.frontButtonLeft = CrossPointSettings::FRONT_HW_BACK; 55 + settings.frontButtonRight = CrossPointSettings::FRONT_HW_CONFIRM; 56 + break; 57 + case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: 58 + settings.frontButtonBack = CrossPointSettings::FRONT_HW_CONFIRM; 59 + settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_LEFT; 60 + settings.frontButtonLeft = CrossPointSettings::FRONT_HW_BACK; 61 + settings.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT; 62 + break; 63 + case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: 64 + settings.frontButtonBack = CrossPointSettings::FRONT_HW_BACK; 65 + settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM; 66 + settings.frontButtonLeft = CrossPointSettings::FRONT_HW_RIGHT; 67 + settings.frontButtonRight = CrossPointSettings::FRONT_HW_LEFT; 68 + break; 69 + case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: 70 + default: 71 + settings.frontButtonBack = CrossPointSettings::FRONT_HW_BACK; 72 + settings.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM; 73 + settings.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT; 74 + settings.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT; 75 + break; 76 + } 77 + } 27 78 } // namespace 28 79 29 80 bool CrossPointSettings::saveToFile() const { ··· 42 93 serialization::writePod(outputFile, shortPwrBtn); 43 94 serialization::writePod(outputFile, statusBar); 44 95 serialization::writePod(outputFile, orientation); 45 - serialization::writePod(outputFile, frontButtonLayout); 96 + serialization::writePod(outputFile, frontButtonLayout); // legacy 46 97 serialization::writePod(outputFile, sideButtonLayout); 47 98 serialization::writePod(outputFile, fontFamily); 48 99 serialization::writePod(outputFile, fontSize); ··· 60 111 serialization::writeString(outputFile, std::string(opdsUsername)); 61 112 serialization::writeString(outputFile, std::string(opdsPassword)); 62 113 serialization::writePod(outputFile, sleepScreenCoverFilter); 63 - // New fields added at end for backward compatibility 64 114 serialization::writePod(outputFile, uiTheme); 115 + serialization::writePod(outputFile, frontButtonBack); 116 + serialization::writePod(outputFile, frontButtonConfirm); 117 + serialization::writePod(outputFile, frontButtonLeft); 118 + serialization::writePod(outputFile, frontButtonRight); 119 + // New fields added at end for backward compatibility 65 120 outputFile.close(); 66 121 67 122 Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); ··· 87 142 88 143 // load settings that exist (support older files with fewer fields) 89 144 uint8_t settingsRead = 0; 145 + // Track whether remap fields were present in the settings file. 146 + bool frontButtonMappingRead = false; 90 147 do { 91 148 readAndValidate(inputFile, sleepScreen, SLEEP_SCREEN_MODE_COUNT); 92 149 if (++settingsRead >= fileSettingsCount) break; ··· 98 155 if (++settingsRead >= fileSettingsCount) break; 99 156 readAndValidate(inputFile, orientation, ORIENTATION_COUNT); 100 157 if (++settingsRead >= fileSettingsCount) break; 101 - readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT); 158 + readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT); // legacy 102 159 if (++settingsRead >= fileSettingsCount) break; 103 160 readAndValidate(inputFile, sideButtonLayout, SIDE_BUTTON_LAYOUT_COUNT); 104 161 if (++settingsRead >= fileSettingsCount) break; ··· 149 206 if (++settingsRead >= fileSettingsCount) break; 150 207 readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT); 151 208 if (++settingsRead >= fileSettingsCount) break; 152 - // New fields added at end for backward compatibility 153 209 serialization::readPod(inputFile, uiTheme); 154 210 if (++settingsRead >= fileSettingsCount) break; 211 + readAndValidate(inputFile, frontButtonBack, FRONT_BUTTON_HARDWARE_COUNT); 212 + if (++settingsRead >= fileSettingsCount) break; 213 + readAndValidate(inputFile, frontButtonConfirm, FRONT_BUTTON_HARDWARE_COUNT); 214 + if (++settingsRead >= fileSettingsCount) break; 215 + readAndValidate(inputFile, frontButtonLeft, FRONT_BUTTON_HARDWARE_COUNT); 216 + if (++settingsRead >= fileSettingsCount) break; 217 + readAndValidate(inputFile, frontButtonRight, FRONT_BUTTON_HARDWARE_COUNT); 218 + frontButtonMappingRead = true; 219 + // New fields added at end for backward compatibility 155 220 } while (false); 221 + 222 + if (frontButtonMappingRead) { 223 + validateFrontButtonMapping(*this); 224 + } else { 225 + applyLegacyFrontButtonLayout(*this); 226 + } 156 227 157 228 inputFile.close(); 158 229 Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
+17 -2
src/CrossPointSettings.h
··· 42 42 ORIENTATION_COUNT 43 43 }; 44 44 45 - // Front button layout options 45 + // Front button layout options (legacy) 46 46 // Default: Back, Confirm, Left, Right 47 47 // Swapped: Left, Right, Back, Confirm 48 48 enum FRONT_BUTTON_LAYOUT { ··· 51 51 LEFT_BACK_CONFIRM_RIGHT = 2, 52 52 BACK_CONFIRM_RIGHT_LEFT = 3, 53 53 FRONT_BUTTON_LAYOUT_COUNT 54 + }; 55 + 56 + // Front button hardware identifiers (for remapping) 57 + enum FRONT_BUTTON_HARDWARE { 58 + FRONT_HW_BACK = 0, 59 + FRONT_HW_CONFIRM = 1, 60 + FRONT_HW_LEFT = 2, 61 + FRONT_HW_RIGHT = 3, 62 + FRONT_BUTTON_HARDWARE_COUNT 54 63 }; 55 64 56 65 // Side button layout options ··· 116 125 // EPUB reading orientation settings 117 126 // 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise 118 127 uint8_t orientation = PORTRAIT; 119 - // Button layouts 128 + // Button layouts (front layout retained for migration only) 120 129 uint8_t frontButtonLayout = BACK_CONFIRM_LEFT_RIGHT; 121 130 uint8_t sideButtonLayout = PREV_NEXT; 131 + // Front button remap (logical -> hardware) 132 + // Used by MappedInputManager to translate logical buttons into physical front buttons. 133 + uint8_t frontButtonBack = FRONT_HW_BACK; 134 + uint8_t frontButtonConfirm = FRONT_HW_CONFIRM; 135 + uint8_t frontButtonLeft = FRONT_HW_LEFT; 136 + uint8_t frontButtonRight = FRONT_HW_RIGHT; 122 137 // Reader font settings 123 138 uint8_t fontFamily = BOOKERLY; 124 139 uint8_t fontSize = MEDIUM;
+49 -32
src/MappedInputManager.cpp
··· 5 5 namespace { 6 6 using ButtonIndex = uint8_t; 7 7 8 - struct FrontLayoutMap { 9 - ButtonIndex back; 10 - ButtonIndex confirm; 11 - ButtonIndex left; 12 - ButtonIndex right; 13 - }; 14 - 15 8 struct SideLayoutMap { 16 9 ButtonIndex pageBack; 17 10 ButtonIndex pageForward; 18 11 }; 19 12 20 - // Order matches CrossPointSettings::FRONT_BUTTON_LAYOUT. 21 - constexpr FrontLayoutMap kFrontLayouts[] = { 22 - {HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT}, 23 - {HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT, HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM}, 24 - {HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_BACK, HalGPIO::BTN_RIGHT}, 25 - {HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_RIGHT, HalGPIO::BTN_LEFT}, 26 - }; 27 - 28 13 // Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT. 29 14 constexpr SideLayoutMap kSideLayouts[] = { 30 15 {HalGPIO::BTN_UP, HalGPIO::BTN_DOWN}, ··· 33 18 } // namespace 34 19 35 20 bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const { 36 - const auto frontLayout = static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout); 37 21 const auto sideLayout = static_cast<CrossPointSettings::SIDE_BUTTON_LAYOUT>(SETTINGS.sideButtonLayout); 38 - const auto& front = kFrontLayouts[frontLayout]; 39 22 const auto& side = kSideLayouts[sideLayout]; 40 23 41 24 switch (button) { 42 25 case Button::Back: 43 - return (gpio.*fn)(front.back); 26 + // Logical Back maps to user-configured front button. 27 + return (gpio.*fn)(SETTINGS.frontButtonBack); 44 28 case Button::Confirm: 45 - return (gpio.*fn)(front.confirm); 29 + // Logical Confirm maps to user-configured front button. 30 + return (gpio.*fn)(SETTINGS.frontButtonConfirm); 46 31 case Button::Left: 47 - return (gpio.*fn)(front.left); 32 + // Logical Left maps to user-configured front button. 33 + return (gpio.*fn)(SETTINGS.frontButtonLeft); 48 34 case Button::Right: 49 - return (gpio.*fn)(front.right); 35 + // Logical Right maps to user-configured front button. 36 + return (gpio.*fn)(SETTINGS.frontButtonRight); 50 37 case Button::Up: 38 + // Side buttons remain fixed for Up/Down. 51 39 return (gpio.*fn)(HalGPIO::BTN_UP); 52 40 case Button::Down: 41 + // Side buttons remain fixed for Up/Down. 53 42 return (gpio.*fn)(HalGPIO::BTN_DOWN); 54 43 case Button::Power: 44 + // Power button bypasses remapping. 55 45 return (gpio.*fn)(HalGPIO::BTN_POWER); 56 46 case Button::PageBack: 47 + // Reader page navigation uses side buttons and can be swapped via settings. 57 48 return (gpio.*fn)(side.pageBack); 58 49 case Button::PageForward: 50 + // Reader page navigation uses side buttons and can be swapped via settings. 59 51 return (gpio.*fn)(side.pageForward); 60 52 } 61 53 ··· 76 68 77 69 MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous, 78 70 const char* next) const { 79 - const auto layout = static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout); 71 + // Build the label order based on the configured hardware mapping. 72 + auto labelForHardware = [&](uint8_t hw) -> const char* { 73 + // Compare against configured logical roles and return the matching label. 74 + if (hw == SETTINGS.frontButtonBack) { 75 + return back; 76 + } 77 + if (hw == SETTINGS.frontButtonConfirm) { 78 + return confirm; 79 + } 80 + if (hw == SETTINGS.frontButtonLeft) { 81 + return previous; 82 + } 83 + if (hw == SETTINGS.frontButtonRight) { 84 + return next; 85 + } 86 + return ""; 87 + }; 80 88 81 - switch (layout) { 82 - case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: 83 - return {previous, next, back, confirm}; 84 - case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: 85 - return {previous, back, confirm, next}; 86 - case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: 87 - return {back, confirm, next, previous}; 88 - case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: 89 - default: 90 - return {back, confirm, previous, next}; 89 + return {labelForHardware(HalGPIO::BTN_BACK), labelForHardware(HalGPIO::BTN_CONFIRM), 90 + labelForHardware(HalGPIO::BTN_LEFT), labelForHardware(HalGPIO::BTN_RIGHT)}; 91 + } 92 + 93 + int MappedInputManager::getPressedFrontButton() const { 94 + // Scan the raw front buttons in hardware order. 95 + // This bypasses remapping so the remap activity can capture physical presses. 96 + if (gpio.wasPressed(HalGPIO::BTN_BACK)) { 97 + return HalGPIO::BTN_BACK; 91 98 } 99 + if (gpio.wasPressed(HalGPIO::BTN_CONFIRM)) { 100 + return HalGPIO::BTN_CONFIRM; 101 + } 102 + if (gpio.wasPressed(HalGPIO::BTN_LEFT)) { 103 + return HalGPIO::BTN_LEFT; 104 + } 105 + if (gpio.wasPressed(HalGPIO::BTN_RIGHT)) { 106 + return HalGPIO::BTN_RIGHT; 107 + } 108 + return -1; 92 109 }
+2
src/MappedInputManager.h
··· 22 22 bool wasAnyReleased() const; 23 23 unsigned long getHeldTime() const; 24 24 Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const; 25 + // Returns the raw front button index that was pressed this frame (or -1 if none). 26 + int getPressedFrontButton() const; 25 27 26 28 private: 27 29 HalGPIO& gpio;
+227
src/activities/settings/ButtonRemapActivity.cpp
··· 1 + #include "ButtonRemapActivity.h" 2 + 3 + #include <GfxRenderer.h> 4 + 5 + #include "CrossPointSettings.h" 6 + #include "MappedInputManager.h" 7 + #include "components/UITheme.h" 8 + #include "fontIds.h" 9 + 10 + namespace { 11 + // UI steps correspond to logical roles in order: Back, Confirm, Left, Right. 12 + constexpr uint8_t kRoleCount = 4; 13 + // Marker used when a role has not been assigned yet. 14 + constexpr uint8_t kUnassigned = 0xFF; 15 + // Duration to show temporary error text when reassigning a button. 16 + constexpr unsigned long kErrorDisplayMs = 1500; 17 + } // namespace 18 + 19 + void ButtonRemapActivity::taskTrampoline(void* param) { 20 + auto* self = static_cast<ButtonRemapActivity*>(param); 21 + self->displayTaskLoop(); 22 + } 23 + 24 + void ButtonRemapActivity::onEnter() { 25 + Activity::onEnter(); 26 + 27 + renderingMutex = xSemaphoreCreateMutex(); 28 + // Start with all roles unassigned to avoid duplicate blocking. 29 + currentStep = 0; 30 + tempMapping[0] = kUnassigned; 31 + tempMapping[1] = kUnassigned; 32 + tempMapping[2] = kUnassigned; 33 + tempMapping[3] = kUnassigned; 34 + errorMessage.clear(); 35 + errorUntil = 0; 36 + updateRequired = true; 37 + 38 + xTaskCreate(&ButtonRemapActivity::taskTrampoline, "ButtonRemapTask", 4096, this, 1, &displayTaskHandle); 39 + } 40 + 41 + void ButtonRemapActivity::onExit() { 42 + Activity::onExit(); 43 + 44 + // Ensure display task is stopped outside of active rendering. 45 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 46 + if (displayTaskHandle) { 47 + vTaskDelete(displayTaskHandle); 48 + displayTaskHandle = nullptr; 49 + } 50 + vSemaphoreDelete(renderingMutex); 51 + renderingMutex = nullptr; 52 + } 53 + 54 + void ButtonRemapActivity::loop() { 55 + // Side buttons: 56 + // - Up: reset mapping to defaults and exit. 57 + // - Down: cancel without saving. 58 + if (mappedInput.wasPressed(MappedInputManager::Button::Up)) { 59 + // Persist default mapping immediately so the user can recover quickly. 60 + SETTINGS.frontButtonBack = CrossPointSettings::FRONT_HW_BACK; 61 + SETTINGS.frontButtonConfirm = CrossPointSettings::FRONT_HW_CONFIRM; 62 + SETTINGS.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT; 63 + SETTINGS.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT; 64 + SETTINGS.saveToFile(); 65 + onBack(); 66 + return; 67 + } 68 + 69 + if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { 70 + // Exit without changing settings. 71 + onBack(); 72 + return; 73 + } 74 + 75 + // Wait for the UI to refresh before accepting another assignment. 76 + // This avoids rapid double-presses that can advance the step without a visible redraw. 77 + if (updateRequired) { 78 + return; 79 + } 80 + 81 + // Wait for a front button press to assign to the current role. 82 + const int pressedButton = mappedInput.getPressedFrontButton(); 83 + if (pressedButton < 0) { 84 + return; 85 + } 86 + 87 + // Update temporary mapping and advance the remap step. 88 + // Only accept the press if this hardware button isn't already assigned elsewhere. 89 + if (!validateUnassigned(static_cast<uint8_t>(pressedButton))) { 90 + updateRequired = true; 91 + return; 92 + } 93 + tempMapping[currentStep] = static_cast<uint8_t>(pressedButton); 94 + currentStep++; 95 + 96 + if (currentStep >= kRoleCount) { 97 + // All roles assigned; save to settings and exit. 98 + applyTempMapping(); 99 + SETTINGS.saveToFile(); 100 + onBack(); 101 + return; 102 + } 103 + 104 + updateRequired = true; 105 + } 106 + 107 + [[noreturn]] void ButtonRemapActivity::displayTaskLoop() { 108 + while (true) { 109 + if (updateRequired) { 110 + // Ensure render calls are serialized with UI thread changes. 111 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 112 + render(); 113 + updateRequired = false; 114 + xSemaphoreGive(renderingMutex); 115 + } 116 + 117 + // Clear any temporary warning after its timeout. 118 + if (errorUntil > 0 && millis() > errorUntil) { 119 + errorMessage.clear(); 120 + errorUntil = 0; 121 + updateRequired = true; 122 + } 123 + 124 + vTaskDelay(50 / portTICK_PERIOD_MS); 125 + } 126 + } 127 + 128 + void ButtonRemapActivity::render() { 129 + renderer.clearScreen(); 130 + 131 + const auto pageWidth = renderer.getScreenWidth(); 132 + const auto labelForHardware = [&](uint8_t hardwareIndex) -> const char* { 133 + for (uint8_t i = 0; i < kRoleCount; i++) { 134 + if (tempMapping[i] == hardwareIndex) { 135 + return getRoleName(i); 136 + } 137 + } 138 + return "-"; 139 + }; 140 + 141 + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Remap Front Buttons", true, EpdFontFamily::BOLD); 142 + renderer.drawCenteredText(UI_10_FONT_ID, 40, "Press a front button for each role"); 143 + 144 + for (uint8_t i = 0; i < kRoleCount; i++) { 145 + const int y = 70 + i * 30; 146 + const bool isSelected = (i == currentStep); 147 + 148 + // Highlight the role that is currently being assigned. 149 + if (isSelected) { 150 + renderer.fillRect(0, y - 2, pageWidth - 1, 30); 151 + } 152 + 153 + const char* roleName = getRoleName(i); 154 + renderer.drawText(UI_10_FONT_ID, 20, y, roleName, !isSelected); 155 + 156 + // Show currently assigned hardware button (or unassigned). 157 + const char* assigned = (tempMapping[i] == kUnassigned) ? "Unassigned" : getHardwareName(tempMapping[i]); 158 + const auto width = renderer.getTextWidth(UI_10_FONT_ID, assigned); 159 + renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, y, assigned, !isSelected); 160 + } 161 + 162 + // Temporary warning banner for duplicates. 163 + if (!errorMessage.empty()) { 164 + renderer.drawCenteredText(UI_10_FONT_ID, 210, errorMessage.c_str(), true); 165 + } 166 + 167 + // Provide side button actions at the bottom of the screen (split across two lines). 168 + renderer.drawCenteredText(SMALL_FONT_ID, 250, "Side button Up: Reset to default layout", true); 169 + renderer.drawCenteredText(SMALL_FONT_ID, 280, "Side button Down: Cancel remapping", true); 170 + 171 + // Live preview of logical labels under front buttons. 172 + // This mirrors the on-device front button order: Back, Confirm, Left, Right. 173 + GUI.drawButtonHints(renderer, labelForHardware(CrossPointSettings::FRONT_HW_BACK), 174 + labelForHardware(CrossPointSettings::FRONT_HW_CONFIRM), 175 + labelForHardware(CrossPointSettings::FRONT_HW_LEFT), 176 + labelForHardware(CrossPointSettings::FRONT_HW_RIGHT)); 177 + renderer.displayBuffer(); 178 + } 179 + 180 + void ButtonRemapActivity::applyTempMapping() { 181 + // Commit temporary mapping into settings (logical role -> hardware). 182 + SETTINGS.frontButtonBack = tempMapping[0]; 183 + SETTINGS.frontButtonConfirm = tempMapping[1]; 184 + SETTINGS.frontButtonLeft = tempMapping[2]; 185 + SETTINGS.frontButtonRight = tempMapping[3]; 186 + } 187 + 188 + bool ButtonRemapActivity::validateUnassigned(const uint8_t pressedButton) { 189 + // Block reusing a hardware button already assigned to another role. 190 + for (uint8_t i = 0; i < kRoleCount; i++) { 191 + if (tempMapping[i] == pressedButton && i != currentStep) { 192 + errorMessage = "Already assigned"; 193 + errorUntil = millis() + kErrorDisplayMs; 194 + return false; 195 + } 196 + } 197 + return true; 198 + } 199 + 200 + const char* ButtonRemapActivity::getRoleName(const uint8_t roleIndex) const { 201 + switch (roleIndex) { 202 + case 0: 203 + return "Back"; 204 + case 1: 205 + return "Confirm"; 206 + case 2: 207 + return "Left"; 208 + case 3: 209 + default: 210 + return "Right"; 211 + } 212 + } 213 + 214 + const char* ButtonRemapActivity::getHardwareName(const uint8_t buttonIndex) const { 215 + switch (buttonIndex) { 216 + case CrossPointSettings::FRONT_HW_BACK: 217 + return "Back (1st button)"; 218 + case CrossPointSettings::FRONT_HW_CONFIRM: 219 + return "Confirm (2nd button)"; 220 + case CrossPointSettings::FRONT_HW_LEFT: 221 + return "Left (3rd button)"; 222 + case CrossPointSettings::FRONT_HW_RIGHT: 223 + return "Right (4th button)"; 224 + default: 225 + return "Unknown"; 226 + } 227 + }
+49
src/activities/settings/ButtonRemapActivity.h
··· 1 + #pragma once 2 + #include <freertos/FreeRTOS.h> 3 + #include <freertos/semphr.h> 4 + #include <freertos/task.h> 5 + 6 + #include <functional> 7 + #include <string> 8 + 9 + #include "activities/Activity.h" 10 + 11 + class ButtonRemapActivity final : public Activity { 12 + public: 13 + explicit ButtonRemapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 14 + const std::function<void()>& onBack) 15 + : Activity("ButtonRemap", renderer, mappedInput), onBack(onBack) {} 16 + 17 + void onEnter() override; 18 + void onExit() override; 19 + void loop() override; 20 + 21 + private: 22 + // Rendering task state. 23 + TaskHandle_t displayTaskHandle = nullptr; 24 + SemaphoreHandle_t renderingMutex = nullptr; 25 + bool updateRequired = false; 26 + 27 + // Callback used to exit the remap flow back to the settings list. 28 + const std::function<void()> onBack; 29 + // Index of the logical role currently awaiting input. 30 + uint8_t currentStep = 0; 31 + // Temporary mapping from logical role -> hardware button index. 32 + uint8_t tempMapping[4] = {0xFF, 0xFF, 0xFF, 0xFF}; 33 + // Error banner timing (used when reassigning duplicate buttons). 34 + unsigned long errorUntil = 0; 35 + std::string errorMessage; 36 + 37 + // FreeRTOS task helpers. 38 + static void taskTrampoline(void* param); 39 + [[noreturn]] void displayTaskLoop(); 40 + void render(); 41 + 42 + // Commit temporary mapping to settings. 43 + void applyTempMapping(); 44 + // Returns false if a hardware button is already assigned to a different role. 45 + bool validateUnassigned(uint8_t pressedButton); 46 + // Labels for UI display. 47 + const char* getRoleName(uint8_t roleIndex) const; 48 + const char* getHardwareName(uint8_t buttonIndex) const; 49 + };
+12 -4
src/activities/settings/SettingsActivity.cpp
··· 3 3 #include <GfxRenderer.h> 4 4 #include <HardwareSerial.h> 5 5 6 + #include "ButtonRemapActivity.h" 6 7 #include "CalibreSettingsActivity.h" 7 8 #include "ClearCacheActivity.h" 8 9 #include "CrossPointSettings.h" ··· 47 48 48 49 constexpr int controlsSettingsCount = 4; 49 50 const SettingInfo controlsSettings[controlsSettingsCount] = { 50 - SettingInfo::Enum( 51 - "Front Button Layout", &CrossPointSettings::frontButtonLayout, 52 - {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght", "Bck, Cnfrm, Rght, Lft"}), 51 + // Launches the remap wizard for front buttons. 52 + SettingInfo::Action("Remap Front Buttons"), 53 53 SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, 54 54 {"Prev, Next", "Next, Prev"}), 55 55 SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), ··· 201 201 SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; 202 202 } 203 203 } else if (setting.type == SettingType::ACTION) { 204 - if (strcmp(setting.name, "KOReader Sync") == 0) { 204 + if (strcmp(setting.name, "Remap Front Buttons") == 0) { 205 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 206 + exitActivity(); 207 + enterNewActivity(new ButtonRemapActivity(renderer, mappedInput, [this] { 208 + exitActivity(); 209 + updateRequired = true; 210 + })); 211 + xSemaphoreGive(renderingMutex); 212 + } else if (strcmp(setting.name, "KOReader Sync") == 0) { 205 213 xSemaphoreTake(renderingMutex, portMAX_DELAY); 206 214 exitActivity(); 207 215 enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {