viernes, 9 de enero de 2026

RELOJ DIGITAL NPT CON CONFIGURACION DE ALARMA VIA WEB USANDO ESP32 C3 SUPERMINI Y TFT ST7789

Las conexiones entre el ESP32 C3 y la TFT están en el propio sketch. La configuración de la alarma es a traves de un servidor web que corre en el ESP32 C3. La alarma puede disparar un buzzer conectado al ESP32 C3, aunque no está implementado aún, es un paso sencillo que puede hacerse en cualquier momento:

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <SPI.h>
#include "time.h"
#include <WebServer.h>
#include <Preferences.h>

// --- CONFIGURACIÓN DE IP ESTÁTICA ---
IPAddress local_IP(192, 168, 0, 160); // Tu IP fija para el servidor web de ESP32 C3
IPAddress gateway(192, 168, 0, 1);    // IP de tu router (ajusta si es 192.168.1.1)
IPAddress subnet(255, 255, 255, 0);
IPAddress primaryDNS(212, 166, 132, 118); //8, 8, 8, 8 // Necesario para NTP y OpenWeather

// ESP32 C3 SUPERMINI Pins
#define TFT_CS    7
#define TFT_RST   9
#define TFT_DC    10

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
WebServer server(80);
Preferences preferences;

const char* ssid = "SSID";//Tu SSID
const char* password = "password";//Wifi password
const String apiKey = "AIKEY";
const String ciudad_fija = "ciudad";
const char* ntpServer = "hora.roa.es";
const long  gmtOffset_sec = 3600;      
const int   daylightOffset_sec = 0;    

// Variables de datos
float temp = 0;
float veloc = 0;
int hum = 0;
String climaDesc = "...";
String site = "...";
String country = "..";
unsigned long ultimaActualizacionClima = 0;
const long intervaloClima = 600000; // Actualizar cada 10 minutos (600,000 ms)
int secuencia = 1;
unsigned long ultimaActualizacionInfo = 0;
const long intervaloInfo = 10000; // Actualizar info 10 segundos
int alarmaHora = -1;
int alarmaMinuto = -1;
bool alarmaActiva = false;

// --- FUNCIONES WEB ---
void handleRoot() {
  String html = "<html><head><meta charset='UTF-8'><title>Alarma ESP32</title>";
  html += "<style>body{font-family:sans-serif; text-align:center; background:#222; color:white;} .btn{background:#00adb5; border:none; color:white; padding:15px; border-radius:5px;}</style></head><body>";
  html += "<h1>Reloj NTP - Alarma</h1>";
  html += "<form action='/set' method='GET'>Hora: <input type='time' name='t' style='padding:10px;' required><br><br>";
  html += "<input type='submit' class='btn' value='Establecer Alarma'></form>";
  if(alarmaActiva) {
    html += "<h3>Alarma programada: " + String(alarmaHora < 10 ? "0" : "") + String(alarmaHora) + ":" + String(alarmaMinuto < 10 ? "0" : "") + String(alarmaMinuto) + "</h3>";
    html += "<a href='/clear'><button style='background:red; color:white; padding:10px;'>Borrar Alarma</button></a>";
  }
  html += "</body></html>";
  server.send(200, "text/html", html);
}

void handleSetAlarm() {
  String t = server.arg("t");
  alarmaHora = t.substring(0, 2).toInt();
  alarmaMinuto = t.substring(3, 5).toInt();
  alarmaActiva = true;
  preferences.begin("reloj", false);
  preferences.putInt("h", alarmaHora);
  preferences.putInt("m", alarmaMinuto);
  preferences.end();
  server.send(200, "text/html", "<h1>OK! Alarma configurada.</h1><a href='/'>Volver</a>");
}

void handleClearAlarm() {
  alarmaActiva = false;
  alarmaHora = -1;
  preferences.begin("reloj", false);
  preferences.putInt("h", -1);
  preferences.end();
  server.send(200, "text/html", "<h1>Alarma desactivada.</h1><a href='/'>Volver</a>");
}

void setup() {
  Serial.begin(115200);

  tft.init(240, 320);
  tft.setRotation(3);
  tft.fillScreen(ST77XX_BLACK);
  tft.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
  tft.setTextSize(3);
  tft.setTextWrap(true);

  tft.setCursor(10, 50);
  tft.print("Inicializando la pantalla.");
  delay(2000);
  tft.fillScreen(ST77XX_BLACK);

  Serial.println(F("TFT Initialized"));

  // 1. Configurar IP Estática ANTES de WiFi.begin
  if (!WiFi.config(local_IP, gateway, subnet, primaryDNS)) {
    Serial.println("Error al configurar IP Estática");
  }

   // 2. Conexión WiFi
  tft.setCursor(10, 50);
  tft.print("Conectando WIFI..");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    tft.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
    tft.setCursor(10, 50);
    tft.print("Conectando WIFI..");
    delay(500);
    tft.setTextColor(ST77XX_YELLOW, ST77XX_BLACK);
    tft.setCursor(10, 50);
    tft.print("Conectando WIFI..");
    delay(500);

    Serial.println(F("Conectando WIFI.."));
     }

  Serial.println("Conectado! IP: " + WiFi.localIP().toString());

  tft.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
  tft.setCursor(10, 70);
  tft.print("Conectado.");
  delay(500);
 
  tft.fillScreen(ST77XX_BLACK);

  // 3. Cargar alarma de memoria
  preferences.begin("reloj", true);
  alarmaHora = preferences.getInt("h", -1);
  alarmaMinuto = preferences.getInt("m", -1);
  alarmaActiva = (alarmaHora != -1);
  preferences.end();

  server.on("/", handleRoot);
  server.on("/set", handleSetAlarm);
  server.on("/clear", handleClearAlarm);
  server.begin();
 
  // 4.. Configurar NTP
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  obtenerClima();
     
}

void loop() {
 
  server.handleClient();

  // 1. Gestionar Tiempo (NTP)
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo)){
    tft.setCursor(10, 10);
    tft.print("Error Hora");
    delay(3000);
    tft.fillScreen(ST77XX_BLACK);
    return;
  }

  // 2. Actualizar clima cada 10 minutos sin bloquear el reloj
  if (millis() - ultimaActualizacionClima > intervaloClima) {
    obtenerClima();
    ultimaActualizacionClima = millis();
  }

  // 3. Comprobar Alarma

  if(alarmaActiva && timeinfo.tm_hour == alarmaHora && timeinfo.tm_min == alarmaMinuto) {
    if(timeinfo.tm_sec % 2 == 0){
     //tft.fillScreen(ST77XX_RED);
    tft.setCursor(20, 20);
    tft.setTextSize(6);
    tft.setTextColor(ST77XX_GREEN, ST77XX_BLACK);
    char timeBuff[10];
    sprintf(timeBuff, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
    tft.print(timeBuff);
    delay(500);
    }
    else{
     //tft.fillScreen(ST77XX_BLACK);
     tft.setCursor(20, 20);
    tft.setTextSize(6);
    tft.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
    char timeBuff[10];
    sprintf(timeBuff, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
    tft.print(timeBuff);
    delay(500);
    }
  }

  // 4. Dibujar Hora
  tft.setCursor(20, 20);
  tft.setTextSize(6);
  tft.setTextColor(ST77XX_RED, ST77XX_BLACK);
  char timeBuff[10];
  sprintf(timeBuff, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
  tft.print(timeBuff);
 
  // 5. Dibujar Fecha debajo
   tft.setTextColor(ST77XX_YELLOW, ST77XX_BLACK);
  tft.setCursor(10, 75);
  tft.setTextSize(2);
  char fecha[12];
  sprintf(fecha, "%02d/%02d/%04d", timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year + 1900);
  tft.print(fecha);

  // 6. Dibujar Info Alarma
  tft.setTextSize(2);
  tft.setCursor(160, 75);
  if(alarmaActiva) {
    tft.setTextColor(ST77XX_GREEN, ST77XX_BLACK);
    char alMsg[20];
    sprintf(alMsg, "ALR ON: %02d:%02d", alarmaHora, alarmaMinuto);
    tft.print(alMsg);
  } else {
    tft.setTextColor(ST77XX_YELLOW, ST77XX_BLACK);
    tft.print("ALR: OFF       ");
  }

  // 7. Actualizar linea de info meteo cada 60 segundows
  if (millis() - ultimaActualizacionInfo > intervaloInfo) {
  tft.setTextSize(3);
  tft.setCursor(1, 165);
  tft.setTextColor(ST77XX_CYAN, ST77XX_BLACK);
  tft.setTextWrap(false);
  tft.print("                     "); // Espacios para limpiar texto anterior
 
  if (secuencia >= 3) {secuencia = 1;}

  if (secuencia == 1) {    
  // DESCRIPCIÓN CLIMA (Usamos setTextWrap(false) para evitar saltos raros)
  tft.setCursor(1, 165);
  tft.setTextColor(ST77XX_CYAN, ST77XX_BLACK);
  tft.setTextWrap(false);
  tft.print(climaDesc + "    "); // Espacios para limpiar texto anterior
  }
  if (secuencia == 2) {    
  // DESCRIPCIÓN CLIMA (Usamos setTextWrap(false) para evitar saltos raros)
  tft.setCursor(1, 165);
  tft.setTextColor(ST77XX_CYAN, ST77XX_BLACK);
  tft.setTextWrap(false);
  tft.print("Viento: "); tft.print(veloc); tft.print(" Km/h");
  }
 
  ultimaActualizacionInfo = millis();
  secuencia = secuencia + 1;
  }

  // TEMPERATURA Y HUMEDAD
  tft.setTextSize(3);
  tft.setTextColor(ST77XX_WHITE, ST77XX_BLACK);
  tft.setCursor(10, 110);
  tft.print("Temperat: "); tft.print(temp, 1); tft.print(" C");
 
  tft.setCursor(10, 135);
  tft.print("Humedad:  "); tft.print(hum); tft.print(" %");

  // 8. CIUDAD Y FECHA (Base)
  tft.setTextSize(3);
  tft.setTextColor(ST77XX_YELLOW, ST77XX_BLACK);
  tft.setCursor(10, 200);
  //tft.print("Site: " + ciudad_fija);
  tft.print("Site:"); tft.print(site);tft.print(",");tft.print(country);

  delay(500);
}

void obtenerClima() {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    String url = "https://api.openweathermap.org/data/2.5/weather?q=" + ciudad_fija + "&appid=" + apiKey + "&units=metric&lang=es";
    http.begin(url);
    int httpCode = http.GET();
    if (httpCode == 200) {
      String payload = http.getString();
      StaticJsonDocument<1024> doc;
      deserializeJson(doc, payload);
     
      temp = doc["main"]["temp"];
      hum = doc["main"]["humidity"];
      veloc = doc["wind"]["speed"];
      climaDesc = doc["weather"][0]["description"].as<String>();
      site = doc["name"].as<String>();
      country = doc["sys"]["country"].as<String>();
      climaDesc.toUpperCase();
    }
    http.end();
  }
}

No hay comentarios:

Publicar un comentario