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.
ceiling-main.mp4
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 coordinates (and swap the runway data) and you're flying.
- Real-time overhead aircraft from a local RTL-SDR (sub-second), or from a free web API with zero code changes - handy for trying it with no radio.
- Type-aware glyphs in a luminous, swept-wing style: widebodies tower over regional jets, helicopters spin their rotors, turboprops and GA aircraft spin their props.
- Smooth motion - interpolates the ~1 Hz fixes to 60 fps by rendering slightly in the past and tweening between real positions (no teleporting).
- Comet trails, altitude-graded color, and range rings + compass for orientation.
- The airport (runways) drawn at its true position, so you watch departures and arrivals line up with the runway.
- Window to elsewhere - each routed flight shows its destination city, local time there, and miles-to-go, plus a faint great-circle arc toward where it's headed.
- Live sky layer - sun, moon (with phase), bright stars + constellation lines, naked-eye planets, and satellites / ISS computed from TLEs. Scrub time forward/back from your phone, or jump straight to the next ISS pass.
- Phone control panel - every setting (rotation, theme, palette, filters, sky toggles, …) is live-tunable over your LAN and persists across reboots.
- Optional sky camera - point a PTZ camera (VISCA-over-IP + RTSP) at the sky and
Skylight automatically films the planes it's projecting: ADS-B-driven pointing
with latency-compensated lead prediction, a hybrid vision system that locks the plane
to center, and a confidence-gated zoom ladder that punches in as the lock holds.
Includes a TV dashboard (
/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. - Vision that knows a plane from a cloud - the camera tracker fuses three signals: a classical blob detector (distant specks) + a large-object detector (big overhead planes), track-before-detect that picks the target by how it moves through the world like ADS-B predicts (clouds are world-static and lose), and an optional neural airplane detector (YOLOX-Nano ONNX, downloaded at setup) for a semantic "is it an airplane?" confirmation. It also continuously self-calibrates the mount from every locked pass, so the aim re-squares itself over time.
- Appliance-ready - boots straight to a full-screen kiosk on a Raspberry Pi 5 (dual-output: projector + TV dashboard).
| 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:
- Display + decode (the core ceiling piece). This is light. The server is a small
Node process; the rendering is a 2D canvas. A Pi 4 (2 GB) runs it comfortably, and a
Pi 3B / Pi Zero 2 W will work for the display if you keep the canvas modest - cap
maxFps(e.g. 30), trimtrailSeconds, and lean onDATA_SOURCE=apiso the Pi isn't also runningdump1090. 1 GB is workable but tight; 2 GB+ is the comfortable floor. - The optional sky-camera tracker (vision + neural detector). This is the heavy part and is what the Pi 5 (8 GB) recommendation is for - real-time RTSP decode plus ONNX inference. Don't expect the vision tracker to keep up on a Pi 4 or smaller.
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- Display: http://localhost:5173/
- Control panel: http://localhost:5173/control.html (or from your phone:
http://<your-ip>:5173/control.html) - Camera tracker debug UI: http://localhost:5173/tracker.html - runs against a built-in camera simulator, so the whole pointing pipeline (target selection, prediction, zoom, calibration) works with zero hardware.
- TV dashboard: http://localhost:5173/tv.html
Set your location in the control panel area is coming; for now set centerLat /
centerLon in shared/src/config.ts (defaults to SFO).
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 devFull 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.shThe 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 == trueIt'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/controlOut 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.jsonConfig 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).
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. |
showStars / showSun / showMoon / showSatellites / showPlanets |
Sky layer toggles. Planets (Venus, Jupiter, Mars, Saturn, Mercury) are drawn at their true positions, sized by brightness and labelled - so the display stays alive even with no traffic. |
skyTimeOffsetMin |
Scrub the sky clock for testing (0 = live). |
showDestArc / showRouteDetail |
"Window to elsewhere". |
tracker.* |
The whole camera subsystem - driver (sim/visca), camera IP, mount calibration, target selection criteria, prediction/pursuit tuning, zoom + vision behavior. All live-tunable from the tracker debug UI. |
Using it somewhere other than SFO: set your location from the control panel's
Location section (or edit centerLat/centerLon). Stars, sun, moon, and satellites
are computed for your coordinates automatically. The runway overlay is still SFO-specific
geometry - turn off Airport runways if you've moved, or replace it in
web/src/display/airports.ts with your local airport
(coordinates from OurAirports).
Location search uses the free Nominatim (OpenStreetMap) service. Set
GEOCODE_USER_AGENTto identify your deployment if you use it heavily.
| Env | Default | Meaning |
|---|---|---|
DATA_SOURCE |
radio |
radio (dump1090) or api (airplanes.live) |
AIRCRAFT_JSON_URL |
http://localhost:8080/aircraft.json |
dump1090 feed |
SUPPLEMENT_API |
1 |
When on radio, merge the API too (keeps landing aircraft alive) |
PORT / HOST |
3000 / 0.0.0.0 |
HTTP + WebSocket |
ALLOWED_HOSTS |
(empty) | Extra Host/Origin allowlist entries, comma-separated. Wildcards: *.example.com. Loopback, RFC1918 LAN, IPv6 ULA / link-local, and *.local are allowed by default. |
ALLOW_PRIVATE_LAN |
1 |
Set 0 to lock the server to loopback + mDNS only (no LAN phone control) |
GEOCODE_USER_AGENT |
(skylight default) | User-Agent sent to Nominatim for location search |
Skylight binds 0.0.0.0 so the phone control panel works on your home Wi-Fi.
To stop browsers on other origins from talking to the server (e.g. a tab on
evil.com opening a WebSocket to your Pi over DNS rebinding), every request
is rejected unless its Host header (and a WebSocket's Origin header)
matches the allowlist.
The defaults cover the documented topology - localhost, 127.0.0.1,
[::1], *.local, and private LAN ranges (10/8, 192.168/16,
172.16/12, IPv6 ULA + link-local). If you publish Skylight on a public
hostname or a tunnel, add it:
ALLOWED_HOSTS=skylight.mydomain.com,*.trycloudflare.com pnpm devRTL-SDR ──USB──> dump1090-fa ──> aircraft.json (:8080)
│ poll ~1 Hz (+ API supplement)
▼
server/ (Node · Express · ws) :3000
• normalize + enrich (airline/type tables + adsbdb routes)
• proxy satellite TLEs (Celestrak)
• persist config, broadcast over WebSocket
├──────────────┬──────────────┬───────────────┐
▼ ▼ ▼ ▼
Display (/) Control (/control) REST /api/* tracker/ :3001
canvas renderer + phone settings UI • target selection + az/el
sky engine → projector (live, two-way) lead prediction (ECEF)
• velocity pursuit + zoom
• vision: sky-masked blob
detector, lag-compensated
• VISCA-over-IP → PTZ camera
• RTSP → H.264 passthrough
(/video-ws) + MJPEG
• TV dashboard + debug UI
shared/- TypeScript types, config schema, and pure geo/projection/pointing math (ECEF az/el, mount model + calibration solver, alpha-beta trackers, FOV/zoom).server/- polls the radio (primary) and API (supplement), enriches aircraft, proxies TLEs, persists config, and pushes everything over a WebSocket.web/- Vite + React, four pages: the display (<canvas>renderer + celestial engine), the mobile control panel, the TV dashboard, and the tracker debug UI.tracker/- the camera brain: picks a target, predicts where it will be when the command actually bites (fix age + decode latency + motor latency), drives the PTZ with closed-loop velocity pursuit (sigma-delta speed dithering, soft limit guards, dead-reckoned pose), verifies the plane on-frame with a vision detector, and zooms in only while the lock holds. Runs against a simulator with zero hardware; replays recorded sessions deterministically for debugging.
Stack: TypeScript · React · Vite · Express · ws · pnpm workspaces · astronomy-engine · satellite.js.
- ADS-B decode: dump1090-fa · RTL-SDR Blog drivers
- Routes / aircraft enrichment: adsbdb · fallback feed: airplanes.live
- Satellite elements: Celestrak · airport data: OurAirports
MIT - be excellent, point it at the sky.
