mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
feat: Add VDL2 mode and ACARS standalone frontend
Add VDL2 (VHF Digital Link Mode 2) decoding via dumpvdl2 as a new mode, and promote ACARS from ADS-B-dashboard-only to a first-class standalone mode in the main SPA. Both aviation datalink modes now have full nav entries, sidebar partials with region-based frequency selectors, and SSE streaming. VDL2 also integrated into the ADS-B dashboard as a collapsible sidebar alongside ACARS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
21
Dockerfile
21
Dockerfile
@@ -95,6 +95,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libfftw3-dev \
|
libfftw3-dev \
|
||||||
liblapack-dev \
|
liblapack-dev \
|
||||||
libcodec2-dev \
|
libcodec2-dev \
|
||||||
|
libglib2.0-dev \
|
||||||
|
libxml2-dev \
|
||||||
# Build dump1090
|
# Build dump1090
|
||||||
&& cd /tmp \
|
&& cd /tmp \
|
||||||
&& git clone --depth 1 https://github.com/flightaware/dump1090.git \
|
&& git clone --depth 1 https://github.com/flightaware/dump1090.git \
|
||||||
@@ -141,6 +143,25 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
&& make \
|
&& make \
|
||||||
&& cp acarsdec /usr/bin/acarsdec \
|
&& cp acarsdec /usr/bin/acarsdec \
|
||||||
&& rm -rf /tmp/acarsdec \
|
&& rm -rf /tmp/acarsdec \
|
||||||
|
# Build libacars (required by dumpvdl2)
|
||||||
|
&& cd /tmp \
|
||||||
|
&& git clone --depth 1 https://github.com/szpajder/libacars.git \
|
||||||
|
&& cd libacars \
|
||||||
|
&& mkdir build && cd build \
|
||||||
|
&& cmake .. \
|
||||||
|
&& make \
|
||||||
|
&& make install \
|
||||||
|
&& ldconfig \
|
||||||
|
&& rm -rf /tmp/libacars \
|
||||||
|
# Build dumpvdl2 (VDL2 aircraft datalink decoder)
|
||||||
|
&& cd /tmp \
|
||||||
|
&& git clone --depth 1 https://github.com/szpajder/dumpvdl2.git \
|
||||||
|
&& cd dumpvdl2 \
|
||||||
|
&& mkdir build && cd build \
|
||||||
|
&& cmake .. \
|
||||||
|
&& make \
|
||||||
|
&& cp src/dumpvdl2 /usr/bin/dumpvdl2 \
|
||||||
|
&& rm -rf /tmp/dumpvdl2 \
|
||||||
# Build slowrx (SSTV decoder) — pinned to known-good commit
|
# Build slowrx (SSTV decoder) — pinned to known-good commit
|
||||||
&& cd /tmp \
|
&& cd /tmp \
|
||||||
&& git clone https://github.com/windytan/slowrx.git \
|
&& git clone https://github.com/windytan/slowrx.git \
|
||||||
|
|||||||
13
app.py
13
app.py
@@ -150,6 +150,11 @@ acars_process = None
|
|||||||
acars_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
acars_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||||
acars_lock = threading.Lock()
|
acars_lock = threading.Lock()
|
||||||
|
|
||||||
|
# VDL2 aircraft datalink
|
||||||
|
vdl2_process = None
|
||||||
|
vdl2_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||||
|
vdl2_lock = threading.Lock()
|
||||||
|
|
||||||
# APRS amateur radio tracking
|
# APRS amateur radio tracking
|
||||||
aprs_process = None
|
aprs_process = None
|
||||||
aprs_rtl_process = None
|
aprs_rtl_process = None
|
||||||
@@ -680,6 +685,7 @@ def health_check() -> Response:
|
|||||||
'adsb': adsb_process is not None and (adsb_process.poll() is None if adsb_process else False),
|
'adsb': adsb_process is not None and (adsb_process.poll() is None if adsb_process else False),
|
||||||
'ais': ais_process is not None and (ais_process.poll() is None if ais_process else False),
|
'ais': ais_process is not None and (ais_process.poll() is None if ais_process else False),
|
||||||
'acars': acars_process is not None and (acars_process.poll() is None if acars_process else False),
|
'acars': acars_process is not None and (acars_process.poll() is None if acars_process else False),
|
||||||
|
'vdl2': vdl2_process is not None and (vdl2_process.poll() is None if vdl2_process else False),
|
||||||
'aprs': aprs_process is not None and (aprs_process.poll() is None if aprs_process else False),
|
'aprs': aprs_process is not None and (aprs_process.poll() is None if aprs_process else False),
|
||||||
'wifi': wifi_process is not None and (wifi_process.poll() is None if wifi_process else False),
|
'wifi': wifi_process is not None and (wifi_process.poll() is None if wifi_process else False),
|
||||||
'bluetooth': bt_process is not None and (bt_process.poll() is None if bt_process else False),
|
'bluetooth': bt_process is not None and (bt_process.poll() is None if bt_process else False),
|
||||||
@@ -702,6 +708,7 @@ def health_check() -> Response:
|
|||||||
def kill_all() -> Response:
|
def kill_all() -> Response:
|
||||||
"""Kill all decoder, WiFi, and Bluetooth processes."""
|
"""Kill all decoder, WiFi, and Bluetooth processes."""
|
||||||
global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process
|
global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process
|
||||||
|
global vdl2_process
|
||||||
global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process, bt_process
|
global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process, bt_process
|
||||||
global dmr_process, dmr_rtl_process
|
global dmr_process, dmr_rtl_process
|
||||||
|
|
||||||
@@ -714,7 +721,7 @@ def kill_all() -> Response:
|
|||||||
processes_to_kill = [
|
processes_to_kill = [
|
||||||
'rtl_fm', 'multimon-ng', 'rtl_433',
|
'rtl_fm', 'multimon-ng', 'rtl_433',
|
||||||
'airodump-ng', 'aireplay-ng', 'airmon-ng',
|
'airodump-ng', 'aireplay-ng', 'airmon-ng',
|
||||||
'dump1090', 'acarsdec', 'direwolf', 'AIS-catcher',
|
'dump1090', 'acarsdec', 'dumpvdl2', 'direwolf', 'AIS-catcher',
|
||||||
'hcitool', 'bluetoothctl', 'satdump', 'dsd',
|
'hcitool', 'bluetoothctl', 'satdump', 'dsd',
|
||||||
'rtl_tcp', 'rtl_power', 'rtlamr', 'ffmpeg',
|
'rtl_tcp', 'rtl_power', 'rtlamr', 'ffmpeg',
|
||||||
'hackrf_transfer', 'hackrf_sweep'
|
'hackrf_transfer', 'hackrf_sweep'
|
||||||
@@ -751,6 +758,10 @@ def kill_all() -> Response:
|
|||||||
with acars_lock:
|
with acars_lock:
|
||||||
acars_process = None
|
acars_process = None
|
||||||
|
|
||||||
|
# Reset VDL2 state
|
||||||
|
with vdl2_lock:
|
||||||
|
vdl2_process = None
|
||||||
|
|
||||||
# Reset APRS state
|
# Reset APRS state
|
||||||
with aprs_lock:
|
with aprs_lock:
|
||||||
aprs_process = None
|
aprs_process = None
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ def register_blueprints(app):
|
|||||||
from .ais import ais_bp
|
from .ais import ais_bp
|
||||||
from .dsc import dsc_bp
|
from .dsc import dsc_bp
|
||||||
from .acars import acars_bp
|
from .acars import acars_bp
|
||||||
|
from .vdl2 import vdl2_bp
|
||||||
from .aprs import aprs_bp
|
from .aprs import aprs_bp
|
||||||
from .satellite import satellite_bp
|
from .satellite import satellite_bp
|
||||||
from .gps import gps_bp
|
from .gps import gps_bp
|
||||||
@@ -46,6 +47,7 @@ def register_blueprints(app):
|
|||||||
app.register_blueprint(ais_bp)
|
app.register_blueprint(ais_bp)
|
||||||
app.register_blueprint(dsc_bp) # VHF DSC maritime distress
|
app.register_blueprint(dsc_bp) # VHF DSC maritime distress
|
||||||
app.register_blueprint(acars_bp)
|
app.register_blueprint(acars_bp)
|
||||||
|
app.register_blueprint(vdl2_bp)
|
||||||
app.register_blueprint(aprs_bp)
|
app.register_blueprint(aprs_bp)
|
||||||
app.register_blueprint(satellite_bp)
|
app.register_blueprint(satellite_bp)
|
||||||
app.register_blueprint(gps_bp)
|
app.register_blueprint(gps_bp)
|
||||||
|
|||||||
357
routes/vdl2.py
Normal file
357
routes/vdl2.py
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
"""VDL2 aircraft datalink routes."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import queue
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, request, Response
|
||||||
|
|
||||||
|
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 format_sse
|
||||||
|
from utils.event_pipeline import process_event
|
||||||
|
from utils.constants import (
|
||||||
|
PROCESS_TERMINATE_TIMEOUT,
|
||||||
|
SSE_KEEPALIVE_INTERVAL,
|
||||||
|
SSE_QUEUE_TIMEOUT,
|
||||||
|
PROCESS_START_WAIT,
|
||||||
|
)
|
||||||
|
from utils.process import register_process, unregister_process
|
||||||
|
|
||||||
|
vdl2_bp = Blueprint('vdl2', __name__, url_prefix='/vdl2')
|
||||||
|
|
||||||
|
# Default VDL2 frequencies (MHz) - common worldwide
|
||||||
|
DEFAULT_VDL2_FREQUENCIES = [
|
||||||
|
'136975000', # Primary worldwide
|
||||||
|
'136725000', # Europe
|
||||||
|
'136775000', # Europe
|
||||||
|
'136800000', # Multi-region
|
||||||
|
'136875000', # Multi-region
|
||||||
|
]
|
||||||
|
|
||||||
|
# Message counter for statistics
|
||||||
|
vdl2_message_count = 0
|
||||||
|
vdl2_last_message_time = None
|
||||||
|
|
||||||
|
# Track which device is being used
|
||||||
|
vdl2_active_device: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def find_dumpvdl2():
|
||||||
|
"""Find dumpvdl2 binary."""
|
||||||
|
return shutil.which('dumpvdl2')
|
||||||
|
|
||||||
|
|
||||||
|
def stream_vdl2_output(process: subprocess.Popen) -> 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'})
|
||||||
|
|
||||||
|
for line in iter(process.stdout.readline, b''):
|
||||||
|
line = line.decode('utf-8', errors='replace').strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(line)
|
||||||
|
|
||||||
|
# Add our metadata
|
||||||
|
data['type'] = 'vdl2'
|
||||||
|
data['timestamp'] = datetime.utcnow().isoformat() + 'Z'
|
||||||
|
|
||||||
|
# Update stats
|
||||||
|
vdl2_message_count += 1
|
||||||
|
vdl2_last_message_time = time.time()
|
||||||
|
|
||||||
|
app_module.vdl2_queue.put(data)
|
||||||
|
|
||||||
|
# Log if enabled
|
||||||
|
if app_module.logging_enabled:
|
||||||
|
try:
|
||||||
|
with open(app_module.log_file_path, 'a') as f:
|
||||||
|
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
f.write(f"{ts} | VDL2 | {json.dumps(data)}\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Not JSON - could be status message
|
||||||
|
if line:
|
||||||
|
logger.debug(f"dumpvdl2 non-JSON: {line[:100]}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"VDL2 stream error: {e}")
|
||||||
|
app_module.vdl2_queue.put({'type': 'error', 'message': str(e)})
|
||||||
|
finally:
|
||||||
|
global vdl2_active_device
|
||||||
|
# Ensure process is terminated
|
||||||
|
try:
|
||||||
|
process.terminate()
|
||||||
|
process.wait(timeout=2)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
process.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
unregister_process(process)
|
||||||
|
app_module.vdl2_queue.put({'type': 'status', 'status': 'stopped'})
|
||||||
|
with app_module.vdl2_lock:
|
||||||
|
app_module.vdl2_process = None
|
||||||
|
# Release SDR device
|
||||||
|
if vdl2_active_device is not None:
|
||||||
|
app_module.release_sdr_device(vdl2_active_device)
|
||||||
|
vdl2_active_device = None
|
||||||
|
|
||||||
|
|
||||||
|
@vdl2_bp.route('/tools')
|
||||||
|
def check_vdl2_tools() -> Response:
|
||||||
|
"""Check for VDL2 decoding tools."""
|
||||||
|
has_dumpvdl2 = find_dumpvdl2() is not None
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'dumpvdl2': has_dumpvdl2,
|
||||||
|
'ready': has_dumpvdl2
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@vdl2_bp.route('/status')
|
||||||
|
def vdl2_status() -> Response:
|
||||||
|
"""Get VDL2 decoder status."""
|
||||||
|
running = False
|
||||||
|
if app_module.vdl2_process:
|
||||||
|
running = app_module.vdl2_process.poll() is None
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'running': running,
|
||||||
|
'message_count': vdl2_message_count,
|
||||||
|
'last_message_time': vdl2_last_message_time,
|
||||||
|
'queue_size': app_module.vdl2_queue.qsize()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@vdl2_bp.route('/start', methods=['POST'])
|
||||||
|
def start_vdl2() -> Response:
|
||||||
|
"""Start VDL2 decoder."""
|
||||||
|
global vdl2_message_count, vdl2_last_message_time, vdl2_active_device
|
||||||
|
|
||||||
|
with app_module.vdl2_lock:
|
||||||
|
if app_module.vdl2_process and app_module.vdl2_process.poll() is None:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'VDL2 decoder already running'
|
||||||
|
}), 409
|
||||||
|
|
||||||
|
# Check for dumpvdl2
|
||||||
|
dumpvdl2_path = find_dumpvdl2()
|
||||||
|
if not dumpvdl2_path:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'dumpvdl2 not found. Install from: https://github.com/szpajder/dumpvdl2'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
data = request.json or {}
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
try:
|
||||||
|
device = validate_device_index(data.get('device', '0'))
|
||||||
|
gain = validate_gain(data.get('gain', '40'))
|
||||||
|
ppm = validate_ppm(data.get('ppm', '0'))
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
# Check if device is available
|
||||||
|
device_int = int(device)
|
||||||
|
error = app_module.claim_sdr_device(device_int, 'vdl2')
|
||||||
|
if error:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'error_type': 'DEVICE_BUSY',
|
||||||
|
'message': error
|
||||||
|
}), 409
|
||||||
|
|
||||||
|
vdl2_active_device = device_int
|
||||||
|
|
||||||
|
# Get frequencies - use provided or defaults
|
||||||
|
# dumpvdl2 expects frequencies in Hz (integers)
|
||||||
|
frequencies = data.get('frequencies', DEFAULT_VDL2_FREQUENCIES)
|
||||||
|
if isinstance(frequencies, str):
|
||||||
|
frequencies = [f.strip() for f in frequencies.split(',')]
|
||||||
|
|
||||||
|
# Clear queue
|
||||||
|
while not app_module.vdl2_queue.empty():
|
||||||
|
try:
|
||||||
|
app_module.vdl2_queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Reset stats
|
||||||
|
vdl2_message_count = 0
|
||||||
|
vdl2_last_message_time = None
|
||||||
|
|
||||||
|
# Resolve SDR type for device selection
|
||||||
|
sdr_type_str = data.get('sdr_type', 'rtlsdr')
|
||||||
|
try:
|
||||||
|
sdr_type = SDRType(sdr_type_str)
|
||||||
|
except ValueError:
|
||||||
|
sdr_type = SDRType.RTL_SDR
|
||||||
|
|
||||||
|
is_soapy = sdr_type not in (SDRType.RTL_SDR,)
|
||||||
|
|
||||||
|
# Build dumpvdl2 command
|
||||||
|
# dumpvdl2 --output decoded:json --rtlsdr <device> --gain <gain> --correction <ppm> <freq1> <freq2> ...
|
||||||
|
cmd = [dumpvdl2_path]
|
||||||
|
cmd.extend(['--output', 'decoded:json'])
|
||||||
|
|
||||||
|
if is_soapy:
|
||||||
|
# SoapySDR device
|
||||||
|
sdr_device = SDRFactory.create_default_device(sdr_type, index=device_int)
|
||||||
|
builder = SDRFactory.get_builder(sdr_type)
|
||||||
|
device_str = builder._build_device_string(sdr_device)
|
||||||
|
cmd.extend(['--soapysdr', device_str])
|
||||||
|
else:
|
||||||
|
cmd.extend(['--rtlsdr', str(device)])
|
||||||
|
|
||||||
|
# Add gain
|
||||||
|
if gain and str(gain) != '0':
|
||||||
|
cmd.extend(['--gain', str(gain)])
|
||||||
|
|
||||||
|
# Add PPM correction if specified
|
||||||
|
if ppm and str(ppm) != '0':
|
||||||
|
cmd.extend(['--correction', str(ppm)])
|
||||||
|
|
||||||
|
# Add frequencies (dumpvdl2 takes them as positional args in Hz)
|
||||||
|
cmd.extend(frequencies)
|
||||||
|
|
||||||
|
logger.info(f"Starting VDL2 decoder: {' '.join(cmd)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
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)
|
||||||
|
|
||||||
|
if process.poll() is not None:
|
||||||
|
# Process died - release device
|
||||||
|
if vdl2_active_device is not None:
|
||||||
|
app_module.release_sdr_device(vdl2_active_device)
|
||||||
|
vdl2_active_device = None
|
||||||
|
stderr = ''
|
||||||
|
if process.stderr:
|
||||||
|
stderr = process.stderr.read().decode('utf-8', errors='replace')
|
||||||
|
error_msg = 'dumpvdl2 failed to start'
|
||||||
|
if stderr:
|
||||||
|
error_msg += f': {stderr[:200]}'
|
||||||
|
logger.error(error_msg)
|
||||||
|
return jsonify({'status': 'error', 'message': error_msg}), 500
|
||||||
|
|
||||||
|
app_module.vdl2_process = process
|
||||||
|
register_process(process)
|
||||||
|
|
||||||
|
# Start output streaming thread
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=stream_vdl2_output,
|
||||||
|
args=(process,),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'started',
|
||||||
|
'frequencies': frequencies,
|
||||||
|
'device': device,
|
||||||
|
'gain': gain
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Release device on failure
|
||||||
|
if vdl2_active_device is not None:
|
||||||
|
app_module.release_sdr_device(vdl2_active_device)
|
||||||
|
vdl2_active_device = None
|
||||||
|
logger.error(f"Failed to start VDL2 decoder: {e}")
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@vdl2_bp.route('/stop', methods=['POST'])
|
||||||
|
def stop_vdl2() -> Response:
|
||||||
|
"""Stop VDL2 decoder."""
|
||||||
|
global vdl2_active_device
|
||||||
|
|
||||||
|
with app_module.vdl2_lock:
|
||||||
|
if not app_module.vdl2_process:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'VDL2 decoder not running'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
app_module.vdl2_process.terminate()
|
||||||
|
app_module.vdl2_process.wait(timeout=PROCESS_TERMINATE_TIMEOUT)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
app_module.vdl2_process.kill()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error stopping VDL2: {e}")
|
||||||
|
|
||||||
|
app_module.vdl2_process = None
|
||||||
|
|
||||||
|
# Release device from registry
|
||||||
|
if vdl2_active_device is not None:
|
||||||
|
app_module.release_sdr_device(vdl2_active_device)
|
||||||
|
vdl2_active_device = None
|
||||||
|
|
||||||
|
return jsonify({'status': 'stopped'})
|
||||||
|
|
||||||
|
|
||||||
|
@vdl2_bp.route('/stream')
|
||||||
|
def stream_vdl2() -> Response:
|
||||||
|
"""SSE stream for VDL2 messages."""
|
||||||
|
def generate() -> Generator[str, None, None]:
|
||||||
|
last_keepalive = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
msg = app_module.vdl2_queue.get(timeout=SSE_QUEUE_TIMEOUT)
|
||||||
|
last_keepalive = time.time()
|
||||||
|
try:
|
||||||
|
process_event('vdl2', msg, msg.get('type'))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
yield format_sse(msg)
|
||||||
|
except queue.Empty:
|
||||||
|
now = time.time()
|
||||||
|
if now - last_keepalive >= SSE_KEEPALIVE_INTERVAL:
|
||||||
|
yield format_sse({'type': 'keepalive'})
|
||||||
|
last_keepalive = now
|
||||||
|
|
||||||
|
response = Response(generate(), mimetype='text/event-stream')
|
||||||
|
response.headers['Cache-Control'] = 'no-cache'
|
||||||
|
response.headers['X-Accel-Buffering'] = 'no'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@vdl2_bp.route('/frequencies')
|
||||||
|
def get_frequencies() -> Response:
|
||||||
|
"""Get default VDL2 frequencies."""
|
||||||
|
return jsonify({
|
||||||
|
'default': DEFAULT_VDL2_FREQUENCIES,
|
||||||
|
'regions': {
|
||||||
|
'north_america': ['136975000', '136100000', '136650000', '136700000', '136800000'],
|
||||||
|
'europe': ['136975000', '136675000', '136725000', '136775000', '136825000'],
|
||||||
|
'asia_pacific': ['136975000', '136900000'],
|
||||||
|
}
|
||||||
|
})
|
||||||
135
setup.sh
135
setup.sh
@@ -226,6 +226,7 @@ check_tools() {
|
|||||||
check_optional "hackrf_sweep" "HackRF spectrum analyzer" hackrf_sweep
|
check_optional "hackrf_sweep" "HackRF spectrum analyzer" hackrf_sweep
|
||||||
check_required "dump1090" "ADS-B decoder" dump1090
|
check_required "dump1090" "ADS-B decoder" dump1090
|
||||||
check_required "acarsdec" "ACARS decoder" acarsdec
|
check_required "acarsdec" "ACARS decoder" acarsdec
|
||||||
|
check_optional "dumpvdl2" "VDL2 decoder" dumpvdl2
|
||||||
check_required "AIS-catcher" "AIS vessel decoder" AIS-catcher aiscatcher
|
check_required "AIS-catcher" "AIS vessel decoder" AIS-catcher aiscatcher
|
||||||
check_optional "satdump" "Weather satellite decoder (NOAA/Meteor)" satdump
|
check_optional "satdump" "Weather satellite decoder (NOAA/Meteor)" satdump
|
||||||
echo
|
echo
|
||||||
@@ -636,6 +637,80 @@ install_acarsdec_from_source_macos() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install_dumpvdl2_from_source_macos() {
|
||||||
|
info "Building dumpvdl2 from source (with libacars dependency)..."
|
||||||
|
|
||||||
|
brew_install cmake
|
||||||
|
brew_install librtlsdr
|
||||||
|
brew_install pkg-config
|
||||||
|
brew_install glib
|
||||||
|
|
||||||
|
(
|
||||||
|
tmp_dir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$tmp_dir"' EXIT
|
||||||
|
|
||||||
|
HOMEBREW_PREFIX="$(brew --prefix)"
|
||||||
|
export PKG_CONFIG_PATH="${HOMEBREW_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH:-}"
|
||||||
|
export CMAKE_PREFIX_PATH="${HOMEBREW_PREFIX}"
|
||||||
|
|
||||||
|
# Build libacars first
|
||||||
|
info "Cloning libacars..."
|
||||||
|
git clone --depth 1 https://github.com/szpajder/libacars.git "$tmp_dir/libacars" >/dev/null 2>&1 \
|
||||||
|
|| { warn "Failed to clone libacars"; exit 1; }
|
||||||
|
|
||||||
|
cd "$tmp_dir/libacars"
|
||||||
|
mkdir -p build && cd build
|
||||||
|
|
||||||
|
info "Compiling libacars..."
|
||||||
|
build_log="$tmp_dir/libacars-build.log"
|
||||||
|
if cmake .. \
|
||||||
|
-DCMAKE_C_FLAGS="-I${HOMEBREW_PREFIX}/include" \
|
||||||
|
-DCMAKE_EXE_LINKER_FLAGS="-L${HOMEBREW_PREFIX}/lib" \
|
||||||
|
>"$build_log" 2>&1 \
|
||||||
|
&& make >>"$build_log" 2>&1; then
|
||||||
|
if [[ -w /usr/local/lib ]]; then
|
||||||
|
make install >>"$build_log" 2>&1
|
||||||
|
else
|
||||||
|
refresh_sudo
|
||||||
|
$SUDO make install >>"$build_log" 2>&1
|
||||||
|
fi
|
||||||
|
ok "libacars installed"
|
||||||
|
else
|
||||||
|
warn "Failed to build libacars."
|
||||||
|
tail -20 "$build_log" | while IFS= read -r line; do warn " $line"; done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build dumpvdl2
|
||||||
|
info "Cloning dumpvdl2..."
|
||||||
|
git clone --depth 1 https://github.com/szpajder/dumpvdl2.git "$tmp_dir/dumpvdl2" >/dev/null 2>&1 \
|
||||||
|
|| { warn "Failed to clone dumpvdl2"; exit 1; }
|
||||||
|
|
||||||
|
cd "$tmp_dir/dumpvdl2"
|
||||||
|
mkdir -p build && cd build
|
||||||
|
|
||||||
|
info "Compiling dumpvdl2..."
|
||||||
|
build_log="$tmp_dir/dumpvdl2-build.log"
|
||||||
|
if cmake .. \
|
||||||
|
-DCMAKE_C_FLAGS="-I${HOMEBREW_PREFIX}/include" \
|
||||||
|
-DCMAKE_EXE_LINKER_FLAGS="-L${HOMEBREW_PREFIX}/lib" \
|
||||||
|
>"$build_log" 2>&1 \
|
||||||
|
&& make >>"$build_log" 2>&1; then
|
||||||
|
if [[ -w /usr/local/bin ]]; then
|
||||||
|
install -m 0755 src/dumpvdl2 /usr/local/bin/dumpvdl2
|
||||||
|
else
|
||||||
|
refresh_sudo
|
||||||
|
$SUDO install -m 0755 src/dumpvdl2 /usr/local/bin/dumpvdl2
|
||||||
|
fi
|
||||||
|
ok "dumpvdl2 installed successfully from source"
|
||||||
|
else
|
||||||
|
warn "Failed to build dumpvdl2. VDL2 decoding will not be available."
|
||||||
|
warn "Build log (last 30 lines):"
|
||||||
|
tail -30 "$build_log" | while IFS= read -r line; do warn " $line"; done
|
||||||
|
fi
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
install_aiscatcher_from_source_macos() {
|
install_aiscatcher_from_source_macos() {
|
||||||
info "AIS-catcher not available via Homebrew. Building from source..."
|
info "AIS-catcher not available via Homebrew. Building from source..."
|
||||||
|
|
||||||
@@ -874,6 +949,13 @@ install_macos_packages() {
|
|||||||
ok "acarsdec already installed"
|
ok "acarsdec already installed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
progress "Installing dumpvdl2 (optional)"
|
||||||
|
if ! cmd_exists dumpvdl2; then
|
||||||
|
install_dumpvdl2_from_source_macos || warn "dumpvdl2 not available. VDL2 decoding will not be available."
|
||||||
|
else
|
||||||
|
ok "dumpvdl2 already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
progress "Installing AIS-catcher"
|
progress "Installing AIS-catcher"
|
||||||
if ! cmd_exists AIS-catcher && ! cmd_exists aiscatcher; then
|
if ! cmd_exists AIS-catcher && ! cmd_exists aiscatcher; then
|
||||||
(brew_install aiscatcher) || install_aiscatcher_from_source_macos || warn "AIS-catcher not available"
|
(brew_install aiscatcher) || install_aiscatcher_from_source_macos || warn "AIS-catcher not available"
|
||||||
@@ -1028,6 +1110,52 @@ install_acarsdec_from_source_debian() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
install_dumpvdl2_from_source_debian() {
|
||||||
|
info "Building dumpvdl2 from source (with libacars dependency)..."
|
||||||
|
|
||||||
|
apt_install build-essential git cmake \
|
||||||
|
librtlsdr-dev libusb-1.0-0-dev libglib2.0-dev libxml2-dev
|
||||||
|
|
||||||
|
(
|
||||||
|
tmp_dir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$tmp_dir"' EXIT
|
||||||
|
|
||||||
|
# Build libacars first
|
||||||
|
info "Cloning libacars..."
|
||||||
|
git clone --depth 1 https://github.com/szpajder/libacars.git "$tmp_dir/libacars" >/dev/null 2>&1 \
|
||||||
|
|| { warn "Failed to clone libacars"; exit 1; }
|
||||||
|
|
||||||
|
cd "$tmp_dir/libacars"
|
||||||
|
mkdir -p build && cd build
|
||||||
|
|
||||||
|
info "Compiling libacars..."
|
||||||
|
if cmake .. >/dev/null 2>&1 && make >/dev/null 2>&1; then
|
||||||
|
$SUDO make install >/dev/null 2>&1
|
||||||
|
$SUDO ldconfig
|
||||||
|
ok "libacars installed"
|
||||||
|
else
|
||||||
|
warn "Failed to build libacars."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build dumpvdl2
|
||||||
|
info "Cloning dumpvdl2..."
|
||||||
|
git clone --depth 1 https://github.com/szpajder/dumpvdl2.git "$tmp_dir/dumpvdl2" >/dev/null 2>&1 \
|
||||||
|
|| { warn "Failed to clone dumpvdl2"; exit 1; }
|
||||||
|
|
||||||
|
cd "$tmp_dir/dumpvdl2"
|
||||||
|
mkdir -p build && cd build
|
||||||
|
|
||||||
|
info "Compiling dumpvdl2..."
|
||||||
|
if cmake .. >/dev/null 2>&1 && make >/dev/null 2>&1; then
|
||||||
|
$SUDO install -m 0755 src/dumpvdl2 /usr/local/bin/dumpvdl2
|
||||||
|
ok "dumpvdl2 installed successfully."
|
||||||
|
else
|
||||||
|
warn "Failed to build dumpvdl2 from source. VDL2 decoding will not be available."
|
||||||
|
fi
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
install_aiscatcher_from_source_debian() {
|
install_aiscatcher_from_source_debian() {
|
||||||
info "AIS-catcher not available via APT. Building from source..."
|
info "AIS-catcher not available via APT. Building from source..."
|
||||||
|
|
||||||
@@ -1344,6 +1472,13 @@ install_debian_packages() {
|
|||||||
fi
|
fi
|
||||||
cmd_exists acarsdec || install_acarsdec_from_source_debian
|
cmd_exists acarsdec || install_acarsdec_from_source_debian
|
||||||
|
|
||||||
|
progress "Installing dumpvdl2 (optional)"
|
||||||
|
if ! cmd_exists dumpvdl2; then
|
||||||
|
install_dumpvdl2_from_source_debian || warn "dumpvdl2 not available. VDL2 decoding will not be available."
|
||||||
|
else
|
||||||
|
ok "dumpvdl2 already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
progress "Installing AIS-catcher"
|
progress "Installing AIS-catcher"
|
||||||
if ! cmd_exists AIS-catcher && ! cmd_exists aiscatcher; then
|
if ! cmd_exists AIS-catcher && ! cmd_exists aiscatcher; then
|
||||||
install_aiscatcher_from_source_debian
|
install_aiscatcher_from_source_debian
|
||||||
|
|||||||
@@ -419,6 +419,163 @@ body {
|
|||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* VDL2 sidebar (left of map, after ACARS) - Collapsible */
|
||||||
|
.vdl2-sidebar {
|
||||||
|
display: none;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
flex-direction: row;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.vdl2-sidebar {
|
||||||
|
display: flex;
|
||||||
|
max-height: calc(100dvh - 160px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-collapse-btn {
|
||||||
|
width: 28px;
|
||||||
|
min-width: 28px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid var(--border-color);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 0;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-collapse-btn:hover {
|
||||||
|
background: rgba(74, 158, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-collapse-label {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
text-orientation: mixed;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar.collapsed .vdl2-collapse-label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar:not(.collapsed) .vdl2-collapse-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#vdl2CollapseIcon {
|
||||||
|
font-size: 10px;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar.collapsed #vdl2CollapseIcon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar-content {
|
||||||
|
width: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: width 0.3s ease, opacity 0.2s ease;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar.collapsed .vdl2-sidebar-content {
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar .panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar .panel::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar .panel-header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar #vdl2PanelContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar .vdl2-info,
|
||||||
|
.vdl2-sidebar .vdl2-controls {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar .vdl2-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar .vdl2-btn {
|
||||||
|
background: var(--accent-green);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar .vdl2-btn:hover {
|
||||||
|
background: #1db954;
|
||||||
|
box-shadow: 0 0 10px rgba(34, 197, 94, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar .vdl2-btn.active {
|
||||||
|
background: var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-sidebar .vdl2-btn.active:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
box-shadow: 0 0 10px rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-message-item {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
font-size: 10px;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vdl2-message-item:hover {
|
||||||
|
background: rgba(74, 158, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
/* Panels */
|
/* Panels */
|
||||||
.panel {
|
.panel {
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
@@ -627,12 +784,18 @@ body {
|
|||||||
/* Selected aircraft panel */
|
/* Selected aircraft panel */
|
||||||
.selected-aircraft {
|
.selected-aircraft {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
max-height: 480px;
|
max-height: 280px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-height: 900px) {
|
||||||
|
.selected-aircraft {
|
||||||
|
max-height: 340px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.selected-info {
|
.selected-info {
|
||||||
padding: 12px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#aircraftPhotoContainer {
|
#aircraftPhotoContainer {
|
||||||
@@ -640,7 +803,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#aircraftPhotoContainer img {
|
#aircraftPhotoContainer img {
|
||||||
max-height: 140px;
|
max-height: 100px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -649,24 +812,24 @@ body {
|
|||||||
|
|
||||||
.selected-callsign {
|
.selected-callsign {
|
||||||
font-family: 'Orbitron', monospace;
|
font-family: 'Orbitron', monospace;
|
||||||
font-size: 20px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
text-shadow: 0 0 15px var(--accent-cyan);
|
text-shadow: 0 0 15px var(--accent-cyan);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.telemetry-grid {
|
.telemetry-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.telemetry-item {
|
.telemetry-item {
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px;
|
padding: 5px 8px;
|
||||||
border-left: 2px solid var(--accent-cyan);
|
border-left: 2px solid var(--accent-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,7 +941,8 @@ body {
|
|||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
border-top: 1px solid rgba(74, 158, 255, 0.3);
|
border-top: 1px solid rgba(74, 158, 255, 0.3);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
overflow: hidden;
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls-bar > .control-group {
|
.controls-bar > .control-group {
|
||||||
@@ -1489,6 +1653,10 @@ body {
|
|||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.strip-stat.source-stat .strip-value {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.strip-stat.session-stat {
|
.strip-stat.session-stat {
|
||||||
background: rgba(34, 197, 94, 0.05);
|
background: rgba(34, 197, 94, 0.05);
|
||||||
border-color: rgba(34, 197, 94, 0.2);
|
border-color: rgba(34, 197, 94, 0.2);
|
||||||
@@ -1779,6 +1947,9 @@ body {
|
|||||||
.strip-btn {
|
.strip-btn {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
background: rgba(74, 158, 255, 0.1);
|
background: rgba(74, 158, 255, 0.1);
|
||||||
border: 1px solid rgba(74, 158, 255, 0.2);
|
border: 1px solid rgba(74, 158, 255, 0.2);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -1789,6 +1960,12 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-btn svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strip-btn:hover:not(:disabled) {
|
.strip-btn:hover:not(:disabled) {
|
||||||
|
|||||||
31
static/css/modes/vdl2.css
Normal file
31
static/css/modes/vdl2.css
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/* VDL2 Mode Styles */
|
||||||
|
|
||||||
|
/* VDL2 Status Indicator */
|
||||||
|
.vdl2-status-dot.listening {
|
||||||
|
background: var(--accent-cyan) !important;
|
||||||
|
animation: vdl2-pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.vdl2-status-dot.receiving {
|
||||||
|
background: var(--accent-green) !important;
|
||||||
|
}
|
||||||
|
.vdl2-status-dot.error {
|
||||||
|
background: var(--accent-red) !important;
|
||||||
|
}
|
||||||
|
@keyframes vdl2-pulse {
|
||||||
|
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(74, 158, 255, 0.7); }
|
||||||
|
50% { opacity: 0.6; box-shadow: 0 0 6px 3px rgba(74, 158, 255, 0.3); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VDL2 message animation */
|
||||||
|
.vdl2-msg {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
animation: vdl2FadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
.vdl2-msg:hover {
|
||||||
|
background: rgba(74, 158, 255, 0.05);
|
||||||
|
}
|
||||||
|
@keyframes vdl2FadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-3px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
@@ -92,8 +92,12 @@
|
|||||||
<span class="strip-value" id="stripAcars">0</span>
|
<span class="strip-value" id="stripAcars">0</span>
|
||||||
<span class="strip-label">ACARS</span>
|
<span class="strip-label">ACARS</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="strip-stat">
|
||||||
|
<span class="strip-value" id="stripVdl2">0</span>
|
||||||
|
<span class="strip-label">VDL2</span>
|
||||||
|
</div>
|
||||||
<div class="strip-stat source-stat" title="Data source (Local or Agent name)">
|
<div class="strip-stat source-stat" title="Data source (Local or Agent name)">
|
||||||
<span class="strip-value" id="stripSource" style="font-size: 10px;">Local</span>
|
<span class="strip-value" id="stripSource">Local</span>
|
||||||
<span class="strip-label">SOURCE</span>
|
<span class="strip-label">SOURCE</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="strip-stat signal-stat" title="Signal quality (messages/errors)">
|
<div class="strip-stat signal-stat" title="Signal quality (messages/errors)">
|
||||||
@@ -106,17 +110,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="strip-divider"></div>
|
<div class="strip-divider"></div>
|
||||||
<button type="button" class="strip-btn" onclick="showSquawkInfo(null)" title="Squawk Code Reference">
|
<button type="button" class="strip-btn" onclick="showSquawkInfo(null)" title="Squawk Code Reference">
|
||||||
📟 Squawk
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2"/><line x1="6" y1="10" x2="6" y2="14"/><line x1="10" y1="10" x2="10" y2="14"/><line x1="14" y1="10" x2="14" y2="14"/><line x1="18" y1="10" x2="18" y2="14"/></svg>
|
||||||
|
Squawk
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="strip-btn" onclick="lookupSelectedFlight()" title="Lookup selected aircraft on FlightAware" id="flightLookupBtn" disabled>
|
<button type="button" class="strip-btn" onclick="lookupSelectedFlight()" title="Lookup selected aircraft on FlightAware" id="flightLookupBtn" disabled>
|
||||||
🔗 Lookup
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||||
|
Lookup
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="strip-btn primary" onclick="generateReport()" title="Generate Session Report">
|
<button type="button" class="strip-btn primary" onclick="generateReport()" title="Generate Session Report">
|
||||||
📊 Report
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
|
||||||
|
Report
|
||||||
</button>
|
</button>
|
||||||
<a class="strip-btn" href="/adsb/history" title="Open History Reporting">
|
<a class="strip-btn" href="/adsb/history" title="Open History Reporting">
|
||||||
📚 History
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||||
|
History
|
||||||
</a>
|
</a>
|
||||||
|
<button type="button" class="strip-btn" onclick="toggleAntennaGuide()" title="1090 MHz Antenna Guide">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12 L12 2 L22 12"/><line x1="12" y1="2" x2="12" y2="22"/><path d="M4.93 4.93 L12 12"/><path d="M19.07 4.93 L12 12"/></svg>
|
||||||
|
Antenna
|
||||||
|
</button>
|
||||||
<div class="strip-divider"></div>
|
<div class="strip-divider"></div>
|
||||||
<div class="strip-status">
|
<div class="strip-status">
|
||||||
<div class="status-dot inactive" id="trackingDot"></div>
|
<div class="status-dot inactive" id="trackingDot"></div>
|
||||||
@@ -176,6 +188,55 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- VDL2 Panel (left of map, after ACARS) - Collapsible -->
|
||||||
|
<div class="vdl2-sidebar" id="vdl2Sidebar">
|
||||||
|
<div class="vdl2-sidebar-content" id="vdl2SidebarContent">
|
||||||
|
<div class="panel vdl2-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>VDL2 MESSAGES</span>
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span id="vdl2Count" style="font-size: 10px; color: var(--accent-cyan);">0</span>
|
||||||
|
<div class="panel-indicator" id="vdl2PanelIndicator"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="vdl2PanelContent">
|
||||||
|
<div class="vdl2-info" style="font-size: 9px; color: var(--text-muted); padding: 5px 8px; border-bottom: 1px solid var(--border-color);">
|
||||||
|
<span style="color: var(--accent-yellow);">⚠</span> Requires separate SDR (VHF ~137 MHz)
|
||||||
|
</div>
|
||||||
|
<div class="vdl2-controls" style="padding: 8px; border-bottom: 1px solid var(--border-color);">
|
||||||
|
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
|
||||||
|
<select id="vdl2DeviceSelect" style="flex: 1; font-size: 10px;">
|
||||||
|
<option value="0">SDR 0</option>
|
||||||
|
<option value="1">SDR 1</option>
|
||||||
|
</select>
|
||||||
|
<select id="vdl2RegionDashSelect" onchange="updateVdl2FreqCheckboxes()" style="flex: 1; font-size: 10px;">
|
||||||
|
<option value="na">N. America</option>
|
||||||
|
<option value="eu">Europe</option>
|
||||||
|
<option value="ap">Asia-Pac</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="vdl2FreqSelector" style="display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; font-size: 9px;">
|
||||||
|
<!-- Frequency checkboxes populated by JS -->
|
||||||
|
</div>
|
||||||
|
<button class="vdl2-btn" id="vdl2ToggleBtn" onclick="toggleVdl2()" style="width: 100%;">
|
||||||
|
▶ START VDL2
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="vdl2-messages" id="vdl2Messages">
|
||||||
|
<div class="no-aircraft" style="padding: 20px; text-align: center;">
|
||||||
|
<div style="font-size: 10px; color: var(--text-muted);">No VDL2 messages</div>
|
||||||
|
<div style="font-size: 9px; color: var(--text-dim); margin-top: 5px;">Start VDL2 to receive digital datalink messages</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="vdl2-collapse-btn" id="vdl2CollapseBtn" onclick="toggleVdl2Sidebar()" title="Toggle VDL2 Panel">
|
||||||
|
<span id="vdl2CollapseIcon">◀</span>
|
||||||
|
<span class="vdl2-collapse-label">VDL2</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Main Display (Map or Radar Scope) -->
|
<!-- Main Display (Map or Radar Scope) -->
|
||||||
<div class="main-display">
|
<div class="main-display">
|
||||||
<div class="display-container">
|
<div class="display-container">
|
||||||
@@ -223,88 +284,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Antenna Guide Panel -->
|
|
||||||
<div class="panel" id="antennaGuidePanel">
|
|
||||||
<div class="panel-header" style="cursor: pointer;" onclick="document.getElementById('antennaGuideContent').style.display = document.getElementById('antennaGuideContent').style.display === 'none' ? 'block' : 'none'; this.querySelector('.panel-toggle').textContent = document.getElementById('antennaGuideContent').style.display === 'none' ? '▶' : '▼';">
|
|
||||||
<span>ANTENNA GUIDE</span>
|
|
||||||
<span class="panel-toggle" style="font-size: 10px; color: var(--text-muted);">▶</span>
|
|
||||||
</div>
|
|
||||||
<div id="antennaGuideContent" style="display: none; padding: 10px; font-size: 11px; color: var(--text-secondary); line-height: 1.5;">
|
|
||||||
<p style="margin-bottom: 8px; color: var(--accent-cyan); font-weight: 600;">
|
|
||||||
1090 MHz — stock SDR antenna can work but is not ideal
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; padding: 8px; margin-bottom: 8px;">
|
|
||||||
<strong style="color: var(--accent-cyan); font-size: 11px;">Stock Telescopic Antenna</strong>
|
|
||||||
<ul style="margin: 4px 0 0 14px; padding: 0; font-size: 10px;">
|
|
||||||
<li><strong style="color: var(--text-primary);">1090 MHz:</strong> Collapse to ~6.9 cm (quarter-wave). It works for nearby aircraft</li>
|
|
||||||
<li><strong style="color: var(--text-primary);">Range:</strong> Expect ~50 NM (90 km) indoors, ~100 NM outdoors</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; padding: 8px; margin-bottom: 8px;">
|
|
||||||
<strong style="color: #00ff88; font-size: 11px;">Recommended: 1090 MHz Collinear (~$10-20 DIY)</strong>
|
|
||||||
<ul style="margin: 4px 0 0 14px; padding: 0; font-size: 10px;">
|
|
||||||
<li><strong style="color: var(--text-primary);">Design:</strong> 8 coaxial collinear elements from RG-6 coax cable</li>
|
|
||||||
<li><strong style="color: var(--text-primary);">Element length:</strong> ~6.9 cm segments soldered alternating center/shield</li>
|
|
||||||
<li><strong style="color: var(--text-primary);">Gain:</strong> ~5–7 dBi omnidirectional, ideal for 360° coverage</li>
|
|
||||||
<li><strong style="color: var(--text-primary);">Range:</strong> 150–250+ NM depending on height and LOS</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; padding: 8px; margin-bottom: 8px;">
|
|
||||||
<strong style="color: var(--accent-cyan); font-size: 11px;">Commercial Options</strong>
|
|
||||||
<ul style="margin: 4px 0 0 14px; padding: 0; font-size: 10px;">
|
|
||||||
<li><strong style="color: var(--text-primary);">FlightAware antenna:</strong> ~$35, 1090 MHz tuned, 66cm fiberglass whip</li>
|
|
||||||
<li><strong style="color: var(--text-primary);">ADSBexchange whip:</strong> ~$40, similar performance</li>
|
|
||||||
<li><strong style="color: var(--text-primary);">Jetvision A3:</strong> ~$50, high-gain 1090 MHz collinear</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; padding: 8px; margin-bottom: 8px;">
|
|
||||||
<strong style="color: var(--accent-cyan); font-size: 11px;">Placement & LNA</strong>
|
|
||||||
<ul style="margin: 4px 0 0 14px; padding: 0; font-size: 10px;">
|
|
||||||
<li><strong style="color: var(--text-primary);">Location:</strong> OUTDOORS, as high as possible. Roof or mast mount</li>
|
|
||||||
<li><strong style="color: var(--text-primary);">Height:</strong> Every 3m higher adds ~10 NM range (line-of-sight)</li>
|
|
||||||
<li><strong style="color: var(--text-primary);">LNA:</strong> 1090 MHz filtered LNA at antenna feed (e.g. Uputronics, ~$30)</li>
|
|
||||||
<li><strong style="color: var(--text-primary);">Filter:</strong> A 1090 MHz bandpass filter removes cell/FM interference</li>
|
|
||||||
<li><strong style="color: var(--text-primary);">Coax:</strong> Keep short. At 1090 MHz, RG-58 loses ~10 dB per 10m</li>
|
|
||||||
<li><strong style="color: var(--text-primary);">Bias-T:</strong> Enable Bias-T in controls above if LNA is powered via coax</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; padding: 8px;">
|
|
||||||
<strong style="color: var(--accent-cyan); font-size: 11px;">Quick Reference</strong>
|
|
||||||
<table style="width: 100%; margin-top: 4px; font-size: 10px; border-collapse: collapse;">
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 2px 4px; color: var(--text-dim);">ADS-B frequency</td>
|
|
||||||
<td style="padding: 2px 4px; color: var(--text-primary); text-align: right;">1090 MHz</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 2px 4px; color: var(--text-dim);">Quarter-wave length</td>
|
|
||||||
<td style="padding: 2px 4px; color: var(--text-primary); text-align: right;">6.9 cm</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 2px 4px; color: var(--text-dim);">Modulation</td>
|
|
||||||
<td style="padding: 2px 4px; color: var(--text-primary); text-align: right;">PPM (pulse)</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 2px 4px; color: var(--text-dim);">Polarization</td>
|
|
||||||
<td style="padding: 2px 4px; color: var(--text-primary); text-align: right;">Vertical</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
|
||||||
<td style="padding: 2px 4px; color: var(--text-dim);">Bandwidth</td>
|
|
||||||
<td style="padding: 2px 4px; color: var(--text-primary); text-align: right;">~2 MHz</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 2px 4px; color: var(--text-dim);">Typical range (outdoor)</td>
|
|
||||||
<td style="padding: 2px 4px; color: var(--text-primary); text-align: right;">100–250 NM</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Controls Bar - Reorganized -->
|
<!-- Controls Bar - Reorganized -->
|
||||||
@@ -4115,6 +4094,211 @@ sudo make install</code>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// VDL2 DATALINK PANEL
|
||||||
|
// ============================================
|
||||||
|
let vdl2EventSource = null;
|
||||||
|
let isVdl2Running = false;
|
||||||
|
let vdl2MessageCount = 0;
|
||||||
|
let vdl2SidebarCollapsed = localStorage.getItem('vdl2SidebarCollapsed') !== 'false';
|
||||||
|
let vdl2Frequencies = {
|
||||||
|
'na': ['136975000', '136100000', '136650000', '136700000', '136800000'],
|
||||||
|
'eu': ['136975000', '136675000', '136725000', '136775000', '136825000'],
|
||||||
|
'ap': ['136975000', '136900000']
|
||||||
|
};
|
||||||
|
let vdl2FreqLabels = {
|
||||||
|
'136975000': '136.975', '136100000': '136.100', '136650000': '136.650',
|
||||||
|
'136700000': '136.700', '136800000': '136.800', '136675000': '136.675',
|
||||||
|
'136725000': '136.725', '136775000': '136.775', '136825000': '136.825',
|
||||||
|
'136900000': '136.900'
|
||||||
|
};
|
||||||
|
|
||||||
|
function toggleVdl2Sidebar() {
|
||||||
|
const sidebar = document.getElementById('vdl2Sidebar');
|
||||||
|
vdl2SidebarCollapsed = !vdl2SidebarCollapsed;
|
||||||
|
sidebar.classList.toggle('collapsed', vdl2SidebarCollapsed);
|
||||||
|
localStorage.setItem('vdl2SidebarCollapsed', vdl2SidebarCollapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const sidebar = document.getElementById('vdl2Sidebar');
|
||||||
|
if (sidebar && vdl2SidebarCollapsed) {
|
||||||
|
sidebar.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
updateVdl2FreqCheckboxes();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateVdl2FreqCheckboxes() {
|
||||||
|
const region = document.getElementById('vdl2RegionDashSelect').value;
|
||||||
|
const freqs = vdl2Frequencies[region] || vdl2Frequencies['na'];
|
||||||
|
const container = document.getElementById('vdl2FreqSelector');
|
||||||
|
|
||||||
|
const previouslyChecked = new Set();
|
||||||
|
container.querySelectorAll('input:checked').forEach(cb => previouslyChecked.add(cb.value));
|
||||||
|
|
||||||
|
container.innerHTML = freqs.map(freq => {
|
||||||
|
const checked = previouslyChecked.size === 0 || previouslyChecked.has(freq) ? 'checked' : '';
|
||||||
|
const label = vdl2FreqLabels[freq] || freq;
|
||||||
|
return `
|
||||||
|
<label style="display: flex; align-items: center; gap: 3px; padding: 2px 6px; background: var(--bg-secondary); border-radius: 3px; cursor: pointer;">
|
||||||
|
<input type="checkbox" class="vdl2-freq-cb" value="${freq}" ${checked} style="margin: 0; cursor: pointer;">
|
||||||
|
<span>${label}</span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVdl2RegionFreqs() {
|
||||||
|
const checkboxes = document.querySelectorAll('.vdl2-freq-cb:checked');
|
||||||
|
const selectedFreqs = Array.from(checkboxes).map(cb => cb.value);
|
||||||
|
if (selectedFreqs.length === 0) {
|
||||||
|
const region = document.getElementById('vdl2RegionDashSelect').value;
|
||||||
|
return vdl2Frequencies[region] || vdl2Frequencies['na'];
|
||||||
|
}
|
||||||
|
return selectedFreqs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleVdl2() {
|
||||||
|
if (isVdl2Running) {
|
||||||
|
stopVdl2();
|
||||||
|
} else {
|
||||||
|
startVdl2();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startVdl2() {
|
||||||
|
const vdl2Select = document.getElementById('vdl2DeviceSelect');
|
||||||
|
const device = vdl2Select.value;
|
||||||
|
const sdr_type = vdl2Select.selectedOptions[0]?.dataset.sdrType || 'rtlsdr';
|
||||||
|
const frequencies = getVdl2RegionFreqs();
|
||||||
|
|
||||||
|
if (isTracking && device === '0') {
|
||||||
|
const useAnyway = confirm(
|
||||||
|
'Warning: ADS-B tracking may be using SDR device 0.\n\n' +
|
||||||
|
'VDL2 uses VHF frequencies (~137 MHz) while ADS-B uses 1090 MHz.\n' +
|
||||||
|
'You need TWO separate SDR devices to receive both simultaneously.\n\n' +
|
||||||
|
'Click OK to start VDL2 on device ' + device + ' anyway.'
|
||||||
|
);
|
||||||
|
if (!useAnyway) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/vdl2/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ device, frequencies, gain: '40', sdr_type })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'started') {
|
||||||
|
isVdl2Running = true;
|
||||||
|
vdl2MessageCount = 0;
|
||||||
|
document.getElementById('vdl2ToggleBtn').innerHTML = '■ STOP VDL2';
|
||||||
|
document.getElementById('vdl2ToggleBtn').classList.add('active');
|
||||||
|
document.getElementById('vdl2PanelIndicator').classList.add('active');
|
||||||
|
startVdl2Stream();
|
||||||
|
} else {
|
||||||
|
alert('VDL2 Error: ' + (data.message || 'Failed to start'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => alert('VDL2 Error: ' + err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopVdl2() {
|
||||||
|
fetch('/vdl2/stop', { method: 'POST' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(() => {
|
||||||
|
isVdl2Running = false;
|
||||||
|
document.getElementById('vdl2ToggleBtn').innerHTML = '▶ START VDL2';
|
||||||
|
document.getElementById('vdl2ToggleBtn').classList.remove('active');
|
||||||
|
document.getElementById('vdl2PanelIndicator').classList.remove('active');
|
||||||
|
if (vdl2EventSource) {
|
||||||
|
vdl2EventSource.close();
|
||||||
|
vdl2EventSource = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startVdl2Stream() {
|
||||||
|
if (vdl2EventSource) vdl2EventSource.close();
|
||||||
|
|
||||||
|
vdl2EventSource = new EventSource('/vdl2/stream');
|
||||||
|
vdl2EventSource.onmessage = function(e) {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
if (data.type === 'vdl2') {
|
||||||
|
vdl2MessageCount++;
|
||||||
|
if (typeof stats !== 'undefined') stats.vdl2Messages = (stats.vdl2Messages || 0) + 1;
|
||||||
|
document.getElementById('vdl2Count').textContent = vdl2MessageCount;
|
||||||
|
document.getElementById('stripVdl2').textContent = vdl2MessageCount;
|
||||||
|
addVdl2Message(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
vdl2EventSource.onerror = function() {
|
||||||
|
console.error('VDL2 stream error');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addVdl2Message(data) {
|
||||||
|
const container = document.getElementById('vdl2Messages');
|
||||||
|
|
||||||
|
const placeholder = container.querySelector('.no-aircraft');
|
||||||
|
if (placeholder) placeholder.remove();
|
||||||
|
|
||||||
|
const msg = document.createElement('div');
|
||||||
|
msg.className = 'vdl2-message-item';
|
||||||
|
msg.style.cssText = 'padding: 6px 8px; border-bottom: 1px solid var(--border-color); font-size: 10px;';
|
||||||
|
|
||||||
|
const station = data.station || '';
|
||||||
|
const avlc = data.avlc || {};
|
||||||
|
const src = avlc.src?.addr || '';
|
||||||
|
const dst = avlc.dst?.addr || '';
|
||||||
|
const acars = avlc.acars || {};
|
||||||
|
const flight = acars.flight || '';
|
||||||
|
const msgText = acars.msg_text || '';
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
const freq = data.freq ? (data.freq / 1000000).toFixed(3) : '';
|
||||||
|
|
||||||
|
msg.innerHTML = `
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 2px;">
|
||||||
|
<span style="color: var(--accent-cyan); font-weight: bold;">${flight || src || 'VDL2'}</span>
|
||||||
|
<span style="color: var(--text-muted);">${time}</span>
|
||||||
|
</div>
|
||||||
|
${freq ? `<div style="color: var(--text-dim); font-size: 9px;">${freq} MHz</div>` : ''}
|
||||||
|
${dst ? `<div style="color: var(--text-muted); font-size: 9px;">To: ${dst}</div>` : ''}
|
||||||
|
${msgText ? `<div style="color: var(--text-primary); margin-top: 3px; word-break: break-word;">${msgText}</div>` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.insertBefore(msg, container.firstChild);
|
||||||
|
|
||||||
|
while (container.children.length > 50) {
|
||||||
|
container.removeChild(container.lastChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate VDL2 device selector
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
fetch('/devices')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(devices => {
|
||||||
|
const select = document.getElementById('vdl2DeviceSelect');
|
||||||
|
select.innerHTML = '';
|
||||||
|
if (devices.length === 0) {
|
||||||
|
select.innerHTML = '<option value="0">No SDR detected</option>';
|
||||||
|
} else {
|
||||||
|
devices.forEach((d, i) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = d.index || i;
|
||||||
|
opt.dataset.sdrType = d.sdr_type || 'rtlsdr';
|
||||||
|
opt.textContent = `SDR ${d.index || i}: ${d.name || d.type || 'SDR'}`;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
if (devices.length > 1) {
|
||||||
|
select.value = '1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// SQUAWK CODE REFERENCE
|
// SQUAWK CODE REFERENCE
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -4174,6 +4358,18 @@ sudo make install</code>
|
|||||||
document.getElementById('squawkModal')?.addEventListener('click', (e) => {
|
document.getElementById('squawkModal')?.addEventListener('click', (e) => {
|
||||||
if (e.target.id === 'squawkModal') closeSquawkModal();
|
if (e.target.id === 'squawkModal') closeSquawkModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ANTENNA GUIDE
|
||||||
|
// ============================================
|
||||||
|
function toggleAntennaGuide() {
|
||||||
|
const modal = document.getElementById('antennaGuideModal');
|
||||||
|
modal.classList.toggle('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('antennaGuideModal')?.addEventListener('click', (e) => {
|
||||||
|
if (e.target.id === 'antennaGuideModal') toggleAntennaGuide();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Squawk Code Reference Modal -->
|
<!-- Squawk Code Reference Modal -->
|
||||||
@@ -4227,6 +4423,72 @@ sudo make install</code>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Antenna Guide Modal -->
|
||||||
|
<div id="antennaGuideModal" class="antenna-guide-modal">
|
||||||
|
<div class="antenna-guide-modal-content">
|
||||||
|
<div class="antenna-guide-modal-header">
|
||||||
|
<span>ANTENNA GUIDE — 1090 MHz ADS-B</span>
|
||||||
|
<button class="antenna-guide-modal-close" onclick="toggleAntennaGuide()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="antenna-guide-modal-body">
|
||||||
|
<p style="margin-bottom: 10px; color: var(--accent-cyan, #00d4ff); font-weight: 600; font-size: 12px;">
|
||||||
|
1090 MHz — stock SDR antenna can work but is not ideal
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="antenna-section">
|
||||||
|
<strong style="color: var(--accent-cyan, #00d4ff);">Stock Telescopic Antenna</strong>
|
||||||
|
<ul>
|
||||||
|
<li><strong>1090 MHz:</strong> Collapse to ~6.9 cm (quarter-wave). It works for nearby aircraft</li>
|
||||||
|
<li><strong>Range:</strong> Expect ~50 NM (90 km) indoors, ~100 NM outdoors</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="antenna-section recommended">
|
||||||
|
<strong style="color: #00ff88;">Recommended: 1090 MHz Collinear (~$10-20 DIY)</strong>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Design:</strong> 8 coaxial collinear elements from RG-6 coax cable</li>
|
||||||
|
<li><strong>Element length:</strong> ~6.9 cm segments soldered alternating center/shield</li>
|
||||||
|
<li><strong>Gain:</strong> ~5–7 dBi omnidirectional, ideal for 360° coverage</li>
|
||||||
|
<li><strong>Range:</strong> 150–250+ NM depending on height and LOS</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="antenna-section">
|
||||||
|
<strong style="color: var(--accent-cyan, #00d4ff);">Commercial Options</strong>
|
||||||
|
<ul>
|
||||||
|
<li><strong>FlightAware antenna:</strong> ~$35, 1090 MHz tuned, 66cm fiberglass whip</li>
|
||||||
|
<li><strong>ADSBexchange whip:</strong> ~$40, similar performance</li>
|
||||||
|
<li><strong>Jetvision A3:</strong> ~$50, high-gain 1090 MHz collinear</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="antenna-section">
|
||||||
|
<strong style="color: var(--accent-cyan, #00d4ff);">Placement & LNA</strong>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Location:</strong> OUTDOORS, as high as possible. Roof or mast mount</li>
|
||||||
|
<li><strong>Height:</strong> Every 3m higher adds ~10 NM range (line-of-sight)</li>
|
||||||
|
<li><strong>LNA:</strong> 1090 MHz filtered LNA at antenna feed (e.g. Uputronics, ~$30)</li>
|
||||||
|
<li><strong>Filter:</strong> A 1090 MHz bandpass filter removes cell/FM interference</li>
|
||||||
|
<li><strong>Coax:</strong> Keep short. At 1090 MHz, RG-58 loses ~10 dB per 10m</li>
|
||||||
|
<li><strong>Bias-T:</strong> Enable Bias-T in controls above if LNA is powered via coax</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="antenna-section">
|
||||||
|
<strong style="color: var(--accent-cyan, #00d4ff);">Quick Reference</strong>
|
||||||
|
<table class="antenna-ref-table">
|
||||||
|
<tr><td>ADS-B frequency</td><td>1090 MHz</td></tr>
|
||||||
|
<tr><td>Quarter-wave length</td><td>6.9 cm</td></tr>
|
||||||
|
<tr><td>Modulation</td><td>PPM (pulse)</td></tr>
|
||||||
|
<tr><td>Polarization</td><td>Vertical</td></tr>
|
||||||
|
<tr><td>Bandwidth</td><td>~2 MHz</td></tr>
|
||||||
|
<tr><td>Typical range (outdoor)</td><td>100–250 NM</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.squawk-clickable {
|
.squawk-clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -4374,6 +4636,108 @@ sudo make install</code>
|
|||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Antenna Guide Modal */
|
||||||
|
.antenna-guide-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
z-index: 1000;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.antenna-guide-modal.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.antenna-guide-modal-content {
|
||||||
|
background: var(--bg-secondary, #1a1a1a);
|
||||||
|
border: 1px solid var(--border-color, #333);
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 480px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.antenna-guide-modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--bg-tertiary, #252525);
|
||||||
|
border-bottom: 1px solid var(--border-color, #333);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--accent-cyan, #00d4ff);
|
||||||
|
}
|
||||||
|
.antenna-guide-modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.antenna-guide-modal-close:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.antenna-guide-modal-body {
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary, #999);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.antenna-section {
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border: 1px solid var(--border-color, #333);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.antenna-section strong {
|
||||||
|
font-size: 11px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.antenna-section ul {
|
||||||
|
margin: 4px 0 0 14px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.antenna-section ul li {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.antenna-section ul li strong {
|
||||||
|
display: inline;
|
||||||
|
color: var(--text-primary, #fff);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.antenna-ref-table {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.antenna-ref-table tr {
|
||||||
|
border-bottom: 1px solid var(--border-color, #333);
|
||||||
|
}
|
||||||
|
.antenna-ref-table td {
|
||||||
|
padding: 3px 4px;
|
||||||
|
}
|
||||||
|
.antenna-ref-table td:first-child {
|
||||||
|
color: var(--text-dim, #666);
|
||||||
|
}
|
||||||
|
.antenna-ref-table td:last-child {
|
||||||
|
color: var(--text-primary, #fff);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
/* Watchlist Modal */
|
/* Watchlist Modal */
|
||||||
.watchlist-modal {
|
.watchlist-modal {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/acars.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/acars.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/vdl2.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/aprs.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/aprs.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/tscm.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/tscm.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-cards.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-cards.css') }}">
|
||||||
@@ -569,6 +570,10 @@
|
|||||||
|
|
||||||
{% include 'partials/modes/bt_locate.html' %}
|
{% include 'partials/modes/bt_locate.html' %}
|
||||||
|
|
||||||
|
{% include 'partials/modes/acars.html' %}
|
||||||
|
|
||||||
|
{% include 'partials/modes/vdl2.html' %}
|
||||||
|
|
||||||
<button class="preset-btn" onclick="killAll()"
|
<button class="preset-btn" onclick="killAll()"
|
||||||
style="width: 100%; margin-top: 10px; border-color: #ff3366; color: #ff3366;">
|
style="width: 100%; margin-top: 10px; border-color: #ff3366; color: #ff3366;">
|
||||||
Kill All Processes
|
Kill All Processes
|
||||||
@@ -3158,7 +3163,8 @@
|
|||||||
const validModes = new Set([
|
const validModes = new Set([
|
||||||
'pager', 'sensor', 'rtlamr', 'aprs', 'listening',
|
'pager', 'sensor', 'rtlamr', 'aprs', 'listening',
|
||||||
'spystations', 'meshtastic', 'wifi', 'bluetooth', 'bt_locate',
|
'spystations', 'meshtastic', 'wifi', 'bluetooth', 'bt_locate',
|
||||||
'tscm', 'satellite', 'sstv', 'weathersat', 'sstv_general', 'gps', 'websdr', 'subghz'
|
'tscm', 'satellite', 'sstv', 'weathersat', 'sstv_general', 'gps', 'websdr', 'subghz',
|
||||||
|
'acars', 'vdl2'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function getModeFromQuery() {
|
function getModeFromQuery() {
|
||||||
@@ -3617,7 +3623,7 @@
|
|||||||
'wifi': 'wireless', 'bluetooth': 'wireless', 'bt_locate': 'wireless',
|
'wifi': 'wireless', 'bluetooth': 'wireless', 'bt_locate': 'wireless',
|
||||||
'tscm': 'security',
|
'tscm': 'security',
|
||||||
'rtlamr': 'sdr', 'ais': 'sdr', 'spystations': 'sdr',
|
'rtlamr': 'sdr', 'ais': 'sdr', 'spystations': 'sdr',
|
||||||
'meshtastic': 'sdr',
|
'meshtastic': 'sdr', 'acars': 'sdr', 'vdl2': 'sdr',
|
||||||
'satellite': 'space', 'sstv': 'space', 'weathersat': 'space', 'sstv_general': 'space', 'gps': 'space',
|
'satellite': 'space', 'sstv': 'space', 'weathersat': 'space', 'sstv_general': 'space', 'gps': 'space',
|
||||||
'subghz': 'sdr'
|
'subghz': 'sdr'
|
||||||
};
|
};
|
||||||
@@ -3722,6 +3728,8 @@
|
|||||||
document.getElementById('dmrMode')?.classList.toggle('active', mode === 'dmr');
|
document.getElementById('dmrMode')?.classList.toggle('active', mode === 'dmr');
|
||||||
document.getElementById('websdrMode')?.classList.toggle('active', mode === 'websdr');
|
document.getElementById('websdrMode')?.classList.toggle('active', mode === 'websdr');
|
||||||
document.getElementById('subghzMode')?.classList.toggle('active', mode === 'subghz');
|
document.getElementById('subghzMode')?.classList.toggle('active', mode === 'subghz');
|
||||||
|
document.getElementById('acarsMode')?.classList.toggle('active', mode === 'acars');
|
||||||
|
document.getElementById('vdl2Mode')?.classList.toggle('active', mode === 'vdl2');
|
||||||
const pagerStats = document.getElementById('pagerStats');
|
const pagerStats = document.getElementById('pagerStats');
|
||||||
const sensorStats = document.getElementById('sensorStats');
|
const sensorStats = document.getElementById('sensorStats');
|
||||||
const satelliteStats = document.getElementById('satelliteStats');
|
const satelliteStats = document.getElementById('satelliteStats');
|
||||||
@@ -3762,7 +3770,9 @@
|
|||||||
'meshtastic': 'MESHTASTIC',
|
'meshtastic': 'MESHTASTIC',
|
||||||
'dmr': 'DIGITAL VOICE',
|
'dmr': 'DIGITAL VOICE',
|
||||||
'websdr': 'WEBSDR',
|
'websdr': 'WEBSDR',
|
||||||
'subghz': 'SUBGHZ'
|
'subghz': 'SUBGHZ',
|
||||||
|
'acars': 'ACARS',
|
||||||
|
'vdl2': 'VDL2'
|
||||||
};
|
};
|
||||||
const activeModeIndicator = document.getElementById('activeModeIndicator');
|
const activeModeIndicator = document.getElementById('activeModeIndicator');
|
||||||
if (activeModeIndicator) activeModeIndicator.innerHTML = '<span class="pulse-dot"></span>' + (modeNames[mode] || mode.toUpperCase());
|
if (activeModeIndicator) activeModeIndicator.innerHTML = '<span class="pulse-dot"></span>' + (modeNames[mode] || mode.toUpperCase());
|
||||||
@@ -3836,7 +3846,9 @@
|
|||||||
'meshtastic': 'Meshtastic Mesh Monitor',
|
'meshtastic': 'Meshtastic Mesh Monitor',
|
||||||
'dmr': 'Digital Voice Decoder',
|
'dmr': 'Digital Voice Decoder',
|
||||||
'websdr': 'HF/Shortwave WebSDR',
|
'websdr': 'HF/Shortwave WebSDR',
|
||||||
'subghz': 'SubGHz Transceiver'
|
'subghz': 'SubGHz Transceiver',
|
||||||
|
'acars': 'ACARS Aircraft Messaging',
|
||||||
|
'vdl2': 'VDL2 Aircraft Datalink'
|
||||||
};
|
};
|
||||||
const outputTitle = document.getElementById('outputTitle');
|
const outputTitle = document.getElementById('outputTitle');
|
||||||
if (outputTitle) outputTitle.textContent = titles[mode] || 'Signal Monitor';
|
if (outputTitle) outputTitle.textContent = titles[mode] || 'Signal Monitor';
|
||||||
@@ -3854,7 +3866,7 @@
|
|||||||
const reconBtn = document.getElementById('reconBtn');
|
const reconBtn = document.getElementById('reconBtn');
|
||||||
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
|
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
|
||||||
const reconPanel = document.getElementById('reconPanel');
|
const reconPanel = document.getElementById('reconPanel');
|
||||||
if (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'gps' || mode === 'listening' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'dmr' || mode === 'websdr' || mode === 'subghz') {
|
if (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'gps' || mode === 'listening' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'dmr' || mode === 'websdr' || mode === 'subghz' || mode === 'acars' || mode === 'vdl2') {
|
||||||
if (reconPanel) reconPanel.style.display = 'none';
|
if (reconPanel) reconPanel.style.display = 'none';
|
||||||
if (reconBtn) reconBtn.style.display = 'none';
|
if (reconBtn) reconBtn.style.display = 'none';
|
||||||
if (intelBtn) intelBtn.style.display = 'none';
|
if (intelBtn) intelBtn.style.display = 'none';
|
||||||
@@ -3869,12 +3881,12 @@
|
|||||||
|
|
||||||
// Show agent selector for modes that support remote agents
|
// Show agent selector for modes that support remote agents
|
||||||
const agentSection = document.getElementById('agentSection');
|
const agentSection = document.getElementById('agentSection');
|
||||||
const agentModes = ['pager', 'sensor', 'rtlamr', 'listening', 'aprs', 'wifi', 'bluetooth', 'aircraft', 'tscm', 'ais', 'acars', 'dsc'];
|
const agentModes = ['pager', 'sensor', 'rtlamr', 'listening', 'aprs', 'wifi', 'bluetooth', 'aircraft', 'tscm', 'ais', 'acars', 'vdl2', 'dsc'];
|
||||||
if (agentSection) agentSection.style.display = agentModes.includes(mode) ? 'block' : 'none';
|
if (agentSection) agentSection.style.display = agentModes.includes(mode) ? 'block' : 'none';
|
||||||
|
|
||||||
// Show RTL-SDR device section for modes that use it
|
// Show RTL-SDR device section for modes that use it
|
||||||
const rtlDeviceSection = document.getElementById('rtlDeviceSection');
|
const rtlDeviceSection = document.getElementById('rtlDeviceSection');
|
||||||
if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'listening' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'dmr') ? 'block' : 'none';
|
if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'listening' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'dmr' || mode === 'acars' || mode === 'vdl2') ? 'block' : 'none';
|
||||||
|
|
||||||
// Show waterfall panel if running in listening mode
|
// Show waterfall panel if running in listening mode
|
||||||
const waterfallPanel = document.getElementById('waterfallPanel');
|
const waterfallPanel = document.getElementById('waterfallPanel');
|
||||||
|
|||||||
208
templates/partials/modes/acars.html
Normal file
208
templates/partials/modes/acars.html
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
<!-- ACARS AIRCRAFT MESSAGING MODE -->
|
||||||
|
<div id="acarsMode" class="mode-content" style="display: none;">
|
||||||
|
<div class="section">
|
||||||
|
<h3>ACARS Messaging</h3>
|
||||||
|
<div class="info-text" style="margin-bottom: 15px;">
|
||||||
|
Decode ACARS (Aircraft Communications Addressing and Reporting System) messages on VHF frequencies (~129-131 MHz). Captures flight data, weather reports, position updates, and operational messages from aircraft.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Region & Frequencies</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Region</label>
|
||||||
|
<select id="acarsRegionSelect" onchange="updateAcarsMainFreqs()" style="width: 100%;">
|
||||||
|
<option value="na">North America</option>
|
||||||
|
<option value="eu">Europe</option>
|
||||||
|
<option value="ap">Asia-Pacific</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="acarsMainFreqSelector" style="display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; font-size: 11px;">
|
||||||
|
<!-- Populated by JS -->
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Gain (dB, 0 = auto)</label>
|
||||||
|
<input type="number" id="acarsGainInput" value="40" min="0" max="50" placeholder="0-50">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Status</h3>
|
||||||
|
<div id="acarsStatusDisplay" class="info-text">
|
||||||
|
<p>Status: <span id="acarsStatusText" style="color: var(--accent-yellow);">Standby</span></p>
|
||||||
|
<p>Messages: <span id="acarsMessageCount">0</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antenna Guide -->
|
||||||
|
<div class="section">
|
||||||
|
<h3>Antenna Guide</h3>
|
||||||
|
<div style="font-size: 11px; color: var(--text-dim); line-height: 1.5;">
|
||||||
|
<p style="margin-bottom: 8px; color: var(--accent-cyan); font-weight: 600;">
|
||||||
|
VHF Airband (~130 MHz) — stock SDR antenna may work at close range
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; margin-bottom: 10px;">
|
||||||
|
<strong style="color: var(--accent-cyan); font-size: 12px;">Simple Dipole</strong>
|
||||||
|
<ul style="margin: 6px 0 0 14px; padding: 0;">
|
||||||
|
<li><strong style="color: var(--text-primary);">Element length:</strong> ~57 cm each (quarter-wave at 130 MHz)</li>
|
||||||
|
<li><strong style="color: var(--text-primary);">Material:</strong> Wire, coat hanger, or copper rod</li>
|
||||||
|
<li><strong style="color: var(--text-primary);">Orientation:</strong> Vertical (airband is vertically polarized)</li>
|
||||||
|
<li><strong style="color: var(--text-primary);">Placement:</strong> Outdoors, as high as possible with clear sky view</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px;">
|
||||||
|
<strong style="color: var(--accent-cyan); font-size: 12px;">Quick Reference</strong>
|
||||||
|
<table style="width: 100%; margin-top: 6px; font-size: 10px; border-collapse: collapse;">
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-dim);">Primary (worldwide)</td>
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">131.550 MHz</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-dim);">Quarter-wave length</td>
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">57 cm</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-dim);">Modulation</td>
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">AM MSK 2400 baud</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-dim);">Polarization</td>
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">Vertical</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="run-btn" id="startAcarsBtn" onclick="startAcarsMode()">
|
||||||
|
Start ACARS
|
||||||
|
</button>
|
||||||
|
<button class="stop-btn" id="stopAcarsBtn" onclick="stopAcarsMode()" style="display: none;">
|
||||||
|
Stop ACARS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let acarsMainEventSource = null;
|
||||||
|
let acarsMainMsgCount = 0;
|
||||||
|
|
||||||
|
const acarsMainFrequencies = {
|
||||||
|
'na': ['129.125', '130.025', '130.450', '131.550'],
|
||||||
|
'eu': ['131.525', '131.725', '131.550'],
|
||||||
|
'ap': ['131.550', '131.450']
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateAcarsMainFreqs() {
|
||||||
|
const region = document.getElementById('acarsRegionSelect').value;
|
||||||
|
const freqs = acarsMainFrequencies[region] || acarsMainFrequencies['na'];
|
||||||
|
const container = document.getElementById('acarsMainFreqSelector');
|
||||||
|
|
||||||
|
const previouslyChecked = new Set();
|
||||||
|
container.querySelectorAll('input:checked').forEach(cb => previouslyChecked.add(cb.value));
|
||||||
|
|
||||||
|
container.innerHTML = freqs.map(freq => {
|
||||||
|
const checked = previouslyChecked.size === 0 || previouslyChecked.has(freq) ? 'checked' : '';
|
||||||
|
return `
|
||||||
|
<label style="display: flex; align-items: center; gap: 3px; padding: 2px 6px; background: var(--bg-secondary); border-radius: 3px; cursor: pointer;">
|
||||||
|
<input type="checkbox" class="acars-main-freq-cb" value="${freq}" ${checked} style="margin: 0; cursor: pointer;">
|
||||||
|
<span>${freq}</span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAcarsMainSelectedFreqs() {
|
||||||
|
const checkboxes = document.querySelectorAll('.acars-main-freq-cb:checked');
|
||||||
|
const selected = Array.from(checkboxes).map(cb => cb.value);
|
||||||
|
if (selected.length === 0) {
|
||||||
|
const region = document.getElementById('acarsRegionSelect').value;
|
||||||
|
return acarsMainFrequencies[region] || acarsMainFrequencies['na'];
|
||||||
|
}
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAcarsMode() {
|
||||||
|
const gain = document.getElementById('acarsGainInput').value || '40';
|
||||||
|
const device = document.getElementById('deviceSelect')?.value || '0';
|
||||||
|
const frequencies = getAcarsMainSelectedFreqs();
|
||||||
|
|
||||||
|
fetch('/acars/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ device, gain, frequencies })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'started') {
|
||||||
|
document.getElementById('startAcarsBtn').style.display = 'none';
|
||||||
|
document.getElementById('stopAcarsBtn').style.display = 'block';
|
||||||
|
document.getElementById('acarsStatusText').textContent = 'Listening';
|
||||||
|
document.getElementById('acarsStatusText').style.color = 'var(--accent-green)';
|
||||||
|
acarsMainMsgCount = 0;
|
||||||
|
startAcarsMainSSE();
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'Failed to start ACARS');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => alert('Error: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAcarsMode() {
|
||||||
|
fetch('/acars/stop', { method: 'POST' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(() => {
|
||||||
|
document.getElementById('startAcarsBtn').style.display = 'block';
|
||||||
|
document.getElementById('stopAcarsBtn').style.display = 'none';
|
||||||
|
document.getElementById('acarsStatusText').textContent = 'Standby';
|
||||||
|
document.getElementById('acarsStatusText').style.color = 'var(--accent-yellow)';
|
||||||
|
if (acarsMainEventSource) {
|
||||||
|
acarsMainEventSource.close();
|
||||||
|
acarsMainEventSource = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAcarsMainSSE() {
|
||||||
|
if (acarsMainEventSource) acarsMainEventSource.close();
|
||||||
|
|
||||||
|
acarsMainEventSource = new EventSource('/acars/stream');
|
||||||
|
acarsMainEventSource.onmessage = function(e) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
if (data.type === 'acars') {
|
||||||
|
acarsMainMsgCount++;
|
||||||
|
document.getElementById('acarsMessageCount').textContent = acarsMainMsgCount;
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
acarsMainEventSource.onerror = function() {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.getElementById('stopAcarsBtn').style.display === 'block') {
|
||||||
|
startAcarsMainSSE();
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check initial status
|
||||||
|
fetch('/acars/status')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.running) {
|
||||||
|
document.getElementById('startAcarsBtn').style.display = 'none';
|
||||||
|
document.getElementById('stopAcarsBtn').style.display = 'block';
|
||||||
|
document.getElementById('acarsStatusText').textContent = 'Listening';
|
||||||
|
document.getElementById('acarsStatusText').style.color = 'var(--accent-green)';
|
||||||
|
document.getElementById('acarsMessageCount').textContent = data.message_count || 0;
|
||||||
|
acarsMainMsgCount = data.message_count || 0;
|
||||||
|
startAcarsMainSSE();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
// Initialize frequency checkboxes
|
||||||
|
document.addEventListener('DOMContentLoaded', () => updateAcarsMainFreqs());
|
||||||
|
</script>
|
||||||
228
templates/partials/modes/vdl2.html
Normal file
228
templates/partials/modes/vdl2.html
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<!-- VDL2 AIRCRAFT DATALINK MODE -->
|
||||||
|
<div id="vdl2Mode" class="mode-content" style="display: none;">
|
||||||
|
<div class="section">
|
||||||
|
<h3>VDL2 Datalink</h3>
|
||||||
|
<div class="info-text" style="margin-bottom: 15px;">
|
||||||
|
Decode VDL Mode 2 (VHF Digital Link) messages on ~136 MHz. VDL2 is the digital successor to ACARS, using D8PSK modulation for higher throughput aircraft datalink communications.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Region & Frequencies</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Region</label>
|
||||||
|
<select id="vdl2RegionSelect" onchange="updateVdl2MainFreqs()" style="width: 100%;">
|
||||||
|
<option value="na">North America</option>
|
||||||
|
<option value="eu">Europe</option>
|
||||||
|
<option value="ap">Asia-Pacific</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="vdl2MainFreqSelector" style="display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; font-size: 11px;">
|
||||||
|
<!-- Populated by JS -->
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Gain (dB, 0 = auto)</label>
|
||||||
|
<input type="number" id="vdl2GainInput" value="40" min="0" max="50" placeholder="0-50">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Status</h3>
|
||||||
|
<div id="vdl2StatusDisplay" class="info-text">
|
||||||
|
<p>Status: <span id="vdl2StatusText" style="color: var(--accent-yellow);">Standby</span></p>
|
||||||
|
<p>Messages: <span id="vdl2MessageCount">0</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Antenna Guide -->
|
||||||
|
<div class="section">
|
||||||
|
<h3>Antenna Guide</h3>
|
||||||
|
<div style="font-size: 11px; color: var(--text-dim); line-height: 1.5;">
|
||||||
|
<p style="margin-bottom: 8px; color: var(--accent-cyan); font-weight: 600;">
|
||||||
|
VHF Airband (~137 MHz) — stock SDR antenna may work at close range
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; margin-bottom: 10px;">
|
||||||
|
<strong style="color: var(--accent-cyan); font-size: 12px;">Simple Dipole</strong>
|
||||||
|
<ul style="margin: 6px 0 0 14px; padding: 0;">
|
||||||
|
<li><strong style="color: var(--text-primary);">Element length:</strong> ~55 cm each (quarter-wave at 137 MHz)</li>
|
||||||
|
<li><strong style="color: var(--text-primary);">Material:</strong> Wire, coat hanger, or copper rod</li>
|
||||||
|
<li><strong style="color: var(--text-primary);">Orientation:</strong> Vertical (airband is vertically polarized)</li>
|
||||||
|
<li><strong style="color: var(--text-primary);">Placement:</strong> Outdoors, as high as possible with clear sky view</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px;">
|
||||||
|
<strong style="color: var(--accent-cyan); font-size: 12px;">Quick Reference</strong>
|
||||||
|
<table style="width: 100%; margin-top: 6px; font-size: 10px; border-collapse: collapse;">
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-dim);">Primary (worldwide)</td>
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">136.975 MHz</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-dim);">Quarter-wave length</td>
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">55 cm</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-dim);">Modulation</td>
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">D8PSK 31.5 kbps</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-dim);">Bandwidth</td>
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">25 kHz</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-dim);">Polarization</td>
|
||||||
|
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">Vertical</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="run-btn" id="startVdl2Btn" onclick="startVdl2Mode()">
|
||||||
|
Start VDL2
|
||||||
|
</button>
|
||||||
|
<button class="stop-btn" id="stopVdl2Btn" onclick="stopVdl2Mode()" style="display: none;">
|
||||||
|
Stop VDL2
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let vdl2MainEventSource = null;
|
||||||
|
let vdl2MainMsgCount = 0;
|
||||||
|
|
||||||
|
// VDL2 frequencies in Hz (as required by dumpvdl2)
|
||||||
|
const vdl2MainFrequencies = {
|
||||||
|
'na': ['136975000', '136100000', '136650000', '136700000', '136800000'],
|
||||||
|
'eu': ['136975000', '136675000', '136725000', '136775000', '136825000'],
|
||||||
|
'ap': ['136975000', '136900000']
|
||||||
|
};
|
||||||
|
|
||||||
|
// Display-friendly MHz labels
|
||||||
|
const vdl2FreqLabels = {
|
||||||
|
'136975000': '136.975',
|
||||||
|
'136100000': '136.100',
|
||||||
|
'136650000': '136.650',
|
||||||
|
'136700000': '136.700',
|
||||||
|
'136800000': '136.800',
|
||||||
|
'136675000': '136.675',
|
||||||
|
'136725000': '136.725',
|
||||||
|
'136775000': '136.775',
|
||||||
|
'136825000': '136.825',
|
||||||
|
'136900000': '136.900'
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateVdl2MainFreqs() {
|
||||||
|
const region = document.getElementById('vdl2RegionSelect').value;
|
||||||
|
const freqs = vdl2MainFrequencies[region] || vdl2MainFrequencies['na'];
|
||||||
|
const container = document.getElementById('vdl2MainFreqSelector');
|
||||||
|
|
||||||
|
const previouslyChecked = new Set();
|
||||||
|
container.querySelectorAll('input:checked').forEach(cb => previouslyChecked.add(cb.value));
|
||||||
|
|
||||||
|
container.innerHTML = freqs.map(freq => {
|
||||||
|
const checked = previouslyChecked.size === 0 || previouslyChecked.has(freq) ? 'checked' : '';
|
||||||
|
const label = vdl2FreqLabels[freq] || freq;
|
||||||
|
return `
|
||||||
|
<label style="display: flex; align-items: center; gap: 3px; padding: 2px 6px; background: var(--bg-secondary); border-radius: 3px; cursor: pointer;">
|
||||||
|
<input type="checkbox" class="vdl2-main-freq-cb" value="${freq}" ${checked} style="margin: 0; cursor: pointer;">
|
||||||
|
<span>${label}</span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVdl2MainSelectedFreqs() {
|
||||||
|
const checkboxes = document.querySelectorAll('.vdl2-main-freq-cb:checked');
|
||||||
|
const selected = Array.from(checkboxes).map(cb => cb.value);
|
||||||
|
if (selected.length === 0) {
|
||||||
|
const region = document.getElementById('vdl2RegionSelect').value;
|
||||||
|
return vdl2MainFrequencies[region] || vdl2MainFrequencies['na'];
|
||||||
|
}
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startVdl2Mode() {
|
||||||
|
const gain = document.getElementById('vdl2GainInput').value || '40';
|
||||||
|
const device = document.getElementById('deviceSelect')?.value || '0';
|
||||||
|
const frequencies = getVdl2MainSelectedFreqs();
|
||||||
|
|
||||||
|
fetch('/vdl2/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ device, gain, frequencies })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'started') {
|
||||||
|
document.getElementById('startVdl2Btn').style.display = 'none';
|
||||||
|
document.getElementById('stopVdl2Btn').style.display = 'block';
|
||||||
|
document.getElementById('vdl2StatusText').textContent = 'Listening';
|
||||||
|
document.getElementById('vdl2StatusText').style.color = 'var(--accent-green)';
|
||||||
|
vdl2MainMsgCount = 0;
|
||||||
|
startVdl2MainSSE();
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'Failed to start VDL2');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => alert('Error: ' + err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopVdl2Mode() {
|
||||||
|
fetch('/vdl2/stop', { method: 'POST' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(() => {
|
||||||
|
document.getElementById('startVdl2Btn').style.display = 'block';
|
||||||
|
document.getElementById('stopVdl2Btn').style.display = 'none';
|
||||||
|
document.getElementById('vdl2StatusText').textContent = 'Standby';
|
||||||
|
document.getElementById('vdl2StatusText').style.color = 'var(--accent-yellow)';
|
||||||
|
if (vdl2MainEventSource) {
|
||||||
|
vdl2MainEventSource.close();
|
||||||
|
vdl2MainEventSource = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startVdl2MainSSE() {
|
||||||
|
if (vdl2MainEventSource) vdl2MainEventSource.close();
|
||||||
|
|
||||||
|
vdl2MainEventSource = new EventSource('/vdl2/stream');
|
||||||
|
vdl2MainEventSource.onmessage = function(e) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
if (data.type === 'vdl2') {
|
||||||
|
vdl2MainMsgCount++;
|
||||||
|
document.getElementById('vdl2MessageCount').textContent = vdl2MainMsgCount;
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
vdl2MainEventSource.onerror = function() {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.getElementById('stopVdl2Btn').style.display === 'block') {
|
||||||
|
startVdl2MainSSE();
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check initial status
|
||||||
|
fetch('/vdl2/status')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.running) {
|
||||||
|
document.getElementById('startVdl2Btn').style.display = 'none';
|
||||||
|
document.getElementById('stopVdl2Btn').style.display = 'block';
|
||||||
|
document.getElementById('vdl2StatusText').textContent = 'Listening';
|
||||||
|
document.getElementById('vdl2StatusText').style.color = 'var(--accent-green)';
|
||||||
|
document.getElementById('vdl2MessageCount').textContent = data.message_count || 0;
|
||||||
|
vdl2MainMsgCount = data.message_count || 0;
|
||||||
|
startVdl2MainSSE();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
// Initialize frequency checkboxes
|
||||||
|
document.addEventListener('DOMContentLoaded', () => updateVdl2MainFreqs());
|
||||||
|
</script>
|
||||||
@@ -67,6 +67,8 @@
|
|||||||
{{ mode_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
{{ mode_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
||||||
{{ mode_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
|
{{ mode_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
|
||||||
{{ mode_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
|
{{ mode_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
|
||||||
|
{{ mode_item('acars', 'ACARS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/><line x1="4" y1="4" x2="8" y2="8"/><line x1="20" y1="4" x2="16" y2="8"/></svg>') }}
|
||||||
|
{{ mode_item('vdl2', 'VDL2', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/><path d="M2 2l4 4M22 2l-4 4M12 12v0"/></svg>') }}
|
||||||
{{ mode_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
|
{{ mode_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
|
||||||
{{ mode_item('listening', 'Listening Post', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
|
{{ mode_item('listening', 'Listening Post', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
|
||||||
{{ mode_item('spystations', 'Spy Stations', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }}
|
{{ mode_item('spystations', 'Spy Stations', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }}
|
||||||
@@ -178,6 +180,8 @@
|
|||||||
{{ mobile_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
{{ mobile_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
||||||
{{ mobile_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
|
{{ mobile_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
|
||||||
{{ mobile_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
|
{{ mobile_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
|
||||||
|
{{ mobile_item('acars', 'ACARS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>') }}
|
||||||
|
{{ mobile_item('vdl2', 'VDL2', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>') }}
|
||||||
{{ mobile_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
|
{{ mobile_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
|
||||||
{{ mobile_item('wifi', 'WiFi', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg>') }}
|
{{ mobile_item('wifi', 'WiFi', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg>') }}
|
||||||
{{ mobile_item('bluetooth', 'BT', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg>') }}
|
{{ mobile_item('bluetooth', 'BT', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg>') }}
|
||||||
|
|||||||
Reference in New Issue
Block a user