From 2e79aca367392123f2d0bad26ce6d0391e90a28c Mon Sep 17 00:00:00 2001 From: javiservices Date: Thu, 18 Jun 2026 13:22:24 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20firmware=20v6.3=20multi-pulsaci=C3=B3n?= =?UTF-8?q?=20con=201=20bot=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cambio importante: el módulo de botones chino tiene los pads cortocircuitados a KCOM (que está a GND), lo que fuerza GPIO 26 y 27 a LOW constantemente y dispara acciones sin tocarlos. Solución temporal: solo GPIO 25 funciona como botón. Multi-pulsación para 3 acciones: - 1 tap = DONE - 2 taps = NEXT (re-poll) - 3 taps = SNOOZE 5 min - mantener 1s+ = sleep/wake pantalla Cuando llegue la placa nueva con 3 pulsadores mecánicos de 6mm, se restaura la lógica de 3 botones en GPIO 25/26/27. --- src/main.cpp | 163 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 124 insertions(+), 39 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 5bc8327..074e05a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,10 +1,20 @@ // ============================================================= -// DummyBot — firmware ESP32 v6.2 (botones invertidos + sin vibrador) +// DummyBot — firmware ESP32 v6.3 (1 botón con multi-pulsación) // OLED SSD1306 128x64 + WiFi STA + polling backend -// Botones: GPIO 25 = PLAY (DONE), 26 = V+ (NEXT), 27 = V- (SNOOZE) -// Lógica: INPUT_PULLUP → HIGH reposo, LOW al pulsar -// KCOM del módulo de botones → GND -// GPIO 25 (era vibrador) ahora es un botón → vibrador desactivado +// +// Cambio importante v6.3: el módulo de botones chino estaba +// cortocircuitado (GPIO 26 y 27 quedaban forzados a LOW, lo +// que disparaba acciones constantemente sin tocar nada). +// +// Solución temporal: solo GPIO 25 funciona como botón físico. +// Multi-pulsación para hacer 3 acciones: +// · 1 tap = DONE (marcar como hecha) +// · 2 taps = NEXT (siguiente tarea, re-poll) +// · 3 taps = SNOOZE 5 min +// · mantener >1s = dormir/despertar pantalla +// +// Cuando llegue la placa nueva con 3 pulsadores mecánicos, +// se restaurará la lógica de 3 botones en GPIO 25/26/27. // ============================================================= #include @@ -19,9 +29,7 @@ #include // ============== HARDWARE ============== -static constexpr uint8_t PIN_BTN_PLAY = 25; // PLAY (DONE: marcar como hecha) -static constexpr uint8_t PIN_BTN_NEXT = 26; // V+ (NEXT: siguiente tarea) -static constexpr uint8_t PIN_BTN_SNOO = 27; // V- (SNOOZE: posponer 5 min) +static constexpr uint8_t PIN_BTN_PLAY = 25; // único botón físico static constexpr uint8_t PIN_BOOT = 0; static constexpr int8_t OLED_SDA = 21; static constexpr int8_t OLED_SCL = 22; @@ -304,52 +312,131 @@ bool snooze() { } // namespace Api // ============================================================= -// INPUT — botones con INPUT_PULLUP, lógica invertida -// HIGH en reposo, LOW al pulsar, KCOM del módulo a GND +// INPUT — 1 botón con multi-pulsación (sustituto temporal) +// +// GPIO 25 (PLAY) con INPUT_PULLUP, lógica invertida. +// Estado: HIGH en reposo, LOW al pulsar. +// +// Patrones: +// - 1 tap = DONE (marca tarea como hecha) +// - 2 taps = NEXT (forzar re-polling) +// - 3 taps = SNOOZE (posponer 5 min) +// - mantener 1s+ = SLEEP (apagar pantalla; tocas otra vez para despertar) +// +// Ventana entre taps: 500 ms +// Timeout multi-tap: 700 ms +// Debounce por tap: 150 ms // ============================================================= namespace Input { -uint32_t lastMs[3] = {0, 0, 0}; -bool lastState[3] = {true, true, true}; // pull-up: idle=HIGH -constexpr uint8_t PINS[3] = { PIN_BTN_NEXT, PIN_BTN_PLAY, PIN_BTN_SNOO }; -// Acción común -void press(uint8_t i) { - switch (i) { - case 0: - Serial.println("[BTN] NEXT (V+)"); - Api::pollPending(); - UI::showToast("Siguiente", true); - toastUntil = millis() + 1200; - break; +constexpr uint32_t TAP_DEBOUNCE_MS = 150; +constexpr uint32_t MULTI_GAP_MS = 500; // máx entre taps +constexpr uint32_t MULTI_TIMEOUT_MS = 700; // ventana para consolidar +constexpr uint32_t LONG_PRESS_MS = 1000; // hold = sleep + +bool lastState = true; // pull-up: idle HIGH +uint32_t pressStartMs = 0; // momento de pulsación actual +bool isPressed = false; // estamos dentro de un LOW +uint8_t tapCount = 0; // taps acumulados +uint32_t lastTapMs = 0; // último tap registrado + +bool sleeping = false; // pantalla apagada por sleep + +void showAction(int action) { + switch (action) { case 1: { - Serial.println("[BTN] DONE (PLAY)"); + Serial.println("[BTN] DONE (1 tap)"); bool ok = Api::markDone(); UI::showToast(ok ? "Hecha!" : "Error", ok); toastUntil = millis() + 1200; break; } case 2: { - Serial.println("[BTN] SNOOZE (V-)"); + Serial.println("[BTN] NEXT (2 taps)"); + Api::pollPending(); + UI::showToast("Siguiente", true); + toastUntil = millis() + 1200; + break; + } + case 3: { + Serial.println("[BTN] SNOOZE (3 taps)"); bool ok = Api::snooze(); UI::showToast(ok ? "Snooze 5m" : "Error", ok); toastUntil = millis() + 1200; break; } } - lastMs[i] = millis(); +} + +void toggleSleep() { + sleeping = !sleeping; + if (sleeping) { + display.ssd1306_command(SSD1306_DISPLAYOFF); + Serial.println("[BTN] SLEEP (long press)"); + } else { + display.ssd1306_command(SSD1306_DISPLAYON); + Serial.println("[BTN] WAKE"); + } } void tick() { - uint32_t now = millis(); - for (uint8_t i = 0; i < 3; i++) { - bool now_low = (digitalRead(PINS[i]) == LOW); // pulsado = LOW - // Flanco descendente: idle HIGH → pulsado LOW - if (now_low && lastState[i] && (now - lastMs[i] > DEBOUNCE_MS)) { - press(i); - } else if (!now_low && !lastState[i]) { - // soltado + if (sleeping) { + // Para despertar: cualquier toque cuenta + bool now_low = (digitalRead(PIN_BTN_PLAY) == LOW); + if (now_low && !lastState) { + // No-op: solo se detecta el flanco } - lastState[i] = now_low; + if (!now_low && lastState) { + // Soltado tras un toque en sleep → despertar + toggleSleep(); + // Resetear estado del multi-tap + tapCount = 0; + lastTapMs = 0; + lastState = now_low; + return; + } + lastState = now_low; + return; + } + + uint32_t now = millis(); + bool now_low = (digitalRead(PIN_BTN_PLAY) == LOW); + + // Flanco descendente (HIGH → LOW): nuevo tap + if (now_low && lastState) { + isPressed = true; + pressStartMs = now; + } + // Flanco ascendente (LOW → HIGH): tap soltado + else if (!now_low && isPressed) { + uint32_t held = now - pressStartMs; + isPressed = false; + + if (held >= LONG_PRESS_MS) { + // Long press → toggle sleep + toggleSleep(); + tapCount = 0; + lastTapMs = 0; + } else { + // Tap corto: incrementar contador + tapCount++; + lastTapMs = now; + Serial.printf("[BTN] tap #%u (held %ums)\n", tapCount, (unsigned)held); + } + } + + lastState = now_low; + + // Consolidar multi-tap cuando se cumple timeout + if (tapCount > 0 && !isPressed && (now - lastTapMs) > MULTI_TIMEOUT_MS) { + if (tapCount <= 3) { + showAction(tapCount); + } else { + Serial.printf("[BTN] %u taps → siguiente\n", tapCount); + showAction(2); // 4+ taps = next + } + tapCount = 0; + lastTapMs = 0; } } } // namespace Input @@ -644,12 +731,10 @@ void wifiTick() { void setup() { Serial.begin(115200); delay(300); - Serial.println("\n=== DummyBot v6.2 (botones invertidos) ==="); + Serial.println("\n=== DummyBot v6.3 (1 botón + multi-pulsación) ==="); - // Botones con INPUT_PULLUP, lógica invertida (LOW al pulsar) - pinMode(PIN_BTN_NEXT, INPUT_PULLUP); // V+ - pinMode(PIN_BTN_PLAY, INPUT_PULLUP); // PLAY (DONE) - pinMode(PIN_BTN_SNOO, INPUT_PULLUP); // V- (SNOOZE) + // Único botón en GPIO 25 con INPUT_PULLUP, lógica invertida + pinMode(PIN_BTN_PLAY, INPUT_PULLUP); Wire.begin(OLED_SDA, OLED_SCL); if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {