LoRaWAN Analyzer

February 26, 2026 ยท View on GitHub

Real-time LoRaWAN traffic analyzer for ChirpStack. Captures uplinks, downlinks, join requests, and TX acknowledgements via MQTT, stores everything in Postgres + TimescaleDB, and serves a web dashboard for monitoring and analysis.

Dashboard

Live Feed

Features

  • Dashboard -- gateway tabs, operator/device tree, traffic charts, channel/SF distribution, duty cycle, device breakdown
  • Device detail -- per-device FCnt timeline, packet loss, RSSI/SNR trends, interval histogram, SF/frequency/gateway distributions
  • Live packet feed -- real-time WebSocket stream with packet type, RSSI range, and ownership filters
  • Operator identification -- built-in LoRa Alliance NetID database (175+ operators), plus custom prefix mappings
  • Visibility filtering -- separate "my devices" from foreign traffic using DevAddr prefix rules
  • Join request tracking -- grouped by JoinEUI with timeline and manufacturer lookup
  • Session tracking -- correlates join requests with subsequent data uplinks
  • Airtime calculation -- per-packet, based on Semtech SX127x datasheet formulas

Setup

1. Configure

cp config.toml.example config.toml

Edit config.toml and point mqtt.server at your ChirpStack MQTT broker (the same one ChirpStack Gateway Bridge publishes to):

[mqtt]
server = "tcp://your-chirpstack-mqtt:1883"
username = ""
password = ""
format = "protobuf"
  • server -- MQTT broker address
  • format -- protobuf for ChirpStack v4 (default), json for v3 or JSON marshaler

Common broker addresses (Docker):

ChirpStack setupmqtt.server value
Same host, separate compose projecttcp://host.docker.internal:1883 or tcp://172.17.0.1:1883
Same Docker networktcp://<mosquitto-container-name>:1883
Remote hosttcp://chirpstack.example.com:1883

If unsure, check your ChirpStack docker-compose.yml for the mosquitto/EMQX service name, or chirpstack-gateway-bridge.toml for the MQTT server address.

2. Start

docker compose up -d
ContainerPortDescription
lorawan-analyzer15337Web dashboard + API
lorawan-postgres--Postgres + TimescaleDB (internal only)

Dashboard: http://localhost:15337

docker compose logs -f analyzer

Upgrading from the ClickHouse version? The old data is incompatible. Delete data/ before starting: rm -rf data/ && docker compose up -d

Configuration

ChirpStack Devices

The DevAddr dropdown on the Live page has a ChirpStack Devices option. When selected, the live feed switches from gateway-sourced packets to the application-level MQTT stream โ€” packets are enriched with ChirpStack device names and application names instead of raw DevAddr labels. The device list in the sidebar also switches to show ChirpStack-registered devices grouped by application.

This mode requires that the analyzer is connected to the same MQTT broker as ChirpStack (the default setup). No extra config is needed.

Custom Operators

Label your own networks by DevAddr prefix. These override the built-in NetID database:

[[operators]]
prefix = "26000000/20"          # hex DevAddr prefix / bit length
name = "My Network"
known_devices = true            # marks as "my devices" for visibility filter
color = "#3b82f6"               # dashboard color

# multiple prefixes per operator
[[operators]]
prefix = ["26011234/32", "26015678/32"]
name = "My Sensors"
known_devices = true

Prefix format: AABBCCDD/N -- the upper N bits of the DevAddr are compared. 26000000/20 matches any DevAddr starting with 0x26000....

Operators can also be defined without a prefix to assign a color to a ChirpStack application name. When the live feed is in ChirpStack Devices mode, packets are grouped by application_name; entries here are matched by name and used to set the color in the dashboard:

[[operators]]
name = "Hydrogen"
color = "#3b82f6"

[[operators]]
name = "Ozone"
color = "#a855f7"

The name must exactly match the application name as it appears in ChirpStack.

Multiple MQTT Servers

Connect to more than one broker simultaneously. Packets from all brokers are merged into the same database:

[[mqtt_servers]]
server = "tcp://chirpstack2.example.com:1883"
format = "protobuf"

[[mqtt_servers]]
server = "tcp://chirpstack3.example.com:1883"
format = "json"

The primary [mqtt] section is always connected. Each [[mqtt_servers]] entry adds an additional connection.

Gateway Names (gateways.csv)

Place data/gateways.csv (next to docker-compose.yml) to pre-seed gateway names and map coordinates. Gateways are registered at startup, so named tabs appear immediately even before any packets arrive.

id,name,alias,latitude,longitude
0016c001f184aa22,wifx,backyard pole,46.9480,7.4474
0016c001f1137226,sensecap,roof panel,,
7076ff0056071e21,kerlink0,,,
ColumnRequiredDescription
idyesGateway EUI (hex, lowercase)
namenoDisplay label (falls back to raw ID if blank)
aliasnoReserved
latitude / longitudenoBoth required to place a map pin

If a gateway already exists in the database, only the CSV fields that are present overwrite existing values. The file is optional.

Hide Rules

Suppress specific traffic from the UI:

[[hide_rules]]
type = "dev_addr"               # or "join_eui"
prefix = "26000000/20"
description = "Hide my sensors"

Operators and hide rules can also be managed at runtime via the API.

See config.toml.example for all available settings.

API

Most endpoints accept hours (time window) and gateway_id (filter by gateway) query parameters. Endpoints returning device data also support filter_mode (owned/foreign/all) and prefixes (comma-separated HEX/bits list).

Gateways

EndpointDescription
GET /api/gatewaysList all gateways with stats
GET /api/gateways/:idSingle gateway details
GET /api/gateways/:id/treeOperator/device tree
GET /api/gateways/:id/operatorsOperators seen on gateway
GET /api/gateways/:id/devicesDevices on gateway
GET /api/gateways/:id/operators/:name/devicesDevices for a specific operator

Devices

EndpointDescription
GET /api/devices/:devaddrDevice activity / recent packets
GET /api/devices/:devaddr/profileSummary stats (packet count, avg RSSI/SNR, airtime)
GET /api/devices/:devaddr/fcnt-timelineFrame counter progression with gap detection
GET /api/devices/:devaddr/intervalsPacket interval histogram
GET /api/devices/:devaddr/signal-trendsRSSI/SNR over time
GET /api/devices/:devaddr/distributionsSF, frequency, gateway breakdown
GET /api/devices/:devaddr/packet-lossMissed packets / loss rate

Joins

EndpointDescription
GET /api/joinsRecent join requests
GET /api/joins/by-euiGrouped by JoinEUI
GET /api/joins/eui/:joinEui/timelineTimeline for a specific JoinEUI

Statistics

EndpointDescription
GET /api/stats/summaryOverview stats (packets, devices, airtime, duty cycle)
GET /api/stats/operatorsPer-operator breakdown
GET /api/stats/timeseriesTime series (accepts interval, metric, group_by)
GET /api/stats/duty-cycleDuty cycle stats
GET /api/stats/downlinksDownlink / TX ack stats
GET /api/packets/recentRecent packets (accepts packet_types, rssi_min, rssi_max)
GET /api/spectrum/:gw/channelsChannel usage distribution
GET /api/spectrum/:gw/spreading-factorsSF distribution

Operators & Config

EndpointDescription
GET /api/operatorsList custom operators
POST /api/operatorsAdd operator ({prefix, name, priority?})
DELETE /api/operators/:idRemove operator
GET /api/hide-rulesList hide rules
POST /api/hide-rulesAdd rule ({type, prefix, description?})
DELETE /api/hide-rules/:idRemove rule
GET /api/config/my-devicesConfigured "my devices" prefixes
GET /api/config/operator-colorsOperator color map

WebSocket

EndpointDescription
WS /api/liveLive packet stream (all gateways)
WS /api/live/:gatewayIdLive stream for a specific gateway

Query parameters: types (comma-separated: data, join_request, downlink, tx_ack), rssi_min, rssi_max, filter_mode, prefixes.

Development

# Rebuild after backend/source changes
docker compose build --no-cache analyzer && docker compose up -d

# Restart after config changes
docker compose restart analyzer

Frontend files (public/) are volume-mounted -- changes apply on browser refresh.

License

MIT