WSPR Protocol Implementation in Python: Encoding Examples
The WSPR protocol uses 4-FSK modulation to transmit data below the noise floor. Each transmission cycle lasts 110.59 seconds, occupies a 5.85 Hz bandwidth, and packs the message into 50 bits using convolutional coding (rate ½). Transmitters activate at the start of every even-numbered minute—within ±1 second—and randomly select a frequency within a 200 Hz band.
Data includes the station’s callsign, a 4-character QTH locator, and transmit power in dBm. Receivers decode signals, generate reports, and upload them to wsprnet.org during the remaining 10 seconds of the transmission slot.
Physics of HF Propagation
The HF band (3–30 MHz) relies on the ionosphere as a waveguide. Signals refract at altitudes between 140–1000 km: some escape into space, others reflect back to Earth. Multiple “hops” enable global coverage.
Ionospheric conditions vary with solar activity, time of day, and season:
- 80 m: best for nighttime communication;
- 40 m: long-range at night, short-range by day;
- 20 m: reliable long-distance during daylight;
- 10 m: opens only near peak solar activity.
Key nuance: direct short-range paths are often impractical due to steep reflection angles.
Protocol Layering Architecture
WSPR structures processing across three layers:
- Application layer: callsign, locator, power;
- Presentation layer: packing into 50 bits using custom character tables;
- Link layer: 4-FSK modulation with synchronization and error correction.
Transmissions are synchronized to UTC; beacons operate pseudo-randomly—up to 7 slots per hour.
Presentation Layer: Character Tables and Codecs
Data is encoded as indices into predefined tables:
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} '
Conversion helper functions:
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
Abstract Interface: MsgItem
Base class for message components:
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('Validation error')
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'Unsupported data type {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
Callsign Encoding
Structure: six positions, each mapped to a distinct alphabet:
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
]
WSPRCallsign class:
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)
Example: R9FEU → 260587010 (binary: 0b1111100010000011111000000010).
QTH Locator Encoding
The locator is a 4-character string (2 letters + 2 digits), representing an 180×180 km grid square. Its encoding maps to a global geodetic grid.
Key takeaways:
- WSPR detects signals below the noise floor thanks to 4-FSK and robust error correction;
- UTC-synchronized 2-minute slots prevent transmission collisions;
- The 50-bit payload breakdown: callsign (22 bits), locator (15 bits), power (7 bits), message type (1 bit), CRC (4 bits);
- Beacons and reporters collectively build a real-time global propagation map;
- A Python implementation enables local, offline testing and validation of encoding logic.
— Editorial Team
No comments yet.