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
182
src/main.cpp
182
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
|
// OLED SSD1306 128x64 + WiFi STA + polling backend
|
||||||
// Sin input físico: botones deshabilitados hasta nueva placa.
|
// Botones: GPIO 25 = PLAY (DONE), 26 = V+ (NEXT), 27 = V- (SNOOZE)
|
||||||
// Mantiene: WiFi config + portal cautivo + API + OLED + vibrador.
|
// 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>
|
#include <Arduino.h>
|
||||||
@ -17,14 +19,16 @@
|
|||||||
#include <Preferences.h>
|
#include <Preferences.h>
|
||||||
|
|
||||||
// ============== HARDWARE ==============
|
// ============== 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_BOOT = 0;
|
static constexpr uint8_t PIN_BTN_NEXT = 26; // V+ (NEXT: siguiente tarea)
|
||||||
static constexpr int8_t OLED_SDA = 21;
|
static constexpr uint8_t PIN_BTN_SNOO = 27; // V- (SNOOZE: posponer 5 min)
|
||||||
static constexpr int8_t OLED_SCL = 22;
|
static constexpr uint8_t PIN_BOOT = 0;
|
||||||
static constexpr uint8_t OLED_ADDR = 0x3C;
|
static constexpr int8_t OLED_SDA = 21;
|
||||||
static constexpr int8_t OLED_RST = -1;
|
static constexpr int8_t OLED_SCL = 22;
|
||||||
static constexpr uint8_t SCREEN_W = 128;
|
static constexpr uint8_t OLED_ADDR = 0x3C;
|
||||||
static constexpr uint8_t SCREEN_H = 64;
|
static constexpr int8_t OLED_RST = -1;
|
||||||
|
static constexpr uint8_t SCREEN_W = 128;
|
||||||
|
static constexpr uint8_t SCREEN_H = 64;
|
||||||
|
|
||||||
// ============== CONFIG COMPILETIME ==============
|
// ============== CONFIG COMPILETIME ==============
|
||||||
#ifndef DEFAULT_WIFI_SSID
|
#ifndef DEFAULT_WIFI_SSID
|
||||||
@ -37,7 +41,7 @@ static constexpr uint8_t SCREEN_H = 64;
|
|||||||
#define BACKEND_URL "http://157.180.77.232:18080"
|
#define BACKEND_URL "http://157.180.77.232:18080"
|
||||||
#endif
|
#endif
|
||||||
#ifndef DEFAULT_DEVICE_TOKEN
|
#ifndef DEFAULT_DEVICE_TOKEN
|
||||||
#define DEFAULT_DEVICE_TOKEN ""
|
#define DEFAULT_DEVICE_TOKEN "UrTQ0byozhEJxZQSC7LFmDq8liH3OnEBaFryUlU4yHaKcq2c"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static constexpr const char* AP_NAME = "DummyBot-Setup";
|
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 POLL_MS = 30000;
|
||||||
static constexpr uint32_t API_TIMEOUT_MS = 5000;
|
static constexpr uint32_t API_TIMEOUT_MS = 5000;
|
||||||
static constexpr uint32_t WIFI_TIMEOUT_MS = 20000;
|
static constexpr uint32_t WIFI_TIMEOUT_MS = 20000;
|
||||||
static constexpr uint32_t VIB_PATTERN_MS = 1500;
|
static constexpr uint32_t DEBOUNCE_MS = 200;
|
||||||
static constexpr uint32_t VIB_BUZZ_MS = 400;
|
static constexpr uint8_t SNOOZE_MINUTES = 5;
|
||||||
|
|
||||||
Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, OLED_RST);
|
Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, OLED_RST);
|
||||||
Preferences nvs;
|
Preferences nvs;
|
||||||
@ -134,13 +138,12 @@ void showConnecting(const String& ssid, uint8_t dots) {
|
|||||||
display.display();
|
display.display();
|
||||||
}
|
}
|
||||||
|
|
||||||
void showIdle(bool wifiOk) {
|
void showIdle() {
|
||||||
display.clearDisplay();
|
display.clearDisplay();
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
display.setTextColor(SSD1306_WHITE);
|
display.setTextColor(SSD1306_WHITE);
|
||||||
display.setCursor(0, 0);
|
display.setCursor(0, 0);
|
||||||
display.print(F("DummyBot "));
|
display.print(F("DummyBot [W]"));
|
||||||
display.print(wifiOk ? F("[W]") : F("[!]"));
|
|
||||||
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
|
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
|
||||||
display.setTextSize(2);
|
display.setTextSize(2);
|
||||||
display.setCursor(8, 22);
|
display.setCursor(8, 22);
|
||||||
@ -176,6 +179,22 @@ void showTask(const String& title, const String& priority, int32_t dueSec) {
|
|||||||
display.display();
|
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) {
|
void showPortal(uint8_t clients) {
|
||||||
display.clearDisplay();
|
display.clearDisplay();
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
@ -195,23 +214,6 @@ void showPortal(uint8_t clients) {
|
|||||||
}
|
}
|
||||||
} // namespace UI
|
} // 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
|
// TAREAS
|
||||||
// =============================================================
|
// =============================================================
|
||||||
@ -225,6 +227,9 @@ struct Task {
|
|||||||
static Task currentTask;
|
static Task currentTask;
|
||||||
static bool hasTask = false;
|
static bool hasTask = false;
|
||||||
static int32_t lastSeenId = 0;
|
static int32_t lastSeenId = 0;
|
||||||
|
static uint32_t toastUntil = 0;
|
||||||
|
static const char* toastMsg = nullptr;
|
||||||
|
static bool toastOk = true;
|
||||||
|
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// API
|
// API
|
||||||
@ -268,12 +273,87 @@ bool pollPending() {
|
|||||||
currentTask.priority = t0["priority"] | "med";
|
currentTask.priority = t0["priority"] | "med";
|
||||||
currentTask.nextDueSec = t0["next_due_in"] | 0;
|
currentTask.nextDueSec = t0["next_due_in"] | 0;
|
||||||
hasTask = true;
|
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;
|
lastSeenId = currentTask.id;
|
||||||
return true;
|
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
|
} // 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
|
// 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}
|
.result{margin-top:1rem;padding:.6rem;border-radius:8px;font-size:.85rem;display:none}
|
||||||
.ok{background:#16a34a22;color:#86efac;border:1px solid #16a34a55}
|
.ok{background:#16a34a22;color:#86efac;border:1px solid #16a34a55}
|
||||||
.err{background:#dc262622;color:#fca5a5;border:1px solid #dc262655}
|
.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">
|
</style></head><body><div class="c"><div class="card">
|
||||||
<h1>DummyBot</h1><p class="lead">Configura WiFi y token del bot</p>
|
<h1>DummyBot</h1><p class="lead">Configura WiFi y token del bot</p>
|
||||||
<form id="f">
|
<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>
|
<button type="button" onclick="saveCfg()">Guardar y reiniciar</button>
|
||||||
</form></div></div>
|
</form></div></div>
|
||||||
<script>
|
<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 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(){
|
function load(){
|
||||||
fetch('/networks').then(r=>r.json()).then(a=>{
|
fetch('/networks').then(r=>r.json()).then(a=>{
|
||||||
@ -332,7 +410,7 @@ function load(){
|
|||||||
const b=document.createElement('button');
|
const b=document.createElement('button');
|
||||||
b.type='button';b.className='net';
|
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.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);
|
el.appendChild(b);
|
||||||
});
|
});
|
||||||
}).catch(()=>{document.getElementById('nets').innerHTML='<div class="help">Error al escanear</div>'})
|
}).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 r=await fetch('/test?ssid='+ssid+'&pass='+pass);
|
||||||
const j=await r.json();
|
const j=await r.json();
|
||||||
if(j.ok){out.className='result ok';out.innerHTML='\u2713 Conectado a "'+j.ssid+'" ('+j.ip+', '+j.rssi+' dBm)'}
|
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}
|
}catch(e){out.className='result err';out.innerHTML='Error: '+e.message}
|
||||||
}
|
}
|
||||||
async function saveCfg(){
|
async function saveCfg(){
|
||||||
@ -566,15 +644,24 @@ void wifiTick() {
|
|||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
delay(300);
|
delay(300);
|
||||||
Serial.println("\n=== DummyBot v6.1 (sin botones) ===");
|
Serial.println("\n=== DummyBot v6.2 (botones invertidos) ===");
|
||||||
|
|
||||||
pinMode(PIN_VIBRATOR, OUTPUT);
|
// Botones con INPUT_PULLUP, lógica invertida (LOW al pulsar)
|
||||||
digitalWrite(PIN_VIBRATOR, LOW);
|
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);
|
Wire.begin(OLED_SDA, OLED_SCL);
|
||||||
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
|
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
|
||||||
Serial.println("[OLED] FAIL");
|
Serial.println("[OLED] FAIL");
|
||||||
} else {
|
} 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();
|
UI::showBoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,21 +680,26 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
Vib::tick();
|
Input::tick();
|
||||||
if (wState == WState::PORTAL) Portal::tick();
|
if (wState == WState::PORTAL) Portal::tick();
|
||||||
wifiTick();
|
wifiTick();
|
||||||
if (wState == WState::CONNECTED) {
|
if (wState == WState::CONNECTED) {
|
||||||
static uint32_t lastPoll = 0;
|
static uint32_t lastPoll = 0;
|
||||||
if (millis() - lastPoll > POLL_MS) {
|
if (millis() - lastPoll > POLL_MS) {
|
||||||
lastPoll = millis();
|
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;
|
static uint32_t lastRender = 0;
|
||||||
if (wState == WState::CONNECTED && millis() - lastRender > 1000) {
|
if (wState == WState::CONNECTED && millis() - lastRender > 1000) {
|
||||||
|
if (millis() < toastUntil) {
|
||||||
|
delay(20);
|
||||||
|
return;
|
||||||
|
}
|
||||||
lastRender = millis();
|
lastRender = millis();
|
||||||
if (hasTask) UI::showTask(currentTask.title, currentTask.priority, currentTask.nextDueSec);
|
if (hasTask) UI::showTask(currentTask.title, currentTask.priority, currentTask.nextDueSec);
|
||||||
else UI::showIdle(true);
|
else UI::showIdle();
|
||||||
}
|
}
|
||||||
delay(20);
|
delay(20);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user