WSPR — цифровой протокол, разработанный Джо Тейлором (K1JT) в 2008-2009 годах, с целью исследования распространения радиосигналов от коротковолновых передатчиков малой и сверхмалой мощности. В этой статье будут рассмотрены устройство и принципы работы протокола.Статья может быть интересна радиолюбителям, как знакомым, так и не знакомым с WSPR, а также тем, кто хочет понять устройство этого протокола.

WSPR — цифровой протокол, разработанный Джо Тейлором (K1JT) в 2008-2009 годах, с целью исследования распространения радиосигналов от коротковолновых передатчиков малой и сверхмалой мощности. В этой статье будут рассмотрены устройство и принципы работы протокола.
Статья может быть интересна радиолюбителям, как знакомым, так и не знакомым с WSPR, а также тем, кто хочет понять устройство этого протокола.
ВведениеДальняя связь на КВ диапазоне (3 МГц-30 МГц) возможна благодаря использованию эффекта отражения и распространения радиоволн в ионосфере, способной выступать в качестве волновода. Достигая слоя ионизированного газа (от 140 до 1000 км), радиоволна, подобно свету, подвергается преломлению (рисунок 1), — часть энергии уходит выше, за пределы ионосферы, другая часть отражается обратно в сторону поверхности земли, где уже может быть зафиксирована приемными устройствами. При определенных условиях поверхность планеты может выступать в качестве пассивного отражателя и отразить сигнал обратно, в сторону ионосферы, — т.н. "прыжок" или "скачок" (англ. bounce или hop), — что позволяет радиосигналу распространяться на очень большие расстояния, вплоть до того, что сигнал, обогнув планету, может дойти до той точки, из которой он был отправлен.
Радиолюбители с хорошими антеннами и мощными передатчиками нередко рассказывают, как переходя на прием, на долю секунды слышат конец своей собственной передачи, то есть принимают свой же собственный радиосигнал, проделавший путь вокруг планеты.

Рисунок 1. Распространение радиоволн в ионосфере.
Условия прохождения и распространения радиоволн в ионосфере во многом определяется ее состоянием, которое в свою очередь зависит от природных факторов, таких как солнечная активность (в т.ч. магнитные бури), время года, время суток. Так, например, участки из нижней части КВ диапазона обеспечивают связь в темное время суток, — например диапазон 80 м, высокочастотные, такие как 20 м, — днем; диапазон 40 м ночью позволяет проводить дальние связи, днем — ближние; диапазон 10 м при максимальной солнечной активности позволяет осуществлять дальние связи в дневное время суток.
Необходимо также упомянуть особенность КВ-диапазона, которая заключается в том, что на нем не всегда возможна связь на близкие расстояния, т.к. в зоне прямой видимости распространение радиоволн не всегда осуществимо, а угол отражения от ионосферы достаточно острый, что делает невозможным использовать ионосферу в качестве отражателя.
WSPR (созвучно со словом whisper, рус. шепот) — Weak Signal Propagation Reporter, цифровой радиолюбительский протокол радиосвязи, основное назначение которого заключается в автоматизации процесса исследования состояния ионосферы путем анализа прохождения радиосигналов.
Концепция заключается в распределении по планете большого количества любительских передатчиков-маяков и приемников, принимающих и анализирующих сигналы от этих маяков, формируя при этом общий журнал с информацией о качестве приема.
Так как протокол в основном рассчитан на анализ прохождения, а не на двусторонний радиообмен, — передаваемой информацией являются: радиолюбительский позывной передатчика, локатор местоположения и мощность передающего устройства.
WSPR также может применяться радиолюбителями в качестве инструмента для оценки качества антенн и прогнозирования территорий для проведения любительских связей (QSO).
Главной особенностью протокола является способность детектировать сигналы, мощность которых намного ниже уровня шума, а также обеспечение достаточной устойчивости к помехам и затуханиям.
Общие технические характеристикиЦикл передачи: 110.59 сек;
Тип модуляции: 4-FSK;
Механизм коррекции ошибок: сверточный код (½);
Ширина полосы: 5.85 Гц:
Размер сообщения: 50 бит.
WSPR реализует достаточно простую структуру, при формировании FSK-символов данные проходят процесс кодирования помехоустойчивым алгоритмом и добавлением синхронизирующего вектора.
Общая схема протокола приведена на рисунке 2. Разделение на уровни модели OSI условное.

Рисунок 2. Общая схема протокола (абстракция по уровням OSI условная).
На рисунке 2 приведена общая схема протокола WSPR при отправке сигнала.
Протокол прикладного уровняWSPR является радиолюбительским протоколом, но не рассчитан на проведение полноценных сеансов радиосвязи между радиолюбителями (QSO). Для идентификации принимаемых сигналов, в качестве пользовательских данных протокол передает позывной станции, QTH-локатор — квадрат локации, в которой расположен передатчик, аналогично протоколу FT8 рассмотренному в соответствующей статье.
Примечание: протокол FT8 был разработан позднее протокола WSPR и заимствовал из него многие принципы и наработки.
В перечень передаваемых данных также входит значение мощности передатчика, определяемое в dBm (децибел на милливатт).
Участники WSPR делятся на две категории: маяки и репортеры.
Так как время передачи сигнала составляет 110 секунд, а мощности передатчиков и соотношения сигнал/шум могут иметь очень низкие значения, протокол имеет строгую привязку ко времени и временным слотам. В начале каждой четной минуты маяки начинают отправку сигнала в эфир, при этом допустимая погрешность во времени не должна превышать 1 секунды. Сигнал передается в течении 110.59 секунд, в это же время приемники принимают и накапливают все принимаемые в полосе частот сигналы. В оставшиеся ~10 секунд интервала происходит анализ и демодуляция сигнала с последующей отправкой отчета на сервер wsprnet.org.
Помимо этого, диапазон частот, в котором осуществляется прием и передача сигналов, ограничена полосой шириной в 200 Гц; для избежания ситуации наложения сигналов, каждый из передатчиков выбирает случайную центральную частоту из этого диапазона (частота, относительно которой формируется FSK-сигнал). Помимо этого, маяки работающие на постоянной основе, могут выбирать случайным образом тайм-слоты для передачи (например не более 7 сеансов передач в час).
Уровень представленияНа уровне представления происходит уплотнение передаваемых данных в 50-битное представление. Алгоритм упаковки во многом похож на FT8, с той лишь разницей, что используются немного другие алфавиты и способ кодирования позывного.
Алфавиты WSPR представляют собой конкатенацию из списка цифр и ASCII-символов.
import string
CHAR_TABLE_NUMERIC = string.digits
CHAR_TABLE_LETTERS = string.ascii_uppercase
WSPR_CHAR_TABLE_NUMERIC = CHAR_TABLE_NUMERIC
WSPR_CHAR_TABLE_LETTERS = CHAR_TABLE_LETTERS
WSPR_CHAR_TABLE_LETTERS_SPACE = f"{WSPR_CHAR_TABLE_LETTERS} "
WSPR_CHAR_TABLE_ALPHANUM = f"{WSPR_CHAR_TABLE_NUMERIC}{WSPR_CHAR_TABLE_LETTERS}"
WSPR_CHAR_TABLE_ALPHANUM_SPACE = f"{WSPR_CHAR_TABLE_NUMERIC}{WSPR_CHAR_TABLE_LETTERS_SPACE}"Аналогично протоколу FT8, при кодировании символы заменяются своими индексами в соответствующих алфавитах; при декодировании происходит обратный процесс.
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
def ct_map_decode(ct_map: typing.List[str], val: int) -> str:
s = ""
for ct_len, ct in map(lambda ct: (len(ct), ct), reversed(ct_map)):
s = charn(val % ct_len, ct) + s
val //= ct_len
return sКодирование сообщенияАналогично FT8 в WSPR данные сообщения подвергаются процессу уплотнения бит.
from abc import ABCMeta, abstractmethod
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
...Абстрактный MsgItem реализует общий интерфейс кодека для каждой из частей сообщения, где упакованные данные преобразуются в целые числа и наоборот.
Кодирование позывного осуществляется по схеме:
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
]До 3-го символа префикс длиной от двух до трех символов, суффикс позывного до трех символов. Символ "пробел" используется для выравнивания, если длина позывного меньше ожидаемой.
Перевод позывного из текстовой формы в упакованную и наоборот, реализуется классом 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)Так, после кодирования, позывной R9FEU будет иметь значение 260587010 (0b1111100010000011111000000010 в двоичном представлении).
В отличие от FT8, в WSPR локатор кодируется как номер квадрата на сетке размером 180*180 (по 18 буквенных квадратов в каждом из которых по 10 цифровых квадратов на сторону, рисунки 3 и 4).

Рисунок 3: Распределение буквенных квадратов на карте мира.

Рисунок 4: Распределение числовых квадратов внутри буквенных.
class WSPRGrid(MsgItem):
@classmethod
def _validate_str(cls, val: str) -> bool:
return len(val) == 4 and ct_validate_map(WSPR_GRID_CHAR_MAP, val)
@classmethod
def _validate_int(cls, val: int) -> bool:
return val < 32400
def to_int(self) -> int:
loc = self.val_str
ct_num = WSPR_CHAR_TABLE_NUMERIC
ct_char = WSPR_CHAR_TABLE_LETTERS
val = (179 - 10 * nchar(loc[0], ct_char) - nchar(loc[2], ct_num)) * 180
val += nchar(loc[1], ct_char) * 10
val += nchar(loc[3], ct_num)
return val
def to_str(self) -> str:
loc = self.val_int
latitude = (loc % 180) - 90
longitude = (loc // 180) * 2 - 178
longitude = (longitude + 180) % 360 - 180
lat = int(24.0 * (latitude + 90))
lon = int(12.0 * (180.0 - longitude))
lo1, lo2 = divmod(lon, 240)
la1, la2 = divmod(lat, 240)
ct_num = WSPR_CHAR_TABLE_NUMERIC
ct_char = WSPR_CHAR_TABLE_LETTERS
val = charn(lo1, ct_char)
val += charn(la1, ct_char)
val += charn(lo2 // 24, ct_num)
val += charn(la2 // 24, ct_num)
return valТак, квадрат LO87 (квадрат с углом в точке с координатами 57°30'00.0"N 57°00'00.0"E) будет представлен в виде значения 11127.
Значение мощности записывается в виде целого числа, соответствующего dBm (децибел на милливатт); так как соотношение мощностей в 2 раза соответствует 3 децибелам, значение мощности в WSPR кратно 3; таким образом значение в 57 dBm соответствует мощности передатчика в 100 ватт, а 60 dBm — 1000 ватт.
В WSPR промежуточные значения округляются в ближайшую к кратной мощности сторону:
@staticmethod
def _normalize_dBm(dBm: int) -> int:
corr = [0, -1, 1, 0, -1, 2, 1, 0, -1, 1]
if dBm < 0:
return 0
elif dBm > 60:
return 60
return dBm + corr[dBm % 10]Минимальное и максимальное значения мощности передатчика можно задать 1 милливатт (0 dBm) и 1000 ватт (60 dBm) соответственно.
Кодек сообщенияКласс WSPRMessage реализует кодек WSPR-сообщения.
class WSPRMessage:
__slots__ = ("callsign", "loc", "dBm")
@staticmethod
def _normalize_dBm(dBm: int) -> int:
...
def __str__(self):
return f"{self.callsign} {self.loc} {self.dBm}"
def encode(self, **kwargs) -> typing.ByteString:
add = 0
pref = self.callsign.as_int
suff = (self.loc.as_int << 7) | (self.dBm + 64 + add)
items = [
byte(pref >> 20),
byte(pref >> 12),
byte(pref >> 4),
byte(((pref & 0x0f) << 4) | ((suff >> 18) & 0x0f)),
byte(suff >> 10),
byte(suff >> 2),
byte((suff & 0x03) << 6),
0,
0,
0,
0,
]
return bytearray(b for b in items)
@classmethod
def decode(cls, payload: typing.ByteString, **kwargs) -> AbstractMessage:
b = payload[0]
pref = b << 20
b = payload[1]
pref += b << 12
b = payload[2]
pref += b << 4
b = payload[3]
pref += (b >> 4) & 15
suff = (b & 15) << 18
b = payload[4]
suff += b << 10
b = payload[5]
suff += b << 2
b = payload[6]
suff += (b >> 6) & 3
cs = WSPRCallsign(pref)
grid = WSPRGrid(suff >> 7)
dbm = (suff & 0x7f) - 64
return cls(cs, grid, dbm)Функция encode конкатенирует в массив байт числовые представления позывного, локатора и мощности.
msg = WSPRMessage(WSPRCallsign("R9FEU"), WSPRGrid("LO87"), 50)
payload = msg.encode()Так, сообщение "R9FEU LO87 50" будет представлять собой запись:
Позывной R9FEU: 260587010 (0b1111100010000011111000000010)
Локатор LO87: 11127 (0b10101101110111)
Мощность 50: 114 (0b1110010)
Результат: f8 83 e0 25 6e fc 80 00 00 00 00.
Статичный метод decode реализует обратную задачу, возвращая, как результат, объект WSPRMessage с данными сообщения.
На данном уровне данные сообщения подвергаются помехоустойчивому кодированию сверточным кодом и интерливингу получаемых символов.
Сверточный кодПередаваемый в радиоэфире сигнал подвергается воздействию шумов, помех, быстрым и медленным затуханиям, многолучевому распространению и т.д., что может сильно ослаблять и искажать сигнал, и на принимающей стороне сообщение может быть не принято или принято с ошибками. Для обеспечения надежной передачи сообщения к данным применяется механизм коррекции ошибок, задача которого нивелировать искажения и потерю данных.
В отличие от протоколов семейства FTX, в которых применяется LDPC-код, в WSPR используется более простой метод — сверточный код (convolutional code).
Сверточный код относится к классу корректирующих кодов на основе свертки входящей последовательности битов данных с импульсной характеристикой кодера, реализуемый через регистр сдвига, являющегося частным случаем памяти. Таким образом, значения бит выходной последовательности рассчитываются на основе текущего входного бита и нескольких предыдущих, что повышает эффективность коррекции ошибок при передаче данных.
Математически, сверточный код описывается как линейный код с памятью над конечным полем (где
,
— простое число,
— натуральное; в случае с битами
), в котором кодовая последовательность
формируется как дискретная свертка информационной последовательности
с набором векторно-значных коэффициентов
(порождающие полиномы).
В каждый момент времени кодовый вектор
выражается линейной комбинацией текущего и
предыдущих информационных векторов
с коэффициентами из
.
Это задает линейные рекуррентные соотношения и переводит код во множество полубесконечных последовательностей, образующих аддитивную подгруппу в . Аддитивная группа
относительно операции сложения по модулю
изоморфна конечной абелевой группе
, из чего следует, что вся кодовая последовательность может рассматриваться как элемент бесконечного прямого произведения таких аддитивных групп.
При фиксированной длине последовательности код можно рассматривать как линейное пространство в
, структуру которого можно интерпретировать как конечную абелеву группу относительно операции сложения по компонентам.
Отношение числа информационных символов на входе кодера к числу кодовых символов на выходе за один такт определяет скорость кода: (где
— число информационных символов,
— число кодовых, при этом
), эта величина следует из размера линейного преобразования
, и определяет степень избыточности и предельную плотность информации в кодовой последовательности.
Для двоичных данных конечное поле процесс кодирования заключается в том, что в каждый момент кодирования бита входных данных осуществляется изменение состояния регистров путем сдвига предыдущего значения; значения выходных бит рассчитываются путем суммирования по модулю 2 (оператор XOR) текущего и предыдущего состояний регистров с порождающими полиномами.
Графически описанный алгоритм представлен на рисунке 5, где блоки n, n-1, n-2 — значения в сдвиговых регистрах, а сумматор — сложение по модулю 2 (XOR).

Рисунок 5: Упрощенное представление сверточного кодера со скоростью ½.
Примечание: сверточное кодирование использовался NASA для коррекции ошибок в каналах связи с Voyager-1/2.
В протоколе WSPR используется сверточный код со скоростью R=½ и полиномами Лейланда-Лашбо (Layland-Lushbaugh) — 0xf2d05351 и 0xe4613c47.
Примечание: это те самые полиномы, которые применялись в Deep Space Network для обеспечения надежной связи с космическими аппаратами в дальнем космосе, в миссиях Mariner и Pioneer. В миссиях Helios и более поздних Pioneer NASA применяли полиномы со значениями 0xbbef6bb7, 0xbbef6bb5. Они были менее надежнее полиномов Лейланда-Лашбо, но позволяли декодировать сигнал намного быстрее, в том числе в полуручном режиме, что было критически важным фактором при получении данных телеметрии.
WSPR_CONV_SYMBOLS = 176
WSPR_CONV_POLY = [0xf2d05351, 0xe4613c47]
def convolutional_encode(payload: typing.ByteString) -> npt.NDArray:
k = 0
state = 0
symbols = np.zeros(WSPR_CONV_SYMBOLS, dtype=np.uint8)
for p in payload:
for i in range(7, -1, -1):
state = (state << 1) | ((p >> i) & 1)
for poly in WSPR_CONV_POLY:
n = state & poly
even = 0
while n:
even = 1 - even
n &= n - 1
symbols[k] = even
k += 1
return symbolsИмпульсная характеристика кодера на цифровую дельта-функцию (отклик на единичный отсчет):
convolutional_encode(b"\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")Результат: 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.
То есть первый бит оказал влияние на результирующие биты в позициях: 1, 3, 5, 8, 12, 13, 16, 18, 21, 23, 24, 25, 27, 28, 33, 40, 43, 44, 45, 46, 50, 53, 56, 58, 59, 60, 61, 62, 63.
Сообщение "R9FEU LO87 50" (8 83 e0 25 6e fc 80 00 00 00 00) после обработки алгоритмом сверточного кодирования принимает вид: 1 1 1 0 1 1 1 1 0 1 1 0 0 0 0 1 0 0 1 0 1 1 0 0 0 1 0 0 0 0 1 1 0 0 1 1 1 1 1 0 0 0 1 1 0 1 0 1 0 1 1 0 1 0 0 0 1 0 0 1 1 0 1 1 0 0 0 0 1 0 1 1 0 1 0 1 0 1 0 0 0 0 0 1 1 0 0 0 1 0 0 1 0 1 1 0 1 0 0 1 1 1 1 1 0 0 1 0 0 1 0 0 0 0 1 0 1 0 0 0 0 0 1 0 1 1 0 1 0 0 1 0 0 1 0 1 0 1 0 0 1 0 1 0 0 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.
Передаваемый в эфире сигнал подвергается влиянию помех и искажениям, которые вносят ошибки в передаваемые символы. Как правило такие ошибки возникают пачками (burst error), искажая тем самым несколько подряд идущих бит данных, ошибки в которых далее будет исправлять код коррекции ошибок.
Коды коррекции ошибок хорошо справляются с одиночными ошибками, но при возникновении пачки ошибок сразу в нескольких идущих друг за другом бит данных, сделают процесс восстановления данных слишком ресурсоемким или невозможным. Для снижения влияния таких ошибок и повышения помехозащищенности и надежности передаваемых данных, применяется процесс перемешивания данных — интерливинг.
Интерливинг (перемешивание/перемежение) — процесс перераспределения бит данных в сообщении таким образом, что подряд идущие биты данных разносятся на некоторое расстояние друг от друга. На передающей стороне происходит перемешивание бит данных перед отправкой; на принимающей стороне принятые биты подвергаются обратной перестановке — деинтерливингу, восстанавливая исходную последовательность бит. Таким образом, при после деинтерливигна, пачка ошибок в подряд идущих битах будет распределена в виде ошибок в нескольких случайных местах, что позволит кодам коррекции ошибок с большей долей вероятности скорректировать данные.
Примечание: интерливинг также применяется в протоколе FT4.
На принимающей стороне во время обработки сигнала, декодер осуществляет поиск признаков искомого сигнала методом кросс-корреляции через согласованный фильтр, для этого часть передаваемых бит перед передачей объединяется с синхронизирующим вектором.
WSPR_ND = 162
WSPR_PR3 = np.array([
1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0,
0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1,
0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1,
1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1,
0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0,
0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1,
0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0,
0, 0
])Вектор синхронизации WSPR_PR3 представляет собой массив однобитных значений, равный по длине количеству тонов пакета данных (WSPR_ND). Синхронизирующий вектор формирует выходную последовательность тонов таким образом, что биты данных, участвующих в синхронизации, всегда будут находиться в одном и том же диапазоне тонов. Так как синхронизирующий вектор одинаков на принимающей и передающей стороне, декодер осуществляет поиск и оценку сигнала, используя вектор синхронизации как область частот для двух тонов, в пределах которой обязательно должен располагаться бит данных.
def wspr_encode(payload: typing.ByteString) -> typing.Iterator[int]:
symbols = convolutional_encode(payload)
tones = np.zeros(WSPR_ND, dtype=np.uint8)
for i in range(WSPR_ND):
p = -1
k = 0
j0 = 0
while p != i:
for j in range(8):
j0 = (((k >> j) & 1) | (j0 << 1)) & 0xff
if j0 < WSPR_ND:
p += 1
k += 1
tones[j0] = WSPR_PR3[j0] | symbols[i] << 1
for tone in tones:
yield toneФункция-генератор wspr_encode принимает на вход данные в двоичном формате, вызывает ранее рассмотренную функцию сверточного кодирования и, рассчитывая значение j0 определяющее позицию размещения очередного бита данных в последовательности тонов. Значение тона рассчитывается как пара бит, один из которых — это значение из вектора синхронизации WSPR_PR3.
Так, сообщение "R9FEU LO87 50" будет представлено в виде 162 тонов, индексы которых: 3 1 0 2 2 2 0 2 1 0 0 0 3 1 3 0 2 0 1 2 0 1 0 3 1 1 3 2 2 0 0 2 0 2 1 2 0 1 0 3 0 0 0 2 2 0 1 2 3 1 2 2 1 1 0 1 0 2 2 3 1 0 1 0 2 2 2 3 1 0 3 0 1 2 1 0 1 2 2 3 0 0 1 0 1 3 2 0 2 3 1 0 3 0 1 2 2 0 3 0 2 2 0 2 3 2 0 1 2 2 3 3 3 2 3 1 2 0 3 1 2 3 2 0 0 1 1 1 2 0 2 0 0 3 2 1 0 2 3 3 0 0 0 2 2 2 2 1 1 0 1 2 1 1 0 0 2 1 3 2 2 0.
На канальном уровне происходит синтезирование сигнала, на основе данных, сформированных на предыдущем уровне. В результате формируется дискретный сигнал, готовый для передачи по аналоговому (звуковому) каналу связи.
4-FSK манипуляцияДля формирования сигнала, протокол WSPR использует частотную манипуляцию с когерентной фазой, как и в протоколах семейства FTX и Q65.
def synth_fsk(
tones: npt.NDArray[np.uint8], sample_rate: int,
samples_per_symbol: int,
f0: float, bandwidth: float) -> npt.NDArray[np.float64]:
dt = 1.0 / sample_rate
phase = 0.0
signal = np.zeros(tones.shape[0] * samples_per_symbol, dtype=np.float64)
for i, tone in np.ndenumerate(tones):
idx, *_ = i
freq = f0 + bandwidth * tone
phase_delta = 2 * np.pi * freq * dt
phases = np.fromiter(
(np.fmod(phase_delta * i + phase, 2 * np.pi)
for i in range(samples_per_symbol)),
dtype=np.float64
)
t_start = idx * samples_per_symbol
t_end = t_start + samples_per_symbol
signal[t_start:t_end] = np.sin(phases)
phase = np.fmod(phase_delta * samples_per_symbol + phase, 2 * np.pi)
return signalПримечание: функция
synth_fskподрлбно рассматривалась в статье, посвященной Q65. Входные параметры: перечень тоновtones, частота дискретизации итогового сигналаsample_rate, количество сэмплов на символsamples_per_symbol, базовая частота нулевого тонаf0и ширина полосыbandwidth. Результат — numpy массив, содержащий дискретный сигнал со значениями от -1 до 1.
Итоговый код генератора сигнала WSPR, включающий в себя все вышеописанные функции:
import typing
import numpy as np
import numpy.typing as npt
from scipy.io.wavfile import write
from consts.wspr import *
from encoders import wspr_encode
from mod import synth_fsk
from msg.message import *
def gen_signal(tones: npt.NDArray[np.uint8], frequency: int, sample_rate: int) -> npt.NDArray[np.float64]:
samples_per_symbol = int(sample_rate * WSPR_SYMBOL_PERIOD)
bandwidth = sample_rate / samples_per_symbol
signal = np.fromiter(
synth_fsk(tones, sample_rate, samples_per_symbol, frequency, bandwidth),
dtype=np.float64
)
return signal
def gen_wspr_tones(payload: typing.ByteString) -> npt.NDArray[np.uint8]:
tones = wspr_encode(payload)
tones = np.fromiter(tones, dtype=np.uint8)
return tones
def main():
sample_rate = 12000
msg = WSPRMessage(WSPRCallsign("R9FEU"), WSPRGrid("LO87"), 50)
payload = msg.encode()
tones = gen_wspr_tones(payload)
signal = gen_signal(tones, frequency=1500, sample_rate=sample_rate)
amplitude = np.iinfo(np.int16).max
data = signal * amplitude
write("examples/signal.wav", sample_rate, data.astype(np.int16))
if __name__ == '__main__':
main()
После запуска кода, сформированный и готовый к передаче в эфир, сигнал с данными записывается в wav-файл с именем signal.wav с частотой дискретизации 12 КГц.
Значение bandwidth рассчитывается как отношение частоты дискретизации к количеству семплов на символ, что составляет 1.46484375 Гц, из чего следует, что общая ширина полосы частот WSPR равняется 5.859375 Гц.
На рисунке 6 представлена спектрограмма сформированного сигнала.

Рисунок 6: Спектр сигнала WSPR.
Передача сигнала в эфирДля передачи в эфир использовался передатчик с выходной мощностью 100 Вт, в режиме однополосной модуляции с верхней боковой полосой на частоте 14.0956 МГц. В качестве антенны использовалась дипольная антенна на диапазон 20м. Передача осуществлялась в темное время суток в 18:16 UTC.
После передачи сигнала на интерактивной карте wspr-агрегатора можно посмотреть где и когда переданный сигнал был зафиксирован (рисунок 7).

Рисунок 7: Карта приемников, которые зафиксировали передаваемый сигнал.

Рисунок 8: Карта Европы более детально.
Из рисунка 7 видно, что в основном сигнал был принят в Европе (рисунок 8), так как там сосредоточено больше всего онлайн-репортеров, а также по причине направленности антенны в сторону запада. Самые удаленные приемники, принявшие сигнал, находились в австралии, антарктиде и близ острова Мадагаскар.
Дополнительно удалось принять из эфира и декодировать сигналы активных в это время маяков, самый дальний из которых (EB1A) был расположен в испании на расстоянии 4662 км.
Журнал принятых сигналов------------------------------------------------------------------- 20m
1814 -17 0.8 14.097024 0 G8MCD IO91 23 3635
1814 -14 -0.2 14.097039 0 <...> JO23VF 23 3145
1814 -20 -0.2 14.097059 0 PH9K JO33 27 3056
1814 -24 -0.2 14.097088 -1 PE2NAN JO21 23 3288
1814 -21 -0.5 14.097095 0 MW0XMG IO81 23 3750
1814 -17 -0.7 14.097096 0 EB1A IN53 27 4662
1814 -17 -0.1 14.097101 0 SM4HVP JP70 37 2327
1814 -26 1.6 14.097135 0 DL6NL JO50 33 2996
1814 -17 -0.4 14.097166 0 OH6CR KP12 23 1882
1814 -17 -0.5 14.097198 0 PA2PGU JO32 23 3112
------------------------------------------------------------------- 20m
1818 -25 -0.0 14.097011 0 PA0PPW JO21 23 3288
1818 -22 -0.4 14.097019 0 SA0ORR JO89 23 2241
1818 -14 0.8 14.097024 0 G8MCD IO91 23 3635
1818 -13 -0.2 14.097042 0 DJ8XD JO52 37 2879
1818 -26 -0.0 14.097082 0 PD0PF JO22 23 3229
1818 -23 -0.4 14.097091 0 MI9PYZ IO65 23 3732
1818 -21 3.1 14.097096 0 GI3VAF IO74 37 3680
1818 -18 0.4 14.097109 -1 DC6ZK JO52 30 2879
1818 -25 0.9 14.097118 0 PE1JXI JO20 10 3349
1818 -20 -0.0 14.097129 0 DC1SAF JN48 33 3243
1818 -20 1.7 14.097134 0 DL6NL JO50 33 2996
1818 -20 1.8 14.097158 0 2E0DLC IO93 23 3514
1818 -15 0.1 14.097177 0 G4HSB IO94 23 3457
1818 -29 -0.0 14.097187 0 PD1RE JO22 23 3229
------------------------------------------------------------------- 20m
1822 -12 0.8 14.097024 0 G8MCD IO91 23 3635
1822 -19 -0.2 14.097050 0 OZ7IT JO65 37 2610
1822 -21 5.3 14.097075 0 SM7WDL JO76 23 2452
1822 -14 0.1 14.097092 0 MW0XMG IO81 23 3750
1822 -17 1.6 14.097114 0 SP9MLI JO94 30 2306
1822 -22 0.3 14.097119 0 PE1JXI JO20 10 3349
1822 -21 1.6 14.097134 0 DL6NL JO50 33 2996
1822 -18 -0.1 14.097157 0 G4GVZ IO81 37 3750
1822 -25 1.8 14.097159 0 2E0DLC IO93 23 3514
1822 -18 0.5 14.097175 0 G6WZA IO80 30 3814
1822 -24 -0.1 14.097189 0 <...> JN48AO 37 3297
1822 -15 -0.2 14.097195 0 G6GN IO81 30 3750
------------------------------------------------------------------- 20m
1824 -26 -0.4 14.096999 0 DL3NAA JN38 23 3361
1824 -22 -0.2 14.097015 0 G4GOC IO92 23 3574
1824 -16 -0.1 14.097029 0 PA7D JO23 23 3172
1824 -16 -0.0 14.097048 0 MW0XMG IO81 23 3750
1824 -23 -0.3 14.097064 0 SM3MTQ JP81 23 2202
1824 -19 5.1 14.097075 0 SM7WDL JO76 23 2452
1824 -16 0.5 14.097084 0 PA2W JO22 37 3229
1824 -18 2.5 14.097089 0 <...> JO22RD 33 3226
1824 -23 -0.1 14.097101 0 SM4HVP JP70 37 2327
1824 -20 1.0 14.097119 0 PE1JXI JO20 10 3349
1824 -24 -0.4 14.097128 0 <...> JN58UA 23 3119
1824 -15 0.1 14.097129 0 G4HSB IO94 23 3457
1824 -26 0.1 14.097139 0 PD0PF JO22 23 3229
1824 -25 1.9 14.097159 0 2E0DLC IO93 23 3514
1824 -16 -0.1 14.097173 0 G0VOK IO83 37 3627
1824 -26 -0.4 14.097182 0 G4UGD IO83 23 3627
1824 -19 -0.1 14.097206 0 M0XDC JO01 37 3520
------------------------------------------------------------------- 20m
1826 -22 5.1 14.096995 0 SM7WDL JO76 23 2452
1826 -12 -0.9 14.097019 0 G4GCI IO90 37 3698
1826 -17 -0.1 14.097022 0 MW0XMG IO81 23 3750
1826 -11 0.9 14.097024 0 G8MCD IO91 23 3635
1826 -16 -0.3 14.097029 0 <PA7D> JO23VF 23 3145
1826 -17 -0.3 14.097032 0 GM8HHC IO85 23 3513
1826 -24 -0.3 14.097047 0 M8HYX IO84 23 3569
1826 -24 -0.0 14.097063 0 PA0PPW JO21 23 3288
1826 -20 0.2 14.097096 0 EB1A IN53 27 4662
1826 -20 0.4 14.097110 -1 DC6ZK JO52 30 2879
1826 -22 0.8 14.097119 0 PE1JXI JO20 10 3349
1826 -27 -0.9 14.097139 0 G2PLX JO02 20 3459
1826 -19 -0.2 14.097159 -3 OH3HE KP32 23 1676
1826 -23 -0.5 14.097175 0 PA2PGU JO32 23 3112
1826 -20 -0.4 14.097185 0 DK3TG JN58 23 3125
------------------------------------------------------------------- 20m
1828 -18 -0.2 14.097006 0 MW0XMG IO81 23 3750
1828 -8 -0.2 14.097007 0 OH6CR KP12 23 1882
1828 -19 -0.0 14.097050 0 DL4BG JO43 30 2941
1828 -20 -0.3 14.097080 0 SM2OTU KP16 23 1936
1828 -18 -0.2 14.097087 0 G0IDE IO83 37 3627
1828 -19 1.5 14.097114 0 SP9MLI JO94 30 2306
1828 -20 0.3 14.097119 0 PE1JXI JO20 10 3349
1828 -22 -0.4 14.097131 0 PC4T JO22 23 3229
1828 -15 -0.3 14.097132 0 DL1FX JN49 23 3177
1828 -16 1.7 14.097135 0 DL6NL JO50 33 2996
1828 -23 -0.4 14.097148 0 SA3LEN JP80 23 2219
1828 -21 5.0 14.097154 0 SM7WDL JO76 23 2452
1828 -20 -0.1 14.097173 0 G0VOK IO83 37 3627
В статье был проведен обзор, анализ и принцип работы протокола WSPR. Протокол представляет интерес тем, что обладая низкой скоростью и узкой полосой частот, позволяет осуществлять передачи сигналов, используя передатчики с очень малыми мощностями; а также представляет интерес как инструмент изучения состояния ионосферы и прохождения радиосигналов в ней. WSPR-приемники расположены по всему миру, что также позволяет использовать протокол для тестирования радиолюбительских антенн и передатчиков, прогнозировать зоны для проведения радиолюбительских связей.
Ссылки| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Все об устройстве Q65 с примерами на Python (часть 2) | 0 | 11.48 | 10-01-2026 |
| 2 | Все об устройстве Q65 с примерами на Python (часть 3) | 0 | 12.87 | 06-02-2026 |
| 3 | Знакомьтесь, Барри Уорсо (цикл заметок о выдающихся питонистах) | 0 | 7.92 | 16-03-2026 |
| 4 | Как звучит JPEG? Или что будет, если сжать спектрограмму как фотографию | 0 | 7.33 | 15-05-2026 |
| 5 | «Росэл» разработал помехозащищенную систему радиосвязи для БПЛА | 0 | 5 | 03-07-2026 |
| 6 | Современный MQTT-сервис на Python | 0 | 11.11 | 01-07-2026 |
| 7 | СМИ: австралийская компания представила wi-fi чип с низким энергопотреблением | 0 | 0 | 30-05-2019 |
| 8 | "Росэлектроника" разработала технологию передачи данных по радиоканалам низкого качества | 0 | 0 | 13-11-2018 |
| 9 | Q/V-Band Dual Circular Polarized, Frequency Diplexer Feed System for Next-Generation Satcom | 0 | 28.18 | 12-05-2026 |
| 10 | From Analog IF to Open Digital RF: Engineering the DIFI Ground Segment | 0 | 10 | 12-05-2026 |