ENPLDEPT
Flash
All docs
LoRaWAN gateway

Your gateway already hears everything. Let it work for the network.

Two ways. Direct — the forwarder also sends to sensmos.com:1700 and you pair the gateway with your wallet in the app; it earns and transmits beacons. Passive — one script on the gateway reports what it hears; nothing on the gateway changes.

radio · 868 MHz · emergency frame
◈
Heard by a neighbour · 2.1 kmgarage · temp 3.4°C · door OPEN · 21:44

Way 1 — direct: pair it with your wallet

Works with any gateway whose packet forwarder can send to more than one server (Crankk, RAK, Dragino, MikroTik, most Helium-era hotspots).

  1. In the gateway's packet forwarder add a server: sensmos.com, port 1700 (UDP, uplink and downlink). Keep your other servers — the forwarder sends every frame to each of them.
  2. Wait a minute, until the gateway has sent us something.
  3. In the Sensmos app: Dashboard → + → Add LoRaWAN gateway. Enter the gateway's EUI (16 hex characters, from its panel), optionally a name and a position — or use your phone's position while standing next to it. Tap Pair with wallet.

The app refuses if the gateway has been silent for more than 10 minutes (check the server entry) or is already paired with another wallet.

What you get: a card in the Dashboard — how many nodes the gateway hears and how many hear it (24 h), Sensmos frames heard in 24 h, the last beacon, earnings. On the live map the gateway has its own colour and panel. Name and position edits it; Unpair gateway on the card releases it.

Until it is paired: a gateway that already forwards to sensmos.com:1700 for 10 minutes starts working for the network under the project's wallet — its Sensmos frames are delivered, it transmits the coverage beacon and counts in the epoch. Pairing moves the card and the earnings to you; a gateway nobody pairs keeps earning for the project.

What the network does with it: every Sensmos frame it hears — emergency broadcasts, sensor frames — is delivered to the owner exactly like a frame heard by a node. Every 5 minutes the network transmits a short coverage beacon through your gateway; nodes that hear it confirm the gateway is where it says it is.

Way 2 — passive: one script, nothing on the gateway changes

For a gateway you do not want to reconfigure. A Python script on the gateway reads the forwarder's traffic on loopback and reports radio metadata. It needs one Sensmos node on the same LAN to vouch for it. Details follow.

Who this is for

Any Linux box running a plain Semtech packet forwarder (UDP, port 1700-style) pointed at a remote server: Helium hotspots and their multi-mining stacks, RAK, Dragino, Mikrotik, a Raspberry Pi with a concentrator HAT. If your gateway runs ChirpStack Gateway Bridge instead, there is a local MQTT broker and a different agent applies.

You also need one Sensmos node on the same LAN as the gateway (any ESP32 with the firmware, no radio required) and its PIN. The node vouches for the gateway; without it the backend will not issue a token.

Why passive, not a relay

The obvious way to tap a forwarder is to put something in the middle — point the forwarder at us and forward onwards. We deliberately do not. On hardware that earns, our outage would become your outage. The forwarder already sends every frame to each configured server separately over loopback; the script just reads along on lo. It is nobody's target, relays nothing and delays nothing.

Quick start

Run all of this on the gateway, not on your laptop. Python 3 only — no pip packages, nothing to build. Root is needed because reading raw packets requires CAP_NET_RAW.

# 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

Step 6 should show active (running), and the log prints connection works — first batch accepted once frames flow, then a status line every minute. In the app, the gateway appears under the node's Attachments; on the live map the node gains the RF layer.

Already paired, no service? The token survives; re-run steps 1 and 5. Only if --install-service says pair first repeat step 4.

Look before you trust us

--dry-run prints, as JSON, exactly the frames it would forward and stops there. Add --log-tx to also see when your gateway transmits. The script is a single readable file; the source is on GitHub.

Pairing — how the token works

The script has no credentials of its own. It asks the Sensmos node on your LAN (with the node's PIN) to vouch for it; the node relays the request to the backend over its own link, and the backend issues a token scoped to radio.frames — reporting received frames, nothing else. The token is stored in /etc/sensmos-gw-udp.json (mode 0600). From then on the script talks to the backend directly; the node is out of the loop.

  • One node can vouch for up to four attachments.
  • The owner can revoke any of them from the app (node → Attachments); the script becomes inert immediately.
  • A token lives 30 days from first use and the script renews it by itself.

The full contract (endpoints, scopes, formats) is on Attachments & the EXT API.

What it reads

Semtech GWMP over UDP, recognised by packet header, not by port, so it works whatever ports your stack uses:

byte
0protocol version (1 or 2)
1–2token
3type — 0 PUSH_DATA (frames received), 3 PULL_RESP (gateway asked to transmit)
4–11gateway EUI
restJSON

From PUSH_DATA it takes the rxpk entries: frequency, spreading factor, RSSI, SNR, size, CRC result and the raw PHY payload. Application payloads are encrypted with keys we do not have; what the network uses is the radio metadata and recognising Sensmos frames among the traffic.

PULL_RESP — a transmission your gateway is about to make — is reported only when it is audible to others (ipol = false, e.g. coverage beacons some networks transmit). A normal LoRaWAN downlink uses inverted IQ that nobody else can hear, proves nothing, and is skipped. Reported ones are proof the gateway is real: their payload is unpredictable, so a Sensmos node that hears the same bytes independently confirms the gateway actually transmitted.

A multi-server forwarder sends the identical frame to every configured server, so the same transmission shows up several times on loopback (RF packets received: 1 / PUSH_DATA datagrams sent: 2 in your forwarder log). The script deduplicates; one transmission is reported once. Frames are batched (once a second or every 50 frames) and sent to https://sensmos.com/v1/ext/radio/frames.

Flags

flagmeaning
--ifaceinterface to watch, default lo
--dry-runprint what would be sent, send nothing
--log-txalso log what your gateway transmits
--findscan the LAN for Sensmos nodes and print the pair command
--pair IP --pin PIN --name "…"one-time pairing through a node
--install-servicecopy to /usr/local/bin, write and enable the systemd unit (shown to you first)
--be URLalternative backend, for testing

The unit it writes, if you would rather do it by hand:

# /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 and containers

The script sniffs AF_PACKET on the host's loopback. Loopback is private per network namespace, so an isolated container sees an empty lo and hears nothing. Running it in a container needs --network host and --cap-add NET_RAW. The supported way is the systemd service on the host; a container build is yours to keep working.

Updates

The script never updates itself — by design. It runs as root on your gateway, in your LAN; auto-pulling code from a server would make that server a way into your network. To update, repeat step 1 and restart the service:

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

Troubleshooting

  • root required: run with sudo — raw capture needs CAP_NET_RAW. Use sudo (or setcap cap_net_raw+ep on the Python binary, which grants it to every Python program — sudo is cleaner).
  • Connected but no frames — check that traffic really is on loopback: sudo tcpdump -i lo -n -c 10 udp. If your forwarder talks to a remote server over the network rather than to a local bridge, point the script at that interface: --iface eth0.
  • AF_PACKET is Linux only — it is. This runs on the gateway, not on a Mac.
  • --find finds nothing — the node must be on the same LAN segment and online (its /info must answer with ws_connected: true). Give it the node's IP from the app instead.
  • pair first on --install-service — pairing did not complete; check the PIN and that the node is online.

Uninstall

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

Revoke the attachment in the app too. Nothing else was ever changed, so there is nothing else to undo.

In the network today

The first foreign gateway joined in August 2026 (Germany), vouched for by a node without a radio of its own. Frames it hears — emergency broadcasts and sensor frames of nodes in range — are delivered like frames heard by any node. Details of what LoRa does for a node owner: LoRa.

Questions, straight answers

Which way should I choose?

Direct, if you can add a server to the packet forwarder — it takes two minutes, the gateway gets its own card and earns. Passive, if you do not want to touch the forwarder's configuration or the gateway belongs to a network that forbids it.

Does a paired gateway earn GALU?

Yes, like a node with weight 0.3 — it takes part in the reward pool but does not add to the mint. Independently of that, when a paid sensor-frame receive is credited to the first foreign listener of the day and that listener is your gateway, 70% of the fee goes to you.

Does the direct way make my gateway transmit?

Yes — a short Sensmos coverage beacon every 5 minutes, sent through the forwarder like any downlink, within the duty cycle of the region. Nodes that hear it confirm the gateway is real. The passive script never transmits.

Is the position required?

No. Without one, an approximate position from the gateway's connection is used. A position you enter must be in the same country as that connection and within 500 km of it.

Do I need an account?

No. The direct way pairs the gateway with your wallet in the app. The passive way is vouched for by a Sensmos node on the same LAN and gets a narrow token scoped to reporting frames; the node owner can revoke it from the app.

What does the network get?

Every LoRa frame the gateway hears, as radio metadata plus the raw bytes. Sensmos frames (emergency broadcasts, sensor frames) are recognised and delivered to their owners; foreign LoRaWAN traffic only counts as spectrum statistics — the payloads are encrypted and we have no keys.

My gateway runs ChirpStack Gateway Bridge, not a bare forwarder

For the direct way it does not matter — add the server in the bridge instead. For the passive way there is a different agent (sensmos-gw-mqtt.py, in the same repository) — untested: we have no such gateway to verify it on.

Last updated: 2026-09-30