obd2_gauge/sketch/obd2_gauge/tri_temp.h
Sebastian Nieto 63a7f8161a feat(ui): transicion fundido a negro 200ms, temas de color y correccion brillo LDR
- Implementa transicion 'Fundido a Negro' (Fade to Black) con atenuacion PWM de ~200ms por curva gamma en reemplazo de deslizamiento.
- Corrige el brillo automatico LDR: umbral calibrado a 1000 para ADC de 12 bits, ejecucion continua en el loop sin depender de Bluetooth y telemetria en tiempo real en ajustes.
- Agrega nuevos temas de color: Yapa (pastillas pastel multicolor por caracter), Rojo Corsa y Ambar BMW.
- Integra efecto arcoiris continuo en menus de configuracion y pantallas completas.
- Unifica tipografia de telemetria GPS y renderizado con antialiasing para coordenadas y valores.
2026-10-13 22:09:37 -03:00

578 lines
23 KiB
C

#pragma once
#include <Arduino.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#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];
// Global states for multi-page system
extern int8_t fullScreenCell;
extern int8_t currentScreenPage; // 0 = 4 cuadrantes, 1 = Tri-Temperatura
extern bool ignitionOn;
// Global temperature values
extern float tempEngineCoolant;
extern float tempInterior;
extern float tempExterior;
extern uint8_t dsSensorCount;
// OneWire and DallasTemperature instances
static OneWire oneWireObj(ONE_WIRE_BUS_PIN);
static DallasTemperature dsSensors(&oneWireObj);
// Previous values for flicker-free redrawing
static float oldTriCoolant = -999.0f;
static float oldTriInterior = -999.0f;
static float oldTriExterior = -999.0f;
static bool oldTriIgnition = true;
static uint8_t oldTriDsSensorCount = 255;
// Pre-rendered smooth title sprites for Page 1 (AGUA, INTERIOR, EXTERIOR)
// 100% anti-aliased Latin_Sans_Bold_26: 0 mallocs during transitions and zero flicker
static TFT_eSprite sprTriTitle[3] = { TFT_eSprite(&tft), TFT_eSprite(&tft), TFT_eSprite(&tft) };
static bool sprTriTitleReady = false;
inline void invalidateTriTempTitles() {
sprTriTitleReady = false;
}
inline void initTriTempTitles() {
if (sprTriTitleReady) return;
uint16_t cardBg = 0x10A2;
const char* titles[3] = { "AGUA", "INTERIOR", "EXTERIOR" };
const int widths[3] = { 80, 136, 136 };
for (int i = 0; i < 3; i++) {
if (sprTriTitle[i].createSprite(widths[i], 26) != nullptr) {
sprTriTitle[i].loadFont(Latin_Sans_Bold_26);
sprTriTitle[i].setTextPadding(0);
sprTriTitle[i].fillSprite(cardBg);
if (isYapaTheme()) {
int curX = 0;
char buf[2] = {0, 0};
String t = titles[i];
for (size_t k = 0; k < t.length(); k++) {
buf[0] = t[k];
sprTriTitle[i].setTextColor(getYapaColor(k), cardBg);
sprTriTitle[i].drawString(buf, curX, 0);
curX += sprTriTitle[i].textWidth(buf);
}
} else {
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;
}
// Initialize 1-Wire sensors
inline void initTriTemp() {
initTriTempTitles();
dsSensors.begin();
dsSensors.setWaitForConversion(false); // Non-blocking conversion (0ms wait)
dsSensorCount = dsSensors.getDeviceCount();
oldTriDsSensorCount = dsSensorCount;
oldTriIgnition = ignitionOn;
if (dsSensorCount > 0) {
dsSensors.requestTemperatures();
}
#ifdef SERIAL_DEBUG
Serial.printf("[1-WIRE] Sensores DS18B20 detectados en GPIO 22: %d\n", dsSensorCount);
#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, bool rainbow = false) {
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 curCol = rainbow ? getRainbowScreenColor(px, py) : color;
uint16_t pixCol = (a >= 250) ? curCol : fastBlend(a, curCol, bgCol);
dst.drawPixel(px, py, pixCol);
}
}
}
}
// Double-buffered 120x8 progress bar sprite (zero jitter)
static TFT_eSprite sprBar(&tft);
static bool sprBarCreated = false;
static int oldTriFillW[3] = { -1, -1, -1 };
static uint16_t oldTriAccentCol[3] = { 0, 0, 0 };
inline void drawTriTempBar(int barX, int barY, int fillW, uint16_t accentCol) {
if (!sprBarCreated) {
sprBarCreated = (sprBar.createSprite(120, 8) != nullptr);
}
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 = 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;
if (cardX + cardW <= 0 || cardX >= 320) return;
uint16_t cardBg = 0x10A2; // Dark graphite navy
uint16_t cardBorder = 0x2945; // Subtle edge border
// 1. Draw Card Background and Border
tft.fillRoundRect(cardX, cardY, cardW, cardH, 6, cardBg);
tft.drawRoundRect(cardX, cardY, cardW, cardH, 6, cardBorder);
// 2. Card Metadata & Accent Color
uint16_t accentCol = getThemePrimaryColor();
const char* titleStr = "";
const char* subStr = "";
float minRange = 0.0f, maxRange = 100.0f;
bool isInt = false;
if (cardIdx == 0) {
titleStr = "AGUA";
subStr = ignitionOn ? "ECU OBD-II (PID 0105)" : "ECU OBD-II (STANDBY)";
minRange = 60.0f;
maxRange = 120.0f;
isInt = true;
float warnT = warningValue[0].toFloat();
if (warnT <= 0) warnT = 110.0f;
if (val < (warnT - 10.0f)) accentCol = isRainbowTheme() ? (uint16_t)0x07FF : (isYapaTheme() ? getYapaColor(0) : getThemePrimaryColor());
else if (val < warnT) accentCol = TFT_YELLOW;
else accentCol = TFT_RED;
} else if (cardIdx == 1) {
titleStr = "INTERIOR";
subStr = (dsSensorCount > 0) ? "DS18B20 SENSOR 1 (GPIO 22)" : "DS18B20 SENSOR 1";
minRange = 10.0f;
maxRange = 40.0f;
accentCol = isRainbowTheme() ? (uint16_t)0x07E0 : (isYapaTheme() ? getYapaColor(1) : getThemeAccentColor());
} 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 = isRainbowTheme() ? (uint16_t)0xFD20 : (isYapaTheme() ? getYapaColor(2) : getThemePrimaryColor());
}
// Left vertical accent bar
tft.fillRoundRect(cardX + 6, cardY + 6, 4, cardH - 12, 2, accentCol);
if (isYapaTheme()) {
tft.loadFont(Latin_Sans_Bold_26);
int curX = cardX + 16;
char buf[2] = {0, 0};
uint8_t cIdx = 0;
for (int i = 0; titleStr[i] != '\0'; i++) {
char c = titleStr[i];
buf[0] = c;
if (c == ' ') { curX += tft.textWidth(buf); continue; }
tft.setTextColor(getYapaColor(cIdx++), cardBg);
tft.drawString(buf, curX, cardY + 5);
curX += tft.textWidth(buf);
}
tft.unloadFont();
int sX = cardX + 16;
for (int i = 0; subStr[i] != '\0'; i++) {
char c = subStr[i];
buf[0] = c;
int cw = tft.textWidth(buf, 1);
if (c == ' ') { sX += cw; continue; }
tft.setTextColor(getYapaColor(cIdx++), cardBg);
tft.drawString(buf, sX, cardY + 32, 1);
sX += cw;
}
} else {
if (!sprTriTitleReady) initTriTempTitles();
if (sprTriTitleReady && sprTriTitle[cardIdx].created()) {
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 + 5, 1);
tft.unloadFont();
}
tft.setTextColor(0x8410, cardBg); // Soft gray
tft.drawString(subStr, cardX + 16, cardY + 32, 1);
}
// 3. Horizontal Progress Gauge Bar
int barX = cardX + 16;
int barY = cardY + 48;
int barW = 120;
float ratio = (val - minRange) / (maxRange - minRange);
if (ratio < 0.0f) ratio = 0.0f;
if (ratio > 1.0f) ratio = 1.0f;
int fillW = (int)(barW * ratio);
if (fillW < 4) fillW = 4;
if (x_offset == 0) {
drawTriTempBar(barX, barY, fillW, accentCol);
oldTriFillW[cardIdx] = fillW;
oldTriAccentCol[cardIdx] = accentCol;
} else {
tft.fillRoundRect(barX, barY, barW, 8, 3, 0x18C3);
tft.fillRoundRect(barX, barY, fillW, 8, 3, accentCol);
}
// 4. Digital Number using DSEG7 Modern Bold Italic 34pt
String numStr;
if (val <= -900.0f) {
numStr = "---";
} else if (isInt) {
numStr = String((int)val);
} else {
numStr = String(val, 1);
}
bool rainbowTxt = isRainbowTheme() && (accentCol != TFT_RED);
drawDseg7RightString34(tft, numStr.c_str(), cardX + 262, cardY + 20, accentCol, cardBg, 100, 240, rainbowTxt);
// Unit "°C" in authentic digital 7-segment italic typography matching numbers
bool rainbowUnit = isRainbowTheme() && (accentCol != TFT_RED);
uint16_t unitCol = rainbowUnit ? getRainbowScreenColor(cardX + 268, cardY + 22) : (isYapaTheme() && accentCol != TFT_RED ? getYapaColor(numStr.length()) : (accentCol == TFT_RED ? TFT_RED : 0xC618));
draw7SegDegreeC(tft, cardX + 268, cardY + 22, unitCol, cardBg, rainbowUnit);
}
// 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 = 19 + cardIdx * 74; // y = 19, 93, 167
int cardH = 68;
int cardX = 4;
uint16_t cardBg = 0x10A2; // Dark graphite navy
float minRange = 0.0f, maxRange = 100.0f;
bool isInt = false;
uint16_t accentCol = getThemePrimaryColor();
uint16_t oldAccentCol = getThemePrimaryColor();
if (cardIdx == 0) {
minRange = 60.0f;
maxRange = 120.0f;
isInt = true;
float warnT = warningValue[0].toFloat();
if (warnT <= 0) warnT = 110.0f;
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 = isRainbowTheme() ? (uint16_t)0x07FF : getThemePrimaryColor();
else if (oldVal < warnT) oldAccentCol = TFT_YELLOW;
else oldAccentCol = TFT_RED;
// Subtitle update if ignition state changed
if (ignitionOn != oldTriIgnition) {
oldTriIgnition = ignitionOn;
const char* subStr = ignitionOn ? "ECU OBD-II (PID 0105)" : "ECU OBD-II (STANDBY)";
tft.fillRect(cardX + 16, cardY + 32, 130, 10, cardBg);
if (isYapaTheme()) {
char buf[2] = {0, 0};
int sX = cardX + 16;
uint8_t cIdx = 4;
for (int i = 0; subStr[i] != '\0'; i++) {
char c = subStr[i];
buf[0] = c;
int cw = tft.textWidth(buf, 1);
if (c == ' ') { sX += cw; continue; }
tft.setTextColor(getYapaColor(cIdx++), cardBg);
tft.drawString(buf, sX, cardY + 32, 1);
sX += cw;
}
} else {
tft.setTextColor(0x8410, cardBg);
tft.drawString(subStr, cardX + 16, cardY + 32, 1);
}
}
} else if (cardIdx == 1) {
minRange = 10.0f;
maxRange = 40.0f;
accentCol = isRainbowTheme() ? (uint16_t)0x07E0 : (isYapaTheme() ? getYapaColor(1) : getThemeAccentColor());
oldAccentCol = accentCol;
} else {
minRange = -10.0f;
maxRange = 50.0f;
accentCol = isRainbowTheme() ? (uint16_t)0xFD20 : (isYapaTheme() ? getYapaColor(2) : getThemePrimaryColor());
oldAccentCol = accentCol;
}
// 1. Accent color changed: update vertical accent bar and unit "°C"
if (accentCol != oldAccentCol) {
tft.fillRoundRect(cardX + 6, cardY + 6, 4, cardH - 12, 2, accentCol);
bool rainbowUnit = isRainbowTheme() && (accentCol != TFT_RED);
uint16_t unitCol = rainbowUnit ? getRainbowScreenColor(cardX + 268, cardY + 22) : (isYapaTheme() && accentCol != TFT_RED ? getYapaColor(3) : (accentCol == TFT_RED ? TFT_RED : 0xC618));
draw7SegDegreeC(tft, cardX + 268, cardY + 22, unitCol, cardBg, rainbowUnit);
}
// 2. Horizontal Progress Gauge Bar update (only redraw when pixel fill changes or accent changes)
int barX = cardX + 16;
int barY = cardY + 48;
int barW = 120;
float ratio = (val - minRange) / (maxRange - minRange);
if (ratio < 0.0f) ratio = 0.0f;
if (ratio > 1.0f) ratio = 1.0f;
int fillW = (int)(barW * ratio);
if (fillW < 4) fillW = 4;
if (fillW != oldTriFillW[cardIdx] || accentCol != oldTriAccentCol[cardIdx]) {
drawTriTempBar(barX, barY, fillW, accentCol);
oldTriFillW[cardIdx] = fillW;
oldTriAccentCol[cardIdx] = accentCol;
}
// 3. Digital Number update using DSEG7 Modern Bold Italic 34pt (setTextPadding prevents flicker)
String numStr;
if (val <= -900.0f) {
numStr = "---";
} else if (isInt) {
numStr = String((int)val);
} else {
numStr = String(val, 1);
}
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)
inline void syncTriTempTrackers() {
oldTriCoolant = tempEngineCoolant;
oldTriInterior = tempInterior;
oldTriExterior = tempExterior;
oldTriIgnition = ignitionOn;
oldTriDsSensorCount = dsSensorCount;
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] = {
isRainbowTheme() ? (uint16_t)0x07FF : (isYapaTheme() ? getYapaColor(0) : getThemePrimaryColor()),
isRainbowTheme() ? (uint16_t)0x07E0 : (isYapaTheme() ? getYapaColor(1) : getThemeAccentColor()),
isRainbowTheme() ? (uint16_t)0xFD20 : (isYapaTheme() ? getYapaColor(2) : 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;
if (ratio > 1.0f) ratio = 1.0f;
int fillW = (int)(120 * ratio);
if (fillW < 4) fillW = 4;
oldTriFillW[i] = fillW;
uint16_t acc = defAcc[i];
if (i == 0) {
float warnT = warningValue[0].toFloat();
if (warnT <= 0) warnT = 110.0f;
if (vals[i] >= warnT) acc = TFT_RED;
else if (vals[i] >= (warnT - 10.0f)) acc = TFT_YELLOW;
}
oldTriAccentCol[i] = acc;
}
}
// Draw full Tri-Temp screen
inline void drawTriTempScreen(int x_offset = 0) {
if (x_offset == 0) {
tft.fillScreen(TFT_BLACK);
} else {
tft.fillRect(0, 0, x_offset, 240, TFT_BLACK);
}
drawTriTempCard(0, tempEngineCoolant, x_offset);
drawTriTempCard(1, tempInterior, x_offset);
drawTriTempCard(2, tempExterior, x_offset);
syncTriTempTrackers();
// Draw Page Indicator: [○ ● ○ ○] (Page 2 of 4 active)
tft.fillCircle(139 + x_offset, 2, 2, 0x5AEB); // Page 1 inactive
tft.fillCircle(153 + x_offset, 2, 2, getThemePrimaryColor()); // Page 2 active
tft.fillCircle(167 + x_offset, 2, 2, 0x5AEB); // Page 3 inactive
tft.fillCircle(181 + x_offset, 2, 2, 0x5AEB); // Page 4 inactive
}
// Update coolant reading dynamically without redrawing entire screen
inline void updateTriTempCoolant(float val) {
tempEngineCoolant = val;
if (currentScreenPage != 1 || fullScreenCell != -1) return;
if (val != oldTriCoolant || ignitionOn != oldTriIgnition) {
updateTriTempCardValue(0, val, oldTriCoolant);
oldTriCoolant = val;
}
}
// Update 1-Wire sensors (Interior and Exterior)
inline void updateTriTempSensors(bool forceRedraw = false) {
// Hotplug scan if 0 sensors detected yet
if (dsSensorCount == 0) {
static uint32_t lastHotplugCheck = 0;
if (millis() - lastHotplugCheck >= 5000) {
lastHotplugCheck = millis();
dsSensors.begin();
dsSensorCount = dsSensors.getDeviceCount();
if (dsSensorCount > 0) {
dsSensors.setWaitForConversion(false);
dsSensors.requestTemperatures();
forceRedraw = true;
}
}
}
if (dsSensorCount > 0) {
float t1 = dsSensors.getTempCByIndex(0);
if (t1 > -50.0f && t1 < 100.0f) {
tempInterior = t1;
}
if (dsSensorCount > 1) {
float t2 = dsSensors.getTempCByIndex(1);
if (t2 > -50.0f && t2 < 100.0f) {
tempExterior = t2;
}
} else {
// Demo exterior offset if only 1 physical sensor connected
tempExterior = tempInterior - 4.5f;
}
dsSensors.requestTemperatures(); // non-blocking next reading request
} else {
// Simulated demo readings if sensors not yet plugged in
tempInterior = 22.4f + 0.3f * sin(millis() / 4000.0f);
tempExterior = 16.8f + 0.2f * cos(millis() / 5000.0f);
}
if (currentScreenPage == 1 && fullScreenCell == -1) {
// If sensor count changed (e.g. plug / unplug), redraw full cards so subtitles update
if (dsSensorCount != oldTriDsSensorCount) {
oldTriDsSensorCount = dsSensorCount;
drawTriTempCard(1, tempInterior, 0);
drawTriTempCard(2, tempExterior, 0);
oldTriInterior = tempInterior;
oldTriExterior = tempExterior;
} else {
if (forceRedraw || abs(tempInterior - oldTriInterior) >= 0.1f) {
updateTriTempCardValue(1, tempInterior, oldTriInterior);
oldTriInterior = tempInterior;
}
if (forceRedraw || abs(tempExterior - oldTriExterior) >= 0.1f) {
updateTriTempCardValue(2, tempExterior, oldTriExterior);
oldTriExterior = tempExterior;
}
}
}
}
// Draw fast slide frame of Page 1 (3 temperature cards) shifted by x_offset
inline void drawSlidePage1(int x_offset) {
tft.fillRect(x_offset, 0, 320, 240, TFT_BLACK);
drawTriTempCard(0, tempEngineCoolant, x_offset);
drawTriTempCard(1, tempInterior, x_offset);
drawTriTempCard(2, tempExterior, x_offset);
}