Implementation Roadmap: Edge Node Deployment
This guide provides step-by-step instructions for building a flight tracking and alert system, from hardware procurement through rapid response network integration.
System Overview
Architecture Summary
[Edge Node] [Central Server] [Alert Recipients]
Raspberry Pi → Cloud VPS → Signal Groups
RTL-SDR PostgreSQL Telegram Channels
tar1090 Python Workers Rapid Response
Kafka/Redis
Component Requirements
| Component | Minimum | Recommended |
|---|---|---|
| Edge nodes | 1 | 3+ (for MLAT) |
| Central server | 1 vCPU, 1GB RAM | 2 vCPU, 4GB RAM |
| Database | SQLite | PostgreSQL |
| Message broker | Redis | Apache Kafka |
Phase 1: Hardware Procurement
Bill of Materials (Edge Node)
| Component | Specification | Est. Cost |
|---|---|---|
| Compute | Raspberry Pi 4 (2GB+) | $45-55 |
| SDR receiver | FlightAware Pro Stick Plus | $35-45 |
| Antenna | 1090 MHz collinear | $40-60 |
| Coax cable | LMR-400, 25ft | $30-40 |
| Power supply | 5V 3A USB-C | $10-15 |
| MicroSD | 32GB+ Class 10 | $10-15 |
| Case | Weatherproof enclosure | $15-25 |
| Cooling | Heatsinks/fan | $10-15 |
Total: $195-270 per edge node
Component Selection Notes
| Component | Recommendation |
|---|---|
| Receiver | Pro Stick Plus has integrated 1090 MHz filter |
| Antenna | Higher = better; minimize coax length |
| Compute | Pi 4 for reliability; Zero 2 W for low power |
| Storage | Use quality MicroSD; enable log rotation |
Phase 2: Base Station Software
Operating System Setup
# Flash Raspberry Pi OS Lite to MicroSD
# Boot and connect via SSH
# Update system
sudo apt update && sudo apt upgrade -y
# Install dependencies
sudo apt install -y git rtl-sdr librtlsdr-dev \
build-essential pkg-config libncurses5-dev \
lighttpd nginx python3 python3-pip
Install readsb (ADS-B Decoder)
# Clone and build readsb
git clone https://github.com/wiedehopf/readsb.git
cd readsb
make -j4
sudo make install
# Create service user
sudo useradd -r -s /bin/false readsb
# Create systemd service
sudo tee /etc/systemd/system/readsb.service << 'EOF'
[Unit]
Description=readsb ADS-B receiver
After=network.target
[Service]
Type=simple
User=readsb
ExecStart=/usr/local/bin/readsb \
--device-type rtlsdr \
--gain -10 \
--ppm 0 \
--net \
--net-ro-size 500 \
--net-ro-interval 1 \
--net-buffer 5 \
--net-connector localhost,30004,beast_reduce_out \
--lat YOUR_LAT \
--lon YOUR_LON
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable readsb
sudo systemctl start readsb
Install tar1090 (Web Interface)
# Clone tar1090
cd /opt
sudo git clone https://github.com/wiedehopf/tar1090.git
# Run installation script
cd tar1090
sudo bash install.sh
# Configure Nginx
sudo tee /etc/nginx/sites-available/tar1090 << 'EOF'
server {
listen 80;
root /run/tar1090;
index index.html;
location /data/ {
alias /run/tar1090/;
}
location / {
try_files $uri $uri/ =404;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/tar1090 /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Verify Installation
# Check services
sudo systemctl status readsb
sudo systemctl status tar1090
# Test local JSON endpoint
curl http://localhost/tar1090/data/aircraft.json | jq '.aircraft | length'
# Check receiver stats
cat /run/readsb/stats.json | jq '.total.messages'
Phase 3: Watchlist Integration
Create Watchlist Database
# watchlist.py
import json
import sqlite3
def init_database():
"""Initialize watchlist database"""
conn = sqlite3.connect('watchlist.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS aircraft (
hex TEXT PRIMARY KEY,
n_number TEXT,
operator TEXT,
aircraft_type TEXT,
confidence TEXT,
last_verified DATE,
notes TEXT
)
''')
conn.commit()
return conn
def load_watchlist(filename):
"""Load watchlist from JSON file"""
with open(filename, 'r') as f:
data = json.load(f)
conn = init_database()
cursor = conn.cursor()
for aircraft in data.get('watchlist', []):
cursor.execute('''
INSERT OR REPLACE INTO aircraft
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (
aircraft['hex'].upper(),
aircraft.get('n_number'),
aircraft.get('operator'),
aircraft.get('aircraft_type'),
aircraft.get('confidence'),
aircraft.get('last_verified'),
aircraft.get('notes')
))
conn.commit()
print(f"Loaded {len(data.get('watchlist', []))} aircraft")
Sample Watchlist JSON
{
"watchlist": [
{
"hex": "A12345",
"n_number": "N801XT",
"operator": "GlobalX Airlines",
"aircraft_type": "A320-200",
"confidence": "high",
"last_verified": "2026-03-15",
"notes": "Primary international removal aircraft"
}
]
}
Phase 4: Geofencing Configuration
Define Geofences
# geofences.py
GEOFENCES = [
{
"name": "Alexandria Staging (AEX)",
"lat": 31.3274,
"lon": -92.5499,
"radius_miles": 30,
"type": "staging_hub",
"priority": "high"
},
{
"name": "Mesa Gateway (IWA)",
"lat": 33.3078,
"lon": -111.6556,
"radius_miles": 30,
"type": "staging_hub",
"priority": "high"
},
{
"name": "Brownsville (BRO)",
"lat": 25.9069,
"lon": -97.4258,
"radius_miles": 30,
"type": "staging_hub",
"priority": "high"
},
{
"name": "San Antonio (SAT)",
"lat": 29.5337,
"lon": -98.4698,
"radius_miles": 30,
"type": "staging_hub",
"priority": "medium"
},
{
"name": "Miami (MIA)",
"lat": 25.7959,
"lon": -80.2870,
"radius_miles": 30,
"type": "staging_hub",
"priority": "medium"
}
]
Haversine Implementation
from math import radians, sin, cos, sqrt, atan2
def haversine(lat1, lon1, lat2, lon2):
"""Calculate great-circle distance in miles"""
R = 3959 # Earth radius in miles
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
c = 2 * atan2(sqrt(a), sqrt(1-a))
return R * c
def check_geofences(lat, lon):
"""Check if position is within any geofence"""
for fence in GEOFENCES:
distance = haversine(lat, lon, fence['lat'], fence['lon'])
if distance <= fence['radius_miles']:
return {
'match': True,
'geofence': fence['name'],
'distance': round(distance, 1),
'priority': fence['priority']
}
return {'match': False}
Phase 5: Alert Service
Main Monitoring Script
#!/usr/bin/env python3
# monitor.py
import requests
import sqlite3
import time
import json
from datetime import datetime
from geofences import GEOFENCES, haversine, check_geofences
# Configuration
LOCAL_URL = "http://localhost/tar1090/data/aircraft.json"
POLL_INTERVAL = 5
DB_PATH = "watchlist.db"
# State tracking
aircraft_history = {}
alerts_sent = {}
ALERT_COOLDOWN = 300 # 5 minutes
def load_watchlist():
"""Load hex codes from database"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT hex FROM aircraft")
return set(row[0] for row in cursor.fetchall())
def poll_aircraft():
"""Poll local receiver for aircraft"""
try:
response = requests.get(LOCAL_URL, timeout=10)
return response.json().get('aircraft', [])
except Exception as e:
print(f"Poll error: {e}")
return []
def should_alert(hex_code, aircraft, geofence_result):
"""Determine if alert should be sent"""
# Check cooldown
if hex_code in alerts_sent:
elapsed = (datetime.utcnow() - alerts_sent[hex_code]).seconds
if elapsed < ALERT_COOLDOWN:
return False
# Must be in geofence
if not geofence_result['match']:
return False
# Check altitude
altitude = aircraft.get('alt_baro', 40000)
if altitude > 10000:
return False
# Check descent
baro_rate = aircraft.get('baro_rate', 0)
if baro_rate >= 0:
return False
return True
def send_alert(hex_code, aircraft, geofence_result):
"""Send alert via configured channels"""
callsign = aircraft.get('flight', 'N/A').strip()
altitude = aircraft.get('alt_baro', 'N/A')
tracking_url = f"https://globe.adsbexchange.com/?icao={hex_code.lower()}"
message = f"""
⚠️ FLIGHT ALERT
Aircraft: {hex_code}
Call Sign: {callsign}
Altitude: {altitude} ft
Location: {geofence_result['geofence']}
Distance: {geofence_result['distance']} mi
Track: {tracking_url}
"""
# Send via your configured channel (Signal, Telegram, etc.)
print(message)
# Update cooldown
alerts_sent[hex_code] = datetime.utcnow()
def main():
"""Main monitoring loop"""
watchlist = load_watchlist()
print(f"Loaded {len(watchlist)} aircraft to watchlist")
while True:
aircraft_list = poll_aircraft()
for aircraft in aircraft_list:
hex_code = aircraft.get('hex', '').upper()
# Check watchlist
if hex_code not in watchlist:
continue
# Get position
lat = aircraft.get('lat')
lon = aircraft.get('lon')
if lat is None or lon is None:
continue
# Check geofences
geofence_result = check_geofences(lat, lon)
# Determine if alert needed
if should_alert(hex_code, aircraft, geofence_result):
send_alert(hex_code, aircraft, geofence_result)
# Update history
if hex_code not in aircraft_history:
aircraft_history[hex_code] = []
aircraft_history[hex_code].append({
'timestamp': datetime.utcnow(),
'lat': lat,
'lon': lon,
'alt_baro': aircraft.get('alt_baro'),
'baro_rate': aircraft.get('baro_rate')
})
# Limit history size
aircraft_history[hex_code] = aircraft_history[hex_code][-20:]
time.sleep(POLL_INTERVAL)
if __name__ == "__main__":
main()
Systemd Service
sudo tee /etc/systemd/system/flight-monitor.service << 'EOF'
[Unit]
Description=Flight Tracking Monitor
After=network.target readsb.service
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/flight-tracking
ExecStart=/usr/bin/python3 /home/pi/flight-tracking/monitor.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable flight-monitor
sudo systemctl start flight-monitor
Phase 6: Signal Integration
Install signal-cli
# Download signal-cli
wget https://github.com/AsamK/signal-cli/releases/download/v0.12.0/signal-cli-0.12.0.tar.gz
tar xf signal-cli-0.12.0.tar.gz
sudo mv signal-cli-0.12.0 /opt/signal-cli
# Link binary
sudo ln -s /opt/signal-cli/bin/signal-cli /usr/local/bin/signal-cli
# Register phone number
signal-cli -u +1XXXXXXXXXX register
signal-cli -u +1XXXXXXXXXX verify CODE
Signal Alert Function
import subprocess
SIGNAL_CLI = "/usr/local/bin/signal-cli"
SENDER = "+1XXXXXXXXXX"
GROUP_ID = "base64_group_id_here"
def send_signal_alert(message):
"""Send alert to Signal group"""
try:
cmd = [
SIGNAL_CLI,
"-u", SENDER,
"send",
"-g", GROUP_ID,
"-m", message
]
subprocess.run(cmd, check=True, timeout=30)
return True
except Exception as e:
print(f"Signal error: {e}")
return False
Phase 7: Central Server Deployment
Server Requirements
| Specification | Minimum | Recommended |
|---|---|---|
| Provider | Any VPS | DigitalOcean, Linode |
| CPU | 1 vCPU | 2 vCPU |
| RAM | 1 GB | 4 GB |
| Storage | 20 GB SSD | 50 GB SSD |
| Network | 1 TB transfer | Unmetered |
Database Setup
# Install PostgreSQL
sudo apt install -y postgresql postgresql-contrib
# Create database
sudo -u postgres psql << EOF
CREATE USER flighttrack WITH PASSWORD 'secure_password';
CREATE DATABASE flighttrack OWNER flighttrack;
\c flighttrack
CREATE EXTENSION IF NOT EXISTS postgis;
EOF
Central Aggregation
# aggregator.py
from flask import Flask, request, jsonify
import psycopg2
from datetime import datetime
app = Flask(__name__)
DB_CONFIG = {
'host': 'localhost',
'database': 'flighttrack',
'user': 'flighttrack',
'password': 'secure_password'
}
@app.route('/api/position', methods=['POST'])
def receive_position():
"""Receive position data from edge nodes"""
data = request.json
conn = psycopg2.connect(**DB_CONFIG)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO positions (timestamp, hex, lat, lon, altitude, source)
VALUES (%s, %s, %s, %s, %s, %s)
''', (
datetime.utcnow(),
data['hex'],
data['lat'],
data['lon'],
data.get('alt_baro'),
data.get('source', 'unknown')
))
conn.commit()
conn.close()
return jsonify({'status': 'ok'})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Phase 8: Rapid Response Integration
Webhook Integration
def notify_rapid_response(alert_data):
"""Send alert to rapid response network webhook"""
WEBHOOK_URL = "https://dispatch.rapidresponse.org/webhook"
payload = {
"type": "flight_alert",
"timestamp": datetime.utcnow().isoformat(),
"aircraft": alert_data['hex'],
"callsign": alert_data['callsign'],
"location": alert_data['geofence'],
"tracking_url": alert_data['tracking_url'],
"action_needed": "Monitor for ground arrival"
}
try:
response = requests.post(
WEBHOOK_URL,
json=payload,
headers={"Content-Type": "application/json"},
timeout=10
)
return response.status_code == 200
except Exception as e:
print(f"Webhook error: {e}")
return False
Alert Protocol Documentation
Provide rapid response networks with:
| Document | Content |
|---|---|
| Alert format | Message structure and fields |
| Response protocol | Actions upon receiving alert |
| Verification steps | How to confirm landing |
| Escalation path | Who to contact |
| False positive handling | When alerts are incorrect |
Maintenance and Monitoring
Health Check Script
#!/bin/bash
# health_check.sh
# Check readsb
if ! systemctl is-active --quiet readsb; then
echo "ALERT: readsb is not running"
systemctl restart readsb
fi
# Check tar1090
if ! systemctl is-active --quiet tar1090; then
echo "ALERT: tar1090 is not running"
systemctl restart tar1090
fi
# Check message rate
MESSAGES=$(curl -s http://localhost/tar1090/data/stats.json | jq '.total.messages')
if [ "$MESSAGES" -lt 1000 ]; then
echo "WARNING: Low message count: $MESSAGES"
fi
# Check disk space
DISK_USAGE=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -gt 80 ]; then
echo "WARNING: Disk usage at $DISK_USAGE%"
fi
Cron Jobs
# /etc/cron.d/flight-tracking
# Health check every 5 minutes
*/5 * * * * pi /home/pi/flight-tracking/health_check.sh
# Rotate logs daily
0 0 * * * pi /usr/sbin/logrotate /home/pi/flight-tracking/logrotate.conf
# Update watchlist weekly
0 3 * * 0 pi /home/pi/flight-tracking/update_watchlist.sh
Related Resources
Last updated: March 25, 2026