dashboard: render parsed cargo status overview
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <HTTPClient.h>
|
#include <HTTPClient.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
#include <esp_display_panel.hpp>
|
#include <esp_display_panel.hpp>
|
||||||
|
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
@@ -9,7 +10,7 @@
|
|||||||
using namespace esp_panel::drivers;
|
using namespace esp_panel::drivers;
|
||||||
using namespace esp_panel::board;
|
using namespace esp_panel::board;
|
||||||
|
|
||||||
static const char *DASHBOARD_VERSION = "0.0.2-wifi-status";
|
static const char *DASHBOARD_VERSION = "0.0.3-status-overview";
|
||||||
|
|
||||||
// Update these if your Cargo ESP AP credentials are different.
|
// Update these if your Cargo ESP AP credentials are different.
|
||||||
static const char *CARGO_WIFI_SSID = "OverlandController";
|
static const char *CARGO_WIFI_SSID = "OverlandController";
|
||||||
@@ -19,7 +20,9 @@ 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 *wifi_label = nullptr;
|
||||||
static lv_obj_t *api_label = nullptr;
|
static lv_obj_t *api_label = nullptr;
|
||||||
static lv_obj_t *status_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 lv_obj_t *touch_count_label = nullptr;
|
||||||
|
|
||||||
static uint32_t touch_count = 0;
|
static uint32_t touch_count = 0;
|
||||||
@@ -29,7 +32,9 @@ static unsigned long last_status_poll_ms = 0;
|
|||||||
|
|
||||||
static String last_wifi_label_text;
|
static String last_wifi_label_text;
|
||||||
static String last_api_label_text;
|
static String last_api_label_text;
|
||||||
static String last_status_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)
|
static void set_label_text_if_changed(lv_obj_t *label, String &last_text, const String &text)
|
||||||
{
|
{
|
||||||
@@ -48,6 +53,11 @@ static void set_label_text_if_changed(lv_obj_t *label, String &last_text, const
|
|||||||
lvgl_port_unlock();
|
lvgl_port_unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String on_off(bool value)
|
||||||
|
{
|
||||||
|
return value ? "ON" : "OFF";
|
||||||
|
}
|
||||||
|
|
||||||
static void touch_test_event_cb(lv_event_t *event)
|
static void touch_test_event_cb(lv_event_t *event)
|
||||||
{
|
{
|
||||||
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
|
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
|
||||||
@@ -107,11 +117,123 @@ static void update_wifi_label()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
static void poll_status_api()
|
||||||
{
|
{
|
||||||
if (WiFi.status() != WL_CONNECTED) {
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
set_label_text_if_changed(api_label, last_api_label_text, "API: waiting for WiFi");
|
set_label_text_if_changed(api_label, last_api_label_text, "API: waiting for WiFi");
|
||||||
set_label_text_if_changed(status_label, last_status_label_text, "No API response yet.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,42 +246,55 @@ static void poll_status_api()
|
|||||||
Serial.print("GET ");
|
Serial.print("GET ");
|
||||||
Serial.println(CARGO_API_STATUS_URL);
|
Serial.println(CARGO_API_STATUS_URL);
|
||||||
|
|
||||||
set_label_text_if_changed(api_label, last_api_label_text, "API: requesting /api/v1/status");
|
|
||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
http.begin(CARGO_API_STATUS_URL);
|
http.begin(CARGO_API_STATUS_URL);
|
||||||
int code = http.GET();
|
int code = http.GET();
|
||||||
|
|
||||||
if (code > 0) {
|
if (code == 200) {
|
||||||
String body = http.getString();
|
String body = http.getString();
|
||||||
|
|
||||||
Serial.print("HTTP ");
|
Serial.println("HTTP 200");
|
||||||
Serial.println(code);
|
|
||||||
Serial.println(body);
|
Serial.println(body);
|
||||||
|
|
||||||
String api_text = "API: HTTP ";
|
update_status_widgets(body);
|
||||||
api_text += String(code);
|
} else {
|
||||||
api_text += "\nGET /api/v1/status";
|
String error_text = "API: HTTP ";
|
||||||
set_label_text_if_changed(api_label, last_api_label_text, api_text);
|
error_text += String(code);
|
||||||
|
if (code <= 0) {
|
||||||
if (body.length() > 900) {
|
error_text += "\n";
|
||||||
body = body.substring(0, 900);
|
error_text += http.errorToString(code);
|
||||||
body += "\n...truncated...";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set_label_text_if_changed(status_label, last_status_label_text, body);
|
Serial.println(error_text);
|
||||||
} else {
|
set_label_text_if_changed(api_label, last_api_label_text, error_text);
|
||||||
String error = "API request failed: ";
|
|
||||||
error += http.errorToString(code);
|
|
||||||
|
|
||||||
Serial.println(error);
|
|
||||||
set_label_text_if_changed(api_label, last_api_label_text, error);
|
|
||||||
set_label_text_if_changed(status_label, last_status_label_text, "No valid response from Cargo ESP.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
http.end();
|
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()
|
static void create_overland_status_screen()
|
||||||
{
|
{
|
||||||
lv_obj_t *screen = lv_scr_act();
|
lv_obj_t *screen = lv_scr_act();
|
||||||
@@ -171,86 +306,49 @@ static void create_overland_status_screen()
|
|||||||
lv_label_set_text(title, "Overland Controller");
|
lv_label_set_text(title, "Overland Controller");
|
||||||
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
|
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_set_style_text_font(title, &lv_font_montserrat_30, 0);
|
||||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 24);
|
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 18);
|
||||||
|
|
||||||
lv_obj_t *subtitle = lv_label_create(screen);
|
lv_obj_t *subtitle = lv_label_create(screen);
|
||||||
lv_label_set_text(subtitle, "ESP32-S3 Dashboard - WiFi/API Bring-Up");
|
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_style_text_color(subtitle, lv_color_hex(0xB8C0C8), 0);
|
||||||
lv_obj_set_width(subtitle, 920);
|
lv_obj_set_width(subtitle, 920);
|
||||||
lv_obj_set_style_text_align(subtitle, LV_TEXT_ALIGN_CENTER, 0);
|
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, 8);
|
||||||
|
|
||||||
lv_obj_t *left_card = lv_obj_create(screen);
|
lv_obj_t *battery_card = create_card(screen, 300, 210, LV_ALIGN_TOP_LEFT, 32, 105);
|
||||||
lv_obj_set_size(left_card, 430, 360);
|
lv_obj_t *temps_card = create_card(screen, 300, 210, LV_ALIGN_TOP_MID, 0, 105);
|
||||||
lv_obj_align(left_card, LV_ALIGN_LEFT_MID, 38, 20);
|
lv_obj_t *outputs_card = create_card(screen, 300, 210, LV_ALIGN_TOP_RIGHT, -32, 105);
|
||||||
lv_obj_set_style_radius(left_card, 16, 0);
|
lv_obj_t *system_card = create_card(screen, 620, 180, LV_ALIGN_BOTTOM_LEFT, 32, -55);
|
||||||
lv_obj_set_style_bg_color(left_card, lv_color_hex(0x1F2933), 0);
|
lv_obj_t *touch_card = create_card(screen, 300, 180, LV_ALIGN_BOTTOM_RIGHT, -32, -55);
|
||||||
lv_obj_set_style_border_color(left_card, lv_color_hex(0x3A4652), 0);
|
|
||||||
lv_obj_set_style_border_width(left_card, 2, 0);
|
|
||||||
lv_obj_set_style_pad_all(left_card, 18, 0);
|
|
||||||
|
|
||||||
lv_obj_t *right_card = lv_obj_create(screen);
|
battery_label = create_card_label(battery_card, "Battery\nWaiting for API...");
|
||||||
lv_obj_set_size(right_card, 500, 360);
|
temps_label = create_card_label(temps_card, "Temperatures\nWaiting for API...");
|
||||||
lv_obj_align(right_card, LV_ALIGN_RIGHT_MID, -38, 20);
|
outputs_label = create_card_label(outputs_card, "Outputs\nWaiting for API...");
|
||||||
lv_obj_set_style_radius(right_card, 16, 0);
|
|
||||||
lv_obj_set_style_bg_color(right_card, lv_color_hex(0x1F2933), 0);
|
|
||||||
lv_obj_set_style_border_color(right_card, lv_color_hex(0x3A4652), 0);
|
|
||||||
lv_obj_set_style_border_width(right_card, 2, 0);
|
|
||||||
lv_obj_set_style_pad_all(right_card, 18, 0);
|
|
||||||
|
|
||||||
lv_obj_t *hardware_label = lv_label_create(left_card);
|
wifi_label = create_card_label(system_card, "WiFi: not connected");
|
||||||
lv_label_set_text(
|
api_label = lv_label_create(system_card);
|
||||||
hardware_label,
|
|
||||||
"Hardware:\n"
|
|
||||||
"Waveshare ESP32-S3 Touch LCD 5B\n\n"
|
|
||||||
"Display:\n"
|
|
||||||
"1024 x 600\n\n"
|
|
||||||
"Cargo API Base:\n"
|
|
||||||
"/api/v1"
|
|
||||||
);
|
|
||||||
lv_obj_set_style_text_color(hardware_label, lv_color_hex(0xFFFFFF), 0);
|
|
||||||
lv_obj_set_width(hardware_label, 380);
|
|
||||||
lv_obj_align(hardware_label, LV_ALIGN_TOP_LEFT, 0, 0);
|
|
||||||
|
|
||||||
wifi_label = lv_label_create(left_card);
|
|
||||||
lv_label_set_text(wifi_label, "WiFi: not connected");
|
|
||||||
lv_obj_set_style_text_color(wifi_label, lv_color_hex(0xB8C0C8), 0);
|
|
||||||
lv_obj_set_width(wifi_label, 380);
|
|
||||||
lv_obj_align(wifi_label, LV_ALIGN_TOP_LEFT, 0, 175);
|
|
||||||
|
|
||||||
api_label = lv_label_create(left_card);
|
|
||||||
lv_label_set_text(api_label, "API: waiting");
|
lv_label_set_text(api_label, "API: waiting");
|
||||||
lv_obj_set_style_text_color(api_label, lv_color_hex(0xB8C0C8), 0);
|
lv_obj_set_style_text_color(api_label, lv_color_hex(0xB8C0C8), 0);
|
||||||
lv_obj_set_width(api_label, 380);
|
lv_obj_set_width(api_label, 560);
|
||||||
lv_obj_align(api_label, LV_ALIGN_TOP_LEFT, 0, 250);
|
lv_obj_align(api_label, LV_ALIGN_TOP_LEFT, 0, 76);
|
||||||
|
|
||||||
lv_obj_t *button = lv_btn_create(left_card);
|
lv_obj_t *button = lv_btn_create(touch_card);
|
||||||
lv_obj_set_size(button, 170, 54);
|
lv_obj_set_size(button, 170, 54);
|
||||||
lv_obj_align(button, LV_ALIGN_BOTTOM_LEFT, 0, 0);
|
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_add_event_cb(button, touch_test_event_cb, LV_EVENT_CLICKED, nullptr);
|
||||||
|
|
||||||
lv_obj_t *button_label = lv_label_create(button);
|
lv_obj_t *button_label = lv_label_create(button);
|
||||||
lv_label_set_text(button_label, "Touch Test");
|
lv_label_set_text(button_label, "Touch Test");
|
||||||
lv_obj_center(button_label);
|
lv_obj_center(button_label);
|
||||||
|
|
||||||
touch_count_label = lv_label_create(left_card);
|
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 test count: 0");
|
||||||
lv_obj_set_style_text_color(touch_count_label, lv_color_hex(0xB8C0C8), 0);
|
lv_obj_set_style_text_color(touch_count_label, lv_color_hex(0xB8C0C8), 0);
|
||||||
lv_obj_align(touch_count_label, LV_ALIGN_BOTTOM_LEFT, 190, -18);
|
lv_obj_set_width(touch_count_label, 250);
|
||||||
|
lv_obj_align(touch_count_label, LV_ALIGN_TOP_LEFT, 0, 78);
|
||||||
lv_obj_t *status_title = lv_label_create(right_card);
|
|
||||||
lv_label_set_text(status_title, "GET /api/v1/status");
|
|
||||||
lv_obj_set_style_text_color(status_title, lv_color_hex(0xFFFFFF), 0);
|
|
||||||
lv_obj_align(status_title, LV_ALIGN_TOP_LEFT, 0, 0);
|
|
||||||
|
|
||||||
status_label = lv_label_create(right_card);
|
|
||||||
lv_label_set_text(status_label, "No API response yet.");
|
|
||||||
lv_obj_set_style_text_color(status_label, lv_color_hex(0xDDE3EA), 0);
|
|
||||||
lv_obj_set_width(status_label, 455);
|
|
||||||
lv_obj_align(status_label, LV_ALIGN_TOP_LEFT, 0, 34);
|
|
||||||
|
|
||||||
lv_obj_t *footer = lv_label_create(screen);
|
lv_obj_t *footer = lv_label_create(screen);
|
||||||
lv_label_set_text(footer, "Current milestone: Dashboard WiFi client -> Cargo ESP /api/v1/status");
|
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_style_text_color(footer, lv_color_hex(0x7D8996), 0);
|
||||||
lv_obj_set_width(footer, 900);
|
lv_obj_set_width(footer, 900);
|
||||||
lv_obj_set_style_text_align(footer, LV_TEXT_ALIGN_CENTER, 0);
|
lv_obj_set_style_text_align(footer, LV_TEXT_ALIGN_CENTER, 0);
|
||||||
@@ -292,12 +390,12 @@ void setup()
|
|||||||
Serial.println("Initializing LVGL");
|
Serial.println("Initializing LVGL");
|
||||||
lvgl_port_init(board->getLCD(), board->getTouch());
|
lvgl_port_init(board->getLCD(), board->getTouch());
|
||||||
|
|
||||||
Serial.println("Creating Overland WiFi/API screen");
|
Serial.println("Creating Overland status overview");
|
||||||
lvgl_port_lock(-1);
|
lvgl_port_lock(-1);
|
||||||
create_overland_status_screen();
|
create_overland_status_screen();
|
||||||
lvgl_port_unlock();
|
lvgl_port_unlock();
|
||||||
|
|
||||||
Serial.println("Dashboard WiFi/API screen ready");
|
Serial.println("Dashboard status overview ready");
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop()
|
void loop()
|
||||||
|
|||||||
Reference in New Issue
Block a user