Two Dongles One Raspi

SDR-Switch – Lab Note

SDR-Switch

Lab Note

tl;dr: Zwei RTL-SDR und nur ein Raspberry Pi, der die Daten auswertet oder weiterleitet. Zwei Dienste (satdump & rtl_tcp), die auf dieselbe Hardware zugreifen. Dabei stehen mehrere USB-Ports zur Verfügung, um beide USB-Geräte gleichzeitig anzuschliessen. Was bisher von Hand geschah, kann jetzt per SSH-Befehl aus der Ferne erledigt werden.

Motivation

Der regelmässige Betrieb von zwei passiven Funkdiensten und das damit verbundene Aufs-Dach-Steigen haben mich bewogen, diesen Fix umzusetzen.

  • Die Auswertung von Wettersatelliten-Daten von LEO-Satelliten (Low Earth Orbit). Näher beschrieben in diesem Artikel.
  • Dem Empfang und dem LAN zur Verfügung stellen von Rohdaten des Satelliten Alphasat, eines der geostationären Kommunikationssatelliten des Betreibers INMARSAT.

Um den Signalweg möglichst kurz zu halten, sollten lange Antennenkabel vermieden werden. Aus diesem Grund werden die analogen Signale räumlich nah an den Antennen entweder gleich ausgewertet (QFH-Antenne → SDR → satdump2) oder dem Netzwerk digital mittels TCP zur Verfügung gestellt (rtl_tcp3).

Ausgangslage

Zwei RTL-SDR-Dongles bleiben permanent am Raspberry Pi angeschlossen, je einer pro Antenne/Use-Case:

SerialGerätAntenneFrequenzVerwendung
QFH001Nooelec NESDR SMArTee v5QFH, 137 MHzMETEOR-M2 LRPTSatDump Autotrack
LBAND01Nooelec NESDR SMArTee v512-turn RHCP Helix, L-BandInmarsat 1.5 GHzrtl_tcp

Die beiden Dienste laufen nicht gleichzeitig – zu jedem Zeitpunkt ist immer nur einer der beiden aktiv. Im Alltag hat das bisher ein Ein- und Ausstecken der Dongles bedeutet.

Umsetzung

Eindeutige Serials per EEPROM

Beide Dongles hatten ab Werk dieselbe Serial 00000001 – ohne eindeutige Kennung lässt sich das Gerät nicht gezielt ansprechen. Einzeln angeschlossen beschrieben:

# Nur QFH-Dongle angeschlossen:
rtl_eeprom -d 0 -s QFH001

# Nur L-Band-Dongle angeschlossen:
rtl_eeprom -d 0 -s LBAND01

Nach Reboot verifiziert:

rtl_eeprom -d 0 2>&1 | head -20
rtl_eeprom -d 1 2>&1 | head -20

Beispielausgabe für 0:

Found 2 device(s):

  0:  Generic RTL2832U OEM
  1:  Generic RTL2832U OEM

Using device 0: Generic RTL2832U OEM
Found Rafael Micro R820T tuner
Current configuration:

__________________________________________
Vendor ID: 0x0bda
Product ID: 0x2838
Manufacturer: Nooelec
Product: NESDR SMArTee v5
Serial number: QFH001
Serial number enabled: yes
IR endpoint enabled: yes
Remote wakeup enabled: no
__________________________________________

SatDump an Serial binden

In der Config-Datei von satdump config.txt muss jetzt noch für den METEOR-Dienst die ID des Dongles eingefügt werden. Das geschieht nach der source-Zeile:

"source": "rtlsdr",
"source_id": "QFH001",

Zwei sich ausschliessende systemd-Services

Das Aufschalten der jeweiligen Dienste geschieht über systemd-Dienste. Dazu werden folgende Dateien angelegt.

/etc/systemd/system/satdump.service:

[Unit]
Description=SatDump AutoTrack
After=network.target mnt-synology\x2dsatdump.mount
Conflicts=rtl_tcp.service

[Service]
Type=simple
User=<user>
WorkingDirectory=/home/<user>
ExecStart=/usr/bin/satdump autotrack /home/<user>/satdump-config/config.txt
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

/etc/systemd/system/rtl_tcp.service:

[Unit]
Description=RTL_TCP Server
After=network.target
Conflicts=satdump.service

[Service]
ExecStart=/usr/bin/rtl_tcp -a 0.0.0.0 -p 1234 -d LBAND01
Restart=always
SuccessExitStatus=1

[Install]
WantedBy=multi-user.target

Conflicts ist dazu da, einen konkurrierenden Dienst anzugeben. So ergibt sich eine Ausschluss-Garantie, damit immer nur ein Service gleichzeitig aktiv ist.

SuccessExitStatus=1: rtl_tcp beendet sich bei SIGTERM mit Exit-Code 1 statt 0. Ursache ist keine Fehlbehandlung des Signals selbst, sondern der Rückgabewert von main(): Die Variable r wird im Verlauf des Programms mehrfach wiederverwendet, zuletzt für den Rückgabewert von select() in der Accept-Schleife. Wird das Programm während eines laufenden select()-Aufrufs per Signal unterbrochen, liefert select() -1 (EINTR), und main() gibt am Ende return r >= 0 ? r : -r; zurück – aus -1 wird so 1.1 Ohne SuccessExitStatus=1 interpretiert systemd das als Fehlschlag und der Service landet auf failed statt inactive.

sdr-switch CLI

Das Skript um die Dienste bequem umzuschalten. Installiert unter /usr/local/bin/sdr-switch:

#!/bin/bash

SERVICES=("satdump" "rtl_tcp")

status() {
    for svc in "${SERVICES[@]}"; do
        echo "  ${svc}: $(systemctl is-active ${svc}.service)"
    done
}

switch_to() {
    local target="$1"
    local other="$( [ "$target" = "satdump" ] && echo "rtl_tcp" || echo "satdump" )"

    echo "Switching to ${target}..."
    sudo systemctl stop "${other}.service"
    sudo systemctl reset-failed "${other}.service" 2>/dev/null
    sudo systemctl start "${target}.service"

    echo ""
    status
}

case "$1" in
    satdump|rtl_tcp)
        switch_to "$1"
        ;;
    status)
        status
        ;;
    *)
        echo "Usage: sdr-switch [satdump|rtl_tcp|status]"
        status
        ;;
esac

Damit der Befehl ohne Passwort-Prompt läuft, ist der Nutzer <user> in /etc/sudoers.d/sdr-switch gezielt für genau diese sechs systemctl-Aufrufe freigeschaltet:

<user> ALL=(ALL) NOPASSWD: /bin/systemctl stop satdump.service, /bin/systemctl start satdump.service, /bin/systemctl reset-failed satdump.service, /bin/systemctl stop rtl_tcp.service, /bin/systemctl start rtl_tcp.service, /bin/systemctl reset-failed rtl_tcp.service

Optional: Steuerung via Agentic AI (OpenClaw)

Agentische KIs eröffnen die Möglichkeit, mittels eines Kanals von aussen mit der KI zu kommunizieren und Befehle ausführen zu lassen oder Abfragen zu machen. Üblicherweise sind diese Kanäle Discord, WhatsApp, Telegram oder Mail. Im konkreten Fall kann dadurch per natürlicher Sprache ein Wechsel der Antennen ausgelöst werden.

Skill installiert unter ~/.openclaw/skills/sdr-switch/SKILL.md. Die agentische KI ruft dieselben Befehle nur per SSH auf:

ssh $USER@<host> "sdr-switch status"
ssh $USER@<host> "sdr-switch satdump"
ssh $USER@<host> "sdr-switch rtl_tcp"

Manpage

Installiert unter /usr/local/share/man/man1/sdr-switch.1, abrufbar mit man sdr-switch.

Ergebnisse

sdr-switch status        # Welcher Service läuft?
sdr-switch satdump       # Auf METEOR/QFH umschalten
sdr-switch rtl_tcp       # Auf Inmarsat/L-Band umschalten

Beispielausgabe von status:

user@host:~$ sdr-switch status

  satdump: active

  rtl_tcp: inactive
satdump Service Status
satdump im aktiven Zustand
rtl_tcp Service Status
rtl_tcp im aktiven Zustand

Herausforderungen

Der rtl_tcp-Exitcode

Beim Testen des Umschaltens fiel auf, dass rtl_tcp.service nach jedem Stop – etwa ausgelöst durch sdr-switch satdump – nicht sauber auf inactive, sondern auf failed stand: systemctl status rtl_tcp.service zeigte Active: failed (Result: exit-code), journalctl -u rtl_tcp dazu Main PID exited, code=exited, status=1. Der Dienst hatte dabei tatsächlich anstandslos aufgehört zu senden, ohne Absturz oder Datenverlust, nur der falsche Status. Das deutete eher auf eine Eigenheit des Exit-Codes selbst hin als auf einen echten Fehler im eigenen Setup.

Der Blick in den rtl_tcp-Quellcode (s. Umsetzung, Fussnote 1) bestätigte das: Der Signal-Handler von rtl_tcp fängt SIGTERM ab und setzt nur ein Flag (do_exit = 1); der tatsächliche Rückgabewert von main() stammt von der letzten Verwendung der wiederverwendeten Variable r, typischerweise vom durch das Signal unterbrochenen select()-Aufruf, der dann -1 liefert – daraus wird per return r >= 0 ? r : -r; der Exit-Code 1. Ein Upstream-Verhalten, kein Bug in der eigenen Konfiguration.

Die Konsequenz war entsprechend kein Fix am eigenen Setup, sondern die schlichte Deklaration SuccessExitStatus=1 in der Unit-Datei, die systemd mitteilt, diesen Exit-Code als regulär zu werten. Der reset-failed-Schritt im sdr-switch-Skript bleibt trotzdem als Absicherung bestehen – für den Fall, dass der Dienst aus einem anderen Grund tatsächlich fehlschlägt.

Ausblick

Um automatisch nach Bedarf umzuschalten und eine hohe Monitoring-Zeit für den Alphasat-Service zu gewährleisten, könnten die Dienste aufgrund einer Orbit-Vorhersage4 der METEOR-Satelliten umgeschaltet werden. Das würde einen fast lückenlosen Empfang der Alphasat-Kommunikation mit mehreren 15-Minuten-Unterbrüchen pro Tag ermöglichen.

Nachtrag

Erst nach Abschluss dieses Setups kam die Frage auf, ob die beiden Dienste überhaupt exklusiv laufen müssen. Ein Blick in den rtl_tcp-Quellcode zeigt: Ein Lock, der zwei unterschiedliche, per Serial adressierte Dongles am gleichzeitigen Betrieb hindern würde, existiert in librtlsdr nicht. Jeder rtlsdr_open()-Aufruf öffnet unabhängig genau ein physisches USB-Gerät. (Nur ohne explizite Serial- oder Index-Angabe -d – etwa bei einem blossen rtl_test – wählen rtl_eeprom, rtl_test, rtl_tcp & Co. per Default Geräteindex 0, das erste von rtlsdr_get_device_count() gefundene Gerät in USB-Enumerationsreihenfolge, unabhängig von Zweck oder Beschriftung.5)

Ein Test hat's bestätigt: satdump und rtl_tcp können tatsächlich parallel laufen, sobald beide über eine feste Serial an ihren jeweiligen Dongle gebunden sind – was in diesem Setup ohnehin schon der Fall war (s. Umsetzung). sdr-switch und die Ausschluss-Garantie über Conflicts= waren also nie eine technische Notwendigkeit, sondern von Anfang an eine Wahl. Das macht die exklusive Umschaltung im Nachhinein aber nicht überflüssig: Immer nur den gerade gebrauchten Dienst laufen zu lassen, spart CPU-Last und Strom und hält den Betrieb schlank – eine bewusste, ressourcenschonende Alternative zum permanenten Parallelbetrieb, gerade weil er möglich wäre.


Anhang

Quelldateien

  • /usr/local/bin/sdr-switch
  • /etc/systemd/system/satdump.service
  • /etc/systemd/system/rtl_tcp.service
  • /etc/sudoers.d/sdr-switch
  • /usr/local/share/man/man1/sdr-switch.1
  • ~/.openclaw/skills/sdr-switch/SKILL.md
  1. osmocom/rtl-sdr, src/rtl_tcp.csighandler() setzt bei SIGTERM/SIGINT/SIGQUIT nur do_exit = 1; der tatsächliche Rückgabewert von main() (return r >= 0 ? r : -r;) stammt aus der letzten Zuweisung von r, typischerweise dem select()-Aufruf in der Accept-Schleife, der bei Signalunterbrechung -1 liefert.
  2. satdump.org
  3. rtl-sdr.com
  4. orbtrack.org
  5. osmocom/rtl-sdr, convenience.cverbose_device_search() interpretiert ein rein numerisches Argument (Default beim Fehlen von -d: "0") als rohen Geräteindex, nicht als Serial-Suche. rtl_test, rtl_tcp und rtl_eeprom rufen bei fehlendem -d alle intern verbose_device_search("0") auf, siehe z.B. rtl_test.c. Jeder rtlsdr_open(&dev, index)-Aufruf öffnet unabhängig genau ein physisches USB-Gerät; ein programm- oder treiberweiter Lock über mehrere Dongles hinweg ist im Quellcode nicht vorhanden.
Zurück
Zurück

Satelliten Selfies