WSPR-Protokoll in Python: Praxisbeispiele zur Datenkodierung
Das WSPR-Protokoll nutzt 4-FSK-Modulation, um Signale unterhalb der Rauschschwelle zu übertragen. Jeder Sendezyklus dauert 110,59 Sekunden, belegt eine Bandbreite von nur 5,85 Hz und kodiert die gesamte Nachricht in 50 Bit mittels Faltungscodierung (Code-Rate ½). Sender aktivieren sich jeweils zu Beginn jeder geraden Minute – mit einer Toleranz von ±1 Sekunde – und wählen zufällig eine Frequenz innerhalb eines 200-Hz-Bandes.
Zum Übertragungsinhalt gehören der Rufname der Station, ein vierstelliger QTH-Locator sowie die Sendeleistung in dBm. Empfänger decodieren die Signale, generieren Berichte und laden diese während der verbleibenden 10 Sekunden des Sendezeitfensters auf wsprnet.org hoch.
Physik der Kurzwellenausbreitung
Der KW-Bereich (3–30 MHz) nutzt die Ionosphäre als natürlichen Wellenleiter. Signale brechen in Höhen zwischen 140 und 1000 km: Ein Teil entweicht ins All, ein anderer wird zur Erdoberfläche reflektiert. Mehrfache „Hops“ ermöglichen weltweite Reichweite.
Die ionosphärischen Bedingungen schwanken je nach Sonnenaktivität, Tageszeit und Jahreszeit:
- 80 m: optimal für Nachtbetrieb;
- 40 m: langstreckenfähig nachts, kurze Reichweite tagsüber;
- 20 m: zuverlässig für Langstreckenverbindungen bei Tageslicht;
- 10 m: nur in Phasen maximaler Sonnenaktivität nutzbar.
Wichtig: Direkte Nahbereichsverbindungen sind oft nicht praktikabel, da die Reflexionswinkel zu steil sind.
Schichtenarchitektur des Protokolls
WSPR strukturiert die Verarbeitung in drei logische Ebenen:
- Anwendungsebene: Rufname, Locator, Sendeleistung;
- Darstellungsebene: Komprimierung in 50 Bit mittels eigener Zeichentabellen;
- Verbindungsebene: 4-FSK-Modulation mit Synchronisation und Fehlerkorrektur.
Alle Übertragungen sind auf UTC synchronisiert; Beacons arbeiten pseudozufällig – bis zu sieben Slots pro Stunde sind möglich.
Darstellungsebene: Zeichentabellen und Codecs
Daten werden als Indexwerte in vordefinierten Tabellen kodiert:
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} '
Hilfsfunktionen für die Konvertierung:
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
Abstrakte Schnittstelle: MsgItem
Basis-Klasse für Nachrichtenkomponenten:
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('Validierungsfehler')
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'Nicht unterstützter Datentyp {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
Rufnamenkodierung
Struktur: sechs Positionen, jede mit einer eigenen Zeichentabelle verknüpft:
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
]
Klasse 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)
Beispiel: R9FEU → 260587010 (Binär: 0b1111100010000011111000000010).
QTH-Locator-Kodierung
Der Locator ist ein vierstelliger String (zwei Buchstaben + zwei Ziffern), der ein Gitterfeld von 180 × 180 km repräsentiert. Die Kodierung bildet diesen Wert auf ein globales geodätisches Raster ab.
Wesentliche Erkenntnisse:
- WSPR erfasst Signale unterhalb der Rauschschwelle dank 4-FSK und robuster Fehlerkorrektur;
- UTC-synchronisierte Zweiminuten-Slots vermeiden Kollisionen;
- Aufteilung des 50-Bit-Nutzdatenpakets: Rufname (22 Bit), Locator (15 Bit), Leistung (7 Bit), Nachrichtentyp (1 Bit), CRC (4 Bit);
- Gemeinsam bauen Beacons und Reporter eine Echtzeit-Karte der globalen Ausbreitungsbedingungen auf;
- Eine Python-Implementierung ermöglicht lokales, offline-fähiges Testen und Validieren der Kodierlogik.
— Editorial Team
Noch keine Kommentare.