Für wen das ist
Jede Linux-Kiste mit einem einfachen Semtech Packet Forwarder (UDP, Port-1700-Stil), der auf einen entfernten Server zeigt: Helium-Hotspots und ihre Multi-Mining-Stacks, RAK, Dragino, Mikrotik, ein Raspberry Pi mit Concentrator-HAT. Läuft dein Gateway stattdessen mit ChirpStack Gateway Bridge, gibt es einen lokalen MQTT-Broker und ein anderer Agent kommt zum Einsatz.
Du brauchst außerdem einen Sensmos-Node im selben LAN wie der Gateway (jeder ESP32 mit der Firmware, kein Funkmodul nötig) und seine PIN. Der Node bürgt für den Gateway; ohne ihn stellt das Backend kein Token aus.
Warum passiv und kein Relay
Der naheliegende Weg, einen Forwarder anzuzapfen, ist, etwas dazwischenzuschalten — den Forwarder auf uns zeigen lassen und weiterleiten. Das tun wir absichtlich nicht. Auf Hardware, die verdient, würde unser Ausfall zu deinem Ausfall. Der Forwarder schickt ohnehin jeden Frame separat an jeden konfigurierten Server über Loopback; das Skript liest auf lo einfach mit. Es ist niemandes Ziel, leitet nichts weiter und verzögert nichts.
Schnellstart
Führe all das auf dem Gateway aus, nicht auf deinem Laptop. Nur Python 3 — keine pip-Pakete, nichts zu bauen. Root ist nötig, weil das Lesen roher Pakete CAP_NET_RAW erfordert.
# 1 — install the script
sudo curl -fsSL -o /usr/local/bin/sensmos-gw-udp.py https://raw.githubusercontent.com/Galusz/sensmos-gateway/main/sensmos-gw-udp.py
sudo chmod +x /usr/local/bin/sensmos-gw-udp.py
# 2 — see what would leave your box (sends nothing, needs no pairing)
sudo sensmos-gw-udp.py --dry-run
# 3 — find a Sensmos node on this network (prints the exact pair command)
sudo sensmos-gw-udp.py --find
# 4 — pair with it (address from step 3, PIN = the node's PIN)
sudo sensmos-gw-udp.py --pair NODE_IP --pin YOUR_PIN --name "rooftop RAK"
# 5 — install & start it as a boot-persistent service
sudo sensmos-gw-udp.py --install-service
# 6 — confirm
systemctl status sensmos-gw-udp --no-pager
journalctl -u sensmos-gw-udp -f
Schritt 6 sollte active (running) zeigen, und das Log gibt connection works — first batch accepted aus, sobald Frames fließen, danach jede Minute eine Statuszeile. In der App erscheint der Gateway unter den Attachments des Nodes; auf der Live-Karte bekommt der Node die RF-Ebene.
Schon gekoppelt, kein Dienst? Das Token bleibt erhalten; wiederhole Schritt 1 und 5. Nur wenn --install-service pair first meldet, wiederhole Schritt 4.
Schau nach, bevor du uns vertraust
--dry-run gibt als JSON genau die Frames aus, die es weiterleiten würde, und hört dort auf. Füge --log-tx hinzu, um auch zu sehen, wann dein Gateway sendet. Das Skript ist eine einzelne lesbare Datei; der Quellcode liegt auf GitHub.
Pairing — wie das Token funktioniert
Das Skript hat keine eigenen Zugangsdaten. Es bittet den Sensmos-Node in deinem LAN (mit der PIN des Nodes), für es zu bürgen; der Node leitet die Anfrage über seine eigene Verbindung ans Backend weiter, und das Backend stellt ein Token aus, das auf radio.frames beschränkt ist — empfangene Frames melden, sonst nichts. Das Token wird in /etc/sensmos-gw-udp.json (Modus 0600) gespeichert. Von da an spricht das Skript direkt mit dem Backend; der Node ist raus aus der Schleife.
- Ein Node kann für bis zu vier Attachments bürgen.
- Der Besitzer kann jedes davon aus der App widerrufen (Node → Attachments); das Skript wird sofort wirkungslos.
- Ein Token lebt 30 Tage ab erster Nutzung, und das Skript verlängert es von selbst.
Der vollständige Vertrag (Endpunkte, Scopes, Formate) steht unter Attachments & die EXT-API.
Was es liest
Semtech GWMP über UDP, erkannt am Paket-Header, nicht am Port, damit es funktioniert, egal welche Ports dein Stack nutzt:
| Byte | |
|---|---|
| 0 | Protokollversion (1 oder 2) |
| 1–2 | Token |
| 3 | Typ — 0 PUSH_DATA (empfangene Frames), 3 PULL_RESP (Gateway soll senden) |
| 4–11 | Gateway-EUI |
| Rest | JSON |
Aus PUSH_DATA nimmt es die rxpk-Einträge: Frequenz, Spreading Factor, RSSI, SNR, Größe, CRC-Ergebnis und die rohe PHY-Payload. Anwendungs-Payloads sind mit Schlüsseln verschlüsselt, die wir nicht haben; was das Netzwerk nutzt, sind die Funk-Metadaten und das Erkennen von Sensmos-Frames im Verkehr.
PULL_RESP — eine Aussendung, die dein Gateway gleich macht — wird nur gemeldet, wenn sie für andere hörbar ist (ipol = false, z. B. Coverage-Beacons, die manche Netze senden). Ein normaler LoRaWAN-Downlink nutzt invertiertes IQ, das niemand sonst hören kann, beweist nichts und wird übersprungen. Gemeldete sind der Beweis, dass der Gateway echt ist: Ihre Payload ist unvorhersehbar, sodass ein Sensmos-Node, der dieselben Bytes unabhängig hört, bestätigt, dass der Gateway tatsächlich gesendet hat.
Ein Multi-Server-Forwarder schickt den identischen Frame an jeden konfigurierten Server, sodass dieselbe Aussendung mehrfach auf Loopback auftaucht (RF packets received: 1 / PUSH_DATA datagrams sent: 2 in deinem Forwarder-Log). Das Skript dedupliziert; eine Aussendung wird einmal gemeldet. Frames werden gebündelt (einmal pro Sekunde oder alle 50 Frames) und an https://sensmos.com/v1/ext/radio/frames geschickt.
Flags
| Flag | Bedeutung |
|---|---|
--iface | zu beobachtende Schnittstelle, Standard lo |
--dry-run | ausgeben, was gesendet würde, nichts senden |
--log-tx | zusätzlich loggen, was dein Gateway sendet |
--find | das LAN nach Sensmos-Nodes durchsuchen und den Pair-Befehl ausgeben |
--pair IP --pin PIN --name "…" | einmaliges Pairing über einen Node |
--install-service | nach /usr/local/bin kopieren, systemd-Unit schreiben und aktivieren (wird dir vorher gezeigt) |
--be URL | alternatives Backend, zum Testen |
Die Unit, die es schreibt, falls du es lieber von Hand machst:
# /etc/systemd/system/sensmos-gw-udp.service
[Unit]
Description=Sensmos passive gateway listener
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/bin/python3 /usr/local/bin/sensmos-gw-udp.py
Restart=always
RestartSec=10
User=root
[Install]
WantedBy=multi-user.target
Docker und Container
Das Skript sniffit AF_PACKET auf dem Loopback des Hosts. Loopback ist pro Netzwerk-Namespace privat, ein isolierter Container sieht also ein leeres lo und hört nichts. In einem Container braucht es --network host und --cap-add NET_RAW. Der unterstützte Weg ist der systemd-Dienst auf dem Host; ein Container-Build ist deine Sache.
Updates
Das Skript aktualisiert sich nie selbst — mit Absicht. Es läuft als Root auf deinem Gateway, in deinem LAN; Code automatisch von einem Server zu ziehen würde diesen Server zu einem Weg in dein Netzwerk machen. Zum Aktualisieren wiederhole Schritt 1 und starte den Dienst neu:
sudo curl -fsSL -o /usr/local/bin/sensmos-gw-udp.py https://raw.githubusercontent.com/Galusz/sensmos-gateway/main/sensmos-gw-udp.py
sudo systemctl restart sensmos-gw-udp
Fehlerbehebung
root required: run with sudo— Raw-Capture brauchtCAP_NET_RAW. Nutzesudo(odersetcap cap_net_raw+epauf der Python-Binary, was es jedem Python-Programm gewährt —sudoist sauberer).- Verbunden, aber keine Frames — prüfe, ob der Verkehr wirklich auf Loopback läuft:
sudo tcpdump -i lo -n -c 10 udp. Spricht dein Forwarder über das Netzwerk mit einem entfernten Server statt mit einer lokalen Bridge, zeig das Skript auf diese Schnittstelle:--iface eth0. AF_PACKET is Linux only— ist es. Das läuft auf dem Gateway, nicht auf einem Mac.--findfindet nichts — der Node muss im selben LAN-Segment und online sein (sein/infomuss mitws_connected: trueantworten). Gib stattdessen die IP des Nodes aus der App an.pair firstbei--install-service— das Pairing ist nicht durchgelaufen; prüfe die PIN und dass der Node online ist.
Deinstallieren
sudo systemctl disable --now sensmos-gw-udp
sudo rm /etc/systemd/system/sensmos-gw-udp.service /usr/local/bin/sensmos-gw-udp.py /etc/sensmos-gw-udp.json
Widerrufe das Attachment auch in der App. Sonst wurde nie etwas geändert, also gibt es nichts weiter rückgängig zu machen.
Im Netzwerk heute
Der erste fremde Gateway kam im August 2026 dazu (Deutschland), verbürgt durch einen Node ohne eigenes Funkmodul. Frames, die er hört — Emergency-Aussendungen und Sensor-Frames von Nodes in Reichweite — werden zugestellt wie Frames, die irgendein Node hört. Details dazu, was LoRa für einen Node-Besitzer tut: LoRa.