#include "Display_ST77916.h" #include "LVGL_Driver.h" #include // BLE and OBD Libraries #include #include "ELMduino.h" // ── Gauge Geometry ──────────────────────────────────────────────── #define CX 180 #define CY 180 #define SCREEN_W 360 #define SCREEN_H 360 #define ANGLE_START 120.0f #define ANGLE_END 380.0f #define GAUGE_MIN -1.0f #define GAUGE_MAX 3.0f #define R_OUTER_TICK 172 #define R_INNER_ORANGE 150 #define R_TICK_MAJOR 150 #define R_TICK_MINOR 160 #define R_LABELS 125 #define R_NEEDLE_TIP 160 #define R_HUB 22 // -- Engine Load Position -- #define LOAD_W 100 #define LOAD_X (CX - (LOAD_W / 2)) #define LOAD_Y (CY - 70) // ── Shared State & Concurrency ──────────────────────────────────── SemaphoreHandle_t xDataMutex; static float boost_target = 0.0f; static float boost_current = -1.0f; static float peak_boost = -1.0f; static uint32_t peak_time = 0; static float clt_temp = 0.0f; static float iat_temp = 0.0f; static float engine_load = 0.0f; static char pbuf[16] = "0.00 bar"; static char clt_buf[24] = "COOLANT: 0 °C"; static char iat_buf[24] = "INTAKE: 0 °C"; static bool startup_anim = true; static int anim_stage = 0; static float anim_speed = 0.35f; static int ble_status = 1; static lv_obj_t *face_obj; static lv_obj_t *needle_obj; // OBD Hardware NimBLEClient* pClient = nullptr; ELM327 myELM327; // ── Helpers ─────────────────────────────────────────────────────── static float bar_to_rad(float bar) { float pct = (bar - GAUGE_MIN) / (GAUGE_MAX - GAUGE_MIN); float deg = ANGLE_START + pct * (ANGLE_END - ANGLE_START); return deg * (float)M_PI / 180.0f; } static lv_coord_t px(float r, float rad) { return (lv_coord_t)(CX + cosf(rad) * r); } static lv_coord_t py(float r, float rad) { return (lv_coord_t)(CY + sinf(rad) * r); } // ── Drawing Functions (Run on Core 1) ───────────────────────────── static void draw_gauge_face(lv_event_t *e) { lv_draw_ctx_t *draw_ctx = lv_event_get_draw_ctx(e); lv_point_t center = {CX, CY}; // 1. Inner Orange Arc lv_draw_arc_dsc_t orange_arc; lv_draw_arc_dsc_init(&orange_arc); orange_arc.color = lv_color_make(0xFF, 0x44, 0x00); orange_arc.width = 2; lv_draw_arc(draw_ctx, &orange_arc, ¢er, R_INNER_ORANGE, (int)ANGLE_START, (int)ANGLE_END); // 2. Ticks for (int i = 0; i <= 40; i++) { float val = GAUGE_MIN + (i * 0.1f); float rad = bar_to_rad(val); bool is_major = (fmod(val + 0.01f, 0.5f) < 0.05f); lv_draw_line_dsc_t tick_dsc; lv_draw_line_dsc_init(&tick_dsc); tick_dsc.color = lv_color_white(); tick_dsc.width = is_major ? 3 : 1; lv_point_t p_out = {px(R_OUTER_TICK, rad), py(R_OUTER_TICK, rad)}; lv_point_t p_in = {px(is_major ? R_TICK_MAJOR : R_TICK_MINOR, rad), py(is_major ? R_TICK_MAJOR : R_TICK_MINOR, rad)}; lv_draw_line(draw_ctx, &tick_dsc, &p_in, &p_out); } // 3. Scale Labels lv_draw_label_dsc_t label_dsc; lv_draw_label_dsc_init(&label_dsc); label_dsc.color = lv_color_white(); label_dsc.font = &lv_font_montserrat_14; float labels[] = {-1.0, -0.5, 0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0}; for (float l : labels) { float rad = bar_to_rad(l); char l_buf[8]; sprintf(l_buf, "%g", (l < 0 ? fabsf(l) : l)); int lx = px(R_LABELS, rad) - 15; int ly = py(R_LABELS, rad) - 10; lv_area_t area = {lx, ly, lx + 40, ly + 20}; lv_draw_label(draw_ctx, &label_dsc, &area, l_buf, NULL); } // 4. ENGINE LOAD SECTION lv_draw_rect_dsc_t load_bg_dsc; lv_draw_rect_dsc_init(&load_bg_dsc); load_bg_dsc.bg_color = lv_color_make(0x1A, 0x1A, 0x1A); load_bg_dsc.radius = 2; lv_area_t load_bg_area = {LOAD_X, LOAD_Y, LOAD_X + LOAD_W, LOAD_Y + 5}; lv_draw_rect(draw_ctx, &load_bg_dsc, &load_bg_area); lv_draw_rect_dsc_t load_fill_dsc; lv_draw_rect_dsc_init(&load_fill_dsc); load_fill_dsc.bg_color = lv_color_make(0x00, 0xFF, 0x00); int fill_w = (int)((engine_load / 100.0f) * LOAD_W); if(fill_w > LOAD_W) fill_w = LOAD_W; lv_area_t load_fill_area = {LOAD_X, LOAD_Y, LOAD_X + fill_w, LOAD_Y + 5}; lv_draw_rect(draw_ctx, &load_fill_dsc, &load_fill_area); lv_draw_label_dsc_t load_lbl_dsc; lv_draw_label_dsc_init(&load_lbl_dsc); load_lbl_dsc.color = lv_color_make(150, 150, 150); load_lbl_dsc.font = &lv_font_montserrat_10; lv_area_t load_txt_area = {LOAD_X, LOAD_Y + 8, LOAD_X + LOAD_W, LOAD_Y + 20}; lv_draw_label(draw_ctx, &load_lbl_dsc, &load_txt_area, "ENGINE LOAD", NULL); // 5. DATA HUB lv_draw_rect_dsc_t box_dsc; lv_draw_rect_dsc_init(&box_dsc); box_dsc.bg_color = lv_color_make(0x1A, 0x1A, 0x1A); box_dsc.bg_opa = 200; box_dsc.radius = 4; lv_area_t data_box = {CX - 35, CY + 55, CX + 115, CY + 145}; lv_draw_rect(draw_ctx, &box_dsc, &data_box); lv_draw_rect_dsc_t accent_dsc; lv_draw_rect_dsc_init(&accent_dsc); accent_dsc.bg_color = lv_color_make(0xFF, 0x44, 0x00); accent_dsc.bg_opa = (ble_status != 2 && !startup_anim) ? (150 + 100 * sin(millis() / 200.0)) : 255; lv_area_t accent_bar = {CX - 33, CY + 60, CX - 30, CY + 140}; lv_draw_rect(draw_ctx, &accent_dsc, &accent_bar); label_dsc.color = lv_color_make(180, 180, 180); label_dsc.font = &lv_font_montserrat_12; lv_area_t pk_label_area = {CX - 20, CY + 62, CX + 100, CY + 75}; lv_draw_label(draw_ctx, &label_dsc, &pk_label_area, "PEAK BOOST", NULL); label_dsc.color = lv_color_make(0xFF, 0xAA, 0x00); label_dsc.font = &lv_font_montserrat_22; lv_area_t peak_val_area = {CX - 20, CY + 76, CX + 110, CY + 102}; lv_draw_label(draw_ctx, &label_dsc, &peak_val_area, pbuf, NULL); lv_draw_line_dsc_t line_dsc; lv_draw_line_dsc_init(&line_dsc); line_dsc.color = lv_color_make(0x33, 0x33, 0x33); line_dsc.width = 1; lv_point_t lp1 = {CX - 20, CY + 105}, lp2 = {CX + 100, CY + 105}; lv_draw_line(draw_ctx, &line_dsc, &lp1, &lp2); label_dsc.font = &lv_font_montserrat_14; label_dsc.color = lv_color_make(220, 220, 220); lv_area_t clt_area = {CX - 20, CY + 110, CX + 110, CY + 125}; lv_draw_label(draw_ctx, &label_dsc, &clt_area, clt_buf, NULL); lv_area_t iat_area = {CX - 20, CY + 127, CX + 110, CY + 142}; lv_draw_label(draw_ctx, &label_dsc, &iat_area, iat_buf, NULL); } static void draw_needle(lv_event_t *e) { lv_draw_ctx_t *draw_ctx = lv_event_get_draw_ctx(e); float rad = bar_to_rad(boost_current); lv_draw_line_dsc_t needle_dsc; lv_draw_line_dsc_init(&needle_dsc); needle_dsc.color = lv_color_make(0xFF, 0x22, 0x00); needle_dsc.width = 5; needle_dsc.round_end = 1; lv_point_t p0 = {px(R_HUB - 5, rad), py(R_HUB - 5, rad)}; lv_point_t p1 = {px(R_NEEDLE_TIP, rad), py(R_NEEDLE_TIP, rad)}; lv_draw_line(draw_ctx, &needle_dsc, &p0, &p1); lv_draw_rect_dsc_t hub_dsc; lv_draw_rect_dsc_init(&hub_dsc); hub_dsc.bg_color = lv_color_make(0x11, 0x11, 0x11); hub_dsc.radius = LV_RADIUS_CIRCLE; hub_dsc.border_width = 2; hub_dsc.border_color = lv_color_make(0x66, 0x66, 0x66); lv_area_t hub_area = {CX - R_HUB, CY - R_HUB, CX + R_HUB, CY + R_HUB}; lv_draw_rect(draw_ctx, &hub_dsc, &hub_area); } // ── OBD Communication (Core 0 Task) ─────────────────────────────── void connect_to_vlink() { NimBLEScan* pScan = NimBLEDevice::getScan(); pScan->setActiveScan(true); pScan->start(1, false); auto results = pScan->getResults(); for (auto it = results.begin(); it != results.end(); ++it) { NimBLEAdvertisedDevice* device = *it; if (device->getName().find("V-LINK") != std::string::npos || device->getName().find("OBDII") != std::string::npos) { pClient = NimBLEDevice::createClient(); if (pClient->connect(device)) { if (myELM327.begin(Serial1, false, 2000)) { ble_status = 2; return; } } } } } void DriverTask(void *parameter) { while (1) { if (ble_status != 2) { connect_to_vlink(); vTaskDelay(pdMS_TO_TICKS(1000)); } else { // Poll hardware float t_clt = myELM327.engineCoolantTemp(); float t_iat = myELM327.intakeAirTemp(); float t_load = myELM327.engineLoad(); float map_kpa = myELM327.manifoldPressure(); // Safely write to shared variables if (xSemaphoreTake(xDataMutex, pdMS_TO_TICKS(20))) { if (myELM327.nb_rx_state == ELM_SUCCESS) { clt_temp = t_clt; iat_temp = t_iat; engine_load = t_load; boost_target = (map_kpa / 100.0f) - 1.01f; if (boost_target > peak_boost) { peak_boost = boost_target; peak_time = millis(); } sprintf(pbuf, "%.2f bar", peak_boost < 0 ? 0.00 : peak_boost); sprintf(clt_buf, "COOLANT: %.0f °C", clt_temp); sprintf(iat_buf, "INTAKE: %.0f °C", iat_temp); } xSemaphoreGive(xDataMutex); } vTaskDelay(pdMS_TO_TICKS(20)); // High polling frequency } } } // ── Initialization ──────────────────────────────────────────────── void gauge_create(void) { lv_obj_t *scr = lv_scr_act(); lv_obj_clean(scr); lv_obj_set_style_bg_color(scr, lv_color_black(), 0); face_obj = lv_obj_create(scr); lv_obj_set_size(face_obj, SCREEN_W, SCREEN_H); lv_obj_set_style_bg_opa(face_obj, 0, 0); lv_obj_set_style_border_width(face_obj, 0, 0); lv_obj_add_event_cb(face_obj, draw_gauge_face, LV_EVENT_DRAW_MAIN, NULL); needle_obj = lv_obj_create(scr); lv_obj_set_size(needle_obj, SCREEN_W, SCREEN_H); lv_obj_set_style_bg_opa(needle_obj, 0, 0); lv_obj_set_style_border_width(needle_obj, 0, 0); lv_obj_add_event_cb(needle_obj, draw_needle, LV_EVENT_DRAW_MAIN, NULL); } void setup() { Serial.begin(115200); Serial1.begin(38400); xDataMutex = xSemaphoreCreateMutex(); TCA9554PWR_Init(0x00); Backlight_Init(); LCD_Init(); Lvgl_Init(); NimBLEDevice::init("ESP32_Gauge"); gauge_create(); // Start Worker on Core 0 xTaskCreatePinnedToCore(DriverTask, "DriverTask", 8192, NULL, 1, NULL, 0); } void loop() { static uint32_t last_face_update = 0; // 1. Logic & Smoothing (Core 1) if (startup_anim) { if (anim_stage == 0) { boost_current += anim_speed; if (boost_current >= GAUGE_MAX) anim_stage = 1; } else if (anim_stage == 1) { boost_current -= (boost_current * 0.07f); if (boost_current <= 0.01f) { boost_current = 0.0f; startup_anim = false; } } } else { // Apply smoothing to target provided by Core 0 if (xSemaphoreTake(xDataMutex, 0)) { boost_current += (boost_target - boost_current) * 0.15f; if (millis() - peak_time > 10000) peak_boost = boost_current; xSemaphoreGive(xDataMutex); } } // 2. UI Refresh lv_obj_invalidate(needle_obj); if (millis() - last_face_update > 100) { lv_obj_invalidate(face_obj); last_face_update = millis(); } Lvgl_Loop(); delay(16); // Target 60fps }