HiroAlleyCat/wigle-to-wdgwars

GitHub: HiroAlleyCat/wigle-to-wdgwars

Stars: 5 | Forks: 0

# wigle-to-wdgwars Push WiGLE-format Wi-Fi/BLE wardrive CSVs (and optionally aircraft JSON) to the **[WDGoWars](https://wdgwars.pl/)** community wardriving leaderboard. A small Python 3 CLI. One dependency: [gungnir](https://github.com/HiroAlleyCat/gungnir), the shared HMAC transport client used by every wdgwars.pl feeder in this family. Install it with `pip install -r requirements.txt` (no git on PATH required — pip fetches it as a tarball over plain HTTPS). ## Family Sibling repos in the WDGoWars feeder family: ## Contents - [What this is](#what-this-is) - [Easiest install — guided setup](#easiest-install--guided-setup) — `./setup.sh` saves both keys and installs a daily timer - [Quick start — one-off push without saving keys](#quick-start--one-off-push-without-saving-keys) - [Installing](#installing) — manual venv + pip flow - [Getting a WiGLE CSV in the first place](#getting-a-wigle-csv-in-the-first-place) - [Running on a schedule (timer)](#running-on-a-schedule-timer) — what `--schedule` installs, plus hand-written recipes - [WDGoWars API reference](#wdgowars-api-reference) — reverse-engineered, since the portal has no public docs - [Aircraft JSON format (signed endpoint)](#aircraft-json-format-signed-endpoint) - [Troubleshooting](#troubleshooting) - [Related tools](#related-tools) - [License](#license) ## What this is 1. Pushes a WiGLE-1.6 CSV to `/api/upload-csv` for Wi-Fi + BLE. 2. Pushes a JSON list of aircraft records to the signed `/api/upload/` endpoint. 3. Optionally **pulls your uploads straight from WiGLE** (`--from-wigle`) and pushes them, so you never touch a file. 4. Documents the wire format so the next person doesn't have to start over (see [WDGoWars API reference](#wdgwars-api-reference)). It's designed to be readable, droppable into a cron job, and friendly to new players who haven't published a wardrive before. ### Who this is for - You wardrive with the **[WiGLE Android app](https://play.google.com/store/apps/details?id=net.wigle.wigleandroid)** or another tool that exports WiGLE-format CSV, and you want a second place to send your captures. - You run a **Kismet** or **hcxdumptool** rig and have converted its output to WiGLE CSV. - You want a **scheduled push** from a Pi/server that keeps a local DB of observations and produces CSVs. - You're a tool author who needs a working reference for the WDGoWars ingest contract. ## Easiest install — guided setup If you just want a daily push running and don't want to read the rest of this README, this is the path. One script does the whole install: venv, deps, both API keys validated, and a daily timer. git clone https://github.com/HiroAlleyCat/wigle-to-wdgwars.git cd wigle-to-wdgwars ./setup.sh # Linux / Mac / Pi REM Windows: double-click setup.bat, or from a terminal: setup.bat What `setup.sh` does, in order: 1. Creates a project-local `.venv/` and installs `requirements.txt` into it (works on PEP 668 distros without `--break-system-packages`). 2. Prompts for your **WDGoWars API key**, validates it against `/api/me`, saves to `~/.config/wigle-to-wdgwars/wdgwars.key` (mode 600). 3. Prompts for your **WiGLE token** (the "Encoded for use" string from [wigle.net/account](https://wigle.net/account)), validates it by listing one transaction, saves to `~/.config/wigle-to-wdgwars/wigle.key` (mode 600). Skippable if you only want to push local CSVs. 4. Offers to install a **daily timer** (systemd user unit / cron entry / Windows scheduled task, depending on what your OS supports) that runs `--from-wigle` at 03:00 local time and uploads your latest WiGLE drive. 5. Defaults the timer to **dry-run** so the first scheduled tick decodes and logs but never POSTs. Re-run `./run.sh --schedule` and answer "no" to the dry-run prompt to flip it live. After that, `./run.sh` (no args) does a one-off push, and the timer takes care of the rest. To remove the schedule later: `./run.sh --unschedule`. You can run `--setup` again at any point to rotate keys or reconfigure the timer — it's idempotent and asks before replacing anything. To do any of those steps without the bootstrap script (e.g. you already have a venv), invoke the same flags directly: .venv/bin/python wigle_to_wdgwars.py --setup # full interactive flow .venv/bin/python wigle_to_wdgwars.py --schedule # just the timer step .venv/bin/python wigle_to_wdgwars.py --unschedule # remove the timer # Non-interactive equivalents (for provisioning): .venv/bin/python wigle_to_wdgwars.py --save-key YOUR_WDGWARS_KEY .venv/bin/python wigle_to_wdgwars.py --save-wigle-key YOUR_WIGLE_TOKEN .venv/bin/python wigle_to_wdgwars.py --schedule --schedule-time 03:00 \ --schedule-chunk-size 10000 --schedule-dry-run ### What to expect after `./setup.sh` A few things that can read as "is this broken?" the first time: - **The first scheduled tick won't show up on your leaderboard.** `--setup` defaults the timer to dry-run — the tick decodes and writes a log but never POSTs. This is intentional so you can verify the install before flipping live. To go live, re-run `./run.sh --schedule` and answer "no" to the dry-run prompt. - **A scheduled run can't read keys from your shell environment.** systemd / cron / schtasks all run in a stripped-down environment without your `$WDGWARS_API_KEY` / `$WIGLE_API_KEY` env vars. The scheduled command reads the saved key files (`~/.config/wigle-to-wdgwars/wdgwars.key` + `wigle.key`) instead. `--setup` saved both for you. If you skip `--setup` and only export env vars, the timer will fail at run time. - **WiGLE rate-limits your own-account pulls.** The auto-installed timer runs `--from-wigle --wigle-latest 1` once daily, which stays comfortably under the WiGLE free-tier query budget. If you bump up `--wigle-latest` or run more often, you can hit a per-account quota and start seeing `HTTP 429` in the log. ### Checking it's running You don't have to wait for the daily fire — verify the install end-to-end right after `./setup.sh`: # Linux (systemd user manager) systemctl --user list-timers wigle-to-wdgwars.timer systemctl --user start wigle-to-wdgwars.service # fire one tick now journalctl --user -u wigle-to-wdgwars.service -n 30 # Linux/Mac (cron — installed when systemd isn't available) crontab -l | grep wigle-to-wdgwars tail -f ~/.wigle-to-wdgwars-cron.log # Windows (schtasks) schtasks /Query /TN WigleToWDGoWars /V /FO LIST :: shows Last Run Result schtasks /Run /TN WigleToWDGoWars :: fire one tick now # Task Scheduler does NOT capture stdout. To see what a run produces, # fire the same command from PowerShell yourself: .venv\Scripts\python wigle_to_wdgwars.py --from-wigle --wigle-latest 1 \ --chunk-size 10000 --dry-run A `--dry-run` tick that succeeded looks like (in the log / journal): [wigle] pulling 1 most-recent upload(s): [wigle] : KB -> WDGoWars [wdgwars] POST https://wdgwars.pl/api/upload-csv field=file file=.csv chunks=1 total= KB [wdgwars] dry-run: not sending The `dry-run: not sending` is the safety stop — your data didn't ship to the leaderboard yet, but everything up to that point worked. To flip live: ./run.sh --schedule # interactive, answer "n" to the dry-run prompt # or, headless: .venv/bin/python wigle_to_wdgwars.py --schedule --schedule-time 03:00 \ --schedule-chunk-size 10000 # no --schedule-dry-run = live ### Common surprises - **`bash: ./setup.sh: Permission denied`** — you downloaded the ZIP instead of `git clone`, and the executable bit didn't survive. Run `bash setup.sh` instead, or `chmod +x *.sh scripts/*.sh` first. - **`error: externally-managed-environment` from `pip install`** — Bookworm / Debian 12+ / Ubuntu 23.04+ / Homebrew Python enforce PEP 668 and refuse to install into system Python. The `./setup.sh` flow uses a project-local `.venv/` and works around this. If you've been pasting `python3 -m pip install -r requirements.txt` from an old README, switch to `./setup.sh` (or to the venv recipe in [Installing](#installing) below). - **`Failed to create venv` from `./setup.sh`** — the `python3-venv` module isn't installed on Debian/Ubuntu/Pi by default. `sudo apt install -y python3-venv python3-full` and re-run. - **`./run.sh` errors with `no API key`** — you skipped `--setup` (or it didn't get to the save step). Run `./run.sh --setup` to do the wizard. - **Timer installed but nothing on the leaderboard the next day** — see the dry-run note above. You're seeing the safety stop, not a broken install. - **`HTTP 429` in the log** — either WDGoWars is asking you to wait (server-side queue is processing your previous upload — the tool sleeps and retries on the next tick) or WiGLE is rate-limiting you for pulling too often. The cooldown file at `~/.config/wigle-to-wdgwars/cooldown.json` is honored across runs. ## Quick start — one-off push without saving keys If you just want to push a single file right now without saving anything to disk, paste the key on the command line. Use the venv from [Installing](#installing) — pasting `python3 wigle_to_wdgwars.py` directly against system Python errors out with `error: externally-managed-environment` on Bookworm / Debian 12+ / Homebrew. The venv path is one extra line and works on every distro. # Inside the venv from the Installing section .venv/bin/python wigle_to_wdgwars.py --whoami --key YOUR_WDGWARS_API_KEY # → [wigle-to-wdgwars] key OK — user=… wifi=… ble=… aircraft=… .venv/bin/python wigle_to_wdgwars.py my-wardrive.wiglecsv.gz \ --key YOUR_WDGWARS_API_KEY --chunk-size 10000 `--chunk-size 10000` is the safe default for anything over ~5 000 rows. See the [Cloudflare 524 footgun](#the-cloudflare-524-footgun) for why. On Windows: `.venv\Scripts\python wigle_to_wdgwars.py ...`. Or just use `run.bat` from the [guided setup](#easiest-install--guided-setup) above. ### No file at all — pull straight from WiGLE If you wardrive with the WiGLE app, your runs already get uploaded to WiGLE. With `--from-wigle` the tool grabs your latest upload from WiGLE directly and pushes it to WDGoWars — you never export, unzip, or move a file. You need two keys: your **WDGoWars** key (`--key`) and your **WiGLE** token (`--wigle-key`, the "Encoded for use" string from [wigle.net/account](https://wigle.net/account)). .venv/bin/python wigle_to_wdgwars.py --from-wigle \ --wigle-key YOUR_WIGLE_ENCODED_TOKEN \ --key YOUR_WDGWARS_API_KEY \ --chunk-size 10000 By default it pulls your single most-recent upload. Use `--wigle-latest N` to push the last N uploads instead. This is the mode the auto-installed [timer](#running-on-a-schedule-timer) uses for a fully hands-off pipeline. ## Installing You need **Python 3.10 or newer** and `pip`. Git is **not** required — pip fetches gungnir (the one dependency) over plain HTTPS using stdlib `urllib`. ### Option A — ZIP download (no git needed) 1. Grab the ZIP from [the GitHub repo](https://github.com/HiroAlleyCat/wigle-to-wdgwars) (Code → Download ZIP) and unzip it. 2. From inside the unzipped folder: python3 -m venv .venv # required on Bookworm / Homebrew (PEP 668) .venv/bin/pip install -r requirements.txt .venv/bin/python wigle_to_wdgwars.py --help ### Option B — clone with git git clone https://github.com/HiroAlleyCat/wigle-to-wdgwars.git cd wigle-to-wdgwars python3 -m venv .venv # required on Bookworm / Homebrew (PEP 668) .venv/bin/pip install -r requirements.txt .venv/bin/python wigle_to_wdgwars.py --help ### Windows It runs on Windows exactly the same way — it's plain Python, no Linux-only bits. python -m pip install -r requirements.txt python wigle_to_wdgwars.py --whoami --key YOUR_API_KEY_HERE python wigle_to_wdgwars.py my-wardrive.wiglecsv.gz --key YOUR_API_KEY_HERE --chunk-size 10000 For a hands-off scheduled push on Windows, see [Running on a schedule → Windows](#windows--task-scheduler). ### Updating The easiest path is `--update`, which does both steps for you: ./run.sh --update That runs `git pull --ff-only` when this is a git checkout, otherwise fetches `wigle_to_wdgwars.py` and `requirements.txt` from raw GitHub atomically. Either way it then refreshes the venv deps, so a release that bumps the gungnir pin self-heals without you having to remember the second step. If you'd rather do it by hand (which is what older releases told you to do): git pull # or: re-download the ZIP and overwrite the folder .venv/bin/pip install --upgrade -r requirements.txt If you skip the second line on a dep-bump release, you'll end up with new code importing the old gungnir bytes, which is a recipe for subtle parity bugs. ### Where the API keys are read from (in order) **WDGoWars** (`--key` / `$WDGWARS_API_KEY` / `wdgwars.key`): 1. `--key YOUR_KEY` on the command line. 2. `$WDGWARS_API_KEY` environment variable. 3. `~/.config/wigle-to-wdgwars/wdgwars.key` (mode 600). **WiGLE** (`--wigle-key` / `$WIGLE_API_KEY` / `wigle.key`, used by `--from-wigle`): 1. `--wigle-key YOUR_TOKEN` on the command line. 2. `$WIGLE_API_KEY` environment variable. 3. `~/.config/wigle-to-wdgwars/wigle.key` (mode 600). `--setup` saves both as files for you. To save them non-interactively (for provisioning from a script): .venv/bin/python wigle_to_wdgwars.py --save-key YOUR_WDGWARS_KEY .venv/bin/python wigle_to_wdgwars.py --save-wigle-key YOUR_WIGLE_TOKEN The script also writes two state files in `~/.config/wigle-to-wdgwars/`: | File | Purpose | |---|---| | `cooldown.json` | Persisted server-cooldown deadline. Set by 429 responses so a scheduled run an hour later still respects it. | | `hwm.json` | High-water mark — last successful upload timestamp and import counts, for monitoring. Pure read-only output. | ## Getting a WiGLE CSV in the first place If you've already been wardriving with the WiGLE Android app, skip to [Option A](#option-a--wigle-android-app). Otherwise, here are the most common paths. ### Option A — WiGLE Android app The easiest entry point. Install [WiGLE WiFi Wardriving](https://play.google.com/store/apps/details?id=net.wigle.wigleandroid) from the Play Store, give it location + Bluetooth permissions, and do a run. Afterwards you can hand the tool the export either way: - **Database → Export to CSV** gives you a plain `WigleWifi_yyyyMMddHHmmss.csv`. - The **share / upload** flow gives you a gzipped `*.wiglecsv.gz` (a single compressed file, sometimes with no inner file extension). You do **not** need to unzip the `.gz` by hand. This tool detects gzip and decompresses it for you, so just point it at whichever file you have: # plain CSV ./run.sh WigleWifi_20260523120000.csv --chunk-size 10000 # the gzipped export works too — no unzipping needed ./run.sh my-run.wiglecsv.gz --chunk-size 10000 If you want BLE included, make sure WiGLE's Bluetooth scanning is enabled in settings before the drive. ### Option B — Kismet + `kismetdb_to_wiglecsv` If you already capture with [Kismet](https://www.kismetwireless.net/), the official conversion tool ships with it: kismetdb_to_wiglecsv \ --in /var/log/kismet/Kismet-20260523.kismet \ --out wardrive.csv ./run.sh wardrive.csv --chunk-size 10000 ### Option C — hcxdumptool + `hcxpcapngtool` If you run [hcxdumptool](https://github.com/ZerBea/hcxdumptool), pipe the pcapng through `hcxpcapngtool --csv=...`: hcxpcapngtool --csv=wardrive.csv capture.pcapng ./run.sh wardrive.csv --chunk-size 10000 ### Option D — Roll your own The WiGLE-1.6 CSV format is two header lines followed by data rows. The columns are: MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type `Type` is `WIFI`, `BLE`, or `GSM` (only WIFI/BLE are honored by WDGoWars). The first header line is a meta comment that WiGLE writes; the tool preserves both header lines when chunking. Minimal example: WigleWifi-1.6,appRelease=v0.0.0 MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type aa:bb:cc:dd:ee:ff,ExampleSSID,[WPA2-PSK-CCMP][ESS],2026-05-23 12:00:00,6,-55,41.0,-81.0,200,10,WIFI ## Running on a schedule (timer) The point of a leaderboard is showing up consistently. Instead of pushing by hand every time, set a timer and forget it. **Fastest path — let the tool install the timer for you.** `--schedule` writes the right artifact for your OS (systemd user unit on Linux-with-systemd, cron entry on Mac / Linux-without-systemd, scheduled task on Windows). Defaults to `--from-wigle` daily at 03:00 with `--chunk-size 10000`, in dry-run mode the first time so the first tick decodes and logs but never POSTs. .venv/bin/python wigle_to_wdgwars.py --schedule # interactive .venv/bin/python wigle_to_wdgwars.py --schedule \ --schedule-time 03:00 --schedule-chunk-size 10000 \ --schedule-dry-run # headless .venv/bin/python wigle_to_wdgwars.py --unschedule # remove later The interactive mode previews the exact unit/cron-line/schtasks command before installing, and asks one last "install now?" confirmation. Re-run `--schedule` and answer "no" to the dry-run prompt to flip from dry-run to live uploads. If you'd rather write the unit / cron entry / scheduled task yourself, the hand-written recipes below still work and they all stay supported. They give you finer control (file-watch mode, custom intervals, multiple drives) than the `--schedule` auto-installer. **The truly hands-off version:** use `--from-wigle` (see [No file at all](#no-file-at-all--pull-straight-from-wigle)). The timer pulls your latest WiGLE upload and pushes it to WDGoWars with no file involved at all. Swap the command in any recipe below for: ./run.sh --from-wigle --wigle-key WIGLE_TOKEN --key WDGWARS_KEY --chunk-size 10000 **The file-based version:** always export (or save) your WiGLE file to the *same path* — e.g. `wardrive.wiglecsv.gz` — and point a timer at that path. Each run re-pushes the file; WDGoWars dedupes server-side, so re-sending the same data is harmless and still picks up any new rows or merged location samples. Pick the recipe for your OS below. ### Windows — Task Scheduler Easiest if you wardrive with your phone and copy the export to your PC. Save a tiny batch file, then point Task Scheduler at it. `push-wardrive.bat` (edit the paths and paste your key after `--key`): @echo off python "C:\Tools\wigle-to-wdgwars\wigle_to_wdgwars.py" "C:\Wardrives\wardrive.wiglecsv.gz" --key YOUR_API_KEY_HERE --chunk-size 10000 >> "C:\Wardrives\push.log" 2>&1 Create the timer (run once in an **admin** PowerShell or Command Prompt — this fires it daily at 3am): schtasks /Create /F /TN "WDGoWars Push" /TR "C:\Wardrives\push-wardrive.bat" /SC DAILY /ST 03:00 (`/F` lets you re-run the same line later to change the time without an overwrite prompt.) To change the time, run the same `schtasks /Create` again with a new `/ST`, or edit it in the Task Scheduler GUI (search "Task Scheduler" in the Start menu → find "WDGoWars Push"). ### cron (Linux / Mac) — push every 6 hours # m h dom mon dow command 0 */6 * * * /usr/bin/env python3 /home/me/bin/wigle_to_wdgwars.py /home/me/wardrives/latest.csv --chunk-size 10000 >> /home/me/wardrives/push.log 2>&1 Point it at whatever file you keep fresh (a `.csv` or `.gz` both work). The tool persists cooldown state to `~/.config/wigle-to-wdgwars/cooldown.json`, so back-to-back jobs that catch a 429 won't hammer the server. ### systemd timer — daily at 03:00 `~/.config/systemd/user/wdgwars-push.service`: [Unit] Description=Push wardrive CSV to WDGoWars [Service] Type=oneshot ExecStart=/usr/bin/env python3 %h/bin/wigle_to_wdgwars.py %h/wardrives/latest.csv --chunk-size 10000 `~/.config/systemd/user/wdgwars-push.timer`: [Unit] Description=Daily WDGoWars push [Timer] OnCalendar=*-*-* 03:00:00 Persistent=true [Install] WantedBy=timers.target Enable: systemctl --user daemon-reload systemctl --user enable --now wdgwars-push.timer ### Pre-flight check Wrap the push in a `--whoami` check so a bad/expired key fails loudly before you try a long upload: #!/bin/sh set -e ./run.sh --whoami > /dev/null exec ./run.sh /home/me/wardrives/latest.csv --chunk-size 10000 ### Parser preview Before you wire a CSV path into a schedule (or push something big you just got out of Kismet / hcxdumptool), it's worth confirming the parser sees what you expect. `--preview` does that without any network calls: ./run.sh --preview /path/to/your.wiglecsv Prints the first 6 data rows as JSON to stdout, no upload, no key needed. Same shape as Heimdall's and Muninn's `--preview` so the mental model carries between feeders. ### Pointing at a staging host `--api-url` overrides the CSV upload endpoint. Useful when you're testing against a local mock or staging server without flipping `/etc/hosts`: ./run.sh --api-url http://localhost:9999/api/upload-csv \ --dry-run /path/to/your.wiglecsv ## WDGoWars API reference ### Endpoints | Method | Path | Purpose | Auth | Body | |---|---|---|---|---| | `GET` | `/api/me` | Validate key, read stats/badges/gang | `X-API-Key: ` | — | | `POST` | `/api/upload-csv` | Bulk Wi-Fi/BLE ingest | `X-API-Key: ` | `multipart/form-data`, field `file=` (WiGLE-1.6 CSV) | | `POST` | `/api/upload/` | Signed JSON ingest (aircraft, mesh, …) | `X-API-Key: ` | `application/json` envelope, see below | **Auth header is `X-API-Key`.** `Authorization: Bearer …` is rejected. ### `GET /api/me` response { "ok": true, "username": "your_handle", "gang": "Your Gang", "gang_id": 1, "country": "US", "joined": "2026-01-01", "wifi": 1234, "ble": 5678, "aircraft": 0, "mesh": 0, "cracked": 0, "total": 6912, "recent_today": 100, "recent_7d": 900, "badges": ["first_blood", "gang_member", "wifi_100", "wifi_1k", "ble_100", "ble_1k"], "credits": {"balance": 0, "lifetime_earned": 0} } ### `POST /api/upload-csv` response { "ok": true, "imported": 701, "captured": 1, "updated": 0, "duplicates": 56673, "no_gps": 0, "bad_rows": 3, "cooldown": 0, "merged_samples": 156, "total": 48421278 } - `imported` — new fingerprints accepted into the user's account. - `captured` — newly-flagged "first to capture" wins (rare). - `duplicates` — rows the server has already seen from this user. - `no_gps` — rows skipped for missing lat/lon. - `bad_rows` — malformed rows the parser rejected. - `merged_samples` — observations folded into an existing fingerprint as additional signal samples. - `total` — **server-wide** row count across all users (not the caller's). - `cooldown` — when nonzero, seconds the server is asking the client to wait before the next upload. ### Rate limiting The server enforces a **per-account upload queue**. While one upload is still being processed, a second request returns HTTP 429: {"error":"Another upload is already being processed for this account. Please wait for it to finish before starting a new one.","retry_after":20} This tool persists `retry_after` to `~/.config/wigle-to-wdgwars/cooldown.json` and sleeps until the deadline on the next run (capped at 15 min to avoid deadlocks if a stale deadline sticks). ### The Cloudflare 524 footgun The origin behind the portal processes each CSV **synchronously in one request**. Cloudflare in front has a **120-second response timeout**. Anything taking longer returns: HTTP 524 — origin_response_timeout to your client, but **the origin keeps ingesting** — you'll see the rows land in your `/api/me` count even though your client errored. **Mitigation:** chunk the CSV into ≤10 000-row chunks. Each chunk lands in 15–35 s comfortably under the cap. This tool does it automatically with `--chunk-size 10000`. Each chunk re-sends the WiGLE 2-line header so the server treats it as a valid file. ### Common error responses | HTTP | Body | Meaning | |---|---|---| | 400 | `{"error":"Invalid data format"}` | Most likely you POSTed a CSV to `/api/upload` (no `-csv` suffix). Wrong endpoint, not a malformed file. | | 401 | `{"error":"Invalid API key"}` | Bad/expired key, or you used `Authorization: Bearer …` instead of `X-API-Key:`. | | 429 | `{"error":"Another upload is already being processed …","retry_after":N}` | Per-account queue. Wait `retry_after` seconds. | | 524 | (HTML from Cloudflare) | Origin timed out. Chunk smaller. Rows are still ingesting on the origin. | ### WiGLE API (the `--from-wigle` pull side) | Method | Path | Purpose | |---|---|---| | `GET` | `/api/v2/file/transactions?pagestart=N&pageend=M` | List your uploads, newest first, paged 100 at a time. Each result has a `transid`. | | `GET` | `/api/v2/file/csv/{transid}` | Download that upload as a WiGLE CSV. | ## Aircraft JSON format (signed endpoint) The signed `/api/upload/` endpoint accepts a different payload shape for aircraft, mesh, and (likely future) other observation types. Use `--aircraft-json FILE` when you have ADS-B data to push. ### Envelope The wire format wraps a payload in an HMAC-SHA256 envelope: { "data": "", "nonce": "", "sig": "" } Sent as `Content-Type: application/json`, with the same `X-API-Key` header as the CSV path. ### Payload The inner payload (pre-base64) is: { "networks": [], "aircraft": [ {}, {}, ... ], "meshcore_nodes": [] } `networks` and `meshcore_nodes` are currently passed empty by this tool — Wi-Fi/BLE goes through the CSV path because of better dedup and merging behavior server-side. ### Aircraft record schema { "icao": "A12345", "callsign": "UAL123", "lat": 41.4712, "lon": -81.7887, "alt_ft": 35000, "speed_kt": 450, "heading": 270, "first_seen": "2026-05-23 12:00:00", "type": "ADSB" } `icao` and at least one of (`lat`, `lon`) are required. `first_seen` should be `YYYY-MM-DD HH:MM:SS` in UTC. Missing fields are tolerated; bad fields silently get zeroed. ### Input file Pass a JSON file containing a top-level **list** of these record dicts: ./run.sh --aircraft-json aircraft.json ### Response { "ok": true, "aircraft_imported": 47, "aircraft_already_seen": 1203, "new_badges": ["plane_hunter"] } ## Troubleshooting **`{"error":"Invalid data format"}`** — You hit `/api/upload` (signed) with a CSV. The CSV endpoint is `/api/upload-csv`. This tool uses the right endpoint by default; only hits when something rewrites the URL. **`HTTP 401`** — Bad key, or you set `Authorization: Bearer …` somewhere. Run `--whoami` to confirm. Make sure your key is the full string from the WDGoWars account page, no extra whitespace. **`HTTP 429` repeating forever** — Your previous upload is still queued server-side. Wait the `retry_after` seconds (the tool does this for you on the next run). If a stale `cooldown.json` is causing >15 min sleeps, delete it: `rm ~/.config/wigle-to-wdgwars/cooldown.json`. **`HTTP 524`** — Cloudflare gave up waiting on the origin. Add or lower `--chunk-size` (try 5000 if 10000 still trips it on a slow link). Your data is probably ingesting anyway — check `--whoami` counts after. **`imported: 0, duplicates: `** — Expected on the second push of the same CSV. WDGoWars dedupes per-fingerprint. Only new BSSIDs/SSIDs (or new locations for existing ones) count. **`bad_rows: `** — Some rows didn't parse. Most often missing or malformed `FirstSeen`, or a non-numeric `Lat`/`Lon`. Validate with: awk -F, 'NR>2 && (length($1)!=17 || $7+0==0) {print NR": "$0}' wardrive.csv **Script hangs on a chunk for minutes** — The origin is grinding through a large chunk. urlopen timeout is 600 s in this tool. If you want to bail out and let the origin finish in the background, Ctrl-C and check `--whoami` 30–60 s later. ## Related tools The wardriving + WDGoWars ecosystem of uploaders: | Tool | Platform | Path | Repo | |---|---|---|---| | **wigle-to-wdgwars** (this) | Linux/Mac/Win (Python) | Wi-Fi + BLE CSV, aircraft JSON | (this repo) | | **Muninn (adsb-to-wdgwars)** | Linux/Mac/Win (Python) + browser | ADS-B aircraft, 12 capture formats | https://github.com/HiroAlleyCat/adsb-to-wdgwars | | **Piglet** | Arduino / RP2040 | Wi-Fi from on-device captures | https://github.com/Hamspiced/piglet | | **Raspyjack `wdgwars_upload`** | Bash Bunny / Pi payload | CSV from Raspyjack payloads | https://github.com/7h30th3r0n3/Raspyjack | | **pineapple_pager_wdgwars** | Wi-Fi Pineapple | Pineapple captures | https://github.com/LOCOSP/pineapple_pager_wdgwars | | **M5MonsterC5 / CardputerADV** | M5Stack ESP32 | On-device captures | https://github.com/C5Lab/M5MonsterC5-CardputerADV | Cross-cutting links: - [WiGLE](https://wigle.net/) — the original wardriving network. - [WiGLE WiFi Wardriving (Android)](https://play.google.com/store/apps/details?id=net.wigle.wigleandroid) — easiest capture stack. - [Kismet](https://www.kismetwireless.net/) — the open-source wireless detector / sniffer / IDS. - [hcxdumptool](https://github.com/ZerBea/hcxdumptool) — fast 802.11 capture for handshake hunting; pairs with `hcxpcapngtool --csv`. ## License MIT. Use it, fork it, send a PR. ## Acknowledgments The reverse-engineered API documentation here was cross-checked against the open-source uploaders in the [Related tools](#related-tools) table — in particular `Hamspiced/piglet` and `7h30th3r0n3/Raspyjack`. The chunking-around-Cloudflare-524 workaround is well-documented across the community; this tool just bakes it in by default.