← LORA Tunnel beta installer · User guide

Release 0.2.2-beta.1 · Public beta · 794956a35501

Offline exports and upload contract research

Checked on 2026-11-01. These tools produce local files and make no network requests. Host tests establish formatting and rejection behavior; they do not establish WiGLE or WDGWars ingestion acceptance, a working device capture, or firmware upload support.

Run

Python 3.10 or newer is sufficient; no packages are required. In Windows use py -3 if python resolves to the Windows Store placeholder.

py -3 tools/export_logs.py wigle path/to/session.jsonl path/to/session-wigle.csv
py -3 tools/export_logs.py wdgwars-mesh-candidate path/to/session.jsonl path/to/session-mesh-candidate.json
py -3 -m unittest discover -s tests -p test_exports.py -v

Output paths must be new files and must differ from the original log. Existing files are never overwritten. The terminal receives a JSON count report with each skip reason and uploaded:false. Keep that report with the export. A failed export can leave an incomplete new output; the command returns a nonzero code. The original JSONL file is retained.

Input is read one line at a time with a 64 KiB limit. Malformed JSON, invalid schemas, oversized lines and an interrupted final JSON record are counted and skipped. Complete earlier records still export. Unsupported event types such as session metadata and LoRa packets are skipped by the WiGLE exporter.

WiGLE CSV

The format follows the actual official Android exporter at c7b87114f9c4d8a269673fedacc675c5b3cf94be, specifically ObservationUploader.java. It uses the WigleWifi-1.6 metadata line and these 14 columns:

MAC,SSID,AuthMode,FirstSeen,Channel,Frequency,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,RCOIs,MfgrId,Type

FirstSeen is populated from each timestamped observation, rendered in UTC as YYYY-MM-DD HH:MM:SS; this matches the upstream writer's per-location timestamp. Frequency is MHz for Wi-Fi. The exporter validates 2.4 GHz channel/frequency consistency, keeps RSSI in dBm, and uses the receiver's contemporaneous GNSS location. Repeated sightings remain repeated rows. This preserves changing signal and receiver position instead of reducing a drive to one row per device.

The file contains WIFI and supported BLE rows. A BLE advertised name goes in SSID, known manufacturer ID in MfgrId, and unavailable channel/frequency stays empty: an ESP32 BLE scan callback does not identify the advertisement's RF channel. AuthMode is [BLE], not a Wi-Fi encryption claim. Full advertising bytes and address type stay in the native log because CSV has no corresponding fields. The official client's NetworkType explicitly includes BLE, but offers no native LoRa/Meshtastic type. LoRa export to WiGLE remains unsupported by this project.

Default eligibility requires a valid receiver fix no older than 10 seconds, finite in-range coordinates, a timestamp with an explicit timezone, a valid address and RSSI, and supported radio metadata. Missing/stale fixes never become zero coordinates. A legitimate valid fix at latitude/longitude zero remains valid for WiGLE export. Unknown altitude or accuracy is blank. HDOP is dimensionless and is never written as AccuracyMeters. Actual GNSS horizontal accuracy can be provided in optional gps.accuracy_m. Kismet's exporter documentation also requires an available GPS location for its map-oriented CSV.

BLE public and public-identity addresses export by default. Random and unknown address types stay local with skip counts. --include-random-ble permits known random address types explicitly and reports how many were included; the CSV cannot communicate that these are temporary identities. Unknown address type still fails eligibility. CSV uses standard quoting for commas/quotes, escapes embedded CR/LF, and preserves the original content in JSONL.

If wifi.ssid_sanitized is true, the exporter uses ssid_hex only when it decodes as exact valid UTF-8 within the 32-byte SSID limit. Otherwise SSID is blank, with a count, instead of exporting display replacement ? characters as an observed name. A BLE name_sanitized flag likewise produces a blank CSV name; its original AD bytes stay in the native payload for later decoding. Existing logs without the optional flags retain their previous text-field behavior.

WDGWars mesh candidate payload

The public WDGWars developer page confirms CSV upload at https://wdgwars.pl/api/upload-csv, with an X-API-Key header and multipart file field. That is the Wi-Fi/BLE route. Its JSON upload route is https://wdgwars.pl/api/upload; it uses a Base64 JSON payload, random nonce, and HMAC-SHA256. Do not put mesh packets into ordinary WiGLE CSV rows.

The separately reviewed Heimdall implementation, v0.13.0, uses https://wdgwars.pl/endpoint/upload/ as the corresponding alias. The payload has networks, aircraft, and meshcore_nodes arrays. Mesh records use node_id, node_type, name, lat, lon, rssi, first_seen, the envelope marker type:MESHCORE, and explicit network:meshcore or network:meshtastic. The marker's historical name does not mean every record is MeshCore.

Its node identity notes describe the two namespaces: MeshCore uses the first 16 hex characters of the node's public key, while Meshtastic uses its 32-bit device number. Never pad a short MeshCore path hash into an invented ID or derive a Meshtastic ID from a MeshCore public key. MeshCore records include the full key when available.

The local exporter requires a decoded observation with this additional shape:

{
  "mesh": {
    "protocol": "meshtastic",
    "decode_status": "decoded",
    "source_id": "abcdef12",
    "node_type": "CLIENT",
    "name": "optional advertised name",
    "hop_count": 2,
    "node_position": {
      "valid": true,
      "source": "node_advertised",
      "lat": 3.5,
      "lon": 4.5
    }
  }
}

This snippet is synthetic and must never be uploaded. These fields must come from real decoded data when used on a capture. The exporter does not manufacture them from raw packet length or a receiver fix. A sender-advertised position is a claim by that sender; this exporter does not authenticate that claim or verify MeshCore Ed25519 signatures.

The candidate exporter requires valid node-advertised coordinates, nonzero node position, explicit UTC, supported captured role, RSSI, and known hop count. Receiver GPS is never substituted for node position, including for relayed traffic. The reviewed server contract treats absent hop information as direct, so unknown hops are skipped rather than accidentally asserting direct reception. Future integration should preserve actual hop fields and verify how the service uses them. Conservative role allowlists can skip future roles visibly.

Meshtastic needs exactly eight hex digits for the source number, excluding broadcast and zero. MeshCore requires the full 64-hex key and an exactly matching 16-hex source prefix. Missing names fall back to the identifier in the interchange file; this does not claim an advertised human-readable name existed.

The output is an unsigned candidate payload, not a ready HTTP request. It is kept separate because the public official page does not publish the full mesh validation contract; the detailed contract comes from this independently maintained integration. Live acceptance still needs a real eligible capture and an explicitly configured upload integration. Do not infer success from HTTP 200 alone: examine imported, rejected, and per-reason counts. No endpoint was called with credentials during development.

Upload design and Marauder reference

WiGLE's current client defines https://api.wigle.net/api/v2/file/upload in UrlConfig.java and AbstractApiRequest.java uses HTTP Basic authentication over API-name:API-token. A future uploader should stream closed CSV files over verified TLS, keep a per-service result journal, preserve source logs, and honor user-controlled donation/sharing options.

WDGWars JSON signing uses the API key's UTF-8 text as the HMAC key, not its hex-decoded bytes. Compact JSON bytes become Base64; an eight-byte random nonce becomes 16 hex characters; the signed message is nonce text concatenated with Base64 text. The request JSON contains data, nonce, sig; authentication also uses the X-API-Key header. This is documented for later implementation only.

The user's recollection is substantially confirmed: ESP32Marauder's develop branch at 79cc6bcba1db6679baee1dab2b0c1ad5010e6363 contains WiGLE and WDGWars upload routines and imports /wigle_api_name.txt, /wigle_api_token.txt, and /wdg_key.txt from SD in WiFiScan.cpp. It also contains touch keyboard code. This inspection did not verify the entire manual credential-entry flow. LORA Tunnel's own keyboard/configuration flow must be implemented and tested. That upstream import path prints credentials to the serial log; LORA Tunnel must not copy that behavior.

Keep configurable auto-upload off until enabled by the user. Upload on known Wi-Fi after capture stops, pause scans deliberately, use bounded batches/retries, back off on 429, and leave partial failures pending. Credentials belong in the SD configuration requested by the user, masked in UI and excluded from all logs, example files, exports, crash output, and source control. No credentials are needed to run these offline tools.

Sources, licenses, and fixtures

This exporter is newly written interoperability code. No upstream source code was copied or vendored. WiGLE Android is BSD-3-Clause; Heimdall is MIT, copyright 2026 HiroAlleyCat; ESP32Marauder is MIT, copyright 2020 Just Call Me Koko. If later implementation copies/adapts code, preserve the applicable notices and record the component and revision. Protocol research does not transfer ownership or establish that the whole project derives from those programs.

Every file beginning with synthetic- in examples/ contains invented test data. None represents a scan, radio decode, GPS measurement, or accepted upload. Do not upload the example files. They demonstrate a valid Wi-Fi row, public BLE, suppressed random BLE, missing receiver GPS, and a separate mesh position. Run the host tests after changing filters, schemas, or field mappings; then validate new exports against services with real observations before claiming compatibility beyond the implemented file format.