mirror of
https://github.com/adrigongv23/G26---Telemetry-Software.git
synced 2026-08-25 03:23:17 +02:00
Compare commits
2 commits
72dc48807d
...
ab5bebe30e
| Author | SHA1 | Date | |
|---|---|---|---|
| ab5bebe30e | |||
| ad23f59ba4 |
6 changed files with 49 additions and 283 deletions
|
|
@ -6,9 +6,15 @@
|
|||
#include "include/data_processor.hpp"
|
||||
|
||||
|
||||
// --- CONFIGURACIÓN SD (VSPI) ---
|
||||
#define SD_CS_PIN 5
|
||||
#define SPI_CLOCK SD_SCK_MHZ(20)
|
||||
// --- CONFIGURACIÓN SD (HSPI, pines de la PCB validados por esquemático) ---
|
||||
#define SD_CS 25
|
||||
#define SD_MOSI 26
|
||||
#define SD_SCK 27
|
||||
#define SD_MISO 14
|
||||
|
||||
SPIClass spiSD(HSPI);
|
||||
#define SD_CONFIG SdSpiConfig(SD_CS, DEDICATED_SPI, SD_SCK_MHZ(1), &spiSD)
|
||||
|
||||
SdFat sd;
|
||||
SdFile logFile;
|
||||
|
||||
|
|
@ -22,38 +28,44 @@ void setup() {
|
|||
Serial.println("\n--- G26 TELEMETRY: INICIO DE SISTEMA ---");
|
||||
|
||||
// 1. INICIALIZACIÓN SD
|
||||
if (!sd.begin(SD_CS_PIN, SPI_CLOCK)) {
|
||||
spiSD.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
|
||||
|
||||
if (!sd.begin(SD_CONFIG)) {
|
||||
Serial.println("[FALLO] SD no detectada. El sistema continuará sin Datalogging.");
|
||||
} else {
|
||||
if (logFile.open("G26.csv", O_RDWR | O_CREAT | O_APPEND)) {
|
||||
char filename[24];
|
||||
int session = 1;
|
||||
bool opened = false;
|
||||
|
||||
// Si el archivo es nuevo (tamaño 0), escribimos la cabecera
|
||||
if (logFile.fileSize() == 0) {
|
||||
logFile.println("Time,ECT");
|
||||
logFile.sync();
|
||||
while (!opened && session < 100000) {
|
||||
snprintf(filename, sizeof(filename), "G26-%d.csv", session);
|
||||
if (logFile.open(filename, O_RDWR | O_CREAT | O_EXCL)) {
|
||||
opened = true;
|
||||
Serial.printf("[OK] Nueva sesion: %s\n", filename);
|
||||
} else {
|
||||
session++;
|
||||
}
|
||||
}
|
||||
|
||||
// El archivo se queda abierto y listo.
|
||||
Serial.println("[OK] Archivo abierto");
|
||||
|
||||
} else {
|
||||
Serial.println("[ERROR] No se pudo abrir el archivo G26.csv");
|
||||
}
|
||||
if (opened) {
|
||||
logFile.println("Time,ECT,RPM,TPS,VBATT");
|
||||
logFile.sync();
|
||||
} else {
|
||||
Serial.println("[ERROR] No se pudo abrir archivo de sesion");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. VINCULACIÓN
|
||||
// Le damos al procesador acceso a la SD para que guarde cuando lleguen datos
|
||||
processor.setLogSystem(&sd, &logFile);
|
||||
can_interface.set_data_proccessor(&processor);
|
||||
|
||||
// 3. INICIO CAN
|
||||
can_interface.start();
|
||||
can_interface.start_listening_task(); // Escucha en Core 1 (Background)
|
||||
|
||||
can_interface.start_listening_task();
|
||||
|
||||
Serial.println("[OK] Sistema ONLINE.");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
|
||||
delay(100);
|
||||
}
|
||||
delay(100);
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
#ifndef CAN_HPP
|
||||
#define CAN_HPP
|
||||
|
||||
#define RX_PIN 13
|
||||
#define TX_PIN 38
|
||||
#define RX_PIN 23
|
||||
#define TX_PIN 22
|
||||
#define POLLING_RATE_MS 1000
|
||||
#define TRANSMIT_RATE_MS 1000
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ public:
|
|||
|
||||
// Estructura de datos
|
||||
struct CarState {
|
||||
int ect = 0; // Temperatura (IMPORTANTE POR AHORA). Luego habría que añadir aquí todas las variables que se vallan a guardar.
|
||||
int ect = 0;
|
||||
int rpm = 0;
|
||||
int tps = 0;
|
||||
double vbatt = 0;
|
||||
|
||||
};
|
||||
|
||||
// Configuración SD
|
||||
|
|
|
|||
254
index.html
254
index.html
|
|
@ -1,254 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -11,7 +11,7 @@ static bool driver_installed = false;
|
|||
void CAN::start() {
|
||||
Serial.println("Starting CAN Controller...");
|
||||
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t)TX_PIN, (gpio_num_t)RX_PIN, TWAI_MODE_NORMAL);
|
||||
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_125KBITS();
|
||||
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();
|
||||
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
|
||||
|
||||
esp_err_t install_status = twai_driver_install(&g_config, &t_config, &f_config);
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@ void DataProcessor::send_serial_frame_0(int rpmh, int rpml, int tpsh, int tpsl,
|
|||
|
||||
car.ect = ect; // GUARDAR LA VARIABLE DEL FRAME EN LA ESTRUCTURA
|
||||
|
||||
// El resto de variables no son relevantes ahora, se quedan comentadas para ahorrar memoria
|
||||
// int rpm = (rpmh * 256) + rpml;
|
||||
// int tps = (tpsh * 256) + tpsl;
|
||||
// double vbatt = ((vbatth * 256) + vbattl) / 100.0;
|
||||
int rpm = (rpmh * 256) + rpml;
|
||||
car.rpm = rpm;
|
||||
|
||||
int tps = (tpsh * 256) + tpsl;
|
||||
car.tps = tps;
|
||||
|
||||
double vbatt = ((vbatth * 256) + vbattl) / 100.0;
|
||||
car.vbatt = vbatt;
|
||||
|
||||
// 2. Escribimos en la SD
|
||||
flushToSD();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue