Skip to content

Commit 8dadbfd

Browse files
authored
Merge pull request #7 from offworldlab/feature/adsblol-fallback
Add adsb.lol fallback support for dual-source aircraft feed
2 parents 5b7c7c5 + a40c3ac commit 8dadbfd

7 files changed

Lines changed: 212 additions & 3 deletions

File tree

.env.example

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@
44
# Latitude in decimal degrees
55
RECEIVER_LAT=-34.9192
66

7-
# Longitude in decimal degrees
7+
# Longitude in decimal degrees (note: LON not LONG)
88
RECEIVER_LON=138.6027
99

1010
# Altitude in meters
1111
RECEIVER_ALT=110
1212

1313
# adsb.lol Integration
14-
# Enable fetching aircraft from adsb.lol public network
14+
# Enable fallback to adsb.lol when local feed is unavailable
15+
# The system will prefer local data and only use adsb.lol if local feed fails
1516
ADSBLOL_ENABLED=true
1617

1718
# Radius in nautical miles for adsb.lol queries
19+
# Aircraft within this radius of your receiver location will be fetched
1820
ADSBLOL_RADIUS=40

Dockerfile.tar1090

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
FROM ghcr.io/sdr-enthusiasts/docker-tar1090:latest
2+
3+
USER root
4+
5+
RUN apt-get update && apt-get install -y \
6+
curl \
7+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
8+
&& apt-get install -y nodejs \
9+
&& rm -rf /var/lib/apt/lists/*
10+
11+
COPY proxy/server.js /opt/proxy/server.js
12+
13+
COPY docker/lighttpd-proxy.conf /etc/lighttpd/conf-available/90-proxy.conf
14+
RUN lighttpd-enable-mod proxy
15+
16+
COPY docker/entrypoint.sh /opt/entrypoint.sh
17+
RUN chmod +x /opt/entrypoint.sh
18+
19+
ENTRYPOINT ["/opt/entrypoint.sh"]

docker-compose.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ services:
2323
- /var/log:size=32M
2424

2525
tar1090:
26-
image: ghcr.io/sdr-enthusiasts/docker-tar1090:latest
26+
build:
27+
context: .
28+
dockerfile: Dockerfile.tar1090
2729
container_name: tar1090
2830
hostname: tar1090
2931
restart: unless-stopped
@@ -38,6 +40,12 @@ services:
3840
- LONG=${RECEIVER_LON:-0}
3941
- TAR1090_DEFAULTCENTERLAT=${RECEIVER_LAT:-0}
4042
- TAR1090_DEFAULTCENTERLON=${RECEIVER_LON:-0}
43+
- READSB_URL=http://127.0.0.1:80/data/aircraft.json
44+
- ADSBLOL_ENABLED=${ADSBLOL_ENABLED:-false}
45+
- RECEIVER_LAT=${RECEIVER_LAT:-0}
46+
- RECEIVER_LON=${RECEIVER_LON:-0}
47+
- ADSBLOL_RADIUS=${ADSBLOL_RADIUS:-40}
48+
- PROXY_PORT=3000
4149

4250
volumes:
4351
readsb-autogain:

docker/entrypoint.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
set -e
3+
4+
echo "Starting aircraft data proxy service..."
5+
node /opt/proxy/server.js &
6+
PROXY_PID=$!
7+
8+
echo "Waiting for proxy to be ready..."
9+
sleep 2
10+
11+
cleanup() {
12+
echo "Shutting down..."
13+
kill $PROXY_PID 2>/dev/null || true
14+
exit 0
15+
}
16+
17+
trap cleanup SIGTERM SIGINT
18+
19+
echo "Starting tar1090..."
20+
exec /init "$@"

docker/lighttpd-proxy.conf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
server.modules += ( "mod_proxy" )
2+
3+
$HTTP["url"] =~ "^/data/aircraft\.json" {
4+
proxy.server = ( "" => (
5+
( "host" => "127.0.0.1", "port" => 3000 )
6+
))
7+
}

proxy/Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM node:20-alpine
2+
3+
WORKDIR /app
4+
5+
COPY server.js .
6+
7+
EXPOSE 3000
8+
9+
CMD ["node", "server.js"]

proxy/server.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
const http = require('http');
2+
const https = require('https');
3+
4+
const READSB_URL = process.env.READSB_URL || 'http://127.0.0.1:80/data/aircraft.json';
5+
const ADSBLOL_ENABLED = process.env.ADSBLOL_ENABLED === 'true';
6+
const RECEIVER_LAT = parseFloat(process.env.RECEIVER_LAT || '0');
7+
const RECEIVER_LON = parseFloat(process.env.RECEIVER_LON || '0');
8+
const ADSBLOL_RADIUS = parseInt(process.env.ADSBLOL_RADIUS || '40');
9+
const PORT = parseInt(process.env.PROXY_PORT || '3000');
10+
11+
const ADSBLOL_API = `https://api.adsb.lol/v2/lat/${RECEIVER_LAT}/lon/${RECEIVER_LON}/dist/${ADSBLOL_RADIUS}`;
12+
13+
function fetchUrl(url) {
14+
return new Promise((resolve, reject) => {
15+
const client = url.startsWith('https') ? https : http;
16+
const timeout = 5000;
17+
18+
const req = client.get(url, { timeout }, (res) => {
19+
if (res.statusCode !== 200) {
20+
reject(new Error(`HTTP ${res.statusCode}`));
21+
return;
22+
}
23+
24+
let data = '';
25+
res.on('data', chunk => data += chunk);
26+
res.on('end', () => {
27+
try {
28+
resolve(JSON.parse(data));
29+
} catch (e) {
30+
reject(new Error('Invalid JSON'));
31+
}
32+
});
33+
});
34+
35+
req.on('timeout', () => {
36+
req.destroy();
37+
reject(new Error('Request timeout'));
38+
});
39+
40+
req.on('error', reject);
41+
});
42+
}
43+
44+
function convertAdsbLolToReadsb(adsbLolData) {
45+
const aircraft = adsbLolData.ac || [];
46+
47+
return {
48+
now: Date.now() / 1000,
49+
messages: 0,
50+
aircraft: aircraft.map(ac => ({
51+
hex: ac.hex,
52+
flight: ac.flight?.trim() || '',
53+
alt_baro: ac.alt_baro === 'ground' ? 'ground' : ac.alt_baro,
54+
alt_geom: ac.alt_geom,
55+
gs: ac.gs,
56+
track: ac.track,
57+
baro_rate: ac.baro_rate,
58+
squawk: ac.squawk,
59+
emergency: ac.emergency,
60+
category: ac.category,
61+
lat: ac.lat,
62+
lon: ac.lon,
63+
nic: ac.nic,
64+
rc: ac.rc,
65+
seen_pos: ac.seen_pos,
66+
version: ac.version,
67+
nic_baro: ac.nic_baro,
68+
nac_p: ac.nac_p,
69+
nac_v: ac.nac_v,
70+
sil: ac.sil,
71+
sil_type: ac.sil_type,
72+
gva: ac.gva,
73+
sda: ac.sda,
74+
mlat: ac.mlat || [],
75+
tisb: ac.tisb || [],
76+
messages: ac.messages || 0,
77+
seen: ac.seen || 0,
78+
rssi: ac.rssi
79+
}))
80+
};
81+
}
82+
83+
async function getAircraftData() {
84+
try {
85+
console.log('Attempting to fetch from local readsb...');
86+
const localData = await fetchUrl(READSB_URL);
87+
console.log(`✓ Local readsb: ${localData.aircraft?.length || 0} aircraft`);
88+
return { data: localData, source: 'local' };
89+
} catch (error) {
90+
console.log(`✗ Local readsb failed: ${error.message}`);
91+
92+
if (!ADSBLOL_ENABLED) {
93+
console.log('✗ adsb.lol fallback disabled');
94+
throw new Error('Local feed unavailable and fallback disabled');
95+
}
96+
97+
try {
98+
console.log('Attempting fallback to adsb.lol...');
99+
const adsbLolData = await fetchUrl(ADSBLOL_API);
100+
const convertedData = convertAdsbLolToReadsb(adsbLolData);
101+
console.log(`✓ adsb.lol fallback: ${convertedData.aircraft?.length || 0} aircraft`);
102+
return { data: convertedData, source: 'adsb.lol' };
103+
} catch (fallbackError) {
104+
console.log(`✗ adsb.lol fallback failed: ${fallbackError.message}`);
105+
throw new Error('Both local and fallback feeds unavailable');
106+
}
107+
}
108+
}
109+
110+
const server = http.createServer(async (req, res) => {
111+
if (req.url === '/data/aircraft.json') {
112+
try {
113+
const { data, source } = await getAircraftData();
114+
res.writeHead(200, {
115+
'Content-Type': 'application/json',
116+
'Access-Control-Allow-Origin': '*',
117+
'X-Data-Source': source
118+
});
119+
res.end(JSON.stringify(data));
120+
} catch (error) {
121+
res.writeHead(503, { 'Content-Type': 'application/json' });
122+
res.end(JSON.stringify({
123+
error: error.message,
124+
aircraft: [],
125+
now: Date.now() / 1000
126+
}));
127+
}
128+
} else if (req.url === '/health') {
129+
res.writeHead(200, { 'Content-Type': 'application/json' });
130+
res.end(JSON.stringify({ status: 'ok' }));
131+
} else {
132+
res.writeHead(404);
133+
res.end('Not Found');
134+
}
135+
});
136+
137+
server.listen(PORT, () => {
138+
console.log(`Aircraft data proxy listening on port ${PORT}`);
139+
console.log(`Local feed: ${READSB_URL}`);
140+
console.log(`adsb.lol fallback: ${ADSBLOL_ENABLED ? 'enabled' : 'disabled'}`);
141+
if (ADSBLOL_ENABLED) {
142+
console.log(`adsb.lol API: ${ADSBLOL_API}`);
143+
}
144+
});

0 commit comments

Comments
 (0)