Supports 2.8/3.5 boards with on-screen station list; ships flashable firmware packages. Co-authored-by: Cursor <cursoragent@cursor.com>
648 lines
21 KiB
C++
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();
|
|
}
|
|
}
|