obd2_gauge/sketch/obd2_gauge/config.h

1570 lines
50 KiB
C

/* "config.h"
Configuration menu for OBD2 Gauge with volume and per-instrument alarm controls
*/
#include <esp_sleep.h>
#include <driver/gpio.h>
#include <driver/rtc_io.h>
// Inactivity Auto-Sleep settings (0: OFF, 1: 30s, 2: 1m, 3: 2m, 4: 5m)
const uint16_t AUTO_SLEEP_SEC_TABLE[5] = { 0, 30, 60, 120, 300 };
const char* const AUTO_SLEEP_NAME_TABLE[5] = { "OFF", "30s", "1m", "2m", "5m" };
extern uint8_t autoSleepIndex;
extern uint32_t lastActivityTime;
extern uint8_t beepSoundType;
extern bool wokeFromSleep;
/*------------ SOUND EFFECTS ----------*/
const char* const BEEP_NAME_TABLE[6] = {
"Casio Chirp",
"Click Corto",
"Tono Suave",
"Tick Digital",
"Doble Pip",
"Campana Retro"
};
void playBeepSample(uint8_t type, uint8_t vol) {
if (vol == 0) return;
uint16_t duty = VOL_DUTY_TABLE[vol > 5 ? 3 : vol];
if (type == 0) { // Casio Chirp (4096 Hz, 25ms)
ledcSetup(buzzerChannel, 4096, 10);
ledcWrite(buzzerChannel, duty);
delay(25);
ledcWrite(buzzerChannel, 0);
} else if (type == 1) { // Click Corto (3200 Hz, 12ms)
ledcSetup(buzzerChannel, 3200, 10);
ledcWrite(buzzerChannel, duty);
delay(12);
ledcWrite(buzzerChannel, 0);
} else if (type == 2) { // Tono Suave (2400 Hz, 28ms)
ledcSetup(buzzerChannel, 2400, 10);
ledcWrite(buzzerChannel, duty);
delay(28);
ledcWrite(buzzerChannel, 0);
} else if (type == 3) { // Tick Digital (4800 Hz, 8ms)
ledcSetup(buzzerChannel, 4800, 10);
ledcWrite(buzzerChannel, duty);
delay(8);
ledcWrite(buzzerChannel, 0);
} else if (type == 4) { // Doble Pip (3500 Hz 12ms -> 4500 Hz 15ms)
ledcSetup(buzzerChannel, 3500, 10);
ledcWrite(buzzerChannel, duty);
delay(12);
ledcWrite(buzzerChannel, 0);
delay(15);
ledcSetup(buzzerChannel, 4500, 10);
ledcWrite(buzzerChannel, duty);
delay(15);
ledcWrite(buzzerChannel, 0);
} else if (type == 5) { // Campana Retro (2093 Hz Do7, 35ms)
ledcSetup(buzzerChannel, 2093, 10);
ledcWrite(buzzerChannel, duty);
delay(35);
ledcWrite(buzzerChannel, 0);
}
}
void touchBeep() {
playBeepSample(beepSoundType, buzzerVolumeLevel);
}
// Crisp High-Pitch Casio Chirp (redirects to touchBeep when freq == 4096)
inline void casioBeep(uint16_t freq = 4096, uint8_t duration_ms = 25) {
if (freq == 4096) {
touchBeep();
return;
}
uint16_t duty = VOL_DUTY_TABLE[buzzerVolumeLevel > 5 ? 3 : buzzerVolumeLevel];
if (duty == 0) return; // Mute mode
ledcSetup(buzzerChannel, freq, 10);
ledcWrite(buzzerChannel, duty);
delay(duration_ms);
ledcWrite(buzzerChannel, 0);
}
void beepbeep() {
touchBeep();
delay(35);
touchBeep();
}
void beep() {
touchBeep();
}
void clickSound() {
touchBeep();
}
// Configuration menu list (7 options)
const String menuList[7] = {
"Alarmas", // 0
"Sonidos", // 1
"Brillo de Pantalla", // 2
"Apariencia", // 3
"Modo Dormir", // 4
"Reiniciar", // 5
"Salir" // 6
};
const uint8_t maxMenu = 7;
// Menu list for warning setting (4 vehicle gauges)
const String parameterList[4][2] = {
{"Temperatura", " `C"},
{"RPM", " rpm"},
{"Voltaje", " V"},
{"Velocidad", " km/h"}
};
// -------------------------------------------------------------
// Componente de Boton SALIR con Animacion de Pulsacion Tactil
// -------------------------------------------------------------
void drawExitButton(bool pressed) {
uint16_t bgCol = pressed ? getThemePrimaryColor() : getThemeAccentColor();
uint16_t txtCol = pressed ? TFT_BLACK : TFT_WHITE;
uint16_t borderCol = pressed ? TFT_WHITE : getThemeDarkColor();
tft.fillRoundRect(80, 186, 160, 42, 5, bgCol);
tft.drawRoundRect(80, 186, 160, 42, 5, borderCol);
if (pressed) {
tft.drawRoundRect(81, 187, 158, 40, 4, TFT_WHITE);
}
tft.setTextColor(txtCol, bgCol);
tft.drawCentreString("SALIR", 160, 194, 4);
}
void animateExitButtonPress() {
drawExitButton(true);
touchBeep();
delay(120);
waitForTouchRelease();
}
// -------------------------------------------------------------
// Sub-screen 0: Ajuste de Alarmas (Activacion Tactil ON/OFF)
// -------------------------------------------------------------
void warningMenu() {
tft.fillScreen(TFT_BLACK);
auto drawAllRows = [&]() {
uint16_t onCol = getThemePrimaryColor(); // Clean active accent
for (uint8_t i = 0; i < 4; i++) {
int y = 10 + i * 44;
tft.fillRoundRect(8, y, 304, 38, 5, getThemeDarkColor());
tft.drawRoundRect(8, y, 304, 38, 5, getThemeAccentColor());
if (alarmEnabled[i]) {
tft.fillRoundRect(10, y + 4, 3, 30, 1, onCol);
}
tft.setTextColor(alarmEnabled[i] ? TFT_WHITE : 0x8410, getThemeDarkColor());
tft.drawString(parameterList[i][0], 22, y + 7, 4);
// Large tactile ON / OFF badge (completely red-free: neutral slate for OFF, theme accent for ON)
uint16_t badgeCol = alarmEnabled[i] ? onCol : 0x2124;
uint16_t txtCol = alarmEnabled[i] ? TFT_BLACK : 0xAD55;
uint16_t borderCol = alarmEnabled[i] ? TFT_WHITE : 0x4A69;
tft.fillRoundRect(220, y + 4, 84, 30, 5, badgeCol);
tft.drawRoundRect(220, y + 4, 84, 30, 5, borderCol);
tft.setTextColor(txtCol, badgeCol);
tft.drawCentreString(alarmEnabled[i] ? "ON" : "OFF", 262, y + 7, 4);
}
};
drawAllRows(); // Initial draw of all alarm rows with ON/OFF badges
// Bottom action button: [ SALIR ] (x: 80, y: 186, w: 160, h: 42)
drawExitButton(false);
waitForTouchRelease();
while (true) {
autoDim();
uint16_t tx = 0, ty = 0;
if (getTouch(&tx, &ty)) {
// 1. Direct touch on any parameter row to toggle ON/OFF
if (ty >= 6 && ty < 184) {
int8_t row = (ty - 10) / 44;
if (row >= 0 && row < 4) {
alarmEnabled[row] = !alarmEnabled[row];
drawAllRows();
touchBeep();
waitForTouchRelease();
}
}
// 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;
}
} else if (digitalRead(SELECTOR_PIN) == LOW) {
delay(200);
break;
}
yield();
}
}
// -------------------------------------------------------------
// Sub-screen 1: Sonido de Alarma (Tipo de Sonido y Volumen)
// -------------------------------------------------------------
const char* const ALARM_NAME_TABLE[6] = {
"Chime BMW",
"Mercedes Gong",
"Audi Cockpit",
"Porsche Sport",
"Casio Clasico",
"Triple Urgente"
};
void playAlarmSample(uint8_t type, uint8_t vol) {
if (vol == 0) return;
uint16_t duty = VOL_DUTY_TABLE[vol > 5 ? 3 : vol];
if (type == 0) { // Chime BMW (Mi7 -> Do7, 2637 Hz -> 2093 Hz)
ledcSetup(buzzerChannel, 2637, 10);
ledcWrite(buzzerChannel, duty);
delay(60);
ledcWrite(buzzerChannel, 0);
delay(15);
ledcSetup(buzzerChannel, 2093, 10);
ledcWrite(buzzerChannel, duty);
delay(110);
ledcWrite(buzzerChannel, 0);
} else if (type == 1) { // Mercedes Gong (Re7 -> Fa#7, 2349 Hz -> 2960 Hz)
ledcSetup(buzzerChannel, 2349, 10);
ledcWrite(buzzerChannel, duty);
delay(45);
ledcWrite(buzzerChannel, 0);
delay(20);
ledcSetup(buzzerChannel, 2960, 10);
ledcWrite(buzzerChannel, duty);
delay(85);
ledcWrite(buzzerChannel, 0);
} else if (type == 2) { // Audi Cockpit (Do7 -> Mi7 -> Sol7, 2093 -> 2637 -> 3136 Hz)
ledcSetup(buzzerChannel, 2093, 10);
ledcWrite(buzzerChannel, duty);
delay(35);
ledcSetup(buzzerChannel, 2637, 10);
ledcWrite(buzzerChannel, duty);
delay(35);
ledcSetup(buzzerChannel, 3136, 10);
ledcWrite(buzzerChannel, duty);
delay(65);
ledcWrite(buzzerChannel, 0);
} else if (type == 3) { // Porsche Sport (Doble staccato rapido 3500 Hz)
ledcSetup(buzzerChannel, 3500, 10);
ledcWrite(buzzerChannel, duty);
delay(30);
ledcWrite(buzzerChannel, 0);
delay(25);
ledcSetup(buzzerChannel, 3500, 10);
ledcWrite(buzzerChannel, duty);
delay(30);
ledcWrite(buzzerChannel, 0);
} else if (type == 4) { // Casio Clasico (Doble chirp 4096 Hz)
ledcSetup(buzzerChannel, 4096, 10);
ledcWrite(buzzerChannel, duty);
delay(24);
ledcWrite(buzzerChannel, 0);
delay(35);
ledcSetup(buzzerChannel, 4096, 10);
ledcWrite(buzzerChannel, duty);
delay(24);
ledcWrite(buzzerChannel, 0);
} else if (type == 5) { // Triple Urgente (3 pulsos rapidos 4096 Hz)
for (uint8_t k = 0; k < 3; k++) {
ledcSetup(buzzerChannel, 4096, 10);
ledcWrite(buzzerChannel, duty);
delay(22);
ledcWrite(buzzerChannel, 0);
if (k < 2) delay(25);
}
}
}
void alarmSoundMenu() {
tft.fillScreen(TFT_BLACK);
auto drawAlarmUI = []() {
tft.fillRect(0, 0, 320, 186, TFT_BLACK);
// 6 Pattern Tiles (3 rows x 2 columns) from y = 8 to y = 120
for (uint8_t i = 0; i < 6; i++) {
int16_t x = (i % 2 == 0) ? 10 : 166;
int16_t y = 8 + (i / 2) * 38;
int16_t w = 144;
int16_t h = 34;
bool isSel = (i == alarmSoundType);
uint16_t bg = isSel ? getThemeAccentColor() : getThemeDarkColor();
uint16_t border = isSel ? getThemePrimaryColor() : getThemeAccentColor();
tft.fillRoundRect(x, y, w, h, 5, bg);
tft.drawRoundRect(x, y, w, h, 5, border);
if (isSel) {
tft.drawRoundRect(x + 1, y + 1, w - 2, h - 2, 4, TFT_WHITE);
}
tft.setTextColor(isSel ? TFT_WHITE : 0xBDD7, bg);
tft.drawCentreString(ALARM_NAME_TABLE[i], x + w / 2, y + 9, 2);
}
// 6 Direct Touch Tiles (0 to 5) for Alarm Volume from y = 126 to y = 180
for (uint8_t i = 0; i <= 5; i++) {
int16_t bx = 7 + i * 52;
int16_t by = 126;
int16_t bw = 46;
int16_t bh = 54;
bool isSel = (i == alarmVolumeLevel);
uint16_t bgCol = isSel ? (i == 0 ? 0x3187 : getThemeAccentColor()) : getThemeDarkColor();
uint16_t borderCol = isSel ? (i == 0 ? TFT_WHITE : getThemePrimaryColor()) : getThemeAccentColor();
tft.fillRoundRect(bx, by, bw, bh, 5, bgCol);
tft.drawRoundRect(bx, by, bw, bh, 5, borderCol);
if (isSel) {
tft.drawRoundRect(bx + 1, by + 1, bw - 2, bh - 2, 4, TFT_WHITE);
}
tft.setTextColor(isSel ? TFT_WHITE : 0xAD55, bgCol);
tft.drawCentreString(String(i).c_str(), bx + bw / 2, by + 8, 6);
}
};
drawAlarmUI(); // Initial draw of sound cards and volume tiles
// Bottom action button: [ SALIR ] (x: 80, y: 186, w: 160, h: 42)
drawExitButton(false);
waitForTouchRelease();
while (true) {
autoDim();
uint16_t tx = 0, ty = 0;
if (getTouch(&tx, &ty)) {
// 1. Touch on pattern selection (y: 0 to 122)
if (ty < 122) {
int8_t col = (tx < 160) ? 0 : 1;
int8_t row = (ty - 6) / 38;
if (row < 0) row = 0;
if (row > 2) row = 2;
uint8_t selType = row * 2 + col;
if (selType < 6) {
alarmSoundType = selType;
drawAlarmUI();
playAlarmSample(alarmSoundType, alarmVolumeLevel);
}
waitForTouchRelease();
}
// 2. Touch on alarm volume tiles (y: 122 to 184)
else if (ty >= 122 && ty < 184) {
int8_t touchedLvl = (tx - 7) / 52;
if (touchedLvl >= 0 && touchedLvl <= 5) {
alarmVolumeLevel = touchedLvl;
drawAlarmUI();
playAlarmSample(alarmSoundType, alarmVolumeLevel);
}
waitForTouchRelease();
}
// 3. Bottom button [ SALIR ] (y: 180 to 240)
else if (ty >= 180 && tx >= 20 && tx <= 300) {
pref.begin("setting", false);
pref.putUChar("alm_vol", alarmVolumeLevel);
pref.putUChar("alm_type", alarmSoundType);
pref.end();
animateExitButtonPress();
break;
}
} else if (digitalRead(SELECTOR_PIN) == LOW) {
delay(200);
break;
}
yield();
}
}
// -------------------------------------------------------------
// Sub-screen 2: Sonido de Bip (Tipo de Sonido y Volumen)
// -------------------------------------------------------------
void beepSoundMenu() {
tft.fillScreen(TFT_BLACK);
auto drawBeepUI = []() {
tft.fillRect(0, 0, 320, 186, TFT_BLACK);
// 6 Pattern Tiles (3 rows x 2 columns) from y = 8 to y = 120
for (uint8_t i = 0; i < 6; i++) {
int16_t x = (i % 2 == 0) ? 10 : 166;
int16_t y = 8 + (i / 2) * 38;
int16_t w = 144;
int16_t h = 34;
bool isSel = (i == beepSoundType);
uint16_t bg = isSel ? getThemeAccentColor() : getThemeDarkColor();
uint16_t border = isSel ? getThemePrimaryColor() : getThemeAccentColor();
tft.fillRoundRect(x, y, w, h, 5, bg);
tft.drawRoundRect(x, y, w, h, 5, border);
if (isSel) {
tft.drawRoundRect(x + 1, y + 1, w - 2, h - 2, 4, TFT_WHITE);
}
tft.setTextColor(isSel ? TFT_WHITE : 0xBDD7, bg);
tft.drawCentreString(BEEP_NAME_TABLE[i], x + w / 2, y + 9, 2);
}
// 6 Direct Touch Tiles (0 to 5) for Beep Volume from y = 126 to y = 180
for (uint8_t i = 0; i <= 5; i++) {
int16_t bx = 7 + i * 52;
int16_t by = 126;
int16_t bw = 46;
int16_t bh = 54;
bool isSel = (i == buzzerVolumeLevel);
uint16_t bgCol = isSel ? (i == 0 ? 0x3187 : getThemeAccentColor()) : getThemeDarkColor();
uint16_t borderCol = isSel ? (i == 0 ? TFT_WHITE : getThemePrimaryColor()) : getThemeAccentColor();
tft.fillRoundRect(bx, by, bw, bh, 5, bgCol);
tft.drawRoundRect(bx, by, bw, bh, 5, borderCol);
if (isSel) {
tft.drawRoundRect(bx + 1, by + 1, bw - 2, bh - 2, 4, TFT_WHITE);
}
tft.setTextColor(isSel ? TFT_WHITE : 0xAD55, bgCol);
tft.drawCentreString(String(i).c_str(), bx + bw / 2, by + 8, 6);
}
};
drawBeepUI(); // Initial draw of sound cards and volume tiles
// Bottom action button: [ SALIR ] (x: 80, y: 186, w: 160, h: 42)
drawExitButton(false);
waitForTouchRelease();
while (true) {
autoDim();
uint16_t tx = 0, ty = 0;
if (getTouch(&tx, &ty)) {
// 1. Touch on pattern selection (y: 0 to 122)
if (ty < 122) {
int8_t col = (tx < 160) ? 0 : 1;
int8_t row = (ty - 6) / 38;
if (row < 0) row = 0;
if (row > 2) row = 2;
uint8_t selType = row * 2 + col;
if (selType < 6) {
beepSoundType = selType;
drawBeepUI();
playBeepSample(beepSoundType, buzzerVolumeLevel);
}
waitForTouchRelease();
}
// 2. Touch on beep volume tiles (y: 122 to 184)
else if (ty >= 122 && ty < 184) {
int8_t touchedLvl = (tx - 7) / 52;
if (touchedLvl >= 0 && touchedLvl <= 5) {
buzzerVolumeLevel = touchedLvl;
drawBeepUI();
playBeepSample(beepSoundType, buzzerVolumeLevel);
}
waitForTouchRelease();
}
// 3. Bottom button [ SALIR ] (y: 180 to 240)
else if (ty >= 180 && tx >= 20 && tx <= 300) {
pref.begin("setting", false);
pref.putUChar("b_vol", buzzerVolumeLevel);
pref.putUChar("b_type", beepSoundType);
pref.end();
animateExitButtonPress();
break;
}
} else if (digitalRead(SELECTOR_PIN) == LOW) {
delay(200);
break;
}
yield();
}
}
void volumeMenu() {
beepSoundMenu();
}
// -------------------------------------------------------------
// Sub-screen 3: Modo Reposo / Sleep (Manual y por Inactividad)
// -------------------------------------------------------------
void enterLightSleep() {
// Acoustic power-down feedback (two gentle tones descending)
casioBeep(3500, 20);
delay(30);
casioBeep(2200, 30);
// 1. Smooth fade out of backlight
for (int b = (int)currentBacklight; b >= 0; b -= 25) {
ledcWrite(backlightChannel, b < 0 ? 0 : b);
delay(10);
}
ledcWrite(backlightChannel, 0);
currentBacklight = 0.0f;
targetBacklight = 0;
// 2. Put display controller to sleep
tft.writecommand(0x10); // ST7789_SLPIN
delay(120);
// Turn OFF all rear RGB LEDs completely during sleep mode
setRearLedOff();
digitalWrite(LED_RED_PIN, HIGH);
digitalWrite(LED_GREEN_PIN, HIGH);
digitalWrite(LED_BLUE_PIN, HIGH);
// Cleanly disconnect and terminate Bluetooth before sleep to release remote link
if (BTSerial.hasClient() || BTSerial.connected() || foundOBD2) {
BTSerial.disconnect();
delay(50);
}
BTSerial.end();
foundOBD2 = false;
// 3. Configure wakeup sources:
// GPIO 36: XPT2046 Touch IRQ (PENIRQ), pulls LOW when touched
// GPIO 27: Selector Button (SELECTOR_PIN), pulls LOW when pressed
esp_sleep_enable_ext0_wakeup(GPIO_NUM_36, 0);
gpio_wakeup_enable((gpio_num_t)SELECTOR_PIN, GPIO_INTR_LOW_LEVEL);
esp_sleep_enable_gpio_wakeup();
// 4. Enter Light Sleep
esp_light_sleep_start();
// === WAKE UP ===
gpio_wakeup_disable((gpio_num_t)SELECTOR_PIN);
rtc_gpio_deinit(GPIO_NUM_36);
// Restart Bluetooth controller stack fresh
BTSerial.begin("OBD2 Gauge", true);
// Restore rear RGB LED status in selected theme color if connected
if (foundOBD2) {
updateRearLedTheme();
}
// 5. Wake LCD controller
tft.writecommand(0x11); // ST7789_SLPOUT
delay(120);
// 6. Restore backlight smoothly according to current brightness setting (Auto LDR or Manual)
uint8_t wakeTarget = 255;
if (brightnessIndex > 0) {
wakeTarget = BRIGHTNESS_PWM_TABLE[brightnessIndex > 5 ? 5 : brightnessIndex];
} else {
// Mode Auto: sample LDR sensor immediately to determine day or night
int light = analogRead(LDR_PIN);
if (light > LIGHT_LEVEL) { // Low light (night)
wakeTarget = 15;
dim = true;
low_count = 6;
high_count = 0;
} else { // Bright light (day)
wakeTarget = 255;
dim = false;
high_count = 6;
low_count = 0;
}
}
// Smooth fade in to target brightness
for (int b = 0; b <= (int)wakeTarget; b += 15) {
ledcWrite(backlightChannel, b > wakeTarget ? wakeTarget : b);
delay(10);
}
ledcWrite(backlightChannel, wakeTarget);
currentBacklight = (float)wakeTarget;
targetBacklight = wakeTarget;
// 7. Wake acoustic confirmation chirp
touchBeep();
// 8. Wait for touch to be fully released
waitForTouchRelease();
wokeFromSleep = true;
}
void drawSleepHeader() {
tft.fillRectVGradient(0, 0, 320, 26, getThemeAccentColor(), getThemeDarkColor());
tft.fillRect(0, 26, 320, 2, getThemePrimaryColor());
tft.setTextColor(TFT_WHITE);
tft.drawCentreString("Modo Dormir", 160, 2, 4);
}
void sleepMenu() {
tft.fillScreen(TFT_BLACK);
drawSleepHeader();
uint8_t subSel = 255; // 0: DORMIR, 1: SALIR
auto drawSleepBottomButton = [&](uint8_t idx, bool isSel, bool isPressed) {
int16_t bx = (idx == 0) ? 14 : 166;
int16_t by = 166;
int16_t bw = 140;
int16_t bh = 48;
const char* label = (idx == 0) ? "DORMIR" : "SALIR";
uint16_t bgCol = isPressed ? getThemePrimaryColor() : getThemeAccentColor();
uint16_t txtCol = isPressed ? TFT_BLACK : TFT_WHITE;
uint16_t borderCol = (isPressed || isSel) ? getThemePrimaryColor() : getThemeDarkColor();
tft.fillRoundRect(bx, by, bw, bh, 6, bgCol);
tft.drawRoundRect(bx, by, bw, bh, 6, borderCol);
if (isPressed || isSel) {
tft.drawRoundRect(bx + 1, by + 1, bw - 2, bh - 2, 5, TFT_WHITE);
}
tft.setTextColor(txtCol, bgCol);
tft.drawCentreString(label, bx + bw / 2, by + 12, 4);
};
auto drawSleepBottomButtons = [&](uint8_t sel) {
drawSleepBottomButton(0, sel == 0, false);
drawSleepBottomButton(1, sel == 1, false);
};
auto drawSleepUI = [&]() {
tft.fillRect(0, 28, 320, 134, TFT_BLACK);
// 1. Inactivity Sleep Section Label
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.drawCentreString("Auto-Sleep por Inactividad", 160, 38, 4);
tft.setTextColor(0xBDD7, TFT_BLACK);
tft.drawCentreString("(Apagado al detenerse el motor)", 160, 68, 2);
// 2. 5 Direct Touch Tiles for Auto-Sleep Timeout: [OFF] [30s] [1m] [2m] [5m]
for (uint8_t i = 0; i < 5; i++) {
int16_t bx = 8 + i * 62;
int16_t by = 94;
int16_t bw = 56;
int16_t bh = 54;
bool isSel = (i == autoSleepIndex);
uint16_t bgCol = isSel ? (i == 0 ? 0x3187 : getThemeAccentColor()) : getThemeDarkColor();
uint16_t borderCol = isSel ? (i == 0 ? TFT_WHITE : getThemePrimaryColor()) : getThemeAccentColor();
tft.fillRoundRect(bx, by, bw, bh, 5, bgCol);
tft.drawRoundRect(bx, by, bw, bh, 5, borderCol);
if (isSel) {
tft.drawRoundRect(bx + 1, by + 1, bw - 2, bh - 2, 4, TFT_WHITE);
}
tft.setTextColor(isSel ? TFT_WHITE : 0xBDD7, bgCol);
tft.drawCentreString(AUTO_SLEEP_NAME_TABLE[i], bx + bw / 2, by + 14, 4);
}
};
drawSleepUI();
drawSleepBottomButtons(subSel);
waitForTouchRelease();
bool pressed = false;
long holdtimer = 0;
while (true) {
autoDim();
uint16_t tx = 0, ty = 0;
if (getTouch(&tx, &ty)) {
// 1. Tap on Auto-Sleep Timeout Tiles (y: 88 to 154)
if (ty >= 88 && ty <= 154) {
int8_t touchedTile = (tx - 8) / 62;
if (touchedTile >= 0 && touchedTile < 5) {
autoSleepIndex = touchedTile;
drawSleepUI();
touchBeep();
pref.begin("setting", false);
pref.putUChar("auto_sleep", autoSleepIndex);
pref.end();
waitForTouchRelease();
}
}
// 2. Tap on Bottom Buttons (y: 158 to 230)
else if (ty >= 158 && ty <= 230) {
if (tx >= 10 && tx <= 158) {
// [ DORMIR ]
drawSleepBottomButton(0, true, true);
touchBeep();
delay(120);
waitForTouchRelease();
enterLightSleep();
if (wokeFromSleep) break;
// Redraw UI upon waking up
tft.fillScreen(TFT_BLACK);
drawSleepHeader();
drawSleepUI();
drawSleepBottomButtons(subSel);
waitForTouchRelease();
} else if (tx >= 162 && tx <= 310) {
// [ SALIR ]
drawSleepBottomButton(1, true, true);
touchBeep();
delay(120);
waitForTouchRelease();
break;
}
}
} else if (digitalRead(SELECTOR_PIN) == LOW) {
if (!pressed) {
pressed = true;
holdtimer = millis();
} else if (millis() - holdtimer > 1000) {
// Long press
if (subSel == 0) {
drawSleepBottomButton(0, true, true);
touchBeep();
delay(120);
enterLightSleep();
if (wokeFromSleep) break;
tft.fillScreen(TFT_BLACK);
drawSleepHeader();
drawSleepUI();
drawSleepBottomButtons(subSel);
waitForTouchRelease();
} else {
drawSleepBottomButton(1, true, true);
touchBeep();
delay(120);
break;
}
}
} else {
if (pressed) {
if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1000) {
subSel = (subSel == 0) ? 1 : 0;
drawSleepBottomButtons(subSel);
clickSound();
}
pressed = false;
delay(150);
}
}
yield();
}
}
// -------------------------------------------------------------
// Sub-screen: Confirmacion de Reinicio de Placa
// -------------------------------------------------------------
void restartConfirmationMenu() {
tft.fillScreen(TFT_BLACK);
tft.fillRectVGradient(0, 0, 320, 26, getThemeAccentColor(), getThemeDarkColor());
tft.fillRect(0, 26, 320, 2, getThemePrimaryColor());
tft.setTextColor(TFT_WHITE);
tft.drawCentreString("Reiniciar Dispositivo", 160, 2, 4);
// Themed confirmation dialog card
tft.fillRoundRect(14, 44, 292, 104, 6, getThemeDarkColor());
tft.drawRoundRect(14, 44, 292, 104, 6, getThemeAccentColor());
tft.fillRoundRect(18, 50, 4, 92, 2, getThemePrimaryColor()); // Left accent pill
tft.setTextColor(TFT_WHITE, getThemeDarkColor());
tft.drawCentreString("Desea reiniciar la placa?", 160, 56, 4);
tft.setTextColor(0xBDD7, getThemeDarkColor());
tft.drawCentreString("El sistema se reiniciara", 160, 92, 2);
tft.drawCentreString("y volvera a conectar con el OBD2", 160, 112, 2);
uint8_t subSel = 1; // 0: REINICIAR, 1: SALIR (por defecto SALIR para seguridad)
auto drawSubButton = [&](uint8_t idx, bool isSel, bool isPressed) {
int16_t bx = (idx == 0) ? 14 : 166;
int16_t by = 166;
int16_t bw = 140;
int16_t bh = 48;
const char* label = (idx == 0) ? "REINICIAR" : "SALIR";
uint16_t bgCol = isPressed ? getThemePrimaryColor() : getThemeAccentColor();
uint16_t txtCol = isPressed ? TFT_BLACK : TFT_WHITE;
uint16_t borderCol = (isPressed || isSel) ? getThemePrimaryColor() : getThemeDarkColor();
tft.fillRoundRect(bx, by, bw, bh, 6, bgCol);
tft.drawRoundRect(bx, by, bw, bh, 6, borderCol);
if (isPressed || isSel) {
tft.drawRoundRect(bx + 1, by + 1, bw - 2, bh - 2, 5, TFT_WHITE);
}
tft.setTextColor(txtCol, bgCol);
tft.drawCentreString(label, bx + bw / 2, by + 12, 4);
};
auto drawSubButtons = [&](uint8_t sel) {
drawSubButton(0, sel == 0, false);
drawSubButton(1, sel == 1, false);
};
drawSubButtons(subSel);
waitForTouchRelease();
bool pressed = false;
long holdtimer = 0;
while (true) {
autoDim();
uint16_t tx = 0, ty = 0;
if (getTouch(&tx, &ty)) {
if (ty >= 155 && ty <= 225) {
if (tx >= 10 && tx <= 158) {
// Confirmado: Reiniciar!
drawSubButton(0, true, true);
touchBeep();
delay(120);
waitForTouchRelease();
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.drawCentreString("Reiniciando...", 160, 105, 4);
delay(400);
ESP.restart();
} else if (tx >= 162 && tx <= 310) {
// Salir
drawSubButton(1, true, true);
touchBeep();
delay(120);
waitForTouchRelease();
break;
}
}
} else if (digitalRead(SELECTOR_PIN) == LOW) {
if (!pressed) {
pressed = true;
holdtimer = millis();
} else if (millis() - holdtimer > 1000) {
// Pulsacion larga: ejecutar opcion seleccionada
if (subSel == 0) {
drawSubButton(0, true, true);
touchBeep();
delay(120);
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.drawCentreString("Reiniciando...", 160, 105, 4);
delay(400);
ESP.restart();
} else {
drawSubButton(1, true, true);
touchBeep();
delay(120);
break;
}
}
} else {
if (pressed) {
if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1000) {
subSel = (subSel == 0) ? 1 : 0;
drawSubButtons(subSel);
clickSound();
}
pressed = false;
delay(150);
}
}
yield();
}
}
// -------------------------------------------------------------
// Sub-screen: Brillo de Pantalla (AUTO LDR y 5 Niveles Manuales)
// -------------------------------------------------------------
void brightnessMenu() {
uint8_t curChoice = brightnessIndex;
bool pressed = false;
long holdtimer = 0;
tft.fillScreen(TFT_BLACK);
auto applyBrightness = [](uint8_t idx) {
if (idx == 0) {
int light = analogRead(LDR_PIN);
targetBacklight = (light > LIGHT_LEVEL) ? 15 : 255;
} else {
targetBacklight = BRIGHTNESS_PWM_TABLE[idx > 5 ? 5 : idx];
}
currentBacklight = (float)targetBacklight;
ledcWrite(backlightChannel, targetBacklight);
};
auto drawBrightnessUI = [&]() {
tft.fillRect(0, 0, 320, 180, TFT_BLACK);
// 1. Status Card (y: 16 to 98, h: 82)
tft.fillRoundRect(10, 16, 300, 82, 5, getThemeDarkColor());
tft.drawRoundRect(10, 16, 300, 82, 5, getThemeAccentColor());
tft.fillRoundRect(12, 22, 4, 70, 2, getThemePrimaryColor());
tft.setTextColor(TFT_WHITE, getThemeDarkColor());
if (brightnessIndex == 0) {
tft.drawString("Modo: AUTOMATICO", 24, 24, 4);
tft.setTextColor(0xBDD7, getThemeDarkColor());
int lightVal = analogRead(LDR_PIN);
String ldrText = String("Sensor LDR: ") + (lightVal > LIGHT_LEVEL ? "Noche (Dim 15)" : "Dia (255)");
tft.drawString(ldrText, 24, 50, 2);
} else {
String title = String("Modo: MANUAL (") + BRIGHTNESS_NAME_TABLE[brightnessIndex] + ")";
tft.drawString(title, 24, 24, 4);
tft.setTextColor(0xBDD7, getThemeDarkColor());
String sub = String("Nivel ") + String(brightnessIndex) + " de 5 fijo";
tft.drawString(sub, 24, 50, 2);
}
// Visual Progress Bar (y = 72, x = 20, w = 280, h = 16)
tft.fillRoundRect(20, 72, 280, 16, 4, 0x0841);
tft.drawRoundRect(20, 72, 280, 16, 4, getThemeDarkColor());
int fillW = 0;
if (brightnessIndex == 0) {
fillW = map((int)currentBacklight, 0, 255, 0, 276);
} else {
fillW = (brightnessIndex * 276) / 5;
}
if (fillW > 4) {
uint16_t barCol = (brightnessIndex == 0) ? getThemePrimaryColor() : (brightnessIndex >= 4 ? TFT_ORANGE : getThemeAccentColor());
tft.fillRoundRect(22, 74, fillW, 12, 3, barCol);
}
// 2. 6 Direct Touch Tiles (y: 112 to 170, h: 58)
for (uint8_t i = 0; i < 6; i++) {
int16_t bx = 7 + i * 52;
int16_t by = 112;
int16_t bw = 46;
int16_t bh = 58;
bool isSel = (i == brightnessIndex);
uint16_t bgCol = isSel ? getThemePrimaryColor() : getThemeDarkColor();
uint16_t borderCol = isSel ? TFT_WHITE : getThemeAccentColor();
tft.fillRoundRect(bx, by, bw, bh, 5, bgCol);
tft.drawRoundRect(bx, by, bw, bh, 5, borderCol);
if (isSel) {
tft.drawRoundRect(bx + 1, by + 1, bw - 2, bh - 2, 4, TFT_WHITE);
}
tft.setTextColor(isSel ? TFT_BLACK : 0xAD55, bgCol);
if (i == 0) {
tft.drawCentreString("AUTO", bx + bw / 2, by + 20, 2);
} else {
tft.drawCentreString(String(i).c_str(), bx + bw / 2, by + 10, 6);
}
}
};
drawBrightnessUI();
// Bottom action button: [ SALIR ] (x: 80, y: 186, w: 160, h: 42)
drawExitButton(false);
waitForTouchRelease();
while (true) {
autoDim();
uint16_t tx = 0, ty = 0;
if (getTouch(&tx, &ty)) {
// 1. Direct Touch on Brightness Tiles (y: 106 to 176)
if (ty >= 106 && ty < 176) {
int8_t touchedTile = (tx - 7) / 52;
if (touchedTile >= 0 && touchedTile <= 5) {
brightnessIndex = touchedTile;
applyBrightness(brightnessIndex);
drawBrightnessUI();
clickSound();
}
waitForTouchRelease();
}
// 2. Bottom button [ SALIR ] (y: 180 to 240)
else if (ty >= 180 && tx >= 20 && tx <= 300) {
pref.begin("setting", false);
pref.putUChar("b_bright", brightnessIndex);
pref.end();
animateExitButtonPress();
break;
}
} else if (digitalRead(SELECTOR_PIN) == LOW) {
if (!pressed) {
pressed = true;
holdtimer = millis();
} else if (millis() - holdtimer > 1000) {
pref.begin("setting", false);
pref.putUChar("b_bright", brightnessIndex);
pref.end();
animateExitButtonPress();
break;
}
} else {
if (pressed) {
if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1000) {
curChoice = (curChoice + 1) % 6;
brightnessIndex = curChoice;
applyBrightness(brightnessIndex);
drawBrightnessUI();
clickSound();
}
pressed = false;
delay(150);
}
}
yield();
}
}
// -------------------------------------------------------------
// Sub-screen: Menu de Sonidos (Alarma y Click)
// -------------------------------------------------------------
void soundsMenu() {
uint8_t choice = 255;
bool pressed = false;
long holdtimer = 0;
auto drawSoundsUI = [&](uint8_t sel) {
tft.fillScreen(TFT_BLACK);
// Card 0: Sonido de Alarma (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("Sonido de Alarma", 24, 28, 4);
String almInfo = String(ALARM_NAME_TABLE[alarmSoundType > 5 ? 0 : alarmSoundType]) + " | " +
(alarmVolumeLevel == 0 ? "Silencio" : ("Vol: " + String(alarmVolumeLevel)));
tft.setTextColor((sel == 0) ? TFT_YELLOW : 0xBDD7, bg0);
tft.drawString(almInfo, 24, 58, 2);
// Card 1: Sonido de Click (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("Sonido de Click", 24, 110, 4);
String beepInfo = String(BEEP_NAME_TABLE[beepSoundType > 5 ? 0 : beepSoundType]) + " | " +
(buzzerVolumeLevel == 0 ? "Silencio" : ("Vol: " + String(buzzerVolumeLevel)));
tft.setTextColor((sel == 1) ? TFT_YELLOW : 0xBDD7, bg1);
tft.drawString(beepInfo, 24, 140, 2);
// 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);
}
};
drawSoundsUI(choice);
waitForTouchRelease();
while (true) {
autoDim();
int8_t executeSelect = -1;
uint16_t tx = 0, ty = 0;
if (getTouch(&tx, &ty)) {
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 {
drawSoundsUI(choice);
touchBeep();
waitForTouchRelease();
}
}
} else if (digitalRead(SELECTOR_PIN) == LOW) {
if (!pressed) {
pressed = true;
holdtimer = millis();
} else if (millis() - holdtimer > 1200) {
if (choice != 255) {
executeSelect = choice;
}
pressed = false;
}
} else {
if (pressed) {
if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1200) {
if (choice == 255) choice = 0;
else choice = (choice + 1) % 3;
drawSoundsUI(choice);
clickSound();
}
pressed = false;
delay(150);
}
}
if (executeSelect >= 0) {
if (executeSelect == 0) {
alarmSoundMenu();
choice = 255;
drawSoundsUI(choice);
waitForTouchRelease();
} else if (executeSelect == 1) {
beepSoundMenu();
choice = 255;
drawSoundsUI(choice);
waitForTouchRelease();
} else if (executeSelect == 2) {
animateExitButtonPress();
break;
}
pressed = false;
}
yield();
}
}
// -------------------------------------------------------------
// Menu de Apariencia / Temas
// -------------------------------------------------------------
void drawThemeHeader() {
tft.fillRectVGradient(0, 0, 320, 26, getThemeAccentColor(), getThemeDarkColor());
tft.fillRect(0, 26, 320, 2, getThemePrimaryColor());
tft.setTextColor(TFT_WHITE);
tft.drawCentreString("Apariencia / Temas", 160, 2, 4);
}
void themeMenu() {
tft.fillScreen(TFT_BLACK);
const char* const THEME_SHORT_NAMES[6] = {
"Cian", "Verde", "Ambar", "Blanco", "Naranja", "Violeta"
};
auto drawThemeUI = [&]() {
// Redraw screen header banner in newly selected theme colors
drawThemeHeader();
// Sincronizar LED RGB trasero con el tema seleccionado en tiempo real
updateRearLedTheme();
// 1. Live Menu UI & Divider Preview Card (y: 33 to 96, h: 64, x: 10, w: 300)
tft.fillRoundRect(10, 33, 300, 64, 5, 0x1082);
tft.drawRoundRect(10, 33, 300, 64, 5, GAUGE_THEMES[themeIndex].accentColor);
tft.drawRoundRect(11, 34, 298, 62, 4, 0x2104);
// Left side: Theme Name and Description
tft.setTextColor(GAUGE_THEMES[themeIndex].primaryColor, 0x1082);
tft.drawString(GAUGE_THEMES[themeIndex].name, 20, 40, 4);
tft.setTextColor(0xAD55, 0x1082);
tft.drawString(GAUGE_THEMES[themeIndex].desc, 20, 69, 2);
// Right side: Simulated Mini Vignette & Menu Preview
// Mini Instrument Vignette + Crosshair Line (x: 168, y: 39, w: 126, h: 18)
tft.fillRectVGradient(168, 39, 126, 12, GAUGE_THEMES[themeIndex].bgColor, TFT_BLACK);
tft.fillRect(168, 51, 126, 6, TFT_BLACK);
tft.fillRect(220, 39, 2, 18, GAUGE_THEMES[themeIndex].divColor); // Sample Crosshair line
tft.fillCircle(188, 48, 3, TFT_GREEN); // Sample green gauge block
tft.setTextColor(0xBDD7, TFT_BLACK);
tft.drawRightString("VIGNETTE", 290, 42, 1);
// Mini Selected Card (x: 168, y: 62, w: 72, h: 28)
tft.fillRoundRect(168, 62, 72, 28, 4, GAUGE_THEMES[themeIndex].accentColor);
tft.drawRoundRect(168, 62, 72, 28, 4, GAUGE_THEMES[themeIndex].primaryColor);
tft.setTextColor(TFT_WHITE, GAUGE_THEMES[themeIndex].accentColor);
tft.drawCentreString("MENU", 204, 71, 2);
// Mini Salir Button (x: 246, y: 62, w: 48, h: 28)
tft.fillRoundRect(246, 62, 48, 28, 4, GAUGE_THEMES[themeIndex].accentColor);
tft.drawRoundRect(246, 62, 48, 28, 4, GAUGE_THEMES[themeIndex].darkColor);
tft.setTextColor(TFT_WHITE, GAUGE_THEMES[themeIndex].accentColor);
tft.drawCentreString("SALIR", 270, 71, 2);
// 2. 6 Theme Selection Tiles (y: 103 to 175, 2 rows of 3)
for (uint8_t i = 0; i < 6; i++) {
uint8_t row = i / 3;
uint8_t col = i % 3;
int16_t tx = 10 + col * 102;
int16_t ty = 103 + row * 38;
int16_t tw = 96;
int16_t th = 34;
bool isSel = (i == themeIndex);
uint16_t bg = isSel ? GAUGE_THEMES[i].darkColor : 0x1082;
tft.fillRoundRect(tx, ty, tw, th, 5, bg);
if (isSel) {
tft.drawRoundRect(tx, ty, tw, th, 5, GAUGE_THEMES[i].primaryColor);
tft.drawRoundRect(tx + 1, ty + 1, tw - 2, th - 2, 4, TFT_WHITE);
} else {
tft.drawRoundRect(tx, ty, tw, th, 5, 0x2104);
}
// Circular color swatch with subtle halo
int16_t cx = tx + 16;
int16_t cy = ty + 17;
tft.drawCircle(cx, cy, 7, isSel ? TFT_WHITE : 0x4208);
tft.fillCircle(cx, cy, 5, GAUGE_THEMES[i].primaryColor);
// Label
tft.setTextColor(isSel ? TFT_WHITE : 0xBDD7, bg);
tft.drawString(THEME_SHORT_NAMES[i], tx + 30, ty + 9, 2);
}
// 3. Redraw Exit button in newly selected theme colors
drawExitButton(false);
};
drawThemeUI();
waitForTouchRelease();
bool pressed = false;
long holdtimer = 0;
while (true) {
autoDim();
uint16_t tx = 0, ty = 0;
if (getTouch(&tx, &ty)) {
// 1. Touch on theme tiles (y: 100 to 180)
if (ty >= 100 && ty < 182) {
uint8_t col = (tx < 110) ? 0 : ((tx < 212) ? 1 : 2);
uint8_t row = (ty < 140) ? 0 : 1;
uint8_t touchedIdx = row * 3 + col;
if (touchedIdx < 6) {
if (touchedIdx != themeIndex) {
themeIndex = touchedIdx;
pref.begin("setting", false);
pref.putUChar("theme_idx", themeIndex);
pref.end();
touchBeep();
drawThemeUI();
}
}
waitForTouchRelease();
}
// 2. Touch on [ SALIR ] button (y: 182 to 240)
else if (ty >= 182 && tx >= 20 && tx <= 300) {
animateExitButtonPress();
break;
}
} else if (digitalRead(SELECTOR_PIN) == LOW) {
if (!pressed) {
pressed = true;
holdtimer = millis();
} else if (millis() - holdtimer > 1000) {
animateExitButtonPress();
break;
}
} else {
if (pressed) {
if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1000) {
themeIndex = (themeIndex + 1) % 6;
pref.begin("setting", false);
pref.putUChar("theme_idx", themeIndex);
pref.end();
touchBeep();
drawThemeUI();
}
pressed = false;
delay(150);
}
}
yield();
}
}
// -------------------------------------------------------------
// Encabezado del Menu de Configuracion (Estilo Azul Oceano & Cian)
// -------------------------------------------------------------
void drawConfigHeader() {
tft.fillRectVGradient(0, 0, 320, 26, getThemeAccentColor(), getThemeDarkColor());
tft.fillRect(0, 26, 320, 2, getThemePrimaryColor());
tft.setTextColor(TFT_WHITE);
tft.drawCentreString("Menu de Configuracion", 160, 2, 4);
}
// -------------------------------------------------------------
// Funciones de Botones de Scroll con Animacion Tactil
// -------------------------------------------------------------
void drawScrollUpButton(bool canScroll, bool pressed) {
uint16_t bg = pressed ? getThemePrimaryColor() : (canScroll ? getThemeDarkColor() : 0x0841);
uint16_t fg = pressed ? TFT_BLACK : (canScroll ? getThemePrimaryColor() : 0x4208);
uint16_t border = pressed ? getThemePrimaryColor() : (canScroll ? getThemeAccentColor() : 0x2104);
tft.fillRoundRect(268, 32, 42, 42, 5, bg);
tft.drawRoundRect(268, 32, 42, 42, 5, border);
if (pressed) {
tft.drawRoundRect(269, 33, 40, 40, 4, TFT_WHITE);
}
tft.fillTriangle(289, 42, 278, 58, 300, 58, fg);
}
void drawScrollDownButton(bool canScroll, bool pressed) {
uint16_t bg = pressed ? getThemePrimaryColor() : (canScroll ? getThemeDarkColor() : 0x0841);
uint16_t fg = pressed ? TFT_BLACK : (canScroll ? getThemePrimaryColor() : 0x4208);
uint16_t border = pressed ? getThemePrimaryColor() : (canScroll ? getThemeAccentColor() : 0x2104);
tft.fillRoundRect(268, 186, 42, 42, 5, bg);
tft.drawRoundRect(268, 186, 42, 42, 5, border);
if (pressed) {
tft.drawRoundRect(269, 187, 40, 40, 4, TFT_WHITE);
}
tft.fillTriangle(289, 214, 278, 198, 300, 198, fg);
}
// -------------------------------------------------------------
// Menu Principal con Scroll Lateral (Opcion 3)
// -------------------------------------------------------------
void listMenu(uint8_t choice, uint8_t scrollOffset) {
// Clear list area, scroll column, and bottom area (y: 28 to 240)
tft.fillRect(0, 28, 320, 212, TFT_BLACK);
// 1. Draw 3 visible cards from menuList[scrollOffset .. scrollOffset + 2]
for (uint8_t i = 0; i < 3; i++) {
uint8_t itemIdx = scrollOffset + i;
if (itemIdx >= 6) break;
int y = 32 + i * 50;
bool isSel = (choice == itemIdx);
uint16_t bgCol = isSel ? getThemeAccentColor() : getThemeDarkColor();
uint16_t borderCol = isSel ? getThemePrimaryColor() : getThemeAccentColor();
tft.fillRoundRect(10, y, 250, 44, 5, bgCol);
tft.drawRoundRect(10, y, 250, 44, 5, borderCol);
if (isSel) {
tft.drawRoundRect(11, y + 1, 248, 42, 4, TFT_WHITE);
} else {
// Sleek vertical theme accent bar on the left of each item card
tft.fillRoundRect(13, y + 6, 4, 32, 2, getThemePrimaryColor());
}
tft.setTextColor(TFT_WHITE, bgCol);
tft.drawString(menuList[itemIdx], 26, y + 9, 4);
// Chevron arrow '>' on right edge of card in theme primary color
tft.fillTriangle(248, y + 22, 242, y + 16, 242, y + 28, isSel ? TFT_WHITE : getThemePrimaryColor());
}
// 2. Right Scroll Column (x: 268 to 310, w: 42)
uint8_t maxScroll = 3; // 6 total items - 3 visible items = 3
// Up Button [ ▲ ] (x: 268, y: 32, w: 42, h: 42)
drawScrollUpButton(scrollOffset > 0, false);
// Extended Middle Track & Thumb (y: 78 to 182, h: 104)
tft.fillRoundRect(286, 78, 6, 104, 3, 0x0841);
tft.drawRoundRect(286, 78, 6, 104, 3, getThemeDarkColor());
int thumbY = 78 + (scrollOffset * 72) / maxScroll;
tft.fillRoundRect(286, thumbY, 6, 32, 3, getThemePrimaryColor());
// Down Button [ ▼ ] (x: 268, y: 186, w: 42, h: 42 - aligned with SALIR)
drawScrollDownButton(scrollOffset < maxScroll, false);
// 3. Bottom action button: [ SALIR ] (x: 80, y: 186, w: 160, h: 42)
drawExitButton(false);
if (choice == 6) {
tft.drawRoundRect(80, 186, 160, 42, 5, getThemePrimaryColor());
tft.drawRoundRect(81, 187, 158, 40, 4, TFT_WHITE);
}
}
void configMenu() {
uint8_t select = 255;
uint8_t scrollOffset = 0;
const uint8_t maxScroll = 3; // 6 items - 3 visible = 3
bool pressed = false;
long holdtimer = 0;
tft.fillScreen(TFT_BLACK);
drawConfigHeader();
listMenu(select, scrollOffset);
waitForTouchRelease();
while (true) {
if (wokeFromSleep) break;
autoDim();
int8_t executeSelect = -1;
uint16_t tx = 0, ty = 0;
if (getTouch(&tx, &ty)) {
int8_t touchedOpt = -1;
// 1. Touch on bottom [ SALIR ] button (x: 10 to 264, y: 182 to 240)
if (ty >= 182 && ty <= 240 && tx >= 10 && tx < 265) {
touchedOpt = 6; // SALIR
}
// 2. Touch on the 3 visible cards (x: 10 to 264, y: 28 to 182)
else if (tx >= 10 && tx < 265) {
if (ty >= 28 && ty < 78) {
touchedOpt = scrollOffset + 0;
} else if (ty >= 78 && ty < 129) {
touchedOpt = scrollOffset + 1;
} else if (ty >= 129 && ty < 182) {
touchedOpt = scrollOffset + 2;
}
}
// 3. Touch on Scroll Column (x: 265 to 318)
else if (tx >= 265 && tx <= 318) {
// Up Button [ ▲ ] (y: 28 to 76)
if (ty >= 28 && ty <= 76) {
drawScrollUpButton(scrollOffset > 0, true);
touchBeep();
if (scrollOffset > 0) {
scrollOffset--;
}
delay(100);
waitForTouchRelease();
listMenu(select, scrollOffset);
}
// Down Button [ ▼ ] (y: 184 to 240 - aligned with SALIR)
else if (ty >= 184 && ty <= 240) {
drawScrollDownButton(scrollOffset < maxScroll, true);
touchBeep();
if (scrollOffset < maxScroll) {
scrollOffset++;
}
delay(100);
waitForTouchRelease();
listMenu(select, scrollOffset);
}
// Middle Track (y: 77 to 183)
else if (ty > 76 && ty < 184) {
if (ty < 130) {
drawScrollUpButton(scrollOffset > 0, true);
touchBeep();
if (scrollOffset > 0) scrollOffset--;
} else {
drawScrollDownButton(scrollOffset < maxScroll, true);
touchBeep();
if (scrollOffset < maxScroll) scrollOffset++;
}
delay(100);
waitForTouchRelease();
listMenu(select, scrollOffset);
}
}
if (touchedOpt >= 0 && touchedOpt < 7) {
select = touchedOpt;
if (select == 6) {
animateExitButtonPress();
break;
} else {
listMenu(select, scrollOffset);
clickSound();
waitForTouchRelease();
executeSelect = select;
}
}
} else if (digitalRead(SELECTOR_PIN) == LOW) {
if (!pressed) {
pressed = true;
holdtimer = millis();
} else if (millis() - holdtimer > 1200) {
if (select != 255) {
executeSelect = select;
}
pressed = false;
}
} else {
if (pressed) {
if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1200) {
if (select == 255) {
select = scrollOffset;
} else {
select++;
if (select >= maxMenu) select = 0;
}
// Auto-scroll to keep selected card visible
if (select < 6) {
if (select < scrollOffset) {
scrollOffset = select;
} else if (select >= scrollOffset + 3) {
scrollOffset = select - 2;
}
}
listMenu(select, scrollOffset);
clickSound();
}
pressed = false;
delay(150);
}
}
if (executeSelect >= 0) {
select = executeSelect;
// Option 0: Alarmas (ON/OFF)
if (select == 0) {
warningMenu();
tft.fillScreen(TFT_BLACK);
drawConfigHeader();
select = 255;
listMenu(select, scrollOffset);
waitForTouchRelease();
}
// Option 1: Sonidos (Menu de Sonido de Alarma y Sonido de Click)
else if (select == 1) {
soundsMenu();
tft.fillScreen(TFT_BLACK);
drawConfigHeader();
select = 255;
listMenu(select, scrollOffset);
waitForTouchRelease();
}
// Option 2: Brillo de Pantalla
else if (select == 2) {
brightnessMenu();
tft.fillScreen(TFT_BLACK);
drawConfigHeader();
select = 255;
listMenu(select, scrollOffset);
waitForTouchRelease();
}
// Option 3: Apariencia / Temas
else if (select == 3) {
themeMenu();
tft.fillScreen(TFT_BLACK);
drawConfigHeader();
select = 255;
listMenu(select, scrollOffset);
waitForTouchRelease();
}
// Option 4: Modo Dormir
else if (select == 4) {
sleepMenu();
tft.fillScreen(TFT_BLACK);
drawConfigHeader();
select = 255;
listMenu(select, scrollOffset);
waitForTouchRelease();
}
// Option 5: Reiniciar
else if (select == 5) {
restartConfirmationMenu();
tft.fillScreen(TFT_BLACK);
drawConfigHeader();
select = 255;
listMenu(select, scrollOffset);
waitForTouchRelease();
}
// Option 6: Exit
else if (select == 6) {
animateExitButtonPress();
break;
}
pressed = false;
prompt = true;
}
delay(30);
}
tft.fillScreen(TFT_BLACK);
}