diff --git a/sketch/obd2_gauge/config.h b/sketch/obd2_gauge/config.h index 124e31e..eec8a2b 100644 --- a/sketch/obd2_gauge/config.h +++ b/sketch/obd2_gauge/config.h @@ -1496,7 +1496,7 @@ void themeMenu() { tft.fillScreen(TFT_BLACK); const char* const THEME_SHORT_NAMES[6] = { - "Cian", "Verde", "Ambar", "Blanco", "Naranja", "Violeta" + "Cian", "Verde", "Arcoiris", "Blanco", "Naranja", "Violeta" }; auto drawThemeUI = [&]() { @@ -1514,6 +1514,12 @@ void themeMenu() { // Left side: Theme Name and Description tft.setTextColor(GAUGE_THEMES[themeIndex].primaryColor, 0x1082); tft.drawString(GAUGE_THEMES[themeIndex].name, 20, 40, 4); + if (themeIndex == 2) { + const uint16_t rainbowMini[6] = {0x03FF, 0x07FF, 0x07E0, 0xFFE0, 0xFD20, 0xF800}; + for (uint8_t b = 0; b < 6; b++) { + tft.fillRect(20 + b * 17, 65, 15, 2, rainbowMini[b]); + } + } tft.setTextColor(0xAD55, 0x1082); tft.drawString(GAUGE_THEMES[themeIndex].desc, 20, 69, 2); @@ -1522,7 +1528,14 @@ void themeMenu() { tft.fillRectVGradient(168, 39, 126, 12, GAUGE_THEMES[themeIndex].bgColor, TFT_BLACK); tft.fillRect(168, 51, 126, 6, TFT_BLACK); tft.fillRect(220, 39, 2, 18, GAUGE_THEMES[themeIndex].divColor); // Sample Crosshair line - tft.fillCircle(188, 48, 3, TFT_GREEN); // Sample green gauge block + if (themeIndex == 2) { + const uint16_t rainbowMini[6] = {0x03FF, 0x07FF, 0x07E0, 0xFFE0, 0xFD20, 0xF800}; + for (uint8_t b = 0; b < 6; b++) { + tft.fillRoundRect(172 + b * 7, 43, 5, 10, 1, rainbowMini[b]); + } + } else { + tft.fillCircle(188, 48, 3, TFT_GREEN); // Sample green gauge block + } tft.setTextColor(0xBDD7, TFT_BLACK); tft.drawRightString("VIGNETTE", 290, 42, 1); @@ -1562,7 +1575,19 @@ void themeMenu() { int16_t cx = tx + 16; int16_t cy = ty + 17; tft.drawCircle(cx, cy, 7, isSel ? TFT_WHITE : 0x4208); - tft.fillCircle(cx, cy, 5, GAUGE_THEMES[i].primaryColor); + if (i == 2) { + // Rainbow theme: color-wheel swatch (hue by angle) + for (int8_t dy = -5; dy <= 5; dy++) { + for (int8_t dx = -5; dx <= 5; dx++) { + if (dx * dx + dy * dy > 25) continue; + float ang = atan2f((float)dy, (float)dx) * 57.2958f; + if (ang < 0) ang += 360.0f; + tft.drawPixel(cx + dx, cy + dy, hueToRgb565((uint16_t)ang)); + } + } + } else { + tft.fillCircle(cx, cy, 5, GAUGE_THEMES[i].primaryColor); + } // Label tft.setTextColor(isSel ? TFT_WHITE : 0xBDD7, bg); diff --git a/sketch/obd2_gauge/gps_clock.h b/sketch/obd2_gauge/gps_clock.h index 2e9ef79..ba37d3e 100644 --- a/sketch/obd2_gauge/gps_clock.h +++ b/sketch/obd2_gauge/gps_clock.h @@ -3,13 +3,16 @@ #include "DSEG7_Modern_BoldItalic_34.h" #include "DSEG7_Modern_BoldItalic_80.h" #include "Latin_Sans_Bold_26.h" +#include "rainbow_text.h" #include "gps_module.h" #include #include extern TFT_eSPI tft; extern uint16_t getThemePrimaryColor(); +extern uint16_t getThemeAccentColor(); extern uint16_t getThemeBgColor(); +extern bool isRainbowTheme(); extern int8_t currentScreenPage; extern int8_t fullScreenCell; @@ -26,6 +29,60 @@ static uint8_t oldClockDay = 255; static uint8_t oldClockMonth = 255; static uint16_t oldClockYear = 0; +// Smooth pre-rendered title sprites for Day of Week and Year (100% anti-aliased Latin_Sans_Bold_26 matching Page 1) +static TFT_eSprite sprDayTitle(&tft); +static uint8_t sprDayWday = 255; + +inline void updateDayTitleSprite(uint8_t wday) { + if (sprDayWday == wday && sprDayTitle.created()) return; + if (!sprDayTitle.created()) { + sprDayTitle.createSprite(160, 26); + } + if (sprDayTitle.created()) { + uint16_t cardBg = 0x10A2; + sprDayTitle.loadFont(Latin_Sans_Bold_26); + sprDayTitle.setTextPadding(0); + sprDayTitle.fillSprite(cardBg); + sprDayTitle.setTextColor(TFT_WHITE, cardBg); + sprDayTitle.drawString(SPANISH_DAYS[wday], 0, 0, 1); + sprDayTitle.unloadFont(); + if (isRainbowTheme()) { + applyRainbowToSprite(sprDayTitle, 4 + 116, 125 + 22, cardBg); + } + sprDayWday = wday; + } +} + +static TFT_eSprite sprYearTitle(&tft); +static uint16_t sprYearVal = 0; +static uint16_t sprYearCol = 0; + +inline void updateYearTitleSprite(uint16_t year, uint16_t priCol) { + if (sprYearVal == year && sprYearCol == priCol && sprYearTitle.created()) return; + if (!sprYearTitle.created()) { + sprYearTitle.createSprite(80, 26); + } + if (sprYearTitle.created()) { + uint16_t cardBg = 0x10A2; + sprYearTitle.loadFont(Latin_Sans_Bold_26); + sprYearTitle.setTextPadding(0); + sprYearTitle.fillSprite(cardBg); + sprYearTitle.setTextColor(priCol, cardBg); + sprYearTitle.drawString(String(year).c_str(), 0, 0, 1); + sprYearTitle.unloadFont(); + if (isRainbowTheme()) { + applyRainbowToSprite(sprYearTitle, 4 + 116, 125 + 64, cardBg); + } + sprYearVal = year; + sprYearCol = priCol; + } +} + +inline void initGpsClockTitles() { + updateDayTitleSprite(gpsDayOfWeek % 7); + updateYearTitleSprite(gpsYear, isRainbowTheme() ? (uint16_t)0xFFE0 : getThemeAccentColor()); +} + // Draw Full-Width Date Card (Bottom) - Clean, prominent, automotive badge // layout inline void drawDateCardContent(int cardX, int cardY, int cardW, int cardH, @@ -33,7 +90,8 @@ inline void drawDateCardContent(int cardX, int cardY, int cardW, int cardH, if (cardX + cardW <= 0 || cardX >= 320) return; - uint16_t priCol = getThemePrimaryColor(); + uint16_t accCol = isRainbowTheme() ? (uint16_t)0xFD20 : getThemeAccentColor(); + uint16_t yearCol = isRainbowTheme() ? (uint16_t)0xFFE0 : accCol; uint16_t cardBorder = 0x2945; uint8_t wday = gpsDayOfWeek % 7; uint8_t mIdx = (gpsMonth >= 1 && gpsMonth <= 12) ? (gpsMonth - 1) : 0; @@ -41,7 +99,7 @@ inline void drawDateCardContent(int cardX, int cardY, int cardW, int cardH, String dayStr = (gpsDay < 10) ? "0" + String(gpsDay) : String(gpsDay); // Left vertical accent bar - tft.fillRoundRect(cardX + 6, cardY + 8, 4, cardH - 16, 2, priCol); + tft.fillRoundRect(cardX + 6, cardY + 8, 4, cardH - 16, 2, accCol); // --- CALENDAR BADGE (Left side: 80x82 px) --- int bx = cardX + 20; @@ -53,10 +111,10 @@ inline void drawDateCardContent(int cardX, int cardY, int cardW, int cardH, tft.fillRoundRect(bx, by, bw, bh, 6, 0x18C3); tft.drawRoundRect(bx, by, bw, bh, 6, cardBorder); - // Badge top header (Month abbreviated in Primary Color) - tft.fillRoundRect(bx, by, bw, 24, 6, priCol); - tft.fillRect(bx, by + 18, bw, 6, priCol); // flatten lower header corners - tft.setTextColor(TFT_BLACK, priCol); + // Badge top header (Month abbreviated in Accent Color) + tft.fillRoundRect(bx, by, bw, 24, 6, accCol); + tft.fillRect(bx, by + 18, bw, 6, accCol); // flatten lower header corners + tft.setTextColor(TFT_BLACK, accCol); tft.drawCentreString(mShort.c_str(), bx + (bw / 2), by + 4, 2); // Badge body: Big Day Number in DSEG7 34pt @@ -69,22 +127,33 @@ inline void drawDateCardContent(int cardX, int cardY, int cardW, int cardH, // --- DATE DETAILS (Right side) --- int tx = cardX + 116; - tft.loadFont(Latin_Sans_Bold_26); - tft.setTextPadding(0); - - // Day of week in Latin Sans Bold 26 (Smooth antialiased, White, uppercase) - tft.setTextColor(TFT_WHITE, cardBg); - tft.drawString(SPANISH_DAYS[wday], tx, cardY + 22, 1); + // Day of week in Latin Sans Bold 26 (Smooth antialiased, White, uppercase matching Page 1) + updateDayTitleSprite(wday); + if (sprDayTitle.created()) { + sprDayTitle.pushSprite(tx, cardY + 22); + } else { + tft.loadFont(Latin_Sans_Bold_26); + tft.setTextPadding(0); + tft.setTextColor(TFT_WHITE, cardBg); + tft.drawString(SPANISH_DAYS[wday], tx, cardY + 22, 1); + tft.unloadFont(); + } // Elegant two-tone horizontal dividing line tft.drawFastHLine(tx, cardY + 54, 180, cardBorder); - tft.drawFastHLine(tx, cardY + 54, 50, priCol); + tft.drawFastHLine(tx, cardY + 54, 50, accCol); - // Year in Latin Sans Bold 26 (Smooth antialiased, Primary color) - tft.setTextColor(priCol, cardBg); - tft.drawString(String(gpsYear).c_str(), tx, cardY + 64, 1); - - tft.unloadFont(); + // Year in Latin Sans Bold 26 (Smooth antialiased, Accent color) + updateYearTitleSprite(gpsYear, yearCol); + if (sprYearTitle.created()) { + sprYearTitle.pushSprite(tx, cardY + 64); + } else { + tft.loadFont(Latin_Sans_Bold_26); + tft.setTextPadding(0); + tft.setTextColor(yearCol, cardBg); + tft.drawString(String(gpsYear).c_str(), tx, cardY + 64, 1); + tft.unloadFont(); + } } // Authentic DSEG7 antialiased dot alpha matrix (from @@ -115,9 +184,10 @@ static const uint8_t DSEG7_DOT_ALPHA[11][13] PROGMEM = { }; // Helper: Draw authentic 7-segment italic colon dots with true anti-aliasing -// matching DSEG7 typography -inline void draw7SegDot(TFT_eSPI &dst, int x0, int y0, uint16_t color, - uint16_t bgCol, int clip_h = 240) { +// matching DSEG7 typography and optional rainbow gradient +template +inline void draw7SegDot(T &dst, int x0, int y0, uint16_t color, + uint16_t bgCol, int clip_h = 240, bool rainbow = false) { dst.startWrite(); for (int dy = 0; dy < 11; dy++) { int curY = y0 + dy; @@ -144,73 +214,32 @@ inline void draw7SegDot(TFT_eSPI &dst, int x0, int y0, uint16_t color, dst.drawFastHLine(fxs, curY, fl, color); fl = 0; } - dst.drawPixel(curX, curY, dst.alphaBlend(a, color, bgCol)); + uint16_t fg = rainbow ? getRainbowScreenColor(curX, curY) : color; + dst.drawPixel(curX, curY, dst.alphaBlend(a, fg, bgCol)); } } if (fl) { - dst.drawFastHLine(fxs, curY, fl, color); - } - } - dst.endWrite(); -} - -inline void draw7SegColon(TFT_eSPI &dst, int clkX, int clkY, uint16_t color, - uint16_t bgCol = 0x10A2, int clip_h = 240) { - // Top dot: centered at clkX + 156.5, font row 19..29 (y = clkY + 30) - draw7SegDot(dst, clkX + 151, clkY + 30, color, bgCol, clip_h); - // Bottom dot: centered at clkX + 153.5, font row 53..63 (y = clkY + 64) - draw7SegDot(dst, clkX + 148, clkY + 64, color, bgCol, clip_h); -} - -// Render authentic DSEG7 80pt italic digit directly into transition sprite -// using raw PROGMEM font bitmap (zero heap allocations, zero font-switching conflicts) -inline void drawDseg7Digit80(TFT_eSprite &dst, int x0, int y0, uint8_t digit, - uint16_t color, uint16_t bgCol, int clip_h = 240) { - if (digit > 9) return; - const uint8_t *bmp = &DSEG7_Modern_BoldItalic_80[388 + digit * 5200]; - dst.startWrite(); - for (int dy = 0; dy < 80; dy++) { - int curY = y0 + dy; - if (curY < 0 || curY >= clip_h) - continue; - - const uint8_t *row = bmp + dy * 65; - int fl = 0; - int fxs = 0; - for (int dx = 0; dx < 65; dx++) { - int curX = x0 + dx; - if (curX < 0 || curX >= 320) { - if (fl) { - dst.drawFastHLine(fxs, curY, fl, color); - fl = 0; + if (rainbow) { + for (int i = 0; i < fl; i++) { + dst.drawPixel(fxs + i, curY, getRainbowScreenColor(fxs + i, curY)); } - continue; - } - uint8_t a = pgm_read_byte(row + dx); - if (a == 0) { - if (fl) { - dst.drawFastHLine(fxs, curY, fl, color); - fl = 0; - } - } else if (a >= 235) { - if (fl == 0) - fxs = curX; - fl++; } else { - if (fl) { - dst.drawFastHLine(fxs, curY, fl, color); - fl = 0; - } - dst.drawPixel(curX, curY, dst.alphaBlend(a, color, bgCol)); + dst.drawFastHLine(fxs, curY, fl, color); } } - if (fl) { - dst.drawFastHLine(fxs, curY, fl, color); - } } dst.endWrite(); } +template +inline void draw7SegColon(T &dst, int clkX, int clkY, uint16_t color, + uint16_t bgCol = 0x10A2, int clip_h = 240, bool rainbow = false) { + // Top dot: centered at clkX + 151, font row 19..29 (y = clkY + 30) + draw7SegDot(dst, clkX + 151, clkY + 30, color, bgCol, clip_h, rainbow); + // Bottom dot: centered at clkX + 148, font row 53..63 (y = clkY + 64) + draw7SegDot(dst, clkX + 148, clkY + 64, color, bgCol, clip_h, rainbow); +} + // Synchronize tracking variables without touching TFT display (zero flicker) inline void syncGpsClockTrackers() { oldClockHour = gpsHour; @@ -230,6 +259,7 @@ inline void drawGpsClockScreen(int x_offset = 0) { } uint16_t priCol = getThemePrimaryColor(); + uint16_t clkAccCol = isRainbowTheme() ? (uint16_t)0x07FF : priCol; uint16_t cardBg = 0x10A2; // Dark graphite navy uint16_t cardBorder = 0x2945; // Subtle edge border bool synced = isGpsSynced(); @@ -251,24 +281,25 @@ inline void drawGpsClockScreen(int x_offset = 0) { tft.drawRoundRect(clkX, clkY, clkW, clkH, 6, cardBorder); // Left vertical accent bar - tft.fillRoundRect(clkX + 6, clkY + 8, 4, clkH - 16, 2, priCol); + tft.fillRoundRect(clkX + 6, clkY + 8, 4, clkH - 16, 2, clkAccCol); - // Digits: Hours HH (Right aligned to clkX + 142) & Minutes MM (Left aligned - // to clkX + 170) - String hStr = (gpsHour < 10) ? "0" + String(gpsHour) : String(gpsHour); - String mStr = (gpsMinute < 10) ? "0" + String(gpsMinute) : String(gpsMinute); + // Digits: Hours HH & Minutes MM with smooth rainbow gradient + uint8_t h10 = (gpsHour / 10) % 10; + uint8_t h1 = gpsHour % 10; + uint8_t m10 = (gpsMinute / 10) % 10; + uint8_t m1 = gpsMinute % 10; + bool rbClk = isRainbowTheme(); - tft.loadFont(DSEG7_Modern_BoldItalic_80); - tft.setTextColor(priCol, cardBg); - tft.setTextPadding(0); - tft.drawRightString(hStr.c_str(), clkX + 142, clkY + 11, 1); - tft.drawString(mStr.c_str(), clkX + 170, clkY + 11, 1); - tft.unloadFont(); + drawDseg7Digit80(tft, clkX + 12, clkY + 11, h10, clkAccCol, cardBg, 240, rbClk); + drawDseg7Digit80(tft, clkX + 77, clkY + 11, h1, clkAccCol, cardBg, 240, rbClk); + drawDseg7Digit80(tft, clkX + 170, clkY + 11, m10, clkAccCol, cardBg, 240, rbClk); + drawDseg7Digit80(tft, clkX + 235, clkY + 11, m1, clkAccCol, cardBg, 240, rbClk); // Blinking Colon Dots between HH and MM (italic 7-segment style with // anti-aliasing) - uint16_t colonCol = (gpsSecond % 2 == 0) ? priCol : 0x18C3; - draw7SegColon(tft, clkX, clkY, colonCol, cardBg); + bool colOn = (gpsSecond % 2 == 0); + uint16_t colonCol = colOn ? clkAccCol : 0x18C3; + draw7SegColon(tft, clkX, clkY, colonCol, cardBg, 240, colOn && rbClk); // 3. FULL-WIDTH DATE CARD (y = 125 to 235, height = 110, width = 312, x = 4) int dateX = 4 + x_offset; @@ -289,6 +320,7 @@ inline void updateGpsClockScreen(bool forceRedraw = false) { return; uint16_t priCol = getThemePrimaryColor(); + uint16_t clkAccCol = isRainbowTheme() ? (uint16_t)0x07FF : priCol; uint16_t cardBg = 0x10A2; int clkX = 4; int clkY = 19; @@ -296,35 +328,29 @@ inline void updateGpsClockScreen(bool forceRedraw = false) { // 1. Seconds change: update colon dots only (no seconds text) if (forceRedraw || gpsSecond != oldClockSec) { oldClockSec = gpsSecond; - - // Colon dots (italic 7-segment style with anti-aliasing) - uint16_t colonCol = (gpsSecond % 2 == 0) ? priCol : 0x18C3; - draw7SegColon(tft, clkX, clkY, colonCol, cardBg); + bool colOn = (gpsSecond % 2 == 0); + uint16_t colonCol = colOn ? clkAccCol : 0x18C3; + draw7SegColon(tft, clkX, clkY, colonCol, cardBg, 240, colOn && isRainbowTheme()); } // 2. Minutes change: update MM digits if (forceRedraw || gpsMinute != oldClockMin) { oldClockMin = gpsMinute; tft.fillRect(clkX + 166, clkY + 10, 142, 82, cardBg); - String mStr = - (gpsMinute < 10) ? "0" + String(gpsMinute) : String(gpsMinute); - tft.loadFont(DSEG7_Modern_BoldItalic_80); - tft.setTextColor(priCol, cardBg); - tft.setTextPadding(0); - tft.drawString(mStr.c_str(), clkX + 170, clkY + 11, 1); - tft.unloadFont(); + uint8_t m10 = (gpsMinute / 10) % 10; + uint8_t m1 = gpsMinute % 10; + drawDseg7Digit80(tft, clkX + 170, clkY + 11, m10, clkAccCol, cardBg, 240, isRainbowTheme()); + drawDseg7Digit80(tft, clkX + 235, clkY + 11, m1, clkAccCol, cardBg, 240, isRainbowTheme()); } // 3. Hours change: update HH digits if (forceRedraw || gpsHour != oldClockHour) { oldClockHour = gpsHour; tft.fillRect(clkX + 12, clkY + 10, 136, 82, cardBg); - String hStr = (gpsHour < 10) ? "0" + String(gpsHour) : String(gpsHour); - tft.loadFont(DSEG7_Modern_BoldItalic_80); - tft.setTextColor(priCol, cardBg); - tft.setTextPadding(0); - tft.drawRightString(hStr.c_str(), clkX + 142, clkY + 11, 1); - tft.unloadFont(); + uint8_t h10 = (gpsHour / 10) % 10; + uint8_t h1 = gpsHour % 10; + drawDseg7Digit80(tft, clkX + 12, clkY + 11, h10, clkAccCol, cardBg, 240, isRainbowTheme()); + drawDseg7Digit80(tft, clkX + 77, clkY + 11, h1, clkAccCol, cardBg, 240, isRainbowTheme()); } // 4. Date changed (midnight rollover): redraw full date card @@ -347,6 +373,8 @@ inline void updateGpsClockScreen(bool forceRedraw = false) { inline void renderPage2ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int slice_h) { uint16_t priCol = getThemePrimaryColor(); + uint16_t accCol = getThemeAccentColor(); + uint16_t clkAccCol = isRainbowTheme() ? (uint16_t)0x07FF : priCol; uint16_t cardBg = 0x10A2; uint16_t cardBorder = 0x2945; @@ -361,17 +389,13 @@ inline void renderPage2ToSprite(TFT_eSprite &s, int x_offset, int y_slice, if (clkX + clkW > 0 && clkX < 320 && clkY + clkH > 0 && clkY < slice_h) { s.fillRoundRect(clkX, clkY, clkW, clkH, 6, cardBg); s.drawRoundRect(clkX, clkY, clkW, clkH, 6, cardBorder); - s.fillRoundRect(clkX + 6, clkY + 8, 4, clkH - 16, 2, priCol); + s.fillRoundRect(clkX + 6, clkY + 8, 4, clkH - 16, 2, clkAccCol); // Colon dots during transition (italic 7-segment style with anti-aliasing) - draw7SegColon(s, clkX, clkY, priCol, cardBg, slice_h); + draw7SegColon(s, clkX, clkY, clkAccCol, cardBg, slice_h, isRainbowTheme()); // Digits: Hours HH (Right aligned to clkX + 142) & Minutes MM (Left aligned // to clkX + 170) - String hStr = (gpsHour < 10) ? "0" + String(gpsHour) : String(gpsHour); - String mStr = - (gpsMinute < 10) ? "0" + String(gpsMinute) : String(gpsMinute); - int digitY = clkY + 11; if (digitY + 80 > 0 && digitY < slice_h) { uint8_t h10 = (gpsHour / 10) % 10; @@ -379,10 +403,10 @@ inline void renderPage2ToSprite(TFT_eSprite &s, int x_offset, int y_slice, uint8_t m10 = (gpsMinute / 10) % 10; uint8_t m1 = gpsMinute % 10; - drawDseg7Digit80(s, clkX + 12, digitY, h10, priCol, cardBg, slice_h); - drawDseg7Digit80(s, clkX + 77, digitY, h1, priCol, cardBg, slice_h); - drawDseg7Digit80(s, clkX + 170, digitY, m10, priCol, cardBg, slice_h); - drawDseg7Digit80(s, clkX + 235, digitY, m1, priCol, cardBg, slice_h); + drawDseg7Digit80(s, clkX + 12, digitY, h10, clkAccCol, cardBg, slice_h, isRainbowTheme()); + drawDseg7Digit80(s, clkX + 77, digitY, h1, clkAccCol, cardBg, slice_h, isRainbowTheme()); + drawDseg7Digit80(s, clkX + 170, digitY, m10, clkAccCol, cardBg, slice_h, isRainbowTheme()); + drawDseg7Digit80(s, clkX + 235, digitY, m1, clkAccCol, cardBg, slice_h, isRainbowTheme()); } } @@ -394,9 +418,11 @@ inline void renderPage2ToSprite(TFT_eSprite &s, int x_offset, int y_slice, if (dateX + dateW > 0 && dateX < 320 && dateY + dateH > 0 && dateY < slice_h) { + uint16_t dateAccCol = isRainbowTheme() ? (uint16_t)0xFD20 : accCol; + uint16_t yearCol = isRainbowTheme() ? (uint16_t)0xFFE0 : accCol; s.fillRoundRect(dateX, dateY, dateW, dateH, 6, cardBg); s.drawRoundRect(dateX, dateY, dateW, dateH, 6, cardBorder); - s.fillRoundRect(dateX + 6, dateY + 8, 4, dateH - 16, 2, priCol); + s.fillRoundRect(dateX + 6, dateY + 8, 4, dateH - 16, 2, dateAccCol); // Badge container int bx = dateX + 20; @@ -408,10 +434,10 @@ inline void renderPage2ToSprite(TFT_eSprite &s, int x_offset, int y_slice, s.drawRoundRect(bx, by, bw, bh, 6, cardBorder); // Header strip - s.fillRoundRect(bx, by, bw, 24, 6, priCol); - s.fillRect(bx, by + 18, bw, 6, priCol); + s.fillRoundRect(bx, by, bw, 24, 6, dateAccCol); + s.fillRect(bx, by + 18, bw, 6, dateAccCol); uint8_t mIdx = (gpsMonth >= 1 && gpsMonth <= 12) ? (gpsMonth - 1) : 0; - s.setTextColor(TFT_BLACK, priCol); + s.setTextColor(TFT_BLACK, dateAccCol); s.drawCentreString(SPANISH_MONTHS_SHORT[mIdx], bx + (bw / 2), by + 4, 2); // Badge day number @@ -427,20 +453,30 @@ inline void renderPage2ToSprite(TFT_eSprite &s, int x_offset, int y_slice, uint8_t wday = gpsDayOfWeek % 7; int dwY = dateY + 22; if (dwY + 26 > 0 && dwY < slice_h) { - s.setTextColor(TFT_WHITE, cardBg); - s.drawString(SPANISH_DAYS[wday], tx, dwY, 4); + updateDayTitleSprite(wday); + if (sprDayTitle.created()) { + sprDayTitle.pushToSprite(&s, tx, dwY); + } else { + s.setTextColor(TFT_WHITE, cardBg); + s.drawString(SPANISH_DAYS[wday], tx, dwY, 4); + } } int divY = dateY + 54; if (divY > 0 && divY < slice_h) { s.drawFastHLine(tx, divY, 180, cardBorder); - s.drawFastHLine(tx, divY, 50, priCol); + s.drawFastHLine(tx, divY, 50, dateAccCol); } int yrY = dateY + 64; if (yrY + 26 > 0 && yrY < slice_h) { - s.setTextColor(priCol, cardBg); - s.drawString(String(gpsYear).c_str(), tx, yrY, 4); + updateYearTitleSprite(gpsYear, yearCol); + if (sprYearTitle.created()) { + sprYearTitle.pushToSprite(&s, tx, yrY); + } else { + s.setTextColor(yearCol, cardBg); + s.drawString(String(gpsYear).c_str(), tx, yrY, 4); + } } } } diff --git a/sketch/obd2_gauge/gps_module.h b/sketch/obd2_gauge/gps_module.h index 88d335b..b1b0072 100644 --- a/sketch/obd2_gauge/gps_module.h +++ b/sketch/obd2_gauge/gps_module.h @@ -531,6 +531,7 @@ inline uint32_t getGpsFailedChecksum() { return gps.failedChecksum(); } inline bool isGpsSerialReceiving() { return (gps.charsProcessed() > 0 && (millis() - lastGpsByteTime < 2500)); } +inline bool isGpsUartLocked() { return gpsUartLocked; } inline const char *getGpsDiagMessage() { if (gpsIsSleeping) diff --git a/sketch/obd2_gauge/gps_nav.h b/sketch/obd2_gauge/gps_nav.h index 443e4db..bbf9b09 100644 --- a/sketch/obd2_gauge/gps_nav.h +++ b/sketch/obd2_gauge/gps_nav.h @@ -4,10 +4,13 @@ #include #include "gps_module.h" #include "DSEG7_Modern_BoldItalic_34.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 int8_t currentScreenPage; extern int8_t fullScreenCell; @@ -27,6 +30,7 @@ static bool lastNavHasCourse = false; static const char* lastNavCardDir = ""; static uint8_t lastNavCardSats = 255; static bool lastNavSatsSync = false; +static bool lastNavUartLocked = false; static float lastNavCardHdop = -999.0f; static char lastNavCardDiag[32] = ""; static bool lastNavDiagSynced = false; @@ -51,6 +55,7 @@ inline void syncGpsNavTrackers() { lastNavCardSats = gpsSats; lastNavSatsSync = synced; + lastNavUartLocked = isGpsUartLocked(); oldNavSats = gpsSats; lastNavCardHdop = gpsHdop; @@ -87,15 +92,16 @@ inline void drawGpsNavScreen(int x_offset = 0) { } uint16_t priCol = getThemePrimaryColor(); + uint16_t accCol = getThemeAccentColor(); + uint16_t c0Col = isRainbowTheme() ? (uint16_t)0x07FF : priCol; + uint16_t c1Col = isRainbowTheme() ? (uint16_t)0x07E0 : accCol; + uint16_t c2Col = isRainbowTheme() ? (uint16_t)0xFFE0 : priCol; + uint16_t c3Col = isRainbowTheme() ? (uint16_t)0xFD20 : accCol; uint16_t cardBg = 0x10A2; // Dark graphite navy uint16_t cardBorder = 0x2945; // Subtle edge border bool synced = isGpsSynced(); // 1. TOP BAR (y = 0 to 18) - // Left: Screen Title - tft.setTextColor(0x8410, TFT_BLACK); - tft.drawString("TELEMETRIA GPS", 8 + x_offset, 5, 1); - // 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 @@ -109,7 +115,7 @@ inline void drawGpsNavScreen(int x_offset = 0) { int cH1 = 102; tft.fillRoundRect(c0X, c0Y, cW, cH1, 6, cardBg); tft.drawRoundRect(c0X, c0Y, cW, cH1, 6, cardBorder); - tft.fillRoundRect(c0X + 6, c0Y + 8, 4, cH1 - 16, 2, priCol); + tft.fillRoundRect(c0X + 6, c0Y + 8, 4, cH1 - 16, 2, c0Col); tft.setTextColor(0x8410, cardBg); tft.drawString("ALTITUD", c0X + 16, c0Y + 8, 2); @@ -118,32 +124,24 @@ inline void drawGpsNavScreen(int x_offset = 0) { bool altValid = isGpsAltitudeValid(); String altStr = altValid ? String((int)round(gpsAltitudeM)) : "---"; - tft.loadFont(DSEG7_Modern_BoldItalic_34); - tft.setTextColor(priCol, cardBg); - tft.setTextPadding(0); - tft.drawRightString(altStr.c_str(), c0X + 140, c0Y + 28, 1); - tft.unloadFont(); + drawDseg7RightString34(tft, altStr.c_str(), c0X + 140, c0Y + 28, c0Col, cardBg, 0, 240, isRainbowTheme()); // 3. CARD 1 (Top Right): RUMBO / BRÚJULA (x = 164, y = 19, w = 152, h = 102) int c1X = 164 + x_offset; int c1Y = 19; tft.fillRoundRect(c1X, c1Y, cW, cH1, 6, cardBg); tft.drawRoundRect(c1X, c1Y, cW, cH1, 6, cardBorder); - tft.fillRoundRect(c1X + 6, c1Y + 8, 4, cH1 - 16, 2, priCol); + tft.fillRoundRect(c1X + 6, c1Y + 8, 4, cH1 - 16, 2, c1Col); tft.setTextColor(0x8410, cardBg); tft.drawString("RUMBO / BRUJULA", c1X + 16, c1Y + 8, 2); bool hasCourse = synced && isGpsCourseValid(); String crsStr = hasCourse ? String((int)round(gpsCourseDeg)) : "---"; - tft.loadFont(DSEG7_Modern_BoldItalic_34); - tft.setTextColor(priCol, cardBg); - tft.setTextPadding(0); - tft.drawRightString(crsStr.c_str(), c1X + 130, c1Y + 28, 1); - tft.unloadFont(); + drawDseg7RightString34(tft, crsStr.c_str(), c1X + 130, c1Y + 28, c1Col, cardBg, 0, 240, isRainbowTheme()); if (hasCourse) { - tft.drawCircle(c1X + 137, c1Y + 33, 3, priCol); - tft.drawCircle(c1X + 137, c1Y + 33, 2, priCol); + tft.drawCircle(c1X + 137, c1Y + 33, 3, c1Col); + tft.drawCircle(c1X + 137, c1Y + 33, 2, c1Col); } else { tft.fillCircle(c1X + 137, c1Y + 33, 4, cardBg); } @@ -158,22 +156,18 @@ inline void drawGpsNavScreen(int x_offset = 0) { int cH2 = 110; tft.fillRoundRect(c2X, c2Y, cW, cH2, 6, cardBg); tft.drawRoundRect(c2X, c2Y, cW, cH2, 6, cardBorder); - tft.fillRoundRect(c2X + 6, c2Y + 8, 4, cH2 - 16, 2, priCol); + tft.fillRoundRect(c2X + 6, c2Y + 8, 4, cH2 - 16, 2, c2Col); tft.setTextColor(0x8410, cardBg); tft.drawString("SATELITES GPS", c2X + 16, c2Y + 8, 2); // Large Satellite Icon (animated body + waves) uint8_t waveState = getGpsWaveAnimState(); - drawLargeSatelliteIcon(c2X + 16, c2Y + 32, priCol, waveState, cardBg); + drawLargeSatelliteIcon(c2X + 16, c2Y + 32, c2Col, waveState, cardBg); - // Large Satellites Count (DSEG7 34pt) - String satStr = String(gpsSats); - tft.loadFont(DSEG7_Modern_BoldItalic_34); - tft.setTextColor(priCol, cardBg); - tft.setTextPadding(0); - tft.drawRightString(satStr.c_str(), c2X + 140, c2Y + 26, 1); - tft.unloadFont(); + // Large Satellites Count (DSEG7 34pt): Shows "---" while scanning/probing UART, "0" or count when locked + String satStr = isGpsUartLocked() ? String(gpsSats) : "---"; + drawDseg7RightString34(tft, satStr.c_str(), c2X + 140, c2Y + 26, c2Col, cardBg, 0, 240, isRainbowTheme()); // Telemetry details: HDOP precision & live diagnostic status String hdopStr = "HDOP: " + (synced ? String(gpsHdop, 1) : "---"); @@ -188,7 +182,7 @@ inline void drawGpsNavScreen(int x_offset = 0) { int c3Y = 125; tft.fillRoundRect(c3X, c3Y, cW, cH2, 6, cardBg); tft.drawRoundRect(c3X, c3Y, cW, cH2, 6, cardBorder); - tft.fillRoundRect(c3X + 6, c3Y + 8, 4, cH2 - 16, 2, priCol); + tft.fillRoundRect(c3X + 6, c3Y + 8, 4, cH2 - 16, 2, c3Col); tft.setTextColor(0x8410, cardBg); tft.drawString("COORDENADAS", c3X + 16, c3Y + 8, 2); @@ -196,13 +190,13 @@ inline void drawGpsNavScreen(int x_offset = 0) { tft.setTextColor(0x8410, cardBg); tft.drawString("LAT", c3X + 16, c3Y + 26, 1); String latStr = gpsLocationValid ? (String(gpsLatitude, 5) + "°") : "--.-----°"; - tft.setTextColor(priCol, cardBg); + tft.setTextColor(c3Col, cardBg); tft.drawString(latStr.c_str(), c3X + 16, c3Y + 38, 2); tft.setTextColor(0x8410, cardBg); tft.drawString("LON", c3X + 16, c3Y + 64, 1); String lonStr = gpsLocationValid ? (String(gpsLongitude, 5) + "°") : "--.-----°"; - tft.setTextColor(priCol, cardBg); + tft.setTextColor(c3Col, cardBg); tft.drawString(lonStr.c_str(), c3X + 16, c3Y + 76, 2); // Sync state trackers @@ -222,6 +216,11 @@ inline void updateGpsNavScreen(bool forceRedraw = false) { } uint16_t priCol = getThemePrimaryColor(); + uint16_t accCol = getThemeAccentColor(); + uint16_t c0Col = isRainbowTheme() ? (uint16_t)0x07FF : priCol; + uint16_t c1Col = isRainbowTheme() ? (uint16_t)0x07E0 : accCol; + uint16_t c2Col = isRainbowTheme() ? (uint16_t)0xFFE0 : priCol; + uint16_t c3Col = isRainbowTheme() ? (uint16_t)0xFD20 : accCol; uint16_t cardBg = 0x10A2; bool synced = isGpsSynced(); @@ -235,12 +234,7 @@ inline void updateGpsNavScreen(bool forceRedraw = false) { int c0X = 4; int c0Y = 19; String altStr = altValid ? String(curAltRound) : "---"; - tft.loadFont(DSEG7_Modern_BoldItalic_34); - tft.setTextColor(priCol, cardBg); - tft.setTextPadding(126); - tft.drawRightString(altStr.c_str(), c0X + 140, c0Y + 28, 1); - tft.setTextPadding(0); - tft.unloadFont(); + drawDseg7RightString34(tft, altStr.c_str(), c0X + 140, c0Y + 28, c0Col, cardBg, 126, 240, isRainbowTheme()); } // 2. Course / Heading changed: update Card 1 @@ -254,16 +248,11 @@ inline void updateGpsNavScreen(bool forceRedraw = false) { oldNavCourse = gpsCourseDeg; lastNavHasCourse = hasCourse; String crsStr = hasCourse ? String(curCrsRound) : "---"; - tft.loadFont(DSEG7_Modern_BoldItalic_34); - tft.setTextColor(priCol, cardBg); - tft.setTextPadding(85); - tft.drawRightString(crsStr.c_str(), c1X + 130, c1Y + 28, 1); - tft.setTextPadding(0); - tft.unloadFont(); + drawDseg7RightString34(tft, crsStr.c_str(), c1X + 130, c1Y + 28, c1Col, cardBg, 85, 240, isRainbowTheme()); if (hasCourse) { - tft.drawCircle(c1X + 137, c1Y + 33, 3, priCol); - tft.drawCircle(c1X + 137, c1Y + 33, 2, priCol); + tft.drawCircle(c1X + 137, c1Y + 33, 3, c1Col); + tft.drawCircle(c1X + 137, c1Y + 33, 2, c1Col); } else { tft.fillCircle(c1X + 137, c1Y + 33, 4, cardBg); } @@ -284,17 +273,14 @@ inline void updateGpsNavScreen(bool forceRedraw = false) { int c2Y = 125; // 3a. Satellites Count (DSEG7 34pt) - if (forceRedraw || gpsSats != lastNavCardSats || synced != lastNavSatsSync) { + bool uartLocked = isGpsUartLocked(); + if (forceRedraw || gpsSats != lastNavCardSats || synced != lastNavSatsSync || uartLocked != lastNavUartLocked) { lastNavCardSats = gpsSats; lastNavSatsSync = synced; + lastNavUartLocked = uartLocked; oldNavSats = gpsSats; - String satStr = String(gpsSats); - tft.loadFont(DSEG7_Modern_BoldItalic_34); - tft.setTextColor(priCol, cardBg); - tft.setTextPadding(85); - tft.drawRightString(satStr.c_str(), c2X + 140, c2Y + 26, 1); - tft.setTextPadding(0); - tft.unloadFont(); + String satStr = uartLocked ? String(gpsSats) : "---"; + drawDseg7RightString34(tft, satStr.c_str(), c2X + 140, c2Y + 26, c2Col, cardBg, 85, 240, isRainbowTheme()); } // 3b. HDOP precision @@ -316,7 +302,7 @@ inline void updateGpsNavScreen(bool forceRedraw = false) { lastNavCardDiag[sizeof(lastNavCardDiag) - 1] = '\0'; lastNavDiagSynced = synced; tft.setTextPadding(130); - tft.setTextColor(synced ? 0x9CD3 : priCol, cardBg); + tft.setTextColor(synced ? 0x9CD3 : c2Col, cardBg); tft.drawString(curDiag, c2X + 16, c2Y + 90, 1); tft.setTextPadding(0); } @@ -326,7 +312,7 @@ inline void updateGpsNavScreen(bool forceRedraw = false) { if (forceRedraw || curWaveState != oldNavWaveState || synced != oldNavSynced) { oldNavWaveState = curWaveState; oldNavSynced = synced; - drawLargeSatelliteIcon(c2X + 16, c2Y + 32, priCol, curWaveState, cardBg); + drawLargeSatelliteIcon(c2X + 16, c2Y + 32, c2Col, curWaveState, cardBg); } // 4. Coordinates changed: update Card 3 (setTextPadding avoids fillRect flicker) @@ -337,7 +323,7 @@ inline void updateGpsNavScreen(bool forceRedraw = false) { int c3Y = 125; String latStr = gpsLocationValid ? (String(gpsLatitude, 5) + "°") : "--.-----°"; tft.setTextPadding(130); - tft.setTextColor(priCol, cardBg); + tft.setTextColor(c3Col, cardBg); tft.drawString(latStr.c_str(), c3X + 16, c3Y + 38, 2); String lonStr = gpsLocationValid ? (String(gpsLongitude, 5) + "°") : "--.-----°"; @@ -349,16 +335,15 @@ inline void updateGpsNavScreen(bool forceRedraw = false) { // Render Page 3 into double-buffered sprite slice for slide / fade transitions inline void renderPage3ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int slice_h) { uint16_t priCol = getThemePrimaryColor(); + uint16_t accCol = getThemeAccentColor(); + uint16_t c0Col = isRainbowTheme() ? (uint16_t)0x07FF : priCol; + uint16_t c1Col = isRainbowTheme() ? (uint16_t)0x07E0 : accCol; + uint16_t c2Col = isRainbowTheme() ? (uint16_t)0xFFE0 : priCol; + uint16_t c3Col = isRainbowTheme() ? (uint16_t)0xFD20 : accCol; uint16_t cardBg = 0x10A2; uint16_t cardBorder = 0x2945; bool synced = isGpsSynced(); - // 1. TOP BAR in top slice - if (y_slice == 0) { - s.setTextColor(0x8410, TFT_BLACK); - s.drawString("TELEMETRIA GPS", 8 + x_offset, 5, 1); - } - // 2. CARD 0 (Top Left): ALTITUD (y = 19 to 121) int c0X = 4 + x_offset; int c0Y = 19 - y_slice; @@ -367,7 +352,7 @@ inline void renderPage3ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s if (c0X + cW > 0 && c0X < 320 && c0Y + cH1 > 0 && c0Y < slice_h) { s.fillRoundRect(c0X, c0Y, cW, cH1, 6, cardBg); s.drawRoundRect(c0X, c0Y, cW, cH1, 6, cardBorder); - s.fillRoundRect(c0X + 6, c0Y + 8, 4, cH1 - 16, 2, priCol); + s.fillRoundRect(c0X + 6, c0Y + 8, 4, cH1 - 16, 2, c0Col); if (c0Y + 24 > 0 && c0Y + 8 < slice_h) { s.setTextColor(0x8410, cardBg); @@ -379,11 +364,7 @@ inline void renderPage3ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s String altStr = altValid ? String((int)round(gpsAltitudeM)) : "---"; int valY = c0Y + 28; if (valY < slice_h && valY + 38 > 0) { - s.fontLoaded = true; - s.setTextColor(priCol, cardBg); - s.setTextPadding(0); - s.drawRightString(altStr.c_str(), c0X + 140, valY, 1); - s.fontLoaded = false; + drawDseg7RightString34(s, altStr.c_str(), c0X + 140, valY, c0Col, cardBg, 0, slice_h, isRainbowTheme()); } } @@ -393,7 +374,7 @@ inline void renderPage3ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s if (c1X + cW > 0 && c1X < 320 && c1Y + cH1 > 0 && c1Y < slice_h) { s.fillRoundRect(c1X, c1Y, cW, cH1, 6, cardBg); s.drawRoundRect(c1X, c1Y, cW, cH1, 6, cardBorder); - s.fillRoundRect(c1X + 6, c1Y + 8, 4, cH1 - 16, 2, priCol); + s.fillRoundRect(c1X + 6, c1Y + 8, 4, cH1 - 16, 2, c1Col); if (c1Y + 24 > 0 && c1Y + 8 < slice_h) { s.setTextColor(0x8410, cardBg); @@ -403,16 +384,12 @@ inline void renderPage3ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s String crsStr = hasCourse ? String((int)round(gpsCourseDeg)) : "---"; int valY = c1Y + 28; if (valY < slice_h && valY + 38 > 0) { - s.fontLoaded = true; - s.setTextColor(priCol, cardBg); - s.setTextPadding(0); - s.drawRightString(crsStr.c_str(), c1X + 130, valY, 1); - s.fontLoaded = false; + drawDseg7RightString34(s, crsStr.c_str(), c1X + 130, valY, c1Col, cardBg, 0, slice_h, isRainbowTheme()); if (hasCourse) { int circY = c1Y + 33; if (circY + 4 > 0 && circY - 4 < slice_h) { - s.drawCircle(c1X + 137, circY, 3, priCol); - s.drawCircle(c1X + 137, circY, 2, priCol); + s.drawCircle(c1X + 137, circY, 3, c1Col); + s.drawCircle(c1X + 137, circY, 2, c1Col); } } } @@ -431,7 +408,7 @@ inline void renderPage3ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s if (c2X + cW > 0 && c2X < 320 && c2Y + cH2 > 0 && c2Y < slice_h) { s.fillRoundRect(c2X, c2Y, cW, cH2, 6, cardBg); s.drawRoundRect(c2X, c2Y, cW, cH2, 6, cardBorder); - s.fillRoundRect(c2X + 6, c2Y + 8, 4, cH2 - 16, 2, priCol); + s.fillRoundRect(c2X + 6, c2Y + 8, 4, cH2 - 16, 2, c2Col); if (c2Y + 24 > 0 && c2Y + 8 < slice_h) { s.setTextColor(0x8410, cardBg); @@ -440,16 +417,12 @@ inline void renderPage3ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s uint8_t waveState = getGpsWaveAnimState(); int icnY = c2Y + 32; if (icnY + 28 > 0 && icnY < slice_h) { - drawLargeSatelliteIconSprite(s, c2X + 16, icnY, priCol, waveState, cardBg); + drawLargeSatelliteIconSprite(s, c2X + 16, icnY, c2Col, waveState, cardBg); } int satY = c2Y + 26; if (satY < slice_h && satY + 38 > 0) { - String satStr = String(gpsSats); - s.fontLoaded = true; - s.setTextColor(priCol, cardBg); - s.setTextPadding(0); - s.drawRightString(satStr.c_str(), c2X + 140, satY, 1); - s.fontLoaded = false; + String satStr = isGpsUartLocked() ? String(gpsSats) : "---"; + drawDseg7RightString34(s, satStr.c_str(), c2X + 140, satY, c2Col, cardBg, 0, slice_h, isRainbowTheme()); } int hdopY = c2Y + 68; if (hdopY < slice_h && hdopY + 16 > 0) { @@ -459,7 +432,7 @@ inline void renderPage3ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s } int qualY = c2Y + 90; if (qualY + 10 > 0 && qualY < slice_h) { - s.setTextColor(synced ? 0x9CD3 : priCol, cardBg); + s.setTextColor(synced ? 0x9CD3 : c2Col, cardBg); s.drawString(getGpsDiagMessage(), c2X + 16, qualY, 1); } } @@ -470,7 +443,7 @@ inline void renderPage3ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s if (c3X + cW > 0 && c3X < 320 && c3Y + cH2 > 0 && c3Y < slice_h) { s.fillRoundRect(c3X, c3Y, cW, cH2, 6, cardBg); s.drawRoundRect(c3X, c3Y, cW, cH2, 6, cardBorder); - s.fillRoundRect(c3X + 6, c3Y + 8, 4, cH2 - 16, 2, priCol); + s.fillRoundRect(c3X + 6, c3Y + 8, 4, cH2 - 16, 2, c3Col); if (c3Y + 24 > 0 && c3Y + 8 < slice_h) { s.setTextColor(0x8410, cardBg); @@ -484,7 +457,7 @@ inline void renderPage3ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s int latValY = c3Y + 38; if (latValY < slice_h && latValY + 18 > 0) { String latStr = gpsLocationValid ? (String(gpsLatitude, 5) + "°") : "--.-----°"; - s.setTextColor(priCol, cardBg); + s.setTextColor(c3Col, cardBg); s.drawString(latStr.c_str(), c3X + 16, latValY, 2); } int lonHdrY = c3Y + 64; @@ -495,7 +468,7 @@ inline void renderPage3ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s int lonValY = c3Y + 76; if (lonValY < slice_h && lonValY + 18 > 0) { String lonStr = gpsLocationValid ? (String(gpsLongitude, 5) + "°") : "--.-----°"; - s.setTextColor(priCol, cardBg); + s.setTextColor(c3Col, cardBg); s.drawString(lonStr.c_str(), c3X + 16, lonValY, 2); } } diff --git a/sketch/obd2_gauge/meter.h b/sketch/obd2_gauge/meter.h index 36582c2..12e1a42 100644 --- a/sketch/obd2_gauge/meter.h +++ b/sketch/obd2_gauge/meter.h @@ -4,9 +4,12 @@ #include "DSEG7_Modern_BoldItalic_44.h" #include "DSEG7_Modern_BoldItalic_80.h" #include "Latin_Sans_Bold_26.h" +#include "rainbow_text.h" extern TFT_eSprite sprTrans; extern bool sprTransAllocated; +extern TFT_eSprite sprFadeB; +extern bool sprFadeBAllocated; const uint16_t screenWidth = tft.height(); // landscape const uint16_t screenHeight = tft.width(); @@ -404,6 +407,8 @@ const float blockC[20][4] PROGMEM = { // Draw an individual solid rectangular block with smooth subpixel anti-aliasing inline void drawBarBlock(uint8_t i, uint16_t color) { + if (isRainbowTheme() && color != TFT_RED && color != METER_INACTIVE_COLOR && color != TFT_BLACK) + color = getRainbowBlockColor(i, NUM_BAR_BLOCKS); int16_t minX = pgm_read_word(&blockMinX[i]); int16_t maxX = pgm_read_word(&blockMaxX[i]); int16_t minY = pgm_read_word(&blockMinY[i]); @@ -531,6 +536,9 @@ const float quadBlockC[20][4] PROGMEM = { // Draw an individual solid rectangular block for 4-quadrant view with smooth // subpixel anti-aliasing inline void drawQuadBarBlock(uint8_t cell, uint8_t i, uint16_t color) { + // Rainbow theme: active blocks become a spectral gradient + if (isRainbowTheme() && color != TFT_RED && color != METER_INACTIVE_COLOR && color != TFT_BLACK) + color = getRainbowBlockColor(i); int16_t ox = cell4_x[cell]; int16_t oy = cell4_y[cell]; int16_t minX = pgm_read_word(&quadBlockMinX[i]); @@ -1174,13 +1182,9 @@ void arcMeter4(uint8_t cell, uint8_t pid, bool redrawHeader = true) { old_data[cell] = -999; plotArc4(cell, last_valid_data[cell], warn, min, max, digit, unit); } else { - // Value placeholder using DSEG7 Modern 34pt smooth font - tft.loadFont(DSEG7_Modern_BoldItalic_34); - tft.setTextColor(getThemePrimaryColor(), TFT_BLACK); - tft.setTextPadding(114); - tft.drawRightString("---", x + 152, y + 74, 1); - tft.setTextPadding(0); - tft.unloadFont(); + // Value placeholder using DSEG7 Modern 34pt smooth font with rainbow gradient + bool rainbowTxt = isRainbowTheme(); + drawDseg7RightString34(tft, "---", x + 152, y + 74, getThemeCellColor(cell), TFT_BLACK, 114, 240, rainbowTxt); old_data[cell] = -999; } if (cell == 1 && isGpsSpeedValid()) { @@ -1257,14 +1261,10 @@ void plotArc4(uint8_t cell, float data, int warn, int min, int max, bool digit, } } - uint16_t txtCol = getThemePrimaryColor(); + uint16_t txtCol = (col == TFT_RED) ? TFT_RED : getThemeCellColor(cell); + bool rainbowTxt = isRainbowTheme() && (col != TFT_RED); - tft.loadFont(DSEG7_Modern_BoldItalic_34); - tft.setTextColor(txtCol, TFT_BLACK); - tft.setTextPadding(114); - tft.drawRightString(numStr.c_str(), x + 152, y + 74, 1); - tft.setTextPadding(0); - tft.unloadFont(); + drawDseg7RightString34(tft, numStr.c_str(), x + 152, y + 74, txtCol, TFT_BLACK, 114, 240, rainbowTxt); old_data[cell] = data; // Draw GPS Satellite Icon & indicator in top-right corner of cell 1 @@ -1320,12 +1320,8 @@ void fullScreenMeter(uint8_t cell, uint8_t pid, bool redrawHeader = true) { drawBarBlock(i, METER_INACTIVE_COLOR); } - tft.loadFont(DSEG7_Modern_BoldItalic_80); - tft.setTextColor(getThemePrimaryColor(), TFT_BLACK); - tft.setTextPadding(240); - tft.drawRightString("---", 312, 148, 1); - tft.setTextPadding(0); - tft.unloadFont(); + bool rainbowTxt = isRainbowTheme(); + drawDseg7RightString80(tft, "---", 312, 148, getThemeCellColor(cell), TFT_BLACK, 240, 240, rainbowTxt); old_data[cell] = -999; } } @@ -1399,15 +1395,11 @@ void plotFullScreen(uint8_t cell, float data, int warn, int min, int max, } } - uint16_t txtCol = getThemePrimaryColor(); + uint16_t txtCol = (col == TFT_RED) ? TFT_RED : getThemeCellColor(cell); + bool rainbowTxt = isRainbowTheme() && (col != TFT_RED); - // Value bottom-right using DSEG7 Modern smooth font - tft.loadFont(DSEG7_Modern_BoldItalic_80); - tft.setTextColor(txtCol, TFT_BLACK); - tft.setTextPadding(240); - tft.drawRightString(numStr.c_str(), 312, 148, 1); - tft.setTextPadding(0); - tft.unloadFont(); + // Value bottom-right using DSEG7 Modern smooth font with rainbow gradient + drawDseg7RightString80(tft, numStr.c_str(), 312, 148, txtCol, TFT_BLACK, 240, 240, rainbowTxt); old_data[cell] = data; if (cell == 1 && isGpsSpeedValid()) { @@ -1442,7 +1434,7 @@ void plotNoData(uint8_t cell) { drawBarBlock(i, METER_INACTIVE_COLOR); } tft.loadFont(DSEG7_Modern_BoldItalic_80); - tft.setTextColor(getThemePrimaryColor(), TFT_BLACK); + tft.setTextColor(getThemeCellColor(cell), TFT_BLACK); tft.setTextPadding(240); tft.drawRightString("---", 312, 148, 1); tft.setTextPadding(0); @@ -1457,7 +1449,7 @@ void plotNoData(uint8_t cell) { drawQuadBarBlock(cell, i, METER_INACTIVE_COLOR); } tft.loadFont(DSEG7_Modern_BoldItalic_34); - tft.setTextColor(getThemePrimaryColor(), TFT_BLACK); + tft.setTextColor(getThemeCellColor(cell), TFT_BLACK); tft.setTextPadding(114); tft.drawRightString("---", x + 152, y + 74, 1); tft.setTextPadding(0); @@ -1529,6 +1521,8 @@ inline void drawPidIconToSprite(TFT_eSprite &s, int ix, int iy, // anti-aliasing inline void drawQuadBarBlockToSprite(TFT_eSprite &s, int16_t qx, int16_t qy, uint8_t i, uint16_t color, int slice_h) { + if (isRainbowTheme() && color != TFT_RED && color != METER_INACTIVE_COLOR && color != TFT_BLACK) + color = getRainbowBlockColor(i); int16_t minX = pgm_read_word(&quadBlockMinX[i]); int16_t maxX = pgm_read_word(&quadBlockMaxX[i]); int16_t minY = pgm_read_word(&quadBlockMinY[i]); @@ -1640,21 +1634,18 @@ inline void renderPage0ToSprite(TFT_eSprite &s, int x_offset, int y_slice, drawQuadBarBlockToSprite(s, qx, qy, i, bCol, slice_h); } - // 4. Digital value string using authentic DSEG7 34pt font (pixel-perfect - // match with resting screen) + // 4. Digital value string using authentic DSEG7 34pt font with rainbow gradient String numStr = (val <= -900.0f) ? "---" : String(val, digit); + uint16_t txtCol = (activeCol == TFT_RED) ? TFT_RED : getThemeCellColor(c); + bool rainbowTxt = isRainbowTheme() && (activeCol != TFT_RED); int textY = qy + 74; if (textY < slice_h && textY + 40 > 0) { - s.fontLoaded = true; - s.setTextColor(priCol, TFT_BLACK); - s.setTextPadding(0); - s.drawRightString(numStr.c_str(), qx + 152, textY, 1); - s.fontLoaded = false; + drawDseg7RightString34(s, numStr.c_str(), qx + 152, textY, txtCol, TFT_BLACK, 0, slice_h, rainbowTxt); } // GPS Satellite icon in top right of Quadrant 1 (Velocidad: cell 1) if (c == 1 && isGpsSpeedValid() && y_slice == 0) { - uint16_t waveCol = priCol; + uint16_t waveCol = isRainbowTheme() ? (uint16_t)0x07E0 : priCol; uint16_t bodyCol = TFT_WHITE; uint16_t bgCol = themeGradQuad[3]; drawGpsSatelliteIcon(s, qx + 136, 3, bodyCol, waveCol, bgCol, @@ -1694,8 +1685,8 @@ inline void renderPage0ToSprite(TFT_eSprite &s, int x_offset, int y_slice, inline void renderPage1ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int slice_h) { for (uint8_t cardIdx = 0; cardIdx < 3; cardIdx++) { - int cardY = (6 + cardIdx * 77) - y_slice; - int cardH = 73; + int cardY = (19 + cardIdx * 74) - y_slice; + int cardH = 68; int cardX = 4 + x_offset; int cardW = 312; @@ -1727,6 +1718,8 @@ inline void renderPage1ToSprite(TFT_eSprite &s, int x_offset, int y_slice, accentCol = TFT_RED; else if (val >= 100.0f) accentCol = TFT_YELLOW; + else + accentCol = isRainbowTheme() ? (uint16_t)0x07FF : getThemePrimaryColor(); } else if (cardIdx == 1) { titleStr = "INTERIOR"; subStr = (dsSensorCount > 0) ? "DS18B20 SENSOR 1 (GPIO 22)" @@ -1734,7 +1727,7 @@ inline void renderPage1ToSprite(TFT_eSprite &s, int x_offset, int y_slice, val = tempInterior; minRange = 10.0f; maxRange = 40.0f; - accentCol = 0x07F3; + accentCol = isRainbowTheme() ? (uint16_t)0x07E0 : getThemeAccentColor(); } else { titleStr = "EXTERIOR"; subStr = (dsSensorCount > 1) ? "DS18B20 SENSOR 2 (GPIO 22)" @@ -1743,26 +1736,26 @@ inline void renderPage1ToSprite(TFT_eSprite &s, int x_offset, int y_slice, val = tempExterior; minRange = -10.0f; maxRange = 50.0f; - accentCol = 0x3D7F; + accentCol = isRainbowTheme() ? (uint16_t)0xFD20 : getThemePrimaryColor(); } - s.fillRoundRect(cardX + 6, cardY + 8, 4, cardH - 16, 2, accentCol); + s.fillRoundRect(cardX + 6, cardY + 6, 4, cardH - 12, 2, accentCol); // Smooth anti-aliased title in transition sprite (zero mallocs, 100% // smooth) if (!sprTriTitleReady) initTriTempTitles(); if (sprTriTitleReady && sprTriTitle[cardIdx].created()) { - sprTriTitle[cardIdx].pushToSprite(&s, cardX + 16, cardY + 6); + sprTriTitle[cardIdx].pushToSprite(&s, cardX + 16, cardY + 5); } else { s.setTextColor(TFT_WHITE, cardBg); - s.drawString(titleStr, cardX + 16, cardY + 6, 4); + s.drawString(titleStr, cardX + 16, cardY + 5, 4); } s.setTextColor(0x8410, cardBg); - s.drawString(subStr, cardX + 16, cardY + 34, 1); + s.drawString(subStr, cardX + 16, cardY + 32, 1); // Progress bar int barX = cardX + 16; - int barY = cardY + 50; + int barY = cardY + 48; int barW = 120; s.fillRoundRect(barX, barY, barW, 8, 3, 0x18C3); float ratio = (val - minRange) / (maxRange - minRange); @@ -1775,22 +1768,18 @@ inline void renderPage1ToSprite(TFT_eSprite &s, int x_offset, int y_slice, fillW = 4; s.fillRoundRect(barX, barY, fillW, 8, 3, accentCol); - // Value text using authentic DSEG7 34pt font matching drawTriTempCard + // Value text using authentic DSEG7 34pt font with rainbow gradient String numStr = (val <= -900.0f) ? "---" : (isInt ? String((int)val) : String(val, 1)); - int numY = cardY + 22; + bool rainbowTxt = isRainbowTheme() && (accentCol != TFT_RED); + int numY = cardY + 20; if (numY < slice_h && numY + 40 > 0) { - s.fontLoaded = true; - s.setTextColor(accentCol, cardBg); - s.setTextPadding(0); - s.drawRightString(numStr.c_str(), cardX + 262, numY, 1); - s.fontLoaded = false; + drawDseg7RightString34(s, numStr.c_str(), cardX + 262, numY, accentCol, cardBg, 0, slice_h, rainbowTxt); } - int unitY = cardY + 32; - if (unitY < slice_h && unitY + 16 > 0) { - s.setTextColor(0xC618, cardBg); - s.drawString("`C", cardX + 268, unitY, 2); + int unitY = cardY + 22; + if (unitY < slice_h && unitY + 26 > 0) { + draw7SegDegreeC(s, cardX + 268, unitY, 0xC618, cardBg); } } } @@ -1935,22 +1924,11 @@ inline void animateFadeTransition(int8_t fromPage = -1, int8_t toPage = -1) { const int slice_h = 40; const uint8_t numSlices = 240 / slice_h; // exactly 6 slices - if (!sprTransAllocated) { + if (!sprTransAllocated || !sprFadeBAllocated) { initScreen(true); return; } - TFT_eSprite sprB(&tft); - if (sprB.createSprite(320, slice_h) == nullptr) { - animateSlideTransition(true, fromPage, toPage); - return; - } - - sprB.setTextFont(1); - sprB.setFreeFont(NULL); - sprB.loadFont(DSEG7_Modern_BoldItalic_34); - sprB.fontLoaded = false; - const uint8_t FRAMES = 10; // Perceptual crossfade steps (0 to 32) const uint8_t alpha_steps[FRAMES] = {3, 6, 10, 14, 18, 22, 26, 29, 31, 32}; @@ -1963,7 +1941,7 @@ inline void animateFadeTransition(int8_t fromPage = -1, int8_t toPage = -1) { int y_slice = s * slice_h; sprTrans.fillSprite(TFT_BLACK); - sprB.fillSprite(TFT_BLACK); + sprFadeB.fillSprite(TFT_BLACK); if (fromPage == 0) renderPage0ToSprite(sprTrans, 0, y_slice, slice_h); @@ -1975,16 +1953,16 @@ inline void animateFadeTransition(int8_t fromPage = -1, int8_t toPage = -1) { renderPage3ToSprite(sprTrans, 0, y_slice, slice_h); if (toPage == 0) - renderPage0ToSprite(sprB, 0, y_slice, slice_h); + renderPage0ToSprite(sprFadeB, 0, y_slice, slice_h); else if (toPage == 1) - renderPage1ToSprite(sprB, 0, y_slice, slice_h); + renderPage1ToSprite(sprFadeB, 0, y_slice, slice_h); else if (toPage == 2) - renderPage2ToSprite(sprB, 0, y_slice, slice_h); + renderPage2ToSprite(sprFadeB, 0, y_slice, slice_h); else - renderPage3ToSprite(sprB, 0, y_slice, slice_h); + renderPage3ToSprite(sprFadeB, 0, y_slice, slice_h); uint16_t *pA = (uint16_t *)sprTrans.getPointer(); - uint16_t *pB = (uint16_t *)sprB.getPointer(); + uint16_t *pB = (uint16_t *)sprFadeB.getPointer(); if (pA && pB) { for (int i = 0; i < totalPixels; i++) { @@ -2032,9 +2010,7 @@ inline void animateFadeTransition(int8_t fromPage = -1, int8_t toPage = -1) { delay(10); } - sprB.fontLoaded = true; - sprB.unloadFont(); - sprB.deleteSprite(); + sprFadeB.fontLoaded = false; sprTrans.fontLoaded = false; } @@ -2720,7 +2696,7 @@ void handleArcAlarmFlash() { drawBarBlock(i, METER_INACTIVE_COLOR); } tft.loadFont(DSEG7_Modern_BoldItalic_80); - tft.setTextColor(getThemePrimaryColor(), TFT_BLACK); + tft.setTextColor(getThemeCellColor(cell), TFT_BLACK); tft.setTextPadding(240); tft.drawRightString(numStr.c_str(), 312, 148, 1); tft.setTextPadding(0); @@ -2787,7 +2763,7 @@ void handleArcAlarmFlash() { drawQuadBarBlock(cell, i, METER_INACTIVE_COLOR); } tft.loadFont(DSEG7_Modern_BoldItalic_34); - tft.setTextColor(getThemePrimaryColor(), TFT_BLACK); + tft.setTextColor(getThemeCellColor(cell), TFT_BLACK); tft.setTextPadding(114); tft.drawRightString(numStr.c_str(), x + 152, y + 74, 1); tft.setTextPadding(0); diff --git a/sketch/obd2_gauge/obd2_gauge.ino b/sketch/obd2_gauge/obd2_gauge.ino index 130cc47..794190a 100644 --- a/sketch/obd2_gauge/obd2_gauge.ino +++ b/sketch/obd2_gauge/obd2_gauge.ino @@ -51,6 +51,8 @@ Preferences pref; // create preference TFT_eSPI tft = TFT_eSPI(); TFT_eSprite sprTrans(&tft); bool sprTransAllocated = false; +TFT_eSprite sprFadeB(&tft); +bool sprFadeBAllocated = false; #define TFT_GREY 0x5AEB // ELM327 PIDs Configuration @@ -129,8 +131,8 @@ const GaugeTheme GAUGE_THEMES[6] = { 180, 255}, // 0: Vivid Electric Cyan {"Verde Sport", "Cockpit Clasico", 0x07E0, 0x0446, 0x0283, 0x0141, 0x0205, 0, 255, 30}, // 1: Vivid Emerald Green - {"Ambar BMW", "Nocturno M-Power", 0xFDE0, 0x93C0, 0x5240, 0x2120, 0x41E0, - 255, 120, 0}, // 2: Vivid M-Power Glowing Amber + {"Arcoiris", "Espectro RGB", 0x07FF, 0x07E0, 0x2965, 0x08E7, 0x08A8, + 0, 200, 255}, // 2: Rainbow spectrum (RGB multi-hue blocks + dynamic spectrum) {"Blanco Puro", "OLED Precision", 0xFFFF, 0x6B6D, 0x4228, 0x18E3, 0x2965, 220, 230, 255}, // 3: Pure Crisp White {"Naranja Sport", "McLaren Papaya", 0xFD20, 0x9260, 0x5160, 0x20A0, 0x4140, @@ -141,7 +143,7 @@ const GaugeTheme GAUGE_THEMES[6] = { uint8_t themeIndex = 0; // Default 0: Azul Cian uint8_t transitionType = - 0; // 0: Deslizamiento (Slide), 1: Desvanecimiento (Fade) + 1; // Default 1: Desvanecimiento (Fade), 0: Deslizamiento (Slide) inline uint16_t getThemePrimaryColor() { if (themeIndex >= 6) @@ -173,33 +175,100 @@ inline uint16_t getThemeBgColor() { return GAUGE_THEMES[themeIndex].bgColor; } +// Rainbow theme helpers (theme index 2) +inline bool isRainbowTheme() { return themeIndex == 2; } + +// Convert hue (0..359, full saturation/value) to RGB565 +inline uint16_t hueToRgb565(uint16_t hue) { + hue %= 360; + uint8_t region = hue / 60; + uint8_t rem = (uint8_t)(((hue % 60) * 255) / 60); + uint8_t r, g, b; + switch (region) { + case 0: r = 255; g = rem; b = 0; break; + case 1: r = 255 - rem; g = 255; b = 0; break; + case 2: r = 0; g = 255; b = rem; break; + case 3: r = 0; g = 255 - rem; b = 255; break; + case 4: r = rem; g = 0; b = 255; break; + default: r = 255; g = 0; b = 255 - rem; break; + } + return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); +} + +// Calibrated 20-block spectral rainbow: Electric Sky Blue -> Cyan -> Green -> Lime -> Yellow -> Orange -> Vivid Red +const uint16_t RAINBOW_SPECTRUM_20[20] PROGMEM = { + 0x03FF, // 0: Azul Cielo Eléctrico (Vivid Electric Sky Blue) + 0x04DF, // 1: Azul Azure Brillante (Bright Azure Blue) + 0x05BF, // 2: Azul Océano (Deep Cyan-Blue) + 0x07FF, // 3: Cian Eléctrico Puro (Pure Electric Cyan) + 0x07F6, // 4: Aguamarina Vibrante (Aquamarine) + 0x07ED, // 5: Turquesa Menta (Mint Turquoise) + 0x07E6, // 6: Verde Mar Brillante (Bright Sea Green) + 0x07E0, // 7: Verde Esmeralda Vivo (Vivid Emerald Green) + 0x37E0, // 8: Verde Primavera (Bright Spring Green) + 0x7FE0, // 9: Verde Lima Neón (Neon Lime Green) + 0xAFE0, // 10: Chartreuse Intenso (Vivid Chartreuse) + 0xDFE0, // 11: Amarillo-Verdoso Eléctrico (Electric Yellow-Green) + 0xFFE0, // 12: Amarillo Puro Brillante (Pure Brilliant Yellow) + 0xFEE0, // 13: Amarillo Oro Cálido (Warm Golden Yellow) + 0xFD80, // 14: Ámbar Radiante (Bright Amber) + 0xFD00, // 15: Naranja Papaya Vivo (Vivid Papaya Orange) + 0xFC60, // 16: Naranja Coral Profundo (Deep Coral Orange) + 0xFA40, // 17: Rojo-Naranja Fuego (Flame Red-Orange) + 0xF880, // 18: Carmesí Intenso (Bright Crimson) + 0xF800 // 19: Rojo Puro Brillante (Pure Vivid Red) +}; + +// Spectral color for gauge block i of n: Electric Blue (low) -> Cyan -> Green -> Yellow -> Orange -> Red (high). +inline uint16_t getRainbowBlockColor(uint8_t i, uint8_t n = 20) { + if (n == 20 && i < 20) { + return pgm_read_word(&RAINBOW_SPECTRUM_20[i]); + } + if (n < 2) + n = 2; + if (i >= n) + i = n - 1; + uint8_t idx = (uint8_t)(((uint32_t)i * 19) / (n - 1)); + if (idx > 19) + idx = 19; + return pgm_read_word(&RAINBOW_SPECTRUM_20[idx]); +} + +// Distinct multi-color identity for each quadrant in Rainbow theme +inline uint16_t getRainbowCellColor(uint8_t cell) { + switch (cell) { + case 0: return 0x07FF; // Cuadrante 0 (ECT Refrigerante): Cian Eléctrico + case 1: return 0x07E0; // Cuadrante 1 (Velocidad): Verde Esmeralda + case 2: return 0xFD20; // Cuadrante 2 (RPM): Naranja Papaya + case 3: return 0xFFE0; // Cuadrante 3 (Voltaje): Amarillo Puro + default: return 0x07FF; + } +} + +inline uint16_t getThemeCellColor(uint8_t cell) { + if (isRainbowTheme()) { + return getRainbowCellColor(cell); + } + return getThemePrimaryColor(); +} + // Control functions for the Rear RGB LED (Active-LOW, Common Anode) +// Permanentemente apagado a petición del usuario (sin luces traseras en Bluetooth, temas ni boot) void setRearLedColor(uint8_t r, uint8_t g, uint8_t b) { - // Ensure pins are attached to LEDC - ledcAttachPin(LED_RED_PIN, LED_RED_CH); - ledcAttachPin(LED_GREEN_PIN, LED_GREEN_CH); - ledcAttachPin(LED_BLUE_PIN, LED_BLUE_CH); - // Duty cycle for 8-bit (0..255): 255 = 3.3V (OFF), 0 = 0V (FULL ON) - ledcWrite(LED_RED_CH, 255 - r); - ledcWrite(LED_GREEN_CH, 255 - g); - ledcWrite(LED_BLUE_CH, 255 - b); + setRearLedOff(); } void setRearLedOff() { - ledcWrite(LED_RED_CH, 255); - ledcWrite(LED_GREEN_CH, 255); - ledcWrite(LED_BLUE_CH, 255); - // Drive pins directly HIGH as well for guaranteed zero leakage during sleep + pinMode(LED_RED_PIN, OUTPUT); + pinMode(LED_GREEN_PIN, OUTPUT); + pinMode(LED_BLUE_PIN, OUTPUT); digitalWrite(LED_RED_PIN, HIGH); digitalWrite(LED_GREEN_PIN, HIGH); digitalWrite(LED_BLUE_PIN, HIGH); } void updateRearLedTheme() { - if (themeIndex >= 6) - return; - setRearLedColor(GAUGE_THEMES[themeIndex].ledR, GAUGE_THEMES[themeIndex].ledG, - GAUGE_THEMES[themeIndex].ledB); + setRearLedOff(); } /* @@ -517,14 +586,13 @@ void setup() { pinMode(LDR_PIN, ANALOG); // ldr analog input read brightness pinMode(BUZZER_PIN, OUTPUT); // speaker pinMode(SELECTOR_PIN, INPUT_PULLUP); // button - // Rear RGB LED PWM setup (Channels 4, 5, 6 - Timers 2 & 3) - ledcSetup(LED_RED_CH, 5000, 8); - ledcSetup(LED_GREEN_CH, 5000, 8); - ledcSetup(LED_BLUE_CH, 5000, 8); - ledcAttachPin(LED_RED_PIN, LED_RED_CH); - ledcAttachPin(LED_GREEN_PIN, LED_GREEN_CH); - ledcAttachPin(LED_BLUE_PIN, LED_BLUE_CH); - setRearLedOff(); + // Rear RGB LED: permanentemente apagado (Active-LOW, pull HIGH) + pinMode(LED_RED_PIN, OUTPUT); + pinMode(LED_GREEN_PIN, OUTPUT); + pinMode(LED_BLUE_PIN, OUTPUT); + digitalWrite(LED_RED_PIN, HIGH); + digitalWrite(LED_GREEN_PIN, HIGH); + digitalWrite(LED_BLUE_PIN, HIGH); // pwm setup ledcSetup(buzzerChannel, 1500, 10); // buzzer 10 bit ledcSetup(backlightChannel, 12000, 8); // backlight 8 bit @@ -627,9 +695,9 @@ recent_client_addr : {0x00,0x00,0x00,0x00,0x00,0x00} array of bytes[6] themeIndex = pref.getUChar("theme_idx", 0); if (themeIndex > 5) themeIndex = 0; - transitionType = pref.getUChar("trans_type", 0); + transitionType = pref.getUChar("trans_type", 1); if (transitionType > 1) - transitionType = 0; + transitionType = 1; currentScreenPage = pref.getChar("scr_page", 0); if (currentScreenPage < 0 || currentScreenPage > 3) currentScreenPage = 0; @@ -649,14 +717,23 @@ recent_client_addr : {0x00,0x00,0x00,0x00,0x00,0x00} array of bytes[6] String(transitionType == 0 ? "Deslizamiento" : "Desvanecimiento")); delay(40); - // Initialize persistent transition sprite (pre-allocated once at boot in clean heap) + // Initialize persistent transition sprites (pre-allocated once at boot in clean heap) if (sprTrans.createSprite(320, 40) != nullptr) { sprTransAllocated = true; sprTrans.setTextFont(1); sprTrans.setFreeFont(NULL); sprTrans.loadFont(DSEG7_Modern_BoldItalic_34); sprTrans.fontLoaded = false; - Terminal("[ OK ] UI: Persistent 25KB Transition Engine Online"); + } + if (sprFadeB.createSprite(320, 40) != nullptr) { + sprFadeBAllocated = true; + sprFadeB.setTextFont(1); + sprFadeB.setFreeFont(NULL); + sprFadeB.loadFont(DSEG7_Modern_BoldItalic_34); + sprFadeB.fontLoaded = false; + } + if (sprTransAllocated && sprFadeBAllocated) { + Terminal("[ OK ] UI: Persistent 50KB Dual Transition Buffers Online"); } else { Terminal("[ WARN ] UI: Transition buffer alloc failed"); } @@ -664,6 +741,7 @@ recent_client_addr : {0x00,0x00,0x00,0x00,0x00,0x00} array of bytes[6] // Initialize 1-Wire DS18B20 digital temperature bus initTriTemp(); + initGpsClockTitles(); if (dsSensorCount > 0) { Terminal("[ OK ] 1-Wire: " + String(dsSensorCount) + "x DS18B20 Temp Sensors on GPIO 22"); @@ -720,6 +798,8 @@ void loop() { handleArcAlarmFlash(); // non-blocking arc alarm flash handler (blinks when in // TFT_RED) + + // Trigger automatico: Cuando el Bluetooth conecta en segundo plano (Core 0), // pasar a pantalla OBD! if (btConnectedTrigger) { diff --git a/sketch/obd2_gauge/rainbow_text.h b/sketch/obd2_gauge/rainbow_text.h new file mode 100644 index 0000000..a72e04f Binary files /dev/null and b/sketch/obd2_gauge/rainbow_text.h differ diff --git a/sketch/obd2_gauge/tri_temp.h b/sketch/obd2_gauge/tri_temp.h index dc37e28..9f05cd1 100644 --- a/sketch/obd2_gauge/tri_temp.h +++ b/sketch/obd2_gauge/tri_temp.h @@ -6,13 +6,16 @@ #include #include "DSEG7_Modern_BoldItalic_34.h" #include "Latin_Sans_Bold_26.h" +#include "rainbow_text.h" // Hardware 1-Wire Pin: Conector CN1 Pin 3 (GPIO 22) #define ONE_WIRE_BUS_PIN 22 extern TFT_eSPI tft; extern uint16_t getThemePrimaryColor(); +extern uint16_t getThemeAccentColor(); extern uint16_t getThemeBgColor(); +extern bool isRainbowTheme(); extern void touchBeep(); extern String warningValue[5]; @@ -57,6 +60,9 @@ inline void initTriTempTitles() { sprTriTitle[i].setTextColor(TFT_WHITE, cardBg); sprTriTitle[i].drawString(titles[i], 0, 0, 1); sprTriTitle[i].unloadFont(); + if (isRainbowTheme()) { + applyRainbowToSprite(sprTriTitle[i], 4 + 16, 19 + i * 74 + 5, cardBg); + } } } sprTriTitleReady = true; @@ -78,7 +84,79 @@ inline void initTriTemp() { #endif } +// 8-bit anti-aliased alpha mask for digital 7-segment italic "°C" (30x26) +const uint8_t s_dsegUnitAlpha[30 * 26] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x10, 0x11, 0x11, 0x12, 0x0E, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x7A, 0xED, 0xEC, 0xED, 0xED, 0xE4, 0x2A, 0x00, 0x02, + 0x02, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x10, 0x11, 0x03, 0x00, 0x01, + 0x00, 0x00, 0x01, 0x02, 0x0C, 0xA2, 0xFF, 0xF2, 0xF3, 0xF4, 0xF1, 0xFF, 0xD2, 0x12, 0x04, + 0x00, 0x01, 0x97, 0xEE, 0xEC, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEC, 0xEE, 0xC1, 0x13, 0x01, + 0x00, 0x00, 0x04, 0x00, 0x5C, 0xFF, 0x9F, 0x10, 0x14, 0x10, 0x13, 0xD9, 0xFF, 0x2B, 0x00, + 0x12, 0xB6, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x77, 0x00, + 0x00, 0x00, 0x04, 0x00, 0x92, 0xFC, 0x63, 0x00, 0x01, 0x00, 0x19, 0xFC, 0xE1, 0x05, 0x31, + 0xD8, 0xFF, 0xFC, 0xFE, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFB, 0xF9, 0x89, 0x0B, 0x02, + 0x00, 0x00, 0x01, 0x00, 0xC8, 0xFF, 0x38, 0x05, 0x0B, 0x05, 0x4E, 0xFF, 0xAD, 0x00, 0xD4, + 0xFF, 0xFC, 0x66, 0x27, 0x2A, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x15, 0xF3, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x79, 0xFF, 0x73, 0x09, 0xF8, + 0xFD, 0xF6, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, + 0x00, 0x02, 0x00, 0x2A, 0xFF, 0xDF, 0x7A, 0x7C, 0x7D, 0x7A, 0xDB, 0xFC, 0x28, 0x3B, 0xFF, + 0xFF, 0xD5, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x84, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0x46, 0x00, 0x7E, 0xFF, + 0xFF, 0xA3, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x70, 0x7B, 0x7A, 0x7A, 0x79, 0x22, 0x00, 0x00, 0xAF, 0xFF, + 0xFF, 0x6E, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x05, 0xDF, 0xFF, + 0xFF, 0x3A, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x27, 0xFB, 0xFF, + 0xED, 0x11, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x57, 0xFF, 0xFF, + 0xC5, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x8A, 0xFF, 0xFF, + 0x91, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0xC0, 0xFF, 0xFF, + 0x5D, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0C, 0xE9, 0xFF, 0xFC, + 0x2D, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x34, 0xFF, 0xFF, 0xE4, + 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x67, 0xFF, 0xFF, 0xB6, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x9C, 0xFF, 0xFF, 0x82, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0xCF, 0xFF, 0xFF, 0x4D, + 0x01, 0x04, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x16, 0xF2, 0xFF, 0xF7, 0x1A, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x4B, 0xFF, 0xFE, 0xD8, 0x12, + 0x12, 0x12, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x20, 0xE4, 0xFF, 0xF7, 0xED, + 0xEE, 0xED, 0xED, 0xED, 0xED, 0xED, 0xEB, 0xED, 0x93, 0x05, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x37, 0xFA, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3D, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x67, 0xF9, 0xF6, + 0xF9, 0xF9, 0xF9, 0xF9, 0xF9, 0xF8, 0xF7, 0xE9, 0x5B, 0x03, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0x2A, + 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x29, 0x2A, 0x1B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; +// Draw 100% anti-aliased digital 7-segment italic unit "°C" matching DSEG7 typography and color +inline void draw7SegDegreeC(TFT_eSPI &dst, int bx, int by, uint16_t color = 0xC618, uint16_t bgCol = 0x10A2) { + for (int j = 0; j < 26; j++) { + int py = by + j; + if (py < 0 || py >= dst.height()) continue; + int rowOffset = j * 30; + for (int i = 0; i < 30; i++) { + int px = bx + i; + if (px < 0 || px >= dst.width()) continue; + uint8_t a = pgm_read_byte(&s_dsegUnitAlpha[rowOffset + i]); + if (a > 8) { + uint16_t pixCol = (a >= 250) ? color : fastBlend(a, color, bgCol); + dst.drawPixel(px, py, pixCol); + } + } + } +} // Double-buffered 120x8 progress bar sprite (zero jitter) static TFT_eSprite sprBar(&tft); @@ -93,21 +171,33 @@ inline void drawTriTempBar(int barX, int barY, int fillW, uint16_t accentCol) { if (sprBarCreated) { sprBar.fillRoundRect(0, 0, 120, 8, 3, 0x18C3); if (fillW > 0) { - sprBar.fillRoundRect(0, 0, fillW, 8, 3, accentCol); + if (isRainbowTheme()) { + for (int bx = 0; bx < fillW; bx++) { + sprBar.drawFastVLine(bx, 0, 8, getRainbowScreenColor(barX + bx, barY)); + } + } else { + sprBar.fillRoundRect(0, 0, fillW, 8, 3, accentCol); + } } sprBar.pushSprite(barX, barY); } else { tft.fillRoundRect(barX, barY, 120, 8, 3, 0x18C3); if (fillW > 0) { - tft.fillRoundRect(barX, barY, fillW, 8, 3, accentCol); + if (isRainbowTheme()) { + for (int bx = 0; bx < fillW; bx++) { + tft.drawFastVLine(barX + bx, barY, 8, getRainbowScreenColor(barX + bx, barY)); + } + } else { + tft.fillRoundRect(barX, barY, fillW, 8, 3, accentCol); + } } } } // Draw a single temperature card with card index (0: Motor, 1: Interior, 2: Exterior) inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) { - int cardY = 6 + cardIdx * 77; // y = 6, 83, 160 - int cardH = 73; + int cardY = 19 + cardIdx * 74; // y = 19, 93, 167 (bounds 19 to 235 matching clock screen) + int cardH = 68; int cardX = 4 + x_offset; int cardW = 312; @@ -135,7 +225,7 @@ inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) { isInt = true; float warnT = warningValue[0].toFloat(); if (warnT <= 0) warnT = 110.0f; - if (val < (warnT - 10.0f)) accentCol = getThemePrimaryColor(); + if (val < (warnT - 10.0f)) accentCol = isRainbowTheme() ? (uint16_t)0x07FF : getThemePrimaryColor(); else if (val < warnT) accentCol = TFT_YELLOW; else accentCol = TFT_RED; } else if (cardIdx == 1) { @@ -143,36 +233,36 @@ inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) { subStr = (dsSensorCount > 0) ? "DS18B20 SENSOR 1 (GPIO 22)" : "DS18B20 SENSOR 1"; minRange = 10.0f; maxRange = 40.0f; - accentCol = 0x07F3; // Mint Green + accentCol = isRainbowTheme() ? (uint16_t)0x07E0 : getThemeAccentColor(); // Green in rainbow } else { titleStr = "EXTERIOR"; subStr = (dsSensorCount > 1) ? "DS18B20 SENSOR 2 (GPIO 22)" : (dsSensorCount == 1 ? "ESTIMADA (-4.5`C)" : "DS18B20 SENSOR 2"); minRange = -10.0f; maxRange = 50.0f; - accentCol = 0x3D7F; // Sky Blue + accentCol = isRainbowTheme() ? (uint16_t)0xFD20 : getThemePrimaryColor(); // Orange in rainbow } // Left vertical accent bar - tft.fillRoundRect(cardX + 6, cardY + 8, 4, cardH - 16, 2, accentCol); + tft.fillRoundRect(cardX + 6, cardY + 6, 4, cardH - 12, 2, accentCol); // Titles (Smooth anti-aliased font) if (!sprTriTitleReady) initTriTempTitles(); if (sprTriTitleReady && sprTriTitle[cardIdx].created()) { - sprTriTitle[cardIdx].pushSprite(cardX + 16, cardY + 6); + sprTriTitle[cardIdx].pushSprite(cardX + 16, cardY + 5); } else { tft.loadFont(Latin_Sans_Bold_26); tft.setTextPadding(0); tft.setTextColor(TFT_WHITE, cardBg); - tft.drawString(titleStr, cardX + 16, cardY + 6, 1); + tft.drawString(titleStr, cardX + 16, cardY + 5, 1); tft.unloadFont(); } tft.setTextColor(0x8410, cardBg); // Soft gray - tft.drawString(subStr, cardX + 16, cardY + 34, 1); + tft.drawString(subStr, cardX + 16, cardY + 32, 1); // 3. Horizontal Progress Gauge Bar int barX = cardX + 16; - int barY = cardY + 50; + int barY = cardY + 48; int barW = 120; float ratio = (val - minRange) / (maxRange - minRange); @@ -200,24 +290,19 @@ inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) { numStr = String(val, 1); } - tft.loadFont(DSEG7_Modern_BoldItalic_34); - tft.setTextColor(accentCol, cardBg); - tft.setTextPadding(100); - tft.drawRightString(numStr.c_str(), cardX + 262, cardY + 22, 1); - tft.setTextPadding(0); - tft.unloadFont(); + bool rainbowTxt = isRainbowTheme() && (accentCol != TFT_RED); + drawDseg7RightString34(tft, numStr.c_str(), cardX + 262, cardY + 20, accentCol, cardBg, 100, 240, rainbowTxt); - // Unit "°C" - tft.setTextColor(0xC618, cardBg); - tft.drawString("`C", cardX + 268, cardY + 32, 2); + // Unit "°C" in authentic digital 7-segment italic typography matching numbers + draw7SegDegreeC(tft, cardX + 268, cardY + 22, isRainbowTheme() ? getRainbowScreenColor(cardX + 268, cardY + 22) : 0xC618, cardBg); } // Flicker-free dynamic value update for a single temperature card inline void updateTriTempCardValue(uint8_t cardIdx, float val, float oldVal) { if (currentScreenPage != 1 || fullScreenCell != -1) return; - int cardY = 6 + cardIdx * 77; // y = 6, 83, 160 - int cardH = 73; + int cardY = 19 + cardIdx * 74; // y = 19, 93, 167 + int cardH = 68; int cardX = 4; uint16_t cardBg = 0x10A2; // Dark graphite navy @@ -232,11 +317,11 @@ inline void updateTriTempCardValue(uint8_t cardIdx, float val, float oldVal) { isInt = true; float warnT = warningValue[0].toFloat(); if (warnT <= 0) warnT = 110.0f; - if (val < (warnT - 10.0f)) accentCol = getThemePrimaryColor(); + if (val < (warnT - 10.0f)) accentCol = isRainbowTheme() ? (uint16_t)0x07FF : getThemePrimaryColor(); else if (val < warnT) accentCol = TFT_YELLOW; else accentCol = TFT_RED; - if (oldVal < (warnT - 10.0f)) oldAccentCol = getThemePrimaryColor(); + if (oldVal < (warnT - 10.0f)) oldAccentCol = isRainbowTheme() ? (uint16_t)0x07FF : getThemePrimaryColor(); else if (oldVal < warnT) oldAccentCol = TFT_YELLOW; else oldAccentCol = TFT_RED; @@ -244,30 +329,30 @@ inline void updateTriTempCardValue(uint8_t cardIdx, float val, float oldVal) { if (ignitionOn != oldTriIgnition) { oldTriIgnition = ignitionOn; const char* subStr = ignitionOn ? "ECU OBD-II (PID 0105)" : "ECU OBD-II (STANDBY)"; - tft.fillRect(cardX + 16, cardY + 34, 130, 10, cardBg); + tft.fillRect(cardX + 16, cardY + 32, 130, 10, cardBg); tft.setTextColor(0x8410, cardBg); - tft.drawString(subStr, cardX + 16, cardY + 34, 1); + tft.drawString(subStr, cardX + 16, cardY + 32, 1); } } else if (cardIdx == 1) { minRange = 10.0f; maxRange = 40.0f; - accentCol = 0x07F3; // Mint Green + accentCol = isRainbowTheme() ? (uint16_t)0x07E0 : getThemeAccentColor(); oldAccentCol = accentCol; } else { minRange = -10.0f; maxRange = 50.0f; - accentCol = 0x3D7F; // Sky Blue + accentCol = isRainbowTheme() ? (uint16_t)0xFD20 : getThemePrimaryColor(); oldAccentCol = accentCol; } // 1. Accent color changed: update vertical accent bar if (accentCol != oldAccentCol) { - tft.fillRoundRect(cardX + 6, cardY + 8, 4, cardH - 16, 2, accentCol); + tft.fillRoundRect(cardX + 6, cardY + 6, 4, cardH - 12, 2, accentCol); } // 2. Horizontal Progress Gauge Bar update (only redraw when pixel fill changes or accent changes) int barX = cardX + 16; - int barY = cardY + 50; + int barY = cardY + 48; int barW = 120; float ratio = (val - minRange) / (maxRange - minRange); @@ -292,12 +377,8 @@ inline void updateTriTempCardValue(uint8_t cardIdx, float val, float oldVal) { numStr = String(val, 1); } - tft.loadFont(DSEG7_Modern_BoldItalic_34); - tft.setTextColor(accentCol, cardBg); - tft.setTextPadding(100); - tft.drawRightString(numStr.c_str(), cardX + 262, cardY + 22, 1); - tft.setTextPadding(0); - tft.unloadFont(); + bool rainbowTxt = isRainbowTheme() && (accentCol != TFT_RED); + drawDseg7RightString34(tft, numStr.c_str(), cardX + 262, cardY + 20, accentCol, cardBg, 100, 240, rainbowTxt); } // Synchronize tracking variables without touching TFT display (zero flicker) @@ -311,7 +392,11 @@ inline void syncTriTempTrackers() { float vals[3] = { tempEngineCoolant, tempInterior, tempExterior }; float minR[3] = { 60.0f, 10.0f, -10.0f }; float maxR[3] = { 120.0f, 40.0f, 50.0f }; - uint16_t defAcc[3] = { getThemePrimaryColor(), 0x07F3, 0x3D7F }; + uint16_t defAcc[3] = { + isRainbowTheme() ? (uint16_t)0x07FF : getThemePrimaryColor(), + isRainbowTheme() ? (uint16_t)0x07E0 : getThemeAccentColor(), + isRainbowTheme() ? (uint16_t)0xFD20 : getThemePrimaryColor() + }; for (int i = 0; i < 3; i++) { float ratio = (vals[i] - minR[i]) / (maxR[i] - minR[i]); if (ratio < 0.0f) ratio = 0.0f; @@ -333,11 +418,10 @@ inline void syncTriTempTrackers() { // Draw full Tri-Temp screen inline void drawTriTempScreen(int x_offset = 0) { - if (x_offset != 0) { - tft.fillRect(0, 0, x_offset, 240, TFT_BLACK); + if (x_offset == 0) { + tft.fillScreen(TFT_BLACK); } else { - tft.fillRect(0, 0, 320, 6, TFT_BLACK); - tft.fillRect(0, 233, 320, 7, TFT_BLACK); + tft.fillRect(0, 0, x_offset, 240, TFT_BLACK); } drawTriTempCard(0, tempEngineCoolant, x_offset);