feat: firmware v6.3 multi-pulsación con 1 botón

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.
This commit is contained in:
javiservices 2026-06-18 13:22:24 +02:00
parent dce87fbc11
commit 2e79aca367

View File

@ -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 // 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 // Cambio importante v6.3: el módulo de botones chino estaba
// KCOM del módulo de botones → GND // cortocircuitado (GPIO 26 y 27 quedaban forzados a LOW, lo
// GPIO 25 (era vibrador) ahora es un botón → vibrador desactivado // 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 <Arduino.h> #include <Arduino.h>
@ -19,9 +29,7 @@
#include <Preferences.h> #include <Preferences.h>
// ============== HARDWARE ============== // ============== HARDWARE ==============
static constexpr uint8_t PIN_BTN_PLAY = 25; // PLAY (DONE: marcar como hecha) static constexpr uint8_t PIN_BTN_PLAY = 25; // único botón físico
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_BOOT = 0; static constexpr uint8_t PIN_BOOT = 0;
static constexpr int8_t OLED_SDA = 21; static constexpr int8_t OLED_SDA = 21;
static constexpr int8_t OLED_SCL = 22; static constexpr int8_t OLED_SCL = 22;
@ -304,52 +312,131 @@ bool snooze() {
} // namespace Api } // namespace Api
// ============================================================= // =============================================================
// INPUT — botones con INPUT_PULLUP, lógica invertida // INPUT — 1 botón con multi-pulsación (sustituto temporal)
// HIGH en reposo, LOW al pulsar, KCOM del módulo a GND //
// 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 { 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 constexpr uint32_t TAP_DEBOUNCE_MS = 150;
void press(uint8_t i) { constexpr uint32_t MULTI_GAP_MS = 500; // máx entre taps
switch (i) { constexpr uint32_t MULTI_TIMEOUT_MS = 700; // ventana para consolidar
case 0: constexpr uint32_t LONG_PRESS_MS = 1000; // hold = sleep
Serial.println("[BTN] NEXT (V+)");
Api::pollPending(); bool lastState = true; // pull-up: idle HIGH
UI::showToast("Siguiente", true); uint32_t pressStartMs = 0; // momento de pulsación actual
toastUntil = millis() + 1200; bool isPressed = false; // estamos dentro de un LOW
break; 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: { case 1: {
Serial.println("[BTN] DONE (PLAY)"); Serial.println("[BTN] DONE (1 tap)");
bool ok = Api::markDone(); bool ok = Api::markDone();
UI::showToast(ok ? "Hecha!" : "Error", ok); UI::showToast(ok ? "Hecha!" : "Error", ok);
toastUntil = millis() + 1200; toastUntil = millis() + 1200;
break; break;
} }
case 2: { 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(); bool ok = Api::snooze();
UI::showToast(ok ? "Snooze 5m" : "Error", ok); UI::showToast(ok ? "Snooze 5m" : "Error", ok);
toastUntil = millis() + 1200; toastUntil = millis() + 1200;
break; 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() { void tick() {
uint32_t now = millis(); if (sleeping) {
for (uint8_t i = 0; i < 3; i++) { // Para despertar: cualquier toque cuenta
bool now_low = (digitalRead(PINS[i]) == LOW); // pulsado = LOW bool now_low = (digitalRead(PIN_BTN_PLAY) == LOW);
// Flanco descendente: idle HIGH → pulsado LOW if (now_low && !lastState) {
if (now_low && lastState[i] && (now - lastMs[i] > DEBOUNCE_MS)) { // No-op: solo se detecta el flanco
press(i);
} else if (!now_low && !lastState[i]) {
// soltado
} }
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 } // namespace Input
@ -644,12 +731,10 @@ void wifiTick() {
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
delay(300); 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) // Único botón en GPIO 25 con INPUT_PULLUP, lógica invertida
pinMode(PIN_BTN_NEXT, INPUT_PULLUP); // V+ pinMode(PIN_BTN_PLAY, INPUT_PULLUP);
pinMode(PIN_BTN_PLAY, INPUT_PULLUP); // PLAY (DONE)
pinMode(PIN_BTN_SNOO, INPUT_PULLUP); // V- (SNOOZE)
Wire.begin(OLED_SDA, OLED_SCL); Wire.begin(OLED_SDA, OLED_SCL);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {