Implémentation du protocole WSPR en Python : exemples de codage
Le protocole WSPR utilise une modulation FSK à 4 niveaux pour transmettre des données sous le seuil du bruit. Chaque cycle d’émission dure 110,59 secondes, occupe une bande passante de 5,85 Hz et compresse le message en 50 bits grâce à un codage convolutif (taux ½). Les émetteurs s’activent au début de chaque minute paire — avec une tolérance de ±1 seconde — et choisissent aléatoirement une fréquence dans une plage de 200 Hz.
Les données transmises comprennent le indicatif d’appel de la station, un localisateur QTH à 4 caractères et la puissance d’émission en dBm. Les récepteurs décodent les signaux, génèrent des rapports et les téléchargent sur wsprnet.org pendant les 10 secondes restantes de la fenêtre d’émission.
Physique de la propagation HF
La bande HF (3–30 MHz) utilise l’ionosphère comme guide d’ondes. Les signaux se réfractent à des altitudes comprises entre 140 et 1000 km : certains s’échappent dans l’espace, d’autres reviennent vers la Terre. Plusieurs « sauts » permettent une couverture mondiale.
Les conditions ionosphériques varient selon l’activité solaire, l’heure de la journée et la saison :
- 80 m : idéal pour les communications nocturnes ;
- 40 m : portée longue la nuit, courte en journée ;
- 20 m : fiabilité élevée sur de longues distances en plein jour ;
- 10 m : ouverte uniquement aux périodes de forte activité solaire.
Nuance essentielle : les trajets directs à courte portée sont souvent impraticables en raison d’angles de réflexion trop raides.
Architecture en couches du protocole
WSPR organise le traitement en trois couches :
- Couche application : indicatif d’appel, localisateur, puissance ;
- Couche présentation : compression en 50 bits via des tables de caractères personnalisées ;
- Couche liaison : modulation FSK à 4 niveaux avec synchronisation et correction d’erreurs.
Les émissions sont synchronisées sur le temps universel coordonné (UTC) ; les balises fonctionnent de façon pseudo-aléatoire — jusqu’à 7 créneaux par heure.
Couche présentation : tables de caractères et codecs
Les données sont encodées sous forme d’indices dans des tables prédéfinies :
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} '
Fonctions d’aide pour la conversion :
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
Interface abstraite : MsgItem
Classe de base pour les composants du message :
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('Erreur de validation')
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'Type de donnée non pris en charge : {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
Encodage de l’indicatif d’appel
Structure : six positions, chacune associée à un alphabet spécifique :
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
]
Classe 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)
Exemple : R9FEU → 260587010 (binaire : 0b1111100010000011111000000010).
Encodage du localisateur QTH
Le localisateur est une chaîne de 4 caractères (2 lettres + 2 chiffres), représentant une maille géographique de 180 × 180 km. Son encodage correspond à une grille géodésique mondiale.
Points clés :
- WSPR détecte des signaux sous le seuil du bruit grâce à la modulation FSK à 4 niveaux et à une correction d’erreurs robuste ;
- Les créneaux d’émission synchronisés sur l’UTC (toutes les 2 minutes) évitent les collisions ;
- Répartition des 50 bits : indicatif (22 bits), localisateur (15 bits), puissance (7 bits), type de message (1 bit), CRC (4 bits) ;
- Balises et rapporteurs construisent collectivement une carte mondiale en temps réel de la propagation ;
- Une implémentation Python permet des tests et validations locaux, hors ligne, de la logique d’encodage.
— Editorial Team
Aucun commentaire pour le moment.