From be5e6b84e15147593c980e46d2be4ba1d301738e Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 9 Jun 2026 23:34:45 -0600 Subject: [PATCH] dashboard: create first real overview UI --- .../esp32-s3-dashboard/dashboard_config.h | 2 +- .../esp32-s3-dashboard/esp32-s3-dashboard.ino | 714 ++++++------------ 2 files changed, 243 insertions(+), 473 deletions(-) diff --git a/firmware/esp32-s3-dashboard/dashboard_config.h b/firmware/esp32-s3-dashboard/dashboard_config.h index 8235810..fb9a8d0 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.0.12-queued-relay-commands"; +static const char *DASHBOARD_VERSION = "0.1.0-overview-ui"; // Update these if your Cargo ESP AP credentials are different. static const char *CARGO_WIFI_SSID = "OverlandController"; diff --git a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino index 1bf37fa..f223d78 100644 --- a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino +++ b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino @@ -12,24 +12,26 @@ using namespace esp_panel::drivers; using namespace esp_panel::board; -static lv_obj_t *battery_label = nullptr; -static lv_obj_t *temps_label = nullptr; -static lv_obj_t *outputs_label = nullptr; +static DashboardStatus status_model; + +static lv_obj_t *soc_value_label = nullptr; +static lv_obj_t *battery_detail_label = nullptr; +static lv_obj_t *inside_temp_label = nullptr; +static lv_obj_t *outside_temp_label = nullptr; +static lv_obj_t *system_status_label = nullptr; static lv_obj_t *relay_buttons[6] = {nullptr}; static lv_obj_t *relay_button_labels[6] = {nullptr}; + static String relay_ids[6]; static String relay_names[6]; static bool relay_states[6] = {false}; static int relay_count = 0; -static lv_obj_t *system_label = nullptr; -static lv_obj_t *wifi_label = nullptr; -static lv_obj_t *touch_count_label = nullptr; -static uint32_t touch_count = 0; static unsigned long last_wifi_attempt_ms = 0; static unsigned long last_wifi_label_update_ms = 0; static unsigned long last_status_poll_ms = 0; static unsigned long last_metadata_poll_ms = 0; + static bool api_request_in_progress = false; static bool fast_status_refresh_requested = false; @@ -39,33 +41,63 @@ static bool pending_relay_state = false; static int pending_relay_index = -1; static bool pending_relay_previous_state = false; -static String last_battery_text; -static String last_temps_text; -static String last_outputs_text; -static String last_system_text; -static String last_wifi_text; - -static DashboardStatus status_model; +static String last_soc_text; +static String last_battery_detail_text; +static String last_inside_temp_text; +static String last_outside_temp_text; +static String last_system_status_text; static String on_off(bool value) { return value ? "ON" : "OFF"; } +static void set_label_text_if_changed(lv_obj_t *label, String &last_text, const String &text) +{ + if (label == nullptr || last_text == text) { + return; + } + + last_text = text; + + lvgl_port_lock(-1); + lv_label_set_text(label, text.c_str()); + lvgl_port_unlock(); +} + static void request_fast_status_refresh() { fast_status_refresh_requested = true; } -static bool post_relay_state(const String &relay_id, bool state) +static void update_relay_buttons() { - if (api_request_in_progress) { - Serial.println("Relay command skipped: API request already in progress"); - return false; + lvgl_port_lock(-1); + + for (int i = 0; i < 6; i++) { + if (relay_buttons[i] == nullptr || relay_button_labels[i] == nullptr) { + continue; + } + + if (i < relay_count) { + lv_obj_clear_flag(relay_buttons[i], LV_OBJ_FLAG_HIDDEN); + + String label = relay_names[i].length() > 0 ? relay_names[i] : relay_ids[i]; + label += "\n"; + label += on_off(relay_states[i]); + + lv_label_set_text(relay_button_labels[i], label.c_str()); + } else { + lv_obj_add_flag(relay_buttons[i], LV_OBJ_FLAG_HIDDEN); + } } - if (WiFi.status() != WL_CONNECTED) { - Serial.println("Relay command skipped: WiFi not connected"); + lvgl_port_unlock(); +} + +static bool post_relay_state(const String &relay_id, bool state) +{ + if (api_request_in_progress || WiFi.status() != WL_CONNECTED) { return false; } @@ -109,175 +141,155 @@ static void relay_button_event_cb(lv_event_t *event) intptr_t index_value = reinterpret_cast(lv_event_get_user_data(event)); int index = static_cast(index_value); - if (index < 0 || index >= relay_count) { - Serial.println("Relay button index out of range"); + if (index < 0 || index >= relay_count || relay_command_pending) { return; } - if (relay_command_pending) { - set_label_text_if_changed(system_label, last_system_text, "Relay command already pending"); - Serial.println("Relay tap ignored: command already pending"); - return; - } - - String relay_id = relay_ids[index]; bool previous_state = relay_states[index]; bool next_state = !previous_state; relay_states[index] = next_state; update_relay_buttons(); - update_outputs_label_from_relays(); - pending_relay_id = relay_id; + pending_relay_id = relay_ids[index]; pending_relay_state = next_state; pending_relay_index = index; pending_relay_previous_state = previous_state; relay_command_pending = true; - String pending_text = "Queued relay command\n"; - pending_text += relay_id; - pending_text += " -> "; - pending_text += on_off(next_state); - set_label_text_if_changed(system_label, last_system_text, pending_text); + String text = "Queued "; + text += relay_names[index]; + text += " "; + text += on_off(next_state); - Serial.print("Queued relay command: "); - Serial.print(relay_id); - Serial.print(" -> "); - Serial.println(on_off(next_state)); + set_label_text_if_changed(system_status_label, last_system_status_text, text); } - -static void update_outputs_label_from_relays() +static void process_pending_relay_command() { - String outputs = ""; - - for (int i = 0; i < relay_count && i < 6; i++) { - outputs += String(i + 1); - outputs += ". "; - outputs += relay_names[i].length() > 0 ? relay_names[i] : relay_ids[i]; - outputs += ": "; - outputs += on_off(relay_states[i]); - outputs += "\n"; - } - - if (outputs.length() == 0) { - outputs = "No outputs reported"; - } - - status_model.outputs_text = outputs; - set_label_text_if_changed(outputs_label, last_outputs_text, outputs); -} - -static void update_relay_buttons() -{ - lvgl_port_lock(-1); - - for (int i = 0; i < 6; i++) { - if (relay_buttons[i] == nullptr || relay_button_labels[i] == nullptr) { - continue; - } - - if (i < relay_count) { - lv_obj_clear_flag(relay_buttons[i], LV_OBJ_FLAG_HIDDEN); - - String label = relay_ids[i]; - label += relay_states[i] ? " OFF" : " ON"; - lv_label_set_text(relay_button_labels[i], label.c_str()); - } else { - lv_obj_add_flag(relay_buttons[i], LV_OBJ_FLAG_HIDDEN); - } - } - - lvgl_port_unlock(); -} - - -static void set_label_text_if_changed(lv_obj_t *label, String &last_text, const String &text) -{ - if (label == nullptr || last_text == text) { + if (!relay_command_pending || api_request_in_progress) { return; } - last_text = text; + String relay_id = pending_relay_id; + bool next_state = pending_relay_state; + int index = pending_relay_index; + bool previous_state = pending_relay_previous_state; - lvgl_port_lock(-1); - lv_label_set_text(label, text.c_str()); - lvgl_port_unlock(); + bool ok = post_relay_state(relay_id, next_state); + + relay_command_pending = false; + pending_relay_id = ""; + pending_relay_index = -1; + + if (ok) { + set_label_text_if_changed(system_status_label, last_system_status_text, "Relay command sent"); + request_fast_status_refresh(); + } else { + if (index >= 0 && index < relay_count) { + relay_states[index] = previous_state; + update_relay_buttons(); + } + + set_label_text_if_changed(system_status_label, last_system_status_text, "Relay command failed"); + } +} + +static void connect_wifi_if_needed() +{ + if (WiFi.status() == WL_CONNECTED) { + return; + } + + if (millis() - last_wifi_attempt_ms < 10000) { + return; + } + + last_wifi_attempt_ms = millis(); + + Serial.print("Connecting to Cargo ESP AP: "); + Serial.println(CARGO_WIFI_SSID); + + WiFi.mode(WIFI_STA); + WiFi.begin(CARGO_WIFI_SSID, CARGO_WIFI_PASSWORD); +} + +static void update_system_status_label() +{ + if (millis() - last_wifi_label_update_ms < 5000) { + return; + } + + last_wifi_label_update_ms = millis(); + + String text; + + if (WiFi.status() == WL_CONNECTED) { + text += "Dashboard WiFi: connected"; + text += "\nIP: "; + text += WiFi.localIP().toString(); + text += " RSSI: "; + text += String(WiFi.RSSI()); + text += " dBm"; + } else { + text += "Dashboard WiFi: connecting"; + text += "\nSSID: "; + text += CARGO_WIFI_SSID; + } + + text += "\nCargo: "; + text += status_model.api_ok ? "online" : "waiting"; + + text += "\nProfile: "; + text += status_model.hardware_profile; + + text += "\nFirmware: "; + text += status_model.firmware_version; + + set_label_text_if_changed(system_status_label, last_system_status_text, text); } static void update_overview_widgets() { - String battery; - battery += "SOC: "; - battery += status_model.soc >= 0 ? String(status_model.soc) + "%" : "n/a"; - battery += "\nVoltage: "; - battery += String(status_model.voltage, 2); - battery += " V\nCurrent: "; - battery += String(status_model.current, 2); - battery += " A\nRemaining: "; - battery += String(status_model.remaining_ah, 1); - battery += " / "; - battery += String(status_model.capacity_ah, 0); - battery += " Ah\nBatt Temp: "; - battery += String(status_model.battery_temp_f, 1); - battery += " F\nBMS: "; - battery += status_model.bms_connected ? "online" : "offline"; + String soc_text; + soc_text += status_model.soc >= 0 ? String(status_model.soc) : "--"; + soc_text += "%"; + + String battery_detail; + battery_detail += String(status_model.voltage, 2); + battery_detail += " V\n"; + battery_detail += String(status_model.current, 2); + battery_detail += " A\n"; + battery_detail += String(status_model.remaining_ah, 1); + battery_detail += " / "; + battery_detail += String(status_model.capacity_ah, 0); + battery_detail += " Ah\n"; + battery_detail += String(status_model.battery_temp_f, 1); + battery_detail += " F battery\nBMS "; + battery_detail += status_model.bms_connected ? "online" : "offline"; if (status_model.cell_delta_mv >= 0) { - battery += "\nCell Delta: "; - battery += String(status_model.cell_delta_mv); - battery += " mV"; + battery_detail += "\nCell delta "; + battery_detail += String(status_model.cell_delta_mv); + battery_detail += " mV"; } - String system; - system += "API: "; - system += status_model.api_ok ? "connected" : "error"; - system += "\nHTTP: "; - system += String(status_model.http_code); - system += "\nProfile: "; - system += status_model.hardware_profile; - system += "\nOutputs: "; - system += String(status_model.output_count); - system += "\nFirmware: "; - system += status_model.firmware_version; - system += "\nUptime: "; - system += String(status_model.uptime_seconds); - system += " sec\nCargo AP: "; - system += status_model.cargo_ap_ssid; - system += "\nCargo STA IP: "; - system += status_model.cargo_sta_ip; - - set_label_text_if_changed(battery_label, last_battery_text, battery); - set_label_text_if_changed(temps_label, last_temps_text, status_model.temps_text); - set_label_text_if_changed(outputs_label, last_outputs_text, status_model.outputs_text); - set_label_text_if_changed(system_label, last_system_text, system); + set_label_text_if_changed(soc_value_label, last_soc_text, soc_text); + set_label_text_if_changed(battery_detail_label, last_battery_detail_text, battery_detail); + set_label_text_if_changed(system_status_label, last_system_status_text, last_system_status_text); } static void parse_status_json(const String &body) { - DynamicJsonDocument doc(20000); + DynamicJsonDocument doc(16000); DeserializationError error = deserializeJson(doc, body); if (error) { status_model.api_ok = false; - status_model.http_code = 200; - - String parse_error; - parse_error += "JSON parse failed\n"; - parse_error += error.c_str(); - - Serial.println(parse_error); - - set_label_text_if_changed(battery_label, last_battery_text, "Battery\nJSON parse failed"); - set_label_text_if_changed(temps_label, last_temps_text, parse_error); - set_label_text_if_changed(outputs_label, last_outputs_text, "Outputs\nNo parsed data"); - set_label_text_if_changed(system_label, last_system_text, parse_error); + set_label_text_if_changed(system_status_label, last_system_status_text, String("JSON parse failed: ") + error.c_str()); return; } - Serial.println("JSON parse OK"); - status_model.api_ok = true; - status_model.http_code = 200; if (doc["battery"].is()) { JsonObject battery = doc["battery"]; @@ -310,281 +322,82 @@ static void parse_status_json(const String &body) } if (doc["temps"].is()) { - String temps = ""; - JsonArray temp_array = doc["temps"]; - int shown_temps = 0; + String inside = "Inside\n--"; + String outside = "Outside\n--"; - for (JsonObject temp : temp_array) { - bool enabled = temp["enabled"] | false; - bool online = temp["online"] | false; + for (JsonObject temp : doc["temps"].as()) { + bool enabled = temp["enabled"] | false; + bool online = temp["online"] | false; + bool weather = temp["weather"] | false; - if (!enabled && shown_temps >= 2) { - continue; - } + if (!enabled || !online || temp["temperature_f"].isNull()) { + continue; + } - const char *name = temp["name"] | temp["id"] | "Temp"; + String value = String((float)(temp["temperature_f"] | 0.0), 1); + value += " F"; - temps += name; - temps += ": "; - - if (!enabled) { - temps += "disabled"; - } else if (!online) { - temps += "offline"; - } else if (temp["temperature_f"].isNull()) { - temps += "n/a"; - } else { - float temperature_f = temp["temperature_f"] | 0.0; - temps += String(temperature_f, 1); - temps += " F"; - } - - temps += "\n"; - shown_temps++; - - if (shown_temps >= 5) { - break; + if (weather) { + outside = "Outside\n" + value; + } else { + const char *name = temp["name"] | "Inside"; + inside = String(name) + "\n" + value; } } - if (temps.length() == 0) { - temps = "No temp sensors reported"; - } - - status_model.temps_text = temps; + set_label_text_if_changed(inside_temp_label, last_inside_temp_text, inside); + set_label_text_if_changed(outside_temp_label, last_outside_temp_text, outside); } if (doc["relays"].is()) { - String outputs = ""; - JsonArray relay_array = doc["relays"]; - relay_count = 0; - for (JsonObject relay : relay_array) { - const char *id = relay["id"] | ""; - const char *name = relay["name"] | relay["id"] | "Output"; - bool enabled = relay["enabled"] | false; - bool available = relay["available"] | true; - bool state = relay["state"] | false; - int channel = relay["hardware_channel"] | 0; + for (JsonObject relay : doc["relays"].as()) { + const char *id = relay["id"] | ""; + const char *name = relay["name"] | relay["id"] | "Output"; + bool enabled = relay["enabled"] | false; + bool available = relay["available"] | true; + bool state = relay["state"] | false; - if (relay_count < 6 && enabled && available) { - relay_ids[relay_count] = String(id); - relay_names[relay_count] = String(name); - relay_states[relay_count] = state; - relay_count++; + if (relay_count < 6 && enabled && available) { + relay_ids[relay_count] = String(id); + relay_names[relay_count] = String(name); + relay_states[relay_count] = state; + relay_count++; + } } - outputs += String(channel); - outputs += ". "; - outputs += name; - outputs += ": "; - - if (!available) { - outputs += "unavailable"; - } else if (!enabled) { - outputs += "disabled"; - } else { - outputs += on_off(state); - } - - outputs += "\n"; - } - - if (outputs.length() == 0) { - outputs = "No outputs reported"; - } - - status_model.outputs_text = outputs; update_relay_buttons(); } - Serial.println("Parsed status summary:"); - Serial.print(" SOC: "); - Serial.println(status_model.soc); - Serial.print(" Voltage: "); - Serial.println(status_model.voltage); - Serial.print(" Temps text: "); - Serial.println(status_model.temps_text); - Serial.print(" Outputs text: "); - Serial.println(status_model.outputs_text); - update_overview_widgets(); -} - -static void touch_test_event_cb(lv_event_t *event) -{ - if (lv_event_get_code(event) != LV_EVENT_CLICKED) { - return; - } - - touch_count++; - - if (touch_count_label != nullptr) { - lvgl_port_lock(-1); - lv_label_set_text_fmt(touch_count_label, "Touch count: %lu", (unsigned long)touch_count); - lvgl_port_unlock(); - } - - Serial.print("Touch count: "); - Serial.println(touch_count); -} - -static void process_pending_relay_command() -{ - if (!relay_command_pending) { - return; - } - - if (api_request_in_progress) { - return; - } - - String relay_id = pending_relay_id; - bool next_state = pending_relay_state; - int index = pending_relay_index; - bool previous_state = pending_relay_previous_state; - - String sending_text = "Sending relay command\n"; - sending_text += relay_id; - sending_text += " -> "; - sending_text += on_off(next_state); - set_label_text_if_changed(system_label, last_system_text, sending_text); - - bool ok = post_relay_state(relay_id, next_state); - - relay_command_pending = false; - pending_relay_id = ""; - pending_relay_index = -1; - - if (ok) { - String sent_text = "Relay command sent\n"; - sent_text += relay_id; - sent_text += " -> "; - sent_text += on_off(next_state); - sent_text += "\nRefreshing status..."; - set_label_text_if_changed(system_label, last_system_text, sent_text); - request_fast_status_refresh(); - } else { - if (index >= 0 && index < relay_count) { - relay_states[index] = previous_state; - update_relay_buttons(); - update_outputs_label_from_relays(); - } - - set_label_text_if_changed(system_label, last_system_text, "Relay command failed"); - } -} - -static void connect_wifi_if_needed() -{ - if (WiFi.status() == WL_CONNECTED) { - return; - } - - if (millis() - last_wifi_attempt_ms < 10000) { - return; - } - - last_wifi_attempt_ms = millis(); - - Serial.print("Connecting to Cargo ESP AP: "); - Serial.println(CARGO_WIFI_SSID); - - WiFi.mode(WIFI_STA); - WiFi.begin(CARGO_WIFI_SSID, CARGO_WIFI_PASSWORD); -} - -static void update_wifi_label() -{ - if (millis() - last_wifi_label_update_ms < 5000) { - return; - } - - last_wifi_label_update_ms = millis(); - - String text; - - if (WiFi.status() == WL_CONNECTED) { - text += "Dashboard WiFi: connected"; - text += "\nIP: "; - text += WiFi.localIP().toString(); - text += "\nRSSI: "; - text += String(WiFi.RSSI()); - text += " dBm"; - } else { - text += "Dashboard WiFi: not connected"; - text += "\nSSID: "; - text += CARGO_WIFI_SSID; - } - - set_label_text_if_changed(wifi_label, last_wifi_text, text); + update_system_status_label(); } static bool fetch_status_fields(const char *url, const char *label) { - if (api_request_in_progress) { - Serial.print("Skipping "); - Serial.print(label); - Serial.println(": request already in progress"); - return false; - } - - if (WiFi.status() != WL_CONNECTED) { - set_label_text_if_changed(system_label, last_system_text, "API: waiting for WiFi"); + if (api_request_in_progress || WiFi.status() != WL_CONNECTED) { return false; } api_request_in_progress = true; unsigned long start_ms = millis(); - Serial.print("GET "); - Serial.println(url); - HTTPClient http; http.setTimeout(5000); - - unsigned long begin_start_ms = millis(); http.begin(url); - unsigned long begin_ms = millis() - begin_start_ms; - - unsigned long get_start_ms = millis(); int code = http.GET(); - unsigned long get_ms = millis() - get_start_ms; status_model.http_code = code; - Serial.print(label); - Serial.print(" HTTP code: "); - Serial.println(code); - if (code == 200) { - unsigned long body_start_ms = millis(); String body = http.getString(); - unsigned long body_ms = millis() - body_start_ms; - - Serial.print(label); - Serial.print(" body length: "); - Serial.println(body.length()); - - unsigned long parse_start_ms = millis(); parse_status_json(body); - unsigned long parse_ms = millis() - parse_start_ms; - - unsigned long total_ms = millis() - start_ms; Serial.print("TIMING "); Serial.print(label); - Serial.print(" begin="); - Serial.print(begin_ms); - Serial.print("ms get="); - Serial.print(get_ms); - Serial.print("ms body="); - Serial.print(body_ms); - Serial.print("ms parse+ui="); - Serial.print(parse_ms); - Serial.print("ms total="); - Serial.print(total_ms); + Serial.print(" total="); + Serial.print(millis() - start_ms); Serial.print("ms bytes="); Serial.println(body.length()); @@ -593,27 +406,8 @@ static bool fetch_status_fields(const char *url, const char *label) return true; } - unsigned long total_ms = millis() - start_ms; - Serial.print("TIMING "); - Serial.print(label); - Serial.print(" failed total="); - Serial.print(total_ms); - Serial.println("ms"); - status_model.api_ok = false; - - String system; - system += "API: error\n"; - system += label; - system += "\nHTTP: "; - system += String(code); - if (code <= 0) { - system += "\n"; - system += http.errorToString(code); - } - - Serial.println(system); - set_label_text_if_changed(system_label, last_system_text, system); + set_label_text_if_changed(system_status_label, last_system_status_text, String("API error: HTTP ") + code); http.end(); api_request_in_progress = false; @@ -623,7 +417,6 @@ static bool fetch_status_fields(const char *url, const char *label) static void poll_status_api() { if (WiFi.status() != WL_CONNECTED) { - set_label_text_if_changed(system_label, last_system_text, "API: waiting for WiFi"); return; } @@ -644,32 +437,21 @@ static lv_obj_t *create_card(lv_obj_t *parent, const char *title, int width, int 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_radius(card, 18, 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, 12, 0); + lv_obj_set_style_pad_all(card, 14, 0); lv_obj_t *title_label = lv_label_create(card); lv_label_set_text(title_label, title); - lv_obj_set_style_text_color(title_label, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_color(title_label, lv_color_hex(0xB8C0C8), 0); lv_obj_align(title_label, LV_ALIGN_TOP_LEFT, 0, 0); return card; } -static lv_obj_t *create_body_label(lv_obj_t *card, int width) -{ - lv_obj_t *label = lv_label_create(card); - lv_label_set_text(label, "Waiting for /api/v1/status..."); - lv_obj_set_style_text_color(label, lv_color_hex(0xDDE3EA), 0); - lv_obj_set_width(label, width); - lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); - lv_obj_align(label, LV_ALIGN_TOP_LEFT, 0, 32); - return label; -} - -static void create_overland_status_screen() +static void create_overland_overview_screen() { lv_obj_t *screen = lv_scr_act(); lv_obj_clean(screen); @@ -681,40 +463,42 @@ static void create_overland_status_screen() lv_obj_set_style_text_font(title, &lv_font_montserrat_30, 0); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 16); - lv_obj_t *subtitle = lv_label_create(screen); - lv_label_set_text(subtitle, "ESP32-S3 Dashboard - 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, 6); + lv_obj_t *battery_card = create_card(screen, "Greta Battery", 300, 255, LV_ALIGN_TOP_LEFT, 32, 80); + lv_obj_t *temp_card = create_card(screen, "Temperatures", 300, 255, LV_ALIGN_TOP_MID, 0, 80); + lv_obj_t *relay_card = create_card(screen, "Outputs", 300, 255, LV_ALIGN_TOP_RIGHT, -32, 80); + lv_obj_t *system_card = create_card(screen, "System", 620, 175, LV_ALIGN_BOTTOM_LEFT, 32, -50); - lv_obj_t *battery_card = create_card(screen, "Battery", 300, 230, LV_ALIGN_TOP_LEFT, 32, 96); - lv_obj_t *temps_card = create_card(screen, "Temperatures", 300, 230, LV_ALIGN_TOP_MID, 0, 96); - lv_obj_t *outputs_card = create_card(screen, "Outputs", 300, 230, LV_ALIGN_TOP_RIGHT, -32, 96); - lv_obj_t *system_card = create_card(screen, "System / Network", 620, 190, LV_ALIGN_BOTTOM_LEFT, 32, -50); - lv_obj_t *touch_card = create_card(screen, "Relay Controls", 300, 190, LV_ALIGN_BOTTOM_RIGHT, -32, -50); + soc_value_label = lv_label_create(battery_card); + lv_label_set_text(soc_value_label, "--%"); + lv_obj_set_style_text_color(soc_value_label, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(soc_value_label, &lv_font_montserrat_30, 0); + lv_obj_align(soc_value_label, LV_ALIGN_TOP_LEFT, 0, 40); - battery_label = create_body_label(battery_card, 260); - temps_label = create_body_label(temps_card, 260); - outputs_label = create_body_label(outputs_card, 260); + battery_detail_label = lv_label_create(battery_card); + lv_label_set_text(battery_detail_label, "Waiting for battery data..."); + lv_obj_set_style_text_color(battery_detail_label, lv_color_hex(0xDDE3EA), 0); + lv_obj_set_width(battery_detail_label, 260); + lv_label_set_long_mode(battery_detail_label, LV_LABEL_LONG_WRAP); + lv_obj_align(battery_detail_label, LV_ALIGN_TOP_LEFT, 0, 95); - lv_label_set_text(battery_label, "Waiting for battery data..."); - lv_label_set_text(temps_label, "Waiting for temp data..."); - lv_label_set_text(outputs_label, "Waiting for output data..."); + inside_temp_label = lv_label_create(temp_card); + lv_label_set_text(inside_temp_label, "Inside\n--"); + lv_obj_set_style_text_color(inside_temp_label, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(inside_temp_label, &lv_font_montserrat_30, 0); + lv_obj_set_width(inside_temp_label, 260); + lv_obj_align(inside_temp_label, LV_ALIGN_TOP_LEFT, 0, 45); - wifi_label = create_body_label(system_card, 275); + outside_temp_label = lv_label_create(temp_card); + lv_label_set_text(outside_temp_label, "Outside\n--"); + lv_obj_set_style_text_color(outside_temp_label, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(outside_temp_label, &lv_font_montserrat_30, 0); + lv_obj_set_width(outside_temp_label, 260); + lv_obj_align(outside_temp_label, LV_ALIGN_TOP_LEFT, 0, 145); - system_label = lv_label_create(system_card); - lv_label_set_text(system_label, "API: waiting"); - lv_obj_set_style_text_color(system_label, lv_color_hex(0xB8C0C8), 0); - lv_obj_set_width(system_label, 285); - lv_label_set_long_mode(system_label, LV_LABEL_LONG_WRAP); - lv_obj_align(system_label, LV_ALIGN_TOP_LEFT, 305, 32); - - for (int i = 0; i < 2; i++) { - relay_buttons[i] = lv_btn_create(touch_card); - lv_obj_set_size(relay_buttons[i], 120, 48); - lv_obj_align(relay_buttons[i], LV_ALIGN_TOP_LEFT, i * 132, 34); + for (int i = 0; i < 6; i++) { + relay_buttons[i] = lv_btn_create(relay_card); + lv_obj_set_size(relay_buttons[i], 125, 60); + lv_obj_align(relay_buttons[i], LV_ALIGN_TOP_LEFT, (i % 2) * 140, 40 + ((i / 2) * 68)); lv_obj_add_event_cb( relay_buttons[i], relay_button_event_cb, @@ -723,32 +507,18 @@ static void create_overland_status_screen() ); relay_button_labels[i] = lv_label_create(relay_buttons[i]); - lv_label_set_text(relay_button_labels[i], "Relay"); - lv_obj_center(relay_button_labels[i]); - } - - for (int i = 2; i < 6; i++) { - relay_buttons[i] = lv_btn_create(touch_card); - lv_obj_set_size(relay_buttons[i], 120, 42); - lv_obj_align(relay_buttons[i], LV_ALIGN_TOP_LEFT, ((i - 2) % 2) * 132, 92 + (((i - 2) / 2) * 48)); - lv_obj_add_event_cb( - relay_buttons[i], - relay_button_event_cb, - LV_EVENT_CLICKED, - reinterpret_cast(static_cast(i)) - ); - - relay_button_labels[i] = lv_label_create(relay_buttons[i]); - lv_label_set_text(relay_button_labels[i], "Relay"); + lv_label_set_text(relay_button_labels[i], "Output\n--"); + lv_obj_set_style_text_align(relay_button_labels[i], LV_TEXT_ALIGN_CENTER, 0); lv_obj_center(relay_button_labels[i]); lv_obj_add_flag(relay_buttons[i], LV_OBJ_FLAG_HIDDEN); } - touch_count_label = lv_label_create(touch_card); - lv_label_set_text(touch_count_label, "Relay controls"); - lv_obj_set_style_text_color(touch_count_label, lv_color_hex(0xB8C0C8), 0); - lv_obj_set_width(touch_count_label, 250); - lv_obj_align(touch_count_label, LV_ALIGN_TOP_LEFT, 0, 142); + system_status_label = lv_label_create(system_card); + lv_label_set_text(system_status_label, "Connecting to Cargo ESP..."); + lv_obj_set_style_text_color(system_status_label, lv_color_hex(0xDDE3EA), 0); + lv_obj_set_width(system_status_label, 580); + lv_label_set_long_mode(system_status_label, LV_LABEL_LONG_WRAP); + lv_obj_align(system_status_label, LV_ALIGN_TOP_LEFT, 0, 35); lv_obj_t *footer = lv_label_create(screen); lv_label_set_text(footer, "Fast: battery/temps/relays every 2s | Slow: system/config/network every 30s"); @@ -792,18 +562,18 @@ void setup() lvgl_port_init(board->getLCD(), board->getTouch()); lvgl_port_lock(-1); - create_overland_status_screen(); + create_overland_overview_screen(); lvgl_port_unlock(); - Serial.println("Dashboard clean overview ready"); + Serial.println("Dashboard overview UI ready"); } void loop() { connect_wifi_if_needed(); - update_wifi_label(); + update_system_status_label(); process_pending_relay_command(); poll_status_api(); - delay(500); + delay(250); }