diff --git a/firmware/esp32-s3-dashboard/dashboard_config.h b/firmware/esp32-s3-dashboard/dashboard_config.h index 77399b1..89085bb 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.1.1-battery-card"; +static const char *DASHBOARD_VERSION = "0.1.2-battery-soc-gauge"; // 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 48a1c4f..b0c2521 100644 --- a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino +++ b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino @@ -14,9 +14,11 @@ using namespace esp_panel::board; static DashboardStatus status_model; -static lv_obj_t *soc_value_label = nullptr; +static lv_obj_t *battery_soc_arc = nullptr; +static lv_obj_t *battery_charge_pulse_arc = nullptr; static lv_obj_t *battery_voltage_label = nullptr; static lv_obj_t *battery_current_label = nullptr; +static lv_obj_t *battery_soc_label = nullptr; static lv_obj_t *battery_capacity_label = nullptr; static lv_obj_t *battery_health_label = nullptr; static lv_obj_t *inside_temp_label = nullptr; @@ -44,9 +46,9 @@ static bool pending_relay_state = false; static int pending_relay_index = -1; static bool pending_relay_previous_state = false; -static String last_soc_text; static String last_battery_voltage_text; static String last_battery_current_text; +static String last_battery_soc_text; static String last_battery_capacity_text; static String last_battery_health_text; static String last_inside_temp_text; @@ -58,6 +60,93 @@ static String on_off(bool value) return value ? "ON" : "OFF"; } +static int clamp_int(int value, int min_value, int max_value) +{ + if (value < min_value) { + return min_value; + } + + if (value > max_value) { + return max_value; + } + + return value; +} + +static void update_battery_current_label(const String &text, float current) +{ + if (battery_current_label == nullptr || last_battery_current_text == text) { + return; + } + + last_battery_current_text = text; + + lv_color_t current_color = lv_color_hex(0xB8C0C8); + if (current > 0.05) { + current_color = lv_color_hex(0x32D583); + } else if (current < -0.05) { + current_color = lv_color_hex(0xF97066); + } + + lvgl_port_lock(-1); + + if (text.length() == 0) { + lv_obj_add_flag(battery_current_label, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_clear_flag(battery_current_label, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text(battery_current_label, text.c_str()); + lv_obj_set_style_text_color(battery_current_label, current_color, 0); + } + + lvgl_port_unlock(); +} + +static void update_battery_soc_gauge() +{ + if (battery_soc_arc == nullptr || battery_charge_pulse_arc == nullptr) { + return; + } + + int soc = clamp_int(status_model.soc, 0, 100); + bool charging = status_model.current > 0.05; + + lvgl_port_lock(-1); + + lv_arc_set_value(battery_soc_arc, soc); + lv_arc_set_value(battery_charge_pulse_arc, soc); + + if (charging) { + lv_obj_clear_flag(battery_charge_pulse_arc, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(battery_charge_pulse_arc, LV_OBJ_FLAG_HIDDEN); + } + + lvgl_port_unlock(); +} + +static void update_battery_charge_animation() +{ + if (battery_charge_pulse_arc == nullptr || status_model.current <= 0.05) { + return; + } + + static unsigned long last_pulse_ms = 0; + static bool pulse_high = false; + + if (millis() - last_pulse_ms < 450) { + return; + } + + last_pulse_ms = millis(); + pulse_high = !pulse_high; + + lvgl_port_lock(-1); + lv_obj_set_style_arc_width(battery_charge_pulse_arc, pulse_high ? 20 : 12, LV_PART_INDICATOR); + lv_obj_set_style_arc_opa(battery_charge_pulse_arc, pulse_high ? LV_OPA_80 : LV_OPA_35, LV_PART_INDICATOR); + 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) { @@ -257,17 +346,20 @@ static void update_system_status_label() static void update_overview_widgets() { - String soc_text; - soc_text += status_model.soc >= 0 ? String(status_model.soc) : "--"; - soc_text += "%"; - String voltage_text; voltage_text += String(status_model.voltage, 2); voltage_text += " V"; - String current_text; - current_text += String(status_model.current, 2); - current_text += " A"; + String current_text = ""; + if (status_model.current > 0.05 || status_model.current < -0.05) { + current_text += status_model.current > 0 ? "+" : ""; + current_text += String(status_model.current, 2); + current_text += " A"; + } + + String soc_text; + soc_text += status_model.soc >= 0 ? String(status_model.soc) : "--"; + soc_text += "% SOC"; String capacity_text; capacity_text += String(status_model.remaining_ah, 1); @@ -287,9 +379,10 @@ static void update_overview_widgets() health_text += " mV"; } - set_label_text_if_changed(soc_value_label, last_soc_text, soc_text); + update_battery_soc_gauge(); set_label_text_if_changed(battery_voltage_label, last_battery_voltage_text, voltage_text); - set_label_text_if_changed(battery_current_label, last_battery_current_text, current_text); + update_battery_current_label(current_text, status_model.current); + set_label_text_if_changed(battery_soc_label, last_battery_soc_text, soc_text); set_label_text_if_changed(battery_capacity_label, last_battery_capacity_text, capacity_text); set_label_text_if_changed(battery_health_label, last_battery_health_text, health_text); set_label_text_if_changed(system_status_label, last_system_status_text, last_system_status_text); @@ -485,39 +578,72 @@ static void create_overland_overview_screen() 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); - 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, 34); + battery_charge_pulse_arc = lv_arc_create(battery_card); + lv_obj_set_size(battery_charge_pulse_arc, 172, 172); + lv_obj_align(battery_charge_pulse_arc, LV_ALIGN_TOP_MID, 0, 30); + lv_arc_set_range(battery_charge_pulse_arc, 0, 100); + lv_arc_set_value(battery_charge_pulse_arc, 0); + lv_arc_set_rotation(battery_charge_pulse_arc, 135); + lv_arc_set_bg_angles(battery_charge_pulse_arc, 0, 270); + lv_obj_remove_style(battery_charge_pulse_arc, NULL, LV_PART_KNOB); + lv_obj_clear_flag(battery_charge_pulse_arc, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_arc_opa(battery_charge_pulse_arc, LV_OPA_TRANSP, LV_PART_MAIN); + lv_obj_set_style_arc_color(battery_charge_pulse_arc, lv_color_hex(0x32D583), LV_PART_INDICATOR); + lv_obj_set_style_arc_width(battery_charge_pulse_arc, 14, LV_PART_INDICATOR); + lv_obj_set_style_arc_opa(battery_charge_pulse_arc, LV_OPA_35, LV_PART_INDICATOR); + lv_obj_add_flag(battery_charge_pulse_arc, LV_OBJ_FLAG_HIDDEN); - lv_obj_t *soc_caption = lv_label_create(battery_card); - lv_label_set_text(soc_caption, "STATE OF CHARGE"); - lv_obj_set_style_text_color(soc_caption, lv_color_hex(0x7D8996), 0); - lv_obj_align(soc_caption, LV_ALIGN_TOP_LEFT, 0, 82); + battery_soc_arc = lv_arc_create(battery_card); + lv_obj_set_size(battery_soc_arc, 158, 158); + lv_obj_align(battery_soc_arc, LV_ALIGN_TOP_MID, 0, 37); + lv_arc_set_range(battery_soc_arc, 0, 100); + lv_arc_set_value(battery_soc_arc, 0); + lv_arc_set_rotation(battery_soc_arc, 135); + lv_arc_set_bg_angles(battery_soc_arc, 0, 270); + lv_obj_remove_style(battery_soc_arc, NULL, LV_PART_KNOB); + lv_obj_clear_flag(battery_soc_arc, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_arc_color(battery_soc_arc, lv_color_hex(0x3A4652), LV_PART_MAIN); + lv_obj_set_style_arc_width(battery_soc_arc, 13, LV_PART_MAIN); + lv_obj_set_style_arc_color(battery_soc_arc, lv_color_hex(0x84CAFF), LV_PART_INDICATOR); + lv_obj_set_style_arc_width(battery_soc_arc, 13, LV_PART_INDICATOR); battery_voltage_label = lv_label_create(battery_card); lv_label_set_text(battery_voltage_label, "-- V"); lv_obj_set_style_text_color(battery_voltage_label, lv_color_hex(0xFFFFFF), 0); - lv_obj_align(battery_voltage_label, LV_ALIGN_TOP_LEFT, 0, 112); + lv_obj_set_style_text_font(battery_voltage_label, &lv_font_montserrat_30, 0); + lv_obj_set_width(battery_voltage_label, 180); + lv_obj_set_style_text_align(battery_voltage_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(battery_voltage_label, LV_ALIGN_TOP_MID, 0, 83); battery_current_label = lv_label_create(battery_card); - lv_label_set_text(battery_current_label, "-- A"); - lv_obj_set_style_text_color(battery_current_label, lv_color_hex(0xFFFFFF), 0); - lv_obj_align(battery_current_label, LV_ALIGN_TOP_LEFT, 135, 112); + lv_label_set_text(battery_current_label, ""); + lv_obj_set_style_text_color(battery_current_label, lv_color_hex(0xB8C0C8), 0); + lv_obj_set_width(battery_current_label, 170); + lv_obj_set_style_text_align(battery_current_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(battery_current_label, LV_ALIGN_TOP_MID, 0, 122); + lv_obj_add_flag(battery_current_label, LV_OBJ_FLAG_HIDDEN); + + battery_soc_label = lv_label_create(battery_card); + lv_label_set_text(battery_soc_label, "--% SOC"); + lv_obj_set_style_text_color(battery_soc_label, lv_color_hex(0xB8C0C8), 0); + lv_obj_set_width(battery_soc_label, 180); + lv_obj_set_style_text_align(battery_soc_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(battery_soc_label, LV_ALIGN_TOP_MID, 0, 155); battery_capacity_label = lv_label_create(battery_card); lv_label_set_text(battery_capacity_label, "-- / -- Ah"); lv_obj_set_style_text_color(battery_capacity_label, lv_color_hex(0xDDE3EA), 0); lv_obj_set_width(battery_capacity_label, 260); - lv_obj_align(battery_capacity_label, LV_ALIGN_TOP_LEFT, 0, 148); + lv_obj_set_style_text_align(battery_capacity_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(battery_capacity_label, LV_ALIGN_TOP_MID, 0, 190); battery_health_label = lv_label_create(battery_card); lv_label_set_text(battery_health_label, "Waiting for BMS..."); lv_obj_set_style_text_color(battery_health_label, lv_color_hex(0xB8C0C8), 0); lv_obj_set_width(battery_health_label, 260); lv_label_set_long_mode(battery_health_label, LV_LABEL_LONG_WRAP); - lv_obj_align(battery_health_label, LV_ALIGN_TOP_LEFT, 0, 182); + lv_obj_set_style_text_align(battery_health_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(battery_health_label, LV_ALIGN_TOP_MID, 0, 214); inside_temp_label = lv_label_create(temp_card); lv_label_set_text(inside_temp_label, "Inside\n--"); @@ -612,6 +738,7 @@ void loop() update_system_status_label(); process_pending_relay_command(); poll_status_api(); + update_battery_charge_animation(); delay(250); }