Gabriel Alejandro Giraldo Santiago
Published © GPL3+

Cosmic IoT Communicator

IoT Platform for Simulated Satellite Reception and Communication using nRF9161 DK

AdvancedWork in progressOver 4 days234

Things used in this project

Hardware components

Nordic Semiconductor nRF9151 DK
×1
Arduino Giga R1 Wifi
×1
SDR
×1

Story

Read more

Schematics

Project PDF Engliish

Code SSTV

Code nRF9151

Code

Cosmic IoT Communicator – SSTV Signal Processor

Python
This script processes SSTV audio signals and converts them into images.
Compatible with PD120, Robot36, and other common formats.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Cosmic IoT Communicator - Procesador de Señales SSTV
Este script procesa señales de audio SSTV y las convierte en imágenes.
Compatible con los modos PD120, Robot36, y otros formatos comunes.
"""

import os
import sys
import time
import argparse
import numpy as np
import scipy.signal as signal
from scipy.io import wavfile
from datetime import datetime
import matplotlib.pyplot as plt
from PIL import Image

# Constantes para modos SSTV
SSTV_MODES = {
    'pd120': {
        'vis_code': 0x3C,
        'line_duration': 0.120,  # segundos
        'pixels_per_line': 640,
        'lines': 496,
        'sync_pulse': 0.0055,    # segundos
        'porch': 0.0015,         # segundos
        'separator': 0.0015,     # segundos
        'has_color': True,
        'color_seq': 'rgb'       # secuencia de colores
    },
    'robot36': {
        'vis_code': 0x08,
        'line_duration': 0.036,  # segundos
        'pixels_per_line': 320,
        'lines': 240,
        'sync_pulse': 0.009,     # segundos
        'porch': 0.003,          # segundos
        'separator': 0.0015,     # segundos
        'has_color': True,
        'color_seq': 'ry'        # secuencia de colores (Y, R-Y, B-Y)
    },
    'scottie1': {
        'vis_code': 0x3C,
        'line_duration': 0.138,  # segundos
        'pixels_per_line': 320,
        'lines': 256,
        'sync_pulse': 0.009,     # segundos
        'porch': 0.001,          # segundos
        'separator': 0.0015,     # segundos
        'has_color': True,
        'color_seq': 'grb'       # secuencia de colores
    }
}

# Frecuencias para decodificación
FREQ_BLACK = 1500    # Hz
FREQ_WHITE = 2300    # Hz
FREQ_SYNC = 1200     # Hz
FREQ_VIS_BIT1 = 1100 # Hz
FREQ_VIS_BIT0 = 1300 # Hz

class SSTVDecoder:
    """Decodificador de señales SSTV"""
    
    def __init__(self, mode='pd120', sample_rate=44100):
        """
        Inicializa el decodificador SSTV
        
        Args:
            mode (str): Modo SSTV ('pd120', 'robot36', 'scottie1')
            sample_rate (int): Frecuencia de muestreo del audio en Hz
        """
        if mode not in SSTV_MODES:
            raise ValueError(f"Modo SSTV no soportado: {mode}")
        
        self.mode = SSTV_MODES[mode]
        self.mode_name = mode
        self.sample_rate = sample_rate
        self.image = None
        self.debug = False
        
        # Calcular parámetros derivados
        self.samples_per_line = int(self.mode['line_duration'] * sample_rate)
        self.sync_samples = int(self.mode['sync_pulse'] * sample_rate)
        self.porch_samples = int(self.mode['porch'] * sample_rate)
        self.separator_samples = int(self.mode['separator'] * sample_rate)
        
        # Calcular muestras por píxel
        self.pixel_samples = (self.samples_per_line - self.sync_samples - 
                             self.porch_samples - 
                             (0 if not self.mode['has_color'] else 
                              (len(self.mode['color_seq'])-1) * self.separator_samples)) / self.mode['pixels_per_line']
        
        print(f"Inicializado decodificador SSTV para modo {mode}")
        print(f"Muestras por línea: {self.samples_per_line}")
        print(f"Muestras por píxel: {self.pixel_samples:.4f}")
    
    def detect_vis_code(self, audio_data):
        """
        Detecta el código VIS en la señal de audio
        
        Args:
            audio_data (numpy.ndarray): Datos de audio
            
        Returns:
            int: Código VIS detectado o None si no se detecta
        """
        print("Buscando código VIS...")
        
        # Buscar pulso de inicio (leader tone)
        leader_tone_min_duration = 0.3  # segundos
        leader_tone_samples = int(leader_tone_min_duration * self.sample_rate)
        
        # Filtrar para detectar 1900 Hz (leader tone)
        sos = signal.butter(10, [1800, 2000], 'bandpass', fs=self.sample_rate, output='sos')
        filtered = signal.sosfilt(sos, audio_data)
        filtered_env = np.abs(signal.hilbert(filtered))
        
        # Buscar segmentos donde la envolvente supera un umbral
        threshold = 0.5 * np.max(filtered_env)
        above_threshold = filtered_env > threshold
        
        # Encontrar segmentos largos por encima del umbral
        segments = []
        start = None
        for i, val in enumerate(above_threshold):
            if val and start is None:
                start = i
            elif not val and start is not None:
                if i - start > leader_tone_samples:
                    segments.append((start, i))
                start = None
        
        if not segments:
            print("No se detectó leader tone")
            return None
        
        # Para cada segmento potencial, buscar el código VIS
        for start, end in segments:
            # El código VIS comienza después del leader tone y un bit de inicio
            vis_start = end + int(0.03 * self.sample_rate)  # 30ms después del leader tone
            
            if vis_start + int(0.3 * self.sample_rate) >= len(audio_data):
                continue  # No hay suficientes datos para el código VIS
            
            # Decodificar 8 bits del código VIS
            vis_code = 0
            for bit in range(8):
                bit_center = vis_start + int((bit + 0.5) * 0.03 * self.sample_rate)
                
                # Analizar frecuencia en este punto
                segment = audio_data[bit_center - 100:bit_center + 100]
                if len(segment) < 200:
                    continue
                
                # Calcular FFT
                fft = np.abs(np.fft.rfft(segment * np.hamming(len(segment))))
                freqs = np.fft.rfftfreq(len(segment), 1/self.sample_rate)
                
                # Encontrar pico de frecuencia
                peak_idx = np.argmax(fft)
                peak_freq = freqs[peak_idx]
                
                # Determinar si es bit 1 o 0
                if abs(peak_freq - FREQ_VIS_BIT1) < abs(peak_freq - FREQ_VIS_BIT0):
                    vis_code |= (1 << bit)
            
            print(f"Código VIS detectado: 0x{vis_code:02X}")
            
            # Verificar si el código VIS corresponde al modo esperado
            if vis_code == self.mode['vis_code']:
                print(f"Código VIS coincide con modo {self.mode_name}")
                return vis_code
            else:
                print(f"Código VIS no coincide con modo esperado")
        
        return None
    
    def find_sync_start(self, audio_data, start_pos=0):
        """
        Encuentra el inicio de un pulso de sincronización
        
        Args:
            audio_data (numpy.ndarray): Datos de audio
            start_pos (int): Posición de inicio para la búsqueda
            
        Returns:
            int: Posición del inicio del pulso de sincronización o None si no se encuentra
        """
        # Filtrar para detectar pulso de sincronización (1200 Hz)
        sos = signal.butter(10, [1100, 1300], 'bandpass', fs=self.sample_rate, output='sos')
        filtered = signal.sosfilt(sos, audio_data[start_pos:start_pos + int(0.5 * self.sample_rate)])
        filtered_env = np.abs(signal.hilbert(filtered))
        
        # Buscar donde la envolvente supera un umbral
        threshold = 0.5 * np.max(filtered_env)
        above_threshold = filtered_env > threshold
        
        # Encontrar el primer punto por encima del umbral
        for i, val in enumerate(above_threshold):
            if val:
                return start_pos + i
        
        return None
    
    def decode_line(self, audio_data, line_start):
        """
        Decodifica una línea de la imagen
        
        Args:
            audio_data (numpy.ndarray): Datos de audio
            line_start (int): Posición de inicio de la línea
            
        Returns:
            tuple: RGB valores para la línea o None si hay error
        """
        if line_start + self.samples_per_line >= len(audio_data):
            return None  # No hay suficientes datos
        
        # Extraer segmento de audio para esta línea
        line_data = audio_data[line_start:line_start + self.samples_per_line]
        
        # Preparar arrays para los componentes de color
        r_line = np.zeros(self.mode['pixels_per_line'])
        g_line = np.zeros(self.mode['pixels_per_line'])
        b_line = np.zeros(self.mode['pixels_per_line'])
        
        # Posición actual después del pulso de sincronización y porch
        current_pos = self.sync_samples + self.porch_samples
        
        # Decodificar cada componente de color
        for color_idx, color in enumerate(self.mode['color_seq']):
            # Añadir separador excepto para el primer color
            if color_idx > 0:
                current_pos += self.separator_samples
            
            # Decodificar píxeles para este componente de color
            for pixel in range(self.mode['pixels_per_line']):
                pixel_start = int(current_pos + pixel * self.pixel_samples)
                pixel_end = int(pixel_start + self.pixel_samples)
                
                if pixel_end >= len(line_data):
                    return None  # Error: datos insuficientes
                
                # Extraer segmento para este píxel
                pixel_data = line_data[pixel_start:pixel_end]
                
                # Calcular frecuencia dominante
                if len(pixel_data) > 10:  # Asegurar suficientes muestras
                    fft = np.abs(np.fft.rfft(pixel_data * np.hamming(len(pixel_data))))
                    freqs = np.fft.rfftfreq(len(pixel_data), 1/self.sample_rate)
                    
                    # Encontrar pico de frecuencia
                    peak_idx = np.argmax(fft)
                    peak_freq = freqs[peak_idx]
                    
                    # Convertir frecuencia a valor de píxel (0-255)
                    pixel_value = int(255 * (peak_freq - FREQ_BLACK) / (FREQ_WHITE - FREQ_BLACK))
                    pixel_value = max(0, min(255, pixel_value))  # Limitar a 0-255
                else:
                    pixel_value = 0
                
                # Asignar valor al componente de color correspondiente
                if color == 'r':
                    r_line[pixel] = pixel_value
                elif color == 'g':
                    g_line[pixel] = pixel_value
                elif color == 'b':
                    b_line[pixel] = pixel_value
                elif color == 'y':  # Luminancia (para modos YUV)
                    r_line[pixel] = pixel_value
                    g_line[pixel] = pixel_value
                    b_line[pixel] = pixel_value
        
        return (r_line, g_line, b_line)
    
    def decode(self, audio_data):
        """
        Decodifica una señal SSTV completa
        
        Args:
            audio_data (numpy.ndarray): Datos de audio
            
        Returns:
            PIL.Image: Imagen decodificada o None si hay error
        """
        # Normalizar audio
        audio_data = audio_data / np.max(np.abs(audio_data))
        
        # Detectar código VIS
        vis_code = self.detect_vis_code(audio_data)
        if vis_code is None:
            print("No se pudo detectar el código VIS, usando configuración predeterminada")
        
        # Crear imagen vacía
        width = self.mode['pixels_per_line']
        height = self.mode['lines']
        img_array = np.zeros((height, width, 3), dtype=np.uint8)
        
        # Encontrar inicio de la primera línea
        line_start = self.find_sync_start(audio_data)
        if line_start is None:
            print("No se pudo encontrar el pulso de sincronización inicial")
            return None
        
        # Decodificar cada línea
        for line in range(height):
            if line_start is None or line_start + self.samples_per_line >= len(audio_data):
                print(f"Datos insuficientes para línea {line}, terminando")
                break
            
            # Decodificar línea actual
            line_result = self.decode_line(audio_data, line_start)
            if line_result is None:
                print(f"Error al decodificar línea {line}")
                break
            
            r_line, g_line, b_line = line_result
            
            # Almacenar en la imagen
            img_array[line, :, 0] = r_line
            img_array[line, :, 1] = g_line
            img_array[line, :, 2] = b_line
            
            # Encontrar inicio de la siguiente línea
            next_line_start = self.find_sync_start(audio_data, line_start + self.samples_per_line - self.sync_samples)
            
            # Si no se encuentra el siguiente pulso de sincronización, estimar su posición
            if next_line_start is None:
                next_line_start = line_start + self.samples_per_line
                print(f"No se encontró pulso de sincronización para línea {line+1}, estimando posición")
            
            line_start = next_line_start
            
            # Mostrar progreso
            if line % 10 == 0:
                print(f"Decodificando línea {line}/{height}")
        
        # Convertir a imagen PIL
        self.image = Image.fromarray(img_array)
        return self.image
    
    def save_image(self, output_path=None):
        """
        Guarda la imagen decodificada
        
        Args:
            output_path (str): Ruta de salida para la imagen
            
        Returns:
            str: Ruta donde se guardó la imagen
        """
        if self.image is None:
            print("No hay imagen para guardar")
            return None
        
        if output_path is None:
            # Generar nombre de archivo basado en fecha y hora
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            output_path = f"sstv_decoded_{timestamp}.png"
        
        self.image.save(output_path)
        print(f"Imagen guardada en: {output_path}")
        return output_path

def main():
    """Función principal"""
    parser = argparse.ArgumentParser(description='Decodificador SSTV para Cosmic IoT Communicator')
    parser.add_argument('input_file', help='Archivo de audio WAV de entrada')
    parser.add_argument('--output', '-o', help='Ruta de salida para la imagen decodificada')
    parser.add_argument('--mode', '-m', default='pd120', choices=SSTV_MODES.keys(),
                        help='Modo SSTV (pd120, robot36, scottie1)')
    parser.add_argument('--debug', '-d', action='store_true', help='Habilitar modo de depuración')
    
    args = parser.parse_args()
    
    # Cargar archivo de audio
    try:
        sample_rate, audio_data = wavfile.read(args.input_file)
        print(f"Archivo cargado: {args.input_file}")
        print(f"Frecuencia de muestreo: {sample_rate} Hz")
        print(f"Duración: {len(audio_data)/sample_rate:.2f} segundos")
        
        # Si es estéreo, usar solo el canal izquierdo
        if len(audio_data.shape) > 1 and audio_data.shape[1] >= 2:
            audio_data = audio_data[:, 0]
            print("Usando solo el canal izquierdo del audio estéreo")
    except Exception as e:
        print(f"Error al cargar el archivo de audio: {e}")
        return 1
    
    # Crear decodificador
    decoder = SSTVDecoder(mode=args.mode, sample_rate=sample_rate)
    decoder.debug = args.debug
    
    # Decodificar
    try:
        image = decoder.decode(audio_data)
        if image is None:
            print("Error al decodificar la imagen")
            return 1
        
        # Guardar imagen
        output_path = decoder.save_image(args.output)
        if output_path:
            print(f"Decodificación exitosa. I

Cosmic IoT Communicator - Firmware Principal

C/C++
/*
 * Cosmic IoT Communicator - Firmware Principal
 * 
 * Este firmware gestiona:
 * - Comunicación con el receptor SDR vía USB
 * - Procesamiento de señales SSTV
 * - Comunicación con servicios cloud vía LTE-M/NB-IoT
 * - Control del sistema de iluminación RGB
 * - Gestión de energía
 */

#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/usb/usb_device.h>
#include <zephyr/net/socket.h>
#include <modem/lte_lc.h>
#include <modem/nrf_modem_lib.h>
#include <modem/modem_info.h>
#include <net/aws_iot.h>
#include <date_time.h>
#include <cJSON.h>

LOG_MODULE_REGISTER(cosmic_iot, LOG_LEVEL_INF);

/* Definiciones para LEDs RGB */
#define LED_RED_NODE DT_ALIAS(led0)
#define LED_GREEN_NODE DT_ALIAS(led1)
#define LED_BLUE_NODE DT_ALIAS(led2)
#define LED_STATUS_NODE DT_ALIAS(led3)

/* Definiciones para botones */
#define BUTTON1_NODE DT_ALIAS(sw0)
#define BUTTON2_NODE DT_ALIAS(sw1)
#define BUTTON3_NODE DT_ALIAS(sw2)
#define BUTTON4_NODE DT_ALIAS(sw3)

/* Configuración de AWS IoT */
#define AWS_IOT_CLIENT_ID_LEN 15
#define AWS_IOT_TOPIC_MAX_LEN 100
static char client_id_buf[AWS_IOT_CLIENT_ID_LEN + 1];
static char pub_topic[AWS_IOT_TOPIC_MAX_LEN + 1];
static char sub_topic[AWS_IOT_TOPIC_MAX_LEN + 1];

/* Configuración de GPIO para LEDs */
static const struct gpio_dt_spec led_red = GPIO_DT_SPEC_GET(LED_RED_NODE, gpios);
static const struct gpio_dt_spec led_green = GPIO_DT_SPEC_GET(LED_GREEN_NODE, gpios);
static const struct gpio_dt_spec led_blue = GPIO_DT_SPEC_GET(LED_BLUE_NODE, gpios);
static const struct gpio_dt_spec led_status = GPIO_DT_SPEC_GET(LED_STATUS_NODE, gpios);

/* Configuración de GPIO para botones */
static const struct gpio_dt_spec button1 = GPIO_DT_SPEC_GET(BUTTON1_NODE, gpios);
static const struct gpio_dt_spec button2 = GPIO_DT_SPEC_GET(BUTTON2_NODE, gpios);
static const struct gpio_dt_spec button3 = GPIO_DT_SPEC_GET(BUTTON3_NODE, gpios);
static const struct gpio_dt_spec button4 = GPIO_DT_SPEC_GET(BUTTON4_NODE, gpios);

/* Buffers para datos de imagen */
#define IMAGE_BUFFER_SIZE (64 * 1024)  // 64KB para almacenar imágenes SSTV
static uint8_t image_buffer[IMAGE_BUFFER_SIZE];
static size_t image_size = 0;

/* Estado del sistema */
enum system_state {
    STATE_INIT,
    STATE_CONNECTING_LTE,
    STATE_CONNECTING_CLOUD,
    STATE_IDLE,
    STATE_RECEIVING_SSTV,
    STATE_PROCESSING_IMAGE,
    STATE_UPLOADING_IMAGE,
    STATE_ERROR
};

static enum system_state current_state = STATE_INIT;

/* Prototipos de funciones */
static void led_set_rgb(bool red, bool green, bool blue);
static void led_update_state(void);
static void button_pressed_cb(const struct device *dev, struct gpio_callback *cb, uint32_t pins);
static void aws_iot_event_handler(const struct aws_iot_evt *const evt);
static void modem_configure(void);
static void cloud_connect(void);
static int process_sstv_data(const uint8_t *data, size_t len);
static int upload_image_to_cloud(void);
static void usb_host_init(void);
static void sdr_data_handler(uint8_t *data, size_t len);

/* Estructura para callback de botones */
static struct gpio_callback button_cb_data;

/* Función principal */
void main(void)
{
    int err;

    LOG_INF("Cosmic IoT Communicator iniciando...");
    
    /* Inicializar LEDs */
    if (!device_is_ready(led_red.port) ||
        !device_is_ready(led_green.port) ||
        !device_is_ready(led_blue.port) ||
        !device_is_ready(led_status.port)) {
        LOG_ERR("LEDs no disponibles");
        return;
    }

    gpio_pin_configure_dt(&led_red, GPIO_OUTPUT_INACTIVE);
    gpio_pin_configure_dt(&led_green, GPIO_OUTPUT_INACTIVE);
    gpio_pin_configure_dt(&led_blue, GPIO_OUTPUT_INACTIVE);
    gpio_pin_configure_dt(&led_status, GPIO_OUTPUT_INACTIVE);

    /* Inicializar botones */
    if (!device_is_ready(button1.port) ||
        !device_is_ready(button2.port) ||
        !device_is_ready(button3.port) ||
        !device_is_ready(button4.port)) {
        LOG_ERR("Botones no disponibles");
        return;
    }

    gpio_pin_configure_dt(&button1, GPIO_INPUT | GPIO_PULL_UP);
    gpio_pin_configure_dt(&button2, GPIO_INPUT | GPIO_PULL_UP);
    gpio_pin_configure_dt(&button3, GPIO_INPUT | GPIO_PULL_UP);
    gpio_pin_configure_dt(&button4, GPIO_INPUT | GPIO_PULL_UP);

    gpio_pin_interrupt_configure_dt(&button1, GPIO_INT_EDGE_TO_ACTIVE);
    gpio_pin_interrupt_configure_dt(&button2, GPIO_INT_EDGE_TO_ACTIVE);
    gpio_pin_interrupt_configure_dt(&button3, GPIO_INT_EDGE_TO_ACTIVE);
    gpio_pin_interrupt_configure_dt(&button4, GPIO_INT_EDGE_TO_ACTIVE);

    gpio_init_callback(&button_cb_data, button_pressed_cb,
                      BIT(button1.pin) | BIT(button2.pin) | 
                      BIT(button3.pin) | BIT(button4.pin));
    
    gpio_add_callback(button1.port, &button_cb_data);

    /* Inicializar USB para comunicación con SDR */
    usb_host_init();

    /* Configurar el módem */
    current_state = STATE_CONNECTING_LTE;
    led_update_state();
    modem_configure();

    /* Conectar a la nube */
    current_state = STATE_CONNECTING_CLOUD;
    led_update_state();
    cloud_connect();

    /* Bucle principal */
    current_state = STATE_IDLE;
    led_update_state();
    
    LOG_INF("Cosmic IoT Communicator listo y esperando señales SSTV");
    
    while (1) {
        /* Procesar eventos y tareas según el estado actual */
        switch (current_state) {
            case STATE_IDLE:
                /* Esperar eventos (recepción de datos, comandos, etc.) */
                k_sleep(K_MSEC(100));
                break;
                
            case STATE_RECEIVING_SSTV:
                /* El procesamiento se realiza en callbacks */
                k_sleep(K_MSEC(100));
                break;
                
            case STATE_PROCESSING_IMAGE:
                /* Procesar la imagen recibida */
                LOG_INF("Procesando imagen SSTV...");
                /* Aquí iría el código de procesamiento */
                current_state = STATE_UPLOADING_IMAGE;
                led_update_state();
                break;
                
            case STATE_UPLOADING_IMAGE:
                /* Subir la imagen a la nube */
                LOG_INF("Subiendo imagen a la nube...");
                err = upload_image_to_cloud();
                if (err) {
                    LOG_ERR("Error al subir imagen: %d", err);
                    current_state = STATE_ERROR;
                } else {
                    LOG_INF("Imagen subida correctamente");
                    current_state = STATE_IDLE;
                }
                led_update_state();
                break;
                
            case STATE_ERROR:
                /* Parpadear LED rojo para indicar error */
                gpio_pin_toggle_dt(&led_red);
                k_sleep(K_MSEC(500));
                break;
                
            default:
                k_sleep(K_MSEC(100));
                break;
        }
    }
}

/* Configurar el color del LED RGB */
static void led_set_rgb(bool red, bool green, bool blue)
{
    gpio_pin_set_dt(&led_red, red);
    gpio_pin_set_dt(&led_green, green);
    gpio_pin_set_dt(&led_blue, blue);
}

/* Actualizar LEDs según el estado del sistema */
static void led_update_state(void)
{
    switch (current_state) {
        case STATE_INIT:
            led_set_rgb(true, false, false);  // Rojo
            break;
        case STATE_CONNECTING_LTE:
            led_set_rgb(true, true, false);   // Amarillo
            break;
        case STATE_CONNECTING_CLOUD:
            led_set_rgb(false, false, true);  // Azul
            break;
        case STATE_IDLE:
            led_set_rgb(false, true, false);  // Verde
            break;
        case STATE_RECEIVING_SSTV:
            led_set_rgb(false, true, true);   // Cian
            break;
        case STATE_PROCESSING_IMAGE:
            led_set_rgb(true, false, true);   // Magenta
            break;
        case STATE_UPLOADING_IMAGE:
            led_set_rgb(true, true, true);    // Blanco
            break;
        case STATE_ERROR:
            led_set_rgb(true, false, false);  // Rojo
            break;
    }
}

/* Callback para botones presionados */
static void button_pressed_cb(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
    if (pins & BIT(button1.pin)) {
        LOG_INF("Botón 1 presionado");
        /* Acción para botón 1 */
    }
    
    if (pins & BIT(button2.pin)) {
        LOG_INF("Botón 2 presionado");
        /* Acción para botón 2 */
    }
    
    if (pins & BIT(button3.pin)) {
        LOG_INF("Botón 3 presionado");
        /* Acción para botón 3 */
    }
    
    if (pins & BIT(button4.pin)) {
        LOG_INF("Botón 4 presionado");
        /* Acción para botón 4 */
    }
}

/* Manejador de eventos de AWS IoT */
static void aws_iot_event_handler(const struct aws_iot_evt *const evt)
{
    switch (evt->type) {
        case AWS_IOT_EVT_CONNECTING:
            LOG_INF("Conectando a AWS IoT");
            break;
        case AWS_IOT_EVT_CONNECTED:
            LOG_INF("Conectado a AWS IoT");
            break;
        case AWS_IOT_EVT_DISCONNECTED:
            LOG_INF("Desconectado de AWS IoT");
            break;
        case AWS_IOT_EVT_DATA_RECEIVED:
            LOG_INF("Datos recibidos de AWS IoT");
            /* Procesar datos recibidos */
            break;
        case AWS_IOT_EVT_PUBACK:
            LOG_INF("PUBACK recibido de AWS IoT");
            break;
        case AWS_IOT_EVT_ERROR:
            LOG_ERR("Error de AWS IoT: %d", evt->data.err);
            break;
        default:
            LOG_WRN("Evento AWS IoT desconocido: %d", evt->type);
            break;
    }
}

/* Configurar el módem LTE */
static void modem_configure(void)
{
    int err;

    LOG_INF("Configurando módem...");

    err = nrf_modem_lib_init();
    if (err) {
        LOG_ERR("Error al inicializar la biblioteca del módem: %d", err);
        current_state = STATE_ERROR;
        led_update_state();
        return;
    }

    err = modem_info_init();
    if (err) {
        LOG_ERR("Error al inicializar modem_info: %d", err);
        current_state = STATE_ERROR;
        led_update_state();
        return;
    }

    /* Configurar modo LTE-M/NB-IoT */
    err = lte_lc_init_and_connect();
    if (err) {
        LOG_ERR("Error al conectar a la red LTE: %d", err);
        current_state = STATE_ERROR;
        led_update_state();
        return;
    }

    LOG_INF("Conectado a la red LTE");
}

/* Conectar a AWS IoT */
static void cloud_connect(void)
{
    int err;
    
    /* Generar ID de cliente único basado en IMEI */
    char imei[15];
    err = modem_info_string_get(MODEM_INFO_IMEI, imei, sizeof(imei));
    if (err < 0) {
        LOG_ERR("Error al obtener IMEI: %d", err);
        strcpy(client_id_buf, "cosmic-iot-dev");
    } else {
        snprintf(client_id_buf, sizeof(client_id_buf), "cosmic-%s", imei + 10);
    }
    
    /* Configurar tópicos */
    snprintf(pub_topic, sizeof(pub_topic), "cosmic/devices/%s/data", client_id_buf);
    snprintf(sub_topic, sizeof(sub_topic), "cosmic/devices/%s/commands", client_id_buf);
    
    /* Configurar AWS IoT */
    struct aws_iot_config config = {
        .client_id = client_id_buf,
        .app_id = "cosmic-iot",
        .host_name = CONFIG_AWS_IOT_BROKER_HOST_NAME,
        .port = CONFIG_AWS_IOT_PORT,
    };
    
    /* Inicializar AWS IoT */
    err = aws_iot_init(&config, aws_iot_event_handler);
    if (err) {
        LOG_ERR("Error al inicializar AWS IoT: %d", err);
        current_state = STATE_ERROR;
        led_update_state();
        return;
    }
    
    /* Conectar a AWS IoT */
    err = aws_iot_connect();
    if (err) {
        LOG_ERR("Error al conectar a AWS IoT: %d", err);
        current_state = STATE_ERROR;
        led_update_state();
        return;
    }
    
    /* Suscribirse al tópico de comandos */
    err = aws_iot_subscription_topics_add(sub_topic, 1);
    if (err) {
        LOG_ERR("Error al suscribirse a tópico: %d", err);
        /* No es un error crítico, continuamos */
    }
    
    LOG_INF("Conectado a AWS IoT como '%s'", client_id_buf);
    LOG_INF("Publicando en: %s", pub_topic);
    LOG_INF("Suscrito a: %s", sub_topic);
}

/* Procesar datos SSTV recibidos */
static int process_sstv_data(const uint8_t *data, size_t len)
{
    /* Aquí iría el código para procesar los datos SSTV */
    /* Por simplicidad, solo almacenamos los datos en el buffer */
    
    if (image_size + len > IMAGE_BUFFER_SIZE) {
        LOG_WRN("Buffer de imagen lleno, descartando datos");
        return -ENOMEM;
    }
    
    memcpy(image_buffer + image_size, data, len);
    image_size += len;
    
    /* Comprobar si hemos recibido una imagen completa */
    /* Esto dependería del formato específico de SSTV utilizado */
    /* Por ahora, simplemente asumimos que es completa después de cierto tamaño */
    if (image_size > 30000) {
        LOG_INF("Imagen SSTV completa recibida (%zu bytes)", image_size);
        current_state = STATE_PROCESSING_IMAGE;
        led_update_state();
        return 1;  /* Imagen completa */
    }
    
    return 0;  /* Continuar recibiendo */
}

/* Subir imagen a la nube */
static int upload_image_to_cloud(void)
{
    int err;
    char time_buf[32];
    char *message;
    
    /* Obtener timestamp */
    date_time_now(time_buf, sizeof(time_buf));
    
    /* Crear mensaje JSON */
    cJSON *root = cJSON_CreateObject();
    if (root == NULL) {
        return -ENOMEM;
    }
    
    cJSON_AddStringToObject(root, "device_id", client_id_buf);
    cJSON_AddStringToObject(root, "timestamp", time_buf);
    cJSON_AddStringToObject(root, "type", "sstv_image");
    cJSON_AddNumberToObject(root, "size", image_size);
    
    /* Codificar imagen en base64 */
    /* Aquí iría el código para codificar la imagen */
    /* Por simplicidad, omitimos este paso */
    
    message = cJSON_Print(root);
    cJSON_Delete(root);
    
    if (message == NULL) {
        return -ENOMEM;
    }
    
    /* Publicar mensaje */
    err = aws_iot_send(pub_topic, message, strlen(message));
    free(message);
    
    if (err) {
        LOG_ERR("Error al publicar mensaje: %d", err);
        return err;
    }
    
    /* Reiniciar buffer de imagen */
    image_size = 0;
    
    return 0;
}

/* Inicializar USB Host para SDR */
static void usb_host_init(void)
{
    int err;
    
    /* Inicializar USB */
    err = usb_enable(NULL);
    if (err) {
        LOG_ERR("Error al habilitar USB: %d", err);
        return;
    }
    
    /* Aquí iría el código para inicializar el host USB y detectar el SDR */
    /* Esto requeriría implementación específica según el modelo de SDR */
    
    LOG_INF("USB Host inicializado");
}

/* Manejador de datos del SDR */
static void sdr_data_handler(uint8_t *data, size_t len)
{
    /* Esta función sería llamada cuando se reciben datos del SDR */
    
    if (current_state == STATE_IDLE) {
        /* Detectamos el inicio de una transmisión SSTV */
        LOG_INF("Detectada señal SSTV, iniciando recepción");
        current_state = STATE_RECEIVING_SSTV;
        led_update_state();
        image_size = 0;  /* Reiniciar buffer */
    }
    
    if (current_state == STATE_RECEIVING_SSTV) {
        /* Procesar los datos recibidos */
        int result = process_sstv_data(data, len);
        
        if (result > 0) {
            /* Imagen completa recibida */
            LOG_INF("Recepción SSTV completada");
        } else if (result < 0) {
            /* Error al procesar datos */
            LOG_ERR("Error al procesar datos SSTV: %d", result);
            current_state = STATE_ERROR;
            led_update_state();
        }
    }
}

Credits

Gabriel Alejandro Giraldo Santiago
15 projects • 93 followers
Seeed Ranger, AI and Computer Vision expert for key industries. Mentor and speaker on AI, startups, and no-code. Maker enthusiast.

Comments