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:
Smittix
2026-01-07 17:42:17 +00:00
parent bcb1a825d3
commit 61ef3f7bdd
2 changed files with 367 additions and 5 deletions

View File

@@ -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
View 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)}