#include "wifi_ui.h" #include "pins.h" #include "wifi_prov.h" #include #include #include #include static constexpr uint32_t COL_BG = 0x0A1016; static constexpr uint32_t COL_INK = 0xEAF0F6; static constexpr uint32_t COL_MUTED = 0x7A8B9C; static constexpr uint32_t COL_LINE = 0x1C2A38; static constexpr uint32_t COL_OK = 0x3DDC97; static constexpr uint32_t COL_BAD = 0xE85D4C; static constexpr uint32_t COL_ACCENT = 0x4FC3F7; 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; static lv_obj_t *lbl_ssid = nullptr; static lv_obj_t *lbl_ip = nullptr; static lv_obj_t *lbl_rssi = nullptr; static lv_obj_t *lbl_hint = nullptr; static lv_obj_t *lbl_setup_status = nullptr; static lv_obj_t *list_wifi = nullptr; static lv_obj_t *overlay = nullptr; static lv_obj_t *dlg = nullptr; static lv_obj_t *lbl_dlg_ssid = nullptr; static lv_obj_t *ta_pass = nullptr; static lv_obj_t *btn_show_pass = nullptr; static lv_obj_t *lbl_show_pass = nullptr; static lv_obj_t *kb = nullptr; static WifiScanItem g_items[WIFI_PROV_MAX_SCAN]; static int g_item_n = 0; 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 WifiUiConnectedCb g_connected_cb = nullptr; 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); lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); lv_obj_set_style_shadow_width(btn, 0, 0); lv_obj_set_style_border_width(btn, 0, 0); lv_obj_set_style_pad_all(btn, 0, 0); lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE); lv_obj_set_ext_click_area(btn, 12); } 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); 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); } return btn; } static void hide_password_dialog() { if (overlay) { lv_obj_add_flag(overlay, LV_OBJ_FLAG_HIDDEN); } if (kb) { lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); } } static void set_pass_visible(bool visible) { g_pass_visible = visible; if (ta_pass) { lv_textarea_set_password_mode(ta_pass, !visible); } if (lbl_show_pass) { lv_label_set_text(lbl_show_pass, visible ? "Hide" : "Show"); } } static void ensure_keyboard() { if (kb || !scr_setup || !ta_pass) { return; } kb = lv_keyboard_create(scr_setup); if (!kb) { Serial.println("[ui] OOM: keyboard"); return; } lv_obj_set_size(kb, UI_HOR_RES, UH(110)); lv_obj_align(kb, LV_ALIGN_BOTTOM_MID, UX(0), UY(0)); lv_keyboard_set_mode(kb, LV_KEYBOARD_MODE_TEXT_LOWER); lv_keyboard_set_textarea(kb, ta_pass); lv_obj_add_event_cb(kb, on_kb, LV_EVENT_ALL, nullptr); 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) { lv_label_set_text(lbl_dlg_ssid, g_pick_ssid); } if (ta_pass) { lv_textarea_set_text(ta_pass, ""); } set_pass_visible(false); ensure_keyboard(); if (overlay) { lv_obj_clear_flag(overlay, LV_OBJ_FLAG_HIDDEN); lv_obj_move_foreground(overlay); } if (kb) { lv_obj_clear_flag(kb, LV_OBJ_FLAG_HIDDEN); lv_obj_move_foreground(kb); } } static void set_setup_status(const char *msg) { if (lbl_setup_status) { lv_label_set_text(lbl_setup_status, msg); } } static void on_row_join(lv_event_t *e) { const int idx = (int)(intptr_t)lv_event_get_user_data(e); if (idx < 0 || idx >= g_item_n) { return; } if (g_items[idx].auth == WIFI_AUTH_OPEN) { set_setup_status("Connecting..."); wifi_prov_connect_to(g_items[idx].ssid, ""); return; } 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; } lv_obj_clean(list_wifi); if (g_item_n <= 0) { lv_obj_t *lab = lv_label_create(list_wifi); if (lab) { lv_label_set_text(lab, "No networks. Tap Scan."); lv_obj_set_style_text_color(lab, lv_color_hex(COL_MUTED), 0); } return; } 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(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); 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_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_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() { if (g_scan_pending || wifi_prov_scan_busy()) { set_setup_status("Scanning..."); return; } set_setup_status("Scanning..."); if (!wifi_prov_scan_start()) { set_setup_status("Scan failed"); return; } g_scan_pending = true; } static void on_scan(lv_event_t *e) { LV_UNUSED(e); Serial.println("[ui] Scan clicked"); do_scan(); } static void on_portal(lv_event_t *e) { LV_UNUSED(e); Serial.println("[ui] Phone AP clicked"); wifi_prov_portal_start(); set_setup_status("Phone: join CYD-Setup"); } static void on_join(lv_event_t *e) { LV_UNUSED(e); char pass_buf[WIFI_PROV_PASS_MAX + 1] = {}; if (ta_pass) { const char *p = lv_textarea_get_text(ta_pass); if (p) { strncpy(pass_buf, p, WIFI_PROV_PASS_MAX); pass_buf[WIFI_PROV_PASS_MAX] = '\0'; } } Serial.printf("[ui] Join \"%s\" pass_len=%u\n", g_pick_ssid, (unsigned)strlen(pass_buf)); hide_password_dialog(); set_setup_status("Connecting..."); wifi_prov_connect_to(g_pick_ssid, pass_buf); } static void on_dlg_back(lv_event_t *e) { LV_UNUSED(e); hide_password_dialog(); } static void on_toggle_pass(lv_event_t *e) { LV_UNUSED(e); set_pass_visible(!g_pass_visible); } static void on_kb(lv_event_t *e) { const lv_event_code_t code = lv_event_get_code(e); if (code == LV_EVENT_READY) { on_join(e); } else if (code == LV_EVENT_CANCEL) { hide_password_dialog(); } } static void on_change_wifi(lv_event_t *e) { LV_UNUSED(e); wifi_ui_show_setup(); do_scan(); } static void on_forget(lv_event_t *e) { LV_UNUSED(e); wifi_prov_clear(); wifi_ui_show_setup(); set_setup_status("Cleared. Scan to setup."); } static void refresh_status_labels() { if (!lbl_ssid) { return; } const WifiProvState st = wifi_prov_state(); if (st == WIFI_PROV_CONNECTED && wifi_prov_is_connected()) { lv_label_set_text(lbl_title, "WiFi Connected"); lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_OK), 0); lv_label_set_text(lbl_ssid, wifi_prov_ssid().c_str()); char ip[40]; snprintf(ip, sizeof(ip), "IP %s", wifi_prov_ip().c_str()); lv_label_set_text(lbl_ip, ip); char rs[24]; snprintf(rs, sizeof(rs), "RSSI %d dBm", (int)wifi_prov_rssi()); lv_label_set_text(lbl_rssi, rs); lv_label_set_text(lbl_hint, "Ready for other projects"); } else if (st == WIFI_PROV_CONNECTING) { lv_label_set_text(lbl_title, "Connecting..."); lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_ACCENT), 0); lv_label_set_text(lbl_ssid, wifi_prov_ssid().c_str()); lv_label_set_text(lbl_ip, "IP —"); lv_label_set_text(lbl_rssi, ""); lv_label_set_text(lbl_hint, "Please wait"); } else if (st == WIFI_PROV_FAILED) { lv_label_set_text(lbl_title, "Connect failed"); lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_BAD), 0); lv_label_set_text(lbl_ssid, wifi_prov_ssid().c_str()); lv_label_set_text(lbl_ip, "IP —"); lv_label_set_text(lbl_rssi, ""); lv_label_set_text(lbl_hint, "Tap Change WiFi"); } else { lv_label_set_text(lbl_title, "Not configured"); lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_MUTED), 0); lv_label_set_text(lbl_ssid, "—"); lv_label_set_text(lbl_ip, "IP —"); lv_label_set_text(lbl_rssi, ""); lv_label_set_text(lbl_hint, "Open setup"); } } 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); lv_obj_clear_flag(scr_status, LV_OBJ_FLAG_SCROLLABLE); lbl_title = lv_label_create(scr_status); lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_20, 0); lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_OK), 0); lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, UX(0), UY(16)); lbl_ssid = lv_label_create(scr_status); lv_obj_set_style_text_font(lbl_ssid, &lv_font_montserrat_18, 0); lv_obj_set_style_text_color(lbl_ssid, lv_color_hex(COL_INK), 0); lv_obj_align(lbl_ssid, LV_ALIGN_TOP_MID, UX(0), UY(52)); lbl_ip = lv_label_create(scr_status); lv_obj_set_style_text_font(lbl_ip, &lv_font_montserrat_14, 0); lv_obj_set_style_text_color(lbl_ip, lv_color_hex(COL_ACCENT), 0); lv_obj_align(lbl_ip, LV_ALIGN_TOP_MID, UX(0), UY(86)); lbl_rssi = lv_label_create(scr_status); lv_obj_set_style_text_font(lbl_rssi, &lv_font_montserrat_14, 0); lv_obj_set_style_text_color(lbl_rssi, lv_color_hex(COL_MUTED), 0); lv_obj_align(lbl_rssi, LV_ALIGN_TOP_MID, UX(0), UY(110)); lbl_hint = lv_label_create(scr_status); lv_obj_set_style_text_font(lbl_hint, &lv_font_montserrat_14, 0); lv_obj_set_style_text_color(lbl_hint, lv_color_hex(COL_MUTED), 0); lv_obj_align(lbl_hint, LV_ALIGN_TOP_MID, UX(0), UY(140)); lv_obj_t *row = lv_obj_create(scr_status); lv_obj_set_size(row, UI_HOR_RES - UW(24), UH(44)); lv_obj_align(row, LV_ALIGN_BOTTOM_MID, UX(0), UY(-12)); 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, "Change WiFi", COL_BTN_ACC, on_change_wifi, UW(140), UH(40)); make_btn(row, "Forget", COL_BTN_WARN, on_forget, UW(100), UH(40)); } 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); lv_obj_clear_flag(scr_setup, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *title = lv_label_create(scr_setup); lv_label_set_text(title, "WiFi Setup"); lv_obj_set_style_text_font(title, &lv_font_montserrat_18, 0); lv_obj_set_style_text_color(title, lv_color_hex(COL_INK), 0); lv_obj_align(title, LV_ALIGN_TOP_LEFT, UX(10), UY(8)); lbl_setup_status = lv_label_create(scr_setup); lv_label_set_text(lbl_setup_status, "Tap Scan"); lv_obj_set_style_text_font(lbl_setup_status, &lv_font_montserrat_14, 0); lv_obj_set_style_text_color(lbl_setup_status, lv_color_hex(COL_MUTED), 0); lv_obj_align(lbl_setup_status, LV_ALIGN_TOP_RIGHT, UX(-10), UY(10)); lv_obj_t *bar = lv_obj_create(scr_setup); lv_obj_set_size(bar, UI_HOR_RES - UW(16), UH(48)); lv_obj_align(bar, LV_ALIGN_TOP_MID, UX(0), UY(34)); lv_obj_set_style_bg_opa(bar, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(bar, 0, 0); lv_obj_set_style_pad_all(bar, 0, 0); lv_obj_set_flex_flow(bar, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(bar, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_column(bar, UW(8), 0); lv_obj_clear_flag(bar, LV_OBJ_FLAG_SCROLLABLE); 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)); make_btn(bar, "Status", COL_BTN, [](lv_event_t *e) { LV_UNUSED(e); wifi_ui_show_status(); }, UW(90), UH(42)); list_wifi = lv_obj_create(scr_setup); lv_obj_set_size(list_wifi, UI_HOR_RES - UW(16), UI_VER_RES - UH(92)); lv_obj_align(list_wifi, LV_ALIGN_TOP_MID, UX(0), UY(86)); lv_obj_set_style_bg_color(list_wifi, lv_color_hex(COL_LINE), 0); lv_obj_set_style_border_width(list_wifi, 0, 0); lv_obj_set_style_radius(list_wifi, 8, 0); lv_obj_set_style_pad_all(list_wifi, 4, 0); lv_obj_set_flex_flow(list_wifi, LV_FLEX_FLOW_COLUMN); 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); 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); /* Password dialog + keyboard created lazily — keep LV_MEM free for scan list. */ } void wifi_ui_set_connected_cb(WifiUiConnectedCb cb) { g_connected_cb = cb; } void wifi_ui_create() { if (wifi_prov_has_credentials()) { build_status(); /* Stay on status until CONNECTED; callback switches to radio. */ wifi_ui_show_status(); } else { build_setup(); wifi_ui_show_setup(); 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; } } 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(); lv_screen_load(scr_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[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); } } const WifiProvState st = wifi_prov_state(); if (st != g_last_shown) { g_last_shown = st; if (st == WIFI_PROV_CONNECTED) { if (g_connected_cb) { g_connected_cb(); } else { wifi_ui_show_status(); } } else if (st == WIFI_PROV_FAILED && g_on_setup) { set_setup_status("Failed — retry"); } else if (st == WIFI_PROV_CONNECTING && g_on_setup) { set_setup_status("Connecting..."); } else if (st == WIFI_PROV_PORTAL && g_on_setup) { set_setup_status("Phone: CYD-Setup"); } } if (!g_on_setup && !g_connected_cb && (millis() - g_last_status_ms >= 500)) { g_last_status_ms = millis(); refresh_status_labels(); } }