Files
MeshCore/src/helpers/CompanionReader.cpp
T
mikecarper d2bea5fb10 Reduce nRF52 Companion flash and stream Reader chapters
Use a 960-byte constant-time Ed25519 base table, compact hash/scalar code and unused-library trims while retaining Reader, BLE P-256 and the 8 KiB stack. RAK4631 Full saves 57,496 flash bytes and 3,152 static RAM bytes versus preview3.

Support complete chapters and verse ranges with bounded app continuation replies and cooperative terminal output. Use nonblocking TCP writes under reader backpressure. Add scalar/signature comparisons and a linked ARM crypto/stack CI gate.

Validated clean RAK4631, T1000-E, T096 and V4 Full builds, host regressions, and physical RAK chapter/reconnect and signing comparisons. Physical signing averages 35.9 ms versus 14.4 ms with the original table.
2026-10-13 01:02:30 -07:00

289 lines
10 KiB
C++

#include "CompanionReader.h"
#if COMPANION_FEATURE_READER
#include <Arduino.h>
#include "bible/ReaderLookup.h"
#include "bible/ReaderData.generated.h"
namespace mesh {
bible::LookupResult readReaderVerse(bible::Reference ref, char* scratch,
size_t capacity, const char*& text) {
return bible::lookup(bible::generated::readerCorpus, ref, scratch, capacity, text);
}
namespace {
constexpr size_t kReplyPageTextBytes = 100;
constexpr const char* kUsage = "ERROR: use get reader <chapter>[:<verse>[-<last>]] [page]";
struct Selection {
bible::Reference first;
uint8_t last;
};
bible::ParseResult parseReaderRequest(const char* command, Selection& selection,
uint8_t& page) {
page = 0; // No page argument keeps text terminals' full-verse behavior.
if (!command) return bible::ParseResult::NoMatch;
bible::skipSpace(command);
if (!bible::token(command, "get")) return bible::ParseResult::NoMatch;
bible::skipSpace(command);
if (!bible::token(command, "reader")) return bible::ParseResult::NoMatch;
bible::skipSpace(command);
bible::Reference& ref = selection.first;
if (!bible::number(command, ref.chapter) || ref.chapter > sizeof(bible::kChapterVerses)) {
return bible::ParseResult::Invalid;
}
ref.verse = 1;
selection.last = bible::kChapterVerses[ref.chapter - 1];
if (*command == ':') {
++command;
if (!bible::number(command, ref.verse)) return bible::ParseResult::Invalid;
selection.last = ref.verse;
if (*command == '-') {
++command;
if (!bible::number(command, selection.last)) return bible::ParseResult::Invalid;
}
if (ref.verse > selection.last || selection.last > bible::kChapterVerses[ref.chapter - 1])
return bible::ParseResult::Invalid;
}
if (*command) {
if (!bible::space(*command)) return bible::ParseResult::Invalid;
bible::skipSpace(command);
if (*command && !bible::number(command, page)) return bible::ParseResult::Invalid;
bible::skipSpace(command);
}
return *command == 0 ? bible::ParseResult::Valid : bible::ParseResult::Invalid;
}
size_t nextReplyPage(const char* text, size_t start, size_t length, size_t limit) {
size_t count = length - start;
if (count > limit) {
count = limit;
// Keep the separating space in this page, so following every page
// reconstructs the supplied verse exactly without lost or repeated words.
for (size_t n = count; n > 0; --n) {
if (text[start + n - 1] == ' ') {
count = n;
break;
}
}
}
return start + count;
}
void nextCommand(char* out, size_t capacity, Selection selection, uint8_t page) {
if (selection.first.verse == selection.last) {
snprintf(out, capacity, "\nNext: get reader %u:%u %u", selection.first.chapter,
selection.first.verse, page);
} else {
snprintf(out, capacity, "\nNext: get reader %u:%u-%u %u", selection.first.chapter,
selection.first.verse, selection.last, page);
}
}
void formatVerseReply(Selection selection, uint8_t page, const char* text,
char* reply, size_t capacity) {
const bible::Reference ref = selection.first;
char next_command[48] = {};
if (ref.verse < selection.last) {
Selection remaining = selection;
++remaining.first.verse;
nextCommand(next_command, sizeof(next_command), remaining, 1);
}
char header[40];
snprintf(header, sizeof(header), "Reader %u:%u (WEB)\n",
static_cast<unsigned>(ref.chapter), static_cast<unsigned>(ref.verse));
const size_t length = strlen(text);
const size_t header_len = strlen(header);
if (header_len + length + strlen(next_command) < capacity) {
if (page != 1) {
snprintf(reply, capacity, "ERROR: Reader page must be 1-1");
} else {
memcpy(reply, header, header_len);
memcpy(reply + header_len, text, length);
strcpy(reply + header_len + length, next_command);
}
return;
}
unsigned parts = 0;
size_t start = 0, end = 0;
for (size_t offset = 0; offset < length;) {
// A range needs a longer continuation command. Keep the tagged 157-byte
// app reply budget, without changing single-verse page boundaries.
const size_t next = nextReplyPage(text, offset, length,
ref.verse < selection.last ? 80 : kReplyPageTextBytes);
if (++parts == page) {
start = offset;
end = next;
}
offset = next;
}
if (page > parts) {
snprintf(reply, capacity, "ERROR: Reader page must be 1-%u", parts);
return;
}
snprintf(header, sizeof(header), "Reader %u:%u [%u/%u]\n",
static_cast<unsigned>(ref.chapter), static_cast<unsigned>(ref.verse),
static_cast<unsigned>(page), parts);
if (page < parts) {
nextCommand(next_command, sizeof(next_command), selection, page + 1);
}
const size_t prefix_len = strlen(header);
const size_t text_len = end - start;
const size_t suffix_len = strlen(next_command);
if (prefix_len + text_len + suffix_len >= capacity) {
snprintf(reply, capacity, "ERROR: Reader reply buffer too small");
return;
}
memcpy(reply, header, prefix_len);
memcpy(reply + prefix_len, text + start, text_len);
memcpy(reply + prefix_len + text_len, next_command, suffix_len + 1);
}
// Only a recognized Reader request enters this 2 KiB decode frame. Normal
// framed commands must not reserve it, including with link-time optimization.
__attribute__((noinline)) void replyVerse(Selection selection, uint8_t page,
char* reply, size_t capacity) {
char scratch[bible::kBlockSize];
const char* text = nullptr;
const bible::LookupResult result = readReaderVerse(selection.first, scratch, sizeof(scratch), text);
if (result == bible::LookupResult::Found) {
formatVerseReply(selection, page, text, reply, capacity);
} else if (result == bible::LookupResult::Missing) {
snprintf(reply, capacity, "This verse is not present in the supplied translation.");
} else {
snprintf(reply, capacity, "ERROR: invalid compressed Reader data");
}
}
__attribute__((noinline)) void printReplyPage(Selection selection, uint8_t page,
Stream& output) {
char reply[160];
replyVerse(selection, page, reply, sizeof(reply));
output.print(" ");
output.print(reply);
output.print("\r\n");
}
// Only a cursor is retained between loop calls: no chapter buffer or heap.
// A slow/unread USB endpoint must not overflow its fixed queue or hold the
// mesh loop waiting. A new command/session cancels the unfinished cursor.
struct PrintJob {
Stream* output = nullptr;
ReaderTryWrite try_write = nullptr;
Selection selection = {};
uint16_t offset = 0;
uint8_t phase = 0;
bool prompt = false;
} print_job;
bool writePart(const char* text) {
const size_t remaining = strlen(text) - print_job.offset;
const size_t count = remaining < 96 ? remaining : 96;
const uint8_t* data = reinterpret_cast<const uint8_t*>(text + print_job.offset);
const size_t written = print_job.try_write
? print_job.try_write(*print_job.output, data, count)
: print_job.output->write(data, count);
print_job.offset += written <= count ? written : count;
if (written < remaining) return false;
print_job.offset = 0;
return true;
}
// Keep decode scratch off the ordinary command/loop frame, including LTO.
__attribute__((noinline)) bool writeVersePart() {
char scratch[bible::kBlockSize];
const char* text = nullptr;
const bible::LookupResult result = readReaderVerse(
print_job.selection.first, scratch, sizeof(scratch), text);
if (result == bible::LookupResult::Missing)
text = "This verse is not present in the supplied translation.";
else if (result != bible::LookupResult::Found) {
text = "ERROR: invalid compressed Reader data";
print_job.selection.last = print_job.selection.first.verse;
}
return writePart(text);
}
} // namespace
void cancelReaderCommand(const Stream* output) {
if (!output || output == print_job.output) print_job.output = nullptr;
}
bool readerCommandActive(const Stream* output) {
return print_job.output && (!output || output == print_job.output);
}
void serviceReaderCommand() {
if (!print_job.output) return;
switch (print_job.phase) {
case 0: {
char header[48];
snprintf(header, sizeof(header), " Reader %u:%u (%s)\r\n",
print_job.selection.first.chapter, print_job.selection.first.verse,
bible::generated::readerCorpus.translation);
if (writePart(header)) ++print_job.phase;
break;
}
case 1:
if (writeVersePart()) ++print_job.phase;
break;
case 2:
if (writePart("\r\n")) {
if (print_job.selection.first.verse < print_job.selection.last) {
++print_job.selection.first.verse;
print_job.phase = 0;
} else ++print_job.phase;
}
break;
case 3:
if (writePart(bible::generated::readerCorpus.attribution)) ++print_job.phase;
break;
default:
if (writePart(print_job.prompt ? "\r\n> " : "\r\n")) cancelReaderCommand();
break;
}
}
bool handleReaderCommand(const char* command, Stream& output, bool prompt,
ReaderTryWrite try_write) {
cancelReaderCommand();
Selection selection = {};
uint8_t page;
const bible::ParseResult parsed = parseReaderRequest(command, selection, page);
if (parsed == bible::ParseResult::NoMatch) return false;
if (parsed == bible::ParseResult::Invalid) {
output.print(" "); output.print(kUsage); output.print("\r\n");
} else if (page != 0) {
printReplyPage(selection, page, output);
} else {
print_job.selection = selection;
print_job.offset = print_job.phase = 0;
print_job.prompt = prompt;
print_job.try_write = try_write;
print_job.output = &output;
}
return true;
}
bool handleReaderReply(const char* command, char* reply, size_t capacity) {
if (reply == nullptr || capacity == 0) return false;
Selection selection = {};
uint8_t page;
const bible::ParseResult parsed = parseReaderRequest(command, selection, page);
if (parsed == bible::ParseResult::NoMatch) return false;
if (parsed == bible::ParseResult::Invalid) {
snprintf(reply, capacity, "%s", kUsage);
} else {
replyVerse(selection, page == 0 ? 1 : page, reply, capacity);
}
return true;
}
} // namespace mesh
#endif