Consolidate companion components

This commit is contained in:
mikecarper committed 2026-09-17 13:41:39 -07:00
1 parent aab8f5038a
commit ebee99af81
32 files changed
+286 -287

No files matched your search

+3 -3
View File
@@ -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.
```
+1 -1
View File
@@ -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 -1
View File
@@ -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
+9 -9
View File
@@ -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
+6 -6
View File
@@ -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);
+5 -5
View File
@@ -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
+3 -3
View File
@@ -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
@@ -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;
}
};
+34 -34
View File
@@ -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);
+7 -7
View File
@@ -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 -2
View File
@@ -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. */
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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.
@@ -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");
}
@@ -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));
+5 -5
View File
@@ -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),
+27 -27
View File
@@ -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"),
+2 -2
View File
@@ -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()
+6 -6
View File
@@ -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",
+1 -1
View File
@@ -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
View File
@@ -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.