#include #include #include #include #include #include #include #include #include "lvgl_v8_port.h" #include "dashboard_config.h" extern const lv_img_dsc_t gas_station; extern const lv_img_dsc_t thermometer_water; #include "dashboard_status_model.h" #include "sensor_node_receiver.h" using namespace esp_panel::drivers; using namespace esp_panel::board; static DashboardStatus status_model; static Preferences dashboard_preferences; static String cargo_wifi_ssid; static String cargo_wifi_password; static String cargo_api_base_url; static String cargo_api_status_url; static String cargo_api_fast_status_url; static String cargo_api_slow_status_url; static String cargo_api_relay_set_url; static bool dashboard_configured = false; static lv_obj_t *setup_ssid_textarea = nullptr; static lv_obj_t *setup_password_textarea = nullptr; static lv_obj_t *setup_api_textarea = nullptr; static lv_obj_t *setup_status_label = nullptr; static lv_obj_t *setup_keyboard = nullptr; static constexpr gpio_num_t OBD_CAN_TX_PIN = GPIO_NUM_15; static constexpr gpio_num_t OBD_CAN_RX_PIN = GPIO_NUM_16; static constexpr uint32_t OBD_REQUEST_ID = 0x7DF; static constexpr uint32_t OBD_RESPONSE_ID_MIN = 0x7E8; static constexpr uint32_t OBD_RESPONSE_ID_MAX = 0x7EF; static constexpr uint8_t OBD_PID_COOLANT_TEMP = 0x05; static constexpr size_t CAN_TRACKED_ID_COUNT = 96; static constexpr unsigned long CAN_LOG_MIN_INTERVAL_MS = 100; struct CanTrackedFrame { bool used = false; bool extended = false; uint32_t identifier = 0; uint8_t dlc = 0; uint8_t data[8] = {}; unsigned long last_log_ms = 0; }; static CanTrackedFrame can_tracked_frames[CAN_TRACKED_ID_COUNT]; static String can_serial_command; static bool obd_can_ready = false; static bool obd_can_started = false; static unsigned long last_obd_poll_ms = 0; static unsigned long last_obd_rx_ms = 0; static unsigned long last_obd_tx_ms = 0; static uint32_t obd_tx_count = 0; static uint32_t obd_tx_fail_count = 0; static uint32_t obd_rx_count = 0; static uint32_t obd_raw_rx_count = 0; static uint32_t obd_last_rx_identifier = 0; static uint8_t obd_last_rx_dlc = 0; static bool obd_last_rx_extended = false; static unsigned long last_obd_raw_rx_ms = 0; static int obd_coolant_temp_f = -999; static int last_rendered_obd_coolant_temp_f = -1000; static String obd_last_error = "not initialized"; static lv_obj_t *battery_card_obj = nullptr; static lv_obj_t *battery_soc_arc = nullptr; static lv_obj_t *battery_charge_pulse_arc = nullptr; static lv_obj_t *battery_current_label = nullptr; static lv_obj_t *battery_estimate_label = nullptr; static lv_obj_t *battery_soc_label = nullptr; static lv_obj_t *temp_tile_objs[2] = {nullptr}; static lv_obj_t *temp_tile_value_labels[2] = {nullptr}; static lv_obj_t *temp_tile_name_labels[2] = {nullptr}; static bool last_temp_tile_alert[2] = {false, false}; static lv_obj_t *system_status_label = nullptr; static lv_obj_t *cargo_status_dot = nullptr; static lv_obj_t *outside_status_temp_label = nullptr; enum DashboardScreen { SCREEN_BATTERY_DETAIL = 0, SCREEN_OVERVIEW = 1, }; static DashboardScreen current_dashboard_screen = SCREEN_OVERVIEW; static lv_obj_t *battery_detail_page = nullptr; static lv_obj_t *battery_detail_soc_label = nullptr; static lv_obj_t *battery_detail_status_label = nullptr; static lv_obj_t *battery_detail_voltage_label = nullptr; static lv_obj_t *battery_detail_current_label = nullptr; static lv_obj_t *battery_detail_power_label = nullptr; static lv_obj_t *battery_detail_remaining_label = nullptr; static lv_obj_t *battery_detail_capacity_label = nullptr; static lv_obj_t *battery_detail_temp_label = nullptr; static lv_obj_t *battery_detail_delta_label = nullptr; static lv_obj_t *battery_detail_estimate_label = nullptr; static String last_battery_detail_soc_text; static String last_battery_detail_status_text; static String last_battery_detail_voltage_text; static String last_battery_detail_current_text; static String last_battery_detail_power_text; static String last_battery_detail_remaining_text; static String last_battery_detail_capacity_text; static String last_battery_detail_temp_text; static String last_battery_detail_delta_text; static String last_battery_detail_estimate_text; static lv_obj_t *vehicle_coolant_meter = nullptr; static lv_meter_indicator_t *vehicle_coolant_needle = nullptr; static lv_obj_t *vehicle_coolant_value_label = nullptr; static lv_obj_t *vehicle_fuel_bar = nullptr; static lv_obj_t *vehicle_fuel_value_label = nullptr; static lv_obj_t *relay_buttons[6] = {nullptr}; static lv_obj_t *relay_button_labels[6] = {nullptr}; static lv_obj_t *diagnostics_overlay = nullptr; static lv_obj_t *diagnostics_label = nullptr; static unsigned long diagnostics_last_update_ms = 0; static unsigned long diagnostics_last_tap_ms = 0; static unsigned long system_card_last_tap_ms = 0; static int system_card_tap_count = 0; static String relay_ids[6]; static String relay_names[6]; static bool relay_states[6] = {false}; static int relay_count = 0; static const int RELAY_STRIP_X = 32; static const int RELAY_STRIP_Y = 376; static const int RELAY_STRIP_W = 960; static const int RELAY_BUTTON_GAP = 10; 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 unsigned long last_relay_command_ms = 0; static bool fast_status_refresh_requested = false; static bool metadata_refresh_requested = true; static bool was_wifi_connected = false; static bool overview_screen_created = false; static bool relay_command_pending[6] = {false}; static bool pending_relay_state[6] = {false}; static bool pending_relay_previous_state[6] = {false}; static String last_battery_current_text; static String last_battery_estimate_text; static String last_battery_soc_text; static bool last_battery_card_alert = false; static String last_temp_tile_value_text[2]; static String last_temp_tile_name_text[2]; static String last_system_status_text; static String last_outside_status_temp_text; static bool last_cargo_api_ok = false; static String on_off(bool value) { return value ? "ON" : "OFF"; } static String normalize_temp_group(String group, const String &name, bool weather) { group.toLowerCase(); group.trim(); if (weather || group == "weather" || group == "outside") { return "outside"; } if (group == "fridge" || group == "freezer" || group == "cooler") { return "fridge"; } if (group == "battery" || group == "bms") { return "battery"; } if (group == "cabin" || group == "inside" || group == "interior") { return "cabin"; } String lower_name = name; lower_name.toLowerCase(); if (lower_name.indexOf("fridge") >= 0 || lower_name.indexOf("freezer") >= 0 || lower_name.indexOf("cooler") >= 0) { return "fridge"; } if (lower_name.indexOf("outside") >= 0 || lower_name.indexOf("ambient") >= 0) { return "outside"; } if (lower_name.indexOf("battery") >= 0 || lower_name.indexOf("bms") >= 0) { return "battery"; } return group.length() ? group : "cabin"; } static String display_temp_group_name(String group) { group.trim(); group.toLowerCase(); if (group == "cabin") return "Cabin"; if (group == "fridge") return "Fridge"; if (group == "battery") return "Battery"; if (group == "other") return "Other"; if (group.length() == 0) { return "Temp"; } group.setCharAt(0, toupper(group.charAt(0))); return group; } static int temp_group_index(const String &group) { if (group == "cabin") return 0; if (group == "fridge") return 1; if (group == "battery") return 2; return 3; } static String temp_group_label(int index, const String &fallback) { if (index == 0) return fallback.length() ? fallback : "Cabin"; if (index == 1) return "Fridge"; if (index == 2) return "Outside"; if (index == 3) return "Other"; return "Temp"; } static String format_hours(float hours) { if (hours <= 0.0 || isnan(hours) || isinf(hours)) { return ""; } int total_minutes = (int)(hours * 60.0 + 0.5); int h = total_minutes / 60; int m = total_minutes % 60; String text; if (h > 0) { text += String(h); text += "h "; } text += String(m); text += "m"; return text; } static void update_outside_status_temp(const String &text) { if (outside_status_temp_label == nullptr) { return; } set_label_text_if_changed(outside_status_temp_label, last_outside_status_temp_text, text); } static void update_status_icons() { if (cargo_status_dot == nullptr) { return; } bool cargo_ok = status_model.api_ok; if (cargo_ok == last_cargo_api_ok) { return; } last_cargo_api_ok = cargo_ok; lvgl_port_lock(-1); lv_obj_set_style_bg_color( cargo_status_dot, cargo_ok ? lv_color_hex(0x32D583) : lv_color_hex(0xF97066), 0 ); lvgl_port_unlock(); } 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); 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; int end_angle = (soc * 270) / 100; lvgl_port_lock(-1); lv_arc_set_value(battery_soc_arc, soc); lv_arc_set_angles(battery_soc_arc, 0, end_angle); lv_color_t soc_color = lv_color_hex(0x32D583); if (soc < 30) { soc_color = lv_color_hex(0xF97066); } else if (soc < 70) { soc_color = lv_color_hex(0xFDB022); } lv_obj_set_style_arc_color( battery_soc_arc, soc_color, LV_PART_INDICATOR ); lv_arc_set_value(battery_charge_pulse_arc, soc); lv_arc_set_angles(battery_charge_pulse_arc, 0, end_angle); lv_obj_add_flag(battery_charge_pulse_arc, LV_OBJ_FLAG_HIDDEN); lvgl_port_unlock(); Serial.print("Battery SOC arc: "); Serial.print(soc); Serial.print("% -> "); Serial.print(end_angle); Serial.println(" degrees"); } static void update_battery_charge_animation() { // Intentionally disabled for now. // Repeated style changes on the RGB panel caused artifacts/freezes during WiFi/API updates. } 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 = false; } static String format_diag_uptime(unsigned long seconds) { unsigned long days = seconds / 86400; seconds %= 86400; unsigned long hours = seconds / 3600; seconds %= 3600; unsigned long minutes = seconds / 60; String out; if (days > 0) { out += String(days); out += "d "; } out += String(hours); out += "h "; out += String(minutes); out += "m"; return out; } static void update_vehicle_coolant_widget() { if (vehicle_coolant_value_label == nullptr || vehicle_coolant_meter == nullptr || vehicle_coolant_needle == nullptr) { return; } if (obd_coolant_temp_f == last_rendered_obd_coolant_temp_f) { return; } last_rendered_obd_coolant_temp_f = obd_coolant_temp_f; lvgl_port_lock(-1); if (obd_coolant_temp_f > -100) { int bounded_temp = clamp_int(obd_coolant_temp_f, 100, 260); lv_meter_set_indicator_value(vehicle_coolant_meter, vehicle_coolant_needle, bounded_temp); lv_label_set_text(vehicle_coolant_value_label, (String(obd_coolant_temp_f) + "°").c_str()); } else { lv_meter_set_indicator_value(vehicle_coolant_meter, vehicle_coolant_needle, 100); lv_label_set_text(vehicle_coolant_value_label, "--°"); } lvgl_port_unlock(); } static bool init_obd_can() { if (obd_can_ready && obd_can_started) { return true; } twai_general_config_t general_config = TWAI_GENERAL_CONFIG_DEFAULT(OBD_CAN_TX_PIN, OBD_CAN_RX_PIN, TWAI_MODE_LISTEN_ONLY); general_config.tx_queue_len = 0; general_config.rx_queue_len = 64; twai_timing_config_t timing_config = TWAI_TIMING_CONFIG_500KBITS(); twai_filter_config_t filter_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); if (!obd_can_ready) { esp_err_t install_result = twai_driver_install(&general_config, &timing_config, &filter_config); if (install_result != ESP_OK && install_result != ESP_ERR_INVALID_STATE) { obd_last_error = "driver install failed: " + String((int)install_result); Serial.println("OBD CAN: " + obd_last_error); return false; } obd_can_ready = true; } esp_err_t start_result = twai_start(); if (start_result != ESP_OK && start_result != ESP_ERR_INVALID_STATE) { obd_last_error = "start failed: " + String((int)start_result); Serial.println("OBD CAN: " + obd_last_error); return false; } obd_can_started = true; obd_last_error = "started"; Serial.println("OBD CAN: passive logger started on RX GPIO16 at 500 kbps"); Serial.println("CAN CSV: CAN,ms,id,format,dlc,b0,b1,b2,b3,b4,b5,b6,b7"); Serial.println("Enter TEMP, when recording a scan-tool coolant reading."); return true; } static void handle_obd_can_message(const twai_message_t &message) { if (message.extd || message.rtr) { return; } if (message.identifier < OBD_RESPONSE_ID_MIN || message.identifier > OBD_RESPONSE_ID_MAX) { return; } if (message.data_length_code < 4) { return; } // Single-frame OBD-II response: // byte0 length, byte1 0x41 response mode, byte2 PID, byte3 value A. if (message.data[1] == 0x41 && message.data[2] == OBD_PID_COOLANT_TEMP) { int coolant_c = (int)message.data[3] - 40; obd_coolant_temp_f = (int)roundf((coolant_c * 9.0f / 5.0f) + 32.0f); obd_rx_count++; last_obd_rx_ms = millis(); obd_last_error = "coolant response OK"; Serial.print("OBD CAN: coolant="); Serial.print(obd_coolant_temp_f); Serial.println(" F"); update_vehicle_coolant_widget(); } } static CanTrackedFrame *find_can_tracked_frame(const twai_message_t &message) { CanTrackedFrame *free_slot = nullptr; for (size_t i = 0; i < CAN_TRACKED_ID_COUNT; i++) { CanTrackedFrame &tracked = can_tracked_frames[i]; if (tracked.used && tracked.identifier == message.identifier && tracked.extended == message.extd) { return &tracked; } if (!tracked.used && free_slot == nullptr) free_slot = &tracked; } return free_slot; } static void log_changed_can_frame(const twai_message_t &message) { if (message.rtr) return; CanTrackedFrame *tracked = find_can_tracked_frame(message); if (tracked == nullptr) return; bool changed = !tracked->used || tracked->dlc != message.data_length_code; for (uint8_t i = 0; i < message.data_length_code && i < 8; i++) { changed = changed || tracked->data[i] != message.data[i]; } unsigned long now = millis(); bool should_log = changed && (!tracked->used || now - tracked->last_log_ms >= CAN_LOG_MIN_INTERVAL_MS); tracked->used = true; tracked->extended = message.extd; tracked->identifier = message.identifier; tracked->dlc = message.data_length_code; memcpy(tracked->data, message.data, sizeof(tracked->data)); if (!should_log) return; tracked->last_log_ms = now; Serial.print("CAN,"); Serial.print(now); Serial.print(","); Serial.print(message.identifier, HEX); Serial.print(message.extd ? ",EXT," : ",STD,"); Serial.print(message.data_length_code); for (uint8_t i = 0; i < 8; i++) { Serial.print(","); if (i < message.data_length_code) { if (message.data[i] < 0x10) Serial.print("0"); Serial.print(message.data[i], HEX); } } Serial.println(); } static void process_can_serial_commands() { while (Serial.available() > 0) { char value = (char)Serial.read(); if (value == '\r') continue; if (value != '\n') { if (can_serial_command.length() < 48) can_serial_command += value; continue; } can_serial_command.trim(); if (can_serial_command.startsWith("TEMP,")) { String temperature = can_serial_command.substring(5); temperature.trim(); Serial.print("TEMP,"); Serial.print(millis()); Serial.print(","); Serial.println(temperature); } else if (can_serial_command.startsWith("MARK,")) { String label = can_serial_command.substring(5); label.trim(); Serial.print("MARK,"); Serial.print(millis()); Serial.print(","); Serial.println(label); } else if (can_serial_command.length() > 0) { Serial.println("ERROR,expected TEMP, or MARK,