fix 3.5 reboot after WiFi scan list OOM.

Cap list to top 8 by RSSI, lazy status/password UI, single-button rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-25 20:06:12 +08:00
parent d9773a8b97
commit 1ca8d32141
3 changed files with 198 additions and 132 deletions

View File

@@ -1,5 +1,5 @@
name: CYD wifitest (cyd_35r)
git: 9cbbb29
git: d9773a8
date: 2026-10-25
board: ESP32-3248S035 (ST7796)
flash_size: 4MB
@@ -11,5 +11,5 @@ offsets:
sha256:
bootloader.bin 3D234A7471F67B013686DABD4DEE7C1FA915C9928463616A94BC9297ACF1ABF8
boot_app0.bin F94C5D786A7A8FAB06AC5D10E33BF37711A6697636DC037559EA19CC410A17F0
firmware.bin DDB6B8EB5091F9C201A1696CEC05BA68E93A512EE7388C66EC7207850B74FC85
firmware.bin 07AE55AB62FE6A53D343B0DDB1E2FE422581F6B02CD1E62E3161AE66C3056897
partitions.bin 148B959CBFF1C38AA8E1D5C0BA9D612C54997B945E56A63F41223EEF650653A1

Binary file not shown.

View File

@@ -18,6 +18,9 @@ static constexpr uint32_t COL_BTN = 0x243140;
static constexpr uint32_t COL_BTN_ACC = 0x1A4A6E;
static constexpr uint32_t COL_BTN_WARN = 0x6B3A1E;
/* 3.5" LV_MEM=32K: dual screens + 16 heavy rows OOMs. Cap list hard. */
static constexpr int WIFI_UI_LIST_MAX = 8;
static lv_obj_t *scr_status = nullptr;
static lv_obj_t *scr_setup = nullptr;
static lv_obj_t *lbl_title = nullptr;
@@ -42,9 +45,24 @@ static char g_pick_ssid[WIFI_PROV_SSID_MAX + 1] = {};
static bool g_on_setup = false;
static bool g_pass_visible = false;
static bool g_scan_pending = false;
static bool g_auto_scan_armed = false;
static uint32_t g_auto_scan_at = 0;
static WifiProvState g_last_shown = WIFI_PROV_IDLE;
static uint32_t g_last_status_ms = 0;
static void on_scan(lv_event_t *e);
static void on_portal(lv_event_t *e);
static void on_join(lv_event_t *e);
static void on_dlg_back(lv_event_t *e);
static void on_toggle_pass(lv_event_t *e);
static void on_kb(lv_event_t *e);
static void on_change_wifi(lv_event_t *e);
static void on_forget(lv_event_t *e);
static void on_row_join(lv_event_t *e);
static void build_status();
static void build_setup();
static void ensure_pass_dialog();
static void style_btn(lv_obj_t *btn, uint32_t bg) {
lv_obj_set_style_radius(btn, 8, 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(bg), 0);
@@ -59,15 +77,19 @@ static void style_btn(lv_obj_t *btn, uint32_t bg) {
static lv_obj_t *make_btn(lv_obj_t *parent, const char *text, uint32_t bg,
lv_event_cb_t cb, int w, int h) {
lv_obj_t *btn = lv_button_create(parent);
if (!btn) {
return nullptr;
}
lv_obj_set_size(btn, w, h);
style_btn(btn, bg);
lv_obj_t *lab = lv_label_create(btn);
lv_label_set_text(lab, text);
lv_obj_set_style_text_color(lab, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lab, &lv_font_montserrat_14, 0);
lv_obj_center(lab);
/* Label must not steal clicks from the button. */
lv_obj_clear_flag(lab, LV_OBJ_FLAG_CLICKABLE);
if (lab) {
lv_label_set_text(lab, text);
lv_obj_set_style_text_color(lab, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lab, &lv_font_montserrat_14, 0);
lv_obj_center(lab);
lv_obj_clear_flag(lab, LV_OBJ_FLAG_CLICKABLE);
}
if (cb) {
lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, nullptr);
}
@@ -93,8 +115,6 @@ static void set_pass_visible(bool visible) {
}
}
static void on_kb(lv_event_t *e); /* defined below — used by ensure_keyboard */
static void ensure_keyboard() {
if (kb || !scr_setup || !ta_pass) {
return;
@@ -112,7 +132,101 @@ static void ensure_keyboard() {
lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN);
}
static void ensure_pass_dialog() {
if (overlay || !scr_setup) {
return;
}
overlay = lv_obj_create(scr_setup);
if (!overlay) {
Serial.println("[ui] OOM: pass overlay");
return;
}
lv_obj_set_size(overlay, UI_HOR_RES, UI_VER_RES);
lv_obj_set_style_bg_color(overlay, lv_color_hex(0x000000), 0);
lv_obj_set_style_bg_opa(overlay, LV_OPA_70, 0);
lv_obj_set_style_border_width(overlay, 0, 0);
lv_obj_set_style_pad_all(overlay, 0, 0);
lv_obj_clear_flag(overlay, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(overlay, LV_OBJ_FLAG_HIDDEN);
dlg = lv_obj_create(overlay);
if (!dlg) {
return;
}
lv_obj_set_size(dlg, UI_HOR_RES - UW(20), UH(100));
lv_obj_align(dlg, LV_ALIGN_TOP_MID, UX(0), UY(8));
lv_obj_set_style_bg_color(dlg, lv_color_hex(0x15202C), 0);
lv_obj_set_style_border_width(dlg, 0, 0);
lv_obj_set_style_radius(dlg, 10, 0);
lv_obj_set_style_pad_all(dlg, 8, 0);
lv_obj_clear_flag(dlg, LV_OBJ_FLAG_SCROLLABLE);
lbl_dlg_ssid = lv_label_create(dlg);
if (lbl_dlg_ssid) {
lv_label_set_text(lbl_dlg_ssid, "");
lv_obj_set_style_text_color(lbl_dlg_ssid, lv_color_hex(COL_ACCENT), 0);
lv_obj_set_style_text_font(lbl_dlg_ssid, &lv_font_montserrat_14, 0);
lv_obj_align(lbl_dlg_ssid, LV_ALIGN_TOP_LEFT, UX(0), UY(0));
}
lv_obj_t *pass_row = lv_obj_create(dlg);
if (pass_row) {
lv_obj_set_size(pass_row, UI_HOR_RES - UW(40), UH(32));
lv_obj_align(pass_row, LV_ALIGN_TOP_LEFT, UX(0), UY(22));
lv_obj_set_style_bg_opa(pass_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(pass_row, 0, 0);
lv_obj_set_style_pad_all(pass_row, 0, 0);
lv_obj_set_flex_flow(pass_row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(pass_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(pass_row, UW(6), 0);
lv_obj_clear_flag(pass_row, LV_OBJ_FLAG_SCROLLABLE);
ta_pass = lv_textarea_create(pass_row);
if (ta_pass) {
lv_obj_set_size(ta_pass, UI_HOR_RES - UW(40) - UW(62), UH(32));
lv_textarea_set_one_line(ta_pass, true);
lv_textarea_set_password_mode(ta_pass, true);
lv_textarea_set_placeholder_text(ta_pass, "Password");
lv_obj_set_style_text_font(ta_pass, &lv_font_montserrat_14, 0);
}
btn_show_pass = lv_button_create(pass_row);
if (btn_show_pass) {
lv_obj_set_size(btn_show_pass, UW(56), UH(28));
style_btn(btn_show_pass, COL_BTN);
lbl_show_pass = lv_label_create(btn_show_pass);
if (lbl_show_pass) {
lv_label_set_text(lbl_show_pass, "Show");
lv_obj_set_style_text_color(lbl_show_pass, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lbl_show_pass, &lv_font_montserrat_14, 0);
lv_obj_center(lbl_show_pass);
lv_obj_clear_flag(lbl_show_pass, LV_OBJ_FLAG_CLICKABLE);
}
lv_obj_add_event_cb(btn_show_pass, on_toggle_pass, LV_EVENT_CLICKED,
nullptr);
}
}
lv_obj_t *row = lv_obj_create(dlg);
if (row) {
lv_obj_set_size(row, UI_HOR_RES - UW(40), UH(28));
lv_obj_align(row, LV_ALIGN_BOTTOM_MID, UX(0), UY(0));
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_set_style_pad_all(row, 0, 0);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
make_btn(row, "Back", COL_BTN, on_dlg_back, UW(70), UH(26));
make_btn(row, "Join", COL_BTN_ACC, on_join, UW(70), UH(26));
}
}
static void show_password_dialog(const char *ssid) {
ensure_pass_dialog();
strncpy(g_pick_ssid, ssid, WIFI_PROV_SSID_MAX);
g_pick_ssid[WIFI_PROV_SSID_MAX] = '\0';
if (lbl_dlg_ssid) {
@@ -152,6 +266,18 @@ static void on_row_join(lv_event_t *e) {
show_password_dialog(g_items[idx].ssid);
}
static void sort_items_by_rssi() {
for (int i = 0; i + 1 < g_item_n; ++i) {
for (int j = i + 1; j < g_item_n; ++j) {
if (g_items[j].rssi > g_items[i].rssi) {
WifiScanItem tmp = g_items[i];
g_items[i] = g_items[j];
g_items[j] = tmp;
}
}
}
}
static void rebuild_list() {
if (!list_wifi) {
return;
@@ -160,67 +286,57 @@ static void rebuild_list() {
if (g_item_n <= 0) {
lv_obj_t *lab = lv_label_create(list_wifi);
if (!lab) {
return;
if (lab) {
lv_label_set_text(lab, "No networks. Tap Scan.");
lv_obj_set_style_text_color(lab, lv_color_hex(COL_MUTED), 0);
}
lv_label_set_text(lab, "No networks. Tap Scan.");
lv_obj_set_style_text_color(lab, lv_color_hex(COL_MUTED), 0);
return;
}
/* Compact rows (label + Join) — 3.5 UI + keyboard needs LV_MEM headroom. */
for (int i = 0; i < g_item_n; ++i) {
lv_obj_t *row = lv_obj_create(list_wifi);
if (!row) {
Serial.printf("[ui] OOM at wifi row %d/%d\n", i, g_item_n);
sort_items_by_rssi();
const int n =
g_item_n > WIFI_UI_LIST_MAX ? WIFI_UI_LIST_MAX : g_item_n;
/* One button per SSID — avoid nested row/label/button (OOM on 3.5). */
for (int i = 0; i < n; ++i) {
char line[56];
snprintf(line, sizeof(line), "%s %ddBm", g_items[i].ssid,
(int)g_items[i].rssi);
lv_obj_t *btn = lv_button_create(list_wifi);
if (!btn) {
Serial.printf("[ui] OOM at wifi row %d/%d\n", i, n);
break;
}
lv_obj_set_width(row, lv_pct(100));
lv_obj_set_height(row, UH(34));
lv_obj_set_style_bg_color(row, lv_color_hex(0x121A24), 0);
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_set_style_radius(row, 6, 0);
lv_obj_set_style_pad_hor(row, 6, 0);
lv_obj_set_style_pad_ver(row, 2, 0);
lv_obj_set_style_margin_bottom(row, 4, 0);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(row, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_flag(row, LV_OBJ_FLAG_EVENT_BUBBLE);
lv_obj_set_width(btn, lv_pct(100));
lv_obj_set_height(btn, UH(40));
lv_obj_set_style_bg_color(btn, lv_color_hex(0x1A4A6E), 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
lv_obj_set_style_radius(btn, 6, 0);
lv_obj_set_style_border_width(btn, 0, 0);
lv_obj_set_style_shadow_width(btn, 0, 0);
lv_obj_set_style_margin_bottom(btn, 4, 0);
lv_obj_set_ext_click_area(btn, 8);
char line[64];
snprintf(line, sizeof(line), LV_SYMBOL_WIFI " %s %ddBm", g_items[i].ssid,
(int)g_items[i].rssi);
lv_obj_t *lab = lv_label_create(row);
lv_obj_t *lab = lv_label_create(btn);
if (!lab) {
break;
}
lv_label_set_text(lab, line);
lv_label_set_long_mode(lab, LV_LABEL_LONG_DOT);
lv_obj_set_flex_grow(lab, 1);
lv_obj_set_width(lab, lv_pct(92));
lv_obj_set_style_text_color(lab, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lab, &lv_font_montserrat_14, 0);
lv_obj_center(lab);
lv_obj_clear_flag(lab, LV_OBJ_FLAG_CLICKABLE);
lv_obj_t *join = lv_button_create(row);
if (!join) {
break;
}
lv_obj_set_size(join, UW(56), UH(28));
style_btn(join, COL_BTN_ACC);
lv_obj_t *jl = lv_label_create(join);
if (jl) {
lv_label_set_text(jl, "Join");
lv_obj_set_style_text_color(jl, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(jl, &lv_font_montserrat_14, 0);
lv_obj_center(jl);
lv_obj_clear_flag(jl, LV_OBJ_FLAG_CLICKABLE);
}
lv_obj_add_event_cb(join, on_row_join, LV_EVENT_CLICKED,
lv_obj_add_event_cb(btn, on_row_join, LV_EVENT_CLICKED,
(void *)(intptr_t)i);
}
if (g_item_n > WIFI_UI_LIST_MAX) {
Serial.printf("[ui] showing top %d of %d by RSSI\n", n, g_item_n);
}
}
static void do_scan() {
@@ -330,6 +446,9 @@ static void refresh_status_labels() {
}
static void build_status() {
if (scr_status) {
return;
}
scr_status = lv_obj_create(NULL);
lv_obj_set_style_bg_color(scr_status, lv_color_hex(COL_BG), 0);
lv_obj_set_style_bg_opa(scr_status, LV_OPA_COVER, 0);
@@ -376,6 +495,9 @@ static void build_status() {
}
static void build_setup() {
if (scr_setup) {
return;
}
scr_setup = lv_obj_create(NULL);
lv_obj_set_style_bg_color(scr_setup, lv_color_hex(COL_BG), 0);
lv_obj_set_style_bg_opa(scr_setup, LV_OPA_COVER, 0);
@@ -404,7 +526,6 @@ static void build_setup() {
LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(bar, UW(8), 0);
lv_obj_clear_flag(bar, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(bar, LV_OBJ_FLAG_CLICKABLE);
make_btn(bar, "Scan", COL_BTN_ACC, on_scan, UW(90), UH(42));
make_btn(bar, "Phone AP", COL_BTN, on_portal, UW(110), UH(42));
@@ -424,101 +545,40 @@ static void build_setup() {
lv_obj_set_flex_align(list_wifi, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START);
lv_obj_set_scroll_dir(list_wifi, LV_DIR_VER);
// 必须 CLICKABLE,否则列表区域收不到拖动手势,只有点到子按钮才可能滚
lv_obj_add_flag(list_wifi, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(list_wifi, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_flag(list_wifi, LV_OBJ_FLAG_SCROLL_ELASTIC);
lv_obj_add_flag(list_wifi, LV_OBJ_FLAG_SCROLL_MOMENTUM);
// 密码层
overlay = lv_obj_create(scr_setup);
lv_obj_set_size(overlay, UI_HOR_RES, UI_VER_RES);
lv_obj_set_style_bg_color(overlay, lv_color_hex(0x000000), 0);
lv_obj_set_style_bg_opa(overlay, LV_OPA_70, 0);
lv_obj_set_style_border_width(overlay, 0, 0);
lv_obj_set_style_pad_all(overlay, 0, 0);
lv_obj_clear_flag(overlay, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(overlay, LV_OBJ_FLAG_HIDDEN);
dlg = lv_obj_create(overlay);
lv_obj_set_size(dlg, UI_HOR_RES - UW(20), UH(100));
lv_obj_align(dlg, LV_ALIGN_TOP_MID, UX(0), UY(8));
lv_obj_set_style_bg_color(dlg, lv_color_hex(0x15202C), 0);
lv_obj_set_style_border_width(dlg, 0, 0);
lv_obj_set_style_radius(dlg, 10, 0);
lv_obj_set_style_pad_all(dlg, 8, 0);
lv_obj_clear_flag(dlg, LV_OBJ_FLAG_SCROLLABLE);
lbl_dlg_ssid = lv_label_create(dlg);
lv_label_set_text(lbl_dlg_ssid, "");
lv_obj_set_style_text_color(lbl_dlg_ssid, lv_color_hex(COL_ACCENT), 0);
lv_obj_set_style_text_font(lbl_dlg_ssid, &lv_font_montserrat_14, 0);
lv_obj_align(lbl_dlg_ssid, LV_ALIGN_TOP_LEFT, UX(0), UY(0));
lv_obj_t *pass_row = lv_obj_create(dlg);
lv_obj_set_size(pass_row, UI_HOR_RES - UW(40), UH(32));
lv_obj_align(pass_row, LV_ALIGN_TOP_LEFT, UX(0), UY(22));
lv_obj_set_style_bg_opa(pass_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(pass_row, 0, 0);
lv_obj_set_style_pad_all(pass_row, 0, 0);
lv_obj_set_flex_flow(pass_row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(pass_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(pass_row, UW(6), 0);
lv_obj_clear_flag(pass_row, LV_OBJ_FLAG_SCROLLABLE);
ta_pass = lv_textarea_create(pass_row);
lv_obj_set_size(ta_pass, UI_HOR_RES - UW(40) - UW(62), UH(32));
lv_textarea_set_one_line(ta_pass, true);
lv_textarea_set_password_mode(ta_pass, true);
lv_textarea_set_placeholder_text(ta_pass, "Password");
lv_obj_set_style_text_font(ta_pass, &lv_font_montserrat_14, 0);
btn_show_pass = lv_button_create(pass_row);
lv_obj_set_size(btn_show_pass, UW(56), UH(28));
style_btn(btn_show_pass, COL_BTN);
lbl_show_pass = lv_label_create(btn_show_pass);
lv_label_set_text(lbl_show_pass, "Show");
lv_obj_set_style_text_color(lbl_show_pass, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lbl_show_pass, &lv_font_montserrat_14, 0);
lv_obj_center(lbl_show_pass);
lv_obj_add_event_cb(btn_show_pass, on_toggle_pass, LV_EVENT_CLICKED, nullptr);
lv_obj_t *row = lv_obj_create(dlg);
lv_obj_set_size(row, UI_HOR_RES - UW(40), UH(28));
lv_obj_align(row, LV_ALIGN_BOTTOM_MID, UX(0), UY(0));
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_set_style_pad_all(row, 0, 0);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
make_btn(row, "Back", COL_BTN, on_dlg_back, UW(70), UH(26));
make_btn(row, "Join", COL_BTN_ACC, on_join, UW(70), UH(26));
/* Keyboard is created lazily in show_password_dialog (saves LV_MEM on 3.5). */
/* Password dialog + keyboard created lazily — keep LV_MEM free for scan list. */
}
void wifi_ui_create() {
build_status();
build_setup();
if (wifi_prov_has_credentials()) {
build_status();
wifi_ui_show_status();
} else {
build_setup();
wifi_ui_show_setup();
set_setup_status("Tap Scan");
set_setup_status("Scanning...");
/* Defer auto-scan so first frame paints; async so UI stays alive. */
g_auto_scan_armed = true;
g_auto_scan_at = millis() + 800;
}
refresh_status_labels();
}
void wifi_ui_show_setup() {
if (!scr_setup) {
build_setup();
}
g_on_setup = true;
hide_password_dialog();
lv_screen_load(scr_setup);
}
void wifi_ui_show_status() {
if (!scr_status) {
build_status();
}
g_on_setup = false;
hide_password_dialog();
refresh_status_labels();
@@ -528,14 +588,21 @@ void wifi_ui_show_status() {
bool wifi_ui_is_setup() { return g_on_setup; }
void wifi_ui_tick() {
if (g_auto_scan_armed && (int32_t)(millis() - g_auto_scan_at) >= 0) {
g_auto_scan_armed = false;
do_scan();
}
if (g_scan_pending) {
const int n = wifi_prov_scan_poll(g_items, WIFI_PROV_MAX_SCAN);
if (n >= 0) {
g_scan_pending = false;
g_item_n = n;
rebuild_list();
char buf[40];
snprintf(buf, sizeof(buf), "Found %d", g_item_n);
char buf[48];
const int shown =
g_item_n > WIFI_UI_LIST_MAX ? WIFI_UI_LIST_MAX : g_item_n;
snprintf(buf, sizeof(buf), "Found %d (top %d)", g_item_n, shown);
set_setup_status(buf);
}
}
@@ -555,7 +622,6 @@ void wifi_ui_tick() {
}
}
/* Throttle status refresh — every 5ms was wasteful and could feel sticky. */
if (!g_on_setup && (millis() - g_last_status_ms >= 500)) {
g_last_status_ms = millis();
refresh_status_labels();