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