
BermejaSat #21: Simulación de Lanzamiento en Cohete, Apogeo, Descenso 3D y Visualización en Google Earth
En las competiciones oficiales de CanSat organizadas por la Agencia Espacial Europea (ESA / ESERO), los satélites se lanzan en cohetes de modelismo espacial hasta alcanzar un apogeo de aproximadamente 1.000 metros sobre el punto de despegue antes de ser eyectados.
Para validar el ecosistema de telemetría de BermejaSat ante una dinámica de vuelo real, hemos desarrollado e integrado un simulador físico tridimensional completo. El sistema reproduce el ascenso propulsado, el apogeo, la liberación del satélite, la apertura del paracaídas y el descenso con deriva por viento sobre el relieve real de Sierra Bermeja (Pico Peñas Blancas).
📐 Perfil Físico de la Misión
El simulador implementa una máquina de estados finitos que modula la altitud y las coordenadas geográficas emitidas por radioenlace LoRa a 868 MHz:
Plaintext
🚀 Apogeo (2.450 m)
/ \ Eyección / Paracaídas
Ascenso Cohete/ \
(50 m/s)/ \ Descenso (~8 m/s)
/ \ + Deriva por Viento
/ \
Rampa (1.450 m) 📍 Aterrizaje (150 m)
- Rampa de Lanzamiento (1.450 m): Punto de partida en la cumbre de Peñas Blancas.
- Fase de Impulso / Ascenso: Propulsión a velocidad vertical $v_y = 50 \text{ m/s}$ (+100 m cada 2 s) hasta romper la cota del apogeo.
- Apogeo y Eyección (2.450 m): A 1.000 m AGL (Above Ground Level), se libera el CanSat y se despliega el paracaídas.
- Descenso Controlado: Caída a velocidad constante $v_y \approx 8 \text{ m/s}$ (-16 m cada 2 s) impulsado por deriva lateral de viento hacia el Sureste.
- Aterrizaje (150 m): Detención del cálculo físico al tocar la cota estimada de suelo.
📡 Trama de Telemetría Expandida
La trama enviada por el CanSat combina lecturas reales de los sensores (DHT22 y GUVA-S12SD) junto a los datos vectoriales de posición:
$$\text{Trama: } \texttt{TM,Temp,Hum,UV,EstadoLED,Latitud,Longitud,Altitud}$$
💻 1. Código Firmware CanSat (TTGO #1 – Emisor)
C++
#include <SPI.h>
#include <LoRa.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_SDA 21
#define OLED_SCL 22
#define OLED_RST 16
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RST);
#define DHTPIN 17
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
#define UV_PIN 34
#define SCK_LORA 5
#define MISO_LORA 19
#define MOSI_LORA 27
#define SS_LORA 18
#define RST_LORA 14
#define DIO0_LORA 26
#define ACTUATOR_LED 2
// --- MÁQUINA DE ESTADOS: SIMULADOR DE COHETE ---
enum EstadoVuelo { ASCENSO, APOGEO, DESCENSO, SUELO };
EstadoVuelo estadoActual = ASCENSO;
String estadoTexto = "ASCENSO";
float latitud = 36.4883; // Base Peñas Blancas
float longitud = -5.1953;
float altitud = 1450.0; // Altitud inicial de la rampa
const float ALTITUD_APOGEO = 2450.0; // 1.000 m de impulso de cohete
const float ALTITUD_SUELO = 150.0; // Cota de suelo estimada
bool statusLED = false;
float temperatura = 0.0, humedad = 0.0, uvIndex = 0.0;
unsigned long ultimoEnvio = 0;
String ultimoComando = "NINGUNO";
void setup() {
Serial.begin(115200);
pinMode(ACTUATOR_LED, OUTPUT);
digitalWrite(ACTUATOR_LED, LOW);
analogReadResolution(12);
pinMode(OLED_RST, OUTPUT);
digitalWrite(OLED_RST, LOW); delay(20); digitalWrite(OLED_RST, HIGH);
Wire.begin(OLED_SDA, OLED_SCL);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
dht.begin();
SPI.begin(SCK_LORA, MISO_LORA, MOSI_LORA, SS_LORA);
LoRa.setPins(SS_LORA, RST_LORA, DIO0_LORA);
LoRa.begin(868E6);
}
void loop() {
// 1. RECEPCIÓN DE TELECOMANDOS (TC)
int packetSize = LoRa.parsePacket();
if (packetSize) {
String command = "";
while (LoRa.available()) command += (char)LoRa.read();
command.trim();
ultimoComando = command;
if (command == "LED_ON") {
statusLED = true;
digitalWrite(ACTUATOR_LED, HIGH);
enviarACK("ACK: LED_ON_OK");
} else if (command == "LED_OFF") {
statusLED = false;
digitalWrite(ACTUATOR_LED, LOW);
enviarACK("ACK: LED_OFF_OK");
}
}
// 2. FÍSICA Y TRANSMISIÓN DE TELEMETRÍA (Cada 2 segundos)
if (millis() - ultimoEnvio > 2000) {
ultimoEnvio = millis();
if (estadoActual == ASCENSO) {
altitud += 100.0; // Impulso de cohete: 50 m/s (+100m / 2s)
latitud += 0.00001;
longitud += 0.00001;
estadoTexto = "ASCENSO";
if (altitud >= ALTITUD_APOGEO) {
altitud = ALTITUD_APOGEO;
estadoActual = APOGEO;
estadoTexto = "APOGEO";
}
}
else if (estadoActual == APOGEO) {
estadoActual = DESCENSO;
estadoTexto = "DESCENSO";
}
else if (estadoActual == DESCENSO) {
altitud -= 16.0; // Caída paracaídas: 8 m/s (-16m / 2s)
latitud += 0.00004; // Deriva viento Poniente
longitud += 0.00006;
estadoTexto = "DESCENSO";
if (altitud <= ALTITUD_SUELO) {
altitud = ALTITUD_SUELO;
estadoActual = SUELO;
estadoTexto = "SUELO";
}
}
// Lectura de sensores reales
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t) && !isnan(h)) { temperatura = t; humedad = h; }
int rawADC = analogRead(UV_PIN);
uvIndex = ((rawADC / 4095.0) * 3.3) / 0.1;
enviarTelemetria();
actualizarOLED();
}
}
void enviarACK(String ackMsg) {
LoRa.beginPacket();
LoRa.print(ackMsg);
LoRa.endPacket();
}
void enviarTelemetria() {
String payload = "TM," + String(temperatura, 1) + "," + String(humedad, 1) + "," +
String(uvIndex, 1) + "," + (statusLED ? "1" : "0") + "," +
String(latitud, 5) + "," + String(longitud, 5) + "," + String(altitud, 1);
LoRa.beginPacket();
LoRa.print(payload);
LoRa.endPacket();
Serial.println(payload);
}
void actualizarOLED() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0, 0); display.print("BERMEJASAT CANSAT");
display.drawLine(0, 10, 128, 10, WHITE);
display.setCursor(0, 14); display.print("Fase: ["); display.print(estadoTexto); display.print("]");
display.setCursor(0, 26); display.print("Altitud: "); display.print(altitud, 0); display.print(" m");
display.setCursor(0, 38); display.print("Lat: "); display.print(latitud, 4); display.print(" | Lon: "); display.print(longitud, 4);
display.setCursor(0, 50); display.print("T: "); display.print(temperatura, 1); display.print("C | UV: "); display.print(uvIndex, 1);
display.display();
}
💻 2. Código Estación Terrena (TTGO #2 – Receptor)
C++
#include <SPI.h>
#include <LoRa.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_SDA 21
#define OLED_SCL 22
#define OLED_RST 16
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RST);
#define SCK_LORA 5
#define MISO_LORA 19
#define MOSI_LORA 27
#define SS_LORA 18
#define RST_LORA 14
#define DIO0_LORA 26
String alt = "--", latStr = "--", lonStr = "--";
int rssi = 0;
void setup() {
Serial.begin(115200);
pinMode(OLED_RST, OUTPUT);
digitalWrite(OLED_RST, LOW); delay(20); digitalWrite(OLED_RST, HIGH);
Wire.begin(OLED_SDA, OLED_SCL);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
SPI.begin(SCK_LORA, MISO_LORA, MOSI_LORA, SS_LORA);
LoRa.setPins(SS_LORA, RST_LORA, DIO0_LORA);
LoRa.begin(868E6);
}
void loop() {
// Reenvío de comandos manuales desde el ordenador (1=LED ON, 0=LED OFF)
if (Serial.available()) {
char in = Serial.read();
if (in == '1') { LoRa.beginPacket(); LoRa.print("LED_ON"); LoRa.endPacket(); }
else if (in == '0') { LoRa.beginPacket(); LoRa.print("LED_OFF"); LoRa.endPacket(); }
}
int packetSize = LoRa.parsePacket();
if (packetSize) {
String rx = "";
while (LoRa.available()) rx += (char)LoRa.read();
rssi = LoRa.packetRssi();
if (rx.startsWith("TM,")) {
// Reenvío por serie a Python inyectando la potencia RSSI
Serial.println(rx + "," + String(rssi));
int p5 = rx.indexOf(',', rx.indexOf(',', rx.indexOf(',', rx.indexOf(',', 0) + 1) + 1) + 1);
int p6 = rx.indexOf(',', p5 + 1);
int p7 = rx.indexOf(',', p6 + 1);
if (p5 != -1 && p6 != -1 && p7 != -1) {
latStr = rx.substring(p5 + 1, p6);
lonStr = rx.substring(p6 + 1, p7);
alt = rx.substring(p7 + 1);
}
} else if (rx.startsWith("ACK:")) {
Serial.println(rx);
}
actualizarOLED();
}
}
void actualizarOLED() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0, 0); display.print("BERMEJASAT TERRENA");
display.drawLine(0, 10, 128, 10, WHITE);
display.setCursor(0, 16); display.print("Altitud: "); display.print(alt); display.print(" m");
display.setCursor(0, 28); display.print("Lat: "); display.print(latStr);
display.setCursor(0, 40); display.print("Lon: "); display.print(lonStr);
display.setCursor(0, 54); display.print("RSSI: "); display.print(rssi); display.print(" dBm");
display.display();
}
🐍 3. Script Python (Logging CSV y Generador KML 3D)
El programa gestiona las líneas DTR/RTS desactivadas para evitar el autoreinicio continuo del ESP32 al abrir la interfaz serie USB, procesando cada ráfaga entrante de forma asíncrona:
Python
import os
import time
import serial
import serial.tools.list_ports
# Configuración del puerto COM de la Terrena
PORT = "COM14" # Sustituir por el puerto COM asignado
BAUD = 115200
csv_file = "telemetria_bermejasat.csv"
kml_file = "trayectoria_bermejasat.kml"
coordinates = []
# Crear archivo CSV con cabecera
with open(csv_file, "w", encoding="utf-8") as f:
f.write(
"Timestamp,Temperatura,Humedad,UVIndex,EstadoLED,Latitud,Longitud,Altitud,RSSI\n"
)
print("==================================================")
print("🚀 SISTEMA DE RECEPCIÓN BERMEJASAT")
print(f"📄 CSV: {csv_file} | 🗺️ KML: {kml_file}")
print("==================================================")
try:
ser = serial.Serial()
ser.port = PORT
ser.baudrate = BAUD
ser.timeout = 1
ser.dtr = False # Previene bloqueo por reset en el ESP32
ser.rts = False
ser.open()
ser.reset_input_buffer()
print(f"✅ Puerto {PORT} conectado.\n")
except Exception as e:
print(f"❌ Error al abrir {PORT}: {e}")
exit()
try:
while True:
if ser.in_waiting > 0:
raw_line = ser.readline()
line = raw_line.decode("utf-8", errors="ignore").strip()
if not line or not line.startswith("TM,"):
continue
data = line.split(",")
if len(data) >= 8:
timestamp = time.strftime("%H:%M:%S")
temp, hum, uv, led, lat, lon, alt = data[1:8]
rssi = data[8] if len(data) >= 9 else "N/A"
# 1. Escritura en CSV
with open(csv_file, "a", encoding="utf-8") as f:
f.write(
f"{timestamp},{temp},{hum},{uv},{led},{lat},{lon},{alt},{rssi}\n"
)
# 2. Acumulación de coordenadas (Longitud, Latitud, Altitud)
coordinates.append(f"{lon},{lat},{alt}")
# 3. Renderizado continuo del archivo KML 3D
kml_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<name>Trayectoria BermejaSat</name>
<Style id="redLine">
<LineStyle>
<color>ff0000ff</color>
<width>4</width>
</LineStyle>
<PolyStyle>
<color>7f0000ff</color>
</PolyStyle>
</Style>
<Placemark>
<name>Vuelo CanSat Sierra Bermeja</name>
<styleUrl>#redLine</styleUrl>
<LineString>
<extrude>1</extrude>
<tessellate>1</tessellate>
<altitudeMode>absolute</altitudeMode>
<coordinates>
{" ".join(coordinates)}
</coordinates>
</LineString>
</Placemark>
</Document>
</kml>"""
with open(kml_file, "w", encoding="utf-8") as f:
f.write(kml_content)
print(
f"💾 [{timestamp}] Registrado -> Altitud: {alt}m | Lat: {lat} | Lon:"
f" {lon}"
)
time.sleep(0.05)
except KeyboardInterrupt:
print("\n⏹️ Captura finalizada por el usuario.")
ser.close()
🗺️ Carga y Visualización Tridimensional en Google Earth
Para inspeccionar el resultado tras la captura de datos (finalizada mediante Ctrl + C en la terminal):
- Google Earth Web / Pro: Abrir earth.google.com o la aplicación de escritorio Google Earth Pro.
- Importación: Seleccionar Proyectos $\rightarrow$ Importar archivo KML (o
Ctrl + Oen escritorio) y cargartrayectoria_bermejasat.kml. - Exploración 3D: El visor centra la vista automáticamente sobre el Pico Peñas Blancas. Manteniendo pulsada la tecla
Shift(Mayús) y desplazando el ratón, es posible inclinar la perspectiva.
La escena renderizada confirma la parábola tridimensional: la cinta roja se eleva verticalmente despegando de la cumbre hasta tocar la cota de apogeo de 2.450 m, dibujando a continuación un plano suavemente inclinado que evidencia el arrastre lateral por viento durante el descenso en paracaídas hasta la cota de aterrizaje.

Con esta última fase, la suite completa de firmware, simulación y procesado en tierra de BermejaSat queda validada y lista para la competición.
Etiqueta:AIDARAC, Apogeo, BermejaSat, cansat, cohete, CSV, Google Earth, ies monterroso, KML 3D, LoRa 868MHz, montesteam, Peñas Blancas, python, sierra bermeja, Simulación de Vuelo


