Vivimos en un mundo donde el monitoreo ambiental se vuelve cada vez más relevante: desde la calidad del aire que respiramos hasta los niveles de ruido y radiación UV a los que estamos expuestos. Por eso nace este proyecto: una plataforma colaborativa e inteligente para medir y visualizar variables ambientales en tiempo real.
El objetivo fue construir un sistema modular, escalable y de bajo costo, capaz de recopilar datos físicos usando sensores reales, transmitirlos a la nube, analizarlos mediante un modelo de Machine Learning y mostrarlos en un dashboard accesible desde cualquier navegador.
🚀 ¿Cómo funciona?Arduino actúa como nodo secundario, sensando CO₂, gases tóxicos y presión atmosférica.
- Arduino actúa como nodo secundario, sensando CO₂, gases tóxicos y presión atmosférica.
ESP32 es el nodo principal. Lee sus propios sensores (temperatura, humedad, luz, UV, oxígeno, sonido) y también recibe datos del Arduino por UART.
- ESP32 es el nodo principal. Lee sus propios sensores (temperatura, humedad, luz, UV, oxígeno, sonido) y también recibe datos del Arduino por UART.
Todos los datos se envían vía MQTT a un broker público.
- Todos los datos se envían vía MQTT a un broker público.
Un servidor FastAPI recibe los datos, los almacena en Firebase Firestore y ofrece endpoints REST.
- Un servidor FastAPI recibe los datos, los almacena en Firebase Firestore y ofrece endpoints REST.
Un modelo de Machine Learning (Random Forest) predice la calidad ambiental en 3 categorías: Buena, Regular, Mala.
- Un modelo de Machine Learning (Random Forest) predice la calidad ambiental en 3 categorías: Buena, Regular, Mala.
Un frontend en React consume estos datos en tiempo real y los presenta en un dashboard con mapa y tarjetas.
- Un frontend en React consume estos datos en tiempo real y los presenta en un dashboard con mapa y tarjetas.
Sensores: SCD41, MICS-6814, DHT22, Grove O2, BMP280, TEMT6000, UV Sensor, KY-037
- Sensores: SCD41, MICS-6814, DHT22, Grove O2, BMP280, TEMT6000, UV Sensor, KY-037
Microcontroladores: Arduino Uno + ESP32 WROOM-32
- Microcontroladores: Arduino Uno + ESP32 WROOM-32
Comunicación: UART (entre Arduino y ESP32) + MQTT (nube)
- Comunicación: UART (entre Arduino y ESP32) + MQTT (nube)
Backend: Python + FastAPI + Firebase Firestore
- Backend: Python + FastAPI + Firebase Firestore
Machine Learning: Scikit-learn (Random Forest)
- Machine Learning: Scikit-learn (Random Forest)
Frontend: React + Leaflet + MQTT.js
- Frontend: React + Leaflet + MQTT.js
Agrega aquí:
Fotos del hardware conectado y montado.
import { useEffect, useState } from "react";
import mqtt from "mqtt";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import "leaflet/dist/leaflet.css";
// 🔹 Configuración de topics MQTT
const MQTT_BROKER = "wss://test.mosquitto.org:8081/mqtt";
const TOPICS = {
temp: "esp32/dht22/temperatura",
hum: "esp32/dht22/humedad",
o2Raw: "esp32/o2/raw",
o2Percent: "esp32/o2/percent",
luxRaw: "esp32/temt6000/lux_raw",
luxEst: "esp32/temt6000/lux_est",
uvRaw: "esp32/uv/raw",
uvPower: "esp32/uv/uWcm2",
soundRaw: "esp32/sound/raw",
soundDB: "esp32/sound/db_est",
scdCo2: "esp32/scd41/co2",
scdTemp: "esp32/scd41/temp",
scdHum: "esp32/scd41/hum",
co: "esp32/gas/co",
no2: "esp32/gas/no2",
etanol: "esp32/gas/etanol",
nh3: "esp32/gas/nh3",
ch4: "esp32/gas/ch4",
bmpTemp: "esp32/bmp280/temp",
bmpPresion: "esp32/bmp280/presion",
ubicacion: "esp32/ubicacion",
prediction: "esp32/model/prediction"
};
function App() {
const [data, setData] = useState({
temp: "---",
hum: "---",
o2Raw: "---",
o2Percent: "---",
luxRaw: "---",
luxEst: "---",
uvRaw: "---",
uvPower: "---",
soundRaw: "---",
soundDB: "---",
scdCo2: "---",
scdTemp: "---",
scdHum: "---",
co: "---",
no2: "---",
etanol: "---",
nh3: "---",
ch4: "---",
bmpTemp: "---",
bmpPresion: "---",
ubicacion: null,
prediction: "---"
});
useEffect(() => {
const client = mqtt.connect(MQTT_BROKER);
client.on("connect", () => {
Object.values(TOPICS).forEach((topic) => client.subscribe(topic));
});
client.on("message", (topic, message) => {
const value = message.toString();
setData((prev) => {
const updated = { ...prev };
for (const key in TOPICS) {
if (TOPICS[key] === topic) {
if (key === "ubicacion") {
try {
const [lat, lon] = value.split(",").map(parseFloat);
updated[key] = { lat, lon };
} catch (err) {
console.error("❌ Error procesando ubicación:", value);
}
} else {
updated[key] = value;
}
}
}
return updated;
});
});
client.on("error", (err) => console.error("MQTT Error:", err));
return () => client.end();
}, []);
const SensorCard = ({ title, value, subtitle, color = "#333" }) => (
<div style={{
background: "#f8f8f8",
padding: "1rem",
borderRadius: "10px",
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
minWidth: "220px",
flex: "1 1 250px",
margin: "10px"
}}>
<h3 style={{ margin: 0, color: "#1e1e1e" }}>{title}</h3>
<p style={{ fontSize: "24px", color, margin: "10px 0" }}><b>{value}</b></p>
{subtitle && <p style={{ fontSize: "14px", color: "#666" }}>{subtitle}</p>}
</div>
);
return (
<div style={{ fontFamily: "Arial", padding: "20px", maxWidth: "1200px", margin: "auto", backgroundColor: "#1e1e1e", color: "#eee" }}>
<h1 style={{ textAlign: "center" }}>🌡️ Monitor IoT - Sensores Ambientales</h1>
<p style={{ textAlign: "center" }}>📡 Datos en tiempo real desde MQTT</p>
{data.ubicacion && (
<div style={{ height: "400px", margin: "20px 0", borderRadius: "10px", overflow: "hidden" }}>
<MapContainer center={[data.ubicacion.lat, data.ubicacion.lon]} zoom={13} style={{ height: "100%", width: "100%" }}>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={[data.ubicacion.lat, data.ubicacion.lon]}>
<Popup>📍 Ubicación estimada de la ESP32</Popup>
</Marker>
</MapContainer>
</div>
)}
<h2>🌐 Sensores Locales (ESP32)</h2>
<div style={{ display: "flex", flexWrap: "wrap", justifyContent: "center" }}>
<SensorCard title="🌡️ Temperatura (DHT22)" value={`${data.temp} °C`} subtitle="Sensor DHT22" ></SensorCard>
<SensorCard title="💧 Humedad (DHT22)" value={`${data.hum} %`} subtitle="Sensor DHT22" ></SensorCard>
<SensorCard title="🧪 O₂ crudo (ADC)" value={data.o2Raw} subtitle="Sensor Grove O₂" ></SensorCard>
<SensorCard title="🔵 Oxígeno estimado" value={`${data.o2Percent} %`} color="#27ae60" subtitle="Sensor Grove O₂ (0–25%)" ></SensorCard>
<SensorCard title="🔆 Luz cruda (ADC)" value={data.luxRaw} subtitle="Sensor TEMT6000" ></SensorCard>
<SensorCard title="🌞 Lux estimado" value={`${data.luxEst} lux`} color="#2980b9" subtitle="Sensor TEMT6000" ></SensorCard>
<SensorCard title="🌤️ UV crudo (ADC)" value={data.uvRaw} subtitle="Sensor UV Waveshare" ></SensorCard>
<SensorCard title="☀️ UV estimado" value={`${data.uvPower} µW/cm²`} color="#8e44ad" subtitle="Waveshare UV (200–370 nm)" ></SensorCard>
<SensorCard title="🎤 Sonido crudo (ADC)" value={data.soundRaw} subtitle="Sensor KY-037" ></SensorCard>
<SensorCard title="🔊 Nivel de sonido" value={`${data.soundDB} dB`} color="#2c3e50" subtitle="Sensor KY-037 (no calibrado)" ></SensorCard>
<SensorCard title="🧠 Predicción del modelo" value={data.prediction} color="#d35400" subtitle="Modelo de clasificación en ESP32" ></SensorCard>
</div>
<h2>🔗 Sensores Externos (Arduino)</h2>
<div style={{ display: "flex", flexWrap: "wrap", justifyContent: "center" }}>
<SensorCard title="🌿 CO₂ (SCD41)" value={`${data.scdCo2} ppm`} color="#16a085" subtitle="Sensor SCD41" ></SensorCard>
<SensorCard title="🌡️ Temp SCD41" value={`${data.scdTemp} °C`} subtitle="Sensor SCD41" ></SensorCard>
<SensorCard title="💧 Hum SCD41" value={`${data.scdHum} %`} subtitle="Sensor SCD41" ></SensorCard>
<SensorCard title="🧪 CO" value={`${data.co} ppm`} subtitle="Multichannel Gas Sensor" ></SensorCard>
<SensorCard title="🧪 NO₂" value={`${data.no2} ppm`} subtitle="Multichannel Gas Sensor" ></SensorCard>
<SensorCard title="🧪 Etanol" value={`${data.etanol} ppm`} subtitle="Multichannel Gas Sensor" ></SensorCard>
<SensorCard title="🧪 NH₃" value={`${data.nh3} ppm`} subtitle="Multichannel Gas Sensor" ></SensorCard>
<SensorCard title="🧪 CH₄" value={`${data.ch4} ppm`} subtitle="Multichannel Gas Sensor" ></SensorCard>
<SensorCard title="🌡️ Temp BMP280" value={`${data.bmpTemp} °C`} subtitle="Sensor BMP280" ></SensorCard>
<SensorCard title="🔵 Presión BMP280" value={`${data.bmpPresion} hPa`} subtitle="Sensor BMP280" ></SensorCard>
</div>
</div>
);
}
export default App;
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <PubSubClient.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
// 🔹 Pines
#define DHTPIN 4
#define DHTTYPE DHT22
#define O2_SENSOR_PIN 34
#define TEMT6000_PIN 35
#define UV_PIN 36
#define SOUND_SENSOR_PIN 32
// 🔹 WiFi
const char* ssid = "JUAN CAMILO";
const char* password = "1085339978";
// 🔹 MQTT
const char* mqtt_server = "test.mosquitto.org";
const int mqtt_port = 1883;
DHT dht(DHTPIN, DHTTYPE);
WiFiClient espClient;
PubSubClient client(espClient);
HardwareSerial SerialNode(2); // UART2 para Arduino
bool ubicacionObtenida = false;
String ubicacionStr = ""; // Se guarda una sola vez
// 🔹 Lectura promediada para sonido
int readAverageSound(int pin, int muestras = 10) {
long suma = 0;
for (int i = 0; i < muestras; i++) {
suma += analogRead(pin);
delay(5);
}
return suma / muestras;
}
void setup() {
Serial.begin(115200); // 🖥️ Monitor serial ESP32
SerialNode.begin(9600, SERIAL_8N1, 16, 17); // 🧷 UART desde Arduino (RX=16)
dht.begin();
pinMode(O2_SENSOR_PIN, INPUT);
pinMode(TEMT6000_PIN, INPUT);
pinMode(UV_PIN, INPUT);
pinMode(SOUND_SENSOR_PIN, INPUT);
Serial.print("📶 Conectando a WiFi...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("\n✅ WiFi conectado con IP: " + WiFi.localIP().toString());
client.setServer(mqtt_server, mqtt_port);
reconnect();
}
void reconnect() {
while (!client.connected()) {
Serial.print("🔄 Conectando a MQTT...");
if (client.connect("ESP32_MainBoard")) {
Serial.println("✅ Conectado a MQTT!");
} else {
Serial.print("❌ Error MQTT: ");
Serial.println(client.state());
delay(3000);
}
}
}
void obtenerUbicacionPorIP() {
if (WiFi.status() == WL_CONNECTED && !ubicacionObtenida) {
HTTPClient http;
http.begin("http://ip-api.com/json");
int httpCode = http.GET();
if (httpCode == 200) {
String payload = http.getString();
DynamicJsonDocument doc(1024);
deserializeJson(doc, payload);
float lat = doc["lat"];
float lon = doc["lon"];
ubicacionStr = String(lat, 6) + "," + String(lon, 6);
Serial.println("📍 Ubicación guardada: " + ubicacionStr);
ubicacionObtenida = true;
} else {
Serial.println("⚠️ Error al obtener ubicación IP");
}
http.end();
}
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
if (!ubicacionObtenida) {
obtenerUbicacionPorIP();
}
if (ubicacionObtenida) {
client.publish("esp32/ubicacion", ubicacionStr.c_str());
Serial.println("📤 MQTT → esp32/ubicacion: " + ubicacionStr);
}
// 🟢 DHT22
float humedad = dht.readHumidity();
float temperatura = dht.readTemperature();
if (!isnan(humedad) && !isnan(temperatura)) {
client.publish("esp32/dht22/temperatura", String(temperatura).c_str());
client.publish("esp32/dht22/humedad", String(humedad).c_str());
Serial.println("🌡️ Temp: " + String(temperatura) + " °C");
Serial.println("💧 Hum: " + String(humedad) + " %");
} else {
Serial.println("⚠️ Error al leer DHT22");
}
// 🧪 Sensor de Oxígeno Grove
int o2Raw = analogRead(O2_SENSOR_PIN);
float o2Voltage = (o2Raw / 4095.0) * 3.3;
float o2Percent = (o2Voltage / 2.0) * 25.0;
client.publish("esp32/o2/raw", String(o2Raw).c_str());
client.publish("esp32/o2/percent", String(o2Percent, 2).c_str());
Serial.println("🧪 O₂ crudo: " + String(o2Raw));
Serial.println("🔵 O₂ estimado: " + String(o2Percent, 2) + " %");
// 🔆 TEMT6000
int luzCruda = analogRead(TEMT6000_PIN);
float voltajeLuz = (luzCruda / 4095.0) * 3.3;
float lux = voltajeLuz * 2000;
client.publish("esp32/temt6000/lux_raw", String(luzCruda).c_str());
client.publish("esp32/temt6000/lux_est", String((int)lux).c_str());
Serial.println("🔆 Luz cruda: " + String(luzCruda));
Serial.println("🌞 Lux estimado: " + String((int)lux));
// ☀️ UV Sensor
int uvRaw = analogRead(UV_PIN);
float voltajeUV = (uvRaw / 4095.0) * 3.3;
float uvPower = voltajeUV * 1000;
client.publish("esp32/uv/raw", String(uvRaw).c_str());
client.publish("esp32/uv/uWcm2", String((int)uvPower).c_str());
Serial.println("🌤️ UV crudo: " + String(uvRaw));
Serial.println("☀️ UV potencia: " + String((int)uvPower) + " µW/cm²");
// 🔊 Sonido
int sonidoRaw = readAverageSound(SOUND_SENSOR_PIN);
float sonidoVolt = (sonidoRaw / 4095.0) * 3.3;
float sonidoDB = sonidoVolt * 100;
client.publish("esp32/sound/raw", String(sonidoRaw).c_str());
client.publish("esp32/sound/db_est", String((int)sonidoDB).c_str());
Serial.println("🎤 Sonido crudo: " + String(sonidoRaw));
Serial.println("🔊 Estimación dB: " + String((int)sonidoDB));
// 🔄 Lectura UART desde Arduino
while (SerialNode.available()) {
String dataLine = SerialNode.readStringUntil('\n');
dataLine.trim();
if (dataLine.startsWith("DATA")) {
Serial.println("📩 Recibido de Arduino: " + dataLine);
dataLine.remove(0, 5); // eliminar "DATA,"
int i = 0;
const char* topics[] = {
"esp32/scd41/co2", "esp32/scd41/temp", "esp32/scd41/hum",
"esp32/gas/co", "esp32/gas/no2", "esp32/gas/etanol",
"esp32/gas/nh3", "esp32/gas/ch4",
"esp32/bmp280/temp", "esp32/bmp280/presion"
};
while (dataLine.length() > 0 && i < 10) {
int sep = dataLine.indexOf(',');
String value = (sep == -1) ? dataLine : dataLine.substring(0, sep);
client.publish(topics[i], value.c_str());
Serial.println("📤 MQTT → " + String(topics[i]) + ": " + value);
if (sep == -1) break;
dataLine.remove(0, sep + 1);
i++;
}
Serial.println("✅ Datos Arduino publicados\n");
}
}
delay(2500); // Espera entre lecturas
}
#include <Wire.h>
#include "SensirionI2cScd4x.h"
#include "MutichannelGasSensor.h"
#include <Adafruit_BMP280.h>
#include <Adafruit_Sensor.h>
// Objetos de sensores
SensirionI2cScd4x scd4x;
Adafruit_BMP280 bmp; // Usa I2C por defecto
void setup() {
Serial.begin(9600);
Wire.begin();
// 🟢 Inicializar SCD41 (CO2, temp, humedad)
scd4x.begin(Wire, 0x62);
scd4x.stopPeriodicMeasurement();
scd4x.startPeriodicMeasurement();
// 🟢 Inicializar sensor de gases multicanal
gas.begin(0x04); // Dirección I2C por defecto
// 🟢 Inicializar BMP280
if (!bmp.begin(0x77)) { // o 0x77 si tu módulo usa esa dirección
Serial.println("❌ No se detecta el BMP280");
while (1);
}
Serial.println("✅ Todos los sensores inicializados.");
}
void loop() {
Serial.println("📡 Lectura de sensores:");
// 🔹 Leer SCD41
uint16_t co2;
float temp, hum;
if (!scd4x.readMeasurement(co2, temp, hum)) {
Serial.print("🌿 CO₂: "); Serial.print(co2); Serial.println(" ppm");
Serial.print("🌡️ Temp SCD41: "); Serial.print(temp); Serial.println(" °C");
Serial.print("💧 Humedad: "); Serial.print(hum); Serial.println(" %");
} else {
Serial.println("⚠️ Error al leer SCD41");
}
// 🔹 Leer gases
Serial.print("🧪 CO: "); Serial.print(gas.measure_CO()); Serial.println(" ppm");
Serial.print("🧪 NO2: "); Serial.print(gas.measure_NO2()); Serial.println(" ppm");
Serial.print("🧪 Etanol: "); Serial.print(gas.measure_C2H5OH()); Serial.println(" ppm");
Serial.print("🧪 NH3: "); Serial.print(gas.measure_NH3()); Serial.println(" ppm");
Serial.print("🧪 CH4: "); Serial.print(gas.measure_CH4()); Serial.println(" ppm");
// 🔹 Leer BMP280
float bmpTemp = bmp.readTemperature(); // °C
float pressure = bmp.readPressure() / 100.0F; // hPa
Serial.print("🌡️ Temp BMP280: "); Serial.print(bmpTemp); Serial.println(" °C");
Serial.print("🔵 Presión: "); Serial.print(pressure); Serial.println(" hPa");
Serial.println("------------------------------");
delay(5000); // cada 5 segundos
}
import threading
import math
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import uvicorn
import paho.mqtt.client as mqtt
import firebase_admin
from firebase_admin import credentials, firestore
import pickle
# ---------------------------
# Cargar el modelo de predicción
# ---------------------------
try:
with open("environment_quality_model.pkl", "rb") as f:
clf = pickle.load(f)
except Exception as e:
raise RuntimeError("No se pudo cargar el modelo: " + str(e))
# ---------------------------
# Inicialización de FastAPI y configuración CORS
# ---------------------------
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # En producción, restringir a los orígenes necesarios, ej.: ["http://localhost:8001"]
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ---------------------------
# Inicialización de Firebase Admin
# ---------------------------
try:
firebase_admin.get_app()
except ValueError:
cred = credentials.Certificate("firebase_credentials.json")
firebase_admin.initialize_app(cred)
db = firestore.client()
# ---------------------------
# Configuración de MQTT
# ---------------------------
MQTT_BROKER = "test.mosquitto.org"
MQTT_PORT = 1883
MQTT_TOPICS = {
"temp": "esp32/dht22/temperatura",
"hum": "esp32/dht22/humedad",
"gas": "esp32/mq5/gas",
"luxRaw": "esp32/temt6000/lux_raw",
"luxEst": "esp32/temt6000/lux_est",
"uvRaw": "esp32/uv/raw",
"uvPower": "esp32/uv/uWcm2",
"soundRaw": "esp32/sound/raw",
"soundDB": "esp32/sound/db_est"
}
# Diccionario global para almacenar los datos de los sensores
sensor_data = {
"temp": "---",
"hum": "---",
"gas": "---",
"gasPPM": "---",
"luxRaw": "---",
"luxEst": "---",
"uvRaw": "---",
"uvPower": "---",
"soundRaw": "---",
"soundDB": "---"
}
# ---------------------------
# Función para actualizar Firebase (crea un nuevo documento con timestamp)
# ---------------------------
def update_firebase(data: dict):
data_with_timestamp = data.copy()
data_with_timestamp["timestamp"] = firestore.SERVER_TIMESTAMP
db.collection("sensors").add(data_with_timestamp)
# ---------------------------
# Callbacks de MQTT
# ---------------------------
def on_connect(client, userdata, flags, rc):
print("Conectado a MQTT con código de resultado:", rc)
for topic in MQTT_TOPICS.values():
client.subscribe(topic)
print("Suscrito al topic:", topic)
def on_message(client, userdata, msg):
topic = msg.topic
value = msg.payload.decode()
print(f"Mensaje recibido en {topic}: {value}")
global sensor_data
if topic == MQTT_TOPICS["temp"]:
sensor_data["temp"] = value
elif topic == MQTT_TOPICS["hum"]:
sensor_data["hum"] = value
elif topic == MQTT_TOPICS["gas"]:
sensor_data["gas"] = value
try:
raw = int(value)
if raw > 0 and raw < 4095:
ratio = (4095 - raw) / raw
gas_ppm = round(116250 * math.pow(ratio, 2.65))
sensor_data["gasPPM"] = gas_ppm
else:
sensor_data["gasPPM"] = "---"
except Exception as e:
sensor_data["gasPPM"] = "---"
elif topic == MQTT_TOPICS["luxRaw"]:
sensor_data["luxRaw"] = value
elif topic == MQTT_TOPICS["luxEst"]:
sensor_data["luxEst"] = value
elif topic == MQTT_TOPICS["uvRaw"]:
sensor_data["uvRaw"] = value
elif topic == MQTT_TOPICS["uvPower"]:
sensor_data["uvPower"] = value
elif topic == MQTT_TOPICS["soundRaw"]:
sensor_data["soundRaw"] = value
elif topic == MQTT_TOPICS["soundDB"]:
sensor_data["soundDB"] = value
update_firebase(sensor_data)
# Configuración del cliente MQTT
mqtt_client = mqtt.Client()
mqtt_client.on_connect = on_connect
mqtt_client.on_message = on_message
def mqtt_loop():
mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60)
mqtt_client.loop_forever()
# ---------------------------
# Definición de modelos y endpoints de FastAPI
# ---------------------------
# Modelo para datos de sensores (para el endpoint /sensors)
class SensorData(BaseModel):
temp: str
hum: str
gas: str
gasPPM: str
luxRaw: str
luxEst: str
uvRaw: str
uvPower: str
soundRaw: str
soundDB: str
@app.get("/sensors", response_model=SensorData)
def get_sensor_data():
"""
Devuelve los datos más recientes de los sensores.
Se intenta recuperar el documento 'latest' de Firestore;
si no existe, se retorna el diccionario en memoria.
"""
doc_ref = db.collection("sensors").document("latest")
doc = doc_ref.get()
if doc.exists:
return doc.to_dict()
return sensor_data
@app.get("/sensors/history")
def get_sensor_history(limit: int = Query(50, gt=0)):
"""
Devuelve los registros históricos de la colección "sensors".
Puedes limitar la cantidad de documentos devueltos con el parámetro 'limit'.
"""
try:
docs = db.collection("sensors") \
.order_by("timestamp", direction=firestore.Query.DESCENDING) \
.limit(limit).stream()
history = []
for doc in docs:
data = doc.to_dict()
data["id"] = doc.id
history.append(data)
return history
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error al obtener la historia: {e}")
@app.get("/sensors/{doc_id}")
def get_sensor_by_id(doc_id: str):
"""
Devuelve los datos de un registro específico identificado por su ID.
"""
doc_ref = db.collection("sensors").document(doc_id)
doc = doc_ref.get()
if doc.exists:
return doc.to_dict()
else:
raise HTTPException(status_code=404, detail="Documento no encontrado")
# Modelo de entrada para el endpoint de predicción (/predict)
class SensorInput(BaseModel):
temp: float
hum: float
gasPPM: float
luxEst: float
uvPower: float
soundDB: float
@app.post("/predict")
def predict_quality(input_data: SensorInput):
"""
Realiza la predicción de calidad ambiental utilizando el modelo entrenado y
publica el resultado en MQTT (topic: "esp32/model/prediction").
"""
X_input = [[
input_data.temp,
input_data.hum,
input_data.gasPPM,
input_data.luxEst,
input_data.uvPower,
input_data.soundDB
]]
try:
prediction = clf.predict(X_input)[0]
# Publicar el resultado de la predicción en MQTT:
mqtt_client.publish("esp32/model/prediction", str(prediction))
except Exception as e:
raise HTTPException(status_code=500, detail="Error al predecir: " + str(e))
# Retornamos un mensaje indicando que la predicción se publicó vía MQTT
return {"detail": "Prediction published via MQTT"}
# ---------------------------
# Iniciar el loop de MQTT al arrancar la aplicación
# ---------------------------
@app.on_event("startup")
def startup_event():
thread = threading.Thread(target=mqtt_loop)
thread.daemon = True
thread.start()
# ---------------------------
# Ejecutar la aplicación con uvicorn
# ---------------------------
if __name__ == "__main__":
uvicorn.run("plataformaDatosAmbientales:app", host="0.0.0.0", port=8000)
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
import pickle
# 1. Cargar el CSV
df = pd.read_csv("firestore_data.csv")
print(df.head())
print("Número de registros:", len(df))
# 2. Convertir columnas a numérico y reemplazar NaN por 0
numeric_cols = [
"temp", "hum", "gas", "gasPPM",
"luxRaw", "luxEst", "uvRaw", "uvPower",
"soundRaw", "soundDB"
]
for col in numeric_cols:
df[col] = pd.to_numeric(df[col], errors='coerce')
df[numeric_cols] = df[numeric_cols].fillna(0)
# 3. Definir las variables que usaremos en la métrica multivariable:
# La fórmula es:
# Calidad_ambiental = w1·Temp + w2·Humedad + w3·GasPPM +
# w4·LuxEst + w5·UV_power + w6·SoundDB
features = ["temp", "hum", "gasPPM", "luxEst", "uvPower", "soundDB"]
# 4. Calcular la métrica de calidad ambiental (quality_score)
# Simulamos unos pesos determinados históricamente (estos valores son un ejemplo)
weights = [0.2, 0.15, 0.3, 0.1, 0.15, 0.1] # w1, w2, ..., w6
df['quality_score'] = (weights[0] * df['temp'] +
weights[1] * df['hum'] +
weights[2] * df['gasPPM'] +
weights[3] * df['luxEst'] +
weights[4] * df['uvPower'] +
weights[5] * df['soundDB'])
# 5. Definir umbrales de clasificación basados en cuantiles del quality_score
threshold1 = df['quality_score'].quantile(0.33)
threshold2 = df['quality_score'].quantile(0.66)
def classify_quality(score):
if score < threshold1:
return "Buena"
elif score < threshold2:
return "Regular"
else:
return "Mala"
df['quality_cat'] = df['quality_score'].apply(classify_quality)
print("Distribución de calidad ambiental:")
print(df['quality_cat'].value_counts())
# 6. Seleccionar características y variable objetivo para el modelo
X = df[features]
y = df["quality_cat"]
# 7. Dividir el dataset en entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 8. Entrenar un modelo de clasificación (RandomForestClassifier)
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train, y_train)
# 9. Evaluar el modelo
y_pred = clf.predict(X_test)
print("Reporte de clasificación:")
print(classification_report(y_test, y_pred))
# 10. Guardar el modelo para uso futuro
with open("environment_quality_model.pkl", "wb") as f:
pickle.dump(clf, f)
print("Modelo entrenado y guardado correctamente.")
import pickle
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
# Cargar el modelo entrenado
try:
with open("environment_quality_model.pkl", "rb") as f:
clf = pickle.load(f)
except Exception as e:
raise RuntimeError("No se pudo cargar el modelo: " + str(e))
# Definir el modelo de entrada con Pydantic
class SensorInput(BaseModel):
temp: float
hum: float
gasPPM: float
luxEst: float
uvPower: float
soundDB: float
app = FastAPI()
@app.post("/predict")
def predict_quality(input_data: SensorInput):
# Orden de características según se entrenó el modelo
X_input = [[
input_data.temp,
input_data.hum,
input_data.gasPPM,
input_data.luxEst,
input_data.uvPower,
input_data.soundDB
]]
try:
prediction = clf.predict(X_input)[0]
except Exception as e:
raise HTTPException(status_code=500, detail="Error al predecir: " + str(e))
return {"prediction": prediction}
Comments