---
stepsCompleted: [step-01-init, step-02-discovery, step-02b-vision, step-02c-executive-summary, step-03-success, step-04-journeys, step-05-domain, step-06-innovation, step-07-project-type, step-08-scoping, step-09-functional, step-10-nonfunctional, step-11-polish, step-12-complete]
inputDocuments: [README.md, CLAUDE.md]
workflowType: 'prd'
classification:
  projectType: iot_embedded
  domain: aerospace
  complexity: medium
  projectContext: greenfield
---

# Product Requirements Document - planeMapper

**Author:** Matt Edholm
**Date:** 2026-04-22

## Executive Summary

planeMapper is a permanently offline, purpose-built ADS-B radar display in a picture-frame form factor. It receives live aircraft positions via an RTL-SDR dongle, renders them on an OpenStreetMap base map centred on the user's home location, and pushes the result to a Waveshare 7.3" 6-colour e-ink display every 60 seconds. Designed to be wall-mounted or desk-standing — always on, always current, zero interaction required after initial setup.

**Target user:** Aviation enthusiasts, pilots, and spotters who want persistent ambient awareness of local air traffic without a phone, tablet, or cloud service.

## What Makes This Special

planeMapper is an instrument, not an app. Every competing solution requires a screen to be unlocked and an app opened. planeMapper is simply *there* — glanceable, ambient, contextual. The e-ink display draws no power between refreshes and reads like a framed instrument, not a consumer screen.

The defining product principle is permanent offline operation. WiFi is used only during initial provisioning to download map tiles, then killed at the radio level. No cloud dependency, no subscription, no data egress — ever. This is an architectural decision elevated to a product identity.

Setup is via a captive portal hotspot broadcast by the device on first boot. The user enters their home address or ICAO code, home WiFi credentials, and coverage radius. The device provisions itself, downloads and validates tiles, kills the WiFi radio, and enters operational mode. A recessed GPIO button on the rear housing triggers reset; an LED provides immediate confirmation.

## Success Criteria

### User Success

- Aircraft overhead or nearby are identifiable at a glance
- Callsign, altitude, and aircraft type are readable at normal desk/wall viewing distance
- Position trail conveys direction of travel without requiring interpretation
- Colour coding and type icons are immediately understood without reference

### Technical Success

- Display refreshes every 60 seconds without failure or intervention
- Device returns to operational state after power cycling without reconfiguration
- Stale data displayed gracefully on dump1090 decode failure — no crashes, no blank display
- Setup completes successfully from a fresh SD card flash
- WiFi radio confirmed off after provisioning

### Measurable Outcomes

- 60s refresh cycle sustained without manual intervention
- Aircraft visible on display within 2 minutes of first boot post-setup
- 72-hour continuous operation without restart

## Product Scope

### MVP (Phase 1)

- Captive portal setup: ICAO code or address/postcode input, WiFi credentials, coverage radius
- ICAO lookup via bundled OurAirports database; address geocoding via Nominatim
- OSM tile download, cache size validation, cache completeness validation, WiFi radio kill via `rfkill`
- Reset button (3s hold) + LED immediate feedback
- 60s refresh loop, resuming automatically after power cycle
- Startup screen displayed during boot before first radar render
- Live aircraft: heading arrow, callsign, altitude label
- 5-dot position trail (oldest dot smallest)
- Altitude colour bands (6 colours across altitude ranges)
- Aircraft type icons: GA/light, commercial/large, helicopter, private jet via ADS-B category + callsign pattern; altitude-based fallback when type unknown (GA <10,000ft, private jet 10,000–30,000ft, airliner >30,000ft)
- MLAT positions visually distinguished from directly received positions
- Stale data detection, retention, and visual indicator on decode failure
- Airspace circular boundaries, outline only (OpenAIP data)
- Home location marked on map

### Growth (Phase 2)

- Own squawk code highlighting
- Aircraft size coding (heavy/medium/light)
- Airspace colour fills

### Vision (Phase 3)

- Packaged enclosure/frame design for gifting or small-batch sale

## User Journeys

### Journey 1 — First Setup (Success Path)

Matt flashes the SD card and powers on the device. His phone picks up **planeMapper-setup**. He connects; the captive portal opens automatically. He types his postcode, hits search — the device resolves it to coordinates and confirms the location. He sets radius to 100nm, enters his home WiFi credentials, hits **Confirm**. The browser shows *"Downloading map data — this takes a few minutes. Do not power off."* The LED pulses. Minutes later the e-ink flickers and settles on a live radar view. Aircraft are visible. Done.

*Capabilities: captive portal, ICAO/address resolution, WiFi credential handling, tile provisioning, cache validation, LED feedback, first render.*

### Journey 2 — Daily Use (Happy Path)

It's a Tuesday afternoon. Matt hears a low rumble and glances at the display. A helicopter is northeast of home at 1,500ft, tracking southwest — the dot trail confirms it's been moving for a few minutes. Below it, a commercial flight at FL280 crossing his coverage area with a BA callsign. He goes back to work. The display asked nothing of him.

*Capabilities: 60s refresh, heading arrows, dot trail, altitude colour bands, callsign labels, aircraft type icons.*

### Journey 3 — Decode Gap / Stale Data (Edge Case)

dump1090 has a bad few minutes — the dongle got bumped. The display refreshes but positions haven't updated. Aircraft from the last good decode remain visible but visually dimmed. Matt notices, reseats the dongle. Next refresh cycle, positions update normally. No crash, no blank screen, no intervention required.

*Capabilities: stale data detection, visual stale indicator, graceful degradation, recovery on next good decode.*

### Journey 4 — Reconfiguration / Reset

Matt moves house. He holds the reset button for three seconds. The LED lights immediately. The e-ink refreshes to the setup screen. He connects his phone and goes through setup with the new address. Same flow as Journey 1.

*Capabilities: GPIO button hold detection, LED immediate feedback, config wipe, return to captive portal state.*

### Journey Requirements Summary

| Capability | Journeys |
|---|---|
| Captive portal + location resolution | 1, 4 |
| Tile provisioning + cache validation + WiFi kill | 1 |
| LED feedback | 1, 4 |
| Live aircraft rendering (arrow, label, trail, colour, type) | 2 |
| 60s refresh loop | 2 |
| Stale data handling | 3 |
| Reset button + config wipe | 4 |

## Domain-Specific Requirements

### ADS-B Data

- dump1090 data is best-effort — callsign, category, and altitude may be absent. The renderer handles missing fields gracefully without crashing
- ADS-B positions are self-reported — no ground-truth validation. Phantom or misplaced positions are possible; the display makes no attempt to filter or correct them
- MLAT positions are estimated, not directly received, and may be less accurate. Visually distinguished when the MLAT flag is present in the dump1090 feed

### Airspace Data

- OpenAIP provides published airspace boundaries (GeoJSON/OpenAir) representing published structures, not real-time restrictions. Purely informational.

### Compliance

None. planeMapper is a passive receiver display. It receives and renders publicly broadcast radio signals. No licensing, certification, or data handling obligations apply.

## Innovation & Novel Patterns

### Permanently Offline IoT Device

planeMapper inverts the dominant IoT trend. Consumer IoT devices move toward cloud dependency; planeMapper eliminates WiFi at the radio level post-provisioning. No cloud, no subscription, no data egress — ever. This is an architectural decision that defines the product identity.

### E-Ink as Ambient Instrument

E-ink is chosen for aesthetics, not power savings. It looks like a framed instrument, not a consumer screen. The display constraint becomes the product's most distinctive quality.

### Market Position

No direct competitor exists in this form. ADS-B displays require either a connected device (apps) or expensive dedicated avionics. The permanently offline picture-frame niche is unoccupied.

### Primary Risk

The tile cache is the single dependency for the offline principle. Mitigation: validate cache completeness and size at end of provisioning before killing the WiFi radio. On failure, remain in provisioning state and prompt retry.

## IoT/Embedded Specific Requirements

### Hardware

| Component | Spec |
|---|---|
| SBC | Raspberry Pi Zero 2W |
| SDR | Nooelec NESDR Smart v5 (SMA, 0.5PPM TCXO) |
| Antenna | FlightAware 1090MHz, 5.5dBi |
| Display | Waveshare 7.3" 6-colour e-ink HAT (Spectra 6, 800×480, SPI) |
| Reset | Momentary push button, GPIO input, recessed on rear housing |
| Feedback | Single LED, GPIO output, rear housing adjacent to button |
| USB | Micro-USB OTG adapter required — RTL-SDR occupies the Zero 2W's single USB port |

### Device State Machine

| State | WiFi | Description |
|---|---|---|
| Provisioning | AP mode (`planeMapper-setup`) | First boot or post-reset; captive portal active |
| Provisioning (downloading) | Client mode | Joined home WiFi; downloading and validating tiles |
| Operational | Off (`rfkill`) | Normal radar display; no network access |
| Reset triggered | Re-enabled | Returns to Provisioning state |

### Power Profile

- Mains powered — no battery
- Pi Zero 2W: ~500mA at 5V idle
- E-ink: power draw only during ~30s refresh cycle, zero between refreshes

### Security Model

- WiFi off in operational state — network attack surface is zero
- Captive portal during provisioning is open, local-only, and short-lived
- Config file (home coordinates, WiFi credentials) stored plaintext on SD card — acceptable for personal device

### Update Mechanism

None. SD card reflash is the update path if needed.

## Functional Requirements

### Device Setup & Provisioning

- FR1: The device broadcasts a WiFi hotspot on first boot and after reset
- FR2: The user can connect to the device hotspot and be served a setup interface automatically (captive portal)
- FR3: The user can enter a location as an ICAO code or address/postcode
- FR4: The device resolves an ICAO code to coordinates using a bundled airport database
- FR5: The device resolves an address or postcode to coordinates using a geocoding service
- FR6: The device displays the resolved location for user confirmation before proceeding
- FR7: The user can set a coverage radius
- FR8: The user can enter home WiFi credentials during setup
- FR9: The device connects to the user's home WiFi and downloads and caches map tiles for the configured area
- FR9a: After tile download, the device validates cache completeness and size before killing the WiFi radio; on failure, the device remains in provisioning state and prompts retry
- FR10: The device kills the WiFi radio after successful provisioning
- FR11: The setup interface confirms provisioning status to the user before the WiFi hotspot is dropped

### Reset & Recovery

- FR12: The user can trigger a device reset by holding the reset button for 3 seconds
- FR13: The device provides immediate visual feedback via LED when a reset hold is detected
- FR14: A confirmed reset wipes device configuration and returns to provisioning state
- FR15: The device displays a setup screen on the e-ink display after reset

### Map Display

- FR16: The device renders an OpenStreetMap base map centred on the configured home location
- FR17: The map covers the configured coverage radius
- FR18: The home location is marked as a distinct point on the map
- FR19: Airspace circular boundaries are rendered as outlines on the map (OpenAIP data)

### Aircraft Display

- FR20: The device fetches live aircraft data from the dump1090 JSON feed
- FR21: Each aircraft is rendered at its current position with a heading arrow aligned to direction of travel
- FR22: Each aircraft displays its callsign and altitude as a label
- FR23: Each aircraft is colour-coded by altitude band
- FR24: Each aircraft is rendered with a type-specific icon determined from ADS-B category data or callsign pattern matching (GA/light, commercial/large, helicopter, private jet)
- FR24a: When aircraft type cannot be determined, icon is assigned by altitude — GA below 10,000ft, private jet 10,000–30,000ft, airliner above 30,000ft
- FR25: Each aircraft displays a trail of up to 5 previous positions as dots, oldest dot smallest
- FR26: Aircraft transmitted via MLAT are visually distinguished from directly received aircraft

### Stale Data Handling

- FR27: The device detects when the dump1090 feed has not produced a fresh decode
- FR28: Aircraft from the last successful decode are retained on display and visually marked as stale
- FR29: Aircraft positions are restored to normal display state when fresh decode data is received

### Refresh Loop & Boot

- FR30: The display refreshes on a 60-second cycle
- FR31: The device continues the refresh loop indefinitely without manual intervention
- FR32: The device resumes the refresh loop automatically after power cycling
- FR33: The device displays a defined startup screen during boot, before the first radar render is complete

## Non-Functional Requirements

### Performance

- Full radar render must complete within 45 seconds on Pi Zero 2W hardware
- Base map tile layer is pre-composited and cached in memory between refresh cycles — only the aircraft overlay is re-rendered each cycle
- dump1090 JSON fetch must complete within 5 seconds; timeout triggers stale data path
- E-ink SPI transfer initiates only after render pipeline is complete

### Reliability

- Refresh loop must sustain 72+ hours of continuous operation without restart or intervention
- Device must recover to operational state within 5 minutes of unclean power loss, without manual intervention
- dump1090 decode failure must not crash the refresh loop

### Storage

- OSM tile cache must not exceed 2GB for any supported coverage radius, ensuring viability on a 16GB SD card alongside OS and software
- Cache size validated during provisioning before WiFi radio is killed

### Integration

- dump1090 JSON feed: `http://localhost:8080/data/aircraft.json` — local, no authentication
- Nominatim geocoding API: single call during provisioning; internet required at that point only
- OurAirports database: bundled with software, no runtime dependency
- OpenAIP airspace data: fetched and cached during provisioning alongside OSM tiles

### Security

- WiFi radio off in operational state — network attack surface is zero
- No external network calls in operational state
- Config stored plaintext on SD card — acceptable for personal single-user device
