Files
webradiotest/src/wifi_ui.cpp
DESKTOP-PUHFO60\dsx2016 d75c252b9e Add CYD web radio: WiFi provision + HTTP MP3 to DAC26.
Supports 2.8/3.5 boards with on-screen station list; ships flashable firmware packages.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 08:04:11 +08:00

648 lines
21 KiB
C++

#include "wifi_ui.h"
#include "pins.h"
#include "wifi_prov.h"
#include <Arduino.h>
#include <cstdio>
#include <cstring>
#include <lvgl.h>
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();
}
}