From 0d3e1bae30d20ff8582cea3a2cc640600856fb90 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Thu, 5 Mar 2026 19:08:28 -0500 Subject: [PATCH] Fix tile download TLS memory and map rendering performance - Route mbedtls allocations to PSRAM via mbedtls_platform_set_calloc_free() to fix TLS handshake failure (-32512 SSL alloc) with only ~36KB internal heap - Use WiFiClientSecure with setInsecure() for HTTPS tile downloads from OSM - Move tile downloads to background FreeRTOS task to avoid blocking UI thread - Add incremental tile loading (one PNG decode per update cycle) to prevent LVGL mutex timeout from decoding 4 tiles synchronously - Enable LVGL image cache (LV_IMG_CACHE_DEF_SIZE=8) so decoded PNGs stay in PSRAM and don't re-decode on every redraw - Add touch drag panning for map navigation via LV_EVENT_PRESSING Co-Authored-By: Claude Opus 4.6 --- lib/lv_conf.h | 2 +- .../Hardware/TDeck/TileDownloader.cpp | 41 ++++++++- lib/tdeck_ui/UI/LXMF/MapScreen.cpp | 92 +++++++++++++++++-- lib/tdeck_ui/UI/LXMF/MapScreen.h | 24 +++++ 4 files changed, 146 insertions(+), 13 deletions(-) diff --git a/lib/lv_conf.h b/lib/lv_conf.h index 224937ad..9dd3b715 100644 --- a/lib/lv_conf.h +++ b/lib/lv_conf.h @@ -54,7 +54,7 @@ #define LV_DRAW_COMPLEX 1 #define LV_SHADOW_CACHE_SIZE 0 #define LV_CIRCLE_CACHE_SIZE 4 -#define LV_IMG_CACHE_DEF_SIZE 0 +#define LV_IMG_CACHE_DEF_SIZE 8 #define LV_GRADIENT_MAX_STOPS 2 #define LV_GRAD_CACHE_DEF_SIZE 0 #define LV_DITHER_GRADIENT 0 diff --git a/lib/tdeck_ui/Hardware/TDeck/TileDownloader.cpp b/lib/tdeck_ui/Hardware/TDeck/TileDownloader.cpp index 9e0cdb81..588fe440 100644 --- a/lib/tdeck_ui/Hardware/TDeck/TileDownloader.cpp +++ b/lib/tdeck_ui/Hardware/TDeck/TileDownloader.cpp @@ -8,8 +8,30 @@ #include "SDAccess.h" #include #include +#include #include #include +#include +#include + +// ESP-IDF's default mbedtls allocator only uses internal SRAM (~36KB free), +// which is too small for TLS handshake (~40-50KB). Redirect to PSRAM. +// ESP32-S3's unified cache makes PSRAM safe for crypto operations. +static void* psram_calloc(size_t n, size_t size) { + void* ptr = heap_caps_calloc(n, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!ptr) { + ptr = calloc(n, size); // Fallback to default allocator + } + return ptr; +} + +static bool _tls_allocator_set = false; + +static void ensure_psram_tls() { + if (_tls_allocator_set) return; + mbedtls_platform_set_calloc_free(psram_calloc, free); + _tls_allocator_set = true; +} namespace Hardware { namespace TDeck { @@ -61,26 +83,37 @@ bool TileDownloader::create_tile_dirs(int z, int x) { } bool TileDownloader::download_tile(int z, int x, int y) { + char log_buf[128]; + if (WiFi.status() != WL_CONNECTED) { + WARNING("TileDownloader: WiFi not connected, skipping download"); return false; } if (!SDAccess::is_ready()) { + WARNING("TileDownloader: SD card not ready, skipping download"); return false; } String url = build_url(z, x, y); String sd_path = build_sd_path(z, x, y); - char log_buf[128]; - snprintf(log_buf, sizeof(log_buf), "TileDownloader: Fetching tile %d/%d/%d", z, x, y); - DEBUG(log_buf); + snprintf(log_buf, sizeof(log_buf), "TileDownloader: Downloading tile %d/%d/%d", z, x, y); + INFO(log_buf); + + // Route mbedtls allocations to PSRAM (one-time, persists for all future TLS) + ensure_psram_tls(); + + WiFiClientSecure secureClient; + secureClient.setInsecure(); + secureClient.setHandshakeTimeout(10); HTTPClient http; http.setUserAgent("Pyxis/1.0 (ESP32; LXMF messenger)"); http.setTimeout(10000); + http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - if (!http.begin(url)) { + if (!http.begin(secureClient, url)) { WARNING("TileDownloader: Failed to begin HTTP connection"); return false; } diff --git a/lib/tdeck_ui/UI/LXMF/MapScreen.cpp b/lib/tdeck_ui/UI/LXMF/MapScreen.cpp index 1d646cc4..5359476d 100644 --- a/lib/tdeck_ui/UI/LXMF/MapScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/MapScreen.cpp @@ -30,6 +30,16 @@ MapScreen::MapScreen(lv_obj_t* parent) memset(_peer_labels, 0, sizeof(_peer_labels)); memset(_loaded_tile_x, -1, sizeof(_loaded_tile_x)); memset(_loaded_tile_y, -1, sizeof(_loaded_tile_y)); + memset(_pending_tiles, 0, sizeof(_pending_tiles)); + _pending_count = 0; + _last_touch_x = 0; + _last_touch_y = 0; + + // Create download queue and background task + _download_queue = xQueueCreate(8, sizeof(TileRequest)); + _download_complete = false; + _download_task = nullptr; + xTaskCreatePinnedToCore(download_task_func, "tile_dl", 16384, this, 1, &_download_task, 0); LVGL_LOCK(); @@ -150,6 +160,10 @@ void MapScreen::create_viewport() { // Register key event on viewport for pan/zoom lv_obj_add_flag(_viewport, LV_OBJ_FLAG_CLICKABLE); lv_obj_add_event_cb(_viewport, on_key_event, LV_EVENT_KEY, this); + + // Touch drag for panning + lv_obj_add_event_cb(_viewport, on_touch_event, LV_EVENT_PRESSED, this); + lv_obj_add_event_cb(_viewport, on_touch_event, LV_EVENT_PRESSING, this); } void MapScreen::show() { @@ -186,6 +200,23 @@ void MapScreen::hide() { } void MapScreen::update_gps_position() { + // Reload tiles after background downloads complete + if (_download_complete) { + _download_complete = false; + _loaded_zoom = -1; // Force tile reload + update_tiles(); + } + + // Incremental tile loading: process one pending tile per cycle + // This prevents LVGL mutex timeout from decoding multiple PNGs at once + if (_pending_count > 0) { + PendingTile& pt = _pending_tiles[_pending_count - 1]; + load_tile(pt.slot, pt.x, pt.y, pt.z); + _loaded_tile_x[pt.slot] = pt.x; + _loaded_tile_y[pt.slot] = pt.y; + _pending_count--; + } + if (!_gps) return; if (_gps->location.isValid()) { @@ -261,6 +292,8 @@ void MapScreen::update_tiles() { {base_tile_x + 1, base_tile_y + 1} }; + _pending_count = 0; + for (int i = 0; i < 4; i++) { int col = i % 2; int row = i / 2; @@ -272,9 +305,22 @@ void MapScreen::update_tiles() { if (_loaded_zoom != _zoom || _loaded_tile_x[i] != tile_coords[i][0] || _loaded_tile_y[i] != tile_coords[i][1]) { - load_tile(i, tile_coords[i][0], tile_coords[i][1], _zoom); - _loaded_tile_x[i] = tile_coords[i][0]; - _loaded_tile_y[i] = tile_coords[i][1]; + + int tx = tile_coords[i][0]; + int ty = tile_coords[i][1]; + + if (Hardware::TDeck::TileDownloader::tile_exists(_zoom, tx, ty)) { + // Tile on SD — load immediately (fast with LVGL image cache) + load_tile(i, tx, ty, _zoom); + _loaded_tile_x[i] = tx; + _loaded_tile_y[i] = ty; + } else { + // Queue download + deferred load + _pending_tiles[_pending_count++] = {i, _zoom, tx, ty}; + TileRequest req = {_zoom, tx, ty}; + xQueueSend(_download_queue, &req, 0); + lv_img_set_src(_tile_imgs[i], ""); + } } } _loaded_zoom = _zoom; @@ -341,16 +387,46 @@ void MapScreen::pan(int dx, int dy) { } void MapScreen::load_tile(int slot, int tile_x, int tile_y, int z) { - // Ensure tile is on SD (download if missing and WiFi available) - Hardware::TDeck::TileDownloader::ensure_tile(z, tile_x, tile_y); - - // Build LVGL FS path: "S:tiles/{z}/{x}/{y}.png" char path[64]; snprintf(path, sizeof(path), "S:tiles/%d/%d/%d.png", z, tile_x, tile_y); - lv_img_set_src(_tile_imgs[slot], path); } +void MapScreen::download_task_func(void* param) { + MapScreen* self = (MapScreen*)param; + TileRequest req; + while (true) { + if (xQueueReceive(self->_download_queue, &req, portMAX_DELAY) == pdTRUE) { + if (Hardware::TDeck::TileDownloader::download_tile(req.z, req.x, req.y)) { + self->_download_complete = true; + } + } + } +} + +void MapScreen::on_touch_event(lv_event_t* event) { + MapScreen* screen = (MapScreen*)lv_event_get_user_data(event); + lv_event_code_t code = lv_event_get_code(event); + lv_indev_t* indev = lv_indev_get_act(); + if (!indev) return; + + lv_point_t point; + lv_indev_get_point(indev, &point); + + if (code == LV_EVENT_PRESSED) { + screen->_last_touch_x = point.x; + screen->_last_touch_y = point.y; + } else if (code == LV_EVENT_PRESSING) { + int dx = screen->_last_touch_x - point.x; + int dy = screen->_last_touch_y - point.y; + if (dx != 0 || dy != 0) { + screen->pan(dx, dy); + screen->_last_touch_x = point.x; + screen->_last_touch_y = point.y; + } + } +} + void MapScreen::on_back_clicked(lv_event_t* event) { MapScreen* screen = (MapScreen*)lv_event_get_user_data(event); if (screen->_back_callback) { diff --git a/lib/tdeck_ui/UI/LXMF/MapScreen.h b/lib/tdeck_ui/UI/LXMF/MapScreen.h index 588088ce..b735a0cc 100644 --- a/lib/tdeck_ui/UI/LXMF/MapScreen.h +++ b/lib/tdeck_ui/UI/LXMF/MapScreen.h @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include "Bytes.h" class TinyGPSPlus; @@ -107,11 +109,33 @@ private: // Load a tile image into one of the 4 slots void load_tile(int slot, int tile_x, int tile_y, int z); + // Touch drag state + int _last_touch_x; + int _last_touch_y; + + // Incremental tile loading — one tile per update cycle to avoid LVGL mutex timeout + struct PendingTile { + int slot, z, x, y; + }; + PendingTile _pending_tiles[4]; + int _pending_count; + + // Async tile downloading + struct TileRequest { + int z, x, y; + }; + QueueHandle_t _download_queue; + TaskHandle_t _download_task; + volatile bool _download_complete; + + static void download_task_func(void* param); + // Event handlers static void on_back_clicked(lv_event_t* event); static void on_zoom_in_clicked(lv_event_t* event); static void on_zoom_out_clicked(lv_event_t* event); static void on_key_event(lv_event_t* event); + static void on_touch_event(lv_event_t* event); }; } // namespace LXMF