613 lines
8.3 KiB
HTML
613 lines
8.3 KiB
HTML
<!DOCTYPE html>
|
|
|
|
<html lang="es">
|
|
|
|
<head>
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
<title>Guardián Fénix</title>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
|
<style>
|
|
|
|
body{
|
|
|
|
font-family:Arial,Helvetica,sans-serif;
|
|
|
|
margin:0;
|
|
|
|
background:#fdf2f2;
|
|
|
|
color:#333;
|
|
|
|
}
|
|
|
|
header{
|
|
|
|
background:linear-gradient(90deg,#8b0000,#c62828);
|
|
|
|
color:#fff;
|
|
|
|
padding:15px;
|
|
|
|
text-align:center;
|
|
|
|
}
|
|
|
|
.nav{
|
|
|
|
display:flex;
|
|
|
|
background:#8b0000;
|
|
|
|
}
|
|
|
|
.nav button{
|
|
|
|
flex:1;
|
|
|
|
padding:12px;
|
|
|
|
border:none;
|
|
|
|
background:#8b0000;
|
|
|
|
color:#fff;
|
|
|
|
font-weight:bold;
|
|
|
|
cursor:pointer;
|
|
|
|
}
|
|
|
|
.nav button.active{
|
|
|
|
background:#c62828;
|
|
|
|
}
|
|
|
|
.section{
|
|
|
|
display:none;
|
|
|
|
padding:10px;
|
|
|
|
}
|
|
|
|
.section.active{
|
|
|
|
display:block;
|
|
|
|
}
|
|
.card{
|
|
background:#fff;
|
|
padding:22px 20px;
|
|
margin:16px 12px;
|
|
border-radius:16px;
|
|
box-shadow:0 4px 10px rgba(0,0,0,.15);
|
|
line-height:1.6;
|
|
|
|
}
|
|
.logo{
|
|
width:70%;
|
|
max-width:280px;
|
|
display:block;
|
|
margin:16px auto;
|
|
}
|
|
h2{color:#8b0000}
|
|
|
|
.data{margin:6px 0}
|
|
|
|
button.action{
|
|
|
|
background:#c62828;
|
|
|
|
color:#fff;
|
|
|
|
border:none;
|
|
|
|
padding:10px 15px;
|
|
|
|
border-radius:5px;
|
|
|
|
cursor:pointer;
|
|
|
|
margin-top:8px;
|
|
|
|
}
|
|
|
|
button.action:hover{background:#8b0000}
|
|
|
|
.alerta{
|
|
|
|
display:none;
|
|
|
|
background:#fdeaea;
|
|
|
|
border-left:6px solid #c62828;
|
|
|
|
padding:15px;
|
|
|
|
margin-top:12px;
|
|
|
|
font-weight:bold;
|
|
|
|
}
|
|
|
|
table{
|
|
|
|
width:100%;
|
|
|
|
border-collapse:collapse;
|
|
|
|
margin-top:10px;
|
|
|
|
}
|
|
|
|
th,td{
|
|
|
|
border:1px solid #ccc;
|
|
|
|
padding:6px;
|
|
|
|
text-align:center;
|
|
|
|
}
|
|
|
|
th{
|
|
|
|
background:#c62828;
|
|
|
|
color:#fff;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<header>
|
|
|
|
<h1>🛡️ Guardián Fénix 🔥</h1><img
|
|
<img src="freepik_23c11d8d-723a-469e-ae09-1d9d4a639e0a.png"
|
|
alt="Guardián Fénix"
|
|
class="logo">
|
|
<p>Prototipo de monitoreo ambiental y vibración</p>
|
|
|
|
</header>
|
|
|
|
<div class="nav">
|
|
|
|
<button class="active" onclick="mostrar('inicio',this)">Inicio</button>
|
|
|
|
<button onclick="mostrar('sensores',this)">Sensores</button>
|
|
|
|
<button onclick="mostrar('graficas',this)">Gráficas</button>
|
|
|
|
</div>
|
|
|
|
<!-- INICIO -->
|
|
|
|
<div id="inicio" class="section active">
|
|
|
|
<div class="card">
|
|
|
|
<h2>Descripción del proyecto</h2>
|
|
|
|
<p>
|
|
|
|
Guardián Fénix es un prototipo de monitoreo diseñado para sitios remotos
|
|
|
|
sin infraestructura eléctrica. El sistema simula sensores físicos
|
|
|
|
utilizando los sensores del teléfono móvil.
|
|
|
|
</p>
|
|
|
|
<p>
|
|
|
|
La batería del dispositivo representa la batería de respaldo del sistema,
|
|
|
|
mientras que la conexión a una fuente eléctrica simula un panel solar activo.
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
<div class="card">
|
|
|
|
<h2>Estado de energía</h2>
|
|
|
|
<p class="data">Batería del sistema: <span id="bat">--</span>%</p>
|
|
|
|
<p class="data">Fuente de energía: <span id="energia">--</span></p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- SENSORES -->
|
|
|
|
<div id="sensores" class="section">
|
|
|
|
<div class="card">
|
|
|
|
<h2>Sensor Ambiental</h2>
|
|
|
|
<p class="data">Temperatura: <span id="temp">--</span> °C</p>
|
|
|
|
<p class="data">Humedad: <span id="hum">--</span> %</p>
|
|
|
|
<p class="data">Presión: <span id="pres">--</span> hPa</p>
|
|
|
|
<button class="action" onclick="clima()">Actualizar clima</button>
|
|
|
|
<p id="errorClima" style="color:#b00000;font-size:0.85em;"></p>
|
|
|
|
</div>
|
|
|
|
<div class="card">
|
|
|
|
<h2>Sensor de Vibración</h2>
|
|
|
|
<p class="data">Vibración: <span id="vib">0.00</span></p>
|
|
|
|
<p class="data">Estado: <span id="estado">Estable</span></p>
|
|
|
|
<button class="action" id="btnVib" onclick="toggleVibracion()">Activar sensor</button>
|
|
|
|
<div class="alerta" id="alerta">
|
|
|
|
🚨 Vibración peligrosa detectada<br><br>
|
|
|
|
<button class="action" onclick="alertar()">Alertar usuarios cercanos</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="card">
|
|
|
|
<h2>Registros</h2>
|
|
|
|
<table>
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th>Hora</th>
|
|
|
|
<th>Temp</th>
|
|
|
|
<th>Hum</th>
|
|
|
|
<th>Pres</th>
|
|
|
|
<th>Evento</th>
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody id="tabla"></tbody>
|
|
|
|
</table>
|
|
|
|
<button class="action" onclick="guardar()">Guardar registro</button>
|
|
|
|
<button class="action" style="background:#555" onclick="borrarUltimo()">Borrar último</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- GRAFICAS -->
|
|
|
|
<div id="graficas" class="section">
|
|
|
|
<div class="card">
|
|
|
|
<h2>Humedad</h2>
|
|
|
|
<canvas id="gHum"></canvas>
|
|
|
|
</div>
|
|
|
|
<div class="card">
|
|
|
|
<h2>Temperatura</h2>
|
|
|
|
<canvas id="gTemp"></canvas>
|
|
|
|
</div>
|
|
|
|
<div class="card">
|
|
|
|
<h2>Presión</h2>
|
|
|
|
<canvas id="gPres"></canvas>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<audio id="sonido" loop>
|
|
|
|
<source src="https://actions.google.com/sounds/v1/alarms/alarm_clock.ogg">
|
|
|
|
</audio>
|
|
|
|
<script>
|
|
|
|
const apiKey="729294e1fe04354f62449218961044b8";
|
|
|
|
let sensorActivo=false;
|
|
|
|
let alertaActiva=false;
|
|
|
|
let vibracion=0;
|
|
|
|
let charts={};
|
|
|
|
// Navegación
|
|
|
|
function mostrar(id,btn){
|
|
|
|
document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));
|
|
|
|
document.querySelectorAll('.nav button').forEach(b=>b.classList.remove('active'));
|
|
|
|
document.getElementById(id).classList.add('active');
|
|
|
|
btn.classList.add('active');
|
|
|
|
actualizarGraficas();
|
|
|
|
}
|
|
|
|
// Batería y panel solar
|
|
|
|
if(navigator.getBattery){
|
|
|
|
navigator.getBattery().then(b=>{
|
|
|
|
function actualizar(){
|
|
|
|
bat.innerText=Math.round(b.level*100);
|
|
|
|
energia.innerText=b.charging?"Panel solar activo":"Batería de respaldo";
|
|
|
|
}
|
|
|
|
actualizar();
|
|
|
|
b.addEventListener("levelchange",actualizar);
|
|
|
|
b.addEventListener("chargingchange",actualizar);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
// Clima
|
|
|
|
async function clima(){
|
|
|
|
errorClima.innerText="";
|
|
|
|
try{
|
|
|
|
const r=await fetch(`https://api.openweathermap.org/data/2.5/weather?lat=25.6866&lon=-100.3161&appid=${apiKey}&units=metric`);
|
|
|
|
const d=await r.json();
|
|
|
|
temp.innerText=Math.round(d.main.temp);
|
|
|
|
hum.innerText=d.main.humidity;
|
|
|
|
pres.innerText=d.main.pressure;
|
|
|
|
}catch{
|
|
|
|
errorClima.innerText="No se pudo acceder al clima";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Vibración
|
|
|
|
function toggleVibracion(){
|
|
|
|
if(!sensorActivo){
|
|
|
|
window.addEventListener("devicemotion",leerVibracion);
|
|
|
|
btnVib.innerText="Desactivar sensor";
|
|
|
|
}else{
|
|
|
|
window.removeEventListener("devicemotion",leerVibracion);
|
|
|
|
btnVib.innerText="Activar sensor";
|
|
|
|
}
|
|
|
|
sensorActivo=!sensorActivo;
|
|
|
|
}
|
|
|
|
function leerVibracion(e){
|
|
|
|
let x=e.accelerationIncludingGravity?.x||0;
|
|
|
|
let y=e.accelerationIncludingGravity?.y||0;
|
|
|
|
let z=e.accelerationIncludingGravity?.z||0;
|
|
|
|
vibracion=Math.sqrt(x*x+y*y+z*z).toFixed(2);
|
|
|
|
vib.innerText=vibracion;
|
|
|
|
if(vibracion>12 && !alertaActiva){
|
|
|
|
alertaActiva=true;
|
|
|
|
estado.innerText="Inestable";
|
|
|
|
alerta.style.display="block";
|
|
|
|
sonido.play();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function alertar(){
|
|
|
|
alerta.style.display="none";
|
|
|
|
sonido.pause();
|
|
|
|
sonido.currentTime=0;
|
|
|
|
estado.innerText="Alerta atendida";
|
|
|
|
alertaActiva=false;
|
|
|
|
alert("Usuarios cercanos alertados");
|
|
|
|
}
|
|
|
|
// Registros
|
|
|
|
function guardar(){
|
|
|
|
const r={
|
|
|
|
h:new Date().toLocaleTimeString(),
|
|
|
|
t:temp.innerText,
|
|
|
|
hu:hum.innerText,
|
|
|
|
p:pres.innerText,
|
|
|
|
e:estado.innerText
|
|
|
|
};
|
|
|
|
let d=JSON.parse(localStorage.getItem("reg"))||[];
|
|
|
|
d.push(r);
|
|
|
|
localStorage.setItem("reg",JSON.stringify(d));
|
|
|
|
cargarTabla();
|
|
|
|
actualizarGraficas();
|
|
|
|
}
|
|
|
|
function cargarTabla(){
|
|
|
|
tabla.innerHTML="";
|
|
|
|
(JSON.parse(localStorage.getItem("reg"))||[]).forEach(r=>{
|
|
|
|
tabla.innerHTML+=`
|
|
|
|
<tr>
|
|
|
|
<td>${r.h}</td>
|
|
|
|
<td>${r.t}</td>
|
|
|
|
<td>${r.hu}</td>
|
|
|
|
<td>${r.p}</td>
|
|
|
|
<td>${r.e}</td>
|
|
|
|
</tr>`;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
cargarTabla();
|
|
|
|
function borrarUltimo(){
|
|
|
|
let d=JSON.parse(localStorage.getItem("reg"))||[];
|
|
|
|
d.pop();
|
|
|
|
localStorage.setItem("reg",JSON.stringify(d));
|
|
|
|
cargarTabla();
|
|
|
|
actualizarGraficas();
|
|
|
|
}
|
|
|
|
// Gráficas automáticas
|
|
|
|
function graf(id,label,data,color){
|
|
|
|
if(charts[id]) charts[id].destroy();
|
|
|
|
charts[id]=new Chart(document.getElementById(id),{
|
|
|
|
type:"line",
|
|
|
|
data:{
|
|
|
|
labels:data.map((_,i)=>i+1),
|
|
|
|
datasets:[{
|
|
|
|
label:label,
|
|
|
|
data:data,
|
|
|
|
borderColor:color,
|
|
|
|
backgroundColor:color+"33",
|
|
|
|
fill:true,
|
|
|
|
tension:0.3
|
|
|
|
}]
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
function actualizarGraficas(){
|
|
|
|
const d=JSON.parse(localStorage.getItem("reg"))||[];
|
|
|
|
graf("gHum","Humedad (%)",d.map(x=>x.hu),"#1976d2");
|
|
|
|
graf("gTemp","Temperatura (°C)",d.map(x=>x.t),"#d32f2f");
|
|
|
|
graf("gPres","Presión (hPa)",d.map(x=>x.p),"#388e3c");
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|