Project the aircraft passing overhead onto your ceiling, in real time - an X-ray through the roof.
🛰️ Get notified when I launch on a crowdfunding platform → skylightceiling.com
A ready-made kit is coming. Join the waitlist for early access & launch pricing.

https://github.com/user-attachments/assets/9256b0eb-cc27-4388-9a4f-0a6c05468304
Skylight decodes ADS-B from a cheap RTL-SDR radio and renders the planes physically flying over you onto a ceiling-pointed projector. A jet you'd hear overhead glides across your ceiling at the same moment - labeled with its airline, type, and where it's headed. Pure-black background so the projector's rectangle disappears and only the aircraft (and stars) are lit.
It also draws the real sky behind the planes - sun, moon, bright stars and constellations, and live satellites including the ISS - all at their true positions for your location and time. Tune everything from your phone.
Reference build is centered on San Francisco International (SFO), but it works anywhere - set your location in the control panel and import your airport's runways by ICAO/IATA code (worldwide, via OurAirports) and you're flying.
/tv.html) with the live feed + radar inset, and a full
debug UI (/tracker.html) with jog pad, target table, and a star-capture
calibration wizard.| Part | Suggested | Notes |
|---|---|---|
| Receiver | RTL-SDR Blog V4 + dipole | The included dipole is plenty - planes are nearly overhead. The V3 and V5 work identically (same RTL2832U); if you're buying now, the V4/V5 are the current models. |
| Compute | Raspberry Pi 5 (8 GB) | Decode + render. Active cooling for 24/7. See minimum specs for lighter setups. |
| Projector | A 1080p projector pointed up | Laser (e.g. Optoma GT2100HDR) gives the deepest blacks, but it's overkill - see the budget tip below. |
| Display link | micro-HDMI → HDMI | The Pi 5 uses micro-HDMI (not mini). |
| Mount | Rotating 1/4-20 stand, pointed up | Lower the stand for a bigger image; tape + a safety tether. |
| Sky camera (optional) | Any VISCA-over-IP PTZ with RTSP (e.g. a 4K NDI conference PTZ) | For the auto-filming tracker. Clamp the base rigidly - fast slews will walk an unclamped mount and ruin the aim calibration. |
💡 Budget tip - you don't need an expensive projector. The pricey laser short-throw is only worth it if you want the image visible in a lit room. If you're happy viewing it in a dim/dark room (the intended vibe), a cheap native-1080p LED projector like the Yaber Buffalo Pro U9 (~$150) works great: - No short-throw needed - from the floor under an ~8 ft ceiling, even a 1.35:1 throw gives a ~5.5 ft image. - Low brightness is fine (even better) - the content is sparse-on-black, so 200–400 lumens in a dark room actually looks deeper. - Just verify it's native 1920×1080 (not "1080p supported"), has a quiet fan, and an HDMI input that shows on power-on.

The build - short-throw projector pointing up, RTL-SDR dipole on the cabinet.
You don't need any of this to try it - see Quick start.
There are two workloads, and they have very different requirements:
maxFps (e.g. 30), trim trailSeconds, and lean on DATA_SOURCE=api so the Pi isn't
also running dump1090. 1 GB is workable but tight; 2 GB+ is the comfortable floor.Rule of thumb: Pi Zero 2 W / 3B = display only, Pi 4 = display + local radio, Pi 5 = everything including the camera tracker.
Runs entirely on your computer against a free public ADS-B API.
pnpm install
DATA_SOURCE=api pnpm dev
http://<your-ip>:5173/control.html)Then set your location from the control panel's Location section - search a
city/airport, tap Current to use the browser's location, or type lat,lon
directly. The default is SFO, so until you change it you'll see San Francisco traffic
(or nothing, if your radius is small). Your airport's runways can be drawn too:
type its ICAO/IATA code into Location → Runways and they're imported automatically.
scripts/install-rtlsdr-fedora.sh # rtl-sdr-blog driver + blacklist DVB-T (Fedora; see script for Debian)
scripts/run-dump1090-local.sh # decode + serve aircraft.json on :8080
DATA_SOURCE=radio pnpm dev
Full walkthrough in pi-setup/README.md: flash + headless
provision the SD card, install the driver + decoder + app, and set up the boot-to-kiosk
display. Once it's running, push updates from your dev machine with:
PI_HOST=skylight.local ./scripts/deploy-to-pi.sh
The camera tracker works out of the box with its classical + motion-tracking vision. For an extra semantic "is it an airplane?" signal (kills cloud locks, nails big overhead planes), download the optional ONNX model on the machine with the camera:
./scripts/fetch-vision-model.sh # YOLOX-Nano, Apache-2.0, ~3.5 MB (not committed)
sudo systemctl restart skylight-tracker
# verify: the tracker state shows vision.net.ready == true
It's fully optional - if the model (or onnxruntime-node) is absent, the tracker runs
classical-only with no errors. On a Pi 5 it adds ~0.8 to load average during a pass;
turn it off with tracker.vision.net.enabled = false or run it less often with
tracker.vision.net.everyNTicks if the Pi runs hot.
If you'd rather drive a projector from a server (no Pi, no cabling to the projector), run the server + display in a container:
docker compose up -d --build
# display: http://<host>:3000/ · phone panel: http://<host>:3000/control
Out of the box it uses the free airplanes.live API, so it runs with no radio.
To use your own ADS-B receiver, set DATA_SOURCE=radio and point AIRCRAFT_JSON_URL
at an existing dump1090 / readsb / PiAware feed on your network (or just change the URL
live from the control panel's Source section):
# compose.yaml
environment:
DATA_SOURCE: radio
AIRCRAFT_JSON_URL: http://192.168.1.50:8080/data/aircraft.json
Config and the route/TLE caches persist in the skylight-data volume. The image is the
server + display only - the optional sky-camera tracker (which wants direct camera +
GPU access) is not containerized. If you reach the server over a custom hostname or a
tunnel rather than a LAN IP / *.local, add it to ALLOWED_HOSTS (see below).
Reaching a decoder in another container: the fetch happens from inside the Skylight container, so
localhostwon't work and a sibling container's name only resolves if both containers share a Docker network. Either use the decoder host's LAN IP (e.g.http://192.168.1.50:8080/data/aircraft.json), or attach Skylight to the decoder's network (see the commentednetworks:block incompose.yaml). The status line in/controlshows why a fetch fails (DNS, refused, timeout).
No planes, using the public API. The view is centered on SFO until you set your
location - open /control and use the Location section (search, Current, or
lat,lon). Then check the status line at the top of /control: it shows the live
source, count, and the exact failure reason if polling is failing. If you're far from
an airport, also widen the Radius slider.
source fetch failed: … in the status line. The reason in parentheses tells you
what's wrong, measured from the server:
- DNS lookup failed (host) - the server can't resolve that hostname. In Docker this
usually means the decoder container isn't on the same Docker network (see the Docker
section above).
- connection refused / host unreachable / timeout - the host resolved but nothing
answered on that port; check the URL's port and that the decoder's web server is up
(curl http://<host>:8080/data/aircraft.json from the machine running Skylight).
- HTTP 429 - the public API is rate-limiting; Skylight now backs off automatically
and recovers on its own. Planes hold position on screen through brief outages.
Pointing at an existing dump1090 / readsb / PiAware feed. Set the Radio URL in
/control → Source to your feed's aircraft.json (dump1090-fa serves it at
http://<host>:8080/data/aircraft.json) and switch the source to radio. No rebuild
needed - it applies on the next poll.
Installer fails with Unsupported architecture: armhf. You're on 32-bit
Raspberry Pi OS; Node.js no longer ships 32-bit ARM builds. Re-flash with
Raspberry Pi OS (64-bit) - every supported Pi (3/4/5, Zero 2 W) can run it.
Runways for my airport. /control → Location → Runways: enter the ICAO or
IATA code (e.g. EGLL, EDDF, SNA). Geometry comes from the public-domain
OurAirports dataset; very small airfields sometimes lack surveyed runway endpoints,
in which case you'll get a clear error.
Clock on the TV dashboard is wrong. It shows the Pi's system time - fix the
timezone with sudo timedatectl set-timezone <Region/City>.
Config (shared/src/config.ts) is the single source of truth,
persisted to server/data/config.json and live-editable from the control panel. Key
fields:
centerLat / centerLon |
Your location - where you're looking up. Editable from the panel's Location section (type a city, airport code, or lat,lon). |
locationName |
Display name for the current location, shown in the control panel. |
locationProfiles |
Saved places (favorite airports). Switch between them from the panel's Location section - tap Save current to store the active spot, then a chip to jump back to it. |
radiusMiles |
How far out to show (default 3 - "what you could realistically see"). |
rotationDeg / mirrorX |
Calibration for the looking-up flip (tune against a real pass). |
theme |
ambient · telemetry · focus. |
| `showStar |
$ claude mcp add skylight \
-- python -m otcore.mcp_server <graph>