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 >**_
This commit is contained in:
Arthur Tazhitdinov 2026-02-05 14:37:17 +03:00 committed by GitHub
parent bf87a7dc60
commit c49a819939
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 432 additions and 43 deletions

View File

@ -22,8 +22,59 @@ 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 = 24;
constexpr uint8_t SETTINGS_COUNT = 28;
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;
}
}
}
}
// Convert legacy front button layout into explicit logical->hardware mapping.
void applyLegacyFrontButtonLayout(CrossPointSettings& settings) {
switch (static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(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 {
@ -42,7 +93,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,8 +111,12 @@ bool CrossPointSettings::saveToFile() const {
serialization::writeString(outputFile, std::string(opdsUsername));
serialization::writeString(outputFile, std::string(opdsPassword));
serialization::writePod(outputFile, sleepScreenCoverFilter);
// New fields added at end for backward compatibility
serialization::writePod(outputFile, uiTheme);
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();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -87,6 +142,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;
@ -98,7 +155,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;
@ -149,11 +206,25 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT);
if (++settingsRead >= fileSettingsCount) break;
// New fields added at end for backward compatibility
serialization::readPod(inputFile, uiTheme);
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);
} else {
applyLegacyFrontButtonLayout(*this);
}
inputFile.close();
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
return true;

View File

@ -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
@ -116,9 +125,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;

View File

@ -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<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout);
const auto sideLayout = static_cast<CrossPointSettings::SIDE_BUTTON_LAYOUT>(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<CrossPointSettings::FRONT_BUTTON_LAYOUT>(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;
}

View File

@ -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;

View File

@ -0,0 +1,227 @@
#include "ButtonRemapActivity.h"
#include <GfxRenderer.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "components/UITheme.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<ButtonRemapActivity*>(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<uint8_t>(pressedButton))) {
updateRequired = true;
return;
}
tempMapping[currentStep] = static_cast<uint8_t>(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 (split across two lines).
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.
GUI.drawButtonHints(renderer, 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";
}
}

View File

@ -0,0 +1,49 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <string>
#include "activities/Activity.h"
class ButtonRemapActivity final : public Activity {
public:
explicit ButtonRemapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& 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<void()> 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;
};

View File

@ -3,6 +3,7 @@
#include <GfxRenderer.h>
#include <HardwareSerial.h>
#include "ButtonRemapActivity.h"
#include "CalibreSettingsActivity.h"
#include "ClearCacheActivity.h"
#include "CrossPointSettings.h"
@ -47,9 +48,8 @@ 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"}),
// 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),
@ -201,7 +201,15 @@ void SettingsActivity::toggleCurrentSetting() {
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
}
} else if (setting.type == SettingType::ACTION) {
if (strcmp(setting.name, "KOReader Sync") == 0) {
if (strcmp(setting.name, "Remap Front Buttons") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new ButtonRemapActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
} else if (strcmp(setting.name, "KOReader Sync") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {