From 1256249c94016b450202cfe3e7894323e8b0cf42 Mon Sep 17 00:00:00 2001 From: Cellie Date: Sat, 13 Sep 2025 15:11:52 +0200 Subject: [PATCH 01/11] Remove `RenderMode` and use a timeout --- src/OpenStreetMap-esp32.cpp | 4 ++-- src/OpenStreetMap-esp32.hpp | 7 +++++-- src/ReusableTileFetcher.cpp | 10 +++++----- src/ReusableTileFetcher.hpp | 4 ++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/OpenStreetMap-esp32.cpp b/src/OpenStreetMap-esp32.cpp index 0594164..721d5cf 100644 --- a/src/OpenStreetMap-esp32.cpp +++ b/src/OpenStreetMap-esp32.cpp @@ -351,7 +351,7 @@ void OpenStreetMap::PNGDraw(PNGDRAW *pDraw) getPNGCurrentCore()->getLineAsRGB565(pDraw, destRow, PNG_RGB565_BIG_ENDIAN, 0xffffffff); } -bool OpenStreetMap::fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result) +bool OpenStreetMap::fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result, unsigned long timeout) { String url = currentProvider->urlTemplate; url.replace("{x}", String(x)); @@ -360,7 +360,7 @@ bool OpenStreetMap::fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, ui if (currentProvider->requiresApiKey && strstr(url.c_str(), "{apiKey}")) url.replace("{apiKey}", currentProvider->apiKey); - MemoryBuffer buffer = fetcher.fetchToBuffer(url, result, renderMode); + MemoryBuffer buffer = fetcher.fetchToBuffer(url, result, renderMode, timeout); if (!buffer.isAllocated()) return false; diff --git a/src/OpenStreetMap-esp32.hpp b/src/OpenStreetMap-esp32.hpp index 027bc87..6241e92 100644 --- a/src/OpenStreetMap-esp32.hpp +++ b/src/OpenStreetMap-esp32.hpp @@ -38,7 +38,7 @@ #include "MemoryBuffer.hpp" #include "ReusableTileFetcher.hpp" #include "fonts/DejaVu9-modded.h" -#include "RenderMode.hpp" +//#include "RenderMode.hpp" constexpr uint16_t OSM_BGCOLOR = lgfx::color565(32, 32, 128); constexpr uint16_t OSM_TILE_TIMEOUT_MS = 1000; @@ -99,6 +99,7 @@ class OpenStreetMap const char *getProviderName() { return currentProvider->name; }; int getMinZoom() const { return currentProvider->minZoom; }; int getMaxZoom() const { return currentProvider->maxZoom; }; + unsigned long timeout() { return mapTimeout; }; private: double lon2tile(double lon, uint8_t zoom); @@ -110,7 +111,7 @@ class OpenStreetMap void runJobs(const std::vector &jobs); CachedTile *findUnusedTile(const tileList &requiredTiles, uint8_t zoom); CachedTile *isTileCached(uint32_t x, uint32_t y, uint8_t z); - bool fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result); + bool fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result, unsigned long timeout = 0); bool composeMap(LGFX_Sprite &mapSprite, TileBufferList &tilePointers); static void tileFetcherTask(void *param); static void PNGDraw(PNGDRAW *pDraw); @@ -127,6 +128,8 @@ class OpenStreetMap std::atomic pendingJobs = 0; bool tasksStarted = false; + unsigned long mapTimeout = 0; // 0 means no timeout + uint16_t mapWidth = 320; uint16_t mapHeight = 240; diff --git a/src/ReusableTileFetcher.cpp b/src/ReusableTileFetcher.cpp index 902ebcd..0393d69 100644 --- a/src/ReusableTileFetcher.cpp +++ b/src/ReusableTileFetcher.cpp @@ -42,7 +42,7 @@ void ReusableTileFetcher::disconnect() currentPort = 80; } -MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &result, RenderMode mode) +MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &result, RenderMode mode, unsigned long timeout) { renderMode = mode; String host, path; @@ -53,7 +53,7 @@ MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &resul return MemoryBuffer::empty(); } - if (!ensureConnection(host, port, result)) + if (!ensureConnection(host, port, timeout, result)) return MemoryBuffer::empty(); sendHttpRequest(host, path); @@ -93,13 +93,13 @@ bool ReusableTileFetcher::parseUrl(const String &url, String &host, String &path return true; } -bool ReusableTileFetcher::ensureConnection(const String &host, uint16_t port, String &result) +bool ReusableTileFetcher::ensureConnection(const String &host, uint16_t port, unsigned long timeout, String &result) { if (!client.connected() || host != currentHost || port != currentPort) { disconnect(); - client.setConnectionTimeout(renderMode == RenderMode::FAST ? 100 : 5000); - if (!client.connect(host.c_str(), port, renderMode == RenderMode::FAST ? 100 : 5000)) + client.setConnectionTimeout(timeout ? 100 : 5000); + if (!client.connect(host.c_str(), port, timeout ? 100 : 5000)) { result = "Connection failed to " + host; return false; diff --git a/src/ReusableTileFetcher.hpp b/src/ReusableTileFetcher.hpp index 37b3716..f6e1193 100644 --- a/src/ReusableTileFetcher.hpp +++ b/src/ReusableTileFetcher.hpp @@ -39,7 +39,7 @@ class ReusableTileFetcher ReusableTileFetcher(const ReusableTileFetcher &) = delete; ReusableTileFetcher &operator=(const ReusableTileFetcher &) = delete; - MemoryBuffer fetchToBuffer(const String &url, String &result, RenderMode mode); + MemoryBuffer fetchToBuffer(const String &url, String &result, RenderMode mode, unsigned long timeout); void disconnect(); private: @@ -49,7 +49,7 @@ class ReusableTileFetcher RenderMode renderMode; bool parseUrl(const String &url, String &host, String &path, uint16_t &port); - bool ensureConnection(const String &host, uint16_t port, String &result); + bool ensureConnection(const String &host, uint16_t port, unsigned long timeout, String &result); void sendHttpRequest(const String &host, const String &path); bool readHttpHeaders(size_t &contentLength, String &result); bool readBody(MemoryBuffer &buffer, size_t contentLength, String &result); From f7595f0f197bd1bf691fc6a62aaa19849fa30ddf Mon Sep 17 00:00:00 2001 From: Cellie Date: Sat, 13 Sep 2025 15:21:33 +0200 Subject: [PATCH 02/11] Fix compile errors --- src/OpenStreetMap-esp32.cpp | 7 +------ src/OpenStreetMap-esp32.hpp | 2 -- src/ReusableTileFetcher.cpp | 17 ++++++++--------- src/ReusableTileFetcher.hpp | 8 +++----- 4 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/OpenStreetMap-esp32.cpp b/src/OpenStreetMap-esp32.cpp index 721d5cf..710974a 100644 --- a/src/OpenStreetMap-esp32.cpp +++ b/src/OpenStreetMap-esp32.cpp @@ -360,7 +360,7 @@ bool OpenStreetMap::fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, ui if (currentProvider->requiresApiKey && strstr(url.c_str(), "{apiKey}")) url.replace("{apiKey}", currentProvider->apiKey); - MemoryBuffer buffer = fetcher.fetchToBuffer(url, result, renderMode, timeout); + MemoryBuffer buffer = fetcher.fetchToBuffer(url, result, timeout); if (!buffer.isAllocated()) return false; @@ -495,8 +495,3 @@ bool OpenStreetMap::setTileProvider(int index) log_i("provider changed to '%s'", currentProvider->name); return true; } - -void OpenStreetMap::setRenderMode(RenderMode mode) -{ - renderMode = mode; -} diff --git a/src/OpenStreetMap-esp32.hpp b/src/OpenStreetMap-esp32.hpp index 6241e92..9673b28 100644 --- a/src/OpenStreetMap-esp32.hpp +++ b/src/OpenStreetMap-esp32.hpp @@ -94,7 +94,6 @@ class OpenStreetMap bool fetchMap(LGFX_Sprite &sprite, double longitude, double latitude, uint8_t zoom); inline void freeTilesCache(); - void setRenderMode(RenderMode mode); bool setTileProvider(int index); const char *getProviderName() { return currentProvider->name; }; int getMinZoom() const { return currentProvider->minZoom; }; @@ -118,7 +117,6 @@ class OpenStreetMap static inline thread_local OpenStreetMap *currentInstance = nullptr; static inline thread_local uint16_t *currentTileBuffer = nullptr; - RenderMode renderMode = RenderMode::ACCURATE; const TileProvider *currentProvider = &tileProviders[0]; std::vector tilesCache; diff --git a/src/ReusableTileFetcher.cpp b/src/ReusableTileFetcher.cpp index 0393d69..d783e7d 100644 --- a/src/ReusableTileFetcher.cpp +++ b/src/ReusableTileFetcher.cpp @@ -23,7 +23,7 @@ #include "ReusableTileFetcher.hpp" -ReusableTileFetcher::ReusableTileFetcher() { renderMode = RenderMode::ACCURATE; } +ReusableTileFetcher::ReusableTileFetcher() {} ReusableTileFetcher::~ReusableTileFetcher() { disconnect(); } void ReusableTileFetcher::sendHttpRequest(const String &host, const String &path) @@ -42,9 +42,8 @@ void ReusableTileFetcher::disconnect() currentPort = 80; } -MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &result, RenderMode mode, unsigned long timeout) +MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &result, unsigned long timeout) { - renderMode = mode; String host, path; uint16_t port; if (!parseUrl(url, host, path, port)) @@ -58,7 +57,7 @@ MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &resul sendHttpRequest(host, path); size_t contentLength = 0; - if (!readHttpHeaders(contentLength, result)) + if (!readHttpHeaders(contentLength, timeout, result)) return MemoryBuffer::empty(); auto buffer = MemoryBuffer(contentLength); @@ -68,7 +67,7 @@ MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &resul return MemoryBuffer::empty(); } - if (!readBody(buffer, contentLength, result)) + if (!readBody(buffer, contentLength, timeout, result)) return MemoryBuffer::empty(); return buffer; @@ -111,7 +110,7 @@ bool ReusableTileFetcher::ensureConnection(const String &host, uint16_t port, un return true; } -bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, String &result) +bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, unsigned long timeout, String &result) { String line; line.reserve(OSM_MAX_HEADERLENGTH); @@ -119,7 +118,7 @@ bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, String &result) bool start = true; while (client.connected()) { - if (!readLineWithTimeout(line, renderMode == RenderMode::FAST ? 300 : 5000)) + if (!readLineWithTimeout(line, timeout ? 300 : 5000)) { result = "Header timeout"; disconnect(); @@ -159,12 +158,12 @@ bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, String &result) return true; } -bool ReusableTileFetcher::readBody(MemoryBuffer &buffer, size_t contentLength, String &result) +bool ReusableTileFetcher::readBody(MemoryBuffer &buffer, size_t contentLength, unsigned long timeout, String &result) { uint8_t *dest = buffer.get(); size_t readSize = 0; unsigned long lastReadTime = millis(); - const unsigned long timeoutMs = (renderMode == RenderMode::FAST) ? 300 : 5000; + const unsigned long timeoutMs = timeout ? 300 : 5000; while (readSize < contentLength) { diff --git a/src/ReusableTileFetcher.hpp b/src/ReusableTileFetcher.hpp index f6e1193..99b8450 100644 --- a/src/ReusableTileFetcher.hpp +++ b/src/ReusableTileFetcher.hpp @@ -26,7 +26,6 @@ #include #include #include "MemoryBuffer.hpp" -#include "RenderMode.hpp" constexpr int OSM_MAX_HEADERLENGTH = 256; @@ -39,19 +38,18 @@ class ReusableTileFetcher ReusableTileFetcher(const ReusableTileFetcher &) = delete; ReusableTileFetcher &operator=(const ReusableTileFetcher &) = delete; - MemoryBuffer fetchToBuffer(const String &url, String &result, RenderMode mode, unsigned long timeout); + MemoryBuffer fetchToBuffer(const String &url, String &result, unsigned long timeout); void disconnect(); private: WiFiClient client; String currentHost; uint16_t currentPort = 80; - RenderMode renderMode; bool parseUrl(const String &url, String &host, String &path, uint16_t &port); bool ensureConnection(const String &host, uint16_t port, unsigned long timeout, String &result); void sendHttpRequest(const String &host, const String &path); - bool readHttpHeaders(size_t &contentLength, String &result); - bool readBody(MemoryBuffer &buffer, size_t contentLength, String &result); + bool readHttpHeaders(size_t &contentLength, unsigned long timeout, String &result); + bool readBody(MemoryBuffer &buffer, size_t contentLength, unsigned long timeout, String &result); bool readLineWithTimeout(String &line, uint32_t timeoutMs); }; From 247705ff4bb33936e2026d22cfbe3195ca6be1b8 Mon Sep 17 00:00:00 2001 From: Cellie Date: Sat, 13 Sep 2025 15:53:10 +0200 Subject: [PATCH 03/11] Skip timed out tiles --- src/OpenStreetMap-esp32.cpp | 18 ++++++++++++++++-- src/OpenStreetMap-esp32.hpp | 6 +++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/OpenStreetMap-esp32.cpp b/src/OpenStreetMap-esp32.cpp index 710974a..f47c5e5 100644 --- a/src/OpenStreetMap-esp32.cpp +++ b/src/OpenStreetMap-esp32.cpp @@ -253,6 +253,7 @@ void OpenStreetMap::runJobs(const std::vector &jobs) log_d("submitting %i jobs", (int)jobs.size()); pendingJobs.store(jobs.size()); + startJobsMS = millis(); for (const TileJob &job : jobs) if (xQueueSend(jobQueue, &job, portMAX_DELAY) != pdPASS) { @@ -298,7 +299,7 @@ bool OpenStreetMap::composeMap(LGFX_Sprite &mapSprite, TileBufferList &tilePoint return true; } -bool OpenStreetMap::fetchMap(LGFX_Sprite &mapSprite, double longitude, double latitude, uint8_t zoom) +bool OpenStreetMap::fetchMap(LGFX_Sprite &mapSprite, double longitude, double latitude, uint8_t zoom, unsigned long timeoutMS) { if (!tasksStarted && !startTileWorkerTasks()) { @@ -335,6 +336,7 @@ bool OpenStreetMap::fetchMap(LGFX_Sprite &mapSprite, double longitude, double la return false; } + mapTimeoutMS = timeoutMS; TileBufferList tilePointers; updateCache(requiredTiles, zoom, tilePointers); if (!composeMap(mapSprite, tilePointers)) @@ -406,8 +408,20 @@ void OpenStreetMap::tileFetcherTask(void *param) if (job.z == 255) break; + const uint32_t elapsedMS = millis() - osm->startJobsMS; + if (osm->mapTimeoutMS && elapsedMS >= osm->mapTimeoutMS) + { + log_w("timeout reached, ignoring queued item"); + const size_t tileByteCount = osm->currentProvider->tileSize * osm->currentProvider->tileSize * 2; + memset(job.tile->buffer, 0, tileByteCount); + job.tile->valid = false; + job.tile->busy = false; + --osm->pendingJobs; + continue; + } + String result; - if (!osm->fetchTile(fetcher, *job.tile, job.x, job.y, job.z, result)) + if (!osm->fetchTile(fetcher, *job.tile, job.x, job.y, job.z, result, osm->mapTimeoutMS)) { log_e("Tile fetch failed: %s", result.c_str()); job.tile->valid = false; diff --git a/src/OpenStreetMap-esp32.hpp b/src/OpenStreetMap-esp32.hpp index 9673b28..87921e2 100644 --- a/src/OpenStreetMap-esp32.hpp +++ b/src/OpenStreetMap-esp32.hpp @@ -91,14 +91,13 @@ class OpenStreetMap void setSize(uint16_t w, uint16_t h); uint16_t tilesNeeded(uint16_t mapWidth, uint16_t mapHeight); bool resizeTilesCache(uint16_t numberOfTiles); - bool fetchMap(LGFX_Sprite &sprite, double longitude, double latitude, uint8_t zoom); + bool fetchMap(LGFX_Sprite &sprite, double longitude, double latitude, uint8_t zoom, unsigned long timeoutMS = 0); inline void freeTilesCache(); bool setTileProvider(int index); const char *getProviderName() { return currentProvider->name; }; int getMinZoom() const { return currentProvider->minZoom; }; int getMaxZoom() const { return currentProvider->maxZoom; }; - unsigned long timeout() { return mapTimeout; }; private: double lon2tile(double lon, uint8_t zoom); @@ -126,7 +125,8 @@ class OpenStreetMap std::atomic pendingJobs = 0; bool tasksStarted = false; - unsigned long mapTimeout = 0; // 0 means no timeout + unsigned long mapTimeoutMS = 0; // 0 means no timeout + unsigned long startJobsMS = 0; uint16_t mapWidth = 320; uint16_t mapHeight = 240; From 464bdc9acff5013f78eec7a8783f2fb8be856ce1 Mon Sep 17 00:00:00 2001 From: Cellie Date: Sat, 13 Sep 2025 17:10:27 +0200 Subject: [PATCH 04/11] Use remaining time budget --- src/OpenStreetMap-esp32.cpp | 39 +++++++++++++++++++++++++++---------- src/OpenStreetMap-esp32.hpp | 3 ++- src/ReusableTileFetcher.cpp | 37 ++++++++++++++++++++++------------- 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/src/OpenStreetMap-esp32.cpp b/src/OpenStreetMap-esp32.cpp index f47c5e5..9842ef0 100644 --- a/src/OpenStreetMap-esp32.cpp +++ b/src/OpenStreetMap-esp32.cpp @@ -411,27 +411,34 @@ void OpenStreetMap::tileFetcherTask(void *param) const uint32_t elapsedMS = millis() - osm->startJobsMS; if (osm->mapTimeoutMS && elapsedMS >= osm->mapTimeoutMS) { - log_w("timeout reached, ignoring queued item"); - const size_t tileByteCount = osm->currentProvider->tileSize * osm->currentProvider->tileSize * 2; - memset(job.tile->buffer, 0, tileByteCount); - job.tile->valid = false; - job.tile->busy = false; + log_w("Map timeout (%lu ms) exceeded after %lu ms, dropping job", + osm->mapTimeoutMS, elapsedMS); + + osm->invalidateTile(job.tile); --osm->pendingJobs; continue; } + // compute remaining time budget for this job + uint32_t remainingMS = 0; + if (osm->mapTimeoutMS > 0) + { + remainingMS = osm->mapTimeoutMS - elapsedMS; + if (remainingMS == 0) + remainingMS = 1; // minimum non-zero + } + String result; - if (!osm->fetchTile(fetcher, *job.tile, job.x, job.y, job.z, result, osm->mapTimeoutMS)) + if (!osm->fetchTile(fetcher, *job.tile, job.x, job.y, job.z, result, remainingMS)) { log_e("Tile fetch failed: %s", result.c_str()); - job.tile->valid = false; - const size_t tileByteCount = osm->currentProvider->tileSize * osm->currentProvider->tileSize * 2; - memset(job.tile->buffer, 0, tileByteCount); + osm->invalidateTile(job.tile); } else { job.tile->valid = true; - log_d("core %i fetched tile z=%u x=%lu, y=%lu in %lu ms", xPortGetCoreID(), job.z, job.x, job.y, millis() - startMS); + log_d("core %i fetched tile z=%u x=%lu, y=%lu in %lu ms", + xPortGetCoreID(), job.z, job.x, job.y, millis() - startMS); } job.tile->busy = false; --osm->pendingJobs; @@ -509,3 +516,15 @@ bool OpenStreetMap::setTileProvider(int index) log_i("provider changed to '%s'", currentProvider->name); return true; } + +void OpenStreetMap::invalidateTile(CachedTile *tile) +{ + if (!tile) + return; + + const size_t tileByteCount = currentProvider->tileSize * currentProvider->tileSize * 2; + memset(tile->buffer, 0, tileByteCount); + + tile->valid = false; + tile->busy = false; +} \ No newline at end of file diff --git a/src/OpenStreetMap-esp32.hpp b/src/OpenStreetMap-esp32.hpp index 87921e2..9c34eb3 100644 --- a/src/OpenStreetMap-esp32.hpp +++ b/src/OpenStreetMap-esp32.hpp @@ -109,10 +109,11 @@ class OpenStreetMap void runJobs(const std::vector &jobs); CachedTile *findUnusedTile(const tileList &requiredTiles, uint8_t zoom); CachedTile *isTileCached(uint32_t x, uint32_t y, uint8_t z); - bool fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result, unsigned long timeout = 0); + bool fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result, unsigned long timeoutMS); bool composeMap(LGFX_Sprite &mapSprite, TileBufferList &tilePointers); static void tileFetcherTask(void *param); static void PNGDraw(PNGDRAW *pDraw); + void invalidateTile(CachedTile *tile); static inline thread_local OpenStreetMap *currentInstance = nullptr; static inline thread_local uint16_t *currentTileBuffer = nullptr; diff --git a/src/ReusableTileFetcher.cpp b/src/ReusableTileFetcher.cpp index d783e7d..b47b986 100644 --- a/src/ReusableTileFetcher.cpp +++ b/src/ReusableTileFetcher.cpp @@ -92,33 +92,39 @@ bool ReusableTileFetcher::parseUrl(const String &url, String &host, String &path return true; } -bool ReusableTileFetcher::ensureConnection(const String &host, uint16_t port, unsigned long timeout, String &result) +bool ReusableTileFetcher::ensureConnection(const String &host, uint16_t port, unsigned long timeoutMS, String &result) { if (!client.connected() || host != currentHost || port != currentPort) { disconnect(); - client.setConnectionTimeout(timeout ? 100 : 5000); - if (!client.connect(host.c_str(), port, timeout ? 100 : 5000)) + + // If caller didn’t set a timeout, fall back to 5000ms + uint32_t connectTimeout = timeoutMS > 0 ? timeoutMS : 5000UL; + if (!client.connect(host.c_str(), port, connectTimeout)) { result = "Connection failed to " + host; return false; } currentHost = host; currentPort = port; - log_i("(Re)connected on core %i", xPortGetCoreID()); + log_i("(Re)connected on core %i (timeout=%lu ms)", xPortGetCoreID(), connectTimeout); } return true; } -bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, unsigned long timeout, String &result) +bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, unsigned long timeoutMS, String &result) { String line; line.reserve(OSM_MAX_HEADERLENGTH); contentLength = 0; bool start = true; + + // Fall back to 5000ms if no timeout is set + uint32_t headerTimeout = timeoutMS > 0 ? timeoutMS : 5000UL; + while (client.connected()) { - if (!readLineWithTimeout(line, timeout ? 300 : 5000)) + if (!readLineWithTimeout(line, headerTimeout)) { result = "Header timeout"; disconnect(); @@ -128,7 +134,7 @@ bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, unsigned long t line.trim(); if (start) { - if (!line.startsWith("HTTP/1.1")) + if (!line.startsWith("HTTP/1.")) { result = "Bad HTTP response: " + line; disconnect(); @@ -150,29 +156,30 @@ bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, unsigned long t if (contentLength == 0) { - result = "Missing or invalid Content-Length"; - disconnect(); - return false; + // treat as valid but empty buffer + log_w("Content-Length was 0"); } return true; } -bool ReusableTileFetcher::readBody(MemoryBuffer &buffer, size_t contentLength, unsigned long timeout, String &result) +bool ReusableTileFetcher::readBody(MemoryBuffer &buffer, size_t contentLength, unsigned long timeoutMS, String &result) { uint8_t *dest = buffer.get(); size_t readSize = 0; unsigned long lastReadTime = millis(); - const unsigned long timeoutMs = timeout ? 300 : 5000; + + // Respect caller’s remaining budget, default to 5000ms if none + const unsigned long maxStall = timeoutMS > 0 ? timeoutMS : 5000UL; while (readSize < contentLength) { size_t availableData = client.available(); if (availableData == 0) { - if (millis() - lastReadTime >= timeoutMs) + if (millis() - lastReadTime >= maxStall) { - result = "Timeout: " + String(timeoutMs) + " ms"; + result = "Body read stalled for " + String(maxStall) + " ms"; disconnect(); return false; } @@ -190,7 +197,9 @@ bool ReusableTileFetcher::readBody(MemoryBuffer &buffer, size_t contentLength, u lastReadTime = millis(); } else + { taskYIELD(); + } } return true; } From 4571127dda8062a599c0dc00be3a105e1f64b097 Mon Sep 17 00:00:00 2001 From: Cellie Date: Sat, 13 Sep 2025 21:42:16 +0200 Subject: [PATCH 05/11] Cleanup --- src/OpenStreetMap-esp32.cpp | 2 +- src/OpenStreetMap-esp32.hpp | 1 - src/RenderMode.hpp | 7 ------- src/ReusableTileFetcher.cpp | 2 +- 4 files changed, 2 insertions(+), 10 deletions(-) delete mode 100644 src/RenderMode.hpp diff --git a/src/OpenStreetMap-esp32.cpp b/src/OpenStreetMap-esp32.cpp index 9842ef0..b4395c2 100644 --- a/src/OpenStreetMap-esp32.cpp +++ b/src/OpenStreetMap-esp32.cpp @@ -527,4 +527,4 @@ void OpenStreetMap::invalidateTile(CachedTile *tile) tile->valid = false; tile->busy = false; -} \ No newline at end of file +} diff --git a/src/OpenStreetMap-esp32.hpp b/src/OpenStreetMap-esp32.hpp index 9c34eb3..c68f97f 100644 --- a/src/OpenStreetMap-esp32.hpp +++ b/src/OpenStreetMap-esp32.hpp @@ -38,7 +38,6 @@ #include "MemoryBuffer.hpp" #include "ReusableTileFetcher.hpp" #include "fonts/DejaVu9-modded.h" -//#include "RenderMode.hpp" constexpr uint16_t OSM_BGCOLOR = lgfx::color565(32, 32, 128); constexpr uint16_t OSM_TILE_TIMEOUT_MS = 1000; diff --git a/src/RenderMode.hpp b/src/RenderMode.hpp deleted file mode 100644 index 50d0960..0000000 --- a/src/RenderMode.hpp +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -enum class RenderMode -{ - FAST, - ACCURATE -}; diff --git a/src/ReusableTileFetcher.cpp b/src/ReusableTileFetcher.cpp index b47b986..47fb56d 100644 --- a/src/ReusableTileFetcher.cpp +++ b/src/ReusableTileFetcher.cpp @@ -156,7 +156,7 @@ bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, unsigned long t if (contentLength == 0) { - // treat as valid but empty buffer + // treat as valid but empty buffer TODO: reason about this because I think this is plain wrong without setting up a empty tile somehow log_w("Content-Length was 0"); } From 9b622d57ee3aa49ec0d480b8fa7865b51b25de80 Mon Sep 17 00:00:00 2001 From: Cellie Date: Sun, 14 Sep 2025 14:54:30 +0200 Subject: [PATCH 06/11] Cleanup --- src/OpenStreetMap-esp32.hpp | 1 - src/ReusableTileFetcher.cpp | 20 ++++++++++---------- src/ReusableTileFetcher.hpp | 1 + 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/OpenStreetMap-esp32.hpp b/src/OpenStreetMap-esp32.hpp index c68f97f..c5cd3fb 100644 --- a/src/OpenStreetMap-esp32.hpp +++ b/src/OpenStreetMap-esp32.hpp @@ -40,7 +40,6 @@ #include "fonts/DejaVu9-modded.h" constexpr uint16_t OSM_BGCOLOR = lgfx::color565(32, 32, 128); -constexpr uint16_t OSM_TILE_TIMEOUT_MS = 1000; constexpr UBaseType_t OSM_TASK_PRIORITY = 1; constexpr uint32_t OSM_TASK_STACKSIZE = 5120; constexpr uint32_t OSM_JOB_QUEUE_SIZE = 50; diff --git a/src/ReusableTileFetcher.cpp b/src/ReusableTileFetcher.cpp index 47fb56d..b5c45f9 100644 --- a/src/ReusableTileFetcher.cpp +++ b/src/ReusableTileFetcher.cpp @@ -60,6 +60,12 @@ MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &resul if (!readHttpHeaders(contentLength, timeout, result)) return MemoryBuffer::empty(); + if (contentLength == 0) + { + result = "Empty response (Content-Length=0)"; + return MemoryBuffer::empty(); + } + auto buffer = MemoryBuffer(contentLength); if (!buffer.isAllocated()) { @@ -99,7 +105,7 @@ bool ReusableTileFetcher::ensureConnection(const String &host, uint16_t port, un disconnect(); // If caller didn’t set a timeout, fall back to 5000ms - uint32_t connectTimeout = timeoutMS > 0 ? timeoutMS : 5000UL; + uint32_t connectTimeout = timeoutMS > 0 ? timeoutMS : OSM_DEFAULT_TIMEOUT_MS; if (!client.connect(host.c_str(), port, connectTimeout)) { result = "Connection failed to " + host; @@ -119,8 +125,7 @@ bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, unsigned long t contentLength = 0; bool start = true; - // Fall back to 5000ms if no timeout is set - uint32_t headerTimeout = timeoutMS > 0 ? timeoutMS : 5000UL; + uint32_t headerTimeout = timeoutMS > 0 ? timeoutMS : OSM_DEFAULT_TIMEOUT_MS; while (client.connected()) { @@ -155,10 +160,7 @@ bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, unsigned long t } if (contentLength == 0) - { - // treat as valid but empty buffer TODO: reason about this because I think this is plain wrong without setting up a empty tile somehow - log_w("Content-Length was 0"); - } + log_w("Content-Length = 0 (valid empty body)"); return true; } @@ -170,7 +172,7 @@ bool ReusableTileFetcher::readBody(MemoryBuffer &buffer, size_t contentLength, u unsigned long lastReadTime = millis(); // Respect caller’s remaining budget, default to 5000ms if none - const unsigned long maxStall = timeoutMS > 0 ? timeoutMS : 5000UL; + const unsigned long maxStall = timeoutMS > 0 ? timeoutMS : OSM_DEFAULT_TIMEOUT_MS; while (readSize < contentLength) { @@ -197,9 +199,7 @@ bool ReusableTileFetcher::readBody(MemoryBuffer &buffer, size_t contentLength, u lastReadTime = millis(); } else - { taskYIELD(); - } } return true; } diff --git a/src/ReusableTileFetcher.hpp b/src/ReusableTileFetcher.hpp index 99b8450..768287e 100644 --- a/src/ReusableTileFetcher.hpp +++ b/src/ReusableTileFetcher.hpp @@ -28,6 +28,7 @@ #include "MemoryBuffer.hpp" constexpr int OSM_MAX_HEADERLENGTH = 256; +constexpr int OSM_DEFAULT_TIMEOUT_MS = 5000; class ReusableTileFetcher { From 66251fa9fad9eb68712cc3c13a573af8c8833f56 Mon Sep 17 00:00:00 2001 From: Cellie Date: Sun, 14 Sep 2025 15:55:57 +0200 Subject: [PATCH 07/11] Clean up variable names --- src/ReusableTileFetcher.cpp | 8 ++++---- src/ReusableTileFetcher.hpp | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ReusableTileFetcher.cpp b/src/ReusableTileFetcher.cpp index b5c45f9..58f2767 100644 --- a/src/ReusableTileFetcher.cpp +++ b/src/ReusableTileFetcher.cpp @@ -42,7 +42,7 @@ void ReusableTileFetcher::disconnect() currentPort = 80; } -MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &result, unsigned long timeout) +MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &result, unsigned long timeoutMS) { String host, path; uint16_t port; @@ -52,12 +52,12 @@ MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &resul return MemoryBuffer::empty(); } - if (!ensureConnection(host, port, timeout, result)) + if (!ensureConnection(host, port, timeoutMS, result)) return MemoryBuffer::empty(); sendHttpRequest(host, path); size_t contentLength = 0; - if (!readHttpHeaders(contentLength, timeout, result)) + if (!readHttpHeaders(contentLength, timeoutMS, result)) return MemoryBuffer::empty(); if (contentLength == 0) @@ -73,7 +73,7 @@ MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &resul return MemoryBuffer::empty(); } - if (!readBody(buffer, contentLength, timeout, result)) + if (!readBody(buffer, contentLength, timeoutMS, result)) return MemoryBuffer::empty(); return buffer; diff --git a/src/ReusableTileFetcher.hpp b/src/ReusableTileFetcher.hpp index 768287e..ec0fbcf 100644 --- a/src/ReusableTileFetcher.hpp +++ b/src/ReusableTileFetcher.hpp @@ -39,7 +39,7 @@ class ReusableTileFetcher ReusableTileFetcher(const ReusableTileFetcher &) = delete; ReusableTileFetcher &operator=(const ReusableTileFetcher &) = delete; - MemoryBuffer fetchToBuffer(const String &url, String &result, unsigned long timeout); + MemoryBuffer fetchToBuffer(const String &url, String &result, unsigned long timeoutMS); void disconnect(); private: @@ -48,9 +48,9 @@ class ReusableTileFetcher uint16_t currentPort = 80; bool parseUrl(const String &url, String &host, String &path, uint16_t &port); - bool ensureConnection(const String &host, uint16_t port, unsigned long timeout, String &result); + bool ensureConnection(const String &host, uint16_t port, unsigned long timeoutMS, String &result); void sendHttpRequest(const String &host, const String &path); - bool readHttpHeaders(size_t &contentLength, unsigned long timeout, String &result); - bool readBody(MemoryBuffer &buffer, size_t contentLength, unsigned long timeout, String &result); + bool readHttpHeaders(size_t &contentLength, unsigned long timeoutMS, String &result); + bool readBody(MemoryBuffer &buffer, size_t contentLength, unsigned long timeoutMS, String &result); bool readLineWithTimeout(String &line, uint32_t timeoutMs); }; From bf548cca0ddb889114afb0ac5af8974530666fc6 Mon Sep 17 00:00:00 2001 From: Cellie Date: Sun, 14 Sep 2025 15:56:10 +0200 Subject: [PATCH 08/11] Update README.md --- README.md | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 89c7b95..cb2e6de 100644 --- a/README.md +++ b/README.md @@ -95,12 +95,16 @@ Use the above `tilesNeeded` function to calculate a safe and sane cache size if ### Fetch a map ```c++ -bool fetchMap(LGFX_Sprite &map, double longitude, double latitude, uint8_t zoom) +bool fetchMap(LGFX_Sprite &map, double longitude, double latitude, uint8_t zoom, unsigned long timeoutMS = 0) ``` - Overflowing `longitude` are wrapped and normalized to +-180°. - Overflowing `latitude` are clamped to +-90°. - Valid range for the `zoom` level is from `getMinZoom()` to `getMaxZoom()`. +- `timeoutMS` can be used to throttle the amount of downloaded tiles per call. +Setting it to anything other than `0` sets a timeout. Sane values start around ~100ms. +**Note:** No more tiles will be downloaded after the timeout expires, but tiles that are downloading will be finished. +**Note:** You might end up with missing map tiles. Or no map at all if you set the timeout too short. ### Free the psram memory used by the tile cache @@ -133,9 +137,8 @@ const int numberOfProviders = OSM_TILEPROVIDERS; **Note:** In the default setup there is only one provider defined. -See `src/TileProvider.hpp` for example setups for [https://www.thunderforest.com/](https://www.thunderforest.com/) that only require an API key and commenting/uncommenting 2 lines. - -Registration and a hobby tier are available for free. +See `src/TileProvider.hpp` for example setups for [https://www.thunderforest.com/](https://www.thunderforest.com/) that only require you to register for a **free** API key and adjusting/uncommenting 2 lines in the config. +Register for a ThunderForest free tier [here](https://manage.thunderforest.com/users/sign_up?price=hobby-project-usd) without needing a creditcard to sign up. ### Adding tile providers @@ -151,23 +154,6 @@ If you encounter a problem or want to request support for a new provider, please char *getProviderName() ``` -### Set the render mode - -```c++ -void setRenderMode(RenderMode mode) -``` - -Available modes: - -- `RenderMode::ACCURATE` (default) -Downloads map tiles **without a timeout**, ensuring a complete map with **no missing tiles** in most cases. -Best suited for reliability and full-quality rendering. - -- `RenderMode::FAST` -Downloads map tiles **with a timeout**. -This mode can produce the map **more quickly**, but some **tiles may be missing** if a request times out. -Ideal when operating under time constraints. - ## Example code ### Example returning the default 320x240 map From 32f0a5d5c10d67c5e8847e91b039ac28bb0fd174 Mon Sep 17 00:00:00 2001 From: Cellie Date: Sun, 14 Sep 2025 16:02:05 +0200 Subject: [PATCH 09/11] Update README.md --- README.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cb2e6de..68a67fe 100644 --- a/README.md +++ b/README.md @@ -137,23 +137,19 @@ const int numberOfProviders = OSM_TILEPROVIDERS; **Note:** In the default setup there is only one provider defined. -See `src/TileProvider.hpp` for example setups for [https://www.thunderforest.com/](https://www.thunderforest.com/) that only require you to register for a **free** API key and adjusting/uncommenting 2 lines in the config. -Register for a ThunderForest free tier [here](https://manage.thunderforest.com/users/sign_up?price=hobby-project-usd) without needing a creditcard to sign up. - -### Adding tile providers - -Other providers should work if a new definition is created in `src/TileProvider.hpp`. -Check out the existing templates to see how this works. - -If you encounter a problem or want to request support for a new provider, please check the [issue tracker](../../issues) for existing reports or [open an issue](../../issues/new). - - ### Get the provider name ```c++ char *getProviderName() ``` +### Adding tile providers + +See `src/TileProvider.hpp` for example setups for [https://www.thunderforest.com/](https://www.thunderforest.com/) that only require you to register for a **free** API key and adjusting/uncommenting 2 lines in the config. +Register for a ThunderForest free tier [here](https://manage.thunderforest.com/users/sign_up?price=hobby-project-usd) without needing a creditcard to sign up. + +If you encounter a problem or want to request support for a new provider, please check the [issue tracker](../../issues) for existing reports or [open an issue](../../issues/new). + ## Example code ### Example returning the default 320x240 map From fd51777fe906339524de9f3b6553df6f0e85cdfb Mon Sep 17 00:00:00 2001 From: Cellie Date: Sun, 14 Sep 2025 16:03:56 +0200 Subject: [PATCH 10/11] Formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 68a67fe..b181c05 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ const int numberOfProviders = OSM_TILEPROVIDERS; char *getProviderName() ``` -### Adding tile providers +## Adding tile providers See `src/TileProvider.hpp` for example setups for [https://www.thunderforest.com/](https://www.thunderforest.com/) that only require you to register for a **free** API key and adjusting/uncommenting 2 lines in the config. Register for a ThunderForest free tier [here](https://manage.thunderforest.com/users/sign_up?price=hobby-project-usd) without needing a creditcard to sign up. From 9e7be419cc39056aa206350602390a7ed51e9e9e Mon Sep 17 00:00:00 2001 From: Cellie Date: Sun, 14 Sep 2025 16:22:36 +0200 Subject: [PATCH 11/11] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b181c05..e1454d7 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ void setSize(uint16_t w, uint16_t h) uint16_t tilesNeeded(uint16_t w, uint16_t h) ``` -This returns the number of tiles required to cache the given map size. +This returns the -most pessimistic- number of tiles required to cache the given map size. ### Resize the tiles cache