mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(sdr): local receivers — browser RTL-SDR, Local ADS-B layer and decoder feeds (#732)
Adds a Local ADS-B layer fed by your own receivers: a browser WebUSB RTL-SDR (with FM listening) and local 1090/978 MHz decoder feeds (LOCAL_RECEIVER_FEEDS, local and private addresses only). Local aircraft draw in magenta with class, silhouettes, 3D models, trails and a position sanity filter; the click card names the band and input that heard them. Builds on Sameh Khamis's SDR prototype and the initial GEV port by Mazeyar Moeini Feizabadi (#134); the decoder-feed design follows the notes on #57. Closes #57. Co-authored-by: Sameh Khamis <[email protected]> Co-authored-by: Mazeyar Moeini Feizabadi <[email protected]>
This commit is contained in:
78 files changed
+12267
-108
No files matched your search
@@ -4104,6 +4104,134 @@ Existing catalog validation, category filters, tuning and voice playback behavio
|
||||
remain unchanged. Audio connects directly to the broadcaster after an explicit
|
||||
play action; the source does not relay or record streams.
|
||||
|
||||
## Local RTL-SDR and Local ADS-B
|
||||
|
||||
The Radio panel holds a Local RTL-SDR card below internet radio. One browser
|
||||
WebUSB session (`src/sdr/controller.js`) drives a USB RTL-SDR in desktop Chrome
|
||||
or Edge on localhost or HTTPS; nothing opens until CONNECT. Demodulation and
|
||||
Mode S decoding run in a worker; FM audio plays through an audio worklet.
|
||||
Local FM and internet radio are one listening surface: starting local FM stops
|
||||
internet-radio playback, and starting internet radio stops local FM. ADS-B
|
||||
reception does not interrupt internet radio.
|
||||
|
||||
- **Modes.** FM (87.5–108 MHz, tune, seek, volume) or ADS-B at 1090 MHz.
|
||||
With a receiver open, selecting ADS-B enables the Local ADS-B layer;
|
||||
selecting FM disables it.
|
||||
Enabling the layer switches the receiver to ADS-B; disabling it leaves the
|
||||
receiver mode unchanged.
|
||||
- **Gain.** AUTO (tuner AGC) or a manual R820T step from 0.0 to 49.6 dB,
|
||||
stored per mode in `gev:sdr:gain:v1`. Defaults: ADS-B 28.0 dB, FM AUTO.
|
||||
Changes apply to the open receiver without reconnecting.
|
||||
- **Receiver stats (ADS-B).** CRC-valid messages per second, aircraft heard,
|
||||
aircraft with a fresh position and the IQ level.
|
||||
- **Devices.** An already-authorized receiver opens without the WebUSB
|
||||
picker. ADS-B prefers a device whose product name matches ADS-B or 1090 (the
|
||||
1090 MHz channel of a dual-channel board); FM avoids ADS-B/UAT channels.
|
||||
A device chosen in the picker is remembered per mode by vendor, product,
|
||||
product name and serial in `gev:sdr:device:v1`. CHANGE DEVICE reopens the
|
||||
picker. LOCATE uses browser location for receiver-relative CPR decoding.
|
||||
|
||||
The Local ADS-B layer (`local-adsb`, `src/layers/localAdsb/`) is off by default
|
||||
and registered as local-only: it never enters share links or stored layer
|
||||
state. Its aircraft get the public Flights treatment in magenta, alongside
|
||||
the public Flights layer. Public Flights renders about one poll interval
|
||||
(~30 s) behind for smooth interpolation while local aircraft are shown as
|
||||
heard, so a local marker usually leads its public twin; both markers are drawn
|
||||
on purpose:
|
||||
|
||||
- **Class.** Each aircraft is classified with `aircraftClass.js` from its
|
||||
ADS-B emitter category (dump1090 strings: `A1` light, `A3` large, `A7`
|
||||
rotorcraft; set A/B/C/D = type code 4/3/2/1) and, once known, its adsbdb
|
||||
ICAO type. Silhouette and per-class scale come from `aircraftIcons.js`
|
||||
(×0.8 on the ground), tinted magenta.
|
||||
- **3D.** The DISPLAY rail's 3D toggle and its proximity/all mode apply: the
|
||||
layer reads the Flights layer's `models3d` params and uses the same ceiling,
|
||||
caps, add/keep radii, visible-first eligibility
|
||||
(`src/data/modelEligibility.js`) and per-class GLB
|
||||
(`src/layers/flights/modelSpec.js`), tinted magenta through the same MIX
|
||||
colour blend Flights uses for military amber. Heights are barometric +
|
||||
geoid N, as Flights renders a contact without a geometric altitude; a
|
||||
grounded model rides the one-shot ground snap and belly offset.
|
||||
- **Motion.** No display delay. The marker is extrapolated forward from the
|
||||
newest fix with `motionModel.js` (arc extrapolation, path-derived course
|
||||
blended over the reported track, rate-limited slew); a new fix is absorbed
|
||||
by a correction that decays over 900 ms. Coasting continues while messages
|
||||
arrive and stops 10 s after the last one (60 s at most).
|
||||
- **Sanity filter.** For airborne positions the browser decoder prefers a
|
||||
fresh even/odd pair, then a frame decoded relative to the aircraft's own
|
||||
position (under 10 minutes old), then the receiver location. Surface
|
||||
positions are stricter: a single surface frame decodes only against the
|
||||
aircraft's own recent fix, and only when that fix is provably within 45 NM
|
||||
(it could not have gone farther since at its speed limit); the receiver
|
||||
location never decodes a lone surface frame and only picks the quadrant of
|
||||
a surface pair. Without an existing suitable fix for that aircraft
|
||||
(airborne or surface, provably within 45 NM), surface acquisition
|
||||
requires an even/odd pair. Every fix
|
||||
must be reachable from the last accepted one at 1.5 × reported ground
|
||||
speed + 50 kt (1,000 kt without a speed, 350 kt for A1/A7/B1/B4) over the
|
||||
elapsed time + 1 s, plus 500 m.
|
||||
Refused fixes are counted (`positionsRejected` in the SDR state; the layer
|
||||
applies the same check to merged feed records and reports the total as
|
||||
`rejectedPositions` in its stats). Three refusals in a row re-anchor.
|
||||
- **Trail.** A selected aircraft draws a magenta trail of the positions the
|
||||
receiver heard (up to 10 minutes / 600 fixes, dropped with the aircraft),
|
||||
with the tracked-flight trail look and a live head segment. No network
|
||||
backfill.
|
||||
- **Enrichment.** Only the selected aircraft (type + route) and aircraft
|
||||
about to render as models (type) are looked up, through the Flights
|
||||
source's cached adsbdb proxy with the same once-per-session, four-in-flight,
|
||||
200 ms drip rules.
|
||||
|
||||
A marker drops once its newest position is 60 s old; an aircraft is forgotten
|
||||
after 60 s without a message. Clicking a marker opens a readout card (ICAO,
|
||||
callsign, class and category, adsbdb operator/type and a plausible route when
|
||||
known, altitude, ground speed, track, vertical rate, position age, message
|
||||
count, "Heard by your receiver"); markers are not camera-followed. Positioned
|
||||
aircraft join detection boxes, labelled only with a decoded callsign.
|
||||
|
||||
Receivers publish one record shape from `src/sources/adsbRecords.js`
|
||||
(`icao`, `callsign`, `category`, `onGround`, `lat`, `lon`, `altitudeFt`,
|
||||
`groundSpeedKt`, `trackDeg`, `verticalRateFpm`, `lastPositionAt`,
|
||||
`lastMessageAt`, `messageCount`, `rssiDbfs`, `band`, `source`). `band` is `1090` or `978`; `source` is
|
||||
`webusb` (browser SDR) or `feed` (decoder feed).
|
||||
`normalizeDump1090Aircraft(json, nowMs, { band })` maps a dump1090/readsb/
|
||||
skyaware978 `aircraft.json` document into the same records. Voice
|
||||
`set_layer_visibility` accepts `local-adsb` ("local ADS-B", "my receiver",
|
||||
"my antenna").
|
||||
|
||||
**Decoder feeds.** The layer's second input is the server route
|
||||
`GET /api/local-receivers/aircraft`
|
||||
(`server/providers/local-receivers.js`), configured only by the
|
||||
`LOCAL_RECEIVER_FEEDS` environment value (`band=url`, comma-separated, e.g.
|
||||
`1090=http://localhost:8080/data/aircraft.json`). Every host must pass
|
||||
`parseTapAddress` (loopback, RFC1918, `localhost`, `*.local`), the scheme must
|
||||
be http(s) and the path must end in `aircraft.json`; an invalid entry is logged
|
||||
at startup, reported `invalid` and never fetched. The route reads all feeds in
|
||||
parallel (2 s timeout, redirects refused, 2 MB cap, single-flight with a 1 s
|
||||
cache) and returns `{ configured, generatedAt, feeds: [{ band, label, status,
|
||||
aircraft, ageMs }], records }`, where a feed is `live`, `stale` (its own `now`
|
||||
is over 10 s old), `unreachable` or `invalid`. Labels name the band only;
|
||||
addresses and upstream error text stay on the server. Unconfigured, it returns
|
||||
`{ configured: false, feeds: [], records: [] }` without fetching.
|
||||
|
||||
The browser session (`src/layers/localAdsb/feeds.js`) polls the route every
|
||||
second only while the layer is enabled and only while the route reports
|
||||
`configured`; the Local RTL-SDR card makes one probe request at startup to show
|
||||
its read-only "Decoder feeds: 1090 live · 978 live" line. Feed records are
|
||||
rebased to the browser clock and kept up to 60 s after their last message.
|
||||
`mergeLocalAdsbRecords` merges both inputs by ICAO, keeping the newest position
|
||||
(tie: newest message) and the bands/sources heard in the last 60 s; the card's
|
||||
receiver line names them ("Heard by your receiver · 1090 MHz + 978 MHz UAT ·
|
||||
browser SDR + decoder feed"). Aircraft heard only on 978 MHz UAT draw with a
|
||||
thin light-magenta ring. With feeds configured the row status reads "2 feeds
|
||||
live · 14 heard" when every feed is live. While any input is producing (the
|
||||
browser receiver streaming, or a feed live) the row stays nominal and names the
|
||||
other feeds as a trailing note ("3 heard · USB 5.8 msg/s · feed 1090 stale"),
|
||||
since a decoder stopped so the browser can take the dongle is not a fault. With
|
||||
nothing producing, every readable feed stale reads STALE and anything else is
|
||||
an error. Without feeds it keeps the WebUSB statuses. See
|
||||
`docs/LOCAL-RECEIVERS.md`.
|
||||
|
||||
## Bundled geography and submarine cable components
|
||||
|
||||
Submarine cables use separate source, geometry, rendering, interaction and
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Local receivers
|
||||
|
||||
The Local ADS-B layer draws aircraft heard by your own receivers. It has two
|
||||
inputs, and it can use both at once.
|
||||
|
||||
| Input | What it is | Bands |
|
||||
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
|
||||
| Browser SDR | A USB RTL-SDR opened by the browser through WebUSB (Radio panel, Local RTL-SDR card). Decoding runs in the page. | 1090 MHz |
|
||||
| Decoder feed | The `aircraft.json` served by a decoder you run: dump1090-fa, readsb or tar1090 (1090 MHz), or dump978-fa + skyaware978 (978 MHz). | 1090 MHz, 978 MHz UAT |
|
||||
|
||||
Aircraft from both inputs are merged by ICAO address. The newest position
|
||||
wins, and the click card names every band and source that heard the aircraft
|
||||
in the last 60 s. Aircraft heard only on 978 MHz UAT draw with a thin ring.
|
||||
|
||||
Local aircraft usually lead their public twins. The public Flights layer
|
||||
renders positions about one poll interval (~30 s) behind so it can interpolate
|
||||
smoothly between fixes, while local receiver aircraft are shown as heard. An
|
||||
aircraft in both layers therefore draws twice, the magenta local marker ahead
|
||||
of the public one; both markers are drawn on purpose.
|
||||
|
||||
## Which to use
|
||||
|
||||
- **One plain RTL-SDR dongle, desktop Chrome or Edge:** use the browser SDR.
|
||||
Nothing else to install.
|
||||
- **A dual-band board, a Raspberry Pi feeder, or a dongle already used by a
|
||||
decoder:** use a decoder feed. A USB dongle can be opened by only one program
|
||||
at a time, so a dongle that dump1090 or dump978 owns cannot also be opened by
|
||||
the browser.
|
||||
|
||||
## Configuring feeds
|
||||
|
||||
Feeds are set in the server environment only (`.env`), never in the browser:
|
||||
|
||||
```sh
|
||||
LOCAL_RECEIVER_FEEDS=1090=http://localhost:8080/data/aircraft.json,978=http://localhost:8978/data/aircraft.json
|
||||
```
|
||||
|
||||
Each entry is `band=url`, where `band` is `1090` or `978`. The URL must:
|
||||
|
||||
- use `http` or `https`;
|
||||
- point at loopback, a private (RFC1918) address, `localhost` or a `*.local`
|
||||
name. Public addresses, link-local addresses (including `169.254.169.254`),
|
||||
IPv6 and other host names are refused;
|
||||
- have a path ending in `aircraft.json`, with no credentials, query or
|
||||
fragment.
|
||||
|
||||
An entry that breaks a rule is logged when the server starts, shown as
|
||||
`invalid` and never fetched. Restart the server after changing the value.
|
||||
|
||||
The server reads every feed about once a second while the Local ADS-B layer is
|
||||
on. A feed is `live`, `stale` (its own `now` is more than 10 s old, usually a
|
||||
stopped decoder), `unreachable` or `invalid`. The Layers panel row and the
|
||||
Local RTL-SDR card show these states.
|
||||
|
||||
## Example: dump1090-fa and dump978-fa on macOS or Linux
|
||||
|
||||
Write each decoder's JSON to a directory, then serve the directory on
|
||||
localhost. Each command keeps running, so start each in its own terminal:
|
||||
|
||||
```sh
|
||||
# 1090 MHz
|
||||
dump1090-fa --device-index 0 --write-json /tmp/adsb1090
|
||||
python3 -m http.server 8080 --bind 127.0.0.1 --directory /tmp/adsb1090
|
||||
|
||||
# 978 MHz UAT: dump978-fa decodes, skyaware978 writes aircraft.json
|
||||
dump978-fa --sdr driver=rtlsdr,rtl=1 --raw-port 30978
|
||||
skyaware978 --connect localhost:30978 --json-dir /tmp/adsb978
|
||||
python3 -m http.server 8978 --bind 127.0.0.1 --directory /tmp/adsb978
|
||||
```
|
||||
|
||||
```sh
|
||||
LOCAL_RECEIVER_FEEDS=1090=http://localhost:8080/aircraft.json,978=http://localhost:8978/aircraft.json
|
||||
```
|
||||
|
||||
A board with two identical-serial channels, such as the Nooelec FlyCatcher,
|
||||
cannot be told apart by serial number. Select each channel by index:
|
||||
`--device-index <n>` for dump1090-fa and `--sdr driver=rtlsdr,rtl=<n>` for
|
||||
dump978-fa. `rtl_test` lists the indexes.
|
||||
|
||||
A tar1090 or readsb install on a Raspberry Pi already serves
|
||||
`http://<pi-address>/tar1090/data/aircraft.json`. Use the Pi's private address
|
||||
or its `.local` name.
|
||||
Reference in new issue
Block a user