258 lines
No EOL
7.6 KiB
Python
258 lines
No EOL
7.6 KiB
Python
# MicroPython ESP32 - Guardián Fénix
|
|
# Sensores: BME680 + ADXL345
|
|
# Servidor web para mostrar datos y registros
|
|
|
|
import machine, time, ujson, network, socket, math
|
|
from machine import Pin, I2C
|
|
import bme680 # Asegúrate de tener la librería BME680 para MicroPython
|
|
import adxl345
|
|
|
|
# -------------------
|
|
# CONFIGURACIÓN SENSORES
|
|
# -------------------
|
|
i2c = I2C(scl=Pin(5), sda=Pin(4))
|
|
|
|
# BME680
|
|
bme = bme680.BME680(i2c)
|
|
|
|
# ADXL345
|
|
acc = adxl345.ADXL345(i2c)
|
|
|
|
# -------------------
|
|
# CONFIGURACIÓN WIFI (HOTSPOT)
|
|
# -------------------
|
|
ssid = "GuardianFenix"
|
|
password = "12345678"
|
|
|
|
ap = network.WLAN(network.AP_IF)
|
|
ap.active(True)
|
|
ap.config(essid=ssid, password=password)
|
|
|
|
print("Hotspot activo:", ssid)
|
|
|
|
# -------------------
|
|
# VARIABLES
|
|
# -------------------
|
|
registros = [] # Lista de registros
|
|
aceptar_vib = False
|
|
alerta_activa = False
|
|
|
|
# -------------------
|
|
# FUNCIONES DE SENSORES
|
|
# -------------------
|
|
def leer_bme680():
|
|
bme.measure(gas=False)
|
|
temp = bme.temperature()
|
|
hum = bme.humidity()
|
|
pres = bme.pressure()
|
|
return {"temp": round(temp,1), "hum": round(hum,1), "pres": round(pres,1)}
|
|
|
|
def leer_adxl345():
|
|
x, y, z = acc.read_g() # m/s²
|
|
vib = abs((x*2 + y*2 + z*2) * 2)
|
|
estado = "Estable"
|
|
global alerta_activa
|
|
if vib > 6:
|
|
estado = "Inestable"
|
|
alerta_activa = True
|
|
else:
|
|
alerta_activa = False
|
|
return {"vib": round(vib,2), "estado": estado}
|
|
|
|
# -------------------
|
|
# REGISTRO AUTOMÁTICO
|
|
# -------------------
|
|
def registrar():
|
|
datos = leer_bme680()
|
|
vib = leer_adxl345()
|
|
datos["vib"] = vib["vib"]
|
|
datos["estado"] = vib["estado"]
|
|
datos["hora"] = time.localtime()[3:6] # HH:MM:SS
|
|
registros.append(datos)
|
|
# Mantener solo últimos 100 registros
|
|
if len(registros) > 100:
|
|
registros.pop(0)
|
|
return datos
|
|
|
|
# -------------------
|
|
# SERVIDOR WEB
|
|
# -------------------
|
|
def web_page():
|
|
return """<!doctype html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Guardián Fénix</title>
|
|
<style>
|
|
body{font-family:Arial,sans-serif;background:#fdf2f2;color:#333;margin:16px}
|
|
h1{margin:8px 0 16px 0}
|
|
.card{background:#fff;padding:16px;margin:12px 0;border-radius:12px;box-shadow:0 4px 10px rgba(0,0,0,.12);}
|
|
button{background:#c62828;color:#fff;border:none;padding:10px 14px;border-radius:8px;cursor:pointer;margin:6px 6px 0 0;}
|
|
.row{display:flex;flex-wrap:wrap;gap:12px}
|
|
.stat{min-width:160px}
|
|
.big{font-size:28px;font-weight:700}
|
|
table{width:100%;border-collapse:collapse;font-size:.9em;margin-top:10px}
|
|
th,td{border:1px solid #ccc;padding:6px;text-align:center}
|
|
th{background:#f3d6d6}
|
|
.mono{font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>🛡️ Guardián Fénix 🔥</h1>
|
|
|
|
<div class="card">
|
|
<h2>🌿 Sensor Ambiental (BME680)</h2>
|
|
<div class="row">
|
|
<div class="stat"><div>Temp</div><div class="big"><span id="temp">--</span> °C</div></div>
|
|
<div class="stat"><div>Hum</div><div class="big"><span id="hum">--</span> %</div></div>
|
|
<div class="stat"><div>Pres</div><div class="big"><span id="pres">--</span> hPa</div></div>
|
|
<div class="stat"><div>Gas</div><div class="big"><span id="gas">--</span> Ω</div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>📳 Vibración (ADXL345)</h2>
|
|
<div class="row">
|
|
<div class="stat"><div>Shake</div><div class="big"><span id="shake">--</span></div></div>
|
|
<div class="stat mono">ax=<span id="ax">--</span>g</div>
|
|
<div class="stat mono">ay=<span id="ay">--</span>g</div>
|
|
<div class="stat mono">az=<span id="az">--</span>g</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>📝 Registros</h2>
|
|
<button id="btnNow">Leer ahora</button>
|
|
<button id="btnLog">Guardar registro</button>
|
|
<button id="btnClear">Borrar último</button>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>#</th><th>Hora</th><th>Temp</th><th>Hum</th><th>Pres</th><th>Gas</th><th>Shake</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="tabla"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<script>
|
|
const el = (id)=>document.getElementById(id);
|
|
|
|
function setText(id, v) {
|
|
el(id).textContent = (v===null || v===undefined) ? "--" : v;
|
|
}
|
|
|
|
function drawTable(list) {
|
|
const tb = el("tabla");
|
|
tb.innerHTML = "";
|
|
const last = list.slice(-20); // show last 20
|
|
last.forEach((r, i) => {
|
|
const hora = Array.isArray(r.hora) ? `${r.hora[0]}:${r.hora[1]}:${r.hora[2]}` : "--";
|
|
tb.innerHTML += `
|
|
<tr>
|
|
<td>${list.length - last.length + i + 1}</td>
|
|
<td>${hora}</td>
|
|
<td>${r.temp ?? "--"}</td>
|
|
<td>${r.hum ?? "--"}</td>
|
|
<td>${r.pres ?? "--"}</td>
|
|
<td>${r.gas ?? "--"}</td>
|
|
<td>${r.shake ?? r.vib ?? "--"}</td>
|
|
</tr>`;
|
|
});
|
|
}
|
|
|
|
async function refresh() {
|
|
try {
|
|
const r = await fetch("/data");
|
|
const d = await r.json();
|
|
|
|
// If /data returns a single object:
|
|
if (!Array.isArray(d)) {
|
|
setText("temp", d.temp ?? d.t_c);
|
|
setText("hum", d.hum ?? d.h_pct);
|
|
setText("pres", d.pres ?? d.p_hpa);
|
|
setText("gas", d.gas ?? d.gas_ohms);
|
|
|
|
setText("ax", d.ax);
|
|
setText("ay", d.ay);
|
|
setText("az", d.az);
|
|
setText("shake", d.shake ?? d.vib);
|
|
|
|
return;
|
|
}
|
|
|
|
// If /data returns the whole registros list:
|
|
drawTable(d);
|
|
const last = d[d.length-1];
|
|
if (last) {
|
|
setText("temp", last.temp);
|
|
setText("hum", last.hum);
|
|
setText("pres", last.pres);
|
|
setText("gas", last.gas);
|
|
setText("shake", last.shake ?? last.vib);
|
|
}
|
|
} catch (e) {
|
|
console.log("refresh error", e);
|
|
}
|
|
}
|
|
|
|
// Buttons call your endpoints AND then refresh screen
|
|
el("btnNow").onclick = async () => { await fetch("/leer"); await refresh(); };
|
|
el("btnLog").onclick = async () => { await fetch("/guardar"); await refresh(); };
|
|
el("btnClear").onclick = async () => { await fetch("/borrar"); await refresh(); };
|
|
|
|
setInterval(refresh, 1000);
|
|
refresh();
|
|
</script>
|
|
</body>
|
|
</html>"""
|
|
|
|
# -------------------
|
|
# SERVIDOR
|
|
# -------------------
|
|
addr = socket.getaddrinfo('192.168.4.1',80)[0][-1]
|
|
s = socket.socket()
|
|
s.bind(addr)
|
|
s.listen(5)
|
|
print('Servidor escuchando en', addr)
|
|
|
|
def handle_client(cl):
|
|
try:
|
|
req = cl.recv(1024)
|
|
req = str(req)
|
|
if 'GET /data' in req:
|
|
res = ujson.dumps(registros)
|
|
cl.send('HTTP/1.0 200 OK\r\nContent-Type: application/json\r\n\r\n')
|
|
cl.send(res)
|
|
elif 'GET /leer' in req:
|
|
d = registrar()
|
|
cl.send('HTTP/1.0 200 OK\r\nContent-Type: application/json\r\n\r\n')
|
|
cl.send(ujson.dumps(d))
|
|
elif 'GET /guardar' in req:
|
|
d = registrar()
|
|
cl.send('HTTP/1.0 200 OK\r\nContent-Type: application/json\r\n\r\n')
|
|
cl.send(ujson.dumps(d))
|
|
elif 'GET /borrar' in req:
|
|
if registros:
|
|
registros.pop()
|
|
cl.send('HTTP/1.0 200 OK\r\nContent-Type: application/json\r\n\r\n')
|
|
cl.send(ujson.dumps(registros))
|
|
elif 'GET /toggle' in req:
|
|
global aceptar_vib
|
|
aceptar_vib = not aceptar_vib
|
|
cl.send('HTTP/1.0 200 OK\r\nContent-Type: application/json\r\n\r\n')
|
|
cl.send(ujson.dumps({"aceptar_vib":aceptar_vib}))
|
|
else:
|
|
cl.send('HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n')
|
|
cl.send(web_page())
|
|
except Exception as e:
|
|
print("Error:", e)
|
|
cl.close()
|
|
|
|
# Loop principal
|
|
while True:
|
|
registrar()
|
|
cl, addr = s.accept()
|
|
handle_client(cl) |