fix: VCC de pantalla en 5V (no 3V3), RST en GPIO 15 (no GPIO 2); firmware con texto + portal (sin GIFs)
This commit is contained in:
parent
6ddc1e20be
commit
fe08f175e7
30
README.md
30
README.md
@ -14,30 +14,28 @@ Firmware del ESP32 para **DummyBot**, task manager para TDAH.
|
|||||||
```
|
```
|
||||||
ESP32 (WROOM-32) M130T-240240-RGB-7-V1.1
|
ESP32 (WROOM-32) M130T-240240-RGB-7-V1.1
|
||||||
───────────────────── ─────────────────────────
|
───────────────────── ─────────────────────────
|
||||||
3V3 ───────▶ VCC
|
5V (VIN) ───────▶ VCC ← IMPORTANTE: 5V, NO 3V3
|
||||||
GND ───────▶ GND
|
GND ───────▶ GND
|
||||||
GPIO 18 ───────▶ SCL (SPI clock)
|
GPIO 18 ───────▶ SCL (SPI clock)
|
||||||
GPIO 23 ───────▶ SDA (SPI MOSI)
|
GPIO 23 ───────▶ SDA (SPI MOSI)
|
||||||
GPIO 2 ───────▶ RES (reset)
|
GPIO 15 ───────▶ RES (reset) ← IMPORTANTE: GPIO 15, NO GPIO 2
|
||||||
GPIO 4 ───────▶ DC (data/command)
|
GPIO 4 ───────▶ DC (data/command)
|
||||||
GPIO 32 ───────▶ BL (backlight)
|
GPIO 32 ───────▶ BL (backlight)
|
||||||
CS a GND (no se usa)
|
CS a GND (no se usa)
|
||||||
|
|
||||||
ESP32 TTP223 (×3)
|
|
||||||
───────── ───────────
|
|
||||||
GPIO 26 ──▶ SIG TTP223 #1 Siguiente tarea
|
|
||||||
GPIO 27 ──▶ SIG TTP223 #2 Marcar como hecha
|
|
||||||
GPIO 14 ──▶ SIG TTP223 #3 Snooze 5 min
|
|
||||||
3V3 ──▶ VCC de los 3
|
|
||||||
GND ──▶ GND de los 3
|
|
||||||
|
|
||||||
ESP32 Módulo vibrador (con MOSFET)
|
|
||||||
───────── ──────────────────────────────
|
|
||||||
GPIO 25 ──▶ Gate del MOSFET
|
|
||||||
5V (VIN) ──▶ V+ del vibrador
|
|
||||||
GND ──▶ GND del vibrador
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Notas importantes sobre cableado
|
||||||
|
|
||||||
|
1. **VCC de la pantalla en 5V (pin VIN)**, NO en 3V3. La pantalla necesita más corriente
|
||||||
|
de la que el regulador 3.3V del ESP32 puede suministrar bajo carga (WiFi + SPI).
|
||||||
|
Con 3V3 el panel se ilumina pero no muestra nada.
|
||||||
|
|
||||||
|
2. **RES (reset) en GPIO 15**, NO en GPIO 2. GPIO 2 está compartido con el LED interno
|
||||||
|
azul del ESP32 DevKitC, lo que interfiere con la señal de reset.
|
||||||
|
|
||||||
|
3. Los **3 botones TTP223** van a GPIO 26, 27 y 14, alimentados desde 3V3.
|
||||||
|
4. El **vibrador** va a GPIO 25 con un transistor (MOSFET AO3400 o similar).
|
||||||
|
|
||||||
Para el motor 1027 (3V, 70-100 mA), un MOSFET AO3400 o 2N7000 + resistencia 100Ω en gate. Si solo tienes NPN (BC547/2N2222), esquema alternativo: motor entre 3V3 y colector, resistencia 1kΩ en base.
|
Para el motor 1027 (3V, 70-100 mA), un MOSFET AO3400 o 2N7000 + resistencia 100Ω en gate. Si solo tienes NPN (BC547/2N2222), esquema alternativo: motor entre 3V3 y colector, resistencia 1kΩ en base.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
#define TFT_SCLK 18
|
#define TFT_SCLK 18
|
||||||
#define TFT_CS -1 // CS no se usa (va a GND en el módulo)
|
#define TFT_CS -1 // CS no se usa (va a GND en el módulo)
|
||||||
#define TFT_DC 4 // Data/Command
|
#define TFT_DC 4 // Data/Command
|
||||||
#define TFT_RST 2 // Reset
|
#define TFT_RST 15 // Reset (movido desde GPIO 2 por conflicto con LED interno)
|
||||||
#define TFT_BL 32 // Backlight (LED) – PWM opcional
|
#define TFT_BL 32 // Backlight (LED) – PWM opcional
|
||||||
|
|
||||||
#define TFT_SPI_FREQUENCY 40000000
|
#define TFT_SPI_FREQUENCY 40000000
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
; PlatformIO Project Configuration File
|
; PlatformIO Project Configuration File
|
||||||
; DummyBot – task manager for ESP32 + ST7789 240x240 + 3x TTP223 + vibrador
|
; DummyBot – task manager ESP32
|
||||||
; Firmware: TFT_eSPI (texto) + SPIFFS + AnimatedGIF (caritas kawaii)
|
|
||||||
; Backend: Laravel (HTTP polling cada 30s)
|
|
||||||
|
|
||||||
[env:esp32dev]
|
[env:esp32dev]
|
||||||
platform = espressif32 @ 6.5.0
|
platform = espressif32 @ 6.5.0
|
||||||
@ -11,27 +9,29 @@ monitor_speed = 115200
|
|||||||
upload_speed = 921600
|
upload_speed = 921600
|
||||||
board_build.partitions = partitions.csv
|
board_build.partitions = partitions.csv
|
||||||
|
|
||||||
; Libs
|
; Libs mínimas (sin GIFs ni WiFiManager)
|
||||||
lib_deps =
|
lib_deps =
|
||||||
bodmer/TFT_eSPI@^2.5.43
|
bodmer/TFT_eSPI@^2.5.43
|
||||||
bblanchon/ArduinoJson@^7.0.4
|
bblanchon/ArduinoJson@^7.0.4
|
||||||
bitbank2/AnimatedGIF@^1.4.1
|
|
||||||
|
|
||||||
; User config del display (TFT_eSPI) y WiFi
|
|
||||||
build_flags =
|
build_flags =
|
||||||
-DCORE_DEBUG_LEVEL=3
|
-DCORE_DEBUG_LEVEL=3
|
||||||
-DUSER_SETUP_LOADED=1
|
-DUSER_SETUP_LOADED=1
|
||||||
-include $PROJECT_DIR/include/User_Setup.h
|
-include $PROJECT_DIR/include/User_Setup.h
|
||||||
-DWM_DEBUG_LEVEL=3
|
|
||||||
|
; ─── CONFIG POR DEFECTO ───
|
||||||
|
; Los defaults están en main.cpp (DEFAULT_WIFI_SSID, etc).
|
||||||
|
; Si quieres cambiarlos sin tocar código, define aquí las macros:
|
||||||
|
; -DDEFAULT_WIFI_SSID=\"otro\" -DDEFAULT_WIFI_PASS=\"otro\"
|
||||||
|
;
|
||||||
|
; Backend y token (sobreescribibles vía portal web)
|
||||||
-DBACKEND_URL=\"http://157.180.77.232:18080\"
|
-DBACKEND_URL=\"http://157.180.77.232:18080\"
|
||||||
-DDEVICE_TOKEN=\"tDL6yCqvVetJpiY8dMyi7QwCW2znO6RnRN5hVeM7H8u0LbjU\"
|
-DDEFAULT_DEVICE_TOKEN=\"tDL6yCqvVetJpiY8dMyi7QwCW2znO6RnRN5hVeM7H8u0LbjU\"
|
||||||
; Para apuntar a tu servidor, sobreescribe BACKEND_URL en una env:
|
|
||||||
; [env:mi-server]
|
|
||||||
; build_flags = ${env:esp32dev.build_flags} -DBACKEND_URL=\"https://midominio.com\"
|
|
||||||
build_src_filter = +<main.cpp> -<tests/>
|
build_src_filter = +<main.cpp> -<tests/>
|
||||||
|
|
||||||
; ─────────────────────────────────────────────────────────────
|
; ─────────────────────────────────────────────────────────────
|
||||||
; Entorno de DIAGNÓSTICO: solo compila diag.cpp (sin main.cpp)
|
; Entorno de DIAGNÓSTICO: solo compila diag.cpp
|
||||||
; ─────────────────────────────────────────────────────────────
|
; ─────────────────────────────────────────────────────────────
|
||||||
[env:diag]
|
[env:diag]
|
||||||
platform = espressif32 @ 6.5.0
|
platform = espressif32 @ 6.5.0
|
||||||
@ -45,16 +45,4 @@ build_flags =
|
|||||||
-DCORE_DEBUG_LEVEL=3
|
-DCORE_DEBUG_LEVEL=3
|
||||||
-DUSER_SETUP_LOADED=1
|
-DUSER_SETUP_LOADED=1
|
||||||
-include $PROJECT_DIR/include/User_Setup.h
|
-include $PROJECT_DIR/include/User_Setup.h
|
||||||
build_src_filter = +<tests/diag.cpp>
|
build_src_filter = +<tests/diag.cpp>
|
||||||
|
|
||||||
; ─────────────────────────────────────────────────────────────
|
|
||||||
; Test SPI loopback (para verificar el bus SPI del ESP32)
|
|
||||||
; ─────────────────────────────────────────────────────────────
|
|
||||||
[env:spiloop]
|
|
||||||
platform = espressif32 @ 6.5.0
|
|
||||||
board = esp32dev
|
|
||||||
framework = arduino
|
|
||||||
monitor_speed = 115200
|
|
||||||
upload_speed = 921600
|
|
||||||
lib_deps =
|
|
||||||
build_src_filter = +<tests/spi_loopback.cpp>
|
|
||||||
678
src/main.cpp
678
src/main.cpp
@ -1,17 +1,18 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// DummyBot – firmware ESP32 v3
|
// DummyBot – firmware ESP32 v4 (TEXTO + Portal)
|
||||||
// - Pantalla M130T-240240-RGB-7-V1.1 (ST7789 240x240 SPI)
|
// - Pantalla M130T-240240-RGB-7-V1.1 (ST7789 240x240)
|
||||||
|
// - ** IMPORTANTE: VCC de la pantalla en 5V (VIN), no 3V3 **
|
||||||
|
// - ** IMPORTANTE: RST de la pantalla en GPIO 15, no GPIO 2 **
|
||||||
// - 3x TTP223 (Siguiente / Hecho / Snooze)
|
// - 3x TTP223 (Siguiente / Hecho / Snooze)
|
||||||
// - 1x vibrador (GPIO 25 con transistor)
|
// - 1x vibrador (GPIO 25)
|
||||||
// - 12 GIFs animados en SPIFFS
|
// - WiFi (con reconexión) + HTTP polling al backend Laravel
|
||||||
// - ** Captive portal ** para configurar WiFi + token desde el móvil
|
// - Captive portal SIN GIFs (texto puro)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <TFT_eSPI.h>
|
#include <TFT_eSPI.h>
|
||||||
#include <SPIFFS.h>
|
#include <SPIFFS.h>
|
||||||
#include <AnimatedGIF.h>
|
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <WebServer.h>
|
#include <WebServer.h>
|
||||||
#include <DNSServer.h>
|
#include <DNSServer.h>
|
||||||
@ -31,114 +32,57 @@
|
|||||||
#define VIB_PATTERN_MS 1500
|
#define VIB_PATTERN_MS 1500
|
||||||
#define SNOOZE_MINUTES 5
|
#define SNOOZE_MINUTES 5
|
||||||
#define API_TIMEOUT_MS 4000
|
#define API_TIMEOUT_MS 4000
|
||||||
#define FACE_SIZE 80
|
|
||||||
|
|
||||||
const char* WIFI_AP_NAME = "DummyBot-Setup";
|
const char* WIFI_AP_NAME = "DummyBot-Setup";
|
||||||
const char* WIFI_AP_PASS = "dummybot"; // >=8 chars (requisito AP)
|
const char* WIFI_AP_PASS = "dummybot";
|
||||||
|
|
||||||
|
#ifndef DEFAULT_WIFI_SSID
|
||||||
|
#define DEFAULT_WIFI_SSID "BatKuevaUp"
|
||||||
|
#endif
|
||||||
|
#ifndef DEFAULT_WIFI_PASS
|
||||||
|
#define DEFAULT_WIFI_PASS "JaMon2026"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef BACKEND_URL
|
#ifndef BACKEND_URL
|
||||||
#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 "tDL6yCqvVetJpiY8dMyi7QwCW2znO6RnRN5hVeM7H8u0LbjU"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef DEFAULT_DEVICE_NAME
|
|
||||||
#define DEFAULT_DEVICE_NAME "Mi DummyBot"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define WIFI_CFG_PATH "/wifi.cfg"
|
|
||||||
|
|
||||||
// -------------------- Estado global --------------------
|
// -------------------- Estado global --------------------
|
||||||
TFT_eSPI tft = TFT_eSPI();
|
TFT_eSPI tft = TFT_eSPI();
|
||||||
AnimatedGIF gif;
|
WebServer web(80);
|
||||||
WebServer web(80);
|
DNSServer dnsServer;
|
||||||
DNSServer dnsServer;
|
|
||||||
|
|
||||||
// Cara
|
|
||||||
enum Emotion {
|
|
||||||
EMO_NEUTRAL = 1, EMO_HAPPY, EMO_WORRIED, EMO_SAD, EMO_CRY,
|
|
||||||
EMO_SURPRISED, EMO_ANGRY, EMO_SLEEPY, EMO_WINK, EMO_LOVE,
|
|
||||||
EMO_WORKING, EMO_EXCITED
|
|
||||||
};
|
|
||||||
struct Task {
|
struct Task {
|
||||||
int id;
|
int id;
|
||||||
char title[64];
|
char title[64];
|
||||||
char priority[8];
|
char priority[8];
|
||||||
int next_due_in;
|
int next_due_in;
|
||||||
};
|
};
|
||||||
|
|
||||||
Task currentTask;
|
Task currentTask;
|
||||||
bool hasTask = false;
|
bool hasTask = false;
|
||||||
uint32_t lastPoll = 0;
|
uint32_t lastPoll = 0;
|
||||||
uint32_t lastSeenId = 0;
|
uint32_t lastSeenId = 0;
|
||||||
Emotion currentEmotion = EMO_NEUTRAL;
|
|
||||||
char currentGifName[32];
|
|
||||||
|
|
||||||
// Config WiFi + backend + token (se carga de /wifi.cfg al arrancar)
|
|
||||||
char cfgSsid[64] = "";
|
|
||||||
char cfgPass[64] = "";
|
|
||||||
char cfgToken[80] = DEFAULT_DEVICE_TOKEN;
|
|
||||||
char cfgBackend[128] = BACKEND_URL;
|
|
||||||
char cfgName[40] = DEFAULT_DEVICE_NAME;
|
|
||||||
|
|
||||||
// GIF rendering
|
|
||||||
int16_t gif_x = 0, gif_y = 0;
|
|
||||||
bool gifOpen = false;
|
|
||||||
int lastGifDelay = 0;
|
|
||||||
uint32_t nextGifMs = 0;
|
|
||||||
|
|
||||||
// Vibrador
|
|
||||||
bool vibActive = false;
|
bool vibActive = false;
|
||||||
uint32_t vibStart = 0;
|
uint32_t vibStart = 0;
|
||||||
|
|
||||||
// Botones
|
|
||||||
uint32_t lastBtnMs[3] = {0,0,0};
|
uint32_t lastBtnMs[3] = {0,0,0};
|
||||||
const uint8_t BTN_PINS[3] = { PIN_BTN_NEXT, PIN_BTN_DONE, PIN_BTN_SNOOZE };
|
const uint8_t BTN_PINS[3] = { PIN_BTN_NEXT, PIN_BTN_DONE, PIN_BTN_SNOOZE };
|
||||||
#define DEBOUNCE_MS 200
|
#define DEBOUNCE_MS 200
|
||||||
|
|
||||||
// Estado de conexión WiFi
|
// Config persistente
|
||||||
enum WifiState { WIFI_BOOTING, WIFI_PORTAL, WIFI_CONNECTING, WIFI_CONNECTED, WIFI_FAILED };
|
#define WIFI_CFG_PATH "/wifi.cfg"
|
||||||
WifiState wifiState = WIFI_BOOTING;
|
char cfgSsid[64] = "";
|
||||||
uint32_t wifiConnectStart = 0;
|
char cfgPass[64] = "";
|
||||||
uint32_t lastPortalTry = 0;
|
char cfgToken[80] = "";
|
||||||
|
char cfgBackend[128] = "";
|
||||||
// -------------------- GIF callbacks --------------------
|
char cfgName[40] = "Mi DummyBot";
|
||||||
static void *gif_open_cb(const char *fname, int32_t *pSize) {
|
bool portalActive = false;
|
||||||
fs::File *f = new fs::File(SPIFFS.open(fname, "r"));
|
|
||||||
if (!*f) { delete f; return nullptr; }
|
|
||||||
*pSize = f->size();
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
static void gif_close_cb(void *handle) {
|
|
||||||
if (!handle) return;
|
|
||||||
fs::File *f = (fs::File *)handle;
|
|
||||||
f->close();
|
|
||||||
delete f;
|
|
||||||
}
|
|
||||||
static int32_t gif_read_cb(GIFFILE *pFile, uint8_t *pBuf, int32_t iLen) {
|
|
||||||
fs::File *f = (fs::File *)pFile->fHandle;
|
|
||||||
return f->read(pBuf, iLen);
|
|
||||||
}
|
|
||||||
static int32_t gif_seek_cb(GIFFILE *pFile, int32_t iPos) {
|
|
||||||
fs::File *f = (fs::File *)pFile->fHandle;
|
|
||||||
return f->seek(iPos) ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void gif_draw_callback(GIFDRAW *pDraw) {
|
|
||||||
uint16_t *pixels = (uint16_t *)pDraw->pPixels;
|
|
||||||
int16_t x = pDraw->iX + gif_x;
|
|
||||||
int16_t y = pDraw->iY + gif_y;
|
|
||||||
int16_t w = pDraw->iWidth;
|
|
||||||
if (pDraw->iY == 0) {
|
|
||||||
tft.startWrite();
|
|
||||||
tft.setAddrWindow(x, y, w, 1);
|
|
||||||
}
|
|
||||||
tft.pushColors(pixels, w, false);
|
|
||||||
if (pDraw->iY == pDraw->iHeight - 1) {
|
|
||||||
tft.endWrite();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- Vib --------------------
|
// -------------------- Vib --------------------
|
||||||
static void startVib() {
|
static void startVib() {
|
||||||
@ -159,41 +103,13 @@ static void updateVib() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- UI --------------------
|
// -------------------- UI --------------------
|
||||||
static void emotionToGif(Emotion e, char *out, size_t sz) {
|
static void drawHeader(const char *title) {
|
||||||
const char *name = "01_neutral.gif";
|
|
||||||
switch (e) {
|
|
||||||
case EMO_NEUTRAL: name = "01_neutral.gif"; break;
|
|
||||||
case EMO_HAPPY: name = "02_happy.gif"; break;
|
|
||||||
case EMO_WORRIED: name = "03_worried.gif"; break;
|
|
||||||
case EMO_SAD: name = "04_sad.gif"; break;
|
|
||||||
case EMO_CRY: name = "05_cry.gif"; break;
|
|
||||||
case EMO_SURPRISED: name = "06_surprised.gif"; break;
|
|
||||||
case EMO_ANGRY: name = "07_angry.gif"; break;
|
|
||||||
case EMO_SLEEPY: name = "08_sleepy.gif"; break;
|
|
||||||
case EMO_WINK: name = "09_wink.gif"; break;
|
|
||||||
case EMO_LOVE: name = "10_love.gif"; break;
|
|
||||||
case EMO_WORKING: name = "11_working.gif"; break;
|
|
||||||
case EMO_EXCITED: name = "12_excited.gif"; break;
|
|
||||||
}
|
|
||||||
strncpy(out, name, sz - 1);
|
|
||||||
out[sz - 1] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
static Emotion emotionForTask(const Task *t) {
|
|
||||||
if (!t) return EMO_NEUTRAL;
|
|
||||||
if (strcmp(t->priority, "high") == 0) return EMO_WORKING;
|
|
||||||
if (strcmp(t->priority, "low") == 0) return EMO_HAPPY;
|
|
||||||
return EMO_NEUTRAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void drawHeader() {
|
|
||||||
tft.fillRect(0, 0, 240, 22, TFT_NAVY);
|
tft.fillRect(0, 0, 240, 22, TFT_NAVY);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
||||||
tft.setTextSize(1);
|
tft.setTextSize(1);
|
||||||
tft.setTextDatum(TL_DATUM);
|
tft.setTextDatum(TL_DATUM);
|
||||||
tft.drawString("DummyBot - tareas", 6, 6, 2);
|
tft.drawString(title, 6, 6, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void drawFooter(const char *line) {
|
static void drawFooter(const char *line) {
|
||||||
tft.fillRect(0, 218, 240, 22, TFT_DARKGREY);
|
tft.fillRect(0, 218, 240, 22, TFT_DARKGREY);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_DARKGREY);
|
tft.setTextColor(TFT_WHITE, TFT_DARKGREY);
|
||||||
@ -201,37 +117,58 @@ static void drawFooter(const char *line) {
|
|||||||
tft.setTextDatum(BC_DATUM);
|
tft.setTextDatum(BC_DATUM);
|
||||||
tft.drawString(line, 120, 230, 1);
|
tft.drawString(line, 120, 230, 1);
|
||||||
}
|
}
|
||||||
|
static void drawWifiIcon(int16_t x, int16_t y, bool connected) {
|
||||||
|
uint16_t color = connected ? TFT_GREEN : TFT_RED;
|
||||||
|
tft.fillCircle(x, y, 4, color);
|
||||||
|
}
|
||||||
|
|
||||||
static void drawIdle() {
|
static void drawIdleScreen() {
|
||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
drawHeader();
|
drawHeader("DummyBot");
|
||||||
|
drawWifiIcon(220, 11, WiFi.status() == WL_CONNECTED);
|
||||||
|
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setTextDatum(MC_DATUM);
|
tft.setTextDatum(MC_DATUM);
|
||||||
tft.setTextSize(2);
|
tft.setTextSize(2);
|
||||||
tft.drawString("Sin tareas :)", 120, 100, 4);
|
tft.drawString("Sin tareas :)", 120, 100, 4);
|
||||||
tft.setTextSize(1);
|
tft.setTextSize(1);
|
||||||
tft.drawString("Anade desde la web", 120, 140, 2);
|
tft.drawString("Anade desde la web", 120, 140, 2);
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
|
||||||
|
tft.drawString("WiFi: sin conexion", 120, 170, 2);
|
||||||
|
} else {
|
||||||
|
tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK);
|
||||||
|
tft.drawString(WiFi.localIP().toString().c_str(), 120, 170, 2);
|
||||||
|
}
|
||||||
drawFooter("Snooze | Hecha | Siguiente");
|
drawFooter("Snooze | Hecha | Siguiente");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void drawTaskScreen() {
|
static void drawTaskScreen() {
|
||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
drawHeader();
|
drawHeader("DummyBot - tarea");
|
||||||
|
drawWifiIcon(220, 11, WiFi.status() == WL_CONNECTED);
|
||||||
|
|
||||||
uint16_t c = (strcmp(currentTask.priority, "high") == 0) ? TFT_RED :
|
uint16_t c = (strcmp(currentTask.priority, "high") == 0) ? TFT_RED :
|
||||||
(strcmp(currentTask.priority, "low") == 0) ? TFT_GREEN : TFT_YELLOW;
|
(strcmp(currentTask.priority, "low") == 0) ? TFT_GREEN : TFT_YELLOW;
|
||||||
tft.fillRect(0, 22, 240, 12, c);
|
tft.fillRect(0, 22, 240, 14, c);
|
||||||
tft.setTextColor(TFT_BLACK, c);
|
tft.setTextColor(TFT_BLACK, c);
|
||||||
tft.setTextDatum(MC_DATUM);
|
tft.setTextDatum(MC_DATUM);
|
||||||
tft.setTextSize(1);
|
tft.setTextSize(1);
|
||||||
String prio = String("PRIORIDAD ") + currentTask.priority;
|
String prio = String("PRIORIDAD ") + currentTask.priority;
|
||||||
tft.drawString(prio, 120, 24, 2);
|
tft.drawString(prio, 120, 24, 2);
|
||||||
|
|
||||||
|
const char* emoji = "(._.)";
|
||||||
|
if (strcmp(currentTask.priority, "high") == 0) emoji = "/(>_<)\\";
|
||||||
|
else if (strcmp(currentTask.priority, "low") == 0) emoji = "^_^";
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
|
tft.setTextDatum(MC_DATUM);
|
||||||
|
tft.setTextSize(4);
|
||||||
|
tft.drawString(emoji, 120, 80, 4);
|
||||||
|
|
||||||
tft.setTextSize(2);
|
tft.setTextSize(2);
|
||||||
tft.setTextDatum(TL_DATUM);
|
tft.setTextDatum(TL_DATUM);
|
||||||
String title = String(currentTask.title);
|
String title = String(currentTask.title);
|
||||||
int y = 110;
|
int y = 130;
|
||||||
int maxChars = 16;
|
int maxChars = 16;
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
while (idx < (int)title.length() && y < 200) {
|
while (idx < (int)title.length() && y < 200) {
|
||||||
@ -240,90 +177,35 @@ static void drawTaskScreen() {
|
|||||||
y += 28;
|
y += 28;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentTask.next_due_in > 0) {
|
|
||||||
tft.setTextSize(1);
|
|
||||||
tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK);
|
|
||||||
int mins = currentTask.next_due_in / 60;
|
|
||||||
String due = "Vence en ";
|
|
||||||
if (mins >= 60) due += String(mins / 60) + "h " + String(mins % 60) + "m";
|
|
||||||
else if (mins > 0) due += String(mins) + " min";
|
|
||||||
else due += "ahora";
|
|
||||||
tft.drawString(due, 8, 195, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawFooter("Snooze | Hecha | Siguiente");
|
drawFooter("Snooze | Hecha | Siguiente");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void drawEmotionFallback(Emotion e) {
|
static void drawWifiScreen(const char *msg) {
|
||||||
static const char* labels[] = {
|
tft.fillScreen(TFT_BLACK);
|
||||||
"OK", "HA", "WO", "SA", "CR", "SU", "AN", "SL", "WI", "LO", "WK", "EX"
|
drawHeader("DummyBot - WiFi");
|
||||||
};
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
int idx = (int)e - 1;
|
|
||||||
if (idx < 0 || idx >= 12) idx = 0;
|
|
||||||
tft.fillRect(gif_x, gif_y, FACE_SIZE, FACE_SIZE, TFT_NAVY);
|
|
||||||
tft.setTextColor(TFT_WHITE, TFT_NAVY);
|
|
||||||
tft.setTextDatum(MC_DATUM);
|
tft.setTextDatum(MC_DATUM);
|
||||||
tft.setTextSize(4);
|
tft.setTextSize(2);
|
||||||
tft.drawString(labels[idx], gif_x + FACE_SIZE / 2, gif_y + FACE_SIZE / 2 - 10, 4);
|
tft.drawString(msg, 120, 80, 2);
|
||||||
tft.setTextSize(1);
|
tft.setTextSize(1);
|
||||||
tft.drawString(currentGifName, gif_x + FACE_SIZE / 2, gif_y + FACE_SIZE / 2 + 18, 1);
|
tft.drawString(WIFI_AP_NAME, 120, 115, 2);
|
||||||
|
tft.drawString(WIFI_AP_PASS, 120, 135, 2);
|
||||||
|
drawFooter("conecta el movil para configurar");
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- GIF control --------------------
|
// -------------------- HTTP --------------------
|
||||||
static void setEmotion(Emotion e) {
|
|
||||||
if (e == currentEmotion && gifOpen) return;
|
|
||||||
char name[32];
|
|
||||||
emotionToGif(e, name, sizeof(name));
|
|
||||||
if (strcmp(name, currentGifName) == 0 && gifOpen) return;
|
|
||||||
|
|
||||||
if (gifOpen) { gif.close(); gifOpen = false; }
|
|
||||||
strncpy(currentGifName, name, sizeof(currentGifName) - 1);
|
|
||||||
|
|
||||||
char path[40];
|
|
||||||
snprintf(path, sizeof(path), "/gifs/%s", currentGifName);
|
|
||||||
gif_x = 240 - FACE_SIZE - 4;
|
|
||||||
gif_y = 28;
|
|
||||||
|
|
||||||
if (SPIFFS.exists(path)) {
|
|
||||||
tft.fillRect(gif_x, gif_y, FACE_SIZE, FACE_SIZE, TFT_WHITE);
|
|
||||||
if (gif.open(path, gif_open_cb, gif_close_cb,
|
|
||||||
gif_read_cb, gif_seek_cb, gif_draw_callback)) {
|
|
||||||
gifOpen = true;
|
|
||||||
currentEmotion = e;
|
|
||||||
nextGifMs = millis();
|
|
||||||
} else {
|
|
||||||
drawEmotionFallback(e);
|
|
||||||
currentEmotion = e;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
drawEmotionFallback(e);
|
|
||||||
currentEmotion = e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void updateGif() {
|
|
||||||
if (!gifOpen) return;
|
|
||||||
if ((int32_t)(millis() - nextGifMs) >= 0) {
|
|
||||||
int delay_ms = 0;
|
|
||||||
if (gif.playFrame(false, &delay_ms) == -1) {
|
|
||||||
gif.reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
nextGifMs = millis() + (delay_ms > 0 ? delay_ms : 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- HTTP cliente --------------------
|
|
||||||
static bool httpGetJson(const char *url, JsonDocument &doc) {
|
static bool httpGetJson(const char *url, JsonDocument &doc) {
|
||||||
if (WiFi.status() != WL_CONNECTED) return false;
|
if (WiFi.status() != WL_CONNECTED) return false;
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
http.begin(url);
|
http.begin(url);
|
||||||
http.setTimeout(API_TIMEOUT_MS);
|
http.setTimeout(API_TIMEOUT_MS);
|
||||||
if (strlen(cfgToken) > 0) {
|
http.addHeader("Authorization", String("Bearer ") + String(DEFAULT_DEVICE_TOKEN));
|
||||||
http.addHeader("Authorization", String("Bearer ") + cfgToken);
|
|
||||||
}
|
|
||||||
int code = http.GET();
|
int code = http.GET();
|
||||||
if (code != 200) { http.end(); return false; }
|
if (code != 200) {
|
||||||
|
Serial.printf("HTTP %d en %s\n", code, url);
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
DeserializationError err = deserializeJson(doc, http.getStream());
|
DeserializationError err = deserializeJson(doc, http.getStream());
|
||||||
http.end();
|
http.end();
|
||||||
return !err;
|
return !err;
|
||||||
@ -334,27 +216,28 @@ static bool httpPostJson(const char *url, const char *body) {
|
|||||||
http.begin(url);
|
http.begin(url);
|
||||||
http.setTimeout(API_TIMEOUT_MS);
|
http.setTimeout(API_TIMEOUT_MS);
|
||||||
http.addHeader("Content-Type", "application/json");
|
http.addHeader("Content-Type", "application/json");
|
||||||
if (strlen(cfgToken) > 0) {
|
http.addHeader("Authorization", String("Bearer ") + String(DEFAULT_DEVICE_TOKEN));
|
||||||
http.addHeader("Authorization", String("Bearer ") + cfgToken);
|
|
||||||
}
|
|
||||||
int code = http.POST(body);
|
int code = http.POST(body);
|
||||||
http.end();
|
http.end();
|
||||||
return code >= 200 && code < 300;
|
return code >= 200 && code < 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Lógica de tareas --------------------
|
// -------------------- Lógica --------------------
|
||||||
static void pollTasks() {
|
static void pollTasks() {
|
||||||
if (WiFi.status() != WL_CONNECTED) return;
|
if (WiFi.status() != WL_CONNECTED) return;
|
||||||
char url[160];
|
char url[160];
|
||||||
snprintf(url, sizeof(url), "%s/api/device/tasks/pending", cfgBackend);
|
snprintf(url, sizeof(url), "%s/api/device/tasks/pending", BACKEND_URL);
|
||||||
|
Serial.printf("GET %s\n", url);
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
if (!httpGetJson(url, doc)) return;
|
if (!httpGetJson(url, doc)) {
|
||||||
|
drawIdleScreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
JsonArray arr = doc.as<JsonArray>();
|
JsonArray arr = doc.as<JsonArray>();
|
||||||
if (arr.size() == 0) {
|
if (arr.size() == 0) {
|
||||||
hasTask = false;
|
hasTask = false;
|
||||||
if (currentEmotion != EMO_SLEEPY) setEmotion(EMO_SLEEPY);
|
drawIdleScreen();
|
||||||
drawIdle();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
JsonObject first = arr[0];
|
JsonObject first = arr[0];
|
||||||
@ -368,42 +251,33 @@ static void pollTasks() {
|
|||||||
if (lastSeenId != 0) startVib();
|
if (lastSeenId != 0) startVib();
|
||||||
lastSeenId = (uint32_t)t.id;
|
lastSeenId = (uint32_t)t.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTask = t;
|
currentTask = t;
|
||||||
hasTask = true;
|
hasTask = true;
|
||||||
drawTaskScreen();
|
drawTaskScreen();
|
||||||
setEmotion(emotionForTask(&t));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void actionDone() {
|
static void actionDone() {
|
||||||
if (!hasTask) return;
|
if (!hasTask) return;
|
||||||
char url[160], body[8];
|
char url[160];
|
||||||
snprintf(url, sizeof(url), "%s/api/device/tasks/%d/done", cfgBackend, currentTask.id);
|
snprintf(url, sizeof(url), "%s/api/device/tasks/%d/done", BACKEND_URL, currentTask.id);
|
||||||
snprintf(body, sizeof(body), "{}");
|
if (httpPostJson(url, "{}")) {
|
||||||
if (httpPostJson(url, body)) {
|
|
||||||
lastSeenId = 0;
|
lastSeenId = 0;
|
||||||
setEmotion(EMO_WINK);
|
|
||||||
delay(800);
|
|
||||||
pollTasks();
|
pollTasks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
static void actionSnooze() {
|
static void actionSnooze() {
|
||||||
if (!hasTask) return;
|
if (!hasTask) return;
|
||||||
char url[160], body[40];
|
char url[160], body[40];
|
||||||
snprintf(url, sizeof(url), "%s/api/device/tasks/%d/snooze", cfgBackend, currentTask.id);
|
snprintf(url, sizeof(url), "%s/api/device/tasks/%d/snooze", BACKEND_URL, currentTask.id);
|
||||||
snprintf(body, sizeof(body), "{\"minutes\":%d}", SNOOZE_MINUTES);
|
snprintf(body, sizeof(body), "{\"minutes\":%d}", SNOOZE_MINUTES);
|
||||||
if (httpPostJson(url, body)) {
|
if (httpPostJson(url, body)) {
|
||||||
lastSeenId = 0;
|
lastSeenId = 0;
|
||||||
setEmotion(EMO_WORRIED);
|
|
||||||
delay(800);
|
|
||||||
pollTasks();
|
pollTasks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
static void actionNext() { pollTasks(); }
|
static void actionNext() { pollTasks(); }
|
||||||
|
|
||||||
// -------------------- Botones --------------------
|
|
||||||
static void handleButtons() {
|
static void handleButtons() {
|
||||||
if (wifiState == WIFI_PORTAL) return; // no atender botones durante config
|
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
if (digitalRead(BTN_PINS[i]) == HIGH && (now - lastBtnMs[i] > DEBOUNCE_MS)) {
|
if (digitalRead(BTN_PINS[i]) == HIGH && (now - lastBtnMs[i] > DEBOUNCE_MS)) {
|
||||||
@ -417,151 +291,71 @@ static void handleButtons() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Persistencia de config --------------------
|
// -------------------- Config persistente --------------------
|
||||||
static bool loadConfig() {
|
static void loadConfig() {
|
||||||
if (!SPIFFS.exists(WIFI_CFG_PATH)) return false;
|
if (!SPIFFS.exists(WIFI_CFG_PATH)) return;
|
||||||
fs::File f = SPIFFS.open(WIFI_CFG_PATH, "r");
|
fs::File f = SPIFFS.open(WIFI_CFG_PATH, "r");
|
||||||
if (!f) return false;
|
if (!f) return;
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
DeserializationError err = deserializeJson(doc, f);
|
if (deserializeJson(doc, f) == DeserializationError::Ok) {
|
||||||
|
strncpy(cfgSsid, doc["ssid"] | "", sizeof(cfgSsid) - 1);
|
||||||
|
strncpy(cfgPass, doc["pass"] | "", sizeof(cfgPass) - 1);
|
||||||
|
strncpy(cfgToken, doc["token"] | DEFAULT_DEVICE_TOKEN, sizeof(cfgToken) - 1);
|
||||||
|
strncpy(cfgBackend, doc["backend"] | BACKEND_URL, sizeof(cfgBackend) - 1);
|
||||||
|
strncpy(cfgName, doc["name"] | "Mi DummyBot", sizeof(cfgName) - 1);
|
||||||
|
Serial.printf("Config cargada de SPIFFS: SSID='%s'\n", cfgSsid);
|
||||||
|
}
|
||||||
f.close();
|
f.close();
|
||||||
if (err) return false;
|
|
||||||
strncpy(cfgSsid, doc["ssid"] | "", sizeof(cfgSsid) - 1);
|
|
||||||
strncpy(cfgPass, doc["pass"] | "", sizeof(cfgPass) - 1);
|
|
||||||
strncpy(cfgToken, doc["token"] | DEFAULT_DEVICE_TOKEN, sizeof(cfgToken) - 1);
|
|
||||||
strncpy(cfgBackend, doc["backend"] | BACKEND_URL, sizeof(cfgBackend) - 1);
|
|
||||||
strncpy(cfgName, doc["name"] | DEFAULT_DEVICE_NAME, sizeof(cfgName) - 1);
|
|
||||||
return strlen(cfgSsid) > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void saveConfig() {
|
static void saveConfig() {
|
||||||
|
fs::File f = SPIFFS.open(WIFI_CFG_PATH, "w");
|
||||||
|
if (!f) return;
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
doc["ssid"] = cfgSsid;
|
doc["ssid"] = cfgSsid;
|
||||||
doc["pass"] = cfgPass;
|
doc["pass"] = cfgPass;
|
||||||
doc["token"] = cfgToken;
|
doc["token"] = cfgToken;
|
||||||
doc["backend"] = cfgBackend;
|
doc["backend"] = cfgBackend;
|
||||||
doc["name"] = cfgName;
|
doc["name"] = cfgName;
|
||||||
fs::File f = SPIFFS.open(WIFI_CFG_PATH, "w");
|
|
||||||
if (!f) {
|
|
||||||
Serial.println("ERROR abriendo /wifi.cfg para escritura");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
serializeJson(doc, f);
|
serializeJson(doc, f);
|
||||||
f.close();
|
f.close();
|
||||||
Serial.println("Config guardada en SPIFFS");
|
Serial.println("Config guardada en SPIFFS");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void resetConfig() {
|
static void resetConfig() {
|
||||||
if (SPIFFS.exists(WIFI_CFG_PATH)) SPIFFS.remove(WIFI_CFG_PATH);
|
if (SPIFFS.exists(WIFI_CFG_PATH)) SPIFFS.remove(WIFI_CFG_PATH);
|
||||||
|
cfgSsid[0] = cfgPass[0] = cfgToken[0] = cfgBackend[0] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Captive portal --------------------
|
// -------------------- Portal --------------------
|
||||||
static const char PORTAL_HTML[] PROGMEM = R"=====(<!DOCTYPE html>
|
static const char PORTAL_HTML[] PROGMEM = R"=====(<!DOCTYPE html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>DummyBot</title><style>:root{color-scheme:dark}*{box-sizing:border-box}body{margin:0;font-family:-apple-system,sans-serif;background:#0a0a0f;color:#f4f4f5;min-height:100vh;padding:1rem;display:flex;align-items:center;justify-content:center}.c{max-width:480px;width:100%}.card{background:#18181b;border:1px solid rgba(255,255,255,0.08);border-radius:16px;padding:1.5rem;box-shadow:0 20px 60px rgba(0,0,0,0.5)}h1{margin:0 0 .25rem;font-size:1.4rem;background:linear-gradient(135deg,#818cf8,#c084fc);-webkit-background-clip:text;background-clip:text;color:transparent}p.lead{margin:0 0 1rem;color:#a1a1aa;font-size:.85rem}label{display:block;font-size:.7rem;color:#a1a1aa;margin:.8rem 0 .25rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em}input{width:100%;padding:.55rem .75rem;background:#09090b;border:1px solid rgba(255,255,255,0.08);color:#f4f4f5;border-radius:8px;font-size:1rem;font-family:inherit}input:focus{outline:0;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,0.2)}button{margin-top:1rem;width:100%;padding:.65rem;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:white;border:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer}.help{font-size:.7rem;color:#71717a;margin-top:.2rem}.div{height:1px;background:rgba(255,255,255,0.08);margin:1rem 0}.emoji{font-size:2rem;margin-bottom:.3rem}</style></head><body><div class="c"><div class="card"><div class="emoji">🤖</div><h1>DummyBot</h1><p class="lead">Configura tu bot. Solo lo harás una vez.</p><form method="POST" action="/save"><label>Nombre del bot</label><input type="text" name="name" value="__NAME__" required maxlength="40"><label>Nombre de tu WiFi (SSID)</label><input type="text" name="ssid" value="__SSID__" required maxlength="32" autofocus><div class="help">Solo redes 2.4 GHz</div><label>Contraseña WiFi</label><input type="password" name="pass" value="__PASS__" maxlength="64"><label>URL del servidor</label><input type="url" name="backend" value="__BACKEND__" required><label>Token del dispositivo</label><input type="text" name="token" value="__TOKEN__" required maxlength="80"><div class="help">En la web: Ajustes → Dispositivos</div><button type="submit">💾 Guardar y conectar</button></form><div class="div"></div><form method="POST" action="/reset" onsubmit="return confirm('¿Borrar config?')"><button type="submit" style="background:transparent;border:1px solid rgba(239,68,68,0.4);color:#fca5a5">🗑 Borrar configuración</button></form></div></div></body></html>)=====";
|
||||||
<html lang="es">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>DummyBot - Configuración</title>
|
|
||||||
<style>
|
|
||||||
:root{color-scheme:dark}
|
|
||||||
*{box-sizing:border-box}
|
|
||||||
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:#0a0a0f;color:#f4f4f5;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem}
|
|
||||||
.wrap{max-width:480px;width:100%}
|
|
||||||
.card{background:#18181b;border:1px solid rgba(255,255,255,0.06);border-radius:16px;padding:1.5rem;box-shadow:0 20px 60px rgba(0,0,0,.5)}
|
|
||||||
h1{margin:0 0 .25rem;font-size:1.5rem;background:linear-gradient(135deg,#818cf8,#c084fc);-webkit-background-clip:text;background-clip:text;color:transparent}
|
|
||||||
p.lead{margin:0 0 1.5rem;color:#a1a1aa;font-size:.9rem}
|
|
||||||
label{display:block;font-size:.75rem;color:#a1a1aa;margin-bottom:.25rem;margin-top:.85rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em}
|
|
||||||
input,select{width:100%;padding:.6rem .8rem;background:#09090b;border:1px solid rgba(255,255,255,0.08);color:#f4f4f5;border-radius:8px;font-size:1rem;font-family:inherit}
|
|
||||||
input:focus{outline:0;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.2)}
|
|
||||||
button{margin-top:1.25rem;width:100%;padding:.7rem;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:white;border:none;border-radius:8px;font-weight:600;font-size:1rem;cursor:pointer}
|
|
||||||
button:hover{transform:translateY(-1px);box-shadow:0 10px 30px rgba(99,102,241,.4)}
|
|
||||||
button:active{transform:translateY(0)}
|
|
||||||
.help{font-size:.7rem;color:#71717a;margin-top:.25rem}
|
|
||||||
.divider{height:1px;background:rgba(255,255,255,0.06);margin:1.25rem 0}
|
|
||||||
.row{display:flex;gap:.5rem;margin-top:.5rem}
|
|
||||||
.row input{flex:1}
|
|
||||||
.status{padding:.5rem .75rem;border-radius:6px;font-size:.8rem;margin-bottom:1rem}
|
|
||||||
.status.warn{background:rgba(234,179,8,.1);color:#fde68a;border:1px solid rgba(234,179,8,.3)}
|
|
||||||
.status.info{background:rgba(99,102,241,.1);color:#c7d2fe;border:1px solid rgba(99,102,241,.3)}
|
|
||||||
@keyframes spin{to{transform:rotate(360deg)}}
|
|
||||||
.spinner{display:inline-block;width:1rem;height:1rem;border:2px solid #6366f1;border-top-color:transparent;border-radius:50%;animation:spin .8s linear infinite;vertical-align:middle;margin-right:.4rem}
|
|
||||||
.emoji{font-size:2.5rem;margin-bottom:.5rem}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="wrap">
|
|
||||||
<div class="card">
|
|
||||||
<div class="emoji">🤖</div>
|
|
||||||
<h1>DummyBot</h1>
|
|
||||||
<p class="lead">Configura tu bot. Solo lo harás una vez.</p>
|
|
||||||
|
|
||||||
<div class="status info">
|
|
||||||
<strong>📡 Estás conectado al bot.</strong> Vuelve a conectarte a tu WiFi normal después de guardar.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="POST" action="/save">
|
|
||||||
<label>Nombre del bot</label>
|
|
||||||
<input type="text" name="name" value="__NAME__" required maxlength="40">
|
|
||||||
<div class="help">Para distinguirlo si tienes varios</div>
|
|
||||||
|
|
||||||
<label>Nombre de tu WiFi (SSID)</label>
|
|
||||||
<input type="text" name="ssid" value="" required maxlength="32" autofocus>
|
|
||||||
<div class="help">Solo redes 2.4 GHz (las 5 GHz no las soporta el ESP32)</div>
|
|
||||||
|
|
||||||
<label>Contraseña WiFi</label>
|
|
||||||
<input type="password" name="pass" value="" maxlength="64">
|
|
||||||
|
|
||||||
<label>URL del servidor DummyBot</label>
|
|
||||||
<input type="url" name="backend" value="__BACKEND__" required>
|
|
||||||
<div class="help">Donde está tu Laravel (ej: https://dummybot.example.com)</div>
|
|
||||||
|
|
||||||
<label>Token del dispositivo</label>
|
|
||||||
<input type="text" name="token" value="__TOKEN__" required maxlength="80">
|
|
||||||
<div class="help">Lo copias en la web: <strong>Ajustes → Dispositivos → Ver token</strong></div>
|
|
||||||
|
|
||||||
<button type="submit">💾 Guardar y conectar</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
<form method="POST" action="/reset" onsubmit="return confirm('¿Borrar config y volver al portal?')">
|
|
||||||
<button type="submit" style="background:transparent;border:1px solid rgba(239,68,68,.4);color:#fca5a5">🗑 Borrar configuración</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>)=====";
|
|
||||||
|
|
||||||
static String renderPortal() {
|
static String renderPortal() {
|
||||||
String html = FPSTR(PORTAL_HTML);
|
String html = FPSTR(PORTAL_HTML);
|
||||||
html.replace("__NAME__", String(cfgName));
|
html.replace("__NAME__", String(cfgName));
|
||||||
html.replace("__BACKEND__", String(cfgBackend));
|
html.replace("__SSID__", String(cfgSsid));
|
||||||
|
html.replace("__PASS__", String(cfgPass));
|
||||||
|
html.replace("__BACKEND__", String(cfgBackend[0] ? cfgBackend : BACKEND_URL));
|
||||||
html.replace("__TOKEN__", String(cfgToken));
|
html.replace("__TOKEN__", String(cfgToken));
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirige cualquier DNS al portal (captive portal style)
|
|
||||||
static void handleCaptive() {
|
|
||||||
web.sendHeader("Location", "http://192.168.4.1/");
|
|
||||||
web.send(302, "text/plain", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void handleRoot() {
|
static void handleRoot() {
|
||||||
web.send(200, "text/html; charset=utf-8", renderPortal());
|
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() {
|
static void handleSave() {
|
||||||
if (!web.hasArg("ssid") || !web.hasArg("backend") || !web.hasArg("token")) {
|
if (!web.hasArg("ssid")) { web.send(400, "text/html", "Falta SSID"); return; }
|
||||||
web.send(400, "text/html", "<h1>Falta SSID, backend o token</h1>");
|
String s = web.arg("ssid"); s.trim();
|
||||||
return;
|
if (!s.length()) { web.send(400, "text/html", "SSID vacio"); return; }
|
||||||
}
|
|
||||||
String s = web.arg("ssid");
|
|
||||||
String p = web.arg("pass");
|
String p = web.arg("pass");
|
||||||
String t = web.arg("token");
|
String t = web.arg("token");
|
||||||
String b = web.arg("backend");
|
String b = web.arg("backend");
|
||||||
String n = web.hasArg("name") ? web.arg("name") : DEFAULT_DEVICE_NAME;
|
String n = web.hasArg("name") ? web.arg("name") : "Mi DummyBot";
|
||||||
|
p.trim(); t.trim(); b.trim(); n.trim();
|
||||||
s.trim(); p.trim(); t.trim(); b.trim(); n.trim();
|
if (!b.length()) b = BACKEND_URL;
|
||||||
if (s.length() == 0) { web.send(400, "text/html", "<h1>SSID vacío</h1>"); return; }
|
if (!t.length()) t = DEFAULT_DEVICE_TOKEN;
|
||||||
|
|
||||||
strncpy(cfgSsid, s.c_str(), sizeof(cfgSsid) - 1);
|
strncpy(cfgSsid, s.c_str(), sizeof(cfgSsid) - 1);
|
||||||
strncpy(cfgPass, p.c_str(), sizeof(cfgPass) - 1);
|
strncpy(cfgPass, p.c_str(), sizeof(cfgPass) - 1);
|
||||||
@ -570,29 +364,12 @@ static void handleSave() {
|
|||||||
strncpy(cfgName, n.c_str(), sizeof(cfgName) - 1);
|
strncpy(cfgName, n.c_str(), sizeof(cfgName) - 1);
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
|
||||||
String ok = R"=====(<!DOCTYPE html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
String ok = R"=====(<!DOCTYPE html><html><head><meta charset="UTF-8"><title>OK</title><style>body{margin:0;font-family:-apple-system,sans-serif;background:#0a0a0f;color:#f4f4f5;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem}.c{max-width:480px;background:#18181b;border:1px solid rgba(255,255,255,0.08);border-radius:16px;padding:2rem;text-align:center}h1{color:#86efac;font-size:1.8rem;margin:0 0 .5rem}.e{font-size:3.5rem;margin-bottom:1rem}p{color:#a1a1aa}</style></head><body><div class="c"><div class="e">✅</div><h1>Guardado</h1><p>El bot se está reiniciando.</p><p style="margin-top:1rem;color:#818cf8">👉 Conéctate a <strong>__SSID__</strong></p></div></body></html>)=====";
|
||||||
<title>DummyBot · Guardado</title><style>
|
|
||||||
body{margin:0;font-family:-apple-system,sans-serif;background:#0a0a0f;color:#f4f4f5;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem}
|
|
||||||
.card{background:#18181b;border:1px solid rgba(255,255,255,0.06);border-radius:16px;padding:2rem;max-width:480px;text-align:center}
|
|
||||||
h1{color:#86efac;margin:0 0 1rem;font-size:2rem}
|
|
||||||
p{color:#a1a1aa;margin:0 0 .5rem}
|
|
||||||
.emoji{font-size:4rem;margin-bottom:1rem}
|
|
||||||
.next{color:#818cf8;font-weight:600;margin-top:1.5rem}
|
|
||||||
</style></head><body>
|
|
||||||
<div class="card">
|
|
||||||
<div class="emoji">✅</div>
|
|
||||||
<h1>Guardado</h1>
|
|
||||||
<p>El bot se está reiniciando para conectarse a tu WiFi.</p>
|
|
||||||
<p class="next">👉 Vuelve a la lista de WiFi y conectate a <strong>__SSID__</strong></p>
|
|
||||||
<p style="margin-top:1.5rem;font-size:.85rem">El bot aparecerá como conectado en la web cuando termine de arrancar (~15 s).</p>
|
|
||||||
</div></body></html>)=====";
|
|
||||||
ok.replace("__SSID__", s);
|
ok.replace("__SSID__", s);
|
||||||
web.send(200, "text/html; charset=utf-8", ok);
|
web.send(200, "text/html; charset=utf-8", ok);
|
||||||
delay(500);
|
delay(500);
|
||||||
Serial.println("Reiniciando tras save...");
|
|
||||||
ESP.restart();
|
ESP.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handleReset() {
|
static void handleReset() {
|
||||||
resetConfig();
|
resetConfig();
|
||||||
web.sendHeader("Location", "/");
|
web.sendHeader("Location", "/");
|
||||||
@ -601,82 +378,42 @@ static void handleReset() {
|
|||||||
ESP.restart();
|
ESP.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handleNotFound() {
|
static void startPortal() {
|
||||||
// Captive portal: cualquier URL redirige al root
|
|
||||||
handleCaptive();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void setupPortal() {
|
|
||||||
WiFi.mode(WIFI_AP);
|
WiFi.mode(WIFI_AP);
|
||||||
WiFi.softAP(WIFI_AP_NAME, WIFI_AP_PASS);
|
WiFi.softAP(WIFI_AP_NAME, WIFI_AP_PASS);
|
||||||
Serial.printf("AP abierto: %s (pass %s)\n", WIFI_AP_NAME, WIFI_AP_PASS);
|
Serial.printf("AP abierto: %s\n", WIFI_AP_NAME);
|
||||||
Serial.printf("IP del AP: %s\n", WiFi.softAPIP().toString().c_str());
|
|
||||||
|
|
||||||
// DNS server: redirige TODO al portal (captive portal)
|
|
||||||
dnsServer.start(53, "*", WiFi.softAPIP());
|
dnsServer.start(53, "*", WiFi.softAPIP());
|
||||||
|
web.on("/", HTTP_GET, handleRoot);
|
||||||
// Endpoints
|
web.on("/save", HTTP_POST, handleSave);
|
||||||
web.on("/", HTTP_GET, handleRoot);
|
web.on("/reset", HTTP_POST, handleReset);
|
||||||
web.on("/save", HTTP_POST, handleSave);
|
web.onNotFound(handleCaptive);
|
||||||
web.on("/reset", HTTP_POST, handleReset);
|
|
||||||
web.onNotFound(handleNotFound);
|
|
||||||
|
|
||||||
web.begin();
|
web.begin();
|
||||||
Serial.println("Servidor web del portal en http://192.168.4.1/");
|
portalActive = true;
|
||||||
|
}
|
||||||
|
static void stopPortal() {
|
||||||
|
if (!portalActive) return;
|
||||||
|
web.stop();
|
||||||
|
dnsServer.stop();
|
||||||
|
WiFi.softAPdisconnect(true);
|
||||||
|
portalActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- WiFi conectar --------------------
|
// -------------------- WiFi --------------------
|
||||||
static void startWifiConnect() {
|
static void startSTA() {
|
||||||
wifiState = WIFI_CONNECTING;
|
const char *ssid = cfgSsid[0] ? cfgSsid : DEFAULT_WIFI_SSID;
|
||||||
wifiConnectStart = millis();
|
const char *pass = cfgPass[0] ? cfgPass : DEFAULT_WIFI_PASS;
|
||||||
Serial.printf("Conectando a WiFi '%s'...\n", cfgSsid);
|
Serial.printf("Conectando a WiFi '%s'...\n", ssid);
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
WiFi.begin(cfgSsid, cfgPass);
|
WiFi.begin(ssid, pass);
|
||||||
}
|
strncpy(cfgSsid, ssid, sizeof(cfgSsid) - 1);
|
||||||
|
strncpy(cfgPass, pass, sizeof(cfgPass) - 1);
|
||||||
static void updateWifi() {
|
|
||||||
if (wifiState == WIFI_CONNECTING) {
|
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
|
||||||
wifiState = WIFI_CONNECTED;
|
|
||||||
Serial.printf("WiFi OK: %s\n", WiFi.localIP().toString().c_str());
|
|
||||||
Serial.printf("Backend: %s\n", cfgBackend);
|
|
||||||
Serial.printf("Bot: %s\n", cfgName);
|
|
||||||
// Render de carita feliz + intentar primer poll
|
|
||||||
setEmotion(EMO_HAPPY);
|
|
||||||
drawTaskScreen(); // limpia
|
|
||||||
drawIdle();
|
|
||||||
pollTasks();
|
|
||||||
lastPoll = millis();
|
|
||||||
} else if (millis() - wifiConnectStart > 20000) {
|
|
||||||
wifiState = WIFI_FAILED;
|
|
||||||
Serial.println("WiFi falló tras 20 s");
|
|
||||||
}
|
|
||||||
} else if (wifiState == WIFI_FAILED) {
|
|
||||||
// Volvemos al portal
|
|
||||||
wifiState = WIFI_PORTAL;
|
|
||||||
setupPortal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- Demo loop (sin tareas) --------------------
|
|
||||||
static void demoLoop() {
|
|
||||||
static uint32_t lastDemo = 0;
|
|
||||||
static int demo_idx = 0;
|
|
||||||
const Emotion demo[] = { EMO_HAPPY, EMO_SURPRISED, EMO_WORRIED, EMO_SLEEPY,
|
|
||||||
EMO_LOVE, EMO_ANGRY, EMO_CRY, EMO_EXCITED };
|
|
||||||
uint32_t now = millis();
|
|
||||||
if (!hasTask && now - lastDemo > 3000) {
|
|
||||||
lastDemo = now;
|
|
||||||
demo_idx = (demo_idx + 1) % 8;
|
|
||||||
setEmotion(demo[demo_idx]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Setup / Loop --------------------
|
// -------------------- Setup / Loop --------------------
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
delay(200);
|
delay(500);
|
||||||
Serial.println("\n=== DummyBot v3 arrancando ===");
|
Serial.println("\n=== DummyBot v4 ===");
|
||||||
|
|
||||||
pinMode(PIN_BTN_NEXT, INPUT);
|
pinMode(PIN_BTN_NEXT, INPUT);
|
||||||
pinMode(PIN_BTN_DONE, INPUT);
|
pinMode(PIN_BTN_DONE, INPUT);
|
||||||
@ -686,84 +423,105 @@ void setup() {
|
|||||||
pinMode(PIN_TFT_BL, OUTPUT);
|
pinMode(PIN_TFT_BL, OUTPUT);
|
||||||
digitalWrite(PIN_TFT_BL, HIGH);
|
digitalWrite(PIN_TFT_BL, HIGH);
|
||||||
|
|
||||||
|
Serial.println("Init TFT...");
|
||||||
tft.init();
|
tft.init();
|
||||||
tft.setRotation(0);
|
tft.setRotation(0);
|
||||||
tft.fillScreen(TFT_BLACK);
|
Serial.println("TFT OK");
|
||||||
|
|
||||||
if (!SPIFFS.begin(true)) {
|
if (!SPIFFS.begin(true)) Serial.println("SPIFFS ERROR");
|
||||||
Serial.println("ERROR: SPIFFS mount failed");
|
loadConfig();
|
||||||
} else {
|
if (!cfgBackend[0]) strncpy(cfgBackend, BACKEND_URL, sizeof(cfgBackend) - 1);
|
||||||
Serial.printf("SPIFFS OK, %u bytes usados / %u totales\n",
|
if (!cfgToken[0]) strncpy(cfgToken, DEFAULT_DEVICE_TOKEN, sizeof(cfgToken) - 1);
|
||||||
SPIFFS.usedBytes(), SPIFFS.totalBytes());
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawIdle();
|
// Pantalla de bienvenida
|
||||||
|
|
||||||
// Pantalla de bienvenida mientras carga
|
|
||||||
tft.fillScreen(TFT_BLACK);
|
tft.fillScreen(TFT_BLACK);
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
tft.setTextDatum(MC_DATUM);
|
tft.setTextDatum(MC_DATUM);
|
||||||
tft.setTextSize(2);
|
tft.setTextSize(3);
|
||||||
tft.drawString("DummyBot", 120, 90, 4);
|
tft.drawString("DummyBot", 120, 100, 4);
|
||||||
tft.setTextSize(1);
|
tft.setTextSize(1);
|
||||||
tft.drawString("Iniciando...", 120, 130, 2);
|
tft.drawString("Iniciando...", 120, 140, 2);
|
||||||
|
|
||||||
// ¿Hay config guardada?
|
startSTA();
|
||||||
bool haveCfg = loadConfig();
|
Serial.println("Setup OK");
|
||||||
if (haveCfg) {
|
|
||||||
Serial.printf("Config cargada: SSID='%s', Bot='%s'\n", cfgSsid, cfgName);
|
|
||||||
startWifiConnect();
|
|
||||||
// Pantalla de "conectando..."
|
|
||||||
tft.fillScreen(TFT_BLACK);
|
|
||||||
tft.setTextDatum(MC_DATUM);
|
|
||||||
tft.setTextSize(2);
|
|
||||||
tft.drawString("WiFi...", 120, 100, 2);
|
|
||||||
tft.setTextSize(1);
|
|
||||||
tft.drawString(cfgSsid, 120, 130, 2);
|
|
||||||
} else {
|
|
||||||
Serial.println("Sin config, abro portal...");
|
|
||||||
wifiState = WIFI_PORTAL;
|
|
||||||
setupPortal();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RunMode { MODE_CONNECTING, MODE_PORTAL, MODE_RUNNING };
|
||||||
|
RunMode mode = MODE_CONNECTING;
|
||||||
|
uint32_t modeStart = 0;
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
// Servidor web (portal) si estamos en modo AP
|
updateVib();
|
||||||
if (wifiState == WIFI_PORTAL) {
|
|
||||||
|
if (mode == MODE_PORTAL) {
|
||||||
dnsServer.processNextRequest();
|
dnsServer.processNextRequest();
|
||||||
web.handleClient();
|
web.handleClient();
|
||||||
// Dibujar pantalla de "configurando"
|
handleButtons();
|
||||||
static uint32_t lastPortalScreen = 0;
|
static uint32_t lastScreen = 0;
|
||||||
if (millis() - lastPortalScreen > 1500) {
|
if (millis() - lastScreen > 2000) {
|
||||||
lastPortalScreen = millis();
|
lastScreen = millis();
|
||||||
tft.fillScreen(TFT_BLACK);
|
int n = WiFi.softAPgetStationNum();
|
||||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
char msg[40];
|
||||||
tft.setTextDatum(MC_DATUM);
|
snprintf(msg, sizeof(msg), "Config WiFi (%d)", n);
|
||||||
tft.setTextSize(2);
|
drawWifiScreen(msg);
|
||||||
tft.drawString("Config", 120, 80, 2);
|
|
||||||
tft.setTextSize(1);
|
|
||||||
tft.drawString("WiFi: DummyBot-Setup", 120, 115, 2);
|
|
||||||
tft.drawString("Pass: dummybot", 120, 135, 2);
|
|
||||||
tft.drawString("Abre 192.168.4.1", 120, 160, 2);
|
|
||||||
drawFooter("portal cautivo activo");
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateWifi();
|
|
||||||
handleButtons();
|
handleButtons();
|
||||||
updateVib();
|
|
||||||
updateGif();
|
|
||||||
|
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
|
|
||||||
if (wifiState == WIFI_CONNECTED) {
|
if (mode == MODE_CONNECTING) {
|
||||||
if (now - lastPoll >= POLL_MS) {
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
mode = MODE_RUNNING;
|
||||||
|
modeStart = now;
|
||||||
|
Serial.printf("WiFi OK: %s\n", WiFi.localIP().toString().c_str());
|
||||||
|
drawIdleScreen();
|
||||||
lastPoll = now;
|
lastPoll = now;
|
||||||
pollTasks();
|
pollTasks();
|
||||||
|
} else if (now - modeStart > 25000) {
|
||||||
|
Serial.println("WiFi falló, abriendo portal...");
|
||||||
|
startPortal();
|
||||||
|
mode = MODE_PORTAL;
|
||||||
|
} else if (now - modeStart > 1000) {
|
||||||
|
static int dots = 0;
|
||||||
|
dots = (dots + 1) % 4;
|
||||||
|
tft.fillScreen(TFT_BLACK);
|
||||||
|
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
|
tft.setTextDatum(MC_DATUM);
|
||||||
|
tft.setTextSize(2);
|
||||||
|
char msg[20];
|
||||||
|
snprintf(msg, sizeof(msg), "WiFi %.*s", dots, "...");
|
||||||
|
tft.drawString(msg, 120, 100, 2);
|
||||||
|
tft.setTextSize(1);
|
||||||
|
tft.drawString(cfgSsid, 120, 140, 2);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
demoLoop();
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
delay(20);
|
Serial.println("WiFi perdido, reintentando...");
|
||||||
|
mode = MODE_CONNECTING;
|
||||||
|
modeStart = now;
|
||||||
|
startSTA();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - lastPoll >= POLL_MS) {
|
||||||
|
lastPoll = now;
|
||||||
|
pollTasks();
|
||||||
|
}
|
||||||
|
delay(50);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user