mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
Merge 4dfa1d6384 into f67c544e16
This commit is contained in:
commit
f2dfb62319
@ -22,8 +22,59 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) {
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||||
// Increment this when adding new persisted settings fields
|
// 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";
|
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
|
} // namespace
|
||||||
|
|
||||||
bool CrossPointSettings::saveToFile() const {
|
bool CrossPointSettings::saveToFile() const {
|
||||||
@ -42,7 +93,7 @@ bool CrossPointSettings::saveToFile() const {
|
|||||||
serialization::writePod(outputFile, shortPwrBtn);
|
serialization::writePod(outputFile, shortPwrBtn);
|
||||||
serialization::writePod(outputFile, statusBar);
|
serialization::writePod(outputFile, statusBar);
|
||||||
serialization::writePod(outputFile, orientation);
|
serialization::writePod(outputFile, orientation);
|
||||||
serialization::writePod(outputFile, frontButtonLayout);
|
serialization::writePod(outputFile, frontButtonLayout); // legacy
|
||||||
serialization::writePod(outputFile, sideButtonLayout);
|
serialization::writePod(outputFile, sideButtonLayout);
|
||||||
serialization::writePod(outputFile, fontFamily);
|
serialization::writePod(outputFile, fontFamily);
|
||||||
serialization::writePod(outputFile, fontSize);
|
serialization::writePod(outputFile, fontSize);
|
||||||
@ -60,6 +111,10 @@ bool CrossPointSettings::saveToFile() const {
|
|||||||
serialization::writeString(outputFile, std::string(opdsUsername));
|
serialization::writeString(outputFile, std::string(opdsUsername));
|
||||||
serialization::writeString(outputFile, std::string(opdsPassword));
|
serialization::writeString(outputFile, std::string(opdsPassword));
|
||||||
serialization::writePod(outputFile, sleepScreenCoverFilter);
|
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
|
// New fields added at end for backward compatibility
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
|
|
||||||
@ -86,6 +141,8 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
|
|
||||||
// load settings that exist (support older files with fewer fields)
|
// load settings that exist (support older files with fewer fields)
|
||||||
uint8_t settingsRead = 0;
|
uint8_t settingsRead = 0;
|
||||||
|
// Track whether remap fields were present in the settings file.
|
||||||
|
bool frontButtonMappingRead = false;
|
||||||
do {
|
do {
|
||||||
readAndValidate(inputFile, sleepScreen, SLEEP_SCREEN_MODE_COUNT);
|
readAndValidate(inputFile, sleepScreen, SLEEP_SCREEN_MODE_COUNT);
|
||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
@ -97,7 +154,7 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
readAndValidate(inputFile, orientation, ORIENTATION_COUNT);
|
readAndValidate(inputFile, orientation, ORIENTATION_COUNT);
|
||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT);
|
readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT); // legacy
|
||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
readAndValidate(inputFile, sideButtonLayout, SIDE_BUTTON_LAYOUT_COUNT);
|
readAndValidate(inputFile, sideButtonLayout, SIDE_BUTTON_LAYOUT_COUNT);
|
||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
@ -148,9 +205,23 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT);
|
readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT);
|
||||||
if (++settingsRead >= fileSettingsCount) break;
|
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
|
// New fields added at end for backward compatibility
|
||||||
} while (false);
|
} while (false);
|
||||||
|
|
||||||
|
if (frontButtonMappingRead) {
|
||||||
|
validateFrontButtonMapping(*this);
|
||||||
|
} else {
|
||||||
|
applyLegacyFrontButtonLayout(*this);
|
||||||
|
}
|
||||||
|
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
|
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -42,7 +42,7 @@ class CrossPointSettings {
|
|||||||
ORIENTATION_COUNT
|
ORIENTATION_COUNT
|
||||||
};
|
};
|
||||||
|
|
||||||
// Front button layout options
|
// Front button layout options (legacy)
|
||||||
// Default: Back, Confirm, Left, Right
|
// Default: Back, Confirm, Left, Right
|
||||||
// Swapped: Left, Right, Back, Confirm
|
// Swapped: Left, Right, Back, Confirm
|
||||||
enum FRONT_BUTTON_LAYOUT {
|
enum FRONT_BUTTON_LAYOUT {
|
||||||
@ -53,6 +53,15 @@ class CrossPointSettings {
|
|||||||
FRONT_BUTTON_LAYOUT_COUNT
|
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
|
// Side button layout options
|
||||||
// Default: Previous, Next
|
// Default: Previous, Next
|
||||||
// Swapped: Next, Previous
|
// Swapped: Next, Previous
|
||||||
@ -113,9 +122,15 @@ class CrossPointSettings {
|
|||||||
// EPUB reading orientation settings
|
// EPUB reading orientation settings
|
||||||
// 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise
|
// 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise
|
||||||
uint8_t orientation = PORTRAIT;
|
uint8_t orientation = PORTRAIT;
|
||||||
// Button layouts
|
// Button layouts (front layout retained for migration only)
|
||||||
uint8_t frontButtonLayout = BACK_CONFIRM_LEFT_RIGHT;
|
uint8_t frontButtonLayout = BACK_CONFIRM_LEFT_RIGHT;
|
||||||
uint8_t sideButtonLayout = PREV_NEXT;
|
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
|
// Reader font settings
|
||||||
uint8_t fontFamily = BOOKERLY;
|
uint8_t fontFamily = BOOKERLY;
|
||||||
uint8_t fontSize = MEDIUM;
|
uint8_t fontSize = MEDIUM;
|
||||||
|
|||||||
@ -5,26 +5,11 @@
|
|||||||
namespace {
|
namespace {
|
||||||
using ButtonIndex = uint8_t;
|
using ButtonIndex = uint8_t;
|
||||||
|
|
||||||
struct FrontLayoutMap {
|
|
||||||
ButtonIndex back;
|
|
||||||
ButtonIndex confirm;
|
|
||||||
ButtonIndex left;
|
|
||||||
ButtonIndex right;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SideLayoutMap {
|
struct SideLayoutMap {
|
||||||
ButtonIndex pageBack;
|
ButtonIndex pageBack;
|
||||||
ButtonIndex pageForward;
|
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.
|
// Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT.
|
||||||
constexpr SideLayoutMap kSideLayouts[] = {
|
constexpr SideLayoutMap kSideLayouts[] = {
|
||||||
{HalGPIO::BTN_UP, HalGPIO::BTN_DOWN},
|
{HalGPIO::BTN_UP, HalGPIO::BTN_DOWN},
|
||||||
@ -33,29 +18,36 @@ constexpr SideLayoutMap kSideLayouts[] = {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const {
|
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 sideLayout = static_cast<CrossPointSettings::SIDE_BUTTON_LAYOUT>(SETTINGS.sideButtonLayout);
|
||||||
const auto& front = kFrontLayouts[frontLayout];
|
|
||||||
const auto& side = kSideLayouts[sideLayout];
|
const auto& side = kSideLayouts[sideLayout];
|
||||||
|
|
||||||
switch (button) {
|
switch (button) {
|
||||||
case Button::Back:
|
case Button::Back:
|
||||||
return (gpio.*fn)(front.back);
|
// Logical Back maps to user-configured front button.
|
||||||
|
return (gpio.*fn)(SETTINGS.frontButtonBack);
|
||||||
case Button::Confirm:
|
case Button::Confirm:
|
||||||
return (gpio.*fn)(front.confirm);
|
// Logical Confirm maps to user-configured front button.
|
||||||
|
return (gpio.*fn)(SETTINGS.frontButtonConfirm);
|
||||||
case Button::Left:
|
case Button::Left:
|
||||||
return (gpio.*fn)(front.left);
|
// Logical Left maps to user-configured front button.
|
||||||
|
return (gpio.*fn)(SETTINGS.frontButtonLeft);
|
||||||
case Button::Right:
|
case Button::Right:
|
||||||
return (gpio.*fn)(front.right);
|
// Logical Right maps to user-configured front button.
|
||||||
|
return (gpio.*fn)(SETTINGS.frontButtonRight);
|
||||||
case Button::Up:
|
case Button::Up:
|
||||||
|
// Side buttons remain fixed for Up/Down.
|
||||||
return (gpio.*fn)(HalGPIO::BTN_UP);
|
return (gpio.*fn)(HalGPIO::BTN_UP);
|
||||||
case Button::Down:
|
case Button::Down:
|
||||||
|
// Side buttons remain fixed for Up/Down.
|
||||||
return (gpio.*fn)(HalGPIO::BTN_DOWN);
|
return (gpio.*fn)(HalGPIO::BTN_DOWN);
|
||||||
case Button::Power:
|
case Button::Power:
|
||||||
|
// Power button bypasses remapping.
|
||||||
return (gpio.*fn)(HalGPIO::BTN_POWER);
|
return (gpio.*fn)(HalGPIO::BTN_POWER);
|
||||||
case Button::PageBack:
|
case Button::PageBack:
|
||||||
|
// Reader page navigation uses side buttons and can be swapped via settings.
|
||||||
return (gpio.*fn)(side.pageBack);
|
return (gpio.*fn)(side.pageBack);
|
||||||
case Button::PageForward:
|
case Button::PageForward:
|
||||||
|
// Reader page navigation uses side buttons and can be swapped via settings.
|
||||||
return (gpio.*fn)(side.pageForward);
|
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,
|
MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous,
|
||||||
const char* next) const {
|
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) {
|
return {labelForHardware(HalGPIO::BTN_BACK), labelForHardware(HalGPIO::BTN_CONFIRM),
|
||||||
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
|
labelForHardware(HalGPIO::BTN_LEFT), labelForHardware(HalGPIO::BTN_RIGHT)};
|
||||||
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};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@ -22,6 +22,8 @@ class MappedInputManager {
|
|||||||
bool wasAnyReleased() const;
|
bool wasAnyReleased() const;
|
||||||
unsigned long getHeldTime() const;
|
unsigned long getHeldTime() const;
|
||||||
Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) 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:
|
private:
|
||||||
HalGPIO& gpio;
|
HalGPIO& gpio;
|
||||||
|
|||||||
226
src/activities/settings/ButtonRemapActivity.cpp
Normal file
226
src/activities/settings/ButtonRemapActivity.cpp
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
#include "ButtonRemapActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#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<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.
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/activities/settings/ButtonRemapActivity.h
Normal file
49
src/activities/settings/ButtonRemapActivity.h
Normal 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;
|
||||||
|
};
|
||||||
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "ButtonRemapActivity.h"
|
||||||
#include "CalibreSettingsActivity.h"
|
#include "CalibreSettingsActivity.h"
|
||||||
#include "ClearCacheActivity.h"
|
#include "ClearCacheActivity.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
@ -127,6 +128,15 @@ void CategorySettingsActivity::toggleCurrentSetting() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}));
|
}));
|
||||||
xSemaphoreGive(renderingMutex);
|
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 {
|
} else {
|
||||||
return;
|
return;
|
||||||
@ -186,7 +196,7 @@ void CategorySettingsActivity::render() const {
|
|||||||
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
|
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
|
||||||
pageHeight - 60, 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.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@ -40,9 +40,8 @@ const SettingInfo readerSettings[readerSettingsCount] = {
|
|||||||
|
|
||||||
constexpr int controlsSettingsCount = 4;
|
constexpr int controlsSettingsCount = 4;
|
||||||
const SettingInfo controlsSettings[controlsSettingsCount] = {
|
const SettingInfo controlsSettings[controlsSettingsCount] = {
|
||||||
SettingInfo::Enum(
|
// Launches the remap wizard for front buttons.
|
||||||
"Front Button Layout", &CrossPointSettings::frontButtonLayout,
|
SettingInfo::Action("Remap Front Buttons"),
|
||||||
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght", "Bck, Cnfrm, Rght, Lft"}),
|
|
||||||
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
|
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
|
||||||
{"Prev, Next", "Next, Prev"}),
|
{"Prev, Next", "Next, Prev"}),
|
||||||
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
|
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
|
||||||
@ -199,7 +198,7 @@ void SettingsActivity::render() const {
|
|||||||
pageHeight - 60, CROSSPOINT_VERSION);
|
pageHeight - 60, CROSSPOINT_VERSION);
|
||||||
|
|
||||||
// Draw help text
|
// 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);
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
// Always use standard refresh for settings screen
|
// Always use standard refresh for settings screen
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user