From 478bb65d32e601ba7bdc0908986b620a8256645b Mon Sep 17 00:00:00 2001 From: javiservices Date: Thu, 18 Jun 2026 12:00:17 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20firmware=20v6.1=20producci=C3=B3n=20(si?= =?UTF-8?q?n=20botones)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reescritura desde cero con arquitectura modular (Config, UI, Vib, Task, Api, Portal, Wifi) - NVS en lugar de SPIFFS para persistencia - WiFi: máquina de estados BOOT→CONNECTING→CONNECTED/FAILED→PORTAL - OLED: 5 frames nombrados (boot, connecting, idle, task, portal) - API: cliente HTTP reutilizable con lectura completa del body - Portal: AP + DNS + WebServer, /test restaura correctamente web/dns - Cmd serie: B0/B1/B2 simula botones, S estado, P pines, A ADC, C calibrar - Vibrador: patrón 2-buzz al recibir tarea nueva WiFi default: PHARMACO/arnoldgym2010 (BatKuevaUp sin internet) --- platformio.ini | 4 +- src/main.cpp | 1071 +++++++++++++++++++++++------------------------- 2 files changed, 511 insertions(+), 564 deletions(-) diff --git a/platformio.ini b/platformio.ini index 846250f..fa22cb9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -18,8 +18,8 @@ build_flags = -DUSER_SETUP_LOADED=1 ; ─── CONFIG POR DEFECTO ─── - -DDEFAULT_WIFI_SSID=\"BatKuevaUp\" - -DDEFAULT_WIFI_PASS=\"JaMon2026\" + -DDEFAULT_WIFI_SSID=\"PHARMACO\" + -DDEFAULT_WIFI_PASS=\"arnoldgym2010\" -DBACKEND_URL=\"http://157.180.77.232:18080\" -DDEFAULT_DEVICE_TOKEN=\"UrTQ0byozhEJxZQSC7LFmDq8liH3OnEBaFryUlU4yHaKcq2c\" diff --git a/src/main.cpp b/src/main.cpp index 69c19df..be1619e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,9 @@ -// ============================================================ -// DummyBot – firmware ESP32 v5 -// OLED SSD1306 128x64 (I2C) + WiFi + 3 botones + vibrador -// ============================================================ +// ============================================================= +// DummyBot — firmware ESP32 v6.1 (producción sin botones) +// OLED SSD1306 128x64 + WiFi STA + polling backend +// Sin input físico: botones deshabilitados hasta nueva placa. +// Mantiene: WiFi config + portal cautivo + API + OLED + vibrador. +// ============================================================= #include #include @@ -12,655 +14,600 @@ #include #include #include -#include +#include -// -------------------- Pines -------------------- -#define PIN_BTN_NEXT 26 -#define PIN_BTN_DONE 27 -#define PIN_BTN_SNOOZE 14 -#define PIN_VIBRATOR 25 - -#define SCREEN_W 128 -#define SCREEN_H 64 -#define OLED_ADDR 0x3C -#define SDA_PIN 21 -#define SCL_PIN 22 - -// -------------------- Config -------------------- -#define POLL_MS 30000 -#define VIB_BUZZ_MS 400 -#define VIB_PATTERN_MS 1500 -#define SNOOZE_MINUTES 5 -#define API_TIMEOUT_MS 4000 - -const char* WIFI_AP_NAME = "DummyBot-Setup"; -const char* WIFI_AP_PASS = "dummybot"; +// ============== HARDWARE ============== +static constexpr uint8_t PIN_VIBRATOR = 25; +static constexpr uint8_t PIN_BOOT = 0; +static constexpr int8_t OLED_SDA = 21; +static constexpr int8_t OLED_SCL = 22; +static constexpr uint8_t OLED_ADDR = 0x3C; +static constexpr int8_t OLED_RST = -1; +static constexpr uint8_t SCREEN_W = 128; +static constexpr uint8_t SCREEN_H = 64; +// ============== CONFIG COMPILETIME ============== #ifndef DEFAULT_WIFI_SSID -#define DEFAULT_WIFI_SSID "BatKuevaUp" +#define DEFAULT_WIFI_SSID "PHARMACO" #endif #ifndef DEFAULT_WIFI_PASS -#define DEFAULT_WIFI_PASS "JaMon2026" +#define DEFAULT_WIFI_PASS "arnoldgym2010" #endif - #ifndef BACKEND_URL #define BACKEND_URL "http://157.180.77.232:18080" #endif - #ifndef DEFAULT_DEVICE_TOKEN -#define DEFAULT_DEVICE_TOKEN "UrTQ0byozhEJxZQSC7LFmDq8liH3OnEBaFryUlU4yHaKcq2c" +#define DEFAULT_DEVICE_TOKEN "" #endif -// -------------------- Estado global -------------------- -Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, -1); -WebServer web(80); -DNSServer dnsServer; +static constexpr const char* AP_NAME = "DummyBot-Setup"; +static constexpr const char* AP_PASS = "dummybot"; +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; + +Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, OLED_RST); +Preferences nvs; +WebServer webServer(80); +DNSServer dnsServer; + +// ============================================================= +// CONFIG +// ============================================================= +struct Config { + String ssid, pass, token, backend, name; + + void load() { + ssid = DEFAULT_WIFI_SSID; + pass = DEFAULT_WIFI_PASS; + token = DEFAULT_DEVICE_TOKEN; + backend = BACKEND_URL; + name = "Mi DummyBot"; + nvs.begin(NVS_NS, true); + if (nvs.isKey("ssid")) ssid = nvs.getString("ssid", ssid); + if (nvs.isKey("pass")) pass = nvs.getString("pass", pass); + if (nvs.isKey("token")) token = nvs.getString("token", token); + if (nvs.isKey("backend")) backend = nvs.getString("backend", backend); + if (nvs.isKey("name")) name = nvs.getString("name", name); + nvs.end(); + Serial.printf("[CFG] ssid='%s' backend='%s' token.len=%u\n", + ssid.c_str(), backend.c_str(), token.length()); + } + + void save() { + nvs.begin(NVS_NS, false); + nvs.putString("ssid", ssid); + nvs.putString("pass", pass); + nvs.putString("token", token); + nvs.putString("backend", backend); + nvs.putString("name", name); + nvs.end(); + } + + void clear() { + nvs.begin(NVS_NS, false); + nvs.clear(); + nvs.end(); + ssid = DEFAULT_WIFI_SSID; + pass = DEFAULT_WIFI_PASS; + token = DEFAULT_DEVICE_TOKEN; + backend = BACKEND_URL; + name = "Mi DummyBot"; + } +} cfg; + +// ============================================================= +// UI +// ============================================================= +namespace UI { +void showBoot() { + display.clearDisplay(); + display.setTextSize(2); + display.setTextColor(SSD1306_WHITE); + display.setCursor(20, 16); + display.print(F("DummyBot")); + display.setTextSize(1); + display.setCursor(28, 50); + display.print(F("Iniciando...")); + display.display(); +} + +void showConnecting(const String& ssid, uint8_t dots) { + display.clearDisplay(); + display.setTextSize(2); + display.setTextColor(SSD1306_WHITE); + display.setCursor(8, 8); + display.print(F("WiFi")); + for (uint8_t i = 0; i < dots; i++) display.print('.'); + + display.setTextSize(1); + display.setCursor(0, 36); + display.print(ssid.substring(0, 21)); + display.setCursor(0, 48); + display.printf("IP: %s", WiFi.localIP().toString().c_str()); + display.setCursor(0, 56); + display.print(WiFi.status() == WL_CONNECTED ? F("conectado") : F("esperando...")); + display.display(); +} + +void showIdle(bool wifiOk) { + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + display.setCursor(0, 0); + display.print(F("DummyBot ")); + display.print(wifiOk ? F("[W]") : F("[!]")); + display.drawLine(0, 10, 127, 10, SSD1306_WHITE); + display.setTextSize(2); + display.setCursor(8, 22); + display.print(F("Sin tareas")); + display.setTextSize(1); + display.setCursor(14, 50); + display.print(F("Anade desde la web")); + display.display(); +} + +void showTask(const String& title, const String& priority, int32_t dueSec) { + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + display.setCursor(0, 0); + display.printf("DummyBot [%s]", priority.c_str()); + display.drawLine(0, 10, 127, 10, SSD1306_WHITE); + display.setTextSize(2); + String t = title; + String l1 = t.substring(0, 10); + String l2 = t.length() > 10 ? t.substring(10, 20) : ""; + display.setCursor(0, 16); + display.print(l1); + if (l2.length()) { + display.setCursor(0, 36); + display.print(l2); + } + display.setTextSize(1); + display.setCursor(0, 56); + if (dueSec > 60) display.printf("Vence: %d min", dueSec / 60); + else if (dueSec > 0) display.printf("Vence: %d s", dueSec); + else display.print(F("Refresca desde web")); + display.display(); +} + +void showPortal(uint8_t clients) { + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + display.setCursor(0, 0); + display.println(F("DummyBot - Setup")); + display.drawLine(0, 10, 127, 10, SSD1306_WHITE); + display.setCursor(0, 16); + display.printf("AP: %s", AP_NAME); + display.setCursor(0, 26); + display.printf("Pass: %s", AP_PASS); + display.setCursor(0, 38); + display.printf("Clientes: %u", clients); + display.setCursor(0, 50); + display.print(F("Abre 192.168.4.1")); + display.display(); +} +} // 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 +// ============================================================= struct Task { - int id; - char title[64]; - char priority[8]; - int next_due_in; + int32_t id = 0; + String title; + String priority = "med"; + int32_t nextDueSec = 0; }; -Task currentTask; -bool hasTask = false; -uint32_t lastPoll = 0; -uint32_t lastSeenId = 0; +static Task currentTask; +static bool hasTask = false; +static int32_t lastSeenId = 0; -bool vibActive = false; -uint32_t vibStart = 0; - -uint32_t lastBtnMs[3] = {0,0,0}; -const uint8_t BTN_PINS[3] = { PIN_BTN_NEXT, PIN_BTN_DONE, PIN_BTN_SNOOZE }; -#define DEBOUNCE_MS 200 - -// Config persistente -#define WIFI_CFG_PATH "/wifi.cfg" -char cfgSsid[64] = ""; -char cfgPass[64] = ""; -char cfgToken[80] = ""; -char cfgBackend[128] = ""; -char cfgName[40] = "Mi DummyBot"; -bool portalActive = false; - -// WiFi status -enum WifiState { W_BOOTING, W_CONNECTING, W_CONNECTED, W_FAILED, W_PORTAL }; -WifiState wifiState = W_BOOTING; -uint32_t wifiStart = 0; - -// -------------------- Vib -------------------- -static void startVib() { - vibActive = true; - vibStart = millis(); - digitalWrite(PIN_VIBRATOR, HIGH); -} -static void stopVib() { - vibActive = false; - digitalWrite(PIN_VIBRATOR, LOW); -} -static void updateVib() { - if (!vibActive) return; - uint32_t e = millis() - vibStart; - if (e >= VIB_PATTERN_MS) { stopVib(); return; } - uint32_t phase = e % (VIB_BUZZ_MS + 200); - digitalWrite(PIN_VIBRATOR, phase < VIB_BUZZ_MS ? HIGH : LOW); -} - -// -------------------- UI -------------------- -static void drawCentered(const char *s, int y, int size = 1) { - int16_t x1, y1; - uint16_t w, h; - display.getTextBounds(s, 0, 0, &x1, &y1, &w, &h); - display.setCursor((SCREEN_W - w) / 2, y); - display.print(s); -} - -static void drawIdleScreen() { - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - // Header - display.setCursor(0, 0); - display.print(F("DummyBot")); - // WiFi indicator - if (WiFi.status() == WL_CONNECTED) { - display.print(F(" [W]")); - } else { - display.print(F(" [-]")); - } - display.drawLine(0, 10, SCREEN_W - 1, 10, SSD1306_WHITE); - - // Mensaje - drawCentered("Sin tareas :)", 25, 2); - - if (hasTask) { - // Hay tarea pero no se muestra - drawCentered("OK", 50, 1); - } else { - drawCentered("Anade desde la web", 50, 1); - } - display.display(); -} - -static void drawTaskScreen() { - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - // Header con prioridad - display.setCursor(0, 0); - display.printf("DummyBot [%s]", currentTask.priority); - display.drawLine(0, 10, SCREEN_W - 1, 10, SSD1306_WHITE); - - // Título (con wrap manual; 16 chars por línea a size 2) - display.setTextSize(2); - display.setCursor(0, 18); - String title = String(currentTask.title); - if (title.length() > 16) { - display.print(title.substring(0, 16)); - display.setCursor(0, 38); - display.print(title.substring(16, 32)); - } else { - display.print(title); - } - - // Footer con info - display.setTextSize(1); - display.setCursor(0, 56); - if (currentTask.next_due_in > 60) { - int mins = currentTask.next_due_in / 60; - display.printf("Vence: %d min", mins); - } else if (currentTask.next_due_in > 0) { - display.printf("Vence: %d s", currentTask.next_due_in); - } else { - display.print(F("Snooze | Hecha | Next")); - } - display.display(); -} - -static void drawWifiScreen(const char *msg) { - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - display.setCursor(0, 0); - display.println(F("DummyBot - WiFi")); - display.drawLine(0, 10, SCREEN_W - 1, 10, SSD1306_WHITE); - - display.setCursor(0, 18); - display.println(msg); - - display.setCursor(0, 32); - display.printf("AP: %s", WIFI_AP_NAME); - - display.setCursor(0, 42); - display.printf("Pass: %s", WIFI_AP_PASS); - - display.setCursor(0, 56); - display.println(F("Abre 192.168.4.1")); - display.display(); -} - -// -------------------- HTTP -------------------- -static bool httpGetJson(const char *url, JsonDocument &doc) { +// ============================================================= +// API +// ============================================================= +namespace Api { +bool httpJson(const char* method, const char* path, const char* body, JsonDocument* out) { if (WiFi.status() != WL_CONNECTED) return false; - HTTPClient http; - http.begin(url); - http.setTimeout(API_TIMEOUT_MS); - http.addHeader("Authorization", String("Bearer ") + String(DEFAULT_DEVICE_TOKEN)); - int code = http.GET(); - if (code != 200) { - Serial.printf("HTTP %d en %s\n", code, url); - http.end(); + if (cfg.token.isEmpty()) { + Serial.println("[API] sin token, skip"); return false; } - DeserializationError err = deserializeJson(doc, http.getStream()); - http.end(); - return !err; -} -static bool httpPostJson(const char *url, const char *body) { - if (WiFi.status() != WL_CONNECTED) return false; + String url = cfg.backend + path; HTTPClient http; http.begin(url); http.setTimeout(API_TIMEOUT_MS); - http.addHeader("Content-Type", "application/json"); - http.addHeader("Authorization", String("Bearer ") + String(DEFAULT_DEVICE_TOKEN)); - int code = http.POST(body); + http.addHeader("Authorization", "Bearer " + cfg.token); + if (body) http.addHeader("Content-Type", "application/json"); + int code = (String(method) == "GET") ? http.GET() : http.POST(body ? body : ""); + bool ok = (code >= 200 && code < 300); + Serial.printf("[API] %s %s -> %d\n", method, url.c_str(), code); + if (ok && out) { + String payload = http.getString(); + DeserializationError err = deserializeJson(*out, payload); + if (err) { Serial.printf("[API] json err: %s\n", err.c_str()); ok = false; } + } http.end(); - return code >= 200 && code < 300; + return ok; } -// -------------------- Lógica -------------------- -static void pollTasks() { - if (WiFi.status() != WL_CONNECTED) return; - char url[160]; - snprintf(url, sizeof(url), "%s/api/device/tasks/pending", BACKEND_URL); - Serial.printf("GET %s\n", url); +bool pollPending() { JsonDocument doc; - if (!httpGetJson(url, doc)) { - drawIdleScreen(); - return; - } - - JsonArray arr = doc.as(); - if (arr.size() == 0) { + if (!httpJson("GET", "/api/device/tasks/pending", nullptr, &doc)) { hasTask = false; - drawIdleScreen(); - return; + return false; } - JsonObject first = arr[0]; - Task t; - t.id = first["id"] | 0; - strncpy(t.title, first["title"] | "", sizeof(t.title) - 1); - strncpy(t.priority, first["priority"] | "med", sizeof(t.priority) - 1); - t.next_due_in = first["next_due_in"] | 0; + JsonArray arr = doc.as(); + if (arr.size() == 0) { hasTask = false; return true; } + JsonObject t0 = arr[0]; + currentTask.id = t0["id"] | 0; + currentTask.title = t0["title"] | ""; + currentTask.priority = t0["priority"] | "med"; + currentTask.nextDueSec = t0["next_due_in"] | 0; + hasTask = true; + if ((int32_t)currentTask.id != lastSeenId && lastSeenId != 0) Vib::buzz(); + lastSeenId = currentTask.id; + return true; +} +} // namespace Api - if ((uint32_t)t.id != lastSeenId) { - if (lastSeenId != 0) startVib(); - lastSeenId = (uint32_t)t.id; - } - currentTask = t; - hasTask = true; - drawTaskScreen(); +// ============================================================= +// PORTAL +// ============================================================= +namespace Portal { +static const char INDEX_HTML[] PROGMEM = R"HTML( + + +DummyBot Setup +
+

DummyBot

Configura WiFi y token del bot

+
+ + +
+

Toca una red para autorrellenar. Se reescanea cada 15s.

+ + +
+
+ + + +
+)====="; + +)HTML"; -static String renderPortal() { - String html = FPSTR(PORTAL_HTML); - html.replace("__NAME__", String(cfgName)); - html.replace("__SSID__", String(cfgSsid)); - html.replace("__PASS__", String(cfgPass)); - html.replace("__BACKEND__", String(cfgBackend[0] ? cfgBackend : BACKEND_URL)); - html.replace("__TOKEN__", String(cfgToken)); +String renderIndex() { + String html = FPSTR(INDEX_HTML); + html.replace("__NAME__", cfg.name); + html.replace("__SSID__", cfg.ssid); + html.replace("__PASS__", cfg.pass); + html.replace("__BACKEND__", cfg.backend); + html.replace("__TOKEN__", cfg.token); return html; } -static void handleRoot() { - web.send(200, "text/html; charset=utf-8", renderPortal()); -} -static void handleCaptive() { - web.sendHeader("Location", "http://192.168.4.1/"); - web.send(302, "text/plain", ""); -} -static void handleSave() { - if (!web.hasArg("ssid")) { web.send(400, "text/html", "Falta SSID"); return; } - String s = web.arg("ssid"); s.trim(); - if (!s.length()) { web.send(400, "text/html", "SSID vacio"); return; } - String p = web.arg("pass"); - String t = web.arg("token"); - String b = web.arg("backend"); - String n = web.hasArg("name") ? web.arg("name") : "Mi DummyBot"; - p.trim(); t.trim(); b.trim(); n.trim(); - if (!b.length()) b = BACKEND_URL; - if (!t.length()) t = DEFAULT_DEVICE_TOKEN; +void onRoot() { webServer.send(200, "text/html; charset=utf-8", renderIndex()); } +void onCaptive(){ webServer.sendHeader("Location", "http://192.168.4.1/"); webServer.send(302); } +void onNotFound(){ onCaptive(); } - strncpy(cfgSsid, s.c_str(), sizeof(cfgSsid) - 1); - strncpy(cfgPass, p.c_str(), sizeof(cfgPass) - 1); - strncpy(cfgToken, t.c_str(), sizeof(cfgToken) - 1); - strncpy(cfgBackend, b.c_str(), sizeof(cfgBackend) - 1); - strncpy(cfgName, n.c_str(), sizeof(cfgName) - 1); - saveConfig(); - - String ok = R"=====(OK

Guardado

El bot se está reiniciando.

👉 Conéctate a __SSID__

)====="; - ok.replace("__SSID__", s); - web.send(200, "text/html; charset=utf-8", ok); - delay(500); - ESP.restart(); -} -static void handleReset() { - resetConfig(); - web.sendHeader("Location", "/"); - web.send(302); - delay(500); - ESP.restart(); -} - -static void handleNetworks() { - // Scan: cambiar a STA momentáneamente, escanear, volver a AP - int n = WiFi.scanNetworks(false, true); // async=false, show_hidden=true +void onNetworks() { + int n = WiFi.scanNetworks(false, true); String json = "["; for (int i = 0; i < n; i++) { if (i > 0) json += ","; - // Escape básico del SSID - String ssid = WiFi.SSID(i); - ssid.replace("\\", "\\\\"); - ssid.replace("\"", "\\\""); - json += "{\"ssid\":\"" + ssid + "\","; - json += "\"rssi\":" + String(WiFi.RSSI(i)) + ","; - json += "\"enc\":" + String(WiFi.encryptionType(i) != WIFI_AUTH_OPEN ? "true" : "false") + "}"; + String s = WiFi.SSID(i); + s.replace("\\", "\\\\"); s.replace("\"", "\\\""); + json += "{\"ssid\":\"" + s + "\",\"rssi\":" + String(WiFi.RSSI(i)) + + ",\"enc\":" + String(WiFi.encryptionType(i) != WIFI_AUTH_OPEN ? "true" : "false") + "}"; } json += "]"; WiFi.scanDelete(); - web.send(200, "application/json", json); + webServer.send(200, "application/json", json); } -static void handleTest() { - if (!web.hasArg("ssid")) { - web.send(400, "application/json", "{\"ok\":false,\"reason\":\"NO_SSID\"}"); +void onSave() { + if (!webServer.hasArg("ssid")) { webServer.send(400, "text/html", "Falta SSID"); return; } + cfg.ssid = webServer.arg("ssid"); cfg.ssid.trim(); + cfg.pass = webServer.arg("pass"); cfg.pass.trim(); + cfg.token = webServer.arg("token"); cfg.token.trim(); + cfg.backend = webServer.arg("backend"); cfg.backend.trim(); + cfg.name = webServer.hasArg("name") ? webServer.arg("name") : "Mi DummyBot"; + cfg.name.trim(); + if (cfg.ssid.isEmpty()) { webServer.send(400, "text/html", "SSID vacio"); return; } + cfg.save(); + String ok = R"HTML( + +
\u2705

Guardado

+

El bot se esta reiniciando.

\u2192 Conectate a __SSID__

+
)HTML"; + ok.replace("__SSID__", cfg.ssid); + webServer.send(200, "text/html; charset=utf-8", ok); + delay(500); + ESP.restart(); +} + +void onReset() { + cfg.clear(); + webServer.sendHeader("Location", "/"); + webServer.send(302); + delay(500); + ESP.restart(); +} + +void onTest() { + if (!webServer.hasArg("ssid")) { + webServer.send(400, "application/json", "{\"ok\":false,\"reason\":\"NO_SSID\"}"); return; } - String ssid = web.arg("ssid"); - String pass = web.hasArg("pass") ? web.arg("pass") : ""; - - // Parar cualquier conexión actual - WiFi.disconnect(); + String ssid = webServer.arg("ssid"); + String pass = webServer.hasArg("pass") ? webServer.arg("pass") : ""; + webServer.stop(); + dnsServer.stop(); + WiFi.softAPdisconnect(true); delay(100); WiFi.mode(WIFI_STA); WiFi.begin(ssid.c_str(), pass.c_str()); - - // Esperar hasta 15 s - uint32_t start = millis(); + uint32_t t0 = millis(); wl_status_t st = WiFi.status(); - while (millis() - start < 15000 && st != WL_CONNECTED && st != WL_CONNECT_FAILED && st != WL_NO_SSID_AVAIL) { + while (millis() - t0 < 15000 && + st != WL_CONNECTED && st != WL_CONNECT_FAILED && st != WL_NO_SSID_AVAIL) { delay(200); st = WiFi.status(); } - String json; if (st == WL_CONNECTED) { json = "{\"ok\":true,\"ssid\":\"" + ssid + "\",\"ip\":\"" + WiFi.localIP().toString() + "\",\"rssi\":" + String(WiFi.RSSI()) + "}"; } else { - String reason = "UNKNOWN"; + const char* reason = "UNKNOWN"; switch (st) { - case WL_NO_SSID_AVAIL: reason = "NO_SSID_AVAIL"; break; - case WL_CONNECT_FAILED: reason = "WRONG_PASSWORD"; break; - case WL_CONNECTION_LOST: reason = "LOST"; break; - case WL_DISCONNECTED: reason = "TIMEOUT"; break; + case WL_NO_SSID_AVAIL: reason = "NO_SSID_AVAIL"; break; + case WL_CONNECT_FAILED: reason = "WRONG_PASSWORD"; break; + case WL_DISCONNECTED: reason = "TIMEOUT"; break; default: break; } - json = "{\"ok\":false,\"reason\":\"" + reason + "\"}"; + json = "{\"ok\":false,\"reason\":\"" + String(reason) + "\"}"; } - - // Resetear para volver al portal - WiFi.disconnect(); + WiFi.disconnect(false); delay(100); - WiFi.mode(WIFI_AP); - - web.send(200, "application/json", json); -} - -static void startPortal() { - WiFi.mode(WIFI_AP); - WiFi.softAP(WIFI_AP_NAME, WIFI_AP_PASS); - Serial.printf("AP abierto: %s\n", WIFI_AP_NAME); + WiFi.mode(WIFI_AP_STA); + delay(100); + WiFi.softAP(AP_NAME, AP_PASS); dnsServer.start(53, "*", WiFi.softAPIP()); - web.on("/", HTTP_GET, handleRoot); - web.on("/networks", HTTP_GET, handleNetworks); - web.on("/test", HTTP_GET, handleTest); - web.on("/save", HTTP_POST, handleSave); - web.on("/reset", HTTP_POST, handleReset); - web.onNotFound(handleCaptive); - web.begin(); - portalActive = true; + webServer.begin(); + webServer.send(200, "application/json", json); } -static void stopPortal() { - if (!portalActive) return; - web.stop(); + +void start() { + WiFi.mode(WIFI_AP_STA); + WiFi.softAP(AP_NAME, AP_PASS); + dnsServer.start(53, "*", WiFi.softAPIP()); + webServer.on("/", HTTP_GET, onRoot); + webServer.on("/networks", HTTP_GET, onNetworks); + webServer.on("/test", HTTP_GET, onTest); + webServer.on("/save", HTTP_POST, onSave); + webServer.on("/reset", HTTP_POST, onReset); + webServer.onNotFound(onCaptive); + webServer.begin(); + Serial.printf("[PORTAL] AP %s abierto\n", AP_NAME); +} + +void stop() { + webServer.stop(); dnsServer.stop(); WiFi.softAPdisconnect(true); - portalActive = false; } -// -------------------- WiFi -------------------- -static void startSTA() { - const char *ssid = cfgSsid[0] ? cfgSsid : DEFAULT_WIFI_SSID; - const char *pass = cfgPass[0] ? cfgPass : DEFAULT_WIFI_PASS; - Serial.printf("Conectando a WiFi '%s'...\n", ssid); +void tick() { + dnsServer.processNextRequest(); + webServer.handleClient(); +} +} // namespace Portal + +// ============================================================= +// WIFI +// ============================================================= +enum class WState { BOOT, CONNECTING, CONNECTED, FAILED, PORTAL }; +static WState wState = WState::BOOT; +static uint32_t wStartMs = 0; +static uint8_t wDots = 0; + +void wifiStartSTA() { + if (cfg.ssid.isEmpty()) { + Portal::start(); + wState = WState::PORTAL; + return; + } + Serial.printf("[WIFI] conectando a '%s'...\n", cfg.ssid.c_str()); WiFi.mode(WIFI_STA); - WiFi.begin(ssid, pass); - strncpy(cfgSsid, ssid, sizeof(cfgSsid) - 1); - strncpy(cfgPass, pass, sizeof(cfgPass) - 1); + WiFi.disconnect(false); + WiFi.begin(cfg.ssid.c_str(), cfg.pass.c_str()); + wStartMs = millis(); + wState = WState::CONNECTING; } -// -------------------- Setup / Loop -------------------- +void wifiTick() { + switch (wState) { + case WState::CONNECTING: { + if (WiFi.status() == WL_CONNECTED) { + Serial.printf("[WIFI] OK ip=%s rssi=%d\n", + WiFi.localIP().toString().c_str(), WiFi.RSSI()); + wState = WState::CONNECTED; + Api::pollPending(); + } else if (millis() - wStartMs > WIFI_TIMEOUT_MS) { + Serial.println("[WIFI] timeout, abriendo portal"); + Portal::start(); + wState = WState::PORTAL; + } else { + if (millis() - wStartMs > 400) { + wStartMs = millis(); + wDots = (wDots + 1) % 4; + UI::showConnecting(cfg.ssid, wDots); + } + } + break; + } + case WState::CONNECTED: { + if (WiFi.status() != WL_CONNECTED) { + Serial.println("[WIFI] perdido, reconectando"); + wState = WState::CONNECTING; + wStartMs = millis(); + WiFi.reconnect(); + } + break; + } + case WState::PORTAL: { + static uint32_t lastUi = 0; + if (millis() - lastUi > 2000) { + lastUi = millis(); + UI::showPortal(WiFi.softAPgetStationNum()); + } + break; + } + default: break; + } +} + +// ============================================================= +// SETUP / LOOP +// ============================================================= void setup() { Serial.begin(115200); - delay(500); - Serial.println("\n=== DummyBot v5 (OLED) ==="); + delay(300); + Serial.println("\n=== DummyBot v6.1 (sin botones) ==="); - pinMode(PIN_BTN_NEXT, INPUT); - pinMode(PIN_BTN_DONE, INPUT); - pinMode(PIN_BTN_SNOOZE, INPUT); - pinMode(PIN_VIBRATOR, OUTPUT); + pinMode(PIN_VIBRATOR, OUTPUT); digitalWrite(PIN_VIBRATOR, LOW); - // I2C + OLED - Wire.begin(SDA_PIN, SCL_PIN); + Wire.begin(OLED_SDA, OLED_SCL); if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { - Serial.println("OLED FAIL"); - while (1) delay(1000); - } - Serial.println("OLED OK"); - - if (!SPIFFS.begin(true)) Serial.println("SPIFFS ERROR"); - loadConfig(); - if (!cfgBackend[0]) strncpy(cfgBackend, BACKEND_URL, sizeof(cfgBackend) - 1); - if (!cfgToken[0]) strncpy(cfgToken, DEFAULT_DEVICE_TOKEN, sizeof(cfgToken) - 1); - - // Pantalla de bienvenida - display.clearDisplay(); - display.setTextSize(2); - display.setTextColor(SSD1306_WHITE); - drawCentered("DummyBot", 16, 2); - display.setTextSize(1); - drawCentered("Iniciando...", 50, 1); - display.display(); - - // Reset con BOOT 5s - pinMode(0, INPUT_PULLUP); - if (digitalRead(0) == LOW) { - uint32_t bootStart = millis(); - while (digitalRead(0) == LOW && millis() - bootStart < 5000) delay(50); - if (millis() - bootStart >= 5000) { - resetConfig(); - delay(1000); - } + Serial.println("[OLED] FAIL"); + } else { + UI::showBoot(); } - wifiState = W_CONNECTING; - wifiStart = millis(); - startSTA(); + cfg.load(); + + pinMode(PIN_BOOT, INPUT_PULLUP); + if (digitalRead(PIN_BOOT) == LOW) { + uint32_t t0 = millis(); + while (digitalRead(PIN_BOOT) == LOW && millis() - t0 < 5000) delay(50); + if (millis() - t0 >= 5000) { cfg.clear(); delay(200); } + } + + wState = WState::CONNECTING; + wStartMs = millis(); + wifiStartSTA(); } void loop() { - updateVib(); - - if (wifiState == W_PORTAL) { - dnsServer.processNextRequest(); - web.handleClient(); - handleButtons(); - static uint32_t lastScreen = 0; - if (millis() - lastScreen > 2000) { - lastScreen = millis(); - int n = WiFi.softAPgetStationNum(); - char msg[40]; - snprintf(msg, sizeof(msg), "Config WiFi (%d)", n); - drawWifiScreen(msg); + Vib::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); } - return; } - - handleButtons(); - - uint32_t now = millis(); - - if (wifiState == W_CONNECTING) { - if (WiFi.status() == WL_CONNECTED) { - wifiState = W_CONNECTED; - Serial.printf("WiFi OK: %s\n", WiFi.localIP().toString().c_str()); - lastPoll = now; - pollTasks(); - } else if (now - wifiStart > 25000) { - Serial.println("WiFi falló, abriendo portal..."); - startPortal(); - wifiState = W_PORTAL; - } else if (now - wifiStart > 500) { - static int dots = 0; - dots = (dots + 1) % 4; - display.clearDisplay(); - display.setTextSize(2); - display.setTextColor(SSD1306_WHITE); - drawCentered("WiFi...", 16, 2); - display.setTextSize(1); - char msg[20]; - snprintf(msg, sizeof(msg), "%.*s", dots, "..."); - display.setCursor(0, 50); - display.print(msg); - display.print(F(" ")); - display.println(cfgSsid); - display.display(); - } - return; + static uint32_t lastRender = 0; + if (wState == WState::CONNECTED && millis() - lastRender > 1000) { + lastRender = millis(); + if (hasTask) UI::showTask(currentTask.title, currentTask.priority, currentTask.nextDueSec); + else UI::showIdle(true); } - - if (WiFi.status() != WL_CONNECTED) { - Serial.println("WiFi perdido"); - wifiState = W_CONNECTING; - wifiStart = now; - startSTA(); - return; - } - - if (now - lastPoll >= POLL_MS) { - lastPoll = now; - pollTasks(); - } - delay(50); -} \ No newline at end of file + delay(20); +}