mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-17 02:54:19 +00:00
337 lines
14 KiB
C++
337 lines
14 KiB
C++
#include <helpers/CompanionJohn.h>
|
|
#include <helpers/bible/JohnBookmarkFiles.h>
|
|
#include <helpers/ui/UIScreen.h>
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <map>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
|
|
using namespace mesh::bible;
|
|
static uint32_t test_now = 100;
|
|
uint32_t millis() { return test_now; }
|
|
ColorVal UIColor::window_bkg = 0, UIColor::title_bkg = 0, UIColor::title_txt = 1;
|
|
ColorVal UIColor::primary_txt = 1, UIColor::secondary_txt = 1, UIColor::warning_txt = 1;
|
|
ColorVal UIColor::popup_bkg = 0, UIColor::popup_txt = 1, UIColor::corp_blue = 1;
|
|
|
|
struct Files {
|
|
std::map<std::string, std::vector<uint8_t>> data;
|
|
int calls = 0, fail_at = -1;
|
|
bool fail() { return calls++ == fail_at; }
|
|
bool exists(const char* p) { return data.count(p); }
|
|
bool remove(const char* p) {
|
|
if (fail()) return false;
|
|
data.erase(p); return true;
|
|
}
|
|
bool rename(const char* a, const char* b) {
|
|
if (fail() || !exists(a) || exists(b)) return false;
|
|
data[b] = data[a]; data.erase(a); return true;
|
|
}
|
|
bool read(const char* p, uint8_t (&out)[kBookmarkBytes]) {
|
|
if (fail() || !exists(p)) return false;
|
|
if (data[p].size() != sizeof(out)) { memset(out, 0, sizeof(out)); return true; }
|
|
memcpy(out, data[p].data(), sizeof(out)); return true;
|
|
}
|
|
bool write(const char* p, const uint8_t (&in)[kBookmarkBytes]) {
|
|
if (fail()) { data[p] = std::vector<uint8_t>(in, in + 3); return false; }
|
|
data[p] = std::vector<uint8_t>(in, in + sizeof(in)); return true;
|
|
}
|
|
};
|
|
struct FakeMesh {
|
|
Files files;
|
|
int saves = 0;
|
|
bool loadJohnBookmark(Position& p) { return loadReaderBookmark(files, p); }
|
|
bool saveJohnBookmark(Position p) { ++saves; return saveReaderBookmark(files, p); }
|
|
} the_mesh;
|
|
|
|
class UITask {
|
|
public:
|
|
bool closed = false;
|
|
std::string alert;
|
|
void closeJohnReader() { closed = true; }
|
|
void showAlert(const char* text, int) { alert = text; }
|
|
};
|
|
#include "../../../examples/companion_radio/ui-new/JohnReaderScreen.h"
|
|
|
|
class Display : public DisplayDriver {
|
|
int x = 0, y = 0;
|
|
public:
|
|
struct Line { int x, y; std::string text; };
|
|
std::vector<Line> lines;
|
|
std::vector<uint8_t> pixels;
|
|
Display(int w, int h) : DisplayDriver(w, h), pixels(w * h, 0) {}
|
|
int bodyY() const { return width() < 64 ? 22 : 12; }
|
|
void resize(int w, int h) { setDimensions(w, h); pixels.assign(w * h, 0); clear(); }
|
|
bool isOn() override { return true; }
|
|
void turnOn() override {}
|
|
void turnOff() override {}
|
|
void clear() override { lines.clear(); std::fill(pixels.begin(), pixels.end(), 0); }
|
|
void startFrame(ColorVal = 0) override { clear(); }
|
|
void endFrame() override {}
|
|
void setTextSize(int) override {}
|
|
void setColor(ColorVal) override {}
|
|
void setCursor(int a, int b) override { x = a; y = b; }
|
|
uint16_t getTextWidth(const char* str) override { return strlen(str) * 6; }
|
|
void print(const char* str) override {
|
|
assert(x >= 0 && x + getTextWidth(str) <= width());
|
|
assert(y >= 0 && y + 8 <= height());
|
|
lines.push_back({x, y, str});
|
|
}
|
|
void fillRect(int a, int b, int w, int h) override {
|
|
assert(a >= 0 && b >= 0 && a + w <= width() && b + h <= height());
|
|
for (int row = b; row < b + h; ++row)
|
|
for (int col = a; col < a + w; ++col) pixels[row * width() + col] = 1;
|
|
}
|
|
void drawRect(int a, int b, int w, int h) override {
|
|
assert(a >= 0 && b >= 0 && a + w <= width() && b + h <= height());
|
|
fillRect(a, b, w, 1); fillRect(a, b + h - 1, w, 1);
|
|
fillRect(a, b, 1, h); fillRect(a + w - 1, b, 1, h);
|
|
}
|
|
void drawXbm(int, int, const uint8_t*, int, int) override {}
|
|
std::string body() const {
|
|
#if UI_SMALL_MESSAGE_FONT
|
|
if (useSmallMessageFont())
|
|
return std::string(pixels.begin() + bodyY() * width(), pixels.end());
|
|
#endif
|
|
std::string result;
|
|
for (const auto& line : lines) if (line.y >= bodyY()) result += line.text;
|
|
return result;
|
|
}
|
|
void dump() const {
|
|
for (const auto& line : lines)
|
|
std::printf("%d\t%d\t%s\n", line.x, line.y, line.text.c_str());
|
|
for (int row = 0; row < height(); ++row)
|
|
for (int col = 0; col < width(); ++col)
|
|
if (pixels[row * width() + col]) std::printf("PIXEL %d %d\n", col, row);
|
|
}
|
|
};
|
|
|
|
static std::string noSpaces(std::string text) {
|
|
text.erase(std::remove(text.begin(), text.end(), ' '), text.end()); return text;
|
|
}
|
|
static Position pos(uint16_t verse, uint16_t offset = 0) {
|
|
Position p; p.verse = verse; p.offset = offset; return p;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
if (argc > 1) {
|
|
if (argc > 2) assert(std::freopen(argv[2], "w", stdout));
|
|
Display display(128, 64);
|
|
UITask task;
|
|
JohnReaderScreen first(&task, &display);
|
|
first.open(); first.render(display);
|
|
std::puts("FRAME 128 64 John 1:1");
|
|
display.dump();
|
|
assert(the_mesh.saveJohnBookmark(pos(91))); // John 3:16
|
|
JohnReaderScreen 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);
|
|
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);
|
|
tiny_verse.open(); tiny_verse.render(tiny);
|
|
std::puts("FRAME 72 40 John 3:16 on a tiny screen (5px)");
|
|
tiny.dump();
|
|
tiny.resize(40, 72);
|
|
tiny_verse.render(tiny);
|
|
std::puts("FRAME 40 72 Same reader rotated (5px)");
|
|
tiny.dump();
|
|
#endif
|
|
return 0;
|
|
}
|
|
// Every verse/part, in both directions, at common small-screen sizes.
|
|
const int cases[][3] = {
|
|
{128,64,6}, {64,128,6}, {160,80,0}, {80,160,0}, {240,135,0},
|
|
{159,80,6}, {80,159,6}, {320,240,0}, {240,320,0}, {250,122,0}, {200,200,0},
|
|
{72,40,5}, {40,72,5}, {128,32,5}, {32,128,5}, {64,48,5}, {48,64,5},
|
|
};
|
|
for (const auto& dimensions : cases) {
|
|
Display display(dimensions[0], dimensions[1]);
|
|
Display expected(dimensions[0], dimensions[1]);
|
|
const int top = expected.bodyY();
|
|
#if UI_SMALL_MESSAGE_FONT
|
|
mesh::ui::SmallMessageText compact(expected);
|
|
const bool small = dimensions[2] != 0;
|
|
assert(expected.useSmallMessageFont() == small);
|
|
if (small) assert(compact.capitalHeight() == dimensions[2]);
|
|
DisplayDriver& expected_text = small ? static_cast<DisplayDriver&>(compact) : expected;
|
|
const int line_height = small ? compact.lineHeight() : 10;
|
|
const int rows = small ? compact.lineCount(top) : (expected.height() - top) / line_height;
|
|
#else
|
|
DisplayDriver& expected_text = expected;
|
|
const int line_height = 10;
|
|
const int rows = (display.height() - top) / line_height;
|
|
#endif
|
|
auto measure = [&expected_text](const char* line) { return expected_text.getTextWidth(line); };
|
|
UITask task;
|
|
the_mesh = FakeMesh{};
|
|
JohnReaderScreen screen(&task, &display);
|
|
screen.open();
|
|
assert(screen.flush() && the_mesh.saves == 0); // don't create John 1:1
|
|
std::vector<std::string> expected_pages;
|
|
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);
|
|
memmove(scratch, text, strlen(text) + 1);
|
|
std::string rebuilt;
|
|
uint16_t offset = 0;
|
|
do {
|
|
const auto page = readerPage(scratch, offset, display.width(), rows, measure);
|
|
assert(page.start == offset && page.next > offset);
|
|
expected.clear();
|
|
uint16_t next = offset;
|
|
for (int row = 0; row < rows && scratch[next]; ++row) {
|
|
char line[kReaderLineBytes];
|
|
next = readerLine(scratch, next, display.width(), measure, line);
|
|
rebuilt += line;
|
|
expected_text.setCursor(0, top + row * line_height);
|
|
expected_text.print(line);
|
|
}
|
|
assert(next == page.next);
|
|
display.clear(); screen.render(display);
|
|
const std::string body = display.body();
|
|
assert(!body.empty());
|
|
assert(body == expected.body());
|
|
assert(display.getTextWidth("ABC") == 18); // header/driver font unchanged
|
|
expected_pages.push_back(body);
|
|
screen.handleInput(KEY_NEXT);
|
|
offset = page.next;
|
|
} while (scratch[offset]);
|
|
if (noSpaces(rebuilt) != noSpaces(scratch)) {
|
|
std::fprintf(stderr, "%dx%d verse index %u\nExpected: %s\nActual: %s\n",
|
|
display.width(), display.height(), verse, scratch, rebuilt.c_str());
|
|
return 1;
|
|
}
|
|
}
|
|
display.clear(); screen.render(display);
|
|
assert(display.body() == expected_pages.back()); // no wrap at end
|
|
for (size_t page = expected_pages.size(); page > 0; --page) {
|
|
display.clear(); screen.render(display);
|
|
assert(display.body() == expected_pages[page - 1]);
|
|
screen.handleInput(KEY_PREV);
|
|
}
|
|
display.clear(); screen.render(display);
|
|
assert(display.body() == expected_pages.front()); // no wrap before start
|
|
screen.handleInput(KEY_ENTER);
|
|
assert(task.closed);
|
|
}
|
|
|
|
// Resume within a split verse; independent screen and store lifecycle.
|
|
the_mesh = FakeMesh{};
|
|
Display display(64,32);
|
|
UITask task;
|
|
JohnReaderScreen 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);
|
|
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());
|
|
|
|
#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);
|
|
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)));
|
|
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), compact_measure);
|
|
assert(compact_page.start <= legacy.next && compact_page.next > legacy.next);
|
|
JohnReaderScreen 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
|
|
|
|
// 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)));
|
|
Display changed(72,40);
|
|
JohnReaderScreen active(&task, &changed);
|
|
active.open(); active.render(changed); assert(active.flush());
|
|
Position before; assert(the_mesh.loadJohnBookmark(before));
|
|
changed.resize(dimensions.first, dimensions.second);
|
|
mesh::ui::SmallMessageText small_font(changed);
|
|
const bool small = changed.useSmallMessageFont();
|
|
DisplayDriver& new_font = small ? static_cast<DisplayDriver&>(small_font) : changed;
|
|
auto new_measure = [&new_font](const char* line) { return new_font.getTextWidth(line); };
|
|
const auto new_page = readerPage(text, before.offset, changed.width(),
|
|
small ? small_font.lineCount(changed.bodyY()) : (changed.height() - changed.bodyY()) / 10,
|
|
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));
|
|
assert(after == pos(91, new_page.start));
|
|
const auto rendered = changed.body();
|
|
JohnReaderScreen reopened(&task, &changed);
|
|
reopened.open(); changed.clear(); reopened.render(changed);
|
|
assert(changed.body() == rendered);
|
|
}
|
|
#endif
|
|
|
|
// Checksum/version/bounds checks, including validly checksummed bad values.
|
|
uint8_t record[kBookmarkBytes];
|
|
for (Position p : {pos(1), pos(878, 2000)}) {
|
|
encodeBookmark(p, record); Position decoded;
|
|
assert(decodeBookmark(record, decoded) && decoded == p);
|
|
for (unsigned i = 0; i < sizeof(record); ++i) {
|
|
record[i] ^= 1; assert(!decodeBookmark(record, decoded)); record[i] ^= 1;
|
|
}
|
|
}
|
|
for (Position p : {pos(0), pos(879), pos(1, 2048)}) {
|
|
encodeBookmark(p, record); assert(!decodeBookmark(record, saved));
|
|
}
|
|
|
|
// Inject failure at each I/O step; reboot must find either the old or the
|
|
// new verified bookmark, never a partially written record or an empty file.
|
|
Files original; assert(saveReaderBookmark(original, pos(4, 12)));
|
|
for (int failure = 0; failure < 12; ++failure) {
|
|
Files files = original; files.calls = 0; files.fail_at = failure;
|
|
const bool ok = saveReaderBookmark(files, pos(30, 45));
|
|
files.fail_at = -1;
|
|
Position recovered; assert(loadReaderBookmark(files, recovered));
|
|
assert(recovered == pos(4, 12) || recovered == pos(30, 45));
|
|
if (ok) assert(recovered == pos(30, 45));
|
|
}
|
|
// A reset between renames recovers the previous record, not the .tmp.
|
|
Files interrupted = original;
|
|
assert(interrupted.rename("/john.pos", "/john.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};
|
|
assert(!loadReaderBookmark(damaged, saved) && saved.atStart());
|
|
assert(saveReaderBookmark(damaged, pos(10)));
|
|
assert(loadReaderBookmark(damaged, saved) && saved == pos(10));
|
|
|
|
ReaderBookmark bookmark;
|
|
bookmark.move(pos(1), UINT32_MAX - 1000);
|
|
assert(!bookmark.due(998) && bookmark.due(999));
|
|
assert(bookmark.dirty()); bookmark.saved(); assert(!bookmark.dirty());
|
|
bookmark.move(pos(0), 2000); assert(bookmark.dirty());
|
|
std::puts("Reader: all 879 verses paginated forward/back, resume, bounds and save-failure tests passed");
|
|
}
|