From c58d711716972cb181dfe9f196d45fdc5071bc62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:03:58 +0100 Subject: [PATCH] perf(ambient): cache empty playlist scans within TTL Co-Authored-By: Claude Opus 4.6 --- ui_freenove_allinone/src/app/main.cpp | 559 +++++++++++++++++++++----- 1 file changed, 464 insertions(+), 95 deletions(-) diff --git a/ui_freenove_allinone/src/app/main.cpp b/ui_freenove_allinone/src/app/main.cpp index 2c333cb..9d1fe0a 100644 --- a/ui_freenove_allinone/src/app/main.cpp +++ b/ui_freenove_allinone/src/app/main.cpp @@ -16,6 +16,12 @@ #if defined(ARDUINO_ARCH_ESP32) #include #include +#if defined(ARDUINO_USB_MODE) && (ARDUINO_USB_MODE != 0) && __has_include() +#include +#define ZACUS_HAS_USB_HWCDC 1 +#else +#define ZACUS_HAS_USB_HWCDC 0 +#endif #if __has_include() #include #define ZACUS_HAS_SD_AUDIO 1 @@ -39,6 +45,7 @@ #include "app/runtime_scene_service.h" #include "app/runtime_serial_service.h" #include "app/runtime_web_service.h" +#include "drivers/display/display_hal.h" #include "runtime/app_coordinator.h" #include "runtime/la_trigger_service.h" #include "runtime/perf/perf_monitor.h" @@ -72,8 +79,8 @@ namespace { constexpr const char* kDefaultScenarioFile = "/story/scenarios/DEFAULT.json"; constexpr const char* kFirmwareName = "freenove_esp32s3"; -constexpr const char* kDiagAudioFile = "/music/boot_radio.mp3"; -constexpr const char* kUsonAmbientTrack = "/music/boot_radio.mp3"; +constexpr const char* kDiagAudioFile = "/apps/audio_player/audio/default.mp3"; +constexpr const char* kUsonAmbientTrack = "/apps/audio_player/audio/default.mp3"; constexpr uint8_t kUsonAmbientVolumeFixed = 21U; constexpr uint8_t kUsonAmbientVolumeMin = (kUsonAmbientVolumeFixed > FREENOVE_AUDIO_MAX_VOLUME) ? FREENOVE_AUDIO_MAX_VOLUME : kUsonAmbientVolumeFixed; @@ -83,6 +90,7 @@ constexpr uint32_t kUsonAmbientDelayMinMs = 1000U; constexpr uint32_t kUsonAmbientDelayMaxMs = 2000U; constexpr uint32_t kWarningSirenBeatIntervalMs = 700U; constexpr size_t kSerialLineCapacity = 192U; +constexpr size_t kUsbConsoleRxBufferSize = 256U; constexpr bool kBootDiagnosticTone = true; constexpr bool kAutoSyncStoryFromSdOnBoot = false; #if defined(ZACUS_SPRINT_DIAG_MODE) && (ZACUS_SPRINT_DIAG_MODE != 0) @@ -152,7 +160,7 @@ struct SceneAmbientAudioFxState { bool enabled = false; bool non_interruptive = true; char mode[24] = "single_track"; - char track[64] = "/music/boot_radio.mp3"; + char track[64] = "/apps/audio_player/audio/default.mp3"; char playlist_root[96] = ""; bool playlist_recursive = true; uint8_t volume_min = kUsonAmbientVolumeMin; @@ -208,11 +216,20 @@ bool g_has_ring_sent_for_win_etape = false; bool g_win_etape_ui_refresh_pending = false; bool g_boot_media_manager_mode = false; bool g_amiga_shell_mode_boot = true; // Force boot to Amiga UI Shell instead of story scenario +bool g_safe_diagnostic_mode = false; +bool g_ui_started = false; +bool g_boot_network_deferred = false; +bool g_safe_diag_display_ready = false; +uint8_t g_safe_diag_last_button = 0U; +bool g_safe_diag_last_button_long = false; +uint32_t g_safe_diag_last_button_ms = 0U; +uint32_t g_safe_diag_last_redraw_ms = 0U; uint8_t g_lcd_backlight_level = 80U; uint8_t g_lcd_backlight_base_level = 80U; bool g_setup_mode = true; bool g_web_auth_required = false; bool g_resource_profile_auto = true; +bool g_usb_console_ready = false; char g_web_auth_token[kWebAuthTokenCapacity] = {0}; char g_espnow_device_name[kEspNowDeviceNameCapacity] = "U_SON"; bool g_espnow_discovery_runtime_enabled = !kSprintDiagMode; @@ -280,7 +297,7 @@ const char* audioPackToFile(const char* pack_id) { return nullptr; } if (std::strcmp(pack_id, "PACK_BOOT_RADIO") == 0) { - return "/music/boot_radio.mp3"; + return "/apps/audio_player/audio/default.mp3"; } if (std::strcmp(pack_id, "PACK_SONAR_HINT") == 0) { return "/music/sonar_hint.mp3"; @@ -825,6 +842,12 @@ void collectAmbientPlaylistFiles(fs::FS& file_system, prefix += "/"; } const unsigned int prefix_len = static_cast(prefix.length()); + String prefix_no_leading_slash = prefix; + if (prefix_no_leading_slash.startsWith("/")) { + prefix_no_leading_slash.remove(0, 1); + } + const unsigned int prefix_no_leading_slash_len = + static_cast(prefix_no_leading_slash.length()); File entry = fs_root.openNextFile(); while (entry) { String entry_name = entry.name(); @@ -833,17 +856,16 @@ void collectAmbientPlaylistFiles(fs::FS& file_system, } if (!entry.isDirectory() && isAmbientAudioFilePath(entry_name)) { int prefix_pos = entry_name.indexOf(prefix); + unsigned int matched_prefix_len = prefix_len; if (prefix_pos < 0) { - String prefix_no_leading_slash = prefix; - if (prefix_no_leading_slash.startsWith("/")) { - prefix_no_leading_slash.remove(0, 1); - } prefix_pos = entry_name.indexOf(prefix_no_leading_slash); + matched_prefix_len = prefix_no_leading_slash_len; } if (prefix_pos >= 0) { bool include = true; if (!recursive) { - const unsigned int child_start = static_cast(prefix_pos) + prefix_len; + const unsigned int child_start = + static_cast(prefix_pos) + matched_prefix_len; include = entry_name.indexOf('/', child_start) < 0; } if (include) { @@ -886,6 +908,19 @@ void collectAmbientPlaylistFiles(fs::FS& file_system, dir.close(); } +namespace { +struct AmbientPlaylistCache { + String root; + bool recursive = false; + bool use_sd = false; + uint32_t expires_at_ms = 0U; + std::vector files; +}; + +AmbientPlaylistCache g_ambient_playlist_cache; +constexpr uint32_t kAmbientPlaylistCacheTtlMs = 12000U; +} // namespace + bool chooseAmbientPlaylistTrack(const SceneAmbientAudioFxState& ambient, uint32_t now_ms, String* out_track) { if (out_track == nullptr) { return false; @@ -902,18 +937,36 @@ bool chooseAmbientPlaylistTrack(const SceneAmbientAudioFxState& ambient, uint32_ return false; } - std::vector files; - files.reserve(24U); - collectAmbientPlaylistFiles(*file_system, root, ambient.playlist_recursive, 0U, &files); + const bool cache_key_match = + (g_ambient_playlist_cache.root == root) && (g_ambient_playlist_cache.recursive == ambient.playlist_recursive) && + (g_ambient_playlist_cache.use_sd == use_sd); + const bool cache_fresh = + cache_key_match && + (static_cast(now_ms - g_ambient_playlist_cache.expires_at_ms) < 0); + + if (!cache_fresh) { + std::vector refreshed_files; + refreshed_files.reserve(24U); + collectAmbientPlaylistFiles(*file_system, root, ambient.playlist_recursive, 0U, &refreshed_files); + g_ambient_playlist_cache.root = root; + g_ambient_playlist_cache.recursive = ambient.playlist_recursive; + g_ambient_playlist_cache.use_sd = use_sd; + g_ambient_playlist_cache.expires_at_ms = now_ms + kAmbientPlaylistCacheTtlMs; + g_ambient_playlist_cache.files.swap(refreshed_files); + } + + const std::vector& files = g_ambient_playlist_cache.files; if (files.empty()) { return false; } const uint32_t seed = hashSceneFxSeed(now_ms ^ 0x59A31BUL ^ static_cast(files.size())); const size_t index = static_cast(seed % static_cast(files.size())); - const String selected = files[index]; + const String& selected = files[index]; if (use_sd) { - *out_track = String("/sd") + selected; + out_track->reserve(selected.length() + 3U); + *out_track = "/sd"; + *out_track += selected; } else { *out_track = selected; } @@ -1815,6 +1868,198 @@ void logBuildMemoryPolicy() { #endif } +void applySafeDiagnosticBootPolicy() { +#if defined(ARDUINO_ARCH_ESP32) + const BootHeapSnapshot heap = bootCaptureHeapSnapshot(); + g_safe_diagnostic_mode = !heap.psram_found; +#else + g_safe_diagnostic_mode = false; +#endif + if (!g_safe_diagnostic_mode) { + return; + } + + g_network_cfg.espnow_enabled_on_boot = false; + g_hardware_cfg.enabled_on_boot = false; + g_hardware_cfg.mic_enabled = false; + g_camera_cfg.enabled_on_boot = false; + g_espnow_discovery_runtime_enabled = false; + g_amiga_shell_mode_boot = false; + + Serial.println("[BOOT] safe diagnostic mode enabled: PSRAM required, app stack disabled"); +} + +constexpr uint32_t kBootNetworkMinFreeHeap = 160000U; +constexpr uint32_t kBootNetworkMinLargestInternal = 120000U; + +const char* bootNetworkDeferReason(const BootHeapSnapshot& heap, uint32_t free_heap) { + if (kBootEspNowOnlyMode) { + return "espnow_only_mode"; + } + if (free_heap < kBootNetworkMinFreeHeap) { + return "free_heap_low"; + } + if (heap.heap_internal_largest < kBootNetworkMinLargestInternal) { + return "largest_internal_low"; + } + return ""; +} + +bool shouldDeferBootNetworkBringup(const BootHeapSnapshot& heap, uint32_t free_heap) { + return bootNetworkDeferReason(heap, free_heap)[0] != '\0'; +} + +int16_t activeDisplayWidth() { + return ((FREENOVE_LCD_ROTATION & 0x1U) != 0U) ? static_cast(FREENOVE_LCD_HEIGHT) + : static_cast(FREENOVE_LCD_WIDTH); +} + +int16_t activeDisplayHeight() { + return ((FREENOVE_LCD_ROTATION & 0x1U) != 0U) ? static_cast(FREENOVE_LCD_WIDTH) + : static_cast(FREENOVE_LCD_HEIGHT); +} + +bool ensureSafeDiagnosticDisplayReady() { + if (g_safe_diag_display_ready) { + return true; + } + + drivers::display::DisplayHalConfig display_cfg = {}; + display_cfg.width = FREENOVE_LCD_WIDTH; + display_cfg.height = FREENOVE_LCD_HEIGHT; + display_cfg.rotation = FREENOVE_LCD_ROTATION; + if (!drivers::display::displayHal().begin(display_cfg)) { + Serial.println("[SAFE] display init failed"); + return false; + } + g_safe_diag_display_ready = true; + return true; +} + +void drawSafeDiagnosticText(int16_t y, + const char* text, + drivers::display::OverlayFontFace font_face, + uint16_t color565, + bool opaque_bg = false) { + if (text == nullptr || text[0] == '\0' || !g_safe_diag_display_ready) { + return; + } + drivers::display::OverlayTextCommand command = {}; + command.text = text; + command.x = 10; + command.y = y; + command.color565 = color565; + command.bg565 = 0x0000U; + command.font_face = font_face; + command.size = 1U; + command.opaque_bg = opaque_bg; + (void)drivers::display::displayHal().drawOverlayText(command); +} + +void renderSafeDiagnosticScreen() { + if (!ensureSafeDiagnosticDisplayReady()) { + return; + } + + drivers::display::DisplayHal& display = drivers::display::displayHal(); + const int16_t width = activeDisplayWidth(); + const int16_t height = activeDisplayHeight(); + const uint16_t bg = display.color565(0x06U, 0x0BU, 0x14U); + const uint16_t header_bg = display.color565(0x52U, 0x12U, 0x12U); + const uint16_t accent = display.color565(0xFFU, 0xD6U, 0x4AU); + const uint16_t fg = display.color565(0xE8U, 0xF1U, 0xFFU); + const uint16_t dim = display.color565(0x98U, 0xA6U, 0xBAU); + + const BootHeapSnapshot heap = bootCaptureHeapSnapshot(); + display.fillScreen(bg); + (void)display.fillOverlayRect(0, 0, width, 44, header_bg); + drawSafeDiagnosticText(10, "PSRAM REQUIRED", drivers::display::OverlayFontFace::kBuiltinLarge, accent); + + char line[96] = {0}; + std::snprintf(line, sizeof(line), "Board: FNK0102H / Freenove ESP32-S3 WROOM N8R8"); + drawSafeDiagnosticText(58, line, drivers::display::OverlayFontFace::kBuiltinMedium, fg); + std::snprintf(line, + sizeof(line), + "PSRAM found=%u total=%lu free=%lu", + heap.psram_found ? 1U : 0U, + static_cast(heap.psram_total), + static_cast(heap.heap_psram_free)); + drawSafeDiagnosticText(84, line, drivers::display::OverlayFontFace::kBuiltinMedium, fg); + std::snprintf(line, + sizeof(line), + "Heap free=%u largest_internal=%lu", + static_cast(ESP.getFreeHeap()), + static_cast(heap.heap_internal_largest)); + drawSafeDiagnosticText(106, line, drivers::display::OverlayFontFace::kBuiltinSmall, fg); + + drawSafeDiagnosticText(142, "Safe diagnostic mode active", drivers::display::OverlayFontFace::kBuiltinMedium, accent); + drawSafeDiagnosticText(166, "Disabled: UI apps, camera, FX, audio, mic", drivers::display::OverlayFontFace::kBuiltinSmall, fg); + drawSafeDiagnosticText(186, "Disabled: Wi-Fi, ESP-NOW, web, file share", drivers::display::OverlayFontFace::kBuiltinSmall, fg); + drawSafeDiagnosticText(218, "Buttons remain active on GPIO19 ladder", drivers::display::OverlayFontFace::kBuiltinSmall, fg); + + if (g_safe_diag_last_button != 0U) { + std::snprintf(line, + sizeof(line), + "Last button: key=%u long=%u at=%lu ms", + static_cast(g_safe_diag_last_button), + g_safe_diag_last_button_long ? 1U : 0U, + static_cast(g_safe_diag_last_button_ms)); + } else { + std::snprintf(line, sizeof(line), "Last button: none"); + } + drawSafeDiagnosticText(252, line, drivers::display::OverlayFontFace::kBuiltinMedium, fg); + + drawSafeDiagnosticText(298, "Serial: PING HELP STATUS BTN_READ LCD_BACKLIGHT", drivers::display::OverlayFontFace::kBuiltinSmall, fg); + drawSafeDiagnosticText(320, "Cause: build expects OPI PSRAM but boot sees none", drivers::display::OverlayFontFace::kBuiltinSmall, dim); + drawSafeDiagnosticText(342, "Action: verify module, flash/psram mode, board wiring", drivers::display::OverlayFontFace::kBuiltinSmall, dim); + drawSafeDiagnosticText(height - 24, "Reboot after fixing PSRAM detection.", drivers::display::OverlayFontFace::kBuiltinSmall, accent); + g_safe_diag_last_redraw_ms = millis(); +} + +void beginSafeDiagnosticMode() { + Serial.println("[SAFE] boot path: storage + serial + display + buttons only"); + g_buttons.begin(); + applyLcdBacklight(g_lcd_backlight_level); + renderSafeDiagnosticScreen(); + Serial.println("[SAFE] serial commands: PING HELP STATUS BTN_READ LCD_BACKLIGHT"); +} + +void printSafeDiagnosticStatus() { + const BootHeapSnapshot heap = bootCaptureHeapSnapshot(); + Serial.printf("STATUS mode=safe_diagnostic psram_found=%u psram_total=%lu psram_free=%lu heap_free=%u internal_largest=%lu " + "ui=0 audio=0 cam=0 hw=0 net=0 web=0 share=0 key=%u mv=%d display=%u\n", + heap.psram_found ? 1U : 0U, + static_cast(heap.psram_total), + static_cast(heap.heap_psram_free), + static_cast(ESP.getFreeHeap()), + static_cast(heap.heap_internal_largest), + static_cast(g_buttons.currentKey()), + g_buttons.lastAnalogMilliVolts(), + g_safe_diag_display_ready ? 1U : 0U); +} + +void runSafeDiagnosticIteration(uint32_t now_ms) { + ButtonEvent event; + bool redraw = false; + while (g_buttons.pollEvent(&event)) { + const uint32_t event_ms = (event.ms != 0U) ? event.ms : now_ms; + g_safe_diag_last_button = event.key; + g_safe_diag_last_button_long = event.long_press; + g_safe_diag_last_button_ms = event_ms; + Serial.printf("[SAFE] button key=%u long=%u mv=%d ms=%lu\n", + static_cast(event.key), + event.long_press ? 1U : 0U, + g_buttons.lastAnalogMilliVolts(), + static_cast(event_ms)); + redraw = true; + } + + if (redraw || g_safe_diag_last_redraw_ms == 0U || static_cast(now_ms - g_safe_diag_last_redraw_ms) >= 5000) { + renderSafeDiagnosticScreen(); + } + yield(); +} + bool parseBoolToken(const char* text, bool* out_value) { if (text == nullptr || out_value == nullptr) { return false; @@ -3216,8 +3461,14 @@ void setCameraSceneActive(bool active) { return; } if (!g_camera.startRecorderSession()) { - Serial.println("[CAM_UI] recorder session start failed"); - g_camera_player.show(); + const bool legacy_ok = g_camera.start(); + Serial.printf("[CAM_UI] recorder session start failed, legacy=%u\n", legacy_ok ? 1U : 0U); + g_camera_scene_active = false; + if (legacy_ok) { + g_camera_player.hide(); + } else { + g_camera_player.show(); + } return; } g_camera_player.show(); @@ -3352,6 +3603,10 @@ void printButtonRead() { } void printRuntimeStatus() { + if (g_safe_diagnostic_mode) { + printSafeDiagnosticStatus(); + return; + } const ScenarioSnapshot snapshot = g_scenario.snapshot(); const NetworkManager::Snapshot net = g_network.snapshot(); const HardwareManager::Snapshot& hw = g_hardware.snapshotRef(); @@ -4832,7 +5087,7 @@ bool executeStoryAction(const char* action_id, const ScenarioSnapshot& snapshot, } if (std::strcmp(action_id, "ACTION_MEDIA_PLAY_FILE") == 0) { - const char* media_file = g_story_action_doc["config"]["file"] | g_story_action_doc["config"]["path"] | "/music/boot_radio.mp3"; + const char* media_file = g_story_action_doc["config"]["file"] | g_story_action_doc["config"]["path"] | "/apps/audio_player/audio/default.mp3"; return g_media.play(media_file, &g_audio); } @@ -7040,6 +7295,38 @@ void handleSerialCommandImpl(const char* command_line, uint32_t now_ms) { Serial.println("PONG"); return; } + if (g_safe_diagnostic_mode) { + if (std::strcmp(command, "HELP") == 0) { + Serial.println("CMDS PING HELP STATUS BTN_READ LCD_BACKLIGHT [0..255]"); + return; + } + if (std::strcmp(command, "STATUS") == 0) { + printSafeDiagnosticStatus(); + return; + } + if (std::strcmp(command, "BTN_READ") == 0) { + printButtonRead(); + return; + } + if (std::strcmp(command, "LCD_BACKLIGHT") == 0) { + if (argument == nullptr || argument[0] == '\0') { + Serial.printf("LCD_BACKLIGHT %u\n", static_cast(g_lcd_backlight_level)); + return; + } + char* end = nullptr; + const long parsed = std::strtol(argument, &end, 10); + if (end == argument || (end != nullptr && *end != '\0') || parsed < 0L || parsed > 255L) { + Serial.println("ERR LCD_BACKLIGHT_ARG"); + return; + } + applyLcdBacklight(static_cast(parsed)); + renderSafeDiagnosticScreen(); + Serial.printf("ACK LCD_BACKLIGHT %lu\n", static_cast(parsed)); + return; + } + Serial.printf("ERR SAFE_DIAGNOSTIC_MODE cmd=%s\n", command); + return; + } if (std::strcmp(command, "HELP") == 0) { Serial.println( "CMDS PING STATUS BTN_READ NEXT UNLOCK RESET " @@ -7858,8 +8145,19 @@ void handleSerialCommandImpl(const char* command_line, uint32_t now_ms) { } void pollSerialCommands(uint32_t now_ms) { - while (Serial.available() > 0) { - const int raw = Serial.read(); + for (;;) { + int raw = -1; +#if ZACUS_HAS_USB_HWCDC + if (g_usb_console_ready && USBSerial.available() > 0) { + raw = USBSerial.read(); + } +#endif + if (raw < 0 && Serial.available() > 0) { + raw = Serial.read(); + } + if (raw < 0) { + break; + } if (raw < 0) { break; } @@ -7869,7 +8167,7 @@ void pollSerialCommands(uint32_t now_ms) { continue; } g_serial_line[g_serial_line_len] = '\0'; - g_app_coordinator.onSerialLine(g_serial_line, now_ms); + handleSerialCommandImpl(g_serial_line, now_ms); g_serial_line_len = 0U; continue; } @@ -7886,9 +8184,19 @@ void pollSerialCommands(uint32_t now_ms) { void setup() { Serial.begin(115200); +#if ZACUS_HAS_USB_HWCDC + USBSerial.setRxBufferSize(kUsbConsoleRxBufferSize); + USBSerial.begin(115200); + g_usb_console_ready = true; +#endif delay(100); RuntimeMetrics::instance().reset(bootResetReasonCode()); Serial.println("[MAIN] Freenove all-in-one boot"); +#if ZACUS_HAS_USB_HWCDC + Serial.printf("[USB] HWCDC bridge ready=%u rx_buf=%u\n", + g_usb_console_ready ? 1U : 0U, + static_cast(kUsbConsoleRxBufferSize)); +#endif bootPrintReport(kFirmwareName, ZACUS_FW_VERSION); logBuildMemoryPolicy(); logBootMemoryProfile(); @@ -7934,9 +8242,13 @@ void setup() { Serial.printf("[DIAG] sprint_mode=1 telemetry_ms=%lu espnow_boot=0 discovery_runtime=0\n", static_cast(g_hardware_cfg.telemetry_period_ms)); } + applySafeDiagnosticBootPolicy(); + if (g_safe_diagnostic_mode) { + beginSafeDiagnosticMode(); + return; + } loadBootProvisioningState(); loadEspNowDeviceNameFromNvs(); - g_file_share_service.begin(g_network_cfg.hostname, g_espnow_device_name); { // Force bootmode to skip story scenario routing when AmigaUI Shell mode is active if (g_amiga_shell_mode_boot) { @@ -7967,7 +8279,7 @@ void setup() { g_web_auth_required ? 1U : 0U, g_web_auth_token[0] != '\0' ? 1U : 0U); - // Initialize audio before camera/network stacks to keep DMA heap pressure low during I2S bring-up. + // Initialize audio before camera/UI stacks to keep DMA heap pressure low during I2S bring-up. g_audio.begin(); Serial.printf("[MAIN] audio profile=%u:%s count=%u\n", g_audio.outputProfile(), @@ -7990,6 +8302,10 @@ void setup() { } } if (g_hardware_cfg.enabled_on_boot) { + if (!g_hardware_cfg.mic_enabled) { + g_hardware.setMicRuntimeEnabled(false); + Serial.println("[HW] mic boot disabled by runtime policy"); + } g_hardware_started = g_hardware.begin(); g_next_hw_telemetry_ms = millis() + g_hardware_cfg.telemetry_period_ms; g_mic_event_armed = true; @@ -8002,48 +8318,6 @@ void setup() { g_buttons.begin(); g_touch.begin(); - g_network.begin(g_network_cfg.hostname); - if (kBootEspNowOnlyMode) { - g_network.configureFallbackAp("", ""); - g_network.configureLocalPolicy("", "", false, g_network_cfg.local_retry_ms, false); - Serial.println("[NET] wifi+web disabled (espnow_only_mode=1)"); - } else { - g_network.configureFallbackAp(g_network_cfg.ap_default_ssid, g_network_cfg.ap_default_password); - g_network.configureLocalPolicy(g_network_cfg.local_ssid, - g_network_cfg.local_password, - g_network_cfg.force_ap_if_not_local, - g_network_cfg.local_retry_ms, - g_network_cfg.pause_local_retry_when_ap_client); - if ((g_setup_mode || kSprintDiagMode) && g_network_cfg.ap_default_ssid[0] != '\0') { - g_network.startAp(g_network_cfg.ap_default_ssid, g_network_cfg.ap_default_password); - } - if (g_network_cfg.local_ssid[0] != '\0') { - const bool connect_started = g_network.connectSta(g_network_cfg.local_ssid, g_network_cfg.local_password); - Serial.printf("[NET] boot wifi target=%s started=%u\n", - g_network_cfg.local_ssid, - connect_started ? 1U : 0U); - } - } - if (g_network_cfg.espnow_enabled_on_boot) { - if (g_network.enableEspNow()) { - for (uint8_t index = 0U; index < g_network_cfg.espnow_boot_peer_count; ++index) { - const char* peer = g_network_cfg.espnow_boot_peers[index]; - if (peer == nullptr || peer[0] == '\0') { - continue; - } - const bool ok = g_network.addEspNowPeer(peer); - Serial.printf("[NET] boot peer add mac=%s ok=%u\n", peer, ok ? 1U : 0U); - } - } - } else { - Serial.println("[NET] ESP-NOW boot disabled by APP_ESPNOW config"); - } - g_next_espnow_discovery_ms = millis() + 2000U; - if (!kBootEspNowOnlyMode) { - setupWebUi(); - } else { - Serial.println("[WEB] disabled (espnow_only_mode=1)"); - } // Scenario loading disabled to boot directly to Amiga UI Shell grid launcher // if (!g_scenario.begin(kDefaultScenarioFile)) { // Serial.println("[MAIN] scenario init failed"); @@ -8061,23 +8335,27 @@ void setup() { // } g_last_action_step_key[0] = '\0'; - g_ui.begin(); - - // Initialize Amiga UI Shell for grid-based app launcher - g_amiga_shell.init(&g_hardware, &g_ui, &g_app_registry); - g_amiga_shell.onStart(); - + g_ui_started = g_ui.begin(); + if (!g_ui_started) { + Serial.println("[BOOT] UI init failed: network/web boot blocked"); + } else { + g_amiga_shell.init(&g_hardware, &g_ui, &g_app_registry); + g_amiga_shell.onStart(); + } + applyLcdBacklight(g_lcd_backlight_level); - g_ui.setHardwareController(&g_hardware); - UiLaMetrics boot_la_metrics = {}; - boot_la_metrics.locked = false; - boot_la_metrics.stability_pct = 0U; - boot_la_metrics.stable_ms = 0U; - boot_la_metrics.stable_target_ms = g_hardware_cfg.mic_la_stable_ms; - boot_la_metrics.gate_elapsed_ms = 0U; - boot_la_metrics.gate_timeout_ms = g_hardware_cfg.mic_la_timeout_ms; - g_ui.setLaMetrics(boot_la_metrics); - g_ui.setHardwareSnapshotRef(&g_hardware.snapshotRef()); + if (g_ui_started) { + g_ui.setHardwareController(&g_hardware); + UiLaMetrics boot_la_metrics = {}; + boot_la_metrics.locked = false; + boot_la_metrics.stability_pct = 0U; + boot_la_metrics.stable_ms = 0U; + boot_la_metrics.stable_target_ms = g_hardware_cfg.mic_la_stable_ms; + boot_la_metrics.gate_elapsed_ms = 0U; + boot_la_metrics.gate_timeout_ms = g_hardware_cfg.mic_la_timeout_ms; + g_ui.setLaMetrics(boot_la_metrics); + g_ui.setHardwareSnapshotRef(&g_hardware.snapshotRef()); + } #if defined(USE_AUDIO) && (USE_AUDIO != 0) g_amp_ready = false; g_amp_scene_active = false; @@ -8085,7 +8363,76 @@ void setup() { Serial.println("[AMP] lazy init (on SCENE_MP3_PLAYER)"); #endif g_camera_scene_active = false; - g_camera_scene_ready = ensureCameraUiInitialized(); + if (!g_ui_started) { + g_camera_scene_ready = false; + Serial.println("[CAM_UI] preload skipped (ui_started=0)"); + } else { + g_camera_scene_ready = ensureCameraUiInitialized(); + } + + if (!g_ui_started) { + Serial.println("[NET] boot skipped (ui_started=0)"); + Serial.println("[WEB] boot skipped (ui_started=0)"); + Serial.println("[SHARE] boot skipped (ui_started=0)"); + } else { + const BootHeapSnapshot post_ui_heap = bootCaptureHeapSnapshot(); + const uint32_t post_ui_free_heap = ESP.getFreeHeap(); + g_boot_network_deferred = shouldDeferBootNetworkBringup(post_ui_heap, post_ui_free_heap); + if (g_boot_network_deferred) { + g_network_cfg.espnow_enabled_on_boot = false; + g_espnow_discovery_runtime_enabled = false; + Serial.printf("[NET] boot deferred reason=%s free_heap=%u largest_internal=%lu\n", + bootNetworkDeferReason(post_ui_heap, post_ui_free_heap), + static_cast(post_ui_free_heap), + static_cast(post_ui_heap.heap_internal_largest)); + Serial.println("[WEB] boot deferred (network_boot_deferred=1)"); + Serial.println("[SHARE] boot skipped (network_boot_deferred=1)"); + } else { + g_file_share_service.begin(g_network_cfg.hostname, g_espnow_device_name); + g_network.begin(g_network_cfg.hostname); + if (kBootEspNowOnlyMode) { + g_network.configureFallbackAp("", ""); + g_network.configureLocalPolicy("", "", false, g_network_cfg.local_retry_ms, false); + Serial.println("[NET] wifi+web disabled (espnow_only_mode=1)"); + } else { + g_network.configureFallbackAp(g_network_cfg.ap_default_ssid, g_network_cfg.ap_default_password); + g_network.configureLocalPolicy(g_network_cfg.local_ssid, + g_network_cfg.local_password, + g_network_cfg.force_ap_if_not_local, + g_network_cfg.local_retry_ms, + g_network_cfg.pause_local_retry_when_ap_client); + if ((g_setup_mode || kSprintDiagMode) && g_network_cfg.ap_default_ssid[0] != '\0') { + g_network.startAp(g_network_cfg.ap_default_ssid, g_network_cfg.ap_default_password); + } + if (g_network_cfg.local_ssid[0] != '\0') { + const bool connect_started = g_network.connectSta(g_network_cfg.local_ssid, g_network_cfg.local_password); + Serial.printf("[NET] boot wifi target=%s started=%u\n", + g_network_cfg.local_ssid, + connect_started ? 1U : 0U); + } + } + if (g_network_cfg.espnow_enabled_on_boot) { + if (g_network.enableEspNow()) { + for (uint8_t index = 0U; index < g_network_cfg.espnow_boot_peer_count; ++index) { + const char* peer = g_network_cfg.espnow_boot_peers[index]; + if (peer == nullptr || peer[0] == '\0') { + continue; + } + const bool ok = g_network.addEspNowPeer(peer); + Serial.printf("[NET] boot peer add mac=%s ok=%u\n", peer, ok ? 1U : 0U); + } + } + } else { + Serial.println("[NET] ESP-NOW boot disabled by APP_ESPNOW config"); + } + if (!kBootEspNowOnlyMode) { + setupWebUi(); + } else { + Serial.println("[WEB] disabled (espnow_only_mode=1)"); + } + } + } + g_next_espnow_discovery_ms = millis() + 2000U; // Boot directly to Amiga UI Shell (skip default scenario rendering) // refreshSceneIfNeeded(true); // startPendingAudioIfAny(); @@ -8121,7 +8468,9 @@ void setup() { g_amiga_shell.setRuntimeBridge(&kAmigaAppRuntimeBridge); g_amiga_shell.setTouchEmulationMode(true); - if (g_amiga_shell_mode_boot) { + if (!g_ui_started) { + Serial.println("[BOOT] skip app_coordinator.begin (ui_started=0)"); + } else if (g_amiga_shell_mode_boot) { Serial.println("[BOOT] skip app_coordinator.begin (amiga_shell_mode=1)"); } else { g_app_coordinator.begin(&g_runtime_services); @@ -8129,6 +8478,16 @@ void setup() { } void runRuntimeIteration(uint32_t now_ms) { + if (!g_ui_started) { + ButtonEvent event; + while (g_buttons.pollEvent(&event)) { + Serial.printf("[BOOT] ui_unavailable button=%u long=%u\n", + static_cast(event.key), + event.long_press ? 1U : 0U); + } + yield(); + return; + } ButtonEvent event; while (g_buttons.pollEvent(&event)) { const uint32_t event_ms = (event.ms != 0U) ? event.ms : now_ms; @@ -8142,6 +8501,9 @@ void runRuntimeIteration(uint32_t now_ms) { ui_event.key = event.key; ui_event.long_press = event.long_press; g_ui.submitInputEvent(ui_event); + if (g_amiga_shell_mode_boot) { + g_amiga_shell.handleButtonInput(event.key); + } if (g_camera_scene_active) { (void)dispatchCameraSceneButton(event.key, event.long_press); } @@ -8167,6 +8529,9 @@ void runRuntimeIteration(uint32_t now_ms) { ui_event.touch_y = touch.y; ui_event.touch_pressed = touch.touched; g_ui.submitInputEvent(ui_event); + if (g_amiga_shell_mode_boot && touch.touched) { + g_amiga_shell.handleTouchInput(touch.x, touch.y); + } } else { UiInputEvent ui_event = {}; ui_event.type = UiInputEventType::kTouch; @@ -8175,6 +8540,9 @@ void runRuntimeIteration(uint32_t now_ms) { ui_event.touch_pressed = false; g_ui.submitInputEvent(ui_event); } + if (g_amiga_shell_mode_boot) { + g_amiga_shell.onTick(16U); + } const uint32_t network_started_us = perfMonitor().beginSample(); g_network.update(now_ms); @@ -8355,23 +8723,24 @@ void runRuntimeIteration(uint32_t now_ms) { void loop() { const uint32_t now_ms = millis(); pollSerialCommands(now_ms); - - // Route to AmigaUI Shell or traditional app coordinator based on boot mode - if (g_amiga_shell_mode_boot) { + + if (g_safe_diagnostic_mode) { + runSafeDiagnosticIteration(now_ms); + return; + } + if (!g_ui_started) { ButtonEvent event; while (g_buttons.pollEvent(&event)) { - g_amiga_shell.handleButtonInput(event.key); + Serial.printf("[BOOT] ui_unavailable button=%u long=%u\n", + static_cast(event.key), + event.long_press ? 1U : 0U); } - - TouchPoint touch; - if (g_touch.poll(&touch) && touch.touched) { - g_amiga_shell.handleTouchInput(touch.x, touch.y); - } - - // AmigaUI Shell mode: draw grid launcher with app icons - g_amiga_shell.onTick(16); // ~60 FPS tick (16ms) - lv_task_handler(); // Process LVGL rendering (critical for display update!) yield(); + return; + } + + if (g_amiga_shell_mode_boot) { + runRuntimeIteration(now_ms); } else { // Traditional scenario/story mode g_app_coordinator.tick(now_ms);