~/blog

HexSDR: odbiornik web SDR z ponad 120 dekoderami, zbudowany od zera

10.07.2026 · 5 min

Na żywo: https://sdr.gut.red

HexSDR to mój autorski projekt: odbiornik radia definiowanego programowo (SDR), który w całości działa w przeglądarce. Do serwera podłączasz tuner RTL-SDR (lub inne wspierane urządzenie), a cała reszta dzieje się z poziomu strony WWW: wodospad widma, strojenie, dekodowanie samolotów, statków, pakietów APRS, sond meteorologicznych, cyfrowego głosu i wielu innych emisji. Projekt wystartował w kwietniu 2026 roku – dziś ma na koncie blisko 2000 commitów, około 490 plików w Pythonie i 310 w TypeScriptcie.

Idea była prosta: nie chciałem kolejnego zestawu osobnych narzędzi radiowych posklejanych na kolanie skryptami w Bashu. Zależało mi na jednym, spójnym systemie, w którym każdy rodzaj sygnału podlega tym samym regułom – tak, bym rozumiał architekturę od A do Z i mógł błyskawicznie wdrażać nowe protokoły.

Stack

  • Backend: Python 3.11, FastAPI, NumPy, SciPy, Numba
  • Frontend: React 19, TypeScript, Vite, Tailwind 4, Zustand
  • Mapa: MapLibre GL 5 (globus 3D, teren, budynki)
  • Wodospad: WebGL (regl)
  • Komunikacja i dane: MQTT, SQLite
  • Docker, GitLab CI/CD

1. Każdy dekoder trzyma się jednego kontraktu

Podczas startu backend rejestruje 125 dekoderów. Każdy z nich jest wtyczką realizującą identyczny interfejs: przyjmuje paczkę próbek IQ i zwraca metadane w ustandaryzowanym formacie. Każdy deklaruje też swoje preferowane pasmo (preferred_bandwidth), na podstawie którego serwis DSP automatycznie konfiguruje wycinek widma (slice) w momencie podpięcia dekodera. Nigdy nie ustawiam tego ręcznie.

Każdy dekoder ma także własne okno w interfejsie użytkownika, skrojone pod specyfikę zwracanych danych. ADS-B wymaga tabeli i mapy z wektorami lotu, a emisje słabosygnałowe zupełnie innego podejścia do wizualizacji. Dodanie nowego protokołu sprowadza się do przejścia przez stałą checklistę miejsc w kodzie (spisałem to w regułach projektu – dokładnie 16 kroków), dzięki czemu o niczym się nie zapomina. Panel administracyjny nie korzysta ze sztywnej listy dekoderów – odpytuje rejestr w locie, więc nowa wtyczka pojawia się w nim automatycznie.

2. Wodospad rysuje WebGL

Wodospad widma częstotliwości rysowany jest w WebGL z rozdzielczością dochodzącą do 128k punktów (bins). Zoom działa bezpośrednio w punkcie kursora: częstotliwość pod myszką pozostaje nieruchoma, a cała reszta skali płynnie rozszerza się wokół niej. Brzmi jak detal, ale przy precyzyjnym strojeniu bardzo wąskich pasm robi to kolosalną różnicę między wygodną pracą a ciągłą irytacją.

3. Wiele odbiorników z jednego dongla

Pojedyncze urządzenie fizyczne może zasilać wiele niezależnych wycinków pasma („slices”). Każdy slice stanowi osobny wirtualny odbiornik w granicach pasma sprzętowego, dysponuje własnym demodulatorem i może mieć podpięty dedykowany dekoder.

Nad całością czuwa harmonogram (scheduler), który potrafi samodzielnie skakać po częstotliwościach i urządzeniach – również równolegle na podstawie zdefiniowanych zakładek. Przetestowałem to w boju, dekodując równocześnie kilka stacji z RDS z jednego pasma przepustowego.

4. Ciężkie DSP, a Python wciąż wystarczająco szybki

Python zazwyczaj nie jest pierwszym wyborem do cyfrowego przetwarzania sygnałów, dlatego narzuciłem sobie twarde reguły architektoniczne. Wszystkie krytyczne pętle obliczeniowe zamknąłem w funkcjach kompilowanych przez Numbę (@njit).

Dla trybów szerokopasmowych (takich jak ADS-B czy DAB) zaimplementowałem tzw. fast path, która całkowicie omija zbędne filtry i tor audio. Jeśli Numba nadal okazuje się wąskim gardłem z powodu GIL-a (Global Interpreter Lock), najbardziej obciążające dekodery są delegowane do osobnych procesów systemowych.

Rozwiązałem też problem, którego początkowo nie przewidziałem: zanikanie słabych sygnałów. Przy konwersji zmiennoprzecinkowych próbek IQ (float) do formatu 8- lub 16-bitowego na potrzeby zewnętrznych narzędzi (np. rtl_433 czy redsea), słaby sygnał potrafi zająć zaledwie kilka najmłodszych bitów i bezpowrotnie zginąć w szumie kwantyzacji. Każdy mostek tego typu wyposażyłem więc w adaptacyjną normalizację – rodzaj wewnętrznego układu AGC, który dba o to, by sygnał zawsze wypełniał przynajmniej połowę dostępnego zakresu dynamiki.

5. Kontrola dostępu wbudowana w fundamenty

Projekt ma cztery poziomy uprawnień: administrator, moderator, zalogowany użytkownik oraz gość (anonim). Granulacja schodzi aż do poziomu pojedynczych dekoderów i demodulatorów – mogę na przykład globalnie zablokować wstęgi LSB i USB dla zwykłych użytkowników.

Logowanie obsługuje standard OpenID Connect z PKCE. Zablokowanie konta natychmiast unieważnia aktywną sesję, a każda akcja administracyjna trafia do niezmiennego dziennika audytowego (audit log). Niezalogowani użytkownicy mogą swobodnie oglądać publiczny podgląd – chciałem, by projekt pozostał otwarty, ale w trybie wyłącznie do odczytu.

Grosz uczciwości

Nie będę udawał, że wszystkie 125 dekoderów przetestowałem na żywym eterze. W repozytorium prowadzę jawną matrycę stanu z czterema statusami: potwierdzony (confirmed), działający (operational), nieprzetestowany (untested) oraz uszkodzony (broken). Status zmienia się na wyższy wyłącznie wtedy, gdy osobiście odbiorę i zdekoduję prawdziwy sygnał z anteny. Kod może być w 100% gotowy, ale dopóki nie złapie fali w terenie, wisi przy nim etykieta „untested”.

Przy starcie systemu pojawia się ekran ładowania stylizowany na stary BIOS – z detekcją sprzętu i animowanym radarem. Nawet numer wersji na powitaniu jest generowany automatycznie na podstawie historii gita. Wiem, że to zupełnie zbędny bajer. Po prostu lubię takie smaczki.

👉 Zobacz na żywo: https://sdr.gut.red

Szczegóły techniczne architektury

Frontend

Aplikacja kliencka powstała w oparciu o React 19, TypeScript oraz Vite, ostylowana za pomocą Tailwind CSS 4. Stan aplikacji spoczywa w wyspecjalizowanych magazynach Zustand (osobny store dla każdego obszaru, np. niezależne stany dla ADS-B, AIS i APRS), do których strumieniowo trafiają zdarzenia bezpośrednio z WebSocketów.

Wodospad generowany jest przez bibliotekę regl bezpośrednio na WebGL, a warstwę kartograficzną obsługuje MapLibre GL 5 z pełnym wsparciem dla ukształtowania terenu, brył budynków 3D oraz projekcji kuli ziemskiej. Interfejs jest celowo gęsty i bezkompromisowy – bliżej mu do profesjonalnego terminala operacyjnego niż do typowej, rozmytej aplikacji webowej.

Backend i przetwarzanie

Sercem backendu jest Python z frameworkiem FastAPI. Tor cyfrowego przetwarzania sygnałów (DSP) opiera się na bibliotekach NumPy i SciPy, wspartych Numbą w miejscach wymagających natychmiastowego czasu wykonania. Dekodery działają jako rejestrowane przy starcie wtyczki, a w uzasadnionych przypadkach komunikują się z wyspecjalizowanymi narzędziami zewnętrznymi przez dedykowane mostki IPC.

Telemetria i komunikaty rozgłaszane są przez brokera MQTT, natomiast konta użytkowników, zakładki częstotliwości i historia tras obiektów zapisywane są w bazie SQLite. Cała konfiguracja jest ściśle zmapowana: każdy parametr w pliku konfiguracyjnym ma swój bezpośredni odpowiednik w zmiennych środowiskowych.

Wdrożenie i CI/CD

Całość została skonteneryzowana w Dockerze. Każdy push do repozytorium uruchamia pełny potok GitLab CI/CD: skanowanie pod kątem wycieku sekretów, statyczną analizę bezpieczeństwa (SAST), lintery, testy jednostkowe, budowanie obrazów, testy dymne (smoke tests) oraz publikację w prywatnym rejestrze. Po pomyślnym przejściu potoku produkcja aktualizuje się w pełni automatycznie przez webhook. W tym samym procesie automatycznie synchronizują się dokumentacja oraz projektowe wiki.

#ADS-B#AIS#APRS#Docker#DSP#FastAPI#GitLab CI#HexSDR#MapLibre#Numba#Python#RBAC#React#rtl-sdr#sdr#software defined radio#web SDR#WebGL

Komentarze

Jeszcze nikt nie skomentował. Bądź pierwszy.