mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
feat: a QR code of the Solo manual on every screen
ui-new: Status › System, Enter shows it full screen. L2: Settings › About. One precomputed 25 x 25 matrix (ui-core/SiteQr.h) for both, so no QR library; the URL in upper case keeps it at version 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -103,7 +103,7 @@ Right** between them, **Up / Down** scrolls):
|
||||
| **Sky** | The satellites on a sky plot, or each one's signal as bars (**Enter** swaps them); Wio Tracker L1 |
|
||||
| **Power** | Battery level, voltage, source and trend, then the readings of any sensors |
|
||||
| **Mesh** | Packets received and sent with a traffic line, forwarded, nodes heard, contacts; **hold Enter** resets the counters |
|
||||
| **System** | Uptime, firmware, device, memory, the send queue, errors, and a sample of every script the font covers |
|
||||
| **System** | Uptime, firmware, device, memory, the send queue, errors, and a sample of every script the font covers; **Enter** shows a QR code that opens this manual on a phone |
|
||||
|
||||
**GPS pwr** in Settings › System turns the GPS off between fixes to save
|
||||
battery; anything that needs your position (the trail, live share, the
|
||||
@@ -116,6 +116,7 @@ locator) keeps it on.
|
||||
> of the satellites and the fix), **Signal** (a bar per satellite with its
|
||||
> strength, and the constellations) and **Details** (position, precision,
|
||||
> time to first fix and what the receiver reports).
|
||||
> The manual's QR code is in Settings › About.
|
||||
|
||||
## GPIO
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// A QR code for the Solo site, the same on every board (Status > System on
|
||||
// ui-new, Settings > About on the L2). Precomputed, so no QR library: the
|
||||
// URL in upper case fits QR's alphanumeric mode, which keeps it at version 2
|
||||
// (25 x 25 modules, error correction M) -- large modules on a small screen.
|
||||
// Regenerate with the Python qrcode package: QRCode(error_correction=M,
|
||||
// border=0), add_data() the string below; module x of row y is bit 31 - x.
|
||||
namespace siteqr {
|
||||
|
||||
static const char* const URL = "solo.marekzegarek.com";
|
||||
static const int N = 25; // modules per side
|
||||
static const int QUIET = 2; // light modules around it; 2 is plenty on a screen
|
||||
|
||||
static const uint32_t ROWS[N] = { // "HTTPS://SOLO.MAREKZEGAREK.COM"
|
||||
0xFEC2BF80u, 0x824FA080u, 0xBAC6AE80u, 0xBA2B2E80u, 0xBA362E80u,
|
||||
0x82D62080u, 0xFEAABF80u, 0x007D8000u, 0xA3679280u, 0xCDA53C80u,
|
||||
0xFE9AEA80u, 0xB02B9C80u, 0x578F0700u, 0x69892480u, 0xE34E8D00u,
|
||||
0x19B92900u, 0xD7C6F980u, 0x00E48F80u, 0xFEB0AB00u, 0x82658E80u,
|
||||
0xBA3DFA00u, 0xBA083C00u, 0xBACE4B80u, 0x821AA080u, 0xFEC76F80u,
|
||||
};
|
||||
|
||||
static inline bool dark(int x, int y) { return (ROWS[y] >> (31 - x)) & 1; }
|
||||
|
||||
} // namespace siteqr
|
||||
@@ -87,6 +87,7 @@ static bool s_chats_stale = false; // Messages: a refresh waits for a held row
|
||||
#include "../ui-core/SoundControl.h"
|
||||
#include "../ui-core/SoundNotifier.h"
|
||||
#include "../ui-core/MessageText.h"
|
||||
#include "../ui-core/SiteQr.h"
|
||||
#include "Theme.h"
|
||||
#include "Anim.h"
|
||||
#include "LvglPort.h"
|
||||
@@ -4479,12 +4480,48 @@ void UITask::buildKeyboardPage(lv_obj_t* body) {
|
||||
groupNote(body, "Hold a letter for accents and other variants.");
|
||||
}
|
||||
|
||||
// Settings > About: this node, the firmware, credits.
|
||||
// The Solo site's QR code (ui-core/SiteQr.h): a 1-bit image, black on white
|
||||
// with its quiet zone, built once at 5 px a module.
|
||||
static lv_obj_t* siteQr(lv_obj_t* parent) {
|
||||
static const int S = 5, SIDE = siteqr::N + 2 * siteqr::QUIET, PX = SIDE * S, STRIDE = (PX + 7) / 8;
|
||||
static uint8_t data[8 + STRIDE * PX]; // the palette (white, black), then the rows
|
||||
static lv_image_dsc_t dsc;
|
||||
if (!dsc.data) {
|
||||
const lv_color32_t pal[2] = { lv_color32_make(0xFF, 0xFF, 0xFF, 0xFF), lv_color32_make(0, 0, 0, 0xFF) };
|
||||
memcpy(data, pal, sizeof(pal));
|
||||
uint8_t* px = data + 8;
|
||||
for (int y = 0; y < PX; y++)
|
||||
for (int x = 0; x < PX; x++) {
|
||||
int mx = x / S - siteqr::QUIET, my = y / S - siteqr::QUIET;
|
||||
if (mx >= 0 && my >= 0 && mx < siteqr::N && my < siteqr::N && siteqr::dark(mx, my))
|
||||
px[y * STRIDE + x / 8] |= 0x80 >> (x % 8);
|
||||
}
|
||||
dsc.header.magic = LV_IMAGE_HEADER_MAGIC;
|
||||
dsc.header.cf = LV_COLOR_FORMAT_I1;
|
||||
dsc.header.w = PX;
|
||||
dsc.header.h = PX;
|
||||
dsc.header.stride = STRIDE;
|
||||
dsc.data_size = sizeof(data);
|
||||
dsc.data = data;
|
||||
}
|
||||
lv_obj_t* img = lv_image_create(parent);
|
||||
lv_image_set_src(img, &dsc);
|
||||
return img;
|
||||
}
|
||||
|
||||
// Settings > About: this node, the firmware, the manual's QR code, credits.
|
||||
void UITask::buildAboutPage(lv_obj_t* body) {
|
||||
lv_obj_t* about = infoCard(body);
|
||||
infoRow(about, "Node", the_mesh.getNodeName());
|
||||
infoRow(about, "Firmware", FIRMWARE_VERSION);
|
||||
if (!strstr(FIRMWARE_VERSION, FIRMWARE_BUILD_DATE)) infoRow(about, "Built", FIRMWARE_BUILD_DATE);
|
||||
sectionTitle(body, "MANUAL");
|
||||
lv_obj_t* man = infoCard(body);
|
||||
lv_obj_set_flex_align(man, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_ver(man, theme::PAD, 0);
|
||||
lv_obj_set_style_pad_row(man, 6, 0);
|
||||
siteQr(man);
|
||||
label(man, siteqr::URL, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
sectionTitle(body, "CREDITS");
|
||||
lv_obj_t* cr = infoCard(body);
|
||||
infoNote(cr, "Map data", creditText((lvport::mountStorage() && mapview::s_provider->available())
|
||||
@@ -4691,7 +4728,7 @@ void UITask::settingsGroup(int i) {
|
||||
schemaRow(g, SETTING(units_imperial));
|
||||
}
|
||||
listRow(g, LV_SYMBOL_DOWNLOAD " Firmware update", FIRMWARE_VERSION, onOpenOta, NULL);
|
||||
listRow(g, LV_SYMBOL_LIST " About", "Node, firmware, credits", onOpenSchemaPage, (void*)(uintptr_t)PG_ABOUT);
|
||||
listRow(g, LV_SYMBOL_LIST " About", "Node, firmware, manual, credits", onOpenSchemaPage, (void*)(uintptr_t)PG_ABOUT);
|
||||
break;
|
||||
|
||||
case 4:
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "PopupMenu.h"
|
||||
#include "TabBar.h"
|
||||
#include "../MyMesh.h"
|
||||
#include "../ui-core/SiteQr.h"
|
||||
#include "../ui-core/Diagnostics.h"
|
||||
#include "../ui-core/Battery.h"
|
||||
#if ENV_INCLUDE_GPS == 1 && defined(GPS_SKYVIEW)
|
||||
@@ -30,6 +31,7 @@ extern MyMesh the_mesh;
|
||||
// Mesh -- packet counts with a traffic line, nodes heard, contacts.
|
||||
// Hold Enter resets the counters.
|
||||
// System -- uptime, firmware, device, memory, queue, errors, a font card.
|
||||
// Enter shows the QR code of the Solo site; any key closes it.
|
||||
// Replaces Tools › Diagnostics and Tools › Satellites. Drawn with InfoKit.h.
|
||||
class StatusScreen : public UIScreen {
|
||||
public:
|
||||
@@ -44,6 +46,7 @@ private:
|
||||
int _scroll = 0; // blocks scrolled off the top
|
||||
bool _more = false; // the last frame cut blocks off at the bottom
|
||||
bool _signal = false; // Sky: the bars instead of the plot
|
||||
bool _qr = false; // System: the site's QR code, full screen
|
||||
PopupMenu _reset_menu; // Mesh, Hold Enter -> confirm
|
||||
CayenneLPP _lpp;
|
||||
unsigned long _lpp_at = 0;
|
||||
@@ -293,6 +296,7 @@ private:
|
||||
row(d, f, rsv, "Firmware", a);
|
||||
row(d, f, rsv, "Built", FIRMWARE_BUILD_DATE);
|
||||
row(d, f, rsv, "Device", board.getManufacturerName());
|
||||
row(d, f, rsv, "Manual", "Enter: QR code");
|
||||
uint32_t heap_free, heap_total;
|
||||
DeviceDiag::getHeapStats(heap_free, heap_total);
|
||||
if (heap_total) snprintf(a, sizeof(a), "%lu of %lu KB", (unsigned long)(heap_free / 1024), (unsigned long)(heap_total / 1024));
|
||||
@@ -316,6 +320,42 @@ private:
|
||||
if (f.place(d.lineStep())) d.drawTextEllipsized(1, f.at, d.width() - rsv - 3, lines[i]);
|
||||
}
|
||||
|
||||
// The site's QR code as large as the screen allows, dark modules on light
|
||||
// (LIGHT is the ink on e-ink, the lit pixel elsewhere), with the address
|
||||
// beside it or under it where it fits.
|
||||
void renderQr(DisplayDriver& d) {
|
||||
const int W = d.width(), H = d.height(), lh = d.getLineHeight();
|
||||
const bool tall = H > W;
|
||||
const int side = siteqr::N + 2 * siteqr::QUIET;
|
||||
const int s = (tall ? W : H) / side;
|
||||
const int px = side * s;
|
||||
// The text: "Solo manual" and the address if that fits on one line.
|
||||
const int tx = tall ? 0 : px + 2, tw = W - tx;
|
||||
const bool url = d.getTextWidth(siteqr::URL) <= tw;
|
||||
const int lines = url ? 2 : 3;
|
||||
// Portrait: the code and the text under it, centred together.
|
||||
const int x0 = tall ? (W - px) / 2 : 0;
|
||||
const int y0 = tall ? (H - px - lh / 2 - lines * lh) / 2 : (H - px) / 2;
|
||||
const bool eink = d.isEink();
|
||||
if (!eink) { d.setColor(DisplayDriver::LIGHT); d.fillRect(x0, y0, px, px); }
|
||||
d.setColor(eink ? DisplayDriver::LIGHT : DisplayDriver::DARK);
|
||||
const int q = x0 + siteqr::QUIET * s, r = y0 + siteqr::QUIET * s;
|
||||
for (int y = 0; y < siteqr::N; y++)
|
||||
for (int x = 0; x < siteqr::N; x++)
|
||||
if (siteqr::dark(x, y)) d.fillRect(q + x * s, r + y * s, s, s);
|
||||
d.setColor(DisplayDriver::LIGHT);
|
||||
const int mid = tx + tw / 2;
|
||||
int ty = tall ? y0 + px + lh / 2 : (H - lines * lh) / 2;
|
||||
if (url) {
|
||||
d.drawTextCentered(mid, ty, "Solo manual"); ty += lh;
|
||||
d.drawTextCentered(mid, ty, siteqr::URL);
|
||||
} else {
|
||||
d.drawTextCentered(mid, ty, "Scan for"); ty += lh;
|
||||
d.drawTextCentered(mid, ty, "the Solo"); ty += lh;
|
||||
d.drawTextCentered(mid, ty, "manual");
|
||||
}
|
||||
}
|
||||
|
||||
// All of a tab's blocks, drawn (or, through a measuring Flow, only counted).
|
||||
void layout(DisplayDriver& d, info::Flow& f, int rsv) {
|
||||
switch (tab()) {
|
||||
@@ -346,6 +386,7 @@ public:
|
||||
|
||||
// Open on a given tab (Status page: Radio; the GPS shortcuts: GPS / Sky).
|
||||
void showTab(uint8_t t) {
|
||||
_qr = false;
|
||||
for (uint8_t i = 0; i < _count; i++) if (_tabs[i] == t) { _cur = i; _scroll = 0; }
|
||||
}
|
||||
// GPS stays awake while its live tabs are on screen.
|
||||
@@ -380,6 +421,7 @@ public:
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
if (_qr) { renderQr(display); return 1000; }
|
||||
tabbar::draw(display, _labels, _count, _cur);
|
||||
const int top = display.listStart(), bottom = display.height();
|
||||
int next = 1000;
|
||||
@@ -415,6 +457,7 @@ public:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (_qr) { _qr = false; return true; }
|
||||
if (keyIsPrev(c)) { _cur = (_cur + _count - 1) % _count; _scroll = 0; return true; }
|
||||
if (keyIsNext(c)) { _cur = (_cur + 1) % _count; _scroll = 0; return true; }
|
||||
if (c == KEY_UP) { if (_scroll > 0) _scroll--; return true; }
|
||||
@@ -423,6 +466,7 @@ public:
|
||||
if (c == KEY_ENTER && tab() == TAB_GPS) { _task->toggleGPS(); return true; }
|
||||
#endif
|
||||
if (c == KEY_ENTER && tab() == TAB_SKY) { _signal = !_signal; return true; }
|
||||
if (c == KEY_ENTER && tab() == TAB_SYSTEM) { _qr = true; return true; }
|
||||
if (c == KEY_CONTEXT_MENU && tab() == TAB_MESH) {
|
||||
_reset_menu.beginConfirm("Reset counters?", "Reset");
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user