Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@
# Latitude in decimal degrees
RECEIVER_LAT=-34.9192

# Longitude in decimal degrees
# Longitude in decimal degrees (note: LON not LONG)
RECEIVER_LON=138.6027

# Altitude in meters
RECEIVER_ALT=110

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

# Radius in nautical miles for adsb.lol queries
# Aircraft within this radius of your receiver location will be fetched
ADSBLOL_RADIUS=40
19 changes: 19 additions & 0 deletions Dockerfile.tar1090
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM ghcr.io/sdr-enthusiasts/docker-tar1090:latest

USER root

RUN apt-get update && apt-get install -y \
curl \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*

COPY proxy/server.js /opt/proxy/server.js

COPY docker/lighttpd-proxy.conf /etc/lighttpd/conf-available/90-proxy.conf
RUN lighttpd-enable-mod proxy

COPY docker/entrypoint.sh /opt/entrypoint.sh
RUN chmod +x /opt/entrypoint.sh

ENTRYPOINT ["/opt/entrypoint.sh"]
10 changes: 9 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ services:
- /var/log:size=32M

tar1090:
image: ghcr.io/sdr-enthusiasts/docker-tar1090:latest
build:
context: .
dockerfile: Dockerfile.tar1090
container_name: tar1090
hostname: tar1090
restart: unless-stopped
Expand All @@ -38,6 +40,12 @@ services:
- LONG=${RECEIVER_LON:-0}
- TAR1090_DEFAULTCENTERLAT=${RECEIVER_LAT:-0}
- TAR1090_DEFAULTCENTERLON=${RECEIVER_LON:-0}
- READSB_URL=http://127.0.0.1:80/data/aircraft.json
- ADSBLOL_ENABLED=${ADSBLOL_ENABLED:-false}
- RECEIVER_LAT=${RECEIVER_LAT:-0}
- RECEIVER_LON=${RECEIVER_LON:-0}
- ADSBLOL_RADIUS=${ADSBLOL_RADIUS:-40}
- PROXY_PORT=3000

volumes:
readsb-autogain:
Expand Down
20 changes: 20 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash
set -e

echo "Starting aircraft data proxy service..."
node /opt/proxy/server.js &
PROXY_PID=$!

echo "Waiting for proxy to be ready..."
sleep 2

cleanup() {
echo "Shutting down..."
kill $PROXY_PID 2>/dev/null || true
exit 0
}

trap cleanup SIGTERM SIGINT

echo "Starting tar1090..."
exec /init "$@"
7 changes: 7 additions & 0 deletions docker/lighttpd-proxy.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
server.modules += ( "mod_proxy" )

$HTTP["url"] =~ "^/data/aircraft\.json" {
proxy.server = ( "" => (
( "host" => "127.0.0.1", "port" => 3000 )
))
}
9 changes: 9 additions & 0 deletions proxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM node:20-alpine

WORKDIR /app

COPY server.js .

EXPOSE 3000

CMD ["node", "server.js"]
144 changes: 144 additions & 0 deletions proxy/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
const http = require('http');
const https = require('https');

const READSB_URL = process.env.READSB_URL || 'http://127.0.0.1:80/data/aircraft.json';
const ADSBLOL_ENABLED = process.env.ADSBLOL_ENABLED === 'true';
const RECEIVER_LAT = parseFloat(process.env.RECEIVER_LAT || '0');
const RECEIVER_LON = parseFloat(process.env.RECEIVER_LON || '0');
const ADSBLOL_RADIUS = parseInt(process.env.ADSBLOL_RADIUS || '40');
const PORT = parseInt(process.env.PROXY_PORT || '3000');

const ADSBLOL_API = `https://api.adsb.lol/v2/lat/${RECEIVER_LAT}/lon/${RECEIVER_LON}/dist/${ADSBLOL_RADIUS}`;

function fetchUrl(url) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;
const timeout = 5000;

const req = client.get(url, { timeout }, (res) => {
if (res.statusCode !== 200) {
reject(new Error(`HTTP ${res.statusCode}`));
return;
}

let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error('Invalid JSON'));
}
});
});

req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});

req.on('error', reject);
});
}

function convertAdsbLolToReadsb(adsbLolData) {
const aircraft = adsbLolData.ac || [];

return {
now: Date.now() / 1000,
messages: 0,
aircraft: aircraft.map(ac => ({
hex: ac.hex,
flight: ac.flight?.trim() || '',
alt_baro: ac.alt_baro === 'ground' ? 'ground' : ac.alt_baro,
alt_geom: ac.alt_geom,
gs: ac.gs,
track: ac.track,
baro_rate: ac.baro_rate,
squawk: ac.squawk,
emergency: ac.emergency,
category: ac.category,
lat: ac.lat,
lon: ac.lon,
nic: ac.nic,
rc: ac.rc,
seen_pos: ac.seen_pos,
version: ac.version,
nic_baro: ac.nic_baro,
nac_p: ac.nac_p,
nac_v: ac.nac_v,
sil: ac.sil,
sil_type: ac.sil_type,
gva: ac.gva,
sda: ac.sda,
mlat: ac.mlat || [],
tisb: ac.tisb || [],
messages: ac.messages || 0,
seen: ac.seen || 0,
rssi: ac.rssi
}))
};
}

async function getAircraftData() {
try {
console.log('Attempting to fetch from local readsb...');
const localData = await fetchUrl(READSB_URL);
console.log(`✓ Local readsb: ${localData.aircraft?.length || 0} aircraft`);
return { data: localData, source: 'local' };
} catch (error) {
console.log(`✗ Local readsb failed: ${error.message}`);

if (!ADSBLOL_ENABLED) {
console.log('✗ adsb.lol fallback disabled');
throw new Error('Local feed unavailable and fallback disabled');
}

try {
console.log('Attempting fallback to adsb.lol...');
const adsbLolData = await fetchUrl(ADSBLOL_API);
const convertedData = convertAdsbLolToReadsb(adsbLolData);
console.log(`✓ adsb.lol fallback: ${convertedData.aircraft?.length || 0} aircraft`);
return { data: convertedData, source: 'adsb.lol' };
} catch (fallbackError) {
console.log(`✗ adsb.lol fallback failed: ${fallbackError.message}`);
throw new Error('Both local and fallback feeds unavailable');
}
}
}

const server = http.createServer(async (req, res) => {
if (req.url === '/data/aircraft.json') {
try {
const { data, source } = await getAircraftData();
res.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'X-Data-Source': source
});
res.end(JSON.stringify(data));
} catch (error) {
res.writeHead(503, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: error.message,
aircraft: [],
now: Date.now() / 1000
}));
}
} else if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
} else {
res.writeHead(404);
res.end('Not Found');
}
});

server.listen(PORT, () => {
console.log(`Aircraft data proxy listening on port ${PORT}`);
console.log(`Local feed: ${READSB_URL}`);
console.log(`adsb.lol fallback: ${ADSBLOL_ENABLED ? 'enabled' : 'disabled'}`);
if (ADSBLOL_ENABLED) {
console.log(`adsb.lol API: ${ADSBLOL_API}`);
}
});