# Wi-Fi Heatmapper: the full guide (version 0.8.2) Generated on 2026-10-28 from https://github.com/hnykda/wifi-heatmapper/tree/main/docs/guide. Index: https://wifi-heatmapper.danielalder.cz/llms.txt --- Source: https://wifi-heatmapper.danielalder.cz/docs/getting-started # Getting started Wi-Fi Heatmapper measures Wi-Fi signal strength, and optionally speed, at the spot you click on a floor plan, then draws heat maps of the results. You walk around with a laptop; the heat maps show which rooms have a weak signal, where a mesh node or extender would help, and whether a slow connection is really the Wi-Fi's fault. It runs on macOS, Windows and Linux. Your surveys stay on your computer: they are plain JSON files, there is no account and no cloud. The app sends only [anonymous usage counts](https://wifi-heatmapper.danielalder.cz/docs/privacy), which you can turn off. ## Install Download the app for your system from the [latest release](https://github.com/hnykda/wifi-heatmapper/releases/latest): a `.dmg` for macOS (13.5 Ventura or later), an installer for Windows, a `.deb` or AppImage for Linux. On a Mac you can use [Homebrew](https://brew.sh) instead, which also installs iperf3: ```bash brew install --cask hnykda/tap/wifi-heatmapper ``` The apps are not signed, so macOS and Windows warn you the first time you open them. [Installing the app](https://wifi-heatmapper.danielalder.cz/docs/install) walks through that for each system. Prefer a terminal? See [running from source](https://wifi-heatmapper.danielalder.cz/docs/run-from-source). On a Mac, click **Allow Location access** in Settings once: macOS then lets the app see your network's name. Your location itself is not used. ## Signal only or with speed? Every survey measures the signal. It can also measure speed: how fast data actually moves between this laptop and a second computer on your network. You choose under **What to measure** in Settings, separately for each survey. New surveys start with **Signal only**. | | Signal only | Signal and speed | |---|---|---| | Setup | Nothing | iperf3 on two computers; one stays put, wired if possible | | Time per spot | About 2 s | About 6–10 s | | Tells you | Where coverage is weak | What speed you actually get; catches interference and slow links that look strong | | Limits | A strong signal is not always fast | A server on Wi-Fi roughly halves the result; the optional fixed-rate (UDP) test stops at 100 Mbps by default | Start with signal only to find the weak rooms. Add speed when you want to know whether the Wi-Fi is really what makes a connection slow, or to compare before and after a change. [Measuring speed](https://wifi-heatmapper.danielalder.cz/docs/throughput) explains the setup. ## Your first survey 1. **Settings.** Pick the bundled floor plan or upload your own image. A photo of a pencil sketch is fine; it does not need to be to scale (see the [FAQ](https://wifi-heatmapper.danielalder.cz/docs/faq#how-do-i-make-a-floor-plan)). Leave **What to measure** at **Signal only**, or [set up speed tests](https://wifi-heatmapper.danielalder.cz/docs/throughput) first. 2. **Floor plan.** Stand somewhere, click that spot. The app measures the signal (and speed, if the survey measures it) and drops a coloured dot there. Do this in every room, twice in big ones, and near the outer walls. 3. **Heat maps.** Drag the radius until neighbouring spots merge into one surface. Download the maps you need. 4. **Survey points.** Check, switch off or delete individual measurements, or export everything as CSV. **A second floor (or house)?** Upload its floor plan in Settings. The new survey starts with the speed settings of the one you used last, so the speed test server is already filled in. To bring over access point names or the heat map look too, use **Copy settings from…** under **Floor plan**. ![Signal strength heat map](https://wifi-heatmapper.danielalder.cz/docs/images/heatmap.jpg) Green is good. The scale runs green, turquoise, blue for acceptable signal, then yellow and red where it gets poor. [Using the app](https://wifi-heatmapper.danielalder.cz/docs/using-the-app) explains every tab and setting. **Rather be guided?** An AI agent such as Claude can plan the survey, tell you where to stand, take each measurement once you say you're there, and explain the result: see [Using it with an AI agent](https://wifi-heatmapper.danielalder.cz/docs/mcp). **Something odd?** Click the **?** in the bottom-right corner and choose **Report a problem**. Wi-Fi behaves differently on every system, so reports really help (see the [FAQ](https://wifi-heatmapper.danielalder.cz/docs/faq#something-went-wrong-how-do-i-report-it)). ## Try it without any hardware Running from source, `npm run dev:mock` fakes the measurements, so you can explore the app on any machine without a Wi-Fi card, sudo or iperf3. The header shows a "Mock data" badge while it is on. --- Source: https://wifi-heatmapper.danielalder.cz/docs/install # Installing the app WiFi Heatmapper comes as a desktop app for macOS, Windows and Linux. It has everything it needs built in: no Node.js, no terminal, no `npm`. > **The desktop apps are new and have barely been tested.** They are built > automatically for every release, but so far only the macOS build has been > used on a real computer, and only on one Mac. See > [what has been tested](https://wifi-heatmapper.danielalder.cz/docs/install#what-has-been-tested) and please > [tell us how it went](https://wifi-heatmapper.danielalder.cz/docs/install#tell-us-how-it-went), good or bad. > If the app gives you trouble, [running from source](https://wifi-heatmapper.danielalder.cz/docs/run-from-source) > still works exactly as before. ## macOS Needs macOS 13.5 (Ventura) or later; on an older Mac, [run it from source](https://wifi-heatmapper.danielalder.cz/docs/run-from-source) with Node.js 22. Download from the [latest release](https://github.com/hnykda/wifi-heatmapper/releases/latest): | Your Mac | File | |---|---| | Apple silicon (M1 and later) | `WiFi.Heatmapper__aarch64.dmg` | | Intel processor | `WiFi.Heatmapper__x64.dmg` | Not sure which? Apple menu → About This Mac → "Chip" says Apple M-something or Intel. Open the `.dmg` and drag WiFi Heatmapper to Applications. Or use [Homebrew](https://brew.sh), which also installs iperf3: ```bash brew install --cask hnykda/tap/wifi-heatmapper ``` ### The first time you open it The app is not signed by Apple (that needs a paid developer account), so macOS stops it the first time: 1. Open WiFi Heatmapper. macOS says it could not verify that the app is free of malware. Click **Done**. 2. Open System Settings → Privacy & Security, scroll down and click **Open Anyway** next to WiFi Heatmapper. Confirm with your password. Or, in Terminal: `xattr -dr com.apple.quarantine "/Applications/WiFi Heatmapper.app"` You will have to do this again after each update, for the same reason. ### Location access macOS only tells apps the name of your Wi-Fi network if they have Location access. In the app's Settings tab, click **Allow Location access**; macOS asks whether **WiFi Heatmapper Helper** may use your location. Click **Allow**. The helper is the small part of the app that reads the Wi-Fi signal. It never looks up where you are. If you say no, everything still works, but surveys record the network name as unknown. Updates keep the permission, so you are asked only once. You can change your mind in System Settings → Privacy & Security → Location Services. You do not need a sudo password in the app. ### Where your data is Surveys and floor plans: `~/Library/Application Support/com.github.hnykda.wifi-heatmapper/data` Log: `~/Library/Logs/com.github.hnykda.wifi-heatmapper/server.log` Deleting the app leaves these in place. `brew uninstall --zap --cask wifi-heatmapper` removes them. ## Windows Windows 10 or 11, x64. Download from the [latest release](https://github.com/hnykda/wifi-heatmapper/releases/latest): | Installer | Installs for | Admin rights | |---|---|---| | `WiFi.Heatmapper__x64-setup.exe` (recommended) | you only | not needed | | `WiFi.Heatmapper__x64_en-US.msi` | everyone on the computer | needed | Pick one and stick with it: installing one after the other can put the app in an unexpected folder. The installers are not signed, so Windows SmartScreen says it protected your PC: click **More info**, then **Run anyway**. ### Location access On Windows 11 24H2 and later, Windows only shares Wi-Fi details with apps that may use your location. Open Settings → Privacy & security → Location and turn on **Location services** and **Let desktop apps access your location**. Windows does not ask by itself here; if this is off, the app tells you when you measure. ### iperf3 Install it so it is on your `PATH`, for example `scoop install iperf3` or `choco install iperf3`. The app finds iperf3 installed by winget, Scoop or Chocolatey. ### Where your data is Surveys and floor plans: `%LOCALAPPDATA%\com.github.hnykda.wifi-heatmapper\data` Log: `%LOCALAPPDATA%\com.github.hnykda.wifi-heatmapper\logs\server.log` Uninstalling leaves these in place. ## Linux Builds are for x64 and 64-bit ARM (Raspberry Pi 4 and 5 with a 64-bit OS). Download from the [latest release](https://github.com/hnykda/wifi-heatmapper/releases/latest): | Your system | File | |---|---| | Ubuntu, Debian, Linux Mint (x64) | `WiFi.Heatmapper__amd64.deb` | | Raspberry Pi OS 64-bit, Ubuntu, Debian (ARM64) | `WiFi.Heatmapper__arm64.deb` | | Any other distribution (x64) | `WiFi.Heatmapper__amd64.AppImage` | | Any other distribution (ARM64) | `WiFi.Heatmapper__aarch64.AppImage` | Install the `.deb`, then start WiFi Heatmapper from the app menu: ```bash sudo apt install ./WiFi.Heatmapper_*.deb ``` Or run the AppImage: ```bash chmod +x WiFi.Heatmapper_*.AppImage ./WiFi.Heatmapper_*.AppImage ``` AppImages need FUSE 2. On Ubuntu 24.04 and later install it with `sudo apt install libfuse2t64` (older Ubuntu: `libfuse2`). The app reads the signal with `iw`; `nmcli` is optional and only lists nearby networks (the `.deb` suggests both). On most systems that works without a password; if it doesn't, Settings asks for your sudo password, which is kept in memory only. Surveys and floor plans: `~/.local/share/com.github.hnykda.wifi-heatmapper/data` Log: `~/.local/share/com.github.hnykda.wifi-heatmapper/logs/server.log` ## iperf3 for speed tests Signal strength works out of the box. To also measure speed you need [iperf3](https://iperf.fr/iperf-download.php) on this computer and on a second computer on your network, ideally wired, running `iperf3 -s`. Homebrew and the `.deb` install it for you on this computer. See [measuring speed](https://wifi-heatmapper.danielalder.cz/docs/throughput). ## What has been tested Honest status for the first desktop release. "CI" means GitHub's build machines: the app is built there, and on Windows and Linux it is also installed, started with fake measurements and checked automatically. Nobody has used it there, and those machines have no Wi-Fi. | Build | Built in CI | Starts in CI | Used on a real computer | |---|---|---|---| | macOS, Apple silicon | yes | – | one Mac (macOS 27): installed, opened past the warning, Location allowed, real Wi-Fi readings without sudo | | macOS, Intel | yes | – | not yet | | Windows x64 `.exe` / `.msi` | yes | yes, both installers | not yet | | Linux x64 `.deb` / AppImage | yes | yes, also in Ubuntu 22.04 and 24.04 containers | not yet | | Linux ARM64 `.deb` / AppImage | yes | yes | not yet, no Raspberry Pi tried | | Homebrew cask | – | – | installed and removed from a local build | Not tested anywhere yet: a full survey with a real iperf3 server in the desktop app, the Windows app on a real PC (its window, SmartScreen, Wi-Fi), Windows 10 and Windows on ARM, Linux on real hardware (GNOME, KDE, Wayland), Intel Macs, and updating from one release to the next. ## Tell us how it went Even "it worked" helps. Open an issue with the [desktop app report](https://github.com/hnykda/wifi-heatmapper/issues/new?template=desktop-app-report.md) template: your OS and version, which file you installed, and what worked or didn't. Attach the log from the paths above if something went wrong. --- Source: https://wifi-heatmapper.danielalder.cz/docs/using-the-app # Using the app wifi-heatmapper has four sections, shown as tabs in the header. The strip under the header always shows which floor plan you are working on, how many points it has, and what it measures: **Signal only**, or **Signal and speed** with the server's address. In a speed survey it also warns when no server is set or the newest point was saved without speed. The header also has a light/dark switch and an "i" button that opens the About dialog with the version, OS and iperf3 details. The **?** in the bottom-right corner opens the guide for the tab you are on, what's new, and **Report a problem**, which fills in a bug report for you (see the [FAQ](https://wifi-heatmapper.danielalder.cz/docs/faq#something-went-wrong-how-do-i-report-it)). The first time you open the app a short welcome message explains the workflow. "Got it" hides it for good; the About dialog can show it again. ## Settings ![Settings](https://wifi-heatmapper.danielalder.cz/docs/images/settings.png) * **Floor plan** – the image you click on. Choose one of the bundled plans or upload a PNG, JPEG or WebP. Each floor plan keeps its own survey in `data/surveys/.json`. The trash button deletes the image and its survey. * **Settings from another survey** – **Copy settings from…** copies settings of another floor plan's survey into this one. Pick the survey, then tick what to copy: * **Speed settings** (ticked to start with) – what to measure, the speed test server, the test duration and extra iperf3 options; * **Access point names**; * **Heat map look** – spot size, opacity, colours and the fixed colour scales set on the Heat maps tab. The spot size is in floor-plan pixels, so it fits best between images of a similar size. The points, the floor plan image and its size are never copied, and neither is the sudo password. * **What to measure** – **Signal only** (nothing to set up, about 2 seconds per spot) or **Signal and speed** (needs iperf3 on this computer and on a second one, about 6 to 10 seconds per spot). Each survey keeps its own choice. A new survey starts with the speed settings (what to measure, server, duration and extra iperf3 options) of the survey you saved last, and says so under **Measurement**: "Speed settings copied from …". The very first survey starts with signal only. See [signal only or with speed?](https://wifi-heatmapper.danielalder.cz/docs/getting-started#signal-only-or-with-speed) The settings below appear only for speed: * **Speed test server (uses iperf3)** – the address of the second computer, which runs `iperf3 -s`: `192.168.1.10`, `nas.local`, `host:port` or `[IPv6]:port`. * **Test connection** and the checklist under it – whether iperf3 is installed on this computer and whether the server answers, plus a reminder to wire the server to the router. * **Test duration** – seconds per test, 5 by default. Below 3 the numbers read low and jump around. * **Jitter and lost packets** – also runs fixed-rate (UDP) tests in both directions, for jitter and lost packets. Off for new surveys; it doubles the time per spot. * **Advanced: iperf3 options** – extra iperf3 options for each test, separated by spaces, such as `-P 4` (four parallel streams). The app sets the server, port, duration, download/upload and TCP/UDP itself; the field shows the whole command. Surveys from before 0.8 stored whole commands; their extra options are kept, and a command with shell syntax (pipes, `;`, redirects) is replaced by the defaults. * **sudo password** – needed only where reading the signal needs it: on macOS without the Wi-Fi helper (running from source) and on Linux when `iw` is refused. The app's server holds it in memory for as long as the app runs, so measurements an [AI agent](https://wifi-heatmapper.danielalder.cz/docs/mcp) starts can use it too. It is never written to disk, never sent back to the page, and gone when the app restarts; emptying the field forgets it. Not shown on Windows, in Docker, in mock mode, or when the Wi-Fi helper makes it unnecessary. * **Access point names** – optional names for access points, by MAC address (BSSID), shown instead of the address in point details, the table and the CSV export. End a MAC address with `*` (for example `aa:bb:cc:dd:ee:*`) to name every BSSID that starts with it, such as the per-band BSSIDs of one access point. An exact MAC address wins over a prefix, and a longer prefix wins over a shorter one. * **Heat map colours** – the colour stops for signal strength (0% = -100 dBm, 100% = -40 dBm) and the opacity range of the overlay. * **Usage counts** – turns the anonymous usage counts (installs, app starts, number of measurements) on or off for this computer, shows the exact event that is sent and when the last one went out, and says why they are off if an environment variable or mock mode turns them off. See [Privacy and usage counts](https://wifi-heatmapper.danielalder.cz/docs/privacy). ## Floor plan ![Floor plan](https://wifi-heatmapper.danielalder.cz/docs/images/floorplan.png) Stand where you want to measure and click that spot. A pulsing dot marks the spot while the measurement runs; the panel in the corner shows progress and has a Cancel button. In a signal-only survey it shows the signal and "Speed: not measured (signal only)"; with speed it shows download / upload (and the fixed-rate UDP results, if they are on) as they come in. When it finishes, the dot is coloured by signal strength and labelled with the percentage. The app's server takes the measurement and saves the point, so closing or reloading the page while it runs doesn't lose it: open the app again and the point is there (a page opened meanwhile shows the progress too). Cancel stops at once, even in the middle of a speed test, and nothing is saved. One measurement runs at a time; other open pages of the same survey show its progress, and so does a measurement an [AI agent](https://wifi-heatmapper.danielalder.cz/docs/mcp) starts. If the survey measures speed but the speed test could not run (iperf3 not installed, no server, server not answering), the point is still saved: the panel says **Saved without speed**, gives the reason and stays open until you dismiss it. See [when the speed test can't run](https://wifi-heatmapper.danielalder.cz/docs/throughput#when-the-speed-test-cant-run). Click a dot to see its details (signal, noise and signal-to-noise ratio where measured, network, access point and speed), switch it off (it is then ignored by the heat maps) or delete it. **Name a point** to find it again: click its name at the top of the details (`Point_1` until you name it), type a name such as "Kitchen" or "Desk upstairs" and press Enter, or click elsewhere. Escape cancels; an empty name goes back to the id. Names are up to 60 characters. The name is shown instead of the id in the details, the points table and the CSV; the id stays as it is. The measured values can't be edited: a point shows what was measured. On Linux, measurements use the first connected managed Wi-Fi interface, ignoring monitor, AP and P2P devices. If the link is disconnected or its signal information is incomplete, the measurement reports an error instead of saving an empty reading. **Zoom and pan.** The plan first shows whole, fitted to the window's width and height, so a tall plan doesn't run off the screen. To look closer: * the buttons in the plan's top-right corner zoom out, zoom in (up to 800%) and go back to the whole plan (**Fit to view**); the number between them is the zoom, where 100% is the whole plan; * hold Ctrl (⌘ on a Mac) and turn the mouse wheel, or pinch on a trackpad or touch screen, to zoom in or out where the pointer is. The wheel alone scrolls the page as usual; * drag the plan to move around it. A drag never measures or selects a point; only a click that stays put does; * after clicking the plan (or tabbing to it), **+** and **-** zoom, **0** fits and the arrow keys move around. Dots and their labels keep their size on screen at any zoom. The zoom is not saved: it goes back to the whole plan when you pick another floor plan or reload the page. In mock mode, "Add sample points" fills the plan with synthetic points. ## Heat maps ![Heat maps](https://wifi-heatmapper.danielalder.cz/docs/images/heatmaps.png) Tick the measurements you want to see. By default, signal strength uses a 0–100% scale; other measurements run from 0 to each map's maximum. The **Speed** maps (download speed, upload speed and the fixed-rate UDP tests) appear once at least one point has a speed reading. Until then a line under the list says the survey measures signal only, with a link to Settings. **Speed detail** picks what each speed map shows: speed in Mbps, jitter (variation in delay), lost or received packets, or resent data (retransmits). **Signal-to-noise ratio** (RSSI minus the noise floor, in dB; 0–50 dB by default, higher is better) uses only points with a noise reading. macOS and Linux drivers that report noise in `iw dev survey dump` record it; Windows does not, so the option is greyed out for surveys without noise. Under **Colour scales** (folded away until you open it), turn on a fixed scale for a map and edit its **Lower** and **Upper** values. Each map has independent bounds: download, upload and the two fixed-rate (UDP) tests do not share ranges, even for the same parameter. Signal strength has its own range too. The first time you enable a fixed scale, it freezes that map's current automatic range. While a field is empty or invalid, the last valid range stays active. Use identical ranges in your before and after surveys to compare them. Ranges are saved with each survey. Speed bounds use Mbps, jitter uses ms, packet values use counts, and signal bounds use percent even when its legend shows dBm. The map, legend and downloaded PNG all use the chosen range. Values outside it use the endpoint colours; measurements are unchanged. Green represents higher values, so lower jitter, packet loss and retransmits are better even though they appear toward the red end. Turn off the fixed scale to restore automatic scaling. Your saved bounds are retained while off, including across reloads; turning it on again restores them rather than freezing a new automatic range. **Radius** controls how far each point's influence reaches. It starts at an automatic value based on how spread out your points are; drag it until neighbouring spots merge into one surface. Hover a map to enlarge or download it. Downloaded PNGs include the legend and a caption with the floor plan name, point count and date. An enlarged map zooms and pans like the floor plan: the buttons in its corner, Ctrl (⌘) + wheel or a pinch, dragging, and **+** / **-** / **0**. ## Survey points ![Survey points](https://wifi-heatmapper.danielalder.cz/docs/images/points.png) Every measurement in the survey. Sort by any column, show more columns (signal in dBm, channel, Wi-Fi standard, coordinates, and more), filter, switch points off, delete a selection, or export everything: **Export CSV** for a spreadsheet, **Export JSON** for the whole survey (points and settings) exactly as the app saves it. The **Name** column shows each point's name (its id, such as `Point_3`, until you name it; the id column is under **Columns**); click it to rename the point, the same way as in its details. Each access point gets its own colour in the **Access point** column and in a point's details, so points on the same access point are easy to spot. The CSV has it in the `label` column. The download and upload columns appear once a point has a speed reading. A test that did not run shows as "—" (an empty cell in the CSV); a test that ran and got nothing through shows as 0, a real dead spot. --- Source: https://wifi-heatmapper.danielalder.cz/docs/throughput # Measuring speed Signal strength tells you where the Wi-Fi is weak. Speed tells you how fast it actually is where you stand. A strong signal is not always fast: interference, a busy channel or a slow link between mesh nodes can make a spot slow that looks fine on the signal map. Technically the app measures *throughput*, the amount of data that gets through per second, in Mbps. To measure speed, choose **Signal and speed** under **What to measure** in Settings. The choice is saved with each survey, so one floor plan can measure signal only and another signal and speed. ## The second computer A speed test needs something to send data to and receive it from. Wi-Fi Heatmapper uses [iperf3](https://iperf.fr), a free tool, on two computers: * **This laptop**, the one you walk around with. The app runs iperf3 here. * **A second computer** on your network that stays put while you survey: a desktop, a Raspberry Pi or a NAS. It runs `iperf3 -s` (the "speed test server") and waits. **Connect the second computer to the router with a cable.** Then the test measures only the Wi-Fi between the laptop and the router. If the server is on Wi-Fi too, every test crosses the air twice and the results come out at roughly half, and they vary with where the server is. Each spot tests **both directions**: download (server to laptop) and upload (laptop to server). Wi-Fi is often slower in one direction than the other, for example when the laptop's antennas are weaker than the router's. ## Set it up 1. Install iperf3 on this laptop. Homebrew (on a Mac) and the Linux `.deb` do this for you. On Windows, install it so it is on your `PATH`, for example `scoop install iperf3` or `choco install iperf3`; the app also finds iperf3 installed by winget. `iperf3 --version` should work in a terminal. Tools called `iperf` or `iperf2` are different and incompatible. 2. Install iperf3 on the second computer and start it with `iperf3 -s`. 3. In Settings, choose **Signal and speed** and enter the second computer's address as the **Speed test server**: `192.168.1.10`, `nas.local`, or with a port, `192.168.1.10:5201`. IPv6 works too: `[fe80::1]:5201`, or a bare address without a port. The port defaults to 5201. 4. Click **Test connection**. The checklist under the field shows whether iperf3 is installed on this computer and whether the server answers. A version newer than 3.17 is recommended on both ends, ideally the same one. On Windows, running from source, you may need to add iperf3's folder to `PATH` yourself (`setx PATH "%PATH%;C:\iperf3"`) and open a new terminal. ## When the speed test can't run The signal is always saved. If iperf3 is not installed on this laptop, no server is set, or the server does not answer, the app skips the speed test, saves the signal reading, and says so: the panel in the corner shows **Saved without speed** with the reason instead of a green tick, and the strip under the header warns that the newest point has no speed. The same happens if a test fails part way, for example because the server is busy with another test. This works the same on macOS, Windows and Linux. Fix the cause (start `iperf3 -s`, check the address, or open port 5201 in the server's firewall) and measure the spot again, or switch the survey back to **Signal only**. ## What each spot runs With speed on, every spot tests download and upload speed (TCP), reading the Wi-Fi signal before, between and after the tests. Turn on **Jitter and lost packets** in Settings to add two fixed-rate tests (UDP); it doubles the time per spot. New surveys start with it off; older surveys keep it on. | Test | Heat maps you get | |---|---| | Download and upload speed (TCP) | speed in Mbps; resent data (retransmits) for download | | Fixed-rate download and upload (UDP) | speed in Mbps, jitter (variation in delay, ms), lost packets, packets received | The download and upload tests send as fast as the connection allows. The fixed-rate tests send at a steady rate and show how evenly the data arrives, which matters for calls and games. For "how good is the coverage here", signal and download/upload speed are enough. **The fixed-rate (UDP) tests are capped at 100 Mbps** by `-b 100M` in their extra iperf3 options, which is safe on most networks. If your UDP results sit at exactly 100 Mbps, the connection can do more: raise the cap under **Advanced: iperf3 options**, for example to `-b 500M`. **Test duration** sets the seconds per test, 5 by default, so a spot takes about 13 seconds in all (about 25 with the UDP tests). A connection takes a second or two to reach full speed, so shorter tests read low and vary more: 3 seconds is the least that gives useful numbers, and longer tests are steadier still. Extra iperf3 options per test (for example `-P 4` for parallel streams, or the UDP cap) go under **Advanced: iperf3 options** in Settings; the server, port and duration come from the settings above. ## Comparing before and after Use **Colour scales** on the **Heat maps** tab to fix the lower and upper value of each map, for example download speed 0–500 Mbps and fixed-rate upload jitter 0–20 ms. Use the same ranges in your before and after surveys (say, before and after adding a mesh node) and the colours mean the same numbers. Ranges are saved with each survey and used in downloaded maps. See [heat maps](https://wifi-heatmapper.danielalder.cz/docs/using-the-app#heat-maps) for units and details. --- Source: https://wifi-heatmapper.danielalder.cz/docs/how-it-works # How it works The app is a small local web server with a browser interface. The desktop app is the same server in its own window. When you click the floor plan, the server reads the Wi-Fi signal with the tools your system already has, runs the speed tests with iperf3 if the survey measures speed, and saves the point to the survey file. ![One measurement: the laptop at a spot on the floor plan reads the Wi-Fi signal from the router, optionally runs a speed test against a second computer wired to the router, and a coloured dot appears on that spot](https://wifi-heatmapper.danielalder.cz/docs/images/one-measurement.svg) Each measurement reads the Wi-Fi signal three times (before, between and after the speed tests, if any) and stores the average. Whether the speed test can run (iperf3 installed, server set and answering) is checked the same way on every system; if it can't, the signal is saved without speed and the app says why. The app's server runs the measurement and saves the point itself, so closing or reloading the browser tab meanwhile doesn't lose it, and Cancel stops it at once. Nothing runs through a shell: the speed test server and iperf3 options you type reach iperf3 as plain arguments, never as a command. ## Where the readings come from | System | Wi-Fi signal | Notes | |---|---|---| | macOS | CoreWLAN, through the app's small Wi-Fi helper | No password. The helper needs Location access to see the network name. From source without the helper: `wdutil` (needs sudo), `ioreg` and `system_profiler`. | | Windows | Native Wifi API (`wlanapi.dll`) | Part of Windows, works in any display language. `netsh` is the fallback. On Windows 11 24H2 and later, Location services must be on. | | Linux | `iw` | `nmcli` is optional and only lists nearby networks. Uses the first connected managed Wi-Fi interface. | | All | `iperf3` | Only for speed tests. See [measuring speed](https://wifi-heatmapper.danielalder.cz/docs/throughput). | Where the system reports it, each point also stores the **noise** level in dBm: macOS does, and so do Linux drivers that print noise in `iw dev survey dump` (many do; Intel's often don't). Windows doesn't. The signal-to-noise ratio is the signal minus the noise, in dB: the higher the better. As a rule of thumb, 25 dB and more is good, and below about 15 dB a connection struggles even when the signal looks strong. The **Signal-to-noise ratio** heat map uses only the points that have it. ## Signal strength in percent and dBm Each point stores both the RSSI in dBm and a percentage. The app treats -40 dBm, typical a couple of feet from an access point, as 100%, and -100 dBm as 0%, in a straight line between them. - **75% and up** (-55 dBm and better): excellent. Green on the heat map. - **50–75%** (-70 to -55 dBm): acceptable, turquoise to blue. - **Below 45%** (-73 dBm and worse): too weak for a reliable connection, yellow to red. | dBm | Percent | |---|---| | -40 | 100% | | -50 | 83% | | -55 | 75% | | -60 | 67% | | -65 | 58% | | -70 | 50% | | -75 | 42% | | -80 | 33% | | -90 | 17% | | -100 | 0% | Each 3 dB drop halves the received power, so 75% (-55 dBm) is already about one thirtieth of the power at 100%. The colour stops are editable under **Heat map colours** in Settings. ## How the heat map is drawn ![From points to a heat map: coloured dots, one per measurement, spread out and blend into one continuous map, green near the router and yellow to red far from it](https://wifi-heatmapper.danielalder.cz/docs/images/points-to-heatmap.svg) The heat map uses inverse distance weighting: each pixel takes a weighted average of the survey points around it, and closer points weigh more. Points only reach as far as the **Radius** on the Heat maps tab. It starts at a value computed from how densely your points cover the plan; drag it until neighbouring spots merge, or back to zero to return to the automatic value. Values are scaled to the colour range before averaging and clamped only after, so a measurement outside a fixed colour scale still pulls on its neighbours. The drawing runs on the GPU with WebGL. ## Network names on macOS Since macOS 15, apps only see the network name (SSID) and access point address (BSSID) if they have Location access. The desktop app and the optional helper ask for it once; nothing looks up where you are. Without it, or when running from source with `wdutil`, names show as `` (see the [FAQ](https://wifi-heatmapper.danielalder.cz/docs/faq#why-do-i-see-redacted-instead-of-a-network-name)). --- Source: https://wifi-heatmapper.danielalder.cz/docs/run-from-source # Running from source The desktop app is the easiest way in, but the same app runs from a clone of the repository in any browser. You need Node.js 22.12 or newer (`node --version`). ```bash git clone https://github.com/hnykda/wifi-heatmapper.git cd wifi-heatmapper npm install npm run dev ``` Open [http://localhost:3000](http://localhost:3000) and follow [your first survey](https://wifi-heatmapper.danielalder.cz/docs/getting-started#your-first-survey). What else each system needs: - **macOS:** enter your sudo password in Settings: `wdutil` needs it to read the signal. The app's server keeps it in memory until you stop the app, and never saves it. To skip the password, run `npm run build:macos-helper` once (needs the Xcode command line tools) and restart `npm run dev`. The Wi-Fi helper reads the signal without sudo; allow it Location access in Settings to also record the network name. See [native/macos-wifi-helper](https://github.com/hnykda/wifi-heatmapper/blob/main/native/macos-wifi-helper/README.md). - **Windows:** nothing; the app uses the Native Wifi API that is part of Windows. - **Linux:** `iw` installed and on `PATH`. `nmcli` (NetworkManager) is optional and only lists nearby networks. If reading the signal needs root, Settings asks for your sudo password, which is kept in memory only. For speed tests, see [measuring speed](https://wifi-heatmapper.danielalder.cz/docs/throughput). ## Try it without any hardware ```bash npm run dev:mock ``` Mock mode fakes the measurements so you can explore the app, or work on it, on any machine without sudo, iperf3 or even a Wi-Fi card. The header shows a "Mock data" badge while it is on. ## Docker (Linux hosts only) The container has to reach the host's Wi-Fi adapter, which is only possible on Linux. On macOS and Windows run the app directly. ```bash docker build -t wifi-heatmapper . docker run --net=host --privileged \ -v ./datas:/app/data \ -v /var/run/dbus:/var/run/dbus \ wifi-heatmapper ``` Surveys and floor plans are kept in `./datas` on the host. NetworkManager inside the container talks to the host over D-Bus, hence the second mount. No sudo password is needed in Docker. The app sends [anonymous usage counts](https://wifi-heatmapper.danielalder.cz/docs/privacy); add `-e WIFI_HEATMAPPER_TELEMETRY=0` to turn them off. The app only answers requests addressed to `localhost`, an IP address, a one-word host name (`nas`) or a local name (`nas.local`, `.lan`, `.home.arpa`), so other web sites can't drive it. To open it under another name, add `-e WIFI_HEATMAPPER_ALLOWED_HOSTS=heatmap.example.com`. ## Where the data is In `data/surveys/.json` next to the app, one file per floor plan, plus the images in `data/media/`. Copy the `data` folder to back up or move a survey. Set `WIFI_HEATMAPPER_DATA_DIR` to keep it elsewhere. ## Logs Set `LOG_LEVEL` to see every command the server runs and its output: `2` is debug, `1` is trace. ```bash LOG_LEVEL=2 npm run dev | tee out.log ``` On Windows (Command Prompt): `set LOG_LEVEL=2` and then `npm run dev`. Attach that log to a bug report. --- Source: https://wifi-heatmapper.danielalder.cz/docs/faq # FAQ ## How do I make a floor plan? Upload an existing plan of your home if you have one. Otherwise take a screenshot of your building from a map, or sketch it on paper and photograph it with your phone. Settings accepts PNG, JPEG and WebP. Heat maps are approximate by nature, so the plan does not need to be perfect or to scale. Reasonably close is enough: upload it and start clicking on the **Floor plan** tab. ## How many points do I need? One or two per room, more in big rooms, and a few near the outer walls and in the places you actually use Wi-Fi (desk, sofa, garden). Gaps between points are filled in by the heat map, so even coverage matters more than many points. ## Why do I see `` instead of a network name? macOS 15 and later only give apps the network name (SSID) and access point address (BSSID) if they have Location access. In the desktop app, click **Allow Location access** in Settings and **Allow** in the macOS dialog. When running from source, build the Wi-Fi helper first (`npm run build:macos-helper`), then do the same. The helper uses the permission only to see the network name; it never looks up where you are. `000000000000` instead of an address has the same cause. ## Where is my data? One JSON file per floor plan, plus the images. In the desktop app: - macOS: `~/Library/Application Support/com.github.hnykda.wifi-heatmapper/data` - Windows: `%LOCALAPPDATA%\com.github.hnykda.wifi-heatmapper\data` - Linux: `~/.local/share/com.github.hnykda.wifi-heatmapper/data` From source it is `data/` in the repository. Copy that folder to back up or move a survey. Set `WIFI_HEATMAPPER_DATA_DIR` to keep it elsewhere. You can edit a survey file by hand. A value the app can't use falls back to its default and the rest still loads; a file that isn't valid JSON is left as it is (the app says it can't read it) until you fix or delete it. Having the same survey open in two tabs is fine: each change is saved on its own, so neither tab undoes the other. ## Can I try it without a Wi-Fi card, sudo or iperf3? Yes: from source, `npm run dev:mock` runs the app with synthetic measurements. Useful for looking around, taking screenshots, or working on the app. ## Is it really free? Yes. The app is MIT licensed and has no ads, accounts or paid tier. If it helped you, star the [repository](https://github.com/hnykda/wifi-heatmapper) so others find it, or [help out](https://wifi-heatmapper.danielalder.cz/#contribute). ## Does it send anything? Only anonymous usage counts: that it was installed, that it started (at most once a day) and that a measurement was saved, with the app version, the system, how the app runs and whether speed tests are switched on. Never any measured value (signal, speed), network names, addresses or anything from your surveys. I build the app on my own, and these counts tell me whether people actually use it. Turn them off in Settings > Usage counts, or with `WIFI_HEATMAPPER_TELEMETRY=0` or `DO_NOT_TRACK=1`. [Privacy and usage counts](https://wifi-heatmapper.danielalder.cz/docs/privacy) lists every field. ## Something went wrong. How do I report it? Please do. Wi-Fi works differently on every system and version (macOS needs Location access and changes what it reports from one release to the next, Windows has two ways in, Linux drivers each report a little differently), and the developers can't test them all. Reports like yours are how it gets fixed. The quickest way is from the app: 1. Click the **help button** (the **?** in the bottom-right corner) and choose **Report a problem**. When a measurement fails (**Could not measure**) or a point is **Saved without speed**, the panel has a **Report this** link that adds the reason; error messages have one too. 2. The dialog shows exactly what would be shared: the app version, your system and version, how the app runs (desktop app, from source, Docker, mock mode), how it reads Wi-Fi on your system (for example the macOS Wi-Fi helper, `wdutil`, `iw`, the Windows WLAN API or `netsh`), whether iperf3 is installed, whether the survey measures signal only or signal and speed, the last measurement and why it failed or had no speed, and the last few hundred lines of the app's log. 3. Click **Open GitHub issue**. GitHub opens in your browser with the form filled in; add what you did and what you expected, check it, and submit. Nothing is sent before you do. If the report is too long for a link, the app copies it, and the issue asks you to paste it. **Copy diagnostics** copies it too, for anywhere else. What is left out: - **Your sudo password, always.** It is removed from everything, including the commands in the log. - **Network names, access point (MAC) addresses and IP addresses**, unless you switch on **Include network names and addresses**. They are replaced with placeholders such as ``, `` and ``; the same network gets the same placeholder throughout, so the report still shows when something changed. Your computer's name is hidden too, and your home folder shows as `~`. The log in the report is what the app printed at its current log level. For more detail, run it with `LOG_LEVEL=2` (see [logs](https://wifi-heatmapper.danielalder.cz/docs/run-from-source#logs)), repeat what went wrong, and report again. Without the app, open an [issue](https://github.com/hnykda/wifi-heatmapper/issues/new?template=bug_report.yml) and fill the form in by hand; the desktop app's log files are listed in [Installing the app](https://wifi-heatmapper.danielalder.cz/docs/install). Tried a desktop app? Tell us how it went, even if it just worked, with the [desktop app report](https://github.com/hnykda/wifi-heatmapper/issues/new?template=desktop-app-report.md). --- Source: https://wifi-heatmapper.danielalder.cz/docs/mcp # Using it with an AI agent (MCP) wifi-heatmapper comes with an optional [MCP](https://modelcontextprotocol.io) server. With it, an AI agent such as Claude Code or Claude Desktop can read your surveys, explain them, help you set up a new one, and guide you through measuring it while you carry the laptop. ## What an agent can and can't do **It can:** - **read** your floor plans and surveys: every point's position, signal (% and dBm), noise and signal-to-noise ratio where measured, network, access point and speed results; - **analyse** them: dead zones, weak rooms, which access point covers which rooms, and whether slow speed comes from weak signal or something else; - **plan** a survey: how many spots, how far apart, where to fill gaps; - **set up** a survey: signal only or signal and speed, the speed test server, test duration, extra iperf3 options, access point names and the heat map look; - **copy settings** from one survey to another, for example to set up the second floor like the first; - **name points** ("Kitchen", "Desk upstairs"), so it and you can talk about rooms instead of point ids; - **measure with you carrying the laptop**: it tells you where to go, waits until you say you're there, measures, and tells you the result in plain words (see [Taking measurements](https://wifi-heatmapper.danielalder.cz/docs/mcp#taking-measurements)); - **report a problem**: it prepares the same redacted report as the app's **Report a problem** button and gives you a GitHub link to open yourself. **It can't:** - **walk around.** You carry the laptop; the agent asks before every measurement. - **see the heat map images.** The app draws them in your browser, so the agent gets a numeric grid computed the same way instead. - **handle the sudo password.** No tool takes or returns it. Where one is needed you type it in the app's Settings (see [The sudo password](https://wifi-heatmapper.danielalder.cz/docs/mcp#the-sudo-password)); never paste it into the chat. - **work when the app isn't running.** It talks to the running app; start the app first. - **delete anything** or change floor plans or measured values. A measurement only adds a point, and `update_point` only changes a point's name. ## How it fits together The MCP server is a small program your AI client starts on your computer. It talks to the running app over the app's own local HTTP API, the same one the browser uses, and never reads or writes the app's files directly. ``` AI agent --stdio--> MCP server --HTTP, this computer only--> wifi-heatmapper app ``` ## Setup Three steps: start the app, find the two paths below, and add them to your AI client. The MCP server needs Node.js 20 or newer; the desktop app brings its own. ### 1. Find the paths **From source.** Build the MCP server once, and again after you update the app, then start the app: ```bash cd wifi-heatmapper npm run mcp:build # writes dist/mcp/wifi-heatmapper-mcp.mjs npm run dev # in another terminal: the app on http://localhost:3000 ``` The command is `node`, the argument is the absolute path of `dist/mcp/wifi-heatmapper-mcp.mjs` (one file, no dependencies), and the app is at `http://localhost:3000`. If the app runs on another port, set `WIFI_HEATMAPPER_URL`, for example `http://localhost:3100`. **Desktop app.** It ships the MCP server and the Node.js to run it: | System | Command (Node) | Argument (MCP server) | | --- | --- | --- | | macOS | `/Applications/WiFi Heatmapper.app/Contents/Resources/server/node` | `/Applications/WiFi Heatmapper.app/Contents/Resources/server/mcp-server.mjs` | | Windows (setup `.exe`) | `%LOCALAPPDATA%\WiFi Heatmapper\server\node.exe` | `%LOCALAPPDATA%\WiFi Heatmapper\server\mcp-server.mjs` | | Windows (`.msi`) | `C:\Program Files\WiFi Heatmapper\server\node.exe` | `C:\Program Files\WiFi Heatmapper\server\mcp-server.mjs` | | Linux (`.deb`) | `/usr/lib/WiFi Heatmapper/server/node` | `/usr/lib/WiFi Heatmapper/server/mcp-server.mjs` | The AppImage mounts itself at a different path on every start, so there is no stable path to point an agent at. Use the `.deb` or run from source. **The desktop app's port.** The desktop app picks a free port each time it starts, so the agent can't know it. Give the app a fixed port with the `WIFI_HEATMAPPER_PORT` environment variable, and use the same port in `WIFI_HEATMAPPER_URL`. The examples use 3917: - **macOS:** start the app from Terminal with `WIFI_HEATMAPPER_PORT=3917 "/Applications/WiFi Heatmapper.app/Contents/MacOS/wifi-heatmapper"`, or run `launchctl setenv WIFI_HEATMAPPER_PORT 3917` once (it lasts until you log out) and then open the app as usual. - **Windows:** run `setx WIFI_HEATMAPPER_PORT 3917` once, then start the app from the Start menu. - **Linux:** `WIFI_HEATMAPPER_PORT=3917 wifi-heatmapper`. Use `http://127.0.0.1:3917` as the URL: the desktop app listens on 127.0.0.1 only. ### 2. Add it to your client #### Claude Code From source: ```bash claude mcp add wifi-heatmapper \ -e WIFI_HEATMAPPER_URL=http://localhost:3000 \ -- node /path/to/wifi-heatmapper/dist/mcp/wifi-heatmapper-mcp.mjs ``` Desktop app on macOS: ```bash claude mcp add wifi-heatmapper \ -e WIFI_HEATMAPPER_URL=http://127.0.0.1:3917 \ -- "/Applications/WiFi Heatmapper.app/Contents/Resources/server/node" \ "/Applications/WiFi Heatmapper.app/Contents/Resources/server/mcp-server.mjs" ``` On Windows or Linux, use the two paths from the table the same way. Add `-s user` to make it available in every project. Check it with `claude mcp list`, or `/mcp` inside Claude Code. #### Claude Desktop Edit `claude_desktop_config.json` (Settings, Developer, Edit config; on macOS it is in `~/Library/Application Support/Claude/`, on Windows in `%APPDATA%\Claude\`) and restart Claude Desktop. From source: ```json { "mcpServers": { "wifi-heatmapper": { "command": "node", "args": ["/path/to/wifi-heatmapper/dist/mcp/wifi-heatmapper-mcp.mjs"], "env": { "WIFI_HEATMAPPER_URL": "http://localhost:3000" } } } } ``` Desktop app on macOS: ```json { "mcpServers": { "wifi-heatmapper": { "command": "/Applications/WiFi Heatmapper.app/Contents/Resources/server/node", "args": ["/Applications/WiFi Heatmapper.app/Contents/Resources/server/mcp-server.mjs"], "env": { "WIFI_HEATMAPPER_URL": "http://127.0.0.1:3917" } } } } ``` Desktop app on Windows (setup `.exe`; replace `you` with your user name): ```json { "mcpServers": { "wifi-heatmapper": { "command": "C:\\Users\\you\\AppData\\Local\\WiFi Heatmapper\\server\\node.exe", "args": ["C:\\Users\\you\\AppData\\Local\\WiFi Heatmapper\\server\\mcp-server.mjs"], "env": { "WIFI_HEATMAPPER_URL": "http://127.0.0.1:3917" } } } } ``` Claude Desktop does not expand `~` or `%LOCALAPPDATA%`, so write paths out in full. If `node` is not found, give its full path (`which node`). #### Any other MCP client It is a standard stdio server: - **command:** `node`, or the desktop app's bundled Node from the table; - **arguments:** the path to `wifi-heatmapper-mcp.mjs` (from source) or `mcp-server.mjs` (desktop app); - **environment:** `WIFI_HEATMAPPER_URL`, if the app is not on `http://localhost:3000`. `node wifi-heatmapper-mcp.mjs --help` prints the options and exits. `npm run --silent mcp` builds and starts it in one go; keep `--silent`, because npm's own output on stdout would break the protocol. ### 3. Try it Ask "List my wifi-heatmapper surveys". The agent should call `list_surveys` and name your floor plans. If it can't reach the app, see [Troubleshooting](https://wifi-heatmapper.danielalder.cz/docs/mcp#troubleshooting). ## An example ``` You: Let's measure the ground floor. I have the laptop. Agent: (check_setup, get_survey "Ground floor.png": 1100 x 700 px, 3 points, all in the living room; get_heatmap_grid shows nothing measured in the east half.) Speed tests are on (server 192.168.1.10). Let's fill the east half first: 6 spots, about 3 m apart. First, the kitchen: on the plan that's the room top right, around (900, 150). Go there, hold the laptop where you'd use it, and tell me when you're there. You: I'm by the kitchen counter. Agent: (start_measurement {"survey":"Ground floor.png","x":900,"y":150}; your client asks you to allow it. Then wait_for_measurement.) Kitchen: signal 48% (-74 dBm) from "Hall AP", 5 GHz; download 180 Mbps, upload 95 Mbps. Weak but usable. Next: the hallway by the front door, around (700, 400). Tell me when you're there. You: There. Agent: (start_measurement, wait_for_measurement) Hallway: 22% (-86 dBm), saved without speed: the speed test server did not answer. That's a dead zone. Next: the study, around (1000, 550). You: Stop, someone needs the laptop. Agent: (cancel_measurement if one is running.) Stopped; nothing more was saved. You have 5 points; when you're back, we'll do the study and the bathroom, and then I can explain the map (analyse_survey). ``` You can also just ask, for example "Which rooms have weak Wi-Fi in my Home.png survey, and where should I put a second access point?" ## Taking measurements The agent can measure, but it can't walk: you carry the laptop. A measurement is the same one you get by clicking the floor plan in the app, started by the agent at a position it names in floor-plan pixels. 1. The agent proposes a spot ("the kitchen, by the window") and you go there with the laptop, holding it where you'd use it. You can also pick the spots yourself. 2. You say you're there. Your MCP client asks you to allow `start_measurement` (keep that on). 3. The app measures, exactly as for a click: signal, and speed if the survey has speed tests on. Keep the laptop still. 4. The server saves the point. The agent reads it back with `wait_for_measurement` and tells you the result in plain words, then moves on. **Watch the map fill in as you go.** Keep the app open on that floor plan in a browser tab (on the Floor plan tab). It shows each measurement the agent starts live, with the same progress panel as your own clicks, and the new point when it's saved. Nothing to set up: every open tab follows the measurement the server is running. ### You stay in control - **You confirm every measurement.** `start_measurement` and `cancel_measurement` are marked as tools that change something (`readOnlyHint: false`), so MCP clients ask before running them. The agent is also told to wait for you to say you're at the spot. A measurement runs commands on this computer (the Wi-Fi tools, iperf3), the same ones a click runs, with the survey's settings. - **One at a time.** The app runs one measurement at a time. While one runs, `start_measurement` is refused with the running job's id, and a click in the app waits too. - **Cancel any time.** Say stop, or press Cancel in the app's progress panel. `cancel_measurement` only stops the job it names, kills a running iperf3 at once and saves nothing. A measurement that is already being saved can't be stopped. - A measurement only adds a point. It never changes or deletes others; you can switch a point off or delete it in the app. The agent can name a point (`update_point`), nothing else. ### The measurement tools `start_measurement(survey, x, y)`: `x` and `y` are floor-plan pixels (origin top left), checked against the floor plan's size (a spot off the plan is refused). It sends the survey's own speed settings, never a password, and waits a few seconds for the first step (can Wi-Fi be read, is a sudo password needed) so a problem there is reported at once. Otherwise it returns the job id: ``` > start_measurement {"survey":"Home.png","x":400,"y":250} {"jobId":"sMOPDKsyE6","survey":"Home.png","x":400,"y":250,"state":"running", "speedTest":"on","next":"Call wait_for_measurement with this jobId. ..."} ``` `wait_for_measurement(jobId, timeoutSeconds)` waits up to `timeoutSeconds` (default 45, at most 300; `0` only checks) and returns the saved point described the way `get_survey` describes points, with a one-line summary: ``` > wait_for_measurement {"jobId":"sMOPDKsyE6"} {"jobId":"sMOPDKsyE6","state":"done","pointId":"Point_1", "summary":"Signal 81% (-51 dBm) from Demo Network, channel 44 (5 GHz); download 650.86 Mbps, upload 488.99 Mbps.", "point":{"id":"Point_1","x":400,"y":250,"signalPercent":81,"rssiDbm":-51, ...}} ``` If the speed test was skipped (iperf3 missing, server unreachable), the point is still saved and the answer says `"savedWithoutSpeed": "Saved without speed: "`. If the measurement failed, it says why (`"state": "error", "reason": ...`) and nothing was saved. If it is still running when the time is up, it answers `"state": "running"` with the current step, and the agent calls it again. Many clients give up on a tool call after 60 seconds, hence the default of 45. `cancel_measurement(jobId)` stops that job. If it had already finished, the answer says so (and which point was saved). `update_point(survey, pointId, label)` names a point, for example right after a measurement, with the name you use for the spot: ``` > update_point {"survey":"Home.png","pointId":"Point_1","label":"Kitchen"} {"survey":"Home.png","pointId":"Point_1","label":"Kitchen", "note":"Saved. An open app tab shows the new name."} ``` The label is trimmed and at most 60 characters; `""` removes it. Only the name changes: the id (`Point_1`) and the measured values stay as they were. The app shows the label instead of the id, `get_survey` returns it as `label`, and the agent uses it when it talks about the point. ### The sudo password On most computers no password is needed and measuring just works: | System | Password? | | --- | --- | | macOS with the Wi-Fi helper (the desktop app, or `npm run build:macos-helper`) | No | | macOS without the helper (`wdutil`) | Yes | | Linux, when `iw` reads the connection as a normal user (most desktops) | No | | Linux, when `iw` is refused | Yes | | Windows | No | | Docker | No (the container runs as root) | | Mock mode | No | Where one is needed, the agent can't ask you for it and you shouldn't give it in chat: no tool takes it. Instead, the measurement stops at its first step and the agent tells you: > This computer needs the sudo password to read Wi-Fi. Enter it once in > the app's Settings (it stays in memory), then try again. Type it in the app's Settings tab. The app's server then holds it **in memory only, for as long as the app runs**: it is never written to a file or a log, never returned by any API (`GET /api/sudo-password` only says whether one is held), and gone when the app restarts. It is only ever given to `sudo` on its standard input, for the app's own Wi-Fi commands (`wdutil info`, `iw dev link`). Measurements you start by clicking use it too, so after a page reload you don't have to type it again. Emptying the field forgets it. If the password is wrong, the agent says so and you correct it in Settings. `check_setup` says whether the app holds one (`sudoPasswordHeld`), and on macOS whether the Wi-Fi helper makes it unnecessary. ## Tools Survey names are floor plan file names, as `list_surveys` returns them (for example `EmptyFloorPlan.png`). Positions are in floor-plan image pixels, with the origin at the top left. Throughput is in Mbps. | Tool | What it does | | --- | --- | | `list_surveys` | Floor plans, and for each survey its point count, whether throughput tests are on, and when it was last saved. A survey file that cannot be read is listed with `unreadable: true`. | | `get_survey(name)` | All points and a summary. Optional thresholds: `weakSignalPercent` (default 50, about -70 dBm), `deadSignalPercent` (25, about -85 dBm), `slowDownloadMbps` (25). `includePoints: false` returns the summary only. | | `get_settings(name)` | The settings `update_settings` can change. | | `update_settings(name, ...)` | Change some of them and save the survey. | | `copy_settings(from, to, parts?)` | Copy settings from one survey to another and save it. | | `check_setup(name?)` | What the app can tell without measuring, plus problems and notes. | | `get_heatmap_grid(name, metric, cells)` | A numeric heat map. | | `report_problem(name?, problem?, includeNetwork?)` | A bug report for the developers and a prefilled GitHub issue link. Posts nothing. | | `start_measurement(survey, x, y)` | Measure at that spot (you are holding the laptop there) and save a point. Returns a job id. | | `wait_for_measurement(jobId, timeoutSeconds?)` | Wait for it and return the saved point, or why it failed. | | `cancel_measurement(jobId)` | Stop it at once. Nothing is saved. | | `update_point(survey, pointId, label)` | Name a point (`""` removes the name). Changes nothing else. | ### get_survey Each point looks like this (shortened): ```json { "id": "Point_7", "label": "Office", "x": 922, "y": 92, "enabled": true, "measuredAt": "2026-10-28T08:20:26.571Z", "ssid": "Demo Network", "bssid": "9e05d696e830", "apName": null, "signalPercent": 29, "rssiDbm": -83, "noiseDbm": -92, "snrDb": 9, "channel": 44, "bandGHz": 5, "tcpDownload": { "mbps": 75.69, "retransmits": 0 }, "tcpUpload": { "mbps": 58.87, "retransmits": 0 }, "udpDownload": null, "udpUpload": null } ``` `label` is the name given to the point in the app or with `update_point`, or `null`. `noiseDbm` and `snrDb` are `null` where the system reports no noise (Windows, some Linux drivers). A test is `null` when it did not run (signal only, no iperf3 server, or it failed); a test that ran and got nothing through is `{ "mbps": 0 }`, a dead spot. `channel`, `bandGHz`, `channelWidthMHz` and `txRateMbps` are `null` where the system did not report them. UDP results have `mbps`, `jitterMs`, `lostPackets` and `packetsReceived`. The summary only uses points that are switched on, like the heat maps: ```json "summary": { "points": 25, "enabledPoints": 25, "throughputMeasuredPoints": 25, "metrics": { "signalPercent": { "count": 25, "min": 20, "median": 63, "max": 93, "unit": "%" }, "tcpDownloadMbps": { "count": 25, "min": 36, "median": 357.2, "max": 778.4, "unit": "Mbps" } }, "deadZonePointIds": ["Point_25"], "weakSignalPointIds": ["Point_7", "Point_13", "Point_18", "Point_19", "Point_23", "Point_24"], "accessPoints": [ { "bssid": "9e05d696e830", "apName": null, "ssid": "Demo Network", "bandGHz": 5, "channels": [44], "points": 25, "medianSignalPercent": 63 } ] } ``` ### get_heatmap_grid `metric` is one of the maps on the Heat maps tab: `signalStrength`, `snr` (signal-to-noise ratio in dB; only points with a noise reading, which macOS and some Linux drivers give and Windows doesn't), `tcpDownload-bitsPerSecond`, `tcpDownload-retransmits`, `tcpUpload-bitsPerSecond`, and for `udpDownload` and `udpUpload` each of `-bitsPerSecond`, `-jitterMs`, `-lostPackets`, `-packetsReceived`. `cells` (2 to 60, default 20) is the number of cells along the longer side. The grid is computed the way the app draws the map: inverse-distance weighting with power 3, from the points that are switched on, using the same radius as the Heat maps tab (yours, or the automatic one). A cell is `null` where no point is close enough, which is blank on the map. Values are in the map's units and are not clamped; the colour scale is returned separately. ``` > get_heatmap_grid {"name":"EmptyFloorPlan.png","metric":"signalStrength","cells":8} {"unit":"%","pointsUsed":25,"radius":{"pixels":156,"automatic":true}, "floorPlan":{"width":1024,"height":763}, "colourScale":{"min":0,"max":100,"fixed":false}, "columns":8,"rows":6, "x":[64,192,320,448,576,704,832,960], "y":[64,191,318,445,572,699], "values":[[71,76.9,83.7,81.8,63.2,51.7,42.2,29], ...]} ``` `values[row][column]` is the value at (`x[column]`, `y[row]`). ### update_settings Only the fields you give change. The rest of the survey, including all points, is kept as it is. | Field | Value | | --- | --- | | `measureSpeed` | `true` measures signal and speed (iperf3), `false` signal only | | `iperfServerAdrs` | the speed test server, `"192.168.1.10"` or `"nas.local:5201"`. Set without `measureSpeed`, `"localhost"` also turns speed off and any other server turns it on, as before | | `testDuration` | seconds per iperf3 test, 1 to 60 | | `udpTests` | `true` also runs the fixed-rate (UDP) tests for jitter and lost packets (doubles the time per point); `false` (new surveys) TCP download and upload only | | `iperfCommands` | extra iperf3 options for any of `tcpDownload`, `tcpUpload`, `udpDownload`, `udpUpload`, one argument per item, e.g. `{ "udpUpload": ["-b", "500M"] }`; the others are kept | | `apMapping` | the whole list of `{ "apName", "macAddress" }`; a MAC address may end in `*` to match every BSSID that starts with it | | `radiusDivider` | heat spot radius in floor-plan pixels, or `null` for automatic | | `maxOpacity`, `minOpacity` | 0 to 1 | | `heatmapRanges` | fixed colour scales per map, for example `{ "tcpDownload-bitsPerSecond": { "min": 0, "max": 600, "enabled": true } }`; `null` removes one, others are kept | Example: ``` > update_settings {"name":"EmptyFloorPlan.png","testDuration":2, "heatmapRanges":{"tcpDownload-bitsPerSecond":{"min":0,"max":600,"enabled":true}}} {"saved":true,"changed":["testDuration","heatmapRanges"], ...} ``` The agent can set what you can set in the app's Settings tab. No command runs through a shell, so the server and the iperf3 options reach iperf3 as separate arguments, never as a command. The server must be a host name, an IPv4 or IPv6 address, with an optional port (`nas.local:5201`, `[fe80::1]:5201`), and options the app sets itself (`-c`, `-p`, `-t`, `-R`, `-u`, `-J`) are refused. If anything is refused, nothing is saved. The app saves each change on its own (one setting, one point), so a change the agent makes while the app is open on that floor plan is kept. The page shows it after its next save, or when you reload it. ### copy_settings Copies settings from the survey `from` into the survey `to`, as **Copy settings from…** does in the app. `parts` is a list of: | Part | What is copied | | --- | --- | | `speed` (the default) | signal only or signal and speed, the iperf3 server, test duration and extra iperf3 options | | `apNames` | access point names | | `heatmapLook` | heat spot radius, opacity, blur, colour gradient and fixed colour scales | Points, the floor plan, its size and the sudo password are never copied. If `to` has no survey yet, it is created. The answer says which settings changed. ``` > copy_settings {"from":"Ground floor.png","to":"First floor.png","parts":["speed","apNames"]} {"copied":["speed","apNames"],"created":false,"changed":["iperfServerAdrs","apMapping"], ...} ``` ### check_setup Reports the app version and OS, mock mode, whether iperf3 is installed, the macOS Wi-Fi helper and its Location access, whether the app holds a sudo password (`sudoPasswordHeld`, yes or no, never the password), and, with a survey name, whether throughput tests are set up. `problems` lists what will stop a measurement from working well; `notes` lists the rest. Some things can't be checked without measuring: whether the iperf3 server answers, whether Wi-Fi can be read, and, on Linux, whether a sudo password is needed. The app checks them as the first step of a measurement, and `start_measurement` reports what it finds within a few seconds. ### report_problem The same report as **Report a problem** in the app (see the [FAQ](https://wifi-heatmapper.danielalder.cz/docs/faq#something-went-wrong-how-do-i-report-it)): app version, OS, how the app runs and reads Wi-Fi, iperf3, the last measurement and the recent log. The sudo password is always removed; network names, MAC and IP addresses are replaced by placeholders unless `includeNetwork` is true. It returns the summary and an `issueUrl` for you to open, check and submit. When the report is too long for a link, it also returns `fullDiagnostics` to paste into the issue. Agents are told to suggest it when they see something that looks like a bug rather than a Wi-Fi problem: an error, impossible readings (0% or -100 dBm everywhere, channel 0), no noise readings on macOS, or Windows falling back from the WLAN API to `netsh`. ## Prompts Prompts are ready-made requests your MCP client offers (in Claude Code, type `/` and look for `wifi-heatmapper`). | Prompt | Arguments | What it asks for | | --- | --- | --- | | `analyse_survey` | `name` | Dead zones, weak areas, which access point serves where, throughput bottlenecks compared with signal, concrete suggestions (move or add an access point), and what the data can't show. | | `compare_surveys` | `before`, `after` | A before and after comparison with the same metrics, cell by cell when the floor plans match, and fixed colour scales for side-by-side maps. | | `plan_survey` | `name` (optional) | How many points and how far apart, where to stand, and how to set up the iperf3 server. | | `survey_walkthrough` | `name`, `rooms` (optional) | The agent guides you spot by spot while you carry the laptop: it plans the spots (filling gaps and weak areas from `get_heatmap_grid`), asks you to go to each, waits for you to confirm, measures and tells you the result. | ## Safety model - **This computer only.** The MCP server refuses any app address other than `localhost`, `127.0.0.1` or `::1`. The app runs commands on the machine it runs on (iperf3, and `sudo` on some systems), with the settings it is given, so it should never be driven over the network. If you really need to, set `WIFI_HEATMAPPER_MCP_ALLOW_REMOTE=1`, and make sure nothing else can reach that port. - **No secrets.** The sudo password is never returned and can't be set through MCP. The app never saves it, its settings API strips it from anything it reads or writes, and the copy the app's server holds for agent-started measurements stays in memory (see [The sudo password](https://wifi-heatmapper.danielalder.cz/docs/mcp#the-sudo-password)). - **Changes are real.** `update_settings` saves the survey file, and the next measurement uses the new settings. `start_measurement` runs the Wi-Fi tools and iperf3 on this computer and adds a point; `update_point` renames one. Nothing can delete anything or change measured values. Most clients ask before calling a tool that changes something; keep that on. - **Sends nothing.** The MCP server talks only to the app on this computer. It has no [usage counts](https://wifi-heatmapper.danielalder.cz/docs/privacy) of its own and no tool that reads or changes the app's. - **Other web sites can't use it.** The app refuses API requests that come from another web site open in your browser, or that are addressed to a name other than a local one. Tools on this computer, such as the MCP server, are allowed. - The app's HTTP API has no login, like the rest of the app. Anything on this computer that can reach the port can use it, with or without MCP. ## Troubleshooting **"Can't reach wifi-heatmapper at http://localhost:3000. Is the app running?"** Start the app (`npm run dev`, or open the desktop app). If it runs on another port, set `WIFI_HEATMAPPER_URL`. For the desktop app, see [the desktop app's port](https://wifi-heatmapper.danielalder.cz/docs/mcp#1-find-the-paths). **"Refusing to talk to ..."** `WIFI_HEATMAPPER_URL` points at another computer. See [Safety model](https://wifi-heatmapper.danielalder.cz/docs/mcp#safety-model). **"Unexpected answer ... not JSON"** Something else is listening on that port. Check the URL in your browser: it should show wifi-heatmapper. **"No survey for ..."** Use a name from `list_surveys`. A floor plan gets a survey file once it has been opened in the app. **"This computer needs the sudo password to read Wi-Fi."** Type the password in the app's Settings tab (it stays in memory until the app quits), then ask the agent to try again. On macOS, the Wi-Fi helper makes it unnecessary. Don't paste it into the chat. **"A measurement is already running."** Only one runs at a time: wait for it (`wait_for_measurement`), or cancel it in the app or with `cancel_measurement`. **"The app doesn't know measurement ..."** The app keeps the last 20 jobs and forgets them when it restarts. Check the survey with `get_survey`. **The open tab doesn't show the agent's measurement.** It follows measurements for the floor plan it shows: open the same floor plan. **The open tab doesn't show the agent's settings change.** The change is saved; the page picks it up after its next save of that survey, or when you reload it. Each change is saved on its own, so the page never undoes it. **The grid is all `null`, or has few numbers.** The survey has few points or a small radius. Each point only reaches as far as the radius (shown in the result); add points or set a larger `radiusDivider`. **The server doesn't show up in the client.** Run the command from your config by hand, for example `node /path/to/dist/mcp/wifi-heatmapper-mcp.mjs --help`. If that fails, fix the path or install Node.js 20+. Clients start it with a minimal environment, so use absolute paths. With `npm run mcp`, add `--silent`. **Mock mode.** `npm run dev:mock` works too; `check_setup` says the measurements are synthetic. ## For developers - Code: `src/mcp/` (`main.ts` is the entry point, `server.ts` the tools, `measure.ts` the measurement helpers, `prompts.ts` the prompts, `app-client.ts` the HTTP client). Measurements use the app's job API (`POST /api/start-task?action=start|stop`, `GET ...?action=status&id=`); the sudo password the Settings tab gives the server is in `src/lib/sudo-session.ts` and `/api/sudo-password`. The heat map grid is the pure `src/lib/heatmap-grid.ts`, which mirrors `src/app/webGL/shaders/heatmapFragmentShader.ts` and `Heatmaps.tsx`; keep them in step. - `npm run mcp:build` bundles it with esbuild (`scripts/build-mcp.mjs`). The MCP SDK is a devDependency used only by that bundle, never by the Next.js app. `desktop/build-server.mjs` writes the same bundle to `desktop/server/mcp-server.mjs`, and `desktop/smoke-test.mjs` checks it. - Tests: `__tests__/heatmap-grid.test.ts`, `__tests__/mcp-*.test.ts`, `__tests__/sudo-session.test.ts`. `mcp-server.test.ts` builds the bundle and drives it with the SDK's client against a fake app (measuring included: start, wait, cancel, one at a time, the sudo message). `e2e/two-writers.spec.ts` checks that an open tab follows a measurement started over the API. - Possible follow-ups: let the desktop app write its URL somewhere the MCP server can find it, or prefer a fixed port, so `WIFI_HEATMAPPER_PORT` isn't needed; have the page pick up a survey that changed on the server without waiting for its next save. --- Source: https://wifi-heatmapper.danielalder.cz/docs/privacy # Privacy and usage counts I build wifi-heatmapper on my own, and I only keep working on it if people actually use it. Anonymous counts of installs, app starts and measurements tell me that, and honestly keep me motivated. Nothing identifies you, and you can turn it off any time. Your surveys, floor plans and results stay on your computer. The only thing the app sends on its own is the small usage counts described here. They are on by default. **Settings > Usage counts** has the switch, why the app counts, and the exact event it sends. ## What is sent Three events, sent by the app's server (never by the browser page): | Event | When | | --- | --- | | `Install` | once, the first time the app runs with usage counts on | | `App start` | when the app starts, at most once a day | | `Measurement` | each time a measurement point is saved | Each event carries these properties, and nothing else: | Property | Values | | --- | --- | | `version` | the app version, e.g. `0.7.0` | | `os` | `macos`, `windows`, `linux` or `other` | | `arch` | the processor type, e.g. `arm64` or `x64` | | `runs_as` | `desktop` (the desktop app), `docker`, `podman` or `browser` (from source, opened in a browser) | | `speed_test` | `Measurement` only: `on` if the survey runs speed tests (iperf3), `off` if it measures signal only | No measured value is ever sent: no signal strength, dBm, speed, noise, network names or addresses. `speed_test` only says whether speed tests are switched on, so I can see if the iperf3 part is worth working on. This is a whole `Measurement` event, exactly as sent: ```text POST https://plan.danielhnyk.cz/api/event User-Agent: wifi-heatmapper/0.7.0 () Content-Type: application/json { "name": "Measurement", "url": "app://wifi-heatmapper/measurement", "domain": "app.wifi-heatmapper.danielalder.cz", "props": { "version": "0.7.0", "os": "macos", "arch": "arm64", "runs_as": "desktop", "speed_test": "on" } } ``` `Install` and `App start` look the same, without `speed_test`. **Settings > Usage counts** in the app shows the event your computer would send, built by the same code that sends it, and when the last one went out. ## What is never sent - network names (SSIDs) and access point addresses (BSSIDs, MAC addresses); - IP addresses of anything you entered, such as the speed test server; - host names, user names, file names, folder paths and floor plans; - any measured value: signal strength, dBm, channels, speeds; - error messages and logs; - the sudo password. The event is built from the fixed fields above only. Nothing from a survey reaches it, and the tests check that with surveys full of real-looking network names and addresses. ## Where it goes To my own [Plausible Analytics](https://plausible.io) server at `plan.danielhnyk.cz` (Plausible Community Edition, self-hosted), not to a third-party analytics service. Plausible counts unique visitors from the IP address and the User-Agent header, mixed with a random salt that changes every day. It stores neither: **no IP address is stored**, and the daily hash can't be traced back or linked from one day to the next. The User-Agent the app sends is `wifi-heatmapper/ ()`. The install id is a random UUID made on your computer the first time usage counts are on. It is not derived from anything about you or your computer, it is only ever in that header, and it is not stored by Plausible. All it does is make one install count as one visitor a day. It is not in bug reports either: they only say whether usage counts are on. The id is kept in `usage-counts.json` in the app's data folder (see [Where is my data?](https://wifi-heatmapper.danielalder.cz/docs/faq#where-is-my-data)), next to the on/off setting. Turning usage counts off in Settings deletes it. The request goes straight from your computer to that server, with a short timeout, in the background. Offline, it is dropped silently: the app works the same with or without it. ## How to turn it off Any one of these: - **In the app:** Settings > Usage counts > turn off **Send anonymous usage counts**. It is saved for this computer (not in your surveys), and the install id is deleted. - **With an environment variable:** start the app with `WIFI_HEATMAPPER_TELEMETRY=0` (`false`, `off` or `no` work too). For example `WIFI_HEATMAPPER_TELEMETRY=0 npm start`, or `docker run -e WIFI_HEATMAPPER_TELEMETRY=0 ...`. - **With `DO_NOT_TRACK=1`**, the [common convention](https://consoledonottrack.com) for command-line tools. It is also always off, and Settings says why, when: - the app runs in mock mode (`WIFI_HEATMAPPER_MOCK`, `npm run dev:mock`), which includes the end-to-end tests; - you run the development server (`npm run dev`), unless you set `WIFI_HEATMAPPER_TELEMETRY=1`; - `CI` is set (automated builds and tests). When it is off, nothing is sent and no install id is made. In Docker, keep the data folder on a volume (`-v ./datas:/app/data`, as in [Running from source](https://wifi-heatmapper.danielalder.cz/docs/run-from-source#docker-linux-hosts-only)), or each new container counts as a new install. ## For developers `WIFI_HEATMAPPER_TELEMETRY_URL` sends the events to another Plausible server (the base URL, or its `/api/event`), and `WIFI_HEATMAPPER_TELEMETRY_DOMAIN` sets the site name. The code is in `src/lib/telemetry.ts`. The [MCP server](https://wifi-heatmapper.danielalder.cz/docs/mcp) sends nothing: it only talks to the app on your computer.