Add button navigation hints to the hidden John reader
This commit is contained in:
1 parent
d89d28f7ed
commit
cf4dc13c62
7 files changed
+125
-38
No files matched your search
@@ -1,14 +1,17 @@
|
||||
# Small-screen message fonts
|
||||
|
||||
Small-screen Companion builds automatically select a compact font for received
|
||||
message text and the channel/sender line, including `Ch 0 Public`. This covers
|
||||
SSD1306 and SH1106 OLEDs, ST7735 small TFTs, and the U8g2 T-Echo Card display.
|
||||
The shared renderer selects by logical panel dimensions, including rotation:
|
||||
message text and the channel/sender line, including `Ch 0 Public`. SSD1306 and
|
||||
SH1106 OLEDs and the U8g2 T-Echo Card display use this on panels smaller than
|
||||
160 pixels on both axes. ST7735 TFTs now use their native 160x80 dimensions
|
||||
and normal font. The shared renderer selects by panel dimensions, including
|
||||
rotation:
|
||||
|
||||
| Panel size | Font | Capital height | Line spacing |
|
||||
| --- | --- | ---: | ---: |
|
||||
| At least 128 x 64, or 64 x 128 rotated | Squeezed Regular 6 | 6 pixels | 8 pixels |
|
||||
| At least 128 x 64, or 64 x 128 rotated; both axes below 160 | Squeezed Regular 6 | 6 pixels | 8 pixels |
|
||||
| Smaller panels, including 72 x 40, 128 x 32 and 64 x 48 | Picopixel | 5 pixels | 7 pixels |
|
||||
| Either axis at least 160 | Normal display font | Depends on panel | Depends on panel |
|
||||
|
||||
Both fonts allow one pixel for descenders and one blank pixel row between
|
||||
lines. Character widths vary: most letters advance by 4 pixels, with narrow
|
||||
@@ -41,9 +44,14 @@ 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](../tools/bible/README.md#on-device-reader), opened
|
||||
by a long press on the radio page, uses the same button hint. It reserves the
|
||||
hint before pagination and resumes saved bookmarks at the page containing
|
||||
the same text, even after the available page size changes.
|
||||
|
||||
Larger display classes keep their existing font. Menus, Bluetooth PINs and WiFi
|
||||
setup QR codes keep their normal layout. Small TFTs retain their existing
|
||||
logical to physical scaling. The normal OLED font uses 7-pixel letters,
|
||||
setup QR codes keep their normal layout. TFT drivers use native panel
|
||||
coordinates; the SenseCAP Indicator retains its existing scaling. The normal OLED font uses 7-pixel letters,
|
||||
8-pixel line spacing and 6-pixel character advances.
|
||||
|
||||
The preview buffer holds a complete 160-byte MeshCore message plus its
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
#include <helpers/CompanionJohn.h>
|
||||
#include <helpers/bible/JohnReader.h>
|
||||
#if UI_SMALL_MESSAGE_FONT
|
||||
#include <helpers/ui/ReaderNavigationHint.h>
|
||||
#if UI_SMALL_MESSAGE_FONT || UI_BUTTON_READER_HINT
|
||||
#include <helpers/ui/SmallMessageText.h>
|
||||
#endif
|
||||
|
||||
@@ -27,17 +28,30 @@ class JohnReaderScreen : public UIScreen {
|
||||
const bool stacked_header = width < _display->getTextWidth("88:88")
|
||||
+ _display->getTextWidth("88/88") + 4;
|
||||
const int top = header_height * (stacked_header ? 2 : 1) + 2;
|
||||
#if UI_SMALL_MESSAGE_FONT
|
||||
#if UI_SMALL_MESSAGE_FONT || UI_BUTTON_READER_HINT
|
||||
// Share message font selection and metrics, including rotated/tiny panels.
|
||||
mesh::ui::SmallMessageText compact(*_display);
|
||||
#endif
|
||||
int bottom = _display->height();
|
||||
#if UI_BUTTON_READER_HINT
|
||||
// Keep the hint compact on tiny displays even if the body font was
|
||||
// overridden. Reserve it before pagination so no verse text is hidden.
|
||||
const bool small_hint = _display->useSmallMessageFont();
|
||||
DisplayDriver& hint_text = small_hint
|
||||
? static_cast<DisplayDriver&>(compact) : *_display;
|
||||
const auto hint = mesh::ui::makeButtonReaderHintLayout(hint_text,
|
||||
small_hint ? compact.glyphHeight() : header_height, bottom);
|
||||
bottom = hint.top;
|
||||
#endif
|
||||
#if UI_SMALL_MESSAGE_FONT
|
||||
const bool small = _display->useSmallMessageFont();
|
||||
DisplayDriver& body = small ? static_cast<DisplayDriver&>(compact) : *_display;
|
||||
const int line_height = small ? compact.lineHeight() : header_height;
|
||||
int rows = small ? compact.lineCount(top) : (_display->height() - top) / line_height;
|
||||
int rows = small ? compact.lineCount(top, bottom) : (bottom - top) / line_height;
|
||||
#else
|
||||
DisplayDriver& body = *_display;
|
||||
const int line_height = header_height;
|
||||
int rows = (_display->height() - top) / line_height;
|
||||
int rows = (bottom - top) / line_height;
|
||||
#endif
|
||||
if (rows < 1) rows = 1;
|
||||
auto measure = [&body](const char* line) {
|
||||
@@ -53,9 +67,11 @@ class JohnReaderScreen : public UIScreen {
|
||||
referenceAt(pos.verse), scratch, sizeof(scratch), text);
|
||||
if (result != LookupResult::Found) {
|
||||
if (draw) {
|
||||
_display->setColor(UIColor::warning_txt);
|
||||
_display->setCursor(0, top);
|
||||
_display->print("John unavailable");
|
||||
body.setColor(UIColor::warning_txt);
|
||||
body.drawTextEllipsized(0, top, width, "John unavailable");
|
||||
#if UI_BUTTON_READER_HINT
|
||||
mesh::ui::drawButtonReaderHint(hint_text, hint);
|
||||
#endif
|
||||
} else _task->showAlert("John data error", 1500);
|
||||
return;
|
||||
}
|
||||
@@ -96,6 +112,9 @@ class JohnReaderScreen : public UIScreen {
|
||||
body.setCursor(0, top + row * line_height);
|
||||
body.print(filtered);
|
||||
}
|
||||
#if UI_BUTTON_READER_HINT
|
||||
mesh::ui::drawButtonReaderHint(hint_text, hint);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
#include "../MyMesh.h"
|
||||
#include "../CompanionWiFi.h"
|
||||
#include "target.h"
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
#include "JohnReaderScreen.h"
|
||||
#include <new>
|
||||
#endif
|
||||
#include <time.h>
|
||||
#ifdef WIFI_SSID
|
||||
#include <WiFi.h>
|
||||
@@ -96,6 +92,11 @@ static uint64_t companionMessageElapsedMillis(uint64_t heard_millis) {
|
||||
#define UI_BUTTON_READER_HINT 0
|
||||
#endif
|
||||
|
||||
#if COMPANION_FEATURE_JOHN
|
||||
#include "JohnReaderScreen.h"
|
||||
#include <new>
|
||||
#endif
|
||||
|
||||
#ifdef COMPANION_EXCLUSIVE_WIFI_BLE
|
||||
static void drawCompanionTransportChoice(DisplayDriver& display,
|
||||
int x, int y, int width, int height,
|
||||
|
||||
@@ -18,6 +18,10 @@ inline ButtonReaderHintLayout makeButtonReaderHintLayout(
|
||||
DisplayDriver& text, int line_height, int bottom) {
|
||||
ButtonReaderHintLayout layout = {
|
||||
0, line_height, 1, {"<- 2tap 1tap -> long press:exit", nullptr, nullptr}};
|
||||
if (text.getTextWidth(layout.lines[0]) > text.width()
|
||||
&& text.getTextWidth("<-2tap 1tap-> hold:exit") <= text.width()) {
|
||||
layout.lines[0] = "<-2tap 1tap-> hold:exit";
|
||||
}
|
||||
if (text.getTextWidth(layout.lines[0]) > text.width()) {
|
||||
layout.line_count = 2;
|
||||
layout.lines[0] = "<- 2tap 1tap ->";
|
||||
|
||||
+51
-9
@@ -56,12 +56,24 @@ public:
|
||||
|
||||
class Display : public DisplayDriver {
|
||||
int x = 0, y = 0;
|
||||
ColorVal color = 1;
|
||||
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; }
|
||||
int bodyBottom() {
|
||||
#if UI_BUTTON_READER_HINT
|
||||
mesh::ui::SmallMessageText compact(*this);
|
||||
const bool small = useSmallMessageFont();
|
||||
DisplayDriver& hint_text = small ? static_cast<DisplayDriver&>(compact) : *this;
|
||||
return mesh::ui::makeButtonReaderHintLayout(hint_text,
|
||||
small ? compact.glyphHeight() : 10, height()).top;
|
||||
#else
|
||||
return height();
|
||||
#endif
|
||||
}
|
||||
void resize(int w, int h) { setDimensions(w, h); pixels.assign(w * h, 0); clear(); }
|
||||
bool isOn() override { return true; }
|
||||
void turnOn() override {}
|
||||
@@ -70,7 +82,7 @@ public:
|
||||
void startFrame(ColorVal = 0) override { clear(); }
|
||||
void endFrame() override {}
|
||||
void setTextSize(int) override {}
|
||||
void setColor(ColorVal) override {}
|
||||
void setColor(ColorVal value) override { color = value; }
|
||||
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 {
|
||||
@@ -81,7 +93,7 @@ public:
|
||||
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;
|
||||
for (int col = a; col < a + w; ++col) pixels[row * width() + col] = color != 0;
|
||||
}
|
||||
void drawRect(int a, int b, int w, int h) override {
|
||||
assert(a >= 0 && b >= 0 && a + w <= width() && b + h <= height());
|
||||
@@ -89,13 +101,14 @@ public:
|
||||
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 {
|
||||
std::string body() {
|
||||
#if UI_SMALL_MESSAGE_FONT
|
||||
if (useSmallMessageFont())
|
||||
return std::string(pixels.begin() + bodyY() * width(), pixels.end());
|
||||
return std::string(pixels.begin() + bodyY() * width(), pixels.begin() + bodyBottom() * width());
|
||||
#endif
|
||||
std::string result;
|
||||
for (const auto& line : lines) if (line.y >= bodyY()) result += line.text;
|
||||
for (const auto& line : lines)
|
||||
if (line.y >= bodyY() && line.y < bodyBottom()) result += line.text;
|
||||
return result;
|
||||
}
|
||||
void dump() const {
|
||||
@@ -163,11 +176,12 @@ int main(int argc, char** argv) {
|
||||
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;
|
||||
const int rows = small ? compact.lineCount(top, expected.bodyBottom())
|
||||
: (expected.bodyBottom() - top) / line_height;
|
||||
#else
|
||||
DisplayDriver& expected_text = expected;
|
||||
const int line_height = 10;
|
||||
const int rows = (display.height() - top) / line_height;
|
||||
const int rows = (display.bodyBottom() - top) / line_height;
|
||||
#endif
|
||||
auto measure = [&expected_text](const char* line) { return expected_text.getTextWidth(line); };
|
||||
UITask task;
|
||||
@@ -175,6 +189,32 @@ int main(int argc, char** argv) {
|
||||
JohnReaderScreen screen(&task, &display);
|
||||
screen.open();
|
||||
assert(screen.flush() && the_mesh.saves == 0); // don't create John 1:1
|
||||
#if UI_BUTTON_READER_HINT
|
||||
display.clear(); screen.render(display);
|
||||
Display footer(display.width(), display.height());
|
||||
mesh::ui::SmallMessageText hint_compact(footer);
|
||||
const bool small_hint = footer.useSmallMessageFont();
|
||||
DisplayDriver& hint_text = small_hint
|
||||
? static_cast<DisplayDriver&>(hint_compact) : footer;
|
||||
const auto hint = mesh::ui::makeButtonReaderHintLayout(hint_text,
|
||||
small_hint ? hint_compact.glyphHeight() : 10, footer.height());
|
||||
mesh::ui::drawButtonReaderHint(hint_text, hint);
|
||||
if (small_hint) {
|
||||
// Compare the real reader's hint pixels independently of the body.
|
||||
auto first = footer.pixels.begin() + hint.top * footer.width();
|
||||
assert(std::find(first, footer.pixels.end(), 1) != footer.pixels.end());
|
||||
assert(std::equal(first, footer.pixels.end(),
|
||||
display.pixels.begin() + hint.top * display.width()));
|
||||
} else {
|
||||
for (const auto& hint_line : footer.lines) {
|
||||
assert(std::any_of(display.lines.begin(), display.lines.end(),
|
||||
[&hint_line](const Display::Line& line) {
|
||||
return line.x == hint_line.x && line.y == hint_line.y
|
||||
&& line.text == hint_line.text;
|
||||
}));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
std::vector<std::string> expected_pages;
|
||||
char scratch[kBlockSize];
|
||||
for (uint16_t verse = 0; verse < kVerseCount; ++verse) {
|
||||
@@ -257,7 +297,8 @@ int main(int argc, char** argv) {
|
||||
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);
|
||||
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);
|
||||
migrated.open(); migrated.render(migrated_display); assert(migrated.flush());
|
||||
@@ -278,7 +319,8 @@ int main(int argc, char** argv) {
|
||||
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,
|
||||
small ? small_font.lineCount(changed.bodyY(), changed.bodyBottom())
|
||||
: (changed.bodyBottom() - changed.bodyY()) / 10,
|
||||
new_measure);
|
||||
assert(new_page.start <= before.offset && new_page.next > before.offset);
|
||||
active.render(changed); assert(active.flush());
|
||||
|
||||
@@ -130,12 +130,14 @@ class CompanionJohnTest(unittest.TestCase):
|
||||
self.run_checked([cc, "-Os", *flags, "-c", str(ROOT / "src/helpers/ota/OtaTinf.c"),
|
||||
"-o", str(obj)])
|
||||
for small_font in (0, 1):
|
||||
self.run_checked([cxx, "-std=c++11", "-Os", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
|
||||
*flags, f"-DUI_SMALL_MESSAGE_FONT={small_font}",
|
||||
"-I" + str(ROOT / "src"), "-I" + str(FIXTURE),
|
||||
str(FIXTURE / "test_reader.cpp"), str(ROOT / "src/helpers/CompanionJohn.cpp"),
|
||||
str(obj), "-o", str(binary)])
|
||||
self.run_checked([str(binary)])
|
||||
for button_hint in (0, 1):
|
||||
self.run_checked([cxx, "-std=c++11", "-Os", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
|
||||
*flags, f"-DUI_SMALL_MESSAGE_FONT={small_font}",
|
||||
f"-DUI_BUTTON_READER_HINT={button_hint}",
|
||||
"-I" + str(ROOT / "src"), "-I" + str(FIXTURE),
|
||||
str(FIXTURE / "test_reader.cpp"), str(ROOT / "src/helpers/CompanionJohn.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")
|
||||
@@ -143,6 +145,8 @@ class CompanionJohnTest(unittest.TestCase):
|
||||
self.assertLess(long_press.index("isJohnReaderActive()"), 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.assertLess(ui.index("#define UI_BUTTON_READER_HINT 1"),
|
||||
ui.index('#include "JohnReaderScreen.h"'))
|
||||
self.assertIn("&& !isJohnReaderActive()", ui)
|
||||
self.assertIn("else if (isJohnReaderActive())", ui)
|
||||
self.assertIn("c = handleLongPress(KEY_ENTER);", ui)
|
||||
|
||||
+15
-6
@@ -27,7 +27,12 @@ reader from the radio-settings page:
|
||||
- Long press exits to the radio page. It does not invoke early-boot CLI rescue
|
||||
while opening or closing the reader.
|
||||
|
||||
The header shows the reference and part count; there is no bottom footer.
|
||||
The header shows the reference and part count. Single-button builds show
|
||||
`<- 2tap 1tap -> long press:exit` at the bottom, matching the message reader.
|
||||
These are taps of the user button. Narrower displays shorten or split the
|
||||
hint across lines; touch/joystick builds keep their own controls. The entry
|
||||
gesture stays hidden on the radio page. The footer has reserved space in the
|
||||
pagination calculation, so text continues on another page when necessary.
|
||||
Text wraps at word boundaries without truncation. Both the screen and CLI use
|
||||
the same text with plain ASCII quotes, apostrophes, dashes and spaces; wording
|
||||
and capitalization are unchanged. Incoming messages are retained without
|
||||
@@ -39,7 +44,8 @@ Sub-160px displays use the same adaptive font selector as received messages:
|
||||
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, six complete 8-pixel-spaced body rows fit, and John
|
||||
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
|
||||
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
|
||||
@@ -47,7 +53,7 @@ least 160px uses its normal font, including rotated 80x160 panels. Larger displa
|
||||
resume at the page containing the same text offset after a font/layout change.
|
||||
|
||||
The T096 and other ST7735 boards use native 160x80 coordinates and the normal
|
||||
body font (six 10-pixel-spaced rows below the header). The existing color
|
||||
body font (five 10-pixel-spaced rows between the header and button hint). The existing color
|
||||
framebuffer is reused, with no framebuffer RAM increase. ST7789, ST7789LCD,
|
||||
NV3001B and GxEPD displays likewise expose their actual panel dimensions and
|
||||
render pixels without coordinate scaling; portrait orientations are retained.
|
||||
@@ -137,7 +143,8 @@ reports a 2,088-byte frame for verse output, a 16-byte command dispatcher,
|
||||
and about 1,720 bytes for the nested decoder calls. Including the terminal
|
||||
and main-loop frames, the decode path is approximately **4.3 KiB**, before
|
||||
framework/task and interrupt overhead; the nRF52 loop task has an 8 KiB stack.
|
||||
The small-font reader's decode/draw function has a 2,480-byte frame on RAK3401
|
||||
Before the navigation-hint addition, the small-font reader's decode/draw
|
||||
function had a 2,480-byte frame on RAK3401
|
||||
and a 2,496-byte frame on V4. Its nested decode path is approximately 4.3 KiB
|
||||
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
|
||||
@@ -152,9 +159,11 @@ every block boundary, exercise malformed references and corrupted block
|
||||
bounds, and verify that unrelated build profiles exclude the data.
|
||||
|
||||
Reader tests additionally traverse every part of all 879 verses in both
|
||||
directions with regular and adaptive 5px/6px fonts at 128x64, 64x128, 160x80,
|
||||
directions, with and without the button hint, using regular and adaptive
|
||||
5px/6px fonts at 128x64, 64x128, 160x80,
|
||||
240x135, 72x40, 40x72, 128x32, 32x128, 64x48 and 48x64. They compare rendered
|
||||
body pixels/text, check active-reader resizing across font thresholds, and cover resume
|
||||
body pixels/text separately from the hint, check active-reader resizing across
|
||||
font thresholds, and cover resume
|
||||
within a verse, book/chapter boundaries, no writes at the start, clearing stale
|
||||
bookmarks,
|
||||
corruption, I/O failure at each replacement step, timer rollover and bookmark
|
||||
|
||||
Reference in new issue
Block a user