dashboard: add hidden diagnostics overlay

This commit is contained in:
2026-06-15 21:20:55 -06:00
parent 44128f83be
commit 33062fb0ba
7 changed files with 239 additions and 1 deletions
+6
View File
@@ -973,3 +973,9 @@ Each temperature sensor supports optional high-temperature alerting:
- `high_alert` - `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. 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.
+6
View File
@@ -295,3 +295,9 @@ When enabled and the live sensor temperature exceeds the configured threshold, t
## M9N GPS Module ## 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. 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.
+6
View File
@@ -362,3 +362,9 @@ When enabled and the live sensor temperature exceeds the configured threshold, t
## M9N GPS Module ## 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. 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.
@@ -1,6 +1,6 @@
#pragma once #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. // Update these if your Cargo ESP AP credentials are different.
static const char *CARGO_WIFI_SSID = "OverlandController"; static const char *CARGO_WIFI_SSID = "OverlandController";
@@ -24,4 +24,10 @@ struct DashboardStatus {
unsigned long uptime_seconds = 0; unsigned long uptime_seconds = 0;
String cargo_ap_ssid = "unknown"; String cargo_ap_ssid = "unknown";
String cargo_sta_ip = "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;
}; };
@@ -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_buttons[6] = {nullptr};
static lv_obj_t *relay_button_labels[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_ids[6];
static String relay_names[6]; static String relay_names[6];
static bool relay_states[6] = {false}; static bool relay_states[6] = {false};
@@ -353,8 +360,200 @@ static void request_fast_status_refresh()
fast_status_refresh_requested = false; 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 create_overland_overview_screen();
static void show_boot_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() static void layout_relay_buttons()
{ {
@@ -903,12 +1102,18 @@ static void parse_status_json(const String &body)
JsonObject system = doc["system"]; JsonObject system = doc["system"];
status_model.firmware_version = String((const char *)(system["firmware_version"] | status_model.firmware_version.c_str())); 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.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>()) { if (doc["network"].is<JsonObject>()) {
JsonObject network = doc["network"]; 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_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_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<JsonArray>()) { if (doc["temps"].is<JsonArray>()) {
@@ -1188,6 +1393,8 @@ static void create_overland_overview_screen()
lv_obj_set_style_bg_color(screen, lv_color_hex(0x101418), 0); 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_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); lv_obj_t *battery_card = create_card(screen, "", 380, 310, LV_ALIGN_TOP_LEFT, 32, 68);
battery_card_obj = battery_card; battery_card_obj = battery_card;
lv_obj_t *temp_card = create_card(screen, "", 260, 310, LV_ALIGN_TOP_LEFT, 426, 68); lv_obj_t *temp_card = create_card(screen, "", 260, 310, LV_ALIGN_TOP_LEFT, 426, 68);
@@ -2442,10 +2442,12 @@ void buildStatusDocument(JsonDocument& doc) {
network["ap_ssid"] = apSsid; network["ap_ssid"] = apSsid;
network["ap_auth"] = "wpa2"; network["ap_auth"] = "wpa2";
network["ap_ip"] = WiFi.softAPIP().toString(); network["ap_ip"] = WiFi.softAPIP().toString();
network["ap_clients"] = WiFi.softAPgetStationNum();
network["sta_enabled"] = wifiNetworkCount > 0; network["sta_enabled"] = wifiNetworkCount > 0;
network["sta_connected"] = WiFi.status() == WL_CONNECTED; network["sta_connected"] = WiFi.status() == WL_CONNECTED;
network["sta_ssid"] = activeStaSsid; network["sta_ssid"] = activeStaSsid;
network["sta_ip"] = WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : ""; 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; network["saved_network_count"] = wifiNetworkCount;
JsonArray savedNetworks = network.createNestedArray("saved_networks"); JsonArray savedNetworks = network.createNestedArray("saved_networks");
for (int i = 0; i < wifiNetworkCount; i++) { for (int i = 0; i < wifiNetworkCount; i++) {
@@ -2470,6 +2472,10 @@ void buildStatusDocument(JsonDocument& doc) {
system["build_date"] = __DATE__; system["build_date"] = __DATE__;
system["build_time"] = __TIME__; system["build_time"] = __TIME__;
system["uptime_seconds"] = millis() / 1000; 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"); JsonObject configObj = doc.createNestedObject("config");
buildConfigDocument(configObj); buildConfigDocument(configObj);
@@ -3355,6 +3361,7 @@ void handleHealth() {
network["ap_ssid"] = apSsid; network["ap_ssid"] = apSsid;
network["ap_auth"] = "wpa2"; network["ap_auth"] = "wpa2";
network["ap_ip"] = WiFi.softAPIP().toString(); network["ap_ip"] = WiFi.softAPIP().toString();
network["ap_clients"] = WiFi.softAPgetStationNum();
network["sta_enabled"] = wifiNetworkCount > 0; network["sta_enabled"] = wifiNetworkCount > 0;
network["sta_connected"] = WiFi.status() == WL_CONNECTED; network["sta_connected"] = WiFi.status() == WL_CONNECTED;
network["sta_ssid"] = activeStaSsid; network["sta_ssid"] = activeStaSsid;