feat(acars): add message translator and ADS-B datalink integration

Add ACARS label translation, message classification, and field parsers
so decoded messages show human-readable descriptions instead of raw
label codes (H1, DF, _d, 5Z, etc.). Integrate translated ACARS
messages into the ADS-B aircraft detail panel and add a live message
feed to the standalone ACARS mode.

- New utils/acars_translator.py with ~50 label codes, type classifier,
  and parsers for position reports, engine data, weather, and OOOI
- Enrich messages at ingest in routes/acars.py with translation fields
- Backfill translation in /adsb/aircraft/<icao>/messages endpoint
- ADS-B dashboard: DATALINK MESSAGES section in aircraft detail panel
  with auto-refresh, color-coded type badges, and parsed field display
- Standalone ACARS mode: scrollable live message feed (max 30 cards)
- Fix default N. America ACARS frequencies to 131.550/130.025/129.125
- Unit tests covering all translator functions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mitchross
2026-02-20 11:11:57 -05:00
parent 9ec316fbe2
commit d28d8cb9ef
9 changed files with 940 additions and 30 deletions
+35 -24
View File
@@ -21,7 +21,7 @@ import app as app_module
from utils.logging import sensor_logger as logger
from utils.validation import validate_device_index, validate_gain, validate_ppm
from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout
from utils.sse import sse_stream_fanout
from utils.event_pipeline import process_event
from utils.constants import (
PROCESS_TERMINATE_TIMEOUT,
@@ -33,10 +33,11 @@ from utils.process import register_process, unregister_process
acars_bp = Blueprint('acars', __name__, url_prefix='/acars')
# Default VHF ACARS frequencies (MHz) - common worldwide
# Default VHF ACARS frequencies (MHz) - North America primary
DEFAULT_ACARS_FREQUENCIES = [
'131.725', # North America
'131.825', # North America
'131.550', # North America primary
'130.025', # North America secondary
'129.125', # North America tertiary
]
# Message counter for statistics
@@ -120,6 +121,16 @@ def stream_acars_output(process: subprocess.Popen, is_text_mode: bool = False) -
data['type'] = 'acars'
data['timestamp'] = datetime.utcnow().isoformat() + 'Z'
# Enrich with translated label and parsed fields
try:
from utils.acars_translator import translate_message
translation = translate_message(data)
data['label_description'] = translation['label_description']
data['message_type'] = translation['message_type']
data['parsed'] = translation['parsed']
except Exception:
pass
# Update stats
acars_message_count += 1
acars_last_message_time = time.time()
@@ -411,25 +422,25 @@ def stop_acars() -> Response:
return jsonify({'status': 'stopped'})
@acars_bp.route('/stream')
def stream_acars() -> Response:
"""SSE stream for ACARS messages."""
def _on_msg(msg: dict[str, Any]) -> None:
process_event('acars', msg, msg.get('type'))
response = Response(
sse_stream_fanout(
source_queue=app_module.acars_queue,
channel_key='acars',
timeout=SSE_QUEUE_TIMEOUT,
keepalive_interval=SSE_KEEPALIVE_INTERVAL,
on_message=_on_msg,
),
mimetype='text/event-stream',
)
response.headers['Cache-Control'] = 'no-cache'
response.headers['X-Accel-Buffering'] = 'no'
return response
@acars_bp.route('/stream')
def stream_acars() -> Response:
"""SSE stream for ACARS messages."""
def _on_msg(msg: dict[str, Any]) -> None:
process_event('acars', msg, msg.get('type'))
response = Response(
sse_stream_fanout(
source_queue=app_module.acars_queue,
channel_key='acars',
timeout=SSE_QUEUE_TIMEOUT,
keepalive_interval=SSE_KEEPALIVE_INTERVAL,
on_message=_on_msg,
),
mimetype='text/event-stream',
)
response.headers['Cache-Control'] = 'no-cache'
response.headers['X-Accel-Buffering'] = 'no'
return response
@acars_bp.route('/frequencies')
@@ -438,7 +449,7 @@ def get_frequencies() -> Response:
return jsonify({
'default': DEFAULT_ACARS_FREQUENCIES,
'regions': {
'north_america': ['131.725', '131.825'],
'north_america': ['131.550', '130.025', '129.125'],
'europe': ['131.525', '131.725', '131.550'],
'asia_pacific': ['131.550', '131.450'],
}
+13
View File
@@ -1189,4 +1189,17 @@ def get_aircraft_messages(icao: str):
from utils.flight_correlator import get_flight_correlator
messages = get_flight_correlator().get_messages_for_aircraft(icao=icao.upper(), callsign=callsign)
# Backfill translation on messages missing label_description
try:
from utils.acars_translator import translate_message
for msg in messages.get('acars', []):
if not msg.get('label_description'):
translation = translate_message(msg)
msg['label_description'] = translation['label_description']
msg['message_type'] = translation['message_type']
msg['parsed'] = translation['parsed']
except Exception:
pass
return jsonify({'status': 'success', 'icao': icao.upper(), **messages})