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`
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
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
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
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";
@@ -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;
};
@@ -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>()) {
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<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_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);
@@ -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;