A Raspberry Pi 5 + iPhone tool for physically hunting down a WiFi transmitter by walking toward increasing signal strength. The Pi hosts a WiFi hotspot and serves an HTTPS page; the iPhone joins the hotspot, opens the page in Safari, and gets a live arrow pointing toward the focused network’s transmitter.

For Agents

Repo lives at /Users/levander/wifi-hunter/. Hardware target is a Raspberry Pi 5 (model 2712, kernel 6.18, aarch64) reachable via ssh raspi (comes in over eth0 / 192.168.1.200, so wlan0 is free for the hotspot). Server is Python 3 stdlib only — no pip deps. Key non-obvious fact: the Pi 5 built-in WiFi scans the full band while hosting the AP, so no second USB dongle is needed — see pi5-scan-while-ap-mode.

How It Works

  1. Pi brings up a WiFi hotspot on wlan0:
    sudo nmcli device wifi hotspot ifname wlan0 ssid hunter-ap password huntme123
  2. Pi runs sudo python3 server.py — an HTTPS server on port 8443 with a self-signed cert (stdlib http.server + ssl). It shells out to iw dev wlan0 scan to enumerate nearby networks with RSSI.
  3. iPhone joins hunter-ap, opens https://<pi-ap-ip>:8443/ in Safari, taps through the self-signed cert warning.
  4. The page (index.html + direction.js) reads the phone’s GPS (Geolocation API) and compass (DeviceOrientation / webkitCompassHeading), shows a live-updating network list with focus/exclude controls, and draws:
    • an arrow toward increasing signal strength of the focused network, and
    • a canvas track of your path, colored by RSSI.

Direction Algorithm

The arrow is a least-squares gradient estimate over recent GPS + RSSI samples:

  1. Project lat/lon to local meters via equirectangular approximation (flat-earth over the small search area).
  2. Fit a plane rssi = a*x + b*y + c by least squares over the recent sample buffer.
  3. Gradient (a, b) points toward the transmitter (direction of increasing RSSI).
  4. bearing = atan2(a, b), then rotate by webkitCompassHeading to get a screen-relative arrow.

Verified by test_direction.js — a Node self-check that passes with < 2° error on synthetic data.

Secure Context Requirement

iOS Safari needs HTTPS

iOS Safari only exposes Geolocation and DeviceOrientation in a secure context (HTTPS). Plain HTTP silently yields no GPS/compass. A self-signed cert + tap-through works — hence port 8443 with ssl rather than a plain http.server.

iOS Geolocation: Diagnosing kCLError (field test 2026-07-15)

kCLError is NOT a cert or permission problem

When the hunter page status line shows Location error: ...kCLError..., that is an Apple Core Location error (kCLErrorLocationUnknown). It means permission was granted and Core Location was reached — the self-signed cert does not block geolocation. Core Location simply couldn’t get a fix.

Root Cause

Testing indoors and/or the hotspot having no internet — iOS can’t do an assisted first-fix (A-GPS uses the network). Raw GPS works without internet, but only outdoors after a cold start, and it takes time to lock.

Workarounds

  • Go outside with open sky.
  • Prime Core Location: open Apple Maps while on the hotspot to force a blue-dot fix, then return to Safari and reload — watchPosition then gets an immediate fix.
  • Ensure Location Services On, Safari → While Using, Precise Location On.
  • Wait 30–60 s for a cold GPS lock.

For Agents

Key diagnostic distinction: kCLErrorLocationUnknown (Core Location reached, no fix — fix by going outside / priming with Maps / waiting) vs a permission denial (different fix). If you see kCLError, the cert is not the blocker — do not chase the HTTPS/cert path.

Deployment & Persistence

The Pi auto-restores the hotspot + server on reboot (~15 s), so no manual bring-up is needed after a power cycle.

  • systemd unit: /etc/systemd/system/wifi-hunter.service, enabled.
    • User=root (needs raw iw / privileged scan).
    • Environment=PATH=/usr/sbin:/usr/bin:/sbin:/binrequired so iw (/usr/sbin/iw) is found under systemd’s minimal PATH (unlike interactive sudo, systemd does not inherit secure_path).
    • WorkingDirectory=/home/levander/wifi-hunter, Restart=always.
  • Hotspot autoconnect: nmcli connection modify Hotspot connection.autoconnect yes with connection.autoconnect-priority 100.
  • Verified: after a reboot, both the hotspot and server auto-return in ~15 s.

Quick Reference

  • Home path: /home/levander (user levander)
  • Hotspot SSID: hunter-ap / password huntme123
  • Server: https://10.42.0.1:8443

Ops Gotchas

SSH over eth0 is flaky — keep commands short and self-contained

Managing the Pi over ssh (eth0) is unreliable: long-held ssh commands drop (especially ones with sleeps). Use short, sleep-free, one-shot commands.

pkill self-matches your ssh session

pkill -f "python3 server.py" also matches the ssh command string running it and kills your own session. Use a bracket to avoid the self-match:

pkill -f "[p]ython3 server.py"

Files

FileRole
server.pyHTTPS server (port 8443, self-signed cert), shells out to iw for scans
index.htmlUI — network list, focus/exclude controls, arrow + RSSI track canvas
direction.jsPlane-fit gradient + bearing math (browser)
test_direction.jsNode self-check for the direction algorithm (< 2° error)
README.mdSetup + run instructions

Environment Notes

  • iw binary is at /usr/sbin/iw. Plain sudo python3 server.py finds it because Debian’s sudo secure_path includes /usr/sbin.
  • Pi reachable via ssh raspi over eth0 (192.168.1.200); wlan0 stays dedicated to the hotspot.