Implementación del protocolo WSPR en Python: ejemplos de codificación
El protocolo WSPR utiliza modulación 4-FSK para transmitir datos por debajo del nivel de ruido. Cada ciclo de transmisión dura 110,59 segundos, ocupa un ancho de banda de 5,85 Hz y empaqueta el mensaje en 50 bits mediante codificación convolucional (tasa ½). Los transmisores se activan al inicio de cada minuto par —con una tolerancia de ±1 segundo— y seleccionan aleatoriamente una frecuencia dentro de una banda de 200 Hz.
Los datos incluyen el indicativo de la estación, un localizador QTH de 4 caracteres y la potencia de transmisión en dBm. Los receptores descodifican las señales, generan informes y los suben a wsprnet.org durante los 10 segundos restantes de la ventana de transmisión.
Física de la propagación en HF
La banda HF (3–30 MHz) aprovecha la ionosfera como guía de ondas. Las señales se refractan a altitudes entre 140 y 1000 km: algunas escapan al espacio, otras regresan a la Tierra. Varias «rebotadas» permiten cobertura global.
Las condiciones ionosféricas varían según la actividad solar, la hora del día y la estación:
- 80 m: ideal para comunicaciones nocturnas;
- 40 m: largo alcance por la noche, corto por el día;
- 20 m: fiable para largas distancias en horario diurno;
- 10 m: solo disponible cerca del máximo de actividad solar.
Matiz clave: las rutas directas a corta distancia suelen ser inviables debido a ángulos de reflexión demasiado pronunciados.
Arquitectura en capas del protocolo
WSPR estructura su procesamiento en tres capas:
- Capa de aplicación: indicativo, localizador y potencia;
- Capa de presentación: empaquetado en 50 bits usando tablas de caracteres personalizadas;
- Capa de enlace: modulación 4-FSK con sincronización y corrección de errores.
Las transmisiones están sincronizadas con UTC; las balizas operan de forma pseudoaleatoria —hasta 7 ventanas por hora.
Capa de presentación: tablas de caracteres y códecs
Los datos se codifican como índices en tablas predefinidas:
import string
CHAR_TABLE_NUMERIC = string.digits
CHAR_TABLE_LETTERS = string.ascii_uppercase
WSPR_CHAR_TABLE_ALPHANUM = f'{CHAR_TABLE_NUMERIC}{CHAR_TABLE_LETTERS}'
WSPR_CHAR_TABLE_ALPHANUM_SPACE = f'{CHAR_TABLE_NUMERIC}{CHAR_TABLE_LETTERS} '
Funciones auxiliares de conversión:
def nchar(c: str, table: str) -> int:
return table.find(c)
def charn(c: int, table: str) -> str:
return table[c]
def ct_decode(ct: str, val: int, l: int) -> str:
s = ''
ct_l = len(ct)
for i in range(l):
s = charn(val % ct_l, ct) + s
val //= ct_l
return s
Interfaz abstracta: MsgItem
Clase base para componentes del mensaje:
from abc import ABCMeta, abstractmethod
import typing
class MsgItem(metaclass=ABCMeta):
__slots__ = ('val_str', 'val_int')
def __init__(self, val: typing.Union[str, int]):
if not self.validate(val):
raise ValueError('Error de validación')
if isinstance(val, str):
self.val_str = val.strip()
self.val_int = self.to_int()
elif isinstance(val, int):
self.val_int = val
self.val_str = self.to_str()
else:
raise TypeError(f'Tipo de dato no soportado {type(val)}')
@classmethod
@abstractmethod
def _validate_str(cls, val: str) -> bool:
...
@classmethod
@abstractmethod
def _validate_int(cls, val: int) -> bool:
...
@classmethod
def validate(cls, val: typing.Union[str, int]) -> bool:
if isinstance(val, str):
return cls._validate_str(val)
elif isinstance(val, int):
return cls._validate_int(val)
return False
@abstractmethod
def to_int(self) -> int:
...
@abstractmethod
def to_str(self) -> str:
...
@property
def as_str(self):
return self.val_str
@property
def as_int(self):
return self.val_int
Codificación del indicativo
Estructura: seis posiciones, cada una mapeada a un alfabeto distinto:
WSPR_BASECALL_CHAR_MAP = [
WSPR_CHAR_TABLE_ALPHANUM_SPACE,
WSPR_CHAR_TABLE_ALPHANUM,
WSPR_CHAR_TABLE_NUMERIC,
WSPR_CHAR_TABLE_LETTERS_SPACE,
WSPR_CHAR_TABLE_LETTERS_SPACE,
WSPR_CHAR_TABLE_LETTERS_SPACE
]
Clase WSPRCallsign:
class WSPRCallsign(MsgItem):
@classmethod
def _validate_str(cls, val: str) -> bool:
return len(val) <= 6
@classmethod
def _validate_int(cls, val: int) -> bool:
return val < 262177560
def to_int(self) -> int:
return hash(self)
def to_str(self) -> str:
return ct_map_decode(WSPR_BASECALL_CHAR_MAP, self.val_int).strip()
@staticmethod
def _normalize_cs(cs: str) -> str:
return ' ' * (6 - len(cs)) + cs
def __hash__(self):
cs_norm = self._normalize_cs(self.val_str)
return ct_map_encode(WSPR_BASECALL_CHAR_MAP, cs_norm)
Ejemplo: R9FEU → 260587010 (binario: 0b1111100010000011111000000010).
Codificación del localizador QTH
El localizador es una cadena de 4 caracteres (2 letras + 2 dígitos), que representa un cuadrado de rejilla de 180×180 km. Su codificación se mapea sobre una rejilla geodésica global.
Conclusiones clave:
- WSPR detecta señales por debajo del nivel de ruido gracias a la modulación 4-FSK y una corrección de errores robusta;
- Las ventanas de transmisión sincronizadas con UTC cada dos minutos evitan colisiones;
- Desglose del payload de 50 bits: indicativo (22 bits), localizador (15 bits), potencia (7 bits), tipo de mensaje (1 bit), CRC (4 bits);
- Balizas e informadores construyen colectivamente un mapa en tiempo real de la propagación global;
- Una implementación en Python permite pruebas y validación locales, sin conexión, de la lógica de codificación.
— Editorial Team
Aún no hay comentarios.