mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Fix ADS-B tracking and aircraft database lookup
- Add debug stats (bytes_received, lines_received) to diagnose connection issues
- Capture stderr from dump1090 to show actual error messages on failure
- Add dump1090_running status to /adsb/status endpoint
- Fix aircraft_db.lookup() to handle Mictronics array format [reg, type, flags]
instead of expecting dict format {r: reg, t: type}
- Add logging for first few SBS lines to help debug parsing issues
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
104
routes/adsb.py
104
routes/adsb.py
@@ -35,6 +35,7 @@ from utils.constants import (
|
||||
ADSB_UPDATE_INTERVAL,
|
||||
DUMP1090_START_WAIT,
|
||||
)
|
||||
from utils import aircraft_db
|
||||
|
||||
adsb_bp = Blueprint('adsb', __name__, url_prefix='/adsb')
|
||||
|
||||
@@ -43,6 +44,14 @@ adsb_using_service = False
|
||||
adsb_connected = False
|
||||
adsb_messages_received = 0
|
||||
adsb_last_message_time = None
|
||||
adsb_bytes_received = 0
|
||||
adsb_lines_received = 0
|
||||
|
||||
# Track ICAOs already looked up in aircraft database (avoid repeated lookups)
|
||||
_looked_up_icaos: set[str] = set()
|
||||
|
||||
# Load aircraft database at module init
|
||||
aircraft_db.load_database()
|
||||
|
||||
# Common installation paths for dump1090 (when not in PATH)
|
||||
DUMP1090_PATHS = [
|
||||
@@ -91,7 +100,7 @@ def check_dump1090_service():
|
||||
|
||||
def parse_sbs_stream(service_addr):
|
||||
"""Parse SBS format data from dump1090 SBS port."""
|
||||
global adsb_using_service, adsb_connected, adsb_messages_received, adsb_last_message_time
|
||||
global adsb_using_service, adsb_connected, adsb_messages_received, adsb_last_message_time, adsb_bytes_received, adsb_lines_received
|
||||
|
||||
host, port = service_addr.split(':')
|
||||
port = int(port)
|
||||
@@ -111,12 +120,16 @@ def parse_sbs_stream(service_addr):
|
||||
buffer = ""
|
||||
last_update = time.time()
|
||||
pending_updates = set()
|
||||
adsb_bytes_received = 0
|
||||
adsb_lines_received = 0
|
||||
|
||||
while adsb_using_service:
|
||||
try:
|
||||
data = sock.recv(SOCKET_BUFFER_SIZE).decode('utf-8', errors='ignore')
|
||||
if not data:
|
||||
logger.warning("SBS connection closed (no data)")
|
||||
break
|
||||
adsb_bytes_received += len(data)
|
||||
buffer += data
|
||||
|
||||
while '\n' in buffer:
|
||||
@@ -125,8 +138,15 @@ def parse_sbs_stream(service_addr):
|
||||
if not line:
|
||||
continue
|
||||
|
||||
adsb_lines_received += 1
|
||||
# Log first few lines for debugging
|
||||
if adsb_lines_received <= 3:
|
||||
logger.info(f"SBS line {adsb_lines_received}: {line[:100]}")
|
||||
|
||||
parts = line.split(',')
|
||||
if len(parts) < 11 or parts[0] != 'MSG':
|
||||
if adsb_lines_received <= 5:
|
||||
logger.debug(f"Skipping non-MSG line: {line[:50]}")
|
||||
continue
|
||||
|
||||
msg_type = parts[1]
|
||||
@@ -136,6 +156,18 @@ def parse_sbs_stream(service_addr):
|
||||
|
||||
aircraft = app_module.adsb_aircraft.get(icao) or {'icao': icao}
|
||||
|
||||
# Look up aircraft type from database (once per ICAO)
|
||||
if icao not in _looked_up_icaos:
|
||||
_looked_up_icaos.add(icao)
|
||||
db_info = aircraft_db.lookup(icao)
|
||||
if db_info:
|
||||
if db_info['registration']:
|
||||
aircraft['registration'] = db_info['registration']
|
||||
if db_info['type_code']:
|
||||
aircraft['type_code'] = db_info['type_code']
|
||||
if db_info['type_desc']:
|
||||
aircraft['type_desc'] = db_info['type_desc']
|
||||
|
||||
if msg_type == '1' and len(parts) > 10:
|
||||
callsign = parts[10].strip()
|
||||
if callsign:
|
||||
@@ -154,7 +186,7 @@ def parse_sbs_stream(service_addr):
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
elif msg_type == '4' and len(parts) > 13:
|
||||
elif msg_type == '4' and len(parts) > 16:
|
||||
if parts[12]:
|
||||
try:
|
||||
aircraft['speed'] = int(float(parts[12]))
|
||||
@@ -165,6 +197,11 @@ def parse_sbs_stream(service_addr):
|
||||
aircraft['heading'] = int(float(parts[13]))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if parts[16]:
|
||||
try:
|
||||
aircraft['vertical_rate'] = int(float(parts[16]))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
elif msg_type == '5' and len(parts) > 11:
|
||||
if parts[10]:
|
||||
@@ -242,15 +279,23 @@ def check_adsb_tools():
|
||||
@adsb_bp.route('/status')
|
||||
def adsb_status():
|
||||
"""Get ADS-B tracking status for debugging."""
|
||||
# Check if dump1090 process is still running
|
||||
dump1090_running = False
|
||||
if app_module.adsb_process:
|
||||
dump1090_running = app_module.adsb_process.poll() is None
|
||||
|
||||
return jsonify({
|
||||
'tracking_active': adsb_using_service,
|
||||
'connected_to_sbs': adsb_connected,
|
||||
'messages_received': adsb_messages_received,
|
||||
'bytes_received': adsb_bytes_received,
|
||||
'lines_received': adsb_lines_received,
|
||||
'last_message_time': adsb_last_message_time,
|
||||
'aircraft_count': len(app_module.adsb_aircraft),
|
||||
'aircraft': dict(app_module.adsb_aircraft), # Full aircraft data
|
||||
'queue_size': app_module.adsb_queue.qsize(),
|
||||
'dump1090_path': find_dump1090(),
|
||||
'dump1090_running': dump1090_running,
|
||||
'port_30003_open': check_dump1090_service() is not None
|
||||
})
|
||||
|
||||
@@ -349,16 +394,29 @@ def start_adsb():
|
||||
app_module.adsb_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
time.sleep(DUMP1090_START_WAIT)
|
||||
|
||||
if app_module.adsb_process.poll() is not None:
|
||||
# Process exited - try to get error message
|
||||
stderr_output = ''
|
||||
if app_module.adsb_process.stderr:
|
||||
try:
|
||||
stderr_output = app_module.adsb_process.stderr.read().decode('utf-8', errors='ignore').strip()
|
||||
except Exception:
|
||||
pass
|
||||
if sdr_type == SDRType.RTL_SDR:
|
||||
return jsonify({'status': 'error', 'message': 'dump1090 failed to start. Check RTL-SDR device permissions or if another process is using it.'})
|
||||
error_msg = 'dump1090 failed to start. Check RTL-SDR device permissions or if another process is using it.'
|
||||
if stderr_output:
|
||||
error_msg += f' Error: {stderr_output[:200]}'
|
||||
return jsonify({'status': 'error', 'message': error_msg})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': f'ADS-B decoder failed to start for {sdr_type.value}. Ensure readsb is installed with SoapySDR support and the device is connected.'})
|
||||
error_msg = f'ADS-B decoder failed to start for {sdr_type.value}. Ensure readsb is installed with SoapySDR support and the device is connected.'
|
||||
if stderr_output:
|
||||
error_msg += f' Error: {stderr_output[:200]}'
|
||||
return jsonify({'status': 'error', 'message': error_msg})
|
||||
|
||||
adsb_using_service = True
|
||||
thread = threading.Thread(target=parse_sbs_stream, args=(f'localhost:{ADSB_SBS_PORT}',), daemon=True)
|
||||
@@ -385,6 +443,7 @@ def stop_adsb():
|
||||
adsb_using_service = False
|
||||
|
||||
app_module.adsb_aircraft.clear()
|
||||
_looked_up_icaos.clear()
|
||||
return jsonify({'status': 'stopped'})
|
||||
|
||||
|
||||
@@ -415,3 +474,38 @@ def stream_adsb():
|
||||
def adsb_dashboard():
|
||||
"""Popout ADS-B dashboard."""
|
||||
return render_template('adsb_dashboard.html')
|
||||
|
||||
|
||||
# ============================================
|
||||
# AIRCRAFT DATABASE MANAGEMENT
|
||||
# ============================================
|
||||
|
||||
@adsb_bp.route('/aircraft-db/status')
|
||||
def aircraft_db_status():
|
||||
"""Get aircraft database status."""
|
||||
return jsonify(aircraft_db.get_db_status())
|
||||
|
||||
|
||||
@adsb_bp.route('/aircraft-db/check-updates')
|
||||
def aircraft_db_check_updates():
|
||||
"""Check for aircraft database updates."""
|
||||
result = aircraft_db.check_for_updates()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@adsb_bp.route('/aircraft-db/download', methods=['POST'])
|
||||
def aircraft_db_download():
|
||||
"""Download/update aircraft database."""
|
||||
global _looked_up_icaos
|
||||
result = aircraft_db.download_database()
|
||||
if result.get('success'):
|
||||
# Clear lookup cache so new data is used
|
||||
_looked_up_icaos.clear()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@adsb_bp.route('/aircraft-db/delete', methods=['POST'])
|
||||
def aircraft_db_delete():
|
||||
"""Delete aircraft database."""
|
||||
result = aircraft_db.delete_database()
|
||||
return jsonify(result)
|
||||
|
||||
268
utils/aircraft_db.py
Normal file
268
utils/aircraft_db.py
Normal file
@@ -0,0 +1,268 @@
|
||||
"""Aircraft database for ICAO hex to type/registration lookup."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from urllib.request import urlopen, Request
|
||||
from urllib.error import URLError
|
||||
|
||||
logger = logging.getLogger('intercept.aircraft_db')
|
||||
|
||||
# Database file location (project root)
|
||||
DB_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
DB_FILE = os.path.join(DB_DIR, 'aircraft_db.json')
|
||||
DB_META_FILE = os.path.join(DB_DIR, 'aircraft_db_meta.json')
|
||||
|
||||
# Mictronics database URLs (raw GitHub)
|
||||
AIRCRAFT_DB_URL = 'https://raw.githubusercontent.com/Mictronics/readsb-protobuf/dev/webapp/src/db/aircrafts.json'
|
||||
TYPES_DB_URL = 'https://raw.githubusercontent.com/Mictronics/readsb-protobuf/dev/webapp/src/db/types.json'
|
||||
GITHUB_API_URL = 'https://api.github.com/repos/Mictronics/readsb-protobuf/commits?path=webapp/src/db/aircrafts.json&per_page=1'
|
||||
|
||||
# In-memory cache
|
||||
_aircraft_cache: dict[str, dict[str, str]] = {}
|
||||
_types_cache: dict[str, str] = {}
|
||||
_cache_lock = threading.Lock()
|
||||
_db_loaded = False
|
||||
_db_version: str | None = None
|
||||
_update_available: bool = False
|
||||
_latest_version: str | None = None
|
||||
|
||||
|
||||
def get_db_status() -> dict[str, Any]:
|
||||
"""Get current database status."""
|
||||
exists = os.path.exists(DB_FILE)
|
||||
meta = _load_meta()
|
||||
|
||||
return {
|
||||
'installed': exists,
|
||||
'version': meta.get('version') if meta else None,
|
||||
'downloaded': meta.get('downloaded') if meta else None,
|
||||
'aircraft_count': len(_aircraft_cache) if _db_loaded else 0,
|
||||
'update_available': _update_available,
|
||||
'latest_version': _latest_version,
|
||||
}
|
||||
|
||||
|
||||
def _load_meta() -> dict[str, Any] | None:
|
||||
"""Load database metadata."""
|
||||
try:
|
||||
if os.path.exists(DB_META_FILE):
|
||||
with open(DB_META_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error loading aircraft db meta: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _save_meta(version: str) -> None:
|
||||
"""Save database metadata."""
|
||||
try:
|
||||
meta = {
|
||||
'version': version,
|
||||
'downloaded': datetime.utcnow().isoformat() + 'Z',
|
||||
}
|
||||
with open(DB_META_FILE, 'w') as f:
|
||||
json.dump(meta, f, indent=2)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error saving aircraft db meta: {e}")
|
||||
|
||||
|
||||
def load_database() -> bool:
|
||||
"""Load aircraft database into memory. Returns True if successful."""
|
||||
global _aircraft_cache, _types_cache, _db_loaded, _db_version
|
||||
|
||||
if not os.path.exists(DB_FILE):
|
||||
logger.info("Aircraft database not installed")
|
||||
return False
|
||||
|
||||
try:
|
||||
with _cache_lock:
|
||||
with open(DB_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
_aircraft_cache = data.get('aircraft', {})
|
||||
_types_cache = data.get('types', {})
|
||||
_db_loaded = True
|
||||
|
||||
meta = _load_meta()
|
||||
_db_version = meta.get('version') if meta else 'unknown'
|
||||
|
||||
logger.info(f"Loaded aircraft database: {len(_aircraft_cache)} aircraft, {len(_types_cache)} types")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading aircraft database: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def lookup(icao: str) -> dict[str, str] | None:
|
||||
"""
|
||||
Look up aircraft by ICAO hex code.
|
||||
|
||||
Returns dict with keys: registration, type_code, type_desc
|
||||
Or None if not found.
|
||||
"""
|
||||
if not _db_loaded:
|
||||
return None
|
||||
|
||||
icao_upper = icao.upper()
|
||||
|
||||
with _cache_lock:
|
||||
aircraft = _aircraft_cache.get(icao_upper)
|
||||
if not aircraft:
|
||||
return None
|
||||
|
||||
# Database format is array: [registration, type_code, flags, ...]
|
||||
# Handle both list format (from Mictronics) and dict format (legacy)
|
||||
if isinstance(aircraft, list):
|
||||
reg = aircraft[0] if len(aircraft) > 0 else ''
|
||||
type_code = aircraft[1] if len(aircraft) > 1 else ''
|
||||
else:
|
||||
# Dict format fallback
|
||||
reg = aircraft.get('r', '')
|
||||
type_code = aircraft.get('t', '')
|
||||
|
||||
# Look up type description
|
||||
type_desc = ''
|
||||
if type_code and type_code in _types_cache:
|
||||
type_desc = _types_cache[type_code]
|
||||
|
||||
return {
|
||||
'registration': reg,
|
||||
'type_code': type_code,
|
||||
'type_desc': type_desc,
|
||||
}
|
||||
|
||||
|
||||
def check_for_updates() -> dict[str, Any]:
|
||||
"""
|
||||
Check GitHub for database updates.
|
||||
Returns status dict with update_available flag.
|
||||
"""
|
||||
global _update_available, _latest_version
|
||||
|
||||
try:
|
||||
req = Request(GITHUB_API_URL, headers={'User-Agent': 'Intercept-SIGINT'})
|
||||
with urlopen(req, timeout=10) as response:
|
||||
commits = json.loads(response.read().decode('utf-8'))
|
||||
|
||||
if commits and len(commits) > 0:
|
||||
latest_sha = commits[0]['sha'][:8]
|
||||
latest_date = commits[0]['commit']['committer']['date']
|
||||
_latest_version = f"{latest_date[:10]}_{latest_sha}"
|
||||
|
||||
meta = _load_meta()
|
||||
current_version = meta.get('version') if meta else None
|
||||
|
||||
_update_available = current_version != _latest_version
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'current_version': current_version,
|
||||
'latest_version': _latest_version,
|
||||
'update_available': _update_available,
|
||||
}
|
||||
except URLError as e:
|
||||
logger.warning(f"Failed to check for updates: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking for updates: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
return {'success': False, 'error': 'Unknown error'}
|
||||
|
||||
|
||||
def download_database(progress_callback=None) -> dict[str, Any]:
|
||||
"""
|
||||
Download latest aircraft database from Mictronics repo.
|
||||
Returns status dict.
|
||||
"""
|
||||
global _update_available
|
||||
|
||||
try:
|
||||
if progress_callback:
|
||||
progress_callback('Downloading aircraft database...')
|
||||
|
||||
# Download aircraft database
|
||||
req = Request(AIRCRAFT_DB_URL, headers={'User-Agent': 'Intercept-SIGINT'})
|
||||
with urlopen(req, timeout=60) as response:
|
||||
aircraft_data = json.loads(response.read().decode('utf-8'))
|
||||
|
||||
if progress_callback:
|
||||
progress_callback('Downloading type codes...')
|
||||
|
||||
# Download types database
|
||||
req = Request(TYPES_DB_URL, headers={'User-Agent': 'Intercept-SIGINT'})
|
||||
with urlopen(req, timeout=30) as response:
|
||||
types_data = json.loads(response.read().decode('utf-8'))
|
||||
|
||||
if progress_callback:
|
||||
progress_callback('Processing database...')
|
||||
|
||||
# Combine into single file
|
||||
combined = {
|
||||
'aircraft': aircraft_data,
|
||||
'types': types_data,
|
||||
}
|
||||
|
||||
# Save to file
|
||||
with open(DB_FILE, 'w') as f:
|
||||
json.dump(combined, f, separators=(',', ':')) # Compact JSON
|
||||
|
||||
# Get version from GitHub
|
||||
version = datetime.utcnow().strftime('%Y-%m-%d')
|
||||
try:
|
||||
req = Request(GITHUB_API_URL, headers={'User-Agent': 'Intercept-SIGINT'})
|
||||
with urlopen(req, timeout=10) as response:
|
||||
commits = json.loads(response.read().decode('utf-8'))
|
||||
if commits:
|
||||
sha = commits[0]['sha'][:8]
|
||||
date = commits[0]['commit']['committer']['date'][:10]
|
||||
version = f"{date}_{sha}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_save_meta(version)
|
||||
_update_available = False
|
||||
|
||||
# Reload into memory
|
||||
load_database()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': f'Downloaded {len(aircraft_data)} aircraft, {len(types_data)} types',
|
||||
'version': version,
|
||||
}
|
||||
|
||||
except URLError as e:
|
||||
logger.error(f"Download failed: {e}")
|
||||
return {'success': False, 'error': f'Download failed: {e}'}
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading database: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
|
||||
def delete_database() -> dict[str, Any]:
|
||||
"""Delete local database files."""
|
||||
global _aircraft_cache, _types_cache, _db_loaded, _db_version
|
||||
|
||||
try:
|
||||
with _cache_lock:
|
||||
_aircraft_cache = {}
|
||||
_types_cache = {}
|
||||
_db_loaded = False
|
||||
_db_version = None
|
||||
|
||||
if os.path.exists(DB_FILE):
|
||||
os.remove(DB_FILE)
|
||||
if os.path.exists(DB_META_FILE):
|
||||
os.remove(DB_META_FILE)
|
||||
|
||||
return {'success': True, 'message': 'Database deleted'}
|
||||
except Exception as e:
|
||||
return {'success': False, 'error': str(e)}
|
||||
Reference in New Issue
Block a user