From 4937cf3b425b9e98984e7b5cc66f2638ec939e9c Mon Sep 17 00:00:00 2001 From: Sebastian Nieto Date: Sun, 11 Oct 2026 19:44:29 -0300 Subject: [PATCH] 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 --- sketch/obd2_gauge/config.h | 294 +++++++++++++++++++++++++++++++++- sketch/obd2_gauge/gps_clock.h | 65 +++++++- sketch/obd2_gauge/tri_temp.h | 23 ++- 3 files changed, 360 insertions(+), 22 deletions(-) diff --git a/sketch/obd2_gauge/config.h b/sketch/obd2_gauge/config.h index 1ef87c4..124e31e 100644 --- a/sketch/obd2_gauge/config.h +++ b/sketch/obd2_gauge/config.h @@ -15,6 +15,9 @@ extern bool wokeFromSleep; extern uint8_t transitionType; extern volatile bool systemSleeping; 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 static uint32_t lastMenuActivityTime = 0; @@ -216,12 +219,6 @@ void warningMenu() { } // 2. Bottom button [ SALIR ] 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(); break; } @@ -232,6 +229,287 @@ void warningMenu() { } 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) { select = executeSelect; - // Option 0: Alarmas (ON/OFF) + // Option 0: Alarmas (Menu de Activacion y Umbrales) if (select == 0) { - warningMenu(); + alarmCategoryMenu(); if (menuExitedDueToTimeout) break; tft.fillScreen(TFT_BLACK); drawConfigHeader(); diff --git a/sketch/obd2_gauge/gps_clock.h b/sketch/obd2_gauge/gps_clock.h index a751773..2e9ef79 100644 --- a/sketch/obd2_gauge/gps_clock.h +++ b/sketch/obd2_gauge/gps_clock.h @@ -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); } +// 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) inline void syncGpsClockTrackers() { 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); int digitY = clkY + 11; - if (digitY + 75 > 0 && digitY < slice_h) { - s.setTextWrap(false, false); - s.setTextColor(priCol, cardBg); - s.setTextPadding(0); - s.drawRightString(hStr.c_str(), clkX + 142, digitY, 8); - s.drawString(mStr.c_str(), clkX + 170, digitY, 8); + if (digitY + 80 > 0 && digitY < slice_h) { + uint8_t h10 = (gpsHour / 10) % 10; + uint8_t h1 = gpsHour % 10; + 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); } } diff --git a/sketch/obd2_gauge/tri_temp.h b/sketch/obd2_gauge/tri_temp.h index 4e3ff91..da71d34 100644 --- a/sketch/obd2_gauge/tri_temp.h +++ b/sketch/obd2_gauge/tri_temp.h @@ -14,6 +14,7 @@ extern TFT_eSPI tft; extern uint16_t getThemePrimaryColor(); extern uint16_t getThemeBgColor(); extern void touchBeep(); +extern String warningValue[5]; // Global states for multi-page system extern int8_t fullScreenCell; @@ -209,8 +210,10 @@ inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) { minRange = 60.0f; maxRange = 120.0f; isInt = true; - if (val < 100.0f) accentCol = getThemePrimaryColor(); - else if (val < 110.0f) accentCol = TFT_YELLOW; + float warnT = warningValue[0].toFloat(); + 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 if (cardIdx == 1) { titleStr = "INTERIOR"; @@ -307,12 +310,14 @@ inline void updateTriTempCardValue(uint8_t cardIdx, float val, float oldVal) { minRange = 60.0f; maxRange = 120.0f; isInt = true; - if (val < 100.0f) accentCol = getThemePrimaryColor(); - else if (val < 110.0f) accentCol = TFT_YELLOW; + float warnT = warningValue[0].toFloat(); + if (warnT <= 0) warnT = 110.0f; + if (val < (warnT - 10.0f)) accentCol = getThemePrimaryColor(); + else if (val < warnT) accentCol = TFT_YELLOW; else accentCol = TFT_RED; - if (oldVal < 100.0f) oldAccentCol = getThemePrimaryColor(); - else if (oldVal < 110.0f) oldAccentCol = TFT_YELLOW; + if (oldVal < (warnT - 10.0f)) oldAccentCol = getThemePrimaryColor(); + else if (oldVal < warnT) oldAccentCol = TFT_YELLOW; else oldAccentCol = TFT_RED; // Subtitle update if ignition state changed @@ -398,8 +403,10 @@ inline void syncTriTempTrackers() { uint16_t acc = defAcc[i]; if (i == 0) { - if (vals[i] >= 110.0f) acc = TFT_RED; - else if (vals[i] >= 100.0f) acc = TFT_YELLOW; + 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; }