А не послушать ли нам «радио Судного дня»… с AI? Создаю АПК радиолюбителя для УВБ-76. DevOps.. DevOps. DIY или Сделай сам.. DevOps. DIY или Сделай сам. RaspberriPI.. DevOps. DIY или Сделай сам. RaspberriPI. Блог компании YADRO.. DevOps. DIY или Сделай сам. RaspberriPI. Блог компании YADRO. искусственный интеллект.. DevOps. DIY или Сделай сам. RaspberriPI. Блог компании YADRO. искусственный интеллект. Любительская радиосвязь.. DevOps. DIY или Сделай сам. RaspberriPI. Блог компании YADRO. искусственный интеллект. Любительская радиосвязь. радио.. DevOps. DIY или Сделай сам. RaspberriPI. Блог компании YADRO. искусственный интеллект. Любительская радиосвязь. радио. радиолюбительство.. DevOps. DIY или Сделай сам. RaspberriPI. Блог компании YADRO. искусственный интеллект. Любительская радиосвязь. радио. радиолюбительство. увб-76.

Я занимаюсь инфраструктурным DevOps в YADRO — кластеры, виртуализация, мониторинг, оптимизация железа. Последнее время на работе был сильный завал. Голова кипела. Захотелось отвлечься на что-нибудь совсем другое: переключить мозги с «как ускорить сборку на три секунды», на «как это вообще работает». Пока думал, как занять подобным образом свободное от работы время, попалась мне статья про УВБ-76.
 
Знаете это чувство? Читаешь про что-то странное и загадочное, и внутри загорается лампочка: «А чего так не системно-то?» Тут и родилась идея объединить свою профессиональную деятельность с чем новым для меня. Не просто послушать разок жужжание из любопытства, а построить целую станцию наблюдения. Сразу пришло понимание, что сидеть ночами с наушниками и слушать странный эфир я не хочу.

А не послушать ли нам «радио Судного дня»… с AI? Создаю АПК радиолюбителя для УВБ-76 - 1

Если можно автоматизировать, давайте автоматизируем, благо технический бэкграунд позволяет. Последние годы я плотно занимаюсь AI и ML. Дома развернут свой GPU-кластер на Kubernetes — есть на чем гонять инференс. Плюс целый ящик железа, которое ждало своего звездного часа: Raspberry Pi 5 на 8 ГБ, M.2 HAT с SSD на 4 ТБ, какие-то платы, провода, блоки питания. Все это лежало и пылилось после прошлых экспериментов.

Вкратце про УВБ-76

Для тех, кто не в курсе: УВБ-76 — это без преувеличения главная радиозагадка постсоветского пространства. Радиостанция, которая уже несколько десятилетий круглосуточно передает в эфир монотонный жужжащий звук. Двадцать четыре часа в сутки, семь дней в неделю — просто жужжащий радиофон на частоте 4 625 кГц.

Казалось бы, жужжит и жужжит, мало ли чем там военные балуются. Но все становится интереснее, когда жужжание внезапно обрывается и мужской (иногда женский) голос зачитывает загадочные сообщения: «МДЖБ, МДЖБ, БОРИС, ИГЛА, ТОПОЛЬ, 55, 22…». После этого жужжание возвращается. И так — годами.

Станцию слышно по всему миру. Ее принимают от Санкт-Петербурга до Бразилии. Есть фанаты, которые слушают ее сутками, записывают каждое голосовое сообщение и строят теории.

Что это вообще такое, точно не знает никто. Официально — никаких объяснений. Но общественный консенсус таков: это военный объект, а именно канал боевого управления. Скорее всего, принадлежит ВМФ или РВСН (ракетным войскам стратегического назначения).

SDS, RTL, SDR и другая матчасть 

Первый стоппер возник почти сразу. Я много лет работал в телекоме с операторами связи, так что радиоволны, частоты, диапазоны и модуляции для меня не были магией. Техническую базу я понимал. Но именно с приемным оборудованием не сталкивался: моя работа всегда была на стороне инфраструктуры, а не радиоэфира. Про домашние SDR-приемники я не знал вообще. Что такое RTL-SDR? Какой свисток взять? Чем они отличаются? Нужна ли специальная антенна для коротких волн? В этих вопросах я был абсолютным новичком и начал разбираться с нуля.

SDR (Software-Defined Radio) — это программно-определяемое радио. Звучит сложно, но на деле все просто. Обычный радиоприемник — это «железная» коробка, в которой все настроено на заводе: фильтры, демодуляторы, декодеры. SDR — это маленький USB-донгл, который оцифровывает радиосигнал и отдает его в компьютер. А компьютер уже делает остальное: фильтрует, демодулирует, декодирует. Таким образом, радио становится программой.

RTL-SDR — это семейство дешевых USB-приемников, построенных на чипе RTL2832U. Изначально они создавались для приема цифрового телевидения (DVB-T), но энтузиасты обнаружили, что чип можно перевести в режим «сырого» приема и получить доступ к радиоспектру от 500 кГц до 1,7 ГГц. Стоит такой свисток копейки — от 500 до 5 000 рублей в зависимости от модели.

Блок-схема Realtek RTL2832U

Блок-схема Realtek RTL2832U

Почему не любой свисток подходит? Дешевые донглы за 500 рублей отлично работают на УКВ (FM-радио, авиадиапазон), но для коротких волн, где живет УВБ-76, им не хватает чувствительности. Нужна модель с поддержкой Direct Sampling — режима, в котором антенна подключается напрямую к чипу, минуя тюнер. Я взял RTL-SDR Blog V4 — у него есть встроенный фильтр помех и термокомпенсированный генератор (TCXO).

Чем хорошо термокомпенсированный генератор? Обычные кварцевые генераторы в дешевых приемниках «плывут» при нагреве. Ты настроился на 4625 кГц, но через полчаса работы частота сдвинулась на 4626 или 4624 — и ты слушаешь пустоту. TCXO держит частоту с точностью до миллионной доли (1 PPM). За 12 суток непрерывной работы Buzzer-1 не зафиксировал ни одного ухода частоты. Так я могу быть уверен: если жужжание пропадет — это событие на станции, а не дрейф приемника.

Пока пересматривал все полезные форумы и обзоры, понял: домашнее радио — это отдельная вселенная со своим сленгом, своими гуру и религиозными войнами на тему «какой приемник лучше».

BOM, Ozon и бесконечное ожидание

С чего начинается любой инженерный проект? Правильно: со списка покупок. Открыл маркетплейс и начал собирать корзину. Это был мой первый BOM (Bill of Materials) для радио, и я чувствовал себя немного странно: обычно я заказываю серверное железо или кабели для дата-центра, а тут — какие-то свистки, антенны-петли и ферритовые защелки.
 
Сразу скажу: без помощи разобраться было бы сложно. Мой напарник, с которым мы обсуждали проект, как раз разбирался в радио и направлял меня. Именно он объяснил, почему не стоит брать дешевый синий донгл за пятьсот рублей, а лучше взять RTL-SDR Blog V4 — с нормальным КВ-трактом, фильтрами и термокомпенсированным генератором. Причины я описал выше. Напарник также посоветовал вместо комплектного телескопа антенну YouLoop, потому что она не собирает помехи от роутеров и зарядок.
 
Один за другим я определился с приемником, антенной, кабелями, переходниками. Отдельная история вышла с GPS-модулем: в перспективе он бы пригодился, чтобы развить проект до радиотелескопа или портативного радара. Но увидев, что подходящая мне модель стоит в районе 11 тысяч, решил: пока без GPS.
 
Пока приехало еще не все. RTL-SDR был у меня уже через три дня, а вот антенну жду до сих пор. Мой Raspberry Pi 5 сам лежит без дела уже года три. Комплектная антенна-телескоп RTL-SDR выглядела игрушечной, но для старта ее оказалось достаточно. Придет YouLoop — заменю и усилю.

А не послушать ли нам «радио Судного дня»… с AI? Создаю АПК радиолюбителя для УВБ-76 - 3

Первый запуск: как я пытался услышать жужжание на комплектную антенну и что из этого вышло

Распаковка и первый старт

Первым делом я записал образ Raspberry Pi OS Lite на microSD, настроил Wi-Fi и SSH, подключил приемник. Признаюсь честно: когда впервые запустил rtl_test -t и увидел в выводе RTLSDRBlog, Blog V4, SN: 00000001, испытал детскую радость. Железо работает! Мой первый в жизни SDR-приемник ожил.

Оставалось главное — услышать ту самую «жужжалку».

Битва с антенной

Я настроил rtl_fm на частоту 4625 кГц, включил прямой режим для коротких волн, запустил и услышал… ничего. Только шум. Белый шум, как из ненастроенного телевизора. Первая мысль: «Приёмник сломан?» Вторая: «Я что-то не так настроил?» Третья: «Может, УВБ-76 вообще не вещает?»

Но нет, проблема была в антенне. Комплектный телескопический штырь из комплектации RTL-SDR рассчитан на УКВ-диапазон — FM-радио, авиация. Для коротких волн на 4,6 МГц нужна была антенна длиной около 16 метров — то есть в 16 раз длиннее моей.

Но я решил не сдаваться раньше времени. Поставил антенну на подоконник, направил в сторону предполагаемого передатчика — и, к моему удивлению, в наушниках раздалось монотонное жужжание. Слабое, с помехами, но совершенно узнаваемое. То самое, легендарное. УВБ-76 была поймана на комплектную антенну без каких-либо модификаций.

Позже, когда приедет заказанная антенна YouLoop, приём станет ещё чище и громче. Но главное, что уже на этом этапе я понял: система работает. Можно строить дальше.

А не послушать ли нам «радио Судного дня»… с AI? Создаю АПК радиолюбителя для УВБ-76 - 4

Первая архитектура

Уже на следующий день после первого приема у меня был работающий прототип:

  1. Rtl_fm захватывает эфир.

  2. Python-скрипт читает аудиопоток и отправляет его в браузер через WebSocket.

  3. Браузер воспроизводит звук через Web Audio API.

Это была минимальная версия, просто «слушалка» с веб-интерфейсом. Но уже на этом этапе я заложил основу для будущего расширения: весь код был разбит на модули, каждый отвечал за свою задачу.

Позже, когда я добавил Whisper для распознавания речи и Ollama для анализа кодовых слов, модульная архитектура спасла мне жизнь. Я мог менять AI-часть, не трогая SDR-захват, и наоборот.

Первые метрики

Почти сразу я начал собирать метрики жужжания, просто чтобы понять, насколько сигнал стабилен. Каждые 60 секунд скрипт записывал в PostgreSQL частоту, RMS (среднеквадратичное значение) и ширину полосы.

Через 4 часа у меня уже было 228 записей, и я мог сделать первые выводы:

  • Частота стабильна как часы: 4.625 МГц без дрейфа.

  • RMS не меняется: передатчик работает с постоянной мощностью.

  • Ширина полосы — около 900 Гц, что широко для чистого тона (должно быть 50–100 Гц).

Код скрипта на Python
#!/usr/bin/env python3
"""
Buzzer-1 Metrics Collector
Каждые 60 секунд захватывает 3-секундный фрагмент эфира,
считает частоту, RMS и ширину полосы, сохраняет в PostgreSQL.
"""
import os, sys, time, logging, subprocess, numpy as np
from pathlib import Path
from datetime import datetime, timezone
BUZZER_FREQ = os.environ.get('BUZZER_FREQ', '4625000')
SAMPLE_RATE = int(os.environ.get('SAMPLE_RATE', '12000'))
GAIN = os.environ.get('SDR_GAIN', '30')
INTERVAL_SEC = int(os.environ.get('METRICS_INTERVAL', '60'))
CAPTURE_SEC = 3  # длительность захвата для анализа
PG_HOST = os.environ.get('PG_HOST', 'localhost')
PG_DB = os.environ.get('PG_DB', 'buzzer_db')
PG_USER = os.environ.get('PG_USER', 'buzzer')
PG_PASS = os.environ.get('PG_PASS', 'buzzer123')
LOG_DIR = Path(os.environ.get('LOG_DIR', '/opt/buzzer/logs'))
LOG_DIR.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler(LOG_DIR / 'metrics.log'),
        logging.StreamHandler(sys.stdout)
    ]
)
log = logging.getLogger(__name__)
def capture_and_analyze():
    """
    Захватывает CAPTURE_SEC секунд аудио через rtl_fm,
    вычисляет метрики жужжания:
    - доминирующую частоту
    - RMS (среднеквадратичное значение)
    - ширину полосы по уровню -10 дБ
    """
    cmd = [
        'rtl_fm',
        '-f', BUZZER_FREQ,
        '-M', 'usb',
        '-s', str(SAMPLE_RATE),
        '-g', GAIN,
        '-E', 'direct',
        '-'
    ]

    try:
        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
        raw = proc.stdout.read(SAMPLE_RATE * CAPTURE_SEC)  # 1 байт на сэмпл (8-bit)
        proc.terminate()
        proc.wait()

        if len(raw) < 1000:
            log.warning("Слишком мало данных — пропускаем")
            return None

        # Преобразуем 8-bit unsigned → float32 [-1.0, 1.0]
        data = np.frombuffer(raw, dtype=np.uint8).astype(np.float32)
        data = (data - 128.0) / 128.0
        data = data - np.mean(data)  # убираем DC-смещение
        rms = float(np.sqrt(np.mean(data ** 2)))
        windowed = data * np.hanning(len(data))
        spectrum = np.abs(np.fft.rfft(windowed))
        freqs = np.fft.rfftfreq(len(data), 1.0 / SAMPLE_RATE)
        peak_idx = np.argmax(spectrum[1:]) + 1  # +1 потому что пропустили нулевой бин
        peak_freq = float(freqs[peak_idx])
        threshold = np.max(spectrum) * 0.316  # -10 дБ = 0.316 по мощности
        above_threshold = spectrum > threshold
        bandwidth = float(np.sum(above_threshold) * (SAMPLE_RATE / len(data)))
        harmonic_threshold = np.max(spectrum) * 0.1  # -20 дБ
        harmonic_region = (spectrum > harmonic_threshold) & (freqs > peak_freq * 1.5)
        harmonics = int(np.sum(np.diff(harmonic_region.astype(int)) == 1))

        return {
            'freq_hz': peak_freq,
            'bandwidth_hz': bandwidth,
            'rms': rms,
            'peak': float(np.max(np.abs(data))),
            'harmonics': harmonics
        }

    except Exception as e:
        log.error(f"Ошибка захвата: {e}")
        return None

def save_metrics(metrics):
    """Сохраняет метрики в PostgreSQL."""
    try:
        import psycopg2
        conn = psycopg2.connect(
            host=PG_HOST,
            database=PG_DB,
            user=PG_USER,
            password=PG_PASS
        )
        cur = conn.cursor()
        cur.execute(
            """INSERT INTO metrics (freq_hz, bandwidth_hz, rms, peak, harmonics)
               VALUES (%s, %s, %s, %s, %s)""",
            (
                metrics['freq_hz'],
                metrics['bandwidth_hz'],
                metrics['rms'],
                metrics['peak'],
                metrics['harmonics']
            )
        )
        conn.commit()
        cur.close()
        conn.close()

        log.info(
            f"✓ f={metrics['freq_hz']:7.0f} Гц | "
            f"BW={metrics['bandwidth_hz']:5.0f} Гц | "
            f"RMS={metrics['rms']:.6f} | "
            f"гармоник={metrics['harmonics']}"
        )

    except Exception as e:
        log.error(f"Ошибка сохранения в БД: {e}")


def main():
    log.info(f"Запуск сборщика метрик | Частота: {BUZZER_FREQ} Гц | Интервал: {INTERVAL_SEC} сек")

    try:
        import psycopg2
    except ImportError:
        log.info("Установка psycopg2...")
        subprocess.run([sys.executable, '-m', 'pip', 'install', 'psycopg2-binary'], check=True)
        import psycopg2

    while True:
        try:
            metrics = capture_and_analyze()
            if metrics:
                save_metrics(metrics)
        except Exception as e:
            log.error(f"Ошибка в главном цикле: {e}")

        time.sleep(INTERVAL_SEC)


if __name__ == '__main__':
    main()

Жужжание с моей антенной выглядит очень похожим на белый шум, причем регулярно повторяющийся. Это натолкнуло меня на мысль: а нет ли внутри жужжания цифровой модуляции? Может быть, FSK или PSK, кодирующая данные?

Я провел спектральный анализ и автокорреляцию. Результат: никакой модуляции здесь нет. Жужжание — это чистый тон. Единственная его функция — маркер занятости канала.

Whisper на Raspberry Pi, Ollama в кластере, и почему 8-битный звук чуть не свел меня с ума

Когда базовая система заработала — прием, веб-интерфейс, метрики — встал вопрос: а что дальше? Сидеть и слушать жужжание сутками в надежде поймать голосовое сообщение? Ну нет, я слишком стар и ленив для этого.

План был такой:

  1. Whisper распознает речь из аудиопотока и превращает ее в текст.

  2. Ollama с моделью Qwen 2.5 7B анализирует текст: определяет тип сообщения, срочность, ключевые слова.

  3. Telegram-бот отправляет мне уведомление, если что-то обнаружено.

Звучит просто. На практике все оказалось несколько сложнее.

Whisper на Raspberry Pi: зачем мучить «малину»

Изначально я хотел запустить Whisper на своем K8s-кластере — там есть GPU, там ему самое место. Но выяснилось, что единственная видеокарта в кластере уже занята Ollama, которая обслуживает несколько проектов.

Пришлось запускать Whisper прямо на Raspberry Pi 5. Да, на CPU. Да, без GPU. И знаете что? Он справляется.

Самая маленькая модель tiny на 80 МБ обрабатывает пятисекундный чанк аудио за две-три секунды. Этого достаточно, чтобы успевать за потоком. Да, качество распознавания ниже, чем у больших моделей. Но для задачи «отличить голос от жужжания и разобрать кодовые слова» хватает.

Позже, когда появится вторая GPU в кластере, я перенесу Whisper туда и включу модель побольше. Но пока работает так.

А не послушать ли нам «радио Судного дня»… с AI? Создаю АПК радиолюбителя для УВБ-76 - 5

Ollama и Qwen: анализ кодовых слов

С этой частью проще. Ollama с моделью Qwen 2.5 7B (7 миллиардов параметров) уже работает в кластере на паре проектов. Я добавил еще одного клиента — свой Pi 5. Схема такая:

  1. Whisper распознал текст на Pi 5.

  2. Текст отправляется в Ollama на K8s.

  3. Qwen анализирует: «Это шум? Или голосовое сообщение? Если голос — какой тип? Какая важность? Какие ключевые слова?».

  4. Ответ возвращается на Pi 5 и пишется в базу.

Дополнительно объясню про важность. Голосовые сообщения УВБ-76 бывают трех типов, и по ним можно косвенно судить о важности передачи.

Проверка связи (срочность 1–3). Самый частый тип. Звучит примерно так: «МДЖБ, МДЖБ, ЖУЖЖАЛКА, 38 929, БОРИС, ИГЛА, 14 83 51». Это рутинная проверка канала. Оператор просто убеждается, что связь работает. Такие сообщения могут идти несколько раз в день или раз в несколько дней.

Учебная тревога (срочность 4–6). Отличается набором кодовых слов. Например, появление слова «ТОПОЛЬ» или «НАНОС» в определенных комбинациях может указывать на учебные маневры. Радиолюбители замечали корреляцию между такими сообщениями и известными датами военных учений.

Боевое управление (срочность 7–10). Самый редкий и самый тревожный тип. Длинные сообщения с большим количеством чисел и кодовых слов. По одной из теорий, именно такие передачи содержат команды для флота или ракетных войск. К счастью, за всю историю наблюдений таких сообщений были единицы, и ни одно из них не привело к реальным пускам (мы бы заметили).

Все сообщения сохраняются в PostgreSQL. Buzzer-1 умеет переключаться и на другие каналы: «пищалка» (5 448 кГц), «скрипящее колесо» (5 473 кГц), авиадиапазон Пулково (118,1 МГц). Так можно проверять, что приемник работает: при переключении характер сигнала меняется. Если сообщения получено именно по каналу УВБ-76, дополнительно уходит уведомление в Telegram. 

Восьмибитная битва, или как я спорил с тремя нейросетями одновременно

На этапе отладки я столкнулся с тем, что звук в браузере был ужасным — треск, шум, искажения. При этом rtl_fm работал, данные шли. Я начал копать.

Проблема оказалась в формате аудиоданных. Rtl_fm по умолчанию выдает восьмибитный unsigned PCM (uint8, диапазон 0–255). Мой код читал его как 16-битный signed PCM (int16, диапазон -32768…+32767). Байты сдвигались, семплы склеивались, и на выходе получалась каша.

Я попросил помощи у трех нейросетей одновременно: DeepSeek, Qwen 2.5 и KIMI. И начался цирк.

Qwen трижды настаивал, что нужно добавить флаг -f S16_LE в команду rtl_fm. Проблема в том, что rtl_fm не поддерживает такой флаг: -f означает frequency (частота), а не format. Qwen галлюцинировал.

DeepSeek помог с архитектурой, но тоже путался в форматах.

KIMI дал самый точный разбор архитектурных проблем, но тоже ошибался насчет формата, предлагая то int16, то S16_LE.

В итоге истина была установлена эмпирически: прямыми тестами на реальном железе. Оказалось, что rtl_fm на моей версии выдает 8-битный unsigned PCM. Решение — конвертировать uint8 → int16 для браузера и uint8 → float32 для Whisper.

Формулы конвертации:

  • для веба: (uint8 – 128) × 256 — дает диапазон -32768..32512, который ждёт JavaScript;

  • для Whisper: (uint8 – 128) / 128 — дает диапазон -1.0..1.0, с которым работает модель.

Давайте их разберем. Формула для браузера: (uint8 – 128) × 256. Браузерный Web Audio API ожидает 16-битные знаковые целые числа — формат Int16. Это числа в диапазоне от –32768 до +32767.

Что нам нужно сделать с uint8 (0–255):

  1. Сдвинуть диапазон, чтобы тишина оказалась в 0. В uint8 тишина — это значение около 128 (середина диапазона). Вычитаем 128: теперь тишина = 0, минимальный сигнал = –128, максимальный = 127.

  2. Растянуть в 16-битный диапазон. У нас есть числа от –128 до 127, а нужно от –32768 до 32767. Умножаем на 256 — и вот почему:

    • –128 × 256 = –32768 (нижняя граница Int16),

    • 0 × 256 = 0 (тишина),

    • 127 × 256 = 32512 (почти верхняя граница Int16, 32767).

Почему не ровно +32767? Потому что 127.5 × 256 = 32640, но у нас целые числа. Разница в 0.4% не имеет значения — человеческое ухо не заметит.

Формула для Whisper такова: (uint8 – 128) / 128. Модель Whisper, как и большинство нейросетей для обработки звука, ожидает числа с плавающей точкой в диапазоне от –1.0 до 1.0 — формат float32.

Что нам нужно сделать:

  1. Снова сдвинуть диапазон. Вычитаем 128, получаем числа от –128 до 127, где 0 = тишина.

  2. Сжать в диапазон -1.0…1.0. Делим на 128:

    • –128 / 128 = –1.0 (нижняя граница).

    • 0 / 128 = 0.0 (тишина).

    • 127 / 128 ≈ +0.992 (почти +1.0).

После этого звук стал чистым, Whisper перестал галлюцинировать. В очередной раз убедился: за нейронками стоит все перепроверять вручную.

AI-стек работает. Whisper слушает эфир, Ollama анализирует, Telegram ждет. За 141 час наблюдения ни одного голосового сообщения, но система к ним готова. Когда УВБ-76 заговорит, я увижу это первым.

А не послушать ли нам «радио Судного дня»… с AI? Создаю АПК радиолюбителя для УВБ-76 - 6

Что мы узнали об УВБ-76

Прошла почти неделя непрерывной работы системы. За это время собрано 8 447 метрик — это больше 141 часа наблюдений. По ним можно довольно уверенно сделать некоторые выводы.

Жужжание — это чистый тон. Никакой цифровой модуляции внутри нет. Я проверял это дважды: спектральным анализом и автокорреляцией. Результат однозначный: FSK, PSK или другие методы передачи данных внутри жужжания не используются. Это просто маркер занятости канала. Станция как бы говорит: «Эта частота занята, не лезьте».

Частота стабильна как атомные часы. За 141 час — ни одного дрейфа. Передатчик, скорее всего, использует прецизионный опорный генератор с GPS-дисциплиной. Никаких «плаваний» частоты, характерных для старого лампового оборудования.

Мощность не меняется. RMS держится на уровне 0,97 с колебаниями в пятом знаке после запятой. Передатчик работает 24/7 с постоянной мощностью — нет ни дневных, ни ночных переключений режима.

Голосовых сообщений не было, ни одного за всю неделю. Детектор пауз молчал. Это нормально: по опыту других наблюдателей, УВБ-76 может молчать неделями.

Архитектурные решения, которыми я горжусь

Когда я начинал этот проект, то думал, что это будет «скрипт на коленке» — запустил и забыл. Но инженерное мышление взяло свое, и в итоге получилась довольно продуманная система:

Модульная архитектура. Код разбит на восемь файлов: config.py, state.py, sdr.py, audio_proc.py, db.py, scan.py, html.py, web_server.py. Каждый модуль отвечает за свою задачу. Можно менять AI-часть, не трогая SDR-захват. Можно обновить веб-интерфейс, не затронув базу данных.

Queue для аудиопотока. Чтобы избежать гонки потоков между SDR-захватом и отправкой аудио в браузер, я использовал queue.Queue. Один поток читает данные из rtl_fm, складывает в очередь. Второй поток забирает данные из очереди и отправляет через WebSocket. Никаких блокировок, никаких потерянных байтов.

CI/CD для хобби-проекта. Да, я настроил пайплайн в Jenkins. Код хранится в Bitbucket. При пуше в main автоматически происходит синтаксическая проверка, копирование файлов на Raspberry Pi и перезапуск сервиса. Это дисциплина. И защита от дурака: править код на проде запрещено. Всё только через репозиторий.

А не послушать ли нам «радио Судного дня»… с AI? Создаю АПК радиолюбителя для УВБ-76 - 7

Что дальше: планы по развитию

Проект далек от завершения. В ближайшее время планирую подключить YouLoop. Эта антенна должна улучшить прием коротких волн. Сейчас комплектный штырь на подоконнике справляется, но с хорошей антенной сигнал будет чище.

В перспективе было бы здорово сделать на базе этого решения радиотелескоп для приема радиоизлучения Юпитера и Солнца. Думаю сначала получить цифровую картинку, а потом с помощью VLLM уже вывести графическое изображение. Звучит безумно, но с RTL-SDR это реально. А я с детства интересовался астрономией.

А не послушать ли нам «радио Судного дня»… с AI? Создаю АПК радиолюбителя для УВБ-76 - 8

Почему я это сделал

Этот проект начался как способ отвлечься от рабочей рутины. Я DevOps-инженер, и мне хотелось попробовать что-то совсем иное. Но в итоге получился целый проект на стыке радио, программирования и искусственного интеллекта. Я пришел в радио полным нулем, а через две недели у меня была работающая станция радиомониторинга с CI/CD и AI-анализом.

Если у вас есть опыт наблюдения за УВБ-76, идеи по архитектуре или вы просто хотите обсудить проект — пишите в комментариях. А если вы слышали «Бориса» или «иглу» в эфире за последнюю неделю — дайте знать. Возможно, моя система просто спала в этот момент.

Автор: artegaspb

Источник