mirror of
https://github.com/adrigongv23/G26---Telemetry-Software.git
synced 2026-05-25 04:21:27 +02:00
Página Telemetria que se va a usar (.html)
This commit is contained in:
parent
74b764c9d8
commit
5ca44845c8
1 changed files with 254 additions and 0 deletions
254
index.html
Normal file
254
index.html
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Telemetría IoT - Sesiones</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; font-family: Arial, sans-serif; background: linear-gradient(135deg,#667eea,#764ba2); min-height:100vh; display:flex; align-items:center; justify-content:center; padding:20px; }
|
||||
#app { background:#fff; width:100%; max-width:980px; border-radius:16px; padding:28px; box-shadow:0 20px 60px rgba(0,0,0,.25); }
|
||||
h1 { margin:0 0 8px; color:#333; text-align:center; }
|
||||
#estado { text-align:center; font-weight:700; padding:10px; border-radius:8px; margin:8px 0 14px; }
|
||||
.activa { color:#155724; background:#d4edda; }
|
||||
.inactiva { color:#721c24; background:#f8d7da; }
|
||||
#peso { text-align:center; font-size:3.2rem; font-weight:800; color:#007bff; margin:6px 0 16px; letter-spacing:-1px; }
|
||||
.grid { display:grid; grid-template-columns: repeat(auto-fit,minmax(180px,1fr)); gap:10px; margin-bottom:16px; }
|
||||
.card { background:#f7f7fb; border-radius:10px; padding:12px; text-align:center; }
|
||||
.label { font-size:.85rem; color:#6c757d; }
|
||||
.value { font-size:1.4rem; font-weight:700; color:#333; }
|
||||
.controls { display:flex; gap:10px; flex-wrap:wrap; justify-content:center; margin-top:8px; }
|
||||
button { background:#007bff; color:#fff; border:0; padding:12px 18px; border-radius:8px; font-weight:700; cursor:pointer; transition:.2s; }
|
||||
button.stop { background:#dc3545; }
|
||||
button.download { background:#28a745; }
|
||||
button:hover { filter:brightness(.95); transform: translateY(-1px); }
|
||||
#log { margin-top:16px; background:#f8f9fa; padding:12px; border-radius:8px; max-height:260px; overflow:auto; font-family: Consolas, monospace; font-size:.9rem; color:#495057; }
|
||||
#conn { text-align:center; margin-top:6px; font-size:.9rem; color:#6c757d; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>🔬 Telemetría IoT</h1>
|
||||
<div id="estado" class="inactiva">❌ Sesión Inactiva</div>
|
||||
<div id="peso">--- kg</div>
|
||||
<div id="conn">Socket: desconocido</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card"><div class="label">Ventanas recibidas</div><div class="value" id="ventanas-count">0</div></div>
|
||||
<div class="card"><div class="label">Muestras totales</div><div class="value" id="muestras-count">0</div></div>
|
||||
<div class="card"><div class="label">Última actualización</div><div class="value" id="ultima-actualizacion">---</div></div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="iniciarSesion()">▶️ Iniciar sesión</button>
|
||||
<button class="stop" onclick="finalizarSesion()">⏸️ Finalizar sesión</button>
|
||||
<button class="download" onclick="descargarCSV()">📥 Descargar CSV</button>
|
||||
</div>
|
||||
|
||||
<div id="log"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js"></script>
|
||||
<script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-database.js"></script>
|
||||
|
||||
<script>
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyB0JaH3ZPXdj-fw2LmKq1DGCEjriJ8hmgc",
|
||||
authDomain: "iot-formula-gades.firebaseapp.com",
|
||||
databaseURL: "https://iot-formula-gades-default-rtdb.europe-west1.firebasedatabase.app",
|
||||
projectId: "iot-formula-gades",
|
||||
storageBucket: "iot-formula-gades.firebasestorage.app",
|
||||
messagingSenderId: "930129619937",
|
||||
appId: "1:930129619937:web:473b802794abe593da40a0",
|
||||
measurementId: "G-CWXQWQQE3R"
|
||||
};
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
const db = firebase.database();
|
||||
|
||||
const controlRef = db.ref('/control/sesion_activa');
|
||||
const sesionesRef = db.ref('/sesiones');
|
||||
const infoConnRef = db.ref('.info/connected'); // [web:263]
|
||||
|
||||
const estadoEl = document.getElementById('estado');
|
||||
const connEl = document.getElementById('conn');
|
||||
const pesoEl = document.getElementById('peso');
|
||||
const ventanasCountEl = document.getElementById('ventanas-count');
|
||||
const muestrasCountEl = document.getElementById('muestras-count');
|
||||
const ultimaActualizacionEl = document.getElementById('ultima-actualizacion');
|
||||
const logEl = document.getElementById('log');
|
||||
|
||||
let ultimaSesionID = null; // clave actual de sesión
|
||||
let windowsRef = null; // referencia sin query
|
||||
let windowsQuery = null; // query activa
|
||||
let lastWindowId = null;
|
||||
let prevLogId = null;
|
||||
let prevSampleTs = null;
|
||||
let ventanasRecibidas = 0;
|
||||
let muestrasTotales = 0;
|
||||
let todasLasMuestras = [];
|
||||
|
||||
function log(msg) {
|
||||
const line = document.createElement('div');
|
||||
line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
|
||||
logEl.prepend(line);
|
||||
while (logEl.children.length > 300) logEl.removeChild(logEl.lastChild);
|
||||
}
|
||||
|
||||
infoConnRef.on('value', s => {
|
||||
connEl.textContent = s.val() ? 'Socket: conectado' : 'Socket: desconectado';
|
||||
});
|
||||
|
||||
function iniciarSesion() { controlRef.set(true); log('▶️ Iniciar sesión'); }
|
||||
function finalizarSesion(){ controlRef.set(false); log('⏸️ Finalizar sesión'); }
|
||||
|
||||
function iniciarSesionUI() {
|
||||
estadoEl.className = 'activa';
|
||||
estadoEl.textContent = '✅ Sesión Activa';
|
||||
pesoEl.style.color = '#28a745';
|
||||
actualizarMetricas();
|
||||
}
|
||||
function finalizarSesionUI() {
|
||||
estadoEl.className = 'inactiva';
|
||||
estadoEl.textContent = '❌ Sesión Inactiva';
|
||||
pesoEl.style.color = '#6c757d';
|
||||
pesoEl.textContent = '--- kg';
|
||||
actualizarMetricas();
|
||||
}
|
||||
function actualizarMetricas() {
|
||||
ventanasCountEl.textContent = ventanasRecibidas;
|
||||
muestrasCountEl.textContent = muestrasTotales;
|
||||
ultimaActualizacionEl.textContent = new Date().toLocaleTimeString();
|
||||
}
|
||||
function resetEstadoSesion() {
|
||||
ventanasRecibidas = 0; muestrasTotales = 0; todasLasMuestras = [];
|
||||
lastWindowId = null; prevLogId = null; prevSampleTs = null;
|
||||
actualizarMetricas();
|
||||
}
|
||||
|
||||
// UI de sesión (no afecta a tracking)
|
||||
controlRef.on('value', snap => {
|
||||
const activo = !!snap.val();
|
||||
if (activo) iniciarSesionUI(); else finalizarSesionUI();
|
||||
});
|
||||
|
||||
// 1) Al cargar, engancharse a la última
|
||||
sesionesRef.orderByKey().limitToLast(1).on('value', snap => {
|
||||
let key = null; snap.forEach(ch => key = ch.key);
|
||||
if (!key) return;
|
||||
const keyNum = Number(key);
|
||||
const curNum = ultimaSesionID ? Number(ultimaSesionID) : -1;
|
||||
if (keyNum > curNum) attachToSession(key);
|
||||
});
|
||||
|
||||
// 2) Y además detectar nuevas que aparezcan después
|
||||
sesionesRef.on('child_added', snap => {
|
||||
const key = snap.key;
|
||||
const keyNum = Number(key);
|
||||
const curNum = ultimaSesionID ? Number(ultimaSesionID) : -1;
|
||||
if (keyNum > curNum) attachToSession(key);
|
||||
});
|
||||
|
||||
function detachWindows() {
|
||||
if (windowsQuery) { windowsQuery.off(); windowsQuery = null; }
|
||||
if (windowsRef) { windowsRef.off(); windowsRef = null; }
|
||||
resetEstadoSesion();
|
||||
}
|
||||
|
||||
// Adjunta una sesión: hace un prefetch inicial y luego activa child_added
|
||||
function attachToSession(sid) {
|
||||
// Detach y reset
|
||||
detachWindows();
|
||||
ultimaSesionID = sid;
|
||||
log(`📂 Sesión activa: ${sid}`);
|
||||
|
||||
windowsRef = db.ref(`/sesiones/${sid}/windows`);
|
||||
|
||||
// Prefetch: traer lo que ya exista ahora mismo (por si se escribió antes de adjuntar)
|
||||
windowsRef.orderByKey().once('value', (snap) => {
|
||||
const batch = [];
|
||||
snap.forEach(ch => {
|
||||
batch.push({ id: Number(ch.key), v: ch.val() });
|
||||
});
|
||||
batch.sort((a,b) => a.id - b.id);
|
||||
for (const it of batch) {
|
||||
if (lastWindowId === null || it.id > lastWindowId) {
|
||||
processVentana(it.v, it.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Ahora activar child_added para lo que venga a partir de aquí
|
||||
windowsQuery = windowsRef.orderByKey().startAt(String((lastWindowId || 0) + 1)); // [web:263]
|
||||
windowsQuery.on('child_added', (snap2) => {
|
||||
const id = Number(snap2.key);
|
||||
if (Number.isFinite(lastWindowId) && id <= lastWindowId) return;
|
||||
processVentana(snap2.val(), id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function processVentana(w, id) {
|
||||
if (!w || !Array.isArray(w.muestras)) return;
|
||||
|
||||
if (prevLogId !== null && id > prevLogId + 1) {
|
||||
log(`⚠️ Faltan ${id - prevLogId - 1} ventanas entre ${prevLogId} y ${id}`);
|
||||
}
|
||||
prevLogId = id;
|
||||
|
||||
const t0 = typeof w.t0_ms === 'number' ? w.t0_ms : 0;
|
||||
const dt = typeof w.dt_ms === 'number' ? w.dt_ms : 500;
|
||||
const numMuestras = w.muestras.length;
|
||||
|
||||
w.muestras.forEach((m, i) => {
|
||||
const ts_ms = (typeof m.ts_ms === 'number') ? m.ts_ms : (t0 + i*dt);
|
||||
const iso = new Date(ts_ms).toISOString();
|
||||
const peso = (typeof m.peso === 'number') ? m.peso : parseFloat(m.peso);
|
||||
todasLasMuestras.push([iso, ts_ms, id, peso]);
|
||||
|
||||
if (prevSampleTs !== null && ts_ms - prevSampleTs > 2000) {
|
||||
const gap = ((ts_ms - prevSampleTs)/1000).toFixed(1);
|
||||
log(`⚠️ Hueco temporal de ${gap}s antes de ventana ${id}`);
|
||||
}
|
||||
prevSampleTs = ts_ms;
|
||||
});
|
||||
|
||||
// UI
|
||||
const last = w.muestras[numMuestras - 1];
|
||||
if (last && last.peso != null) {
|
||||
const v = parseFloat(last.peso);
|
||||
if (!Number.isNaN(v)) pesoEl.textContent = v.toFixed(3) + ' kg';
|
||||
}
|
||||
ventanasRecibidas++;
|
||||
muestrasTotales = todasLasMuestras.length;
|
||||
actualizarMetricas();
|
||||
|
||||
const lastTs = (typeof last?.ts_ms === 'number') ? last.ts_ms : (t0 + (numMuestras-1)*dt);
|
||||
log(`📦 Ventana ${id} (+${numMuestras} muestras) @ ${new Date(lastTs).toISOString()}`);
|
||||
|
||||
lastWindowId = id;
|
||||
}
|
||||
|
||||
function descargarCSV() {
|
||||
if (!ultimaSesionID) { alert('⚠️ No hay sesión.'); return; }
|
||||
if (todasLasMuestras.length === 0) { alert('⚠️ Sin datos.'); return; }
|
||||
todasLasMuestras.sort((a,b) => a[1] - b[1]);
|
||||
const BOM = "\uFEFF";
|
||||
let csv = "sep=,\r\n";
|
||||
csv += "Timestamp,Timestamp_ms,Window_ID,Peso_kg\r\n";
|
||||
for (const r of todasLasMuestras) {
|
||||
csv += [r[0], String(r[1]), String(r[2]), String(r[3])].join(',') + "\r\n";
|
||||
}
|
||||
const blob = new Blob([BOM + csv], { type: "text/csv;charset=utf-8;" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = `sesion_${ultimaSesionID}.csv`;
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
log(`📥 CSV exportado (${todasLasMuestras.length} filas)`);
|
||||
}
|
||||
|
||||
window.iniciarSesion = iniciarSesion;
|
||||
window.finalizarSesion = finalizarSesion;
|
||||
window.descargarCSV = descargarCSV;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue