Powrót do strony głównej

Protokół WSPR: kodowanie znaków wywoławczych w Pythonie

WSPR — protokół do analizy propagacji słabych sygnałów KF za pomocą rozproszonej sieci radiomajaków. Opisana struktura: 4-FSK, 50-bitowe wiadomości, implementacja Python kodeków dla znaków wywoławczych i lokatorów. Przykłady klas MsgItem i WSPRCallsign.

WSPR w Pythonie: kodowanie jonosferowych radiomajaków
Advertisement 728x90

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.

Google AdInline article slot

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:

Google AdInline article slot
  • 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:

Google AdInline article slot
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: R9FEU260587010 (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

Advertisement 728x90

Czytaj dalej