Skip to main content

A walker on a Montevideo street, carrying a laptop in a backpack, pressing a headset button and holding a notebook of street crossings.

Enodia

Know where you are in a city from its Wi-Fi alone. No GPS, no location service: a map you walked yourself.

tests PyPI version Python versions License: MIT DeepWiki

Enodia turns a Linux laptop in a backpack into a Wi-Fi scanner that talks. You walk, and it logs every network in reach and tells you through your headphones what changes. At each corner you press the headset button and write the corner down. Back home, Enodia lines the two up: every scan gets its place along your route, every router an estimated position, and the walk becomes a map. Walk those streets again and it tells you where you are, as you go.

It all runs on your machine. The one command that goes online looks your corners up on OpenStreetMap, and only when you ask it to.

A real walk in Dolores, Uruguay: 31 blocks and 39 corners, and where each of 652 access points probably stands, drawn over the neighbourhood from OpenStreetMap.

A real walk, published in samples/dolores/ with every network pseudonymised: the route, the corners marked along it, and where each access point probably stands. A dot is a router the walk pinned down, a ring one it only heard from around there.

What it does

  • Records a walk by ear. A scan every five seconds, every network logged with its signal, and the news in your headphones: "New network found", "Mark 3". Nothing to look at while you walk.
  • Places everything along your route. It joins the log with the corners you noted, places each scan between two of them, and estimates where each router stands. Out come a plan in SVG, a table in CSV and the walk in GeoJSON.
  • Builds a map from your walks and tells you where a scan was taken: between "Rivera y Brito del Pino" and "Rivera y Simón Bolívar", 53% of the way, or at the corner of "Rivera y Brito del Pino".
  • Follows you live. --locate --watch scans as you walk, says where you are when it changes, and draws you on a page that reloads itself, over the streets, the water and the parks, with no connection.
  • Checks itself. Each estimate has a command that measures it against the walk: a corner held out and placed again, a block walked twice set against itself, a pass held out and found again from the rest of the map.

Install

You need Linux, Python 3.10 or newer, and a Wi-Fi interface run by iwd, NetworkManager or wpa_supplicant that you may scan with over D-Bus. For the voice, espeak-ng or SVOX Pico. Without either, Enodia prints what it would have said.

uv tool install enodia

uv tool upgrade enodia follows new releases. The proxy support of --geocode needs the socks extra: uv tool install "enodia[socks]". Before the first walk, set up the machine so that it keeps scanning with the lid closed and hears the headset button.

To work on Enodia itself, run uv sync in a clone and uv run enodia instead.

Try it now

The samples live in the repository rather than in the package, so clone it for them. No Wi-Fi card is involved:

git clone https://github.com/carlosplanchon/enodia.git
cd enodia
enodia --locate samples/synthetic_montevideo/rivera-query.jsonl \
  --map samples/synthetic_montevideo/rivera-map.jsonl --voice none
You are between "Rivera y Brito del Pino" and "Rivera y Simón Bolívar", 53% of the way
  around [-34.90311, -56.15836]
  2 walks agree, best similarity 89%, spread 0% of the stretch (1 m)
  from evidence last gathered 2026-09-17 17:02

That one is synthetic: real streets of Montevideo, invented radios, made to show the workflow (how it was made). The walk in Dolores is real. To see how well its map finds its own passes, which takes about two minutes:

enodia --check-map --map samples/dolores/dolores-map.jsonl

To see the real walk over a map, open samples/dolores/dolores-walk.geojson in a map viewer that reads GeoJSON, such as GPXSee (on Arch Linux, sudo pacman -S gpxsee), or look at samples/dolores/dolores-plan.svg, the same walk drawn with no map service at all.

In the field

Enodia locating a real street corner from Wi-Fi alone.

The street signs at the corner of José Pedro Varela and Carlos María Solari in Dolores, photographed at night. The live map on a laptop at the same corner, at night: the dot on the crossing of José Pedro Varela and Carlos María Solari, the streets around it named, and at the top, 22:45:09 at the corner of Carlos María Solari y José Pedro Varela.

The corner of "Carlos María Solari y José Pedro Varela" in Dolores, marked on the walk in samples/dolores/ and found again two days later: the street signs on the left, the live map on the right.

Your first walk

1. Check the machine, then walk.

enodia --preflight --log walk.jsonl
enodia --log walk.jsonl

Press the headset button at every corner, the first and the last included. Enodia answers "Mark 1", "Mark 2" and so on. Two or three blocks are enough for a first try. Ctrl+C ends the walk.

2. Write the corners down in notebook.txt, one line per mark:

#1 Rivera y Avenida Doctor Francisco Soca
#2 Rivera y Brito del Pino

Without a button, write the time instead of the mark: 17:45:00 Rivera y Brito del Pino.

3. Put the walk on the map.

enodia --geocode notebook.txt --area Montevideo --streets streets.jsonl --surroundings
enodia --reconcile walk.jsonl notebook.geo.txt --streets streets.jsonl --svg plan.svg
enodia --map-add walk.jsonl notebook.geo.txt --streets streets.jsonl

--geocode looks the corners up on OpenStreetMap and writes notebook.geo.txt beside the notebook, with the streets and the neighbourhood into streets.jsonl. It is optional: without coordinates, every place is a fraction of the way between two corners.

The lookup is the one command that goes online. To keep it apart from your own address, send it through Tor: install the socks extra (uv tool install "enodia[socks]"), start the Tor daemon (on Arch Linux, sudo pacman -S tor and sudo systemctl start tor), and add --proxy socks5://127.0.0.1:9050. Through Tor says what that hides and what it does not.

4. Come back, and find yourself.

enodia --locate --watch --streets streets.jsonl --live-map live.html --log watch.jsonl

Open live.html in a browser once and leave it open. --log keeps the scans, so the run can be located again later with --locate watch.jsonl, against another map or with other flags. enodia --assistant is a menu for the same steps, the lookup aside: the walk, the reconciliation, the map and finding yourself.

The live map in a browser: where the scan puts you on Río Negro, with the streets it may be on shaded around it, the last answers fading behind it, the map's own fingerprints as violet dots, the streets named, and in the corner a panel of what the run is doing.

The live map, zoomed in with Follow me.

How it works

Every spot sounds different. From any point on a street you hear a particular set of Wi-Fi networks, some loud and some faint. That set is the spot's fingerprint (Wi-Fi fingerprinting), and a walk records one every five seconds.

Placing the walk. You press the button at each corner, and Enodia spreads the scans between two corners by how fast the networks around you changed (the Jaccard distance between one scan and the next, added up along the block). If they stayed the same for a while, you had stopped. If they turned over quickly, you were moving. Where they say nothing either way, it assumes a steady pace (linear interpolation on the clock).

Placing the routers. Each router goes in the middle of where it was heard, leaning towards where it was loudest (a weighted centroid, with weights from the log-distance path loss model). One heard all along the walk gets a ring instead of a dot, because the walk never pinned it down.

Finding yourself. Later, Enodia listens and asks which recorded spot sounds most like now. It counts the networks the two have in common, out of all the ones either of them heard, and a rare network counts for more, the way a distinctive voice in a crowd tells you more than a common one (weighted Jaccard similarity, with rarity weights like IDF). Each earlier walk gets one vote, the block with the best match wins, and you are placed along it between its best matches (k nearest neighbours, one per walk, grouped by block).

Saying what it does not know. If nothing sounds alike enough, the answer is "not on the map" (a similarity floor). If two blocks sound about as alike, it says so, and the scans just before it decide, since nobody jumps a block in five seconds (a softmax share of the evidence, and a vote of the last three scans). While you walk, the dot moves at a walking pace (a one-dimensional Kalman filter), and the streets you may be on are shaded around it: the worse the match, the wider the shade (an error band fitted on held-out walks).

Checking itself. The numbers below come from hiding part of a walk and finding it again from the rest, like covering the answers of a quiz (hold-out validation, one walk or one corner left out at a time).

How good is it?

One real walk so far. It is in the repository, so the first three numbers can be run again. The fourth comes from walking the same streets with Enodia the next day.

  • The corners. Held out one at a time and placed again from the walk alone, they land 9 m from where they were on average, and 4 m with --pace clock (--check-pace).
  • The routers. Seven blocks walked twice place the same networks within 16% of a block of each other on average (--check-passes).
  • Finding yourself. A pass held out and located from the rest of the map lands 38 m from where it was on average (--check-map). Most of those answers are on the block next door, because most blocks were walked once and holding that pass out left nothing on them. The experimental --along levels brings it to 33 m.
  • In the street. Walking with --locate --watch, matching on which networks are in view lost no scan on streets the map knows. Matching on their signal strength too lost a third of them, and it is not the default.

A second walk over the same streets, on another day, is the measurement that settles these. The methodology says what each check can and cannot tell you.

Limits

  • The map knows the streets you walked, and it ages as routers move or disappear.
  • Positions are estimates, and the report says how far to trust each one. A router the walk only heard from around there is drawn as a ring, never as a dot.
  • Enodia has no GPS on purpose: the question is whether a map of radios can place you by itself, and a receiver in the loop would answer it for the radio. GPS belongs as ground truth, written beside a corner in the notebook (@ -34.9066, -56.2001).
  • Fresh scans send probe requests. --preflight checks that the scanning address is randomised, and changes nothing.
  • Logs and maps hold your neighbours' network names and addresses, and where they are. --export-public makes a copy you can publish, and says what it cannot hide.

Documentation

  • Machine setup: the lid, the Wi-Fi daemon, the voice, the headset button.
  • CLI reference: every flag, by the command you reach for it with.
  • File formats: the notebook you write, and the log and streets files Enodia writes.
  • Methodology: how scans, routers and fingerprints are placed, and what each check measures.
  • Design notes: each decision, and the failure behind it.
  • Publishing a walk: what --export-public promises, and what it cannot.
  • From Python: the same operations as function calls.

License

MIT.

Release files for enodia 0.4.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for enodia 0.4.1
File Size Uploaded
enodia-0.4.1.tar.gz 658.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for enodia 0.4.1
File Interpreter ABI Platform
enodia-0.4.1-py3-none-any.whl Python 3 none any Details

Total release size: 838.2 kB

Release files / enodia-0.4.1.tar.gz

Download URL enodia-0.4.1.tar.gz
Size 658.9 kB
Tags Source
SHA-256 checksum
How to use checksums
f6fa452e1e0691264ac8b149e00dcd6c76c844d723a308e90a7d3fa3a10c6cbb
BLAKE2b-256 checksum
How to use checksums
8baa0b6a318396403725b596780828190b64acd5c4dfc5a41456ddff3d3b6d29
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / enodia-0.4.1-py3-none-any.whl

Download URL enodia-0.4.1-py3-none-any.whl
Size 179.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3bf8c009bb24f3fafcbe96a427c673459d40d4ce3660ea7db9691a0fbafa08bf
BLAKE2b-256 checksum
How to use checksums
8569a9dcd2c00f9df9d491675e78b4e6c64b1ced4f818ac524ed0f2d6dc267a4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.1 This release

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page