mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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])
|
||||
|
||||
@@ -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...");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<FailedTile>(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;
|
||||
|
||||
Reference in New Issue
Block a user