Compare commits
9 Commits
661c1a69af
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| aa66d49d4f | |||
| adb0be24a6 | |||
| 63aae4a629 | |||
| 53217b6bbd | |||
| 67bb5fe954 | |||
| 6bbb4c671d | |||
| cdfd9ed359 | |||
| e130b84d0b | |||
| 58b7e6ebae |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -130,3 +130,5 @@ dist
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# MacOS Files
|
||||
.DS_Store
|
||||
117
README.md
117
README.md
@@ -1,2 +1,119 @@
|
||||
# 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
|
||||
|
||||
1. **Node.js**:
|
||||
- Versión recomendada: LTS (Long Term Support).
|
||||
- Descárgala de https://nodejs.org/
|
||||
|
||||
2. **Acceso al dispositivo CanSat** conectado al puerto USB de tu ordenador.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **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`.
|
||||
|
||||
---
|
||||
|
||||
## 📂 Estructura de archivos
|
||||
|
||||
- **`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.
|
||||
|
||||
- **`config.js`**
|
||||
Contiene toda la configuración del proyecto (ThingSpeak y serial).
|
||||
|
||||
- **`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).
|
||||
|
||||
- **`api.js`** (en `libs/`)
|
||||
- `subirDato(datos)`: prepara y acumula peticiones para ThingSpeak.
|
||||
- Bucle que envía datos en bloques cada 20 segundos.
|
||||
|
||||
- **`package.json`**
|
||||
- Define dependencias y scripts disponibles.
|
||||
|
||||
---
|
||||
|
||||
## ▶️ 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.
|
||||
16
config.js
Normal file
16
config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
// Configuración de ThingSpeak
|
||||
"thingSpeak": {
|
||||
"channelId": "",
|
||||
"writeKey": ""
|
||||
},
|
||||
// Configuración del CanSat
|
||||
"canSat": {
|
||||
|
||||
},
|
||||
// Configuración del Serial
|
||||
"serial": {
|
||||
"path": "/dev/tty.usbserial-0001",
|
||||
"baudRate": 9600
|
||||
}
|
||||
}
|
||||
69
csv2json.js
Normal file
69
csv2json.js
Normal 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}`);
|
||||
});
|
||||
104
libs/api.js
Normal file
104
libs/api.js
Normal file
@@ -0,0 +1,104 @@
|
||||
// Importamos las utilidades que necesitemos y la configuración del programa
|
||||
import { sleep, saveDataInFile } from './utils.js';
|
||||
import config from '../config.js';
|
||||
|
||||
// Creamos una lista de elementos que contendrá las peticiones acumuladas para enviarlas de golpe
|
||||
const peticionesRetrasadas = [];
|
||||
|
||||
/**
|
||||
* Añade el dato a la lista de peticiones en el formato que necesita la api
|
||||
* @param {object} datos Objeto con los datos a subir
|
||||
*/
|
||||
async function subirDato(datos) {
|
||||
if (datos === null || datos === undefined) {
|
||||
return;
|
||||
}
|
||||
const datosParaEnviar = {
|
||||
created_at: datos.datetime,
|
||||
//delta_t: 1, // Activar si da problemas con el tiempo
|
||||
field1: datos.paquetes,
|
||||
field2: datos.temperatura,
|
||||
field3: datos.presion,
|
||||
field4: datos.altitudSegunPresion,
|
||||
field5: datos.altitudMetros,
|
||||
field6: datos.velocidadKmph,
|
||||
field7: datos.numSatelites,
|
||||
latitude: datos.latitud,
|
||||
longitude: datos.longitud
|
||||
};
|
||||
|
||||
peticionesRetrasadas.push(datosParaEnviar);
|
||||
saveDataInFile(datos);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sube los datos de golpe al ThingSpeak
|
||||
* @param {Array} datosParaSubir Array de objetos con los datos a subir
|
||||
* @returns {Promise<boolean>} True si se subieron los datos correctamente, false si no
|
||||
*/
|
||||
async function subirDatosDeGolpe(datosParaSubir) {
|
||||
//console.log("Subiendo datos de golpe...");
|
||||
|
||||
// Si no hay datos, no subimos nada
|
||||
if(datosParaSubir.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Preparamos los datos para subir añadiendo la apiKey
|
||||
const datosParaEnviar = {
|
||||
write_api_key: config.thingSpeak.writeKey,
|
||||
updates: datosParaSubir
|
||||
}
|
||||
|
||||
//console.log(datosParaEnviar);
|
||||
|
||||
// Hacemos la petición a la API de ThingSpeak
|
||||
let resultado = false;
|
||||
await fetch(`https://api.thingspeak.com/channels/${config.thingSpeak.channelId}/bulk_update.json`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(datosParaEnviar) // Convertimos el objeto a JSON para enviarlo
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
resultado = true; // Si todo sale bien, devolvemos true
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error al subir los datos:', error);
|
||||
resultado = false; // Si algo sale mal, devolvemos false
|
||||
})
|
||||
return resultado;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bucle infinito que sube los datos de golpe al ThingSpeak cada 20 segundos
|
||||
*/
|
||||
async function bucleSubidaDatos() {
|
||||
// Creamos bucle infinito
|
||||
while(true) {
|
||||
await sleep(20000); // Esperamos 20 segundos
|
||||
// console.log("Lista por subir: " + peticionesRetrasadas.length);
|
||||
const datosQueSeVanASubir = JSON.parse(JSON.stringify(peticionesRetrasadas))// Creamos una copia de los datos del array
|
||||
if (await subirDatosDeGolpe(datosQueSeVanASubir)) { // Subimos los datos de golpe
|
||||
if (peticionesRetrasadas.length == datosQueSeVanASubir.length) { // Si la cantidad de datos subida es diferente a la cantidad de datos en el array reiniciamos el array
|
||||
peticionesRetrasadas.length = 0;
|
||||
} else { // Puede darse el caso que mientras se suben los datos, lleguen nuevos y los eliminemos sin querer, por eso, sólo eliminamos de la lista original los datos que se han subido
|
||||
peticionesRetrasadas.splice(0, datosQueSeVanASubir.length);
|
||||
//console.log("Datos que se hubieran perdido: " + peticionesRetrasadas)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
subirDato,
|
||||
subirDatosDeGolpe
|
||||
}
|
||||
|
||||
bucleSubidaDatos();
|
||||
|
||||
|
||||
153
libs/utils.js
Normal file
153
libs/utils.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// Aseguramos que el directorio de logs exista en cualquier sistema operativo
|
||||
if (!fs.existsSync('logs')) {
|
||||
fs.mkdirSync('logs', { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte una línea de datos en un objeto
|
||||
* @param {string} line Línea de datos para convertir a objeto
|
||||
* @returns {object} Objeto con los datos convertidos
|
||||
*/
|
||||
function dataLineToObject(line) {
|
||||
//Separamos nuestra línea en diferentes trozos usando como separador el ';'
|
||||
const lineArray = line.split(";");
|
||||
|
||||
console.log("Línea de datos: " + line);
|
||||
saveDataInCSVFile(line);
|
||||
|
||||
//Devolvemos un objeto con sus propiedades convertidas
|
||||
|
||||
//Usamos el trim() para eliminar los espacios delante y detrás de los valores
|
||||
//Usamos el parseFloat para convertir esos valores a decimales, ya que son cadenas de texto
|
||||
//Usamos el parseInt para convertir esos valores a enteros, ya que son cadenas de texto
|
||||
try {
|
||||
return {
|
||||
paquetes: parseInt(lineArray[0].trim()),
|
||||
temperatura: parseFloat(lineArray[1].trim()),
|
||||
presion: parseFloat(lineArray[2].trim()),
|
||||
altitudSegunPresion: parseFloat(lineArray[3].trim()),
|
||||
nombreEstacion: lineArray[4].trim(),
|
||||
latitud: parseFloat(lineArray[5].trim()),
|
||||
longitud: parseFloat(lineArray[6].trim()),
|
||||
altitudMetros: parseFloat(lineArray[7].trim()),
|
||||
velocidadKmph: parseFloat(lineArray[8].trim()),
|
||||
direccionNorte: parseFloat(lineArray[9].trim()),
|
||||
numSatelites: parseInt(lineArray[10].trim()),
|
||||
datetimeGPS: new Date(lineArray[11].trim().replace("Date/Time: ", "")),
|
||||
datetime: new Date().toISOString() // Obtenemos la fecha/hora actual
|
||||
};
|
||||
} catch (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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Espera un cierto tiempo, esto sirve para crear temporizadores
|
||||
* @param {number} ms Milisegundos que hay que esperar
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const fechaHoraActual = new Date();
|
||||
async function saveDataInFile(data) {
|
||||
fs.writeFile(
|
||||
path.join('logs', `${fechaHoraActual.toISOString().replace(/:/g, '-')}.json`),
|
||||
JSON.stringify(data) + ',\n',
|
||||
{ flag: 'a' },
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
} else {
|
||||
//console.log('Datos guardados correctamente');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
saveDataInGeoJSONFile(data);
|
||||
}
|
||||
|
||||
var geoJSONData = {
|
||||
type: "FeatureCollection",
|
||||
features: [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"coordinates": [],
|
||||
"type": "LineString"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async function saveDataInGeoJSONFile(data) {
|
||||
|
||||
if (data.latitud === null || data.longitud === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
geoJSONData.features.push({
|
||||
type: "Feature",
|
||||
properties: {
|
||||
numPaquete: data.paquetes,
|
||||
temperatura: data.temperatura,
|
||||
altitudSegunPresion: data.altitudSegunPresion,
|
||||
altitudMetros: data.altitudMetros,
|
||||
velocidadKmph: data.velocidadKmph,
|
||||
numSatelites: data.numSatelites,
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [data.longitud, data.latitud]
|
||||
}
|
||||
});
|
||||
|
||||
geoJSONData.features[0].geometry.coordinates.push([data.longitud, data.latitud]);
|
||||
|
||||
|
||||
fs.writeFile(
|
||||
path.join('logs', `${fechaHoraActual.toISOString().replace(/:/g, '-')}.geojson`),
|
||||
JSON.stringify(geoJSONData),
|
||||
{ flag: 'w+' },
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
} else {
|
||||
// file written successfully
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
saveDataInCSVFile("Nº Paquete; Temperatura; Presión; Altitud según presión; Latitud; Longitud; Altitud (m); Velocidad (Km/h); Dirección/Norte; Número de Satelites; Fecha/Hora GPS; Fecha/Hora del Sistema");
|
||||
async function saveDataInCSVFile(data) {
|
||||
fs.writeFile(
|
||||
path.join('logs', `${fechaHoraActual.toISOString().replace(/:/g, '-')}.csv`),
|
||||
data + '\n',
|
||||
{ flag: 'a' },
|
||||
err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
} else {
|
||||
//console.log('Datos guardados correctamente');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Exportamos las funciones para poder usarlas en otras partes del programa
|
||||
export {
|
||||
dataLineToObject,
|
||||
sleep,
|
||||
saveDataInFile
|
||||
}
|
||||
57
main.js
Normal file
57
main.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// Importamos las librerias necesarias
|
||||
import { SerialPort } from 'serialport';
|
||||
import { ReadlineParser } from '@serialport/parser-readline';
|
||||
|
||||
// Importamos las funciones de utilidad que vayamos a usar
|
||||
import { dataLineToObject } from './libs/utils.js';
|
||||
|
||||
// Importamos las funciones de la API
|
||||
import { subirDato } from './libs/api.js';
|
||||
|
||||
// Importamos toda la configuración
|
||||
import config from './config.js';
|
||||
|
||||
// Creamos una conexión con el puerto serial
|
||||
const port = new SerialPort({
|
||||
path: config.serial.path,
|
||||
baudRate: config.serial.baudRate,
|
||||
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.`);
|
||||
});
|
||||
|
||||
// Cada vez que recibamos un dato, le decimos al programa que lo procese sin bloquear la ejecución principal
|
||||
parser.on('data', async (data) => {
|
||||
// Convertimos la línea recibida en un objeto y lo mandamos a la api para que se encargue de la subida
|
||||
subirDato(dataLineToObject(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);
|
||||
});
|
||||
328
package-lock.json
generated
Normal file
328
package-lock.json
generated
Normal file
@@ -0,0 +1,328 @@
|
||||
{
|
||||
"name": "cansat",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cansat",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/parser-readline": "^13.0.0",
|
||||
"path": "^0.12.7",
|
||||
"serialport": "^13.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/binding-mock": {
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz",
|
||||
"integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/bindings-interface": "^1.2.1",
|
||||
"debug": "^4.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-13.0.0.tgz",
|
||||
"integrity": "sha512-r25o4Bk/vaO1LyUfY/ulR6hCg/aWiN6Wo2ljVlb4Pj5bqWGcSRC4Vse4a9AcapuAu/FeBzHCbKMvRQeCuKjzIQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/bindings-interface": "1.2.2",
|
||||
"@serialport/parser-readline": "12.0.0",
|
||||
"debug": "4.4.0",
|
||||
"node-addon-api": "8.3.0",
|
||||
"node-gyp-build": "4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz",
|
||||
"integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz",
|
||||
"integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/parser-delimiter": "12.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-interface": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz",
|
||||
"integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.22 || ^14.13 || >=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-byte-length": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-13.0.0.tgz",
|
||||
"integrity": "sha512-32yvqeTAqJzAEtX5zCrN1Mej56GJ5h/cVFsCDPbF9S1ZSC9FWjOqNAgtByseHfFTSTs/4ZBQZZcZBpolt8sUng==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-cctalk": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-13.0.0.tgz",
|
||||
"integrity": "sha512-RErAe57g9gvnlieVYGIn1xymb1bzNXb2QtUQd14FpmbQQYlcrmuRnJwKa1BgTCujoCkhtaTtgHlbBWOxm8U2uA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-delimiter": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-13.0.0.tgz",
|
||||
"integrity": "sha512-Qqyb0FX1avs3XabQqNaZSivyVbl/yl0jywImp7ePvfZKLwx7jBZjvL+Hawt9wIG6tfq6zbFM24vzCCK7REMUig==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-inter-byte-timeout": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-13.0.0.tgz",
|
||||
"integrity": "sha512-a0w0WecTW7bD2YHWrpTz1uyiWA2fDNym0kjmPeNSwZ2XCP+JbirZt31l43m2ey6qXItTYVuQBthm75sPVeHnGA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-packet-length": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-13.0.0.tgz",
|
||||
"integrity": "sha512-60ZDDIqYRi0Xs2SPZUo4Jr5LLIjtb+rvzPKMJCohrO6tAqSDponcNpcB1O4W21mKTxYjqInSz+eMrtk0LLfZIg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-readline": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-13.0.0.tgz",
|
||||
"integrity": "sha512-dov3zYoyf0dt1Sudd1q42VVYQ4WlliF0MYvAMA3MOyiU1IeG4hl0J6buBA2w4gl3DOCC05tGgLDN/3yIL81gsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/parser-delimiter": "13.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-ready": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-13.0.0.tgz",
|
||||
"integrity": "sha512-JNUQA+y2Rfs4bU+cGYNqOPnNMAcayhhW+XJZihSLQXOHcZsFnOa2F9YtMg9VXRWIcnHldHYtisp62Etjlw24bw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-regex": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-13.0.0.tgz",
|
||||
"integrity": "sha512-m7HpIf56G5XcuDdA3DB34Z0pJiwxNRakThEHjSa4mG05OnWYv0IG8l2oUyYfuGMowQWaVnQ+8r+brlPxGVH+eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-slip-encoder": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-13.0.0.tgz",
|
||||
"integrity": "sha512-fUHZEExm6izJ7rg0A1yjXwu4sOzeBkPAjDZPfb+XQoqgtKAk+s+HfICiYn7N2QU9gyaeCO8VKgWwi+b/DowYOg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-spacepacket": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-13.0.0.tgz",
|
||||
"integrity": "sha512-DoXJ3mFYmyD8X/8931agJvrBPxqTaYDsPoly9/cwQSeh/q4EjQND9ySXBxpWz5WcpyCU4jOuusqCSAPsbB30Eg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/stream": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-13.0.0.tgz",
|
||||
"integrity": "sha512-F7xLJKsjGo2WuEWMSEO1SimRcOA+WtWICsY13r0ahx8s2SecPQH06338g28OT7cW7uRXI7oEQAk62qh5gHJW3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/bindings-interface": "1.2.2",
|
||||
"debug": "4.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz",
|
||||
"integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/path": {
|
||||
"version": "0.12.7",
|
||||
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
|
||||
"integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"process": "^0.11.1",
|
||||
"util": "^0.10.3"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serialport": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz",
|
||||
"integrity": "sha512-PHpnTd8isMGPfFTZNCzOZp9m4mAJSNWle9Jxu6BPTcWq7YXl5qN7tp8Sgn0h+WIGcD6JFz5QDgixC2s4VW7vzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/binding-mock": "10.2.2",
|
||||
"@serialport/bindings-cpp": "13.0.0",
|
||||
"@serialport/parser-byte-length": "13.0.0",
|
||||
"@serialport/parser-cctalk": "13.0.0",
|
||||
"@serialport/parser-delimiter": "13.0.0",
|
||||
"@serialport/parser-inter-byte-timeout": "13.0.0",
|
||||
"@serialport/parser-packet-length": "13.0.0",
|
||||
"@serialport/parser-readline": "13.0.0",
|
||||
"@serialport/parser-ready": "13.0.0",
|
||||
"@serialport/parser-regex": "13.0.0",
|
||||
"@serialport/parser-slip-encoder": "13.0.0",
|
||||
"@serialport/parser-spacepacket": "13.0.0",
|
||||
"@serialport/stream": "13.0.0",
|
||||
"debug": "4.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/util": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
|
||||
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "2.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
package.json
Normal file
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "cansat",
|
||||
"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": "h4ckx0r",
|
||||
"type": "module",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"csv2json": "node csv2json.js",
|
||||
"uploadcsv": "node uploadcsv.js",
|
||||
"start": "node main.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@serialport/parser-readline": "^13.0.0",
|
||||
"path": "^0.12.7",
|
||||
"serialport": "^13.0.0"
|
||||
}
|
||||
}
|
||||
89
uploadcsv.js
Normal file
89
uploadcsv.js
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user