feat: firmware v6.2 botones invertidos (INPUT_PULLUP)
- Botones en GPIO 25 (PLAY=DONE), 26 (V+=NEXT), 27 (V-=SNOOZE) - KCOM del módulo a GND, lógica invertida (HIGH reposo, LOW al pulsar) - INPUT_PULLUP, detección por flanco descendente con debounce 200ms - GPIO 25 ya no es vibrador (ahora es botón DONE), vibrador desactivado - OLED con feedback toast al pulsar (1.2s sin pisar) - Toast 'Nueva!' al recibir tarea nueva del polling - Quitado código de test ADC y de matriz resistiva Fixes pantalla: - GPIO 14 ya no es botón (era strapping pin, causaba glitches y reset) - Botones solo en 25/26/27 que no son strapping pins
This commit is contained in:
parent
478bb65d32
commit
dce87fbc11
168
src/main.cpp
168
src/main.cpp
@ -1,8 +1,10 @@
|
||||
// =============================================================
|
||||
// DummyBot — firmware ESP32 v6.1 (producción sin botones)
|
||||
// DummyBot — firmware ESP32 v6.2 (botones invertidos + sin vibrador)
|
||||
// OLED SSD1306 128x64 + WiFi STA + polling backend
|
||||
// Sin input físico: botones deshabilitados hasta nueva placa.
|
||||
// Mantiene: WiFi config + portal cautivo + API + OLED + vibrador.
|
||||
// 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
|
||||
// =============================================================
|
||||
|
||||
#include <Arduino.h>
|
||||
@ -17,7 +19,9 @@
|
||||
#include <Preferences.h>
|
||||
|
||||
// ============== HARDWARE ==============
|
||||
static constexpr uint8_t PIN_VIBRATOR = 25;
|
||||
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_BOOT = 0;
|
||||
static constexpr int8_t OLED_SDA = 21;
|
||||
static constexpr int8_t OLED_SCL = 22;
|
||||
@ -37,7 +41,7 @@ static constexpr uint8_t SCREEN_H = 64;
|
||||
#define BACKEND_URL "http://157.180.77.232:18080"
|
||||
#endif
|
||||
#ifndef DEFAULT_DEVICE_TOKEN
|
||||
#define DEFAULT_DEVICE_TOKEN ""
|
||||
#define DEFAULT_DEVICE_TOKEN "UrTQ0byozhEJxZQSC7LFmDq8liH3OnEBaFryUlU4yHaKcq2c"
|
||||
#endif
|
||||
|
||||
static constexpr const char* AP_NAME = "DummyBot-Setup";
|
||||
@ -47,8 +51,8 @@ static constexpr const char* NVS_NS = "dummybot";
|
||||
static constexpr uint32_t POLL_MS = 30000;
|
||||
static constexpr uint32_t API_TIMEOUT_MS = 5000;
|
||||
static constexpr uint32_t WIFI_TIMEOUT_MS = 20000;
|
||||
static constexpr uint32_t VIB_PATTERN_MS = 1500;
|
||||
static constexpr uint32_t VIB_BUZZ_MS = 400;
|
||||
static constexpr uint32_t DEBOUNCE_MS = 200;
|
||||
static constexpr uint8_t SNOOZE_MINUTES = 5;
|
||||
|
||||
Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, OLED_RST);
|
||||
Preferences nvs;
|
||||
@ -134,13 +138,12 @@ void showConnecting(const String& ssid, uint8_t dots) {
|
||||
display.display();
|
||||
}
|
||||
|
||||
void showIdle(bool wifiOk) {
|
||||
void showIdle() {
|
||||
display.clearDisplay();
|
||||
display.setTextSize(1);
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
display.setCursor(0, 0);
|
||||
display.print(F("DummyBot "));
|
||||
display.print(wifiOk ? F("[W]") : F("[!]"));
|
||||
display.print(F("DummyBot [W]"));
|
||||
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
|
||||
display.setTextSize(2);
|
||||
display.setCursor(8, 22);
|
||||
@ -176,6 +179,22 @@ void showTask(const String& title, const String& priority, int32_t dueSec) {
|
||||
display.display();
|
||||
}
|
||||
|
||||
void showToast(const char* msg, bool ok) {
|
||||
display.clearDisplay();
|
||||
display.setTextSize(1);
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
display.setCursor(0, 0);
|
||||
display.print(F("DummyBot"));
|
||||
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
|
||||
display.setTextSize(2);
|
||||
display.setCursor(8, 22);
|
||||
display.print(msg);
|
||||
display.setTextSize(1);
|
||||
display.setCursor(0, 56);
|
||||
display.print(ok ? F("[OK]") : F("[ERROR]"));
|
||||
display.display();
|
||||
}
|
||||
|
||||
void showPortal(uint8_t clients) {
|
||||
display.clearDisplay();
|
||||
display.setTextSize(1);
|
||||
@ -195,23 +214,6 @@ void showPortal(uint8_t clients) {
|
||||
}
|
||||
} // namespace UI
|
||||
|
||||
// =============================================================
|
||||
// VIBRADOR
|
||||
// =============================================================
|
||||
namespace Vib {
|
||||
bool active = false;
|
||||
uint32_t startMs = 0;
|
||||
|
||||
void buzz() { active = true; startMs = millis(); digitalWrite(PIN_VIBRATOR, HIGH); }
|
||||
void stop() { active = false; digitalWrite(PIN_VIBRATOR, LOW); }
|
||||
void tick() {
|
||||
if (!active) return;
|
||||
uint32_t e = millis() - startMs;
|
||||
if (e >= VIB_PATTERN_MS) { stop(); return; }
|
||||
digitalWrite(PIN_VIBRATOR, (e % (VIB_BUZZ_MS + 200)) < VIB_BUZZ_MS ? HIGH : LOW);
|
||||
}
|
||||
} // namespace Vib
|
||||
|
||||
// =============================================================
|
||||
// TAREAS
|
||||
// =============================================================
|
||||
@ -225,6 +227,9 @@ struct Task {
|
||||
static Task currentTask;
|
||||
static bool hasTask = false;
|
||||
static int32_t lastSeenId = 0;
|
||||
static uint32_t toastUntil = 0;
|
||||
static const char* toastMsg = nullptr;
|
||||
static bool toastOk = true;
|
||||
|
||||
// =============================================================
|
||||
// API
|
||||
@ -268,12 +273,87 @@ bool pollPending() {
|
||||
currentTask.priority = t0["priority"] | "med";
|
||||
currentTask.nextDueSec = t0["next_due_in"] | 0;
|
||||
hasTask = true;
|
||||
if ((int32_t)currentTask.id != lastSeenId && lastSeenId != 0) Vib::buzz();
|
||||
if ((int32_t)currentTask.id != lastSeenId && lastSeenId != 0) {
|
||||
// Nueva tarea detectada — pequeño feedback visual
|
||||
UI::showToast("Nueva!", true);
|
||||
toastUntil = millis() + 800;
|
||||
}
|
||||
lastSeenId = currentTask.id;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool markDone() {
|
||||
if (!hasTask) return false;
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "/api/device/tasks/%d/done", currentTask.id);
|
||||
bool ok = httpJson("POST", path, "{}", nullptr);
|
||||
if (ok) { lastSeenId = 0; pollPending(); }
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool snooze() {
|
||||
if (!hasTask) return false;
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "/api/device/tasks/%d/snooze", currentTask.id);
|
||||
char body[32];
|
||||
snprintf(body, sizeof(body), "{\"minutes\":%u}", SNOOZE_MINUTES);
|
||||
bool ok = httpJson("POST", path, body, nullptr);
|
||||
if (ok) { lastSeenId = 0; pollPending(); }
|
||||
return ok;
|
||||
}
|
||||
} // namespace Api
|
||||
|
||||
// =============================================================
|
||||
// INPUT — botones con INPUT_PULLUP, lógica invertida
|
||||
// HIGH en reposo, LOW al pulsar, KCOM del módulo a GND
|
||||
// =============================================================
|
||||
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;
|
||||
case 1: {
|
||||
Serial.println("[BTN] DONE (PLAY)");
|
||||
bool ok = Api::markDone();
|
||||
UI::showToast(ok ? "Hecha!" : "Error", ok);
|
||||
toastUntil = millis() + 1200;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
Serial.println("[BTN] SNOOZE (V-)");
|
||||
bool ok = Api::snooze();
|
||||
UI::showToast(ok ? "Snooze 5m" : "Error", ok);
|
||||
toastUntil = millis() + 1200;
|
||||
break;
|
||||
}
|
||||
}
|
||||
lastMs[i] = millis();
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
lastState[i] = now_low;
|
||||
}
|
||||
}
|
||||
} // namespace Input
|
||||
|
||||
// =============================================================
|
||||
// PORTAL
|
||||
// =============================================================
|
||||
@ -303,7 +383,6 @@ button.sec{background:transparent;border:1px solid #3f3f46;margin-top:.5rem}
|
||||
.result{margin-top:1rem;padding:.6rem;border-radius:8px;font-size:.85rem;display:none}
|
||||
.ok{background:#16a34a22;color:#86efac;border:1px solid #16a34a55}
|
||||
.err{background:#dc262622;color:#fca5a5;border:1px solid #dc262655}
|
||||
.emoji{font-size:2rem;margin-bottom:.3rem}
|
||||
</style></head><body><div class="c"><div class="card">
|
||||
<h1>DummyBot</h1><p class="lead">Configura WiFi y token del bot</p>
|
||||
<form id="f">
|
||||
@ -320,7 +399,6 @@ button.sec{background:transparent;border:1px solid #3f3f46;margin-top:.5rem}
|
||||
<button type="button" onclick="saveCfg()">Guardar y reiniciar</button>
|
||||
</form></div></div>
|
||||
<script>
|
||||
let picked=null;
|
||||
function bars(r){const d=Math.max(0,Math.min(4,Math.floor((r+100)/10)));let s='';for(let i=0;i<4;i++)s+=i<d?'\u2588':'\u2003';return s}
|
||||
function load(){
|
||||
fetch('/networks').then(r=>r.json()).then(a=>{
|
||||
@ -332,7 +410,7 @@ function load(){
|
||||
const b=document.createElement('button');
|
||||
b.type='button';b.className='net';
|
||||
b.innerHTML='<span>'+n.ssid+(n.enc?' \ud83d\udd12':'')+'</span><span class="rssi"><span class="bars">'+bars(n.rssi)+'</span> '+n.rssi+'dBm</span>';
|
||||
b.onclick=function(){picked=n.ssid;document.getElementById('ssid').value=n.ssid;document.querySelectorAll('.net').forEach(x=>x.style.background='');this.style.background='#6366f133'};
|
||||
b.onclick=function(){document.getElementById('ssid').value=n.ssid;document.querySelectorAll('.net').forEach(x=>x.style.background='');this.style.background='#6366f133'};
|
||||
el.appendChild(b);
|
||||
});
|
||||
}).catch(()=>{document.getElementById('nets').innerHTML='<div class="help">Error al escanear</div>'})
|
||||
@ -347,7 +425,7 @@ async function testConn(){
|
||||
const r=await fetch('/test?ssid='+ssid+'&pass='+pass);
|
||||
const j=await r.json();
|
||||
if(j.ok){out.className='result ok';out.innerHTML='\u2713 Conectado a "'+j.ssid+'" ('+j.ip+', '+j.rssi+' dBm)'}
|
||||
else{let m='\u2717 ';switch(j.reason){case 'NO_SSID_AVAIL':m+='red no encontrada';break;case 'WRONG_PASSWORD':m+='contrasena incorrecta';break;case 'NO_IP':m+='sin IP';break;case 'TIMEOUT':m+='timeout';break;default:m+=j.reason}out.className='result err';out.innerHTML=m}
|
||||
else{let m='\u2717 ';switch(j.reason){case 'NO_SSID_AVAIL':m+='red no encontrada';break;case 'WRONG_PASSWORD':m+='contrasena incorrecta';break;case 'TIMEOUT':m+='timeout';break;default:m+=j.reason}out.className='result err';out.innerHTML=m}
|
||||
}catch(e){out.className='result err';out.innerHTML='Error: '+e.message}
|
||||
}
|
||||
async function saveCfg(){
|
||||
@ -566,15 +644,24 @@ void wifiTick() {
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(300);
|
||||
Serial.println("\n=== DummyBot v6.1 (sin botones) ===");
|
||||
Serial.println("\n=== DummyBot v6.2 (botones invertidos) ===");
|
||||
|
||||
pinMode(PIN_VIBRATOR, OUTPUT);
|
||||
digitalWrite(PIN_VIBRATOR, LOW);
|
||||
// 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)
|
||||
|
||||
Wire.begin(OLED_SDA, OLED_SCL);
|
||||
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
|
||||
Serial.println("[OLED] FAIL");
|
||||
} else {
|
||||
display.clearDisplay();
|
||||
display.setTextSize(1);
|
||||
display.setTextColor(SSD1306_WHITE);
|
||||
display.setCursor(0, 0);
|
||||
display.print(F("OLED OK"));
|
||||
display.display();
|
||||
delay(500);
|
||||
UI::showBoot();
|
||||
}
|
||||
|
||||
@ -593,21 +680,26 @@ void setup() {
|
||||
}
|
||||
|
||||
void loop() {
|
||||
Vib::tick();
|
||||
Input::tick();
|
||||
if (wState == WState::PORTAL) Portal::tick();
|
||||
wifiTick();
|
||||
if (wState == WState::CONNECTED) {
|
||||
static uint32_t lastPoll = 0;
|
||||
if (millis() - lastPoll > POLL_MS) {
|
||||
lastPoll = millis();
|
||||
if (!Api::pollPending()) UI::showIdle(true);
|
||||
if (!Api::pollPending()) UI::showIdle();
|
||||
}
|
||||
}
|
||||
// Render principal: respeta el toast (no pisar durante 1.2s tras pulsar)
|
||||
static uint32_t lastRender = 0;
|
||||
if (wState == WState::CONNECTED && millis() - lastRender > 1000) {
|
||||
if (millis() < toastUntil) {
|
||||
delay(20);
|
||||
return;
|
||||
}
|
||||
lastRender = millis();
|
||||
if (hasTask) UI::showTask(currentTask.title, currentTask.priority, currentTask.nextDueSec);
|
||||
else UI::showIdle(true);
|
||||
else UI::showIdle();
|
||||
}
|
||||
delay(20);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user