diff --git a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino index b970e41..319d81e 100644 --- a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino +++ b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino @@ -10,19 +10,18 @@ using namespace esp_panel::drivers; using namespace esp_panel::board; -static const char *DASHBOARD_VERSION = "0.0.3-status-overview"; +static const char *DASHBOARD_VERSION = "0.0.4-clean-overview"; // Update these if your Cargo ESP AP credentials are different. static const char *CARGO_WIFI_SSID = "OverlandController"; static const char *CARGO_WIFI_PASSWORD = "overland1234"; - 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 *battery_label = nullptr; static lv_obj_t *temps_label = nullptr; static lv_obj_t *outputs_label = nullptr; +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; @@ -30,19 +29,46 @@ 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 String last_wifi_label_text; -static String last_api_label_text; -static String last_battery_label_text; -static String last_temps_label_text; -static String last_outputs_label_text; +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; + +struct DashboardStatus { + bool api_ok = false; + int http_code = 0; + + bool bms_connected = false; + int soc = -1; + float voltage = 0.0; + float current = 0.0; + float battery_temp_f = 0.0; + float remaining_ah = 0.0; + float capacity_ah = 0.0; + int cell_delta_mv = -1; + + String temps_text; + String outputs_text; + + String hardware_profile = "unknown"; + int output_count = 0; + String firmware_version = "unknown"; + unsigned long uptime_seconds = 0; + String cargo_ap_ssid = "unknown"; + String cargo_sta_ip = "unknown"; +}; + +static DashboardStatus status_model; + +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) { - return; - } - - if (last_text == text) { + if (label == nullptr || last_text == text) { return; } @@ -53,9 +79,167 @@ static void set_label_text_if_changed(lv_obj_t *label, String &last_text, const lvgl_port_unlock(); } -static String on_off(bool value) +static void update_overview_widgets() { - return value ? "ON" : "OFF"; + 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"; + if (status_model.cell_delta_mv >= 0) { + battery += "\nCell Delta: "; + battery += String(status_model.cell_delta_mv); + battery += " 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); +} + +static void parse_status_json(const String &body) +{ + DynamicJsonDocument doc(20000); + DeserializationError error = deserializeJson(doc, body); + + if (error) { + status_model.api_ok = false; + status_model.http_code = 200; + status_model.temps_text = "JSON parse failed"; + status_model.outputs_text = error.c_str(); + update_overview_widgets(); + return; + } + + status_model.api_ok = true; + status_model.http_code = 200; + + JsonObject battery = doc["battery"]; + status_model.bms_connected = battery["connected"] | false; + status_model.soc = battery["soc"] | -1; + status_model.voltage = battery["voltage"] | 0.0; + status_model.current = battery["current"] | 0.0; + status_model.battery_temp_f = battery["temperature_f"] | 0.0; + status_model.remaining_ah = battery["remaining_ah"] | 0.0; + status_model.capacity_ah = battery["capacity_ah"] | 0.0; + status_model.cell_delta_mv = battery["cell_delta_mv"] | -1; + + JsonObject config = doc["config"]; + status_model.hardware_profile = String((const char *)(config["hardware_profile"] | "unknown")); + status_model.output_count = config["output_count"] | 0; + + JsonObject system = doc["system"]; + status_model.firmware_version = String((const char *)(system["firmware_version"] | "unknown")); + status_model.uptime_seconds = system["uptime_seconds"] | 0; + + JsonObject network = doc["network"]; + status_model.cargo_ap_ssid = String((const char *)(network["ap_ssid"] | "unknown")); + status_model.cargo_sta_ip = String((const char *)(network["sta_ip"] | "n/a")); + + String temps = ""; + JsonArray temp_array = doc["temps"]; + int shown_temps = 0; + + for (JsonObject temp : temp_array) { + bool enabled = temp["enabled"] | false; + bool online = temp["online"] | false; + + if (!enabled && shown_temps >= 2) { + continue; + } + + const char *name = temp["name"] | temp["id"] | "Temp"; + + 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 (temps.length() == 0) { + temps = "No temp sensors reported"; + } + + String outputs = ""; + JsonArray relay_array = doc["relays"]; + + for (JsonObject relay : relay_array) { + 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; + + 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.temps_text = temps; + status_model.outputs_text = outputs; + + update_overview_widgets(); } static void touch_test_event_cb(lv_event_t *event) @@ -68,11 +252,11 @@ static void touch_test_event_cb(lv_event_t *event) if (touch_count_label != nullptr) { lvgl_port_lock(-1); - lv_label_set_text_fmt(touch_count_label, "Touch test count: %lu", (unsigned long)touch_count); + lv_label_set_text_fmt(touch_count_label, "Touch count: %lu", (unsigned long)touch_count); lvgl_port_unlock(); } - Serial.print("Touch test count: "); + Serial.print("Touch count: "); Serial.println(touch_count); } @@ -91,8 +275,6 @@ static void connect_wifi_if_needed() Serial.print("Connecting to Cargo ESP AP: "); Serial.println(CARGO_WIFI_SSID); - set_label_text_if_changed(wifi_label, last_wifi_label_text, "WiFi: connecting..."); - WiFi.mode(WIFI_STA); WiFi.begin(CARGO_WIFI_SSID, CARGO_WIFI_PASSWORD); } @@ -105,135 +287,27 @@ static void update_wifi_label() last_wifi_label_update_ms = millis(); + String text; + if (WiFi.status() == WL_CONNECTED) { - String text = "WiFi: connected\nIP: "; + text += "Dashboard WiFi: connected"; + text += "\nIP: "; text += WiFi.localIP().toString(); text += "\nRSSI: "; text += String(WiFi.RSSI()); text += " dBm"; - set_label_text_if_changed(wifi_label, last_wifi_label_text, text); } else { - set_label_text_if_changed(wifi_label, last_wifi_label_text, "WiFi: not connected\nSSID: " + String(CARGO_WIFI_SSID)); - } -} - -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; + text += "Dashboard WiFi: not connected"; + text += "\nSSID: "; + text += CARGO_WIFI_SSID; } - 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); + set_label_text_if_changed(wifi_label, last_wifi_text, 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"); return; } @@ -250,29 +324,30 @@ static void poll_status_api() http.begin(CARGO_API_STATUS_URL); int code = http.GET(); + status_model.http_code = code; + if (code == 200) { String body = http.getString(); - Serial.println("HTTP 200"); - Serial.println(body); - - update_status_widgets(body); + parse_status_json(body); } else { - String error_text = "API: HTTP "; - error_text += String(code); + status_model.api_ok = false; + + String system; + system += "API: error\nHTTP: "; + system += String(code); if (code <= 0) { - error_text += "\n"; - error_text += http.errorToString(code); + system += "\n"; + system += http.errorToString(code); } - Serial.println(error_text); - set_label_text_if_changed(api_label, last_api_label_text, error_text); + set_label_text_if_changed(system_label, last_system_text, system); } http.end(); } -static lv_obj_t *create_card(lv_obj_t *parent, int width, int height, lv_align_t align, int x, int y) +static lv_obj_t *create_card(lv_obj_t *parent, const char *title, 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); @@ -281,17 +356,23 @@ static lv_obj_t *create_card(lv_obj_t *parent, int width, int height, lv_align_t 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); + lv_obj_set_style_pad_all(card, 12, 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_align(title_label, LV_ALIGN_TOP_LEFT, 0, 0); + return card; } -static lv_obj_t *create_card_label(lv_obj_t *card, const char *text) +static lv_obj_t *create_body_label(lv_obj_t *card) { 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); + 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, lv_obj_get_width(card) - 34); + lv_obj_align(label, LV_ALIGN_TOP_LEFT, 0, 32); return label; } @@ -299,42 +380,42 @@ static void create_overland_status_screen() { lv_obj_t *screen = lv_scr_act(); lv_obj_clean(screen); - lv_obj_set_style_bg_color(screen, lv_color_hex(0x101418), 0); lv_obj_t *title = lv_label_create(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, 18); + 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 - Live Cargo ESP Overview"); + 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, 8); + lv_obj_align_to(subtitle, title, LV_ALIGN_OUT_BOTTOM_MID, 0, 6); - 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 *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, "Touch", 300, 190, LV_ALIGN_BOTTOM_RIGHT, -32, -50); - 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..."); + battery_label = create_body_label(battery_card); + temps_label = create_body_label(temps_card); + outputs_label = create_body_label(outputs_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, 560); - lv_obj_align(api_label, LV_ALIGN_TOP_LEFT, 0, 76); + wifi_label = create_body_label(system_card); + + 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, 300); + lv_obj_align(system_label, LV_ALIGN_TOP_LEFT, 305, 32); lv_obj_t *button = lv_btn_create(touch_card); lv_obj_set_size(button, 170, 54); - lv_obj_align(button, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_align(button, LV_ALIGN_TOP_LEFT, 0, 36); lv_obj_add_event_cb(button, touch_test_event_cb, LV_EVENT_CLICKED, nullptr); lv_obj_t *button_label = lv_label_create(button); @@ -342,17 +423,17 @@ static void create_overland_status_screen() lv_obj_center(button_label); touch_count_label = lv_label_create(touch_card); - lv_label_set_text(touch_count_label, "Touch test count: 0"); + lv_label_set_text(touch_count_label, "Touch count: 0"); 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, 78); + lv_obj_align(touch_count_label, LV_ALIGN_TOP_LEFT, 0, 105); lv_obj_t *footer = lv_label_create(screen); 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); - lv_obj_align(footer, LV_ALIGN_BOTTOM_MID, 0, -18); + lv_obj_align(footer, LV_ALIGN_BOTTOM_MID, 0, -14); } void setup() @@ -370,7 +451,6 @@ void setup() Serial.println("Display: 1024x600"); Serial.println("API Base: /api/v1"); - Serial.println("Initializing board"); Board *board = new Board(); board->init(); @@ -387,15 +467,13 @@ void setup() assert(board->begin()); - Serial.println("Initializing LVGL"); lvgl_port_init(board->getLCD(), board->getTouch()); - Serial.println("Creating Overland status overview"); lvgl_port_lock(-1); create_overland_status_screen(); lvgl_port_unlock(); - Serial.println("Dashboard status overview ready"); + Serial.println("Dashboard clean overview ready"); } void loop()