diff --git a/docs/API.md b/docs/API.md index e886b84..f36f9d6 100644 --- a/docs/API.md +++ b/docs/API.md @@ -973,3 +973,9 @@ Each temperature sensor supports optional high-temperature alerting: - `high_alert` When enabled and the live sensor temperature exceeds the configured threshold, the Cargo ESP includes `high_alert: true` for that sensor in `/api/v1/status`. The ESP32-S3 dashboard colors the affected grouped temperature tile red if any sensor in that displayed group is in alert. + +### Hidden Dashboard Diagnostics + +The dashboard has a hidden diagnostics overlay. Tap the top status card five times within roughly three seconds to open it. Double-tap the diagnostics overlay to return to the main dashboard. + +The diagnostics overlay shows dashboard version, uptime, heap, WiFi RSSI/IP, Cargo API status, Cargo firmware version, Cargo uptime, Cargo heap, Cargo AP client count, BMS status, relay count, and basic control-loop state. diff --git a/docs/dashboard-esp32s3.md b/docs/dashboard-esp32s3.md index e48c63a..cea3475 100644 --- a/docs/dashboard-esp32s3.md +++ b/docs/dashboard-esp32s3.md @@ -295,3 +295,9 @@ When enabled and the live sensor temperature exceeds the configured threshold, t ## M9N GPS Module The dashboard enclosure will include an M9N GPS module connected to the ESP32-S3 dashboard over UART. It will provide GPS fix status, satellite count, position, speed, and UTC time. Future work will use GPS time/location for automatic timezone and DST handling. + +### Hidden Dashboard Diagnostics + +The dashboard has a hidden diagnostics overlay. Tap the top status card five times within roughly three seconds to open it. Double-tap the diagnostics overlay to return to the main dashboard. + +The diagnostics overlay shows dashboard version, uptime, heap, WiFi RSSI/IP, Cargo API status, Cargo firmware version, Cargo uptime, Cargo heap, Cargo AP client count, BMS status, relay count, and basic control-loop state. diff --git a/docs/project-state.md b/docs/project-state.md index 58e3606..b204366 100644 --- a/docs/project-state.md +++ b/docs/project-state.md @@ -362,3 +362,9 @@ When enabled and the live sensor temperature exceeds the configured threshold, t ## M9N GPS Module The dashboard enclosure will include an M9N GPS module connected to the ESP32-S3 dashboard over UART. It will provide GPS fix status, satellite count, position, speed, and UTC time. Future work will use GPS time/location for automatic timezone and DST handling. + +### Hidden Dashboard Diagnostics + +The dashboard has a hidden diagnostics overlay. Tap the top status card five times within roughly three seconds to open it. Double-tap the diagnostics overlay to return to the main dashboard. + +The diagnostics overlay shows dashboard version, uptime, heap, WiFi RSSI/IP, Cargo API status, Cargo firmware version, Cargo uptime, Cargo heap, Cargo AP client count, BMS status, relay count, and basic control-loop state. diff --git a/firmware/esp32-s3-dashboard/dashboard_config.h b/firmware/esp32-s3-dashboard/dashboard_config.h index 34b6250..f97282b 100644 --- a/firmware/esp32-s3-dashboard/dashboard_config.h +++ b/firmware/esp32-s3-dashboard/dashboard_config.h @@ -1,6 +1,6 @@ #pragma once -static const char *DASHBOARD_VERSION = "0.1.74-relay-latest-state-queue"; +static const char *DASHBOARD_VERSION = "0.1.75-hidden-diagnostics"; // Update these if your Cargo ESP AP credentials are different. static const char *CARGO_WIFI_SSID = "OverlandController"; diff --git a/firmware/esp32-s3-dashboard/dashboard_status_model.h b/firmware/esp32-s3-dashboard/dashboard_status_model.h index b1e7c7d..10095a2 100644 --- a/firmware/esp32-s3-dashboard/dashboard_status_model.h +++ b/firmware/esp32-s3-dashboard/dashboard_status_model.h @@ -24,4 +24,10 @@ struct DashboardStatus { unsigned long uptime_seconds = 0; String cargo_ap_ssid = "unknown"; String cargo_sta_ip = "unknown"; + int cargo_ap_clients = 0; + int cargo_sta_rssi_dbm = 0; + unsigned long cargo_free_heap = 0; + unsigned long cargo_min_free_heap = 0; + String cargo_chip_model = "unknown"; + int cargo_cpu_mhz = 0; }; diff --git a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino index 2abd020..4a1aee7 100644 --- a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino +++ b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino @@ -40,6 +40,13 @@ static lv_obj_t *vehicle_fuel_value_label = nullptr; static lv_obj_t *relay_buttons[6] = {nullptr}; static lv_obj_t *relay_button_labels[6] = {nullptr}; +static lv_obj_t *diagnostics_overlay = nullptr; +static lv_obj_t *diagnostics_label = nullptr; +static unsigned long diagnostics_last_update_ms = 0; +static unsigned long diagnostics_last_tap_ms = 0; +static unsigned long system_card_last_tap_ms = 0; +static int system_card_tap_count = 0; + static String relay_ids[6]; static String relay_names[6]; static bool relay_states[6] = {false}; @@ -353,8 +360,200 @@ static void request_fast_status_refresh() fast_status_refresh_requested = false; } + +static String format_diag_uptime(unsigned long seconds) +{ + unsigned long days = seconds / 86400; + seconds %= 86400; + unsigned long hours = seconds / 3600; + seconds %= 3600; + unsigned long minutes = seconds / 60; + + String out; + if (days > 0) { + out += String(days); + out += "d "; + } + out += String(hours); + out += "h "; + out += String(minutes); + out += "m"; + return out; +} + +static void update_diagnostics_overlay() +{ + if (diagnostics_overlay == nullptr || diagnostics_label == nullptr) { + return; + } + + if (millis() - diagnostics_last_update_ms < 500) { + return; + } + + diagnostics_last_update_ms = millis(); + + String text; + text += "DIAGNOSTICS\n\n"; + + text += "Dashboard ESP32-S3\n"; + text += " Version: "; + text += DASHBOARD_VERSION; + text += "\n Uptime: "; + text += format_diag_uptime(millis() / 1000); + text += "\n Free heap: "; + text += String(ESP.getFreeHeap()); + text += "\n Min heap: "; + text += String(ESP.getMinFreeHeap()); + text += "\n WiFi: "; + text += WiFi.status() == WL_CONNECTED ? "connected" : "offline"; + text += "\n RSSI: "; + text += WiFi.status() == WL_CONNECTED ? String(WiFi.RSSI()) : String("--"); + text += " dBm\n IP: "; + text += WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : String("--"); + text += "\n Last HTTP: "; + text += String(status_model.http_code); + text += "\n Cargo API: "; + text += status_model.api_ok ? "OK" : "FAIL"; + + text += "\n\nCargo ESP\n"; + text += " Version: "; + text += status_model.firmware_version; + text += "\n Uptime: "; + text += format_diag_uptime(status_model.uptime_seconds); + text += "\n Chip: "; + text += status_model.cargo_chip_model; + text += "\n CPU: "; + text += String(status_model.cargo_cpu_mhz); + text += " MHz"; + text += "\n Free heap: "; + text += String(status_model.cargo_free_heap); + text += "\n Min heap: "; + text += String(status_model.cargo_min_free_heap); + + text += "\n\nCargo Network\n"; + text += " AP SSID: "; + text += status_model.cargo_ap_ssid; + text += "\n AP clients: "; + text += String(status_model.cargo_ap_clients); + text += "\n STA IP: "; + text += status_model.cargo_sta_ip.length() ? status_model.cargo_sta_ip : String("--"); + text += "\n STA RSSI: "; + text += status_model.cargo_sta_rssi_dbm == 0 ? String("--") : String(status_model.cargo_sta_rssi_dbm); + text += " dBm"; + + text += "\n\nPower / Sensors\n"; + text += " BMS: "; + text += status_model.bms_connected ? "connected" : "offline"; + text += "\n SOC: "; + text += status_model.soc >= 0 ? String(status_model.soc) + "%" : String("--"); + text += "\n Current: "; + text += String(status_model.current, 2); + text += " A"; + text += "\n Cell delta: "; + text += String(status_model.cell_delta_mv); + text += " mV"; + + text += "\n\nControls\n"; + text += " Relays visible: "; + text += String(relay_count); + text += "\n API busy: "; + text += api_request_in_progress ? "yes" : "no"; + + text += "\n\nDouble tap anywhere to return."; + + lvgl_port_lock(-1); + lv_label_set_text(diagnostics_label, text.c_str()); + lvgl_port_unlock(); +} + +static void close_diagnostics_overlay() +{ + if (diagnostics_overlay == nullptr) { + return; + } + + lvgl_port_lock(-1); + lv_obj_del(diagnostics_overlay); + diagnostics_overlay = nullptr; + diagnostics_label = nullptr; + lvgl_port_unlock(); +} + +static void diagnostics_overlay_event_cb(lv_event_t *event) +{ + if (lv_event_get_code(event) != LV_EVENT_CLICKED) { + return; + } + + unsigned long now = millis(); + if (now - diagnostics_last_tap_ms < 650) { + close_diagnostics_overlay(); + diagnostics_last_tap_ms = 0; + return; + } + + diagnostics_last_tap_ms = now; +} + +static void show_diagnostics_overlay() +{ + if (diagnostics_overlay != nullptr) { + return; + } + + lvgl_port_lock(-1); + + diagnostics_overlay = lv_obj_create(lv_scr_act()); + lv_obj_set_size(diagnostics_overlay, 1024, 600); + lv_obj_align(diagnostics_overlay, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_bg_color(diagnostics_overlay, lv_color_hex(0x101418), 0); + lv_obj_set_style_bg_opa(diagnostics_overlay, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(diagnostics_overlay, 0, 0); + lv_obj_set_style_pad_all(diagnostics_overlay, 22, 0); + lv_obj_add_flag(diagnostics_overlay, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(diagnostics_overlay, diagnostics_overlay_event_cb, LV_EVENT_CLICKED, nullptr); + + diagnostics_label = lv_label_create(diagnostics_overlay); + lv_label_set_text(diagnostics_label, "DIAGNOSTICS\nLoading..."); + lv_obj_set_style_text_color(diagnostics_label, lv_color_hex(0xDDE3EA), 0); + lv_obj_set_style_text_font(diagnostics_label, &lv_font_montserrat_26, 0); + lv_obj_set_width(diagnostics_label, 960); + lv_label_set_long_mode(diagnostics_label, LV_LABEL_LONG_WRAP); + lv_obj_align(diagnostics_label, LV_ALIGN_TOP_LEFT, 8, 8); + + lvgl_port_unlock(); + + diagnostics_last_update_ms = 0; + update_diagnostics_overlay(); +} + +static void system_card_event_cb(lv_event_t *event) +{ + if (lv_event_get_code(event) != LV_EVENT_CLICKED) { + return; + } + + unsigned long now = millis(); + if (now - system_card_last_tap_ms > 3000) { + system_card_tap_count = 0; + } + + system_card_last_tap_ms = now; + system_card_tap_count++; + + if (system_card_tap_count >= 5) { + system_card_tap_count = 0; + show_diagnostics_overlay(); + } +} + + static void create_overland_overview_screen(); static void show_boot_screen(); +static void show_diagnostics_overlay(); +static void close_diagnostics_overlay(); +static void update_diagnostics_overlay(); static void layout_relay_buttons() { @@ -903,12 +1102,18 @@ static void parse_status_json(const String &body) JsonObject system = doc["system"]; status_model.firmware_version = String((const char *)(system["firmware_version"] | status_model.firmware_version.c_str())); status_model.uptime_seconds = system["uptime_seconds"] | status_model.uptime_seconds; + status_model.cargo_free_heap = system["free_heap"] | status_model.cargo_free_heap; + status_model.cargo_min_free_heap = system["min_free_heap"] | status_model.cargo_min_free_heap; + status_model.cargo_chip_model = String((const char *)(system["chip_model"] | status_model.cargo_chip_model.c_str())); + status_model.cargo_cpu_mhz = system["cpu_mhz"] | status_model.cargo_cpu_mhz; } if (doc["network"].is()) { JsonObject network = doc["network"]; status_model.cargo_ap_ssid = String((const char *)(network["ap_ssid"] | status_model.cargo_ap_ssid.c_str())); status_model.cargo_sta_ip = String((const char *)(network["sta_ip"] | status_model.cargo_sta_ip.c_str())); + status_model.cargo_ap_clients = network["ap_clients"] | status_model.cargo_ap_clients; + status_model.cargo_sta_rssi_dbm = network["sta_rssi_dbm"] | status_model.cargo_sta_rssi_dbm; } if (doc["temps"].is()) { @@ -1188,6 +1393,8 @@ static void create_overland_overview_screen() lv_obj_set_style_bg_color(screen, lv_color_hex(0x101418), 0); lv_obj_t *system_card = create_card(screen, "", 960, 48, LV_ALIGN_TOP_MID, 0, 12); + lv_obj_add_flag(system_card, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(system_card, system_card_event_cb, LV_EVENT_CLICKED, nullptr); lv_obj_t *battery_card = create_card(screen, "", 380, 310, LV_ALIGN_TOP_LEFT, 32, 68); battery_card_obj = battery_card; lv_obj_t *temp_card = create_card(screen, "", 260, 310, LV_ALIGN_TOP_LEFT, 426, 68); diff --git a/firmware/esp32/overland-controller/overland-controller.ino b/firmware/esp32/overland-controller/overland-controller.ino index 626a4e4..c5c7da1 100644 --- a/firmware/esp32/overland-controller/overland-controller.ino +++ b/firmware/esp32/overland-controller/overland-controller.ino @@ -2442,10 +2442,12 @@ void buildStatusDocument(JsonDocument& doc) { network["ap_ssid"] = apSsid; network["ap_auth"] = "wpa2"; network["ap_ip"] = WiFi.softAPIP().toString(); + network["ap_clients"] = WiFi.softAPgetStationNum(); network["sta_enabled"] = wifiNetworkCount > 0; network["sta_connected"] = WiFi.status() == WL_CONNECTED; network["sta_ssid"] = activeStaSsid; network["sta_ip"] = WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : ""; + network["sta_rssi_dbm"] = WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : 0; network["saved_network_count"] = wifiNetworkCount; JsonArray savedNetworks = network.createNestedArray("saved_networks"); for (int i = 0; i < wifiNetworkCount; i++) { @@ -2470,6 +2472,10 @@ void buildStatusDocument(JsonDocument& doc) { system["build_date"] = __DATE__; system["build_time"] = __TIME__; system["uptime_seconds"] = millis() / 1000; + system["free_heap"] = ESP.getFreeHeap(); + system["min_free_heap"] = ESP.getMinFreeHeap(); + system["chip_model"] = ESP.getChipModel(); + system["cpu_mhz"] = ESP.getCpuFreqMHz(); JsonObject configObj = doc.createNestedObject("config"); buildConfigDocument(configObj); @@ -3355,6 +3361,7 @@ void handleHealth() { network["ap_ssid"] = apSsid; network["ap_auth"] = "wpa2"; network["ap_ip"] = WiFi.softAPIP().toString(); + network["ap_clients"] = WiFi.softAPgetStationNum(); network["sta_enabled"] = wifiNetworkCount > 0; network["sta_connected"] = WiFi.status() == WL_CONNECTED; network["sta_ssid"] = activeStaSsid;