Compare commits

...

4 Commits

Author SHA1 Message Date
Boris Faure
addd1c8617
Merge 5ae10a7eb6 into 78d6e5931c 2026-02-04 09:18:11 +11:00
Jake Kenneally
78d6e5931c
fix: Correct debugging_monitor.py script instructions (#676)
Some checks are pending
CI / build (push) Waiting to run
## Summary

**What is the goal of this PR?**
- Minor correction to the `debugging_monitor.py` script instructions

**What changes are included?**
- `pyserial` should be installed, NOT `serial`, which is a [different
lib](https://pypi.org/project/serial/)
- Added macOS serial port

## Additional Context

- Just a minor docs update. I can confirm the debugging script is
working great on macOS

---

### 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? _**< NO >**_
2026-02-04 00:33:20 +03:00
Luke Stein
dac11c3fdd
fix: Correct instruction text to match actual button text (#672)
## Summary

* Instruction text says "Press OK to scan again" but button label is
actually "Connect" (not OK)
* Corrects instruction text

---

### AI Usage

Did you use AI tools to help write this code? **No**
2026-02-04 00:32:52 +03:00
Boris Faure
5ae10a7eb6
feat: rework CrossPointSettings
Multiple goals are achieved with this change:
- make descriptions of settings close to their definitions
- settings validation on loading with reset to default in case of bad
  value
- do not boot-loop on bad string values
- (de)serialization is based on a single list of descriptors: single point
  of truth
- possibly to have default values for the String configuration type
- more constexpr to reduce RAM usage
- less hardcoded values
- more maintainable

RAM Usage:
 From
RAM:   [===       ]  32.5% (used 106516 bytes from 327680 bytes)
Flash: [========= ]  94.9% (used 6217240 bytes from 6553600 bytes)
 To
RAM:   [===       ]  32.3% (used 105844 bytes from 327680 bytes)
Flash: [========= ]  94.8% (used 6213378 bytes from 6553600 bytes)

Boot config validation with a test where the status bar config is wrong:
[1256] [SD] SD card detected
[1256] [CPS] Loading settings from file
[1265] [CPS] Invalid value (0x3) for Status Bar, resetting to default
[1265] [CPS] Settings loaded from file
2026-01-28 16:10:45 +01:00
9 changed files with 429 additions and 290 deletions

View File

@ -102,13 +102,18 @@ After flashing the new features, its recommended to capture detailed logs fro
First, make sure all required Python packages are installed:
```python
python3 -m pip install serial colorama matplotlib
python3 -m pip install pyserial colorama matplotlib
```
after that run the script:
```sh
# For Linux
# This was tested on Debian and should work on most Linux systems.
python3 scripts/debugging_monitor.py
# For macOS
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
```
This was tested on Debian and should work on most Linux systems. Minor adjustments may be required for Windows or macOS.
Minor adjustments may be required for Windows.
## Internals

View File

@ -49,4 +49,12 @@ static void readString(FsFile& file, std::string& s) {
s.resize(len);
file.read(&s[0], len);
}
static void readString(FsFile& file, char* buffer, size_t maxLen) {
uint32_t len;
readPod(file, len);
const uint32_t bytesToRead = (len < maxLen - 1) ? len : (maxLen - 1);
file.read(reinterpret_cast<uint8_t*>(buffer), bytesToRead);
buffer[bytesToRead] = '\0';
}
} // namespace serialization

View File

@ -11,21 +11,132 @@
// 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;
// SettingDescriptor implementations
bool SettingDescriptor::validate(const CrossPointSettings& settings) const {
if (type == SettingType::STRING) {
return true; // Strings are always valid
}
if (!validator) {
return true; // No validator means always valid
}
const uint8_t value = settings.*(memberPtr);
return validator(value);
}
uint8_t SettingDescriptor::getValue(const CrossPointSettings& settings) const { return settings.*(memberPtr); }
void SettingDescriptor::setValue(CrossPointSettings& settings, uint8_t value) const { settings.*(memberPtr) = value; }
void SettingDescriptor::resetToDefault(CrossPointSettings& settings) const {
if (type == SettingType::STRING) {
strncpy(stringPtr, stringData.defaultString, stringData.maxSize - 1);
stringPtr[stringData.maxSize - 1] = '\0';
return;
}
setValue(settings, defaultValue);
}
void SettingDescriptor::save(FsFile& file, const CrossPointSettings& settings) const {
if (type == SettingType::STRING) {
serialization::writeString(file, std::string(stringPtr));
return;
}
serialization::writePod(file, settings.*(memberPtr));
}
void SettingDescriptor::load(FsFile& file, CrossPointSettings& settings) const {
if (type == SettingType::STRING) {
serialization::readString(file, stringPtr, stringData.maxSize);
return;
}
uint8_t value;
serialization::readPod(file, value);
settings.*(memberPtr) = value;
}
namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 23;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace
// Define enum value arrays
namespace {
constexpr const char* sleepScreenValues[] = {"Dark", "Light", "Custom", "Cover", "None"};
constexpr const char* shortPwrBtnValues[] = {"Ignore", "Sleep", "Page Turn"};
constexpr const char* statusBarValues[] = {"None", "No Progress", "Full"};
constexpr const char* orientationValues[] = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"};
constexpr const char* frontButtonLayoutValues[] = {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm",
"Lft, Bck, Cnfrm, Rght", "Bck, Cnfrm, Rght, Lft"};
constexpr const char* sideButtonLayoutValues[] = {"Prev/Next", "Next/Prev"};
constexpr const char* fontFamilyValues[] = {"Bookerly", "Noto Sans", "Open Dyslexic"};
constexpr const char* fontSizeValues[] = {"Small", "Medium", "Large", "X Large"};
constexpr const char* lineSpacingValues[] = {"Tight", "Normal", "Wide"};
constexpr const char* paragraphAlignmentValues[] = {"Justify", "Left", "Center", "Right"};
constexpr const char* sleepTimeoutValues[] = {"1 min", "5 min", "10 min", "15 min", "30 min"};
constexpr const char* refreshFrequencyValues[] = {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"};
constexpr const char* sleepScreenCoverModeValues[] = {"Fit", "Crop"};
constexpr const char* hideBatteryPercentageValues[] = {"Never", "In Reader", "Always"};
constexpr const char* sleepScreenCoverFilterValues[] = {"None", "Contrast", "Inverted"};
// Helper function template to deduce array size automatically
template <size_t N>
constexpr SettingDescriptor makeEnumDescriptor(const char* name, uint8_t CrossPointSettings::* ptr,
uint8_t defaultValue, const char* const (&enumValues)[N]) {
return SettingDescriptor(name, SettingType::ENUM, ptr, defaultValue, validateEnum<N>, enumValues, N);
}
// Helper macro to create STRING descriptors without repetition
#define makeStringDescriptor(name, member, defStr) \
SettingDescriptor(name, SettingType::STRING, CrossPointSettings::instance.member, defStr, \
sizeof(CrossPointSettings::member))
} // namespace
// Define static constexpr members (required in C++14 and earlier)
constexpr size_t CrossPointSettings::DESCRIPTOR_COUNT;
// Define the static constexpr array of all setting descriptors
// Order must match current serialization order for file format compatibility!
const std::array<SettingDescriptor, CrossPointSettings::DESCRIPTOR_COUNT> CrossPointSettings::descriptors = {{
makeEnumDescriptor("Sleep Screen", &CrossPointSettings::sleepScreen, CrossPointSettings::DARK, sleepScreenValues),
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, 1, validateToggle,
nullptr, 0},
makeEnumDescriptor("Short Power Button Click", &CrossPointSettings::shortPwrBtn, CrossPointSettings::IGNORE,
shortPwrBtnValues),
makeEnumDescriptor("Status Bar", &CrossPointSettings::statusBar, CrossPointSettings::FULL, statusBarValues),
makeEnumDescriptor("Reading Orientation", &CrossPointSettings::orientation, CrossPointSettings::PORTRAIT,
orientationValues),
makeEnumDescriptor("Front Button Layout", &CrossPointSettings::frontButtonLayout,
CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT, frontButtonLayoutValues),
makeEnumDescriptor("Side Button Layout", &CrossPointSettings::sideButtonLayout, CrossPointSettings::PREV_NEXT,
sideButtonLayoutValues),
makeEnumDescriptor("Reader Font Family", &CrossPointSettings::fontFamily, CrossPointSettings::BOOKERLY,
fontFamilyValues),
makeEnumDescriptor("Reader Font Size", &CrossPointSettings::fontSize, CrossPointSettings::MEDIUM, fontSizeValues),
makeEnumDescriptor("Reader Line Spacing", &CrossPointSettings::lineSpacing, CrossPointSettings::NORMAL,
lineSpacingValues),
makeEnumDescriptor("Reader Paragraph Alignment", &CrossPointSettings::paragraphAlignment,
CrossPointSettings::JUSTIFIED, paragraphAlignmentValues),
makeEnumDescriptor("Time to Sleep", &CrossPointSettings::sleepTimeout, CrossPointSettings::SLEEP_10_MIN,
sleepTimeoutValues),
makeEnumDescriptor("Refresh Frequency", &CrossPointSettings::refreshFrequency, CrossPointSettings::REFRESH_15,
refreshFrequencyValues),
{"Reader Screen Margin", SettingType::VALUE, &CrossPointSettings::screenMargin, 5, validateRange<5, 40>,
ValueRange{5, 40, 5}},
makeEnumDescriptor("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, CrossPointSettings::FIT,
sleepScreenCoverModeValues),
makeStringDescriptor("OPDS Server URL", opdsServerUrl, ""),
{"Text Anti-Aliasing", SettingType::TOGGLE, &CrossPointSettings::textAntiAliasing, 1, validateToggle, nullptr, 0},
makeEnumDescriptor("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, CrossPointSettings::HIDE_NEVER,
hideBatteryPercentageValues),
{"Long-press Chapter Skip", SettingType::TOGGLE, &CrossPointSettings::longPressChapterSkip, 1, validateToggle,
nullptr, 0},
{"Hyphenation", SettingType::TOGGLE, &CrossPointSettings::hyphenationEnabled, 0, validateToggle, nullptr, 0},
makeStringDescriptor("Username", opdsUsername, ""),
makeStringDescriptor("Password", opdsPassword, ""),
makeEnumDescriptor("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter,
CrossPointSettings::NO_FILTER, sleepScreenCoverFilterValues),
}};
bool CrossPointSettings::saveToFile() const {
// Make sure the directory exists
SdMan.mkdir("/.crosspoint");
@ -36,31 +147,15 @@ bool CrossPointSettings::saveToFile() const {
}
serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
serialization::writePod(outputFile, SETTINGS_COUNT);
serialization::writePod(outputFile, sleepScreen);
serialization::writePod(outputFile, extraParagraphSpacing);
serialization::writePod(outputFile, shortPwrBtn);
serialization::writePod(outputFile, statusBar);
serialization::writePod(outputFile, orientation);
serialization::writePod(outputFile, frontButtonLayout);
serialization::writePod(outputFile, sideButtonLayout);
serialization::writePod(outputFile, fontFamily);
serialization::writePod(outputFile, fontSize);
serialization::writePod(outputFile, lineSpacing);
serialization::writePod(outputFile, paragraphAlignment);
serialization::writePod(outputFile, sleepTimeout);
serialization::writePod(outputFile, refreshFrequency);
serialization::writePod(outputFile, screenMargin);
serialization::writePod(outputFile, sleepScreenCoverMode);
serialization::writeString(outputFile, std::string(opdsServerUrl));
serialization::writePod(outputFile, textAntiAliasing);
serialization::writePod(outputFile, hideBatteryPercentage);
serialization::writePod(outputFile, longPressChapterSkip);
serialization::writePod(outputFile, hyphenationEnabled);
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, static_cast<uint8_t>(CrossPointSettings::DESCRIPTOR_COUNT));
// Use descriptors to automatically serialize all uint8_t settings
uint8_t descriptorIndex = 0;
for (const auto& desc : descriptors) {
desc.save(outputFile, *this);
descriptorIndex++;
}
outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -68,8 +163,10 @@ bool CrossPointSettings::saveToFile() const {
}
bool CrossPointSettings::loadFromFile() {
Serial.printf("[%lu] [CPS] Loading settings from file\n", millis());
FsFile inputFile;
if (!SdMan.openFileForRead("CPS", SETTINGS_FILE, inputFile)) {
Serial.printf("[%lu] [CPS] Deserialization failed: Could not open settings file\n", millis());
return false;
}
@ -84,74 +181,26 @@ bool CrossPointSettings::loadFromFile() {
uint8_t fileSettingsCount = 0;
serialization::readPod(inputFile, fileSettingsCount);
// load settings that exist (support older files with fewer fields)
uint8_t settingsRead = 0;
do {
readAndValidate(inputFile, sleepScreen, SLEEP_SCREEN_MODE_COUNT);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, extraParagraphSpacing);
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, shortPwrBtn, SHORT_PWRBTN_COUNT);
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, statusBar, STATUS_BAR_MODE_COUNT);
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, orientation, ORIENTATION_COUNT);
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT);
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, sideButtonLayout, SIDE_BUTTON_LAYOUT_COUNT);
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;
serialization::readPod(inputFile, 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;
// New fields added at end for backward compatibility
} while (false);
// Use descriptors to automatically deserialize all uint8_t settings
uint8_t descriptorIndex = 0;
uint8_t filePosition = 0;
for (const auto& desc : descriptors) {
if (filePosition >= fileSettingsCount) {
break; // File has fewer settings than current version
}
desc.load(inputFile, *this);
if (!desc.validate(*this)) {
Serial.printf("[%lu] [CPS] Invalid value (0x%X) for %s, resetting to default\n", millis(), desc.getValue(*this),
desc.name);
desc.resetToDefault(*this);
}
descriptorIndex++;
filePosition++;
}
inputFile.close();
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
return true;
}

View File

@ -1,6 +1,112 @@
#pragma once
#include <array>
#include <cstdint>
#include <iosfwd>
#include <string>
#include <vector>
// Setting descriptor infrastructure
enum class SettingType { TOGGLE, ENUM, VALUE, STRING };
// Validator function pointer (not std::function to save memory)
using SettingValidator = bool (*)(uint8_t);
// Forward declare for descriptors
class CrossPointSettings;
// Forward declare file type
class FsFile;
// Base descriptor for all settings (non-virtual for constexpr)
struct SettingDescriptorBase {
const char* name; // Display name
SettingType type;
};
// Value range for VALUE type settings
struct ValueRange {
uint8_t min, max, step;
};
// Concrete descriptor for uint8_t settings (constexpr-compatible)
struct SettingDescriptor : public SettingDescriptorBase {
union {
uint8_t CrossPointSettings::* memberPtr; // For TOGGLE/ENUM/VALUE types
char* stringPtr; // For STRING type
};
uint8_t defaultValue;
SettingValidator validator; // Optional validator function
union {
// For ENUM types
struct {
const char* const* values;
uint8_t count;
} enumData;
// For VALUE types
ValueRange valueRange;
// For STRING types
struct {
const char* defaultString; // Default string value
size_t maxSize; // Max size of the string buffer
} stringData;
};
// Constexpr constructors for different setting types
// TOGGLE/ENUM constructor
constexpr SettingDescriptor(const char* name_, SettingType type_, uint8_t CrossPointSettings::* ptr, uint8_t defVal,
SettingValidator val, const char* const* enumVals, uint8_t enumCnt)
: SettingDescriptorBase{name_, type_},
memberPtr(ptr),
defaultValue(defVal),
validator(val),
enumData{enumVals, enumCnt} {}
// VALUE constructor
constexpr SettingDescriptor(const char* name_, SettingType type_, uint8_t CrossPointSettings::* ptr, uint8_t defVal,
SettingValidator val, ValueRange valRange)
: SettingDescriptorBase{name_, type_},
memberPtr(ptr),
defaultValue(defVal),
validator(val),
valueRange(valRange) {}
// STRING constructor
constexpr SettingDescriptor(const char* name_, SettingType type_, char* strPtr, const char* defStr, size_t maxSz)
: SettingDescriptorBase{name_, type_},
stringPtr(strPtr),
defaultValue(0),
validator(nullptr),
stringData{defStr, maxSz} {}
bool validate(const CrossPointSettings& settings) const;
uint8_t getValue(const CrossPointSettings& settings) const;
void setValue(CrossPointSettings& settings, uint8_t value) const;
void resetToDefault(CrossPointSettings& settings) const;
void save(FsFile& file, const CrossPointSettings& settings) const;
void load(FsFile& file, CrossPointSettings& settings) const;
// Helper to get enum value as string
const char* getEnumValueString(uint8_t index) const {
if (index < enumData.count && enumData.values) {
return enumData.values[index];
}
return "";
}
};
// Validator functions (constexpr for compile-time optimization)
constexpr bool validateToggle(uint8_t v) { return v <= 1; }
template <uint8_t MAX>
constexpr bool validateEnum(uint8_t v) {
return v < MAX;
}
template <uint8_t MIN, uint8_t MAX>
constexpr bool validateRange(uint8_t v) {
return v >= MIN && v <= MAX;
}
class CrossPointSettings {
private:
@ -15,13 +121,17 @@ class CrossPointSettings {
CrossPointSettings(const CrossPointSettings&) = delete;
CrossPointSettings& operator=(const CrossPointSettings&) = delete;
enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4, SLEEP_SCREEN_MODE_COUNT };
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT };
// Static constexpr array of all setting descriptors
static constexpr size_t DESCRIPTOR_COUNT = 23;
static const std::array<SettingDescriptor, DESCRIPTOR_COUNT> descriptors;
// Should match with SettingsActivity text
enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4 };
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1 };
enum SLEEP_SCREEN_COVER_FILTER {
NO_FILTER = 0,
BLACK_AND_WHITE = 1,
INVERTED_BLACK_AND_WHITE = 2,
SLEEP_SCREEN_COVER_FILTER_COUNT
};
// Status bar display type enum
@ -31,7 +141,6 @@ class CrossPointSettings {
FULL = 2,
FULL_WITH_PROGRESS_BAR = 3,
ONLY_PROGRESS_BAR = 4,
STATUS_BAR_MODE_COUNT
};
enum ORIENTATION {
@ -39,7 +148,6 @@ class CrossPointSettings {
LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
INVERTED = 2, // 480x800 logical coordinates, inverted
LANDSCAPE_CCW = 3, // 800x480 logical coordinates, native panel orientation
ORIENTATION_COUNT
};
// Front button layout options
@ -50,25 +158,23 @@ class CrossPointSettings {
LEFT_RIGHT_BACK_CONFIRM = 1,
LEFT_BACK_CONFIRM_RIGHT = 2,
BACK_CONFIRM_RIGHT_LEFT = 3,
FRONT_BUTTON_LAYOUT_COUNT
};
// Side button layout options
// Default: Previous, Next
// Swapped: Next, Previous
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTON_LAYOUT_COUNT };
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1 };
// Font family options
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_FAMILY_COUNT };
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2 };
// Font size options
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3, FONT_SIZE_COUNT };
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2, LINE_COMPRESSION_COUNT };
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 };
enum PARAGRAPH_ALIGNMENT {
JUSTIFIED = 0,
LEFT_ALIGN = 1,
CENTER_ALIGN = 2,
RIGHT_ALIGN = 3,
PARAGRAPH_ALIGNMENT_COUNT
};
// Auto-sleep timeout options (in minutes)
@ -78,7 +184,6 @@ class CrossPointSettings {
SLEEP_10_MIN = 2,
SLEEP_15_MIN = 3,
SLEEP_30_MIN = 4,
SLEEP_TIMEOUT_COUNT
};
// E-ink refresh frequency (pages between full refreshes)
@ -88,14 +193,13 @@ class CrossPointSettings {
REFRESH_10 = 2,
REFRESH_15 = 3,
REFRESH_30 = 4,
REFRESH_FREQUENCY_COUNT
};
// Short power button press actions
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, SHORT_PWRBTN_COUNT };
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2 };
// Hide battery percentage
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT };
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2 };
// Sleep screen settings
uint8_t sleepScreen = DARK;

View File

@ -520,7 +520,7 @@ void WifiSelectionActivity::renderNetworkList() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
renderer.drawCenteredText(UI_10_FONT_ID, top, "No networks found");
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press OK to scan again");
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press Connect to scan again");
} else {
// Calculate how many networks we can display
constexpr int startY = 60;

View File

@ -62,77 +62,84 @@ void CategorySettingsActivity::loop() {
}
// Handle navigation
const int totalItemsCount = descriptors.size() + actionItems.size();
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1);
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (totalItemsCount - 1);
updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0;
selectedSettingIndex = (selectedSettingIndex < totalItemsCount - 1) ? (selectedSettingIndex + 1) : 0;
updateRequired = true;
}
}
void CategorySettingsActivity::toggleCurrentSetting() {
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
const int totalItemsCount = descriptors.size() + actionItems.size();
if (selectedSettingIndex < 0 || selectedSettingIndex >= totalItemsCount) {
return;
}
const auto& setting = settingsList[selectedSettingIndex];
// Check if it's a descriptor or an action item
if (selectedSettingIndex < static_cast<int>(descriptors.size())) {
// Handle descriptor
const auto* desc = descriptors[selectedSettingIndex];
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
// Toggle the boolean value using the member pointer
const bool currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = !currentValue;
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
const int8_t currentValue = SETTINGS.*(setting.valuePtr);
if (currentValue + setting.valueRange.step > setting.valueRange.max) {
SETTINGS.*(setting.valuePtr) = setting.valueRange.min;
} else {
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
}
} else if (setting.type == SettingType::ACTION) {
if (strcmp(setting.name, "KOReader Sync") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
} else if (strcmp(setting.name, "OPDS Browser") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
} else if (strcmp(setting.name, "Clear Cache") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
} else if (strcmp(setting.name, "Check for updates") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
if (desc->type == SettingType::TOGGLE) {
uint8_t currentValue = desc->getValue(SETTINGS);
desc->setValue(SETTINGS, !currentValue);
} else if (desc->type == SettingType::ENUM) {
uint8_t currentValue = desc->getValue(SETTINGS);
desc->setValue(SETTINGS, (currentValue + 1) % desc->enumData.count);
} else if (desc->type == SettingType::VALUE) {
uint8_t currentValue = desc->getValue(SETTINGS);
if (currentValue + desc->valueRange.step > desc->valueRange.max) {
desc->setValue(SETTINGS, desc->valueRange.min);
} else {
desc->setValue(SETTINGS, currentValue + desc->valueRange.step);
}
}
SETTINGS.saveToFile();
} else {
return;
}
// Handle action item
const int actionIndex = selectedSettingIndex - descriptors.size();
const auto& action = actionItems[actionIndex];
SETTINGS.saveToFile();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
switch (action.type) {
case ActionItem::Type::KOREADER_SYNC:
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
break;
case ActionItem::Type::CALIBRE_SETTINGS:
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
break;
case ActionItem::Type::CLEAR_CACHE:
enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
break;
case ActionItem::Type::CHECK_UPDATES:
enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
break;
}
xSemaphoreGive(renderingMutex);
}
}
void CategorySettingsActivity::displayTaskLoop() {
@ -153,29 +160,31 @@ void CategorySettingsActivity::render() const {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header with category name
renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD);
// Draw selection highlight
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
// Draw all settings
for (int i = 0; i < settingsCount; i++) {
const int settingY = 60 + i * 30; // 30 pixels between settings
// Draw all descriptors
for (size_t i = 0; i < descriptors.size(); i++) {
const auto* desc = descriptors[i];
const int settingY = 60 + i * 30;
const bool isSelected = (i == selectedSettingIndex);
// Draw setting name
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected);
renderer.drawText(UI_10_FONT_ID, 20, settingY, desc->name, !isSelected);
// Draw value based on setting type
std::string valueText;
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
const bool value = SETTINGS.*(settingsList[i].valuePtr);
if (desc->type == SettingType::TOGGLE) {
const bool value = desc->getValue(SETTINGS);
valueText = value ? "ON" : "OFF";
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
valueText = settingsList[i].enumValues[value];
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
} else if (desc->type == SettingType::ENUM) {
const uint8_t value = desc->getValue(SETTINGS);
valueText = desc->getEnumValueString(value);
} else if (desc->type == SettingType::VALUE) {
valueText = std::to_string(desc->getValue(SETTINGS));
}
if (!valueText.empty()) {
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
@ -183,9 +192,22 @@ void CategorySettingsActivity::render() const {
}
}
// Draw all action items
for (size_t i = 0; i < actionItems.size(); i++) {
const auto& action = actionItems[i];
const int itemIndex = descriptors.size() + i;
const int settingY = 60 + itemIndex * 30;
const bool isSelected = (itemIndex == selectedSettingIndex);
// Draw action name
renderer.drawText(UI_10_FONT_ID, 20, settingY, action.name, !isSelected);
}
// Draw version text above button hints
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60, CROSSPOINT_VERSION);
// Draw help text
const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);

View File

@ -7,38 +7,14 @@
#include <string>
#include <vector>
#include "CrossPointSettings.h"
#include "activities/ActivityWithSubactivity.h"
class CrossPointSettings;
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE };
struct SettingInfo {
// Action items for the System category
struct ActionItem {
const char* name;
SettingType type;
uint8_t CrossPointSettings::* valuePtr;
std::vector<std::string> enumValues;
struct ValueRange {
uint8_t min;
uint8_t max;
uint8_t step;
};
ValueRange valueRange;
static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) {
return {name, SettingType::TOGGLE, ptr};
}
static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector<std::string> values) {
return {name, SettingType::ENUM, ptr, std::move(values)};
}
static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr}; }
static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) {
return {name, SettingType::VALUE, ptr, {}, valueRange};
}
enum class Type { KOREADER_SYNC, CALIBRE_SETTINGS, CLEAR_CACHE, CHECK_UPDATES };
Type type;
};
class CategorySettingsActivity final : public ActivityWithSubactivity {
@ -47,8 +23,8 @@ class CategorySettingsActivity final : public ActivityWithSubactivity {
bool updateRequired = false;
int selectedSettingIndex = 0;
const char* categoryName;
const SettingInfo* settingsList;
int settingsCount;
const std::vector<const SettingDescriptor*> descriptors;
const std::vector<ActionItem> actionItems;
const std::function<void()> onGoBack;
static void taskTrampoline(void* param);
@ -58,11 +34,12 @@ class CategorySettingsActivity final : public ActivityWithSubactivity {
public:
CategorySettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const char* categoryName,
const SettingInfo* settingsList, int settingsCount, const std::function<void()>& onGoBack)
const std::vector<const SettingDescriptor*>& descriptors,
const std::vector<ActionItem>& actionItems, const std::function<void()>& onGoBack)
: ActivityWithSubactivity("CategorySettings", renderer, mappedInput),
categoryName(categoryName),
settingsList(settingsList),
settingsCount(settingsCount),
descriptors(descriptors),
actionItems(actionItems),
onGoBack(onGoBack) {}
void onEnter() override;
void onExit() override;

View File

@ -3,6 +3,8 @@
#include <GfxRenderer.h>
#include <HardwareSerial.h>
#include <algorithm>
#include "CategorySettingsActivity.h"
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
@ -10,51 +12,12 @@
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
namespace {
constexpr int displaySettingsCount = 6;
const SettingInfo displaySettings[displaySettingsCount] = {
// Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}),
SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter,
{"None", "Contrast", "Inverted"}),
SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar,
{"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}),
SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}),
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};
constexpr int readerSettingsCount = 9;
const SettingInfo readerSettings[readerSettingsCount] = {
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}),
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment,
{"Justify", "Left", "Center", "Right"}),
SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled),
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing),
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)};
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"}),
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
{"Prev, Next", "Next, Prev"}),
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})};
constexpr int systemSettingsCount = 5;
const SettingInfo systemSettings[systemSettingsCount] = {
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
SettingInfo::Action("KOReader Sync"), SettingInfo::Action("OPDS Browser"), SettingInfo::Action("Clear Cache"),
SettingInfo::Action("Check for updates")};
} // namespace
// Helper function to find descriptor by member pointer
static const SettingDescriptor* findDescriptor(uint8_t CrossPointSettings::* memberPtr) {
auto it = std::find_if(CrossPointSettings::descriptors.begin(), CrossPointSettings::descriptors.end(),
[memberPtr](const SettingDescriptor& desc) { return desc.memberPtr == memberPtr; });
return (it != CrossPointSettings::descriptors.end()) ? &(*it) : nullptr;
}
void SettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<SettingsActivity*>(param);
@ -125,37 +88,51 @@ void SettingsActivity::loop() {
}
void SettingsActivity::enterCategory(int categoryIndex) {
if (categoryIndex < 0 || categoryIndex >= categoryCount) {
return;
std::vector<const SettingDescriptor*> descriptors;
std::vector<ActionItem> actionItems;
switch (categoryIndex) {
case 0: // Display
descriptors.push_back(findDescriptor(&CrossPointSettings::sleepScreen));
descriptors.push_back(findDescriptor(&CrossPointSettings::sleepScreenCoverMode));
descriptors.push_back(findDescriptor(&CrossPointSettings::sleepScreenCoverFilter));
descriptors.push_back(findDescriptor(&CrossPointSettings::statusBar));
descriptors.push_back(findDescriptor(&CrossPointSettings::hideBatteryPercentage));
descriptors.push_back(findDescriptor(&CrossPointSettings::refreshFrequency));
break;
case 1: // Reader
descriptors.push_back(findDescriptor(&CrossPointSettings::fontFamily));
descriptors.push_back(findDescriptor(&CrossPointSettings::fontSize));
descriptors.push_back(findDescriptor(&CrossPointSettings::lineSpacing));
descriptors.push_back(findDescriptor(&CrossPointSettings::screenMargin));
descriptors.push_back(findDescriptor(&CrossPointSettings::paragraphAlignment));
descriptors.push_back(findDescriptor(&CrossPointSettings::hyphenationEnabled));
descriptors.push_back(findDescriptor(&CrossPointSettings::orientation));
descriptors.push_back(findDescriptor(&CrossPointSettings::extraParagraphSpacing));
descriptors.push_back(findDescriptor(&CrossPointSettings::textAntiAliasing));
break;
case 2: // Controls
descriptors.push_back(findDescriptor(&CrossPointSettings::frontButtonLayout));
descriptors.push_back(findDescriptor(&CrossPointSettings::sideButtonLayout));
descriptors.push_back(findDescriptor(&CrossPointSettings::longPressChapterSkip));
descriptors.push_back(findDescriptor(&CrossPointSettings::shortPwrBtn));
break;
case 3: // System
descriptors.push_back(findDescriptor(&CrossPointSettings::sleepTimeout));
actionItems.push_back({"KOReader Sync", ActionItem::Type::KOREADER_SYNC});
actionItems.push_back({"Calibre Settings", ActionItem::Type::CALIBRE_SETTINGS});
actionItems.push_back({"Clear Cache", ActionItem::Type::CLEAR_CACHE});
actionItems.push_back({"Check for updates", ActionItem::Type::CHECK_UPDATES});
break;
}
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
const SettingInfo* settingsList = nullptr;
int settingsCount = 0;
switch (categoryIndex) {
case 0: // Display
settingsList = displaySettings;
settingsCount = displaySettingsCount;
break;
case 1: // Reader
settingsList = readerSettings;
settingsCount = readerSettingsCount;
break;
case 2: // Controls
settingsList = controlsSettings;
settingsCount = controlsSettingsCount;
break;
case 3: // System
settingsList = systemSettings;
settingsCount = systemSettingsCount;
break;
}
enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], settingsList,
settingsCount, [this] {
enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], descriptors,
actionItems, [this] {
exitActivity();
updateRequired = true;
}));

View File

@ -9,9 +9,6 @@
#include "activities/ActivityWithSubactivity.h"
class CrossPointSettings;
struct SettingInfo;
class SettingsActivity final : public ActivityWithSubactivity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;