Mejoras generales

This commit is contained in:
2025-04-24 01:18:40 +02:00
parent adb0be24a6
commit aa66d49d4f
8 changed files with 300 additions and 37 deletions

2
.gitignore vendored
View File

@@ -130,3 +130,5 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
# MacOS Files
.DS_Store

124
README.md
View File

@@ -1,39 +1,119 @@
# CanSat # 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: "<TU_CHANNEL_ID>",
writeKey: "<TU_WRITE_KEY>"
},
// 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. ## 📂 Estructura de archivos
2. Una vez ubicado en la carpeta del proyecto, ejecute en la terminal el siguiente comando para instalar las dependencias:
```bash - **`main.js`**
npm install 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 - **`api.js`** (en `libs/`)
node main.js - `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`. ---
## ▶️ 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.

69
csv2json.js Normal file
View File

@@ -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}`);
});

View File

@@ -15,7 +15,7 @@ async function subirDato(datos) {
} }
const datosParaEnviar = { const datosParaEnviar = {
created_at: datos.datetime, created_at: datos.datetime,
delta_t: 1, //delta_t: 1, // Activar si da problemas con el tiempo
field1: datos.paquetes, field1: datos.paquetes,
field2: datos.temperatura, field2: datos.temperatura,
field3: datos.presion, field3: datos.presion,
@@ -27,11 +27,6 @@ async function subirDato(datos) {
longitude: datos.longitud 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); peticionesRetrasadas.push(datosParaEnviar);
saveDataInFile(datos); saveDataInFile(datos);
} }
@@ -69,7 +64,7 @@ async function subirDatosDeGolpe(datosParaSubir) {
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
//console.log(data); console.log(data);
resultado = true; // Si todo sale bien, devolvemos true resultado = true; // Si todo sale bien, devolvemos true
}) })
.catch(error => { .catch(error => {
@@ -100,7 +95,8 @@ async function bucleSubidaDatos() {
} }
export { export {
subirDato subirDato,
subirDatosDeGolpe
} }
bucleSubidaDatos(); bucleSubidaDatos();

View File

@@ -14,7 +14,8 @@ if (!fs.existsSync('logs')) {
function dataLineToObject(line) { function dataLineToObject(line) {
//Separamos nuestra línea en diferentes trozos usando como separador el ';' //Separamos nuestra línea en diferentes trozos usando como separador el ';'
const lineArray = line.split(";"); const lineArray = line.split(";");
console.log(line);
console.log("Línea de datos: " + line);
saveDataInCSVFile(line); saveDataInCSVFile(line);
//Devolvemos un objeto con sus propiedades convertidas //Devolvemos un objeto con sus propiedades convertidas
@@ -39,7 +40,8 @@ function dataLineToObject(line) {
datetime: new Date().toISOString() // Obtenemos la fecha/hora actual datetime: new Date().toISOString() // Obtenemos la fecha/hora actual
}; };
} catch (error) { } 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; return null;
} }
} }
@@ -98,7 +100,11 @@ async function saveDataInGeoJSONFile(data) {
type: "Feature", type: "Feature",
properties: { properties: {
numPaquete: data.paquetes, numPaquete: data.paquetes,
temperatura: data.temperatura temperatura: data.temperatura,
altitudSegunPresion: data.altitudSegunPresion,
altitudMetros: data.altitudMetros,
velocidadKmph: data.velocidadKmph,
numSatelites: data.numSatelites,
}, },
geometry: { geometry: {
type: "Point", type: "Point",

21
main.js
View File

@@ -15,12 +15,25 @@ import config from './config.js';
const port = new SerialPort({ const port = new SerialPort({
path: config.serial.path, path: config.serial.path,
baudRate: config.serial.baudRate, 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 // 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' })); 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 // 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', () => { port.on('open', () => {
console.log(`Puerto ${config.serial.path} conectado correctamente.`); console.log(`Puerto ${config.serial.path} conectado correctamente.`);
@@ -36,3 +49,9 @@ parser.on('data', async (data) => {
port.on('error', (err) => { port.on('error', (err) => {
console.error('Error en el puerto:', err.message); 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);
});

View File

@@ -1,13 +1,15 @@
{ {
"name": "cansat", "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.", "description": "Programa que recibe los datos por serial y los manda a ThingSpeak para mostrarlos luego en un dashboard.",
"license": "MIT", "license": "MIT",
"author": "", "author": "h4ckx0r",
"type": "module", "type": "module",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "csv2json": "node csv2json.js",
"uploadcsv": "node uploadcsv.js",
"start": "node main.js"
}, },
"dependencies": { "dependencies": {
"@serialport/parser-readline": "^13.0.0", "@serialport/parser-readline": "^13.0.0",

89
uploadcsv.js Normal file
View File

@@ -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);
});