mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Add ACARS aircraft messaging feature with collapsible sidebar
- Add routes/acars.py with start/stop/stream endpoints for ACARS decoding - Build acarsdec from source in Dockerfile (not available in Debian slim) - Add acarsdec installation script to setup.sh for native installs - Add ACARS to dependency checker in utils/dependencies.py - Add collapsible ACARS sidebar next to map in aircraft tracking tab - Add collapsible ACARS panel in ADS-B dashboard with same layout - Include guidance about needing two SDRs for simultaneous ADS-B + ACARS - Support regional frequency presets (N.America, Europe, Asia-Pacific) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
16
Dockerfile
16
Dockerfile
@@ -35,25 +35,37 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
procps \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Build dump1090-fa from source (packages not available in slim repos)
|
||||
# Build dump1090-fa and acarsdec from source (packages not available in slim repos)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
git \
|
||||
pkg-config \
|
||||
cmake \
|
||||
libncurses-dev \
|
||||
libsndfile1-dev \
|
||||
# Build dump1090
|
||||
&& cd /tmp \
|
||||
&& git clone --depth 1 https://github.com/flightaware/dump1090.git \
|
||||
&& cd dump1090 \
|
||||
&& make \
|
||||
&& cp dump1090 /usr/bin/dump1090-fa \
|
||||
&& ln -s /usr/bin/dump1090-fa /usr/bin/dump1090 \
|
||||
&& cd /app \
|
||||
&& rm -rf /tmp/dump1090 \
|
||||
# Build acarsdec
|
||||
&& cd /tmp \
|
||||
&& git clone --depth 1 https://github.com/TLeconte/acarsdec.git \
|
||||
&& cd acarsdec \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -Drtl=ON \
|
||||
&& make \
|
||||
&& cp acarsdec /usr/bin/acarsdec \
|
||||
&& rm -rf /tmp/acarsdec \
|
||||
# Cleanup build tools to reduce image size
|
||||
&& apt-get remove -y \
|
||||
build-essential \
|
||||
git \
|
||||
pkg-config \
|
||||
cmake \
|
||||
libncurses-dev \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
16
app.py
16
app.py
@@ -103,6 +103,11 @@ satellite_process = None
|
||||
satellite_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||
satellite_lock = threading.Lock()
|
||||
|
||||
# ACARS aircraft messaging
|
||||
acars_process = None
|
||||
acars_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||
acars_lock = threading.Lock()
|
||||
|
||||
# ============================================
|
||||
# GLOBAL STATE DICTIONARIES
|
||||
# ============================================
|
||||
@@ -416,6 +421,7 @@ def health_check() -> Response:
|
||||
'pager': current_process is not None and (current_process.poll() is None if current_process else False),
|
||||
'sensor': sensor_process is not None and (sensor_process.poll() is None if sensor_process else False),
|
||||
'adsb': adsb_process is not None and (adsb_process.poll() is None if adsb_process else False),
|
||||
'acars': acars_process is not None and (acars_process.poll() is None if acars_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),
|
||||
},
|
||||
@@ -431,7 +437,7 @@ def health_check() -> Response:
|
||||
@app.route('/killall', methods=['POST'])
|
||||
def kill_all() -> Response:
|
||||
"""Kill all decoder and WiFi processes."""
|
||||
global current_process, sensor_process, wifi_process, adsb_process
|
||||
global current_process, sensor_process, wifi_process, adsb_process, acars_process
|
||||
|
||||
# Import adsb module to reset its state
|
||||
from routes import adsb as adsb_module
|
||||
@@ -440,7 +446,7 @@ def kill_all() -> Response:
|
||||
processes_to_kill = [
|
||||
'rtl_fm', 'multimon-ng', 'rtl_433',
|
||||
'airodump-ng', 'aireplay-ng', 'airmon-ng',
|
||||
'dump1090'
|
||||
'dump1090', 'acarsdec'
|
||||
]
|
||||
|
||||
for proc in processes_to_kill:
|
||||
@@ -465,6 +471,10 @@ def kill_all() -> Response:
|
||||
adsb_process = None
|
||||
adsb_module.adsb_using_service = False
|
||||
|
||||
# Reset ACARS state
|
||||
with acars_lock:
|
||||
acars_process = None
|
||||
|
||||
return jsonify({'status': 'killed', 'processes': killed})
|
||||
|
||||
|
||||
@@ -517,7 +527,7 @@ def main() -> None:
|
||||
|
||||
print("=" * 50)
|
||||
print(" INTERCEPT // Signal Intelligence")
|
||||
print(" Pager / 433MHz / Aircraft / Satellite / WiFi / BT")
|
||||
print(" Pager / 433MHz / Aircraft / ACARS / Satellite / WiFi / BT")
|
||||
print("=" * 50)
|
||||
print()
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ def register_blueprints(app):
|
||||
from .wifi import wifi_bp
|
||||
from .bluetooth import bluetooth_bp
|
||||
from .adsb import adsb_bp
|
||||
from .acars import acars_bp
|
||||
from .satellite import satellite_bp
|
||||
from .gps import gps_bp
|
||||
from .settings import settings_bp
|
||||
@@ -18,6 +19,7 @@ def register_blueprints(app):
|
||||
app.register_blueprint(wifi_bp)
|
||||
app.register_blueprint(bluetooth_bp)
|
||||
app.register_blueprint(adsb_bp)
|
||||
app.register_blueprint(acars_bp)
|
||||
app.register_blueprint(satellite_bp)
|
||||
app.register_blueprint(gps_bp)
|
||||
app.register_blueprint(settings_bp)
|
||||
|
||||
290
routes/acars.py
Normal file
290
routes/acars.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""ACARS aircraft messaging 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.sse import format_sse
|
||||
from utils.constants import (
|
||||
PROCESS_TERMINATE_TIMEOUT,
|
||||
SSE_KEEPALIVE_INTERVAL,
|
||||
SSE_QUEUE_TIMEOUT,
|
||||
PROCESS_START_WAIT,
|
||||
)
|
||||
|
||||
acars_bp = Blueprint('acars', __name__, url_prefix='/acars')
|
||||
|
||||
# Default VHF ACARS frequencies (MHz) - common worldwide
|
||||
DEFAULT_ACARS_FREQUENCIES = [
|
||||
'131.550', # Primary worldwide
|
||||
'130.025', # Secondary USA/Canada
|
||||
'129.125', # USA
|
||||
'131.525', # Europe
|
||||
'131.725', # Europe secondary
|
||||
]
|
||||
|
||||
# Message counter for statistics
|
||||
acars_message_count = 0
|
||||
acars_last_message_time = None
|
||||
|
||||
|
||||
def find_acarsdec():
|
||||
"""Find acarsdec binary."""
|
||||
return shutil.which('acarsdec')
|
||||
|
||||
|
||||
def stream_acars_output(process: subprocess.Popen) -> None:
|
||||
"""Stream acarsdec JSON output to queue."""
|
||||
global acars_message_count, acars_last_message_time
|
||||
|
||||
try:
|
||||
app_module.acars_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:
|
||||
# acarsdec -j outputs JSON, one message per line
|
||||
data = json.loads(line)
|
||||
|
||||
# Add our metadata
|
||||
data['type'] = 'acars'
|
||||
data['timestamp'] = datetime.utcnow().isoformat() + 'Z'
|
||||
|
||||
# Update stats
|
||||
acars_message_count += 1
|
||||
acars_last_message_time = time.time()
|
||||
|
||||
app_module.acars_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} | ACARS | {json.dumps(data)}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# Not JSON - could be status message
|
||||
if line:
|
||||
logger.debug(f"acarsdec non-JSON: {line[:100]}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"ACARS stream error: {e}")
|
||||
app_module.acars_queue.put({'type': 'error', 'message': str(e)})
|
||||
finally:
|
||||
app_module.acars_queue.put({'type': 'status', 'status': 'stopped'})
|
||||
with app_module.acars_lock:
|
||||
app_module.acars_process = None
|
||||
|
||||
|
||||
@acars_bp.route('/tools')
|
||||
def check_acars_tools() -> Response:
|
||||
"""Check for ACARS decoding tools."""
|
||||
has_acarsdec = find_acarsdec() is not None
|
||||
|
||||
return jsonify({
|
||||
'acarsdec': has_acarsdec,
|
||||
'ready': has_acarsdec
|
||||
})
|
||||
|
||||
|
||||
@acars_bp.route('/status')
|
||||
def acars_status() -> Response:
|
||||
"""Get ACARS decoder status."""
|
||||
running = False
|
||||
if app_module.acars_process:
|
||||
running = app_module.acars_process.poll() is None
|
||||
|
||||
return jsonify({
|
||||
'running': running,
|
||||
'message_count': acars_message_count,
|
||||
'last_message_time': acars_last_message_time,
|
||||
'queue_size': app_module.acars_queue.qsize()
|
||||
})
|
||||
|
||||
|
||||
@acars_bp.route('/start', methods=['POST'])
|
||||
def start_acars() -> Response:
|
||||
"""Start ACARS decoder."""
|
||||
global acars_message_count, acars_last_message_time
|
||||
|
||||
with app_module.acars_lock:
|
||||
if app_module.acars_process and app_module.acars_process.poll() is None:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'ACARS decoder already running'
|
||||
}), 409
|
||||
|
||||
# Check for acarsdec
|
||||
acarsdec_path = find_acarsdec()
|
||||
if not acarsdec_path:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'acarsdec not found. Install with: sudo apt install acarsdec'
|
||||
}), 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
|
||||
|
||||
# Get frequencies - use provided or defaults
|
||||
frequencies = data.get('frequencies', DEFAULT_ACARS_FREQUENCIES)
|
||||
if isinstance(frequencies, str):
|
||||
frequencies = [f.strip() for f in frequencies.split(',')]
|
||||
|
||||
# Clear queue
|
||||
while not app_module.acars_queue.empty():
|
||||
try:
|
||||
app_module.acars_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
# Reset stats
|
||||
acars_message_count = 0
|
||||
acars_last_message_time = None
|
||||
|
||||
# Build acarsdec command
|
||||
# acarsdec -j -r <device> -g <gain> -p <ppm> <freq1> <freq2> ...
|
||||
cmd = [
|
||||
acarsdec_path,
|
||||
'-j', # JSON output
|
||||
'-r', str(device), # RTL-SDR device index
|
||||
]
|
||||
|
||||
# Add gain if not auto
|
||||
if gain and str(gain) != '0':
|
||||
cmd.extend(['-g', str(gain)])
|
||||
|
||||
# Add PPM correction if specified
|
||||
if ppm and str(ppm) != '0':
|
||||
cmd.extend(['-p', str(ppm)])
|
||||
|
||||
# Add frequencies
|
||||
cmd.extend(frequencies)
|
||||
|
||||
logger.info(f"Starting ACARS 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
|
||||
stderr = ''
|
||||
if process.stderr:
|
||||
stderr = process.stderr.read().decode('utf-8', errors='replace')
|
||||
error_msg = f'acarsdec failed to start'
|
||||
if stderr:
|
||||
error_msg += f': {stderr[:200]}'
|
||||
logger.error(error_msg)
|
||||
return jsonify({'status': 'error', 'message': error_msg}), 500
|
||||
|
||||
app_module.acars_process = process
|
||||
|
||||
# Start output streaming thread
|
||||
thread = threading.Thread(
|
||||
target=stream_acars_output,
|
||||
args=(process,),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
return jsonify({
|
||||
'status': 'started',
|
||||
'frequencies': frequencies,
|
||||
'device': device,
|
||||
'gain': gain
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start ACARS decoder: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@acars_bp.route('/stop', methods=['POST'])
|
||||
def stop_acars() -> Response:
|
||||
"""Stop ACARS decoder."""
|
||||
with app_module.acars_lock:
|
||||
if not app_module.acars_process:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'ACARS decoder not running'
|
||||
}), 400
|
||||
|
||||
try:
|
||||
app_module.acars_process.terminate()
|
||||
app_module.acars_process.wait(timeout=PROCESS_TERMINATE_TIMEOUT)
|
||||
except subprocess.TimeoutExpired:
|
||||
app_module.acars_process.kill()
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping ACARS: {e}")
|
||||
|
||||
app_module.acars_process = None
|
||||
|
||||
return jsonify({'status': 'stopped'})
|
||||
|
||||
|
||||
@acars_bp.route('/stream')
|
||||
def stream_acars() -> Response:
|
||||
"""SSE stream for ACARS messages."""
|
||||
def generate() -> Generator[str, None, None]:
|
||||
last_keepalive = time.time()
|
||||
|
||||
while True:
|
||||
try:
|
||||
msg = app_module.acars_queue.get(timeout=SSE_QUEUE_TIMEOUT)
|
||||
last_keepalive = time.time()
|
||||
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
|
||||
|
||||
|
||||
@acars_bp.route('/frequencies')
|
||||
def get_frequencies() -> Response:
|
||||
"""Get default ACARS frequencies."""
|
||||
return jsonify({
|
||||
'default': DEFAULT_ACARS_FREQUENCIES,
|
||||
'regions': {
|
||||
'north_america': ['129.125', '130.025', '130.450', '131.550'],
|
||||
'europe': ['131.525', '131.725', '131.550'],
|
||||
'asia_pacific': ['131.550', '131.450'],
|
||||
}
|
||||
})
|
||||
42
setup.sh
42
setup.sh
@@ -139,6 +139,7 @@ check_tools() {
|
||||
check_required "multimon-ng" "Pager decoder" multimon-ng
|
||||
check_required "rtl_433" "433MHz sensor decoder" rtl_433 rtl433
|
||||
check_required "dump1090" "ADS-B decoder" dump1090
|
||||
check_required "acarsdec" "ACARS decoder" acarsdec
|
||||
|
||||
echo
|
||||
info "GPS:"
|
||||
@@ -305,7 +306,7 @@ install_multimon_ng_from_source_macos() {
|
||||
}
|
||||
|
||||
install_macos_packages() {
|
||||
TOTAL_STEPS=12
|
||||
TOTAL_STEPS=13
|
||||
CURRENT_STEP=0
|
||||
|
||||
progress "Checking Homebrew"
|
||||
@@ -331,6 +332,9 @@ install_macos_packages() {
|
||||
progress "Installing dump1090"
|
||||
(brew_install dump1090-mutability) || warn "dump1090 not available via Homebrew"
|
||||
|
||||
progress "Installing acarsdec"
|
||||
(brew_install acarsdec) || warn "acarsdec not available via Homebrew"
|
||||
|
||||
progress "Installing aircrack-ng"
|
||||
brew_install aircrack-ng
|
||||
|
||||
@@ -412,6 +416,34 @@ install_dump1090_from_source_debian() {
|
||||
)
|
||||
}
|
||||
|
||||
install_acarsdec_from_source_debian() {
|
||||
info "acarsdec not available via APT. Building from source..."
|
||||
|
||||
apt_install build-essential git cmake \
|
||||
librtlsdr-dev libusb-1.0-0-dev libsndfile1-dev
|
||||
|
||||
# Run in subshell to isolate EXIT trap
|
||||
(
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
info "Cloning acarsdec..."
|
||||
git clone --depth 1 https://github.com/TLeconte/acarsdec.git "$tmp_dir/acarsdec" >/dev/null 2>&1 \
|
||||
|| { warn "Failed to clone acarsdec"; exit 1; }
|
||||
|
||||
cd "$tmp_dir/acarsdec"
|
||||
mkdir -p build && cd build
|
||||
|
||||
info "Compiling acarsdec..."
|
||||
if cmake .. -Drtl=ON >/dev/null 2>&1 && make >/dev/null 2>&1; then
|
||||
$SUDO install -m 0755 acarsdec /usr/local/bin/acarsdec
|
||||
ok "acarsdec installed successfully."
|
||||
else
|
||||
warn "Failed to build acarsdec from source. ACARS decoding will not be available."
|
||||
fi
|
||||
)
|
||||
}
|
||||
|
||||
setup_udev_rules_debian() {
|
||||
[[ -d /etc/udev/rules.d ]] || { warn "udev not found; skipping RTL-SDR udev rules."; return 0; }
|
||||
|
||||
@@ -464,7 +496,7 @@ install_debian_packages() {
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
export NEEDRESTART_MODE=a
|
||||
|
||||
TOTAL_STEPS=16
|
||||
TOTAL_STEPS=17
|
||||
CURRENT_STEP=0
|
||||
|
||||
progress "Updating APT package lists"
|
||||
@@ -520,6 +552,12 @@ install_debian_packages() {
|
||||
fi
|
||||
cmd_exists dump1090 || install_dump1090_from_source_debian
|
||||
|
||||
progress "Installing acarsdec"
|
||||
if ! cmd_exists acarsdec; then
|
||||
apt_install acarsdec || true
|
||||
fi
|
||||
cmd_exists acarsdec || install_acarsdec_from_source_debian
|
||||
|
||||
progress "Configuring udev rules"
|
||||
setup_udev_rules_debian
|
||||
|
||||
|
||||
@@ -185,13 +185,138 @@ body {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 340px;
|
||||
grid-template-columns: 1fr auto 300px;
|
||||
grid-template-rows: 1fr auto;
|
||||
gap: 0;
|
||||
height: calc(100vh - 60px);
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
/* ACARS sidebar (between map and main sidebar) - Collapsible */
|
||||
.acars-sidebar {
|
||||
background: var(--bg-panel);
|
||||
border-left: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
transition: width 0.3s ease;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.acars-sidebar.collapsed {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.acars-collapse-btn {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
background: var(--bg-card);
|
||||
border: none;
|
||||
border-right: 1px solid var(--border-color);
|
||||
color: var(--accent-cyan);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 0;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.acars-collapse-btn:hover {
|
||||
background: rgba(74, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.acars-collapse-label {
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.acars-sidebar.collapsed .acars-collapse-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.acars-sidebar:not(.collapsed) .acars-collapse-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#acarsCollapseIcon {
|
||||
font-size: 10px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.acars-sidebar.collapsed #acarsCollapseIcon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.acars-sidebar-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.acars-sidebar.collapsed .acars-sidebar-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.acars-sidebar .panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.acars-sidebar .panel::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.acars-sidebar .acars-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.acars-sidebar .acars-btn {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--accent-cyan);
|
||||
color: var(--accent-cyan);
|
||||
padding: 6px 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.acars-sidebar .acars-btn:hover {
|
||||
background: rgba(74, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.acars-message-item {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 10px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.acars-message-item:hover {
|
||||
background: rgba(74, 158, 255, 0.05);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-5px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Panels */
|
||||
.panel {
|
||||
background: var(--bg-panel);
|
||||
@@ -228,8 +353,14 @@ body {
|
||||
.panel-indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--accent-cyan);
|
||||
background: var(--text-dim);
|
||||
border-radius: 50%;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.panel-indicator.active {
|
||||
background: var(--accent-green);
|
||||
opacity: 1;
|
||||
animation: blink 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -656,8 +787,20 @@ body {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1000px) {
|
||||
/* Responsive - medium screens (hide ACARS sidebar, keep main sidebar) */
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard {
|
||||
grid-template-columns: 1fr 300px;
|
||||
grid-template-rows: 1fr auto;
|
||||
}
|
||||
|
||||
.acars-sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive - small screens (single column) */
|
||||
@media (max-width: 900px) {
|
||||
.dashboard {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr auto auto;
|
||||
@@ -667,6 +810,10 @@ body {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.acars-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
|
||||
@@ -53,6 +53,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ACARS Panel (right of map) - Collapsible -->
|
||||
<div class="acars-sidebar" id="acarsSidebar">
|
||||
<button class="acars-collapse-btn" id="acarsCollapseBtn" onclick="toggleAcarsSidebar()" title="Toggle ACARS Panel">
|
||||
<span id="acarsCollapseIcon">◀</span>
|
||||
<span class="acars-collapse-label">ACARS</span>
|
||||
</button>
|
||||
<div class="acars-sidebar-content" id="acarsSidebarContent">
|
||||
<div class="panel acars-panel">
|
||||
<div class="panel-header">
|
||||
<span>ACARS MESSAGES</span>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span id="acarsCount" style="font-size: 10px; color: var(--accent-cyan);">0</span>
|
||||
<div class="panel-indicator" id="acarsPanelIndicator"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="acarsPanelContent">
|
||||
<div class="acars-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 ~131 MHz)
|
||||
</div>
|
||||
<div class="acars-controls" style="padding: 8px; border-bottom: 1px solid var(--border-color);">
|
||||
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
|
||||
<select id="acarsDeviceSelect" style="flex: 1; font-size: 10px;">
|
||||
<option value="0">SDR 0</option>
|
||||
<option value="1">SDR 1</option>
|
||||
</select>
|
||||
<select id="acarsRegionSelect" onchange="setAcarsFreqs()" 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>
|
||||
<button class="acars-btn" id="acarsToggleBtn" onclick="toggleAcars()" style="width: 100%;">
|
||||
▶ START ACARS
|
||||
</button>
|
||||
</div>
|
||||
<div class="acars-messages" id="acarsMessages">
|
||||
<div class="no-aircraft" style="padding: 20px; text-align: center;">
|
||||
<div style="font-size: 10px; color: var(--text-muted);">No ACARS messages</div>
|
||||
<div style="font-size: 9px; color: var(--text-dim); margin-top: 5px;">Start ACARS to receive aircraft datalink messages</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar">
|
||||
<!-- View Toggle -->
|
||||
@@ -2215,6 +2261,179 @@ sudo make install</code>
|
||||
// Initialize airband on page load
|
||||
document.addEventListener('DOMContentLoaded', initAirband);
|
||||
|
||||
// ============================================
|
||||
// ACARS Functions
|
||||
// ============================================
|
||||
let acarsEventSource = null;
|
||||
let isAcarsRunning = false;
|
||||
let acarsMessageCount = 0;
|
||||
let acarsSidebarCollapsed = localStorage.getItem('acarsSidebarCollapsed') === 'true';
|
||||
let acarsFrequencies = {
|
||||
'na': ['129.125', '130.025', '130.450', '131.550'],
|
||||
'eu': ['131.525', '131.725', '131.550'],
|
||||
'ap': ['131.550', '131.450']
|
||||
};
|
||||
|
||||
function toggleAcarsSidebar() {
|
||||
const sidebar = document.getElementById('acarsSidebar');
|
||||
acarsSidebarCollapsed = !acarsSidebarCollapsed;
|
||||
sidebar.classList.toggle('collapsed', acarsSidebarCollapsed);
|
||||
localStorage.setItem('acarsSidebarCollapsed', acarsSidebarCollapsed);
|
||||
}
|
||||
|
||||
// Initialize ACARS sidebar state
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const sidebar = document.getElementById('acarsSidebar');
|
||||
if (sidebar && acarsSidebarCollapsed) {
|
||||
sidebar.classList.add('collapsed');
|
||||
}
|
||||
});
|
||||
|
||||
function setAcarsFreqs() {
|
||||
// Just updates the region selection - frequencies are sent on start
|
||||
}
|
||||
|
||||
function getAcarsRegionFreqs() {
|
||||
const region = document.getElementById('acarsRegionSelect').value;
|
||||
return acarsFrequencies[region] || acarsFrequencies['na'];
|
||||
}
|
||||
|
||||
function toggleAcars() {
|
||||
if (isAcarsRunning) {
|
||||
stopAcars();
|
||||
} else {
|
||||
startAcars();
|
||||
}
|
||||
}
|
||||
|
||||
function startAcars() {
|
||||
const device = document.getElementById('acarsDeviceSelect').value;
|
||||
const frequencies = getAcarsRegionFreqs();
|
||||
|
||||
// Warn if using same device as ADS-B
|
||||
if (isTracking && device === '0') {
|
||||
const useAnyway = confirm(
|
||||
'Warning: ADS-B tracking may be using SDR device 0.\n\n' +
|
||||
'ACARS uses VHF frequencies (129-131 MHz) while ADS-B uses 1090 MHz.\n' +
|
||||
'You need TWO separate SDR devices to receive both simultaneously.\n\n' +
|
||||
'Click OK to start ACARS on device ' + device + ' anyway.'
|
||||
);
|
||||
if (!useAnyway) return;
|
||||
}
|
||||
|
||||
fetch('/acars/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ device, frequencies, gain: '40' })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started') {
|
||||
isAcarsRunning = true;
|
||||
acarsMessageCount = 0;
|
||||
document.getElementById('acarsToggleBtn').textContent = '■ STOP ACARS';
|
||||
document.getElementById('acarsToggleBtn').style.background = 'var(--accent-red)';
|
||||
document.getElementById('acarsPanelIndicator').classList.add('active');
|
||||
startAcarsStream();
|
||||
} else {
|
||||
alert('ACARS Error: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(err => alert('ACARS Error: ' + err));
|
||||
}
|
||||
|
||||
function stopAcars() {
|
||||
fetch('/acars/stop', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(() => {
|
||||
isAcarsRunning = false;
|
||||
document.getElementById('acarsToggleBtn').textContent = '▶ START ACARS';
|
||||
document.getElementById('acarsToggleBtn').style.background = '';
|
||||
document.getElementById('acarsPanelIndicator').classList.remove('active');
|
||||
if (acarsEventSource) {
|
||||
acarsEventSource.close();
|
||||
acarsEventSource = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startAcarsStream() {
|
||||
if (acarsEventSource) acarsEventSource.close();
|
||||
acarsEventSource = new EventSource('/acars/stream');
|
||||
|
||||
acarsEventSource.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'acars') {
|
||||
acarsMessageCount++;
|
||||
document.getElementById('acarsCount').textContent = acarsMessageCount;
|
||||
addAcarsMessage(data);
|
||||
}
|
||||
};
|
||||
|
||||
acarsEventSource.onerror = function() {
|
||||
console.error('ACARS stream error');
|
||||
};
|
||||
}
|
||||
|
||||
function addAcarsMessage(data) {
|
||||
const container = document.getElementById('acarsMessages');
|
||||
|
||||
// Remove "no messages" placeholder if present
|
||||
const placeholder = container.querySelector('.no-aircraft');
|
||||
if (placeholder) placeholder.remove();
|
||||
|
||||
const msg = document.createElement('div');
|
||||
msg.className = 'acars-message-item';
|
||||
msg.style.cssText = 'padding: 6px 8px; border-bottom: 1px solid var(--border-color); font-size: 10px;';
|
||||
|
||||
const flight = data.flight || 'UNKNOWN';
|
||||
const reg = data.reg || '';
|
||||
const label = data.label || '';
|
||||
const text = data.text || data.msg || '';
|
||||
const time = new Date().toLocaleTimeString();
|
||||
|
||||
msg.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 2px;">
|
||||
<span style="color: var(--accent-cyan); font-weight: bold;">${flight}</span>
|
||||
<span style="color: var(--text-muted);">${time}</span>
|
||||
</div>
|
||||
${reg ? `<div style="color: var(--text-muted); font-size: 9px;">Reg: ${reg}</div>` : ''}
|
||||
${label ? `<div style="color: var(--accent-green);">Label: ${label}</div>` : ''}
|
||||
${text ? `<div style="color: var(--text-primary); margin-top: 3px; word-break: break-word;">${text}</div>` : ''}
|
||||
`;
|
||||
|
||||
container.insertBefore(msg, container.firstChild);
|
||||
|
||||
// Keep max 50 messages
|
||||
while (container.children.length > 50) {
|
||||
container.removeChild(container.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Populate ACARS device selector
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetch('/devices')
|
||||
.then(r => r.json())
|
||||
.then(devices => {
|
||||
const select = document.getElementById('acarsDeviceSelect');
|
||||
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.textContent = `Device ${d.index || i}: ${d.name || d.type || 'SDR'}`;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
// Default to device 1 if available (device 0 likely used for ADS-B)
|
||||
if (devices.length > 1) {
|
||||
select.value = '1';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// SQUAWK CODE REFERENCE
|
||||
// ============================================
|
||||
|
||||
@@ -857,6 +857,50 @@
|
||||
<button class="stop-btn" id="stopAdsbBtn" onclick="stopAdsbScan()" style="display: none;">
|
||||
Stop Tracking
|
||||
</button>
|
||||
|
||||
<!-- ACARS Sub-section -->
|
||||
<div class="section" style="margin-top: 15px; border-top: 1px solid var(--border-color); padding-top: 15px;">
|
||||
<h3 style="display: flex; align-items: center; justify-content: space-between; cursor: pointer;" onclick="toggleAcarsPanel()">
|
||||
<span>ACARS Messaging</span>
|
||||
<span id="acarsToggleIcon" style="font-size: 10px; color: var(--text-secondary);">▼</span>
|
||||
</h3>
|
||||
<div id="acarsPanel">
|
||||
<div class="info-text" style="margin-bottom: 10px;">
|
||||
Decode aircraft digital messages (ACARS) on VHF frequencies.
|
||||
</div>
|
||||
<div style="background: rgba(255,193,7,0.1); border: 1px solid var(--accent-yellow); border-radius: 4px; padding: 8px; margin-bottom: 10px; font-size: 10px;">
|
||||
<strong style="color: var(--accent-yellow);">⚠ Two SDRs Required</strong><br>
|
||||
<span style="color: var(--text-secondary);">ADS-B uses 1090 MHz, ACARS uses VHF (~131 MHz). To receive both simultaneously, you need two separate RTL-SDR devices.</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Frequencies (MHz)</label>
|
||||
<input type="text" id="acarsFrequencies" value="131.550,130.025,129.125,131.525" placeholder="131.550,130.025,...">
|
||||
<div class="info-text" style="margin-top: 4px;">Comma-separated VHF ACARS frequencies</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Gain (dB, 0 = auto)</label>
|
||||
<input type="text" id="acarsGain" value="40" placeholder="40">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>PPM Correction</label>
|
||||
<input type="text" id="acarsPpm" value="0" placeholder="0">
|
||||
</div>
|
||||
<div class="preset-buttons" style="margin-bottom: 10px;">
|
||||
<button class="preset-btn" onclick="setAcarsRegion('na')">N. America</button>
|
||||
<button class="preset-btn" onclick="setAcarsRegion('eu')">Europe</button>
|
||||
<button class="preset-btn" onclick="setAcarsRegion('ap')">Asia-Pacific</button>
|
||||
</div>
|
||||
<div class="info-text" style="margin-top: 8px; display: grid; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;" id="acarsToolStatus">
|
||||
<span>acarsdec:</span><span class="tool-status" id="acarsdecStatus">Checking...</span>
|
||||
</div>
|
||||
<button class="run-btn" id="startAcarsBtn" onclick="startAcars()" style="margin-top: 10px;">
|
||||
Start ACARS
|
||||
</button>
|
||||
<button class="stop-btn" id="stopAcarsBtn" onclick="stopAcars()" style="display: none; margin-top: 10px;">
|
||||
Stop ACARS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SATELLITE MODE -->
|
||||
@@ -1189,18 +1233,55 @@
|
||||
|
||||
<!-- Aircraft Visualizations - Leaflet Map -->
|
||||
<div class="wifi-visuals" id="aircraftVisuals" style="display: none;">
|
||||
<!-- Map Panel -->
|
||||
<div class="wifi-visual-panel" style="grid-column: span 2;">
|
||||
<h5 style="color: var(--accent-cyan); text-shadow: 0 0 10px var(--accent-cyan);">ADS-B AIRCRAFT TRACKING</h5>
|
||||
<div class="aircraft-map-container">
|
||||
<div class="map-header">
|
||||
<span id="radarTime">--:--:--</span>
|
||||
<span id="radarStatus">TRACKING</span>
|
||||
<!-- Map Panel with ACARS sidebar -->
|
||||
<div class="wifi-visual-panel" style="grid-column: span 2; display: flex; gap: 0;">
|
||||
<!-- Map -->
|
||||
<div style="flex: 1; display: flex; flex-direction: column;">
|
||||
<h5 style="color: var(--accent-cyan); text-shadow: 0 0 10px var(--accent-cyan); padding: 0 10px;">ADS-B AIRCRAFT TRACKING</h5>
|
||||
<div class="aircraft-map-container" style="flex: 1;">
|
||||
<div class="map-header">
|
||||
<span id="radarTime">--:--:--</span>
|
||||
<span id="radarStatus">TRACKING</span>
|
||||
</div>
|
||||
<div id="aircraftMap"></div>
|
||||
<div class="map-footer">
|
||||
<span>AIRCRAFT: <span id="aircraftCount">0</span></span>
|
||||
<span>CENTER: <span id="mapCenter">--</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="aircraftMap"></div>
|
||||
<div class="map-footer">
|
||||
<span>AIRCRAFT: <span id="aircraftCount">0</span></span>
|
||||
<span>CENTER: <span id="mapCenter">--</span></span>
|
||||
</div>
|
||||
<!-- ACARS Sidebar (Collapsible) -->
|
||||
<div class="main-acars-sidebar" id="mainAcarsSidebar">
|
||||
<button class="main-acars-collapse-btn" onclick="toggleMainAcarsSidebar()" title="Toggle ACARS">
|
||||
<span id="mainAcarsCollapseIcon">◀</span>
|
||||
<span class="main-acars-collapse-label">ACARS</span>
|
||||
</button>
|
||||
<div class="main-acars-content" id="mainAcarsContent">
|
||||
<div style="padding: 8px; border-bottom: 1px solid var(--border-color);">
|
||||
<h5 style="color: var(--accent-green); margin-bottom: 8px; font-size: 11px;">ACARS MESSAGES</h5>
|
||||
<div style="font-size: 9px; color: var(--text-muted); margin-bottom: 8px;">
|
||||
<span style="color: var(--accent-yellow);">⚠</span> Requires separate SDR
|
||||
</div>
|
||||
<div style="display: flex; gap: 4px; margin-bottom: 5px;">
|
||||
<select id="mainAcarsDevice" style="flex: 1; font-size: 9px; padding: 3px;">
|
||||
<option value="0">SDR 0</option>
|
||||
<option value="1">SDR 1</option>
|
||||
</select>
|
||||
<select id="mainAcarsRegion" style="flex: 1; font-size: 9px; padding: 3px;">
|
||||
<option value="na">N.America</option>
|
||||
<option value="eu">Europe</option>
|
||||
<option value="ap">Asia-Pac</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="mainAcarsToggleBtn" onclick="toggleMainAcars()" style="width: 100%; padding: 5px; font-size: 9px; background: var(--bg-dark); border: 1px solid var(--accent-cyan); color: var(--accent-cyan); cursor: pointer;">
|
||||
▶ START
|
||||
</button>
|
||||
</div>
|
||||
<div class="main-acars-messages" id="mainAcarsMessages" style="flex: 1; overflow-y: auto; font-size: 10px;">
|
||||
<div style="padding: 15px; text-align: center; color: var(--text-muted);">
|
||||
No ACARS messages
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1516,6 +1597,78 @@
|
||||
0%, 100% { opacity: 0.3; transform: scale(0.8); }
|
||||
50% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Main ACARS Sidebar (Collapsible) */
|
||||
.main-acars-sidebar {
|
||||
width: 220px;
|
||||
background: var(--bg-panel);
|
||||
border-left: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.main-acars-sidebar.collapsed {
|
||||
width: 28px;
|
||||
}
|
||||
.main-acars-collapse-btn {
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border: none;
|
||||
border-right: 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;
|
||||
}
|
||||
.main-acars-collapse-btn:hover {
|
||||
background: rgba(74, 158, 255, 0.1);
|
||||
}
|
||||
.main-acars-collapse-label {
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.main-acars-sidebar.collapsed .main-acars-collapse-label { display: block; }
|
||||
.main-acars-sidebar:not(.collapsed) .main-acars-collapse-label { display: none; }
|
||||
#mainAcarsCollapseIcon {
|
||||
font-size: 9px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.main-acars-sidebar.collapsed #mainAcarsCollapseIcon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.main-acars-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
.main-acars-sidebar.collapsed .main-acars-content {
|
||||
display: none;
|
||||
}
|
||||
.main-acars-messages {
|
||||
max-height: 350px;
|
||||
}
|
||||
.main-acars-msg {
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
animation: fadeInMsg 0.3s ease;
|
||||
}
|
||||
.main-acars-msg:hover {
|
||||
background: rgba(74, 158, 255, 0.05);
|
||||
}
|
||||
@keyframes fadeInMsg {
|
||||
from { opacity: 0; transform: translateY(-3px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Satellite Dashboard (Embedded) -->
|
||||
@@ -2264,6 +2417,7 @@
|
||||
initBtRadar();
|
||||
} else if (mode === 'aircraft') {
|
||||
checkAdsbTools();
|
||||
checkAcarsTools();
|
||||
initAircraftRadar();
|
||||
} else if (mode === 'satellite') {
|
||||
initPolarPlot();
|
||||
@@ -7207,6 +7361,274 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ACARS Functions
|
||||
// ============================================
|
||||
let acarsEventSource = null;
|
||||
let isAcarsRunning = false;
|
||||
let acarsMessageCount = 0;
|
||||
|
||||
function toggleAcarsPanel() {
|
||||
const panel = document.getElementById('acarsPanel');
|
||||
const icon = document.getElementById('acarsToggleIcon');
|
||||
if (panel.style.display === 'none') {
|
||||
panel.style.display = 'block';
|
||||
icon.innerHTML = '▼';
|
||||
} else {
|
||||
panel.style.display = 'none';
|
||||
icon.innerHTML = '▶';
|
||||
}
|
||||
}
|
||||
|
||||
function setAcarsRegion(region) {
|
||||
const freqInput = document.getElementById('acarsFrequencies');
|
||||
const regions = {
|
||||
'na': '129.125,130.025,130.450,131.550',
|
||||
'eu': '131.525,131.725,131.550',
|
||||
'ap': '131.550,131.450'
|
||||
};
|
||||
freqInput.value = regions[region] || regions['na'];
|
||||
}
|
||||
|
||||
function checkAcarsTools() {
|
||||
fetch('/acars/tools')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const status = document.getElementById('acarsdecStatus');
|
||||
if (data.acarsdec) {
|
||||
status.textContent = 'OK';
|
||||
status.className = 'tool-status ok';
|
||||
} else {
|
||||
status.textContent = 'Missing';
|
||||
status.className = 'tool-status missing';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
const status = document.getElementById('acarsdecStatus');
|
||||
status.textContent = 'Error';
|
||||
status.className = 'tool-status missing';
|
||||
});
|
||||
}
|
||||
|
||||
function startAcars() {
|
||||
const frequencies = document.getElementById('acarsFrequencies').value.split(',').map(f => f.trim());
|
||||
const gain = document.getElementById('acarsGain').value;
|
||||
const ppm = document.getElementById('acarsPpm').value;
|
||||
const device = getSelectedDevice();
|
||||
|
||||
fetch('/acars/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ frequencies, gain, ppm, device })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started') {
|
||||
isAcarsRunning = true;
|
||||
acarsMessageCount = 0;
|
||||
document.getElementById('startAcarsBtn').style.display = 'none';
|
||||
document.getElementById('stopAcarsBtn').style.display = 'block';
|
||||
startAcarsStream();
|
||||
} else {
|
||||
alert('ACARS Error: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(err => alert('ACARS Error: ' + err));
|
||||
}
|
||||
|
||||
function stopAcars() {
|
||||
fetch('/acars/stop', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
isAcarsRunning = false;
|
||||
document.getElementById('startAcarsBtn').style.display = 'block';
|
||||
document.getElementById('stopAcarsBtn').style.display = 'none';
|
||||
if (acarsEventSource) {
|
||||
acarsEventSource.close();
|
||||
acarsEventSource = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startAcarsStream() {
|
||||
if (acarsEventSource) acarsEventSource.close();
|
||||
acarsEventSource = new EventSource('/acars/stream');
|
||||
|
||||
acarsEventSource.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'acars') {
|
||||
acarsMessageCount++;
|
||||
addAcarsToOutput(data);
|
||||
} else if (data.type === 'error') {
|
||||
console.error('ACARS error:', data.message);
|
||||
}
|
||||
};
|
||||
|
||||
acarsEventSource.onerror = function() {
|
||||
console.error('ACARS stream error');
|
||||
};
|
||||
}
|
||||
|
||||
function addAcarsToOutput(data) {
|
||||
const output = document.getElementById('outputList');
|
||||
const item = document.createElement('div');
|
||||
item.className = 'output-item acars-message';
|
||||
|
||||
const flight = data.flight || 'Unknown';
|
||||
const reg = data.reg || '';
|
||||
const label = data.label || '';
|
||||
const text = data.text || data.msg || '';
|
||||
|
||||
item.innerHTML = `
|
||||
<span class="timestamp">${new Date().toLocaleTimeString()}</span>
|
||||
<span class="acars-flight" style="color: var(--accent-cyan); font-weight: bold;">${flight}</span>
|
||||
${reg ? `<span class="acars-reg" style="color: var(--text-secondary);">[${reg}]</span>` : ''}
|
||||
<span class="acars-label" style="color: var(--accent-green);">${label}</span>
|
||||
<span class="acars-text">${text}</span>
|
||||
`;
|
||||
|
||||
output.insertBefore(item, output.firstChild);
|
||||
|
||||
// Keep output manageable
|
||||
while (output.children.length > 200) {
|
||||
output.removeChild(output.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Main ACARS Sidebar (Collapsible - Aircraft Tab)
|
||||
// ============================================
|
||||
let mainAcarsSidebarCollapsed = localStorage.getItem('mainAcarsSidebarCollapsed') === 'true';
|
||||
let mainAcarsEventSource = null;
|
||||
let isMainAcarsRunning = false;
|
||||
let mainAcarsMessageCount = 0;
|
||||
|
||||
// Initialize sidebar state on load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const sidebar = document.getElementById('mainAcarsSidebar');
|
||||
if (sidebar && mainAcarsSidebarCollapsed) {
|
||||
sidebar.classList.add('collapsed');
|
||||
}
|
||||
});
|
||||
|
||||
function toggleMainAcarsSidebar() {
|
||||
const sidebar = document.getElementById('mainAcarsSidebar');
|
||||
mainAcarsSidebarCollapsed = !mainAcarsSidebarCollapsed;
|
||||
sidebar.classList.toggle('collapsed', mainAcarsSidebarCollapsed);
|
||||
localStorage.setItem('mainAcarsSidebarCollapsed', mainAcarsSidebarCollapsed);
|
||||
}
|
||||
|
||||
function toggleMainAcars() {
|
||||
if (isMainAcarsRunning) {
|
||||
stopMainAcars();
|
||||
} else {
|
||||
startMainAcars();
|
||||
}
|
||||
}
|
||||
|
||||
function startMainAcars() {
|
||||
const device = document.getElementById('mainAcarsDevice').value;
|
||||
const region = document.getElementById('mainAcarsRegion').value;
|
||||
|
||||
const regions = {
|
||||
'na': ['129.125', '130.025', '130.450', '131.550'],
|
||||
'eu': ['131.525', '131.725', '131.550'],
|
||||
'ap': ['131.550', '131.450']
|
||||
};
|
||||
const frequencies = regions[region] || regions['na'];
|
||||
|
||||
fetch('/acars/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ frequencies, device: parseInt(device), gain: 40 })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started') {
|
||||
isMainAcarsRunning = true;
|
||||
mainAcarsMessageCount = 0;
|
||||
document.getElementById('mainAcarsToggleBtn').innerHTML = '■ STOP';
|
||||
document.getElementById('mainAcarsToggleBtn').style.background = 'var(--accent-red)';
|
||||
document.getElementById('mainAcarsToggleBtn').style.borderColor = 'var(--accent-red)';
|
||||
document.getElementById('mainAcarsToggleBtn').style.color = '#fff';
|
||||
startMainAcarsStream();
|
||||
} else {
|
||||
alert('ACARS Error: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(err => alert('ACARS Error: ' + err));
|
||||
}
|
||||
|
||||
function stopMainAcars() {
|
||||
fetch('/acars/stop', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
isMainAcarsRunning = false;
|
||||
document.getElementById('mainAcarsToggleBtn').innerHTML = '▶ START';
|
||||
document.getElementById('mainAcarsToggleBtn').style.background = 'var(--bg-dark)';
|
||||
document.getElementById('mainAcarsToggleBtn').style.borderColor = 'var(--accent-cyan)';
|
||||
document.getElementById('mainAcarsToggleBtn').style.color = 'var(--accent-cyan)';
|
||||
if (mainAcarsEventSource) {
|
||||
mainAcarsEventSource.close();
|
||||
mainAcarsEventSource = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startMainAcarsStream() {
|
||||
if (mainAcarsEventSource) mainAcarsEventSource.close();
|
||||
mainAcarsEventSource = new EventSource('/acars/stream');
|
||||
|
||||
mainAcarsEventSource.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'acars') {
|
||||
mainAcarsMessageCount++;
|
||||
addMainAcarsMessage(data);
|
||||
// Also add to main output if ADS-B mode is active
|
||||
addAcarsToOutput(data);
|
||||
}
|
||||
};
|
||||
|
||||
mainAcarsEventSource.onerror = function() {
|
||||
console.error('Main ACARS stream error');
|
||||
};
|
||||
}
|
||||
|
||||
function addMainAcarsMessage(data) {
|
||||
const container = document.getElementById('mainAcarsMessages');
|
||||
|
||||
// Remove placeholder if present
|
||||
const placeholder = container.querySelector('div[style*="text-align: center"]');
|
||||
if (placeholder && placeholder.textContent.includes('No ACARS')) {
|
||||
placeholder.remove();
|
||||
}
|
||||
|
||||
const flight = data.flight || 'Unknown';
|
||||
const reg = data.reg || '';
|
||||
const text = data.text || data.msg || '';
|
||||
const label = data.label || '';
|
||||
const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
|
||||
const msgEl = document.createElement('div');
|
||||
msgEl.className = 'main-acars-msg';
|
||||
msgEl.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2px;">
|
||||
<span style="color: var(--accent-cyan); font-weight: bold;">${flight}</span>
|
||||
<span style="color: var(--text-muted); font-size: 8px;">${time}</span>
|
||||
</div>
|
||||
${reg ? `<div style="color: var(--text-secondary); font-size: 9px;">${reg}</div>` : ''}
|
||||
${label ? `<div style="color: var(--accent-green); font-size: 9px;">[${label}]</div>` : ''}
|
||||
${text ? `<div style="color: var(--text-primary); font-size: 9px; margin-top: 2px; word-break: break-word;">${text.substring(0, 100)}${text.length > 100 ? '...' : ''}</div>` : ''}
|
||||
`;
|
||||
|
||||
container.insertBefore(msgEl, container.firstChild);
|
||||
|
||||
// Keep list manageable
|
||||
while (container.children.length > 50) {
|
||||
container.removeChild(container.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Batching state for aircraft updates to prevent browser freeze
|
||||
let pendingAircraftUpdate = false;
|
||||
let pendingAircraftData = [];
|
||||
|
||||
@@ -195,6 +195,20 @@ TOOL_DEPENDENCIES = {
|
||||
}
|
||||
}
|
||||
},
|
||||
'acars': {
|
||||
'name': 'Aircraft Messaging (ACARS)',
|
||||
'tools': {
|
||||
'acarsdec': {
|
||||
'required': True,
|
||||
'description': 'ACARS VHF decoder',
|
||||
'install': {
|
||||
'apt': 'sudo apt install acarsdec',
|
||||
'brew': 'brew install acarsdec',
|
||||
'manual': 'https://github.com/TLeconte/acarsdec'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'satellite': {
|
||||
'name': 'Satellite Tracking',
|
||||
'tools': {
|
||||
|
||||
Reference in New Issue
Block a user