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();
  }
}

sábado, 3 de enero de 2026

RELOJ DIGITAL NPT Y CLIMA CON ESP32 C3 SUPERMINI Y TFT ST7789

 Proyecto de reloj IOT que muestra hora y fecha, temperatura, humedad ambiente y estado del tiempo exterior en la ciudad elegida. Se ha usado un ESP32 C3 Supermini y un display GMT020-02-7P 240X320 con chipset ST7789. El código siguiente está realizado con Arduino IDE ver. 2.3.6

Conexion de pines:

TFT.........................ESP32 C3

CS .............................7

DC ............................20

RST ..........................21

SDA .........................9

SCL .........................4

VCC........................3V.

GND .......................GND


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

// ESP32 C3 SUPERMINI y TFT 2.0 TFTSPI GMT020-02-7P
#define TFT_CS    7
#define TFT_RST   21
#define TFT_DC    20
//#define TFT_MOSI  6  // SDA, HW MOSI
//#define TFT_SCLK  4  // SCL, HW SCLK
//#define TFT_MISO  5  // not used

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);

const char* ssid = "SSID WIFI"; //"LowiCD10"
const char* password = "PASSWORD WIFI"; //"2P3WHFC259TK3B"
const String apiKey = "APIKEY OPENWEATHERMAP"; // Tu clave de OpenWeatherMap
//const String ciudad_fija = "Granada,ES"; // Puedes fijarla o usar la detectada
const String ciudad_fija = "TU CIUDAD";
const char* ntpServer = "hora.roa.es"; //NPT HORA ESPAÑA
const long  gmtOffset_sec = 3600;      // Ajusta según tu país
const int   daylightOffset_sec = 0;    // 3600 si hay horario de verano

// 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

void obtenerClima() {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    // URL para obtener clima por ciudad
    String url = "https://api.openweathermap.org/data/2.5/weather?q=" + ciudad_fija + "&appid=" + apiKey + "&units=metric&lang=es";
    //String url = "https://api.openweathermap.org/data/2.5/weather?q=granada&appid=20e3cd6f640b1ae47e37932983b07a09&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();
  }
}

void setup() {
  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"));

  // 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);
     }
 
  tft.fillScreen(ST77XX_BLACK);

  // Configurar NTP
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  obtenerClima();
 
}

void loop() {
  // 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();
  }

  // --- DIBUJAR EN PANTALLA ---
  tft.setTextColor(ST77XX_RED, ST77XX_BLACK);

  // HORA
  tft.setCursor(20, 20);
  tft.setTextSize(6);
 
  // Formato HH:MM:SS
  char buffer[10];
  sprintf(buffer, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
  tft.print(buffer);

  // Dibujar Fecha debajo
   tft.setTextColor(ST77XX_YELLOW, ST77XX_BLACK);
  tft.setCursor(55, 70);
  tft.setTextSize(3);
  char fecha[12];
  sprintf(fecha, "%02d/%02d/%04d", timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year + 1900);
  tft.print(fecha);

  //delay(1000); // Actualiza cada segundo

  // 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(" %");

  // 3. Actualizar linea de info meteo cada 60 segundows
  if (millis() - ultimaActualizacionInfo > intervaloInfo) {
 
  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;
  }

  // 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(1000); // Actualiza cada segundo
}