diff --git a/README.md b/README.md index d967425..2ff3352 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,23 @@ 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 diff --git a/src/HTTPClientRAII.hpp b/src/HTTPClientRAII.hpp deleted file mode 100644 index 5f71daf..0000000 --- a/src/HTTPClientRAII.hpp +++ /dev/null @@ -1,63 +0,0 @@ -/* - Copyright (c) 2025 Cellie https://github.com/CelliesProjects/OpenStreetMap-esp32 - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - SPDX-License-Identifier: MIT - */ -#ifndef HTTPCLIENTRAII_HPP_ -#define HTTPCLIENTRAII_HPP_ - -#include -#include -#include - -class HTTPClientRAII -{ -public: - HTTPClientRAII(const HTTPClientRAII &) = delete; - HTTPClientRAII &operator=(const HTTPClientRAII &) = delete; - HTTPClientRAII(HTTPClientRAII &&) = delete; - HTTPClientRAII &operator=(HTTPClientRAII &&) = delete; - - HTTPClientRAII() noexcept : http(new HTTPClient()) {} - - ~HTTPClientRAII() noexcept - { - if (http) - http->end(); - } - - bool begin(const String &url) - { - if (!http) - return false; - http->setUserAgent("OpenStreetMap-esp32/1.0 (+https://github.com/CelliesProjects/OpenStreetMap-esp32)"); - return http->begin(url); - } - - int GET() { return http ? http->GET() : -1; } - size_t getSize() const { return http ? http->getSize() : 0; } - WiFiClient *getStreamPtr() { return http ? http->getStreamPtr() : nullptr; } - bool isInitialized() const { return static_cast(http); } - -private: - std::unique_ptr http; -}; - -#endif diff --git a/src/MemoryBuffer.cpp b/src/MemoryBuffer.cpp index 56a617e..f07db98 100644 --- a/src/MemoryBuffer.cpp +++ b/src/MemoryBuffer.cpp @@ -44,3 +44,8 @@ bool MemoryBuffer::isAllocated() { return buffer_.get() != nullptr; } + +MemoryBuffer MemoryBuffer::empty() +{ + return MemoryBuffer(0); +} diff --git a/src/MemoryBuffer.hpp b/src/MemoryBuffer.hpp index 9d0f551..d242284 100644 --- a/src/MemoryBuffer.hpp +++ b/src/MemoryBuffer.hpp @@ -27,66 +27,15 @@ #include #include -/** - * @class MemoryBuffer - * @brief A class that handles memory allocation and deallocation for a buffer. - * - * This class provides an RAII approach to manage a dynamically allocated buffer. It ensures that memory is - * allocated during object creation and automatically freed when the object goes out of scope. - * - * @note It is recommended to use the `MemoryBuffer` class when dealing with dynamic memory allocation, - * to avoid memory leaks and ensure proper memory management. - * - * Example use: - * ```cpp - * { - * MemoryBuffer buffer(512); - * if (buffer.isAllocated()) { // Check if allocated! - * // Access buffer here... - * } else { - * // Handle error (e.g., log error, retry later) - * } - * } // buffer automatically freed - * - * ``` - */ class MemoryBuffer { public: - /** - * @brief Constructs a `MemoryBuffer` object and allocates memory of the specified size. - * - * The constructor allocates memory of the specified size for the buffer. If allocation fails, - * the buffer will not be valid. - * - * @param size The size of the buffer in bytes. - * - * @example - * // Example usage of the constructor - * MemoryBuffer buffer(512); // Allocates a buffer of 512 bytes - */ explicit MemoryBuffer(size_t size); - /** - * @brief Returns a pointer to the allocated memory buffer. - * - * @return A pointer to the allocated memory, or `nullptr` if memory allocation failed. - */ uint8_t *get(); - - /** - * @brief Returns the size of the allocated buffer. - * - * @return The size of the allocated buffer in bytes. - */ size_t size() const; - - /** - * @brief Checks whether memory allocation was successful. - * - * @return `true` if memory was successfully allocated, `false` if the buffer is `nullptr`. - */ bool isAllocated(); + static MemoryBuffer empty(); private: size_t size_; diff --git a/src/OpenStreetMap-esp32.cpp b/src/OpenStreetMap-esp32.cpp index 19ad77c..0594164 100644 --- a/src/OpenStreetMap-esp32.cpp +++ b/src/OpenStreetMap-esp32.cpp @@ -205,7 +205,7 @@ void OpenStreetMap::updateCache(const tileList &requiredTiles, uint8_t zoom, Til if (!jobs.empty()) { runJobs(jobs); - log_i("Updated %i tiles in %lu ms - %i ms/tile", jobs.size(), millis() - startMS, (millis() - startMS) / jobs.size()); + log_d("Finished %i jobs in %lu ms - %i ms/job", jobs.size(), millis() - startMS, (millis() - startMS) / jobs.size()); } } @@ -345,84 +345,6 @@ bool OpenStreetMap::fetchMap(LGFX_Sprite &mapSprite, double longitude, double la return true; } -bool OpenStreetMap::fillBuffer(WiFiClient *stream, MemoryBuffer &buffer, size_t contentSize, String &result) -{ - size_t readSize = 0; - unsigned long lastReadTime = millis(); - while (readSize < contentSize) - { - const size_t availableData = stream->available(); - if (!availableData) - { - if (millis() - lastReadTime >= OSM_TILE_TIMEOUT_MS) - { - result = "Timeout: " + String(OSM_TILE_TIMEOUT_MS) + " ms"; - return false; - } - taskYIELD(); - continue; - } - - const size_t remaining = contentSize - readSize; - const size_t toRead = std::min(availableData, remaining); - if (toRead == 0) - continue; - - const int bytesRead = stream->readBytes(buffer.get() + readSize, toRead); - if (bytesRead > 0) - { - readSize += bytesRead; - lastReadTime = millis(); - } - else - taskYIELD(); - } - return true; -} - -std::unique_ptr OpenStreetMap::urlToBuffer(const char *url, String &result) -{ - HTTPClientRAII http; - if (!http.begin(url)) - { - result = "Failed to initialize HTTP client"; - return nullptr; - } - - const int httpCode = http.GET(); - if (httpCode != HTTP_CODE_OK) - { - result = "HTTP Error: " + String(httpCode); - return nullptr; - } - - const size_t contentSize = http.getSize(); - if (contentSize < 1) - { - result = "Empty or chunked response"; - return nullptr; - } - - WiFiClient *stream = http.getStreamPtr(); - if (!stream) - { - result = "Failed to get HTTP stream"; - return nullptr; - } - - auto buffer = std::make_unique(contentSize); - if (!buffer->isAllocated()) - { - result = "Failed to allocate buffer"; - return nullptr; - } - - if (!fillBuffer(stream, *buffer, contentSize, result)) - return nullptr; - - return buffer; -} - void OpenStreetMap::PNGDraw(PNGDRAW *pDraw) { uint16_t *destRow = currentInstance->currentTileBuffer + (pDraw->y * currentInstance->currentProvider->tileSize); @@ -431,7 +353,6 @@ void OpenStreetMap::PNGDraw(PNGDRAW *pDraw) bool OpenStreetMap::fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result) { - String url = currentProvider->urlTemplate; url.replace("{x}", String(x)); url.replace("{y}", String(y)); @@ -439,12 +360,12 @@ bool OpenStreetMap::fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, ui if (currentProvider->requiresApiKey && strstr(url.c_str(), "{apiKey}")) url.replace("{apiKey}", currentProvider->apiKey); - const std::unique_ptr buffer = fetcher.fetchToBuffer(url, result); - if (!buffer) + MemoryBuffer buffer = fetcher.fetchToBuffer(url, result, renderMode); + if (!buffer.isAllocated()) return false; PNG *png = getPNGCurrentCore(); - const int16_t rc = png->openRAM(buffer->get(), buffer->size(), PNGDraw); + const int16_t rc = png->openRAM(buffer.get(), buffer.size(), PNGDraw); if (rc != PNG_SUCCESS) { result = "PNG Decoder Error: " + String(rc); @@ -488,21 +409,18 @@ void OpenStreetMap::tileFetcherTask(void *param) String result; if (!osm->fetchTile(fetcher, *job.tile, job.x, job.y, job.z, result)) { + 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); - job.tile->valid = false; - log_e("Tile fetch failed: %s", result.c_str()); } 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); } - job.tile->busy = false; --osm->pendingJobs; - if (!uxQueueMessagesWaiting(osm->jobQueue)) - fetcher.close(); } log_d("task on core %i exiting", xPortGetCoreID()); xTaskNotifyGive(osm->ownerTask); @@ -577,3 +495,8 @@ 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 9edf0af..027bc87 100644 --- a/src/OpenStreetMap-esp32.hpp +++ b/src/OpenStreetMap-esp32.hpp @@ -36,9 +36,9 @@ #include "CachedTile.hpp" #include "TileJob.hpp" #include "MemoryBuffer.hpp" -#include "HTTPClientRAII.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; @@ -94,6 +94,7 @@ 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; }; @@ -109,15 +110,14 @@ 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); - std::unique_ptr urlToBuffer(const char *url, String &result); bool fetchTile(ReusableTileFetcher &fetcher, CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result); - bool fillBuffer(WiFiClient *stream, MemoryBuffer &buffer, size_t contentSize, String &result); bool composeMap(LGFX_Sprite &mapSprite, TileBufferList &tilePointers); static void tileFetcherTask(void *param); static void PNGDraw(PNGDRAW *pDraw); 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/RenderMode.hpp b/src/RenderMode.hpp new file mode 100644 index 0000000..50d0960 --- /dev/null +++ b/src/RenderMode.hpp @@ -0,0 +1,7 @@ +#pragma once + +enum class RenderMode +{ + FAST, + ACCURATE +}; diff --git a/src/ReusableTileFetcher.cpp b/src/ReusableTileFetcher.cpp index 32084d2..f43193c 100644 --- a/src/ReusableTileFetcher.cpp +++ b/src/ReusableTileFetcher.cpp @@ -23,36 +23,53 @@ #include "ReusableTileFetcher.hpp" -ReusableTileFetcher::ReusableTileFetcher() {} -ReusableTileFetcher::~ReusableTileFetcher() { client.stop(); } +ReusableTileFetcher::ReusableTileFetcher() { renderMode = RenderMode::ACCURATE; } +ReusableTileFetcher::~ReusableTileFetcher() { disconnect(); } -std::unique_ptr ReusableTileFetcher::fetchToBuffer(const String &url, String &result) +void ReusableTileFetcher::sendHttpRequest(const String &host, const String &path) { + client.print(String("GET ") + path + " HTTP/1.1\r\n"); + client.print(String("Host: ") + host + "\r\n"); + client.print("User-Agent: OpenStreetMap-esp32/1.0 (+https://github.com/CelliesProjects/OpenStreetMap-esp32)\r\n"); + client.print("Connection: keep-alive\r\n"); + client.print("\r\n"); +} + +void ReusableTileFetcher::disconnect() +{ + client.stop(); + currentHost = ""; + currentPort = 80; +} + +MemoryBuffer ReusableTileFetcher::fetchToBuffer(const String &url, String &result, RenderMode mode) +{ + renderMode = mode; String host, path; uint16_t port; if (!parseUrl(url, host, path, port)) { result = "Invalid URL"; - return nullptr; + return MemoryBuffer::empty(); } if (!ensureConnection(host, port, result)) - return nullptr; + return MemoryBuffer::empty(); sendHttpRequest(host, path); size_t contentLength = 0; if (!readHttpHeaders(contentLength, result)) - return nullptr; + return MemoryBuffer::empty(); - auto buffer = std::make_unique(contentLength); - if (!buffer->isAllocated()) + auto buffer = MemoryBuffer(contentLength); + if (!buffer.isAllocated()) { result = "Buffer allocation failed"; - return nullptr; + return MemoryBuffer::empty(); } - if (!readBody(*buffer, contentLength, result)) - return nullptr; + if (!readBody(buffer, contentLength, result)) + return MemoryBuffer::empty(); return buffer; } @@ -80,35 +97,47 @@ bool ReusableTileFetcher::ensureConnection(const String &host, uint16_t port, St { if (!client.connected() || host != currentHost || port != currentPort) { - client.stop(); // Close old connection if mismatched - if (!client.connect(host.c_str(), port)) + disconnect(); + client.setConnectionTimeout(renderMode == RenderMode::FAST ? 100 : 5000); + if (!client.connect(host.c_str(), port, renderMode == RenderMode::FAST ? 100 : 5000)) { result = "Connection failed to " + host; return false; } currentHost = host; currentPort = port; + log_i("(Re)connected on core %i", xPortGetCoreID()); } return true; } -void ReusableTileFetcher::sendHttpRequest(const String &host, const String &path) -{ - client.print(String("GET ") + path + " HTTP/1.1\r\n"); - client.print(String("Host: ") + host + "\r\n"); - client.print("User-Agent: OpenStreetMap-esp32/1.0 (+https://github.com/CelliesProjects/OpenStreetMap-esp32)\r\n"); - client.print("Connection: keep-alive\r\n"); - client.print("\r\n"); -} - bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, String &result) { String line; + line.reserve(OSM_MAX_HEADERLENGTH); contentLength = 0; + bool start = true; while (client.connected()) { - line = client.readStringUntil('\n'); + if (!readLineWithTimeout(line, renderMode == RenderMode::FAST ? 300 : 5000)) + { + result = "Header timeout"; + disconnect(); + return false; + } + line.trim(); + if (start) + { + if (!line.startsWith("HTTP/1.1")) + { + result = "Bad HTTP response: " + line; + disconnect(); + return false; + } + start = false; + } + if (line.length() == 0) break; // End of headers @@ -118,22 +147,12 @@ bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, String &result) val.trim(); contentLength = val.toInt(); } - - else if (line.startsWith("HTTP/1.1")) - { - if (!line.startsWith("HTTP/1.1 200")) - { - result = "HTTP error: " + line; - client.stop(); - return false; - } - } } if (contentLength == 0) { result = "Missing or invalid Content-Length"; - client.stop(); + disconnect(); return false; } @@ -143,40 +162,60 @@ bool ReusableTileFetcher::readHttpHeaders(size_t &contentLength, String &result) bool ReusableTileFetcher::readBody(MemoryBuffer &buffer, size_t contentLength, String &result) { uint8_t *dest = buffer.get(); - size_t remaining = contentLength; - size_t offset = 0; + size_t readSize = 0; + unsigned long lastReadTime = millis(); + const unsigned long timeoutMs = (renderMode == RenderMode::FAST) ? 300 : 5000; - unsigned long start = millis(); - while (remaining > 0 && millis() - start < 3000) + while (readSize < contentLength) { - int len = client.read(dest + offset, remaining); - if (len > 0) + size_t availableData = client.available(); + if (availableData == 0) { - remaining -= len; - offset += len; + if (millis() - lastReadTime >= timeoutMs) + { + result = "Timeout: " + String(timeoutMs) + " ms"; + disconnect(); + return false; + } + taskYIELD(); + continue; } - else if (len < 0) + + size_t remaining = contentLength - readSize; + size_t toRead = std::min(availableData, remaining); + + int bytesRead = client.readBytes(dest + readSize, toRead); + if (bytesRead > 0) { - result = "Read error"; - client.stop(); - return false; + readSize += bytesRead; + lastReadTime = millis(); } else taskYIELD(); } + return true; +} - if (remaining > 0) +bool ReusableTileFetcher::readLineWithTimeout(String &line, uint32_t timeoutMs) +{ + line = ""; + const uint32_t deadline = millis() + timeoutMs; + + while (millis() < deadline) { - result = "Incomplete read"; - client.stop(); - return false; + if (client.available()) + { + String part = client.readStringUntil('\n'); + if ((line.length() + part.length()) >= OSM_MAX_HEADERLENGTH) + return false; + + line += part; + return true; // Found end of line + } + taskYIELD(); } - return true; + return false; // Timed out } -void ReusableTileFetcher::close() -{ - if (client) - client.stop(); -} + diff --git a/src/ReusableTileFetcher.hpp b/src/ReusableTileFetcher.hpp index 55f6cc7..37b3716 100644 --- a/src/ReusableTileFetcher.hpp +++ b/src/ReusableTileFetcher.hpp @@ -26,6 +26,9 @@ #include #include #include "MemoryBuffer.hpp" +#include "RenderMode.hpp" + +constexpr int OSM_MAX_HEADERLENGTH = 256; class ReusableTileFetcher { @@ -36,17 +39,19 @@ class ReusableTileFetcher ReusableTileFetcher(const ReusableTileFetcher &) = delete; ReusableTileFetcher &operator=(const ReusableTileFetcher &) = delete; - std::unique_ptr fetchToBuffer(const String &url, String &result); - void close(); + MemoryBuffer fetchToBuffer(const String &url, String &result, RenderMode mode); + 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, 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 readLineWithTimeout(String &line, uint32_t timeoutMs); };