Merge branch 'master' into xsn/idle_cpu_freq
This commit is contained in:
@@ -230,6 +230,7 @@ Accessible by pressing **Confirm** while inside a book.
|
|||||||
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
|
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
|
||||||
|
|
||||||
* **Images:** Embedded images in e-books will not render.
|
* **Images:** Embedded images in e-books will not render.
|
||||||
|
* **Cover Images:** Large cover images embedded into EPUB require several seconds (~10s for ~2000 pixel tall image) to convert for sleep screen and home screen thumbnail. Consider optimizing the EPUB with e.g. https://github.com/bigbag/epub-to-xtc-converter to speed this up.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -242,3 +243,5 @@ pio device monitor
|
|||||||
```
|
```
|
||||||
|
|
||||||
If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen.
|
If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen.
|
||||||
|
|
||||||
|
There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.bin`, `state.bin`, or `epub_*` cache directories in the `.crosspoint/` folder).
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ fi
|
|||||||
# --modified: files tracked by git that have been modified (staged or unstaged)
|
# --modified: files tracked by git that have been modified (staged or unstaged)
|
||||||
# --exclude-standard: ignores files in .gitignore
|
# --exclude-standard: ignores files in .gitignore
|
||||||
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
|
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
|
||||||
|
# Also exclude files in 'lib/Epub/Epub/hyphenation/generated/' as they are script-generated.
|
||||||
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
||||||
| grep -E '\.(c|cpp|h|hpp)$' \
|
| grep -E '\.(c|cpp|h|hpp)$' \
|
||||||
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
||||||
|
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
||||||
| xargs -r clang-format -style=file -i
|
| xargs -r clang-format -style=file -i
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "generated/hyph-en.trie.h"
|
#include "generated/hyph-en.trie.h"
|
||||||
#include "generated/hyph-es.trie.h"
|
#include "generated/hyph-es.trie.h"
|
||||||
#include "generated/hyph-fr.trie.h"
|
#include "generated/hyph-fr.trie.h"
|
||||||
|
#include "generated/hyph-it.trie.h"
|
||||||
#include "generated/hyph-ru.trie.h"
|
#include "generated/hyph-ru.trie.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -18,15 +19,17 @@ LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
|||||||
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
||||||
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||||
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
|
||||||
using EntryArray = std::array<LanguageEntry, 5>;
|
using EntryArray = std::array<LanguageEntry, 6>;
|
||||||
|
|
||||||
const EntryArray& entries() {
|
const EntryArray& entries() {
|
||||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||||
{"french", "fr", &frenchHyphenator},
|
{"french", "fr", &frenchHyphenator},
|
||||||
{"german", "de", &germanHyphenator},
|
{"german", "de", &germanHyphenator},
|
||||||
{"russian", "ru", &russianHyphenator},
|
{"russian", "ru", &russianHyphenator},
|
||||||
{"spanish", "es", &spanishHyphenator}}};
|
{"spanish", "es", &spanishHyphenator},
|
||||||
|
{"italian", "it", &italianHyphenator}}};
|
||||||
return kEntries;
|
return kEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
113
lib/Epub/Epub/hyphenation/generated/hyph-it.trie.h
Normal file
113
lib/Epub/Epub/hyphenation/generated/hyph-it.trie.h
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "../SerializedHyphenationTrie.h"
|
||||||
|
|
||||||
|
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||||
|
alignas(4) constexpr uint8_t it_trie_data[] = {
|
||||||
|
0x00, 0x00, 0x05, 0xC4, 0x17, 0x0C, 0x33, 0x35, 0x0C, 0x29, 0x22, 0x0D, 0x3E, 0x0B, 0x47, 0x20,
|
||||||
|
0x0D, 0x16, 0x0B, 0x34, 0x0D, 0x21, 0x0C, 0x3D, 0x1F, 0x0C, 0x2A, 0x17, 0x2A, 0x0B, 0x02, 0x0C,
|
||||||
|
0x01, 0x02, 0x16, 0x02, 0x0D, 0x0C, 0x0C, 0x0D, 0x03, 0x0C, 0x01, 0x0C, 0x0E, 0x0D, 0x04, 0x02,
|
||||||
|
0x0B, 0xA0, 0x00, 0x42, 0x21, 0x6E, 0xFD, 0xA0, 0x00, 0x72, 0x21, 0x6E, 0xFD, 0xA1, 0x00, 0x61,
|
||||||
|
0x6D, 0xFD, 0x21, 0x69, 0xFB, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x6E, 0xEC, 0xFD, 0xA0, 0x00, 0x91,
|
||||||
|
0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x00, 0xA2, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0xA0,
|
||||||
|
0x00, 0xC2, 0x21, 0x6D, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00,
|
||||||
|
0xE1, 0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA3, 0x01, 0x11,
|
||||||
|
0x61, 0x69, 0x6F, 0xDF, 0xEE, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
|
||||||
|
0x69, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xA1, 0x01, 0x11, 0x69, 0xFD, 0xA0, 0x01, 0x12,
|
||||||
|
0x21, 0x75, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x78, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x6B, 0xFD, 0x21,
|
||||||
|
0x6E, 0xFD, 0xA0, 0x00, 0x71, 0x21, 0x65, 0xFD, 0x22, 0x61, 0x65, 0xF7, 0xFD, 0x21, 0x72, 0xFB,
|
||||||
|
0xA0, 0x01, 0x52, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0xA0,
|
||||||
|
0x01, 0x71, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x00,
|
||||||
|
0x61, 0x21, 0x6F, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x70, 0xFF, 0x50, 0x21, 0x6F, 0xFC, 0x21, 0x74,
|
||||||
|
0xFD, 0x22, 0x70, 0x72, 0xF3, 0xFD, 0x21, 0x61, 0xE8, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xF1, 0x22,
|
||||||
|
0x6C, 0x72, 0xFD, 0xFD, 0x21, 0x69, 0xE3, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFF, 0x43, 0xA0, 0x01,
|
||||||
|
0x11, 0x25, 0x61, 0x68, 0x6F, 0x72, 0x73, 0xE8, 0xEE, 0xF6, 0xF9, 0xFD, 0xA0, 0x01, 0x82, 0x21,
|
||||||
|
0x72, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x01,
|
||||||
|
0xA2, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x75, 0xFF, 0x4C, 0x42, 0x6C,
|
||||||
|
0x72, 0xFF, 0xFC, 0xFF, 0x48, 0x21, 0x62, 0xF9, 0x22, 0x68, 0x75, 0xEF, 0xFD, 0x47, 0x63, 0x64,
|
||||||
|
0x6C, 0x6E, 0x70, 0x72, 0x74, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF,
|
||||||
|
0x5C, 0xFF, 0x5C, 0x21, 0x73, 0xEA, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0xA1, 0x01, 0x11, 0x72,
|
||||||
|
0xFD, 0x41, 0x6E, 0xFF, 0x15, 0x21, 0x67, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x74, 0xFD, 0x21, 0x6C,
|
||||||
|
0xFD, 0x22, 0x61, 0x65, 0xF4, 0xFD, 0x52, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x6C, 0x6E, 0x6F,
|
||||||
|
0x70, 0x72, 0x73, 0x74, 0x77, 0x68, 0x6A, 0x6B, 0x7A, 0xFE, 0xC2, 0xFE, 0xCD, 0xFE, 0xF7, 0xFF,
|
||||||
|
0x12, 0xFF, 0x20, 0xFF, 0x37, 0xFF, 0x46, 0xFF, 0x55, 0xFF, 0x6B, 0xFF, 0x8B, 0xFF, 0xA5, 0xFF,
|
||||||
|
0xC2, 0xFF, 0xE6, 0xFF, 0xFB, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xA0, 0x01, 0xE2,
|
||||||
|
0xA0, 0x00, 0xD1, 0x24, 0x61, 0x65, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0x21, 0x6F, 0xF4, 0x21,
|
||||||
|
0x61, 0xF1, 0xA0, 0x01, 0xE1, 0x21, 0x2E, 0xFD, 0x24, 0x69, 0x75, 0x79, 0x74, 0xEB, 0xF4, 0xF7,
|
||||||
|
0xFD, 0x21, 0x75, 0xDF, 0xA0, 0x00, 0x51, 0x22, 0x69, 0x77, 0xFA, 0xFD, 0x21, 0x69, 0xD7, 0xAE,
|
||||||
|
0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0x6C, 0x72, 0x2E, 0x27,
|
||||||
|
0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xF5, 0xF5, 0xE3, 0xE3, 0x22, 0x2E,
|
||||||
|
0x27, 0xC4, 0xC7, 0xC6, 0x00, 0x51, 0x68, 0x2E, 0x27, 0x62, 0x72, 0x6E, 0xFF, 0xBF, 0xFF, 0xBF,
|
||||||
|
0xFF, 0xFB, 0xFF, 0xBF, 0xFE, 0xFB, 0xFF, 0xBF, 0xD0, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6B,
|
||||||
|
0x6D, 0x6E, 0x71, 0x73, 0x74, 0x7A, 0x68, 0x6C, 0x72, 0x2E, 0x27, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF,
|
||||||
|
0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF,
|
||||||
|
0xAA, 0xFF, 0xEB, 0xFF, 0xBC, 0xFF, 0xBC, 0xFF, 0xAA, 0xFF, 0xAA, 0xCE, 0x02, 0x01, 0x62, 0x64,
|
||||||
|
0x67, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFF, 0x77, 0xFF, 0x77,
|
||||||
|
0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x89, 0xFF, 0x77, 0xFF, 0x77,
|
||||||
|
0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xCA, 0x02, 0x01, 0x62, 0x67, 0x66, 0x6E, 0x6C,
|
||||||
|
0x72, 0x73, 0x74, 0x2E, 0x27, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x5C, 0xFF,
|
||||||
|
0x5C, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xA0, 0x02, 0x12, 0xA1, 0x00, 0x51, 0x74,
|
||||||
|
0xFD, 0xD1, 0x02, 0x01, 0x62, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74,
|
||||||
|
0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0xFB, 0xFF,
|
||||||
|
0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF,
|
||||||
|
0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0x41, 0x70, 0xFD, 0x4D, 0xCB, 0x02, 0x01, 0x62, 0x64,
|
||||||
|
0x68, 0x69, 0x6C, 0x6D, 0x6E, 0x72, 0x76, 0x2E, 0x27, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFF,
|
||||||
|
0xFC, 0xFE, 0xF9, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xC2,
|
||||||
|
0x02, 0x01, 0x2E, 0x27, 0xFE, 0xC3, 0xFE, 0xC3, 0xCB, 0x02, 0x01, 0x67, 0x66, 0x68, 0x6B, 0x6C,
|
||||||
|
0x6D, 0x72, 0x73, 0x74, 0x2E, 0x27, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC,
|
||||||
|
0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xA0, 0x02, 0x33, 0x42,
|
||||||
|
0x2E, 0x27, 0xFE, 0x93, 0xFE, 0x93, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||||
|
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFE, 0x8C,
|
||||||
|
0xFE, 0x8C, 0xFE, 0x8C, 0xFF, 0xF6, 0xFE, 0x8C, 0xFE, 0x9E, 0xFE, 0x9E, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||||
|
0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||||
|
0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFF, 0xF9, 0xCF, 0x02, 0x01, 0x62, 0x63, 0x66, 0x6C, 0x6D,
|
||||||
|
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
|
||||||
|
0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
|
||||||
|
0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xA0, 0x02, 0x62, 0xA1, 0x01, 0xE1, 0x6E, 0xFD,
|
||||||
|
0x21, 0x72, 0xF8, 0x21, 0x65, 0xFD, 0xA1, 0x01, 0xE1, 0x66, 0xFD, 0x41, 0x74, 0xFE, 0x07, 0x21,
|
||||||
|
0x69, 0xFC, 0x21, 0x65, 0xFD, 0xD3, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D,
|
||||||
|
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x7A, 0x68, 0x2E, 0x27, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||||
|
0xFD, 0xFD, 0xFD, 0xFF, 0xE6, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||||
|
0xFD, 0xFD, 0xFD, 0xFF, 0xF1, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||||
|
0xFD, 0xA0, 0x02, 0x82, 0xA1, 0x01, 0xE1, 0x65, 0xFD, 0x21, 0x63, 0xF8, 0xA1, 0x01, 0xE1, 0x69,
|
||||||
|
0xFD, 0xCB, 0x02, 0x01, 0x64, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x7A, 0x2E, 0x27, 0xFD,
|
||||||
|
0xB1, 0xFD, 0xC3, 0xFD, 0xC3, 0xFF, 0xF3, 0xFD, 0xB1, 0xFD, 0xC3, 0xFF, 0xFB, 0xFD, 0xB1, 0xFD,
|
||||||
|
0xB1, 0xFD, 0xB1, 0xFD, 0xB1, 0xC3, 0x02, 0x01, 0x71, 0x2E, 0x27, 0xFD, 0x8D, 0xFD, 0x8D, 0xFD,
|
||||||
|
0x8D, 0xA0, 0x02, 0x53, 0xA1, 0x01, 0xE1, 0x73, 0xFD, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66,
|
||||||
|
0x68, 0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x78, 0x77, 0x7A, 0x2E,
|
||||||
|
0x27, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x8B, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
|
||||||
|
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFF, 0xFB, 0xFD,
|
||||||
|
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0x43, 0x6D, 0x2E, 0x27, 0xFD,
|
||||||
|
0x37, 0xFD, 0x37, 0xFD, 0x37, 0xA0, 0x02, 0xC2, 0xA1, 0x02, 0x32, 0x6D, 0xFD, 0x41, 0x6E, 0xFE,
|
||||||
|
0x8F, 0x4B, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0xFD, 0x21, 0xFD,
|
||||||
|
0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD,
|
||||||
|
0x21, 0xFD, 0x21, 0xA0, 0x02, 0xE1, 0x22, 0x2E, 0x27, 0xFD, 0xFD, 0xC7, 0x02, 0xA2, 0x68, 0x73,
|
||||||
|
0x70, 0x74, 0x7A, 0x2E, 0x27, 0xFF, 0xC0, 0xFF, 0xCD, 0xFF, 0xD2, 0xFF, 0xD6, 0xFC, 0xF7, 0xFF,
|
||||||
|
0xF8, 0xFF, 0xFB, 0xC1, 0x00, 0x51, 0x2E, 0xFC, 0xDF, 0x41, 0x68, 0xFF, 0x18, 0xA1, 0x00, 0x51,
|
||||||
|
0x63, 0xFC, 0xC1, 0x01, 0xE1, 0x73, 0xFE, 0xB6, 0xC2, 0x00, 0x51, 0x6B, 0x73, 0xFC, 0xCA, 0xFC,
|
||||||
|
0x06, 0xD2, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73,
|
||||||
|
0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1,
|
||||||
|
0xFF, 0xE2, 0xFC, 0xD3, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xD3, 0xFF, 0xEC, 0xFF, 0xF1,
|
||||||
|
0xFC, 0xC1, 0xFC, 0xC1, 0xFF, 0xF7, 0xFC, 0xC1, 0xFE, 0x2E, 0xC6, 0x02, 0x01, 0x63, 0x6C, 0x72,
|
||||||
|
0x76, 0x2E, 0x27, 0xFC, 0x88, 0xFC, 0x9A, 0xFC, 0x9A, 0xFC, 0x88, 0xFC, 0x88, 0xFD, 0xF5, 0x41,
|
||||||
|
0x72, 0xFB, 0xAF, 0xA0, 0x02, 0xF2, 0xC5, 0x02, 0x01, 0x68, 0x61, 0x79, 0x2E, 0x27, 0xFC, 0x7E,
|
||||||
|
0xFF, 0xF9, 0xFF, 0xFD, 0xFC, 0x6C, 0xFC, 0x6C, 0xCA, 0x02, 0x01, 0x62, 0x63, 0x66, 0x68, 0x6D,
|
||||||
|
0x70, 0x74, 0x77, 0x2E, 0x27, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC,
|
||||||
|
0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x42, 0x6F, 0x69, 0xFC, 0x48, 0xFC, 0x27,
|
||||||
|
0xCB, 0x02, 0x01, 0x62, 0x64, 0x6C, 0x6E, 0x70, 0x74, 0x73, 0x76, 0x7A, 0x2E, 0x27, 0xFC, 0x32,
|
||||||
|
0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32,
|
||||||
|
0xFC, 0x32, 0xFD, 0x9F, 0x5A, 0x2E, 0x27, 0x61, 0x65, 0x6F, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68,
|
||||||
|
0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xFB,
|
||||||
|
0xC2, 0xFB, 0xF9, 0xFC, 0x14, 0xFC, 0x23, 0xFC, 0x28, 0xFC, 0x2B, 0xFC, 0x64, 0xFC, 0x97, 0xFC,
|
||||||
|
0xC4, 0xFC, 0xED, 0xFD, 0x27, 0xFD, 0x4B, 0xFD, 0x54, 0xFD, 0x82, 0xFD, 0xC4, 0xFE, 0x11, 0xFE,
|
||||||
|
0x5D, 0xFE, 0x81, 0xFE, 0x95, 0xFF, 0x17, 0xFF, 0x4D, 0xFF, 0x86, 0xFF, 0xA2, 0xFF, 0xB4, 0xFF,
|
||||||
|
0xD5, 0xFF, 0xDC,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr SerializedHyphenationPatterns it_patterns = {
|
||||||
|
it_trie_data,
|
||||||
|
sizeof(it_trie_data),
|
||||||
|
};
|
||||||
@@ -11,7 +11,7 @@ const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
|||||||
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
||||||
|
|
||||||
// Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
|
// Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
|
||||||
constexpr size_t MIN_SIZE_FOR_POPUP = 50 * 1024; // 50KB
|
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
|
||||||
|
|
||||||
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
||||||
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
default_envs = default
|
default_envs = default
|
||||||
|
|
||||||
[crosspoint]
|
[crosspoint]
|
||||||
version = 0.16.0
|
version = 1.0.0
|
||||||
|
|
||||||
[base]
|
[base]
|
||||||
platform = espressif32 @ 6.12.0
|
platform = espressif32 @ 6.12.0
|
||||||
@@ -60,3 +60,9 @@ extends = base
|
|||||||
build_flags =
|
build_flags =
|
||||||
${base.build_flags}
|
${base.build_flags}
|
||||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
|
||||||
|
|
||||||
|
[env:gh_release_rc]
|
||||||
|
extends = base
|
||||||
|
build_flags =
|
||||||
|
${base.build_flags}
|
||||||
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
|
||||||
|
|||||||
@@ -1,32 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
ESP32 Serial Monitor with Memory Graph
|
||||||
|
|
||||||
|
This script provides a real-time serial monitor for ESP32 devices with
|
||||||
|
integrated memory usage graphing capabilities. It reads serial output,
|
||||||
|
parses memory information, and displays it in both console and graphical form.
|
||||||
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from collections import deque
|
from collections import deque
|
||||||
import time
|
|
||||||
|
|
||||||
# Try to import potentially missing packages
|
# Try to import potentially missing packages
|
||||||
|
PACKAGE_MAPPING: dict[str, str] = {
|
||||||
|
"serial": "pyserial",
|
||||||
|
"colorama": "colorama",
|
||||||
|
"matplotlib": "matplotlib",
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import serial
|
import serial
|
||||||
from colorama import init, Fore, Style
|
from colorama import init, Fore, Style
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import matplotlib.animation as animation
|
from matplotlib import animation
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
missing_package = e.name
|
ERROR_MSG = str(e).lower()
|
||||||
|
missing_packages = [pkg for mod, pkg in PACKAGE_MAPPING.items() if mod in ERROR_MSG]
|
||||||
|
|
||||||
|
if not missing_packages:
|
||||||
|
# Fallback if mapping doesn't cover
|
||||||
|
missing_packages = ["pyserial", "colorama", "matplotlib"]
|
||||||
|
|
||||||
print("\n" + "!" * 50)
|
print("\n" + "!" * 50)
|
||||||
print(f" Error: The required package '{missing_package}' is not installed.")
|
print(f" Error: Required package(s) not installed: {', '.join(missing_packages)}")
|
||||||
print("!" * 50)
|
print("!" * 50)
|
||||||
|
|
||||||
print(f"\nTo fix this, please run the following command in your terminal:\n")
|
print("\nTo fix this, please run the following command in your terminal:\n")
|
||||||
|
INSTALL_CMD = "pip install " if sys.platform.startswith("win") else "pip3 install "
|
||||||
install_cmd = "pip install "
|
print(f" {INSTALL_CMD}{' '.join(missing_packages)}")
|
||||||
packages = []
|
|
||||||
if 'serial' in str(e): packages.append("pyserial")
|
|
||||||
if 'colorama' in str(e): packages.append("colorama")
|
|
||||||
if 'matplotlib' in str(e): packages.append("matplotlib")
|
|
||||||
|
|
||||||
print(f" {install_cmd}{' '.join(packages)}")
|
|
||||||
|
|
||||||
print("\nExiting...")
|
print("\nExiting...")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -34,50 +48,92 @@ except ImportError as e:
|
|||||||
# --- Global Variables for Data Sharing ---
|
# --- Global Variables for Data Sharing ---
|
||||||
# Store last 50 data points
|
# Store last 50 data points
|
||||||
MAX_POINTS = 50
|
MAX_POINTS = 50
|
||||||
time_data = deque(maxlen=MAX_POINTS)
|
time_data: deque[str] = deque(maxlen=MAX_POINTS)
|
||||||
free_mem_data = deque(maxlen=MAX_POINTS)
|
free_mem_data: deque[float] = deque(maxlen=MAX_POINTS)
|
||||||
total_mem_data = deque(maxlen=MAX_POINTS)
|
total_mem_data: deque[float] = deque(maxlen=MAX_POINTS)
|
||||||
data_lock = threading.Lock() # Prevent reading while writing
|
data_lock: threading.Lock = threading.Lock() # Prevent reading while writing
|
||||||
|
|
||||||
# Initialize colors
|
# Initialize colors
|
||||||
init(autoreset=True)
|
init(autoreset=True)
|
||||||
|
|
||||||
def get_color_for_line(line):
|
# Color mapping for log lines
|
||||||
|
COLOR_KEYWORDS: dict[str, list[str]] = {
|
||||||
|
Fore.RED: ["ERROR", "[ERR]", "[SCT]", "FAILED", "WARNING"],
|
||||||
|
Fore.CYAN: ["[MEM]", "FREE:"],
|
||||||
|
Fore.MAGENTA: [
|
||||||
|
"[GFX]",
|
||||||
|
"[ERS]",
|
||||||
|
"DISPLAY",
|
||||||
|
"RAM WRITE",
|
||||||
|
"RAM COMPLETE",
|
||||||
|
"REFRESH",
|
||||||
|
"POWERING ON",
|
||||||
|
"FRAME BUFFER",
|
||||||
|
"LUT",
|
||||||
|
],
|
||||||
|
Fore.GREEN: [
|
||||||
|
"[EBP]",
|
||||||
|
"[BMC]",
|
||||||
|
"[ZIP]",
|
||||||
|
"[PARSER]",
|
||||||
|
"[EHP]",
|
||||||
|
"LOADING EPUB",
|
||||||
|
"CACHE",
|
||||||
|
"DECOMPRESSED",
|
||||||
|
"PARSING",
|
||||||
|
],
|
||||||
|
Fore.YELLOW: ["[ACT]", "ENTERING ACTIVITY", "EXITING ACTIVITY"],
|
||||||
|
Fore.BLUE: ["RENDERED PAGE", "[LOOP]", "DURATION", "WAIT COMPLETE"],
|
||||||
|
Fore.LIGHTYELLOW_EX: [
|
||||||
|
"[CPS]",
|
||||||
|
"SETTINGS",
|
||||||
|
"[CLEAR_CACHE]",
|
||||||
|
"[CHAP]",
|
||||||
|
"[OPDS]",
|
||||||
|
"[COF]",
|
||||||
|
],
|
||||||
|
Fore.LIGHTBLACK_EX: [
|
||||||
|
"ESP-ROM",
|
||||||
|
"BUILD:",
|
||||||
|
"RST:",
|
||||||
|
"BOOT:",
|
||||||
|
"SPIWP:",
|
||||||
|
"MODE:",
|
||||||
|
"LOAD:",
|
||||||
|
"ENTRY",
|
||||||
|
"[SD]",
|
||||||
|
"STARTING CROSSPOINT",
|
||||||
|
"VERSION",
|
||||||
|
],
|
||||||
|
Fore.LIGHTCYAN_EX: ["[RBS]"],
|
||||||
|
Fore.LIGHTMAGENTA_EX: [
|
||||||
|
"[KRS]",
|
||||||
|
"EINKDISPLAY:",
|
||||||
|
"STATIC FRAME",
|
||||||
|
"INITIALIZING",
|
||||||
|
"SPI INITIALIZED",
|
||||||
|
"GPIO PINS",
|
||||||
|
"RESETTING",
|
||||||
|
"SSD1677",
|
||||||
|
"E-INK",
|
||||||
|
],
|
||||||
|
Fore.LIGHTGREEN_EX: ["[FNS]", "FOOTNOTE"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=R0912
|
||||||
|
def get_color_for_line(line: str) -> str:
|
||||||
"""
|
"""
|
||||||
Classify log lines by type and assign appropriate colors.
|
Classify log lines by type and assign appropriate colors.
|
||||||
"""
|
"""
|
||||||
line_upper = line.upper()
|
line_upper = line.upper()
|
||||||
|
for color, keywords in COLOR_KEYWORDS.items():
|
||||||
if any(keyword in line_upper for keyword in ["ERROR", "[ERR]", "[SCT]", "FAILED", "WARNING"]):
|
if any(keyword in line_upper for keyword in keywords):
|
||||||
return Fore.RED
|
return color
|
||||||
if "[MEM]" in line_upper or "FREE:" in line_upper:
|
|
||||||
return Fore.CYAN
|
|
||||||
if any(keyword in line_upper for keyword in ["[GFX]", "[ERS]", "DISPLAY", "RAM WRITE", "RAM COMPLETE", "REFRESH", "POWERING ON", "FRAME BUFFER", "LUT"]):
|
|
||||||
return Fore.MAGENTA
|
|
||||||
if any(keyword in line_upper for keyword in ["[EBP]", "[BMC]", "[ZIP]", "[PARSER]", "[EHP]", "LOADING EPUB", "CACHE", "DECOMPRESSED", "PARSING"]):
|
|
||||||
return Fore.GREEN
|
|
||||||
if "[ACT]" in line_upper or "ENTERING ACTIVITY" in line_upper or "EXITING ACTIVITY" in line_upper:
|
|
||||||
return Fore.YELLOW
|
|
||||||
if any(keyword in line_upper for keyword in ["RENDERED PAGE", "[LOOP]", "DURATION", "WAIT COMPLETE"]):
|
|
||||||
return Fore.BLUE
|
|
||||||
if any(keyword in line_upper for keyword in ["[CPS]", "SETTINGS", "[CLEAR_CACHE]"]):
|
|
||||||
return Fore.LIGHTYELLOW_EX
|
|
||||||
if any(keyword in line_upper for keyword in ["ESP-ROM", "BUILD:", "RST:", "BOOT:", "SPIWP:", "MODE:", "LOAD:", "ENTRY", "[SD]", "STARTING CROSSPOINT", "VERSION"]):
|
|
||||||
return Fore.LIGHTBLACK_EX
|
|
||||||
if "[RBS]" in line_upper:
|
|
||||||
return Fore.LIGHTCYAN_EX
|
|
||||||
if "[KRS]" in line_upper:
|
|
||||||
return Fore.LIGHTMAGENTA_EX
|
|
||||||
if any(keyword in line_upper for keyword in ["EINKDISPLAY:", "STATIC FRAME", "INITIALIZING", "SPI INITIALIZED", "GPIO PINS", "RESETTING", "SSD1677", "E-INK"]):
|
|
||||||
return Fore.LIGHTMAGENTA_EX
|
|
||||||
if any(keyword in line_upper for keyword in ["[FNS]", "FOOTNOTE"]):
|
|
||||||
return Fore.LIGHTGREEN_EX
|
|
||||||
if any(keyword in line_upper for keyword in ["[CHAP]", "[OPDS]", "[COF]"]):
|
|
||||||
return Fore.LIGHTYELLOW_EX
|
|
||||||
|
|
||||||
return Fore.WHITE
|
return Fore.WHITE
|
||||||
|
|
||||||
def parse_memory_line(line):
|
|
||||||
|
def parse_memory_line(line: str) -> tuple[int | None, int | None]:
|
||||||
"""
|
"""
|
||||||
Extracts Free and Total bytes from the specific log line.
|
Extracts Free and Total bytes from the specific log line.
|
||||||
Format: [MEM] Free: 196344 bytes, Total: 226412 bytes, Min Free: 112620 bytes
|
Format: [MEM] Free: 196344 bytes, Total: 226412 bytes, Min Free: 112620 bytes
|
||||||
@@ -93,12 +149,29 @@ def parse_memory_line(line):
|
|||||||
return None, None
|
return None, None
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
def serial_worker(port, baud):
|
|
||||||
|
def serial_worker(port: str, baud: int, kwargs: dict[str, str]) -> None:
|
||||||
"""
|
"""
|
||||||
Runs in a background thread. Handles reading serial, printing to console,
|
Runs in a background thread. Handles reading serial, printing to console,
|
||||||
and updating the data lists.
|
and updating the data lists.
|
||||||
"""
|
"""
|
||||||
print(f"{Fore.CYAN}--- Opening {port} at {baud} baud ---{Style.RESET_ALL}")
|
print(f"{Fore.CYAN}--- Opening {port} at {baud} baud ---{Style.RESET_ALL}")
|
||||||
|
filter_keyword = kwargs.get("filter", "").lower()
|
||||||
|
suppress = kwargs.get("suppress", "").lower()
|
||||||
|
if filter_keyword and suppress and filter_keyword == suppress:
|
||||||
|
print(
|
||||||
|
f"{Fore.YELLOW}Warning: Filter and Suppress keywords are the same. "
|
||||||
|
f"This may result in no output.{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
if filter_keyword:
|
||||||
|
print(
|
||||||
|
f"{Fore.YELLOW}Filtering lines to only show those containing: "
|
||||||
|
f"'{filter_keyword}'{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
if suppress:
|
||||||
|
print(
|
||||||
|
f"{Fore.YELLOW}Suppressing lines containing: '{suppress}'{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ser = serial.Serial(port, baud, timeout=0.1)
|
ser = serial.Serial(port, baud, timeout=0.1)
|
||||||
@@ -111,7 +184,7 @@ def serial_worker(port, baud):
|
|||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
raw_data = ser.readline().decode('utf-8', errors='replace')
|
raw_data = ser.readline().decode("utf-8", errors="replace")
|
||||||
|
|
||||||
if not raw_data:
|
if not raw_data:
|
||||||
continue
|
continue
|
||||||
@@ -127,33 +200,38 @@ def serial_worker(port, baud):
|
|||||||
# Check for Memory Line
|
# Check for Memory Line
|
||||||
if "[MEM]" in formatted_line:
|
if "[MEM]" in formatted_line:
|
||||||
free_val, total_val = parse_memory_line(formatted_line)
|
free_val, total_val = parse_memory_line(formatted_line)
|
||||||
if free_val is not None:
|
if free_val is not None and total_val is not None:
|
||||||
with data_lock:
|
with data_lock:
|
||||||
time_data.append(pc_time)
|
time_data.append(pc_time)
|
||||||
free_mem_data.append(free_val / 1024) # Convert to KB
|
free_mem_data.append(free_val / 1024) # Convert to KB
|
||||||
total_mem_data.append(total_val / 1024) # Convert to KB
|
total_mem_data.append(total_val / 1024) # Convert to KB
|
||||||
|
# Apply filters
|
||||||
|
if filter_keyword and filter_keyword not in formatted_line.lower():
|
||||||
|
continue
|
||||||
|
if suppress and suppress in formatted_line.lower():
|
||||||
|
continue
|
||||||
# Print to console
|
# Print to console
|
||||||
line_color = get_color_for_line(formatted_line)
|
line_color = get_color_for_line(formatted_line)
|
||||||
print(f"{line_color}{formatted_line}")
|
print(f"{line_color}{formatted_line}")
|
||||||
|
|
||||||
except OSError:
|
except (OSError, UnicodeDecodeError):
|
||||||
print(f"{Fore.RED}Device disconnected.{Style.RESET_ALL}")
|
print(f"{Fore.RED}Device disconnected or data error.{Style.RESET_ALL}")
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except KeyboardInterrupt:
|
||||||
# If thread is killed violently (e.g. main exit), silence errors
|
# If thread is killed violently (e.g. main exit), silence errors
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
if 'ser' in locals() and ser.is_open:
|
if "ser" in locals() and ser.is_open:
|
||||||
ser.close()
|
ser.close()
|
||||||
|
|
||||||
def update_graph(frame):
|
|
||||||
|
def update_graph(frame) -> list: # pylint: disable=unused-argument
|
||||||
"""
|
"""
|
||||||
Called by Matplotlib animation to redraw the chart.
|
Called by Matplotlib animation to redraw the chart.
|
||||||
"""
|
"""
|
||||||
with data_lock:
|
with data_lock:
|
||||||
if not time_data:
|
if not time_data:
|
||||||
return
|
return []
|
||||||
|
|
||||||
# Convert deques to lists for plotting
|
# Convert deques to lists for plotting
|
||||||
x = list(time_data)
|
x = list(time_data)
|
||||||
@@ -163,52 +241,105 @@ def update_graph(frame):
|
|||||||
plt.cla() # Clear axis
|
plt.cla() # Clear axis
|
||||||
|
|
||||||
# Plot Total RAM
|
# Plot Total RAM
|
||||||
plt.plot(x, y_total, label='Total RAM (KB)', color='red', linestyle='--')
|
plt.plot(x, y_total, label="Total RAM (KB)", color="red", linestyle="--")
|
||||||
|
|
||||||
# Plot Free RAM
|
# Plot Free RAM
|
||||||
plt.plot(x, y_free, label='Free RAM (KB)', color='green', marker='o')
|
plt.plot(x, y_free, label="Free RAM (KB)", color="green", marker="o")
|
||||||
|
|
||||||
# Fill area under Free RAM
|
# Fill area under Free RAM
|
||||||
plt.fill_between(x, y_free, color='green', alpha=0.1)
|
plt.fill_between(x, y_free, color="green", alpha=0.1)
|
||||||
|
|
||||||
plt.title("ESP32 Memory Monitor")
|
plt.title("ESP32 Memory Monitor")
|
||||||
plt.ylabel("Memory (KB)")
|
plt.ylabel("Memory (KB)")
|
||||||
plt.xlabel("Time")
|
plt.xlabel("Time")
|
||||||
plt.legend(loc='upper left')
|
plt.legend(loc="upper left")
|
||||||
plt.grid(True, linestyle=':', alpha=0.6)
|
plt.grid(True, linestyle=":", alpha=0.6)
|
||||||
|
|
||||||
# Rotate date labels
|
# Rotate date labels
|
||||||
plt.xticks(rotation=45, ha='right')
|
plt.xticks(rotation=45, ha="right")
|
||||||
plt.tight_layout()
|
plt.tight_layout()
|
||||||
|
|
||||||
def main():
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""
|
||||||
|
Main entry point for the ESP32 monitor application.
|
||||||
|
Sets up argument parsing, starts serial monitoring thread, and initializes the memory graph.
|
||||||
|
"""
|
||||||
parser = argparse.ArgumentParser(description="ESP32 Monitor with Graph")
|
parser = argparse.ArgumentParser(description="ESP32 Monitor with Graph")
|
||||||
parser.add_argument("port", nargs="?", default="/dev/ttyACM0", help="Serial port")
|
if sys.platform.startswith("win"):
|
||||||
parser.add_argument("--baud", type=int, default=115200, help="Baud rate")
|
default_port = "COM8"
|
||||||
|
elif sys.platform.startswith("darwin"):
|
||||||
|
default_port = "/dev/cu.usbmodem101"
|
||||||
|
else:
|
||||||
|
default_port = "/dev/ttyACM0"
|
||||||
|
default_baudrate = 115200
|
||||||
|
parser.add_argument(
|
||||||
|
"port",
|
||||||
|
nargs="?",
|
||||||
|
default=default_port,
|
||||||
|
help=f"Serial port (default: {default_port})",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--baud",
|
||||||
|
type=int,
|
||||||
|
default=default_baudrate,
|
||||||
|
help=f"Baud rate (default: {default_baudrate})",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--filter",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help="Only display lines containing this keyword (case-insensitive)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--suppress",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help="Suppress lines containing this keyword (case-insensitive)",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# 1. Start the Serial Reader in a separate thread
|
# 1. Start the Serial Reader in a separate thread
|
||||||
# Daemon=True means this thread dies when the main program closes
|
# Daemon=True means this thread dies when the main program closes
|
||||||
t = threading.Thread(target=serial_worker, args=(args.port, args.baud), daemon=True)
|
myargs = vars(args) # Convert Namespace to dict for easier passing
|
||||||
|
t = threading.Thread(
|
||||||
|
target=serial_worker, args=(args.port, args.baud, myargs), daemon=True
|
||||||
|
)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
# 2. Set up the Graph (Main Thread)
|
# 2. Set up the Graph (Main Thread)
|
||||||
try:
|
try:
|
||||||
plt.style.use('light_background')
|
import matplotlib.style as mplstyle # pylint: disable=import-outside-toplevel
|
||||||
except:
|
default_styles = ("light_background", "ggplot", "seaborn", "dark_background", )
|
||||||
|
styles = list(mplstyle.available)
|
||||||
|
for default_style in default_styles:
|
||||||
|
if default_style in styles:
|
||||||
|
print(
|
||||||
|
f"\n{Fore.CYAN}--- Using Matplotlib style: {default_style} ---{Style.RESET_ALL}"
|
||||||
|
)
|
||||||
|
mplstyle.use(default_style)
|
||||||
|
break
|
||||||
|
except (AttributeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
fig = plt.figure(figsize=(10, 6))
|
fig = plt.figure(figsize=(10, 6))
|
||||||
|
|
||||||
# Update graph every 1000ms
|
# Update graph every 1000ms
|
||||||
ani = animation.FuncAnimation(fig, update_graph, interval=1000)
|
_ = animation.FuncAnimation(
|
||||||
|
fig, update_graph, interval=1000, cache_frame_data=False
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print(f"{Fore.YELLOW}Starting Graph Window... (Close window to exit){Style.RESET_ALL}")
|
print(
|
||||||
|
f"{Fore.YELLOW}Starting Graph Window... (Close window to exit){Style.RESET_ALL}"
|
||||||
|
)
|
||||||
plt.show()
|
plt.show()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print(f"\n{Fore.YELLOW}Exiting...{Style.RESET_ALL}")
|
print(f"\n{Fore.YELLOW}Exiting...{Style.RESET_ALL}")
|
||||||
plt.close('all') # Force close any lingering plot windows
|
plt.close("all") # Force close any lingering plot windows
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class MappedInputManager {
|
|||||||
|
|
||||||
explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {}
|
explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {}
|
||||||
|
|
||||||
|
void update() const { gpio.update(); }
|
||||||
bool wasPressed(Button button) const;
|
bool wasPressed(Button button) const;
|
||||||
bool wasReleased(Button button) const;
|
bool wasReleased(Button button) const;
|
||||||
bool isPressed(Button button) const;
|
bool isPressed(Button button) const;
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ RecentBook RecentBooksStore::getDataFromBook(std::string path) const {
|
|||||||
lastBookFileName = path.substr(lastSlash + 1);
|
lastBookFileName = path.substr(lastSlash + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("Loading recent book: %s\n", path.c_str());
|
Serial.printf("[%lu] [RBS] Loading recent book: %s\n", millis(), path.c_str());
|
||||||
|
|
||||||
// If epub, try to load the metadata for title/author and cover
|
// If epub, try to load the metadata for title/author and cover
|
||||||
if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) {
|
if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ WifiCredentialStore WifiCredentialStore::instance;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// File format version
|
// File format version
|
||||||
constexpr uint8_t WIFI_FILE_VERSION = 1;
|
constexpr uint8_t WIFI_FILE_VERSION = 2; // Increased version
|
||||||
|
|
||||||
// WiFi credentials file path
|
// WiFi credentials file path
|
||||||
constexpr char WIFI_FILE[] = "/.crosspoint/wifi.bin";
|
constexpr char WIFI_FILE[] = "/.crosspoint/wifi.bin";
|
||||||
@@ -38,6 +38,7 @@ bool WifiCredentialStore::saveToFile() const {
|
|||||||
|
|
||||||
// Write header
|
// Write header
|
||||||
serialization::writePod(file, WIFI_FILE_VERSION);
|
serialization::writePod(file, WIFI_FILE_VERSION);
|
||||||
|
serialization::writeString(file, lastConnectedSsid); // Save last connected SSID
|
||||||
serialization::writePod(file, static_cast<uint8_t>(credentials.size()));
|
serialization::writePod(file, static_cast<uint8_t>(credentials.size()));
|
||||||
|
|
||||||
// Write each credential
|
// Write each credential
|
||||||
@@ -67,12 +68,18 @@ bool WifiCredentialStore::loadFromFile() {
|
|||||||
// Read and verify version
|
// Read and verify version
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(file, version);
|
serialization::readPod(file, version);
|
||||||
if (version != WIFI_FILE_VERSION) {
|
if (version > WIFI_FILE_VERSION) {
|
||||||
Serial.printf("[%lu] [WCS] Unknown file version: %u\n", millis(), version);
|
Serial.printf("[%lu] [WCS] Unknown file version: %u\n", millis(), version);
|
||||||
file.close();
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version >= 2) {
|
||||||
|
serialization::readString(file, lastConnectedSsid);
|
||||||
|
} else {
|
||||||
|
lastConnectedSsid.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// Read credential count
|
// Read credential count
|
||||||
uint8_t count;
|
uint8_t count;
|
||||||
serialization::readPod(file, count);
|
serialization::readPod(file, count);
|
||||||
@@ -128,6 +135,9 @@ bool WifiCredentialStore::removeCredential(const std::string& ssid) {
|
|||||||
if (cred != credentials.end()) {
|
if (cred != credentials.end()) {
|
||||||
credentials.erase(cred);
|
credentials.erase(cred);
|
||||||
Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str());
|
Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str());
|
||||||
|
if (ssid == lastConnectedSsid) {
|
||||||
|
clearLastConnectedSsid();
|
||||||
|
}
|
||||||
return saveToFile();
|
return saveToFile();
|
||||||
}
|
}
|
||||||
return false; // Not found
|
return false; // Not found
|
||||||
@@ -146,8 +156,25 @@ const WifiCredential* WifiCredentialStore::findCredential(const std::string& ssi
|
|||||||
|
|
||||||
bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { return findCredential(ssid) != nullptr; }
|
bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { return findCredential(ssid) != nullptr; }
|
||||||
|
|
||||||
|
void WifiCredentialStore::setLastConnectedSsid(const std::string& ssid) {
|
||||||
|
if (lastConnectedSsid != ssid) {
|
||||||
|
lastConnectedSsid = ssid;
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& WifiCredentialStore::getLastConnectedSsid() const { return lastConnectedSsid; }
|
||||||
|
|
||||||
|
void WifiCredentialStore::clearLastConnectedSsid() {
|
||||||
|
if (!lastConnectedSsid.empty()) {
|
||||||
|
lastConnectedSsid.clear();
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WifiCredentialStore::clearAll() {
|
void WifiCredentialStore::clearAll() {
|
||||||
credentials.clear();
|
credentials.clear();
|
||||||
|
lastConnectedSsid.clear();
|
||||||
saveToFile();
|
saveToFile();
|
||||||
Serial.printf("[%lu] [WCS] Cleared all WiFi credentials\n", millis());
|
Serial.printf("[%lu] [WCS] Cleared all WiFi credentials\n", millis());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class WifiCredentialStore {
|
|||||||
private:
|
private:
|
||||||
static WifiCredentialStore instance;
|
static WifiCredentialStore instance;
|
||||||
std::vector<WifiCredential> credentials;
|
std::vector<WifiCredential> credentials;
|
||||||
|
std::string lastConnectedSsid;
|
||||||
|
|
||||||
static constexpr size_t MAX_NETWORKS = 8;
|
static constexpr size_t MAX_NETWORKS = 8;
|
||||||
|
|
||||||
@@ -48,6 +49,11 @@ class WifiCredentialStore {
|
|||||||
// Check if a network is saved
|
// Check if a network is saved
|
||||||
bool hasSavedCredential(const std::string& ssid) const;
|
bool hasSavedCredential(const std::string& ssid) const;
|
||||||
|
|
||||||
|
// Last connected network
|
||||||
|
void setLastConnectedSsid(const std::string& ssid);
|
||||||
|
const std::string& getLastConnectedSsid() const;
|
||||||
|
void clearLastConnectedSsid();
|
||||||
|
|
||||||
// Clear all credentials
|
// Clear all credentials
|
||||||
void clearAll();
|
void clearAll();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -218,12 +218,12 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|||||||
// Handle XTC file
|
// Handle XTC file
|
||||||
Xtc lastXtc(APP_STATE.openEpubPath, "/.crosspoint");
|
Xtc lastXtc(APP_STATE.openEpubPath, "/.crosspoint");
|
||||||
if (!lastXtc.load()) {
|
if (!lastXtc.load()) {
|
||||||
Serial.println("[SLP] Failed to load last XTC");
|
Serial.printf("[%lu] [SLP] Failed to load last XTC\n", millis());
|
||||||
return (this->*renderNoCoverSleepScreen)();
|
return (this->*renderNoCoverSleepScreen)();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lastXtc.generateCoverBmp()) {
|
if (!lastXtc.generateCoverBmp()) {
|
||||||
Serial.println("[SLP] Failed to generate XTC cover bmp");
|
Serial.printf("[%lu] [SLP] Failed to generate XTC cover bmp\n", millis());
|
||||||
return (this->*renderNoCoverSleepScreen)();
|
return (this->*renderNoCoverSleepScreen)();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,12 +232,12 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|||||||
// Handle TXT file - looks for cover image in the same folder
|
// Handle TXT file - looks for cover image in the same folder
|
||||||
Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint");
|
Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint");
|
||||||
if (!lastTxt.load()) {
|
if (!lastTxt.load()) {
|
||||||
Serial.println("[SLP] Failed to load last TXT");
|
Serial.printf("[%lu] [SLP] Failed to load last TXT\n", millis());
|
||||||
return (this->*renderNoCoverSleepScreen)();
|
return (this->*renderNoCoverSleepScreen)();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lastTxt.generateCoverBmp()) {
|
if (!lastTxt.generateCoverBmp()) {
|
||||||
Serial.println("[SLP] No cover image found for TXT file");
|
Serial.printf("[%lu] [SLP] No cover image found for TXT file\n", millis());
|
||||||
return (this->*renderNoCoverSleepScreen)();
|
return (this->*renderNoCoverSleepScreen)();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,12 +247,12 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|||||||
Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint");
|
Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint");
|
||||||
// Skip loading css since we only need metadata here
|
// Skip loading css since we only need metadata here
|
||||||
if (!lastEpub.load(true, true)) {
|
if (!lastEpub.load(true, true)) {
|
||||||
Serial.println("[SLP] Failed to load last epub");
|
Serial.printf("[%lu] [SLP] Failed to load last epub\n", millis());
|
||||||
return (this->*renderNoCoverSleepScreen)();
|
return (this->*renderNoCoverSleepScreen)();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lastEpub.generateCoverBmp(cropped)) {
|
if (!lastEpub.generateCoverBmp(cropped)) {
|
||||||
Serial.println("[SLP] Failed to generate cover bmp");
|
Serial.printf("[%lu] [SLP] Failed to generate cover bmp\n", millis());
|
||||||
return (this->*renderNoCoverSleepScreen)();
|
return (this->*renderNoCoverSleepScreen)();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ void SleepActivity::renderCoverSleepScreen() const {
|
|||||||
if (Storage.openFileForRead("SLP", coverBmpPath, file)) {
|
if (Storage.openFileForRead("SLP", coverBmpPath, file)) {
|
||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
Serial.printf("[SLP] Rendering sleep cover: %s\n", coverBmpPath.c_str());
|
Serial.printf("[%lu] [SLP] Rendering sleep cover: %s\n", millis(), coverBmpPath.c_str());
|
||||||
renderBitmapSleepScreen(bitmap);
|
renderBitmapSleepScreen(bitmap);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int PAGE_ITEMS = 23;
|
constexpr int PAGE_ITEMS = 23;
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::taskTrampoline(void* param) {
|
void OpdsBookBrowserActivity::taskTrampoline(void* param) {
|
||||||
@@ -118,12 +117,6 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
|
|
||||||
// Handle browsing state
|
// Handle browsing state
|
||||||
if (state == BrowserState::BROWSING) {
|
if (state == BrowserState::BROWSING) {
|
||||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
|
||||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (!entries.empty()) {
|
if (!entries.empty()) {
|
||||||
const auto& entry = entries[selectorIndex];
|
const auto& entry = entries[selectorIndex];
|
||||||
@@ -135,20 +128,29 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
}
|
}
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else if (prevReleased && !entries.empty()) {
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + entries.size()) % entries.size();
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + entries.size() - 1) % entries.size();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle navigation
|
||||||
|
if (!entries.empty()) {
|
||||||
|
buttonNavigator.onNextRelease([this] {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size());
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (nextReleased && !entries.empty()) {
|
});
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % entries.size();
|
buttonNavigator.onPreviousRelease([this] {
|
||||||
} else {
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size());
|
||||||
selectorIndex = (selectorIndex + 1) % entries.size();
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../ActivityWithSubactivity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity for browsing and downloading books from an OPDS server.
|
* Activity for browsing and downloading books from an OPDS server.
|
||||||
@@ -37,6 +38,7 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
|
|||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|
||||||
BrowserState state = BrowserState::LOADING;
|
BrowserState state = BrowserState::LOADING;
|
||||||
@@ -62,4 +64,5 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
|
|||||||
void navigateToEntry(const OpdsEntry& entry);
|
void navigateToEntry(const OpdsEntry& entry);
|
||||||
void navigateBack();
|
void navigateBack();
|
||||||
void downloadBook(const OpdsEntry& book);
|
void downloadBook(const OpdsEntry& book);
|
||||||
|
bool preventAutoSleep() override { return true; }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -196,13 +196,18 @@ void HomeActivity::freeCoverBuffer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HomeActivity::loop() {
|
void HomeActivity::loop() {
|
||||||
const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left);
|
|
||||||
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right);
|
|
||||||
|
|
||||||
const int menuCount = getMenuItemCount();
|
const int menuCount = getMenuItemCount();
|
||||||
|
|
||||||
|
buttonNavigator.onNext([this, menuCount] {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, menuCount);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPrevious([this, menuCount] {
|
||||||
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, menuCount);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
// Calculate dynamic indices based on which options are available
|
// Calculate dynamic indices based on which options are available
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
@@ -226,12 +231,6 @@ void HomeActivity::loop() {
|
|||||||
} else if (menuSelectedIndex == settingsIdx) {
|
} else if (menuSelectedIndex == settingsIdx) {
|
||||||
onSettingsOpen();
|
onSettingsOpen();
|
||||||
}
|
}
|
||||||
} else if (prevPressed) {
|
|
||||||
selectorIndex = (selectorIndex + menuCount - 1) % menuCount;
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (nextPressed) {
|
|
||||||
selectorIndex = (selectorIndex + 1) % menuCount;
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
#include "./MyLibraryActivity.h"
|
#include "./MyLibraryActivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
struct RecentBook;
|
struct RecentBook;
|
||||||
struct Rect;
|
struct Rect;
|
||||||
@@ -15,6 +16,7 @@ struct Rect;
|
|||||||
class HomeActivity final : public Activity {
|
class HomeActivity final : public Activity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
bool recentsLoading = false;
|
bool recentsLoading = false;
|
||||||
|
|||||||
@@ -11,17 +11,58 @@
|
|||||||
#include "util/StringUtils.h"
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
constexpr unsigned long GO_HOME_MS = 1000;
|
constexpr unsigned long GO_HOME_MS = 1000;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void sortFileList(std::vector<std::string>& strs) {
|
void sortFileList(std::vector<std::string>& strs) {
|
||||||
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
||||||
if (str1.back() == '/' && str2.back() != '/') return true;
|
// Directories first
|
||||||
if (str1.back() != '/' && str2.back() == '/') return false;
|
bool isDir1 = str1.back() == '/';
|
||||||
return lexicographical_compare(
|
bool isDir2 = str2.back() == '/';
|
||||||
begin(str1), end(str1), begin(str2), end(str2),
|
if (isDir1 != isDir2) return isDir1;
|
||||||
[](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); });
|
|
||||||
|
// Start naive natural sort
|
||||||
|
const char* s1 = str1.c_str();
|
||||||
|
const char* s2 = str2.c_str();
|
||||||
|
|
||||||
|
// Iterate while both strings have characters
|
||||||
|
while (*s1 && *s2) {
|
||||||
|
// Check if both are at the start of a number
|
||||||
|
if (isdigit(*s1) && isdigit(*s2)) {
|
||||||
|
// Skip leading zeros and track them
|
||||||
|
const char* start1 = s1;
|
||||||
|
const char* start2 = s2;
|
||||||
|
while (*s1 == '0') s1++;
|
||||||
|
while (*s2 == '0') s2++;
|
||||||
|
|
||||||
|
// Count digits to compare lengths first
|
||||||
|
int len1 = 0, len2 = 0;
|
||||||
|
while (isdigit(s1[len1])) len1++;
|
||||||
|
while (isdigit(s2[len2])) len2++;
|
||||||
|
|
||||||
|
// Different length so return smaller integer value
|
||||||
|
if (len1 != len2) return len1 < len2;
|
||||||
|
|
||||||
|
// Same length so compare digit by digit
|
||||||
|
for (int i = 0; i < len1; i++) {
|
||||||
|
if (s1[i] != s2[i]) return s1[i] < s2[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numbers equal so advance pointers
|
||||||
|
s1 += len1;
|
||||||
|
s2 += len2;
|
||||||
|
} else {
|
||||||
|
// Regular case-insensitive character comparison
|
||||||
|
char c1 = tolower(*s1);
|
||||||
|
char c2 = tolower(*s2);
|
||||||
|
if (c1 != c2) return c1 < c2;
|
||||||
|
s1++;
|
||||||
|
s2++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One string is prefix of other
|
||||||
|
return *s1 == '\0' && *s2 != '\0';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,13 +150,6 @@ void MyLibraryActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Up);
|
|
||||||
;
|
|
||||||
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Down);
|
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
@@ -157,21 +191,26 @@ void MyLibraryActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int listSize = static_cast<int>(files.size());
|
int listSize = static_cast<int>(files.size());
|
||||||
if (upReleased) {
|
|
||||||
if (skipPage) {
|
buttonNavigator.onNextRelease([this, listSize] {
|
||||||
selectorIndex = std::max(static_cast<int>((selectorIndex / pageItems - 1) * pageItems), 0);
|
selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize);
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + listSize - 1) % listSize;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (downReleased) {
|
});
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = std::min(static_cast<int>((selectorIndex / pageItems + 1) * pageItems), listSize - 1);
|
buttonNavigator.onPreviousRelease([this, listSize] {
|
||||||
} else {
|
selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize);
|
||||||
selectorIndex = (selectorIndex + 1) % listSize;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this, listSize, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this, listSize, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyLibraryActivity::displayTaskLoop() {
|
void MyLibraryActivity::displayTaskLoop() {
|
||||||
@@ -207,7 +246,7 @@ void MyLibraryActivity::render() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Help text
|
// Help text
|
||||||
const auto labels = mappedInput.mapLabels("« Home", "Open", "Up", "Down");
|
const auto labels = mappedInput.mapLabels(basepath == "/" ? "« Home" : "« Back", "Open", "Up", "Down");
|
||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@@ -8,11 +8,14 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
#include "RecentBooksStore.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class MyLibraryActivity final : public Activity {
|
class MyLibraryActivity final : public Activity {
|
||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
size_t selectorIndex = 0;
|
size_t selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
#include "util/StringUtils.h"
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
constexpr unsigned long GO_HOME_MS = 1000;
|
constexpr unsigned long GO_HOME_MS = 1000;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -70,18 +69,11 @@ void RecentBooksActivity::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RecentBooksActivity::loop() {
|
void RecentBooksActivity::loop() {
|
||||||
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Up);
|
|
||||||
;
|
|
||||||
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Down);
|
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true);
|
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true);
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||||
Serial.printf("Selected recent book: %s\n", recentBooks[selectorIndex].path.c_str());
|
Serial.printf("[%lu] [RBA] Selected recent book: %s\n", millis(), recentBooks[selectorIndex].path.c_str());
|
||||||
onSelectBook(recentBooks[selectorIndex].path);
|
onSelectBook(recentBooks[selectorIndex].path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -92,21 +84,26 @@ void RecentBooksActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int listSize = static_cast<int>(recentBooks.size());
|
int listSize = static_cast<int>(recentBooks.size());
|
||||||
if (upReleased) {
|
|
||||||
if (skipPage) {
|
buttonNavigator.onNextRelease([this, listSize] {
|
||||||
selectorIndex = std::max(static_cast<int>((selectorIndex / pageItems - 1) * pageItems), 0);
|
selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize);
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + listSize - 1) % listSize;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (downReleased) {
|
});
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = std::min(static_cast<int>((selectorIndex / pageItems + 1) * pageItems), listSize - 1);
|
buttonNavigator.onPreviousRelease([this, listSize] {
|
||||||
} else {
|
selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize);
|
||||||
selectorIndex = (selectorIndex + 1) % listSize;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this, listSize, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this, listSize, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void RecentBooksActivity::displayTaskLoop() {
|
void RecentBooksActivity::displayTaskLoop() {
|
||||||
|
|||||||
@@ -9,11 +9,13 @@
|
|||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class RecentBooksActivity final : public Activity {
|
class RecentBooksActivity final : public Activity {
|
||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
size_t selectorIndex = 0;
|
size_t selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|||||||
@@ -348,6 +348,9 @@ void CrossPointWebServerActivity::loop() {
|
|||||||
// Yield and check for exit button every 64 iterations
|
// Yield and check for exit button every 64 iterations
|
||||||
if ((i & 0x3F) == 0x3F) {
|
if ((i & 0x3F) == 0x3F) {
|
||||||
yield();
|
yield();
|
||||||
|
// Force trigger an update of which buttons are being pressed so be have accurate state
|
||||||
|
// for back button checking
|
||||||
|
mappedInput.update();
|
||||||
// Check for exit button inside loop for responsiveness
|
// Check for exit button inside loop for responsiveness
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
onGoBack();
|
onGoBack();
|
||||||
|
|||||||
@@ -73,18 +73,15 @@ void NetworkModeSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation
|
// Handle navigation
|
||||||
const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
buttonNavigator.onNext([this] {
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left);
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEM_COUNT);
|
||||||
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
updateRequired = true;
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right);
|
});
|
||||||
|
|
||||||
if (prevPressed) {
|
buttonNavigator.onPrevious([this] {
|
||||||
selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT;
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEM_COUNT);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (nextPressed) {
|
});
|
||||||
selectedIndex = (selectedIndex + 1) % MENU_ITEM_COUNT;
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkModeSelectionActivity::displayTaskLoop() {
|
void NetworkModeSelectionActivity::displayTaskLoop() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
// Enum for network mode selection
|
// Enum for network mode selection
|
||||||
enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT };
|
enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT };
|
||||||
@@ -22,6 +23,8 @@ enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT };
|
|||||||
class NetworkModeSelectionActivity final : public Activity {
|
class NetworkModeSelectionActivity final : public Activity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
int selectedIndex = 0;
|
int selectedIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
const std::function<void(NetworkMode)> onModeSelected;
|
const std::function<void(NetworkMode)> onModeSelected;
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ void WifiSelectionActivity::onEnter() {
|
|||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
// Load saved WiFi credentials - SD card operations need lock as we use SPI for both
|
// Load saved WiFi credentials - SD card operations need lock as we use SPI
|
||||||
|
// for both
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
WIFI_STORE.loadFromFile();
|
WIFI_STORE.loadFromFile();
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
@@ -37,6 +38,7 @@ void WifiSelectionActivity::onEnter() {
|
|||||||
usedSavedPassword = false;
|
usedSavedPassword = false;
|
||||||
savePromptSelection = 0;
|
savePromptSelection = 0;
|
||||||
forgetPromptSelection = 0;
|
forgetPromptSelection = 0;
|
||||||
|
autoConnecting = false;
|
||||||
|
|
||||||
// Cache MAC address for display
|
// Cache MAC address for display
|
||||||
uint8_t mac[6];
|
uint8_t mac[6];
|
||||||
@@ -46,9 +48,7 @@ void WifiSelectionActivity::onEnter() {
|
|||||||
mac[5]);
|
mac[5]);
|
||||||
cachedMacAddress = std::string(macStr);
|
cachedMacAddress = std::string(macStr);
|
||||||
|
|
||||||
// Trigger first update to show scanning message
|
// Task creation
|
||||||
updateRequired = true;
|
|
||||||
|
|
||||||
xTaskCreate(&WifiSelectionActivity::taskTrampoline, "WifiSelectionTask",
|
xTaskCreate(&WifiSelectionActivity::taskTrampoline, "WifiSelectionTask",
|
||||||
4096, // Stack size (larger for WiFi operations)
|
4096, // Stack size (larger for WiFi operations)
|
||||||
this, // Parameters
|
this, // Parameters
|
||||||
@@ -56,7 +56,26 @@ void WifiSelectionActivity::onEnter() {
|
|||||||
&displayTaskHandle // Task handle
|
&displayTaskHandle // Task handle
|
||||||
);
|
);
|
||||||
|
|
||||||
// Start WiFi scan
|
// Attempt to auto-connect to the last network
|
||||||
|
if (allowAutoConnect) {
|
||||||
|
const std::string lastSsid = WIFI_STORE.getLastConnectedSsid();
|
||||||
|
if (!lastSsid.empty()) {
|
||||||
|
const auto* cred = WIFI_STORE.findCredential(lastSsid);
|
||||||
|
if (cred) {
|
||||||
|
Serial.printf("[%lu] [WIFI] Attempting to auto-connect to %s\n", millis(), lastSsid.c_str());
|
||||||
|
selectedSSID = cred->ssid;
|
||||||
|
enteredPassword = cred->password;
|
||||||
|
selectedRequiresPassword = !cred->password.empty();
|
||||||
|
usedSavedPassword = true;
|
||||||
|
autoConnecting = true;
|
||||||
|
attemptConnection();
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to scanning
|
||||||
startWifiScan();
|
startWifiScan();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,15 +89,17 @@ void WifiSelectionActivity::onExit() {
|
|||||||
WiFi.scanDelete();
|
WiFi.scanDelete();
|
||||||
Serial.printf("[%lu] [WIFI] [MEM] Free heap after scanDelete: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WIFI] [MEM] Free heap after scanDelete: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
// Note: We do NOT disconnect WiFi here - the parent activity (CrossPointWebServerActivity)
|
// Note: We do NOT disconnect WiFi here - the parent activity
|
||||||
// manages WiFi connection state. We just clean up the scan and task.
|
// (CrossPointWebServerActivity) manages WiFi connection state. We just clean
|
||||||
|
// up the scan and task.
|
||||||
|
|
||||||
// Acquire mutex before deleting task to ensure task isn't using it
|
// Acquire mutex before deleting task to ensure task isn't using it
|
||||||
// This prevents hangs/crashes if the task holds the mutex when deleted
|
// This prevents hangs/crashes if the task holds the mutex when deleted
|
||||||
Serial.printf("[%lu] [WIFI] Acquiring rendering mutex before task deletion...\n", millis());
|
Serial.printf("[%lu] [WIFI] Acquiring rendering mutex before task deletion...\n", millis());
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
|
||||||
// Delete the display task (we now hold the mutex, so task is blocked if it needs it)
|
// Delete the display task (we now hold the mutex, so task is blocked if it
|
||||||
|
// needs it)
|
||||||
Serial.printf("[%lu] [WIFI] Deleting display task...\n", millis());
|
Serial.printf("[%lu] [WIFI] Deleting display task...\n", millis());
|
||||||
if (displayTaskHandle) {
|
if (displayTaskHandle) {
|
||||||
vTaskDelete(displayTaskHandle);
|
vTaskDelete(displayTaskHandle);
|
||||||
@@ -96,6 +117,7 @@ void WifiSelectionActivity::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::startWifiScan() {
|
void WifiSelectionActivity::startWifiScan() {
|
||||||
|
autoConnecting = false;
|
||||||
state = WifiSelectionState::SCANNING;
|
state = WifiSelectionState::SCANNING;
|
||||||
networks.clear();
|
networks.clear();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@@ -181,6 +203,7 @@ void WifiSelectionActivity::selectNetwork(const int index) {
|
|||||||
selectedRequiresPassword = network.isEncrypted;
|
selectedRequiresPassword = network.isEncrypted;
|
||||||
usedSavedPassword = false;
|
usedSavedPassword = false;
|
||||||
enteredPassword.clear();
|
enteredPassword.clear();
|
||||||
|
autoConnecting = false;
|
||||||
|
|
||||||
// Check if we have saved credentials for this network
|
// Check if we have saved credentials for this network
|
||||||
const auto* savedCred = WIFI_STORE.findCredential(selectedSSID);
|
const auto* savedCred = WIFI_STORE.findCredential(selectedSSID);
|
||||||
@@ -223,7 +246,7 @@ void WifiSelectionActivity::selectNetwork(const int index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::attemptConnection() {
|
void WifiSelectionActivity::attemptConnection() {
|
||||||
state = WifiSelectionState::CONNECTING;
|
state = autoConnecting ? WifiSelectionState::AUTO_CONNECTING : WifiSelectionState::CONNECTING;
|
||||||
connectionStartTime = millis();
|
connectionStartTime = millis();
|
||||||
connectedIP.clear();
|
connectedIP.clear();
|
||||||
connectionError.clear();
|
connectionError.clear();
|
||||||
@@ -239,7 +262,7 @@ void WifiSelectionActivity::attemptConnection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::checkConnectionStatus() {
|
void WifiSelectionActivity::checkConnectionStatus() {
|
||||||
if (state != WifiSelectionState::CONNECTING) {
|
if (state != WifiSelectionState::CONNECTING && state != WifiSelectionState::AUTO_CONNECTING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +274,13 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
|||||||
char ipStr[16];
|
char ipStr[16];
|
||||||
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
||||||
connectedIP = ipStr;
|
connectedIP = ipStr;
|
||||||
|
autoConnecting = false;
|
||||||
|
|
||||||
|
// Save this as the last connected network - SD card operations need lock as
|
||||||
|
// we use SPI for both
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
WIFI_STORE.setLastConnectedSsid(selectedSSID);
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
|
||||||
// If we entered a new password, ask if user wants to save it
|
// If we entered a new password, ask if user wants to save it
|
||||||
// Otherwise, immediately complete so parent can start web server
|
// Otherwise, immediately complete so parent can start web server
|
||||||
@@ -260,7 +290,10 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else {
|
} else {
|
||||||
// Using saved password or open network - complete immediately
|
// Using saved password or open network - complete immediately
|
||||||
Serial.printf("[%lu] [WIFI] Connected with saved/open credentials, completing immediately\n", millis());
|
Serial.printf(
|
||||||
|
"[%lu] [WIFI] Connected with saved/open credentials, "
|
||||||
|
"completing immediately\n",
|
||||||
|
millis());
|
||||||
onComplete(true);
|
onComplete(true);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -299,7 +332,7 @@ void WifiSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check connection progress
|
// Check connection progress
|
||||||
if (state == WifiSelectionState::CONNECTING) {
|
if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) {
|
||||||
checkConnectionStatus();
|
checkConnectionStatus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -368,17 +401,16 @@ void WifiSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Go back to network list (whether Cancel or Forget network was selected)
|
// Go back to network list (whether Cancel or Forget network was selected)
|
||||||
state = WifiSelectionState::NETWORK_LIST;
|
startWifiScan();
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
// Skip forgetting, go back to network list
|
// Skip forgetting, go back to network list
|
||||||
state = WifiSelectionState::NETWORK_LIST;
|
startWifiScan();
|
||||||
updateRequired = true;
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle connected state (should not normally be reached - connection completes immediately)
|
// Handle connected state (should not normally be reached - connection
|
||||||
|
// completes immediately)
|
||||||
if (state == WifiSelectionState::CONNECTED) {
|
if (state == WifiSelectionState::CONNECTED) {
|
||||||
// Safety fallback - immediately complete
|
// Safety fallback - immediately complete
|
||||||
onComplete(true);
|
onComplete(true);
|
||||||
@@ -389,12 +421,14 @@ void WifiSelectionActivity::loop() {
|
|||||||
if (state == WifiSelectionState::CONNECTION_FAILED) {
|
if (state == WifiSelectionState::CONNECTION_FAILED) {
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
// If we used saved credentials, offer to forget the network
|
// If we were auto-connecting or using a saved credential, offer to forget
|
||||||
if (usedSavedPassword) {
|
// the network
|
||||||
|
if (autoConnecting || usedSavedPassword) {
|
||||||
|
autoConnecting = false;
|
||||||
state = WifiSelectionState::FORGET_PROMPT;
|
state = WifiSelectionState::FORGET_PROMPT;
|
||||||
forgetPromptSelection = 0; // Default to "Cancel"
|
forgetPromptSelection = 0; // Default to "Cancel"
|
||||||
} else {
|
} else {
|
||||||
// Go back to network list on failure
|
// Go back to network list on failure for non-saved credentials
|
||||||
state = WifiSelectionState::NETWORK_LIST;
|
state = WifiSelectionState::NETWORK_LIST;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@@ -420,20 +454,33 @@ void WifiSelectionActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle UP/DOWN navigation
|
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
startWifiScan();
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
return;
|
||||||
if (selectedNetworkIndex > 0) {
|
}
|
||||||
selectedNetworkIndex--;
|
|
||||||
|
const bool leftPressed = mappedInput.wasPressed(MappedInputManager::Button::Left);
|
||||||
|
if (leftPressed) {
|
||||||
|
const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword;
|
||||||
|
if (hasSavedPassword) {
|
||||||
|
selectedSSID = networks[selectedNetworkIndex].ssid;
|
||||||
|
state = WifiSelectionState::FORGET_PROMPT;
|
||||||
|
forgetPromptSelection = 0; // Default to "Cancel"
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
}
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
||||||
if (!networks.empty() && selectedNetworkIndex < static_cast<int>(networks.size()) - 1) {
|
// Handle navigation
|
||||||
selectedNetworkIndex++;
|
buttonNavigator.onNext([this] {
|
||||||
|
selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size());
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
buttonNavigator.onPrevious([this] {
|
||||||
|
selectedNetworkIndex = ButtonNavigator::previousIndex(selectedNetworkIndex, networks.size());
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,6 +530,9 @@ void WifiSelectionActivity::render() const {
|
|||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
|
case WifiSelectionState::AUTO_CONNECTING:
|
||||||
|
renderConnecting();
|
||||||
|
break;
|
||||||
case WifiSelectionState::SCANNING:
|
case WifiSelectionState::SCANNING:
|
||||||
renderConnecting(); // Reuse connecting screen with different message
|
renderConnecting(); // Reuse connecting screen with different message
|
||||||
break;
|
break;
|
||||||
@@ -586,7 +636,11 @@ void WifiSelectionActivity::renderNetworkList() const {
|
|||||||
|
|
||||||
// Draw help text
|
// Draw help text
|
||||||
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved");
|
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved");
|
||||||
const auto labels = mappedInput.mapLabels("« Back", "Connect", "", "");
|
|
||||||
|
const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword;
|
||||||
|
const char* forgetLabel = hasSavedPassword ? "Forget" : "";
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels("« Back", "Connect", forgetLabel, "Refresh");
|
||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,8 +744,7 @@ void WifiSelectionActivity::renderForgetPrompt() const {
|
|||||||
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
const auto top = (pageHeight - height * 3) / 2;
|
const auto top = (pageHeight - height * 3) / 2;
|
||||||
|
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connection Failed", true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Forget Network", true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
std::string ssidInfo = "Network: " + selectedSSID;
|
std::string ssidInfo = "Network: " + selectedSSID;
|
||||||
if (ssidInfo.length() > 28) {
|
if (ssidInfo.length() > 28) {
|
||||||
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
|
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
// Structure to hold WiFi network information
|
// Structure to hold WiFi network information
|
||||||
struct WifiNetworkInfo {
|
struct WifiNetworkInfo {
|
||||||
@@ -21,6 +22,7 @@ struct WifiNetworkInfo {
|
|||||||
|
|
||||||
// WiFi selection states
|
// WiFi selection states
|
||||||
enum class WifiSelectionState {
|
enum class WifiSelectionState {
|
||||||
|
AUTO_CONNECTING, // Trying to connect to the last known network
|
||||||
SCANNING, // Scanning for networks
|
SCANNING, // Scanning for networks
|
||||||
NETWORK_LIST, // Displaying available networks
|
NETWORK_LIST, // Displaying available networks
|
||||||
PASSWORD_ENTRY, // Entering password for selected network
|
PASSWORD_ENTRY, // Entering password for selected network
|
||||||
@@ -45,6 +47,7 @@ enum class WifiSelectionState {
|
|||||||
class WifiSelectionActivity final : public ActivityWithSubactivity {
|
class WifiSelectionActivity final : public ActivityWithSubactivity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
WifiSelectionState state = WifiSelectionState::SCANNING;
|
WifiSelectionState state = WifiSelectionState::SCANNING;
|
||||||
int selectedNetworkIndex = 0;
|
int selectedNetworkIndex = 0;
|
||||||
@@ -68,6 +71,12 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
|
|||||||
// Whether network was connected using a saved password (skip save prompt)
|
// Whether network was connected using a saved password (skip save prompt)
|
||||||
bool usedSavedPassword = false;
|
bool usedSavedPassword = false;
|
||||||
|
|
||||||
|
// Whether to attempt auto-connect on entry
|
||||||
|
const bool allowAutoConnect;
|
||||||
|
|
||||||
|
// Whether we are attempting to auto-connect
|
||||||
|
bool autoConnecting = false;
|
||||||
|
|
||||||
// Save/forget prompt selection (0 = Yes, 1 = No)
|
// Save/forget prompt selection (0 = Yes, 1 = No)
|
||||||
int savePromptSelection = 0;
|
int savePromptSelection = 0;
|
||||||
int forgetPromptSelection = 0;
|
int forgetPromptSelection = 0;
|
||||||
@@ -96,8 +105,10 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
const std::function<void(bool connected)>& onComplete)
|
const std::function<void(bool connected)>& onComplete, bool autoConnect = true)
|
||||||
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput), onComplete(onComplete) {}
|
: ActivityWithSubactivity("WifiSelection", renderer, mappedInput),
|
||||||
|
onComplete(onComplete),
|
||||||
|
allowAutoConnect(autoConnect) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
@@ -664,9 +664,9 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC
|
|||||||
data[5] = (pageCount >> 8) & 0xFF;
|
data[5] = (pageCount >> 8) & 0xFF;
|
||||||
f.write(data, 6);
|
f.write(data, 6);
|
||||||
f.close();
|
f.close();
|
||||||
Serial.printf("[ERS] Progress saved: Chapter %d, Page %d\n", spineIndex, currentPage);
|
Serial.printf("[%lu] [ERS] Progress saved: Chapter %d, Page %d\n", millis(), spineIndex, currentPage);
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[ERS] Could not save progress!\n");
|
Serial.printf("[%lu] [ERS] Could not save progress!\n", millis());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
|
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
|
||||||
|
|||||||
@@ -6,11 +6,6 @@
|
|||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
|
||||||
// Time threshold for treating a long press as a page-up/page-down
|
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); }
|
int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); }
|
||||||
|
|
||||||
int EpubReaderChapterSelectionActivity::getPageItems() const {
|
int EpubReaderChapterSelectionActivity::getPageItems() const {
|
||||||
@@ -77,12 +72,6 @@ void EpubReaderChapterSelectionActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
|
||||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
const int totalItems = getTotalItems();
|
const int totalItems = getTotalItems();
|
||||||
|
|
||||||
@@ -95,21 +84,27 @@ void EpubReaderChapterSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
onGoBack();
|
onGoBack();
|
||||||
} else if (prevReleased) {
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + totalItems) % totalItems;
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + totalItems - 1) % totalItems;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buttonNavigator.onNextRelease([this, totalItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (nextReleased) {
|
});
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % totalItems;
|
buttonNavigator.onPreviousRelease([this, totalItems] {
|
||||||
} else {
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
||||||
selectorIndex = (selectorIndex + 1) % totalItems;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::displayTaskLoop() {
|
void EpubReaderChapterSelectionActivity::displayTaskLoop() {
|
||||||
|
|||||||
@@ -7,12 +7,14 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "../ActivityWithSubactivity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity {
|
class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity {
|
||||||
std::shared_ptr<Epub> epub;
|
std::shared_ptr<Epub> epub;
|
||||||
std::string epubPath;
|
std::string epubPath;
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
int currentSpineIndex = 0;
|
int currentSpineIndex = 0;
|
||||||
int currentPage = 0;
|
int currentPage = 0;
|
||||||
int totalPagesInSpine = 0;
|
int totalPagesInSpine = 0;
|
||||||
|
|||||||
@@ -48,16 +48,19 @@ void EpubReaderMenuActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle navigation
|
||||||
|
buttonNavigator.onNext([this] {
|
||||||
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPrevious([this] {
|
||||||
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
// Use local variables for items we need to check after potential deletion
|
// Use local variables for items we need to check after potential deletion
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
|
||||||
selectedIndex = (selectedIndex + menuItems.size() - 1) % menuItems.size();
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % menuItems.size();
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
||||||
const auto selectedAction = menuItems[selectedIndex].action;
|
const auto selectedAction = menuItems[selectedIndex].action;
|
||||||
if (selectedAction == MenuAction::ROTATE_SCREEN) {
|
if (selectedAction == MenuAction::ROTATE_SCREEN) {
|
||||||
// Cycle orientation preview locally; actual rotation happens on menu exit.
|
// Cycle orientation preview locally; actual rotation happens on menu exit.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../ActivityWithSubactivity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
||||||
public:
|
public:
|
||||||
@@ -48,6 +49,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
std::string title = "Reader Menu";
|
std::string title = "Reader Menu";
|
||||||
uint8_t pendingOrientation = 0;
|
uint8_t pendingOrientation = 0;
|
||||||
const std::vector<const char*> orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"};
|
const std::vector<const char*> orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"};
|
||||||
|
|||||||
@@ -79,25 +79,11 @@ void EpubReaderPercentSelectionActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustPercent(-kSmallStep); });
|
||||||
adjustPercent(-kSmallStep);
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustPercent(kSmallStep); });
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { adjustPercent(kLargeStep); });
|
||||||
adjustPercent(kSmallStep);
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); });
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
|
||||||
adjustPercent(kLargeStep);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
|
||||||
adjustPercent(-kLargeStep);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderPercentSelectionActivity::renderScreen() {
|
void EpubReaderPercentSelectionActivity::renderScreen() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity {
|
class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity {
|
||||||
public:
|
public:
|
||||||
@@ -31,6 +32,7 @@ class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity
|
|||||||
// FreeRTOS task and mutex for rendering.
|
// FreeRTOS task and mutex for rendering.
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
// Callback invoked when the user confirms a percent.
|
// Callback invoked when the user confirms a percent.
|
||||||
const std::function<void(int)> onSelect;
|
const std::function<void(int)> onSelect;
|
||||||
|
|||||||
@@ -8,10 +8,6 @@
|
|||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int XtcReaderChapterSelectionActivity::getPageItems() const {
|
int XtcReaderChapterSelectionActivity::getPageItems() const {
|
||||||
constexpr int lineHeight = 30;
|
constexpr int lineHeight = 30;
|
||||||
|
|
||||||
@@ -78,13 +74,8 @@ void XtcReaderChapterSelectionActivity::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void XtcReaderChapterSelectionActivity::loop() {
|
void XtcReaderChapterSelectionActivity::loop() {
|
||||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
|
||||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
|
const int totalItems = static_cast<int>(xtc->getChapters().size());
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
const auto& chapters = xtc->getChapters();
|
const auto& chapters = xtc->getChapters();
|
||||||
@@ -93,29 +84,27 @@ void XtcReaderChapterSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
onGoBack();
|
onGoBack();
|
||||||
} else if (prevReleased) {
|
|
||||||
const int total = static_cast<int>(xtc->getChapters().size());
|
|
||||||
if (total == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + total) % total;
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + total - 1) % total;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buttonNavigator.onNextRelease([this, totalItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (nextReleased) {
|
});
|
||||||
const int total = static_cast<int>(xtc->getChapters().size());
|
|
||||||
if (total == 0) {
|
buttonNavigator.onPreviousRelease([this, totalItems] {
|
||||||
return;
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
||||||
}
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % total;
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + 1) % total;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void XtcReaderChapterSelectionActivity::displayTaskLoop() {
|
void XtcReaderChapterSelectionActivity::displayTaskLoop() {
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class XtcReaderChapterSelectionActivity final : public Activity {
|
class XtcReaderChapterSelectionActivity final : public Activity {
|
||||||
std::shared_ptr<Xtc> xtc;
|
std::shared_ptr<Xtc> xtc;
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
uint32_t currentPage = 0;
|
uint32_t currentPage = 0;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|||||||
@@ -63,15 +63,16 @@ void CalibreSettingsActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
// Handle navigation
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
buttonNavigator.onNext([this] {
|
||||||
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPrevious([this] {
|
||||||
|
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void CalibreSettingsActivity::handleSelection() {
|
void CalibreSettingsActivity::handleSelection() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submenu for OPDS Browser settings.
|
* Submenu for OPDS Browser settings.
|
||||||
@@ -24,6 +25,7 @@ class CalibreSettingsActivity final : public ActivityWithSubactivity {
|
|||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|
||||||
int selectedIndex = 0;
|
int selectedIndex = 0;
|
||||||
|
|||||||
@@ -64,15 +64,16 @@ void KOReaderSettingsActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
// Handle navigation
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
buttonNavigator.onNext([this] {
|
||||||
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPrevious([this] {
|
||||||
|
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void KOReaderSettingsActivity::handleSelection() {
|
void KOReaderSettingsActivity::handleSelection() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submenu for KOReader Sync settings.
|
* Submenu for KOReader Sync settings.
|
||||||
@@ -24,6 +25,7 @@ class KOReaderSettingsActivity final : public ActivityWithSubactivity {
|
|||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|
||||||
int selectedIndex = 0;
|
int selectedIndex = 0;
|
||||||
|
|||||||
@@ -11,15 +11,12 @@
|
|||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "OtaUpdateActivity.h"
|
#include "OtaUpdateActivity.h"
|
||||||
#include "SettingsList.h"
|
#include "SettingsList.h"
|
||||||
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
|
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr int changeTabsMs = 700;
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void SettingsActivity::taskTrampoline(void* param) {
|
void SettingsActivity::taskTrampoline(void* param) {
|
||||||
auto* self = static_cast<SettingsActivity*>(param);
|
auto* self = static_cast<SettingsActivity*>(param);
|
||||||
self->displayTaskLoop();
|
self->displayTaskLoop();
|
||||||
@@ -50,11 +47,13 @@ void SettingsActivity::onEnter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Append device-only ACTION items
|
// Append device-only ACTION items
|
||||||
controlsSettings.insert(controlsSettings.begin(), SettingInfo::Action("Remap Front Buttons"));
|
controlsSettings.insert(controlsSettings.begin(),
|
||||||
systemSettings.push_back(SettingInfo::Action("KOReader Sync"));
|
SettingInfo::Action("Remap Front Buttons", SettingAction::RemapFrontButtons));
|
||||||
systemSettings.push_back(SettingInfo::Action("OPDS Browser"));
|
systemSettings.push_back(SettingInfo::Action("Network", SettingAction::Network));
|
||||||
systemSettings.push_back(SettingInfo::Action("Clear Cache"));
|
systemSettings.push_back(SettingInfo::Action("KOReader Sync", SettingAction::KOReaderSync));
|
||||||
systemSettings.push_back(SettingInfo::Action("Check for updates"));
|
systemSettings.push_back(SettingInfo::Action("OPDS Browser", SettingAction::OPDSBrowser));
|
||||||
|
systemSettings.push_back(SettingInfo::Action("Clear Cache", SettingAction::ClearCache));
|
||||||
|
systemSettings.push_back(SettingInfo::Action("Check for updates", SettingAction::CheckForUpdates));
|
||||||
|
|
||||||
// Reset selection to first category
|
// Reset selection to first category
|
||||||
selectedCategoryIndex = 0;
|
selectedCategoryIndex = 0;
|
||||||
@@ -116,28 +115,28 @@ void SettingsActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up);
|
|
||||||
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down);
|
|
||||||
const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left);
|
|
||||||
const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right);
|
|
||||||
const bool changeTab = mappedInput.getHeldTime() > changeTabsMs;
|
|
||||||
|
|
||||||
// Handle navigation
|
// Handle navigation
|
||||||
if (upReleased && changeTab) {
|
buttonNavigator.onNextRelease([this] {
|
||||||
|
selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousRelease([this] {
|
||||||
|
selectedSettingIndex = ButtonNavigator::previousIndex(selectedSettingIndex, settingsCount + 1);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this, &hasChangedCategory] {
|
||||||
hasChangedCategory = true;
|
hasChangedCategory = true;
|
||||||
selectedCategoryIndex = (selectedCategoryIndex > 0) ? (selectedCategoryIndex - 1) : (categoryCount - 1);
|
selectedCategoryIndex = ButtonNavigator::nextIndex(selectedCategoryIndex, categoryCount);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (downReleased && changeTab) {
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this, &hasChangedCategory] {
|
||||||
hasChangedCategory = true;
|
hasChangedCategory = true;
|
||||||
selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0;
|
selectedCategoryIndex = ButtonNavigator::previousIndex(selectedCategoryIndex, categoryCount);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (upReleased || leftReleased) {
|
});
|
||||||
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount);
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (rightReleased || downReleased) {
|
|
||||||
selectedSettingIndex = (selectedSettingIndex < settingsCount) ? (selectedSettingIndex + 1) : 0;
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasChangedCategory) {
|
if (hasChangedCategory) {
|
||||||
selectedSettingIndex = (selectedSettingIndex == 0) ? 0 : 1;
|
selectedSettingIndex = (selectedSettingIndex == 0) ? 0 : 1;
|
||||||
@@ -182,46 +181,45 @@ void SettingsActivity::toggleCurrentSetting() {
|
|||||||
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
|
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
|
||||||
}
|
}
|
||||||
} else if (setting.type == SettingType::ACTION) {
|
} else if (setting.type == SettingType::ACTION) {
|
||||||
if (strcmp(setting.name, "Remap Front Buttons") == 0) {
|
auto enterSubActivity = [this](Activity* activity) {
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new ButtonRemapActivity(renderer, mappedInput, [this] {
|
enterNewActivity(activity);
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto onComplete = [this] {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}));
|
};
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
} else if (strcmp(setting.name, "KOReader Sync") == 0) {
|
auto onCompleteBool = [this](bool) {
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {
|
|
||||||
exitActivity();
|
exitActivity();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}));
|
};
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
} else if (strcmp(setting.name, "OPDS Browser") == 0) {
|
switch (setting.action) {
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
case SettingAction::RemapFrontButtons:
|
||||||
exitActivity();
|
enterSubActivity(new ButtonRemapActivity(renderer, mappedInput, onComplete));
|
||||||
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
|
break;
|
||||||
exitActivity();
|
case SettingAction::KOReaderSync:
|
||||||
updateRequired = true;
|
enterSubActivity(new KOReaderSettingsActivity(renderer, mappedInput, onComplete));
|
||||||
}));
|
break;
|
||||||
xSemaphoreGive(renderingMutex);
|
case SettingAction::OPDSBrowser:
|
||||||
} else if (strcmp(setting.name, "Clear Cache") == 0) {
|
enterSubActivity(new CalibreSettingsActivity(renderer, mappedInput, onComplete));
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
break;
|
||||||
exitActivity();
|
case SettingAction::Network:
|
||||||
enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] {
|
enterSubActivity(new WifiSelectionActivity(renderer, mappedInput, onCompleteBool, false));
|
||||||
exitActivity();
|
break;
|
||||||
updateRequired = true;
|
case SettingAction::ClearCache:
|
||||||
}));
|
enterSubActivity(new ClearCacheActivity(renderer, mappedInput, onComplete));
|
||||||
xSemaphoreGive(renderingMutex);
|
break;
|
||||||
} else if (strcmp(setting.name, "Check for updates") == 0) {
|
case SettingAction::CheckForUpdates:
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
enterSubActivity(new OtaUpdateActivity(renderer, mappedInput, onComplete));
|
||||||
exitActivity();
|
break;
|
||||||
enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] {
|
case SettingAction::None:
|
||||||
exitActivity();
|
// Do nothing
|
||||||
updateRequired = true;
|
break;
|
||||||
}));
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -8,16 +8,28 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class CrossPointSettings;
|
class CrossPointSettings;
|
||||||
|
|
||||||
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING };
|
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING };
|
||||||
|
|
||||||
|
enum class SettingAction {
|
||||||
|
None,
|
||||||
|
RemapFrontButtons,
|
||||||
|
KOReaderSync,
|
||||||
|
OPDSBrowser,
|
||||||
|
Network,
|
||||||
|
ClearCache,
|
||||||
|
CheckForUpdates,
|
||||||
|
};
|
||||||
|
|
||||||
struct SettingInfo {
|
struct SettingInfo {
|
||||||
const char* name;
|
const char* name;
|
||||||
SettingType type;
|
SettingType type;
|
||||||
uint8_t CrossPointSettings::* valuePtr = nullptr;
|
uint8_t CrossPointSettings::* valuePtr = nullptr;
|
||||||
std::vector<std::string> enumValues;
|
std::vector<std::string> enumValues;
|
||||||
|
SettingAction action = SettingAction::None;
|
||||||
|
|
||||||
struct ValueRange {
|
struct ValueRange {
|
||||||
uint8_t min;
|
uint8_t min;
|
||||||
@@ -62,10 +74,11 @@ struct SettingInfo {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
static SettingInfo Action(const char* name) {
|
static SettingInfo Action(const char* name, SettingAction action) {
|
||||||
SettingInfo s;
|
SettingInfo s;
|
||||||
s.name = name;
|
s.name = name;
|
||||||
s.type = SettingType::ACTION;
|
s.type = SettingType::ACTION;
|
||||||
|
s.action = action;
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +137,7 @@ struct SettingInfo {
|
|||||||
class SettingsActivity final : public ActivityWithSubactivity {
|
class SettingsActivity final : public ActivityWithSubactivity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
int selectedCategoryIndex = 0; // Currently selected category
|
int selectedCategoryIndex = 0; // Currently selected category
|
||||||
int selectedSettingIndex = 0;
|
int selectedSettingIndex = 0;
|
||||||
|
|||||||
@@ -142,37 +142,24 @@ void KeyboardEntryActivity::handleKeyPress() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardEntryActivity::loop() {
|
void KeyboardEntryActivity::loop() {
|
||||||
// Navigation
|
// Handle navigation
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] {
|
||||||
if (selectedRow > 0) {
|
selectedRow = ButtonNavigator::previousIndex(selectedRow, NUM_ROWS);
|
||||||
selectedRow--;
|
|
||||||
// Clamp column to valid range for new row
|
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
|
||||||
} else {
|
|
||||||
// Wrap to bottom row
|
|
||||||
selectedRow = NUM_ROWS - 1;
|
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
|
|
||||||
if (selectedRow < NUM_ROWS - 1) {
|
|
||||||
selectedRow++;
|
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
const int maxCol = getRowLength(selectedRow) - 1;
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
if (selectedCol > maxCol) selectedCol = maxCol;
|
||||||
} else {
|
|
||||||
// Wrap to top row
|
|
||||||
selectedRow = 0;
|
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] {
|
||||||
|
selectedRow = ButtonNavigator::nextIndex(selectedRow, NUM_ROWS);
|
||||||
|
|
||||||
|
const int maxCol = getRowLength(selectedRow) - 1;
|
||||||
|
if (selectedCol > maxCol) selectedCol = maxCol;
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] {
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
const int maxCol = getRowLength(selectedRow) - 1;
|
||||||
|
|
||||||
// Special bottom row case
|
// Special bottom row case
|
||||||
@@ -191,20 +178,14 @@ void KeyboardEntryActivity::loop() {
|
|||||||
// At done button, move to backspace
|
// At done button, move to backspace
|
||||||
selectedCol = BACKSPACE_COL;
|
selectedCol = BACKSPACE_COL;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCol > 0) {
|
|
||||||
selectedCol--;
|
|
||||||
} else {
|
} else {
|
||||||
// Wrap to end of current row
|
selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1);
|
||||||
selectedCol = maxCol;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] {
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
const int maxCol = getRowLength(selectedRow) - 1;
|
||||||
|
|
||||||
// Special bottom row case
|
// Special bottom row case
|
||||||
@@ -223,18 +204,11 @@ void KeyboardEntryActivity::loop() {
|
|||||||
// At done button, wrap to beginning of row
|
// At done button, wrap to beginning of row
|
||||||
selectedCol = SHIFT_COL;
|
selectedCol = SHIFT_COL;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCol < maxCol) {
|
|
||||||
selectedCol++;
|
|
||||||
} else {
|
} else {
|
||||||
// Wrap to beginning of current row
|
selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1);
|
||||||
selectedCol = 0;
|
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable keyboard entry activity for text input.
|
* Reusable keyboard entry activity for text input.
|
||||||
@@ -65,6 +66,7 @@ class KeyboardEntryActivity : public Activity {
|
|||||||
bool isPassword;
|
bool isPassword;
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|
||||||
// Keyboard state
|
// Keyboard state
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#include "activities/util/FullScreenMessageActivity.h"
|
#include "activities/util/FullScreenMessageActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
HalDisplay display;
|
HalDisplay display;
|
||||||
HalGPIO gpio;
|
HalGPIO gpio;
|
||||||
@@ -337,6 +338,7 @@ void setup() {
|
|||||||
SETTINGS.loadFromFile();
|
SETTINGS.loadFromFile();
|
||||||
KOREADER_STORE.loadFromFile();
|
KOREADER_STORE.loadFromFile();
|
||||||
UITheme::getInstance().reload();
|
UITheme::getInstance().reload();
|
||||||
|
ButtonNavigator::setMappedInputManager(mappedInputManager);
|
||||||
|
|
||||||
switch (gpio.getWakeupReason()) {
|
switch (gpio.getWakeupReason()) {
|
||||||
case HalGPIO::WakeupReason::PowerButton:
|
case HalGPIO::WakeupReason::PowerButton:
|
||||||
|
|||||||
124
src/util/ButtonNavigator.cpp
Normal file
124
src/util/ButtonNavigator.cpp
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#include "ButtonNavigator.h"
|
||||||
|
|
||||||
|
const MappedInputManager* ButtonNavigator::mappedInput = nullptr;
|
||||||
|
|
||||||
|
void ButtonNavigator::onNext(const Callback& callback) {
|
||||||
|
onNextPress(callback);
|
||||||
|
onNextContinuous(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonNavigator::onPrevious(const Callback& callback) {
|
||||||
|
onPreviousPress(callback);
|
||||||
|
onPreviousContinuous(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonNavigator::onPressAndContinuous(const Buttons& buttons, const Callback& callback) {
|
||||||
|
onPress(buttons, callback);
|
||||||
|
onContinuous(buttons, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonNavigator::onNextPress(const Callback& callback) { onPress(getNextButtons(), callback); }
|
||||||
|
|
||||||
|
void ButtonNavigator::onPreviousPress(const Callback& callback) { onPress(getPreviousButtons(), callback); }
|
||||||
|
|
||||||
|
void ButtonNavigator::onNextRelease(const Callback& callback) { onRelease(getNextButtons(), callback); }
|
||||||
|
|
||||||
|
void ButtonNavigator::onPreviousRelease(const Callback& callback) { onRelease(getPreviousButtons(), callback); }
|
||||||
|
|
||||||
|
void ButtonNavigator::onNextContinuous(const Callback& callback) { onContinuous(getNextButtons(), callback); }
|
||||||
|
|
||||||
|
void ButtonNavigator::onPreviousContinuous(const Callback& callback) { onContinuous(getPreviousButtons(), callback); }
|
||||||
|
|
||||||
|
void ButtonNavigator::onPress(const Buttons& buttons, const Callback& callback) {
|
||||||
|
const bool wasPressed = std::any_of(buttons.begin(), buttons.end(), [](const MappedInputManager::Button button) {
|
||||||
|
return mappedInput != nullptr && mappedInput->wasPressed(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wasPressed) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonNavigator::onRelease(const Buttons& buttons, const Callback& callback) {
|
||||||
|
const bool wasReleased = std::any_of(buttons.begin(), buttons.end(), [](const MappedInputManager::Button button) {
|
||||||
|
return mappedInput != nullptr && mappedInput->wasReleased(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wasReleased) {
|
||||||
|
if (lastContinuousNavTime == 0) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastContinuousNavTime = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonNavigator::onContinuous(const Buttons& buttons, const Callback& callback) {
|
||||||
|
const bool isPressed = std::any_of(buttons.begin(), buttons.end(), [this](const MappedInputManager::Button button) {
|
||||||
|
return mappedInput != nullptr && mappedInput->isPressed(button) && shouldNavigateContinuously();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isPressed) {
|
||||||
|
callback();
|
||||||
|
lastContinuousNavTime = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ButtonNavigator::shouldNavigateContinuously() const {
|
||||||
|
if (!mappedInput) return false;
|
||||||
|
|
||||||
|
const bool buttonHeldLongEnough = mappedInput->getHeldTime() > continuousStartMs;
|
||||||
|
const bool navigationIntervalElapsed = (millis() - lastContinuousNavTime) > continuousIntervalMs;
|
||||||
|
|
||||||
|
return buttonHeldLongEnough && navigationIntervalElapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ButtonNavigator::nextIndex(const int currentIndex, const int totalItems) {
|
||||||
|
if (totalItems <= 0) return 0;
|
||||||
|
|
||||||
|
// Calculate the next index with wrap-around
|
||||||
|
return (currentIndex + 1) % totalItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ButtonNavigator::previousIndex(const int currentIndex, const int totalItems) {
|
||||||
|
if (totalItems <= 0) return 0;
|
||||||
|
|
||||||
|
// Calculate the previous index with wrap-around
|
||||||
|
return (currentIndex + totalItems - 1) % totalItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ButtonNavigator::nextPageIndex(const int currentIndex, const int totalItems, const int itemsPerPage) {
|
||||||
|
if (totalItems <= 0 || itemsPerPage <= 0) return 0;
|
||||||
|
|
||||||
|
// When items fit on one page, use index navigation instead
|
||||||
|
if (totalItems <= itemsPerPage) {
|
||||||
|
return nextIndex(currentIndex, totalItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int lastPageIndex = (totalItems - 1) / itemsPerPage;
|
||||||
|
const int currentPageIndex = currentIndex / itemsPerPage;
|
||||||
|
|
||||||
|
if (currentPageIndex < lastPageIndex) {
|
||||||
|
return (currentPageIndex + 1) * itemsPerPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ButtonNavigator::previousPageIndex(const int currentIndex, const int totalItems, const int itemsPerPage) {
|
||||||
|
if (totalItems <= 0 || itemsPerPage <= 0) return 0;
|
||||||
|
|
||||||
|
// When items fit on one page, use index navigation instead
|
||||||
|
if (totalItems <= itemsPerPage) {
|
||||||
|
return previousIndex(currentIndex, totalItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int lastPageIndex = (totalItems - 1) / itemsPerPage;
|
||||||
|
const int currentPageIndex = currentIndex / itemsPerPage;
|
||||||
|
|
||||||
|
if (currentPageIndex > 0) {
|
||||||
|
return (currentPageIndex - 1) * itemsPerPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastPageIndex * itemsPerPage;
|
||||||
|
}
|
||||||
53
src/util/ButtonNavigator.h
Normal file
53
src/util/ButtonNavigator.h
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
|
||||||
|
class ButtonNavigator final {
|
||||||
|
using Callback = std::function<void()>;
|
||||||
|
using Buttons = std::vector<MappedInputManager::Button>;
|
||||||
|
|
||||||
|
const uint16_t continuousStartMs;
|
||||||
|
const uint16_t continuousIntervalMs;
|
||||||
|
uint32_t lastContinuousNavTime = 0;
|
||||||
|
static const MappedInputManager* mappedInput;
|
||||||
|
|
||||||
|
[[nodiscard]] bool shouldNavigateContinuously() const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ButtonNavigator(const uint16_t continuousIntervalMs = 500, const uint16_t continuousStartMs = 500)
|
||||||
|
: continuousStartMs(continuousStartMs), continuousIntervalMs(continuousIntervalMs) {}
|
||||||
|
|
||||||
|
static void setMappedInputManager(const MappedInputManager& mappedInputManager) { mappedInput = &mappedInputManager; }
|
||||||
|
|
||||||
|
void onNext(const Callback& callback);
|
||||||
|
void onPrevious(const Callback& callback);
|
||||||
|
void onPressAndContinuous(const Buttons& buttons, const Callback& callback);
|
||||||
|
|
||||||
|
void onNextPress(const Callback& callback);
|
||||||
|
void onPreviousPress(const Callback& callback);
|
||||||
|
void onPress(const Buttons& buttons, const Callback& callback);
|
||||||
|
|
||||||
|
void onNextRelease(const Callback& callback);
|
||||||
|
void onPreviousRelease(const Callback& callback);
|
||||||
|
void onRelease(const Buttons& buttons, const Callback& callback);
|
||||||
|
|
||||||
|
void onNextContinuous(const Callback& callback);
|
||||||
|
void onPreviousContinuous(const Callback& callback);
|
||||||
|
void onContinuous(const Buttons& buttons, const Callback& callback);
|
||||||
|
|
||||||
|
[[nodiscard]] static int nextIndex(int currentIndex, int totalItems);
|
||||||
|
[[nodiscard]] static int previousIndex(int currentIndex, int totalItems);
|
||||||
|
|
||||||
|
[[nodiscard]] static int nextPageIndex(int currentIndex, int totalItems, int itemsPerPage);
|
||||||
|
[[nodiscard]] static int previousPageIndex(int currentIndex, int totalItems, int itemsPerPage);
|
||||||
|
|
||||||
|
[[nodiscard]] static Buttons getNextButtons() {
|
||||||
|
return {MappedInputManager::Button::Down, MappedInputManager::Button::Right};
|
||||||
|
}
|
||||||
|
[[nodiscard]] static Buttons getPreviousButtons() {
|
||||||
|
return {MappedInputManager::Button::Up, MappedInputManager::Button::Left};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -25,6 +25,10 @@ std::string extractHost(const std::string& url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string buildUrl(const std::string& serverUrl, const std::string& path) {
|
std::string buildUrl(const std::string& serverUrl, const std::string& path) {
|
||||||
|
// If path is already an absolute URL (has protocol), use it directly
|
||||||
|
if (path.find("://") != std::string::npos) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
const std::string urlWithProtocol = ensureProtocol(serverUrl);
|
const std::string urlWithProtocol = ensureProtocol(serverUrl);
|
||||||
if (path.empty()) {
|
if (path.empty()) {
|
||||||
return urlWithProtocol;
|
return urlWithProtocol;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const std::vector<LanguageConfig> kSupportedLanguages = {
|
|||||||
{"german", "test/hyphenation_eval/resources/german_hyphenation_tests.txt", "de"},
|
{"german", "test/hyphenation_eval/resources/german_hyphenation_tests.txt", "de"},
|
||||||
{"russian", "test/hyphenation_eval/resources/russian_hyphenation_tests.txt", "ru"},
|
{"russian", "test/hyphenation_eval/resources/russian_hyphenation_tests.txt", "ru"},
|
||||||
{"spanish", "test/hyphenation_eval/resources/spanish_hyphenation_tests.txt", "es"},
|
{"spanish", "test/hyphenation_eval/resources/spanish_hyphenation_tests.txt", "es"},
|
||||||
|
{"italian", "test/hyphenation_eval/resources/italian_hyphenation_tests.txt", "it"},
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<size_t> expectedPositionsFromAnnotatedWord(const std::string& annotated) {
|
std::vector<size_t> expectedPositionsFromAnnotatedWord(const std::string& annotated) {
|
||||||
|
|||||||
5012
test/hyphenation_eval/resources/italian_hyphenation_tests.txt
Normal file
5012
test/hyphenation_eval/resources/italian_hyphenation_tests.txt
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user