From c18bc71359653332e25198f3a0be1849b9f94ba1 Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 2 Jul 2026 00:38:41 -0600 Subject: [PATCH] dashboard: add first-boot cargo WiFi setup --- docs/dashboard-esp32s3.md | 11 + .../esp32-s3-dashboard/dashboard_config.h | 22 +- .../esp32-s3-dashboard/esp32-s3-dashboard.ino | 268 +++++++++++++++++- 3 files changed, 278 insertions(+), 23 deletions(-) diff --git a/docs/dashboard-esp32s3.md b/docs/dashboard-esp32s3.md index 731a714..51b4e92 100644 --- a/docs/dashboard-esp32s3.md +++ b/docs/dashboard-esp32s3.md @@ -319,3 +319,14 @@ The diagnostics overlay shows dashboard version, uptime, heap, WiFi RSSI/IP, Car - Sends standard 11-bit functional request ID `0x7DF` and accepts ECU responses from `0x7E8` through `0x7EF`. - Diagnostics page shows TWAI state, coolant value, TX/RX counts, and last OBD status. - Overview coolant gauge shows `--°` until a valid OBD response is received. + + +## Dashboard first-boot WiFi setup + +The ESP32-S3 dashboard no longer hardcodes Cargo ESP WiFi credentials or the Cargo API base URL. On first boot, it shows a touchscreen setup page where the user enters: + +- Cargo ESP WiFi SSID +- Cargo ESP WiFi password +- Cargo API base URL, for example `http://192.168.4.1` + +The dashboard saves these values in ESP32 Preferences/NVS and builds all `/api/v1` endpoint URLs at runtime. The boot screen also provides a Setup button for changing saved settings later. diff --git a/firmware/esp32-s3-dashboard/dashboard_config.h b/firmware/esp32-s3-dashboard/dashboard_config.h index f918f63..54265b3 100644 --- a/firmware/esp32-s3-dashboard/dashboard_config.h +++ b/firmware/esp32-s3-dashboard/dashboard_config.h @@ -1,20 +1,8 @@ #pragma once -static const char *DASHBOARD_VERSION = "0.1.126-restore-boot-screen"; +static const char *DASHBOARD_VERSION = "0.1.127-first-boot-wifi"; -// Update these if your Cargo ESP AP credentials are different. -static const char *CARGO_WIFI_SSID = "OverlandController"; -static const char *CARGO_WIFI_PASSWORD = "Ward1707"; - -//static const char *CARGO_WIFI_SSID = "WardAP"; -//static const char *CARGO_WIFI_PASSWORD = "Ward5213"; - -static const char *CARGO_API_STATUS_URL = "http://192.168.4.1/api/v1/status"; -static const char *CARGO_API_FAST_STATUS_URL = "http://192.168.4.1/api/v1/status?fields=battery,temps,relays"; -static const char *CARGO_API_SLOW_STATUS_URL = "http://192.168.4.1/api/v1/status?fields=network,system,config"; -static const char *CARGO_API_RELAY_SET_URL = "http://192.168.4.1/api/v1/relay/set"; - -//static const char *CARGO_API_STATUS_URL = "http://192.168.88.181/api/v1/status"; -//static const char *CARGO_API_FAST_STATUS_URL = "http://192.168.88.181/api/v1/status?fields=battery,temps,relays"; -//static const char *CARGO_API_SLOW_STATUS_URL = "http://192.168.88.181/api/v1/status?fields=network,system,config"; -//static const char *CARGO_API_RELAY_SET_URL = "http://192.168.88.181/api/v1/relay/set"; +// First-boot defaults. Credentials are stored in Preferences/NVS after setup. +static const char *DASHBOARD_DEFAULT_CARGO_WIFI_SSID = ""; +static const char *DASHBOARD_DEFAULT_CARGO_WIFI_PASSWORD = ""; +static const char *DASHBOARD_DEFAULT_CARGO_API_BASE_URL = "http://192.168.4.1"; diff --git a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino index 5c7be52..5c13bc3 100644 --- a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino +++ b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -16,6 +17,23 @@ using namespace esp_panel::drivers; using namespace esp_panel::board; static DashboardStatus status_model; + +static Preferences dashboard_preferences; +static String cargo_wifi_ssid; +static String cargo_wifi_password; +static String cargo_api_base_url; +static String cargo_api_status_url; +static String cargo_api_fast_status_url; +static String cargo_api_slow_status_url; +static String cargo_api_relay_set_url; +static bool dashboard_configured = false; + +static lv_obj_t *setup_ssid_textarea = nullptr; +static lv_obj_t *setup_password_textarea = nullptr; +static lv_obj_t *setup_api_textarea = nullptr; +static lv_obj_t *setup_status_label = nullptr; +static lv_obj_t *setup_keyboard = nullptr; + static constexpr gpio_num_t OBD_CAN_TX_PIN = GPIO_NUM_15; static constexpr gpio_num_t OBD_CAN_RX_PIN = GPIO_NUM_16; static constexpr uint32_t OBD_REQUEST_ID = 0x7DF; @@ -733,6 +751,7 @@ static void reset_battery_detail_widget_cache() static void update_battery_detail_widgets(); static void show_boot_screen(); +static void show_dashboard_setup_screen(); static void show_diagnostics_overlay(); static void close_diagnostics_overlay(); static void update_diagnostics_overlay(); @@ -1142,6 +1161,228 @@ static void update_relay_buttons() lvgl_port_unlock(); } + +static String normalize_cargo_api_base_url(String value) +{ + value.trim(); + + if (value.length() == 0) { + value = DASHBOARD_DEFAULT_CARGO_API_BASE_URL; + } + + if (!value.startsWith("http://") && !value.startsWith("https://")) { + value = "http://" + value; + } + + while (value.endsWith("/")) { + value.remove(value.length() - 1); + } + + return value; +} + +static void rebuild_cargo_api_urls() +{ + cargo_api_base_url = normalize_cargo_api_base_url(cargo_api_base_url); + cargo_api_status_url = cargo_api_base_url + "/api/v1/status"; + cargo_api_fast_status_url = cargo_api_base_url + "/api/v1/status?fields=battery,temps,relays"; + cargo_api_slow_status_url = cargo_api_base_url + "/api/v1/status?fields=network,system,config"; + cargo_api_relay_set_url = cargo_api_base_url + "/api/v1/relay/set"; +} + +static void load_dashboard_connection_config() +{ + dashboard_preferences.begin("dashcfg", false); + + cargo_wifi_ssid = dashboard_preferences.getString("wifi_ssid", DASHBOARD_DEFAULT_CARGO_WIFI_SSID); + cargo_wifi_password = dashboard_preferences.getString("wifi_pass", DASHBOARD_DEFAULT_CARGO_WIFI_PASSWORD); + cargo_api_base_url = dashboard_preferences.getString("api_base", DASHBOARD_DEFAULT_CARGO_API_BASE_URL); + + rebuild_cargo_api_urls(); + dashboard_configured = cargo_wifi_ssid.length() > 0; + + Serial.print("Dashboard cargo SSID configured: "); + Serial.println(dashboard_configured ? "yes" : "no"); + Serial.print("Dashboard cargo API base: "); + Serial.println(cargo_api_base_url); +} + +static void save_dashboard_connection_config(const String &ssid, const String &password, const String &api_base) +{ + cargo_wifi_ssid = ssid; + cargo_wifi_password = password; + cargo_api_base_url = api_base; + rebuild_cargo_api_urls(); + + dashboard_preferences.putString("wifi_ssid", cargo_wifi_ssid); + dashboard_preferences.putString("wifi_pass", cargo_wifi_password); + dashboard_preferences.putString("api_base", cargo_api_base_url); + + dashboard_configured = cargo_wifi_ssid.length() > 0; +} + +static void setup_textarea_event_cb(lv_event_t *event) +{ + lv_event_code_t code = lv_event_get_code(event); + lv_obj_t *target = lv_event_get_target(event); + + if (code == LV_EVENT_FOCUSED || code == LV_EVENT_CLICKED) { + if (setup_keyboard != nullptr) { + lv_keyboard_set_textarea(setup_keyboard, target); + lv_obj_clear_flag(setup_keyboard, LV_OBJ_FLAG_HIDDEN); + } + } +} + +static lv_obj_t *create_setup_textarea(lv_obj_t *parent, const char *label_text, const String &value, int y, bool password) +{ + lv_obj_t *label = lv_label_create(parent); + lv_label_set_text(label, label_text); + lv_obj_set_style_text_color(label, lv_color_hex(0xDDE3EA), 0); + lv_obj_set_style_text_font(label, &lv_font_montserrat_26, 0); + lv_obj_align(label, LV_ALIGN_TOP_LEFT, 80, y); + + lv_obj_t *textarea = lv_textarea_create(parent); + lv_obj_set_size(textarea, 760, 52); + lv_obj_align(textarea, LV_ALIGN_TOP_LEFT, 80, y + 32); + lv_textarea_set_one_line(textarea, true); + lv_textarea_set_text(textarea, value.c_str()); + lv_textarea_set_password_mode(textarea, password); + lv_obj_set_style_text_font(textarea, &lv_font_montserrat_26, 0); + lv_obj_add_event_cb(textarea, setup_textarea_event_cb, LV_EVENT_ALL, nullptr); + + return textarea; +} + +static void setup_save_event_cb(lv_event_t *event) +{ + if (lv_event_get_code(event) != LV_EVENT_CLICKED) { + return; + } + + String ssid = lv_textarea_get_text(setup_ssid_textarea); + String password = lv_textarea_get_text(setup_password_textarea); + String api_base = lv_textarea_get_text(setup_api_textarea); + + ssid.trim(); + api_base.trim(); + + if (ssid.length() == 0) { + if (setup_status_label != nullptr) { + lv_label_set_text(setup_status_label, "SSID is required."); + } + return; + } + + save_dashboard_connection_config(ssid, password, api_base); + + WiFi.disconnect(true); + was_wifi_connected = false; + overview_screen_created = false; + last_wifi_attempt_ms = 0; + last_status_poll_ms = 0; + last_metadata_poll_ms = 0; + metadata_refresh_requested = true; + + lvgl_port_lock(-1); + if (dashboard_configured) { + show_boot_screen(); + } else { + show_dashboard_setup_screen(); + } + lvgl_port_unlock(); + + Serial.println("Dashboard connection settings saved."); +} + +static void setup_reset_event_cb(lv_event_t *event) +{ + if (lv_event_get_code(event) != LV_EVENT_CLICKED) { + return; + } + + dashboard_preferences.clear(); + cargo_wifi_ssid = ""; + cargo_wifi_password = ""; + cargo_api_base_url = DASHBOARD_DEFAULT_CARGO_API_BASE_URL; + rebuild_cargo_api_urls(); + dashboard_configured = false; + + WiFi.disconnect(true); + + lv_textarea_set_text(setup_ssid_textarea, ""); + lv_textarea_set_text(setup_password_textarea, ""); + lv_textarea_set_text(setup_api_textarea, cargo_api_base_url.c_str()); + + if (setup_status_label != nullptr) { + lv_label_set_text(setup_status_label, "Saved settings cleared."); + } +} + +static void show_dashboard_setup_screen() +{ + lv_obj_t *screen = lv_scr_act(); + lv_obj_clean(screen); + lv_obj_set_style_bg_color(screen, lv_color_hex(0x101418), 0); + + lv_obj_t *title = lv_label_create(screen); + lv_label_set_text(title, "Dashboard Setup"); + lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(title, &lv_font_montserrat_48, 0); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 28); + + lv_obj_t *subtitle = lv_label_create(screen); + lv_label_set_text(subtitle, "Enter Cargo ESP WiFi and API settings."); + lv_obj_set_style_text_color(subtitle, lv_color_hex(0xB8C0C8), 0); + lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_26, 0); + lv_obj_align(subtitle, LV_ALIGN_TOP_MID, 0, 86); + + setup_ssid_textarea = create_setup_textarea(screen, "Cargo WiFi SSID", cargo_wifi_ssid, 130, false); + setup_password_textarea = create_setup_textarea(screen, "Cargo WiFi Password", cargo_wifi_password, 220, true); + setup_api_textarea = create_setup_textarea(screen, "Cargo API Base URL", cargo_api_base_url, 310, false); + + lv_obj_t *save_button = lv_btn_create(screen); + lv_obj_set_size(save_button, 210, 58); + lv_obj_align(save_button, LV_ALIGN_TOP_LEFT, 80, 408); + lv_obj_add_event_cb(save_button, setup_save_event_cb, LV_EVENT_CLICKED, nullptr); + lv_obj_t *save_label = lv_label_create(save_button); + lv_label_set_text(save_label, "Save"); + lv_obj_center(save_label); + + lv_obj_t *clear_button = lv_btn_create(screen); + lv_obj_set_size(clear_button, 210, 58); + lv_obj_align(clear_button, LV_ALIGN_TOP_LEFT, 310, 408); + lv_obj_add_event_cb(clear_button, setup_reset_event_cb, LV_EVENT_CLICKED, nullptr); + lv_obj_t *clear_label = lv_label_create(clear_button); + lv_label_set_text(clear_label, "Clear"); + lv_obj_center(clear_label); + + setup_status_label = lv_label_create(screen); + lv_label_set_text(setup_status_label, dashboard_configured ? "Settings loaded." : "First boot: setup required."); + lv_obj_set_style_text_color(setup_status_label, lv_color_hex(0xDDE3EA), 0); + lv_obj_set_style_text_font(setup_status_label, &lv_font_montserrat_26, 0); + lv_obj_set_width(setup_status_label, 420); + lv_label_set_long_mode(setup_status_label, LV_LABEL_LONG_WRAP); + lv_obj_align(setup_status_label, LV_ALIGN_TOP_LEFT, 548, 410); + + setup_keyboard = lv_keyboard_create(screen); + lv_obj_set_size(setup_keyboard, 1024, 180); + lv_obj_align(setup_keyboard, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_obj_add_flag(setup_keyboard, LV_OBJ_FLAG_HIDDEN); +} + +static void setup_button_event_cb(lv_event_t *event) +{ + if (lv_event_get_code(event) != LV_EVENT_CLICKED) { + return; + } + + lvgl_port_lock(-1); + show_dashboard_setup_screen(); + lvgl_port_unlock(); +} + + static bool post_relay_state(const String &relay_id, bool state) { if (api_request_in_progress || WiFi.status() != WL_CONNECTED) { @@ -1158,12 +1399,12 @@ static bool post_relay_state(const String &relay_id, bool state) serializeJson(request, payload); Serial.print("POST "); - Serial.println(CARGO_API_RELAY_SET_URL); + Serial.println(cargo_api_relay_set_url); Serial.println(payload); HTTPClient http; http.setTimeout(1200); - http.begin(CARGO_API_RELAY_SET_URL); + http.begin(cargo_api_relay_set_url); http.setReuse(false); http.addHeader("Connection", "close"); http.addHeader("Content-Type", "application/json"); @@ -1280,6 +1521,10 @@ static void process_pending_relay_command() static void connect_wifi_if_needed() { + if (!dashboard_configured) { + return; + } + if (WiFi.status() == WL_CONNECTED) { return; } @@ -1291,10 +1536,10 @@ static void connect_wifi_if_needed() last_wifi_attempt_ms = millis(); Serial.print("Connecting to Cargo ESP AP: "); - Serial.println(CARGO_WIFI_SSID); + Serial.println(cargo_wifi_ssid); WiFi.mode(WIFI_STA); - WiFi.begin(CARGO_WIFI_SSID, CARGO_WIFI_PASSWORD); + WiFi.begin(cargo_wifi_ssid.c_str(), cargo_wifi_password.c_str()); } static void update_system_status_label() @@ -1835,13 +2080,13 @@ static void poll_status_api() if (fast_status_refresh_requested || millis() - last_status_poll_ms >= 1500) { fast_status_refresh_requested = false; last_status_poll_ms = millis(); - fetch_status_fields(CARGO_API_FAST_STATUS_URL, "fast status"); + fetch_status_fields(cargo_api_fast_status_url.c_str(), "fast status"); } if (metadata_refresh_requested || millis() - last_metadata_poll_ms >= 30000) { metadata_refresh_requested = false; last_metadata_poll_ms = millis(); - fetch_status_fields(CARGO_API_SLOW_STATUS_URL, "slow status"); + fetch_status_fields(cargo_api_slow_status_url.c_str(), "slow status"); } } @@ -1908,6 +2153,15 @@ static void show_boot_screen() lv_obj_set_width(api, 700); lv_obj_set_style_text_align(api, LV_TEXT_ALIGN_CENTER, 0); lv_obj_align(api, LV_ALIGN_CENTER, 0, 118); + + lv_obj_t *setup_button = lv_btn_create(screen); + lv_obj_set_size(setup_button, 180, 54); + lv_obj_align(setup_button, LV_ALIGN_BOTTOM_MID, 0, -34); + lv_obj_add_event_cb(setup_button, setup_button_event_cb, LV_EVENT_CLICKED, nullptr); + + lv_obj_t *setup_label = lv_label_create(setup_button); + lv_label_set_text(setup_label, "Setup"); + lv_obj_center(setup_label); } static void create_overland_overview_screen() @@ -2181,6 +2435,8 @@ void setup() Serial.println("Display: 1024x600"); Serial.println("API Base: /api/v1"); + load_dashboard_connection_config(); + Board *board = new Board(); board->init();