From c41c850644e18dab1f4ab3e379144372d71f4771 Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 9 Jun 2026 22:24:59 -0600 Subject: [PATCH] dashboard: render parsed cargo status overview --- .../esp32-s3-dashboard/esp32-s3-dashboard.ino | 266 ++++++++++++------ 1 file changed, 182 insertions(+), 84 deletions(-) diff --git a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino index 3a26663..b970e41 100644 --- a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino +++ b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -9,7 +10,7 @@ using namespace esp_panel::drivers; using namespace esp_panel::board; -static const char *DASHBOARD_VERSION = "0.0.2-wifi-status"; +static const char *DASHBOARD_VERSION = "0.0.3-status-overview"; // Update these if your Cargo ESP AP credentials are different. static const char *CARGO_WIFI_SSID = "OverlandController"; @@ -19,7 +20,9 @@ static const char *CARGO_API_STATUS_URL = "http://192.168.4.1/api/v1/status"; static lv_obj_t *wifi_label = nullptr; static lv_obj_t *api_label = nullptr; -static lv_obj_t *status_label = nullptr; +static lv_obj_t *battery_label = nullptr; +static lv_obj_t *temps_label = nullptr; +static lv_obj_t *outputs_label = nullptr; static lv_obj_t *touch_count_label = nullptr; static uint32_t touch_count = 0; @@ -29,7 +32,9 @@ static unsigned long last_status_poll_ms = 0; static String last_wifi_label_text; static String last_api_label_text; -static String last_status_label_text; +static String last_battery_label_text; +static String last_temps_label_text; +static String last_outputs_label_text; static void set_label_text_if_changed(lv_obj_t *label, String &last_text, const String &text) { @@ -48,6 +53,11 @@ static void set_label_text_if_changed(lv_obj_t *label, String &last_text, const lvgl_port_unlock(); } +static String on_off(bool value) +{ + return value ? "ON" : "OFF"; +} + static void touch_test_event_cb(lv_event_t *event) { if (lv_event_get_code(event) != LV_EVENT_CLICKED) { @@ -107,11 +117,123 @@ static void update_wifi_label() } } +static void update_status_widgets(const String &body) +{ + DynamicJsonDocument doc(16384); + DeserializationError error = deserializeJson(doc, body); + + if (error) { + String text = "JSON parse failed: "; + text += error.c_str(); + set_label_text_if_changed(api_label, last_api_label_text, text); + return; + } + + JsonObject battery = doc["battery"]; + int soc = battery["soc"] | -1; + float voltage = battery["voltage"] | 0.0; + float current = battery["current"] | 0.0; + bool bms_connected = battery["connected"] | false; + int cell_delta_mv = battery["cell_delta_mv"] | -1; + + String battery_text = "Battery\n"; + battery_text += "SOC: "; + battery_text += soc >= 0 ? String(soc) + "%" : "n/a"; + battery_text += "\nVoltage: "; + battery_text += String(voltage, 2); + battery_text += " V\nCurrent: "; + battery_text += String(current, 2); + battery_text += " A\nBMS: "; + battery_text += bms_connected ? "online" : "offline"; + if (cell_delta_mv >= 0) { + battery_text += "\nCell delta: "; + battery_text += String(cell_delta_mv); + battery_text += " mV"; + } + + String temps_text = "Temperatures\n"; + JsonArray temps = doc["temps"]; + if (temps.isNull() || temps.size() == 0) { + temps_text += "No sensors reported"; + } else { + int shown = 0; + for (JsonObject temp : temps) { + if (shown >= 5) { + temps_text += "\n..."; + break; + } + + const char *name = temp["name"] | temp["id"] | "Temp"; + bool online = temp["online"] | false; + bool enabled = temp["enabled"] | false; + + temps_text += name; + temps_text += ": "; + + if (!enabled) { + temps_text += "disabled"; + } else if (!online) { + temps_text += "offline"; + } else { + float temperature_f = temp["temperature_f"] | 0.0; + temps_text += String(temperature_f, 1); + temps_text += " F"; + } + + temps_text += "\n"; + shown++; + } + } + + String outputs_text = "Outputs\n"; + JsonArray relays = doc["relays"]; + if (relays.isNull() || relays.size() == 0) { + outputs_text += "No outputs reported"; + } else { + int shown = 0; + for (JsonObject relay : relays) { + if (shown >= 6) { + outputs_text += "\n..."; + break; + } + + const char *name = relay["name"] | relay["id"] | "Output"; + bool enabled = relay["enabled"] | false; + bool available = relay["available"] | true; + bool state = relay["state"] | false; + + outputs_text += name; + outputs_text += ": "; + + if (!available) { + outputs_text += "unavailable"; + } else if (!enabled) { + outputs_text += "disabled"; + } else { + outputs_text += on_off(state); + } + + outputs_text += "\n"; + shown++; + } + } + + String api_text = "API: connected\nGET /api/v1/status\n"; + api_text += "Profile: "; + api_text += doc["hardware_profile"] | "unknown"; + api_text += "\nOutputs: "; + api_text += String((int)(doc["output_count"] | relays.size())); + + set_label_text_if_changed(api_label, last_api_label_text, api_text); + set_label_text_if_changed(battery_label, last_battery_label_text, battery_text); + set_label_text_if_changed(temps_label, last_temps_label_text, temps_text); + set_label_text_if_changed(outputs_label, last_outputs_label_text, outputs_text); +} + static void poll_status_api() { if (WiFi.status() != WL_CONNECTED) { set_label_text_if_changed(api_label, last_api_label_text, "API: waiting for WiFi"); - set_label_text_if_changed(status_label, last_status_label_text, "No API response yet."); return; } @@ -124,42 +246,55 @@ static void poll_status_api() Serial.print("GET "); Serial.println(CARGO_API_STATUS_URL); - set_label_text_if_changed(api_label, last_api_label_text, "API: requesting /api/v1/status"); - HTTPClient http; http.begin(CARGO_API_STATUS_URL); int code = http.GET(); - if (code > 0) { + if (code == 200) { String body = http.getString(); - Serial.print("HTTP "); - Serial.println(code); + Serial.println("HTTP 200"); Serial.println(body); - String api_text = "API: HTTP "; - api_text += String(code); - api_text += "\nGET /api/v1/status"; - set_label_text_if_changed(api_label, last_api_label_text, api_text); - - if (body.length() > 900) { - body = body.substring(0, 900); - body += "\n...truncated..."; + update_status_widgets(body); + } else { + String error_text = "API: HTTP "; + error_text += String(code); + if (code <= 0) { + error_text += "\n"; + error_text += http.errorToString(code); } - set_label_text_if_changed(status_label, last_status_label_text, body); - } else { - String error = "API request failed: "; - error += http.errorToString(code); - - Serial.println(error); - set_label_text_if_changed(api_label, last_api_label_text, error); - set_label_text_if_changed(status_label, last_status_label_text, "No valid response from Cargo ESP."); + Serial.println(error_text); + set_label_text_if_changed(api_label, last_api_label_text, error_text); } http.end(); } +static lv_obj_t *create_card(lv_obj_t *parent, int width, int height, lv_align_t align, int x, int y) +{ + lv_obj_t *card = lv_obj_create(parent); + lv_obj_set_size(card, width, height); + lv_obj_align(card, align, x, y); + lv_obj_set_style_radius(card, 16, 0); + lv_obj_set_style_bg_color(card, lv_color_hex(0x1F2933), 0); + lv_obj_set_style_border_color(card, lv_color_hex(0x3A4652), 0); + lv_obj_set_style_border_width(card, 2, 0); + lv_obj_set_style_pad_all(card, 16, 0); + return card; +} + +static lv_obj_t *create_card_label(lv_obj_t *card, const char *text) +{ + lv_obj_t *label = lv_label_create(card); + lv_label_set_text(label, text); + lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_width(label, lv_obj_get_width(card) - 36); + lv_obj_align(label, LV_ALIGN_TOP_LEFT, 0, 0); + return label; +} + static void create_overland_status_screen() { lv_obj_t *screen = lv_scr_act(); @@ -171,86 +306,49 @@ static void create_overland_status_screen() lv_label_set_text(title, "Overland Controller"); lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_text_font(title, &lv_font_montserrat_30, 0); - lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 24); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 18); lv_obj_t *subtitle = lv_label_create(screen); - lv_label_set_text(subtitle, "ESP32-S3 Dashboard - WiFi/API Bring-Up"); + lv_label_set_text(subtitle, "ESP32-S3 Dashboard - Live Cargo ESP Overview"); lv_obj_set_style_text_color(subtitle, lv_color_hex(0xB8C0C8), 0); lv_obj_set_width(subtitle, 920); lv_obj_set_style_text_align(subtitle, LV_TEXT_ALIGN_CENTER, 0); lv_obj_align_to(subtitle, title, LV_ALIGN_OUT_BOTTOM_MID, 0, 8); - lv_obj_t *left_card = lv_obj_create(screen); - lv_obj_set_size(left_card, 430, 360); - lv_obj_align(left_card, LV_ALIGN_LEFT_MID, 38, 20); - lv_obj_set_style_radius(left_card, 16, 0); - lv_obj_set_style_bg_color(left_card, lv_color_hex(0x1F2933), 0); - lv_obj_set_style_border_color(left_card, lv_color_hex(0x3A4652), 0); - lv_obj_set_style_border_width(left_card, 2, 0); - lv_obj_set_style_pad_all(left_card, 18, 0); + lv_obj_t *battery_card = create_card(screen, 300, 210, LV_ALIGN_TOP_LEFT, 32, 105); + lv_obj_t *temps_card = create_card(screen, 300, 210, LV_ALIGN_TOP_MID, 0, 105); + lv_obj_t *outputs_card = create_card(screen, 300, 210, LV_ALIGN_TOP_RIGHT, -32, 105); + lv_obj_t *system_card = create_card(screen, 620, 180, LV_ALIGN_BOTTOM_LEFT, 32, -55); + lv_obj_t *touch_card = create_card(screen, 300, 180, LV_ALIGN_BOTTOM_RIGHT, -32, -55); - lv_obj_t *right_card = lv_obj_create(screen); - lv_obj_set_size(right_card, 500, 360); - lv_obj_align(right_card, LV_ALIGN_RIGHT_MID, -38, 20); - lv_obj_set_style_radius(right_card, 16, 0); - lv_obj_set_style_bg_color(right_card, lv_color_hex(0x1F2933), 0); - lv_obj_set_style_border_color(right_card, lv_color_hex(0x3A4652), 0); - lv_obj_set_style_border_width(right_card, 2, 0); - lv_obj_set_style_pad_all(right_card, 18, 0); + battery_label = create_card_label(battery_card, "Battery\nWaiting for API..."); + temps_label = create_card_label(temps_card, "Temperatures\nWaiting for API..."); + outputs_label = create_card_label(outputs_card, "Outputs\nWaiting for API..."); - lv_obj_t *hardware_label = lv_label_create(left_card); - lv_label_set_text( - hardware_label, - "Hardware:\n" - "Waveshare ESP32-S3 Touch LCD 5B\n\n" - "Display:\n" - "1024 x 600\n\n" - "Cargo API Base:\n" - "/api/v1" - ); - lv_obj_set_style_text_color(hardware_label, lv_color_hex(0xFFFFFF), 0); - lv_obj_set_width(hardware_label, 380); - lv_obj_align(hardware_label, LV_ALIGN_TOP_LEFT, 0, 0); - - wifi_label = lv_label_create(left_card); - lv_label_set_text(wifi_label, "WiFi: not connected"); - lv_obj_set_style_text_color(wifi_label, lv_color_hex(0xB8C0C8), 0); - lv_obj_set_width(wifi_label, 380); - lv_obj_align(wifi_label, LV_ALIGN_TOP_LEFT, 0, 175); - - api_label = lv_label_create(left_card); + wifi_label = create_card_label(system_card, "WiFi: not connected"); + api_label = lv_label_create(system_card); lv_label_set_text(api_label, "API: waiting"); lv_obj_set_style_text_color(api_label, lv_color_hex(0xB8C0C8), 0); - lv_obj_set_width(api_label, 380); - lv_obj_align(api_label, LV_ALIGN_TOP_LEFT, 0, 250); + lv_obj_set_width(api_label, 560); + lv_obj_align(api_label, LV_ALIGN_TOP_LEFT, 0, 76); - lv_obj_t *button = lv_btn_create(left_card); + lv_obj_t *button = lv_btn_create(touch_card); lv_obj_set_size(button, 170, 54); - lv_obj_align(button, LV_ALIGN_BOTTOM_LEFT, 0, 0); + lv_obj_align(button, LV_ALIGN_TOP_LEFT, 0, 0); lv_obj_add_event_cb(button, touch_test_event_cb, LV_EVENT_CLICKED, nullptr); lv_obj_t *button_label = lv_label_create(button); lv_label_set_text(button_label, "Touch Test"); lv_obj_center(button_label); - touch_count_label = lv_label_create(left_card); + touch_count_label = lv_label_create(touch_card); lv_label_set_text(touch_count_label, "Touch test count: 0"); lv_obj_set_style_text_color(touch_count_label, lv_color_hex(0xB8C0C8), 0); - lv_obj_align(touch_count_label, LV_ALIGN_BOTTOM_LEFT, 190, -18); - - lv_obj_t *status_title = lv_label_create(right_card); - lv_label_set_text(status_title, "GET /api/v1/status"); - lv_obj_set_style_text_color(status_title, lv_color_hex(0xFFFFFF), 0); - lv_obj_align(status_title, LV_ALIGN_TOP_LEFT, 0, 0); - - status_label = lv_label_create(right_card); - lv_label_set_text(status_label, "No API response yet."); - lv_obj_set_style_text_color(status_label, lv_color_hex(0xDDE3EA), 0); - lv_obj_set_width(status_label, 455); - lv_obj_align(status_label, LV_ALIGN_TOP_LEFT, 0, 34); + lv_obj_set_width(touch_count_label, 250); + lv_obj_align(touch_count_label, LV_ALIGN_TOP_LEFT, 0, 78); lv_obj_t *footer = lv_label_create(screen); - lv_label_set_text(footer, "Current milestone: Dashboard WiFi client -> Cargo ESP /api/v1/status"); + lv_label_set_text(footer, "Polling Cargo ESP /api/v1/status every 5 seconds"); lv_obj_set_style_text_color(footer, lv_color_hex(0x7D8996), 0); lv_obj_set_width(footer, 900); lv_obj_set_style_text_align(footer, LV_TEXT_ALIGN_CENTER, 0); @@ -292,12 +390,12 @@ void setup() Serial.println("Initializing LVGL"); lvgl_port_init(board->getLCD(), board->getTouch()); - Serial.println("Creating Overland WiFi/API screen"); + Serial.println("Creating Overland status overview"); lvgl_port_lock(-1); create_overland_status_screen(); lvgl_port_unlock(); - Serial.println("Dashboard WiFi/API screen ready"); + Serial.println("Dashboard status overview ready"); } void loop()