feat(ui-lvgl): navigation map, clock tools, settings pages, radio

- Home > Map is the navigation map: waypoints (list, add at GPS / by
  coordinates / long-press spot), live shares, trail recording + tools
  (save, load, GPX export, track back), live share controls, target bar
  with ETA; the nodes map opens from Nearby.
- Map: smooth panning (no decode during a drag, idle read-ahead, cached
  coarser stand-ins), overzoom limited to 2 levels, resumable downloads,
  map credit behind a "©" and in Settings > About; target ring and trail
  computed in double.
- Home in swipeable pages; Clock (alarm, timer, stopwatch, ring card).
- Settings rendered from the Core schema (display & power with a
  brightness slider, messages & contacts, trail / live share / alerts),
  GPS switch, Radio screen; low-battery shutdown.
- WioTrackerL2Display: honour the 0-4 brightness level.
- docs: L2 hardware checklist, ui-core notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-25 19:20:42 +02:00
co-authored by Claude Opus 5.5
parent 1cc6a61747
commit 84d8a4df2a
19 changed files with 3510 additions and 399 deletions
@@ -1,7 +1,9 @@
#pragma once
// Decoded map tiles (RGB565, 128 KB each) kept for reuse while panning / zooming
// back, least-recently-used evicted. Buffers live in PSRAM on the ESP32 and are
// allocated on first use, then kept for the session.
// allocated on first use, then kept for the session. Tiles found missing are
// remembered separately (no buffer), so looking up coarser zoom levels for the
// overzoom fallback doesn't push real tiles out.
//
// Single-TU fragment: included by ui-lvgl/UITask.cpp only.
@@ -15,7 +17,7 @@ namespace mapview {
class TileCache {
public:
static const int SLOTS = 12; // 6 on screen at 320x218 + a ring of neighbours
static const int SLOTS = 24; // 6 on screen at 320x218 + the 14-tile read-ahead ring + a few coarser (3 MB)
struct Slot {
int16_t z = -1;
@@ -26,10 +28,13 @@ public:
lv_image_dsc_t dsc;
};
// The slot holding z/x/y if it was loaded (present or known missing), else nullptr.
// The slot holding z/x/y if it was loaded (present, or known missing: a
// shared slot with present == false), else nullptr.
Slot* find(int z, int x, int y) {
for (Slot& s : _slots)
if (s.z == z && s.x == x && s.y == y) { s.used = ++_tick; return &s; }
for (const Missing& m : _missing)
if (m.z == z && m.x == x && m.y == y) return &_miss_slot;
return nullptr;
}
@@ -49,6 +54,12 @@ public:
s->z = z; s->x = x; s->y = y;
s->used = ++_tick;
s->present = src.renderTile(z, x, y, s->px);
if (!s->present) { // remember the miss without holding a buffer slot
Missing& m = _missing[_miss_next++ % MISSING];
m.z = (int16_t)z; m.x = x; m.y = y;
s->z = -1; s->used = 0;
return &_miss_slot;
}
memset(&s->dsc, 0, sizeof(s->dsc));
s->dsc.header.magic = LV_IMAGE_HEADER_MAGIC;
s->dsc.header.cf = LV_COLOR_FORMAT_RGB565;
@@ -61,13 +72,18 @@ public:
}
// Forget the "no tile here" answers (new tiles may have been written).
void forgetMissing() { for (Slot& s : _slots) if (s.z >= 0 && !s.present) { s.z = -1; s.used = 0; } }
void forgetMissing() { for (Missing& m : _missing) m.z = -1; }
// Forget everything (e.g. the card was swapped); keeps the buffers.
void invalidate() { for (Slot& s : _slots) { s.z = -1; s.used = 0; } }
void invalidate() { for (Slot& s : _slots) { s.z = -1; s.used = 0; } forgetMissing(); }
private:
struct Missing { int16_t z = -1; int32_t x = 0, y = 0; };
static const int MISSING = 48;
Slot _slots[SLOTS];
Missing _missing[MISSING];
int _miss_next = 0;
Slot _miss_slot; // present == false
uint32_t _tick = 0;
};
@@ -4,6 +4,10 @@
// alternative is tools/maps/fetch_tiles.py). Tiles already on the card are
// skipped, so a download can be repeated to fill gaps or extend an area.
//
// The job (area + zoom range) is kept in <root>/.job until it completes, so a
// download cut short by a power-off, a lost network or Stop can be resumed:
// resuming re-runs the same job, and the tiles already written are skipped.
//
// Driven by loop() from the UI loop: one small step per call, the HTTP GET
// itself runs asynchronously in the board's fetcher (lvport::fetch*), so the
// mesh keeps being serviced during a long download.
@@ -78,6 +82,20 @@ public:
const char* message() const { return _msg; }
const char* sourceHost() { loadSource(); return _host; }
// Unfinished job from <root>/.job (read once, then tracked in memory).
bool savedJob(TileArea& a) {
if (!_job_checked) { _job_checked = true; _has_job = readJob(_job); }
if (_has_job) a = _job;
return _has_job;
}
void discardJob() {
char path[64];
jobPath(path, sizeof(path));
remove(path);
_has_job = false;
_job_checked = true;
}
bool start(const TileArea& a, const char* ssid, const char* pass) {
if (active()) return false;
_area = a;
@@ -88,6 +106,7 @@ public:
if (_total == 0 || _total > MAX_TILES) { fail("Area too large: use the PC tool"); return false; }
loadSource();
writeAttribution();
writeJob(a);
_z = a.zmin;
tileRange(a, _z, _x0, _y0, _x1, _y1);
_x = _x0; _y = _y0;
@@ -174,6 +193,8 @@ private:
char _attr[96] = "";
char _host[48] = "";
char _msg[64] = "";
TileArea _job = {0, 0, 0, 0, 0, 0};
bool _has_job = false, _job_checked = false;
void fail(const char* m) { snprintf(_msg, sizeof(_msg), "%s", m); _state = FAILED; }
@@ -181,6 +202,35 @@ private:
snprintf(_msg, sizeof(_msg), "%s", m);
_state = s;
lvport::netEnd();
if (s == DONE) discardJob(); // anything else stays resumable
}
void jobPath(char* out, size_t n) const { snprintf(out, n, "%s/.job", _root); }
void writeJob(const TileArea& a) {
char path[64];
jobPath(path, sizeof(path));
makeParents(path);
if (FILE* f = fopen(path, "w")) {
fprintf(f, "%.6f %.6f %.6f %.6f %d %d\n", a.lon0, a.lat0, a.lon1, a.lat1, a.zmin, a.zmax);
fclose(f);
}
_job = a;
_has_job = true;
_job_checked = true;
}
bool readJob(TileArea& a) const {
char path[64];
jobPath(path, sizeof(path));
FILE* f = fopen(path, "r");
if (!f) return false;
TileArea t;
int n = fscanf(f, "%lf %lf %lf %lf %d %d", &t.lon0, &t.lat0, &t.lon1, &t.lat1, &t.zmin, &t.zmax);
fclose(f);
if (n != 6 || t.zmin < 0 || t.zmax > 20 || t.zmin > t.zmax) return false;
a = t;
return true;
}
void onFailure(const char* m) {