#include #include #include #include #include #include #include "lvgl_v8_port.h" using namespace esp_panel::drivers; using namespace esp_panel::board; 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"; 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 *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 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 void set_label_text_if_changed(lv_obj_t *label, String &last_text, const String &text) { if (label == nullptr) { return; } if (last_text == text) { return; } last_text = text; lvgl_port_lock(-1); lv_label_set_text(label, text.c_str()); 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) { return; } touch_count++; 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); lvgl_port_unlock(); } Serial.print("Touch test count: "); Serial.println(touch_count); } 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); 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); } static void update_wifi_label() { if (millis() - last_wifi_label_update_ms < 5000) { return; } last_wifi_label_update_ms = millis(); if (WiFi.status() == WL_CONNECTED) { String text = "WiFi: connected\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; } 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"); return; } if (millis() - last_status_poll_ms < 5000) { return; } last_status_poll_ms = millis(); Serial.print("GET "); Serial.println(CARGO_API_STATUS_URL); HTTPClient http; http.begin(CARGO_API_STATUS_URL); int code = http.GET(); if (code == 200) { String body = http.getString(); Serial.println("HTTP 200"); Serial.println(body); 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); } 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(); 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_t *subtitle = lv_label_create(screen); 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 *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); 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..."); 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); 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_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(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_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, "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); } void setup() { Serial.begin(115200); delay(1000); Serial.println(); Serial.println("================================"); Serial.println("Overland Controller Dashboard"); Serial.println("================================"); Serial.print("Version: "); Serial.println(DASHBOARD_VERSION); Serial.println("Target: Waveshare ESP32-S3 Touch LCD 5B"); Serial.println("Display: 1024x600"); Serial.println("API Base: /api/v1"); Serial.println("Initializing board"); Board *board = new Board(); board->init(); #if LVGL_PORT_AVOID_TEARING_MODE auto lcd = board->getLCD(); lcd->configFrameBufferNumber(LVGL_PORT_DISP_BUFFER_NUM); #if ESP_PANEL_DRIVERS_BUS_ENABLE_RGB && CONFIG_IDF_TARGET_ESP32S3 auto lcd_bus = lcd->getBus(); if (lcd_bus->getBasicAttributes().type == ESP_PANEL_BUS_TYPE_RGB) { static_cast(lcd_bus)->configRGB_BounceBufferSize(lcd->getFrameWidth() * 10); } #endif #endif 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"); } void loop() { connect_wifi_if_needed(); update_wifi_label(); poll_status_api(); delay(500); }