Server-side companion tools for RNS wardriving: a Sideband plugin for GPS beacon transmission, a collector bot that stores coverage data, and a web map server with uplink/downlink heatmap overlays.
  • Python 82.1%
  • JavaScript 15.5%
  • Shell 2.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Nickie Deuxyeux efdc540916 site: refresh the i18n.js mirror
The live file gained Partyline directory strings on 2026-09-09, edited outside
this repo. Re-copied so the mirror matches what is actually served.

The map integration is unaffected: the diff is additions only, and the
postMessage block that tells the embedded map which language to label in is
still present and the file parses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019MFBSTjHR2KBqY4yRWUxsP
2026-09-09 22:31:27 +03:00
bot bot: add "destinations" LXMF command, document nodes/announce in README 2026-07-09 23:28:33 +03:00
init_scripts Fix systemd scripts: rnsd.service -> rns.service 2026-05-26 13:59:00 +03:00
map map: open an expanded cluster's popup beside the column, not over it 2026-09-09 22:11:50 +03:00
plugin Update collector destination hash in plugin config 2026-05-26 14:11:12 +03:00
site site: refresh the i18n.js mirror 2026-09-09 22:31:27 +03:00
.gitignore bot: add LXMF "announce" command with delayed reannounce and throttling 2026-07-09 20:31:55 +03:00
README.md map: shape RTL labels and keep build artifacts out of the web root 2026-09-07 19:05:54 +03:00

rns-wardrive-tools

A collection of tools for wardriving LoRa coverage using the Reticulum Network Stack and Sideband.

To feed location data to the collector bot you can use either the Sideband plugin below or the dedicated Android app, rns-wardrive-app, which provides a live map, transmission log, and a bi-directional Probe mode for two-way RF link quality surveys.

Components

plugin/ — Sideband Plugin

A Sideband service plugin that runs on your mobile device and periodically sends your GPS location to the collector bot over RNS. If you'd rather use a standalone app instead of running it inside Sideband, see rns-wardrive-app.

Installation:

Copy wardrive_plugin.py and wardrive_config.json to your Sideband plugins directory (configured in Sideband settings under Plugins path).

Configuration (wardrive_config.json):

{
  "announce_interval_secs": 30,
  "announce_mode": "auto",
  "send_mode": "packet",
  "bot_hashes": ["<bot collector destination hash>"]
}
Option Values Description
announce_interval_secs integer (min 15) How often to send location in auto mode
announce_mode auto / manual auto: sends on interval; manual: sends when you press Sideband's announce button
send_mode announce / packet announce: broadcasts on the network; packet: sends directly and privately to the bot
bot_hashes list of hex strings Collector destination hashes of bots to send to (packet mode only)

In packet mode, location data is sent encrypted directly to each listed bot — it is not broadcast to the wider network. RSSI/SNR are still recorded on the receiving end.

In announce mode, location is broadcast as a wardrive.beacon announce visible to all nodes.


bot/ — Collector Bot

A server-side RNS daemon that listens for incoming location data, records RF signal statistics, and responds to LXMF commands.

Requirements: rns, lxmf, rnsd running

Installation:

Copy wardrive_bot.py to /opt/reticulum/bin/ (or any location on your $PATH).

Usage:

python3 wardrive_bot.py

The bot writes to ~/wardrive_bot.db (SQLite) and logs to /etc/reticulum/wardrive_bot_logfile.

LXMF commands (send via Sideband or any LXMF client):

Command Response
ping Pong! [14:32:01, RSSI: -74dBm, SNR: 6.5dB] — RF stats included if message arrived over LoRa
status Point count and timestamp of last received beacon
nodes Count of discoverable nodes within the configured radius
destinations Collector/probe destination hashes, plus the relay (aggregator) hash if this bot is the aggregator
announce Reannounces all of the bot's destinations after a short randomized delay

map/ — Coverage Map Server

A Flask web server that reads the bot's database and serves a live interactive coverage heatmap.

Requirements: flask, rns, plus a basemap tileset (see Self-hosted basemap below)

Installation:

Copy map_server.py to /opt/reticulum/bin/ and wardrive_map.json to /etc/reticulum/.

Configuration (wardrive_map.json):

{
  "db_path":       "~/wardrive_bot.db",
  "discovery_dir": "/etc/reticulum/storage/discovery/interfaces",
  "listen_on":     "0.0.0.0",
  "port":          5000,
  "page_title":    "Wardrive Coverage Map",
  "initial_lat":   null,
  "initial_lon":   null,
  "initial_zoom":  null
}

Set initial_lat, initial_lon, and initial_zoom to pre-position the map on load; leave all three as null to auto-fit to recorded points.

Usage:

python3 map_server.py

Serves on http://0.0.0.0:5000 by default.

Features:

  • Live heatmap of recorded coverage points, coloured by RSSI
  • Live data stream panel showing incoming packets in real time (source, coordinates, RSSI/SNR/Q)
  • Date range filter with day/week/month/year presets
  • Light/dark UI theme with tileset selector (Dark, Voyager, Voyager no labels, Light, OSM)
  • RNS infrastructure discovery panel — shows RNodes, Backbone, and I2P interfaces that have GPS coordinates, sourced from the local RNS discovery cache
  • Toggleable node labels per interface type

Self-hosted basemap

The map renders with MapLibre GL JS against vector tiles served from this machine, using the dark-matter OpenMapTiles style, alongside positron, osm-bright and maptiler-basic. Nothing on the page is fetched from a third party: tiles, glyphs, sprites, JavaScript, and reverse geocoding are all local.

All five basemaps share the single vector tileset — a GL style is only a JSON recipe, so each extra basemap costs a style file and a sprite sheet, not another copy of the map data. The "no labels" entry is generated by dropping the symbol layers from Voyager.

Assets live outside the repo because the tileset is far too large to version:

Path Contents
<tileset_dir>/static/tiles/basemap.pmtiles Vector basemap (planet z0-12 + Central FD z13-14)
<tileset_dir>/static/fonts/ Composed glyph stacks
<tileset_dir>/static/sprite/ Style sprite sheet
<tileset_dir>/static/styles/ Localized GL style JSON
<tileset_dir>/static/js/ maplibre-gl + pmtiles
<tileset_dir>/static/js/ (cont.) RTL text plugin for Arabic/Hebrew shaping
<tileset_dir>/geo/cities.tsv.gz Offline reverse-geocoding dataset
<tileset_dir>/build/ Build intermediates — deliberately outside the served tree

tileset_dir defaults to /mnt/sdb1/tilesets and is set in wardrive_map.json. Only static/ and geo/ are read at runtime; keep font archives and unlocalized styles in build/ so they are not exposed over HTTP.

Every style renders {name:nonlatin} beside {name:latin}, so a worldwide basemap shows place names in local scripts. The glyph stacks cover Cyrillic, Arabic, Hebrew, CJK, Hangul and Devanagari; Arabic and Hebrew additionally need the RTL text plugin, which is loaded lazily from local storage.

Build tooling lives in map/tiles/ and map/geo/:

map/tiles/build.sh planet      # planet z0-12   (long; needs the planet .pbf)
map/tiles/build.sh cfd         # regional z13-14
map/tiles/build.sh merge       # stitch + convert to .pmtiles

map/tiles/compose_glyphs.py fonts-src fonts        # merge fontstacks for static serving
map/tiles/localize_style.py  style.json out.json   # strip third-party URLs from a GL style
map/tiles/make_nolabels.py   in.json out.json      # derive a label-free style variant
map/tiles/check_styles.py                          # verify styles against tiles/fonts/sprites
map/geo/build_geodb.py       geo/cities.tsv.gz     # refresh the geocoding dataset

check_styles.py is worth running after adding a basemap: a style that references a source-layer the tileset lacks, or a fontstack with no glyph directory, renders as a map quietly missing those features rather than raising an error.

Planetiler writes its scratch to /var/tmp/planetiler-tmp by default. Keep that on fast storage — if the tileset volume is a spinning disk, building there is many times slower than building on an SSD and copying the result across.

In production nginx serves /tiles/, /fonts/, /sprite/, and /static/ directly off disk (see map/nginx/wardrive-map-assets.conf); the Flask routes for those paths are a fallback so the map still works when hitting the port directly. Serving the .pmtiles archive requires HTTP range request support, which nginx provides for static files natively — there is no tile server process.


init_scripts/ — Service Files

Systemd and OpenRC service files for both the bot and the map server.

OpenRC:

cp init_scripts/openrc/wardrive-bot  /etc/init.d/
cp init_scripts/openrc/wardrive-map  /etc/init.d/
chmod +x /etc/init.d/wardrive-bot /etc/init.d/wardrive-map
rc-update add wardrive-bot default
rc-update add wardrive-map default

Systemd:

cp init_scripts/systemd/wardrive-bot.service /etc/systemd/system/
cp init_scripts/systemd/wardrive-map.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now wardrive-bot wardrive-map

Data Flow

[Sideband + plugin]
       |
       | wardrive.beacon announce  (announce mode, visible to all)
       | wardrive.collector packet (packet mode, encrypted to bot only)
       |
[wardrive_bot.py] ──── wardrive_bot.db ────[map_server.py]──── browser
       |
  LXMF replies (ping, status)