
BermejaSat #8: Estación Terrena en Python — Visualización de Telemetría en Tiempo Real y Simulación HIL
Introducción
En el desarrollo de misiones espaciales reales, las agencias como la ESA o la NASA nunca esperan a tener la estructura física y los sensores definitivos soldados para empezar a construir el software de control en tierra. Se aplica una metodología estándar en ingeniería aeroespacial llamada Hardware-in-the-Loop (HIL): utilizar la propia placa de vuelo para simular las condiciones de la atmósfera y enviar telemetría a los sistemas de recepción.
En esta séptima entrega de BermejaSat, damos un salto cuantitativo en el proyecto. Mientras ultimamos los trabajos de soldadura del sensor barométrico en el aula-taller, convertimos nuestra placa LILYGO LoRa V1.3 en un simulador de vuelo autónomo y desarrollamos una Estación Terrena profesional en Python capaz de recibir la telemetría por el puerto serie USB y graficar las magnitudes ambientales en tiempo real.
🛠️ Requisitos e Instalación del Entorno (Python)
Para construir nuestro Centro de Control de Misión en el ordenador, necesitamos dos librerías estándar en el ecosistema de análisis de datos y control:
pyserial: Permite a Python abrir el puerto COM/USB y escuchar el flujo de datos que envía la placa en tiempo real.matplotlib: La librería gráfica estándar para renderizar y animar las curvas de altitud, presión y calidad de señal.
Paso 1: Instalación de librerías
Abre la terminal de tu sistema operativo (Símbolo del sistema / PowerShell en Windows, o Terminal en macOS/Linux) y ejecuta el siguiente comando:
Bash
pip install pyserial matplotlib
💻 Parte 1: Código del Satélite (Arduino IDE / ESP32)
Este código transforma la placa LILYGO V1.3 en un micro-simulador de vuelo. Utiliza nuestro motor de tiempo no-bloqueante con millis() para recrear la física de un descenso en paracaídas desde $1000\text{ metros}$ de altitud a una velocidad reglamentaria de unos $8\text{ m/s}$.
La placa refresca su pantalla OLED local y transmite continuamente la trama de telemetría serializada en formato CSV a través de la conexión USB a 115200 baudios.
C++
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// --- Configuración Pantalla OLED (GPIO 21 SDA, GPIO 22 SCL) ---
Adafruit_SSD1306 display(128, 64, &Wire, -1);
// --- Control de Tiempo (millis) ---
unsigned long ultimoEnvio = 0;
const long intervalo = 1000; // Enviar paquete cada 1000 ms (1 segundo)
// --- Variables de Simulación del Satélite ---
float altitud = 1000.0; // Metros iniciales
float presion = 898.7; // hPa inicial a 1000m
float temperatura = 18.5; // Grados Celsius
int rssiSimulado = -42; // Potencia de señal (dBm)
int paqueteID = 0;
void setup() {
// Inicializar puerto serie USB a alta velocidad
Serial.begin(115200);
// Inicializar bus I2C y Pantalla OLED
Wire.begin(21, 22);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("Error al iniciar OLED");
}
// Pantalla de bienvenida
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 15);
display.println("BERMEJASAT - HIL SIM");
display.setCursor(0, 35);
display.println("Transmitiendo a PC...");
display.display();
delay(1500);
}
void loop() {
unsigned long tiempoActual = millis();
if (tiempoActual - ultimoEnvio >= intervalo) {
ultimoEnvio = tiempoActual;
paqueteID++;
// --- FÍSICA DE DESCENSO SIMULADA (ESA CanSat Specs) ---
if (altitud > 0) {
altitud -= 8.0; // Tasa de descenso ~8 m/s
presion += 0.95; // La presión atmosférica sube al caer
temperatura += 0.04; // Ligero aumento de temperatura
rssiSimulado -= (paqueteID % 3 == 0) ? 1 : 0; // Atenuación paulatina de radio
} else {
altitud = 0.0; // Satélite en tierra
}
// --- EMISIÓN DE TELEMETRÍA POR PUERTO SERIE (CSV) ---
// Estructura de la trama: ID,Altitud,Presion,Temperatura,RSSI
Serial.print(paqueteID);
Serial.print(",");
Serial.print(altitud, 1);
Serial.print(",");
Serial.print(presion, 1);
Serial.print(",");
Serial.print(temperatura, 1);
Serial.print(",");
Serial.println(rssiSimulado);
// --- REFRESTO DE PANTALLA OLED LOCAL ---
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("BERMEJASAT - EMISOR USB");
display.drawLine(0, 10, 128, 10, SSD1306_WHITE);
display.setCursor(0, 20);
display.print("Pkt: #"); display.print(paqueteID);
display.print(" | "); display.print(rssiSimulado); display.println("dBm");
display.setCursor(0, 38);
display.print("Alt: "); display.setTextSize(2); display.print(altitud, 0); display.setTextSize(1); display.println(" m");
display.setCursor(0, 52);
display.print("P: "); display.print(presion, 1); display.print(" hPa");
display.display();
}
}
🐍 Parte 2: Software de la Estación Terrena (Python)
Guarda este script en tu ordenador con el nombre estacion_terrena.py. El programa abre el puerto serie, parsea los valores que llegan de la placa LILYGO y dibuja tres gráficas animadas en tiempo real: el Perfil de Descenso (Altitud), la Presión Barométrica y la Calidad de Enlace Radio (RSSI).
Importante: Asegúrate de modificar el valor de
PUERTO_SERIEcon el puerto exacto asignado a tu placa (por ejemplo,'COM3','COM5'en Windows, o'/dev/ttyUSB0'en Linux/macOS).
Python
import serial
import time
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
# --- CONFIGURACIÓN DE CONEXIÓN SERIE ---
PUERTO_SERIE = 'COM3' # Cambia esto por el puerto de tu placa
BAUD_RATE = 115200
# Conectar al microcontrolador
try:
ser = serial.Serial(PUERTO_SERIE, BAUD_RATE, timeout=1)
time.sleep(2) # Pausa de seguridad para estabilizar la conexión USB
print(f"[OK] Conectado exitosamente a BermejaSat en {PUERTO_SERIE}")
except Exception as e:
print(f"[ERROR] No se pudo abrir el puerto serie {PUERTO_SERIE}: {e}")
exit()
# Estructuras de datos para las gráficas
tiempos = []
altitudes = []
presiones = []
rssis = []
tiempo_inicio = time.time()
# --- CONFIGURACIÓN DE LA INTERFAZ GRÁFICA ---
fig, (ax_alt, ax_pres, ax_rssi) = plt.subplots(3, 1, figsize=(10, 8))
fig.canvas.manager.set_window_title('Centro de Control BermejaSat — Telemetría en Tiempo Real')
def actualizar_graficas(frame):
if ser.in_waiting > 0:
try:
# Leer y decodificar la trama CSV
linea = ser.readline().decode('utf-8').strip()
datos = linea.split(',')
# Comprobar integridad de la trama (5 magnitudes recibidas)
if len(datos) == 5:
pkt_id = int(datos[0])
alt = float(datos[1])
pres = float(datos[2])
temp = float(datos[3])
rssi = int(datos[4])
t_relativo = round(time.time() - tiempo_inicio, 1)
# Almacenar en histórico
tiempos.append(t_relativo)
altitudes.append(alt)
presiones.append(pres)
rssis.append(rssi)
# Mantener en pantalla los últimos 30 registros para dinamismo
t_plot = tiempos[-30:]
alt_plot = altitudes[-30:]
pres_plot = presiones[-30:]
rssi_plot = rssis[-30:]
# 1. Gráfica de Altitud (Perfil de vuelo)
ax_alt.clear()
ax_alt.plot(t_plot, alt_plot, color='#d9534f', linewidth=2, marker='o', markersize=4)
ax_alt.set_title(f'Perfil de Descenso | Altitud Actual: {alt:.1f} m', fontsize=10, fontweight='bold')
ax_alt.set_ylabel('Altitud (m)')
ax_alt.grid(True, linestyle='--', alpha=0.5)
# 2. Gráfica de Presión Atmosférica
ax_pres.clear()
ax_pres.plot(t_plot, pres_plot, color='#0275d8', linewidth=2, marker='s', markersize=4)
ax_pres.set_title(f'Presión Barométrica: {pres:.1f} hPa', fontsize=10, fontweight='bold')
ax_pres.set_ylabel('Presión (hPa)')
ax_pres.grid(True, linestyle='--', alpha=0.5)
# 3. Gráfica de Cobertura LoRa (RSSI)
ax_rssi.clear()
ax_rssi.plot(t_plot, rssi_plot, color='#5cb85c', linewidth=2)
ax_rssi.set_title(f'Calidad de Enlace de Radio: {rssi} dBm', fontsize=10, fontweight='bold')
ax_rssi.set_ylabel('RSSI (dBm)')
ax_rssi.set_xlabel('Tiempo de Misión (s)')
ax_rssi.grid(True, linestyle='--', alpha=0.5)
plt.tight_layout()
except ValueError:
pass # Ignorar tramas incompletas de arranque
# Bucle de animación (refresco cada 500 ms)
ani = FuncAnimation(fig, actualizar_graficas, interval=500, cache_frame_data=False)
plt.tight_layout()
plt.show()
# Cerrar puerto de manera limpia al cerrar la ventana
ser.close()

📖 Guía de Trabajo en el Aula (Tutorial Paso a Paso)
Para poner en marcha la simulación en el taller informático, el alumnado debe seguir el orden secuencial de ejecución:
- Cargar el código C++ en la placa: Subir el sketch de simulación a la LILYGO LoRa V1.3 mediante el IDE de Arduino.
- Verificar la salida del Monitor Serie: Abrir el Monitor Serie de Arduino a 115200 baudios para confirmar que la placa está emitiendo líneas con formato
1,992.0,899.6,18.5,-42. - CERRAR el Monitor Serie de Arduino: Este paso es crucial. El puerto USB solo puede ser utilizado por un programa a la vez. Si el Monitor Serie de Arduino se queda abierto, el script de Python no podrá acceder al puerto y dará un error de permiso denegado.
- Identificar el Puerto COM: Anotar el número de puerto serie (por ejemplo
COM3). - Ajustar y ejecutar el script en Python: Abrir
estacion_terrena.py, colocar el puerto correcto en la variablePUERTO_SERIEy ejecutar el archivo desde la consola:
Bashpython estacion_terrena.py
Conclusión Pedagógica
La implementación del simulador HIL y el desarrollo de la Estación Terrena en Python capacita al alumnado en tres disciplinas clave de la ingeniería moderna:
- Trazabilidad de datos end-to-end: Desde la estructura interna de los datos en el microcontrolador hasta su procesamiento visual en la estación terrena.
- Tolerancia a fallos y desarrollo ágil: El proyecto no se detiene ante la falta temporal de un componente físico; el software de tierra se deja probado, validado y listo para recibir los datos reales en cuanto los sensores físicos se suelden.
- Procesamiento de señal y formato CSV: Comprensión práctica de los protocolos de serialización ligeros para transmisión de baja tasa de transferencia.
En la próxima entrega: Integración de hardware definitivo. Soldaremos los conectores del módulo barométrico BMP280/BME280 y conectaremos la telemetría inalámbrica por radio a 868 MHz directamente con nuestra nueva Estación Terrena en Python.
Etiqueta:BermejaSat, cansat, ESA, esp32, Hardware in the Loop, HIL, ies monterroso, LILYGO, matplotlib, montesteam, pySerial, python, Telemetría



