745 lines
25 KiB
Arduino
745 lines
25 KiB
Arduino
#include <Arduino.h>
|
|
#include <WiFi.h>
|
|
#include <HTTPClient.h>
|
|
#include <ArduinoJson.h>
|
|
#include <esp_display_panel.hpp>
|
|
|
|
#include <lvgl.h>
|
|
#include "lvgl_v8_port.h"
|
|
#include "dashboard_config.h"
|
|
#include "dashboard_status_model.h"
|
|
|
|
using namespace esp_panel::drivers;
|
|
using namespace esp_panel::board;
|
|
|
|
static DashboardStatus status_model;
|
|
|
|
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;
|
|
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 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;
|
|
|
|
static bool relay_command_pending = false;
|
|
static String pending_relay_id;
|
|
static bool pending_relay_state = false;
|
|
static int pending_relay_index = -1;
|
|
static bool pending_relay_previous_state = false;
|
|
|
|
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;
|
|
static String last_outside_temp_text;
|
|
static String last_system_status_text;
|
|
|
|
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_30, 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) {
|
|
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 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_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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
api_request_in_progress = true;
|
|
|
|
DynamicJsonDocument request(256);
|
|
request["id"] = relay_id;
|
|
request["state"] = state;
|
|
|
|
String payload;
|
|
serializeJson(request, payload);
|
|
|
|
Serial.print("POST ");
|
|
Serial.println(CARGO_API_RELAY_SET_URL);
|
|
Serial.println(payload);
|
|
|
|
HTTPClient http;
|
|
http.setTimeout(5000);
|
|
http.begin(CARGO_API_RELAY_SET_URL);
|
|
http.addHeader("Content-Type", "application/json");
|
|
|
|
int code = http.POST(payload);
|
|
String body = http.getString();
|
|
|
|
Serial.print("Relay HTTP code: ");
|
|
Serial.println(code);
|
|
Serial.println(body);
|
|
|
|
http.end();
|
|
api_request_in_progress = false;
|
|
|
|
return code >= 200 && code < 300;
|
|
}
|
|
|
|
static void relay_button_event_cb(lv_event_t *event)
|
|
{
|
|
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
|
|
return;
|
|
}
|
|
|
|
intptr_t index_value = reinterpret_cast<intptr_t>(lv_event_get_user_data(event));
|
|
int index = static_cast<int>(index_value);
|
|
|
|
if (index < 0 || index >= relay_count || relay_command_pending) {
|
|
return;
|
|
}
|
|
|
|
bool previous_state = relay_states[index];
|
|
bool next_state = !previous_state;
|
|
|
|
relay_states[index] = next_state;
|
|
update_relay_buttons();
|
|
|
|
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 text = "Queued ";
|
|
text += relay_names[index];
|
|
text += " ";
|
|
text += on_off(next_state);
|
|
|
|
set_label_text_if_changed(system_status_label, last_system_status_text, text);
|
|
}
|
|
|
|
static void process_pending_relay_command()
|
|
{
|
|
if (!relay_command_pending || 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;
|
|
|
|
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 voltage_text;
|
|
voltage_text += String(status_model.voltage, 2);
|
|
voltage_text += " V";
|
|
|
|
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);
|
|
capacity_text += " / ";
|
|
capacity_text += String(status_model.capacity_ah, 0);
|
|
capacity_text += " Ah";
|
|
|
|
String health_text;
|
|
health_text += "BMS ";
|
|
health_text += status_model.bms_connected ? "online" : "offline";
|
|
health_text += " | ";
|
|
health_text += String(status_model.battery_temp_f, 1);
|
|
health_text += " F";
|
|
if (status_model.cell_delta_mv >= 0) {
|
|
health_text += "\nCell delta ";
|
|
health_text += String(status_model.cell_delta_mv);
|
|
health_text += " mV";
|
|
}
|
|
|
|
update_battery_soc_gauge();
|
|
set_label_text_if_changed(battery_voltage_label, last_battery_voltage_text, voltage_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);
|
|
}
|
|
|
|
static void parse_status_json(const String &body)
|
|
{
|
|
DynamicJsonDocument doc(16000);
|
|
DeserializationError error = deserializeJson(doc, body);
|
|
|
|
if (error) {
|
|
status_model.api_ok = false;
|
|
set_label_text_if_changed(system_status_label, last_system_status_text, String("JSON parse failed: ") + error.c_str());
|
|
return;
|
|
}
|
|
|
|
status_model.api_ok = true;
|
|
|
|
if (doc["battery"].is<JsonObject>()) {
|
|
JsonObject battery = doc["battery"];
|
|
status_model.bms_connected = battery["connected"] | status_model.bms_connected;
|
|
status_model.soc = battery["soc"] | status_model.soc;
|
|
status_model.voltage = battery["voltage"] | status_model.voltage;
|
|
status_model.current = battery["current"] | status_model.current;
|
|
status_model.battery_temp_f = battery["temperature_f"] | status_model.battery_temp_f;
|
|
status_model.remaining_ah = battery["remaining_ah"] | status_model.remaining_ah;
|
|
status_model.capacity_ah = battery["capacity_ah"] | status_model.capacity_ah;
|
|
status_model.cell_delta_mv = battery["cell_delta_mv"] | status_model.cell_delta_mv;
|
|
}
|
|
|
|
if (doc["config"].is<JsonObject>()) {
|
|
JsonObject config = doc["config"];
|
|
status_model.hardware_profile = String((const char *)(config["hardware_profile"] | status_model.hardware_profile.c_str()));
|
|
status_model.output_count = config["output_count"] | status_model.output_count;
|
|
}
|
|
|
|
if (doc["system"].is<JsonObject>()) {
|
|
JsonObject system = doc["system"];
|
|
status_model.firmware_version = String((const char *)(system["firmware_version"] | status_model.firmware_version.c_str()));
|
|
status_model.uptime_seconds = system["uptime_seconds"] | status_model.uptime_seconds;
|
|
}
|
|
|
|
if (doc["network"].is<JsonObject>()) {
|
|
JsonObject network = doc["network"];
|
|
status_model.cargo_ap_ssid = String((const char *)(network["ap_ssid"] | status_model.cargo_ap_ssid.c_str()));
|
|
status_model.cargo_sta_ip = String((const char *)(network["sta_ip"] | status_model.cargo_sta_ip.c_str()));
|
|
}
|
|
|
|
if (doc["temps"].is<JsonArray>()) {
|
|
String inside = "Inside\n--";
|
|
String outside = "Outside\n--";
|
|
|
|
for (JsonObject temp : doc["temps"].as<JsonArray>()) {
|
|
bool enabled = temp["enabled"] | false;
|
|
bool online = temp["online"] | false;
|
|
bool weather = temp["weather"] | false;
|
|
|
|
if (!enabled || !online || temp["temperature_f"].isNull()) {
|
|
continue;
|
|
}
|
|
|
|
String value = String((float)(temp["temperature_f"] | 0.0), 1);
|
|
value += " F";
|
|
|
|
if (weather) {
|
|
outside = "Outside\n" + value;
|
|
} else {
|
|
const char *name = temp["name"] | "Inside";
|
|
inside = String(name) + "\n" + value;
|
|
}
|
|
}
|
|
|
|
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<JsonArray>()) {
|
|
relay_count = 0;
|
|
|
|
for (JsonObject relay : doc["relays"].as<JsonArray>()) {
|
|
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++;
|
|
}
|
|
}
|
|
|
|
update_relay_buttons();
|
|
}
|
|
|
|
update_overview_widgets();
|
|
update_system_status_label();
|
|
}
|
|
|
|
static bool fetch_status_fields(const char *url, const char *label)
|
|
{
|
|
if (api_request_in_progress || WiFi.status() != WL_CONNECTED) {
|
|
return false;
|
|
}
|
|
|
|
api_request_in_progress = true;
|
|
unsigned long start_ms = millis();
|
|
|
|
HTTPClient http;
|
|
http.setTimeout(5000);
|
|
http.begin(url);
|
|
int code = http.GET();
|
|
|
|
status_model.http_code = code;
|
|
|
|
if (code == 200) {
|
|
String body = http.getString();
|
|
parse_status_json(body);
|
|
|
|
Serial.print("TIMING ");
|
|
Serial.print(label);
|
|
Serial.print(" total=");
|
|
Serial.print(millis() - start_ms);
|
|
Serial.print("ms bytes=");
|
|
Serial.println(body.length());
|
|
|
|
http.end();
|
|
api_request_in_progress = false;
|
|
return true;
|
|
}
|
|
|
|
status_model.api_ok = false;
|
|
set_label_text_if_changed(system_status_label, last_system_status_text, String("API error: HTTP ") + code);
|
|
|
|
http.end();
|
|
api_request_in_progress = false;
|
|
return false;
|
|
}
|
|
|
|
static void poll_status_api()
|
|
{
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
return;
|
|
}
|
|
|
|
if (fast_status_refresh_requested || millis() - last_status_poll_ms >= 2000) {
|
|
fast_status_refresh_requested = false;
|
|
last_status_poll_ms = millis();
|
|
fetch_status_fields(CARGO_API_FAST_STATUS_URL, "fast status");
|
|
}
|
|
|
|
if (millis() - last_metadata_poll_ms >= 30000) {
|
|
last_metadata_poll_ms = millis();
|
|
fetch_status_fields(CARGO_API_SLOW_STATUS_URL, "slow status");
|
|
}
|
|
}
|
|
|
|
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);
|
|
lv_obj_align(card, align, x, y);
|
|
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, 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(0xB8C0C8), 0);
|
|
lv_obj_align(title_label, LV_ALIGN_TOP_LEFT, 0, 0);
|
|
|
|
return card;
|
|
}
|
|
|
|
static void create_overland_overview_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, 16);
|
|
|
|
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);
|
|
|
|
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_30, LV_PART_INDICATOR);
|
|
lv_obj_add_flag(battery_charge_pulse_arc, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
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_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, "");
|
|
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_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_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--");
|
|
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);
|
|
|
|
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);
|
|
|
|
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,
|
|
LV_EVENT_CLICKED,
|
|
reinterpret_cast<void *>(static_cast<intptr_t>(i))
|
|
);
|
|
|
|
relay_button_labels[i] = lv_label_create(relay_buttons[i]);
|
|
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);
|
|
}
|
|
|
|
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");
|
|
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, -14);
|
|
}
|
|
|
|
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");
|
|
|
|
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<BusRGB *>(lcd_bus)->configRGB_BounceBufferSize(lcd->getFrameWidth() * 10);
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
assert(board->begin());
|
|
|
|
lvgl_port_init(board->getLCD(), board->getTouch());
|
|
|
|
lvgl_port_lock(-1);
|
|
create_overland_overview_screen();
|
|
lvgl_port_unlock();
|
|
|
|
Serial.println("Dashboard overview UI ready");
|
|
}
|
|
|
|
void loop()
|
|
{
|
|
connect_wifi_if_needed();
|
|
update_system_status_label();
|
|
process_pending_relay_command();
|
|
poll_status_api();
|
|
update_battery_charge_animation();
|
|
|
|
delay(250);
|
|
}
|