mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-21 04:52:02 +00:00
feat(ui-lvgl): live map tiles kept apart, under a size limit
Tiles fetched while browsing online go to /sdcard/maps-live with an index of arrival order; past the limit (Settings > Storage, 16 MB-1 GB, default 64 MB) the oldest are deleted a few per loop. Storage shows their count and size and can delete them all. Downloaded maps are never touched. Roadmap: live tiles done, second batch of user items added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -225,7 +225,26 @@ Ideas to come back to (2026-09-26):
|
|||||||
remove it?
|
remove it?
|
||||||
- [ ] Import routes from the SD card (optional extra).
|
- [ ] Import routes from the SD card (optional extra).
|
||||||
- [ ] Battery life without losing features, above all CPU sleep.
|
- [ ] Battery life without losing features, above all CPU sleep.
|
||||||
- [ ] Live tiles: don't keep them, or give them a bounded cache on the card
|
- [x] Live tiles: bounded cache on the card (2026-09-26) -- they go to
|
||||||
so they never fill it.
|
/sdcard/maps-live, apart from downloaded maps; an index keeps their
|
||||||
|
order and the oldest are deleted past the limit (Settings > Storage >
|
||||||
|
Live map tiles: 16 / 64 / 256 MB / 1 GB, default 64 MB; Delete).
|
||||||
|
Live tiles saved by earlier builds sit in /sdcard/maps and can't be
|
||||||
|
told apart from downloaded ones.
|
||||||
- [ ] Vector maps and other tile sources (to think through).
|
- [ ] Vector maps and other tile sources (to think through).
|
||||||
|
|
||||||
|
User list, second batch (2026-09-26):
|
||||||
|
|
||||||
|
- [ ] Status bar icons look like different sizes?
|
||||||
|
- [ ] Under own messages: a small counter as in the original (L1), not a
|
||||||
|
checkmark with "relayed".
|
||||||
|
- [ ] Polish the path popup of a sent / received message.
|
||||||
|
- [ ] Map download popup runs off the screen; take the WiFi settings out
|
||||||
|
of it (one place: Settings > WiFi).
|
||||||
|
- [ ] Splash: "MeshCore Solo" with the upstream version and ours, all in
|
||||||
|
the MeshCore title font.
|
||||||
|
- [ ] Emoji, ideally the full range (glyphs could live on the SD card).
|
||||||
|
- [ ] Tap-to-wake toggle.
|
||||||
|
- [ ] Favourites as their own screen in the menu rather than a tile.
|
||||||
|
- [ ] WiFi indicator icon in the status bar.
|
||||||
|
|
||||||
|
|||||||
@@ -212,6 +212,20 @@ static void saveHistKeep(int idx) {
|
|||||||
p.putChar("hkeep", (int8_t)idx);
|
p.putChar("hkeep", (int8_t)idx);
|
||||||
p.end();
|
p.end();
|
||||||
}
|
}
|
||||||
|
// Settings > Storage > Live map tiles: index into mapview::LIVE_CAP_MB, -1 = default.
|
||||||
|
static int loadLiveCap() {
|
||||||
|
Preferences p;
|
||||||
|
if (!p.begin("mc_ui", true)) return -1;
|
||||||
|
int v = p.getChar("ltcap", -1);
|
||||||
|
p.end();
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
static void saveLiveCap(int idx) {
|
||||||
|
Preferences p;
|
||||||
|
if (!p.begin("mc_ui", false)) return;
|
||||||
|
p.putChar("ltcap", (int8_t)idx);
|
||||||
|
p.end();
|
||||||
|
}
|
||||||
// Filesystem size and space in use (Settings > Storage), plus the card's own
|
// Filesystem size and space in use (Settings > Storage), plus the card's own
|
||||||
// size -- a card whose FAT partition is small (e.g. written by a Raspberry Pi
|
// size -- a card whose FAT partition is small (e.g. written by a Raspberry Pi
|
||||||
// imager) shows both. The first free-space count on a big card takes a moment.
|
// imager) shows both. The first free-space count on a big card takes a moment.
|
||||||
@@ -508,6 +522,9 @@ static void saveAccent(int idx) { s_accent = idx; }
|
|||||||
static int s_hist_keep = -1;
|
static int s_hist_keep = -1;
|
||||||
static int loadHistKeep() { return s_hist_keep; }
|
static int loadHistKeep() { return s_hist_keep; }
|
||||||
static void saveHistKeep(int idx) { s_hist_keep = idx; }
|
static void saveHistKeep(int idx) { s_hist_keep = idx; }
|
||||||
|
static int s_live_cap = -1;
|
||||||
|
static int loadLiveCap() { return s_live_cap; }
|
||||||
|
static void saveLiveCap(int idx) { s_live_cap = idx; }
|
||||||
// The browser has no card: a nominal 32 GB, used = what the files add up to
|
// The browser has no card: a nominal 32 GB, used = what the files add up to
|
||||||
// (the storage screen counts them anyway; it passes that in).
|
// (the storage screen counts them anyway; it passes that in).
|
||||||
static bool sdInfo(uint64_t& total, uint64_t& used, uint64_t& card) { total = card = 32ULL << 30; used = 0; return true; }
|
static bool sdInfo(uint64_t& total, uint64_t& used, uint64_t& card) { total = card = 32ULL << 30; used = 0; return true; }
|
||||||
|
|||||||
@@ -17,11 +17,12 @@
|
|||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include "map/TileProvider.h"
|
#include "map/TileProvider.h"
|
||||||
#include "map/TileCache.h"
|
#include "map/TileCache.h"
|
||||||
|
#include "map/LiveCache.h"
|
||||||
#include "map/TileDownloader.h"
|
#include "map/TileDownloader.h"
|
||||||
|
|
||||||
namespace mapview {
|
namespace mapview {
|
||||||
|
|
||||||
static RasterTileProvider s_raster("/sdcard/maps");
|
static RasterTileProvider s_raster("/sdcard/maps", LIVE_ROOT);
|
||||||
static TileProvider* s_provider = &s_raster; // the one place to swap in a vector renderer
|
static TileProvider* s_provider = &s_raster; // the one place to swap in a vector renderer
|
||||||
static TileCache& s_cache = *new (psramBuf<TileCache>(1)) TileCache(); // decoded tiles, in PSRAM
|
static TileCache& s_cache = *new (psramBuf<TileCache>(1)) TileCache(); // decoded tiles, in PSRAM
|
||||||
static TileDownloader s_dl("/sdcard/maps");
|
static TileDownloader s_dl("/sdcard/maps");
|
||||||
@@ -787,6 +788,7 @@ void UITask::setLiveTiles(bool on) {
|
|||||||
void UITask::mapDownloadTick() {
|
void UITask::mapDownloadTick() {
|
||||||
mapview::TileDownloader& dl = mapview::s_dl;
|
mapview::TileDownloader& dl = mapview::s_dl;
|
||||||
dl.loop();
|
dl.loop();
|
||||||
|
mapview::s_live_cache.service(6); // trims live tiles past the Storage limit
|
||||||
if (dl.liveOn()) {
|
if (dl.liveOn()) {
|
||||||
int z, x, y;
|
int z, x, y;
|
||||||
bool got = false;
|
bool got = false;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Settings > Storage: how full the SD card and the internal flash are and
|
// Settings > Storage: how full the SD card and the internal flash are and
|
||||||
// what takes the card's space (maps, message history, GPX trails, the rest),
|
// what takes the card's space (maps, message history, GPX trails, the rest),
|
||||||
// how many messages each conversation keeps (HistoryStore.h) and deleting
|
// how many messages each conversation keeps (HistoryStore.h) and deleting
|
||||||
// the history.
|
// the history, and how much space live map tiles may take (map/LiveCache.h).
|
||||||
//
|
//
|
||||||
// The card's files are counted in the background while the screen is open
|
// The card's files are counted in the background while the screen is open
|
||||||
// (a map can be tens of thousands of tiles), a few milliseconds per loop.
|
// (a map can be tens of thousands of tiles), a few milliseconds per loop.
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
|
|
||||||
namespace storeview {
|
namespace storeview {
|
||||||
|
|
||||||
enum Cat : uint8_t { C_MAPS, C_MSGS, C_TRAILS, C_OTHER, C_COUNT };
|
enum Cat : uint8_t { C_MAPS, C_LIVE, C_MSGS, C_TRAILS, C_OTHER, C_COUNT };
|
||||||
static const char* const CAT_NAME[C_COUNT] = { "Maps", "Messages", "Trails (GPX)", "Other" };
|
static const char* const CAT_NAME[C_COUNT] = { "Maps", "Live map tiles", "Messages", "Trails (GPX)", "Other" };
|
||||||
|
|
||||||
static uint64_t s_bytes[C_COUNT];
|
static uint64_t s_bytes[C_COUNT];
|
||||||
static uint32_t s_files = 0;
|
static uint32_t s_files = 0;
|
||||||
@@ -34,6 +34,9 @@ static lv_obj_t* s_cat_val[C_COUNT];
|
|||||||
static lv_obj_t* s_status = nullptr;
|
static lv_obj_t* s_status = nullptr;
|
||||||
static lv_obj_t* s_clear_lbl = nullptr;
|
static lv_obj_t* s_clear_lbl = nullptr;
|
||||||
static uint32_t s_clear_armed_ms = 0;
|
static uint32_t s_clear_armed_ms = 0;
|
||||||
|
static lv_obj_t* s_live_lbl = nullptr; // "Delete live map tiles" row label
|
||||||
|
static lv_obj_t* s_live_info = nullptr; // count / size from the index
|
||||||
|
static uint32_t s_live_armed_ms = 0;
|
||||||
static uint32_t s_shown_ms = 0;
|
static uint32_t s_shown_ms = 0;
|
||||||
|
|
||||||
static void fmtBytes(char* out, size_t n, uint64_t b) {
|
static void fmtBytes(char* out, size_t n, uint64_t b) {
|
||||||
@@ -65,6 +68,7 @@ static void startWalk() {
|
|||||||
// What a top-level folder counts as.
|
// What a top-level folder counts as.
|
||||||
static uint8_t topCat(const char* name) {
|
static uint8_t topCat(const char* name) {
|
||||||
if (!strcmp(name, "maps")) return C_MAPS;
|
if (!strcmp(name, "maps")) return C_MAPS;
|
||||||
|
if (!strcmp(name, "maps-live")) return C_LIVE;
|
||||||
if (!strcmp(name, "meshcore")) return C_MSGS;
|
if (!strcmp(name, "meshcore")) return C_MSGS;
|
||||||
if (!strcmp(name, "trails")) return C_TRAILS;
|
if (!strcmp(name, "trails")) return C_TRAILS;
|
||||||
return C_OTHER;
|
return C_OTHER;
|
||||||
@@ -193,6 +197,10 @@ static void onHistKeep(lv_event_t* e) {
|
|||||||
s_ui->storageKeep((int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
s_ui->storageKeep((int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
||||||
}
|
}
|
||||||
static void onHistClear(lv_event_t* e) { (void)e; s_ui->storageClearHistory(); }
|
static void onHistClear(lv_event_t* e) { (void)e; s_ui->storageClearHistory(); }
|
||||||
|
static void onLiveCap(lv_event_t* e) {
|
||||||
|
s_ui->storageLiveCap((int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
||||||
|
}
|
||||||
|
static void onLiveClear(lv_event_t* e) { (void)e; s_ui->storageClearLive(); }
|
||||||
|
|
||||||
void UITask::showStorage() {
|
void UITask::showStorage() {
|
||||||
_screen = SCR_STORAGE;
|
_screen = SCR_STORAGE;
|
||||||
@@ -202,7 +210,7 @@ void UITask::showStorage() {
|
|||||||
void UITask::buildStorage() {
|
void UITask::buildStorage() {
|
||||||
using namespace storeview;
|
using namespace storeview;
|
||||||
lv_obj_t* body = newScreen("Storage", true);
|
lv_obj_t* body = newScreen("Storage", true);
|
||||||
s_sd_bar = s_sd_lbl = s_status = s_clear_lbl = nullptr;
|
s_sd_bar = s_sd_lbl = s_status = s_clear_lbl = s_live_lbl = s_live_info = nullptr;
|
||||||
memset(s_cat_val, 0, sizeof(s_cat_val));
|
memset(s_cat_val, 0, sizeof(s_cat_val));
|
||||||
s_clear_armed_ms = 0;
|
s_clear_armed_ms = 0;
|
||||||
|
|
||||||
@@ -245,6 +253,21 @@ void UITask::buildStorage() {
|
|||||||
lv_obj_t* row = listRow(body, LV_SYMBOL_TRASH " Delete message history", "Every chat, on the card and in memory",
|
lv_obj_t* row = listRow(body, LV_SYMBOL_TRASH " Delete message history", "Every chat, on the card and in memory",
|
||||||
onHistClear, NULL);
|
onHistClear, NULL);
|
||||||
s_clear_lbl = lv_obj_get_child(row, 0);
|
s_clear_lbl = lv_obj_get_child(row, 0);
|
||||||
|
|
||||||
|
sectionTitle(body, "LIVE MAP TILES");
|
||||||
|
int cap_idx = mapview::LIVE_CAP_DEFAULT;
|
||||||
|
for (int i = 0; i < mapview::LIVE_CAP_COUNT; i++)
|
||||||
|
if (((uint64_t)mapview::LIVE_CAP_MB[i] << 20) == mapview::s_live_cache.limit()) cap_idx = i;
|
||||||
|
lv_obj_t* cd = dropdownRow(body, "Keep up to", mapview::LIVE_CAP_OPTS, cap_idx);
|
||||||
|
lv_obj_remove_event_cb(cd, onKeyboardAlphabet);
|
||||||
|
lv_obj_add_event_cb(cd, onLiveCap, LV_EVENT_VALUE_CHANGED, NULL);
|
||||||
|
s_live_info = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||||
|
lv_label_set_long_mode(s_live_info, LV_LABEL_LONG_WRAP);
|
||||||
|
lv_obj_set_width(s_live_info, LV_PCT(100));
|
||||||
|
lv_obj_t* lrow = listRow(body, LV_SYMBOL_TRASH " Delete live map tiles", "Downloaded maps stay",
|
||||||
|
onLiveClear, NULL);
|
||||||
|
s_live_lbl = lv_obj_get_child(lrow, 0);
|
||||||
|
s_live_armed_ms = 0;
|
||||||
s_shown_ms = 0;
|
s_shown_ms = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +297,18 @@ void UITask::pollStorage() {
|
|||||||
if (used == 0) for (int c = 0; c < C_COUNT; c++) used += s_bytes[c]; // the sim counts files
|
if (used == 0) for (int c = 0; c < C_COUNT; c++) used += s_bytes[c]; // the sim counts files
|
||||||
showUsage(s_sd_bar, s_sd_lbl, s_sd_total, used);
|
showUsage(s_sd_bar, s_sd_lbl, s_sd_total, used);
|
||||||
}
|
}
|
||||||
|
if (s_live_info) {
|
||||||
|
char b[16], t[96];
|
||||||
|
fmtBytes(b, sizeof(b), mapview::s_live_cache.bytes());
|
||||||
|
if (mapview::s_live_cache.clearing()) snprintf(t, sizeof(t), "Deleting... %lu left", (unsigned long)mapview::s_live_cache.count());
|
||||||
|
else snprintf(t, sizeof(t), "Fetched while browsing online: %lu tiles, %s. Past the limit, the oldest go first.",
|
||||||
|
(unsigned long)mapview::s_live_cache.count(), b);
|
||||||
|
lv_label_set_text(s_live_info, t);
|
||||||
|
}
|
||||||
|
if (s_live_armed_ms && millis() - s_live_armed_ms > 4000) {
|
||||||
|
s_live_armed_ms = 0;
|
||||||
|
if (s_live_lbl) lv_label_set_text(s_live_lbl, LV_SYMBOL_TRASH " Delete live map tiles");
|
||||||
|
}
|
||||||
if (s_clear_armed_ms && millis() - s_clear_armed_ms > 4000) { // the confirm tap timed out
|
if (s_clear_armed_ms && millis() - s_clear_armed_ms > 4000) { // the confirm tap timed out
|
||||||
s_clear_armed_ms = 0;
|
s_clear_armed_ms = 0;
|
||||||
if (s_clear_lbl) lv_label_set_text(s_clear_lbl, LV_SYMBOL_TRASH " Delete message history");
|
if (s_clear_lbl) lv_label_set_text(s_clear_lbl, LV_SYMBOL_TRASH " Delete message history");
|
||||||
@@ -304,3 +339,23 @@ void UITask::storageClearHistory() {
|
|||||||
showToast("Message history deleted");
|
showToast("Message history deleted");
|
||||||
startWalk();
|
startWalk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UITask::storageLiveCap(int idx) {
|
||||||
|
if (idx < 0 || idx >= mapview::LIVE_CAP_COUNT) return;
|
||||||
|
lvport::saveLiveCap(idx);
|
||||||
|
mapview::s_live_cache.setLimit((uint64_t)mapview::LIVE_CAP_MB[idx] << 20); // trimmed in the background
|
||||||
|
showToast("Saved");
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::storageClearLive() {
|
||||||
|
using namespace storeview;
|
||||||
|
if (!s_live_armed_ms) { // first tap: ask
|
||||||
|
s_live_armed_ms = millis();
|
||||||
|
if (s_live_lbl) lv_label_set_text(s_live_lbl, LV_SYMBOL_TRASH " Tap again to delete");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s_live_armed_ms = 0;
|
||||||
|
mapview::s_live_cache.clearAll(); // a few files per loop (mapDownloadTick)
|
||||||
|
if (s_live_lbl) lv_label_set_text(s_live_lbl, LV_SYMBOL_TRASH " Delete live map tiles");
|
||||||
|
showToast("Deleting live map tiles");
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ template <class T> static T* psramBuf(size_t n) {
|
|||||||
#include "Anim.h"
|
#include "Anim.h"
|
||||||
#include "LvglPort.h"
|
#include "LvglPort.h"
|
||||||
#include "HistoryStore.h"
|
#include "HistoryStore.h"
|
||||||
|
#include "map/LiveCache.h"
|
||||||
static histstore::SdArchive s_archive; // message history on the SD card
|
static histstore::SdArchive s_archive; // message history on the SD card
|
||||||
namespace storeview { static void stopWalk(); } // StorageScreen.h
|
namespace storeview { static void stopWalk(); } // StorageScreen.h
|
||||||
#include "../ui-core/KeyboardData.h"
|
#include "../ui-core/KeyboardData.h"
|
||||||
@@ -216,6 +217,8 @@ void UITask::begin(DisplayDriver* display_drv, SensorManager* sensors, NodePrefs
|
|||||||
{ // history kept on the SD card: back into the ring, then every new entry to it
|
{ // history kept on the SD card: back into the ring, then every new entry to it
|
||||||
int hk = lvport::loadHistKeep();
|
int hk = lvport::loadHistKeep();
|
||||||
s_archive.setKeep(histstore::KEEP[hk >= 0 && hk < histstore::KEEP_COUNT ? hk : histstore::KEEP_DEFAULT]);
|
s_archive.setKeep(histstore::KEEP[hk >= 0 && hk < histstore::KEEP_COUNT ? hk : histstore::KEEP_DEFAULT]);
|
||||||
|
int lc = lvport::loadLiveCap();
|
||||||
|
mapview::s_live_cache.setLimit((uint64_t)mapview::LIVE_CAP_MB[lc >= 0 && lc < mapview::LIVE_CAP_COUNT ? lc : mapview::LIVE_CAP_DEFAULT] << 20);
|
||||||
s_archive.restore(_core->history);
|
s_archive.restore(_core->history);
|
||||||
_core->history.setArchive(&s_archive);
|
_core->history.setArchive(&s_archive);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ public:
|
|||||||
void pollStorage();
|
void pollStorage();
|
||||||
void storageKeep(int idx);
|
void storageKeep(int idx);
|
||||||
void storageClearHistory();
|
void storageClearHistory();
|
||||||
|
void storageLiveCap(int idx);
|
||||||
|
void storageClearLive();
|
||||||
bool locked() const;
|
bool locked() const;
|
||||||
void pinKey(const char* key); // the lock screen's keypad
|
void pinKey(const char* key); // the lock screen's keypad
|
||||||
void pinSetupPopup(); // Settings > Display & power > Screen PIN
|
void pinSetupPopup(); // Settings > Display & power > Screen PIN
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
#pragma once
|
||||||
|
// Live map tiles (fetched while browsing online) live apart from downloaded
|
||||||
|
// maps, under LIVE_ROOT/{z}/{x}/{y}.png, and only up to a size limit
|
||||||
|
// (Settings > Storage): an index remembers the order they came in, and the
|
||||||
|
// oldest are deleted once the tiles add up to more than the limit. Maps
|
||||||
|
// downloaded on purpose (TileDownloader jobs, the PC tool) are never touched.
|
||||||
|
//
|
||||||
|
// Index: LIVE_ROOT/index.bin, a header and a ring of RING records. Eviction
|
||||||
|
// runs a few files per loop (service()), so lowering the limit or clearing
|
||||||
|
// thousands of tiles never blocks the UI.
|
||||||
|
//
|
||||||
|
// Single-TU fragment: included by ui-lvgl/UITask.cpp only (after LvglPort.h).
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
namespace mapview {
|
||||||
|
|
||||||
|
static const char* const LIVE_ROOT = "/sdcard/maps-live";
|
||||||
|
|
||||||
|
// Settings > Storage > Live map tiles: "Keep up to".
|
||||||
|
static const uint32_t LIVE_CAP_MB[] = { 16, 64, 256, 1024 };
|
||||||
|
static const int LIVE_CAP_COUNT = sizeof(LIVE_CAP_MB) / sizeof(LIVE_CAP_MB[0]);
|
||||||
|
static const int LIVE_CAP_DEFAULT = 1;
|
||||||
|
static const char* const LIVE_CAP_OPTS = "16 MB\n64 MB\n256 MB\n1 GB";
|
||||||
|
|
||||||
|
class LiveCache {
|
||||||
|
public:
|
||||||
|
static const uint32_t RING = 16384; // tiles; ~400 MB of typical tiles, 256 KB of index
|
||||||
|
|
||||||
|
void setLimit(uint64_t bytes) { _limit = bytes; }
|
||||||
|
uint64_t limit() const { return _limit; }
|
||||||
|
uint64_t bytes() { load(); return _h.bytes; }
|
||||||
|
uint32_t count() { load(); return _h.count; }
|
||||||
|
bool clearing() const { return _clearing; }
|
||||||
|
|
||||||
|
// A tile was written to LIVE_ROOT: remember it (eviction follows in service()).
|
||||||
|
void add(int z, int x, int y, uint32_t size) {
|
||||||
|
load();
|
||||||
|
if (_h.count >= RING) evictOne(); // the ring is full: make room right away
|
||||||
|
Rec r = { (int32_t)z, x, y, size };
|
||||||
|
uint32_t slot = (_h.head + _h.count) % RING;
|
||||||
|
_h.count++;
|
||||||
|
_h.bytes += size;
|
||||||
|
writeRec(slot, r);
|
||||||
|
writeHdr();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete every live tile (a few per service() call).
|
||||||
|
void clearAll() { load(); _clearing = true; }
|
||||||
|
|
||||||
|
// Evicts while over the limit (or clearing), for up to `budget_ms`.
|
||||||
|
void service(uint32_t budget_ms) {
|
||||||
|
if (!_loaded && !_clearing) return; // nothing written or asked for yet
|
||||||
|
load();
|
||||||
|
uint32_t t0 = millis();
|
||||||
|
bool changed = false;
|
||||||
|
while ((_clearing ? _h.count > 0 : _h.bytes > _limit) && millis() - t0 < budget_ms) {
|
||||||
|
evictOne(false);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed) writeHdr();
|
||||||
|
if (_clearing && _h.count == 0) { _clearing = false; _h.bytes = 0; writeHdr(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tilePath(char* out, size_t n, int z, int x, int y) {
|
||||||
|
snprintf(out, n, "%s/%d/%d/%d.png", LIVE_ROOT, z, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Hdr { uint32_t magic, ring, head, count; uint64_t bytes; };
|
||||||
|
struct Rec { int32_t z, x, y; uint32_t size; };
|
||||||
|
static const uint32_t MAGIC = 0x3143544C; // "LTC1"
|
||||||
|
|
||||||
|
Hdr _h = { MAGIC, RING, 0, 0, 0 };
|
||||||
|
bool _loaded = false, _clearing = false;
|
||||||
|
uint64_t _limit = 64ULL << 20;
|
||||||
|
|
||||||
|
static void indexPath(char* out, size_t n) { snprintf(out, n, "%s/index.bin", LIVE_ROOT); }
|
||||||
|
|
||||||
|
void load() {
|
||||||
|
if (_loaded) return;
|
||||||
|
_loaded = true;
|
||||||
|
char p[128];
|
||||||
|
indexPath(p, sizeof(p));
|
||||||
|
FILE* f = fopen(p, "rb");
|
||||||
|
Hdr h;
|
||||||
|
if (f && fread(&h, sizeof(h), 1, f) == 1 && h.magic == MAGIC && h.ring == RING && h.count <= RING && h.head < RING) _h = h;
|
||||||
|
else _h = { MAGIC, RING, 0, 0, 0 };
|
||||||
|
if (f) fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE* openIndex() {
|
||||||
|
char p[128];
|
||||||
|
indexPath(p, sizeof(p));
|
||||||
|
FILE* f = fopen(p, "r+b");
|
||||||
|
if (!f) {
|
||||||
|
mkdir(LIVE_ROOT, 0777);
|
||||||
|
f = fopen(p, "w+b");
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
void writeHdr() {
|
||||||
|
if (FILE* f = openIndex()) { fwrite(&_h, sizeof(_h), 1, f); fclose(f); }
|
||||||
|
}
|
||||||
|
void writeRec(uint32_t slot, const Rec& r) {
|
||||||
|
if (FILE* f = openIndex()) {
|
||||||
|
if (fseek(f, (long)(sizeof(Hdr) + slot * sizeof(Rec)), SEEK_SET) == 0) fwrite(&r, sizeof(r), 1, f);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool readRec(uint32_t slot, Rec& r) {
|
||||||
|
char p[128];
|
||||||
|
indexPath(p, sizeof(p));
|
||||||
|
FILE* f = fopen(p, "rb");
|
||||||
|
if (!f) return false;
|
||||||
|
bool ok = fseek(f, (long)(sizeof(Hdr) + slot * sizeof(Rec)), SEEK_SET) == 0 && fread(&r, sizeof(r), 1, f) == 1;
|
||||||
|
fclose(f);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
void evictOne(bool save = true) {
|
||||||
|
if (_h.count == 0) return;
|
||||||
|
Rec r;
|
||||||
|
if (readRec(_h.head, r)) {
|
||||||
|
char path[128];
|
||||||
|
tilePath(path, sizeof(path), r.z, r.x, r.y);
|
||||||
|
remove(path);
|
||||||
|
_h.bytes = _h.bytes > r.size ? _h.bytes - r.size : 0;
|
||||||
|
}
|
||||||
|
_h.head = (_h.head + 1) % RING;
|
||||||
|
_h.count--;
|
||||||
|
if (save) writeHdr();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static LiveCache s_live_cache;
|
||||||
|
|
||||||
|
} // namespace mapview
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
// Live tiles: while the map is open, single tiles it is missing are fetched
|
// Live tiles: while the map is open, single tiles it is missing are fetched
|
||||||
// the same way (one at a time, between the job's steps when there is no job),
|
// the same way (one at a time, between the job's steps when there is no job),
|
||||||
// the WiFi connected on the first miss and dropped by the caller (liveEnd)
|
// the WiFi connected on the first miss and dropped by the caller (liveEnd)
|
||||||
// once the map has been closed for a while.
|
// once the map has been closed for a while. They go to LIVE_ROOT, not
|
||||||
|
// <root>, and only up to the Storage limit (LiveCache.h).
|
||||||
//
|
//
|
||||||
// Single-TU fragment: included by ui-lvgl/UITask.cpp only (after LvglPort.h).
|
// Single-TU fragment: included by ui-lvgl/UITask.cpp only (after LvglPort.h).
|
||||||
|
|
||||||
@@ -317,7 +318,10 @@ private:
|
|||||||
_lv_fetching = false;
|
_lv_fetching = false;
|
||||||
size_t len = 0;
|
size_t len = 0;
|
||||||
const uint8_t* data = r > 0 ? lvport::fetchData(len) : nullptr;
|
const uint8_t* data = r > 0 ? lvport::fetchData(len) : nullptr;
|
||||||
if (r > 0 && looksLikeImage(data, len) && !isNoTilePicture(data, len) && writeTile(_lv_cur.z, _lv_cur.x, _lv_cur.y, data, len)) {
|
char lpath[64];
|
||||||
|
LiveCache::tilePath(lpath, sizeof(lpath), _lv_cur.z, _lv_cur.x, _lv_cur.y);
|
||||||
|
if (r > 0 && looksLikeImage(data, len) && !isNoTilePicture(data, len) && writeFile(lpath, data, len)) {
|
||||||
|
s_live_cache.add(_lv_cur.z, _lv_cur.x, _lv_cur.y, (uint32_t)len); // kept apart, within the Storage limit
|
||||||
_lv_consec_fail = 0;
|
_lv_consec_fail = 0;
|
||||||
if (_lv_done_n < LV_DONE) _lv_done[_lv_done_n++] = _lv_cur;
|
if (_lv_done_n < LV_DONE) _lv_done[_lv_done_n++] = _lv_cur;
|
||||||
} else {
|
} else {
|
||||||
@@ -476,8 +480,12 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool writeTile(int z, int x, int y, const uint8_t* d, size_t n) {
|
bool writeTile(int z, int x, int y, const uint8_t* d, size_t n) {
|
||||||
char path[96], tmp[100];
|
char path[96];
|
||||||
tilePath(path, sizeof(path), z, x, y);
|
tilePath(path, sizeof(path), z, x, y);
|
||||||
|
return writeFile(path, d, n);
|
||||||
|
}
|
||||||
|
static bool writeFile(const char* path, const uint8_t* d, size_t n) {
|
||||||
|
char tmp[100];
|
||||||
makeParents(path);
|
makeParents(path);
|
||||||
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
|
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
|
||||||
FILE* f = fopen(tmp, "wb");
|
FILE* f = fopen(tmp, "wb");
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ public:
|
|||||||
// which copies onto a FAT card far faster than millions of loose files.
|
// which copies onto a FAT card far faster than millions of loose files.
|
||||||
class RasterTileProvider : public TileProvider {
|
class RasterTileProvider : public TileProvider {
|
||||||
public:
|
public:
|
||||||
explicit RasterTileProvider(const char* root) : _root(root) {}
|
// `live`: a second folder in the same layout (live tiles, LiveCache.h), read
|
||||||
|
// when <root> has no tile.
|
||||||
|
explicit RasterTileProvider(const char* root, const char* live = nullptr) : _root(root), _live(live) {}
|
||||||
|
|
||||||
// Also reads <root>/attribution.txt (written by tools/maps/fetch_tiles.py and
|
// Also reads <root>/attribution.txt (written by tools/maps/fetch_tiles.py and
|
||||||
// the on-device downloader): the tile set's credit line, shown on the map.
|
// the on-device downloader): the tile set's credit line, shown on the map.
|
||||||
@@ -68,8 +70,9 @@ public:
|
|||||||
|
|
||||||
bool renderTile(int z, int x, int y, uint16_t* out) override {
|
bool renderTile(int z, int x, int y, uint16_t* out) override {
|
||||||
uint32_t len = 0;
|
uint32_t len = 0;
|
||||||
uint8_t* png = readLoose(z, x, y, len);
|
uint8_t* png = readLoose(_root, z, x, y, len);
|
||||||
if (!png) png = readPacked(z, x, y, len);
|
if (!png) png = readPacked(z, x, y, len);
|
||||||
|
if (!png && _live) png = readLoose(_live, z, x, y, len);
|
||||||
if (!png) return false;
|
if (!png) return false;
|
||||||
|
|
||||||
unsigned char* res = nullptr;
|
unsigned char* res = nullptr;
|
||||||
@@ -94,6 +97,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
const char* _root;
|
const char* _root;
|
||||||
|
const char* _live;
|
||||||
char _attr[96] = "";
|
char _attr[96] = "";
|
||||||
|
|
||||||
static uint8_t* readRange(FILE* f, long off, uint32_t len) {
|
static uint8_t* readRange(FILE* f, long off, uint32_t len) {
|
||||||
@@ -104,9 +108,9 @@ private:
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t* readLoose(int z, int x, int y, uint32_t& len) {
|
uint8_t* readLoose(const char* root, int z, int x, int y, uint32_t& len) {
|
||||||
char path[64];
|
char path[64];
|
||||||
snprintf(path, sizeof(path), "%s/%d/%d/%d.png", _root, z, x, y);
|
snprintf(path, sizeof(path), "%s/%d/%d/%d.png", root, z, x, y);
|
||||||
FILE* f = fopen(path, "rb");
|
FILE* f = fopen(path, "rb");
|
||||||
if (!f) return nullptr;
|
if (!f) return nullptr;
|
||||||
fseek(f, 0, SEEK_END);
|
fseek(f, 0, SEEK_END);
|
||||||
|
|||||||
Reference in New Issue
Block a user