From 6495f0e1077adb8a9be15e6a3960430d9329c544 Mon Sep 17 00:00:00 2001 From: chenjintao Date: Tue, 25 Aug 2026 14:09:44 +0800 Subject: [PATCH] fix(connectivity): harden BLE reprovisioning under memory pressure --- .../runtime/app_composition_default_runtime.c | 88 ++++++--- .../control_plane/include/control_plane.h | 9 + .../control_plane_connectivity_runtime.c | 187 +++++++++++++++--- docs/ble-provisioning-memory-recovery.md | 74 +++++++ main/Kconfig.projbuild | 21 ++ sdkconfig.defaults | 2 + 6 files changed, 323 insertions(+), 58 deletions(-) create mode 100644 docs/ble-provisioning-memory-recovery.md diff --git a/components/app_composition/src/runtime/app_composition_default_runtime.c b/components/app_composition/src/runtime/app_composition_default_runtime.c index 922707e..543ef65 100644 --- a/components/app_composition/src/runtime/app_composition_default_runtime.c +++ b/components/app_composition/src/runtime/app_composition_default_runtime.c @@ -105,22 +105,37 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) esp_err_to_name(battery_guard_err)); } - app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_AUDIO_INIT, - "audio_init", - "boot_sound_policy_begin"); - app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_BOOT_SOUND, - "boot_sound", - "play_boot_sound_with_timeout"); - esp_err_t boot_sound_err = - control_plane_play_boot_sound(CONFIG_TQ_BOOT_SOUND_TIMEOUT_MS); - if (boot_sound_err != ESP_OK) { - ESP_LOGW(TAG, - "Boot sound unavailable, continue boot: %s", - esp_err_to_name(boot_sound_err)); + bool provisioning_recovery_boot = + control_plane_take_mini_program_recovery_boot_request(); + + if (provisioning_recovery_boot) { + app_composition_log_stage_skipped(APP_COMPOSITION_BOOT_STAGE_AUDIO_INIT, + "audio_init", + "ble_recovery_boot"); + app_composition_log_stage_skipped(APP_COMPOSITION_BOOT_STAGE_BOOT_SOUND, + "boot_sound", + "ble_recovery_boot"); + app_composition_log_stage_skipped(APP_COMPOSITION_BOOT_STAGE_AUDIO_READY, + "audio_ready", + "ble_recovery_boot"); + } else { + app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_AUDIO_INIT, + "audio_init", + "boot_sound_policy_begin"); + app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_BOOT_SOUND, + "boot_sound", + "play_boot_sound_with_timeout"); + esp_err_t boot_sound_err = + control_plane_play_boot_sound(CONFIG_TQ_BOOT_SOUND_TIMEOUT_MS); + if (boot_sound_err != ESP_OK) { + ESP_LOGW(TAG, + "Boot sound unavailable, continue boot: %s", + esp_err_to_name(boot_sound_err)); + } + app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_AUDIO_READY, + "audio_ready", + "boot_sound_policy_end_keep_runtime"); } - app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_AUDIO_READY, - "audio_ready", - "boot_sound_policy_end_keep_runtime"); #if CONFIG_TQ_SCREEN_ENABLE app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_LCD_DEVICE, @@ -131,7 +146,7 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) "lcd_device", lcd_err); bool lcd_ready = (lcd_err == ESP_OK); - if (lcd_ready) { + if (lcd_ready && !provisioning_recovery_boot) { app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_LCD_RENDER_LOOP, "lcd_render_loop", "control_plane_start_lcd_render_worker"); @@ -142,10 +157,14 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) if (lcd_worker_err != ESP_OK) { lcd_ready = false; } - } else { + } else if (!lcd_ready) { app_composition_log_stage_skipped(APP_COMPOSITION_BOOT_STAGE_LCD_RENDER_LOOP, "lcd_render_loop", "lcd_device_unavailable"); + } else { + app_composition_log_stage_skipped(APP_COMPOSITION_BOOT_STAGE_LCD_RENDER_LOOP, + "lcd_render_loop", + "ble_recovery_boot"); } #else app_composition_log_stage_skipped(APP_COMPOSITION_BOOT_STAGE_LCD_DEVICE, @@ -173,15 +192,17 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) } #if CONFIG_TQ_FACTORY_CLI_ENABLE - err = control_plane_factory_cli_start(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Factory CLI start failed: %s", esp_err_to_name(err)); - return err; + if (!provisioning_recovery_boot) { + err = control_plane_factory_cli_start(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Factory CLI start failed: %s", esp_err_to_name(err)); + return err; + } } #endif #if CONFIG_TQ_SCREEN_ENABLE - if (lcd_ready) { + if (lcd_ready && !provisioning_recovery_boot) { esp_err_t expression_err = control_plane_request_boot_expression(); if (expression_err != ESP_OK) { ESP_LOGW(TAG, @@ -192,14 +213,21 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) #endif #if CONFIG_TQ_BOOT_TURNTABLE_CALIBRATION_ENABLE - app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_TURNTABLE_CALIBRATION, - "turntable_calibration", - "control_plane_calibrate"); - esp_err_t calibration_err = control_plane_calibrate(); - if (calibration_err != ESP_OK) { - ESP_LOGW(TAG, - "Boot turntable calibration failed, continue degraded: %s", - esp_err_to_name(calibration_err)); + if (provisioning_recovery_boot) { + app_composition_log_stage_skipped( + APP_COMPOSITION_BOOT_STAGE_TURNTABLE_CALIBRATION, + "turntable_calibration", + "ble_recovery_boot"); + } else { + app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_TURNTABLE_CALIBRATION, + "turntable_calibration", + "control_plane_calibrate"); + esp_err_t calibration_err = control_plane_calibrate(); + if (calibration_err != ESP_OK) { + ESP_LOGW(TAG, + "Boot turntable calibration failed, continue degraded: %s", + esp_err_to_name(calibration_err)); + } } #else app_composition_log_stage_skipped(APP_COMPOSITION_BOOT_STAGE_TURNTABLE_CALIBRATION, diff --git a/components/control_plane/include/control_plane.h b/components/control_plane/include/control_plane.h index 36bb3ea..cc25934 100644 --- a/components/control_plane/include/control_plane.h +++ b/components/control_plane/include/control_plane.h @@ -236,6 +236,15 @@ esp_err_t control_plane_calibrate(void); esp_err_t control_plane_bootstrap_connectivity(const char *seed_ssid, const char *seed_password, int connect_timeout_ms); + +/** + * @brief Consume a one-shot request to use the low-memory BLE recovery boot. + * + * The request survives a software restart in RTC no-init memory. A true result + * is latched for the current boot so connectivity skips STA reconnect and + * starts BLE provisioning before full runtime workers. + */ +bool control_plane_take_mini_program_recovery_boot_request(void); bool control_plane_is_sta_connected(void); esp_err_t control_plane_stop_mini_program_provisioning(int timeout_ms); bool control_plane_is_mini_program_provisioning_running(void); diff --git a/components/control_plane/src/runtime/control_plane_connectivity_runtime.c b/components/control_plane/src/runtime/control_plane_connectivity_runtime.c index a4fd794..1e8b84a 100644 --- a/components/control_plane/src/runtime/control_plane_connectivity_runtime.c +++ b/components/control_plane/src/runtime/control_plane_connectivity_runtime.c @@ -9,6 +9,7 @@ #include #include "control_plane_internal.h" +#include "esp_attr.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/event_groups.h" @@ -40,26 +41,103 @@ #define CONFIG_TQ_MINI_PROGRAM_PROVISIONING_CONNECT_TIMEOUT_MS 30000 #endif +#ifndef CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_FREE_INTERNAL_BYTES +#define CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_FREE_INTERNAL_BYTES 49152 +#endif + +#ifndef CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES +#define CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES 12288 +#endif + #define CONTROL_PLANE_WIFI_SUCCESS_PROMPT_FILE "/littlefs/audio_prompts/network_connect_success.opus" #define CONTROL_PLANE_STA_CONNECT_MAX_ATTEMPTS 3 #define CONTROL_PLANE_STA_RETRY_BASE_DELAY_MS 300 #define CONTROL_PLANE_PROVISIONING_QUEUE_LENGTH 1 -#define CONTROL_PLANE_PROVISIONING_TASK_STACK_WORDS 4096 +#define CONTROL_PLANE_PROVISIONING_TASK_STACK_BYTES 4096 #define CONTROL_PLANE_PROVISIONING_TASK_PRIORITY 5 #define CONTROL_PLANE_PROVISIONING_STOP_REQUESTED_BIT BIT0 #define CONTROL_PLANE_PROVISIONING_WORKER_DONE_BIT BIT1 #define CONTROL_PLANE_PROVISIONING_POLL_MS 100 +#define CONTROL_PLANE_PROVISIONING_WAIT_LOG_MS 10000 #define CONTROL_PLANE_PROVISIONING_NOTIFY_SETTLE_MS 750 #define CONTROL_PLANE_PROVISIONING_TRANSPORT_STOP_MS 2000 +#define CONTROL_PLANE_PROVISIONING_RUNTIME_QUIESCE_MS 2000 +#define CONTROL_PLANE_PROVISIONING_RESTART_DELAY_MS 100 +#define CONTROL_PLANE_PROVISIONING_RECOVERY_MAGIC 0x5451424cU static const char *TAG = "control_plane_rt"; static bool s_wifi_success_prompt_played; +static bool s_provisioning_recovery_boot; +RTC_NOINIT_ATTR static uint32_t s_provisioning_recovery_marker; +RTC_NOINIT_ATTR static uint32_t s_provisioning_recovery_marker_inverse; static portMUX_TYPE s_provisioning_lock = portMUX_INITIALIZER_UNLOCKED; static QueueHandle_t s_provisioning_queue; static EventGroupHandle_t s_provisioning_events; static TaskHandle_t s_provisioning_task; +bool control_plane_take_mini_program_recovery_boot_request(void) +{ + bool requested = + s_provisioning_recovery_marker == CONTROL_PLANE_PROVISIONING_RECOVERY_MAGIC && + s_provisioning_recovery_marker_inverse == + ~CONTROL_PLANE_PROVISIONING_RECOVERY_MAGIC; + s_provisioning_recovery_marker = 0U; + s_provisioning_recovery_marker_inverse = 0U; + s_provisioning_recovery_boot = requested; + if (requested) { + ESP_LOGW(TAG, "Consumed one-shot low-memory BLE recovery boot request"); + } + return requested; +} + +static void control_plane_request_provisioning_recovery_restart( + const domain_ota_memory_headroom_t *headroom) +{ + s_provisioning_recovery_marker = CONTROL_PLANE_PROVISIONING_RECOVERY_MAGIC; + s_provisioning_recovery_marker_inverse = + ~CONTROL_PLANE_PROVISIONING_RECOVERY_MAGIC; + ESP_LOGW(TAG, + "Restarting into low-memory BLE recovery boot: free_internal=%u largest_internal=%u largest_dma=%u", + headroom != NULL ? (unsigned)headroom->free_internal : 0U, + headroom != NULL ? (unsigned)headroom->largest_internal : 0U, + headroom != NULL ? (unsigned)headroom->largest_dma : 0U); + vTaskDelay(pdMS_TO_TICKS(CONTROL_PLANE_PROVISIONING_RESTART_DELAY_MS)); + domain_runtime_restart(); +} + +static esp_err_t control_plane_check_provisioning_memory_headroom(void) +{ + domain_ota_memory_headroom_t headroom = {0}; + esp_err_t err = domain_runtime_ota_get_memory_headroom(&headroom); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Read BLE provisioning memory headroom failed: %s", + esp_err_to_name(err)); + return err; + } + + ESP_LOGI(TAG, + "BLE provisioning memory headroom: free_internal=%u largest_internal=%u largest_dma=%u", + (unsigned)headroom.free_internal, + (unsigned)headroom.largest_internal, + (unsigned)headroom.largest_dma); + if (headroom.free_internal >= + CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_FREE_INTERNAL_BYTES && + headroom.largest_dma >= + CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES) { + return ESP_OK; + } + + ESP_LOGE(TAG, + "BLE provisioning memory headroom insufficient: need free_internal>=%u largest_dma>=%u", + (unsigned)CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_FREE_INTERNAL_BYTES, + (unsigned)CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES); + if (!s_provisioning_recovery_boot) { + control_plane_request_provisioning_recovery_restart(&headroom); + } + return ESP_ERR_NO_MEM; +} + static int elapsed_ms_since(TickType_t start_tick) { TickType_t elapsed_ticks = xTaskGetTickCount() - start_tick; @@ -225,6 +303,19 @@ static void control_plane_finish_provisioning_ui(void) } } +static void control_plane_show_provisioning_ui(void) +{ + const domain_expression_request_t request = { + .id = DOMAIN_EXPRESSION_WIFI_CONNECTING, + .trigger = DOMAIN_EXPRESSION_TRIGGER_WIFI_BOOTSTRAP, + }; + esp_err_t err = domain_runtime_request_expression(&request); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGW(TAG, "Wi-Fi provisioning display unavailable: %s", + esp_err_to_name(err)); + } +} + static void control_plane_provisioning_event_callback( const domain_wifi_provisioning_event_t *event, void *user_data) @@ -254,12 +345,24 @@ static void control_plane_provisioning_worker(void *arg) { (void)arg; bool provisioning_succeeded = false; + TickType_t last_wait_log_tick = xTaskGetTickCount(); + TickType_t wait_log_ticks = + pdMS_TO_TICKS(CONTROL_PLANE_PROVISIONING_WAIT_LOG_MS); + if (wait_log_ticks == 0U) { + wait_log_ticks = 1U; + } while (!control_plane_provisioning_stop_requested()) { domain_wifi_provisioning_event_t event = {0}; if (xQueueReceive(s_provisioning_queue, &event, pdMS_TO_TICKS(CONTROL_PLANE_PROVISIONING_POLL_MS)) != pdTRUE) { + TickType_t now_tick = xTaskGetTickCount(); + if ((now_tick - last_wait_log_tick) >= wait_log_ticks) { + ESP_LOGI(TAG, + "BLE provisioning active; waiting for mini-program credentials"); + last_wait_log_tick = now_tick; + } continue; } @@ -319,7 +422,14 @@ static void control_plane_provisioning_worker(void *arg) (void)xEventGroupSetBits(s_provisioning_events, CONTROL_PLANE_PROVISIONING_WORKER_DONE_BIT); - if (provisioning_succeeded && stop_err == ESP_OK) { + if (provisioning_succeeded && stop_err == ESP_OK && + s_provisioning_recovery_boot) { + ESP_LOGI(TAG, + "BLE recovery provisioning completed; restarting into normal runtime"); + s_provisioning_recovery_boot = false; + vTaskDelay(pdMS_TO_TICKS(CONTROL_PLANE_PROVISIONING_RESTART_DELAY_MS)); + domain_runtime_restart(); + } else if (provisioning_succeeded && stop_err == ESP_OK) { control_plane_finish_provisioning_ui(); ESP_LOGI(TAG, "BLE provisioning completed; starting deferred runtime workers"); @@ -365,31 +475,31 @@ static esp_err_t control_plane_start_mini_program_provisioning(void) CONTROL_PLANE_PROVISIONING_STOP_REQUESTED_BIT | CONTROL_PLANE_PROVISIONING_WORKER_DONE_BIT); + esp_err_t err = control_plane_check_provisioning_memory_headroom(); + if (err != ESP_OK) { + return err; + } + + err = domain_runtime_mini_program_provisioning_start( + control_plane_provisioning_event_callback, + NULL); + if (err != ESP_OK) { + return err; + } + BaseType_t created = xTaskCreate(control_plane_provisioning_worker, "mini_prog_wifi", - CONTROL_PLANE_PROVISIONING_TASK_STACK_WORDS, + CONTROL_PLANE_PROVISIONING_TASK_STACK_BYTES, NULL, CONTROL_PLANE_PROVISIONING_TASK_PRIORITY, &s_provisioning_task); if (created != pdPASS) { s_provisioning_task = NULL; + ESP_LOGE(TAG, "Create mini-program provisioning worker failed after BLE start"); + (void)domain_runtime_mini_program_provisioning_stop( + CONTROL_PLANE_PROVISIONING_TRANSPORT_STOP_MS); return ESP_ERR_NO_MEM; } - - esp_err_t err = domain_runtime_mini_program_provisioning_start( - control_plane_provisioning_event_callback, - NULL); - if (err != ESP_OK) { - (void)xEventGroupSetBits(s_provisioning_events, - CONTROL_PLANE_PROVISIONING_STOP_REQUESTED_BIT); - (void)xEventGroupWaitBits( - s_provisioning_events, - CONTROL_PLANE_PROVISIONING_WORKER_DONE_BIT, - pdFALSE, - pdTRUE, - pdMS_TO_TICKS(CONTROL_PLANE_PROVISIONING_TRANSPORT_STOP_MS)); - return err; - } ESP_LOGI(TAG, "Mini-program Wi-Fi provisioning started"); return ESP_OK; #endif @@ -443,12 +553,24 @@ esp_err_t control_plane_enter_mini_program_reprovisioning(const char *reason) ESP_LOGW(TAG, "Entering mini-program BLE reprovisioning: reason=%s", reason != NULL ? reason : "unspecified"); control_plane_wifi_set_keep_awake(true); + control_plane_set_runtime_stop_requested(true); + control_plane_set_ptt_active(false); + (void)control_plane_local_wake_stop( + CONTROL_PLANE_PROVISIONING_RUNTIME_QUIESCE_MS); + (void)control_plane_stop_multimodal_client(); + (void)domain_runtime_multimodal_audio_stop_downstream_playback(); + domain_runtime_multimodal_audio_deinit(); + control_plane_show_provisioning_ui(); + esp_err_t err = domain_runtime_wifi_portal_stop_sta(); if (err == ESP_OK) { err = control_plane_start_mini_program_provisioning(); } if (err != ESP_OK) { control_plane_wifi_set_keep_awake(false); + if (!s_provisioning_recovery_boot) { + control_plane_request_provisioning_recovery_restart(NULL); + } } return err; #endif @@ -469,7 +591,10 @@ esp_err_t control_plane_bootstrap_connectivity(const char *seed_ssid, return err; } - if (domain_runtime_wifi_portal_has_saved_credentials()) { + if (s_provisioning_recovery_boot) { + ESP_LOGW(TAG, + "Low-memory recovery boot skips STA reconnect and enters BLE provisioning"); + } else if (domain_runtime_wifi_portal_has_saved_credentials()) { int timeout_ms = control_plane_normalize_connect_timeout_ms(connect_timeout_ms, 0); err = control_plane_connect_sta_with_feedback(timeout_ms, true); if (err == ESP_OK) { @@ -480,20 +605,26 @@ esp_err_t control_plane_bootstrap_connectivity(const char *seed_ssid, esp_err_to_name(err)); } - const domain_expression_request_t connecting_request = { - .id = DOMAIN_EXPRESSION_WIFI_CONNECTING, - .trigger = DOMAIN_EXPRESSION_TRIGGER_WIFI_BOOTSTRAP, - }; - esp_err_t display_err = domain_runtime_request_expression(&connecting_request); - if (display_err != ESP_OK && display_err != ESP_ERR_INVALID_STATE) { - ESP_LOGW(TAG, "Wi-Fi connecting QR display unavailable: %s", - esp_err_to_name(display_err)); - } + control_plane_show_provisioning_ui(); control_plane_wifi_set_keep_awake(true); err = control_plane_start_mini_program_provisioning(); + if (err == ESP_OK && s_provisioning_recovery_boot) { + esp_err_t lcd_err = control_plane_start_lcd_render_worker(); + if (lcd_err != ESP_OK && lcd_err != ESP_ERR_NOT_SUPPORTED) { + ESP_LOGW(TAG, + "Start LCD after BLE recovery provisioning failed: %s", + esp_err_to_name(lcd_err)); + } + } if (err != ESP_OK) { control_plane_wifi_set_keep_awake(false); + if (s_provisioning_recovery_boot) { + ESP_LOGE(TAG, + "Low-memory BLE recovery boot failed; resume normal boot: %s", + esp_err_to_name(err)); + s_provisioning_recovery_boot = false; + } ESP_LOGE(TAG, "BLE provisioning unavailable; SoftAP fallback disabled: %s", esp_err_to_name(err)); } diff --git a/docs/ble-provisioning-memory-recovery.md b/docs/ble-provisioning-memory-recovery.md new file mode 100644 index 0000000..15a6439 --- /dev/null +++ b/docs/ble-provisioning-memory-recovery.md @@ -0,0 +1,74 @@ +# BLE Provisioning Memory Recovery + +## Failure Mode + +Runtime Wi-Fi recovery can fall back to mini-program BLE provisioning after +saved network candidates fail. The BLE controller requires contiguous internal +DMA-capable memory even when the NimBLE host is configured to use external RAM. +Starting BLE late in a fragmented runtime previously allowed a controller +allocation to fail inside ROM code, followed by an assertion and an interrupt +watchdog reset. + +## Runtime Transition + +Before runtime reprovisioning starts BLE, the control plane now: + +1. Blocks new runtime work through the runtime stop gate. +2. Clears active PTT state. +3. Stops local wake detection and its owned capture path. +4. Stops the multimodal client and downstream playback. +5. Fully deinitializes the multimodal audio pipeline. +6. Switches the running LCD renderer to the provisioning QR expression. +7. Stops the Wi-Fi station. + +The provisioning queue and event group are allocated first. Memory headroom is +then checked before entering the BLE controller. NimBLE is initialized before +the 4096-byte provisioning worker is created, preserving one additional +contiguous internal block during controller startup. + +## Memory Gate + +BLE initialization proceeds only when both configured thresholds are met: + +```text +CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_FREE_INTERNAL_BYTES=49152 +CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES=12288 +``` + +Both current values and thresholds are logged before BLE startup. These values +are conservative guards against the observed 4096-byte controller allocation +failure and leave room for the controller's preceding allocations. + +## Recovery Boot + +If the runtime cannot provide enough headroom, the control plane writes a +magic value and its inverse to RTC no-init memory, waits 100 ms for logs, and +performs a controlled software restart. The marker is one-shot and does not +write flash. + +On the next boot, application composition keeps the battery safety service but +defers the boot sound, LCD render worker, factory CLI worker, and turntable +calibration. Connectivity skips the saved-credential STA attempt and starts +BLE provisioning before full runtime workers. The LCD renderer starts only +after BLE has secured its controller memory. Successful provisioning tears +down BLE and restarts into the normal boot sequence. + +While provisioning is active, the worker logs every 10 seconds that it is +waiting for mini-program credentials. Runtime transitions keep the LCD active +on the provisioning QR expression, so a healthy wait state is distinguishable +from a stalled device. + +If recovery-boot BLE startup itself fails, the one-shot mode is cleared and +normal runtime startup resumes instead of entering a restart loop. + +## Validation + +Required device validation covers: + +- normal boot provisioning with no saved credentials; +- runtime Wi-Fi loss followed by failed candidate traversal; +- low-memory fallback through the one-shot recovery boot; +- successful provisioning followed by a normal reboot; +- repeated Wi-Fi/BLE transitions under audio and local-wake load; +- confirmation that no `BLE_INIT: Malloc failed`, ROM assertion, or interrupt + watchdog reset occurs. diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 7d28267..6e988b7 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -180,6 +180,27 @@ menu "TQ Cat Configuration" default 2 range 1 10 + config TQ_MINI_PROGRAM_PROVISIONING_MIN_FREE_INTERNAL_BYTES + int "Minimum free internal memory before BLE provisioning" + depends on TQ_MINI_PROGRAM_PROVISIONING_ENABLE + default 49152 + range 16384 131072 + help + BLE controller initialization requires internal DMA-capable + memory even when the NimBLE host uses external RAM. Runtime + reprovisioning performs a controlled recovery boot when free + internal memory remains below this threshold after teardown. + + config TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES + int "Minimum largest DMA block before BLE provisioning" + depends on TQ_MINI_PROGRAM_PROVISIONING_ENABLE + default 12288 + range 4096 65536 + help + Minimum contiguous DMA-capable block required before entering + the BLE controller. This headroom prevents the controller ROM + from asserting after a late allocation failure. + endmenu menu "Runtime Diagnostics" diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 130b0ca..f593fee 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -19,6 +19,8 @@ CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_EXTERNAL=y CONFIG_TQ_MINI_PROGRAM_PROVISIONING_ENABLE=y CONFIG_TQ_MINI_PROGRAM_PROVISIONING_CONNECT_TIMEOUT_MS=30000 CONFIG_TQ_MINI_PROGRAM_PROVISIONING_RECONNECT_FAILURES=2 +CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_FREE_INTERNAL_BYTES=49152 +CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES=12288 CONFIG_TQ_WIFI_CONNECT_CYCLE_TIMEOUT_MS=20000 # External PSRAM (ESP32-S3 octal mode)