diff --git a/sketch/obd2_gauge/DSEG7_Modern_BoldItalic_44.h b/sketch/obd2_gauge/DSEG7_Modern_BoldItalic_44.h index c7b298c..4122a5b 100644 --- a/sketch/obd2_gauge/DSEG7_Modern_BoldItalic_44.h +++ b/sketch/obd2_gauge/DSEG7_Modern_BoldItalic_44.h @@ -1,3 +1,5 @@ +#pragma once +#include const uint8_t DSEG7_Modern_BoldItalic_44[] PROGMEM = { 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x2C, diff --git a/sketch/obd2_gauge/cluster_screen.h b/sketch/obd2_gauge/cluster_screen.h new file mode 100644 index 0000000..0051165 --- /dev/null +++ b/sketch/obd2_gauge/cluster_screen.h @@ -0,0 +1,490 @@ +#pragma once + +#include +#include +#include "gps_module.h" +#include "DSEG7_Modern_BoldItalic_34.h" +#include "DSEG7_Modern_BoldItalic_44.h" +#include "rainbow_text.h" + +extern TFT_eSPI tft; +extern uint16_t getThemePrimaryColor(); +extern uint16_t getThemeAccentColor(); +extern uint16_t getThemeBgColor(); +extern bool isRainbowTheme(); +extern bool isYapaTheme(); +extern int8_t currentScreenPage; +extern int8_t fullScreenCell; +extern float currentRpm; +extern float last_valid_data[]; +extern float tempEngineCoolant; +extern void drawPidIcon(int x, int y, const String &pidStr); +extern void drawPidIconToSprite(TFT_eSprite &s, int ix, int iy, const String &pidStr, int y_slice, int slice_h, int global_y); +extern void draw7SegDegreeC(TFT_eSPI &dst, int bx, int by, uint16_t color, uint16_t bgCol, bool rainbow); + +// Data extractors +inline float getClusterRpmVal() { + if (currentRpm > 0.0f) return currentRpm; + if (last_valid_data[2] > 0.0f) return last_valid_data[2]; + return 0.0f; +} + +inline float getClusterSpeedVal() { + if (isGpsSpeedValid()) return gpsSpeedKmph; + if (last_valid_data[1] >= 0.0f) return last_valid_data[1]; + return 0.0f; +} + +inline bool isClusterSpeedFromGps() { + return isGpsSpeedValid(); +} + +inline float getClusterCoolantVal() { + if (last_valid_data[0] > -900.0f) return last_valid_data[0]; + if (tempEngineCoolant > -900.0f) return tempEngineCoolant; + return -999.0f; +} + +inline float getClusterVoltVal() { + if (last_valid_data[3] > 0.0f) return last_valid_data[3]; + return -999.0f; +} + +// Caching variables for zero-flicker live redraws +static int oldClusterRpm = -999; +static int oldClusterSpd = -999; +static int oldClusterCoolant = -999; +static float oldClusterVolt = -999.0f; +static bool oldClusterGpsLock = false; +static uint32_t lastClusterUpdate = 0; + +inline void syncClusterTrackers() { + oldClusterRpm = (int)round(getClusterRpmVal()); + oldClusterSpd = (int)round(getClusterSpeedVal()); + float c = getClusterCoolantVal(); + oldClusterCoolant = (c > -900.0f) ? (int)round(c) : -999; + oldClusterVolt = getClusterVoltVal(); + oldClusterGpsLock = isClusterSpeedFromGps(); + lastClusterUpdate = millis(); +} + +// Helper: draw 5 top pagination dots (page 4 active) +inline void drawClusterDots(int x_offset = 0) { + uint16_t actCol = isRainbowTheme() ? getRainbowScreenColor(188, 2) : (isYapaTheme() ? getYapaColor(4) : getThemePrimaryColor()); + tft.fillCircle(132 + x_offset, 2, 2, 0x5AEB); + tft.fillCircle(146 + x_offset, 2, 2, 0x5AEB); + tft.fillCircle(160 + x_offset, 2, 2, 0x5AEB); + tft.fillCircle(174 + x_offset, 2, 2, 0x5AEB); + tft.fillCircle(188 + x_offset, 2, 2, actCol); +} + +// Draw static framing of all 4 cards +inline void drawClusterBaseCards(int x_offset = 0) { + uint16_t cardBg = 0x10A2; + uint16_t border = 0x2945; + uint16_t priCol = getThemePrimaryColor(); + uint16_t accCol = getThemeAccentColor(); + + uint16_t cRpm = isRainbowTheme() ? getRainbowScreenColor(20, 20) : (isYapaTheme() ? getYapaColor(0) : priCol); + uint16_t cSpd = isRainbowTheme() ? getRainbowScreenColor(20, 70) : (isYapaTheme() ? getYapaColor(1) : accCol); + uint16_t cCool = isRainbowTheme() ? getRainbowScreenColor(20, 150) : (isYapaTheme() ? getYapaColor(2) : priCol); + uint16_t cVolt = isRainbowTheme() ? getRainbowScreenColor(180, 150) : (isYapaTheme() ? getYapaColor(3) : accCol); + + // 1. TACHOMETER CARD (x: 8, y: 10, w: 304, h: 42) + tft.fillRoundRect(8 + x_offset, 10, 304, 42, 5, cardBg); + tft.drawRoundRect(8 + x_offset, 10, 304, 42, 5, border); + tft.fillRoundRect(12 + x_offset, 14, 3, 34, 1, cRpm); + drawPidIcon(18 + x_offset, 14, "010C"); + tft.setTextColor(0x8410, cardBg); + tft.drawString("RPM", 38 + x_offset, 16, 2); + // Tachometer background track (x: 76 to 216, w: 140, h: 12) + tft.fillRoundRect(76 + x_offset, 16, 140, 12, 3, 0x0841); + // RPM ticks + tft.drawString("1 2 3 4 5 6 7", 86 + x_offset, 31, 1); + + // 2. SPEEDOMETER CARD (x: 8, y: 56, w: 304, h: 76) + tft.fillRoundRect(8 + x_offset, 56, 304, 76, 5, cardBg); + tft.drawRoundRect(8 + x_offset, 56, 304, 76, 5, border); + tft.fillRoundRect(12 + x_offset, 60, 3, 68, 1, cSpd); + drawPidIcon(18 + x_offset, 60, "010D"); + tft.setTextColor(0x8410, cardBg); + tft.drawString("VELOCIDAD", 38 + x_offset, 62, 2); + // Background speed glow rail + tft.fillRoundRect(18 + x_offset, 124, 284, 3, 1, 0x0841); + + // 3. COOLANT TEMP CARD (x: 8, y: 136, w: 148, h: 98) + tft.fillRoundRect(8 + x_offset, 136, 148, 98, 5, cardBg); + tft.drawRoundRect(8 + x_offset, 136, 148, 98, 5, border); + tft.fillRoundRect(12 + x_offset, 140, 3, 90, 1, cCool); + drawPidIcon(18 + x_offset, 140, "0105"); + tft.setTextColor(0x8410, cardBg); + tft.drawString("REFRIG.", 38 + x_offset, 142, 2); + // Thermal bar rail (x: 18, y: 196, w: 128, h: 8) + tft.fillRoundRect(18 + x_offset, 196, 128, 8, 3, 0x0841); + tft.drawString("40`C", 18 + x_offset, 210, 1); + tft.drawString("120`C", 112 + x_offset, 210, 1); + + // 4. BATTERY VOLTAGE CARD (x: 164, y: 136, w: 148, h: 98) + tft.fillRoundRect(164 + x_offset, 136, 148, 98, 5, cardBg); + tft.drawRoundRect(164 + x_offset, 136, 148, 98, 5, border); + tft.fillRoundRect(168 + x_offset, 140, 3, 90, 1, cVolt); + drawPidIcon(174 + x_offset, 140, "ATRV"); + tft.setTextColor(0x8410, cardBg); + tft.drawString("BATERIA", 194 + x_offset, 142, 2); + // Voltage bar rail (x: 174, y: 196, w: 128, h: 8) + tft.fillRoundRect(174 + x_offset, 196, 128, 8, 3, 0x0841); + tft.drawString("10V", 174 + x_offset, 210, 1); + tft.drawString("16V", 272 + x_offset, 210, 1); +} + +// Initial direct draw of Page 4 +inline void drawClusterScreen(int x_offset = 0) { + tft.fillScreen(TFT_BLACK); + drawClusterDots(x_offset); + drawClusterBaseCards(x_offset); + + // Force full initial value render + oldClusterRpm = -999; + oldClusterSpd = -999; + oldClusterCoolant = -999; + oldClusterVolt = -999.0f; + oldClusterGpsLock = false; + + void updateClusterScreen(bool forceRedraw = false); + updateClusterScreen(true); +} + +// Live zero-flicker non-blocking update of Page 4 +inline void updateClusterScreen(bool forceRedraw) { + if (currentScreenPage != 4 || fullScreenCell != -1) return; + + uint16_t cardBg = 0x10A2; + uint16_t priCol = getThemePrimaryColor(); + uint16_t accCol = getThemeAccentColor(); + + uint16_t cRpm = isRainbowTheme() ? getRainbowScreenColor(20, 20) : (isYapaTheme() ? getYapaColor(0) : priCol); + uint16_t cSpd = isRainbowTheme() ? getRainbowScreenColor(20, 70) : (isYapaTheme() ? getYapaColor(1) : accCol); + uint16_t cCool = isRainbowTheme() ? getRainbowScreenColor(20, 150) : (isYapaTheme() ? getYapaColor(2) : priCol); + uint16_t cVolt = isRainbowTheme() ? getRainbowScreenColor(180, 150) : (isYapaTheme() ? getYapaColor(3) : accCol); + + // ------------------------------------------------------------- + // 1. TACHOMETER (RPM) + // ------------------------------------------------------------- + float curRpmF = getClusterRpmVal(); + int curRpm = (int)round(curRpmF); + if (forceRedraw || abs(curRpm - oldClusterRpm) >= 20 || (curRpm == 0 && oldClusterRpm != 0)) { + oldClusterRpm = curRpm; + + // Erase numeric text area + tft.fillRect(222, 14, 84, 26, cardBg); + tft.setTextPadding(0); + if (curRpm > 0) { + uint16_t rpmCol = (curRpm >= 6000) ? TFT_RED : ((curRpm >= 4500) ? 0xFD60 : cRpm); + tft.setTextColor(rpmCol, cardBg); + String rpmStr = String(curRpm); + tft.drawRightString(rpmStr, 302, 16, 4); + } else { + tft.setTextColor(0x632C, cardBg); + tft.drawRightString("---", 302, 16, 4); + } + + // Segmented Tachometer Bar (12 segments between x: 78 and 214) + // 0 to 7000 RPM -> ~583 RPM per segment + int activeSegs = map(constrain(curRpm, 0, 7000), 0, 7000, 0, 12); + for (int s = 0; s < 12; s++) { + int sx = 78 + s * 11; + uint16_t segCol; + if (s < activeSegs) { + if (s >= 10) segCol = TFT_RED; + else if (s >= 8) segCol = 0xFD60; + else segCol = isRainbowTheme() ? getRainbowScreenColor(sx, 20) : cRpm; + } else { + segCol = 0x0841; // inactive dark track + } + tft.fillRect(sx, 18, 9, 8, segCol); + } + } + + // ------------------------------------------------------------- + // 2. SPEEDOMETER (VELOCIDAD) + // ------------------------------------------------------------- + float curSpdF = getClusterSpeedVal(); + int curSpd = (int)round(curSpdF); + bool isGps = isClusterSpeedFromGps(); + + if (forceRedraw || curSpd != oldClusterSpd || isGps != oldClusterGpsLock) { + oldClusterSpd = curSpd; + oldClusterGpsLock = isGps; + + // Source Pill (GPS / OBD) + tft.fillRect(116, 61, 46, 14, cardBg); + if (isGps) { + tft.fillRoundRect(116, 61, 42, 14, 3, 0x0254); + tft.setTextColor(0x07FF, 0x0254); + tft.drawString("GPS", 125, 63, 1); + } else { + tft.fillRoundRect(116, 61, 42, 14, 3, 0x18C3); + tft.setTextColor(0x8410, 0x18C3); + tft.drawString("OBD", 125, 63, 1); + } + + // Giant Speed Display + tft.fillRect(166, 60, 138, 56, cardBg); + String spdStr = String(curSpd); + + tft.loadFont(DSEG7_Modern_BoldItalic_44); + uint16_t spdCol = isRainbowTheme() ? getRainbowScreenColor(210, 80) : cSpd; + tft.setTextColor(spdCol, cardBg); + tft.drawRightString(spdStr, 248, 64, 1); + tft.unloadFont(); + + // Unit "km/h" + tft.setTextColor(0xBDD7, cardBg); + tft.drawString("km/h", 254, 88, 2); + + // Dynamic Bottom Speed Rail (w = 284) + int fillSpdW = map(constrain(curSpd, 0, 200), 0, 200, 0, 284); + tft.fillRect(18, 124, 284, 3, 0x0841); + if (fillSpdW > 0) { + if (isRainbowTheme()) { + for (int bx = 0; bx < fillSpdW; bx++) { + tft.drawFastVLine(18 + bx, 124, 3, getRainbowScreenColor(18 + bx, 124)); + } + } else { + tft.fillRect(18, 124, fillSpdW, 3, cSpd); + } + } + } + + // ------------------------------------------------------------- + // 3. COOLANT TEMP (REFRIGERANTE) + // ------------------------------------------------------------- + float curCoolF = getClusterCoolantVal(); + int curCool = (curCoolF > -900.0f) ? (int)round(curCoolF) : -999; + if (forceRedraw || curCool != oldClusterCoolant) { + oldClusterCoolant = curCool; + + tft.fillRect(20, 158, 130, 36, cardBg); + if (curCool > -900) { + uint16_t coolCol = (curCool >= 105) ? TFT_RED : ((curCool < 70) ? 0x05BF : cCool); + tft.setTextColor(coolCol, cardBg); + String valStr = String(curCool); + tft.loadFont(DSEG7_Modern_BoldItalic_34); + tft.drawRightString(valStr, 102, 160, 1); + tft.unloadFont(); + + draw7SegDegreeC(tft, 106, 164, coolCol, cardBg, isRainbowTheme()); + + // Progress bar (40 to 120 °C) + int barW = map(constrain(curCool, 40, 120), 40, 120, 0, 128); + tft.fillRect(18, 196, 128, 8, 0x0841); + if (barW > 0) { + tft.fillRoundRect(18, 196, barW, 8, 2, coolCol); + } + } else { + tft.setTextColor(0x632C, cardBg); + tft.drawString("---", 60, 164, 4); + tft.fillRect(18, 196, 128, 8, 0x0841); + } + } + + // ------------------------------------------------------------- + // 4. BATTERY VOLTAGE (BATERÍA) + // ------------------------------------------------------------- + float curVolt = getClusterVoltVal(); + if (forceRedraw || abs(curVolt - oldClusterVolt) >= 0.08f) { + oldClusterVolt = curVolt; + + tft.fillRect(174, 158, 130, 36, cardBg); + if (curVolt > 0.0f) { + uint16_t voltCol = (curVolt < 11.8f || curVolt > 15.0f) ? TFT_RED : ((curVolt >= 13.5f) ? 0x07E0 : cVolt); + String voltStr = String(curVolt, 1); + + tft.loadFont(DSEG7_Modern_BoldItalic_34); + tft.setTextColor(voltCol, cardBg); + tft.drawRightString(voltStr, 268, 160, 1); + tft.unloadFont(); + + tft.setTextColor(0xBDD7, cardBg); + tft.drawString("V", 274, 172, 2); + + // Voltage bar (10.0 to 16.0 V) + int vBarW = map(constrain((int)(curVolt * 10), 100, 160), 100, 160, 0, 128); + tft.fillRect(174, 196, 128, 8, 0x0841); + if (vBarW > 0) { + tft.fillRoundRect(174, 196, vBarW, 8, 2, voltCol); + } + } else { + tft.setTextColor(0x632C, cardBg); + tft.drawString("---", 216, 164, 4); + tft.fillRect(174, 196, 128, 8, 0x0841); + } + } +} + +// Render Page 4 slice-by-slice into sprite (for double-buffered transitions) +inline void renderPage4ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int slice_h) { + uint16_t cardBg = 0x10A2; + uint16_t border = 0x2945; + uint16_t priCol = getThemePrimaryColor(); + uint16_t accCol = getThemeAccentColor(); + + uint16_t cRpm = isRainbowTheme() ? getRainbowScreenColor(20, 20) : (isYapaTheme() ? getYapaColor(0) : priCol); + uint16_t cSpd = isRainbowTheme() ? getRainbowScreenColor(20, 70) : (isYapaTheme() ? getYapaColor(1) : accCol); + uint16_t cCool = isRainbowTheme() ? getRainbowScreenColor(20, 150) : (isYapaTheme() ? getYapaColor(2) : priCol); + uint16_t cVolt = isRainbowTheme() ? getRainbowScreenColor(180, 150) : (isYapaTheme() ? getYapaColor(3) : accCol); + + // Pagination dots on top slice (page 4 active) + if (y_slice == 0) { + uint16_t actCol = isRainbowTheme() ? getRainbowScreenColor(188, 2) : (isYapaTheme() ? getYapaColor(4) : priCol); + s.fillCircle(132, 2, 2, 0x5AEB); + s.fillCircle(146, 2, 2, 0x5AEB); + s.fillCircle(160, 2, 2, 0x5AEB); + s.fillCircle(174, 2, 2, 0x5AEB); + s.fillCircle(188, 2, 2, actCol); + } + + // 1. TACHOMETER CARD (y: 10 to 52) + int c0Y = 10 - y_slice; + if (c0Y < slice_h && c0Y + 42 > 0) { + s.fillRoundRect(8, c0Y, 304, 42, 5, cardBg); + s.drawRoundRect(8, c0Y, 304, 42, 5, border); + s.fillRoundRect(12, c0Y + 4, 3, 34, 1, cRpm); + drawPidIconToSprite(s, 18, c0Y + 4, "010C", y_slice, slice_h, 10); + s.setTextColor(0x8410, cardBg); + s.drawString("RPM", 38, c0Y + 6, 2); + + // Tachometer background rail + s.fillRoundRect(76, c0Y + 6, 140, 12, 3, 0x0841); + s.drawString("1 2 3 4 5 6 7", 86, c0Y + 21, 1); + + float curRpmF = getClusterRpmVal(); + int curRpm = (int)round(curRpmF); + if (curRpm > 0) { + uint16_t rpmCol = (curRpm >= 6000) ? TFT_RED : ((curRpm >= 4500) ? 0xFD60 : cRpm); + s.setTextColor(rpmCol, cardBg); + s.drawRightString(String(curRpm), 302, c0Y + 6, 4); + + int activeSegs = map(constrain(curRpm, 0, 7000), 0, 7000, 0, 12); + for (int seg = 0; seg < 12; seg++) { + int sx = 78 + seg * 11; + uint16_t segCol = 0x0841; + if (seg < activeSegs) { + if (seg >= 10) segCol = TFT_RED; + else if (seg >= 8) segCol = 0xFD60; + else segCol = isRainbowTheme() ? getRainbowScreenColor(sx, 20) : cRpm; + } + s.fillRect(sx, c0Y + 8, 9, 8, segCol); + } + } else { + s.setTextColor(0x632C, cardBg); + s.drawRightString("---", 302, c0Y + 6, 4); + } + } + + // 2. SPEEDOMETER CARD (y: 56 to 132) + int c1Y = 56 - y_slice; + if (c1Y < slice_h && c1Y + 76 > 0) { + s.fillRoundRect(8, c1Y, 304, 76, 5, cardBg); + s.drawRoundRect(8, c1Y, 304, 76, 5, border); + s.fillRoundRect(12, c1Y + 4, 3, 68, 1, cSpd); + drawPidIconToSprite(s, 18, c1Y + 4, "010D", y_slice, slice_h, 56); + s.setTextColor(0x8410, cardBg); + s.drawString("VELOCIDAD", 38, c1Y + 6, 2); + + bool isGps = isClusterSpeedFromGps(); + if (isGps) { + s.fillRoundRect(116, c1Y + 5, 42, 14, 3, 0x0254); + s.setTextColor(0x07FF, 0x0254); + s.drawString("GPS", 125, c1Y + 7, 1); + } else { + s.fillRoundRect(116, c1Y + 5, 42, 14, 3, 0x18C3); + s.setTextColor(0x8410, 0x18C3); + s.drawString("OBD", 125, c1Y + 7, 1); + } + + int curSpd = (int)round(getClusterSpeedVal()); + String spdStr = String(curSpd); + s.loadFont(DSEG7_Modern_BoldItalic_44); + uint16_t spdCol = isRainbowTheme() ? getRainbowScreenColor(210, 80) : cSpd; + s.setTextColor(spdCol, cardBg); + s.drawRightString(spdStr, 248, c1Y + 8, 1); + s.unloadFont(); + + s.setTextColor(0xBDD7, cardBg); + s.drawString("km/h", 254, c1Y + 32, 2); + + int fillSpdW = map(constrain(curSpd, 0, 200), 0, 200, 0, 284); + s.fillRect(18, c1Y + 68, 284, 3, 0x0841); + if (fillSpdW > 0) { + s.fillRect(18, c1Y + 68, fillSpdW, 3, cSpd); + } + } + + // 3. COOLANT TEMP CARD (y: 136 to 234) + int c2Y = 136 - y_slice; + if (c2Y < slice_h && c2Y + 98 > 0) { + s.fillRoundRect(8, c2Y, 148, 98, 5, cardBg); + s.drawRoundRect(8, c2Y, 148, 98, 5, border); + s.fillRoundRect(12, c2Y + 4, 3, 90, 1, cCool); + drawPidIconToSprite(s, 18, c2Y + 4, "0105", y_slice, slice_h, 136); + s.setTextColor(0x8410, cardBg); + s.drawString("REFRIG.", 38, c2Y + 6, 2); + + s.fillRect(18, c2Y + 60, 128, 8, 0x0841); + s.drawString("40`C", 18, c2Y + 74, 1); + s.drawString("120`C", 112, c2Y + 74, 1); + + float curCoolF = getClusterCoolantVal(); + int curCool = (curCoolF > -900.0f) ? (int)round(curCoolF) : -999; + if (curCool > -900) { + uint16_t coolCol = (curCool >= 105) ? TFT_RED : ((curCool < 70) ? 0x05BF : cCool); + s.loadFont(DSEG7_Modern_BoldItalic_34); + s.setTextColor(coolCol, cardBg); + s.drawRightString(String(curCool), 102, c2Y + 24, 1); + s.unloadFont(); + + int barW = map(constrain(curCool, 40, 120), 40, 120, 0, 128); + if (barW > 0) { + s.fillRoundRect(18, c2Y + 60, barW, 8, 2, coolCol); + } + } else { + s.setTextColor(0x632C, cardBg); + s.drawString("---", 60, c2Y + 28, 4); + } + } + + // 4. BATTERY VOLTAGE CARD (y: 136 to 234) + int c3Y = 136 - y_slice; + if (c3Y < slice_h && c3Y + 98 > 0) { + s.fillRoundRect(164, c3Y, 148, 98, 5, cardBg); + s.drawRoundRect(164, c3Y, 148, 98, 5, border); + s.fillRoundRect(168, c3Y + 4, 3, 90, 1, cVolt); + drawPidIconToSprite(s, 174, c3Y + 4, "ATRV", y_slice, slice_h, 136); + s.setTextColor(0x8410, cardBg); + s.drawString("BATERIA", 194, c3Y + 6, 2); + + s.fillRect(174, c3Y + 60, 128, 8, 0x0841); + s.drawString("10V", 174, c3Y + 74, 1); + s.drawString("16V", 272, c3Y + 74, 1); + + float curVolt = getClusterVoltVal(); + if (curVolt > 0.0f) { + uint16_t voltCol = (curVolt < 11.8f || curVolt > 15.0f) ? TFT_RED : ((curVolt >= 13.5f) ? 0x07E0 : cVolt); + s.loadFont(DSEG7_Modern_BoldItalic_34); + s.setTextColor(voltCol, cardBg); + s.drawRightString(String(curVolt, 1), 268, c3Y + 24, 1); + s.unloadFont(); + s.setTextColor(0xBDD7, cardBg); + s.drawString("V", 274, c3Y + 36, 2); + + int vBarW = map(constrain((int)(curVolt * 10), 100, 160), 100, 160, 0, 128); + if (vBarW > 0) { + s.fillRoundRect(174, c3Y + 60, vBarW, 8, 2, voltCol); + } + } else { + s.setTextColor(0x632C, cardBg); + s.drawString("---", 216, c3Y + 28, 4); + } + } +} diff --git a/sketch/obd2_gauge/config.h b/sketch/obd2_gauge/config.h index 942377d..177a988 100644 --- a/sketch/obd2_gauge/config.h +++ b/sketch/obd2_gauge/config.h @@ -2479,14 +2479,16 @@ void transitionMenu() { // 2. Touch on [ PROBAR EFECTO ] (y: 142 to 176) else if (ty >= 142 && ty <= 176 && tx >= 40 && tx <= 280) { touchBeep(); - // Demonstrate transition across all 4 pages (Page 0 -> Page 1 -> Page 2 -> Page 3 -> Page 0) + // Demonstrate transition across all 5 pages (Page 0 -> Page 1 -> Page 2 -> Page 3 -> Page 4 -> Page 0) performScreenTransition(true, 0, 1); delay(350); performScreenTransition(true, 1, 2); delay(350); performScreenTransition(true, 2, 3); delay(350); - performScreenTransition(false, 3, 0); + performScreenTransition(true, 3, 4); + delay(350); + performScreenTransition(false, 4, 0); delay(250); tft.fillScreen(TFT_BLACK); drawTransitionUI(); diff --git a/sketch/obd2_gauge/gps_clock.h b/sketch/obd2_gauge/gps_clock.h index 52b2ec8..ad52a26 100644 --- a/sketch/obd2_gauge/gps_clock.h +++ b/sketch/obd2_gauge/gps_clock.h @@ -348,12 +348,12 @@ inline void drawGpsClockScreen(int x_offset = 0) { uint16_t cardBorder = 0x2945; // Subtle edge border bool synced = isGpsSynced(); - // 1. TOP BAR (y = 0 to 18) - // Center: 4 Pagination Indicator Dots [○ ○ ● ○] (Page 3 active) - tft.fillCircle(139 + x_offset, 2, 2, 0x5AEB); // Page 1 inactive - tft.fillCircle(153 + x_offset, 2, 2, 0x5AEB); // Page 2 inactive - tft.fillCircle(167 + x_offset, 2, 2, priCol); // Page 3 active - tft.fillCircle(181 + x_offset, 2, 2, 0x5AEB); // Page 4 inactive + // Center: 5 Pagination Indicator Dots [○ ○ ● ○ ○] (Page 2 of 5 active) + tft.fillCircle(132 + x_offset, 2, 2, 0x5AEB); // Page 0 inactive + tft.fillCircle(146 + x_offset, 2, 2, 0x5AEB); // Page 1 inactive + tft.fillCircle(160 + x_offset, 2, 2, priCol); // Page 2 active + tft.fillCircle(174 + x_offset, 2, 2, 0x5AEB); // Page 3 inactive + tft.fillCircle(188 + x_offset, 2, 2, 0x5AEB); // Page 4 inactive // 2. MAIN CLOCK CARD (y = 19 to 121, height = 102, width = 312, x = 4) int clkX = 4 + x_offset; diff --git a/sketch/obd2_gauge/gps_nav.h b/sketch/obd2_gauge/gps_nav.h index 322fd89..b07aa7e 100644 --- a/sketch/obd2_gauge/gps_nav.h +++ b/sketch/obd2_gauge/gps_nav.h @@ -103,12 +103,12 @@ inline void drawGpsNavScreen(int x_offset = 0) { uint16_t cardBorder = 0x2945; // Subtle edge border bool synced = isGpsSynced(); - // 1. TOP BAR (y = 0 to 18) - // Center: 4 Pagination Indicator Dots [○ ○ ○ ●] (Page 4 active) - tft.fillCircle(139 + x_offset, 2, 2, 0x5AEB); // Page 1 inactive - tft.fillCircle(153 + x_offset, 2, 2, 0x5AEB); // Page 2 inactive - tft.fillCircle(167 + x_offset, 2, 2, 0x5AEB); // Page 3 inactive - tft.fillCircle(181 + x_offset, 2, 2, priCol); // Page 4 active + // Center: 5 Pagination Indicator Dots [○ ○ ○ ● ○] (Page 3 of 5 active) + tft.fillCircle(132 + x_offset, 2, 2, 0x5AEB); // Page 0 inactive + tft.fillCircle(146 + x_offset, 2, 2, 0x5AEB); // Page 1 inactive + tft.fillCircle(160 + x_offset, 2, 2, 0x5AEB); // Page 2 inactive + tft.fillCircle(174 + x_offset, 2, 2, priCol); // Page 3 active + tft.fillCircle(188 + x_offset, 2, 2, 0x5AEB); // Page 4 inactive // 2. CARD 0 (Top Left): ALTITUD (x = 4, y = 19, w = 152, h = 102) int c0X = 4 + x_offset; diff --git a/sketch/obd2_gauge/meter.h b/sketch/obd2_gauge/meter.h index 35f4c65..44288e9 100644 --- a/sketch/obd2_gauge/meter.h +++ b/sketch/obd2_gauge/meter.h @@ -1844,19 +1844,23 @@ inline void animateBlackTransition(int8_t toPage = -1) { renderPage1ToSprite(sprTrans, 0, y_slice, slice_h); else if (toPage == 2) renderPage2ToSprite(sprTrans, 0, y_slice, slice_h); - else + else if (toPage == 3) renderPage3ToSprite(sprTrans, 0, y_slice, slice_h); + else + renderPage4ToSprite(sprTrans, 0, y_slice, slice_h); - // Pagination indicator dots on top slice (4 dots for 4 pages) + // Pagination indicator dots on top slice (5 dots for 5 pages) if (y_slice == 0) { - sprTrans.fillCircle(139, 2, 2, + sprTrans.fillCircle(132, 2, 2, (toPage == 0) ? getThemePrimaryColor() : 0x5AEB); - sprTrans.fillCircle(153, 2, 2, + sprTrans.fillCircle(146, 2, 2, (toPage == 1) ? getThemePrimaryColor() : 0x5AEB); - sprTrans.fillCircle(167, 2, 2, + sprTrans.fillCircle(160, 2, 2, (toPage == 2) ? getThemePrimaryColor() : 0x5AEB); - sprTrans.fillCircle(181, 2, 2, + sprTrans.fillCircle(174, 2, 2, (toPage == 3) ? getThemePrimaryColor() : 0x5AEB); + sprTrans.fillCircle(188, 2, 2, + (toPage == 4) ? getThemePrimaryColor() : 0x5AEB); } sprTrans.pushSprite(0, y_slice); @@ -1916,8 +1920,10 @@ inline void animateFadeTransition(int8_t fromPage = -1, int8_t toPage = -1) { renderPage1ToSprite(sprTrans, 0, y_slice, slice_h); else if (fromPage == 2) renderPage2ToSprite(sprTrans, 0, y_slice, slice_h); - else + else if (fromPage == 3) renderPage3ToSprite(sprTrans, 0, y_slice, slice_h); + else + renderPage4ToSprite(sprTrans, 0, y_slice, slice_h); if (toPage == 0) renderPage0ToSprite(sprFadeB, 0, y_slice, slice_h); @@ -1925,8 +1931,10 @@ inline void animateFadeTransition(int8_t fromPage = -1, int8_t toPage = -1) { renderPage1ToSprite(sprFadeB, 0, y_slice, slice_h); else if (toPage == 2) renderPage2ToSprite(sprFadeB, 0, y_slice, slice_h); - else + else if (toPage == 3) renderPage3ToSprite(sprFadeB, 0, y_slice, slice_h); + else + renderPage4ToSprite(sprFadeB, 0, y_slice, slice_h); uint16_t *pA = (uint16_t *)sprTrans.getPointer(); uint16_t *pB = (uint16_t *)sprFadeB.getPointer(); @@ -1956,17 +1964,19 @@ inline void animateFadeTransition(int8_t fromPage = -1, int8_t toPage = -1) { } } - // Pagination indicator dots on top slice (4 dots for 4 pages) + // Pagination indicator dots on top slice (5 dots for 5 pages) if (y_slice == 0) { uint8_t activeDot = (alpha < 16) ? fromPage : toPage; - sprTrans.fillCircle(139, 2, 2, + sprTrans.fillCircle(132, 2, 2, (activeDot == 0) ? getThemePrimaryColor() : 0x5AEB); - sprTrans.fillCircle(153, 2, 2, + sprTrans.fillCircle(146, 2, 2, (activeDot == 1) ? getThemePrimaryColor() : 0x5AEB); - sprTrans.fillCircle(167, 2, 2, + sprTrans.fillCircle(160, 2, 2, (activeDot == 2) ? getThemePrimaryColor() : 0x5AEB); - sprTrans.fillCircle(181, 2, 2, + sprTrans.fillCircle(174, 2, 2, (activeDot == 3) ? getThemePrimaryColor() : 0x5AEB); + sprTrans.fillCircle(188, 2, 2, + (activeDot == 4) ? getThemePrimaryColor() : 0x5AEB); } sprTrans.pushSprite(0, y_slice); @@ -1987,7 +1997,7 @@ inline void performScreenTransition(bool swipeLeft, int8_t fromPage = -1, if (fromPage < 0) fromPage = currentScreenPage; if (toPage < 0) - toPage = (fromPage + 1) % 4; + toPage = (fromPage + 1) % 5; if (transitionType == 1) { animateFadeTransition(fromPage, toPage); } else { @@ -2026,6 +2036,8 @@ void initScreen(bool redrawTFT) { syncGpsClockTrackers(); } else if (currentScreenPage == 3) { syncGpsNavTrackers(); + } else if (currentScreenPage == 4) { + syncClusterTrackers(); } else if (currentScreenPage == 0 && fullScreenCell == -1) { syncPage0Trackers(); } @@ -2060,6 +2072,11 @@ void initScreen(bool redrawTFT) { Serial.println(">>> [DB] initScreen: drawing drawGpsNavScreen(0)"); #endif drawGpsNavScreen(0); + } else if (currentScreenPage == 4) { +#ifdef SERIAL_DEBUG + Serial.println(">>> [DB] initScreen: drawing drawClusterScreen(0)"); +#endif + drawClusterScreen(0); } else { #ifdef SERIAL_DEBUG Serial.println(">>> [DB] initScreen: drawing Page 0 quadrants"); @@ -2070,11 +2087,12 @@ void initScreen(bool redrawTFT) { arcMeter4(c, pid); } drawQuadrantDividers(); - // Pagination indicator dots [● ○ ○ ○] (Page 1 active) - tft.fillCircle(139, 2, 2, getThemePrimaryColor()); // Page 1 active - tft.fillCircle(153, 2, 2, 0x5AEB); // Page 2 inactive - tft.fillCircle(167, 2, 2, 0x5AEB); // Page 3 inactive - tft.fillCircle(181, 2, 2, 0x5AEB); // Page 4 inactive + // Pagination indicator dots [● ○ ○ ○ ○] (Page 0 active) + tft.fillCircle(132, 2, 2, getThemePrimaryColor()); + tft.fillCircle(146, 2, 2, 0x5AEB); + tft.fillCircle(160, 2, 2, 0x5AEB); + tft.fillCircle(174, 2, 2, 0x5AEB); + tft.fillCircle(188, 2, 2, 0x5AEB); } if (currentScreenPage == 0 && fullScreenCell == -1) { badgeEcoDrawn = false; @@ -2408,6 +2426,8 @@ void updateMeter(uint8_t pidNo, String response) { // update parameter on screen } } else if (currentScreenPage == 0) { plotArc4(pidNo, data, warn, min, max, digit, unit); + } else if (currentScreenPage == 4) { + updateClusterScreen(false); } bt_message = ""; // reset } diff --git a/sketch/obd2_gauge/obd2_gauge.ino b/sketch/obd2_gauge/obd2_gauge.ino index f1e8503..241a8ea 100644 --- a/sketch/obd2_gauge/obd2_gauge.ino +++ b/sketch/obd2_gauge/obd2_gauge.ino @@ -301,8 +301,8 @@ int8_t currentScreenPage = inline void setScreenPage(int8_t page) { if (page < 0) page = 0; - if (page > 3) - page = 3; + if (page > 4) + page = 4; currentScreenPage = page; pref.begin("setting", false); pref.putChar("scr_page", page); @@ -488,6 +488,7 @@ String sendATCommand(const String &cmd, uint32_t timeoutMs = 2000) { #include "gps_module.h" #include "gps_clock.h" #include "gps_nav.h" +#include "cluster_screen.h" #include "meter.h" #include "config.h" @@ -710,7 +711,7 @@ recent_client_addr : {0x00,0x00,0x00,0x00,0x00,0x00} array of bytes[6] if (transitionType > 1) transitionType = 0; currentScreenPage = pref.getChar("scr_page", 0); - if (currentScreenPage < 0 || currentScreenPage > 3) + if (currentScreenPage < 0 || currentScreenPage > 4) currentScreenPage = 0; if (brightnessIndex > 0) { targetBacklight = BRIGHTNESS_PWM_TABLE[brightnessIndex]; @@ -862,6 +863,11 @@ void loop() { updateGpsNavScreen(false); } + // Non-blocking live refresh for resting Page 4 (Cockpit Instrumental Deportivo) + if (currentScreenPage == 4 && fullScreenCell == -1) { + updateClusterScreen(false); + } + if (wokeFromSleep) { wokeFromSleep = false; lastActivityTime = millis(); @@ -921,10 +927,10 @@ void loop() { // (disabled in full-screen mode) if (!longPressHandled) { if (fullScreenCell == -1 && liveDeltaX < -28 && abs(liveDeltaY) < 85) { - // SWIPE LEFT: Transition to next page (0 -> 1 -> 2 -> 3 -> 0) + // SWIPE LEFT: Transition to next page (0 -> 1 -> 2 -> 3 -> 4 -> 0) longPressHandled = true; touchBeep(); - uint8_t nextPage = (currentScreenPage + 1) % 4; + uint8_t nextPage = (currentScreenPage + 1) % 5; performScreenTransition(true, currentScreenPage, nextPage); setScreenPage(nextPage); initScreen(false); // Seamless: keep transition frame without blanking @@ -934,11 +940,11 @@ void loop() { lastActivityTime = millis(); } else if (fullScreenCell == -1 && liveDeltaX > 28 && abs(liveDeltaY) < 85) { - // SWIPE RIGHT: Transition to previous page (0 -> 3 -> 2 -> 1 -> 0) + // SWIPE RIGHT: Transition to previous page (0 -> 4 -> 3 -> 2 -> 1 -> 0) longPressHandled = true; touchBeep(); uint8_t nextPage = - (currentScreenPage == 0) ? 3 : (currentScreenPage - 1); + (currentScreenPage == 0) ? 4 : (currentScreenPage - 1); performScreenTransition(false, currentScreenPage, nextPage); setScreenPage(nextPage); initScreen(false); // Seamless: keep transition frame without blanking @@ -978,7 +984,7 @@ void loop() { // SWIPE LEFT longPressHandled = true; touchBeep(); - uint8_t nextPage = (currentScreenPage + 1) % 4; + uint8_t nextPage = (currentScreenPage + 1) % 5; performScreenTransition(true, currentScreenPage, nextPage); setScreenPage(nextPage); initScreen(false); // Seamless: keep transition frame without blanking @@ -991,7 +997,7 @@ void loop() { longPressHandled = true; touchBeep(); uint8_t nextPage = - (currentScreenPage == 0) ? 3 : (currentScreenPage - 1); + (currentScreenPage == 0) ? 4 : (currentScreenPage - 1); performScreenTransition(false, currentScreenPage, nextPage); setScreenPage(nextPage); initScreen(false); // Seamless: keep transition frame without blanking @@ -1023,6 +1029,9 @@ void loop() { } else if (currentScreenPage == 3) { // Tap on GPS Navigation screen: immediate telemetry refresh updateGpsNavScreen(true); + } else if (currentScreenPage == 4) { + // Tap on Cockpit Instrumental: immediate cluster refresh + updateClusterScreen(true); } waitForTouchRelease(); lastActivityTime = millis(); diff --git a/sketch/obd2_gauge/tri_temp.h b/sketch/obd2_gauge/tri_temp.h index 24c49f4..964c27a 100644 --- a/sketch/obd2_gauge/tri_temp.h +++ b/sketch/obd2_gauge/tri_temp.h @@ -492,11 +492,12 @@ inline void drawTriTempScreen(int x_offset = 0) { syncTriTempTrackers(); - // Draw Page Indicator: [○ ● ○ ○] (Page 2 of 4 active) - tft.fillCircle(139 + x_offset, 2, 2, 0x5AEB); // Page 1 inactive - tft.fillCircle(153 + x_offset, 2, 2, getThemePrimaryColor()); // Page 2 active - tft.fillCircle(167 + x_offset, 2, 2, 0x5AEB); // Page 3 inactive - tft.fillCircle(181 + x_offset, 2, 2, 0x5AEB); // Page 4 inactive + // Draw Page Indicator: [○ ● ○ ○ ○] (Page 2 of 5 active) + tft.fillCircle(132 + x_offset, 2, 2, 0x5AEB); // Page 0 inactive + tft.fillCircle(146 + x_offset, 2, 2, getThemePrimaryColor()); // Page 1 active + tft.fillCircle(160 + x_offset, 2, 2, 0x5AEB); // Page 2 inactive + tft.fillCircle(174 + x_offset, 2, 2, 0x5AEB); // Page 3 inactive + tft.fillCircle(188 + x_offset, 2, 2, 0x5AEB); // Page 4 inactive } // Update coolant reading dynamically without redrawing entire screen