Skip to content

Commit 6a51eb5

Browse files
Remove RenderMode and use a timeout (#121)
1 parent 999d592 commit 6a51eb5

File tree

6 files changed

+89
-79
lines changed

6 files changed

+89
-79
lines changed

README.md

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ void setSize(uint16_t w, uint16_t h)
7575
uint16_t tilesNeeded(uint16_t w, uint16_t h)
7676
```
7777

78-
This returns the number of tiles required to cache the given map size.
78+
This returns the -most pessimistic- number of tiles required to cache the given map size.
7979

8080
### Resize the tiles cache
8181

@@ -95,12 +95,16 @@ Use the above `tilesNeeded` function to calculate a safe and sane cache size if
9595
### Fetch a map
9696
9797
```c++
98-
bool fetchMap(LGFX_Sprite &map, double longitude, double latitude, uint8_t zoom)
98+
bool fetchMap(LGFX_Sprite &map, double longitude, double latitude, uint8_t zoom, unsigned long timeoutMS = 0)
9999
```
100100

101101
- Overflowing `longitude` are wrapped and normalized to +-180°.
102102
- Overflowing `latitude` are clamped to +-90°.
103103
- Valid range for the `zoom` level is from `getMinZoom()` to `getMaxZoom()`.
104+
- `timeoutMS` can be used to throttle the amount of downloaded tiles per call.
105+
Setting it to anything other than `0` sets a timeout. Sane values start around ~100ms.
106+
**Note:** No more tiles will be downloaded after the timeout expires, but tiles that are downloading will be finished.
107+
**Note:** You might end up with missing map tiles. Or no map at all if you set the timeout too short.
104108

105109
### Free the psram memory used by the tile cache
106110

@@ -133,40 +137,18 @@ const int numberOfProviders = OSM_TILEPROVIDERS;
133137

134138
**Note:** In the default setup there is only one provider defined.
135139

136-
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.
137-
138-
Registration and a hobby tier are available for free.
139-
140-
### Adding tile providers
141-
142-
Other providers should work if a new definition is created in `src/TileProvider.hpp`.
143-
Check out the existing templates to see how this works.
144-
145-
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).
146-
147-
148140
### Get the provider name
149141

150142
```c++
151143
char *getProviderName()
152144
```
153145

154-
### Set the render mode
155-
156-
```c++
157-
void setRenderMode(RenderMode mode)
158-
```
159-
160-
Available modes:
146+
## Adding tile providers
161147

162-
- `RenderMode::ACCURATE` (default)
163-
Downloads map tiles **without a timeout**, ensuring a complete map with **no missing tiles** in most cases.
164-
Best suited for reliability and full-quality rendering.
148+
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.
149+
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.
165150

166-
- `RenderMode::FAST`
167-
Downloads map tiles **with a timeout**.
168-
This mode can produce the map **more quickly**, but some **tiles may be missing** if a request times out.
169-
Ideal when operating under time constraints.
151+
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).
170152

171153
## Example code
172154

src/OpenStreetMap-esp32.cpp

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ void OpenStreetMap::runJobs(const std::vector<TileJob> &jobs)
253253
log_d("submitting %i jobs", (int)jobs.size());
254254

255255
pendingJobs.store(jobs.size());
256+
startJobsMS = millis();
256257
for (const TileJob &job : jobs)
257258
if (xQueueSend(jobQueue, &job, portMAX_DELAY) != pdPASS)
258259
{
@@ -298,7 +299,7 @@ bool OpenStreetMap::composeMap(LGFX_Sprite &mapSprite, TileBufferList &tilePoint
298299
return true;
299300
}
300301

301-
bool OpenStreetMap::fetchMap(LGFX_Sprite &mapSprite, double longitude, double latitude, uint8_t zoom)
302+
bool OpenStreetMap::fetchMap(LGFX_Sprite &mapSprite, double longitude, double latitude, uint8_t zoom, unsigned long timeoutMS)
302303
{
303304
if (!tasksStarted && !startTileWorkerTasks())
304305
{
@@ -335,6 +336,7 @@ bool OpenStreetMap::fetchMap(LGFX_Sprite &mapSprite, double longitude, double la
335336
return false;
336337
}
337338

339+
mapTimeoutMS = timeoutMS;
338340
TileBufferList tilePointers;
339341
updateCache(requiredTiles, zoom, tilePointers);
340342
if (!composeMap(mapSprite, tilePointers))
@@ -351,7 +353,7 @@ void OpenStreetMap::PNGDraw(PNGDRAW *pDraw)
351353
getPNGCurrentCore()->getLineAsRGB565(pDraw, destRow, PNG_RGB565_BIG_ENDIAN, 0xffffffff);
352354
}
353355

354-
bool OpenStreetMap::fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result)
356+
bool OpenStreetMap::fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result, unsigned long timeout)
355357
{
356358
String url = currentProvider->urlTemplate;
357359
url.replace("{x}", String(x));
@@ -360,7 +362,7 @@ bool OpenStreetMap::fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, ui
360362
if (currentProvider->requiresApiKey && strstr(url.c_str(), "{apiKey}"))
361363
url.replace("{apiKey}", currentProvider->apiKey);
362364

363-
MemoryBuffer buffer = fetcher.fetchToBuffer(url, result, renderMode);
365+
MemoryBuffer buffer = fetcher.fetchToBuffer(url, result, timeout);
364366
if (!buffer.isAllocated())
365367
return false;
366368

@@ -406,18 +408,37 @@ void OpenStreetMap::tileFetcherTask(void *param)
406408
if (job.z == 255)
407409
break;
408410

411+
const uint32_t elapsedMS = millis() - osm->startJobsMS;
412+
if (osm->mapTimeoutMS && elapsedMS >= osm->mapTimeoutMS)
413+
{
414+
log_w("Map timeout (%lu ms) exceeded after %lu ms, dropping job",
415+
osm->mapTimeoutMS, elapsedMS);
416+
417+
osm->invalidateTile(job.tile);
418+
--osm->pendingJobs;
419+
continue;
420+
}
421+
422+
// compute remaining time budget for this job
423+
uint32_t remainingMS = 0;
424+
if (osm->mapTimeoutMS > 0)
425+
{
426+
remainingMS = osm->mapTimeoutMS - elapsedMS;
427+
if (remainingMS == 0)
428+
remainingMS = 1; // minimum non-zero
429+
}
430+
409431
String result;
410-
if (!osm->fetchTile(fetcher, *job.tile, job.x, job.y, job.z, result))
432+
if (!osm->fetchTile(fetcher, *job.tile, job.x, job.y, job.z, result, remainingMS))
411433
{
412434
log_e("Tile fetch failed: %s", result.c_str());
413-
job.tile->valid = false;
414-
const size_t tileByteCount = osm->currentProvider->tileSize * osm->currentProvider->tileSize * 2;
415-
memset(job.tile->buffer, 0, tileByteCount);
435+
osm->invalidateTile(job.tile);
416436
}
417437
else
418438
{
419439
job.tile->valid = true;
420-
log_d("core %i fetched tile z=%u x=%lu, y=%lu in %lu ms", xPortGetCoreID(), job.z, job.x, job.y, millis() - startMS);
440+
log_d("core %i fetched tile z=%u x=%lu, y=%lu in %lu ms",
441+
xPortGetCoreID(), job.z, job.x, job.y, millis() - startMS);
421442
}
422443
job.tile->busy = false;
423444
--osm->pendingJobs;
@@ -496,7 +517,14 @@ bool OpenStreetMap::setTileProvider(int index)
496517
return true;
497518
}
498519

499-
void OpenStreetMap::setRenderMode(RenderMode mode)
520+
void OpenStreetMap::invalidateTile(CachedTile *tile)
500521
{
501-
renderMode = mode;
522+
if (!tile)
523+
return;
524+
525+
const size_t tileByteCount = currentProvider->tileSize * currentProvider->tileSize * 2;
526+
memset(tile->buffer, 0, tileByteCount);
527+
528+
tile->valid = false;
529+
tile->busy = false;
502530
}

src/OpenStreetMap-esp32.hpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,8 @@
3838
#include "MemoryBuffer.hpp"
3939
#include "ReusableTileFetcher.hpp"
4040
#include "fonts/DejaVu9-modded.h"
41-
#include "RenderMode.hpp"
4241

4342
constexpr uint16_t OSM_BGCOLOR = lgfx::color565(32, 32, 128);
44-
constexpr uint16_t OSM_TILE_TIMEOUT_MS = 1000;
4543
constexpr UBaseType_t OSM_TASK_PRIORITY = 1;
4644
constexpr uint32_t OSM_TASK_STACKSIZE = 5120;
4745
constexpr uint32_t OSM_JOB_QUEUE_SIZE = 50;
@@ -91,10 +89,9 @@ class OpenStreetMap
9189
void setSize(uint16_t w, uint16_t h);
9290
uint16_t tilesNeeded(uint16_t mapWidth, uint16_t mapHeight);
9391
bool resizeTilesCache(uint16_t numberOfTiles);
94-
bool fetchMap(LGFX_Sprite &sprite, double longitude, double latitude, uint8_t zoom);
92+
bool fetchMap(LGFX_Sprite &sprite, double longitude, double latitude, uint8_t zoom, unsigned long timeoutMS = 0);
9593
inline void freeTilesCache();
9694

97-
void setRenderMode(RenderMode mode);
9895
bool setTileProvider(int index);
9996
const char *getProviderName() { return currentProvider->name; };
10097
int getMinZoom() const { return currentProvider->minZoom; };
@@ -110,14 +107,14 @@ class OpenStreetMap
110107
void runJobs(const std::vector<TileJob> &jobs);
111108
CachedTile *findUnusedTile(const tileList &requiredTiles, uint8_t zoom);
112109
CachedTile *isTileCached(uint32_t x, uint32_t y, uint8_t z);
113-
bool fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result);
110+
bool fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result, unsigned long timeoutMS);
114111
bool composeMap(LGFX_Sprite &mapSprite, TileBufferList &tilePointers);
115112
static void tileFetcherTask(void *param);
116113
static void PNGDraw(PNGDRAW *pDraw);
114+
void invalidateTile(CachedTile *tile);
117115

118116
static inline thread_local OpenStreetMap *currentInstance = nullptr;
119117
static inline thread_local uint16_t *currentTileBuffer = nullptr;
120-
RenderMode renderMode = RenderMode::ACCURATE;
121118
const TileProvider *currentProvider = &tileProviders[0];
122119
std::vector<CachedTile> tilesCache;
123120

@@ -127,6 +124,9 @@ class OpenStreetMap
127124
std::atomic<int> pendingJobs = 0;
128125
bool tasksStarted = false;
129126

127+
unsigned long mapTimeoutMS = 0; // 0 means no timeout
128+
unsigned long startJobsMS = 0;
129+
130130
uint16_t mapWidth = 320;
131131
uint16_t mapHeight = 240;
132132

src/RenderMode.hpp

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/ReusableTileFetcher.cpp

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
#include "ReusableTileFetcher.hpp"
2525

26-
ReusableTileFetcher::ReusableTileFetcher() { renderMode = RenderMode::ACCURATE; }
26+
ReusableTileFetcher::ReusableTileFetcher() {}
2727
ReusableTileFetcher::~ReusableTileFetcher() { disconnect(); }
2828

2929
void ReusableTileFetcher::sendHttpRequest(const String &host, const String &path)
@@ -42,9 +42,8 @@ void ReusableTileFetcher::disconnect()
4242
currentPort = 80;
4343
}
4444

45-
MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &result, RenderMode mode)
45+
MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &result, unsigned long timeoutMS)
4646
{
47-
renderMode = mode;
4847
String host, path;
4948
uint16_t port;
5049
if (!parseUrl(url, host, path, port))
@@ -53,22 +52,28 @@ MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &resul
5352
return MemoryBuffer::empty();
5453
}
5554

56-
if (!ensureConnection(host, port, result))
55+
if (!ensureConnection(host, port, timeoutMS, result))
5756
return MemoryBuffer::empty();
5857

5958
sendHttpRequest(host, path);
6059
size_t contentLength = 0;
61-
if (!readHttpHeaders(contentLength, result))
60+
if (!readHttpHeaders(contentLength, timeoutMS, result))
6261
return MemoryBuffer::empty();
6362

63+
if (contentLength == 0)
64+
{
65+
result = "Empty response (Content-Length=0)";
66+
return MemoryBuffer::empty();
67+
}
68+
6469
auto buffer = MemoryBuffer(contentLength);
6570
if (!buffer.isAllocated())
6671
{
6772
result = "Buffer allocation failed";
6873
return MemoryBuffer::empty();
6974
}
7075

71-
if (!readBody(buffer, contentLength, result))
76+
if (!readBody(buffer, contentLength, timeoutMS, result))
7277
return MemoryBuffer::empty();
7378

7479
return buffer;
@@ -93,33 +98,38 @@ bool ReusableTileFetcher::parseUrl(const String &url, String &host, String &path
9398
return true;
9499
}
95100

96-
bool ReusableTileFetcher::ensureConnection(const String &host, uint16_t port, String &result)
101+
bool ReusableTileFetcher::ensureConnection(const String &host, uint16_t port, unsigned long timeoutMS, String &result)
97102
{
98103
if (!client.connected() || host != currentHost || port != currentPort)
99104
{
100105
disconnect();
101-
client.setConnectionTimeout(renderMode == RenderMode::FAST ? 100 : 5000);
102-
if (!client.connect(host.c_str(), port, renderMode == RenderMode::FAST ? 100 : 5000))
106+
107+
// If caller didn’t set a timeout, fall back to 5000ms
108+
uint32_t connectTimeout = timeoutMS > 0 ? timeoutMS : OSM_DEFAULT_TIMEOUT_MS;
109+
if (!client.connect(host.c_str(), port, connectTimeout))
103110
{
104111
result = "Connection failed to " + host;
105112
return false;
106113
}
107114
currentHost = host;
108115
currentPort = port;
109-
log_i("(Re)connected on core %i", xPortGetCoreID());
116+
log_i("(Re)connected on core %i (timeout=%lu ms)", xPortGetCoreID(), connectTimeout);
110117
}
111118
return true;
112119
}
113120

114-
bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, String &result)
121+
bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, unsigned long timeoutMS, String &result)
115122
{
116123
String line;
117124
line.reserve(OSM_MAX_HEADERLENGTH);
118125
contentLength = 0;
119126
bool start = true;
127+
128+
uint32_t headerTimeout = timeoutMS > 0 ? timeoutMS : OSM_DEFAULT_TIMEOUT_MS;
129+
120130
while (client.connected())
121131
{
122-
if (!readLineWithTimeout(line, renderMode == RenderMode::FAST ? 300 : 5000))
132+
if (!readLineWithTimeout(line, headerTimeout))
123133
{
124134
result = "Header timeout";
125135
disconnect();
@@ -129,7 +139,7 @@ bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, String &result)
129139
line.trim();
130140
if (start)
131141
{
132-
if (!line.startsWith("HTTP/1.1"))
142+
if (!line.startsWith("HTTP/1."))
133143
{
134144
result = "Bad HTTP response: " + line;
135145
disconnect();
@@ -150,30 +160,28 @@ bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, String &result)
150160
}
151161

152162
if (contentLength == 0)
153-
{
154-
result = "Missing or invalid Content-Length";
155-
disconnect();
156-
return false;
157-
}
163+
log_w("Content-Length = 0 (valid empty body)");
158164

159165
return true;
160166
}
161167

162-
bool ReusableTileFetcher::readBody(MemoryBuffer &buffer, size_t contentLength, String &result)
168+
bool ReusableTileFetcher::readBody(MemoryBuffer &buffer, size_t contentLength, unsigned long timeoutMS, String &result)
163169
{
164170
uint8_t *dest = buffer.get();
165171
size_t readSize = 0;
166172
unsigned long lastReadTime = millis();
167-
const unsigned long timeoutMs = (renderMode == RenderMode::FAST) ? 300 : 5000;
173+
174+
// Respect caller’s remaining budget, default to 5000ms if none
175+
const unsigned long maxStall = timeoutMS > 0 ? timeoutMS : OSM_DEFAULT_TIMEOUT_MS;
168176

169177
while (readSize < contentLength)
170178
{
171179
size_t availableData = client.available();
172180
if (availableData == 0)
173181
{
174-
if (millis() - lastReadTime >= timeoutMs)
182+
if (millis() - lastReadTime >= maxStall)
175183
{
176-
result = "Timeout: " + String(timeoutMs) + " ms";
184+
result = "Body read stalled for " + String(maxStall) + " ms";
177185
disconnect();
178186
return false;
179187
}

0 commit comments

Comments
 (0)