368 lines
13 KiB
C++
368 lines
13 KiB
C++
#include "CrossPointSettings.h"
|
|
|
|
#include <HalStorage.h>
|
|
#include <JsonSettingsIO.h>
|
|
#include <Logging.h>
|
|
#include <Serialization.h>
|
|
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#include "SdCardFontGlobals.h"
|
|
#include "fontIds.h"
|
|
|
|
// Font ID 0 is reserved as the SD card font "not found" sentinel
|
|
// (SdCardFontManager::computeFontId() never returns 0). Guard against any
|
|
// hash accidentally producing 0 — would cause silent fallback to built-in.
|
|
static_assert(BOOKERLY_12_FONT_ID != 0, "Font ID collision with sentinel");
|
|
static_assert(BOOKERLY_14_FONT_ID != 0, "Font ID collision with sentinel");
|
|
static_assert(BOOKERLY_16_FONT_ID != 0, "Font ID collision with sentinel");
|
|
static_assert(BOOKERLY_18_FONT_ID != 0, "Font ID collision with sentinel");
|
|
static_assert(BOOKERLY_10_FONT_ID != 0, "Font ID collision with sentinel");
|
|
static_assert(NOTOSANS_12_FONT_ID != 0, "Font ID collision with sentinel");
|
|
static_assert(NOTOSANS_14_FONT_ID != 0, "Font ID collision with sentinel");
|
|
static_assert(NOTOSANS_16_FONT_ID != 0, "Font ID collision with sentinel");
|
|
static_assert(NOTOSANS_18_FONT_ID != 0, "Font ID collision with sentinel");
|
|
static_assert(NOTOSANS_10_FONT_ID != 0, "Font ID collision with sentinel");
|
|
static_assert(UI_10_FONT_ID != 0, "Font ID collision with sentinel");
|
|
static_assert(UI_12_FONT_ID != 0, "Font ID collision with sentinel");
|
|
static_assert(SMALL_FONT_ID != 0, "Font ID collision with sentinel");
|
|
|
|
// Initialize the static instance
|
|
CrossPointSettings CrossPointSettings::instance;
|
|
|
|
void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) {
|
|
uint8_t tempValue;
|
|
serialization::readPod(file, tempValue);
|
|
if (tempValue < maxValue) {
|
|
member = tempValue;
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
|
constexpr char SETTINGS_FILE_BIN[] = "/.crosspoint/settings.bin";
|
|
constexpr char SETTINGS_FILE_JSON[] = "/.crosspoint/settings.json";
|
|
constexpr char SETTINGS_FILE_BAK[] = "/.crosspoint/settings.bin.bak";
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
void enforceFixedShortActions(CrossPointSettings& settings) {
|
|
settings.btnShortBack = static_cast<uint8_t>(CrossPointSettings::BUTTON_ACTION::BTN_DEFAULT);
|
|
settings.btnShortConfirm = static_cast<uint8_t>(CrossPointSettings::BUTTON_ACTION::BTN_DEFAULT);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void CrossPointSettings::validateFrontButtonMapping(CrossPointSettings& settings) {
|
|
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]) {
|
|
settings.frontButtonBack = FRONT_HW_BACK;
|
|
settings.frontButtonConfirm = FRONT_HW_CONFIRM;
|
|
settings.frontButtonLeft = FRONT_HW_LEFT;
|
|
settings.frontButtonRight = FRONT_HW_RIGHT;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CrossPointSettings::saveToFile() const {
|
|
Storage.mkdir("/.crosspoint");
|
|
return JsonSettingsIO::saveSettings(*this, SETTINGS_FILE_JSON);
|
|
}
|
|
|
|
bool CrossPointSettings::loadFromFile() {
|
|
// Try JSON first
|
|
if (Storage.exists(SETTINGS_FILE_JSON)) {
|
|
String json = Storage.readFile(SETTINGS_FILE_JSON);
|
|
if (!json.isEmpty()) {
|
|
bool resave = false;
|
|
bool result = JsonSettingsIO::loadSettings(*this, json.c_str(), &resave);
|
|
if (result) {
|
|
enforceFixedShortActions(*this);
|
|
if (resave) {
|
|
if (saveToFile()) {
|
|
LOG_DBG("CPS", "Resaved settings to update format");
|
|
} else {
|
|
LOG_ERR("CPS", "Failed to resave settings after format update");
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Fall back to binary migration
|
|
if (Storage.exists(SETTINGS_FILE_BIN)) {
|
|
if (loadFromBinaryFile()) {
|
|
enforceFixedShortActions(*this);
|
|
if (saveToFile()) {
|
|
Storage.rename(SETTINGS_FILE_BIN, SETTINGS_FILE_BAK);
|
|
LOG_DBG("CPS", "Migrated settings.bin to settings.json");
|
|
return true;
|
|
} else {
|
|
LOG_ERR("CPS", "Failed to save migrated settings to JSON");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool CrossPointSettings::loadFromBinaryFile() {
|
|
FsFile inputFile;
|
|
if (!Storage.openFileForRead("CPS", SETTINGS_FILE_BIN, inputFile)) {
|
|
return false;
|
|
}
|
|
|
|
uint8_t version;
|
|
serialization::readPod(inputFile, version);
|
|
if (version != SETTINGS_FILE_VERSION) {
|
|
LOG_ERR("CPS", "Deserialization failed: Unknown version %u", version);
|
|
inputFile.close();
|
|
return false;
|
|
}
|
|
|
|
uint8_t fileSettingsCount = 0;
|
|
serialization::readPod(inputFile, fileSettingsCount);
|
|
|
|
uint8_t settingsRead = 0;
|
|
bool frontButtonMappingRead = false;
|
|
do {
|
|
readAndValidate(inputFile, sleepScreen, SLEEP_SCREEN_MODE_COUNT);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
serialization::readPod(inputFile, extraParagraphSpacing);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
{
|
|
uint8_t ignored;
|
|
serialization::readPod(inputFile, ignored);
|
|
} // legacy shortPwrBtn field
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
readAndValidate(inputFile, statusBar, STATUS_BAR_MODE_COUNT); // legacy
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
readAndValidate(inputFile, orientation, ORIENTATION_COUNT);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
{
|
|
uint8_t ignored;
|
|
serialization::readPod(inputFile, ignored);
|
|
} // legacy sideButtonLayout field
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
readAndValidate(inputFile, fontFamily, FONT_FAMILY_COUNT);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
readAndValidate(inputFile, fontSize, FONT_SIZE_COUNT);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
readAndValidate(inputFile, lineSpacing, LINE_COMPRESSION_COUNT);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
readAndValidate(inputFile, paragraphAlignment, PARAGRAPH_ALIGNMENT_COUNT);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
readAndValidate(inputFile, sleepTimeout, SLEEP_TIMEOUT_COUNT);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
readAndValidate(inputFile, refreshFrequency, REFRESH_FREQUENCY_COUNT);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
serialization::readPod(inputFile, screenMargin);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
readAndValidate(inputFile, sleepScreenCoverMode, SLEEP_SCREEN_COVER_MODE_COUNT);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
{
|
|
std::string urlStr;
|
|
serialization::readString(inputFile, urlStr);
|
|
strncpy(opdsServerUrl, urlStr.c_str(), sizeof(opdsServerUrl) - 1);
|
|
opdsServerUrl[sizeof(opdsServerUrl) - 1] = '\0';
|
|
}
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
serialization::readPod(inputFile, textAntiAliasing);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
readAndValidate(inputFile, hideBatteryPercentage, HIDE_BATTERY_PERCENTAGE_COUNT);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
{
|
|
uint8_t ignored;
|
|
serialization::readPod(inputFile, ignored);
|
|
} // was longPressChapterSkip
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
serialization::readPod(inputFile, hyphenationEnabled);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
{
|
|
std::string usernameStr;
|
|
serialization::readString(inputFile, usernameStr);
|
|
strncpy(opdsUsername, usernameStr.c_str(), sizeof(opdsUsername) - 1);
|
|
opdsUsername[sizeof(opdsUsername) - 1] = '\0';
|
|
}
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
{
|
|
std::string passwordStr;
|
|
serialization::readString(inputFile, passwordStr);
|
|
strncpy(opdsPassword, passwordStr.c_str(), sizeof(opdsPassword) - 1);
|
|
opdsPassword[sizeof(opdsPassword) - 1] = '\0';
|
|
}
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
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;
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
serialization::readPod(inputFile, fadingFix);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
serialization::readPod(inputFile, embeddedStyle);
|
|
if (++settingsRead >= fileSettingsCount) break;
|
|
} while (false);
|
|
|
|
if (frontButtonMappingRead) {
|
|
CrossPointSettings::validateFrontButtonMapping(*this);
|
|
} else {
|
|
applyLegacyFrontButtonLayout(*this);
|
|
}
|
|
|
|
inputFile.close();
|
|
LOG_DBG("CPS", "Settings loaded from binary file");
|
|
return true;
|
|
}
|
|
|
|
float CrossPointSettings::getReaderLineCompression() const {
|
|
const int effectiveFontId = getReaderFontId();
|
|
const int notosansId = getBuiltinReaderFontId(NOTOSANS, fontSize);
|
|
|
|
if (effectiveFontId == notosansId) {
|
|
switch (lineSpacing) {
|
|
case TIGHT:
|
|
return 0.90f;
|
|
case NORMAL:
|
|
default:
|
|
return 0.95f;
|
|
case WIDE:
|
|
return 1.0f;
|
|
}
|
|
}
|
|
|
|
// Bookerly or any SD card font: use the Bookerly-style neutral values.
|
|
switch (lineSpacing) {
|
|
case TIGHT:
|
|
return 0.95f;
|
|
case NORMAL:
|
|
default:
|
|
return 1.0f;
|
|
case WIDE:
|
|
return 1.1f;
|
|
}
|
|
}
|
|
|
|
unsigned long CrossPointSettings::getSleepTimeoutMs() const {
|
|
switch (sleepTimeout) {
|
|
case SLEEP_1_MIN:
|
|
return 1UL * 60 * 1000;
|
|
case SLEEP_5_MIN:
|
|
return 5UL * 60 * 1000;
|
|
case SLEEP_10_MIN:
|
|
default:
|
|
return 10UL * 60 * 1000;
|
|
case SLEEP_15_MIN:
|
|
return 15UL * 60 * 1000;
|
|
case SLEEP_30_MIN:
|
|
return 30UL * 60 * 1000;
|
|
}
|
|
}
|
|
|
|
int CrossPointSettings::getRefreshFrequency() const {
|
|
switch (refreshFrequency) {
|
|
case REFRESH_1:
|
|
return 1;
|
|
case REFRESH_5:
|
|
return 5;
|
|
case REFRESH_10:
|
|
return 10;
|
|
case REFRESH_15:
|
|
default:
|
|
return 15;
|
|
case REFRESH_30:
|
|
return 30;
|
|
}
|
|
}
|
|
|
|
int CrossPointSettings::getBuiltinReaderFontId(uint8_t family, uint8_t size) {
|
|
switch (family) {
|
|
case BOOKERLY:
|
|
default:
|
|
switch (size) {
|
|
case TINY:
|
|
return BOOKERLY_10_FONT_ID;
|
|
case SMALL:
|
|
return BOOKERLY_12_FONT_ID;
|
|
case MEDIUM:
|
|
default:
|
|
return BOOKERLY_14_FONT_ID;
|
|
case LARGE:
|
|
return BOOKERLY_16_FONT_ID;
|
|
case EXTRA_LARGE:
|
|
return BOOKERLY_18_FONT_ID;
|
|
}
|
|
case NOTOSANS:
|
|
switch (size) {
|
|
case TINY:
|
|
return NOTOSANS_10_FONT_ID;
|
|
case SMALL:
|
|
return NOTOSANS_12_FONT_ID;
|
|
case MEDIUM:
|
|
default:
|
|
return NOTOSANS_14_FONT_ID;
|
|
case LARGE:
|
|
return NOTOSANS_16_FONT_ID;
|
|
case EXTRA_LARGE:
|
|
return NOTOSANS_18_FONT_ID;
|
|
}
|
|
}
|
|
}
|
|
|
|
int CrossPointSettings::getReaderFontId() const {
|
|
// SD card font takes priority when one is selected globally.
|
|
// resolveSdCardFontId() returns 0 if the named family isn't loaded
|
|
// (e.g. SD card removed since selection) — fall through to built-in.
|
|
if (sdFontFamilyName[0] != '\0') {
|
|
int id = resolveSdCardFontId(sdFontFamilyName, fontSize);
|
|
if (id != 0) return id;
|
|
}
|
|
return getBuiltinReaderFontId(fontFamily, fontSize);
|
|
}
|