WSPR: implementacja protokołu w Pythonie z przykładami kodowania
Protokół WSPR wykorzystuje modulację 4-FSK do przesyłania danych o mocy poniżej poziomu szumu. Cykl nadawania trwa 110,59 sekundy, szerokość pasma wynosi 5,85 Hz, a wiadomość jest pakowana do 50 bitów przy użyciu kodowania splotowego (stosunek ½). Nadajniki aktywują się na początku każdej parzystej minuty z dokładnością do 1 sekundy, wybierając losową częstotliwość w paśmie 200 Hz.
Dane zawierają znak wywoławczy stacji, czteroznakowy lokalizator QTH oraz moc w dBm. Odbiorniki dekodują sygnały, generują raporty i wysyłają je na stronę wsprnet.org w ostatnich 10 sekundach okienka transmisji.
Fizyczne zasady propagacji
Zakres fal krótkich (3–30 MHz) wykorzystuje jonosferę jako falowód. Sygnały załamują się na wysokości 140–1000 km: część uchodzi w przestrzeń kosmiczną, część odbija się z powrotem ku Ziemi. Wielokrotne „skoki” (hops) zapewniają globalne zasięgi.
Stan jonosfery zależy od aktywności słonecznej, pory dnia i pory roku:
- pasmo 80 m: komunikacja nocna;
- pasmo 40 m: dalekie połączenia w nocy, bliższe w dzień;
- pasmo 20 m: dalekie połączenia w ciągu dnia;
- pasmo 10 m: tylko przy maksymalnej aktywności słonecznej.
Uwaga: bezpośrednie połączenia na krótkie odległości są utrudnione przez ostry kąt odbicia.
Struktura protokołu według warstw
WSPR dzieli przetwarzanie na warstwy:
- Aplikacyjną: znak wywoławczy, lokalizator, moc;
- Prezentacji: pakowanie do 50 bitów przy użyciu tablic znaków;
- Łącza: modulacja 4-FSK z synchronizacją i korekcją błędów.
Transmisje są zsynchronizowane z czasem UTC; sygnały beaconowe działają losowo — do 7 okienek na godzinę.
Warstwa prezentacji: tablice znaków i kodeki
Dane są kodowane jako indeksy w tabelach:
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} '
Funkcje konwersji:
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
Abstrakcyjny interfejs MsgItem
Klasa bazowa dla elementów wiadomości:
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('Błąd walidacji')
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'Nieobsługiwany typ danych {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
Kodowanie znaku wywołkowego
Schemat: 6 pozycji z różnymi zestawami znaków:
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
]
Klasa 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)
Przykład: R9FEU → 260587010 (0b1111100010000011111000000010).
Kodowanie lokalizatora QTH
Lokalizator składa się z 4 znaków (2 litery + 2 cyfry) i odpowiada kwadratowi o wymiarach 180×180 km. Formuła kodowania uwzględnia globalną siatkę współrzędnych.
Na co zwrócić uwagę:
- WSPR wykrywa sygnały poniżej poziomu szumu dzięki modulacji 4-FSK i korekcji błędów;
- Synchronizacja z czasem UTC i dwuminutowe okienka eliminują kolizje;
- Pakowanie do 50 bitów: znak wywoławczy (22 bity), lokalizator (15 bitów), moc (7 bitów), typ (1 bit), CRC (4 bity);
- Sygnały beaconowe i raporty tworzą globalną mapę propagacji;
- Implementacja w Pythonie umożliwia lokalne testowanie kodowania.
— Editorial Team
Brak komentarzy.