mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 16:52:22 +00:00
Consolidate companion components
This commit is contained in:
1 parent
aab8f5038a
commit
ebee99af81
32 files changed
+286
-287
No files matched your search
@@ -179,12 +179,12 @@ for the state machine and limitations.
|
||||
ESP32 and nRF52 Full Companions include an offline World English Bible lookup:
|
||||
|
||||
```text
|
||||
get John 3:16
|
||||
get reader 3:16
|
||||
```
|
||||
|
||||
This returns a single complete verse from John, using plain ASCII punctuation
|
||||
This returns a single complete verse from the embedded corpus, using plain ASCII punctuation
|
||||
with unchanged wording and capitalization, through the local USB/TCP terminal.
|
||||
See [John lookup and compression](https://github.com/mikecarper/MeshCore/blob/keymindCascade/tools/bible/README.md) for the
|
||||
See [Reader lookup and compression](https://github.com/mikecarper/MeshCore/blob/keymindCascade/tools/bible/README.md) for the
|
||||
source, supported build profiles and memory costs.
|
||||
|
||||
```
|
||||
|
||||
@@ -51,7 +51,7 @@ CLI rescue instead; wait for that startup window to finish before opening
|
||||
the inbox. Exiting a message preview and the WiFi setup page's hold action
|
||||
remain available immediately.
|
||||
|
||||
The hidden [John reader](https://github.com/mikecarper/MeshCore/blob/keymindCascade/tools/bible/README.md#on-device-reader), opened
|
||||
The hidden [Reader](https://github.com/mikecarper/MeshCore/blob/keymindCascade/tools/bible/README.md#on-device-reader), opened
|
||||
by a long press on the radio page, uses the same button hints. Three taps jump
|
||||
to the first verse of the next chapter; four taps jump to the first verse of
|
||||
the previous chapter. Chapter navigation stops at the beginning and end of
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#include <helpers/CompanionJohnConfig.h>
|
||||
#include <helpers/CompanionReaderConfig.h>
|
||||
#include <helpers/ota/OtaMemoryPolicy.h>
|
||||
|
||||
// The role terminal is also used by WebConfig on WiFi-only Companions. Its
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
#include <helpers/bible/JohnBookmarkFiles.h>
|
||||
#if COMPANION_FEATURE_READER
|
||||
#include <helpers/bible/ReaderBookmarkFiles.h>
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
@@ -446,12 +446,12 @@ File DataStore::openRead(const char* filename) {
|
||||
return openRead(_fs, filename);
|
||||
}
|
||||
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
#if COMPANION_FEATURE_READER
|
||||
namespace {
|
||||
class JohnBookmarkFiles {
|
||||
class ReaderBookmarkFiles {
|
||||
FILESYSTEM* _fs;
|
||||
public:
|
||||
explicit JohnBookmarkFiles(FILESYSTEM* fs) : _fs(fs) {}
|
||||
explicit ReaderBookmarkFiles(FILESYSTEM* fs) : _fs(fs) {}
|
||||
bool exists(const char* path) {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
bool present = false;
|
||||
@@ -496,20 +496,20 @@ public:
|
||||
};
|
||||
} // namespace
|
||||
|
||||
bool DataStore::loadJohnBookmark(mesh::bible::Position& pos) {
|
||||
bool DataStore::loadReaderBookmark(mesh::bible::Position& pos) {
|
||||
pos = mesh::bible::Position{};
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (_primary_storage_unavailable) return false;
|
||||
#endif
|
||||
JohnBookmarkFiles files(_fs);
|
||||
ReaderBookmarkFiles files(_fs);
|
||||
return mesh::bible::loadReaderBookmark(files, pos);
|
||||
}
|
||||
|
||||
bool DataStore::saveJohnBookmark(mesh::bible::Position pos) {
|
||||
bool DataStore::saveReaderBookmark(mesh::bible::Position pos) {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (_primary_storage_unavailable) return false;
|
||||
#endif
|
||||
JohnBookmarkFiles files(_fs);
|
||||
ReaderBookmarkFiles files(_fs);
|
||||
return mesh::bible::saveReaderBookmark(files, pos);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
#if MESH_CONTACT_CACHE
|
||||
#include <helpers/ContactSecretCache.h>
|
||||
#endif
|
||||
#include <helpers/CompanionJohnConfig.h>
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
#include <helpers/bible/JohnReader.h>
|
||||
#include <helpers/CompanionReaderConfig.h>
|
||||
#if COMPANION_FEATURE_READER
|
||||
#include <helpers/bible/Reader.h>
|
||||
#endif
|
||||
|
||||
class DataStoreHost {
|
||||
@@ -116,9 +116,9 @@ public:
|
||||
bool loadPrefs(CompanionNodePrefs& prefs, double& node_lat,
|
||||
double& node_lon);
|
||||
bool savePrefs(const CompanionNodePrefs& prefs, double node_lat, double node_lon);
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
bool loadJohnBookmark(mesh::bible::Position& pos);
|
||||
bool saveJohnBookmark(mesh::bible::Position pos);
|
||||
#if COMPANION_FEATURE_READER
|
||||
bool loadReaderBookmark(mesh::bible::Position& pos);
|
||||
bool saveReaderBookmark(mesh::bible::Position pos);
|
||||
#endif
|
||||
void loadContacts(DataStoreHost* host);
|
||||
bool saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include <Mesh.h>
|
||||
#include <helpers/CompanionHardwareCommandCompat.h>
|
||||
#include <helpers/CompanionStatusResponse.h>
|
||||
#include <helpers/CompanionJohn.h>
|
||||
#include <helpers/CompanionReader.h>
|
||||
#include <helpers/IdentityGeneration.h>
|
||||
#include <helpers/LazyPersistence.h>
|
||||
#include <helpers/StorageLayout.h>
|
||||
@@ -7881,8 +7881,8 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
if (*command == 0) return;
|
||||
mesh::cli::normalizeCommandVerb(command);
|
||||
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
if (mesh::handleJohnCommand(command, terminalOutput())) return;
|
||||
#if COMPANION_FEATURE_READER
|
||||
if (mesh::handleReaderCommand(command, terminalOutput())) return;
|
||||
#endif
|
||||
|
||||
char local_reply[160];
|
||||
@@ -8362,8 +8362,8 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
terminalOutput().print(" board\r\n");
|
||||
terminalOutput().print(" version\r\n");
|
||||
terminalOutput().print(" get storage.layout\r\n");
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
terminalOutput().print(" get John <chapter>:<verse> (World English Bible, offline)\r\n");
|
||||
#if COMPANION_FEATURE_READER
|
||||
terminalOutput().print(" get reader <chapter>:<verse> (World English Bible, offline)\r\n");
|
||||
#endif
|
||||
terminalOutput().print(" get pwrmgt.bootreason\r\n");
|
||||
#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS
|
||||
|
||||
@@ -360,9 +360,9 @@ public:
|
||||
if (saved) _prefs.clearDirty();
|
||||
return saved;
|
||||
}
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
bool loadJohnBookmark(mesh::bible::Position& pos) { return _store->loadJohnBookmark(pos); }
|
||||
bool saveJohnBookmark(mesh::bible::Position pos) { return _store->saveJohnBookmark(pos); }
|
||||
#if COMPANION_FEATURE_READER
|
||||
bool loadReaderBookmark(mesh::bible::Position& pos) { return _store->loadReaderBookmark(pos); }
|
||||
bool saveReaderBookmark(mesh::bible::Position pos) { return _store->saveReaderBookmark(pos); }
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
|
||||
+11
-11
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
#include <helpers/CompanionJohn.h>
|
||||
#include <helpers/bible/JohnReader.h>
|
||||
#include <helpers/CompanionReader.h>
|
||||
#include <helpers/bible/Reader.h>
|
||||
#include <helpers/ui/ReaderNavigationHint.h>
|
||||
#if UI_SMALL_MESSAGE_FONT || UI_BUTTON_READER_HINT
|
||||
#include <helpers/ui/SmallMessageText.h>
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
// Included by UITask.cpp after the task and MyMesh declarations. Only a small
|
||||
// cursor/checkpoint object persists; the 2 KiB decode buffer is on demand.
|
||||
class JohnReaderScreen : public UIScreen {
|
||||
class ReaderScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
DisplayDriver* _display;
|
||||
mesh::bible::ReaderBookmark _bookmark;
|
||||
@@ -79,12 +79,12 @@ class JohnReaderScreen : public UIScreen {
|
||||
// At most two lookups: the current verse, then its adjacent verse when
|
||||
// navigation crosses a boundary. No recursive decode or second buffer.
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
const LookupResult result = mesh::readJohnVerse(
|
||||
const LookupResult result = mesh::readReaderVerse(
|
||||
referenceAt(pos.verse), scratch, sizeof(scratch), text);
|
||||
if (result != LookupResult::Found) {
|
||||
if (draw) {
|
||||
body.setColor(UIColor::warning_txt);
|
||||
body.drawTextEllipsized(0, top, width, "John unavailable");
|
||||
body.drawTextEllipsized(0, top, width, "Reader unavailable");
|
||||
#if UI_BUTTON_READER_HINT
|
||||
#if UI_READER_TOUCH_BAR
|
||||
mesh::ui::drawReaderTouchBar(hint_text, _touch_bar);
|
||||
@@ -92,7 +92,7 @@ class JohnReaderScreen : public UIScreen {
|
||||
mesh::ui::drawButtonReaderHint(hint_text, hint);
|
||||
#endif
|
||||
#endif
|
||||
} else _task->showAlert("John data error", 1500);
|
||||
} else _task->showAlert("Reader data error", 1500);
|
||||
return;
|
||||
}
|
||||
memmove(scratch, text, strlen(text) + 1);
|
||||
@@ -114,7 +114,7 @@ class JohnReaderScreen : public UIScreen {
|
||||
if (!draw) return;
|
||||
char label[32], progress[16];
|
||||
const Reference ref = referenceAt(pos.verse);
|
||||
snprintf(label, sizeof(label), "John %u:%u", ref.chapter, ref.verse);
|
||||
snprintf(label, sizeof(label), "Reader %u:%u", ref.chapter, ref.verse);
|
||||
snprintf(progress, sizeof(progress), "%u/%u", page.part, page.parts);
|
||||
const int progress_width = _display->getTextWidth(progress);
|
||||
if (stacked_header || _display->getTextWidth(label) > width - progress_width - 4)
|
||||
@@ -147,18 +147,18 @@ public:
|
||||
#if UI_READER_TOUCH_BAR
|
||||
const mesh::ui::TouchNavigationBar* readerTouchBar() const { return &_touch_bar; }
|
||||
#endif
|
||||
JohnReaderScreen(UITask* task, DisplayDriver* display) : _task(task), _display(display) {}
|
||||
ReaderScreen(UITask* task, DisplayDriver* display) : _task(task), _display(display) {}
|
||||
void open() {
|
||||
if (!_loaded) {
|
||||
mesh::bible::Position pos;
|
||||
the_mesh.loadJohnBookmark(pos);
|
||||
the_mesh.loadReaderBookmark(pos);
|
||||
_bookmark.restore(pos);
|
||||
_loaded = true;
|
||||
}
|
||||
}
|
||||
bool flush() {
|
||||
if (!_bookmark.dirty()) return true;
|
||||
if (!the_mesh.saveJohnBookmark(_bookmark.position())) {
|
||||
if (!the_mesh.saveReaderBookmark(_bookmark.position())) {
|
||||
_retry_at = millis() + 30000;
|
||||
return false;
|
||||
}
|
||||
@@ -186,7 +186,7 @@ public:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (key == KEY_ENTER || key == KEY_CANCEL) { _task->closeJohnReader(); return true; }
|
||||
if (key == KEY_ENTER || key == KEY_CANCEL) { _task->closeReader(); return true; }
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -90,8 +90,8 @@ static uint64_t companionMessageElapsedMillis(uint64_t heard_millis) {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
#include "JohnReaderScreen.h"
|
||||
#if COMPANION_FEATURE_READER
|
||||
#include "ReaderScreen.h"
|
||||
#include <new>
|
||||
#endif
|
||||
|
||||
@@ -575,7 +575,7 @@ public:
|
||||
}
|
||||
|
||||
void showFirstPage() { _page = HomePage::FIRST; }
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
#if COMPANION_FEATURE_READER
|
||||
bool isRadioPage() const { return _page == HomePage::RADIO; }
|
||||
#endif
|
||||
|
||||
@@ -1169,9 +1169,9 @@ public:
|
||||
_page = (_page + HomePage::Count - 1) % HomePage::Count;
|
||||
return true;
|
||||
}
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
#if COMPANION_FEATURE_READER
|
||||
if (key == KEY_ENTER && _page == HomePage::RADIO) {
|
||||
_task->showJohnReader();
|
||||
_task->showReader();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -1823,17 +1823,17 @@ void UITask::showMessages() {
|
||||
setCurrScreen(msg_preview);
|
||||
}
|
||||
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
void UITask::showJohnReader() {
|
||||
#if COMPANION_FEATURE_READER
|
||||
void UITask::showReader() {
|
||||
if (!_display || isPairingScreenActive()) return;
|
||||
if (!_display->isOn()) _display->wake(mesh::ui::DisplayWake::Button);
|
||||
if (!john_reader) john_reader = new (std::nothrow) JohnReaderScreen(this, _display);
|
||||
if (!john_reader) { showAlert("Reader: no memory", 1500); return; }
|
||||
static_cast<JohnReaderScreen*>(john_reader)->open();
|
||||
setCurrScreen(john_reader);
|
||||
if (!reader) reader = new (std::nothrow) ReaderScreen(this, _display);
|
||||
if (!reader) { showAlert("Reader: no memory", 1500); return; }
|
||||
static_cast<ReaderScreen*>(reader)->open();
|
||||
setCurrScreen(reader);
|
||||
}
|
||||
|
||||
void UITask::closeJohnReader() {
|
||||
void UITask::closeReader() {
|
||||
// Home retains the selected radio page while the reader is open.
|
||||
setCurrScreen(home);
|
||||
}
|
||||
@@ -1911,8 +1911,8 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text,
|
||||
// pairing completes or the pairing display window expires.
|
||||
_deferred_msg_preview = true;
|
||||
}
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
else if (isJohnReaderActive()) {
|
||||
#if COMPANION_FEATURE_READER
|
||||
else if (isReaderActive()) {
|
||||
// Queue incoming messages without stealing the reader's page.
|
||||
_deferred_msg_preview = true;
|
||||
}
|
||||
@@ -1951,9 +1951,9 @@ void UITask::userLedHandler() {
|
||||
}
|
||||
|
||||
void UITask::setCurrScreen(UIScreen* c) {
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
if (isJohnReaderActive() && c != john_reader
|
||||
&& !static_cast<JohnReaderScreen*>(john_reader)->flush()) {
|
||||
#if COMPANION_FEATURE_READER
|
||||
if (isReaderActive() && c != reader
|
||||
&& !static_cast<ReaderScreen*>(reader)->flush()) {
|
||||
showAlert("Bookmark save failed", 1500);
|
||||
}
|
||||
#endif
|
||||
@@ -2033,9 +2033,9 @@ void UITask::servicePairingState() {
|
||||
_board->isExternalPowered() || _board->isUsbHostConnected(),
|
||||
hasConnection(), isPairingScreenActive())) {
|
||||
_next_refresh = 0;
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
if (!_display->isOn() && isJohnReaderActive())
|
||||
static_cast<JohnReaderScreen*>(john_reader)->flush();
|
||||
#if COMPANION_FEATURE_READER
|
||||
if (!_display->isOn() && isReaderActive())
|
||||
static_cast<ReaderScreen*>(reader)->flush();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -2045,8 +2045,8 @@ void UITask::servicePairingState() {
|
||||
*/
|
||||
void UITask::shutdown(bool restart){
|
||||
if (!prepareForShutdown()) return;
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
if (john_reader) static_cast<JohnReaderScreen*>(john_reader)->flush();
|
||||
#if COMPANION_FEATURE_READER
|
||||
if (reader) static_cast<ReaderScreen*>(reader)->flush();
|
||||
#endif
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
@@ -2126,8 +2126,8 @@ void UITask::loop() {
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
c = checkDisplayOn(KEY_ENTER);
|
||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
if (isJohnReaderActive() || (curr == home && static_cast<HomeScreen*>(home)->isRadioPage()))
|
||||
#if COMPANION_FEATURE_READER
|
||||
if (isReaderActive() || (curr == home && static_cast<HomeScreen*>(home)->isRadioPage()))
|
||||
c = handleLongPress(KEY_ENTER);
|
||||
else
|
||||
#endif
|
||||
@@ -2277,8 +2277,8 @@ void UITask::loop() {
|
||||
#endif
|
||||
|
||||
if (curr) curr->poll();
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
if (john_reader && curr != john_reader) john_reader->poll();
|
||||
#if COMPANION_FEATURE_READER
|
||||
if (reader && curr != reader) reader->poll();
|
||||
#endif
|
||||
|
||||
if (_display != NULL && _display->isOn()) {
|
||||
@@ -2372,9 +2372,9 @@ void UITask::getTouchControls(mesh::ui::TouchSplitSelector& transport_touch_sele
|
||||
#if UI_READER_TOUCH_BAR
|
||||
if (curr == msg_preview)
|
||||
reader_touch_bar = static_cast<MsgPreviewScreen*>(msg_preview)->readerTouchBar();
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
else if (isJohnReaderActive())
|
||||
reader_touch_bar = static_cast<JohnReaderScreen*>(john_reader)->readerTouchBar();
|
||||
#if COMPANION_FEATURE_READER
|
||||
else if (isReaderActive())
|
||||
reader_touch_bar = static_cast<ReaderScreen*>(reader)->readerTouchBar();
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
@@ -2396,12 +2396,12 @@ char UITask::handleLongPress(char c) {
|
||||
// Exit an incoming message preview even during the startup rescue window.
|
||||
// The on-screen reader hint must not unexpectedly enter CLI rescue.
|
||||
if (curr == msg_preview) return c;
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
#if COMPANION_FEATURE_READER
|
||||
// The radio page's reader gesture, including exit, takes precedence over
|
||||
// the global early-boot rescue gesture just like the WiFi setup page.
|
||||
if (isJohnReaderActive()) { closeJohnReader(); return 0; }
|
||||
if (isReaderActive()) { closeReader(); return 0; }
|
||||
if (curr == home && static_cast<HomeScreen*>(home)->isRadioPage()) {
|
||||
showJohnReader();
|
||||
showReader();
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
@@ -2428,8 +2428,8 @@ char UITask::handleDoubleClick(char c) {
|
||||
char UITask::handleMultiClick(char c, bool backwards) {
|
||||
MESH_DEBUG_PRINTLN("UITask: %s-click triggered", backwards ? "quadruple" : "triple");
|
||||
if (curr == msg_preview
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
|| isJohnReaderActive()
|
||||
#if COMPANION_FEATURE_READER
|
||||
|| isReaderActive()
|
||||
#endif
|
||||
) {
|
||||
return checkDisplayOn(backwards ? KEY_UP : KEY_DOWN);
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
#include "../AbstractUITask.h"
|
||||
#include "../NodePrefs.h"
|
||||
#include <helpers/CompanionJohnConfig.h>
|
||||
#include <helpers/CompanionReaderConfig.h>
|
||||
|
||||
class UITask : public AbstractUITask {
|
||||
DisplayDriver* _display;
|
||||
@@ -94,8 +94,8 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* home;
|
||||
UIScreen* msg_preview = nullptr;
|
||||
UIScreen* curr;
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
UIScreen* john_reader = nullptr;
|
||||
#if COMPANION_FEATURE_READER
|
||||
UIScreen* reader = nullptr;
|
||||
#endif
|
||||
|
||||
void userLedHandler();
|
||||
@@ -132,10 +132,10 @@ public:
|
||||
|
||||
void gotoHomeScreen() { setCurrScreen(home); }
|
||||
void showMessages();
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
bool isJohnReaderActive() const { return john_reader && curr == john_reader; }
|
||||
void showJohnReader();
|
||||
void closeJohnReader();
|
||||
#if COMPANION_FEATURE_READER
|
||||
bool isReaderActive() const { return reader && curr == reader; }
|
||||
void showReader();
|
||||
void closeReader();
|
||||
#endif
|
||||
void showAlert(const char* text, int duration_millis);
|
||||
int getMsgCount() const { return _msgcount; }
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
#include "CompanionJohn.h"
|
||||
#include "CompanionReader.h"
|
||||
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
#if COMPANION_FEATURE_READER
|
||||
#include <Arduino.h>
|
||||
#include "bible/JohnLookup.h"
|
||||
#include "bible/ReaderLookup.h"
|
||||
|
||||
#include "bible/JohnData.generated.h"
|
||||
#include "bible/ReaderData.generated.h"
|
||||
|
||||
namespace mesh {
|
||||
bible::LookupResult readJohnVerse(bible::Reference ref, char* scratch,
|
||||
bible::LookupResult readReaderVerse(bible::Reference ref, char* scratch,
|
||||
size_t capacity, const char*& text) {
|
||||
return bible::lookup(bible::generated::johnCorpus, ref, scratch, capacity, text);
|
||||
return bible::lookup(bible::generated::readerCorpus, ref, scratch, capacity, text);
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -19,30 +19,30 @@ namespace {
|
||||
__attribute__((noinline)) void printVerse(bible::Reference ref, Stream& output) {
|
||||
char scratch[bible::kBlockSize];
|
||||
const char* text = nullptr;
|
||||
const bible::LookupResult result = readJohnVerse(ref, scratch, sizeof(scratch), text);
|
||||
const bible::LookupResult result = readReaderVerse(ref, scratch, sizeof(scratch), text);
|
||||
if (result == bible::LookupResult::Found) {
|
||||
output.printf(" John %u:%u (%s)\r\n", static_cast<unsigned>(ref.chapter),
|
||||
static_cast<unsigned>(ref.verse), bible::generated::johnCorpus.translation);
|
||||
output.printf(" Reader %u:%u (%s)\r\n", static_cast<unsigned>(ref.chapter),
|
||||
static_cast<unsigned>(ref.verse), bible::generated::readerCorpus.translation);
|
||||
// Do not feed long text through Adafruit Print::printf's 256-byte scratch.
|
||||
output.print(text);
|
||||
output.print("\r\n");
|
||||
output.print(bible::generated::johnCorpus.attribution);
|
||||
output.print(bible::generated::readerCorpus.attribution);
|
||||
output.print("\r\n");
|
||||
} else if (result == bible::LookupResult::Missing) {
|
||||
output.print(" This verse is not present in the supplied translation.\r\n");
|
||||
} else {
|
||||
output.print(" ERROR: invalid compressed John data\r\n");
|
||||
output.print(" ERROR: invalid compressed Reader data\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool handleJohnCommand(const char* command, Stream& output) {
|
||||
bool handleReaderCommand(const char* command, Stream& output) {
|
||||
bible::Reference ref = {};
|
||||
const bible::ParseResult parsed = bible::parse(command, ref);
|
||||
if (parsed == bible::ParseResult::NoMatch) return false;
|
||||
if (parsed == bible::ParseResult::Invalid) {
|
||||
output.print(" ERROR: use get John <chapter>:<verse> (example: get John 3:16)\r\n");
|
||||
output.print(" ERROR: use get reader <chapter>:<verse> (example: get reader 3:16)\r\n");
|
||||
} else {
|
||||
printVerse(ref, output);
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
#pragma once
|
||||
#include "CompanionJohnConfig.h"
|
||||
#include "CompanionReaderConfig.h"
|
||||
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
#include "bible/JohnLookup.h"
|
||||
#if COMPANION_FEATURE_READER
|
||||
#include "bible/ReaderLookup.h"
|
||||
class Stream;
|
||||
namespace mesh {
|
||||
// Caller-owned scratch lets the terminal and screen share one flash corpus.
|
||||
bible::LookupResult readJohnVerse(bible::Reference ref, char* scratch,
|
||||
bible::LookupResult readReaderVerse(bible::Reference ref, char* scratch,
|
||||
size_t capacity, const char*& text);
|
||||
// Terminal-only: verses can exceed the framed/remote CLI's 160-byte reply.
|
||||
bool handleJohnCommand(const char* command, Stream& output);
|
||||
bool handleReaderCommand(const char* command, Stream& output);
|
||||
}
|
||||
#endif
|
||||
@@ -1,16 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef COMPANION_FEATURE_JOHN
|
||||
#ifndef COMPANION_FEATURE_READER
|
||||
#if defined(COMPANION_RADIO_FULL) && defined(ENABLE_USB_INTERFACE) \
|
||||
&& (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM))
|
||||
#define COMPANION_FEATURE_JOHN 1
|
||||
#define COMPANION_FEATURE_READER 1
|
||||
#else
|
||||
#define COMPANION_FEATURE_JOHN 0
|
||||
#define COMPANION_FEATURE_READER 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if COMPANION_FEATURE_JOHN \
|
||||
#if COMPANION_FEATURE_READER \
|
||||
&& !(defined(COMPANION_RADIO_FULL) && defined(ENABLE_USB_INTERFACE) \
|
||||
&& (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)))
|
||||
#error "John lookup requires an ESP32 or nRF52 Full Companion USB terminal"
|
||||
#error "Reader lookup requires an ESP32 or nRF52 Full Companion USB terminal"
|
||||
#endif
|
||||
@@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#include "JohnLookup.h"
|
||||
#include "ReaderLookup.h"
|
||||
|
||||
namespace mesh { namespace bible {
|
||||
|
||||
@@ -92,7 +92,7 @@ inline uint32_t bookmarkChecksum(const uint8_t* data) {
|
||||
return hash;
|
||||
}
|
||||
inline void encodeBookmark(Position pos, uint8_t (&out)[kBookmarkBytes]) {
|
||||
out[0] = 'J'; out[1] = 'N'; out[2] = 1; out[3] = 0;
|
||||
out[0] = 'R'; out[1] = 'D'; out[2] = 1; out[3] = 0;
|
||||
out[4] = pos.verse; out[5] = pos.verse >> 8;
|
||||
out[6] = pos.offset; out[7] = pos.offset >> 8;
|
||||
const uint32_t hash = bookmarkChecksum(out);
|
||||
@@ -100,7 +100,7 @@ inline void encodeBookmark(Position pos, uint8_t (&out)[kBookmarkBytes]) {
|
||||
}
|
||||
inline bool decodeBookmark(const uint8_t (&in)[kBookmarkBytes], Position& pos) {
|
||||
pos = Position{};
|
||||
if (in[0] != 'J' || in[1] != 'N' || in[2] != 1 || in[3] != 0) return false;
|
||||
if (in[0] != 'R' || in[1] != 'D' || in[2] != 1 || in[3] != 0) return false;
|
||||
const uint32_t hash = bookmarkChecksum(in);
|
||||
for (unsigned i = 0; i < 4; ++i)
|
||||
if (in[8 + i] != static_cast<uint8_t>(hash >> (8 * i))) return false;
|
||||
@@ -112,7 +112,7 @@ inline bool decodeBookmark(const uint8_t (&in)[kBookmarkBytes], Position& pos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Debounced checkpointing: no writes just for opening John 1:1, no rewrite
|
||||
// Debounced checkpointing: no writes just for opening reader 1:1, no rewrite
|
||||
// of unchanged positions, and a failed save remains pending for retry.
|
||||
class ReaderBookmark {
|
||||
Position _position, _saved;
|
||||
@@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#include "JohnReader.h"
|
||||
#include "Reader.h"
|
||||
|
||||
namespace mesh { namespace bible {
|
||||
// A verified replacement plus one previous record also works on SPIFFS,
|
||||
@@ -9,8 +9,8 @@ template <typename Files>
|
||||
bool loadReaderBookmark(Files& files, Position& pos) {
|
||||
uint8_t data[kBookmarkBytes];
|
||||
pos = Position{};
|
||||
if (files.read("/john.pos", data) && decodeBookmark(data, pos)) return true;
|
||||
return files.read("/john.pos.bak", data) && decodeBookmark(data, pos);
|
||||
if (files.read("/reader.pos", data) && decodeBookmark(data, pos)) return true;
|
||||
return files.read("/reader.pos.bak", data) && decodeBookmark(data, pos);
|
||||
}
|
||||
|
||||
template <typename Files>
|
||||
@@ -19,26 +19,26 @@ bool saveReaderBookmark(Files& files, Position pos) {
|
||||
if (pos.atStart()) {
|
||||
// Remove the fallback before the live record so a completed clear cannot
|
||||
// resurrect an earlier place on reboot. Do not create a start marker.
|
||||
return files.remove("/john.pos.tmp") && files.remove("/john.pos.bak")
|
||||
&& files.remove("/john.pos");
|
||||
return files.remove("/reader.pos.tmp") && files.remove("/reader.pos.bak")
|
||||
&& files.remove("/reader.pos");
|
||||
}
|
||||
uint8_t data[kBookmarkBytes], verify[kBookmarkBytes];
|
||||
encodeBookmark(pos, data);
|
||||
if (!files.remove("/john.pos.tmp") || !files.write("/john.pos.tmp", data)) return false;
|
||||
if (!files.read("/john.pos.tmp", verify) || memcmp(data, verify, sizeof(data)) != 0)
|
||||
if (!files.remove("/reader.pos.tmp") || !files.write("/reader.pos.tmp", data)) return false;
|
||||
if (!files.read("/reader.pos.tmp", verify) || memcmp(data, verify, sizeof(data)) != 0)
|
||||
return false;
|
||||
if (files.exists("/john.pos")) {
|
||||
if (files.exists("/reader.pos")) {
|
||||
// Keep a valid fallback if the live record was interrupted/corrupt.
|
||||
Position old;
|
||||
// A failed read is not proof of corruption. Preserve the live record on
|
||||
// transient I/O failure rather than deleting it before publication.
|
||||
if (!files.read("/john.pos", verify)) return false;
|
||||
if (!files.read("/reader.pos", verify)) return false;
|
||||
if (decodeBookmark(verify, old)) {
|
||||
if (!files.remove("/john.pos.bak") || !files.rename("/john.pos", "/john.pos.bak"))
|
||||
if (!files.remove("/reader.pos.bak") || !files.rename("/reader.pos", "/reader.pos.bak"))
|
||||
return false;
|
||||
} else if (!files.remove("/john.pos")) return false;
|
||||
} else if (!files.remove("/reader.pos")) return false;
|
||||
}
|
||||
if (!files.rename("/john.pos.tmp", "/john.pos")) return false;
|
||||
if (!files.rename("/reader.pos.tmp", "/reader.pos")) return false;
|
||||
// Retain the last valid fallback until the next successful replacement.
|
||||
return true;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
// Generated by tools/bible/pack_john.py; do not edit.
|
||||
// World English Bible (engwebp), John. Public domain; see tools/bible/README.md.
|
||||
// Generated by tools/bible/pack_reader.py; do not edit.
|
||||
// Offline Reader corpus. Public domain; see tools/bible/README.md.
|
||||
// Firmware typography: ASCII quotes, apostrophes, dashes and spaces; wording unchanged.
|
||||
// Canonical source SHA-256: 916bf9d1feae0ad410d69f61e6c5f328ac6acba10651d751b5bb29719732c49e
|
||||
#pragma once
|
||||
#include "JohnLookup.h"
|
||||
#include "ReaderLookup.h"
|
||||
namespace mesh { namespace bible { namespace generated {
|
||||
static_assert(kBlockSize == 2048, "Regenerate John data after changing block size");
|
||||
static_assert(sizeof(BlockIndex) == 12, "John flash-size accounting expects a 12-byte index");
|
||||
static const uint8_t johnData[] = {
|
||||
static_assert(kBlockSize == 2048, "Regenerate Reader data after changing block size");
|
||||
static_assert(sizeof(BlockIndex) == 12, "Reader flash-size accounting expects a 12-byte index");
|
||||
static const uint8_t readerData[] = {
|
||||
0x8d,0x54,0xc1,0x6e,0xdb,0x30,0x0c,0xcd,0xa7,0x10,0xbe,0xf4,0x12,0xf8,0x03,0x76,0x29,0xb6,0x61,0xed,
|
||||
0x5a,0x6c,0xbb,0xac,0x40,0xcf,0x72,0x4c,0x5b,0x5a,0x64,0x29,0x90,0xec,0x06,0xd9,0xd7,0xef,0x91,0x92,
|
||||
0xdd,0xb4,0xdb,0x61,0x97,0xc4,0xa2,0x28,0xf2,0xf1,0xf1,0x91,0x0f,0x81,0x66,0xcb,0xd4,0xf1,0xe8,0x42,
|
||||
@@ -2175,7 +2175,7 @@ static const uint8_t johnData[] = {
|
||||
0xc6,0x51,0x3c,0x7b,0xbd,0x92,0xc0,0xdb,0xfa,0x70,0xd9,0x94,0x44,0xf6,0xab,0x34,0x0b,0xc1,0x09,0x7c,
|
||||
0x7b,0xf3,0x17,
|
||||
};
|
||||
static const BlockIndex johnBlocks[] = {
|
||||
static const BlockIndex readerBlocks[] = {
|
||||
{0, 841, 1930, 0},
|
||||
{841, 910, 1946, 22},
|
||||
{1751, 884, 1881, 41},
|
||||
@@ -2226,10 +2226,10 @@ static const BlockIndex johnBlocks[] = {
|
||||
{41668, 923, 2027, 854},
|
||||
{42591, 712, 1669, 869},
|
||||
};
|
||||
static const Corpus johnCorpus = {
|
||||
static const Corpus readerCorpus = {
|
||||
"WEB",
|
||||
"World English Bible - public domain (eBible.org).",
|
||||
johnData, sizeof(johnData), johnBlocks,
|
||||
sizeof(johnBlocks) / sizeof(johnBlocks[0]), kVerseCount
|
||||
readerData, sizeof(readerData), readerBlocks,
|
||||
sizeof(readerBlocks) / sizeof(readerBlocks[0]), kVerseCount
|
||||
};
|
||||
}}}
|
||||
@@ -71,7 +71,7 @@ inline ParseResult parse(const char* command, Reference& ref) {
|
||||
skipSpace(command);
|
||||
if (!token(command, "get")) return ParseResult::NoMatch;
|
||||
skipSpace(command);
|
||||
if (!token(command, "john")) return ParseResult::NoMatch;
|
||||
if (!token(command, "reader")) return ParseResult::NoMatch;
|
||||
skipSpace(command);
|
||||
if (!number(command, ref.chapter) || *command != ':') return ParseResult::Invalid;
|
||||
++command;
|
||||
@@ -2,8 +2,8 @@
|
||||
* Compile the vendored tinf core as C. Building the same source as C++ costs
|
||||
* several extra kilobytes with the embedded toolchains used by MeshCore.
|
||||
*/
|
||||
#include "../CompanionJohnConfig.h"
|
||||
#if defined(ENABLE_OTA) || defined(OTA_TRANSPORT_DEFLATE_TEST) || COMPANION_FEATURE_JOHN
|
||||
#include "../CompanionReaderConfig.h"
|
||||
#if defined(ENABLE_OTA) || defined(OTA_TRANSPORT_DEFLATE_TEST) || COMPANION_FEATURE_READER
|
||||
#if defined(__GNUC__) && !defined(__clang__)
|
||||
/* nRF52's Arduino recipe defaults to -Ofast, which more than doubles this
|
||||
* decoder. Keep the size policy local to tinf instead of changing MeshCore. */
|
||||
|
||||
@@ -13,7 +13,7 @@ MeshCore changes are marked in tinf.h and tinflate.c. They add
|
||||
tinf_uncompress_exact(), which accepts legal padding bits in the byte containing
|
||||
the final end-of-block code but rejects any trailing whole input byte. The C
|
||||
implementation is included by ../OtaTinf.c for ENABLE_OTA application builds,
|
||||
COMPANION_FEATURE_JOHN offline lookup, and the native OTA test profile.
|
||||
COMPANION_FEATURE_READER offline lookup, and the native OTA test profile.
|
||||
../OtaDeflate.cpp is the small C++
|
||||
callback adapter. Keeping the decoder in a true C translation unit avoids the
|
||||
several-kilobyte penalty produced by the embedded C++ toolchains. Firmware
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
* MeshCore modifications (2026):
|
||||
* - expose tinf_uncompress_exact() to reject trailing whole input bytes;
|
||||
* - compile this vendored implementation through OtaTinf.c only, keeping
|
||||
* it out of builds which need neither OTA nor offline John lookup.
|
||||
* it out of builds which need neither OTA nor offline reader lookup.
|
||||
* See README.meshcore.txt for the upstream revision and integration details.
|
||||
*/
|
||||
|
||||
|
||||
File renamed without changes.
Vendored
+14
-14
@@ -1,6 +1,6 @@
|
||||
#include <Arduino.h>
|
||||
#include <helpers/CompanionJohn.h>
|
||||
#include <helpers/bible/JohnData.generated.h>
|
||||
#include <helpers/CompanionReader.h>
|
||||
#include <helpers/bible/ReaderData.generated.h>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
@@ -10,23 +10,23 @@ using namespace mesh::bible;
|
||||
|
||||
int main(int argc, char**) {
|
||||
Reference ref = {};
|
||||
for (const char* cmd : {"get John 3:16", " GET\tJoHn 3:16 \t", "get john 03:16"}) {
|
||||
for (const char* cmd : {"get reader 3:16", " GET\treader 3:16 \t", "get reader 03:16"}) {
|
||||
assert(parse(cmd, ref) == ParseResult::Valid && ref.chapter == 3 && ref.verse == 16);
|
||||
}
|
||||
for (const char* cmd : {"get John", "get John 0:1", "get John 22:1",
|
||||
"get John 1:52", "get John 3:0", "get John -3:16", "get John 3:16-18",
|
||||
"get John 3:16 junk", "get John 9999999999:1", "get John 1:9999999999",
|
||||
"get John 3:", "get John 3:16\x1b", "get John 3 :16", "get John :16"}) {
|
||||
for (const char* cmd : {"get reader", "get reader 0:1", "get reader 22:1",
|
||||
"get reader 1:52", "get reader 3:0", "get reader -3:16", "get reader 3:16-18",
|
||||
"get reader 3:16 junk", "get reader 9999999999:1", "get reader 1:9999999999",
|
||||
"get reader 3:", "get reader 3:16\x1b", "get reader 3 :16", "get reader :16"}) {
|
||||
assert(parse(cmd, ref) == ParseResult::Invalid);
|
||||
}
|
||||
for (const char* cmd : {"get name", "forget John 3:16", "get johnny 3:16",
|
||||
"get 1 John 3:16", "getJohn 3:16", "", "get J", "get", "g"}) {
|
||||
for (const char* cmd : {"get name", "forget reader 3:16", "get readerny 3:16",
|
||||
"get 1 reader 3:16", "getreader 3:16", "", "get J", "get", "g"}) {
|
||||
assert(parse(cmd, ref) == ParseResult::NoMatch);
|
||||
Stream output;
|
||||
assert(!mesh::handleJohnCommand(cmd, output) && output.text.empty());
|
||||
assert(!mesh::handleReaderCommand(cmd, output) && output.text.empty());
|
||||
}
|
||||
assert(parse(nullptr, ref) == ParseResult::NoMatch);
|
||||
const Corpus& corpus = generated::johnCorpus;
|
||||
const Corpus& corpus = generated::readerCorpus;
|
||||
struct Guarded { uint32_t pre; char scratch[kBlockSize]; uint32_t post; } buf = {};
|
||||
buf.pre = 0x11223344; buf.post = 0xaabbccdd;
|
||||
unsigned count = 0;
|
||||
@@ -36,9 +36,9 @@ int main(int argc, char**) {
|
||||
assert(lookup(corpus, {chapter, verse}, buf.scratch, kBlockSize, text) == LookupResult::Found);
|
||||
assert(buf.pre == 0x11223344 && buf.post == 0xaabbccdd);
|
||||
char cmd[32];
|
||||
snprintf(cmd, sizeof(cmd), "get John %u:%u", chapter, verse);
|
||||
snprintf(cmd, sizeof(cmd), "get reader %u:%u", chapter, verse);
|
||||
Stream output;
|
||||
assert(mesh::handleJohnCommand(cmd, output));
|
||||
assert(mesh::handleReaderCommand(cmd, output));
|
||||
const std::string expected = "\r\n" + std::string(text) + "\r\n";
|
||||
assert(output.text.find(expected) != std::string::npos);
|
||||
for (const unsigned char* c = reinterpret_cast<const unsigned char*>(text); *c; ++c)
|
||||
@@ -88,7 +88,7 @@ int main(int argc, char**) {
|
||||
assert(text == nullptr && buf.pre == 0x11223344 && buf.post == 0xaabbccdd);
|
||||
}
|
||||
Stream invalid;
|
||||
assert(mesh::handleJohnCommand("get John 3:99", invalid));
|
||||
assert(mesh::handleReaderCommand("get reader 3:99", invalid));
|
||||
assert(invalid.text.find("ERROR") != std::string::npos);
|
||||
if (argc == 1) std::puts("All 879 verses, terminal output, parser and corruption checks passed");
|
||||
}
|
||||
test/fixtures/companion_john/test_reader.cpp → test/fixtures/companion_reader/test_reader_screen.cpp
Vendored
+50
-50
@@ -1,5 +1,5 @@
|
||||
#include <helpers/CompanionJohn.h>
|
||||
#include <helpers/bible/JohnBookmarkFiles.h>
|
||||
#include <helpers/CompanionReader.h>
|
||||
#include <helpers/bible/ReaderBookmarkFiles.h>
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
@@ -41,18 +41,18 @@ struct Files {
|
||||
struct FakeMesh {
|
||||
Files files;
|
||||
int saves = 0;
|
||||
bool loadJohnBookmark(Position& p) { return loadReaderBookmark(files, p); }
|
||||
bool saveJohnBookmark(Position p) { ++saves; return saveReaderBookmark(files, p); }
|
||||
bool loadReaderBookmark(Position& p) { return mesh::bible::loadReaderBookmark(files, p); }
|
||||
bool saveReaderBookmark(Position p) { ++saves; return mesh::bible::saveReaderBookmark(files, p); }
|
||||
} the_mesh;
|
||||
|
||||
class UITask {
|
||||
public:
|
||||
bool closed = false;
|
||||
std::string alert;
|
||||
void closeJohnReader() { closed = true; }
|
||||
void closeReader() { closed = true; }
|
||||
void showAlert(const char* text, int) { alert = text; }
|
||||
};
|
||||
#include "../../../examples/companion_radio/ui-new/JohnReaderScreen.h"
|
||||
#include "../../../examples/companion_radio/ui-new/ReaderScreen.h"
|
||||
|
||||
class Display : public DisplayDriver {
|
||||
int x = 0, y = 0;
|
||||
@@ -141,25 +141,25 @@ int main(int argc, char** argv) {
|
||||
if (argc > 2) assert(std::freopen(argv[2], "w", stdout));
|
||||
Display display(128, 64);
|
||||
UITask task;
|
||||
JohnReaderScreen first(&task, &display);
|
||||
ReaderScreen first(&task, &display);
|
||||
first.open(); first.render(display);
|
||||
std::puts("FRAME 128 64 John 1:1");
|
||||
std::puts("FRAME 128 64 reader 1:1");
|
||||
display.dump();
|
||||
assert(the_mesh.saveJohnBookmark(pos(91))); // John 3:16
|
||||
JohnReaderScreen verse(&task, &display);
|
||||
assert(the_mesh.saveReaderBookmark(pos(91))); // reader 3:16
|
||||
ReaderScreen verse(&task, &display);
|
||||
verse.open();
|
||||
for (int part = 0; part < (UI_SMALL_MESSAGE_FONT ? 1 : 2); ++part) {
|
||||
display.clear(); verse.render(display);
|
||||
std::printf("FRAME 128 64 John 3:16 part %d\n", part + 1);
|
||||
std::printf("FRAME 128 64 reader 3:16 part %d\n", part + 1);
|
||||
display.dump();
|
||||
verse.handleInput(KEY_NEXT);
|
||||
}
|
||||
#if UI_SMALL_MESSAGE_FONT
|
||||
Display tiny(72, 40);
|
||||
assert(the_mesh.saveJohnBookmark(pos(91)));
|
||||
JohnReaderScreen tiny_verse(&task, &tiny);
|
||||
assert(the_mesh.saveReaderBookmark(pos(91)));
|
||||
ReaderScreen tiny_verse(&task, &tiny);
|
||||
tiny_verse.open(); tiny_verse.render(tiny);
|
||||
std::puts("FRAME 72 40 John 3:16 on a tiny screen (5px)");
|
||||
std::puts("FRAME 72 40 reader 3:16 on a tiny screen (5px)");
|
||||
tiny.dump();
|
||||
tiny.resize(40, 72);
|
||||
tiny_verse.render(tiny);
|
||||
@@ -199,9 +199,9 @@ int main(int argc, char** argv) {
|
||||
auto measure = [&expected_text](const char* line) { return expected_text.getTextWidth(line); };
|
||||
UITask task;
|
||||
the_mesh = FakeMesh{};
|
||||
JohnReaderScreen screen(&task, &display);
|
||||
ReaderScreen screen(&task, &display);
|
||||
screen.open();
|
||||
assert(screen.flush() && the_mesh.saves == 0); // don't create John 1:1
|
||||
assert(screen.flush() && the_mesh.saves == 0); // don't create reader 1:1
|
||||
#if UI_BUTTON_READER_HINT
|
||||
display.clear(); screen.render(display);
|
||||
Display footer(display.width(), display.height());
|
||||
@@ -237,7 +237,7 @@ int main(int argc, char** argv) {
|
||||
char scratch[kBlockSize];
|
||||
for (uint16_t verse = 0; verse < kVerseCount; ++verse) {
|
||||
const char* text;
|
||||
assert(mesh::readJohnVerse(referenceAt(verse), scratch, sizeof(scratch), text) == LookupResult::Found);
|
||||
assert(mesh::readReaderVerse(referenceAt(verse), scratch, sizeof(scratch), text) == LookupResult::Found);
|
||||
memmove(scratch, text, strlen(text) + 1);
|
||||
std::string rebuilt;
|
||||
uint16_t offset = 0;
|
||||
@@ -304,27 +304,27 @@ int main(int argc, char** argv) {
|
||||
touch_gesture(x, screen.readerTouchBar()->top, x, screen.readerTouchBar()->top);
|
||||
};
|
||||
tap(2); assert(screen.flush());
|
||||
Position saved; assert(the_mesh.loadJohnBookmark(saved) && saved.verse == 1);
|
||||
Position saved; assert(the_mesh.loadReaderBookmark(saved) && saved.verse == 1);
|
||||
tap(1); assert(screen.flush());
|
||||
assert(!the_mesh.loadJohnBookmark(saved) && saved.atStart());
|
||||
assert(!the_mesh.loadReaderBookmark(saved) && saved.atStart());
|
||||
tap(3); assert(screen.flush());
|
||||
assert(the_mesh.loadJohnBookmark(saved) && referenceAt(saved.verse).chapter == 2);
|
||||
assert(the_mesh.loadReaderBookmark(saved) && referenceAt(saved.verse).chapter == 2);
|
||||
tap(0); assert(screen.flush());
|
||||
assert(!the_mesh.loadJohnBookmark(saved) && saved.atStart());
|
||||
assert(!the_mesh.loadReaderBookmark(saved) && saved.atStart());
|
||||
tap(4); assert(task.closed);
|
||||
task.closed = false;
|
||||
touch_gesture(120,70,120,70); assert(screen.flush());
|
||||
assert(the_mesh.loadJohnBookmark(saved) && saved.verse == 1);
|
||||
assert(the_mesh.loadReaderBookmark(saved) && saved.verse == 1);
|
||||
touch_gesture(40,70,40,70); assert(screen.flush());
|
||||
assert(!the_mesh.loadJohnBookmark(saved) && saved.atStart());
|
||||
assert(!the_mesh.loadReaderBookmark(saved) && saved.atStart());
|
||||
touch_gesture(120,70,40,70); assert(screen.flush());
|
||||
assert(the_mesh.loadJohnBookmark(saved) && saved.verse == 1);
|
||||
assert(the_mesh.loadReaderBookmark(saved) && saved.verse == 1);
|
||||
touch_gesture(40,70,120,70); assert(screen.flush());
|
||||
assert(!the_mesh.loadJohnBookmark(saved) && saved.atStart());
|
||||
assert(!the_mesh.loadReaderBookmark(saved) && saved.atStart());
|
||||
touch_gesture(80,100,80,50); assert(screen.flush());
|
||||
assert(the_mesh.loadJohnBookmark(saved) && referenceAt(saved.verse).chapter == 2);
|
||||
assert(the_mesh.loadReaderBookmark(saved) && referenceAt(saved.verse).chapter == 2);
|
||||
touch_gesture(80,50,80,100); assert(screen.flush());
|
||||
assert(!the_mesh.loadJohnBookmark(saved) && saved.atStart());
|
||||
assert(!the_mesh.loadReaderBookmark(saved) && saved.atStart());
|
||||
touch_gesture(80,0,80,0); assert(task.closed);
|
||||
#endif
|
||||
}
|
||||
@@ -337,7 +337,7 @@ int main(int argc, char** argv) {
|
||||
the_mesh = FakeMesh{};
|
||||
Display chapters_display(128, 64);
|
||||
UITask chapters_task;
|
||||
JohnReaderScreen chapters(&chapters_task, &chapters_display);
|
||||
ReaderScreen chapters(&chapters_task, &chapters_display);
|
||||
chapters.open();
|
||||
assert(chapters.handleInput(KEY_UP));
|
||||
assert(chapters.flush() && the_mesh.saves == 0);
|
||||
@@ -345,7 +345,7 @@ int main(int argc, char** argv) {
|
||||
assert(chapters.handleInput(KEY_DOWN));
|
||||
assert(chapters.flush());
|
||||
Position saved;
|
||||
assert(the_mesh.loadJohnBookmark(saved));
|
||||
assert(the_mesh.loadReaderBookmark(saved));
|
||||
assert(referenceAt(saved.verse).chapter == chapter && saved.offset == 0);
|
||||
assert(referenceAt(saved.verse).verse == 1);
|
||||
}
|
||||
@@ -353,66 +353,66 @@ int main(int argc, char** argv) {
|
||||
assert(chapters.handleInput(KEY_DOWN));
|
||||
assert(chapters.flush() && the_mesh.saves == writes);
|
||||
// Moving backwards from the middle of a chapter reaches the preceding one.
|
||||
assert(the_mesh.saveJohnBookmark(pos(91, 12))); // 3:16, with a stale page offset
|
||||
JohnReaderScreen middle(&chapters_task, &chapters_display);
|
||||
assert(the_mesh.saveReaderBookmark(pos(91, 12))); // 3:16, with a stale page offset
|
||||
ReaderScreen middle(&chapters_task, &chapters_display);
|
||||
middle.open();
|
||||
assert(middle.handleInput(KEY_UP) && middle.flush());
|
||||
Position chapter_saved;
|
||||
assert(the_mesh.loadJohnBookmark(chapter_saved));
|
||||
assert(the_mesh.loadReaderBookmark(chapter_saved));
|
||||
assert(referenceAt(chapter_saved.verse).chapter == 2);
|
||||
assert(referenceAt(chapter_saved.verse).verse == 1 && chapter_saved.offset == 0);
|
||||
assert(middle.handleInput(KEY_UP) && middle.flush());
|
||||
assert(!the_mesh.loadJohnBookmark(chapter_saved) && chapter_saved.atStart());
|
||||
assert(!the_mesh.loadReaderBookmark(chapter_saved) && chapter_saved.atStart());
|
||||
|
||||
// Resume within a split verse; independent screen and store lifecycle.
|
||||
the_mesh = FakeMesh{};
|
||||
Display display(64,32);
|
||||
UITask task;
|
||||
JohnReaderScreen first(&task, &display);
|
||||
ReaderScreen first(&task, &display);
|
||||
first.open(); first.handleInput(KEY_NEXT);
|
||||
display.clear(); first.render(display); const auto body = display.body();
|
||||
first.poll(); assert(the_mesh.saves == 0);
|
||||
test_now += 2000; first.poll(); assert(the_mesh.saves == 1);
|
||||
first.poll(); assert(the_mesh.saves == 1);
|
||||
Position saved;
|
||||
assert(the_mesh.loadJohnBookmark(saved) && saved.verse == 0 && saved.offset > 0);
|
||||
JohnReaderScreen rebooted(&task, &display);
|
||||
assert(the_mesh.loadReaderBookmark(saved) && saved.verse == 0 && saved.offset > 0);
|
||||
ReaderScreen rebooted(&task, &display);
|
||||
rebooted.open(); display.clear(); rebooted.render(display);
|
||||
assert(display.body() == body);
|
||||
rebooted.handleInput(KEY_PREV); assert(rebooted.flush());
|
||||
assert(!the_mesh.files.exists("/john.pos") && !the_mesh.files.exists("/john.pos.bak"));
|
||||
assert(!the_mesh.loadJohnBookmark(saved) && saved.atStart());
|
||||
assert(!the_mesh.files.exists("/reader.pos") && !the_mesh.files.exists("/reader.pos.bak"));
|
||||
assert(!the_mesh.loadReaderBookmark(saved) && saved.atStart());
|
||||
|
||||
#if UI_SMALL_MESSAGE_FONT
|
||||
// A bookmark from the old 6-pixel-wide font resumes on the new page that
|
||||
// contains those words; it does not advance to another verse or lose them.
|
||||
char scratch[kBlockSize];
|
||||
const char* text;
|
||||
assert(mesh::readJohnVerse({3, 16}, scratch, sizeof(scratch), text) == LookupResult::Found);
|
||||
assert(mesh::readReaderVerse({3, 16}, scratch, sizeof(scratch), text) == LookupResult::Found);
|
||||
const auto legacy_measure = [](const char* line) { return strlen(line) * 6; };
|
||||
const auto legacy = readerPage(text, 0, 128, 5, legacy_measure);
|
||||
assert(text[legacy.next]);
|
||||
the_mesh = FakeMesh{};
|
||||
assert(the_mesh.saveJohnBookmark(pos(91, legacy.next)));
|
||||
assert(the_mesh.saveReaderBookmark(pos(91, legacy.next)));
|
||||
Display migrated_display(128,64);
|
||||
mesh::ui::SmallMessageText compact(migrated_display);
|
||||
const auto compact_measure = [&compact](const char* line) { return compact.getTextWidth(line); };
|
||||
const auto compact_page = readerPage(text, legacy.next, 128,
|
||||
compact.lineCount(12, migrated_display.bodyBottom()), compact_measure);
|
||||
assert(compact_page.start <= legacy.next && compact_page.next > legacy.next);
|
||||
JohnReaderScreen migrated(&task, &migrated_display);
|
||||
ReaderScreen migrated(&task, &migrated_display);
|
||||
migrated.open(); migrated.render(migrated_display); assert(migrated.flush());
|
||||
assert(the_mesh.loadJohnBookmark(saved) && saved == pos(91, compact_page.start));
|
||||
assert(compact_page.parts == 1); // John 3:16 now fits in one V4 screen
|
||||
assert(the_mesh.loadReaderBookmark(saved) && saved == pos(91, compact_page.start));
|
||||
assert(compact_page.parts == 1); // reader 3:16 now fits in one V4 screen
|
||||
|
||||
// Reflow an already-open reader when panel geometry crosses the 5px/6px
|
||||
// threshold, and when a tiny panel rotates into the stacked-header layout.
|
||||
for (const auto& dimensions : {std::make_pair(72,40), {128,64}, {40,72}, {64,128}, {32,128}, {160,80}, {80,160}, {159,80}}) {
|
||||
assert(the_mesh.saveJohnBookmark(pos(91, legacy.next)));
|
||||
assert(the_mesh.saveReaderBookmark(pos(91, legacy.next)));
|
||||
Display changed(72,40);
|
||||
JohnReaderScreen active(&task, &changed);
|
||||
ReaderScreen active(&task, &changed);
|
||||
active.open(); active.render(changed); assert(active.flush());
|
||||
Position before; assert(the_mesh.loadJohnBookmark(before));
|
||||
Position before; assert(the_mesh.loadReaderBookmark(before));
|
||||
changed.resize(dimensions.first, dimensions.second);
|
||||
mesh::ui::SmallMessageText small_font(changed);
|
||||
const bool small = changed.useSmallMessageFont();
|
||||
@@ -424,10 +424,10 @@ int main(int argc, char** argv) {
|
||||
new_measure);
|
||||
assert(new_page.start <= before.offset && new_page.next > before.offset);
|
||||
active.render(changed); assert(active.flush());
|
||||
Position after; assert(the_mesh.loadJohnBookmark(after));
|
||||
Position after; assert(the_mesh.loadReaderBookmark(after));
|
||||
assert(after == pos(91, new_page.start));
|
||||
const auto rendered = changed.body();
|
||||
JohnReaderScreen reopened(&task, &changed);
|
||||
ReaderScreen reopened(&task, &changed);
|
||||
reopened.open(); changed.clear(); reopened.render(changed);
|
||||
assert(changed.body() == rendered);
|
||||
}
|
||||
@@ -459,12 +459,12 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
// A reset between renames recovers the previous record, not the .tmp.
|
||||
Files interrupted = original;
|
||||
assert(interrupted.rename("/john.pos", "/john.pos.bak"));
|
||||
assert(interrupted.rename("/reader.pos", "/reader.pos.bak"));
|
||||
assert(loadReaderBookmark(interrupted, saved) && saved == pos(4, 12));
|
||||
assert(saveReaderBookmark(interrupted, pos(20)));
|
||||
assert(saveReaderBookmark(interrupted, pos(0)) && interrupted.data.empty());
|
||||
Files damaged;
|
||||
damaged.data["/john.pos"] = {1, 2, 3};
|
||||
damaged.data["/reader.pos"] = {1, 2, 3};
|
||||
assert(!loadReaderBookmark(damaged, saved) && saved.atStart());
|
||||
assert(saveReaderBookmark(damaged, pos(10)));
|
||||
assert(loadReaderBookmark(damaged, saved) && saved == pos(10));
|
||||
@@ -159,20 +159,20 @@ int main() {
|
||||
displayPowerPrefs().inbox = DisplayInboxMode::Pending;
|
||||
assert(!messages.hasMessages());
|
||||
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
#if COMPANION_FEATURE_READER
|
||||
// Receiving and downloading messages must leave an open reader alone.
|
||||
Screen reader;
|
||||
task.john_reader = task.curr = &reader;
|
||||
task.reader = task.curr = &reader;
|
||||
for (bool connected : {false, true}) {
|
||||
task.connected = connected;
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
mesh.receive("while reading");
|
||||
assert(task.isJohnReaderActive());
|
||||
assert(task.isReaderActive());
|
||||
}
|
||||
for (int remaining : {1, 0}) {
|
||||
mesh.download();
|
||||
assert(task.getMsgCount() == remaining);
|
||||
assert(task.isJohnReaderActive());
|
||||
assert(task.isReaderActive());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -284,7 +284,7 @@ int main() {
|
||||
with tempfile.TemporaryDirectory(prefix="mesh-inbox-test-") as directory:
|
||||
executable = Path(directory) / "inbox.exe"
|
||||
result = subprocess.run([
|
||||
compiler, "-std=c++17", f"-DCOMPANION_FEATURE_JOHN={reader_enabled}",
|
||||
compiler, "-std=c++17", f"-DCOMPANION_FEATURE_READER={reader_enabled}",
|
||||
"-I" + str(ROOT / "src"), "-I" + str(ROOT / "test/mocks"),
|
||||
"-x", "c++", "-", str(ROOT / "src/helpers/ui/MomentaryButton.cpp"),
|
||||
str(ROOT / "src/helpers/ui/DisplayDriver.cpp"), "-o", str(executable),
|
||||
|
||||
@@ -14,8 +14,8 @@ from test_message_navigation import PREAMBLE
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = ROOT / "test/fixtures/companion_john"
|
||||
spec = importlib.util.spec_from_file_location("pack_john", ROOT / "tools/bible/pack_john.py")
|
||||
FIXTURE = ROOT / "test/fixtures/companion_reader"
|
||||
spec = importlib.util.spec_from_file_location("pack_reader", ROOT / "tools/bible/pack_reader.py")
|
||||
packer = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(packer)
|
||||
|
||||
@@ -48,10 +48,10 @@ class CompanionReaderTest(unittest.TestCase):
|
||||
|
||||
def test_ascii_typography_preserves_words_case_and_pinned_source(self):
|
||||
original = copy.deepcopy(self.document)
|
||||
sample = '\u201cJohn\u2019s\u00a0Word\u201d\u2014\u2018Test\u2019\u2013Yes!'
|
||||
self.assertEqual(packer.ascii_verse(sample, 64), b'"John\'s Word"-\'Test\'-Yes!')
|
||||
self.assertEqual(packer.ascii_verse('Already ASCII: John 3:16.', 64),
|
||||
b'Already ASCII: John 3:16.')
|
||||
sample = '\u201creader\u2019s\u00a0Word\u201d\u2014\u2018Test\u2019\u2013Yes!'
|
||||
self.assertEqual(packer.ascii_verse(sample, 64), b'"reader\'s Word"-\'Test\'-Yes!')
|
||||
self.assertEqual(packer.ascii_verse('Already ASCII: reader 3:16.', 64),
|
||||
b'Already ASCII: reader 3:16.')
|
||||
# Bounds apply after conversion, not to the original multibyte form.
|
||||
self.assertEqual(packer.ascii_verse('\u201cHi\u201d', 4), b'"Hi"')
|
||||
with self.assertRaises(ValueError):
|
||||
@@ -92,8 +92,8 @@ class CompanionReaderTest(unittest.TestCase):
|
||||
str(ROOT / "src/helpers/ota/OtaTinf.c"), "-o", str(obj)])
|
||||
self.run_checked([cxx, "-std=c++17", "-Os", "-Wall", "-Wextra", "-Werror",
|
||||
*flags, "-I" + str(ROOT / "src"), "-I" + str(FIXTURE),
|
||||
str(FIXTURE / "test_john.cpp"),
|
||||
str(ROOT / "src/helpers/CompanionJohn.cpp"), str(obj), "-o", str(binary)])
|
||||
str(FIXTURE / "test_reader_lookup.cpp"),
|
||||
str(ROOT / "src/helpers/CompanionReader.cpp"), str(obj), "-o", str(binary)])
|
||||
result = self.run_checked([str(binary), "--dump"])
|
||||
actual = dict(line.split("\t", 1) for line in result.splitlines())
|
||||
self.assertEqual(actual, self.expected_verses)
|
||||
@@ -105,28 +105,28 @@ class CompanionReaderTest(unittest.TestCase):
|
||||
for flags in ([], ["-DESP32_PLATFORM=1"], ["-DNRF52_PLATFORM=1"],
|
||||
["-DESP32_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1"],
|
||||
["-DESP32_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1",
|
||||
"-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_JOHN=0"]):
|
||||
"-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_READER=0"]):
|
||||
result = self.run_checked([cxx, "-E", "-P", *flags,
|
||||
str(ROOT / "src/helpers/CompanionJohn.cpp")])
|
||||
self.assertNotIn("johnData", result)
|
||||
self.assertNotIn("handleJohnCommand", result)
|
||||
str(ROOT / "src/helpers/CompanionReader.cpp")])
|
||||
self.assertNotIn("readerData", result)
|
||||
self.assertNotIn("handleReaderCommand", result)
|
||||
|
||||
def test_terminal_dispatch_is_separate_from_bounded_api_replies(self):
|
||||
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text(encoding="utf-8")
|
||||
start = source.index("void MyMesh::handleTerminalCommand(")
|
||||
end = source.index("\nvoid MyMesh::enterCLIRescue()", start)
|
||||
terminal = source[start:end]
|
||||
self.assertEqual(source.count("mesh::handleJohnCommand("), 1)
|
||||
self.assertLess(terminal.index("mesh::handleJohnCommand("),
|
||||
self.assertEqual(source.count("mesh::handleReaderCommand("), 1)
|
||||
self.assertLess(terminal.index("mesh::handleReaderCommand("),
|
||||
terminal.index("char local_reply[160]"))
|
||||
self.assertIn("get John <chapter>:<verse> (World English Bible, offline)", terminal)
|
||||
self.assertEqual(terminal.count("#if COMPANION_FEATURE_JOHN"), 2)
|
||||
self.assertIn("get reader <chapter>:<verse> (World English Bible, offline)", terminal)
|
||||
self.assertEqual(terminal.count("#if COMPANION_FEATURE_READER"), 2)
|
||||
|
||||
def test_reader_navigation_and_bookmark_recovery(self):
|
||||
cc, cxx = shutil.which("gcc"), shutil.which("g++")
|
||||
if not cc or not cxx:
|
||||
self.skipTest("host GCC and G++ required")
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-john-reader-") as directory:
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-reader-reader-") as directory:
|
||||
directory = Path(directory)
|
||||
flags = ["-DNRF52_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"]
|
||||
obj, binary = directory / "tinf.o", directory / "reader.exe"
|
||||
@@ -139,21 +139,21 @@ class CompanionReaderTest(unittest.TestCase):
|
||||
f"-DUI_BUTTON_READER_HINT={button_hint}",
|
||||
f"-DUI_READER_TOUCH_BAR={touch_bar}",
|
||||
"-I" + str(ROOT / "src"), "-I" + str(FIXTURE),
|
||||
str(FIXTURE / "test_reader.cpp"), str(ROOT / "src/helpers/CompanionJohn.cpp"),
|
||||
str(FIXTURE / "test_reader_screen.cpp"), str(ROOT / "src/helpers/CompanionReader.cpp"),
|
||||
str(obj), "-o", str(binary)])
|
||||
self.run_checked([str(binary)])
|
||||
|
||||
def test_reader_button_routing_and_profile_gate(self):
|
||||
ui = (ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text(encoding="utf-8")
|
||||
long_press = ui[ui.index("char UITask::handleLongPress("):]
|
||||
self.assertLess(long_press.index("isJohnReaderActive()"), long_press.index("enterCLIRescue()"))
|
||||
self.assertLess(long_press.index("isReaderActive()"), long_press.index("enterCLIRescue()"))
|
||||
self.assertLess(long_press.index("isRadioPage()"), long_press.index("enterCLIRescue()"))
|
||||
self.assertIn("#if COMPANION_FEATURE_JOHN\n#include \"JohnReaderScreen.h\"", ui)
|
||||
self.assertIn("#if COMPANION_FEATURE_READER\n#include \"ReaderScreen.h\"", ui)
|
||||
self.assertLess(ui.index("#define UI_BUTTON_READER_HINT 1"),
|
||||
ui.index('#include "JohnReaderScreen.h"'))
|
||||
ui.index('#include "ReaderScreen.h"'))
|
||||
# Queue downloads preserve the current screen unconditionally now;
|
||||
# test_companion_inbox exercises this with the reader enabled.
|
||||
self.assertIn("else if (isJohnReaderActive())", ui)
|
||||
self.assertIn("else if (isReaderActive())", ui)
|
||||
self.assertIn("c = handleLongPress(KEY_ENTER);", ui)
|
||||
self.assertIn("handleDoubleClick(KEY_PREV)", ui)
|
||||
|
||||
@@ -166,7 +166,7 @@ struct Board {
|
||||
bool isUsbHostConnected() const { return host; }
|
||||
};
|
||||
struct Interfaces { bool takePairingRequest() { return false; } };
|
||||
struct JohnReaderScreen : Screen { int saves = 0; void flush() { ++saves; } };
|
||||
struct ReaderScreen : Screen { int saves = 0; void flush() { ++saves; } };
|
||||
'''
|
||||
members = r'''
|
||||
Board* _board;
|
||||
@@ -189,12 +189,12 @@ int main() {
|
||||
displayPowerPrefs().usb = {DisplayMode::On, 5};
|
||||
Display display;
|
||||
Screen home;
|
||||
JohnReaderScreen reader;
|
||||
ReaderScreen reader;
|
||||
Board board;
|
||||
Interfaces interfaces;
|
||||
UITask task(display);
|
||||
task._board = &board; task._interfaceManager = &interfaces;
|
||||
task.curr = task.home = &home; task.john_reader = &reader;
|
||||
task.curr = task.home = &home; task.reader = &reader;
|
||||
task.servicePairingState();
|
||||
assert(!display.isOn() && reader.saves == 0);
|
||||
task.curr = &reader;
|
||||
@@ -204,7 +204,7 @@ int main() {
|
||||
assert(display.isOn() && reader.saves == 0);
|
||||
g_mock_millis = 5000; task.servicePairingState();
|
||||
assert(!display.isOn());
|
||||
const int expected_saves = COMPANION_FEATURE_JOHN ? 1 : 0;
|
||||
const int expected_saves = COMPANION_FEATURE_READER ? 1 : 0;
|
||||
assert(reader.saves == expected_saves);
|
||||
task.servicePairingState();
|
||||
assert(reader.saves == expected_saves); // No repeated writes while dark.
|
||||
@@ -233,7 +233,7 @@ int main() {
|
||||
for enabled in (0, 1):
|
||||
with self.subTest(reader_enabled=enabled):
|
||||
binary = directory / "power.exe"
|
||||
self.run_checked([compiler, "-std=c++17", f"-DCOMPANION_FEATURE_JOHN={enabled}",
|
||||
self.run_checked([compiler, "-std=c++17", f"-DCOMPANION_FEATURE_READER={enabled}",
|
||||
"-I" + str(ROOT / "src"), "-I" + str(ROOT / "test/mocks"),
|
||||
str(directory / "power.cpp"),
|
||||
str(ROOT / "src/helpers/ui/MomentaryButton.cpp"),
|
||||
|
||||
@@ -22,14 +22,14 @@ class IndicatorMessagesProfileTest(unittest.TestCase):
|
||||
# Compile the real gate: HAS_TOUCH must not overwrite the explicit
|
||||
# physical-button footer setting with zero.
|
||||
start = ui.index("#ifndef UI_BUTTON_READER_HINT")
|
||||
gate = ui[start:ui.index("#if COMPANION_FEATURE_JOHN", start)]
|
||||
gate = ui[start:ui.index("#if COMPANION_FEATURE_READER", start)]
|
||||
result = subprocess.run(["c++", "-E", "-P", "-x", "c++", "-"],
|
||||
input="#define HAS_TOUCH 1\n#define UI_BUTTON_READER_HINT 1\n" + gate +
|
||||
"\n#if UI_BUTTON_READER_HINT != 1\n#error reader hint disabled\n#endif\n",
|
||||
text=True, capture_output=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("static_cast<MsgPreviewScreen*>(msg_preview)->readerTouchBar()", ui)
|
||||
self.assertIn("static_cast<JohnReaderScreen*>(john_reader)->readerTouchBar()", ui)
|
||||
self.assertIn("static_cast<ReaderScreen*>(reader)->readerTouchBar()", ui)
|
||||
|
||||
def test_indicator_enables_messages_home_page(self):
|
||||
profile = PROFILE.read_text()
|
||||
|
||||
@@ -117,15 +117,15 @@ public:
|
||||
UIScreen* curr=nullptr;
|
||||
UIScreen* msg_preview=nullptr;
|
||||
UIScreen* home=nullptr;
|
||||
UIScreen* john_reader=nullptr;
|
||||
UIScreen* reader=nullptr;
|
||||
uint32_t _auto_off=0,_next_refresh=0;
|
||||
int buzzer_changes=0;
|
||||
int getMsgCount() const { return 0; }
|
||||
explicit UITask(DisplayDriver& display) : _display(&display) {}
|
||||
bool isJohnReaderActive() const { return john_reader && curr==john_reader; }
|
||||
bool isReaderActive() const { return reader && curr==reader; }
|
||||
void gotoHomeScreen() { curr=home; }
|
||||
void toggleBuzzer() { ++buzzer_changes; }
|
||||
void showJohnReader() { curr=john_reader; }
|
||||
void showReader() { curr=reader; }
|
||||
void showAlert(const char*,int) {}
|
||||
char handleLongPress(char c) { return c; }
|
||||
char handleMultiClick(char,bool);
|
||||
@@ -159,7 +159,7 @@ int main() {
|
||||
HomeScreen home(&task);
|
||||
Screen group;
|
||||
MsgPreviewScreen messages(&task);
|
||||
task.home=&home; task.msg_preview=task.curr=&messages; task.john_reader=&group;
|
||||
task.home=&home; task.msg_preview=task.curr=&messages; task.reader=&group;
|
||||
messages.addPreview(1,"Alice","public old",0,"Public");
|
||||
messages.addPreview(1,"Bob","public new",0,"Public");
|
||||
messages.addPreview(1,"Carol","second",2,"Second");
|
||||
@@ -289,7 +289,7 @@ int main() {
|
||||
g_mock_millis+=25; task.pollButton();
|
||||
g_mock_millis+=MOMENTARY_BUTTON_MULTI_CLICK_MS; task.pollButton();
|
||||
assert(home.key==0);
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
#if COMPANION_FEATURE_READER
|
||||
task.curr=&group;
|
||||
gesture(task,1); assert(group.key==KEY_NEXT && group.events==1);
|
||||
gesture(task,2); assert(group.key==KEY_PREV && group.events==2);
|
||||
@@ -391,7 +391,7 @@ class MessageNavigationTest(unittest.TestCase):
|
||||
binary = Path(temp) / "navigation"
|
||||
result = subprocess.run([
|
||||
"c++", "-std=c++17", signedness,
|
||||
f"-DCOMPANION_FEATURE_JOHN={enabled}",
|
||||
f"-DCOMPANION_FEATURE_READER={enabled}",
|
||||
*flags,
|
||||
"-I" + str(ROOT / "src"), "-I" + str(ROOT / "test/mocks"),
|
||||
"-fsanitize=address,undefined", "-fno-pie", "-no-pie",
|
||||
|
||||
@@ -223,7 +223,7 @@ class ReaderTouchCoordinatesTest(unittest.TestCase):
|
||||
source = (ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text()
|
||||
self.assertIn("layout.header_divider_y + 1", source)
|
||||
self.assertIn("if (!_display->isOn()) reader_touch_bar = nullptr;", source)
|
||||
reader = (ROOT / "examples/companion_radio/ui-new/JohnReaderScreen.h").read_text()
|
||||
reader = (ROOT / "examples/companion_radio/ui-new/ReaderScreen.h").read_text()
|
||||
self.assertIn("makeReaderTouchBar(hint_text, bottom, top - 1)", reader)
|
||||
|
||||
|
||||
|
||||
+25
-26
@@ -1,10 +1,10 @@
|
||||
# Offline John lookup
|
||||
# Offline Reader lookup
|
||||
|
||||
ESP32 and nRF52 **Full Companion** USB terminals (and the ESP32 TCP terminal)
|
||||
include the 879 verses of John from the World English Bible:
|
||||
include an embedded 879-verse corpus from the World English Bible:
|
||||
|
||||
```text
|
||||
get John 3:16
|
||||
get reader 3:16
|
||||
```
|
||||
|
||||
Book/verb case and surrounding spaces are ignored. One chapter:verse reference
|
||||
@@ -13,14 +13,14 @@ verse text is printed in full, including verses longer than the ordinary
|
||||
160-byte CLI response. This is a local terminal command, not a LoRa request or
|
||||
an extension of the framed Companion API. No WiFi, phone, API key, filesystem
|
||||
space or bootloader update is needed. Reduced/ordinary Companion and other
|
||||
roles omit the corpus. `COMPANION_FEATURE_JOHN=0` excludes it from a Full build.
|
||||
roles omit the corpus. `COMPANION_FEATURE_READER=0` excludes it from a Full build.
|
||||
|
||||
## On-device reader
|
||||
## On-device Reader
|
||||
|
||||
Full Companions using the `ui-new` display (including the V4) also offer a
|
||||
reader from the radio-settings page:
|
||||
Reader from the radio-settings page:
|
||||
|
||||
- Long press opens John 1:1 initially, or resumes the saved position.
|
||||
- Long press opens at the first reference, or resumes the saved position.
|
||||
- Single click advances one screenful, then to the next verse.
|
||||
- Double click goes back one screenful; at a verse boundary it shows the
|
||||
previous verse's last screenful.
|
||||
@@ -50,8 +50,7 @@ Squeezed Regular 6 on panels at least 128x64 (or 64x128 when rotated), and
|
||||
5px Picopixel on smaller panels. The reference and part counter retain their
|
||||
normal header font, stacking on narrow rotated screens to avoid overlap. On
|
||||
the V4's 128x64 display, five complete 8-pixel-spaced body rows fit above the
|
||||
navigation hint, and John
|
||||
3:16 fits on one screen. Wrapping and pagination use the selected font's
|
||||
navigation hint, and reference 3:16 fits on one screen. Wrapping and pagination use the selected font's
|
||||
actual character widths and line height. Any panel with either dimension at
|
||||
least 160px uses its normal font, including rotated 80x160 panels. Larger display classes, and builds with
|
||||
`UI_SMALL_MESSAGE_FONT=0`, retain the regular body font. Existing bookmarks
|
||||
@@ -70,18 +69,18 @@ It is checkpointed after two seconds without navigation and flushed on exit,
|
||||
screen sleep, or UI shutdown. Writes occur only for changed positions; failures
|
||||
remain pending for retry. Abrupt power loss during the two-second debounce may
|
||||
lose that last movement. Returning to the very first screen clears the bookmark
|
||||
instead of saving a John 1:1 record. Opening and closing there creates no file.
|
||||
instead of saving a first-reference record. Opening and closing there creates no file.
|
||||
|
||||
The bookmark uses `/john.pos` on the primary filesystem, with a verified `.tmp`
|
||||
The bookmark uses `/reader.pos` on the primary filesystem, with a verified `.tmp`
|
||||
replacement and `.bak` fallback for interrupted saves. It does not change the
|
||||
radio-preference layout, contacts, channels, or keys. A malformed bookmark
|
||||
falls back to the previous valid record or John 1:1; no filesystem is formatted.
|
||||
falls back to the previous valid record or the first reference; no filesystem is formatted.
|
||||
The reader allocates only a small cursor object on first use, reusing the
|
||||
same flash corpus and on-demand 2 KiB decode scratch as the terminal.
|
||||
|
||||
## Source and reproduction
|
||||
|
||||
`john-web.json` contains only John, copied from eBible.org's verse-per-line
|
||||
`reader-web.json` contains the embedded corpus, copied from eBible.org's verse-per-line
|
||||
**engwebp** source (World English Bible, American English Protestant edition,
|
||||
a subset of the World English Bible Updated). The JSON records the archive URL,
|
||||
source date and SHA-256 hashes. The verse-per-line export omits footnotes and
|
||||
@@ -102,14 +101,14 @@ moving upstream text. See the [official download inventory](https://ebible.org/f
|
||||
Regenerate or verify the firmware header:
|
||||
|
||||
```sh
|
||||
python tools/bible/pack_john.py
|
||||
python tools/bible/pack_john.py --check
|
||||
python tools/bible/pack_john.py --compare
|
||||
python test/test_companion_john.py
|
||||
python tools/bible/pack_reader.py
|
||||
python tools/bible/pack_reader.py --check
|
||||
python tools/bible/pack_reader.py --compare
|
||||
python test/test_companion_reader.py
|
||||
```
|
||||
|
||||
To deliberately update the source, download `engwebp_vpl.zip` from the recorded
|
||||
URL, then run `python tools/bible/import_web_john.py /path/to/engwebp_vpl.zip`.
|
||||
URL, then run `python tools/bible/import_web_reader.py /path/to/engwebp_vpl.zip`.
|
||||
Review the plaintext changes and regenerate the header. This importer does
|
||||
not download content automatically or add other books to the firmware.
|
||||
|
||||
@@ -155,7 +154,7 @@ on RAK3401 and 4.7 KiB on V4, including the main-loop/UI frames but before
|
||||
framework/task and interrupt overhead. The ESP32-S3 V4 terminal's corresponding
|
||||
decode path is approximately 4.6 KiB.
|
||||
This is a compiler-derived estimate, not a hardware stack high-water reading.
|
||||
Ordinary non-John commands never enter the decode-buffer function. Its object
|
||||
Ordinary non-reader commands never enter the decode-buffer function. Its object
|
||||
file has zero `.data`/`.bss`; the screen reader adds only its small UI state.
|
||||
|
||||
Tests compare all 879 decoded verses and terminal responses with an independently
|
||||
@@ -185,7 +184,7 @@ The small-font C++11 reader fixture also passes AddressSanitizer and
|
||||
UndefinedBehaviorSanitizer, the 22 existing nRF52 ExtraFS contract tests pass,
|
||||
and the separate shared-font check matches all 95 printable ASCII glyphs
|
||||
against Adafruit Picopixel. All 13 memory-policy tests pass in WSL. The following
|
||||
USA Cascade Full builds include the local ASCII John reader changes on top of
|
||||
USA Cascade Full builds include the local ASCII Reader changes on top of
|
||||
`03e42830`; both pass firmware-size, runtime-RAM and capability checks:
|
||||
|
||||
| Build | Flash bytes | Static RAM bytes | Runtime RAM available / required |
|
||||
@@ -205,7 +204,7 @@ readings; those earlier builds were not flashed to hardware.
|
||||
### Adaptive 6px/5px update, 2026-09-08
|
||||
|
||||
The reader now shares `SmallMessageText` with messages on base `8f5614c9`.
|
||||
All ten John host tests pass on both Windows and Linux. The actual reader
|
||||
All ten reader host tests pass on both Windows and Linux. The actual reader
|
||||
also passes AddressSanitizer and UndefinedBehaviorSanitizer across all ten
|
||||
screen geometries above, including resizing and bookmark migration. The
|
||||
separate shared-font test checks all 95 glyphs of both fonts against their
|
||||
@@ -214,7 +213,7 @@ upstream fixtures; the four T096 footer/profile checks also pass.
|
||||
The T096 Full FEM-on USA Cascade build uses 573,384 flash bytes and 135,452
|
||||
static RAM bytes, with 100,060 runtime bytes available against 75,298 required.
|
||||
Its ten capability markers pass. The application-only UF2 was installed on
|
||||
the T096, then the full running version and John 1:1, 3:16 and 21:25 were
|
||||
the T096, then the full running version and references 1:1, 3:16 and 21:25 were
|
||||
verified over USB. The API snapshots confirm unchanged identity, contacts,
|
||||
channels, self settings, auto-add, tuning and custom variables. No bootloader
|
||||
or filesystem was erased. Physical on-screen readability and button behavior
|
||||
@@ -225,7 +224,7 @@ with 263,936 internal runtime bytes available against 175,872 required. All 13
|
||||
capability markers pass. The nonmerged application was installed in both
|
||||
existing V4 app slots; their hashes were verified, and the bootloader, partition
|
||||
table, NVS, OTA selector, SPIFFS and coredump regions compare byte-for-byte with
|
||||
the pre-update full-flash backup. The full running version and John 1:1, 3:16,
|
||||
the pre-update full-flash backup. The full running version and references 1:1, 3:16,
|
||||
21:17 (the longest verse) and 21:25 were verified over USB. Pre/post API snapshots
|
||||
match for identity, contacts, channels, self settings, auto-add, tuning, custom
|
||||
variables, Bluetooth name and default flood scope.
|
||||
@@ -235,7 +234,7 @@ same 135,452 static RAM bytes and runtime budget as the scaled build. The actual
|
||||
driver header/drawing-method fixture passes in native and legacy modes, with
|
||||
both constructor variants; it checks complete pixel coverage and all 95 native
|
||||
6px glyphs. Linux additionally runs that fixture with address/undefined-behavior
|
||||
sanitizers. The John tests include 160x80 navigation and bookmark migration.
|
||||
sanitizers. The reader tests include 160x80 navigation and bookmark migration.
|
||||
The adaptive reader's compiler-reported process frame is 2,504 bytes on T096
|
||||
and 2,512 bytes on V4; these are not total stack high-water measurements.
|
||||
|
||||
@@ -254,7 +253,7 @@ the compact-panel footer policy. Normal-font line heights drive message and
|
||||
radio spacing; Indicator render profiles keep their existing geometry.
|
||||
|
||||
Validation: 35 Python-host tests (including actual-driver recording fixtures,
|
||||
all 879 John verses across 17 geometries, shared fonts, wake and pairing),
|
||||
all 879 corpus verses across 17 geometries, shared fonts, wake and pairing),
|
||||
30 native dashboard tests, and nine message-history/layout tests pass. Linux
|
||||
pixel/transfer/font fixtures use address/undefined-behavior sanitizers. The
|
||||
T096 normal-font routing check was rerun after its final assertion was added.
|
||||
@@ -263,7 +262,7 @@ The T096 Full FEM-on USA Cascade build on `0058f721` plus these local changes
|
||||
uses 573,480 flash bytes and 135,452 static RAM bytes. Runtime capacity is
|
||||
100,060 bytes against 75,298 required; all ten capability checks pass. Its
|
||||
application-only UF2 was installed, and USB reports
|
||||
`v1.17.1.5-native-normal-0058f721`. John 1:1, 3:16 and 21:25 work. API snapshots
|
||||
`v1.17.1.5-native-normal-0058f721`. References 1:1, 3:16 and 21:25 work. API snapshots
|
||||
match for identity/private key, contacts, channels, self settings, auto-add,
|
||||
tuning and custom variables. The bootloader and storage were not flashed.
|
||||
Other panel families were host-tested, not physically flashed or visually
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Import only John from eBible.org's public-domain engwebp VPL download."""
|
||||
"""Import the source for the offline Reader from eBible.org's engwebp VPL download."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
from pack_john import SOURCE, pack, references
|
||||
from pack_reader import SOURCE, pack, references
|
||||
|
||||
SOURCE_URL = "https://ebible.org/Scriptures/engwebp_vpl.zip"
|
||||
MEMBER = "engwebp_vpl.txt"
|
||||
@@ -24,10 +24,10 @@ def import_archive(path):
|
||||
continue
|
||||
match = re.fullmatch(r"JOH ([0-9]+:[0-9]+) (.+)", line)
|
||||
if not match or match[1] in verses:
|
||||
raise ValueError("invalid or duplicate John verse in source")
|
||||
raise ValueError("invalid or duplicate Reader verse in source")
|
||||
verses[match[1]] = match[2]
|
||||
if set(verses) != set(references()):
|
||||
raise ValueError("the source must contain exactly the 879 verses of John")
|
||||
raise ValueError("the source must contain exactly the 879 Reader verses")
|
||||
document = {
|
||||
"translation": "WEB",
|
||||
"edition": "World English Bible, American English, Protestant Edition (engwebp)",
|
||||
@@ -52,7 +52,7 @@ def main():
|
||||
document = import_archive(args.archive)
|
||||
args.output.write_text(json.dumps(document, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8", newline="\n")
|
||||
print(f"Imported {len(document['verses'])} verses of John; source {document['source_date']}")
|
||||
print(f"Imported {len(document['verses'])} Reader verses; source {document['source_date']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pack the pinned public-domain WEB John source into independent DEFLATE blocks."""
|
||||
"""Pack the pinned public-domain source for the offline Reader into DEFLATE blocks."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
@@ -8,8 +8,8 @@ import unicodedata
|
||||
import zlib
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE = Path(__file__).with_name("john-web.json")
|
||||
HEADER = ROOT / "src/helpers/bible/JohnData.generated.h"
|
||||
SOURCE = Path(__file__).with_name("reader-web.json")
|
||||
HEADER = ROOT / "src/helpers/bible/ReaderData.generated.h"
|
||||
BLOCK_SIZE = 2048
|
||||
ASCII_TYPOGRAPHY = str.maketrans({
|
||||
"\u00a0": " ", "\u2013": "-", "\u2014": "-",
|
||||
@@ -42,7 +42,7 @@ def ascii_verse(value, limit):
|
||||
try:
|
||||
encoded = value.translate(ASCII_TYPOGRAPHY).encode("ascii")
|
||||
except UnicodeEncodeError as error:
|
||||
raise ValueError("unsupported non-ASCII character in John source") from error
|
||||
raise ValueError("unsupported non-ASCII character in Reader source") from error
|
||||
if len(encoded) > limit:
|
||||
raise ValueError(f"text exceeds {limit} ASCII bytes")
|
||||
return encoded
|
||||
@@ -55,7 +55,7 @@ def pack(document, block_size=BLOCK_SIZE):
|
||||
checked_text(document["attribution"], 256)
|
||||
verses = document["verses"]
|
||||
if not isinstance(verses, dict) or set(verses) != set(references()):
|
||||
raise ValueError("source must explicitly include all 879 John references")
|
||||
raise ValueError("source must explicitly include all 879 Reader references")
|
||||
blocks, starts, current = [], [0], bytearray()
|
||||
for index, reference in enumerate(references()):
|
||||
verse = verses[reference]
|
||||
@@ -82,24 +82,24 @@ def render(document):
|
||||
packed, blocks = pack(document)
|
||||
digest = hashlib.sha256(json.dumps(document, ensure_ascii=False, sort_keys=True,
|
||||
separators=(",", ":")).encode()).hexdigest()
|
||||
lines = ["// Generated by tools/bible/pack_john.py; do not edit.",
|
||||
"// World English Bible (engwebp), John. Public domain; see tools/bible/README.md.",
|
||||
lines = ["// Generated by tools/bible/pack_reader.py; do not edit.",
|
||||
"// Offline Reader corpus. Public domain; see tools/bible/README.md.",
|
||||
"// Firmware typography: ASCII quotes, apostrophes, dashes and spaces; wording unchanged.",
|
||||
f"// Canonical source SHA-256: {digest}",
|
||||
"#pragma once", '#include "JohnLookup.h"',
|
||||
"#pragma once", '#include "ReaderLookup.h"',
|
||||
"namespace mesh { namespace bible { namespace generated {",
|
||||
f'static_assert(kBlockSize == {BLOCK_SIZE}, "Regenerate John data after changing block size");',
|
||||
'static_assert(sizeof(BlockIndex) == 12, "John flash-size accounting expects a 12-byte index");',
|
||||
"static const uint8_t johnData[] = {"]
|
||||
f'static_assert(kBlockSize == {BLOCK_SIZE}, "Regenerate Reader data after changing block size");',
|
||||
'static_assert(sizeof(BlockIndex) == 12, "Reader flash-size accounting expects a 12-byte index");',
|
||||
"static const uint8_t readerData[] = {"]
|
||||
for start in range(0, len(packed), 20):
|
||||
lines.append(" " + ",".join(f"0x{v:02x}" for v in packed[start:start + 20]) + ",")
|
||||
lines += ["};", "static const BlockIndex johnBlocks[] = {"]
|
||||
lines += ["};", "static const BlockIndex readerBlocks[] = {"]
|
||||
lines += [" {%d, %d, %d, %d}," % block for block in blocks]
|
||||
lines += ["};", "static const Corpus johnCorpus = {",
|
||||
lines += ["};", "static const Corpus readerCorpus = {",
|
||||
f" {json.dumps(document['translation'], ensure_ascii=False)},",
|
||||
f" {json.dumps(document['attribution'], ensure_ascii=False)},",
|
||||
" johnData, sizeof(johnData), johnBlocks,",
|
||||
" sizeof(johnBlocks) / sizeof(johnBlocks[0]), kVerseCount", "};", "}}}", ""]
|
||||
" readerData, sizeof(readerData), readerBlocks,",
|
||||
" sizeof(readerBlocks) / sizeof(readerBlocks[0]), kVerseCount", "};", "}}}", ""]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -124,14 +124,14 @@ def main():
|
||||
header = render(document)
|
||||
if args.check:
|
||||
if args.output.read_text(encoding="utf-8") != header:
|
||||
raise SystemExit("John header differs; regenerate with tools/bible/pack_john.py")
|
||||
raise SystemExit("Reader header differs; regenerate with tools/bible/pack_reader.py")
|
||||
else:
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(header, encoding="utf-8", newline="\n")
|
||||
packed, blocks = pack(document)
|
||||
plain = sum(block[2] for block in blocks)
|
||||
tables = len(blocks) * 12
|
||||
print(f"John: {len(references())} verses, {len(blocks)} blocks; "
|
||||
print(f"Reader: {len(references())} verses, {len(blocks)} blocks; "
|
||||
f"{plain} plain bytes -> {len(packed)} DEFLATE + {tables} index bytes "
|
||||
f"({100 * (1 - (len(packed) + tables) / plain):.1f}% smaller incl. indexes)")
|
||||
|
||||
File renamed without changes.
Reference in new issue
Block a user