From aa66d49d4f27cf336eb1fe51803a15b4cbe38ea8 Mon Sep 17 00:00:00 2001 From: h4ckx0r Date: Thu, 24 Apr 2025 01:18:40 +0200 Subject: [PATCH] Mejoras generales --- .gitignore | 2 + README.md | 124 +++++++++++++++++++++++++++++++++++++++++--------- csv2json.js | 69 ++++++++++++++++++++++++++++ libs/api.js | 12 ++--- libs/utils.js | 12 +++-- main.js | 21 ++++++++- package.json | 8 ++-- uploadcsv.js | 89 ++++++++++++++++++++++++++++++++++++ 8 files changed, 300 insertions(+), 37 deletions(-) create mode 100644 csv2json.js create mode 100644 uploadcsv.js diff --git a/.gitignore b/.gitignore index ceaea36..b8dbd02 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,5 @@ dist .yarn/install-state.gz .pnp.* +# MacOS Files +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index c07d725..3f5945e 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,119 @@ # CanSat -Este documento describe el proceso para ejecutar el código que gestionará la recepción de datos del CanSat. +Este proyecto se encarga de leer datos desde un dispositivo CanSat vía puerto serie, procesarlos y enviarlos a ThingSpeak para visualizarlos en un dashboard. -## Requisitos Previos +--- -Es indispensable contar con [Node.js](https://nodejs.org/) instalado en el equipo. Se recomienda descargar la versión LTS (Long Term Support) para garantizar la estabilidad del proyecto. +## 📋 Requisitos previos -## Verificación de la Instalación de Node.js +1. **Node.js**: + - Versión recomendada: LTS (Long Term Support). + - Descárgala de https://nodejs.org/ -Para confirmar que Node.js y npm se han instalado correctamente, abra una terminal y ejecute los siguientes comandos: +2. **Acceso al dispositivo CanSat** conectado al puerto USB de tu ordenador. -```bash -node -v -npm -v +--- + +## 🚀 Instalación + +1. Abre una terminal. +2. Clona o descarga el proyecto y accede a su carpeta: + ```bash + # El git es para clonar, pero puedes descargar el código en ZIP si no tienes Git + git clone https://git.h4ckdata.es/h4ckx0r/CanSat.git + cd CanSat + ``` +3. Instala las dependencias: + ```bash + npm install + ``` + +--- + +## ⚙️ Configuración + +Antes de ejecutar el proyecto, edita el archivo `config.js` y ajusta estos valores: + +```js +export default { + // Identificadores de tu canal ThingSpeak + thingSpeak: { + channelId: "", + writeKey: "" + }, + // Opciones del puerto serial donde está conectado el CanSat + serial: { + path: "/dev/ttyUSB0", // Cambia al puerto correcto de tu sistema + baudRate: 9600 // Ajusta si tu dispositivo usa otra velocidad + } +} ``` -Si los comandos no muestran la versión correspondiente, es posible que la instalación no haya finalizado correctamente. En ese caso, consulte las [instrucciones oficiales de Node.js](https://nodejs.org/) para completar la instalación. +- **thingSpeak.channelId**: ID del canal dónde se mostrarán los datos. +- **thingSpeak.writeKey**: Clave de escritura para enviar datos a ThingSpeak. +- **serial.path**: Ruta del dispositivo serie (por ejemplo `/dev/ttyUSB0` en Linux, `COM3` en Windows, o `/dev/tty.usbserial-XXXX` en macOS). +- **serial.baudRate**: Velocidad en baudios, normalmente `9600`. -## Preparación del Código +--- -1. Descargue el código del proyecto en formato ZIP desde el repositorio o, alternativamente, realice un clon mediante Git. -2. Una vez ubicado en la carpeta del proyecto, ejecute en la terminal el siguiente comando para instalar las dependencias: +## 📂 Estructura de archivos -```bash -npm install -``` +- **`main.js`** + Punto de entrada: + - Abre el puerto serie. + - Procesa cada línea recibida con `ReadlineParser`. + - Convierte la línea a objeto y llama a la función de envío. -## Ejecución del Código +- **`config.js`** + Contiene toda la configuración del proyecto (ThingSpeak y serial). -Para iniciar la ejecución del programa, utilice el siguiente comando en la terminal: +- **`utils.js`** (en `libs/`) + - `dataLineToObject(line)`: convierte una línea CSV (`;` separado) en un objeto con propiedades claras. + - Funciones de guardado en `logs/` (JSON, CSV y GeoJSON). -```bash -node main.js -``` +- **`api.js`** (en `libs/`) + - `subirDato(datos)`: prepara y acumula peticiones para ThingSpeak. + - Bucle que envía datos en bloques cada 20 segundos. -## Configuración +- **`package.json`** + - Define dependencias y scripts disponibles. -Antes de ejecutar la aplicación, asegúrese de configurar correctamente las opciones necesarias en el archivo `config.js`. \ No newline at end of file +--- + +## ▶️ Uso + +1. Asegúrate de haber configurado `config.js`. +2. En la terminal, ejecuta: + ```bash + npm start + ``` + Esto lanza `main.js` y comienza a leer y enviar datos. + +--- + +## 🔧 Scripts disponibles + +- **`npm start`** + Ejecuta el programa principal: `node main.js`. + + Si da error al comunicarse con el puerto serie, cierre el monitor serie de Arduino IDE o cierre completamente el Arduino IDE y compruebe que en la configuración ha escrito correctamente el puerto serial del equipo. + + En el caso de que necesite cancelar la ejecución del script, pulse `Ctrl + C` 2 veces seguidas. + +- **`npm run csv2json -- nombreArchivoCSVAConvertir.csv`** + Convierte un registro de los datos CSV a JSON y GeoJSON. + + Es importante que ejecute este comando en la raiz del proyecto y que el archivo CSV a convertir esté en la raiz del proyecto también. El script generará los dos archivos en la misma raiz con el mismo nombre, sólo cambiando la extensión. + + Y el CSV debe contener sólo los datos, no puede contener los títulos de las columnas. + +- **`npm run uploadcsv -- nombreArchivoCSVAConvertir.csv`** + Convierte el CSV de datos a un CSV listo para subir a ThingSpeak. + + El comando es igual al de convertir a JSON y GeoJSON, pero esta vez generará un CSV que se llama csvParaSubir.csv y este lo importamos en ThingSpeak. + + Es posible que ThingSpeak se pille, yo he tenido que recargar y borrar los datos del canal varias veces para que lo pille bien y pille todos los datos. Igualmente, esperad unos minutos a que cargue los datos. + +--- + +¡Y ya está! Con esto tendrás tu CanSat enviando datos en tiempo real a ThingSpeak. Si tienes dudas, consulta la documentación de [Node.js](https://nodejs.org/) o busca ayuda en la comunidad. \ No newline at end of file diff --git a/csv2json.js b/csv2json.js new file mode 100644 index 0000000..4bfb238 --- /dev/null +++ b/csv2json.js @@ -0,0 +1,69 @@ +// Importamos módulos necesarios +import fs from 'node:fs'; +import path from 'node:path'; +import readline from 'node:readline'; +import { dataLineToObject } from './libs/utils.js'; + +// Ruta del archivo CSV: se pasa como argumento o usa 'logs/datos.csv' por defecto +const inputPath = process.argv[2] || path.join('.', 'datos.csv'); + +// Arrays para acumular datos +const dataObjects = []; +const geoFeatures = []; + +// Creamos interfaz de lectura de líneas +const rl = readline.createInterface({ + input: fs.createReadStream(inputPath), + crlfDelay: Infinity +}); + +rl.on('line', (line) => { + const obj = dataLineToObject(line); + if (obj) { + dataObjects.push(obj); + // Solo añadimos puntos válidos + if (obj.latitud != null && obj.longitud != null) { + geoFeatures.push({ + type: 'Feature', + properties: { + paquete: obj.paquetes, + temperatura: obj.temperatura, + presion: obj.presion, + altitud: obj.altitudMetros, + velocidad: obj.velocidadKmph, + numSatelites: obj.numSatelites, + datetimeGPS: obj.datetimeGPS + }, + geometry: { + type: 'Point', + coordinates: [obj.longitud, obj.latitud] + } + }); + } + } +}); + +rl.on('close', () => { + // Escribimos JSON + const jsonPath = path.join(path.dirname(inputPath), 'datos.json'); + fs.writeFileSync(jsonPath, JSON.stringify(dataObjects, null, 2), 'utf8'); + console.log(`Archivo JSON creado en ${jsonPath}`); + + // Escribimos GeoJSON + const geojson = { + type: 'FeatureCollection', + features: geoFeatures + }; + + geojson.features.push({ + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: geoFeatures.map(feature => feature.geometry.coordinates) + } + }); + const geoPath = path.join(path.dirname(inputPath), 'datos.geojson'); + fs.writeFileSync(geoPath, JSON.stringify(geojson, null, 2), 'utf8'); + console.log(`Archivo GeoJSON creado en ${geoPath}`); +}); diff --git a/libs/api.js b/libs/api.js index 7fec503..8ed79a7 100644 --- a/libs/api.js +++ b/libs/api.js @@ -15,7 +15,7 @@ async function subirDato(datos) { } const datosParaEnviar = { created_at: datos.datetime, - delta_t: 1, + //delta_t: 1, // Activar si da problemas con el tiempo field1: datos.paquetes, field2: datos.temperatura, field3: datos.presion, @@ -27,11 +27,6 @@ async function subirDato(datos) { longitude: datos.longitud }; - //console.log(datos); - //console.log(datosParaEnviar); - - // Guardar datos en archivo: https://nodejs.org/en/learn/manipulating-files/writing-files-with-nodejs - peticionesRetrasadas.push(datosParaEnviar); saveDataInFile(datos); } @@ -69,7 +64,7 @@ async function subirDatosDeGolpe(datosParaSubir) { }) .then(response => response.json()) .then(data => { - //console.log(data); + console.log(data); resultado = true; // Si todo sale bien, devolvemos true }) .catch(error => { @@ -100,7 +95,8 @@ async function bucleSubidaDatos() { } export { - subirDato + subirDato, + subirDatosDeGolpe } bucleSubidaDatos(); diff --git a/libs/utils.js b/libs/utils.js index b98726b..df8edad 100644 --- a/libs/utils.js +++ b/libs/utils.js @@ -14,7 +14,8 @@ if (!fs.existsSync('logs')) { function dataLineToObject(line) { //Separamos nuestra línea en diferentes trozos usando como separador el ';' const lineArray = line.split(";"); - console.log(line); + + console.log("Línea de datos: " + line); saveDataInCSVFile(line); //Devolvemos un objeto con sus propiedades convertidas @@ -39,7 +40,8 @@ function dataLineToObject(line) { datetime: new Date().toISOString() // Obtenemos la fecha/hora actual }; } catch (error) { - console.error('Error al convertir los datos:', error); + console.warn('No se ha podido convertir la línea de datos, es posible que se hayan perdido parte de los datos, por lo que esta línea será ignorada'); + //console.error('Error al convertir los datos:', error); return null; } } @@ -98,7 +100,11 @@ async function saveDataInGeoJSONFile(data) { type: "Feature", properties: { numPaquete: data.paquetes, - temperatura: data.temperatura + temperatura: data.temperatura, + altitudSegunPresion: data.altitudSegunPresion, + altitudMetros: data.altitudMetros, + velocidadKmph: data.velocidadKmph, + numSatelites: data.numSatelites, }, geometry: { type: "Point", diff --git a/main.js b/main.js index 6966e59..431f46e 100644 --- a/main.js +++ b/main.js @@ -15,12 +15,25 @@ import config from './config.js'; const port = new SerialPort({ path: config.serial.path, baudRate: config.serial.baudRate, - autoOpen: true + autoOpen: false }); // Le establecemos que procese los datos recibidos con el parser de readline y le decimos que diferencie las líneas por saltos de línea const parser = port.pipe(new ReadlineParser({ delimiter: '\n' })); +// Función para (re)conectar al puerto serie con reintentos cada 1 segundo +function connectPort() { + port.open(err => { + if (err) { + console.error(`Error al conectar al puerto ${config.serial.path}:`, err.message); + setTimeout(connectPort, 1000); + } + }); +} + +// Iniciar primer intento de conexión +connectPort(); + // Esperamos a que se abra el puerto y cuando lo haga ejecutamos un console log para indicar que se ha abierto correctamente port.on('open', () => { console.log(`Puerto ${config.serial.path} conectado correctamente.`); @@ -35,4 +48,10 @@ parser.on('data', async (data) => { // Si hay un error en el puerto, lo mostramos en la consola port.on('error', (err) => { console.error('Error en el puerto:', err.message); +}); + +// Si el puerto se cierra, reintentamos después de 1 segundo +port.on('close', () => { + console.warn(`Puerto ${config.serial.path} cerrado. Reintentando en 1 segundo...`); + setTimeout(connectPort, 1000); }); \ No newline at end of file diff --git a/package.json b/package.json index b107921..08e7b95 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,15 @@ { "name": "cansat", - "version": "0.0.1", + "version": "0.1.0", "description": "Programa que recibe los datos por serial y los manda a ThingSpeak para mostrarlos luego en un dashboard.", "license": "MIT", - "author": "", + "author": "h4ckx0r", "type": "module", "main": "main.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "csv2json": "node csv2json.js", + "uploadcsv": "node uploadcsv.js", + "start": "node main.js" }, "dependencies": { "@serialport/parser-readline": "^13.0.0", diff --git a/uploadcsv.js b/uploadcsv.js new file mode 100644 index 0000000..3ca0188 --- /dev/null +++ b/uploadcsv.js @@ -0,0 +1,89 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import readline from 'node:readline'; +import { dataLineToObject } from './libs/utils.js'; + +// Ruta del CSV de salida: tercer argumento o 'csvParaSubir.csv' por defecto +const outputPath = process.argv[3] || path.join('.', 'csvParaSubir.csv'); +const output = fs.createWriteStream(outputPath); +// Timestamp de la línea anterior para evitar duplicados +let prevTimestamps = []; +// Encabezados para el CSV de salida +output.write('created_at,entry_id,field1,field2,field3,field4,field5,field6,field7,latitude,longitude,elevation,status\n'); + +// Ruta del CSV: primer argumento o 'logs/datos.csv' por defecto +const inputPath = process.argv[2] || path.join('.', 'datos.csv'); + +// Comprobamos que el archivo existe +if (!fs.existsSync(inputPath)) { + console.error(`Archivo no encontrado: ${inputPath}`); + process.exit(1); +} +// Creamos interfaz de lectura de líneas +const rl = readline.createInterface({ + input: fs.createReadStream(inputPath), + crlfDelay: Infinity +}); + +rl.on('line', async (line) => { + const obj = dataLineToObject(line); + if (obj) { + try { + if (obj === null || obj === undefined) { + return; + } + const datosParaEnviar = { + created_at: obj.datetimeGPS.toISOString(), + delta_t: 1, // Activar si da problemas con el tiempo + field1: obj.paquetes, + field2: obj.temperatura, + field3: obj.presion, + field4: obj.altitudSegunPresion, + field5: obj.altitudMetros, + field6: obj.velocidadKmph, + field7: obj.numSatelites, + latitude: obj.latitud, + longitude: obj.longitud + }; + // Ajustar created_at si coincide con alguno de los últimos 10 timestamps + let currentDate = new Date(datosParaEnviar.created_at); + // Mientras choque con alguno de los últimos 10, suma un segundo + while (prevTimestamps.some(ts => ts.getTime() === currentDate.getTime())) { + currentDate.setSeconds(currentDate.getSeconds() + 1); + } + datosParaEnviar.created_at = currentDate.toISOString(); + // Actualizar lista y mantener solo 10 elementos + prevTimestamps.push(new Date(datosParaEnviar.created_at)); + if (prevTimestamps.length > 10) { + prevTimestamps.shift(); + } + // Escribir una línea en el CSV de salida + const entryId = obj.paquetes; + const csvLine = [ + datosParaEnviar.created_at, + entryId, + datosParaEnviar.field1, + datosParaEnviar.field2, + datosParaEnviar.field3, + datosParaEnviar.field4, + datosParaEnviar.field5, + datosParaEnviar.field6, + datosParaEnviar.field7, + datosParaEnviar.latitude, + datosParaEnviar.longitude, + '', // elevation + '' // status + ].join(',') + '\n'; + output.write(csvLine); + //console.log(`Enviado paquete ${obj.paquetes}`); + } catch (err) { + console.error('Error al subir dato:', err); + } + } +}); + +rl.on('close', () => { + output.end(); + console.log(`CSV listo para subir a thingspeak: ${outputPath}`); + process.exit(0); +});