Стенд «Радиошторм»инструкция для команд
Скачать бейзлайн Консоль команды
Кейс «Адаптивный модем для нестабильного канала»

Как подключить модем к стенду

Ваша команда пишет две программы: узел A (передатчик) и узел B (приёмник). Стенд — это радиоэфир между ними: он принимает сигнал от A, пропускает его через моделируемый радиоканал и отдаёт B. От B к A идёт такой же отдельный канал. Задача — чтобы B восстановил переданный файл как можно полнее и быстрее.

1. Как подключиться

  1. Получите ключ команды у организаторовС ним вы входите в консоль команды и создаёте тестовые сессии.
  2. Войдите в консоль команды и создайте сессиюВыберите сценарий и нажмите «Создать сессию». Стенд выдаст два разных токена — для узла A и для узла B — и адреса подключения.
  3. Запустите узел A и узел BДве программы или один Docker-образ, запущенный дважды с разной ролью. Параметры подключения передаются через переменные окружения (ниже). Узел A скачивает файл, который нужно передать; узел B файла не видит.
  4. Дождитесь конца эфираКогда оба узла подключены и прислали ready, начинается эфир. После его окончания узел B отправляет восстановленный файл, и стенд показывает результат в консоли.
Переменные окружения узла SIGNAL_ROLE = A или B · SIGNAL_SESSION_ID · SIGNAL_NODE_TOKEN · SIGNAL_API_URL (адрес REST API) · SIGNAL_WS_URL (база адреса WebSocket). Программа должна читать всё из этих переменных и работать без ручных действий.

2. Бейзлайн — готовый пример решения

Рабочее решение-образец: оба узла в одном проекте, запуск одной командой. С него удобно начать, чтобы увидеть весь путь от файла до результата.

  1. Скачайте и распакуйтеradioshtorm-baseline.zip — внутри README с описанием.
  2. Создайте сессию в консоли командыНа странице сессии есть блок «session.env для бейзлайна» — скопируйте его в файл session.env в папке бейзлайна.
  3. Запустите оба узлаВ командной строке из папки бейзлайна (PowerShell на Windows, Терминал на macOS и Linux): docker compose --env-file session.env up --build — нужен Docker. Без Docker: python run_local.py — нужен Python 3.11+. Работает на Windows, macOS и Linux; подробности и команды для каждой системы — в README бейзлайна.

3. Как идёт сессия

Узел A Стенд Узел B GET /input — скачать файл (только A) WebSocket + «ready»WebSocket + «ready» «start»«start» Эфир: каждые 20,48 мс — блок 4096 отсчётов туда и обратно A → [канал A→B] → B, B → [канал B→A] → A. Условия меняются по участкам сценария. «stop»«stop» PUT /result — восстановленный файл (только B, до 30 с)
Оба узла держат по одному WebSocket-соединению. Текстовые сообщения — служебные (hello, ready, start, stop, error), бинарные — только сигнал.

4. Эфир

Как в настоящем радиоканале, сигнал по пути может ослабевать, тонуть в шуме, приходить с задержкой и со сдвигом по частоте и фазе, рядом могут появляться посторонние сигналы. Условия меняются во время сессии и различаются от сценария к сценарию. Сигнал от B к A идёт по своему каналу со своими условиями.

Стенд ничего не сообщает о состоянии канала — ни узлам, ни в консоли. Узел B знает только то, что сам принял. Узел A знает о приёме на стороне B только то, что B сообщит ему по радио.

Узел A решает, как передавать дальше Узел B принимает кадры оценивает, насколько хорошо слышно собирает файл Канал A → B кадры с данными Канал B → A служебные сообщения B Всё, что A знает о приёме на стороне B, приходит к нему только по радио
Как устроить служебные сообщения, что в них передавать и как на них реагировать — решает команда.

5. Формат сигнала и правила обмена

Частота дискретизации200 000 отсч/с
Блок4096 отсчётов = 20,48 мс
ОтсчётI, Q — float32 LE
Размер блока32 768 байт
Диапазон I и Q−1 … +1
Рабочая полоса−80 … +80 кГц
  • Стенд ведёт эфир по своим часам: каждые 20,48 мс берёт у каждого узла по одному блоку и отдаёт каждому по одному блоку.
  • Если к очередному такту блок от узла не пришёл, в эфир уходит тишина. Вперёд стенд принимает не больше 10 блоков, лишние отбрасываются.
  • Значения I и Q за пределами ±1 обрезаются. NaN и Inf запрещены.
  • Узел B сдаёт файл один раз, после окончания эфира. Стенд сравнивает байты по их позициям в исходном файле.
import numpy as np
# один блок: 4096 комплексных отсчётов -> 32 768 байт I0,Q0,I1,Q1,... (float32 little-endian)
def pack(iq):
    buf = np.empty(8192, dtype="<f4"); buf[0::2] = iq.real; buf[1::2] = iq.imag
    return buf.tobytes()
def unpack(data):
    v = np.frombuffer(data, dtype="<f4"); return v[0::2] + 1j * v[1::2]

ws_url = f"{SIGNAL_WS_URL}/sessions/{SIGNAL_SESSION_ID}/nodes/{SIGNAL_ROLE}?token={SIGNAL_NODE_TOKEN}"
# hello -> {"type":"ready"} -> start -> обмен блоками -> stop
# узел A: GET {SIGNAL_API_URL}/sessions/{id}/input      (Authorization: Bearer токен A)
# узел B: PUT {SIGNAL_API_URL}/sessions/{id}/result     (Content-Type: application/octet-stream)

Полное описание протокола — все сообщения, коды ошибок и ограничения.

6. Частые ошибки

Что видитеПочему
Эфир не начинаетсяСтарт — только когда оба узла подключились и прислали {"type":"ready"}.
Много «пропусков такта»Узел не успевает присылать блоки, и в эфир уходит тишина.
«Переполнения» / отброшенные блокиУзел присылает блоки быстрее, чем стенд их забирает.
КлиппингЗначения I или Q выходят за ±1.
bad_frame_sizeБинарное сообщение должно быть ровно 32 768 байт.
Результат не принятФайл сдаётся только после stop, только узлом B и только один раз.