diff --git a/docker-compose.yml b/docker-compose.yml index b0daa37..078ef4c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,8 +16,6 @@ services: image: ${INTERCEPT_IMAGE:-intercept:latest} build: . container_name: intercept - profiles: - - basic ports: - "5050:5050" # Uncomment for HTTPS support (set INTERCEPT_HTTPS=true below) diff --git a/routes/acars.py b/routes/acars.py index 8976781..d365a70 100644 --- a/routes/acars.py +++ b/routes/acars.py @@ -13,7 +13,7 @@ import subprocess import threading import time from datetime import datetime -from typing import Generator +from typing import Any, Generator from flask import Blueprint, jsonify, request, Response @@ -35,9 +35,11 @@ acars_bp = Blueprint('acars', __name__, url_prefix='/acars') # Default VHF ACARS frequencies (MHz) - North America primary DEFAULT_ACARS_FREQUENCIES = [ - '131.550', # North America primary + '131.550', # Primary worldwide / North America '130.025', # North America secondary '129.125', # North America tertiary + '131.725', # North America (major US carriers) + '131.825', # North America (major US carriers) ] # Message counter for statistics @@ -456,10 +458,11 @@ def get_acars_messages() -> Response: @acars_bp.route('/clear', methods=['POST']) def clear_acars_messages() -> Response: """Clear stored ACARS messages and reset counter.""" - global acars_message_count + global acars_message_count, acars_last_message_time from utils.flight_correlator import get_flight_correlator get_flight_correlator().clear_acars() acars_message_count = 0 + acars_last_message_time = None return jsonify({'status': 'cleared'}) @@ -469,7 +472,7 @@ def get_frequencies() -> Response: return jsonify({ 'default': DEFAULT_ACARS_FREQUENCIES, 'regions': { - 'north_america': ['131.550', '130.025', '129.125'], + 'north_america': ['131.550', '130.025', '129.125', '131.725', '131.825'], 'europe': ['131.525', '131.725', '131.550'], 'asia_pacific': ['131.550', '131.450'], } diff --git a/routes/vdl2.py b/routes/vdl2.py index 637571d..1714f34 100644 --- a/routes/vdl2.py +++ b/routes/vdl2.py @@ -1,19 +1,19 @@ """VDL2 aircraft datalink routes.""" -from __future__ import annotations - -import io -import json -import os -import platform -import pty -import queue -import shutil -import subprocess -import threading -import time +from __future__ import annotations + +import io +import json +import os +import platform +import pty +import queue +import shutil +import subprocess +import threading +import time from datetime import datetime -from typing import Generator +from typing import Any, Generator from flask import Blueprint, jsonify, request, Response @@ -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, @@ -55,22 +55,22 @@ def find_dumpvdl2(): return shutil.which('dumpvdl2') -def stream_vdl2_output(process: subprocess.Popen, is_text_mode: bool = False) -> None: - """Stream dumpvdl2 JSON output to queue.""" - global vdl2_message_count, vdl2_last_message_time - - try: - app_module.vdl2_queue.put({'type': 'status', 'status': 'started'}) - - # Use appropriate sentinel based on mode (text mode for pty on macOS) - sentinel = '' if is_text_mode else b'' - for line in iter(process.stdout.readline, sentinel): - if is_text_mode: - line = line.strip() - else: - line = line.decode('utf-8', errors='replace').strip() - if not line: - continue +def stream_vdl2_output(process: subprocess.Popen, is_text_mode: bool = False) -> None: + """Stream dumpvdl2 JSON output to queue.""" + global vdl2_message_count, vdl2_last_message_time + + try: + app_module.vdl2_queue.put({'type': 'status', 'status': 'started'}) + + # Use appropriate sentinel based on mode (text mode for pty on macOS) + sentinel = '' if is_text_mode else b'' + for line in iter(process.stdout.readline, sentinel): + if is_text_mode: + line = line.strip() + else: + line = line.decode('utf-8', errors='replace').strip() + if not line: + continue try: data = json.loads(line) @@ -79,7 +79,7 @@ def stream_vdl2_output(process: subprocess.Popen, is_text_mode: bool = False) -> data['type'] = 'vdl2' data['timestamp'] = datetime.utcnow().isoformat() + 'Z' - # Enrich embedded ACARS payload with translated label + # Enrich with translated ACARS label at top level (consistent with ACARS route) try: vdl2_inner = data.get('vdl2', data) acars_payload = (vdl2_inner.get('avlc') or {}).get('acars') @@ -89,9 +89,9 @@ def stream_vdl2_output(process: subprocess.Popen, is_text_mode: bool = False) -> 'label': acars_payload.get('label'), 'text': acars_payload.get('msg_text', ''), }) - acars_payload['label_description'] = translation['label_description'] - acars_payload['message_type'] = translation['message_type'] - acars_payload['parsed'] = translation['parsed'] + data['label_description'] = translation['label_description'] + data['message_type'] = translation['message_type'] + data['parsed'] = translation['parsed'] except Exception: pass @@ -268,28 +268,28 @@ def start_vdl2() -> Response: logger.info(f"Starting VDL2 decoder: {' '.join(cmd)}") try: - is_text_mode = False - - # On macOS, use pty to avoid stdout buffering issues - if platform.system() == 'Darwin': - master_fd, slave_fd = pty.openpty() - process = subprocess.Popen( - cmd, - stdout=slave_fd, - stderr=subprocess.PIPE, - start_new_session=True - ) - os.close(slave_fd) - # Wrap master_fd as a text file for line-buffered reading - process.stdout = io.open(master_fd, 'r', buffering=1) - is_text_mode = True - else: - process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - start_new_session=True - ) + is_text_mode = False + + # On macOS, use pty to avoid stdout buffering issues + if platform.system() == 'Darwin': + master_fd, slave_fd = pty.openpty() + process = subprocess.Popen( + cmd, + stdout=slave_fd, + stderr=subprocess.PIPE, + start_new_session=True + ) + os.close(slave_fd) + # Wrap master_fd as a text file for line-buffered reading + process.stdout = io.open(master_fd, 'r', buffering=1) + is_text_mode = True + else: + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + start_new_session=True + ) # Wait briefly to check if process started time.sleep(PROCESS_START_WAIT) @@ -311,12 +311,12 @@ def start_vdl2() -> Response: app_module.vdl2_process = process register_process(process) - # Start output streaming thread - thread = threading.Thread( - target=stream_vdl2_output, - args=(process, is_text_mode), - daemon=True - ) + # Start output streaming thread + thread = threading.Thread( + target=stream_vdl2_output, + args=(process, is_text_mode), + daemon=True + ) thread.start() return jsonify({ @@ -365,25 +365,25 @@ def stop_vdl2() -> Response: return jsonify({'status': 'stopped'}) -@vdl2_bp.route('/stream') -def stream_vdl2() -> Response: - """SSE stream for VDL2 messages.""" - def _on_msg(msg: dict[str, Any]) -> None: - process_event('vdl2', msg, msg.get('type')) - - response = Response( - sse_stream_fanout( - source_queue=app_module.vdl2_queue, - channel_key='vdl2', - 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 +@vdl2_bp.route('/stream') +def stream_vdl2() -> Response: + """SSE stream for VDL2 messages.""" + def _on_msg(msg: dict[str, Any]) -> None: + process_event('vdl2', msg, msg.get('type')) + + response = Response( + sse_stream_fanout( + source_queue=app_module.vdl2_queue, + channel_key='vdl2', + 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 @vdl2_bp.route('/messages') @@ -399,10 +399,11 @@ def get_vdl2_messages() -> Response: @vdl2_bp.route('/clear', methods=['POST']) def clear_vdl2_messages() -> Response: """Clear stored VDL2 messages and reset counter.""" - global vdl2_message_count + global vdl2_message_count, vdl2_last_message_time from utils.flight_correlator import get_flight_correlator get_flight_correlator().clear_vdl2() vdl2_message_count = 0 + vdl2_last_message_time = None return jsonify({'status': 'cleared'})