mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user