feat(config,ui): menu de umbrales de alarmas y tipografia DSEG7 80pt en transicion de reloj

- Submenu de alarmas con opciones separadas de 'Seteo de Activacion' y 'Seteo de Umbrales'
- Pantalla tactil interactiva para ajuste de umbrales (Temp, RPM, Voltaje, Vel) con persistencia en NVS
- Vinculacion del umbral de temperatura de refrigerante en tri-temp con el valor configurado
- Renderizado nativo de DSEG7 80pt desde PROGMEM en renderPage2ToSprite para coincidencia exacta de tipografia en transicion de reloj sin interferir con fuentes de otras pantallas
This commit is contained in:
Sebastián Nieto 2026-10-11 19:44:29 -03:00
parent d124d013ab
commit 4937cf3b42
3 changed files with 360 additions and 22 deletions

View file

@ -15,6 +15,9 @@ extern bool wokeFromSleep;
extern uint8_t transitionType; extern uint8_t transitionType;
extern volatile bool systemSleeping; extern volatile bool systemSleeping;
extern TaskHandle_t btTaskHandle; extern TaskHandle_t btTaskHandle;
extern String warningValue[5];
extern bool alarmEnabled[5];
extern const String pidConfig[5][9];
// Control de Inactividad de 10 segundos en Menu de Configuracion y Submenus // Control de Inactividad de 10 segundos en Menu de Configuracion y Submenus
static uint32_t lastMenuActivityTime = 0; static uint32_t lastMenuActivityTime = 0;
@ -216,12 +219,6 @@ void warningMenu() {
} }
// 2. Bottom button [ SALIR ] // 2. Bottom button [ SALIR ]
else if (ty >= 180 && tx >= 20 && tx <= 300) { else if (ty >= 180 && tx >= 20 && tx <= 300) {
pref.begin("setting", false);
for (uint8_t i = 0; i < 4; i++) {
String almKey = "alm_" + String(i);
pref.putBool(almKey.c_str(), alarmEnabled[i]);
}
pref.end();
animateExitButtonPress(); animateExitButtonPress();
break; break;
} }
@ -232,6 +229,287 @@ void warningMenu() {
} }
delay(25); delay(25);
} }
// Save alarm enablement on any exit
pref.begin("setting", false);
for (uint8_t i = 0; i < 4; i++) {
String almKey = "alm_" + String(i);
pref.putBool(almKey.c_str(), alarmEnabled[i]);
}
pref.end();
}
// -------------------------------------------------------------
// Sub-screen: Seteo de Umbrales de Alarma
// -------------------------------------------------------------
void thresholdMenu() {
tft.fillScreen(TFT_BLACK);
// Helper to step a parameter threshold
auto stepThreshold = [&](uint8_t row, bool increment) {
if (row == 0) { // Temperatura ECT: 70 to 125, step 1 °C
int v = warningValue[0].toInt();
v += increment ? 1 : -1;
if (v < 70) v = 70;
if (v > 125) v = 125;
warningValue[0] = String(v);
} else if (row == 1) { // RPM: 2000 to 6500, step 100 rpm
int v = warningValue[1].toInt();
v += increment ? 100 : -100;
if (v < 2000) v = 2000;
if (v > 6500) v = 6500;
warningValue[1] = String(v);
} else if (row == 2) { // Voltaje: 13.0 to 16.0, step 0.1 V
float v = warningValue[2].toFloat();
v += increment ? 0.1f : -0.1f;
if (v < 13.0f) v = 13.0f;
if (v > 16.0f) v = 16.0f;
v = roundf(v * 10.0f) / 10.0f;
warningValue[2] = String(v, 1);
} else if (row == 3) { // Velocidad: 60 to 180, step 5 km/h
int v = warningValue[3].toInt();
v += increment ? 5 : -5;
if (v < 60) v = 60;
if (v > 180) v = 180;
warningValue[3] = String(v);
}
};
// Draw single row value box (fast partial redraw for smooth hold-to-repeat)
auto drawRowValue = [&](uint8_t i) {
int y = 10 + i * 44;
int valX = 176;
int valW = 80;
int valH = 30;
tft.fillRoundRect(valX, y + 4, valW, valH, 4, 0x0821);
tft.drawRoundRect(valX, y + 4, valW, valH, 4, 0x2945);
String dispVal = "";
if (i == 0) {
dispVal = warningValue[0] + " `C";
} else if (i == 1) {
dispVal = warningValue[1];
} else if (i == 2) {
dispVal = String(warningValue[2].toFloat(), 1) + " V";
} else if (i == 3) {
dispVal = warningValue[3] + " km";
}
tft.setTextColor(getThemePrimaryColor(), 0x0821);
tft.setTextDatum(MC_DATUM);
tft.drawString(dispVal, valX + valW / 2, y + 19, 4);
tft.setTextDatum(TL_DATUM); // restore default
};
auto drawAllThresholdRows = [&]() {
uint16_t onCol = getThemePrimaryColor();
for (uint8_t i = 0; i < 4; i++) {
int y = 10 + i * 44;
// Card background
tft.fillRoundRect(8, y, 304, 38, 5, getThemeDarkColor());
tft.drawRoundRect(8, y, 304, 38, 5, getThemeAccentColor());
tft.fillRoundRect(10, y + 4, 3, 30, 1, onCol);
// Parameter label
const char* const thrNames[4] = { "Temp.", "RPM", "Voltaje", "Veloc." };
tft.setTextColor(TFT_WHITE, getThemeDarkColor());
tft.drawString(thrNames[i], 18, y + 7, 4);
// Decrement button [-] (x: 132, w: 38, h: 30)
tft.fillRoundRect(132, y + 4, 38, 30, 4, getThemeAccentColor());
tft.drawRoundRect(132, y + 4, 38, 30, 4, onCol);
tft.setTextColor(TFT_WHITE, getThemeAccentColor());
tft.setTextDatum(MC_DATUM);
tft.drawString("-", 132 + 19, y + 19, 4);
// Value Box (x: 176, w: 80, h: 30)
drawRowValue(i);
// Increment button [+] (x: 262, w: 38, h: 30)
tft.fillRoundRect(262, y + 4, 38, 30, 4, getThemeAccentColor());
tft.drawRoundRect(262, y + 4, 38, 30, 4, onCol);
tft.setTextColor(TFT_WHITE, getThemeAccentColor());
tft.setTextDatum(MC_DATUM);
tft.drawString("+", 262 + 19, y + 19, 4);
tft.setTextDatum(TL_DATUM); // restore default
}
};
drawAllThresholdRows();
drawExitButton(false);
waitForTouchRelease();
while (true) {
if (checkMenuTimeout()) break;
autoDim();
uint16_t tx = 0, ty = 0;
if (getTouch(&tx, &ty)) {
resetMenuActivity();
// 1. Direct Touch on [-] or [+] buttons (y: 6 to 184)
if (ty >= 6 && ty < 184) {
int8_t row = (ty - 10) / 44;
if (row >= 0 && row < 4) {
bool isDec = (tx >= 122 && tx < 174);
bool isInc = (tx >= 256 && tx <= 308);
if (isDec || isInc) {
touchBeep();
stepThreshold(row, isInc);
drawRowValue(row);
// Button press highlight
int btnX = isDec ? 132 : 262;
int btnY = 10 + row * 44 + 4;
tft.drawRoundRect(btnX, btnY, 38, 30, 4, TFT_WHITE);
// Repeat handling if user keeps pressing
uint32_t pressStart = millis();
uint32_t lastRepeat = millis();
while (getTouch(&tx, &ty)) {
if (millis() - pressStart > 350 && millis() - lastRepeat > 90) {
lastRepeat = millis();
stepThreshold(row, isInc);
drawRowValue(row);
touchBeep();
}
delay(15);
yield();
}
// Un-highlight button
tft.drawRoundRect(btnX, btnY, 38, 30, 4, getThemePrimaryColor());
}
}
}
// 2. Bottom button [ SALIR ]
else if (ty >= 180 && tx >= 20 && tx <= 300) {
animateExitButtonPress();
break;
}
} else if (digitalRead(SELECTOR_PIN) == LOW) {
resetMenuActivity();
delay(200);
break;
}
delay(25);
}
// Save threshold values to persistent storage on any exit
pref.begin("setting", false);
for (uint8_t i = 0; i < 4; i++) {
pref.putString(pidConfig[i][0].c_str(), warningValue[i]);
}
pref.end();
}
// -------------------------------------------------------------
// Sub-screen: Menu Principal de Alarmas (Activacion y Umbrales)
// -------------------------------------------------------------
void alarmCategoryMenu() {
uint8_t choice = 255;
auto drawCategoryUI = [&](uint8_t sel) {
tft.fillScreen(TFT_BLACK);
// Card 0: Activacion de Alarmas (y: 20 to 88, h: 68)
uint16_t bg0 = (sel == 0) ? getThemeAccentColor() : getThemeDarkColor();
uint16_t border0 = (sel == 0) ? getThemePrimaryColor() : getThemeAccentColor();
tft.fillRoundRect(10, 20, 300, 68, 5, bg0);
tft.drawRoundRect(10, 20, 300, 68, 5, border0);
if (sel == 0) {
tft.drawRoundRect(11, 21, 298, 66, 4, TFT_WHITE);
} else {
tft.fillRoundRect(13, 26, 4, 56, 2, getThemePrimaryColor());
}
tft.setTextColor(TFT_WHITE, bg0);
tft.drawString("Seteo de Activacion", 24, 28, 4);
uint8_t activeCount = 0;
for (uint8_t i = 0; i < 4; i++) {
if (alarmEnabled[i]) activeCount++;
}
String actInfo = String(activeCount) + " de 4 alarmas activas (ON/OFF)";
tft.setTextColor((sel == 0) ? TFT_YELLOW : 0xBDD7, bg0);
tft.drawString(actInfo, 24, 58, 2);
tft.fillTriangle(296, 54, 290, 48, 290, 60, (sel == 0) ? TFT_WHITE : getThemePrimaryColor());
// Card 1: Seteo de Umbrales (y: 102 to 170, h: 68)
uint16_t bg1 = (sel == 1) ? getThemeAccentColor() : getThemeDarkColor();
uint16_t border1 = (sel == 1) ? getThemePrimaryColor() : getThemeAccentColor();
tft.fillRoundRect(10, 102, 300, 68, 5, bg1);
tft.drawRoundRect(10, 102, 300, 68, 5, border1);
if (sel == 1) {
tft.drawRoundRect(11, 103, 298, 66, 4, TFT_WHITE);
} else {
tft.fillRoundRect(13, 108, 4, 56, 2, getThemePrimaryColor());
}
tft.setTextColor(TFT_WHITE, bg1);
tft.drawString("Seteo de Umbrales", 24, 110, 4);
String thrInfo = "T: " + warningValue[0] + "`C | RPM: " + warningValue[1] + " | V: " + warningValue[2] + "V | Vel: " + warningValue[3] + "k/h";
tft.setTextColor((sel == 1) ? TFT_YELLOW : 0xBDD7, bg1);
tft.drawString(thrInfo, 24, 140, 2);
tft.fillTriangle(296, 136, 290, 130, 290, 142, (sel == 1) ? TFT_WHITE : getThemePrimaryColor());
// Bottom action button: [ SALIR ] (x: 80, y: 186, w: 160, h: 42)
drawExitButton(false);
if (sel == 2) {
tft.drawRoundRect(80, 186, 160, 42, 5, getThemePrimaryColor());
tft.drawRoundRect(81, 187, 158, 40, 4, TFT_WHITE);
}
};
drawCategoryUI(choice);
waitForTouchRelease();
while (true) {
if (checkMenuTimeout()) break;
autoDim();
int8_t executeSelect = -1;
uint16_t tx = 0, ty = 0;
if (getTouch(&tx, &ty)) {
resetMenuActivity();
if (ty >= 16 && ty < 96) {
executeSelect = 0;
} else if (ty >= 96 && ty < 176) {
executeSelect = 1;
} else if (ty >= 178 && ty <= 240 && tx >= 20 && tx <= 300) {
executeSelect = 2;
}
if (executeSelect >= 0) {
choice = executeSelect;
if (executeSelect == 2) {
animateExitButtonPress();
break;
} else {
drawCategoryUI(choice);
touchBeep();
delay(120);
waitForTouchRelease();
if (executeSelect == 0) {
warningMenu(); // Seteo de Activacion ON/OFF
} else if (executeSelect == 1) {
thresholdMenu(); // Seteo de Umbrales
}
if (menuExitedDueToTimeout) break;
choice = 255;
drawCategoryUI(choice);
waitForTouchRelease();
}
}
} else if (digitalRead(SELECTOR_PIN) == LOW) {
resetMenuActivity();
delay(200);
break;
}
delay(25);
}
} }
// ------------------------------------------------------------- // -------------------------------------------------------------
@ -1892,9 +2170,9 @@ void configMenu() {
if (executeSelect >= 0) { if (executeSelect >= 0) {
select = executeSelect; select = executeSelect;
// Option 0: Alarmas (ON/OFF) // Option 0: Alarmas (Menu de Activacion y Umbrales)
if (select == 0) { if (select == 0) {
warningMenu(); alarmCategoryMenu();
if (menuExitedDueToTimeout) break; if (menuExitedDueToTimeout) break;
tft.fillScreen(TFT_BLACK); tft.fillScreen(TFT_BLACK);
drawConfigHeader(); drawConfigHeader();

View file

@ -162,6 +162,55 @@ inline void draw7SegColon(TFT_eSPI &dst, int clkX, int clkY, uint16_t color,
draw7SegDot(dst, clkX + 148, clkY + 64, color, bgCol, clip_h); 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();
}
// Synchronize tracking variables without touching TFT display (zero flicker) // Synchronize tracking variables without touching TFT display (zero flicker)
inline void syncGpsClockTrackers() { inline void syncGpsClockTrackers() {
oldClockHour = gpsHour; oldClockHour = gpsHour;
@ -324,12 +373,16 @@ inline void renderPage2ToSprite(TFT_eSprite &s, int x_offset, int y_slice,
(gpsMinute < 10) ? "0" + String(gpsMinute) : String(gpsMinute); (gpsMinute < 10) ? "0" + String(gpsMinute) : String(gpsMinute);
int digitY = clkY + 11; int digitY = clkY + 11;
if (digitY + 75 > 0 && digitY < slice_h) { if (digitY + 80 > 0 && digitY < slice_h) {
s.setTextWrap(false, false); uint8_t h10 = (gpsHour / 10) % 10;
s.setTextColor(priCol, cardBg); uint8_t h1 = gpsHour % 10;
s.setTextPadding(0); uint8_t m10 = (gpsMinute / 10) % 10;
s.drawRightString(hStr.c_str(), clkX + 142, digitY, 8); uint8_t m1 = gpsMinute % 10;
s.drawString(mStr.c_str(), clkX + 170, digitY, 8);
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);
} }
} }

View file

@ -14,6 +14,7 @@ extern TFT_eSPI tft;
extern uint16_t getThemePrimaryColor(); extern uint16_t getThemePrimaryColor();
extern uint16_t getThemeBgColor(); extern uint16_t getThemeBgColor();
extern void touchBeep(); extern void touchBeep();
extern String warningValue[5];
// Global states for multi-page system // Global states for multi-page system
extern int8_t fullScreenCell; extern int8_t fullScreenCell;
@ -209,8 +210,10 @@ inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) {
minRange = 60.0f; minRange = 60.0f;
maxRange = 120.0f; maxRange = 120.0f;
isInt = true; isInt = true;
if (val < 100.0f) accentCol = getThemePrimaryColor(); float warnT = warningValue[0].toFloat();
else if (val < 110.0f) accentCol = TFT_YELLOW; if (warnT <= 0) warnT = 110.0f;
if (val < (warnT - 10.0f)) accentCol = getThemePrimaryColor();
else if (val < warnT) accentCol = TFT_YELLOW;
else accentCol = TFT_RED; else accentCol = TFT_RED;
} else if (cardIdx == 1) { } else if (cardIdx == 1) {
titleStr = "INTERIOR"; titleStr = "INTERIOR";
@ -307,12 +310,14 @@ inline void updateTriTempCardValue(uint8_t cardIdx, float val, float oldVal) {
minRange = 60.0f; minRange = 60.0f;
maxRange = 120.0f; maxRange = 120.0f;
isInt = true; isInt = true;
if (val < 100.0f) accentCol = getThemePrimaryColor(); float warnT = warningValue[0].toFloat();
else if (val < 110.0f) accentCol = TFT_YELLOW; if (warnT <= 0) warnT = 110.0f;
if (val < (warnT - 10.0f)) accentCol = getThemePrimaryColor();
else if (val < warnT) accentCol = TFT_YELLOW;
else accentCol = TFT_RED; else accentCol = TFT_RED;
if (oldVal < 100.0f) oldAccentCol = getThemePrimaryColor(); if (oldVal < (warnT - 10.0f)) oldAccentCol = getThemePrimaryColor();
else if (oldVal < 110.0f) oldAccentCol = TFT_YELLOW; else if (oldVal < warnT) oldAccentCol = TFT_YELLOW;
else oldAccentCol = TFT_RED; else oldAccentCol = TFT_RED;
// Subtitle update if ignition state changed // Subtitle update if ignition state changed
@ -398,8 +403,10 @@ inline void syncTriTempTrackers() {
uint16_t acc = defAcc[i]; uint16_t acc = defAcc[i];
if (i == 0) { if (i == 0) {
if (vals[i] >= 110.0f) acc = TFT_RED; float warnT = warningValue[0].toFloat();
else if (vals[i] >= 100.0f) acc = TFT_YELLOW; 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; oldTriAccentCol[i] = acc;
} }