From e7efdf68c040c51c98cb711908326d93a1a8c6d5 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Thu, 7 May 2026 20:13:28 -0400 Subject: [PATCH] perf(boot): remove synchronous GPS+WiFi waits, defer LVGL task start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Boot was 48.5s end-to-end on this hardware: 92% of that was two synchronous waits — gps_sync (15s) and wifi_connect (30s). Both have async paths already (gps.encode is fed in loop(); the periodic status check sets up TCP when WL_CONNECTED appears post-boot), so blocking at boot wasn't actually load-bearing. Drop both waits to brief opportunistic checks (500ms / 1s) and let the main loop pick up async events. Three follow-on changes were needed to keep functionality: - Extract on_wifi_connected() (NTP + OTA + UDP logging setup) from setup_wifi so the post-boot WL_CONNECTED transition in loop() can run it. Idempotent via _wifi_post_connect_done. - Add periodic GPS-time-sync retry in loop() — every 30s while unsynced, attempt a fast (timeout=0) sync_time_from_gps. The cheap path returns immediately when TinyGPSPlus already holds a valid date+time. - Defer LVGL task start until AFTER setup_ui_manager. Previously the LVGL task started immediately after lv_init and refreshed its empty default screen on top of the boot splash, causing a visible flash to black before the first real UI frame painted. With the start moved to after the UIManager has built screens and configured the active one, the splash stays on-screen until the first real frame. Net: boot drops from ~48s to ~3-5s. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main.cpp | 177 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 120 insertions(+), 57 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 8d835a63..ea9bba01 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -199,6 +199,7 @@ extern "C" void pyxis_log(const char* msg) { // Forward declarations void start_tcp_interface(); +void on_wifi_connected(); // Screen timeout bool screen_off = false; @@ -511,57 +512,80 @@ void setup_wifi() { WiFi.mode(WIFI_STA); WiFi.begin(app_settings.wifi_ssid.c_str(), app_settings.wifi_password.c_str()); + // Don't block boot waiting for WiFi association — the main loop + // already sets up TCP and does NTP sync once WL_CONNECTED is + // observed (search for `last_wifi_connected`). Give association + // ~1s in case it lands fast (so we can do NTP+TCP synchronously + // when possible), then continue. BOOT_PROFILE_WAIT_START("wifi_connect"); uint32_t start = millis(); - while (WiFi.status() != WL_CONNECTED && millis() - start < 30000) { - delay(500); + while (WiFi.status() != WL_CONNECTED && millis() - start < 1000) { + delay(50); Serial.print("."); } Serial.println(); BOOT_PROFILE_WAIT_END("wifi_connect"); if (WiFi.status() == WL_CONNECTED) { - INFO("WiFi connected!"); - msg = " IP address: " + WiFi.localIP().toString(); - INFO(msg.c_str()); - msg = " RSSI: " + String(WiFi.RSSI()) + " dBm"; - INFO(msg.c_str()); + on_wifi_connected(); + } else { + INFO("WiFi association deferred — main-loop event handler will " + "do NTP/OTA/UDP setup when the connect lands"); + } +} - // Try GPS time sync first (if GPS is initialized and we haven't synced already) - if (!gps_time_synced) { - // Fallback to NTP if GPS didn't work - INFO("Syncing time via NTP (GPS not available)..."); +// One-shot post-WiFi-connect setup. Runs the first time WL_CONNECTED is +// observed — either at boot (via setup_wifi) or after async association +// completes (via the periodic-status-check branch in loop()). Pulls +// NTP, OTA, and UDP logging out of setup_wifi so the boot fast-path +// (no synchronous WiFi wait) doesn't lose them. +static bool _wifi_post_connect_done = false; +void on_wifi_connected() { + if (_wifi_post_connect_done) return; + _wifi_post_connect_done = true; - // Use configTzTime for proper timezone handling on ESP32 - // Eastern Time: EST5EDT = UTC-5, DST starts 2nd Sunday March, ends 1st Sunday Nov - configTzTime("EST5EDT,M3.2.0,M11.1.0", "pool.ntp.org", "time.nist.gov"); + INFO("WiFi connected!"); + String msg = " IP address: " + WiFi.localIP().toString(); + INFO(msg.c_str()); + msg = " RSSI: " + String(WiFi.RSSI()) + " dBm"; + INFO(msg.c_str()); - // Wait for time to be set (max 10 seconds) - struct tm timeinfo; - int retry = 0; - while (!getLocalTime(&timeinfo) && retry < 20) { - delay(500); - retry++; - } + // Try GPS time sync first (if GPS is initialized and we haven't synced already) + if (!gps_time_synced) { + // Fallback to NTP if GPS didn't work + INFO("Syncing time via NTP (GPS not available)..."); - if (retry < 20) { - // Set the time offset for Utilities::OS::time() - time_t now = time(nullptr); - uint64_t uptime_ms = millis(); - uint64_t unix_ms = (uint64_t)now * 1000; - RNS::Utilities::OS::setTimeOffset(unix_ms - uptime_ms); + // Use configTzTime for proper timezone handling on ESP32 + // Eastern Time: EST5EDT = UTC-5, DST starts 2nd Sunday March, ends 1st Sunday Nov + configTzTime("EST5EDT,M3.2.0,M11.1.0", "pool.ntp.org", "time.nist.gov"); - char time_str[64]; - strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &timeinfo); - msg = " NTP time synced: " + String(time_str); - INFO(msg.c_str()); - } else { - WARNING("NTP time sync failed!"); - } - } else { - INFO("Time already synced via GPS"); + // Wait for time to be set (max 10 seconds) + struct tm timeinfo; + int retry = 0; + while (!getLocalTime(&timeinfo) && retry < 20) { + delay(500); + retry++; } + if (retry < 20) { + // Set the time offset for Utilities::OS::time() + time_t now = time(nullptr); + uint64_t uptime_ms = millis(); + uint64_t unix_ms = (uint64_t)now * 1000; + RNS::Utilities::OS::setTimeOffset(unix_ms - uptime_ms); + + char time_str[64]; + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &timeinfo); + msg = " NTP time synced: " + String(time_str); + INFO(msg.c_str()); + } else { + WARNING("NTP time sync failed!"); + } + } else { + INFO("Time already synced via GPS"); + } + + { // Initialize ArduinoOTA for wireless flashing ArduinoOTA.setHostname("pyxis-tdeck"); ArduinoOTA.onStart([]() { @@ -635,8 +659,6 @@ void setup_wifi() { } }); INFO("UDP log broadcasting on port 9999"); - } else { - ERROR("WiFi connection failed!"); } } @@ -692,17 +714,8 @@ void setup_lvgl_and_ui() { lv_obj_set_style_bg_opa(lv_scr_act(), LV_OPA_COVER, 0); INFO("LVGL initialized"); - - // Start LVGL on its own FreeRTOS task for responsive UI - // Core 1, priority 1 (same as loopTask — round-robin scheduling). - // Previously priority 2, but this starved loopTask of CPU time during - // heavy rendering, causing 30s WDT timeouts on loopTask. - if (!UI::LVGL::LVGLInit::start_task(1, 1)) { - ERROR("Failed to start LVGL task!"); - while (1) delay(1000); - } - - INFO("LVGL task started on core 1"); + INFO("LVGL task start deferred until UI manager is ready " + "(splash stays visible until first real frame)"); // Initialize memory monitoring (if enabled) #ifdef MEMORY_INSTRUMENTATION_ENABLED @@ -1315,14 +1328,20 @@ void setup() { load_app_settings(); BOOT_PROFILE_END("settings"); - // Initialize GPS and try to sync time (before WiFi) + // Initialize GPS but DON'T block boot waiting for a fix. The 15s + // synchronous wait was 31% of boot on this hardware; GPS rarely + // cold-starts in 15s anyway, so we'd usually just eat the full + // timeout. Keep a small (500ms) opportunistic check in case GPS + // already has a fix from before the boot (warm restart). After + // boot, the main loop's per-tick gps.encode + a periodic + // try_gps_sync retry will pick up the time the moment it lands. BOOT_PROFILE_START("gps"); setup_gps(); if (app_settings.gps_time_sync) { INFO("\n=== Time Synchronization ==="); BOOT_PROFILE_WAIT_START("gps_sync"); - if (!sync_time_from_gps(15000)) { // 15 second timeout for GPS - INFO("GPS time sync not available, will try NTP after WiFi"); + if (!sync_time_from_gps(500)) { // brief warm-restart check only + INFO("GPS time sync deferred (will retry async)"); } BOOT_PROFILE_WAIT_END("gps_sync"); } else { @@ -1330,7 +1349,13 @@ void setup() { } BOOT_PROFILE_END("gps"); - // Initialize WiFi + // Initialize WiFi non-blocking. Previously we'd block boot up to + // 30s waiting for association; with a wrong password the device + // ate the full 30s and booted broken anyway. The main loop + // already handles "WiFi just associated" via the + // last_wifi_connected -> wifi_connected transition (sets up TCP + // interface and does NTP sync at that point), so blocking here + // adds nothing except boot latency. BOOT_PROFILE_START("wifi"); setup_wifi(); BOOT_PROFILE_END("wifi"); @@ -1410,6 +1435,22 @@ void setup() { setup_ui_manager(); BOOT_PROFILE_END("ui_manager"); + // Now that UIManager has built screens and configured the active + // one, start the LVGL render task. Doing this any earlier means + // the LVGL task refreshes its empty default screen on top of the + // boot splash (visible flash to black), then later refreshes the + // real UI. Deferring keeps the splash on-screen until the first + // real frame. + // + // Core 1, priority 1 (same as loopTask — round-robin scheduling). + // Previously priority 2, but that starved loopTask of CPU time + // during heavy rendering, causing 30s WDT timeouts on loopTask. + if (!UI::LVGL::LVGLInit::start_task(1, 1)) { + ERROR("Failed to start LVGL task!"); + while (1) delay(1000); + } + INFO("LVGL task started on core 1"); + // Send initial LXST voice destination announce if (ui_manager) { ui_manager->announce_lxst(); @@ -1957,11 +1998,17 @@ void loop() { if (millis() - last_status_check > STATUS_CHECK_INTERVAL) { last_status_check = millis(); - // Start TCP interface when WiFi becomes available + // Start TCP interface + run one-shot NTP/OTA/UDP setup when + // WiFi becomes available. on_wifi_connected is idempotent + // (guarded by _wifi_post_connect_done) so it's safe whether + // the connect happened during boot or here later. bool wifi_connected = (WiFi.status() == WL_CONNECTED); - if (wifi_connected && !last_wifi_connected && !tcp_interface_impl && app_settings.tcp_enabled) { - INFO("WiFi connected - starting TCP interface"); - start_tcp_interface(); + if (wifi_connected && !last_wifi_connected) { + INFO("WiFi connected (post-boot) — running on_wifi_connected"); + on_wifi_connected(); + if (!tcp_interface_impl && app_settings.tcp_enabled) { + start_tcp_interface(); + } } last_wifi_connected = wifi_connected; @@ -2011,6 +2058,22 @@ void loop() { gps.encode(GPSSerial.read()); } + // Async GPS time sync retry. We dropped the synchronous 15s wait + // from boot — instead, retry every 30s here until sync succeeds. + // This also gives a path to catch fixes that arrive after boot + // (warm starts, stationary cold starts, etc). + static uint32_t _gps_sync_last_attempt = 0; + if (!gps_time_synced + && app_settings.gps_time_sync + && millis() - _gps_sync_last_attempt > 30000) { + _gps_sync_last_attempt = millis(); + if (gps.date.isValid() && gps.time.isValid() && gps.date.year() >= 2024) { + // Cheap path: TinyGPSPlus already has a valid timestamp, + // so sync_time_from_gps will return immediately. + sync_time_from_gps(0); + } + } + // Screen timeout handling LOOP_STEP(13); // Screen timeout if (app_settings.screen_timeout > 0) { // 0 = never timeout