Estación meteorológica y bot de Mastodon

Descripción General del Sistema

El programa lee los valores de temperatura, humedad y presión atmosférica mediante un sensor BME280 conectado por I2C. Sincroniza la hora exacta mediante un servidor NTP vía Wi-Fi, da formato al reporte y realiza una publicación (Toot) en la red social Mastodon a través de su API REST HTTPS. Al finalizar el ciclo, el ESP8266 entra en modo Deep Sleep durante 15 minutos para optimizar el consumo energético.

Esquema de la estación meteorológica
Fotografía de los componentes reales utilizados para el proyecto
Placa solar utilizada para el proyecto por razones de disponibilidad. Se puede cargar la batería con una placa mucho menor, siempre que ofrezca 5V

Código

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <time.h>
 
// --- CONFIGURACIÓN ---
const char* WIFI_SSID     = "[El nombre de tu red WiFi]";
const char* WIFI_PASSWORD = "[La clave de tu red Wifi]";
const char* MASTODON_HOST = "[El nombre de la instancia, sin https://, por ejemplo mastodon.social]"; 
const char* ACCESS_TOKEN  = "[El token de acceso al usuario en el que se publicará]"; 
 
// --- CONFIGURACIÓN PINES ---
const int PIN_SDA = D2;
const int PIN_SCL = D1;
 
// --- CONFIGURACIÓN HORARIA (NTP) ---
const char* MY_TZ = "CET-1CEST,M3.5.0,M10.5.0/3"; 
const char* NTP_SERVER = "pool.ntp.org";
 
// --- TIEMPO DE HIBERNACIÓN (DEEP SLEEP) ---
const uint64_t TIEMPO_HIBERNACION_US = 15ULL * 60ULL * 1000000ULL; // 15 minutos
 
Adafruit_BME280 bme;
 
// --- FUNCIONES AUXILIARES ---
 
bool conectarWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Conectando a WiFi");
 
  int intentos = 0;
  while (WiFi.status() != WL_CONNECTED && intentos < 30) {
    delay(500);
    Serial.print(".");
    intentos++;
  }
 
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\n✅ WiFi conectado.");
    return true;
  }
 
  Serial.println("\n❌ Error: No se pudo conectar a WiFi.");
  return false;
}
 
void sincronizarNTP() {
  configTime(MY_TZ, NTP_SERVER);
  Serial.print("Sincronizando hora NTP");
 
  time_t now = time(nullptr);
  int intentos = 0;
  while (now < 8 * 3600 * 2 && intentos < 30) {
    delay(500);
    Serial.print(".");
    now = time(nullptr);
    intentos++;
  }
  Serial.println("\n✅ Hora sincronizada.");
}
 
bool leerBME280(float &temp, float &hum, float &pres) {
  delay(100);
  bme.readTemperature(); // Descarte de lectura inicial
  delay(100);
 
  temp = bme.readTemperature();
  hum  = bme.readHumidity();
  pres = bme.readPressure() / 100.0F;
 
  if (isnan(temp) || isnan(hum) || isnan(pres)) {
    return false;
  }
 
  Serial.printf("Lectura Obtenida -> Temp: %.1f °C | Hum: %.1f %% | Pres: %.1f hPa\n", temp, hum, pres);
  return true;
}
 
String obtenerFechaHora() {
  time_t now = time(nullptr);
  struct tm timeinfo;
  localtime_r(&now, &timeinfo);
 
  char buffer[40];
  strftime(buffer, sizeof(buffer), "%d/%m/%Y - %H:%M:%S", &timeinfo);
  return String(buffer);
}
 
String urlEncode(String str) {
  String encodedString = "";
  char c, code0, code1;
  for (size_t i = 0; i < str.length(); i++) {
    c = str.charAt(i);
    if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
      encodedString += c;
    } else if (c == ' ') {
      encodedString += "+";
    } else {
      code0 = (c >> 4) & 0xF;
      code1 = c & 0xF;
      encodedString += '%';
      encodedString += (char)(code0 < 10 ? code0 + '0' : code0 - 10 + 'A');
      encodedString += (char)(code1 < 10 ? code1 + '0' : code1 - 10 + 'A');
    }
  }
  return encodedString;
}
 
void publicarToot(String mensaje) {
  WiFiClientSecure client;
  client.setInsecure(); 
 
  HTTPClient http;
  String url = "https://" + String(MASTODON_HOST) + "/api/v1/statuses";
 
  if (http.begin(client, url)) {
    http.addHeader("User-Agent", "ESP8266_WeatherBot/1.0");
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    http.addHeader("Authorization", "Bearer " + String(ACCESS_TOKEN));
 
    String postData = "status=" + urlEncode(mensaje) + "&visibility=public";
    int httpCode = http.POST(postData);
 
    if (httpCode > 0) {
      Serial.printf("Respuesta HTTP: %d\n", httpCode);
      Serial.println("Servidor: " + http.getString());
    } else {
      Serial.printf("Error al enviar POST: %s\n", http.errorToString(httpCode).c_str());
    }
    http.end();
  }
}
 
// --- FLUJO PRINCIPAL DE ARDUINO ---
 
void setup() {
  Serial.begin(9600);
  delay(100);
 
  if (conectarWiFi()) {
    sincronizarNTP();
 
    Wire.begin(PIN_SDA, PIN_SCL);
    delay(200);
 
    float temp, hum, pres;
    if (bme.begin(0x76, &Wire) && leerBME280(temp, hum, pres)) {
      String mensaje = "📊 *Estación Meteorológica*\n"
                       "📅 " + obtenerFechaHora() + "\n\n"
                       "🌡️ Temperatura: " + String(temp, 1) + " °C\n"
                       "💧 Humedad: " + String(hum, 1) + " %\n"
                       "⏲️ Presión: " + String(pres, 1) + " hPa\n\n"
                       "#ESP8266 #WeatherBot #BME280";
 
      Serial.println("Enviando publicación a Mastodon...");
      publicarToot(mensaje);
    } else {
      Serial.println("❌ Error: No se pudo comunicar con el BME280.");
    }
  }
 
  Serial.println("Entrando en Deep Sleep...");
  ESP.deepSleep(TIEMPO_HIBERNACION_US);
}
 
void loop() {
  // Vacío por el uso de Deep Sleep
}

Librerías utilizadas

  • <ESP8266WiFi.h>,Gestión de la pila Wi-Fi en modo estación (WIFI_STA).
  • <ESP8266HTTPClient.h>,Cliente para construir y enviar solicitudes HTTP/HTTPS (POST).
  • <WiFiClientSecure.h>,Manejo de conexiones TLS/SSL seguras para HTTPS.
  • <Wire.h>,Comunicación por bus I2C con el sensor BME280.
  • <Adafruit_Sensor.h>,Clase base abstracta unificada para sensores de Adafruit.
  • <Adafruit_BME280.h>,“Controlador de bajo nivel para el sensor de presión, humedad y temperatura BME280.”
  • <time.h>,Funciones estándar de C/C++ para sincronización NTP y formateo de fechas/horas.

Parámetros de Configuración Global

  • WIFI_SSID / WIFI_PASSWORD: Nombre y credenciales de la red inalámbrica.
  • MASTODON_HOST: Dominio de la instancia de Mastodon donde está alojada la cuenta del bot (ej. social.hispa.net).
  • ACCESS_TOKEN: Token de autenticación Bearer de OAuth2 para publicar en la API.
  • PIN_SDA (D2) / PIN_SCL (D1): Pines GPIO asignados al bus I2C.
  • MY_TZ: Cadena de zona horaria POSIX (CET-1CEST,M3.5.0,M10.5.0/3 gestiona automáticamente el horario de verano/invierno en Europa Central).
  • NTP_SERVER: Servidor de tiempo (pool.ntp.org).
  • TIEMPO_HIBERNACION_US: Duración del reposo profundo calculada en microsegundos (15x60x1000000=9000000 microsegundos)

Módulos y Funciones Auxiliares

  1. conectarWiFi()
    • Función: Configura el ESP8266 en modo cliente (WIFI_STA) e inicia la asociación a la red Wi-Fi.
    • Elemento de lista desordenadaMecanismo de tiempo de espera (Timeout): Realiza hasta 30 intentos con retardos de 500 ms (máximo 15 segundos). Retorna true si la conexión es exitosa (WL_CONNECTED) o false en caso de fallo.
  2. sincronizarNTP()
    • Función: Configura el cliente SNTP interno de la pila de código mediante configTime().
    • Mecanismo de validación: Consulta la hora del sistema en bucle hasta que devuelva una marca de tiempo (epoch) válida ($> 57\,600\text{ s}$), garantizando que la sincronización con el servidor ha finalizado.
  3. leerBME280(float &temp, float &hum, float &pres)
    • Función: Realiza la adquisición de datos de los tres sensores físicos.
    • Uso de punteros/referencias: Pasa los parámetros por referencia (&) para actualizar directamente las variables de destino.
    • Filtrado y Conversión:Realiza un descarte térmico inicial.Convierte la presión de Pascales (Pa) a hectopascales (hPa) dividiendo entre $100.0$.
    • Comprueba errores de hardware o lectura mediante la función isnan().
  4. obtenerFechaHora()
    • Función: Obtiene la marca de tiempo UTC actual, le aplica los desplazamientos de la zona horaria (MY_TZ) mediante localtime_r() y genera un String con formato DD/MM/YYYY - HH:MM:SS usando strftime().
  5. urlEncode(String str)
    • Función: Codifica caracteres especiales para cumplir con el estándar application/x-www-form-urlencoded. Convierte caracteres no alfanuméricos en secuencias %HEX.
  6. publicarToot(String mensaje)
    • Función: Construye y ejecuta la petición HTTPS POST hacia el endpoint /api/v1/statuses de Mastodon.
    • Seguridad SSL: Utiliza client.setInsecure() para omitir la validación rigurosa de la cadena del certificado X.509 de la CA (ahorra memoria RAM en la placa).
    • Cabeceras HTTP:
      • User-Agent: Identificador de la aplicación cliente.
      • Content-Type: application/x-www-form-urlencoded.
      • Authorization: Inyección del token Bearer (Bearer <ACCESS_TOKEN>).

Flujo de Ejecución Principal

Debido al uso de Deep Sleep, el flujo completo del programa se ejecuta exclusivamente dentro de la función setup(). La función loop() permanece vacía puesto que el procesador se reinicia completamente tras cada despertar.