fix(ui-lvgl): no watchdog reset during tile downloads, map download rides out a busy server

- Tile fetch and OTA tasks run at idle priority: HTTPClient's read loop
  only yields with delay(0), which at priority 1 starved IDLE0 until the
  task watchdog reset the device (decoded from the core dump)
- A failed tile is retried once; failures in a row pause 5 s ... 60 s on
  the same tile, and the download gives up only after ~5 min of nothing
- The source's last zoom level (OpenTopoMap 17, else 19, or line 3 of
  source.txt) caps the download range and live tiles; past it the map
  magnifies the parent tile
- OpenTopoMap's "no tile" picture is never saved, and copies saved by
  earlier builds are dropped when read
- The placeholder check only runs on a fresh area and for pictures with
  content: plain sea / empty land tiles are genuinely identical

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-26 19:46:36 +02:00
co-authored by Claude Opus 5.5
parent 277c0cddf1
commit 690df304d7
5 changed files with 78 additions and 20 deletions
+5 -2
View File
@@ -327,7 +327,10 @@ static int scanResults(char names[][33], int max) {
// TLS handshakes and slow servers would stall the mesh loop, so the request
// runs on its own task; the UI loop polls. The HTTP connection is reused
// between tiles (keep-alive). Tiles are public pictures, so the TLS peer
// isn't verified (no CA bundle in flash for this).
// isn't verified (no CA bundle in flash for this). The task runs at the idle
// priority: HTTPClient's read loop only yields with delay(0), which never lets
// a lower-priority task in, so at priority 1 a slow transfer starved IDLE0
// until the task watchdog reset the device.
enum { F_IDLE, F_REQUESTED, F_BUSY, F_DONE, F_FAILED };
static volatile int s_fstate = F_IDLE;
static volatile int s_fcode = 0;
@@ -416,7 +419,7 @@ static void fetchTask(void*) {
// memory for the worker task.
static int fetchStart(const char* url) {
if (s_fstate != F_IDLE) return 0;
if (!s_ftask && xTaskCreatePinnedToCore(fetchTask, "tilefetch", 10240, nullptr, 1, &s_ftask, 0) != pdPASS) {
if (!s_ftask && xTaskCreatePinnedToCore(fetchTask, "tilefetch", 10240, nullptr, tskIDLE_PRIORITY, &s_ftask, 0) != pdPASS) {
s_ftask = nullptr;
return -1;
}
+4 -2
View File
@@ -578,7 +578,9 @@ static int dlZmin(int z) { return z - 4 < 5 ? (z < 5 ? z : 5) : z - 4; } // a
void UITask::mapDownloadPopup() {
if (_dl_overlay) return;
if (_dl_zmax < _map_z) _dl_zmax = _map_z + 3 > 17 ? 17 : _map_z + 3;
int src_max = mapview::s_dl.sourceMaxZ();
if (_dl_zmax < _map_z) _dl_zmax = _map_z + 3;
if (_dl_zmax > src_max) _dl_zmax = src_max; // the server has nothing finer
_dl_overlay = lv_obj_create(screen());
lv_obj_remove_style_all(_dl_overlay);
@@ -666,7 +668,7 @@ void UITask::mapDownloadClose() {
void UITask::mapDownloadZmax(int delta) {
if (mapview::s_dl.active()) return;
int z = _dl_zmax + delta;
if (z < _map_z || z > mapview::MAX_Z) return;
if (z < _map_z || z > mapview::MAX_Z || z > mapview::s_dl.sourceMaxZ()) return;
_dl_zmax = z;
refreshDownloadPopup();
}
+3 -1
View File
@@ -153,7 +153,9 @@ static void task(void*) {
static bool startTask() {
s_state = s_job == 1 ? CHECKING : INSTALLING;
if (xTaskCreatePinnedToCore(task, "ota", 12288, nullptr, 1, &s_task, 0) != pdPASS) {
// Idle priority, as the tile fetch (LvglPort.h): HTTPClient only yields
// with delay(0), which at priority 1 starves IDLE0 into a watchdog reset.
if (xTaskCreatePinnedToCore(task, "ota", 12288, nullptr, tskIDLE_PRIORITY, &s_task, 0) != pdPASS) {
s_task = nullptr;
fail("Out of memory (update task)");
return false;
@@ -13,7 +13,8 @@
// mesh keeps being serviced during a long download.
//
// The server comes from <root>/source.txt (line 1: URL template with {z} {x}
// {y}, line 2: attribution); without it, OpenTopoMap. Whatever the source,
// {y}, line 2: attribution, optional line 3: the server's last zoom level,
// default 19 -- or 17 for OpenTopoMap); without it, OpenTopoMap. Whatever the source,
// its attribution is written to <root>/attribution.txt for the map to show.
//
// Live tiles: while the map is open, single tiles it is missing are fetched
@@ -86,6 +87,8 @@ public:
uint32_t failed() const { return _failed; }
const char* message() const { return _msg; }
const char* sourceHost() { loadSource(); return _host; }
// The server's last zoom level: past it, the map magnifies the parent tile.
int sourceMaxZ() { loadSource(); return _src_zmax; }
// Unfinished job from <root>/.job (read once, then tracked in memory).
bool savedJob(TileArea& a) {
@@ -105,17 +108,20 @@ public:
if (active()) return false;
if (_lv_fetching) { lvport::fetchAbandon(); _lv_fetching = false; } // the job takes the fetcher
_lv_state = LV_IDLE;
loadSource();
_area = a;
_total = countTiles(a);
if (_area.zmax > _src_zmax) _area.zmax = _src_zmax; // the server has nothing finer
if (_area.zmin > _area.zmax) { fail("The server has no finer zoom: zoom out"); return false; }
_total = countTiles(_area);
_done = _skipped = _failed = _consec_fail = 0;
_retried = false;
_placeholder_hash = 0; _placeholder_hits = 0;
_msg[0] = '\0';
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);
writeJob(_area);
_z = _area.zmin;
tileRange(_area, _z, _x0, _y0, _x1, _y1);
_x = _x0; _y = _y0;
_fetching = false;
_state = CONNECTING;
@@ -151,7 +157,7 @@ public:
int liveQueued() const { return _live ? _lv_n + (_lv_fetching ? 1 : 0) : 0; }
// A tile the map is missing (duplicates, recent failures and a full queue ignored).
void liveRequest(int z, int x, int y) {
if (!_live || active()) return;
if (!_live || active() || z > _src_zmax) return; // past the server's zoom: the map magnifies
if (_lv_fetching && _lv_cur.z == z && _lv_cur.x == x && _lv_cur.y == y) return;
for (int i = 0; i < _lv_n; i++) if (_lv_q[i].z == z && _lv_q[i].x == x && _lv_q[i].y == y) return;
for (const LiveTile& f : _lv_failed) if (f.z == z && f.x == x && f.y == y) return;
@@ -184,8 +190,7 @@ public:
lvport::fetchAbandon(); // hung past every timeout: give up on this tile
_fetching = false;
onFailure("Request hung (60 s)");
if (_consec_fail >= 8) { finish(FAILED, _msg); return; }
advance();
afterFailure();
return;
}
_fetching = false;
@@ -193,16 +198,24 @@ public:
size_t len = 0;
const uint8_t* data = lvport::fetchData(len);
if (!looksLikeImage(data, len)) onFailure("Server didn't send a PNG tile");
else if (isNoTilePicture(data, len)) { _skipped++; _consec_fail = 0; } // "no tile here": nothing to keep
else if (isPlaceholder(data, len)) { lvport::fetchRelease(); finish(FAILED, "Server sends a placeholder (blocked / key?)"); return; }
else if (!writeTile(_z, _x, _y, data, len)) { lvport::fetchRelease(); finish(FAILED, "Can't write to the SD card"); return; }
else { _done++; _consec_fail = 0; }
} else if (!_retried && r != -403 && r != -401 && r != -404) {
// One more try after a pause: a dropped keep-alive connection or a
// busy server usually answers the second time.
lvport::fetchRelease();
_retried = true;
_last_start = millis() + 1000;
return;
} else {
const char* why = lvport::fetchError();
onFailure(r == -403 || r == -401 ? "Server refused (403)" : why[0] ? why : "Download failed");
}
lvport::fetchRelease();
if (_consec_fail >= 8) { finish(FAILED, _msg[0] ? _msg : "Server not answering"); return; }
advance();
if (_consec_fail) afterFailure();
else advance();
return;
}
@@ -213,7 +226,7 @@ public:
tilePath(path, sizeof(path), _z, _x, _y);
struct stat st;
if (stat(path, &st) == 0 && st.st_size > 0) { _skipped++; advance(); continue; }
if (millis() - _last_start < 120) return; // be gentle with the server
if ((int32_t)(millis() - _last_start) < 120) return; // be gentle with the server (a retry waits longer)
char url[200];
buildUrl(url, sizeof(url), _z, _x, _y);
int r = lvport::fetchStart(url);
@@ -231,12 +244,14 @@ private:
uint8_t _consec_fail = 0;
int _z = 0, _x = 0, _y = 0, _x0 = 0, _y0 = 0, _x1 = 0, _y1 = 0;
bool _fetching = false;
bool _retried = false; // the current tile already failed once
uint32_t _last_start = 0, _connect_started = 0;
uint32_t _placeholder_hash = 0;
uint8_t _placeholder_hits = 0;
char _url_tpl[160] = "";
char _attr[96] = "";
char _host[48] = "";
int _src_zmax = 17;
char _msg[64] = "";
TileArea _job = {0, 0, 0, 0, 0, 0};
bool _has_job = false, _job_checked = false;
@@ -302,7 +317,7 @@ private:
_lv_fetching = false;
size_t len = 0;
const uint8_t* data = r > 0 ? lvport::fetchData(len) : nullptr;
if (r > 0 && looksLikeImage(data, len) && writeTile(_lv_cur.z, _lv_cur.x, _lv_cur.y, data, len)) {
if (r > 0 && looksLikeImage(data, len) && !isNoTilePicture(data, len) && writeTile(_lv_cur.z, _lv_cur.x, _lv_cur.y, data, len)) {
_lv_consec_fail = 0;
if (_lv_done_n < LV_DONE) _lv_done[_lv_done_n++] = _lv_cur;
} else {
@@ -358,6 +373,22 @@ private:
return true;
}
// A failed tile is skipped -- unless failures come in a row: then the server
// (OpenTopoMap is often overloaded) or the network is down, so the same tile
// waits 5 s, 10 s, 20 s ... up to a minute, and the download stops only after
// about five minutes without a single tile.
void afterFailure() {
if (_consec_fail >= 12) { finish(FAILED, _msg[0] ? _msg : "Server not answering"); return; }
if (_consec_fail < 3) { advance(); return; }
_failed--; // this tile gets tried again, it isn't lost yet
uint32_t wait_s = 5u << (_consec_fail - 3);
if (wait_s > 60) wait_s = 60;
_last_start = millis() + wait_s * 1000;
_retried = true; // no extra quick retry on top of the pause
size_t o = strlen(_msg);
snprintf(_msg + o, sizeof(_msg) - o, " - retry in %lus", (unsigned long)wait_s);
}
void onFailure(const char* m) {
_failed++;
_consec_fail++;
@@ -365,6 +396,7 @@ private:
}
void advance() {
_retried = false;
if (++_y <= _y1) return;
_y = _y0;
if (++_x <= _x1) return;
@@ -392,14 +424,17 @@ private:
void loadSource() {
snprintf(_url_tpl, sizeof(_url_tpl), "%s", DEFAULT_TILE_URL);
snprintf(_attr, sizeof(_attr), "%s", DEFAULT_TILE_ATTR);
_src_zmax = -1;
char path[64];
snprintf(path, sizeof(path), "%s/source.txt", _root);
if (FILE* f = fopen(path, "r")) {
char line[160];
if (fgets(line, sizeof(line), f)) { trim(line); if (strstr(line, "{z}")) snprintf(_url_tpl, sizeof(_url_tpl), "%s", line); }
if (fgets(line, sizeof(line), f)) { trim(line); if (line[0]) snprintf(_attr, sizeof(_attr), "%s", line); }
if (fgets(line, sizeof(line), f)) { int z = atoi(line); if (z >= 1 && z <= 22) _src_zmax = z; }
fclose(f);
}
if (_src_zmax < 0) _src_zmax = strstr(_url_tpl, "opentopomap.org") ? 17 : 19;
const char* h = strstr(_url_tpl, "://");
h = h ? h + 3 : _url_tpl;
size_t n = strcspn(h, "/");
@@ -426,9 +461,12 @@ private:
}
// A blocked / key-required server answers every tile with the same picture:
// four identical payloads among the first downloads stop the job.
// four identical payloads among the first downloads stop the job. Only on a
// fresh area (nothing on the card yet -- a resumed one proves the server
// works) and only for pictures with some content: plain sea / empty land
// tiles are genuinely identical, and compress to a few hundred bytes.
bool isPlaceholder(const uint8_t* d, size_t n) {
if (_done >= 8) return false;
if (_done >= 8 || _skipped > 0 || n < 1500) return false;
uint32_t h = 2166136261u;
for (size_t i = 0; i < n; i++) h = (h ^ d[i]) * 16777619u;
if (h == _placeholder_hash) return ++_placeholder_hits >= 3;
@@ -18,6 +18,18 @@ extern "C" {
namespace mapview {
// OpenTopoMap answers every tile past its last zoom level (17) with the same
// "no tile" picture. Earlier builds saved it; the provider drops such a file
// (so the map magnifies the parent instead) and the downloader never writes it.
static const uint32_t OTM_NOTILE_LEN = 4343, OTM_NOTILE_FNV = 0x0B80D772;
static bool isNoTilePicture(const uint8_t* d, size_t n) {
if (n != OTM_NOTILE_LEN) return false;
uint32_t h = 2166136261u;
for (size_t i = 0; i < n; i++) h = (h ^ d[i]) * 16777619u;
return h == OTM_NOTILE_FNV;
}
static const int TILE_PX = 256;
class TileProvider {
@@ -101,6 +113,7 @@ private:
long sz = ftell(f);
uint8_t* buf = sz > 0 ? readRange(f, 0, (uint32_t)sz) : nullptr;
fclose(f);
if (buf && isNoTilePicture(buf, (size_t)sz)) { lv_free(buf); remove(path); return nullptr; }
if (buf) len = (uint32_t)sz;
return buf;
}