mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 06:37:38 +03:00
Merge 8df957e8a5 into 78d6e5931c
This commit is contained in:
commit
692d5ecc5d
@ -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 = 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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,6 +111,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 +141,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 +154,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 +205,23 @@ 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);
|
||||
} else {
|
||||
applyLegacyFrontButtonLayout(*this);
|
||||
}
|
||||
|
||||
inputFile.close();
|
||||
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
|
||||
return true;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
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 "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();
|
||||
|
||||
@ -40,9 +40,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),
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user