Two Dongles One Raspi
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:
| Serial | Gerät | Antenne | Frequenz | Verwendung |
|---|---|---|---|---|
QFH001 | Nooelec NESDR SMArTee v5 | QFH, 137 MHz | METEOR-M2 LRPT | SatDump Autotrack |
LBAND01 | Nooelec NESDR SMArTee v5 | 12-turn RHCP Helix, L-Band | Inmarsat 1.5 GHz | rtl_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
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
- osmocom/rtl-sdr,
src/rtl_tcp.c–sighandler()setzt beiSIGTERM/SIGINT/SIGQUITnurdo_exit = 1; der tatsächliche Rückgabewert vonmain()(return r >= 0 ? r : -r;) stammt aus der letzten Zuweisung vonr, typischerweise demselect()-Aufruf in der Accept-Schleife, der bei Signalunterbrechung-1liefert. ↩ - satdump.org ↩
- rtl-sdr.com ↩
- orbtrack.org ↩
- osmocom/rtl-sdr,
convenience.c–verbose_device_search()interpretiert ein rein numerisches Argument (Default beim Fehlen von-d:"0") als rohen Geräteindex, nicht als Serial-Suche.rtl_test,rtl_tcpundrtl_eepromrufen bei fehlendem-dalle internverbose_device_search("0")auf, siehe z.B.rtl_test.c. Jederrtlsdr_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. ↩