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

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


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).
Сверточный код относится к классу корректирующих кодов на основе свертки входящей последовательности битов данных с импульсной характеристикой кодера, реализуемый через регистр сдвига, являющегося частным случаем памяти. Таким образом, значения бит выходной последовательности рассчитываются на основе текущего входного бита и нескольких предыдущих, что повышает эффективность коррекции ошибок при передаче данных.
Математически, сверточный код описывается как линейный код с памятью над конечным полем (где
,
— простое число,
— натуральное; в случае с битами
), в котором кодовая последовательность v формируется как дискретная свертка информационной последовательности u с набором векторно-значных коэффициентов
(порождающие полиномы).
В каждый момент времени кодовый вектор
выражается линейной комбинацией текущего и
предыдущих информационных векторов
с коэффициентами из
.
Это задает линейные рекуррентные соотношения и переводит код во множество полубесконечных последовательностей, образующих аддитивную подгруппу в . Аддитивная группа
относительно операции сложения по модулю
изоморфна конечной абелевой группе
, из чего следует, что вся кодовая последовательность может рассматриваться как элемент бесконечного прямого произведения таких аддитивных групп.
При фиксированной длине последовательности код можно рассматривать как линейное пространство в
, структуру которого можно интерпретировать как конечную абелеву группу относительно операции сложения по компонентам.
Отношение числа информационных символов на входе кодера к числу кодовых символов на выходе за один такт определяет скорость кода: (где
— число информационных символов,
— число кодовых, при этом
), эта величина следует из размера линейного преобразования
, и определяет степень избыточности и предельную плотность информации в кодовой последовательности.
Для двоичных данных конечное поле процесс кодирования заключается в том, что в каждый момент кодирования бита входных данных осуществляется изменение состояния регистров путем сдвига предыдущего значения; значения выходных бит рассчитываются путем суммирования по модулю 2 (оператор XOR) текущего и предыдущего состояний регистров с порождающими полиномами.
Графически описанный алгоритм представлен на рисунке 5, где блоки n, n-1, n-2 — значения в сдвиговых регистрах, а сумматор — сложение по модулю 2 (XOR).

Примечание: сверточное кодирование использовался 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 представлена спектрограмма сформированного сигнала.

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


Из рисунка 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-приемники расположены по всему миру, что также позволяет использовать протокол для тестирования радиолюбительских антенн и передатчиков, прогнозировать зоны для проведения радиолюбительских связей.
