
BermejaSat #20: Integración Total TC/TM con Sensor UV GUVA-S12SD, AM2302, OLEDs Duales y Actuación Remota
Alcanzamos el hito más importante de la fase de prototipado de software y radioenlace de BermejaSat. Hemos unificado la carga útil de sensores ambientales, la misión científica de radiación ultravioleta, la visualización gráfica en pantalla OLED local y la ejecución remota de Telecomandos (TC) con confirmación ACK a través del protocolo Dúplex LoRa a 868 MHz.
🛠️ Esquema de Montaje y Mapeo de Pines
Para esta prueba de banco completa utilizamos dos módulos TTGO LoRa32 v2.1.6. En el nodo CanSat se concentran los sensores y el actuador sin necesidad de multiplexores I2C ni soldaduras externas.
| Componente | Pin / Función | Pin en TTGO LoRa32 (CanSat) | Notas |
| ASAIR AM2302 (DHT22) | Datos | GPIO 17 | Temperatura y Humedad |
| GUVA-S12SD (UV) | Salida Analógica (OUT) | GPIO 34 | Canal ADC1_CH6 (0 – 3.3V) |
| OLED SSD1306 | SDA / SCL / RST | GPIO 21 / 22 / 16 | Bus I2C integrado |
| Actuador Físico | Salida Digital | GPIO 2 | LED azul integrado / Carga útil |
| Transceptor LoRa | SPI / Control | GPIO 5, 19, 27, 18, 14, 26 | Módulo SX1276 interno (868 MHz) |
📡 Arquitectura de Comunicaciones y Lógica del Enlace
El sistema opera mediante dos bucles concurrentes no bloqueantes controlados por millis():
- Canal de Telemetría (TM): Cada 2000 ms, el CanSat emite una trama empaquetada en formato CSV:$$\text{Trama TM} = \text{“TM,”} + \text{Temp} + \text{“,”} + \text{Hum} + \text{“,”} + \text{UV} + \text{“,”} + \text{EstadoLED}$$
- Canal de Telecomando (TC): La Estación Terrena escucha la consola serie. Al presionar
1o0, transmite por radio la ordenLED_ONoLED_OFF. - Bucle de Confirmación (ACK): Al recibir la orden, el CanSat conmuta el estado de su pin digital, fuerza la actualización inmediata de la pantalla OLED local y responde con un paquete
ACK: LED_ON_OKa la base para verificar el éxito del comando.
💻 Firmware 1: Nodo CanSat (TTGO #1 + AM2302 + GUVA-S12SD + OLED + Actuador)
C++
#include <SPI.h>
#include <LoRa.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
// --- PINES PANTALLA OLED ---
#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);
// --- PINES SENSORES ---
#define DHTPIN 17
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
#define UV_PIN 34 // Pin analógico para el GUVA-S12SD
// --- PINES LORA SX1276 ---
#define SCK_LORA 5
#define MISO_LORA 19
#define MOSI_LORA 27
#define SS_LORA 18
#define RST_LORA 14
#define DIO0_LORA 26
// --- ACTUADOR / LED ---
#define ACTUATOR_LED 2
// Variables de estado
bool statusLED = false;
float temperatura = 0.0;
float humedad = 0.0;
float 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);
// Inicializar Pantalla OLED
pinMode(OLED_RST, OUTPUT);
digitalWrite(OLED_RST, LOW); delay(20); digitalWrite(OLED_RST, HIGH);
Wire.begin(OLED_SDA, OLED_SCL);
if (display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("BERMEJASAT CANSAT");
display.println("Iniciando sensores...");
display.display();
}
dht.begin();
// Inicializar Radio LoRa
SPI.begin(SCK_LORA, MISO_LORA, MOSI_LORA, SS_LORA);
LoRa.setPins(SS_LORA, RST_LORA, DIO0_LORA);
if (!LoRa.begin(868E6)) {
Serial.println("❌ Error LoRa CanSat!");
while (1);
}
}
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");
}
actualizarOLED();
}
// 2. LECTURA ADQUISICIÓN Y TRANSMISIÓN PERIÓDICA
if (millis() - ultimoEnvio > 2000) {
ultimoEnvio = millis();
// Lectura ambiental
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t) && !isnan(h)) { temperatura = t; humedad = h; }
// Lectura analógica de radiación UV
int rawADC = analogRead(UV_PIN);
float voltage = (rawADC / 4095.0) * 3.3;
uvIndex = voltage / 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");
LoRa.beginPacket();
LoRa.print(payload);
LoRa.endPacket();
Serial.print("📡 TX Telemetría: "); Serial.println(payload);
}
void actualizarOLED() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0); display.print("BERMEJASAT CANSAT");
display.drawLine(0, 10, 128, 10, WHITE);
display.setCursor(0, 14); display.print("Temp: "); display.print(temperatura, 1); display.print(" C");
display.setCursor(0, 26); display.print("Hum: "); display.print(humedad, 1); display.print(" %");
display.setCursor(0, 38); display.print("UV: "); display.print(uvIndex, 1); display.print(" Index");
display.setCursor(0, 50); display.print("LED: "); display.print(statusLED ? "[ON]" : "[OFF]");
display.print(" | Cmd: "); display.print(ultimoComando);
display.display();
}
💻 Firmware 2: Nodo Estación Terrena (TTGO #2 + OLED + Control Serie)
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 temp = "--", hum = "--", uv = "--", estadoLED = "OFF";
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);
if (display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("BERMEJASAT BASE");
display.println("Esperando senal...");
display.display();
}
SPI.begin(SCK_LORA, MISO_LORA, MOSI_LORA, SS_LORA);
LoRa.setPins(SS_LORA, RST_LORA, DIO0_LORA);
if (!LoRa.begin(868E6)) {
Serial.println("❌ Error LoRa Base!");
while (1);
}
}
void loop() {
// 1. INTERFAZ DE TECLADO DESDE EL MONITOR SERIE
if (Serial.available()) {
char in = Serial.read();
if (in == '1') enviarComando("LED_ON");
else if (in == '0') enviarComando("LED_OFF");
}
// 2. PARSER DE TELEMETRÍA Y RESPUESTAS ACK
int packetSize = LoRa.parsePacket();
if (packetSize) {
String rx = "";
while (LoRa.available()) rx += (char)LoRa.read();
rssi = LoRa.packetRssi();
if (rx.startsWith("TM,")) {
int p1 = rx.indexOf(',', 3);
int p2 = rx.indexOf(',', p1 + 1);
int p3 = rx.indexOf(',', p2 + 1);
if (p1 != -1 && p2 != -1 && p3 != -1) {
temp = rx.substring(3, p1);
hum = rx.substring(p1 + 1, p2);
uv = rx.substring(p2 + 1, p3);
estadoLED = (rx.substring(p3 + 1) == "1") ? "ON" : "OFF";
}
Serial.print("📊 [TM] Temp: "); Serial.print(temp);
Serial.print(" C | Hum: "); Serial.print(hum);
Serial.print(" % | UV: "); Serial.print(uv);
Serial.print(" | LED: "); Serial.print(estadoLED);
Serial.print(" | RSSI: "); Serial.print(rssi); Serial.println(" dBm");
}
else if (rx.startsWith("ACK:")) {
Serial.print("✅ "); Serial.println(rx);
}
actualizarOLED();
}
}
void enviarComando(String cmd) {
LoRa.beginPacket();
LoRa.print(cmd);
LoRa.endPacket();
Serial.print("🚀 TC Enviado: "); Serial.println(cmd);
}
void actualizarOLED() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0); display.print("BERMEJASAT TERRENA");
display.drawLine(0, 10, 128, 10, WHITE);
display.setCursor(0, 14); display.print("T: "); display.print(temp); display.print(" C | H: "); display.print(hum); display.print("%");
display.setCursor(0, 26); display.print("UV: "); display.print(uv); display.print(" Index");
display.setCursor(0, 38); display.print("Actuador Remoto: "); display.print(estadoLED);
display.setCursor(0, 52); display.print("RSSI: "); display.print(rssi); display.print(" dBm");
display.display();
}
🔬 Verificación en el Banco de Pruebas
- Estabilidad del sensor UV en penumbra: En interiores, la lectura del canal analógico se consolida en 0.0 Index (0.00 V), eliminando falso ruido de fondo.
- Sincronización gráfica dual: Las dos pantallas OLED representan los mismos parámetros de misión al instante.
- Respuesta remota a telecomandos: Al lanzar órdenes desde la consola de comandos de tierra, la latencia de conmutación del actuador en el CanSat y la devolución del paquete ACK es inferior a 45 ms, con una cobertura de señal excelente (-38 a -42 dBm en pruebas de laboratorio).
¡El software central de BermejaSat queda totalmente funcional y listo para la integración de los sensores I2C finales!


