feat(ui,theme): tema de color arcoiris con degradado espectral global y desactivacion de led trasero

- Lienzo espectral continuo (320px) visible a traves de letras y numeros
- Renderizado de digitos DSEG7 (34pt y 80pt) con degradado continuo pixel a pixel
- Preservacion del suavizado anti-aliased subpixel en titulos y fuentes de texto
- Integracion del tema Arcoiris en pantallas de instrumentos, tri-temp, reloj y navegacion
- Desactivacion permanente del LED RGB trasero de la placa a nivel de hardware y software
This commit is contained in:
Sebastián Nieto 2026-10-12 21:46:35 -03:00
parent cb77100a49
commit 4c5967d3c5
8 changed files with 545 additions and 370 deletions

View file

@ -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
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);
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);

View file

@ -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 <Arduino.h>
#include <TFT_eSPI.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;
@ -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,23 +127,34 @@ inline void drawDateCardContent(int cardX, int cardY, int cardW, int cardH,
// --- DATE DETAILS (Right side) ---
int tx = cardX + 116;
// 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);
// 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);
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);
// 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
// DSEG7_Modern_BoldItalic_80)
@ -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 <typename T>
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);
if (rainbow) {
for (int i = 0; i < fl; i++) {
dst.drawPixel(fxs + i, curY, getRainbowScreenColor(fxs + i, curY));
}
}
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;
}
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));
}
}
if (fl) {
dst.drawFastHLine(fxs, curY, fl, color);
}
}
dst.endWrite();
}
template <typename T>
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) {
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);
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);
}
}
}
}

View file

@ -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)

View file

@ -4,10 +4,13 @@
#include <TFT_eSPI.h>
#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);
}
}

View file

@ -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);

View file

@ -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) {

Binary file not shown.

View file

@ -6,13 +6,16 @@
#include <TFT_eSPI.h>
#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) {
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) {
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);