Das Vertrauensmodell
Zwei Barrieren, jede schließt eine andere Tür:
| Barriere | beweist | geprüft von |
|---|---|---|
| Node-PIN | du bist physisch im Netzwerk des Besitzers | dem Node |
| Backend-Grant | der Node existiert, hat einen Besitzer, hat einen freien Slot | dem Backend |
Drei Fragen werden bei jeder Anfrage getrennt gehalten:
| Frage | getragen von |
|---|---|
| wer bist du | Token (sma_…) |
| was du darfst | Scopes (Zeilen auf dem Server, keine Bytes im Token) |
| welches Format du sendest | die URL |
Ein Token sind 24 zufällige Bytes mit sma_-Präfix — es kodiert nichts. Berechtigungen liegen serverseitig und können bearbeitet oder widerrufen werden, ohne irgendetwas neu auszustellen.
Pairing — wie du ein Token bekommst
All das passiert im LAN, gegen den lokalen HTTP-Server des Nodes (Port 80). Du brauchst die IP des Nodes und seine PIN. (Um Nodes zu finden, frag /info im Subnetz ab — was antwortet, ist ein Node.)
1 — Node prüfen
GET http://<node-ip>/info
→ 200 { "device_id": "…", "firmware": "1.00", "ws_connected": true }
ws_connected muss true sein — der Node leitet deine Anfrage über seine eigene Verbindung ans Backend weiter.
2 — Autorisierung anfragen
POST http://<node-ip>/ext/authorize
Authorization: Bearer <PIN>
{ "kind": "lora-gw", "name": "rooftop RAK", "scopes": "radio.frames,radio.spectrum" }
scopes ist ein kommagetrennter String, kein JSON-Array. Antworten: 202 {"req":"<id>"} weitergeleitet, auf die Antwort pollen · 403 falsche PIN · 400 kind/scopes fehlt.
3 — auf den Grant pollen
GET http://<node-ip>/ext/authorize?req=<id>
Authorization: Bearer <PIN>
→ { "status": "pending" }
→ { "status": "granted", "token": "sma_…", "id": "<attachment-id>", "exp": "<iso>" }
→ { "status": "denied", "reason": "…" }
Einmal pro Sekunde pollen; der Node behält die Antwort etwa 5 Minuten. Speichere das Token mit Modus 0600 — von jetzt an sprichst du direkt mit dem Backend, und der Node ist raus aus der Schleife.
Ein erteiltes, aber ungenutztes Token läuft nach 30 Minuten ab. Seine volle Lebensdauer (30 Tage, verlängerbar) beginnt bei der ersten Nutzung. Ein Node bürgt für höchstens vier Attachments; der Besitzer kann jedes davon aus der App widerrufen, was das Token sofort wirkungslos macht.
Mit dem Backend sprechen
Basis-URL https://sensmos.com/v1/ext, jede Anfrage mit Authorization: Bearer sma_….
| Aufruf | Zweck |
|---|---|
GET /v1/ext/me | bist du noch am Leben, was darfst du senden, welche Integrationen/Formate der Server akzeptiert (catalog) — beim Start aufrufen |
POST /v1/ext/renew | Ablauf des Tokens verlängern; einmal am Tag reicht völlig |
POST /v1/ext/<integration>/<format> | Daten senden → 200 {"ok":true,"accepted":n} |
Live-Endpunkte:
| URL | Scope | Body |
|---|---|---|
/v1/ext/radio/rxpk | radio.frames | Semtech-Packet-Forwarder-PUSH_DATA-JSON, unverändert |
/v1/ext/radio/frames | radio.frames | natives Frame-Format (unten) |
/v1/ext/radio/channel | radio.spectrum | ein Spektrum-Messwert (unten) |
Datenformate
rxpk — Semtech Packet Forwarder, eins zu eins. Sende das Objekt genau so, wie der Forwarder es in PUSH_DATA erzeugt hat:
{ "rxpk": [ {
"time": "2026-08-21T20:48:11Z",
"freq": 867.3, "datr": "SF9BW125", "modu": "LORA",
"rssi": -44, "lsnr": 12.0, "size": 38, "stat": 1,
"data": "4FNNT1MgY2Nk…"
} ] }
datr wird nur für LoRa geparst; die Modulation kommt aus modu. stat folgt Semtech (1 CRC OK, -1 fehlgeschlagen, 0 keine). tmst wird ignoriert; ohne time stempelt der Server die Ankunftszeit.
frames — natives Format, für Attachments, die für uns geschrieben wurden (eigener Empfänger, SDR, Meshtastic-Bridge):
{ "frames": [ {
"ts": 1755808091, "freq": 867.3, "sf": 9, "mode": 0,
"rssi": -44, "snr": 12.0, "len": 38, "crc": true,
"hex": "e0534d4f53…", "dir": "rx"
} ] }
mode 0 LoRa / 1 FSK; sf 0 bei FSK; hex = PHY-Payload, die ersten 128 Bytes werden behalten. dir: "tx" ist für Aussendungen deines Gateways, die andere hören können (normale Downlinks mit invertiertem IQ beweisen nichts — überspringen); bei tx trägt rssi die TX-Leistung.
channel — ein Spektrum-Messwert:
{ "freq": 869.618, "bw": 62.5, "sf": 8, "sync": 18, "noise": -118, "peak": -97 }
freq ist Pflicht, der Rest hat Standardwert 0; noise/peak in dBm.
Grenzen und Fehler
| Code | Body | Bedeutung |
|---|---|---|
| 401 | invalid_token | Token unbekannt, abgelaufen oder widerrufen |
| 403 | missing_scope | Token gültig, aber nicht für diesen Endpunkt |
| 400 | Parser-Meldung | Body passt nicht zum Format |
| 404 | unknown_endpoint + Katalog | keine solche Integration/kein solches Format |
| 413 | too_many | über der Batch-Grenze — 200 Frames pro Anfrage |
Bündle auf deiner Seite — einmal pro Sekunde oder wenn sich 50 Frames angesammelt haben, je nachdem, was zuerst eintritt. Anwendungs-Payloads bleiben Ende-zu-Ende verschlüsselt: Der Server arbeitet mit Funk-Metadaten und erkennt Sensmos-Frames im Verkehr; er hat keine Schlüssel für irgendjemandes LoRaWAN-Payloads und will keine.
Was die Daten bewirken
Frames landen in denselben Tabellen und auf derselben Karte wie Frames, die das eigene Funkmodul eines Nodes hört — ein Attachment ist kein eigenes Wesen, es ist eine bessere Antenne für den Node, der dafür gebürgt hat. Sensmos-Emergency-Aussendungen und Sensor-Frames werden erkannt und an ihre Besitzer zugestellt; verifizierte Sensmos-Beacons bauen Funkabdeckungs-Kanten zwischen Nodes; das Datenprofil des bürgenden Nodes bekommt die Kategorie RF.
Fertige Attachments: LoRaWAN-Gateway (passiver Hörer). Der Quellcode beider Agents liegt auf GitHub.
Einen neuen Integrationstyp hinzufügen
Die Serverseite jeder Integration ist ein deklarativer Eintrag in einem Register (Scopes, Formate, Parser, Handler). Kandidaten nach demselben Muster, noch keiner davon live: GNSS-Himmelsansicht und Störungen (gnss.sky, bewusst ohne Position), ADS-B- und AIS-Empfangszusammenfassungen pro Zeitfenster, rtl_433, Stromzähler. Wenn du Hardware hast und ein Format akzeptiert haben willst, melde dich auf Discord — ein Format-Eintrag ist eine kleine, überprüfbare Änderung.