diff --git a/Tactility/Source/app/development/Development.cpp b/Tactility/Source/app/development/Development.cpp index 2ada04271..acc4c0f63 100644 --- a/Tactility/Source/app/development/Development.cpp +++ b/Tactility/Source/app/development/Development.cpp @@ -175,19 +175,25 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { return 0; } - ctx.timer = std::make_unique(Timer::Type::Periodic, pdMS_TO_TICKS(1000), [&ctx] { - if (lvgl_is_running()) { - lvgl_lock(); - updateViewState(&ctx); - lvgl_unlock(); - } - }); - AppEventSubscription sub {}; sub.app_instance_id = appInstanceId; app_event_subscribe(&sub); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); + + ctx.timer = std::make_unique(Timer::Type::Periodic, pdMS_TO_TICKS(1000), [&ctx, window] { + if (lvgl_is_running()) { + lvgl_lock(); + // Widgets only exist while this window is topmost - skip otherwise. Another app + // (started non-modally, e.g. via app_manager_start()) can bury this window without + // stopping this instance or notifying it; window_manager deletes a buried window's + // widgets, so touching ctx->statusLabel here would use-after-free it. + if (window_manager_get_state(window) == WINDOW_STATE_GRANTED) { + updateViewState(&ctx); + } + lvgl_unlock(); + } + }); ctx.timer->start(); bool shouldClose = false; diff --git a/Tactility/Source/file/FileMutexLvgl.cpp b/Tactility/Source/file/FileMutexLvgl.cpp index e22fc8ed7..f8df9430b 100644 --- a/Tactility/Source/file/FileMutexLvgl.cpp +++ b/Tactility/Source/file/FileMutexLvgl.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -75,6 +76,29 @@ void initFileMutexForLvgl() { return true; }); + + // SDMMC-backed SD cards aren't parented under SPI_CONTROLLER_TYPE, so the pass above never + // sees them - but on some chips (classic ESP32) SDMMC and SPI still contend for DMA/bus + // access. Lock every SD card mount if a display exists anywhere, regardless of bus topology. + if (!device_exists_of_type(&DISPLAY_TYPE)) { + return; + } + + file_system_for_each(nullptr, [](FileSystem* fs, void* context) { + char mount_path[64]; + if (file_system_get_path(fs, mount_path, sizeof(mount_path)) != ERROR_NONE) { + return true; + } + + auto* owner = file_system_get_owner(fs); + if (owner == nullptr || device_get_type(owner) != &SDCARD_TYPE) { + return true; + } + + LOG_I(TAG, "Adding file mutex for %s (SD card) - a display is present and may contend for bus/DMA resources", mount_path); + file_mutex_register(&lvgl_mutex, mount_path); + return true; + }); } } diff --git a/Tactility/Source/network/HttpdReq.cpp b/Tactility/Source/network/HttpdReq.cpp index d575f4ae0..3569480fc 100644 --- a/Tactility/Source/network/HttpdReq.cpp +++ b/Tactility/Source/network/HttpdReq.cpp @@ -1,8 +1,8 @@ #include #include -#include #include +#include #include #include @@ -186,30 +186,59 @@ size_t receiveFile(httpd_req_t* request, size_t length, const std::string& fileP char buffer[BUFFER_SIZE]; size_t bytes_received = 0; - file::FileMutexGuard guard(filePath); + // Locked only around each actual disk I/O call below, not across the httpd_req_recv() waits + // in between - this file's mutex may resolve to lvgl_lock() (see FileMutexLvgl.cpp), and + // holding that for the whole (potentially multi-second) network transfer starves LVGL's own + // task for the entire upload instead of just for each brief write. + FileMutex mutex {}; + file_mutex_get(&mutex, filePath.c_str()); + file_mutex_lock(&mutex); auto* file = fopen(filePath.c_str(), "wb"); + file_mutex_unlock(&mutex); if (file == nullptr) { LOG_E(TAG, "Failed to open file for writing: %s", filePath.c_str()); return 0; } + constexpr int MAX_TIMEOUT_RETRIES = 5; + int timeout_retries = 0; while (bytes_received < length) { auto expected_chunk_size = std::min(BUFFER_SIZE, length - bytes_received); - size_t receive_chunk_size = httpd_req_recv(request, buffer, expected_chunk_size); - if (receive_chunk_size <= 0) { + int received = httpd_req_recv(request, buffer, expected_chunk_size); + if (received == HTTPD_SOCK_ERR_TIMEOUT) { + // Timeout - retry with backoff, same as receiveByteArray(). A large file takes many + // more chunks (and much longer overall) than the small reads elsewhere in this file, + // so it's far more likely to hit at least one transient stall somewhere along the way. + timeout_retries++; + if (timeout_retries >= MAX_TIMEOUT_RETRIES) { + LOG_E(TAG, "Recv timeout after %d retries, wrote %zu/%zu bytes", timeout_retries, bytes_received, length); + break; + } + LOG_W(TAG, "Recv timeout, retry %d/%d", timeout_retries, MAX_TIMEOUT_RETRIES); + vTaskDelay(pdMS_TO_TICKS(100 * timeout_retries)); // Exponential backoff + continue; + } + if (received <= 0) { LOG_E(TAG, "Receive failed, got 0 bytes but expected %zu more", length - bytes_received); break; } - if (fwrite(buffer, 1, receive_chunk_size, file) != receive_chunk_size) { + timeout_retries = 0; + size_t receive_chunk_size = (size_t)received; + + file_mutex_lock(&mutex); + bool write_ok = fwrite(buffer, 1, receive_chunk_size, file) == receive_chunk_size; + file_mutex_unlock(&mutex); + if (!write_ok) { LOG_E(TAG, "Failed to write all bytes"); break; } bytes_received += receive_chunk_size; } - // Write file + file_mutex_lock(&mutex); fclose(file); + file_mutex_unlock(&mutex); return bytes_received; } diff --git a/Tactility/Source/service/wifi/WifiApSettings.cpp b/Tactility/Source/service/wifi/WifiApSettings.cpp index 4cf0ff7ce..0b6424d46 100644 --- a/Tactility/Source/service/wifi/WifiApSettings.cpp +++ b/Tactility/Source/service/wifi/WifiApSettings.cpp @@ -138,20 +138,24 @@ bool contains(const std::string& ssid) { bool load(const std::string& ssid, WifiApSettings& apSettings) { auto service_context = findServiceContext(); if (service_context == nullptr) { + LOG_E(TAG, "No service context"); return false; } const auto file_path = getApPropertiesFilePath(service_context->getPaths(), ssid); if (!file::isFile(file_path)) { + LOG_E(TAG, "Not a file: %s", file_path.c_str()); return false; } std::map map; if (!file::loadPropertiesFile(file_path, map)) { + LOG_E(TAG, "Failed to load properties from %s", file_path.c_str()); return false; } // SSID is required if (!map.contains(AP_PROPERTIES_KEY_SSID)) { + LOG_E(TAG, "File does not contain SSID: %s", file_path.c_str()); return false; } @@ -166,6 +170,7 @@ bool load(const std::string& ssid, WifiApSettings& apSettings) { } else if (decrypt(ssid, encrypted_password, password_decrypted)) { apSettings.password = password_decrypted; } else { + LOG_E(TAG, "Failed to decrypt password from %s", file_path.c_str()); return false; } } else {