From 910a7c01974333ad035da6ab425a460202bbd842 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 1 Oct 2026 10:54:09 +0200 Subject: [PATCH] fix(l2): map download no longer stalls on busy tile servers A 404/503 answer (OpenTopoMap renders on demand) went to an immediate 2 s retry and counted as network loss, so each tile then waited up to 60 s. Answered errors now go straight to the retry list; long pauses only when there is no answer at all. Three retry rounds (20/60/120 s) for the tiles still missing, transfers cut after 30 s, failures logged on serial. Download popup tidied: title + floating close, percentage instead of tile counts (also on the map pill), no dead zoom row, no finish toast after Stop or while the popup shows the result. Co-Authored-By: Claude Opus 5.5 --- examples/companion_radio/ui-lvgl/LvglPort.h | 11 +- examples/companion_radio/ui-lvgl/MapScreen.h | 189 +++++++----------- examples/companion_radio/ui-lvgl/UITask.cpp | 2 +- examples/companion_radio/ui-lvgl/UITask.h | 4 +- .../ui-lvgl/map/TileDownloader.h | 86 +++++--- 5 files changed, 138 insertions(+), 154 deletions(-) diff --git a/examples/companion_radio/ui-lvgl/LvglPort.h b/examples/companion_radio/ui-lvgl/LvglPort.h index aeb7767f..637d9f7d 100644 --- a/examples/companion_radio/ui-lvgl/LvglPort.h +++ b/examples/companion_radio/ui-lvgl/LvglPort.h @@ -574,12 +574,17 @@ static size_t s_fcap = 0; static volatile size_t s_flen = 0; static volatile bool s_fabandoned = false; static TaskHandle_t s_ftask = nullptr; +static uint32_t s_fstart_ms = 0; +static const uint32_t FETCH_MAX_MS = 30000; // a body still trickling in after this is cut off // Stream sink for HTTPClient::writeToStream() (which also undoes chunking). class FetchSink : public Stream { public: size_t write(uint8_t c) override { return write(&c, 1); } size_t write(const uint8_t* d, size_t n) override { + // A stalled server can keep a body trickling in for minutes, each read + // inside HTTPClient's timeout; cut off, the tile is retried later. + if (millis() - s_fstart_ms > FETCH_MAX_MS) return 0; if (s_flen + n > s_fcap) { size_t cap = s_fcap ? s_fcap : 32 * 1024; while (cap < s_flen + n) cap *= 2; @@ -622,6 +627,7 @@ static void fetchTask(void*) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); s_fstate = F_BUSY; s_flen = 0; + s_fstart_ms = millis(); bool https = strncmp(s_furl, "https:", 6) == 0; int code = -1; s_ferr[0] = '\0'; @@ -629,7 +635,10 @@ static void fetchTask(void*) { code = http.GET(); if (code == 200) { FetchSink sink; - if (http.writeToStream(&sink) < 0) { code = -1; snprintf(s_ferr, sizeof(s_ferr), "read failed / out of memory"); } + if (http.writeToStream(&sink) < 0) { + code = -1; + snprintf(s_ferr, sizeof(s_ferr), "%s", millis() - s_fstart_ms > FETCH_MAX_MS ? "too slow (30 s)" : "read failed / out of memory"); + } } else if (code < 0) { char tls_err[40] = ""; if (https && tls.lastError(tls_err, sizeof(tls_err)) != 0 && tls_err[0]) diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index 42a63335..9ea08911 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -275,8 +275,6 @@ static void onMapCenter(lv_event_t* e) { (void)e; s_ui->mapCenterOnMe(); } static void onMapDownload(lv_event_t* e) { (void)e; s_ui->mapDownloadPopup(); } static void onDlClose(lv_event_t* e) { (void)e; s_ui->mapDownloadClose(); } static void onDlStart(lv_event_t* e) { (void)e; s_ui->mapDownloadStart(); } -static void onDlZoomMinus(lv_event_t* e) { (void)e; s_ui->mapDownloadZmax(-1); } -static void onDlZoomPlus(lv_event_t* e) { (void)e; s_ui->mapDownloadZmax(+1); } static void onDlResume(lv_event_t* e) { (void)e; s_ui->mapDownloadResume(); } static void onDlDiscard(lv_event_t* e) { (void)e; s_ui->mapDownloadDiscard(); } static void onNavTools(lv_event_t* e); // NavMap.h @@ -800,76 +798,45 @@ void UITask::mapLongPress(int x, int y) { // ── Area download ───────────────────────────────────────────────────────────── -// The visible view as a lon/lat box. -static mapview::TileArea visibleArea(double cx, double cy, int z, int w, int h, int zmin, int zmax) { - double n = (double)(1 << z); - auto lon = [n](double tx) { return tx / n * 360.0 - 180.0; }; - auto lat = [n](double ty) { return atan(sinh(M_PI * (1.0 - 2.0 * ty / n))) * 180.0 / M_PI; }; - double hw = w / 2.0 / mapview::TILE_PX, hh = h / 2.0 / mapview::TILE_PX; - mapview::TileArea a; - a.lon0 = lon(cx - hw); a.lon1 = lon(cx + hw); - a.lat1 = lat(cy - hh); a.lat0 = lat(cy + hh); - a.zmin = zmin; a.zmax = zmax; - return a; -} - -static int dlZmin(int z) { return z - 4 < 5 ? (z < 5 ? z : 5) : z - 4; } // a few overview levels (cheap) - // The download button: progress / an unfinished job in a popup, else the -// frame to pick a new area (MapAreas.h). +// frame to pick a new area (MapAreas.h), which sets the zoom range and shows +// the size. void UITask::mapDownloadPopup() { if (_dl_overlay) return; mapview::TileArea job; if (!mapview::s_dl.active() && !mapview::s_dl.savedJob(job)) { areaSelectBegin(); return; } - 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 lv_obj_t* panel = popupOpen(screen(), POP_FIT, _dl_overlay); + lv_obj_set_style_pad_row(panel, 6, 0); - lv_obj_t* hdr = lv_obj_create(panel); - styleSurface(hdr, theme::BG); - lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(hdr, LV_PCT(100), 26); - lv_obj_align(label(hdr, "Download this area", THEME_FONT_TITLE, theme::TEXT), LV_ALIGN_LEFT_MID, 0, 0); - headerButton(hdr, LV_SYMBOL_CLOSE, onDlClose, 0, NULL); - - // Detail range: from a few overview levels up to the chosen max zoom. - lv_obj_t* zr = lv_obj_create(panel); - styleSurface(zr, theme::BG); - lv_obj_remove_flag(zr, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(zr, LV_PCT(100), 30); - lv_obj_align(label(zr, "Up to zoom", THEME_FONT_BODY, theme::TEXT_MUTED), LV_ALIGN_LEFT_MID, 0, 0); - headerButton(zr, LV_SYMBOL_PLUS, onDlZoomPlus, 0, NULL); - _dl_zoom_lbl = label(zr, "", THEME_FONT_TITLE, theme::ACCENT); - lv_obj_align(_dl_zoom_lbl, LV_ALIGN_RIGHT_MID, -56, 0); - headerButton(zr, LV_SYMBOL_MINUS, onDlZoomMinus, 96, NULL); + // The title in the column, the close button floating over its right end: + // no box of their own to lay out and draw. + lv_obj_set_style_pad_top(label(panel, "Map download", THEME_FONT_TITLE, theme::TEXT), 4, 0); + lv_obj_t* x = headerButton(panel, LV_SYMBOL_CLOSE, onDlClose, 0, NULL); + lv_obj_add_flag(x, LV_OBJ_FLAG_FLOATING); + lv_obj_align(x, LV_ALIGN_TOP_RIGHT, 0, 0); // Unfinished job (power-off, lost WiFi, Stop): resume or drop it. _dl_job_row = lv_obj_create(panel); styleSurface(_dl_job_row, theme::SURFACE); lv_obj_remove_flag(_dl_job_row, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(_dl_job_row, LV_PCT(100), 40); + lv_obj_set_size(_dl_job_row, LV_PCT(100), 44); lv_obj_set_style_radius(_dl_job_row, theme::RADIUS, 0); _dl_job_lbl = label(_dl_job_row, "", THEME_FONT_SMALL, theme::TEXT); - lv_obj_set_width(_dl_job_lbl, 170); + lv_obj_set_width(_dl_job_lbl, 150); lv_label_set_long_mode(_dl_job_lbl, LV_LABEL_LONG_WRAP); lv_obj_align(_dl_job_lbl, LV_ALIGN_LEFT_MID, theme::PAD, 0); stylePrimary(headerButton(_dl_job_row, LV_SYMBOL_PLAY " Resume", onDlResume, 48, NULL)); headerButton(_dl_job_row, LV_SYMBOL_TRASH, onDlDiscard, 4, NULL); lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); - _dl_info = noteLabel(panel, "", THEME_FONT_SMALL, theme::TEXT); - lv_obj_set_style_text_line_space(_dl_info, 2, 0); + _dl_info = noteLabel(panel, "", THEME_FONT_BODY, theme::TEXT); // what's going on _dl_bar = lv_bar_create(panel); lv_obj_set_size(_dl_bar, LV_PCT(100), 6); lv_obj_set_style_bg_color(_dl_bar, lv_color_hex(theme::SURFACE_2), 0); lv_obj_set_style_bg_color(_dl_bar, lv_color_hex(theme::ACCENT), LV_PART_INDICATOR); - lv_obj_add_flag(_dl_bar, LV_OBJ_FLAG_HIDDEN); - _dl_err = label(panel, "", THEME_FONT_SMALL, theme::FAIL); - lv_label_set_long_mode(_dl_err, LV_LABEL_LONG_DOT); // one line, whatever the server said - lv_obj_set_width(_dl_err, LV_PCT(100)); - lv_obj_add_flag(_dl_err, LV_OBJ_FLAG_HIDDEN); + _dl_sub = noteLabel(panel, ""); // the details, muted + _dl_err = noteLabel(panel, "", THEME_FONT_SMALL, theme::FAIL); // tiles missing / WiFi lv_obj_t* go = barButton(buttonBar(panel), "", onDlStart, 0, true); _dl_start_lbl = lv_obj_get_child(go, 0); @@ -880,15 +847,7 @@ void UITask::mapDownloadPopup() { void UITask::mapDownloadClose() { if (_dl_overlay) lv_obj_delete_async(_dl_overlay); // may be closing from its own button - _dl_overlay = _dl_info = _dl_bar = _dl_err = _dl_zoom_lbl = _dl_start_lbl = _dl_job_row = _dl_job_lbl = nullptr; -} - -void UITask::mapDownloadZmax(int delta) { - if (mapview::s_dl.active()) return; - int z = _dl_zmax + delta; - if (z < _map_z || z > mapview::MAX_Z || z > mapview::s_dl.sourceMaxZ()) return; - _dl_zmax = z; - refreshDownloadPopup(); + _dl_overlay = _dl_info = _dl_bar = _dl_sub = _dl_err = _dl_start_lbl = _dl_job_row = _dl_job_lbl = nullptr; } // "a.tile.opentopomap.org" -> "opentopomap.org": the name people know. @@ -901,69 +860,65 @@ static const char* shortHost(const char* h) { } } -static void dlSize(char* out, size_t n, uint64_t bytes) { - if (bytes < 1024 * 1024) snprintf(out, n, "%lu KB", (unsigned long)(bytes / 1024)); - else snprintf(out, n, "%lu MB", (unsigned long)((bytes + 512 * 1024) / (1024 * 1024))); +static int dlPercent(const mapview::TileDownloader& dl) { + return dl.total() ? (int)((uint64_t)dl.processed() * 100 / dl.total()) : 0; +} + +// A label's text, or the label hidden when there's none. +static void textOrHide(lv_obj_t* l, const char* text) { + if (!text || !text[0]) { lv_obj_add_flag(l, LV_OBJ_FLAG_HIDDEN); return; } + lv_obj_remove_flag(l, LV_OBJ_FLAG_HIDDEN); + setText(l, text); } void UITask::refreshDownloadPopup() { if (!_dl_info) return; - mapview::TileDownloader& dl = mapview::s_dl; - lv_label_set_text_fmt(_dl_zoom_lbl, "z%d", _dl_zmax); + using mapview::TileDownloader; + TileDownloader& dl = mapview::s_dl; const char* host = shortHost(dl.sourceHost()); - lv_obj_t* zoom_row = lv_obj_get_parent(_dl_zoom_lbl); - if (dl.active()) { // the range is fixed while it runs: room for the progress instead - lv_obj_add_flag(zoom_row, LV_OBJ_FLAG_HIDDEN); - if (_dl_job_row) lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(_dl_bar, LV_OBJ_FLAG_HIDDEN); + char info[64] = "", sub[96] = "", err[96] = ""; + if (dl.active()) { + lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); + bool connecting = dl.state() == TileDownloader::CONNECTING; + if (connecting) { + snprintf(info, sizeof(info), "Connecting to WiFi..."); + } else { + snprintf(info, sizeof(info), "Downloading %d%%", dlPercent(dl)); + snprintf(sub, sizeof(sub), "%lu new tiles from %s\nCarries on in the background", (unsigned long)dl.downloaded(), host); + } + if (dl.retryRound()) + snprintf(err, sizeof(err), "Trying %lu missing tiles again (%d of %d)", (unsigned long)dl.failed(), + dl.retryRound(), TileDownloader::RETRY_ROUNDS); + else if (dl.failed()) + snprintf(err, sizeof(err), "%lu missing, tried again at the end (%s)", (unsigned long)dl.failed(), dl.message()); uint32_t total = dl.total(); lv_bar_set_range(_dl_bar, 0, total > 0 ? (int32_t)total : 1); lv_bar_set_value(_dl_bar, (int32_t)dl.processed(), LV_ANIM_OFF); - if (dl.state() == mapview::TileDownloader::CONNECTING) - lv_label_set_text_fmt(_dl_info, "Connecting to WiFi... - from %s", host); - else - lv_label_set_text_fmt(_dl_info, "%lu / %lu tiles, %lu new - from %s\nYou can close this, it carries on", - (unsigned long)dl.processed(), (unsigned long)total, - (unsigned long)dl.downloaded(), host); - if (dl.failed()) { - lv_label_set_text_fmt(_dl_err, "%lu failed: %s", (unsigned long)dl.failed(), dl.message()); - lv_obj_remove_flag(_dl_err, LV_OBJ_FLAG_HIDDEN); + if (connecting) lv_obj_add_flag(_dl_bar, LV_OBJ_FLAG_HIDDEN); + else lv_obj_remove_flag(_dl_bar, LV_OBJ_FLAG_HIDDEN); + setText(_dl_start_lbl, LV_SYMBOL_STOP " Stop"); + } else { + lv_obj_add_flag(_dl_bar, LV_OBJ_FLAG_HIDDEN); + mapview::TileArea job; + bool has_job = dl.savedJob(job); + if (has_job) { + setTextFmt(_dl_job_lbl, "Unfinished: z%d-%d,\n%lu tiles", job.zmin, job.zmax, (unsigned long)mapview::countTiles(job)); + lv_obj_remove_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); } else { - lv_obj_add_flag(_dl_err, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); } - lv_label_set_text(_dl_start_lbl, LV_SYMBOL_STOP " Stop"); - return; + uint8_t st = dl.state(); + if (st == TileDownloader::DONE || st == TileDownloader::FAILED || st == TileDownloader::CANCELLED) + snprintf(sub, sizeof(sub), "Last download: %s, %lu new tiles", dl.message(), (unsigned long)dl.downloaded()); + // The network itself is set up in Settings > WiFi only. + char ssid[33], pass[65]; + if (!lvport::wifiAllowed()) snprintf(err, sizeof(err), "WiFi is off - Settings > WiFi"); + else if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) snprintf(err, sizeof(err), "No WiFi network - Settings > WiFi"); + setText(_dl_start_lbl, LV_SYMBOL_PLUS " New area"); } - lv_obj_remove_flag(zoom_row, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(_dl_bar, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(_dl_err, LV_OBJ_FLAG_HIDDEN); - mapview::TileArea job; - if (_dl_job_row && dl.savedJob(job)) { - lv_label_set_text_fmt(_dl_job_lbl, "Unfinished: z%d-%d,\n%lu tiles", job.zmin, job.zmax, - (unsigned long)mapview::countTiles(job)); - lv_obj_remove_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); - } else if (_dl_job_row) { - lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); - } - int w = _map_area ? lv_obj_get_width(_map_area) : 320, h = _map_area ? lv_obj_get_height(_map_area) : 218; - mapview::TileArea a = visibleArea(_map_cx, _map_cy, _map_z, w, h, dlZmin(_map_z), _dl_zmax); - uint32_t n = mapview::countTiles(a); - char size[16]; - dlSize(size, sizeof(size), (uint64_t)n * mapview::AVG_TILE_BYTES); - // The network itself is set up in Settings > WiFi only; here just which one. - char ssid[33], pass[65], net[64]; - if (!lvport::wifiAllowed()) snprintf(net, sizeof(net), "WiFi is off - Settings > WiFi"); - else if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) snprintf(net, sizeof(net), "No WiFi network - Settings > WiFi"); - else snprintf(net, sizeof(net), "Over WiFi: %s", ssid); - char last[64] = ""; - bool job_shown = _dl_job_row && !lv_obj_has_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); // it says enough - if (!job_shown && (dl.state() == mapview::TileDownloader::DONE || dl.state() == mapview::TileDownloader::FAILED || - dl.state() == mapview::TileDownloader::CANCELLED)) - snprintf(last, sizeof(last), "\nLast: %s (%lu new)", dl.message(), (unsigned long)dl.downloaded()); - lv_label_set_text_fmt(_dl_info, "z%d-%d: %lu tiles, about %s - %s\n%s%s", - a.zmin, a.zmax, (unsigned long)n, size, host, net, last); - (void)n; - lv_label_set_text(_dl_start_lbl, LV_SYMBOL_PLUS " New area"); + textOrHide(_dl_info, info); + textOrHide(_dl_sub, sub); + textOrHide(_dl_err, err); } void UITask::mapDownloadStart() { @@ -1069,9 +1024,11 @@ void UITask::mapDownloadTick() { if (was_active && !dl.active()) { // just finished if (st == mapview::TileDownloader::DONE) mapview::s_areas.setFlag(mapview::s_areas.find(dl.area()), mapview::AreaStore::F_COMPLETE, true); - char t[64]; - snprintf(t, sizeof(t), "Map: %s, %lu new tiles", dl.message(), (unsigned long)dl.downloaded()); - showToast(t, 4000); + if (st != mapview::TileDownloader::CANCELLED && !_dl_overlay) { // Stop was theirs; the popup says it itself + char t[64]; + snprintf(t, sizeof(t), "Map download: %s, %lu new tiles", dl.message(), (unsigned long)dl.downloaded()); + showToast(t, 4000); + } mapview::s_cache.forgetMissing(); // tiles that were missing may be there now mapview::s_available = mapview::s_provider->available(); if (_screen == SCR_MAP) layoutMap(); @@ -1086,12 +1043,12 @@ void UITask::mapDownloadTick() { setText(_map_dl_pill, LV_SYMBOL_WIFI " Connecting..."); lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN); } else if (dl.active()) { - if (dl.failed()) - setTextFmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu " LV_SYMBOL_WARNING " %lu", - (unsigned long)dl.processed(), (unsigned long)dl.total(), (unsigned long)dl.failed()); + if (dl.retryRound()) // the count stands still: the tiles still missing instead + setTextFmt(_map_dl_pill, LV_SYMBOL_REFRESH " %lu missing", (unsigned long)dl.failed()); + else if (dl.failed()) + setTextFmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %d%% " LV_SYMBOL_WARNING " %lu", dlPercent(dl), (unsigned long)dl.failed()); else - setTextFmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu", (unsigned long)dl.processed(), - (unsigned long)dl.total()); + setTextFmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %d%%", dlPercent(dl)); lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN); } else if (dl.liveConnecting()) { setText(_map_dl_pill, LV_SYMBOL_WIFI " Connecting..."); diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index efe59f2f..a3720a14 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -2311,7 +2311,7 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) { _node_info = _node_ping = _node_delete_lbl = nullptr; _scan_overlay = _scan_list = _scan_status = nullptr; // the popup went with the old screen _map_area = _map_marks = _map_me = _map_zoom_lbl = _map_hint = _map_dl_pill = _map_center_btn = nullptr; - _dl_overlay = _dl_info = _dl_zoom_lbl = _dl_start_lbl = _dl_job_row = _dl_job_lbl = nullptr; + _dl_overlay = _dl_info = _dl_sub = _dl_start_lbl = _dl_job_row = _dl_job_lbl = nullptr; _nav_bar = _nav_title = _nav_info = _nav_clear = nullptr; _nav_overlay = _nav_ta = _nav_kb = _nav_del_lbl = _nav_rec = _nav_avg_pill = nullptr; _nav_trail_lbl = _nav_trail_btn = _nav_reset_lbl = _nav_share_lbl = _nav_share_btn = _nav_tb_btn = nullptr; diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index 28c9e7e3..96510479 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -102,7 +102,6 @@ public: void mapDownloadStop(); void mapDownloadResume(); void mapDownloadDiscard(); - void mapDownloadZmax(int delta); void setLiveTiles(bool on); void setTrails(bool on); void setVectorMap(bool on); @@ -567,11 +566,10 @@ private: lv_obj_t* _dl_info = nullptr; lv_obj_t* _dl_bar = nullptr; // progress while downloading lv_obj_t* _dl_err = nullptr; // failures, one line - lv_obj_t* _dl_zoom_lbl = nullptr; + lv_obj_t* _dl_sub = nullptr; lv_obj_t* _dl_start_lbl = nullptr; lv_obj_t* _dl_job_row = nullptr; // "Unfinished ... Resume" in the popup lv_obj_t* _dl_job_lbl = nullptr; - int _dl_zmax = 0; uint8_t _dl_last_state = 0; // Navigation map (SCR_MAP with _map_nav) diff --git a/examples/companion_radio/ui-lvgl/map/TileDownloader.h b/examples/companion_radio/ui-lvgl/map/TileDownloader.h index c8c4bb8e..d8a0af52 100644 --- a/examples/companion_radio/ui-lvgl/map/TileDownloader.h +++ b/examples/companion_radio/ui-lvgl/map/TileDownloader.h @@ -79,10 +79,17 @@ static void makeParents(const char* path) { } } +#ifdef ARDUINO +#define TILE_LOG(...) Serial.printf(__VA_ARGS__) +#else +#define TILE_LOG(...) printf(__VA_ARGS__) +#endif + class TileDownloader { public: enum State : uint8_t { IDLE, CONNECTING, RUNNING, DONE, FAILED, CANCELLED }; static const uint32_t MAX_TILES = 40000; // ~1 GB; beyond that, the PC tool + static const int RETRY_ROUNDS = 3; // goes at the tiles that failed, after the first pass explicit TileDownloader(const char* root) : _root(root) {} @@ -94,6 +101,7 @@ public: uint32_t processed() const { return _done + _skipped + _failed; } uint32_t downloaded() const { return _done; } uint32_t failed() const { return _failed; } + int retryRound() const { return _round; } // 0: the first pass 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. @@ -136,8 +144,8 @@ public: _tries = 0; _gap_ms = GAP_MS; _ok_run = 0; - _fl_n = 0; - _fl_i = -1; + _fl_n = _fl_i = _fl_keep = 0; + _round = 0; _refused = 0; _last_ok_ms = millis(); _placeholder_hash = 0; _placeholder_hits = 0; @@ -217,7 +225,7 @@ public: if (millis() - _last_start < 60000) return; lvport::fetchAbandon(); // hung past every timeout: give up on this tile _fetching = false; - tileFailed("Request hung (60 s)"); + tileFailed("Request hung (60 s)", false); return; } _fetching = false; @@ -231,7 +239,7 @@ public: if (!writeTrails(_z, _x, _y, data, len)) { lvport::fetchRelease(); finish(FAILED, "Can't write to the SD card"); return; } tileOk(); } - else if (isNoTilePicture(data, len)) { _skipped++; if (_fl_i >= 0) _failed--; _consec_fail = 0; _tries = 0; _last_ok_ms = millis(); } // "no tile here": nothing to keep + else if (isNoTilePicture(data, len)) { _skipped++; if (_round) _failed--; _consec_fail = 0; _tries = 0; _last_ok_ms = millis(); } // "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 tileOk(); @@ -246,41 +254,46 @@ public: lvport::fetchRelease(); if (r == -403 || r == -401) { // refused: no point asking again if (++_refused >= 10) { finish(FAILED, "Server refused (403)"); return; } - tileFailed(why); + tileFailed(why, true); return; } _refused = 0; // Anything else is taken as passing: both tile servers answer every - // tile of the world, but a busy one sends 503s (and OpenTopoMap the odd - // 404 for a tile it hasn't rendered yet), and a keep-alive connection - // drops. An HTTP error gets one more try; no answer at all (r == -1: - // the connection, TLS, a timeout) up to three, 2, 8 and 30 s apart. A - // server that says it's overloaded also gets the next requests further apart. + // tile of the world (an empty one too), but a busy one sends 503s, and + // OpenTopoMap a 404 for a tile it couldn't render in time. A tile the + // server answered so goes straight to the retry rounds at the end -- + // asked again seconds later, it fails again -- and the job moves on; a + // server that says it's overloaded also gets the next requests further + // apart. No answer at all (r == -1: the connection, TLS, a timeout) is + // more likely the network: up to three tries, 2, 8 and 30 s apart. if (r == -503 || r == -429 || r == -502 || r == -504) { _gap_ms = _gap_ms * 2 < 500 ? 500 : _gap_ms * 2 > GAP_MAX_MS ? GAP_MAX_MS : _gap_ms * 2; _ok_run = 0; } - if (_tries < (r == -1 ? RETRIES : 1)) { + if (r == -1 && _tries < RETRIES) { static const uint8_t WAIT_S[RETRIES] = { 2, 8, 30 }; _last_start = millis() + WAIT_S[_tries] * 1000u; _tries++; snprintf(_msg, sizeof(_msg), "%s - retrying", why); return; } - tileFailed(why); + tileFailed(why, r != -1); return; } // Skip tiles already on the card (a few stat()s per pass), then start the next GET. for (int i = 0; i < 16 && _state == RUNNING; i++) { if (_z > _area.zmax) { - // The tiles that failed get a second go once the server has had a - // break; after that, the same download again fetches what's still - // missing (tiles on the card are skipped). - if (_fl_i < 0 && _fl_n > 0) { - _fl_i = 0; - _last_start = millis() + RETRY_PASS_WAIT_MS; - snprintf(_msg, sizeof(_msg), "Retrying %u missing tiles", (unsigned)_fl_n); + // The tiles that failed get up to RETRY_ROUNDS more goes, each after + // a longer break for the server; after that, the same download again + // fetches what's still missing (tiles on the card are skipped). + if (_round > 0) _fl_n = _fl_keep; // a round over: what failed again is left + if (_fl_n > 0 && _round < RETRY_ROUNDS) { + _fl_i = _fl_keep = 0; + _consec_fail = 0; + _last_start = millis() + RETRY_WAIT_S[_round] * 1000u; + _round++; + snprintf(_msg, sizeof(_msg), "Retrying %u missing tiles (%d of %d)", (unsigned)_fl_n, _round, RETRY_ROUNDS); nextRetry(); return; } @@ -314,11 +327,11 @@ private: bool _trails = false; // this job fetches the trails overlay too bool _force = false; // refetches tiles already on the card uint8_t _layer = 0; // of the current tile: 0 the base map, 1 its trails - // A tile that doesn't come is asked for again (RETRIES, spaced out), then - // noted in _fl for a second pass at the end of the job. + // A tile that doesn't come is asked for again (RETRIES, spaced out, when + // there was no answer), then noted in _fl for the retry rounds at the end. static const uint8_t RETRIES = 3; static const uint16_t GAP_MS = 150, GAP_MAX_MS = 2000; // between requests; wider while the server is busy - static const uint32_t RETRY_PASS_WAIT_MS = 20000; + static constexpr uint8_t RETRY_WAIT_S[RETRY_ROUNDS] = { 20, 60, 120 }; // before each round static const int FAIL_LIST = 512; struct FailedTile { int32_t x, y; int16_t z; uint8_t layer; }; uint8_t _tries = 0; // retries of the current tile so far @@ -328,7 +341,9 @@ private: uint32_t _last_ok_ms = 0; // the last tile that came static const uint32_t GIVE_UP_MS = 10 * 60000; // nothing at all for this long: stop (resumable) FailedTile* _fl = nullptr; // (PSRAM, on the first failure) - int _fl_n = 0, _fl_i = -1; // _fl_i >= 0: the second pass, the next to retry + int _fl_n = 0, _fl_i = 0; // in a retry round: the next to retry + int _fl_keep = 0; // ... and those of it that failed again, moved to the front + int _round = 0; // 0: the first pass, then the retry round uint32_t _last_start = 0, _connect_started = 0; uint32_t _placeholder_hash = 0; uint8_t _placeholder_hits = 0; @@ -473,26 +488,31 @@ private: void tileOk() { _done++; - if (_fl_i >= 0) _failed--; // a second-pass tile made it after all + if (_round) _failed--; // a retried tile made it after all _consec_fail = 0; _tries = 0; _last_ok_ms = millis(); - if (_gap_ms > GAP_MS && ++_ok_run >= 20) { _ok_run = 0; _gap_ms = _gap_ms / 2 < GAP_MS ? GAP_MS : _gap_ms / 2; } + if (_gap_ms > GAP_MS && ++_ok_run >= 8) { _ok_run = 0; _gap_ms = _gap_ms / 2 < GAP_MS ? GAP_MS : _gap_ms / 2; } } - // Given up on this tile (for now): noted for the second pass, and the job - // moves on. Tile after tile failing is a busy server or a lost network: the - // next one waits 5 s, 10 s ... up to a minute, and the job stops (to be - // resumed) only after GIVE_UP_MS without a single tile. - void tileFailed(const char* m) { + // Given up on this tile (for now): noted for the retry rounds, and the job + // moves on. Tile after tile with no answer is a lost network: the next one + // waits 5 s, 10 s ... up to a minute. A server that answers (`answered`, + // with an error) is up, and only spaces the requests out (loop()). The job + // stops (to be resumed) after GIVE_UP_MS without a single tile. + void tileFailed(const char* m, bool answered) { snprintf(_msg, sizeof(_msg), "%s", m); _tries = 0; - if (_fl_i < 0) { + TILE_LOG("map: %d/%d/%d%s failed (round %d): %s\n", _z, _x, _y, _layer ? " trails" : "", _round, m); + if (_round) { + _fl[_fl_keep++] = { _x, _y, (int16_t)_z, _layer }; // left for the next round + } else { _failed++; if (!_fl) _fl = psramBuf(FAIL_LIST); if (_fl && _fl_n < FAIL_LIST) _fl[_fl_n++] = { _x, _y, (int16_t)_z, _layer }; } if (millis() - _last_ok_ms > GIVE_UP_MS) { finish(FAILED, "Server not answering - resume later"); return; } + if (answered) { _consec_fail = 0; advance(); return; } if (_consec_fail < 255) _consec_fail++; if (_consec_fail >= 3) { uint32_t wait_s = 5u << (_consec_fail - 3 < 4 ? _consec_fail - 3 : 4); @@ -504,7 +524,7 @@ private: advance(); } - // Second pass: the next failed tile, or past the last zoom when none is left. + // A retry round: the next failed tile, or past the last zoom when none is left. void nextRetry() { if (_fl_i >= _fl_n) { _z = _area.zmax + 1; return; } const FailedTile& t = _fl[_fl_i++]; @@ -513,7 +533,7 @@ private: void advance() { _tries = 0; - if (_fl_i >= 0) { nextRetry(); return; } + if (_round) { nextRetry(); return; } if (_layer == 0 && _trails && _z <= TRAILS_MAX_Z) { _layer = 1; return; } // this tile's trails next _layer = 0; if (++_y <= _y1) return;