Compare commits

...

26 Commits

Author SHA1 Message Date
Smittix d427f69dcd chore: Bump version to 2.19.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:42:41 +00:00
Smittix cab04e6e2c feat: Make trails checked by default and remove both radar modes from ADS-B
Trails checkbox now defaults to checked (on). Removed the RADAR view
toggle, Radar overlay checkbox, RadarScope class, and all associated
animation/overlay JS and CSS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:39:50 +00:00
Smittix 8969fefe2e feat: Bundle Roboto Condensed woff2 for offline mode
Add latin and latin-ext woff2 variable font files for Roboto Condensed.
Update fonts-local.css with @font-face declarations using weight range
300-700. Restore conditional CDN/local font loading across all templates
and fix nested Jinja conditionals in dashboard pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:33:49 +00:00
Smittix 5e9fcc5c49 feat: Switch application font to Roboto Condensed
Replace IBM Plex Mono, Space Mono, and JetBrains Mono with Roboto
Condensed across all CSS variables, inline styles, canvas ctx.font
references, and Google Fonts CDN links. Updates 28 files covering
templates, stylesheets, and JS modules for consistent typography.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:29:05 +00:00
Smittix 53b23fc2f7 fix: Replace emoji icons with SVGs, deduplicate help modal, fix fonts
- Replace all emoji HTML entities in Stats Bar Icons with matching SVGs
  from the actual stats bar implementation
- Remove stale inline help modal from index.html, use shared partial
- Set help modal font-family to match app-wide IBM Plex Mono
- Reduce font sizes for cleaner, more professional appearance
- Tighten padding and spacing throughout the help modal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:20:29 +00:00
Smittix eeb3a29ecf docs: Update help modal with all modes and correct SVG icons
Replace emoji icons with actual SVG icons matching nav.html. Add missing
mode descriptions for WebSDR, SubGHz, ISS SSTV, Weather Sat, HF SSTV,
GPS, and BT Locate. Update requirements section with all mode prereqs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:14:42 +00:00
Smittix 4cdfa98a4e feat: Add Support & Contact section with Buy Me a Coffee and email
Add a 4-card support section with Buy Me a Coffee (highlighted in gold),
obfuscated email (click-to-reveal to defeat scrapers), Discord, and
GitHub Issues. Email is assembled via JS at runtime with no plaintext
address in the HTML source. Added links to footer as well.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:04:58 +00:00
Smittix 9fcec6cbb8 feat: Add animated satellite/signal background to GitHub Pages
Canvas-based animation with orbiting satellite dots, signal pulse rings,
drifting particles, and a faint grid overlay. Uses the accent cyan color
at very low opacity to stay subtle. Particles brighten near the cursor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 22:58:09 +00:00
Smittix a527ac191a docs: Update dashboard screenshot with v2.18.0 main screen
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 22:55:50 +00:00
Smittix 8cd3aafd10 docs: Update documentation with new modes, screenshots, and carousel UI
Add VDL2 to README, FEATURES.md, and USAGE.md. Add missing usage guides
for ACARS, WebSDR, ISS SSTV, HF SSTV, TSCM, Spy Stations, and Offline
Mode. Add ISS SSTV section to FEATURES.md. Add 7 new screenshots to
GitHub Pages (Spy Stations, GPS, WebSDR, VDL2, Weather Satellite,
Satellite Tracker, ISS SSTV). Redesign features section as a filterable
carousel with category tabs, SVG icons, and scroll indicators.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 22:52:22 +00:00
Smittix 5c76a423af feat: Remove ACARS as standalone mode, already in ADS-B dashboard
Same as VDL2 - ACARS is integrated into the ADS-B dashboard sidebar
so it doesn't need its own separate mode entry in the nav or index.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 22:25:21 +00:00
Smittix c80bf99b91 fix: Raise nav bar z-index so Space dropdown isn't clipped by content
The mode-nav dropdown menus were being visually covered by the main
content area (maps, visuals) below. Increase z-index from 100 to 1100
so dropdown menus render above all page content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 22:22:15 +00:00
Smittix 6e5cb0a23e fix: Hide cyan scrollbar on controls bar and prevent airband cutoff
The blue bar at the bottom was the cyan-styled horizontal scrollbar on
the controls-bar. Hide it and allow the airband group to flex/wrap so
it stays within the viewport instead of overflowing off-screen.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 22:17:49 +00:00
Smittix ffb98425f1 feat: Consolidate VDL2 into ADS-B dashboard, fix blue bar, add CSV export
Remove VDL2 as a standalone mode since it's already integrated into the
ADS-B dashboard sidebar. Remove the blue border-top on the controls bar.
Add CSV export button to VDL2 panel for downloading collected messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 22:12:03 +00:00
Smittix 533e92c711 fix: Unwrap dumpvdl2 nested vdl2 key so modal displays data correctly
dumpvdl2 JSON nests all fields under a "vdl2" object. Both the sidebar
cards and modal now unwrap this correctly. Modal sections reorganized
into Radio (signal/noise/freq/FEC), AVLC Frame, ACARS, XID, and
Message body with all available fields extracted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 21:52:36 +00:00
Smittix 9f32b05719 feat: Replace VDL2 inline expand with modal for easier reading
Message cards now open a centered modal overlay on click with organized
sections (Connection, ACARS, Position, Message) in a readable grid
layout. Includes raw JSON toggle, closes via X button, backdrop click,
or Escape key.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 21:49:40 +00:00
Smittix 2a05aaa4d8 feat: Make VDL2 message cards clickable with expandable details
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 21:39:51 +00:00
Smittix 6529febcfa fix: Overhaul setup.sh for reliability and macOS compatibility
- Use pre-built SatDump DMG on macOS instead of building from source
  (avoids sol2/Apple Clang deprecation errors)
- Fix `python: command not found` by using explicit venv/bin/python paths
- Split pip install into core + optional packages to avoid all-or-nothing
  failures on newer Python versions
- Make dumpvdl2 optional (warn instead of fail) since VDL2 is one feature
- Fix Homebrew volk package name (libvolk -> volk)
- Add GCC 13+ sol2 deprecation pragma patch for Debian SatDump build
- Quote $(which) to handle paths with spaces
- Remove macOS sed fallback from Debian-only function
- Update TOTAL_STEPS counts (macOS: 22, Debian: 28)
- Add hdiutil detach cleanup to SatDump DMG install trap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 21:25:15 +00:00
Smittix bd87d4b4c6 fix: Use full dumpvdl2 output specifier for v2.6.0 compatibility
dumpvdl2 2.6.0 requires the complete output specifier format
'decoded:json:file:path=-' instead of just 'decoded:json'.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 20:36:13 +00:00
Smittix 5a0589dd69 fix: Wire up VDL2 agent mode, fix dashboard layout gap and stats strip sizing
- Add VDL2 to syncModeUI setter map and allModes array in agents.js
  so agent state sync works for VDL2
- Fix dashboard bottom gap by using flex layout on body instead of
  hardcoded calc(100dvh - 160px) height
- Match source stat font-size to other stats (14px) for consistent
  strip sizing
- Add left-sidebars wrapper, VDL2 agent mode support, mutual sidebar
  collapse, and ACARS/VDL2 modeNames in index.html

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 20:27:25 +00:00
Smittix 5605ae0359 fix: Preserve GPS satellites across DOP-only SKY messages
gpsd sends multiple SKY messages per cycle — some contain only DOP
values with an empty satellites array. Previously this would overwrite
the satellite list, causing the sky view to flicker empty. Now DOP-only
SKY messages preserve the existing satellite list. Also adds a 5-second
polling fallback for satellite data since SSE can miss sky updates due
to queue contention.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 19:48:46 +00:00
Smittix 2b3f351ff0 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>
2026-02-16 19:48:10 +00:00
Smittix 126b9ba2ee fix: Handle CMake 4.0+ compatibility for acarsdec build (#136)
CMake 4.0 removed backward compat with cmake_minimum_required < 3.5.
Add -DCMAKE_POLICY_VERSION_MINIMUM=3.5 to acarsdec cmake invocations
in setup.sh (macOS + Debian) and Dockerfile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:41:22 +00:00
Smittix c0498ebe68 fix: Resolve gpsd deadlock causing GPS connect to hang
start_gpsd_daemon() acquires _gpsd_process_lock then calls
stop_gpsd_daemon() which tries to acquire the same non-reentrant Lock,
causing an immediate deadlock. Changed to RLock so the same thread can
re-enter the lock.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:38:00 +00:00
Smittix 99d52eafe7 chore: Bump version to v2.18.0
Bluetooth enhancements (service data inspector, appearance codes, MAC
cluster tracking, behavioral flags, IRK badges, distance estimation),
ACARS SoapySDR multi-backend support, dump1090 stale process cleanup,
GPS error state, and proximity radar/signal card UI improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:12:10 +00:00
Smittix 2a73318457 chore: Bump version to v2.17.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 22:00:32 +00:00
75 changed files with 4901 additions and 1596 deletions
+23 -1
View File
@@ -41,6 +41,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
bluez \
bluetooth \
# GPS support
gpsd \
gpsd-clients \
# Utilities
# APRS
@@ -94,6 +95,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libfftw3-dev \
liblapack-dev \
libcodec2-dev \
libglib2.0-dev \
libxml2-dev \
# Build dump1090
&& cd /tmp \
&& git clone --depth 1 https://github.com/flightaware/dump1090.git \
@@ -136,10 +139,29 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& git clone --depth 1 https://github.com/TLeconte/acarsdec.git \
&& cd acarsdec \
&& mkdir build && cd build \
&& cmake .. -Drtl=ON \
&& cmake .. -Drtl=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
&& make \
&& cp acarsdec /usr/bin/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
&& cd /tmp \
&& git clone https://github.com/windytan/slowrx.git \
+2
View File
@@ -32,6 +32,7 @@ Support the developer of this open-source project
- **Aircraft Tracking** - ADS-B via dump1090 with real-time map and radar
- **Vessel Tracking** - AIS ship tracking with VHF DSC distress monitoring
- **ACARS Messaging** - Aircraft datalink messages via acarsdec
- **VDL2** - VHF Data Link Mode 2 aircraft datalink decoding via dumpvdl2
- **Listening Post** - Wideband frequency scanner with real-time audio monitoring
- **Weather Satellites** - NOAA APT and Meteor LRPT image decoding via SatDump with auto-scheduler
- **WebSDR** - Remote HF/shortwave listening via KiwiSDR network
@@ -244,6 +245,7 @@ Created by **smittix** - [GitHub](https://github.com/smittix)
[acarsdec](https://github.com/TLeconte/acarsdec) |
[direwolf](https://github.com/wb2osz/direwolf) |
[rtl_amr](https://github.com/bemasher/rtlamr) |
[dumpvdl2](https://github.com/szpajder/dumpvdl2) |
[aircrack-ng](https://www.aircrack-ng.org/) |
[Leaflet.js](https://leafletjs.com/) |
[SatDump](https://github.com/SatDump/SatDump) |
+41 -29
View File
@@ -29,7 +29,7 @@ from flask import Flask, render_template, jsonify, send_file, Response, request,
from werkzeug.security import check_password_hash
from config import VERSION, CHANGELOG, SHARED_OBSERVER_LOCATION_ENABLED, DEFAULT_LATITUDE, DEFAULT_LONGITUDE
from utils.dependencies import check_tool, check_all_dependencies, TOOL_DEPENDENCIES
from utils.process import cleanup_stale_processes
from utils.process import cleanup_stale_processes, cleanup_stale_dump1090
from utils.sdr import SDRFactory
from utils.cleanup import DataStore, cleanup_manager
from utils.constants import (
@@ -150,6 +150,11 @@ acars_process = None
acars_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
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_process = None
aprs_rtl_process = None
@@ -647,27 +652,27 @@ def export_bluetooth() -> Response:
})
def _get_subghz_active() -> bool:
"""Check if SubGHz manager has an active process."""
try:
from utils.subghz import get_subghz_manager
return get_subghz_manager().active_mode != 'idle'
except Exception:
return False
def _get_dmr_active() -> bool:
"""Check if Digital Voice decoder has an active process."""
try:
from routes import dmr as dmr_module
proc = dmr_module.dmr_dsd_process
return bool(dmr_module.dmr_running and proc and proc.poll() is None)
except Exception:
return False
@app.route('/health')
def health_check() -> Response:
def _get_subghz_active() -> bool:
"""Check if SubGHz manager has an active process."""
try:
from utils.subghz import get_subghz_manager
return get_subghz_manager().active_mode != 'idle'
except Exception:
return False
def _get_dmr_active() -> bool:
"""Check if Digital Voice decoder has an active process."""
try:
from routes import dmr as dmr_module
proc = dmr_module.dmr_dsd_process
return bool(dmr_module.dmr_running and proc and proc.poll() is None)
except Exception:
return False
@app.route('/health')
def health_check() -> Response:
"""Health check endpoint for monitoring."""
import time
return jsonify({
@@ -680,13 +685,14 @@ def health_check() -> Response:
'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),
'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),
'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),
'dsc': dsc_process is not None and (dsc_process.poll() is None if dsc_process else False),
'dmr': _get_dmr_active(),
'subghz': _get_subghz_active(),
},
'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),
'dsc': dsc_process is not None and (dsc_process.poll() is None if dsc_process else False),
'dmr': _get_dmr_active(),
'subghz': _get_subghz_active(),
},
'data': {
'aircraft_count': len(adsb_aircraft),
'vessel_count': len(ais_vessels),
@@ -702,6 +708,7 @@ def health_check() -> Response:
def kill_all() -> Response:
"""Kill all decoder, WiFi, and Bluetooth processes."""
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 dmr_process, dmr_rtl_process
@@ -714,7 +721,7 @@ def kill_all() -> Response:
processes_to_kill = [
'rtl_fm', 'multimon-ng', 'rtl_433',
'airodump-ng', 'aireplay-ng', 'airmon-ng',
'dump1090', 'acarsdec', 'direwolf', 'AIS-catcher',
'dump1090', 'acarsdec', 'dumpvdl2', 'direwolf', 'AIS-catcher',
'hcitool', 'bluetoothctl', 'satdump', 'dsd',
'rtl_tcp', 'rtl_power', 'rtlamr', 'ffmpeg',
'hackrf_transfer', 'hackrf_sweep'
@@ -751,6 +758,10 @@ def kill_all() -> Response:
with acars_lock:
acars_process = None
# Reset VDL2 state
with vdl2_lock:
vdl2_process = None
# Reset APRS state
with aprs_lock:
aprs_process = None
@@ -877,6 +888,7 @@ def main() -> None:
# Clean up any stale processes from previous runs
cleanup_stale_processes()
cleanup_stale_dump1090()
# Initialize database for settings storage
from utils.database import init_db
+36 -1
View File
@@ -7,10 +7,45 @@ import os
import sys
# Application version
VERSION = "2.16.0"
VERSION = "2.19.0"
# Changelog - latest release notes (shown on welcome screen)
CHANGELOG = [
{
"version": "2.19.0",
"date": "February 2026",
"highlights": [
"VDL2 mode with modal message viewer, consolidated into ADS-B dashboard",
"ADS-B: trails enabled by default, radar modes removed, CSV export added",
"Bundled Roboto Condensed font for offline mode with SVG icon overhaul",
"Help modal updated with all modes and correct SVG icons",
"Setup script overhauled for reliability and macOS compatibility",
"GPS fix for preserving satellites across DOP-only SKY messages",
"Fix gpsd deadlock causing GPS connect to hang",
]
},
{
"version": "2.18.0",
"date": "February 2026",
"highlights": [
"Bluetooth: service data inspector, appearance codes, MAC cluster tracking, and behavioral flags",
"Bluetooth: IRK badge display, distance estimation with confidence, and signal stability metrics",
"ACARS: SoapySDR device support for SDRplay, LimeSDR, Airspy, and other non-RTL backends",
"ADS-B: stale dump1090 process cleanup via PID file tracking",
"GPS: error state indicator and UI refinements",
"Proximity radar and signal card UI improvements",
]
},
{
"version": "2.17.0",
"date": "February 2026",
"highlights": [
"BT Locate: SAR Bluetooth device location with GPS-tagged signal trail and proximity alerts",
"IRK auto-detection: extract Identity Resolving Keys from paired devices (macOS/Linux)",
"GPS mode: real-time position tracking with live map, speed, altitude, and satellite info",
"Bluetooth scanner lifecycle fix for bleak scan timeout tracking",
]
},
{
"version": "2.16.0",
"date": "February 2026",
+25 -1
View File
@@ -99,6 +99,18 @@ Digital Selective Calling (DSC) monitoring on the international maritime distres
- **Multi-SDR support** - RTL-SDR, HackRF, LimeSDR, Airspy, SDRplay
- **Message filtering** - filter by message type, flight, or registration
## VDL2 (VHF Data Link Mode 2)
- **Real-time VDL2 decoding** via dumpvdl2 on standard VDL2 frequencies
- **ACARS-over-AVLC** message capture with full frame parsing
- **Signal analysis** - frequency, signal level, noise level, SNR, burst length
- **AVLC frame details** - source/destination addresses, frame type, command/response
- **Raw JSON inspection** - expandable raw message data for each frame
- **Multi-frequency monitoring** - simultaneous reception on multiple VDL2 channels
- **Multi-SDR support** - RTL-SDR, HackRF, LimeSDR, Airspy, SDRplay
- **CSV/JSON export** - export captured messages for offline analysis
- **Integrated with ADS-B dashboard** - VDL2 messages linked to aircraft tracking
## Listening Post
- **Wideband frequency scanning** via rtl_power sweep with SNR filtering
@@ -122,11 +134,23 @@ Digital Selective Calling (DSC) monitoring on the international maritime distres
- **Receiver discovery** with automatic caching
- **Frequency tuning** with band presets
## ISS SSTV
- **ISS SSTV image reception** on 145.800 MHz FM during special event transmissions
- **Real-time ISS tracking** with world map and pass predictions
- **Doppler correction** - optional lat/lon input for real-time frequency shift compensation
- **Next pass countdown** - time remaining until ISS is overhead
- **Image gallery** with timestamped decoded imagery
- **TLE updates** - fetch latest ISS orbital elements
- **Multi-SDR support** - RTL-SDR, HackRF, LimeSDR, Airspy, SDRplay
## HF SSTV
- **Terrestrial SSTV decoding** across HF (80m-10m), VHF (6m, 2m), and UHF (70cm) bands
- **Predefined frequency lookup** for active SSTV calling frequencies
- **Predefined frequency lookup** for 13 active SSTV calling frequencies
- **Auto-modulation selection** - frequency table maps to correct mode (USB, LSB, FM)
- **Image gallery** with decoded transmissions
- **Common modes supported** - PD120, PD180, Martin1, Scottie1, Robot36
## APRS
+144
View File
@@ -69,6 +69,22 @@ INTERCEPT automatically detects known trackers:
Common ISM band protocols including garage doors, key fobs, weather stations, and IoT devices in the 300-928 MHz range.
## VDL2 (Aircraft Datalink)
1. **Select Hardware** - Choose your SDR type
2. **Select Device** - Choose your SDR device
3. **Set Frequencies** - Default VDL2 frequencies are pre-configured (136.975, 136.725, 136.775 MHz etc.)
4. **Start Decoding** - Click "Start" to begin VDL2 reception via dumpvdl2
5. **View Messages** - AVLC frames appear with source/destination, signal levels, and decoded content
6. **Inspect Details** - Click a message to view full AVLC frame details and raw JSON
7. **Export** - Use CSV or JSON export buttons to save captured messages
### Tips
- VDL2 is most active near airports and along flight corridors
- Multiple frequencies can be monitored simultaneously for better coverage
- VDL2 data is also accessible from the ADS-B dashboard
## Listening Post
1. **Select Hardware** - Choose your SDR type
@@ -110,6 +126,23 @@ The system highlights aircraft transmitting emergency squawks:
- **7600** - Radio failure
- **7700** - General emergency
## ACARS Messaging
1. **Select Hardware** - Choose your SDR type
2. **Select Device** - Choose your SDR device
3. **Select Region** - Choose North America, Europe, or Asia-Pacific to auto-populate frequencies
4. **Select Frequencies** - Check one or more ACARS frequencies (131.550 MHz primary worldwide, 130.025 MHz secondary USA/Canada, etc.)
5. **Adjust Gain** - Set gain (0 for auto, or 0-50 dB)
6. **Start Decoding** - Click "Start" to begin ACARS reception via acarsdec
7. **View Messages** - Aircraft messages appear in real-time with flight ID, registration, and content
### Tips
- A vertical polarization antenna works best for ACARS
- Quarter-wave dipole: 57 cm per element at 130 MHz
- Stock SDR antenna may work at close range near airports
- Outdoor placement with clear sky view significantly improves reception
## ADS-B History (Optional)
The history dashboard persists aircraft messages and per-aircraft snapshots to Postgres for long-running tracking and reporting.
@@ -221,6 +254,61 @@ Digital Selective Calling monitoring runs alongside AIS:
- Distress positions plotted with pulsing alert markers
- Audio alerts for critical messages
## WebSDR
1. **Set Frequency** - Enter a frequency in kHz (e.g., 6500 for 6.5 MHz)
2. **Select Mode** - Choose demodulation mode (USB, LSB, AM, CW)
3. **Find Receivers** - Click "Find Receivers" to discover available KiwiSDR nodes worldwide
4. **Select Receiver** - Click a receiver from the list to connect
5. **Listen** - Audio streams in real-time via WebSocket
6. **Adjust Volume** - Use the volume slider and monitor the S-meter
7. **Spy Station Presets** - Use the quick-tune buttons to jump to known number station frequencies
### Tips
- Requires an internet connection to access the KiwiSDR network
- Receiver list is cached for 1 hour to reduce API load
- Receivers are sorted by distance from your location
- Integrated spy station presets allow quick tuning to SIGINT targets
## ISS SSTV
1. **Select Hardware** - Choose your SDR type
2. **Select Device** - Choose your SDR device
3. **Set Frequency** - Default is 145.800 MHz (ISS downlink)
4. **Set Location** - Enter lat/lon for Doppler correction and pass prediction
5. **Update TLE** - Click "Update TLE" to fetch latest ISS orbital elements
6. **Wait for Pass** - The next pass countdown shows when ISS will be overhead
7. **Start Decoding** - Click "Start" to begin SSTV reception
8. **View Images** - Decoded SSTV images appear in the gallery with timestamps
### Tips
- A V-dipole or better antenna is required (stock antenna will not work)
- V-dipole construction: 51 cm per element at 145.8 MHz, 120-degree angle between elements
- ISS SSTV events occur during special anniversaries and missions — check ARISS for schedules
- Best passes have elevation > 30 degrees above horizon
- Doppler shift tracking dramatically improves reception quality
- Common SSTV modes: PD120, PD180, Martin1, Scottie1
- Outdoor antenna placement with clear sky view is essential
## HF SSTV
1. **Select Hardware** - Choose your SDR type
2. **Select Device** - Choose your SDR device
3. **Select Frequency** - Choose from 13 preset frequencies or enter a custom one
4. **Modulation** - Auto-selected based on frequency (USB for HF, FM for VHF/UHF)
5. **Start Decoding** - Click "Start" to begin SSTV reception
6. **View Images** - Decoded amateur radio images appear in the gallery
### Tips
- HF frequencies (3-30 MHz) require an upconverter with RTL-SDR
- VHF/UHF frequencies (145 MHz, 433 MHz) work directly with RTL-SDR
- Most popular frequency: 14.230 MHz USB (20m band) with regular activity
- Weekend activity peaks on most HF bands
- Amateur license is not required to receive (listen-only)
## APRS
1. **Select Hardware** - Choose your SDR type
@@ -283,6 +371,46 @@ Digital Selective Calling monitoring runs alongside AIS:
- GPS fix may take 30-60 seconds after cold start
- Accuracy improves with more satellites in view
## TSCM (Counter-Surveillance)
1. **Select Sweep Type** - Choose from Quick Scan (2 min), Standard (5 min), Full Sweep (15 min), or presets for Wireless Cameras, Body-Worn Devices, or GPS Trackers
2. **Select Scan Sources** - Toggle WiFi, Bluetooth, and/or RF/SDR scanning and select the appropriate interfaces
3. **Select Baseline** - Optionally choose a previously recorded baseline to compare against
4. **Start Sweep** - Click "Start Sweep" to begin scanning
5. **Review Results** - Detected devices are classified and scored by threat level
6. **Record Baseline** - In a known clean environment, record a baseline for future comparison
7. **Export Report** - Generate PDF report, JSON annex, or CSV data
### Threat Levels
- **Informational (0-2)** - Known or expected devices
- **Needs Review (3-5)** - Unusual devices requiring assessment
- **High Interest (6+)** - Multiple indicators warrant investigation
### Tips
- Record a baseline in a known clean environment before conducting sweeps
- Use the meeting window feature to flag new RF signatures during sensitive periods
- Full functionality requires WiFi adapter, Bluetooth adapter, and SDR hardware
- Threat detection uses a database of 47K+ known tracker fingerprints
## Spy Stations
1. **Browse Database** - View the full list of documented number stations and diplomatic networks
2. **Filter by Type** - Toggle between Number Stations and Diplomatic Networks
3. **Filter by Country** - Select specific countries (Russia, Cuba, Israel, Poland, etc.)
4. **Filter by Mode** - Filter by demodulation mode (USB, AM, CW, OFDM)
5. **View Details** - Click "Details" on a station card for full information
6. **Tune In** - Click "Tune In" to route the station frequency to the Listening Post or WebSDR
### Tips
- Data sourced from priyom.org (non-profit monitoring community)
- Most activity is on HF bands (3-30 MHz) — propagation varies by time of day
- Notable stations: UVB-76 "The Buzzer" (4625 kHz), E06 English Man, HM01 Cuban Numbers
- Legal to monitor in most countries (check local regulations)
- No decryption or content decoding is included — this is a reference database
## Meshtastic
1. **Connect Device** - Plug in a Meshtastic device via USB or connect via TCP
@@ -291,6 +419,22 @@ Digital Selective Calling monitoring runs alongside AIS:
4. **View Nodes** - Connected nodes displayed with signal metrics (RSSI, SNR)
5. **Send Messages** - Type messages to broadcast on the mesh
## Offline Mode
1. **Open Settings** - Click the gear icon in the navigation bar
2. **Offline Tab** - Toggle "Offline Mode" to enable local assets
3. **Configure Sources** - Switch assets and fonts from CDN to local
4. **Set Tile Provider** - Choose a map tile provider or enter a custom tile server URL
5. **Check Assets** - Click "Check Assets" to verify all local files are present
### Tips
- Download required assets: Leaflet JS/CSS, Chart.js, Inter and JetBrains Mono fonts
- Assets are stored in the `static/vendor/` directory
- For maps, you need a local tile server (e.g., self-hosted OpenStreetMap tiles)
- Missing assets fail gracefully with console warnings
- Useful for air-gapped environments, field deployments, or reducing latency
## Remote Agents (Distributed SIGINT)
Deploy lightweight sensor nodes across multiple locations and aggregate data to a central controller.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 694 KiB

After

Width:  |  Height:  |  Size: 790 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

+526 -145
View File
@@ -11,6 +11,7 @@
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<canvas id="bg-canvas"></canvas>
<nav class="navbar">
<div class="nav-container">
<a href="#" class="nav-logo">iNTERCEPT</a>
@@ -35,7 +36,7 @@
</div>
<div class="hero-stats">
<div class="stat">
<span class="stat-value">20+</span>
<span class="stat-value">25+</span>
<span class="stat-label">Modes</span>
</div>
<div class="stat">
@@ -58,151 +59,143 @@
<h2>Capabilities</h2>
<p class="section-subtitle">Everything you need for signal intelligence in one interface</p>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">📟</div>
<h3>Pager Decoding</h3>
<p>Decode POCSAG and FLEX pager messages using rtl_fm and multimon-ng. Monitor emergency services and legacy paging systems.</p>
</div>
<div class="feature-card">
<div class="feature-icon">✈️</div>
<h3>Aircraft Tracking</h3>
<p>Real-time ADS-B tracking with interactive maps, aircraft photos, emergency squawk detection, and range visualization.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📡</div>
<h3>433MHz Sensors</h3>
<p>Decode 200+ protocols including weather stations, TPMS, smart home devices, and IoT sensors via rtl_433.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📻</div>
<h3>Sub-GHz Analyzer</h3>
<p>HackRF-based signal capture and protocol decoding for 300-928 MHz ISM bands with spectrum analysis and replay.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📻</div>
<h3>Listening Post</h3>
<p>Frequency scanner with real-time audio monitoring, fine-tuning controls, and customizable frequency presets.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🛰️</div>
<h3>Satellite Tracking</h3>
<p>Track satellites with TLE data, sky plots, ground track visualization, and pass predictions for your location.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📶</div>
<h3>WiFi Scanning</h3>
<p>Monitor mode reconnaissance via aircrack-ng. Network discovery, client tracking, and handshake capture.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔵</div>
<h3>Bluetooth Scanning</h3>
<p>Device discovery with tracker detection for AirTags, Tile, Samsung SmartTag, and other Bluetooth devices.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📍</div>
<h3>BT Locate</h3>
<p>SAR Bluetooth device location with GPS-tagged signal trail mapping, IRK-based RPA resolution, and proximity audio alerts.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🛰️</div>
<h3>GPS Tracking</h3>
<p>Real-time GPS position tracking with live map, speed, heading, altitude, satellite info, and track recording.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🛡️</div>
<h3>TSCM</h3>
<p>Counter-surveillance with baseline recording, threat detection, device correlation, and risk scoring.</p>
</div>
<div class="feature-card">
<div class="feature-icon"></div>
<h3>Meter Reading</h3>
<p>Intercept smart utility meters via rtl_amr. Monitor electricity, gas, and water meter transmissions.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🚢</div>
<h3>Vessel Tracking</h3>
<p>Real-time AIS ship tracking via AIS-catcher. Monitor maritime traffic with vessel details, course, speed, and destination.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔢</div>
<h3>Spy Stations</h3>
<p>Number stations and diplomatic HF network database. Frequencies, schedules, and background info from priyom.org.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🌐</div>
<h3>Remote Agents</h3>
<p>Distributed signal intelligence with remote sensor nodes. Deploy agents across multiple locations and aggregate data to a central controller.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📴</div>
<h3>Offline Mode</h3>
<p>Run without internet using bundled assets. Choose from multiple map tile providers or use your own local tile server.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📡</div>
<h3>Meshtastic</h3>
<p>LoRa mesh network integration. Connect to Meshtastic devices for decentralized, long-range communication monitoring.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🌧️</div>
<h3>Weather Satellites</h3>
<p>NOAA APT and Meteor LRPT image decoding via SatDump with auto-scheduler, polar plot, and ground track map.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🖼️</div>
<h3>ISS SSTV</h3>
<p>Receive Slow Scan Television from the ISS. Real-time tracking globe, pass predictions, and image decoding.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🖼️</div>
<h3>HF SSTV</h3>
<p>Terrestrial SSTV on shortwave frequencies. Decode amateur radio image transmissions across HF, VHF, and UHF bands.</p>
</div>
<div class="feature-card">
<div class="feature-icon">✈️</div>
<h3>ACARS</h3>
<p>Aircraft datalink messages via acarsdec. Decode operational, weather, and position reports from commercial flights.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📍</div>
<h3>APRS</h3>
<p>Amateur packet radio position reports and telemetry via direwolf. Track amateur radio operators on an interactive map.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🌐</div>
<h3>WebSDR</h3>
<p>Remote HF/shortwave listening via the KiwiSDR network. Access receivers worldwide with real-time audio streaming.</p>
</div>
<div class="feature-card">
<div class="feature-icon"></div>
<h3>Utility Meters</h3>
<p>Smart meter monitoring via rtl_amr. Receive electric, gas, and water meter broadcasts in real time.</p>
</div>
<div class="carousel-filters">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="sdr">SDR / RF</button>
<button class="filter-btn" data-filter="aviation">Aviation & Maritime</button>
<button class="filter-btn" data-filter="space">Space & Satellite</button>
<button class="filter-btn" data-filter="wireless">Wireless & Security</button>
<button class="filter-btn" data-filter="platform">Platform</button>
</div>
<div class="carousel-wrapper">
<button class="carousel-arrow carousel-arrow-left" aria-label="Scroll left">&#8249;</button>
<div class="carousel-track">
<div class="feature-card" data-category="sdr">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="6" y1="9" x2="6" y2="15"/><line x1="10" y1="9" x2="10" y2="15"/><line x1="14" y1="11" x2="18" y2="11"/><line x1="14" y1="13" x2="18" y2="13"/></svg></div>
<h3>Pager Decoding</h3>
<p>Decode POCSAG and FLEX pager messages using rtl_fm and multimon-ng. Monitor emergency services and legacy paging systems.</p>
</div>
<div class="feature-card" data-category="sdr">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="M12 18v4"/><circle cx="12" cy="12" r="4"/><path d="M4.93 4.93l2.83 2.83"/><path d="M16.24 16.24l2.83 2.83"/><path d="M2 12h4"/><path d="M18 12h4"/><path d="M4.93 19.07l2.83-2.83"/><path d="M16.24 7.76l2.83-2.83"/></svg></div>
<h3>433MHz Sensors</h3>
<p>Decode 200+ protocols including weather stations, TPMS, smart home devices, and IoT sensors via rtl_433.</p>
</div>
<div class="feature-card" data-category="sdr">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h4l3-9 6 18 3-9h4"/></svg></div>
<h3>Sub-GHz Analyzer</h3>
<p>HackRF-based signal capture and protocol decoding for 300-928 MHz ISM bands with spectrum analysis and replay.</p>
</div>
<div class="feature-card" data-category="sdr">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 18.5a6.5 6.5 0 1 1 0-13"/><path d="M17 12h5"/><path d="M12 7V2"/><circle cx="12" cy="12" r="2"/><path d="M8.5 8.5L5 5"/></svg></div>
<h3>Listening Post</h3>
<p>Frequency scanner with real-time audio monitoring, fine-tuning controls, and customizable frequency presets.</p>
</div>
<div class="feature-card" data-category="sdr">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></div>
<h3>WebSDR</h3>
<p>Remote HF/shortwave listening via the KiwiSDR network. Access receivers worldwide with real-time audio streaming.</p>
</div>
<div class="feature-card" data-category="sdr">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M8 8h2v2H8z"/><path d="M14 8h2v2h-2z"/><path d="M8 14h2v2H8z"/><path d="M14 14h2v2h-2z"/><path d="M11 8h2v2h-2z"/><path d="M11 11h2v2h-2z"/></svg></div>
<h3>Spy Stations</h3>
<p>Number stations and diplomatic HF network database. Frequencies, schedules, and background info from priyom.org.</p>
</div>
<div class="feature-card" data-category="sdr">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg></div>
<h3>APRS</h3>
<p>Amateur packet radio position reports and telemetry via direwolf. Track amateur radio operators on an interactive map.</p>
</div>
<div class="feature-card" data-category="sdr">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg></div>
<h3>Utility Meters</h3>
<p>Smart meter monitoring via rtl_amr. Receive electric, gas, and water meter broadcasts in real time.</p>
</div>
<div class="feature-card" data-category="aviation">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17.8 19.2L16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z"/></svg></div>
<h3>Aircraft Tracking</h3>
<p>Real-time ADS-B tracking with interactive maps, aircraft photos, emergency squawk detection, and range visualization.</p>
</div>
<div class="feature-card" data-category="aviation">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 8h10"/><path d="M7 12h6"/><path d="M7 16h8"/></svg></div>
<h3>ACARS</h3>
<p>Aircraft datalink messages via acarsdec. Decode operational, weather, and position reports from commercial flights.</p>
</div>
<div class="feature-card" data-category="aviation">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/><circle cx="12" cy="12" r="9"/><path d="M3.5 9h17"/><path d="M3.5 15h17"/></svg></div>
<h3>VDL2</h3>
<p>VHF Data Link Mode 2 aircraft datalink decoding via dumpvdl2. Real-time ACARS-over-AVLC message capture with signal analysis.</p>
</div>
<div class="feature-card" data-category="aviation">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20l4-4h3l4-7 2 4h2l5-9"/><path d="M22 20H2"/><path d="M6 16v4"/></svg></div>
<h3>Vessel Tracking</h3>
<p>Real-time AIS ship tracking via AIS-catcher. Monitor maritime traffic with vessel details, course, speed, and destination.</p>
</div>
<div class="feature-card" data-category="space">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v4"/><path d="M12 19v4"/><path d="M5 5l2 2"/><path d="M17 17l2 2"/><path d="M1 12h4"/><path d="M19 12h4"/><path d="M5 19l2-2"/><path d="M17 7l2-2"/><ellipse cx="12" cy="12" rx="10" ry="4" transform="rotate(45 12 12)"/></svg></div>
<h3>Satellite Tracking</h3>
<p>Track satellites with TLE data, sky plots, ground track visualization, and pass predictions for your location.</p>
</div>
<div class="feature-card" data-category="space">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/><circle cx="12" cy="12" r="4"/><path d="M16 12a4 4 0 0 0-4-4"/></svg></div>
<h3>Weather Satellites</h3>
<p>NOAA APT and Meteor LRPT image decoding via SatDump with auto-scheduler, polar plot, and ground track map.</p>
</div>
<div class="feature-card" data-category="space">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg></div>
<h3>ISS SSTV</h3>
<p>Receive Slow Scan Television from the ISS. Real-time tracking globe, pass predictions, and image decoding.</p>
</div>
<div class="feature-card" data-category="space">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 3v18"/></svg></div>
<h3>HF SSTV</h3>
<p>Terrestrial SSTV on shortwave frequencies. Decode amateur radio image transmissions across HF, VHF, and UHF bands.</p>
</div>
<div class="feature-card" data-category="space">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/><path d="M12 8l4 4-4 4"/></svg></div>
<h3>GPS Tracking</h3>
<p>Real-time GPS position tracking with live map, speed, heading, altitude, satellite info, and track recording.</p>
</div>
<div class="feature-card" data-category="wireless">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1"/></svg></div>
<h3>WiFi Scanning</h3>
<p>Monitor mode reconnaissance via aircrack-ng. Network discovery, client tracking, and handshake capture.</p>
</div>
<div class="feature-card" data-category="wireless">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="6"/><path d="M12 16v5"/><path d="M8 21h8"/><path d="M9.5 7.5L12 10l2.5-2.5"/></svg></div>
<h3>Bluetooth Scanning</h3>
<p>Device discovery with tracker detection for AirTags, Tile, Samsung SmartTag, and other Bluetooth devices.</p>
</div>
<div class="feature-card" data-category="wireless">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="7" stroke-dasharray="4 2"/><circle cx="12" cy="12" r="11" stroke-dasharray="2 3"/><line x1="12" y1="1" x2="12" y2="3"/></svg></div>
<h3>BT Locate</h3>
<p>SAR Bluetooth device location with GPS-tagged signal trail mapping, IRK-based RPA resolution, and proximity audio alerts.</p>
</div>
<div class="feature-card" data-category="wireless">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s-8-4.5-8-11.8A8 8 0 0 1 12 2a8 8 0 0 1 8 8.2c0 7.3-8 11.8-8 11.8z"/><circle cx="12" cy="10" r="3"/><path d="M12 2v3"/><path d="M4.93 4.93l2.12 2.12"/><path d="M20 12h-3"/></svg></div>
<h3>TSCM</h3>
<p>Counter-surveillance with baseline recording, threat detection, device correlation, and risk scoring.</p>
</div>
<div class="feature-card" data-category="wireless">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg></div>
<h3>Meshtastic</h3>
<p>LoRa mesh network integration. Connect to Meshtastic devices for decentralized, long-range communication monitoring.</p>
</div>
<div class="feature-card" data-category="platform">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/><circle cx="9" cy="10" r="1.5"/><circle cx="15" cy="10" r="1.5"/><path d="M5 10h2"/><path d="M17 10h2"/></svg></div>
<h3>Remote Agents</h3>
<p>Distributed signal intelligence with remote sensor nodes. Deploy agents across multiple locations and aggregate data to a central controller.</p>
</div>
<div class="feature-card" data-category="platform">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18.36 6.64A9 9 0 0 1 20.77 15"/><path d="M6.16 6.16a9 9 0 0 0-2.57 8.84"/><path d="M12 2v4"/><path d="M2 12h4"/><line x1="2" y1="2" x2="22" y2="22"/><circle cx="12" cy="12" r="3"/></svg></div>
<h3>Offline Mode</h3>
<p>Run without internet using bundled assets. Choose from multiple map tile providers or use your own local tile server.</p>
</div>
</div>
<button class="carousel-arrow carousel-arrow-right" aria-label="Scroll right">&#8250;</button>
</div>
<div class="carousel-indicators" id="carousel-indicators"></div>
</div>
</section>
@@ -252,6 +245,34 @@
<img src="images/bt-locate.png" alt="BT Locate SAR Tracker">
<span class="screenshot-label">BT Locate — SAR Tracker</span>
</div>
<div class="screenshot-item">
<img src="images/spy-stations.png" alt="Spy Stations Database">
<span class="screenshot-label">Spy Stations</span>
</div>
<div class="screenshot-item">
<img src="images/gps.png" alt="GPS Receiver">
<span class="screenshot-label">GPS Receiver</span>
</div>
<div class="screenshot-item">
<img src="images/websdr.png" alt="WebSDR Remote Listening">
<span class="screenshot-label">WebSDR</span>
</div>
<div class="screenshot-item">
<img src="images/vdl2.png" alt="VDL2 Aircraft Datalink">
<span class="screenshot-label">VDL2 Aircraft Datalink</span>
</div>
<div class="screenshot-item">
<img src="images/weather-satellite.png" alt="Weather Satellite Decoder">
<span class="screenshot-label">Weather Satellite</span>
</div>
<div class="screenshot-item">
<img src="images/satellite-tracker.png" alt="Satellite Tracker">
<span class="screenshot-label">Satellite Tracker</span>
</div>
<div class="screenshot-item">
<img src="images/iss-sstv.png" alt="ISS SSTV Decoder">
<span class="screenshot-label">ISS SSTV</span>
</div>
</div>
</div>
</section>
@@ -336,6 +357,36 @@ docker compose up -d</code></pre>
</div>
</section>
<section class="support">
<div class="container">
<h2>Support & Contact</h2>
<p class="section-subtitle">Help keep iNTERCEPT alive or get in touch</p>
<div class="support-grid">
<a href="https://www.buymeacoffee.com/smittix" target="_blank" class="support-card support-coffee">
<div class="support-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 8h1a4 4 0 0 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V8z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/></svg></div>
<h3>Buy Me a Coffee</h3>
<p>Support development with a one-time donation</p>
</a>
<a href="#" id="email-card" class="support-card" onclick="return false;">
<div class="support-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 4L12 13 2 4"/></svg></div>
<h3>Email</h3>
<p id="email-text">Click to reveal</p>
</a>
<a href="https://discord.gg/EyeksEJmWE" target="_blank" class="support-card">
<div class="support-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><circle cx="12" cy="12" r="10"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
<h3>Discord</h3>
<p>Join the community for help and discussion</p>
</a>
<a href="https://github.com/smittix/intercept/issues" target="_blank" class="support-card">
<div class="support-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div>
<h3>Report an Issue</h3>
<p>Bug reports and feature requests on GitHub</p>
</a>
</div>
</div>
</section>
<footer class="footer">
<div class="container">
<div class="footer-content">
@@ -346,6 +397,8 @@ docker compose up -d</code></pre>
<div class="footer-links">
<a href="https://github.com/smittix/intercept" target="_blank">GitHub</a>
<a href="https://discord.gg/EyeksEJmWE" target="_blank">Discord</a>
<a href="#" id="footer-email">Email</a>
<a href="https://www.buymeacoffee.com/smittix" target="_blank">Donate</a>
<a href="https://github.com/smittix/intercept/blob/main/docs/USAGE.md">Documentation</a>
<a href="https://github.com/smittix/intercept/blob/main/docs/DISTRIBUTED_AGENTS.md">Remote Agents</a>
</div>
@@ -394,6 +447,334 @@ docker compose up -d</code></pre>
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeLightbox();
});
// Carousel functionality
(function() {
const track = document.querySelector('.carousel-track');
const cards = Array.from(track.querySelectorAll('.feature-card'));
const leftArrow = document.querySelector('.carousel-arrow-left');
const rightArrow = document.querySelector('.carousel-arrow-right');
const filterBtns = document.querySelectorAll('.filter-btn');
const indicatorContainer = document.getElementById('carousel-indicators');
const SCROLL_AMOUNT = 300;
function updateArrows() {
leftArrow.disabled = track.scrollLeft <= 0;
rightArrow.disabled = track.scrollLeft + track.clientWidth >= track.scrollWidth - 2;
}
function buildIndicators() {
const visible = cards.filter(c => !c.classList.contains('hidden'));
const totalWidth = visible.length * 300;
const pages = Math.max(1, Math.ceil(totalWidth / track.clientWidth));
indicatorContainer.innerHTML = '';
for (let i = 0; i < pages; i++) {
const dot = document.createElement('button');
dot.className = 'carousel-dot' + (i === 0 ? ' active' : '');
dot.addEventListener('click', () => {
track.scrollTo({ left: (track.scrollWidth / pages) * i, behavior: 'smooth' });
});
indicatorContainer.appendChild(dot);
}
}
function updateIndicators() {
const dots = indicatorContainer.querySelectorAll('.carousel-dot');
if (!dots.length) return;
const ratio = track.scrollLeft / Math.max(1, track.scrollWidth - track.clientWidth);
const idx = Math.round(ratio * (dots.length - 1));
dots.forEach((d, i) => d.classList.toggle('active', i === idx));
}
leftArrow.addEventListener('click', () => {
track.scrollBy({ left: -SCROLL_AMOUNT, behavior: 'smooth' });
});
rightArrow.addEventListener('click', () => {
track.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' });
});
track.addEventListener('scroll', () => {
updateArrows();
updateIndicators();
});
filterBtns.forEach(btn => {
btn.addEventListener('click', () => {
filterBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const filter = btn.dataset.filter;
cards.forEach(card => {
if (filter === 'all' || card.dataset.category === filter) {
card.classList.remove('hidden');
} else {
card.classList.add('hidden');
}
});
track.scrollTo({ left: 0 });
buildIndicators();
updateArrows();
});
});
buildIndicators();
updateArrows();
window.addEventListener('resize', () => { buildIndicators(); updateArrows(); });
})();
// Obfuscated email - assembled at runtime to defeat scrapers
(function() {
const p = ['smittix', 'outlook', 'com'];
const addr = p[0] + '@' + p[1] + '.' + p[2];
const card = document.getElementById('email-card');
const text = document.getElementById('email-text');
const footerLink = document.getElementById('footer-email');
let revealed = false;
card.addEventListener('click', function(e) {
e.preventDefault();
if (!revealed) {
text.textContent = addr;
revealed = true;
} else {
window.location.href = 'mail' + 'to:' + addr;
}
});
footerLink.addEventListener('click', function(e) {
e.preventDefault();
window.location.href = 'mail' + 'to:' + addr;
});
})();
</script>
<script>
// Animated satellite & signal background
(function() {
const canvas = document.getElementById('bg-canvas');
const ctx = canvas.getContext('2d');
let w, h, dpr;
let orbits = [];
let pulses = [];
let particles = [];
let mouse = { x: -1000, y: -1000 };
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
w = window.innerWidth;
h = document.documentElement.scrollHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
// Orbital paths with satellites
function createOrbits() {
orbits = [];
const count = Math.max(4, Math.floor(w / 300));
for (let i = 0; i < count; i++) {
const cx = Math.random() * w;
const cy = Math.random() * h;
const rx = 120 + Math.random() * 280;
const ry = 40 + Math.random() * 100;
const tilt = (Math.random() - 0.5) * 1.2;
const speed = (0.0002 + Math.random() * 0.0004) * (Math.random() > 0.5 ? 1 : -1);
const sats = [];
const satCount = 1 + Math.floor(Math.random() * 2);
for (let j = 0; j < satCount; j++) {
sats.push({ angle: Math.random() * Math.PI * 2, pulseTimer: 0 });
}
orbits.push({ cx, cy, rx, ry, tilt, speed, sats });
}
}
// Floating signal particles (tiny dots drifting upward)
function createParticles() {
particles = [];
const count = Math.max(30, Math.floor((w * h) / 25000));
for (let i = 0; i < count; i++) {
particles.push({
x: Math.random() * w,
y: Math.random() * h,
vy: -(0.08 + Math.random() * 0.15),
vx: (Math.random() - 0.5) * 0.1,
size: 0.5 + Math.random() * 1.2,
alpha: 0.1 + Math.random() * 0.25,
flicker: Math.random() * Math.PI * 2,
});
}
}
function spawnPulse(x, y) {
pulses.push({ x, y, r: 2, maxR: 50 + Math.random() * 40, alpha: 0.35 });
}
function drawOrbitPath(orbit) {
ctx.save();
ctx.translate(orbit.cx, orbit.cy);
ctx.rotate(orbit.tilt);
ctx.beginPath();
ctx.ellipse(0, 0, orbit.rx, orbit.ry, 0, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(0, 212, 170, 0.04)';
ctx.lineWidth = 1;
ctx.stroke();
ctx.restore();
}
function drawSatellite(orbit, sat, dt) {
sat.angle += orbit.speed * dt;
const cos = Math.cos(orbit.tilt);
const sin = Math.sin(orbit.tilt);
const ex = orbit.rx * Math.cos(sat.angle);
const ey = orbit.ry * Math.sin(sat.angle);
const sx = orbit.cx + ex * cos - ey * sin;
const sy = orbit.cy + ex * sin + ey * cos;
// Satellite dot
ctx.beginPath();
ctx.arc(sx, sy, 2, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0, 212, 170, 0.7)';
ctx.fill();
// Faint glow
ctx.beginPath();
ctx.arc(sx, sy, 6, 0, Math.PI * 2);
const g = ctx.createRadialGradient(sx, sy, 0, sx, sy, 6);
g.addColorStop(0, 'rgba(0, 212, 170, 0.15)');
g.addColorStop(1, 'rgba(0, 212, 170, 0)');
ctx.fillStyle = g;
ctx.fill();
// Periodic signal pulse
sat.pulseTimer += dt;
if (sat.pulseTimer > 3000 + Math.random() * 500) {
sat.pulseTimer = 0;
spawnPulse(sx, sy);
}
}
function drawPulses(dt) {
for (let i = pulses.length - 1; i >= 0; i--) {
const p = pulses[i];
p.r += dt * 0.025;
p.alpha = 0.35 * (1 - p.r / p.maxR);
if (p.r >= p.maxR) { pulses.splice(i, 1); continue; }
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(0, 212, 170, ${p.alpha})`;
ctx.lineWidth = 1;
ctx.stroke();
// Second ring
if (p.r > 12) {
ctx.beginPath();
ctx.arc(p.x, p.y, p.r * 0.6, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(0, 136, 255, ${p.alpha * 0.5})`;
ctx.stroke();
}
}
}
function drawParticles(dt, time) {
for (const p of particles) {
p.y += p.vy * dt * 0.06;
p.x += p.vx * dt * 0.06;
p.flicker += dt * 0.002;
if (p.y < -10) { p.y = h + 10; p.x = Math.random() * w; }
if (p.x < -10) p.x = w + 10;
if (p.x > w + 10) p.x = -10;
const flick = p.alpha * (0.6 + 0.4 * Math.sin(p.flicker));
// Mouse interaction - subtle brighten
const dx = p.x - mouse.x;
const dy = p.y - mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const boost = dist < 150 ? 0.3 * (1 - dist / 150) : 0;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(0, 212, 170, ${Math.min(flick + boost, 0.6)})`;
ctx.fill();
}
}
// Faint grid lines (signal grid)
function drawGrid(time) {
ctx.strokeStyle = 'rgba(0, 212, 170, 0.015)';
ctx.lineWidth = 1;
const spacing = 120;
const offset = (time * 0.005) % spacing;
for (let x = -spacing + offset; x < w + spacing; x += spacing) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, h);
ctx.stroke();
}
for (let y = -spacing + offset * 0.7; y < h + spacing; y += spacing) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
}
let last = 0;
function animate(now) {
const dt = last ? Math.min(now - last, 50) : 16;
last = now;
ctx.clearRect(0, 0, w, h);
drawGrid(now);
for (const orbit of orbits) {
drawOrbitPath(orbit);
for (const sat of orbit.sats) {
drawSatellite(orbit, sat, dt);
}
}
drawPulses(dt);
drawParticles(dt, now);
requestAnimationFrame(animate);
}
// Track mouse for particle interaction
document.addEventListener('mousemove', (e) => {
mouse.x = e.clientX;
mouse.y = e.clientY + window.scrollY;
});
// Resize handling
let resizeTimer;
function handleResize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
resize();
createOrbits();
createParticles();
}, 200);
}
// Keep canvas height synced with document
const ro = new ResizeObserver(() => { handleResize(); });
ro.observe(document.documentElement);
window.addEventListener('resize', handleResize);
resize();
createOrbits();
createParticles();
requestAnimationFrame(animate);
})();
</script>
</body>
</html>
+258 -9
View File
@@ -17,6 +17,22 @@
--gradient-end: #0088ff;
}
/* Animated background canvas */
#bg-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
body > *:not(#bg-canvas) {
position: relative;
z-index: 1;
}
* {
margin: 0;
padding: 0;
@@ -245,18 +261,74 @@ section h2 {
background: var(--bg-secondary);
}
.features-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
/* Category filter tabs */
.carousel-filters {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 40px;
flex-wrap: wrap;
}
.filter-btn {
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
font-weight: 500;
padding: 8px 20px;
border-radius: 20px;
border: 1px solid var(--border);
background: transparent;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.25s;
letter-spacing: 0.5px;
}
.filter-btn:hover {
border-color: var(--accent);
color: var(--text-primary);
}
.filter-btn.active {
background: var(--accent);
color: var(--bg-primary);
border-color: var(--accent);
}
/* Carousel */
.carousel-wrapper {
position: relative;
padding: 0 56px;
}
.carousel-track {
display: flex;
gap: 20px;
overflow-x: auto;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
padding: 8px 0 16px;
}
.carousel-track::-webkit-scrollbar {
display: none;
}
.feature-card {
flex: 0 0 280px;
scroll-snap-align: start;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 32px 24px;
transition: all 0.3s;
min-height: 200px;
}
.feature-card.hidden {
display: none;
}
.feature-card:hover {
@@ -266,8 +338,15 @@ section h2 {
}
.feature-icon {
font-size: 2rem;
width: 36px;
height: 36px;
margin-bottom: 16px;
color: var(--accent);
}
.feature-icon svg {
width: 100%;
height: 100%;
}
.feature-card h3 {
@@ -283,6 +362,81 @@ section h2 {
line-height: 1.7;
}
/* Carousel arrows */
.carousel-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-primary);
font-size: 1.5rem;
cursor: pointer;
transition: all 0.25s;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
line-height: 1;
}
.carousel-arrow:hover {
background: var(--bg-card-hover);
border-color: var(--accent);
color: var(--accent);
}
.carousel-arrow:disabled {
opacity: 0.3;
cursor: default;
}
.carousel-arrow:disabled:hover {
background: var(--bg-card);
border-color: var(--border);
color: var(--text-primary);
}
.carousel-arrow-left {
left: 0;
}
.carousel-arrow-right {
right: 0;
}
/* Carousel indicators */
.carousel-indicators {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 28px;
}
.carousel-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--border);
border: none;
cursor: pointer;
transition: all 0.25s;
padding: 0;
}
.carousel-dot.active {
background: var(--accent);
width: 24px;
border-radius: 4px;
}
.carousel-dot:hover {
background: var(--text-muted);
}
/* Screenshots */
.screenshot-gallery {
display: grid;
@@ -550,6 +704,72 @@ section h2 {
gap: 16px;
}
/* Support & Contact */
.support {
background: var(--bg-secondary);
}
.support-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
}
.support-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 32px 24px;
text-align: center;
text-decoration: none;
transition: all 0.3s;
display: block;
}
.support-card:hover {
background: var(--bg-card-hover);
border-color: var(--accent);
transform: translateY(-4px);
}
.support-card.support-coffee {
border-color: rgba(255, 193, 59, 0.3);
}
.support-card.support-coffee:hover {
border-color: #ffc13b;
box-shadow: 0 8px 30px rgba(255, 193, 59, 0.1);
}
.support-card.support-coffee .support-icon {
color: #ffc13b;
}
.support-icon {
width: 36px;
height: 36px;
margin: 0 auto 16px;
color: var(--accent);
}
.support-icon svg {
width: 100%;
height: 100%;
}
.support-card h3 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 8px;
color: var(--text-primary);
}
.support-card p {
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.6;
}
/* Footer */
.footer {
background: var(--bg-secondary);
@@ -641,14 +861,22 @@ section h2 {
margin: 0 auto;
}
.features-grid {
grid-template-columns: repeat(2, 1fr);
.carousel-wrapper {
padding: 0 48px;
}
.feature-card {
flex: 0 0 260px;
}
.screenshot-gallery {
grid-template-columns: repeat(2, 1fr);
}
.support-grid {
grid-template-columns: repeat(2, 1fr);
}
.install-options {
grid-template-columns: 1fr;
}
@@ -669,14 +897,35 @@ section h2 {
gap: 24px;
}
.features-grid {
grid-template-columns: 1fr;
.carousel-wrapper {
padding: 0 4px;
}
.carousel-arrow {
display: none;
}
.feature-card {
flex: 0 0 260px;
}
.carousel-filters {
gap: 6px;
}
.filter-btn {
font-size: 0.7rem;
padding: 6px 14px;
}
.screenshot-gallery {
grid-template-columns: 1fr;
}
.support-grid {
grid-template-columns: 1fr;
}
.nav-links {
display: none;
}
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "intercept"
version = "2.16.0"
version = "2.19.0"
description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth"
readme = "README.md"
requires-python = ">=3.9"
+2
View File
@@ -13,6 +13,7 @@ def register_blueprints(app):
from .ais import ais_bp
from .dsc import dsc_bp
from .acars import acars_bp
from .vdl2 import vdl2_bp
from .aprs import aprs_bp
from .satellite import satellite_bp
from .gps import gps_bp
@@ -46,6 +47,7 @@ def register_blueprints(app):
app.register_blueprint(ais_bp)
app.register_blueprint(dsc_bp) # VHF DSC maritime distress
app.register_blueprint(acars_bp)
app.register_blueprint(vdl2_bp)
app.register_blueprint(aprs_bp)
app.register_blueprint(satellite_bp)
app.register_blueprint(gps_bp)
+37 -14
View File
@@ -20,8 +20,9 @@ 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.event_pipeline import process_event
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,
@@ -250,12 +251,22 @@ def start_acars() -> Response:
acars_message_count = 0
acars_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 acarsdec command
# Different forks have different syntax:
# - TLeconte v4+: acarsdec -j -g <gain> -p <ppm> -r <device> <freq1> <freq2> ...
# - TLeconte v3: acarsdec -o 4 -g <gain> -p <ppm> -r <device> <freq1> <freq2> ...
# - f00b4r0 (DragonOS): acarsdec --output json:file:- -g <gain> -p <ppm> -r <device> <freq1> ...
# Note: gain/ppm must come BEFORE -r
# SoapySDR devices: TLeconte uses -d <device_string>, f00b4r0 uses --soapysdr <device_string>
# Note: gain/ppm must come BEFORE -r/-d
json_flag = get_acarsdec_json_flag(acarsdec_path)
cmd = [acarsdec_path]
if json_flag == '--output':
@@ -266,21 +277,33 @@ def start_acars() -> Response:
else:
cmd.extend(['-o', '4']) # JSON output (TLeconte v3.x)
# Add gain if not auto (must be before -r)
# Add gain if not auto (must be before -r/-d)
if gain and str(gain) != '0':
cmd.extend(['-g', str(gain)])
# Add PPM correction if specified (must be before -r)
# Add PPM correction if specified (must be before -r/-d)
if ppm and str(ppm) != '0':
cmd.extend(['-p', str(ppm)])
# Add device and frequencies
# f00b4r0 uses --rtlsdr <device>, TLeconte uses -r <device>
if json_flag == '--output':
if is_soapy:
# SoapySDR device (SDRplay, LimeSDR, Airspy, etc.)
sdr_device = SDRFactory.create_default_device(sdr_type, index=device_int)
# Build SoapySDR driver string (e.g., "driver=sdrplay,serial=...")
builder = SDRFactory.get_builder(sdr_type)
device_str = builder._build_device_string(sdr_device)
if json_flag == '--output':
cmd.extend(['-m', '256'])
cmd.extend(['--soapysdr', device_str])
else:
cmd.extend(['-d', device_str])
elif json_flag == '--output':
# f00b4r0 fork RTL-SDR: --rtlsdr <device>
# Use 3.2 MS/s sample rate for wider bandwidth (handles NA frequency span)
cmd.extend(['-m', '256'])
cmd.extend(['--rtlsdr', str(device)])
else:
# TLeconte fork RTL-SDR: -r <device>
cmd.extend(['-r', str(device)])
cmd.extend(frequencies)
@@ -392,13 +415,13 @@ def stream_acars() -> Response:
while True:
try:
msg = app_module.acars_queue.get(timeout=SSE_QUEUE_TIMEOUT)
last_keepalive = time.time()
try:
process_event('acars', msg, msg.get('type'))
except Exception:
pass
yield format_sse(msg)
msg = app_module.acars_queue.get(timeout=SSE_QUEUE_TIMEOUT)
last_keepalive = time.time()
try:
process_event('acars', msg, msg.get('type'))
except Exception:
pass
yield format_sse(msg)
except queue.Empty:
now = time.time()
if now - last_keepalive >= SSE_KEEPALIVE_INTERVAL:
+7
View File
@@ -38,6 +38,7 @@ from config import (
SHARED_OBSERVER_LOCATION_ENABLED,
)
from utils.logging import adsb_logger as logger
from utils.process import write_dump1090_pid, clear_dump1090_pid, cleanup_stale_dump1090
from utils.validation import (
validate_device_index, validate_gain,
validate_rtl_tcp_host, validate_rtl_tcp_port
@@ -633,6 +634,9 @@ def start_adsb():
'session': session
})
# Kill any stale app-spawned dump1090 from a previous run before checking the port
cleanup_stale_dump1090()
# Check if dump1090 is already running externally (e.g., user started it manually)
existing_service = check_dump1090_service()
if existing_service:
@@ -685,6 +689,7 @@ def start_adsb():
except (ProcessLookupError, OSError):
pass
app_module.adsb_process = None
clear_dump1090_pid()
logger.info("Killed stale ADS-B process")
# Check if device is available before starting local dump1090
@@ -721,6 +726,7 @@ def start_adsb():
stderr=subprocess.PIPE,
start_new_session=True # Create new process group for clean shutdown
)
write_dump1090_pid(app_module.adsb_process.pid)
time.sleep(DUMP1090_START_WAIT)
@@ -819,6 +825,7 @@ def stop_adsb():
except (ProcessLookupError, OSError):
pass
app_module.adsb_process = None
clear_dump1090_pid()
logger.info("ADS-B process stopped")
# Release device from registry
+1 -1
View File
@@ -229,7 +229,7 @@ def start_scan():
rssi_threshold = data.get('rssi_threshold', -100)
# Validate mode
valid_modes = ('auto', 'dbus', 'bleak', 'hcitool', 'bluetoothctl')
valid_modes = ('auto', 'dbus', 'bleak', 'hcitool', 'bluetoothctl', 'ubertooth')
if mode not in valid_modes:
return jsonify({'error': f'Invalid mode. Must be one of: {valid_modes}'}), 400
+36 -15
View File
@@ -11,10 +11,14 @@ from flask import Blueprint, Response, jsonify
from utils.gps import (
GPSPosition,
GPSSkyData,
detect_gps_devices,
get_current_position,
get_gps_reader,
is_gpsd_running,
start_gpsd,
start_gpsd_daemon,
stop_gps,
stop_gpsd_daemon,
)
from utils.logging import get_logger
from utils.sse import format_sse
@@ -58,10 +62,9 @@ def auto_connect_gps():
Automatically connect to gpsd if available.
Called on page load to seamlessly enable GPS if gpsd is running.
If gpsd is not running, attempts to detect GPS devices and start gpsd.
Returns current status if already connected.
"""
import socket
# Check if already running
reader = get_gps_reader()
if reader and reader.is_running:
@@ -75,21 +78,28 @@ def auto_connect_gps():
'sky': sky.to_dict() if sky else None,
})
# Try to connect to gpsd on localhost:2947
host = 'localhost'
port = 2947
# First check if gpsd is reachable
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1.0)
sock.connect((host, port))
sock.close()
except Exception:
return jsonify({
'status': 'unavailable',
'message': 'gpsd not running'
})
# If gpsd isn't running, try to detect a device and start it
if not is_gpsd_running(host, port):
devices = detect_gps_devices()
if not devices:
return jsonify({
'status': 'unavailable',
'message': 'No GPS device detected'
})
# Try to start gpsd with the first detected device
device_path = devices[0]['path']
success, msg = start_gpsd_daemon(device_path, host, port)
if not success:
return jsonify({
'status': 'unavailable',
'message': msg,
'devices': devices,
})
logger.info(f"Auto-started gpsd on {device_path}")
# Clear the queue
while not _gps_queue.empty():
@@ -118,15 +128,26 @@ def auto_connect_gps():
})
@gps_bp.route('/devices')
def list_gps_devices():
"""List detected GPS serial devices."""
devices = detect_gps_devices()
return jsonify({
'devices': devices,
'gpsd_running': is_gpsd_running(),
})
@gps_bp.route('/stop', methods=['POST'])
def stop_gps_reader():
"""Stop GPS client."""
"""Stop GPS client and gpsd daemon if we started it."""
reader = get_gps_reader()
if reader:
reader.remove_callback(_position_callback)
reader.remove_sky_callback(_sky_callback)
stop_gps()
stop_gpsd_daemon()
return jsonify({'status': 'stopped'})
+7 -1
View File
@@ -199,10 +199,16 @@ def start_sensor() -> Response:
thread.start()
# Monitor stderr
# Filter noisy rtl_433 diagnostics that aren't useful to display
_stderr_noise = (
'bitbuffer_add_bit',
'row count limit',
)
def monitor_stderr():
for line in app_module.sensor_process.stderr:
err = line.decode('utf-8', errors='replace').strip()
if err:
if err and not any(noise in err for noise in _stderr_noise):
logger.debug(f"[rtl_433] {err}")
app_module.sensor_queue.put({'type': 'info', 'text': f'[rtl_433] {err}'})
+357
View 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:file:path=-'])
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'],
}
})
+263 -74
View File
@@ -137,6 +137,14 @@ need_sudo() {
fi
}
# Refresh sudo credential cache so long-running builds don't trigger
# mid-compilation password prompts (which can fail due to TTY issues
# inside subshells). Safe to call multiple times.
refresh_sudo() {
[[ -z "${SUDO:-}" ]] && return 0
sudo -v 2>/dev/null || true
}
detect_os() {
if [[ "${OSTYPE:-}" == "darwin"* ]]; then
OS="macos"
@@ -218,6 +226,7 @@ check_tools() {
check_optional "hackrf_sweep" "HackRF spectrum analyzer" hackrf_sweep
check_required "dump1090" "ADS-B decoder" dump1090
check_required "acarsdec" "ACARS decoder" acarsdec
check_optional "dumpvdl2" "VDL2 decoder" dumpvdl2
check_required "AIS-catcher" "AIS vessel decoder" AIS-catcher aiscatcher
check_optional "satdump" "Weather satellite decoder (NOAA/Meteor)" satdump
echo
@@ -304,28 +313,41 @@ install_python_deps() {
# shellcheck disable=SC1091
source venv/bin/activate
local PIP="venv/bin/python -m pip"
local PY="venv/bin/python"
python -m pip install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
$PIP install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
ok "Upgraded pip tooling"
progress "Installing Python dependencies"
# Try pip install, but don't fail if apt packages already satisfied deps
if ! python -m pip install -r requirements.txt 2>/dev/null; then
warn "Some pip packages failed - checking if apt packages cover them..."
# Verify critical packages are available
python -c "import flask; import requests; from flask_limiter import Limiter" 2>/dev/null || {
fail "Critical Python packages (flask, requests, flask-limiter) not installed"
echo "Try: pip install flask requests flask-limiter"
exit 1
}
ok "Core Python dependencies available"
else
ok "Python dependencies installed"
fi
# Ensure Flask 3.0+ is installed (required for Werkzeug 3.x compatibility)
# System apt packages may have older Flask 2.x which is incompatible
python -m pip install --upgrade "flask>=3.0.0" >/dev/null 2>&1 || true
# Install critical packages first to avoid all-or-nothing failures
# (C extension packages like scipy/numpy can fail on newer Python versions
# and cause pip to roll back pure-Python packages like flask)
info "Installing core packages..."
$PIP install "flask>=3.0.0" "flask-limiter>=2.5.4" "requests>=2.28.0" \
"Werkzeug>=3.1.5" "pyserial>=3.5" "flask-sock" "websocket-client>=1.6.0" 2>&1 \
| tail -5 || true
# Verify critical packages
$PY -c "import flask; import requests; from flask_limiter import Limiter" 2>/dev/null || {
fail "Critical Python packages (flask, requests, flask-limiter) not installed"
echo "Try: venv/bin/pip install flask requests flask-limiter"
exit 1
}
ok "Core Python packages installed"
# Install optional packages individually (some may fail on newer Python)
info "Installing optional packages..."
for pkg in "numpy>=1.24.0" "scipy>=1.10.0" "Pillow>=9.0.0" "skyfield>=1.45" \
"bleak>=0.21.0" "psycopg2-binary>=2.9.9" "meshtastic>=2.0.0" \
"scapy>=2.4.5" "qrcode[pil]>=7.4" "cryptography>=41.0.0"; do
pkg_name="${pkg%%>=*}"
if ! $PIP install "$pkg" 2>/dev/null; then
warn "${pkg_name} failed to install (optional - related features may be unavailable)"
fi
done
ok "Optional packages processed"
echo
}
@@ -388,7 +410,7 @@ install_rtlamr_from_source() {
if [[ -w /usr/local/bin ]]; then
ln -sf "$GOPATH/bin/rtlamr" /usr/local/bin/rtlamr
else
sudo ln -sf "$GOPATH/bin/rtlamr" /usr/local/bin/rtlamr
$SUDO ln -sf "$GOPATH/bin/rtlamr" /usr/local/bin/rtlamr
fi
else
$SUDO ln -sf "$GOPATH/bin/rtlamr" /usr/local/bin/rtlamr
@@ -430,7 +452,8 @@ install_multimon_ng_from_source_macos() {
if [[ -w /usr/local/bin ]]; then
install -m 0755 multimon-ng /usr/local/bin/multimon-ng
else
sudo install -m 0755 multimon-ng /usr/local/bin/multimon-ng
refresh_sudo
$SUDO install -m 0755 multimon-ng /usr/local/bin/multimon-ng
fi
ok "multimon-ng installed successfully from source"
)
@@ -471,7 +494,8 @@ install_dsd_from_source() {
if [[ -w /usr/local/lib ]]; then
make install >/dev/null 2>&1
else
sudo make install >/dev/null 2>&1
refresh_sudo
$SUDO make install >/dev/null 2>&1
fi
else
$SUDO make install >/dev/null 2>&1
@@ -507,7 +531,8 @@ install_dsd_from_source() {
if [[ -w /usr/local/bin ]]; then
install -m 0755 dsd-fme /usr/local/bin/dsd 2>/dev/null || install -m 0755 dsd /usr/local/bin/dsd 2>/dev/null || true
else
sudo install -m 0755 dsd-fme /usr/local/bin/dsd 2>/dev/null || sudo install -m 0755 dsd /usr/local/bin/dsd 2>/dev/null || true
refresh_sudo
$SUDO install -m 0755 dsd-fme /usr/local/bin/dsd 2>/dev/null || $SUDO install -m 0755 dsd /usr/local/bin/dsd 2>/dev/null || true
fi
else
$SUDO make install >/dev/null 2>&1 \
@@ -545,7 +570,8 @@ install_dump1090_from_source_macos() {
if [[ -w /usr/local/bin ]]; then
install -m 0755 dump1090 /usr/local/bin/dump1090
else
sudo install -m 0755 dump1090 /usr/local/bin/dump1090
refresh_sudo
$SUDO install -m 0755 dump1090 /usr/local/bin/dump1090
fi
ok "dump1090 installed successfully from source"
else
@@ -604,6 +630,7 @@ install_acarsdec_from_source_macos() {
info "Compiling acarsdec..."
build_log="$tmp_dir/acarsdec-build.log"
if cmake .. -Drtl=ON \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_C_FLAGS="-I${HOMEBREW_PREFIX}/include" \
-DCMAKE_EXE_LINKER_FLAGS="-L${HOMEBREW_PREFIX}/lib" \
>"$build_log" 2>&1 \
@@ -611,7 +638,8 @@ install_acarsdec_from_source_macos() {
if [[ -w /usr/local/bin ]]; then
install -m 0755 acarsdec /usr/local/bin/acarsdec
else
sudo install -m 0755 acarsdec /usr/local/bin/acarsdec
refresh_sudo
$SUDO install -m 0755 acarsdec /usr/local/bin/acarsdec
fi
ok "acarsdec installed successfully from source"
else
@@ -622,6 +650,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() {
info "AIS-catcher not available via Homebrew. Building from source..."
@@ -646,7 +748,8 @@ install_aiscatcher_from_source_macos() {
if [[ -w /usr/local/bin ]]; then
install -m 0755 AIS-catcher /usr/local/bin/AIS-catcher
else
sudo install -m 0755 AIS-catcher /usr/local/bin/AIS-catcher
refresh_sudo
$SUDO install -m 0755 AIS-catcher /usr/local/bin/AIS-catcher
fi
ok "AIS-catcher installed successfully from source"
else
@@ -673,6 +776,21 @@ install_satdump_from_source_debian() {
|| { warn "Failed to clone SatDump"; exit 1; }
cd "$tmp_dir/SatDump"
# Patch: fix deprecated std::allocator usage for newer compilers
# GCC 13+ errors on deprecated allocator members in sol2.
# Pragmas must go in lua_utils.cpp (the instantiation site), not sol.hpp (definition site).
lua_utils="src-core/common/lua/lua_utils.cpp"
if [ -f "$lua_utils" ]; then
{
echo '#pragma GCC diagnostic push'
echo '#pragma GCC diagnostic ignored "-Wdeprecated"'
echo '#pragma GCC diagnostic ignored "-Wdeprecated-declarations"'
cat "$lua_utils"
echo '#pragma GCC diagnostic pop'
} > "${lua_utils}.patched" && mv "${lua_utils}.patched" "$lua_utils"
fi
mkdir -p build && cd build
info "Compiling SatDump (this is a large C++ project and may take 10-30 minutes)..."
@@ -717,67 +835,78 @@ install_satdump_from_source_debian() {
)
}
install_satdump_from_source_macos() {
info "Building SatDump v1.2.2 from source (weather satellite decoder)..."
install_satdump_macos() {
info "Installing SatDump v1.2.2 from pre-built release (weather satellite decoder)..."
brew_install cmake
brew_install libpng
brew_install libtiff
brew_install jemalloc
brew_install libvolk
brew_install nng
brew_install zstd
brew_install soapysdr
brew_install hackrf
brew_install fftw
# Determine architecture
local arch
arch="$(uname -m)"
local dmg_name
if [ "$arch" = "arm64" ]; then
dmg_name="SatDump-macOS-Silicon.dmg"
else
dmg_name="SatDump-macOS-Intel.dmg"
fi
local dmg_url="https://github.com/SatDump/SatDump/releases/download/1.2.2/${dmg_name}"
local install_dir="/usr/local/lib/satdump"
# Run in subshell to isolate EXIT trap
(
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
trap 'hdiutil detach "$tmp_dir/mnt" -quiet 2>/dev/null || true; rm -rf "$tmp_dir"' EXIT
info "Cloning SatDump v1.2.2..."
git clone --depth 1 --branch 1.2.2 https://github.com/SatDump/SatDump.git "$tmp_dir/SatDump" >/dev/null 2>&1 \
|| { warn "Failed to clone SatDump"; exit 1; }
info "Downloading ${dmg_name}..."
if ! curl -sL -o "$tmp_dir/satdump.dmg" "$dmg_url"; then
warn "Failed to download SatDump. Weather satellite decoding will not be available."
exit 1
fi
cd "$tmp_dir/SatDump"
mkdir -p build && cd build
info "Installing SatDump..."
# Mount the DMG
hdiutil attach "$tmp_dir/satdump.dmg" -nobrowse -quiet -mountpoint "$tmp_dir/mnt" \
|| { warn "Failed to mount SatDump DMG"; exit 1; }
info "Compiling SatDump (this is a large C++ project and may take 10-30 minutes)..."
build_log="$tmp_dir/satdump-build.log"
local app_dir="$tmp_dir/mnt/SatDump.app"
if [ ! -d "$app_dir" ]; then
warn "SatDump.app not found in DMG"
exit 1
fi
# Show periodic progress while building so the user knows it's not hung
(
while true; do
sleep 30
if [ -f "$build_log" ]; then
local_lines=$(wc -l < "$build_log" 2>/dev/null || echo 0)
printf " [*] Still compiling SatDump... (%s lines of build output so far)\n" "$local_lines"
fi
done
) &
progress_pid=$!
# Install: copy app contents to /usr/local/lib/satdump
refresh_sudo
$SUDO mkdir -p "$install_dir"
$SUDO cp -R "$app_dir/Contents/MacOS/"* "$install_dir/"
$SUDO cp -R "$app_dir/Contents/Resources/"* "$install_dir/"
if cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_GUI=OFF .. >"$build_log" 2>&1 \
&& make -j "$(sysctl -n hw.ncpu)" >>"$build_log" 2>&1; then
kill $progress_pid 2>/dev/null; wait $progress_pid 2>/dev/null
if [[ -w /usr/local/bin ]]; then
make install >/dev/null 2>&1
else
sudo make install >/dev/null 2>&1
fi
ok "SatDump installed successfully."
# Create wrapper script so satdump can find its resources via @executable_path
$SUDO tee /usr/local/bin/satdump >/dev/null <<'WRAPPER'
#!/bin/sh
exec /usr/local/lib/satdump/satdump "$@"
WRAPPER
$SUDO chmod +x /usr/local/bin/satdump
hdiutil detach "$tmp_dir/mnt" -quiet 2>/dev/null
# Verify installation
if /usr/local/lib/satdump/satdump 2>&1 | grep -q "Usage"; then
ok "SatDump v1.2.2 installed successfully."
else
kill $progress_pid 2>/dev/null; wait $progress_pid 2>/dev/null
warn "Failed to build SatDump from source. Weather satellite decoding will not be available."
warn "Build log (last 30 lines):"
tail -30 "$build_log" | while IFS= read -r line; do warn " $line"; done
warn "SatDump installed but may not work correctly."
fi
)
}
install_macos_packages() {
TOTAL_STEPS=19
need_sudo
# Prime sudo credentials upfront so builds don't prompt mid-compilation
if [[ -n "${SUDO:-}" ]]; then
info "Some tools require sudo to install. You may be prompted for your password."
sudo -v || { fail "sudo authentication failed"; exit 1; }
fi
TOTAL_STEPS=22
CURRENT_STEP=0
progress "Checking Homebrew"
@@ -850,6 +979,13 @@ install_macos_packages() {
ok "acarsdec already installed"
fi
progress "Installing dumpvdl2"
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"
if ! cmd_exists AIS-catcher && ! cmd_exists aiscatcher; then
(brew_install aiscatcher) || install_aiscatcher_from_source_macos || warn "AIS-catcher not available"
@@ -862,7 +998,7 @@ install_macos_packages() {
echo
info "SatDump is used for weather satellite imagery (NOAA APT & Meteor LRPT)."
if ask_yes_no "Do you want to install SatDump?"; then
install_satdump_from_source_macos || warn "SatDump build failed. Weather satellite decoding will not be available."
install_satdump_macos || warn "SatDump installation failed. Weather satellite decoding will not be available."
else
warn "Skipping SatDump installation. You can install it later if needed."
fi
@@ -954,7 +1090,7 @@ install_dump1090_from_source_debian() {
cd "$tmp_dir/dump1090"
# Remove -Werror to prevent build failures on newer GCC versions
sed -i 's/-Werror//g' Makefile 2>/dev/null || sed -i '' 's/-Werror//g' Makefile
sed -i 's/-Werror//g' Makefile 2>/dev/null || true
info "Compiling FlightAware dump1090..."
if make BLADERF=no RTLSDR=yes >/dev/null 2>&1; then
$SUDO install -m 0755 dump1090 /usr/local/bin/dump1090
@@ -995,7 +1131,7 @@ install_acarsdec_from_source_debian() {
mkdir -p build && cd build
info "Compiling acarsdec..."
if cmake .. -Drtl=ON >/dev/null 2>&1 && make >/dev/null 2>&1; then
if cmake .. -Drtl=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5 >/dev/null 2>&1 && make >/dev/null 2>&1; then
$SUDO install -m 0755 acarsdec /usr/local/bin/acarsdec
ok "acarsdec installed successfully."
else
@@ -1004,6 +1140,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() {
info "AIS-catcher not available via APT. Building from source..."
@@ -1167,7 +1349,7 @@ install_debian_packages() {
export NEEDRESTART_MODE=a
fi
TOTAL_STEPS=26
TOTAL_STEPS=28
CURRENT_STEP=0
progress "Updating APT package lists"
@@ -1309,7 +1491,7 @@ install_debian_packages() {
fi
if ! cmd_exists dump1090; then
if cmd_exists dump1090-mutability; then
$SUDO ln -s $(which dump1090-mutability) /usr/local/sbin/dump1090
$SUDO ln -s "$(which dump1090-mutability)" /usr/local/sbin/dump1090
fi
fi
cmd_exists dump1090 || install_dump1090_from_source_debian
@@ -1320,6 +1502,13 @@ install_debian_packages() {
fi
cmd_exists acarsdec || install_acarsdec_from_source_debian
progress "Installing dumpvdl2"
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"
if ! cmd_exists AIS-catcher && ! cmd_exists aiscatcher; then
install_aiscatcher_from_source_debian
+410 -97
View File
@@ -5,8 +5,8 @@
}
:root {
--font-sans: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', 'Menlo', monospace;
--font-mono: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', 'Menlo', monospace;
--font-sans: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--bg-dark: #0b1118;
--bg-panel: #101823;
--bg-card: #151f2b;
@@ -31,8 +31,11 @@ body {
font-family: var(--font-sans);
background: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
height: 100dvh;
height: 100vh; /* Fallback */
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Animated radar sweep background */
@@ -227,16 +230,14 @@ body {
}
/* Main dashboard grid - Mobile first */
/* Header ~52px + Nav 44px + Stats strip ~55px = ~151px, using 160px for safety */
.dashboard {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
gap: 0;
height: calc(100dvh - 160px);
height: calc(100vh - 160px); /* Fallback */
min-height: 400px;
flex: 1;
min-height: 0;
}
/* Tablet: Two-column layout */
@@ -249,13 +250,29 @@ body {
}
}
/* Desktop: Full layout with ACARS */
/* Desktop: Full layout with ACARS/VDL2 + map + sidebar */
@media (min-width: 1024px) {
.dashboard {
grid-template-columns: auto 1fr 300px;
}
}
/* Left sidebars wrapper (ACARS + VDL2) */
.left-sidebars {
display: none;
}
@media (min-width: 1024px) {
.left-sidebars {
display: flex;
flex-direction: row;
grid-column: 1;
grid-row: 1;
height: 100%;
overflow: hidden;
}
}
/* ACARS sidebar (left of map) - Collapsible */
.acars-sidebar {
display: none;
@@ -267,12 +284,10 @@ body {
min-height: 0;
}
/* Show ACARS sidebar on desktop */
@media (min-width: 1024px) {
.acars-sidebar {
display: flex;
max-height: calc(100dvh - 160px);
}
/* Show ACARS sidebar inside wrapper */
.left-sidebars .acars-sidebar {
display: flex;
height: 100%;
}
.acars-collapse-btn {
@@ -419,6 +434,335 @@ body {
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;
}
/* Show VDL2 sidebar inside wrapper */
.left-sidebars .vdl2-sidebar {
display: flex;
height: 100%;
}
.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;
cursor: pointer;
transition: background 0.2s;
}
.vdl2-message-item:hover {
background: rgba(74, 158, 255, 0.08);
}
/* VDL2 Message Modal */
.vdl2-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
animation: vdl2ModalFadeIn 0.15s ease;
}
@keyframes vdl2ModalFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.vdl2-modal {
background: var(--bg-panel, #1a1a2e);
border: 1px solid var(--accent-cyan, #4a9eff);
border-radius: 8px;
width: 520px;
max-width: 90vw;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 1px var(--accent-cyan, #4a9eff);
animation: vdl2ModalSlideIn 0.15s ease;
}
@keyframes vdl2ModalSlideIn {
from { opacity: 0; transform: scale(0.95) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.vdl2-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 18px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.vdl2-modal-title {
font-size: 14px;
font-weight: 700;
color: var(--accent-cyan, #4a9eff);
letter-spacing: 0.5px;
}
.vdl2-modal-time {
font-size: 11px;
color: var(--text-muted);
}
.vdl2-modal-close {
background: none;
border: 1px solid var(--border-color);
color: var(--text-muted);
width: 28px;
height: 28px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
margin-left: 12px;
}
.vdl2-modal-close:hover {
background: rgba(239, 68, 68, 0.15);
border-color: var(--accent-red, #ef4444);
color: var(--accent-red, #ef4444);
}
.vdl2-modal-body {
padding: 16px 18px;
overflow-y: auto;
flex: 1;
min-height: 0;
}
.vdl2-modal-section {
margin-bottom: 14px;
}
.vdl2-modal-section:last-child {
margin-bottom: 0;
}
.vdl2-modal-section-title {
font-size: 9px;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: 8px;
}
.vdl2-modal-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 16px;
}
.vdl2-modal-field {
display: flex;
flex-direction: column;
gap: 1px;
}
.vdl2-modal-field-label {
font-size: 10px;
color: var(--text-dim);
}
.vdl2-modal-field-value {
font-size: 12px;
color: var(--text-primary);
font-weight: 500;
}
.vdl2-modal-msg-body {
padding: 10px 12px;
background: rgba(0, 0, 0, 0.25);
border-radius: 4px;
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-primary);
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
max-height: 250px;
overflow-y: auto;
}
.vdl2-modal-raw-toggle {
display: inline-block;
margin-top: 10px;
font-size: 10px;
color: var(--accent-cyan, #4a9eff);
cursor: pointer;
opacity: 0.7;
transition: opacity 0.15s;
}
.vdl2-modal-raw-toggle:hover {
opacity: 1;
}
.vdl2-modal-raw-json {
display: none;
margin-top: 8px;
padding: 10px 12px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border-color);
border-radius: 4px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-dim);
white-space: pre-wrap;
word-break: break-all;
max-height: 300px;
overflow-y: auto;
line-height: 1.4;
}
/* Panels */
.panel {
background: var(--bg-panel);
@@ -495,6 +839,8 @@ body {
position: relative;
flex: 1;
min-height: 300px;
min-width: 0;
overflow: hidden;
}
@media (min-width: 768px) {
@@ -526,42 +872,6 @@ body {
display: block;
}
#radarOverlayCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 500;
display: none;
}
#radarOverlayCanvas.active {
display: block;
}
#radarScope {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
background: var(--radar-bg);
}
#radarScope.active {
display: flex;
justify-content: center;
align-items: center;
}
#radarCanvas {
max-width: 100%;
max-height: 100%;
}
/* Right sidebar - Mobile first */
.sidebar {
display: flex;
@@ -588,51 +898,21 @@ body {
}
}
/* View toggle */
.view-toggle {
display: flex;
padding: 10px;
gap: 8px;
background: var(--bg-panel);
border-bottom: 1px solid rgba(74, 158, 255, 0.2);
}
.view-btn {
flex: 1;
padding: 10px;
border: 1px solid rgba(74, 158, 255, 0.3);
background: transparent;
color: var(--text-secondary);
font-family: 'Orbitron', monospace;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 2px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.view-btn:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
.view-btn.active {
background: var(--accent-cyan);
border-color: var(--accent-cyan);
color: var(--bg-dark);
}
/* Selected aircraft panel */
.selected-aircraft {
flex-shrink: 0;
max-height: 480px;
max-height: 280px;
overflow-y: auto;
}
@media (min-height: 900px) {
.selected-aircraft {
max-height: 340px;
}
}
.selected-info {
padding: 12px;
padding: 8px;
}
#aircraftPhotoContainer {
@@ -640,7 +920,7 @@ body {
}
#aircraftPhotoContainer img {
max-height: 140px;
max-height: 100px;
width: 100%;
object-fit: cover;
border-radius: 6px;
@@ -649,24 +929,24 @@ body {
.selected-callsign {
font-family: 'Orbitron', monospace;
font-size: 20px;
font-size: 16px;
font-weight: 700;
color: var(--accent-cyan);
text-shadow: 0 0 15px var(--accent-cyan);
text-align: center;
margin-bottom: 12px;
margin-bottom: 6px;
}
.telemetry-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
gap: 4px;
}
.telemetry-item {
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
padding: 8px;
padding: 5px 8px;
border-left: 2px solid var(--accent-cyan);
}
@@ -776,9 +1056,10 @@ body {
gap: 8px;
padding: 8px 15px;
background: var(--bg-panel);
border-top: 1px solid rgba(74, 158, 255, 0.3);
border-top: none;
font-size: 11px;
overflow: hidden;
overflow-x: auto;
overflow-y: hidden;
}
.controls-bar > .control-group {
@@ -907,6 +1188,15 @@ body {
.control-group.airband-group {
background: rgba(245, 158, 11, 0.05);
border-color: rgba(245, 158, 11, 0.2);
flex: 1 1 auto;
min-width: 0;
}
.control-group.airband-group > .control-group-items {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
}
.control-group.airband-group .control-group-label {
@@ -1010,6 +1300,7 @@ body {
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
@@ -1021,6 +1312,15 @@ body {
border-radius: 3px;
}
/* Hide scrollbar on controls bar */
.controls-bar::-webkit-scrollbar {
display: none;
}
.controls-bar {
scrollbar-width: none;
}
/* No aircraft message */
.no-aircraft {
text-align: center;
@@ -1289,7 +1589,7 @@ body {
display: flex !important;
flex-direction: column !important;
height: auto !important;
min-height: calc(100dvh - 160px);
min-height: 400px;
overflow-y: auto !important;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
@@ -1489,6 +1789,10 @@ body {
margin-top: 1px;
}
.strip-stat.source-stat .strip-value {
font-size: 14px;
}
.strip-stat.session-stat {
background: rgba(34, 197, 94, 0.05);
border-color: rgba(34, 197, 94, 0.2);
@@ -1779,6 +2083,9 @@ body {
.strip-btn {
position: relative;
z-index: 10;
display: inline-flex;
align-items: center;
gap: 4px;
background: rgba(74, 158, 255, 0.1);
border: 1px solid rgba(74, 158, 255, 0.2);
color: var(--text-primary);
@@ -1789,6 +2096,12 @@ body {
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
text-decoration: none;
}
.strip-btn svg {
flex-shrink: 0;
opacity: 0.7;
}
.strip-btn:hover:not(:disabled) {
+2 -2
View File
@@ -5,8 +5,8 @@
}
:root {
--font-sans: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', 'Menlo', monospace;
--font-mono: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', 'Menlo', monospace;
--font-sans: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--bg-dark: #0b1118;
--bg-panel: #101823;
--bg-card: #151f2b;
+6 -6
View File
@@ -8,8 +8,8 @@
}
:root {
--font-sans: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', 'Menlo', monospace;
--font-mono: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', 'Menlo', monospace;
--font-sans: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--bg-dark: #0b1118;
--bg-panel: #101823;
--bg-card: #151f2b;
@@ -496,7 +496,7 @@ body {
padding: 10px 15px;
background: rgba(74, 158, 255, 0.05);
border-bottom: 1px solid rgba(74, 158, 255, 0.1);
font-family: 'Orbitron', 'Space Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 11px;
font-weight: 500;
letter-spacing: 2px;
@@ -568,7 +568,7 @@ body {
}
.vessel-name {
font-family: 'Orbitron', 'Space Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 16px;
font-weight: 700;
color: var(--accent-cyan);
@@ -662,7 +662,7 @@ body {
}
.vessel-item-name {
font-family: 'Orbitron', 'Space Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 12px;
font-weight: 600;
color: var(--accent-cyan);
@@ -1223,7 +1223,7 @@ body {
}
.dsc-distress-alert .dsc-alert-header {
font-family: 'Orbitron', 'Space Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 24px;
font-weight: 700;
color: var(--accent-red);
+2 -2
View File
@@ -78,8 +78,8 @@
/* ============================================
TYPOGRAPHY
============================================ */
--font-sans: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', 'Menlo', monospace;
--font-mono: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', 'Menlo', monospace;
--font-sans: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
/* Font sizes */
--text-xs: 10px;
+9 -7
View File
@@ -1,18 +1,20 @@
/* Local font declarations for offline mode */
/* Roboto Condensed - variable font, one file covers all weights */
/* Space Mono - Console font */
@font-face {
font-family: 'Space Mono';
font-family: 'Roboto Condensed';
font-style: normal;
font-weight: 400;
font-weight: 300 700;
font-display: swap;
src: url('/static/vendor/fonts/SpaceMono-Regular.woff2') format('woff2');
src: url('/static/vendor/fonts/RobotoCondensed-Latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Space Mono';
font-family: 'Roboto Condensed';
font-style: normal;
font-weight: 700;
font-weight: 300 700;
font-display: swap;
src: url('/static/vendor/fonts/SpaceMono-Bold.woff2') format('woff2');
src: url('/static/vendor/fonts/RobotoCondensed-LatinExt.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
+2 -2
View File
@@ -30,7 +30,7 @@
border-bottom: 1px solid var(--border-color, #202833);
padding: 0 20px;
position: relative;
z-index: 100;
z-index: 1100;
backdrop-filter: blur(10px);
}
@@ -434,6 +434,6 @@ a.nav-dashboard-btn:hover {
}
.nav-dashboard-btn .nav-label {
font-family: var(--font-mono, 'JetBrains Mono', monospace);
font-family: var(--font-mono, 'Roboto Condensed', 'Arial Narrow', sans-serif);
letter-spacing: 0.5px;
}
+49 -27
View File
@@ -14,6 +14,7 @@
z-index: 10000;
overflow-y: auto;
padding: 40px 20px;
font-family: var(--font-mono, 'Roboto Condensed', 'Arial Narrow', sans-serif);
}
.help-modal.active {
@@ -26,37 +27,41 @@
background: var(--bg-card, var(--bg-secondary, #0f1218));
border: 1px solid var(--border-color, #1f2937);
border-radius: 8px;
padding: 30px;
padding: 24px;
position: relative;
}
.help-content h2 {
color: var(--accent-cyan, #4a9eff);
margin-bottom: 20px;
font-size: 24px;
margin-bottom: 16px;
font-size: 15px;
letter-spacing: 2px;
text-transform: uppercase;
font-weight: 600;
}
.help-content h3 {
color: var(--text-primary, #e8eaed);
margin: 25px 0 15px 0;
font-size: 14px;
margin: 20px 0 10px 0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
border-bottom: 1px solid var(--border-color, #1f2937);
padding-bottom: 8px;
padding-bottom: 6px;
font-weight: 600;
}
.help-close {
position: absolute;
top: 15px;
right: 15px;
top: 12px;
right: 12px;
background: none;
border: none;
color: var(--text-dim, #4b5563);
font-size: 24px;
font-size: 20px;
cursor: pointer;
transition: color 0.2s;
line-height: 1;
}
.help-close:hover {
@@ -66,43 +71,54 @@
.help-modal .icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
margin: 15px 0;
gap: 8px;
margin: 10px 0;
}
.help-modal .icon-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
gap: 8px;
padding: 6px 8px;
background: var(--bg-primary, #0a0c10);
border: 1px solid var(--border-color, #1f2937);
border-radius: 4px;
font-size: 12px;
font-size: 11px;
}
.help-modal .icon-item .icon {
font-size: 18px;
width: 30px;
text-align: center;
width: 20px;
height: 20px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.help-modal .icon-item .icon svg {
width: 16px;
height: 16px;
}
.help-modal .icon-item .desc {
color: var(--text-secondary, #9ca3af);
font-size: 10.5px;
line-height: 1.3;
}
.help-modal .tip-list {
list-style: none;
padding: 0;
margin: 15px 0;
margin: 10px 0;
}
.help-modal .tip-list li {
padding: 8px 0;
padding-left: 20px;
padding: 5px 0;
padding-left: 16px;
position: relative;
color: var(--text-secondary, #9ca3af);
font-size: 13px;
font-size: 11px;
line-height: 1.5;
border-bottom: 1px solid var(--border-color, #1f2937);
}
@@ -118,10 +134,15 @@
font-weight: bold;
}
.help-modal .tip-list li strong {
color: var(--text-primary, #e8eaed);
font-weight: 600;
}
.help-tabs {
display: flex;
gap: 0;
margin-bottom: 20px;
margin-bottom: 16px;
border: 1px solid var(--border-color, #1f2937);
border-radius: 4px;
overflow: hidden;
@@ -129,12 +150,13 @@
.help-tab {
flex: 1;
padding: 10px;
padding: 8px;
background: var(--bg-primary, #0a0c10);
border: none;
color: var(--text-secondary, #9ca3af);
cursor: pointer;
font-size: 11px;
font-family: var(--font-mono, 'Roboto Condensed', 'Arial Narrow', sans-serif);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.15s ease;
@@ -176,9 +198,9 @@
/* Ensure code tags are styled */
.help-modal code {
background: var(--bg-tertiary, #151a23);
padding: 2px 6px;
padding: 1px 5px;
border-radius: 3px;
font-family: var(--font-mono, 'JetBrains Mono', monospace);
font-size: 11px;
font-family: var(--font-mono, 'Roboto Condensed', 'Arial Narrow', sans-serif);
font-size: 10.5px;
color: var(--accent-cyan, #4a9eff);
}
+133 -12
View File
@@ -5,8 +5,8 @@
}
:root {
--font-sans: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', 'Menlo', monospace;
--font-mono: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', 'Menlo', monospace;
--font-sans: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
/* Tactical dark palette */
--bg-primary: #0b1118;
--bg-secondary: #101823;
@@ -706,6 +706,8 @@ header h1 {
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
padding: 0 20px;
position: relative;
z-index: 1100;
}
@media (min-width: 1024px) {
@@ -4119,7 +4121,7 @@ header h1 .tagline {
border: 1px solid var(--border-color);
border-radius: 6px;
flex-shrink: 0;
height: 140px;
max-height: 340px;
overflow: hidden;
}
@@ -4140,7 +4142,9 @@ header h1 .tagline {
.bt-detail-body {
padding: 8px 10px;
height: calc(100% - 30px);
height: auto;
max-height: calc(100% - 30px);
overflow-y: auto;
}
.bt-detail-placeholder {
@@ -4319,6 +4323,110 @@ header h1 .tagline {
color: #9fffd1;
}
/* Service Data Inspector */
.bt-detail-service-inspector {
margin-bottom: 6px;
}
.bt-inspector-toggle {
font-size: 10px;
color: var(--accent-cyan);
cursor: pointer;
padding: 3px 0;
user-select: none;
}
.bt-inspector-toggle:hover {
color: #fff;
}
.bt-inspector-arrow {
display: inline-block;
transition: transform 0.2s;
font-size: 9px;
}
.bt-inspector-arrow.open {
transform: rotate(90deg);
}
.bt-inspector-content {
background: var(--bg-secondary);
border-radius: 3px;
padding: 6px 8px;
margin-top: 4px;
font-family: var(--font-mono);
font-size: 9px;
color: var(--text-dim);
max-height: 100px;
overflow-y: auto;
word-break: break-all;
}
.bt-inspector-row {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 2px 0;
border-bottom: 1px solid rgba(255,255,255,0.04);
}
.bt-inspector-row:last-child {
border-bottom: none;
}
.bt-inspector-label {
color: var(--text-dim);
flex-shrink: 0;
}
.bt-inspector-value {
color: var(--text-primary);
text-align: right;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
/* MAC Cluster Badge */
.bt-mac-cluster-badge {
display: inline-block;
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
font-size: 8px;
font-weight: 600;
padding: 1px 4px;
border-radius: 3px;
margin-left: 6px;
vertical-align: middle;
}
/* Behavioral Flag Badges */
.bt-flag-badge {
display: inline-block;
font-size: 8px;
font-weight: 600;
padding: 1px 4px;
border-radius: 3px;
margin-left: 3px;
vertical-align: middle;
}
.bt-flag-badge.persistent {
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
}
.bt-flag-badge.beacon-like {
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
}
.bt-flag-badge.strong-stable {
background: rgba(34, 197, 94, 0.15);
color: #22c55e;
}
/* Selected device highlight */
.bt-device-row.selected {
background: rgba(0, 212, 255, 0.1);
@@ -4469,14 +4577,15 @@ header h1 .tagline {
.bt-row-main {
display: flex;
justify-content: space-between;
align-items: center;
align-items: flex-start;
gap: 12px;
}
.bt-row-left {
display: flex;
align-items: center;
gap: 8px;
align-items: baseline;
flex-wrap: wrap;
gap: 4px 8px;
min-width: 0;
flex: 1;
}
@@ -4521,13 +4630,25 @@ header h1 .tagline {
color: #22c55e;
}
.bt-irk-badge {
display: inline-block;
padding: 1px 4px;
border-radius: 3px;
font-size: 9px;
font-weight: 600;
letter-spacing: 0.3px;
background: rgba(168, 85, 247, 0.15);
color: #a855f7;
border: 1px solid rgba(168, 85, 247, 0.3);
}
.bt-device-name {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: break-word;
word-break: break-word;
min-width: 0;
}
.bt-rssi-container {
@@ -6251,7 +6372,7 @@ body::before {
}
.module-header {
font-family: 'Orbitron', 'Space Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
font-weight: 600;
color: var(--accent-cyan);
@@ -6429,7 +6550,7 @@ body::before {
/* Listening Mode Selector Buttons */
.radio-mode-btn {
padding: 12px 24px;
font-family: 'Orbitron', 'Space Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
+5
View File
@@ -59,6 +59,11 @@
box-shadow: 0 0 6px rgba(255, 170, 0, 0.4);
}
.gps-status-dot.error {
background: #ff4444;
box-shadow: 0 0 6px rgba(255, 68, 68, 0.4);
}
.gps-status-text {
font-size: 11px;
color: var(--text-secondary);
+57 -57
View File
@@ -5,7 +5,7 @@
padding: 8px 10px;
background: var(--bg-tertiary, #1a1f2e);
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 11px;
}
@@ -86,7 +86,7 @@
border-radius: 4px;
background: var(--bg-tertiary, #1a1f2e);
color: var(--text-primary, #e0e0e0);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 11px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
@@ -113,7 +113,7 @@
border-bottom: 2px solid transparent;
background: transparent;
color: var(--text-dim, #666);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 11px;
text-transform: uppercase;
cursor: pointer;
@@ -153,7 +153,7 @@
display: inline-flex;
align-items: center;
gap: 7px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
color: var(--text-secondary, #999);
text-transform: uppercase;
@@ -168,7 +168,7 @@
}
.subghz-trigger-grid label {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 9px;
color: var(--text-dim, #666);
text-transform: uppercase;
@@ -182,7 +182,7 @@
border-radius: 4px;
background: var(--bg-primary, #0d1117);
color: var(--text-primary, #e0e0e0);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
}
@@ -192,7 +192,7 @@
.subghz-trigger-help {
margin: 0;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 9px;
color: var(--text-dim, #666);
line-height: 1.4;
@@ -207,7 +207,7 @@
background: var(--bg-tertiary, #1a1f2e);
border-radius: 4px;
margin-bottom: 10px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 11px;
}
@@ -264,7 +264,7 @@
border-radius: 4px;
background: transparent;
color: var(--text-primary, #e0e0e0);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 12px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
@@ -369,7 +369,7 @@
background: var(--bg-tertiary, #1a1f2e);
border: 1px solid var(--border-color, #2a3040);
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 11px;
min-width: 0;
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
@@ -416,7 +416,7 @@
display: inline-flex;
align-items: center;
gap: 8px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -446,7 +446,7 @@
padding: 1px 6px;
border-radius: 999px;
border: 1px solid var(--border-color, #2a3040);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 9px;
letter-spacing: 0.35px;
color: var(--text-dim, #666);
@@ -512,7 +512,7 @@
color: var(--text-dim, #666);
font-size: 10px;
cursor: pointer;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.subghz-capture-actions button:hover {
@@ -554,7 +554,7 @@
border-radius: 4px;
color: var(--accent-red, #ff4444);
font-size: 10px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
line-height: 1.4;
margin-bottom: 8px;
}
@@ -591,7 +591,7 @@
border-radius: 6px;
padding: 12px;
overflow-y: auto;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 12px;
color: var(--text-primary, #e0e0e0);
min-height: 200px;
@@ -695,12 +695,12 @@
.subghz-tx-modal .tx-freq {
color: var(--accent-cyan, #00d4ff);
font-weight: 600;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.subghz-tx-modal .tx-duration {
color: var(--text-dim, #666);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.subghz-tx-segment-box {
@@ -742,7 +742,7 @@
border-radius: 4px;
background: var(--bg-primary, #0d1117);
color: var(--text-primary, #e0e0e0);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 11px;
}
@@ -755,7 +755,7 @@
margin-bottom: 0 !important;
font-size: 11px !important;
color: var(--accent-cyan, #00d4ff) !important;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.subghz-tx-burst-assist {
@@ -768,7 +768,7 @@
}
.subghz-tx-burst-title {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
color: var(--text-dim, #666);
text-transform: uppercase;
@@ -805,7 +805,7 @@
.subghz-tx-burst-range {
margin: 0 0 8px 0;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
color: var(--accent-cyan, #00d4ff);
}
@@ -839,7 +839,7 @@
padding: 6px;
border: 1px dashed var(--border-color, #2a3040);
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
color: var(--text-dim, #666);
line-height: 1.4;
@@ -854,7 +854,7 @@
border: 1px solid var(--border-color, #2a3040);
border-radius: 4px;
background: rgba(0, 0, 0, 0.15);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
color: var(--text-secondary, #999);
}
@@ -865,7 +865,7 @@
border-radius: 3px;
background: transparent;
color: #00d4ff;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
cursor: pointer;
}
@@ -884,7 +884,7 @@
.subghz-tx-modal-actions button {
padding: 8px 20px;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 12px;
cursor: pointer;
border: 1px solid;
@@ -926,7 +926,7 @@
color: var(--text-dim, #666);
font-size: 12px;
padding: 24px 12px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.subghz-captures-list-main .subghz-empty {
@@ -943,7 +943,7 @@
border: 1px solid #2a3040;
border-radius: 4px;
padding: 5px 9px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 11px;
z-index: 9999;
display: none;
@@ -970,7 +970,7 @@
min-width: 180px;
padding: 4px 0;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.6);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 11px;
}
@@ -1029,7 +1029,7 @@
border-radius: 3px;
background: transparent;
color: var(--text-primary, #e0e0e0);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
cursor: pointer;
transition: background 0.12s, border-color 0.12s, color 0.12s;
@@ -1068,7 +1068,7 @@
content: 'No peaks detected';
color: var(--text-dim, #666);
font-size: 10px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
padding: 6px 0;
text-align: center;
}
@@ -1082,7 +1082,7 @@
border: 1px solid var(--border-color, #2a3040);
border-radius: 3px;
cursor: pointer;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
transition: border-color 0.12s;
}
@@ -1108,7 +1108,7 @@
border: 1px solid var(--border-color, #2a3040);
border-radius: 6px;
padding: 6px 12px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 11px;
flex-shrink: 0;
flex-wrap: wrap;
@@ -1192,7 +1192,7 @@
border-radius: 3px;
background: transparent;
color: #22c55e;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
cursor: pointer;
text-transform: uppercase;
@@ -1211,7 +1211,7 @@
padding: 2px 8px;
border: 1px solid var(--border-color, #2a3040);
border-radius: 3px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
color: var(--text-secondary, #999);
letter-spacing: 0.3px;
@@ -1263,7 +1263,7 @@
display: flex;
align-items: center;
gap: 6px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
}
@@ -1300,7 +1300,7 @@
border-radius: 999px;
margin-left: auto;
margin-right: 8px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
color: var(--text-dim, #666);
background: rgba(0, 0, 0, 0.15);
@@ -1365,7 +1365,7 @@
padding: 6px 12px;
overflow-y: auto;
max-height: 114px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
line-height: 1.6;
}
@@ -1402,7 +1402,7 @@
}
.subghz-hub-header-title {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 20px;
font-weight: 700;
color: var(--accent-cyan, #00d4ff);
@@ -1410,7 +1410,7 @@
}
.subghz-hub-header-sub {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 11px;
color: var(--text-dim, #666);
margin-top: 2px;
@@ -1472,14 +1472,14 @@
}
.subghz-hub-title {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 14px;
font-weight: 600;
color: var(--text-primary, #e0e0e0);
}
.subghz-hub-desc {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
color: var(--text-dim, #666);
}
@@ -1526,7 +1526,7 @@
}
.subghz-saved-selection-count {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
color: var(--accent-cyan, #00d4ff);
margin-right: 4px;
@@ -1538,7 +1538,7 @@
border-radius: 4px;
background: transparent;
color: var(--text-secondary, #999);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 11px;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
@@ -1550,7 +1550,7 @@
}
.subghz-op-panel-title {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 12px;
color: var(--text-primary, #e0e0e0);
text-transform: uppercase;
@@ -1620,7 +1620,7 @@
display: flex;
align-items: center;
gap: 8px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 14px;
font-weight: 600;
color: var(--accent-red, #ff4444);
@@ -1654,14 +1654,14 @@
}
.subghz-rx-info-label {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 9px;
color: var(--text-dim, #666);
letter-spacing: 0.5px;
}
.subghz-rx-info-value {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 16px;
font-weight: 600;
color: var(--text-primary, #e0e0e0);
@@ -1688,7 +1688,7 @@
border: 1px solid var(--border-color, #2a3040);
border-radius: 4px;
background: rgba(0, 0, 0, 0.22);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.subghz-rx-hint-label {
@@ -1722,7 +1722,7 @@
padding: 2px 8px;
border: 1px solid var(--border-color, #2a3040);
border-radius: 999px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
color: var(--text-dim, #666);
background: rgba(0, 0, 0, 0.2);
@@ -1741,7 +1741,7 @@
}
.subghz-rx-level-label {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 9px;
color: var(--text-dim, #666);
letter-spacing: 0.5px;
@@ -1772,7 +1772,7 @@
}
.subghz-rx-scope-label {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 9px;
color: var(--text-dim, #666);
letter-spacing: 0.5px;
@@ -1832,7 +1832,7 @@
display: flex;
align-items: center;
gap: 6px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 9px;
color: var(--text-dim, #666);
letter-spacing: 0.4px;
@@ -1854,7 +1854,7 @@
border-radius: 4px;
background: transparent;
color: var(--text-secondary, #999);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
cursor: pointer;
transition: border-color 0.15s, color 0.15s, background 0.15s;
@@ -1938,7 +1938,7 @@
}
.subghz-tx-label {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 14px;
font-weight: 600;
color: var(--accent-red, #ff4444);
@@ -1958,14 +1958,14 @@
}
.subghz-tx-info-label {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 9px;
color: var(--text-dim, #666);
letter-spacing: 0.5px;
}
.subghz-tx-info-value {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 16px;
font-weight: 600;
color: var(--text-primary, #e0e0e0);
@@ -1998,7 +1998,7 @@
}
.subghz-sweep-peaks-title {
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
color: var(--text-dim, #666);
text-transform: uppercase;
+31
View 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); }
}
+22 -22
View File
@@ -49,7 +49,7 @@
.wxsat-strip-status-text {
font-size: 12px;
color: var(--text-secondary, #999);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.wxsat-strip-btn {
@@ -59,7 +59,7 @@
background: transparent;
color: var(--text-primary, #e0e0e0);
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
cursor: pointer;
transition: all 0.2s;
}
@@ -92,7 +92,7 @@
.wxsat-strip-value {
font-size: 13px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
color: var(--text-primary, #e0e0e0);
}
@@ -114,7 +114,7 @@
gap: 6px;
cursor: pointer;
font-size: 10px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
color: var(--text-dim, #666);
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -146,7 +146,7 @@
border-radius: 3px;
color: var(--text-primary, #e0e0e0);
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.wxsat-loc-input:focus {
@@ -225,7 +225,7 @@
.wxsat-cd-value {
font-size: 16px;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
color: var(--text-primary, #e0e0e0);
line-height: 1;
}
@@ -248,13 +248,13 @@
font-size: 12px;
font-weight: 600;
color: var(--accent-cyan, #00d4ff);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.wxsat-countdown-detail {
font-size: 10px;
color: var(--text-dim, #666);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
/* ===== Timeline ===== */
@@ -314,7 +314,7 @@
justify-content: space-between;
font-size: 8px;
color: var(--text-dim, #666);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
/* ===== Pass Predictions Panel ===== */
@@ -349,7 +349,7 @@
.wxsat-passes-count {
font-size: 11px;
color: var(--accent-cyan, #00d4ff);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.wxsat-passes-list {
@@ -387,7 +387,7 @@
background: rgba(255, 187, 0, 0.15);
color: #ffbb00;
margin-left: 6px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
text-transform: uppercase;
letter-spacing: 0.5px;
}
@@ -409,7 +409,7 @@
font-size: 10px;
padding: 2px 6px;
border-radius: 3px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.wxsat-pass-mode.apt {
@@ -428,7 +428,7 @@
gap: 4px;
font-size: 11px;
color: var(--text-dim, #666);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.wxsat-pass-detail-label {
@@ -499,7 +499,7 @@
.wxsat-panel-subtitle {
font-size: 10px;
color: var(--accent-cyan, #00d4ff);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
#wxsatPolarCanvas {
@@ -547,7 +547,7 @@
.wxsat-gallery-count {
font-size: 11px;
color: var(--accent-cyan, #00d4ff);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.wxsat-gallery-grid {
@@ -636,7 +636,7 @@
.wxsat-image-product {
font-size: 10px;
color: var(--accent-cyan, #00d4ff);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.wxsat-image-timestamp {
@@ -649,7 +649,7 @@
.wxsat-date-header {
grid-column: 1 / -1;
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
color: var(--text-dim, #666);
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -708,7 +708,7 @@
.wxsat-capture-message {
font-size: 11px;
color: var(--text-secondary, #999);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -719,7 +719,7 @@
.wxsat-capture-elapsed {
font-size: 11px;
color: var(--text-dim, #666);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
flex-shrink: 0;
}
@@ -785,7 +785,7 @@
border-radius: 4px;
color: var(--text-secondary, #999);
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
text-align: center;
}
@@ -941,7 +941,7 @@
display: flex;
align-items: center;
gap: 4px;
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
}
.wxsat-phase-step {
@@ -1012,7 +1012,7 @@
max-height: 160px;
padding: 6px 12px;
background: var(--bg-primary, #0d1117);
font-family: 'JetBrains Mono', monospace;
font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;
font-size: 10px;
line-height: 1.6;
}
+2 -2
View File
@@ -5,8 +5,8 @@
}
:root {
--font-sans: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', 'Menlo', monospace;
--font-mono: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', 'Menlo', monospace;
--font-sans: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--bg-dark: #0b1118;
--bg-panel: #101823;
--bg-card: #151f2b;
+144 -25
View File
@@ -36,6 +36,7 @@ const ProximityRadar = (function() {
let isHovered = false;
let renderPending = false;
let renderTimer = null;
let interactionLockUntil = 0; // timestamp: suppress renders briefly after click
/**
* Initialize the radar component
@@ -119,6 +120,36 @@ const ProximityRadar = (function() {
svg = container.querySelector('svg');
// Event delegation on the devices group (survives innerHTML rebuilds)
const devicesGroup = svg.querySelector('.radar-devices');
devicesGroup.addEventListener('click', (e) => {
const deviceEl = e.target.closest('.radar-device');
if (!deviceEl) return;
const deviceKey = deviceEl.getAttribute('data-device-key');
if (onDeviceClick && deviceKey) {
// Lock out re-renders briefly so the DOM stays stable after click
interactionLockUntil = Date.now() + 500;
onDeviceClick(deviceKey);
}
});
devicesGroup.addEventListener('mouseenter', (e) => {
if (e.target.closest('.radar-device')) {
isHovered = true;
}
}, true); // capture phase so we catch enter on child elements
devicesGroup.addEventListener('mouseleave', (e) => {
if (e.target.closest('.radar-device')) {
isHovered = false;
if (renderPending) {
renderPending = false;
renderDevices();
}
}
}, true);
// Add sweep animation
animateSweep();
}
@@ -165,8 +196,8 @@ const ProximityRadar = (function() {
devices.set(device.device_key, device);
});
// Defer render while user is hovering to prevent DOM rebuild flicker
if (isHovered) {
// Defer render while user is hovering or interacting to prevent DOM rebuild flicker
if (isHovered || Date.now() < interactionLockUntil) {
renderPending = true;
return;
}
@@ -229,7 +260,7 @@ const ProximityRadar = (function() {
style="cursor: pointer;">
<!-- Invisible hit area to prevent hover flicker -->
<circle class="radar-device-hitarea" r="${hitAreaSize}" fill="transparent" />
${isSelected ? `<circle r="${dotSize + 8}" fill="none" stroke="#00d4ff" stroke-width="2" stroke-opacity="0.8">
${isSelected ? `<circle class="radar-select-ring" r="${dotSize + 8}" fill="none" stroke="#00d4ff" stroke-width="2" stroke-opacity="0.8">
<animate attributeName="r" values="${dotSize + 6};${dotSize + 10};${dotSize + 6}" dur="1.5s" repeatCount="indefinite"/>
<animate attributeName="stroke-opacity" values="0.8;0.4;0.8" dur="1.5s" repeatCount="indefinite"/>
</circle>` : ''}
@@ -244,24 +275,6 @@ const ProximityRadar = (function() {
}).join('');
devicesGroup.innerHTML = dots;
// Attach event handlers
devicesGroup.querySelectorAll('.radar-device').forEach(el => {
el.addEventListener('click', (e) => {
const deviceKey = el.getAttribute('data-device-key');
if (onDeviceClick && deviceKey) {
onDeviceClick(deviceKey);
}
});
el.addEventListener('mouseenter', () => { isHovered = true; });
el.addEventListener('mouseleave', () => {
isHovered = false;
if (renderPending) {
renderPending = false;
renderDevices();
}
});
});
}
/**
@@ -345,19 +358,125 @@ const ProximityRadar = (function() {
}
/**
* Highlight a specific device on the radar
* Highlight a specific device on the radar (in-place update, no full re-render)
*/
function highlightDevice(deviceKey) {
const prev = selectedDeviceKey;
selectedDeviceKey = deviceKey;
renderDevices();
if (!svg) { return; }
const devicesGroup = svg.querySelector('.radar-devices');
if (!devicesGroup) { return; }
// Remove highlight from previously selected node
if (prev && prev !== deviceKey) {
const oldEl = devicesGroup.querySelector(`.radar-device[data-device-key="${CSS.escape(prev)}"]`);
if (oldEl) {
oldEl.classList.remove('selected');
// Remove animated selection ring
const ring = oldEl.querySelector('.radar-select-ring');
if (ring) ring.remove();
// Restore dot opacity
const dot = oldEl.querySelector('circle:not(.radar-device-hitarea):not(.radar-select-ring)');
if (dot && dot.getAttribute('fill') !== 'none' && dot.getAttribute('fill') !== 'transparent') {
const device = devices.get(prev);
const confidence = device ? (device.distance_confidence || 0.5) : 0.5;
dot.setAttribute('fill-opacity', 0.4 + confidence * 0.5);
dot.setAttribute('stroke', dot.getAttribute('fill'));
dot.setAttribute('stroke-width', '1');
}
}
}
// Add highlight to newly selected node
if (deviceKey) {
const newEl = devicesGroup.querySelector(`.radar-device[data-device-key="${CSS.escape(deviceKey)}"]`);
if (newEl) {
applySelectionToElement(newEl, deviceKey);
} else {
// Node not in DOM yet; full render needed on next cycle
renderDevices();
}
}
}
/**
* Clear device highlighting
* Apply selection styling to a radar device element in-place
*/
function applySelectionToElement(el, deviceKey) {
el.classList.add('selected');
const device = devices.get(deviceKey);
const confidence = device ? (device.distance_confidence || 0.5) : 0.5;
const dotSize = CONFIG.dotMinSize + (CONFIG.dotMaxSize - CONFIG.dotMinSize) * confidence;
// Update dot styling
const dot = el.querySelector('circle:not(.radar-device-hitarea):not(.radar-select-ring)');
if (dot && dot.getAttribute('fill') !== 'none' && dot.getAttribute('fill') !== 'transparent') {
dot.setAttribute('fill-opacity', '1');
dot.setAttribute('stroke', '#00d4ff');
dot.setAttribute('stroke-width', '2');
}
// Add animated selection ring if not already present
if (!el.querySelector('.radar-select-ring')) {
const ns = 'http://www.w3.org/2000/svg';
const ring = document.createElementNS(ns, 'circle');
ring.classList.add('radar-select-ring');
ring.setAttribute('r', dotSize + 8);
ring.setAttribute('fill', 'none');
ring.setAttribute('stroke', '#00d4ff');
ring.setAttribute('stroke-width', '2');
ring.setAttribute('stroke-opacity', '0.8');
const animR = document.createElementNS(ns, 'animate');
animR.setAttribute('attributeName', 'r');
animR.setAttribute('values', `${dotSize + 6};${dotSize + 10};${dotSize + 6}`);
animR.setAttribute('dur', '1.5s');
animR.setAttribute('repeatCount', 'indefinite');
ring.appendChild(animR);
const animO = document.createElementNS(ns, 'animate');
animO.setAttribute('attributeName', 'stroke-opacity');
animO.setAttribute('values', '0.8;0.4;0.8');
animO.setAttribute('dur', '1.5s');
animO.setAttribute('repeatCount', 'indefinite');
ring.appendChild(animO);
// Insert after the hit area
const hitArea = el.querySelector('.radar-device-hitarea');
if (hitArea && hitArea.nextSibling) {
el.insertBefore(ring, hitArea.nextSibling);
} else {
el.insertBefore(ring, el.firstChild);
}
}
}
/**
* Clear device highlighting (in-place update, no full re-render)
*/
function clearHighlight() {
const prev = selectedDeviceKey;
selectedDeviceKey = null;
renderDevices();
if (!svg || !prev) { return; }
const devicesGroup = svg.querySelector('.radar-devices');
if (!devicesGroup) { return; }
const oldEl = devicesGroup.querySelector(`.radar-device[data-device-key="${CSS.escape(prev)}"]`);
if (oldEl) {
oldEl.classList.remove('selected');
const ring = oldEl.querySelector('.radar-select-ring');
if (ring) ring.remove();
const dot = oldEl.querySelector('circle:not(.radar-device-hitarea):not(.radar-select-ring)');
if (dot && dot.getAttribute('fill') !== 'none' && dot.getAttribute('fill') !== 'transparent') {
const device = devices.get(prev);
const confidence = device ? (device.distance_confidence || 0.5) : 0.5;
dot.setAttribute('fill-opacity', 0.4 + confidence * 0.5);
dot.setAttribute('stroke', dot.getAttribute('fill'));
dot.setAttribute('stroke-width', '1');
}
}
}
/**
+7 -1
View File
@@ -302,7 +302,13 @@ const SignalCards = (function() {
*/
function formatRelativeTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
let date = new Date(timestamp);
// Handle time-only strings like "HH:MM:SS" (from pager/sensor backends)
if (isNaN(date.getTime()) && /^\d{1,2}:\d{2}(:\d{2})?$/.test(timestamp)) {
const today = new Date();
date = new Date(today.toDateString() + ' ' + timestamp);
}
if (isNaN(date.getTime())) return timestamp;
const now = new Date();
const diff = Math.floor((now - date) / 1000);
+9 -8
View File
@@ -423,7 +423,7 @@ async function syncAgentModeStates(agentId) {
});
// Also check modes that might need to be marked as stopped
const allModes = ['sensor', 'pager', 'adsb', 'wifi', 'bluetooth', 'ais', 'dsc', 'acars', 'aprs', 'rtlamr', 'tscm', 'satellite', 'listening_post'];
const allModes = ['sensor', 'pager', 'adsb', 'wifi', 'bluetooth', 'ais', 'dsc', 'acars', 'vdl2', 'aprs', 'rtlamr', 'tscm', 'satellite', 'listening_post'];
allModes.forEach(mode => {
if (!agentRunningModes.includes(mode)) {
syncModeUI(mode, false, agentId);
@@ -704,6 +704,7 @@ function syncModeUI(mode, isRunning, agentId = null) {
'wifi': 'setWiFiRunning',
'bluetooth': 'setBluetoothRunning',
'acars': 'setAcarsRunning',
'vdl2': 'setVdl2Running',
'listening_post': 'setListeningPostRunning'
};
@@ -865,12 +866,12 @@ function connectAgentStream(mode, onMessage) {
}
let streamUrl;
if (currentAgent === 'local') {
streamUrl = `/${mode}/stream`;
} else {
// For remote agents, proxy SSE through controller
streamUrl = `/controller/agents/${currentAgent}/${mode}/stream`;
}
if (currentAgent === 'local') {
streamUrl = `/${mode}/stream`;
} else {
// For remote agents, proxy SSE through controller
streamUrl = `/controller/agents/${currentAgent}/${mode}/stream`;
}
agentEventSource = new EventSource(streamUrl);
@@ -878,7 +879,7 @@ function connectAgentStream(mode, onMessage) {
try {
const data = JSON.parse(event.data);
onMessage(data);
onMessage(data);
} catch (e) {
console.error('Error parsing SSE message:', e);
}
+2 -2
View File
@@ -373,7 +373,7 @@ function showInfo(text) {
const infoEl = document.createElement('div');
infoEl.className = 'info-msg';
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "Space Mono", monospace; font-size: 11px; color: #888; word-break: break-all;';
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "Roboto Condensed", "Arial Narrow", sans-serif; font-size: 11px; color: #888; word-break: break-all;';
infoEl.textContent = text;
output.insertBefore(infoEl, output.firstChild);
}
@@ -387,7 +387,7 @@ function showError(text) {
const errorEl = document.createElement('div');
errorEl.className = 'error-msg';
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "Space Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;';
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "Roboto Condensed", "Arial Narrow", sans-serif; font-size: 11px; color: #ff6688; word-break: break-all;';
errorEl.textContent = '⚠ ' + text;
output.insertBefore(errorEl, output.firstChild);
}
+2 -2
View File
@@ -833,11 +833,11 @@ function renderUpdateStatus(data) {
<div style="display: grid; gap: 8px; font-size: 12px;">
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--text-dim);">Current Version</span>
<span style="font-family: 'Space Mono', monospace; color: var(--text-primary);">v${data.current_version}</span>
<span style="font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; color: var(--text-primary);">v${data.current_version}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--text-dim);">Latest Version</span>
<span style="font-family: 'Space Mono', monospace; color: ${data.update_available ? 'var(--accent-green)' : 'var(--text-primary)'};">v${data.latest_version}</span>
<span style="font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; color: ${data.update_available ? 'var(--accent-green)' : 'var(--text-primary)'};">v${data.latest_version}</span>
</div>
${data.last_check ? `
<div style="display: flex; justify-content: space-between;">
+173 -4
View File
@@ -356,7 +356,9 @@ const BluetoothMode = (function() {
// Update panel elements
document.getElementById('btDetailName').textContent = device.name || formatDeviceId(device.address);
document.getElementById('btDetailAddress').textContent = device.address;
document.getElementById('btDetailAddress').textContent = isUuidAddress(device)
? 'CB: ' + device.address
: device.address;
// RSSI
const rssiEl = document.getElementById('btDetailRssi');
@@ -458,8 +460,98 @@ const BluetoothMode = (function() {
? new Date(device.last_seen).toLocaleTimeString()
: '--';
// New stat cells
document.getElementById('btDetailTxPower').textContent = device.tx_power != null
? device.tx_power + ' dBm' : '--';
document.getElementById('btDetailSeenRate').textContent = device.seen_rate != null
? device.seen_rate.toFixed(1) + '/min' : '--';
// Stability from variance
const stabilityEl = document.getElementById('btDetailStability');
if (device.rssi_variance != null) {
let stabLabel, stabColor;
if (device.rssi_variance <= 5) { stabLabel = 'Stable'; stabColor = '#22c55e'; }
else if (device.rssi_variance <= 25) { stabLabel = 'Moderate'; stabColor = '#eab308'; }
else { stabLabel = 'Unstable'; stabColor = '#ef4444'; }
stabilityEl.textContent = stabLabel;
stabilityEl.style.color = stabColor;
} else {
stabilityEl.textContent = '--';
stabilityEl.style.color = '';
}
// Distance with confidence
const distEl = document.getElementById('btDetailDistance');
if (device.estimated_distance_m != null) {
const confPct = Math.round((device.distance_confidence || 0) * 100);
distEl.textContent = device.estimated_distance_m.toFixed(1) + 'm ±' + confPct + '%';
} else {
distEl.textContent = '--';
}
// Appearance badge
if (device.appearance_name) {
badgesHtml += '<span class="bt-detail-badge flag">' + escapeHtml(device.appearance_name) + '</span>';
badgesEl.innerHTML = badgesHtml;
}
// MAC cluster indicator
const macClusterEl = document.getElementById('btDetailMacCluster');
if (macClusterEl) {
if (device.mac_cluster_count > 1) {
macClusterEl.textContent = device.mac_cluster_count + ' MACs';
macClusterEl.style.display = '';
} else {
macClusterEl.style.display = 'none';
}
}
// Service data inspector
const inspectorEl = document.getElementById('btDetailServiceInspector');
const inspectorContent = document.getElementById('btInspectorContent');
if (inspectorEl && inspectorContent) {
const hasData = device.manufacturer_bytes || device.appearance != null ||
(device.service_data && Object.keys(device.service_data).length > 0);
if (hasData) {
inspectorEl.style.display = '';
let inspHtml = '';
if (device.appearance != null) {
const name = device.appearance_name || '';
inspHtml += '<div class="bt-inspector-row"><span class="bt-inspector-label">Appearance</span><span class="bt-inspector-value">0x' + device.appearance.toString(16).toUpperCase().padStart(4, '0') + (name ? ' (' + escapeHtml(name) + ')' : '') + '</span></div>';
}
if (device.manufacturer_bytes) {
inspHtml += '<div class="bt-inspector-row"><span class="bt-inspector-label">Mfr Data</span><span class="bt-inspector-value">' + escapeHtml(device.manufacturer_bytes) + '</span></div>';
}
if (device.service_data) {
Object.entries(device.service_data).forEach(([uuid, hex]) => {
inspHtml += '<div class="bt-inspector-row"><span class="bt-inspector-label">' + escapeHtml(uuid) + '</span><span class="bt-inspector-value">' + escapeHtml(hex) + '</span></div>';
});
}
inspectorContent.innerHTML = inspHtml;
} else {
inspectorEl.style.display = 'none';
}
}
updateWatchlistButton(device);
// IRK
const irkContainer = document.getElementById('btDetailIrk');
if (irkContainer) {
if (device.has_irk) {
irkContainer.style.display = 'block';
const irkVal = document.getElementById('btDetailIrkValue');
if (irkVal) {
const label = device.irk_source_name
? device.irk_source_name + ' — ' + device.irk_hex
: device.irk_hex;
irkVal.textContent = label;
}
} else {
irkContainer.style.display = 'none';
}
}
// Services
const servicesContainer = document.getElementById('btDetailServices');
const servicesList = document.getElementById('btDetailServicesList');
@@ -600,9 +692,25 @@ const BluetoothMode = (function() {
if (parts.length === 6) {
return parts[0] + ':' + parts[1] + ':...:' + parts[4] + ':' + parts[5];
}
// CoreBluetooth UUID format (8-4-4-4-12)
if (/^[0-9A-F]{8}-[0-9A-F]{4}-/i.test(address)) {
return address.substring(0, 8) + '...';
}
return address;
}
function isUuidAddress(device) {
return device.address_type === 'uuid';
}
function formatAddress(device) {
if (!device || !device.address) return '--';
if (isUuidAddress(device)) {
return device.address.substring(0, 8) + '-...' + device.address.slice(-4);
}
return device.address;
}
/**
* Check system capabilities
*/
@@ -660,6 +768,12 @@ const BluetoothMode = (function() {
hideCapabilityWarning();
}
// Show/hide Ubertooth option based on capabilities
const ubertoothOption = document.getElementById('btScanModeUbertooth');
if (ubertoothOption) {
ubertoothOption.style.display = data.has_ubertooth ? '' : 'none';
}
if (scanModeSelect && data.preferred_backend) {
const option = scanModeSelect.querySelector(`option[value="${data.preferred_backend}"]`);
if (option) option.selected = true;
@@ -1085,7 +1199,7 @@ const BluetoothMode = (function() {
'</div>' +
'</div>' +
'<div style="display:flex;justify-content:space-between;margin-top:3px;">' +
'<span style="font-size:9px;color:#888;font-family:monospace;">' + t.address + '</span>' +
'<span style="font-size:9px;color:#888;font-family:monospace;">' + (t.address_type === 'uuid' ? formatAddress(t) : t.address) + '</span>' +
'<span style="font-size:9px;color:#666;">Seen ' + (t.seen_count || 0) + 'x</span>' +
'</div>' +
evidenceHtml +
@@ -1142,7 +1256,7 @@ const BluetoothMode = (function() {
const displayName = device.name || formatDeviceId(device.address);
const name = escapeHtml(displayName);
const addr = escapeHtml(device.address || 'Unknown');
const addr = escapeHtml(isUuidAddress(device) ? formatAddress(device) : (device.address || 'Unknown'));
const mfr = device.manufacturer_name ? escapeHtml(device.manufacturer_name) : '';
const seenCount = device.seen_count || 0;
const deviceIdEscaped = escapeHtml(device.device_id).replace(/'/g, "\\'");
@@ -1167,6 +1281,12 @@ const BluetoothMode = (function() {
trackerBadge = '<span class="bt-tracker-badge" style="background:' + confBg + ';color:' + confColor + ';font-size:9px;padding:1px 4px;border-radius:3px;margin-left:4px;font-weight:600;">' + typeLabel + '</span>';
}
// IRK badge - show if paired IRK is available
let irkBadge = '';
if (device.has_irk) {
irkBadge = '<span class="bt-irk-badge">IRK</span>';
}
// Risk badge - show if risk score is significant
let riskBadge = '';
if (riskScore >= 0.3) {
@@ -1184,9 +1304,36 @@ const BluetoothMode = (function() {
statusDot = '<span class="bt-status-dot known"></span>';
}
// Distance display
const distM = device.estimated_distance_m;
let distStr = '';
if (distM != null) {
distStr = '~' + distM.toFixed(1) + 'm';
}
// Behavioral flag badges
const hFlags = device.heuristic_flags || [];
let flagBadges = '';
if (device.is_persistent || hFlags.includes('persistent')) {
flagBadges += '<span class="bt-flag-badge persistent">PERSIST</span>';
}
if (device.is_beacon_like || hFlags.includes('beacon_like')) {
flagBadges += '<span class="bt-flag-badge beacon-like">BEACON</span>';
}
if (device.is_strong_stable || hFlags.includes('strong_stable')) {
flagBadges += '<span class="bt-flag-badge strong-stable">STABLE</span>';
}
// MAC cluster badge
let clusterBadge = '';
if (device.mac_cluster_count > 1) {
clusterBadge = '<span class="bt-mac-cluster-badge">' + device.mac_cluster_count + ' MACs</span>';
}
// Build secondary info line
let secondaryParts = [addr];
if (mfr) secondaryParts.push(mfr);
if (distStr) secondaryParts.push(distStr);
secondaryParts.push('Seen ' + seenCount + '×');
if (seenBefore) secondaryParts.push('<span class="bt-history-badge">SEEN BEFORE</span>');
// Add agent name if not Local
@@ -1205,7 +1352,10 @@ const BluetoothMode = (function() {
protoBadge +
'<span class="bt-device-name">' + name + '</span>' +
trackerBadge +
irkBadge +
riskBadge +
flagBadges +
clusterBadge +
'</div>' +
'<div class="bt-row-right">' +
'<div class="bt-rssi-container">' +
@@ -1300,6 +1450,18 @@ const BluetoothMode = (function() {
}
}
/**
* Toggle the service data inspector panel
*/
function toggleServiceInspector() {
const content = document.getElementById('btInspectorContent');
const arrow = document.getElementById('btInspectorArrow');
if (!content) return;
const open = content.style.display === 'none';
content.style.display = open ? '' : 'none';
if (arrow) arrow.classList.toggle('open', open);
}
// ==========================================================================
// Agent Handling
// ==========================================================================
@@ -1425,9 +1587,15 @@ const BluetoothMode = (function() {
BtLocate.handoff({
device_id: device.device_id,
mac_address: device.address,
address_type: device.address_type || null,
irk_hex: device.irk_hex || null,
known_name: device.name || null,
known_manufacturer: device.manufacturer_name || null,
last_known_rssi: device.rssi_current
last_known_rssi: device.rssi_current,
tx_power: device.tx_power || null,
appearance_name: device.appearance_name || null,
fingerprint_id: device.fingerprint_id || null,
mac_cluster_count: device.mac_cluster_count || 0
});
}
}
@@ -1447,6 +1615,7 @@ const BluetoothMode = (function() {
toggleWatchlist,
locateDevice,
locateById,
toggleServiceInspector,
// Agent handling
handleAgentChange,
+20 -3
View File
@@ -322,7 +322,8 @@ const BtLocate = (function() {
const t = data.target;
const name = t.known_name || t.name_pattern || '';
const addr = t.mac_address || t.device_id || '';
targetEl.textContent = name ? (name + (addr ? ' (' + addr.substring(0, 8) + '...)' : '')) : addr || '--';
const addrDisplay = formatAddr(addr);
targetEl.textContent = name ? (name + (addrDisplay ? ' (' + addrDisplay + ')' : '')) : addrDisplay || '--';
}
// Environment info
@@ -602,6 +603,16 @@ const BtLocate = (function() {
}).catch(() => {});
}
function isUuid(addr) {
return addr && /^[0-9A-F]{8}-[0-9A-F]{4}-/i.test(addr);
}
function formatAddr(addr) {
if (!addr) return '';
if (isUuid(addr)) return addr.substring(0, 8) + '-...' + addr.slice(-4);
return addr;
}
function handoff(deviceInfo) {
console.log('[BtLocate] Handoff received:', deviceInfo);
handoffData = deviceInfo;
@@ -617,15 +628,21 @@ const BtLocate = (function() {
const nameEl = document.getElementById('btLocateHandoffName');
const metaEl = document.getElementById('btLocateHandoffMeta');
if (card) card.style.display = '';
if (nameEl) nameEl.textContent = deviceInfo.known_name || deviceInfo.mac_address || 'Unknown';
if (nameEl) nameEl.textContent = deviceInfo.known_name || formatAddr(deviceInfo.mac_address) || 'Unknown';
if (metaEl) {
const parts = [];
if (deviceInfo.mac_address) parts.push(deviceInfo.mac_address);
if (deviceInfo.mac_address) parts.push(formatAddr(deviceInfo.mac_address));
if (deviceInfo.known_manufacturer) parts.push(deviceInfo.known_manufacturer);
if (deviceInfo.last_known_rssi != null) parts.push(deviceInfo.last_known_rssi + ' dBm');
metaEl.textContent = parts.join(' \u00b7 ');
}
// Auto-fill IRK if available from scanner
if (deviceInfo.irk_hex) {
const irkInput = document.getElementById('btLocateIrk');
if (irkInput) irkInput.value = deviceInfo.irk_hex;
}
// Switch to bt_locate mode
if (typeof switchMode === 'function') {
switchMode('bt_locate');
+78 -41
View File
@@ -5,10 +5,10 @@
*/
const GPS = (function() {
let eventSource = null;
let connected = false;
let lastPosition = null;
let lastSky = null;
let skyPollTimer = null;
// Constellation color map
const CONST_COLORS = {
@@ -26,6 +26,7 @@ const GPS = (function() {
}
function connect() {
updateConnectionUI(false, false, 'connecting');
fetch('/gps/auto-connect', { method: 'POST' })
.then(r => r.json())
.then(data => {
@@ -40,23 +41,26 @@ const GPS = (function() {
lastSky = data.sky;
updateSkyUI(data.sky);
}
startStream();
subscribeToStream();
startSkyPolling();
// Ensure the global GPS stream is running
if (typeof startGpsStream === 'function' && !gpsEventSource) {
startGpsStream();
}
} else {
connected = false;
updateConnectionUI(false);
updateConnectionUI(false, false, 'error', data.message || 'gpsd not available');
}
})
.catch(() => {
connected = false;
updateConnectionUI(false);
updateConnectionUI(false, false, 'error', 'Connection failed — is the server running?');
});
}
function disconnect() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
unsubscribeFromStream();
stopSkyPolling();
fetch('/gps/stop', { method: 'POST' })
.then(() => {
connected = false;
@@ -64,36 +68,64 @@ const GPS = (function() {
});
}
function startStream() {
if (eventSource) {
eventSource.close();
function onGpsStreamData(data) {
if (!connected) return;
if (data.type === 'position') {
lastPosition = data;
updatePositionUI(data);
updateConnectionUI(true, true);
} else if (data.type === 'sky') {
lastSky = data;
updateSkyUI(data);
}
eventSource = new EventSource('/gps/stream');
eventSource.onmessage = function(e) {
try {
const data = JSON.parse(e.data);
if (data.type === 'position') {
lastPosition = data;
updatePositionUI(data);
updateConnectionUI(true, true);
} else if (data.type === 'sky') {
lastSky = data;
updateSkyUI(data);
}
function startSkyPolling() {
stopSkyPolling();
// Poll satellite data every 5 seconds as a reliable fallback
// SSE stream may miss sky updates due to queue contention with position messages
pollSatellites();
skyPollTimer = setInterval(pollSatellites, 5000);
}
function stopSkyPolling() {
if (skyPollTimer) {
clearInterval(skyPollTimer);
skyPollTimer = null;
}
}
function pollSatellites() {
if (!connected) return;
fetch('/gps/satellites')
.then(r => r.json())
.then(data => {
if (data.status === 'ok' && data.sky) {
lastSky = data.sky;
updateSkyUI(data.sky);
}
} catch (err) {
// ignore parse errors
}
};
eventSource.onerror = function() {
// Reconnect handled by browser automatically
};
})
.catch(() => {});
}
function subscribeToStream() {
// Subscribe to the global GPS stream instead of opening a separate SSE connection
if (typeof addGpsStreamSubscriber === 'function') {
addGpsStreamSubscriber(onGpsStreamData);
}
}
function unsubscribeFromStream() {
if (typeof removeGpsStreamSubscriber === 'function') {
removeGpsStreamSubscriber(onGpsStreamData);
}
}
// ========================
// UI Updates
// ========================
function updateConnectionUI(isConnected, hasFix) {
function updateConnectionUI(isConnected, hasFix, state, message) {
const dot = document.getElementById('gpsStatusDot');
const text = document.getElementById('gpsStatusText');
const connectBtn = document.getElementById('gpsConnectBtn');
@@ -102,15 +134,22 @@ const GPS = (function() {
if (dot) {
dot.className = 'gps-status-dot';
if (isConnected && hasFix) dot.classList.add('connected');
if (state === 'connecting') dot.classList.add('waiting');
else if (state === 'error') dot.classList.add('error');
else if (isConnected && hasFix) dot.classList.add('connected');
else if (isConnected) dot.classList.add('waiting');
}
if (text) {
if (isConnected && hasFix) text.textContent = 'Connected (Fix)';
if (state === 'connecting') text.textContent = 'Connecting...';
else if (state === 'error') text.textContent = message || 'Connection failed';
else if (isConnected && hasFix) text.textContent = 'Connected (Fix)';
else if (isConnected) text.textContent = 'Connected (No Fix)';
else text.textContent = 'Disconnected';
}
if (connectBtn) connectBtn.style.display = isConnected ? 'none' : '';
if (connectBtn) {
connectBtn.style.display = isConnected ? 'none' : '';
connectBtn.disabled = state === 'connecting';
}
if (disconnectBtn) disconnectBtn.style.display = isConnected ? '' : 'none';
if (devicePath) devicePath.textContent = isConnected ? 'gpsd://localhost:2947' : '';
}
@@ -252,7 +291,7 @@ const GPS = (function() {
// PRN label
ctx.fillStyle = color;
ctx.font = '8px JetBrains Mono, monospace';
ctx.font = '8px Roboto Condensed, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText(sat.prn, px, py - dotSize - 2);
@@ -260,7 +299,7 @@ const GPS = (function() {
// SNR value
if (sat.snr != null) {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '7px JetBrains Mono, monospace';
ctx.font = '7px Roboto Condensed, monospace';
ctx.textBaseline = 'top';
ctx.fillText(Math.round(sat.snr), px, py + dotSize + 1);
}
@@ -292,7 +331,7 @@ const GPS = (function() {
ctx.stroke();
// Label
ctx.fillStyle = '#555';
ctx.font = '9px JetBrains Mono, monospace';
ctx.font = '9px Roboto Condensed, monospace';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(el + '\u00b0', cx + gr + 3, cy - 2);
@@ -307,7 +346,7 @@ const GPS = (function() {
// Cardinal directions
ctx.fillStyle = '#888';
ctx.font = 'bold 11px JetBrains Mono, monospace';
ctx.font = 'bold 11px Roboto Condensed, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('N', cx, cy - r - 12);
@@ -386,10 +425,8 @@ const GPS = (function() {
// ========================
function destroy() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
unsubscribeFromStream();
stopSkyPolling();
}
return {
+1 -1
View File
@@ -1483,7 +1483,7 @@ function drawAudioVisualizer() {
}
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.font = '8px Space Mono';
ctx.font = '8px Roboto Condensed';
ctx.fillText('0', 2, canvas.height - 2);
ctx.fillText('4kHz', canvas.width / 4, canvas.height - 2);
ctx.fillText('8kHz', canvas.width / 2, canvas.height - 2);
+1 -1
View File
@@ -84,7 +84,7 @@ const SpyStations = (function() {
modeContainer.innerHTML = modes.map(m => `
<label class="inline-checkbox">
<input type="checkbox" data-mode="${m}" checked onchange="SpyStations.applyFilters()">
<span style="font-family: 'Space Mono', monospace; font-size: 10px;">${m}</span>
<span style="font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; font-size: 10px;">${m}</span>
</label>
`).join('');
}
+2 -2
View File
@@ -1754,7 +1754,7 @@ const SubGhz = (function() {
// Grid
ctx.strokeStyle = '#1a1f2e';
ctx.lineWidth = 1;
ctx.font = '10px JetBrains Mono, monospace';
ctx.font = '10px Roboto Condensed, monospace';
ctx.fillStyle = '#666';
for (let db = powerMin; db <= powerMax; db += 20) {
@@ -1824,7 +1824,7 @@ const SubGhz = (function() {
ctx.lineTo(x + 4, y - 2);
ctx.closePath();
ctx.fill();
ctx.font = '9px JetBrains Mono, monospace';
ctx.font = '9px Roboto Condensed, monospace';
ctx.fillStyle = 'rgba(255, 170, 0, 0.8)';
ctx.textAlign = 'center';
ctx.fillText(peak.freq.toFixed(1), x, y - 10);
+4 -4
View File
@@ -566,7 +566,7 @@ const WeatherSat = (function() {
</div>
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 4px;">
<span class="wxsat-pass-quality ${pass.quality}">${pass.quality}</span>
<span style="font-size: 10px; color: var(--text-dim); font-family: 'JetBrains Mono', monospace;">${countdown}</span>
<span style="font-size: 10px; color: var(--text-dim); font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">${countdown}</span>
</div>
<div style="margin-top: 6px; text-align: right;">
<button class="wxsat-strip-btn" onclick="event.stopPropagation(); WeatherSat.startPass('${escapeHtml(pass.satellite)}')" style="font-size: 10px; padding: 2px 8px;">Capture</button>
@@ -610,7 +610,7 @@ const WeatherSat = (function() {
ctx.stroke();
// Label
ctx.fillStyle = '#555';
ctx.font = '9px JetBrains Mono, monospace';
ctx.font = '9px Roboto Condensed, monospace';
ctx.textAlign = 'left';
ctx.fillText(el + '\u00b0', cx + gr + 3, cy - 2);
});
@@ -624,7 +624,7 @@ const WeatherSat = (function() {
// Cardinal directions
ctx.fillStyle = '#666';
ctx.font = '10px JetBrains Mono, monospace';
ctx.font = '10px Roboto Condensed, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('N', cx, cy - r - 10);
@@ -692,7 +692,7 @@ const WeatherSat = (function() {
ctx.arc(cx + r * maxR * Math.cos(maxAz), cy + r * maxR * Math.sin(maxAz), 3, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = color;
ctx.font = '9px JetBrains Mono, monospace';
ctx.font = '9px Roboto Condensed, monospace';
ctx.textAlign = 'center';
ctx.fillText(Math.round(maxEl) + '\u00b0', cx + r * maxR * Math.cos(maxAz), cy + r * maxR * Math.sin(maxAz) - 8);
}
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -7,7 +7,7 @@
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
@@ -472,7 +472,7 @@
if (!points.length) {
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
ctx.font = '12px "Space Mono", monospace';
ctx.font = '12px "Roboto Condensed", "Arial Narrow", sans-serif';
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
return;
}
@@ -480,7 +480,7 @@
const series = points.map(p => p[field]).filter(v => v !== null && v !== undefined);
if (!series.length) {
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
ctx.font = '12px "Space Mono", monospace';
ctx.font = '12px "Roboto Condensed", "Arial Narrow", sans-serif';
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
return;
}
@@ -521,7 +521,7 @@
}
ctx.fillStyle = 'rgba(226, 232, 240, 0.8)';
ctx.font = '11px "Space Mono", monospace';
ctx.font = '11px "Roboto Condensed", "Arial Narrow", sans-serif';
ctx.fillText(`${maxVal} ${unit}`, 12, padding);
ctx.fillText(`${minVal} ${unit}`, 12, height - padding);
}
+1 -1
View File
@@ -8,7 +8,7 @@
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
<!-- Leaflet.js - Conditional CDN/Local loading -->
{% if offline_settings.assets_source == 'local' %}
+72 -320
View File
@@ -29,7 +29,7 @@
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
<!-- Leaflet.js for APRS map - Conditional CDN/Local loading -->
{% if offline_settings.assets_source == 'local' %}
@@ -50,7 +50,6 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.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/modes/acars.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/components/signal-cards.css') }}">
@@ -216,6 +215,10 @@
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg></span>
<span class="mode-name">Bluetooth</span>
</button>
<button class="mode-card mode-card-sm" onclick="selectMode('bt_locate')">
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/><path d="M9.5 8.5l3 3 2-4-2 4-3 3"/></svg></span>
<span class="mode-name">BT Locate</span>
</button>
</div>
</div>
@@ -439,6 +442,7 @@
<label style="font-size: 11px; color: #888; margin-bottom: 4px;">Hardware Type</label>
<select id="sdrTypeSelect" onchange="onSDRTypeChanged()">
<option value="rtlsdr">RTL-SDR</option>
<option value="sdrplay">SDRplay</option>
<option value="limesdr">LimeSDR</option>
<option value="hackrf">HackRF</option>
<option value="airspy">Airspy</option>
@@ -564,6 +568,8 @@
{% include 'partials/modes/bt_locate.html' %}
<button class="preset-btn" onclick="killAll()"
style="width: 100%; margin-top: 10px; border-color: #ff3366; color: #ff3366;">
Kill All Processes
@@ -790,7 +796,10 @@
<div class="bt-detail-top-row">
<div class="bt-detail-identity">
<div class="bt-detail-name" id="btDetailName">Device Name</div>
<div class="bt-detail-address" id="btDetailAddress">00:00:00:00:00:00</div>
<div class="bt-detail-address">
<span id="btDetailAddress">00:00:00:00:00:00</span>
<span class="bt-mac-cluster-badge" id="btDetailMacCluster" style="display:none;"></span>
</div>
</div>
<div class="bt-detail-rssi-display">
<span class="bt-detail-rssi-value" id="btDetailRssi">--</span>
@@ -831,8 +840,36 @@
<span class="bt-detail-stat-label">Mfr ID</span>
<span class="bt-detail-stat-value" id="btDetailMfrId">--</span>
</div>
<div class="bt-detail-stat">
<span class="bt-detail-stat-label">TX Power</span>
<span class="bt-detail-stat-value" id="btDetailTxPower">--</span>
</div>
<div class="bt-detail-stat">
<span class="bt-detail-stat-label">Seen Rate</span>
<span class="bt-detail-stat-value" id="btDetailSeenRate">--</span>
</div>
<div class="bt-detail-stat">
<span class="bt-detail-stat-label">Stability</span>
<span class="bt-detail-stat-value" id="btDetailStability">--</span>
</div>
<div class="bt-detail-stat">
<span class="bt-detail-stat-label">Distance</span>
<span class="bt-detail-stat-value" id="btDetailDistance">--</span>
</div>
</div>
<!-- Service Data Inspector (collapsible) -->
<div class="bt-detail-service-inspector" id="btDetailServiceInspector" style="display:none;">
<div class="bt-inspector-toggle" onclick="BluetoothMode.toggleServiceInspector()">
<span class="bt-inspector-arrow" id="btInspectorArrow">&#9656;</span> Raw Data
</div>
<div class="bt-inspector-content" id="btInspectorContent" style="display:none;">
</div>
</div>
<div class="bt-detail-bottom-row">
<div class="bt-detail-irk" id="btDetailIrk" style="display: none;">
<span class="bt-irk-badge">IRK</span>
<span class="bt-detail-irk-value" id="btDetailIrkValue" style="font-size:10px;color:var(--text-dim);font-family:var(--font-mono);margin-left:6px;word-break:break-all;"></span>
</div>
<div class="bt-detail-services" id="btDetailServices" style="display: none;">
<span class="bt-detail-services-list" id="btDetailServicesList"></span>
</div>
@@ -2525,7 +2562,7 @@
<!-- Signal Scope -->
<div id="sstvScopePanel" style="display: none; margin-bottom: 12px;">
<div style="background: #0a0a0a; border: 1px solid #1e1a2e; border-radius: 6px; padding: 8px 10px; font-family: 'JetBrains Mono', 'Fira Code', monospace;">
<div style="background: #0a0a0a; border: 1px solid #1e1a2e; border-radius: 6px; padding: 8px 10px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
<span>Signal Scope</span>
<div style="display: flex; gap: 14px;">
@@ -2792,7 +2829,7 @@
<!-- Signal Scope -->
<div id="sstvGeneralScopePanel" style="display: none; margin-bottom: 12px;">
<div style="background: #0a0a0a; border: 1px solid #1e1a2e; border-radius: 6px; padding: 8px 10px; font-family: 'JetBrains Mono', 'Fira Code', monospace;">
<div style="background: #0a0a0a; border: 1px solid #1e1a2e; border-radius: 6px; padding: 8px 10px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
<span>Signal Scope</span>
<div style="display: flex; gap: 14px;">
@@ -2884,7 +2921,7 @@
<!-- Pager Signal Scope -->
<div id="pagerScopePanel" style="display: none; margin-bottom: 12px;">
<div style="background: #0a0a0a; border: 1px solid #1a1a2e; border-radius: 6px; padding: 8px 10px; font-family: 'JetBrains Mono', 'Fira Code', monospace;">
<div style="background: #0a0a0a; border: 1px solid #1a1a2e; border-radius: 6px; padding: 8px 10px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
<span>Signal Scope</span>
<div style="display: flex; gap: 14px;">
@@ -2902,7 +2939,7 @@
<!-- Sensor Signal Scope -->
<div id="sensorScopePanel" style="display: none; margin-bottom: 12px;">
<div style="background: #0a0a0a; border: 1px solid #1a2e1a; border-radius: 6px; padding: 8px 10px; font-family: 'JetBrains Mono', 'Fira Code', monospace;">
<div style="background: #0a0a0a; border: 1px solid #1a2e1a; border-radius: 6px; padding: 8px 10px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
<span>Signal Scope</span>
<div style="display: flex; gap: 14px;">
@@ -3686,6 +3723,8 @@
document.getElementById('dmrMode')?.classList.toggle('active', mode === 'dmr');
document.getElementById('websdrMode')?.classList.toggle('active', mode === 'websdr');
document.getElementById('subghzMode')?.classList.toggle('active', mode === 'subghz');
const pagerStats = document.getElementById('pagerStats');
const sensorStats = document.getElementById('sensorStats');
const satelliteStats = document.getElementById('satelliteStats');
@@ -3833,7 +3872,7 @@
// Show agent selector for modes that support remote agents
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', 'dsc'];
if (agentSection) agentSection.style.display = agentModes.includes(mode) ? 'block' : 'none';
// Show RTL-SDR device section for modes that use it
@@ -4942,8 +4981,10 @@
// SDR hardware capabilities
const sdrCapabilities = {
'rtlsdr': { name: 'RTL-SDR', freq_min: 24, freq_max: 1766, gain_min: 0, gain_max: 50 },
'sdrplay': { name: 'SDRplay', freq_min: 0.001, freq_max: 2000, gain_min: 0, gain_max: 59 },
'limesdr': { name: 'LimeSDR', freq_min: 0.1, freq_max: 3800, gain_min: 0, gain_max: 73 },
'hackrf': { name: 'HackRF', freq_min: 1, freq_max: 6000, gain_min: 0, gain_max: 62 }
'hackrf': { name: 'HackRF', freq_min: 1, freq_max: 6000, gain_min: 0, gain_max: 62 },
'airspy': { name: 'Airspy', freq_min: 24, freq_max: 1800, gain_min: 0, gain_max: 21 }
};
// Current device list with SDR type info
@@ -5811,7 +5852,7 @@
const infoEl = document.createElement('div');
infoEl.className = 'info-msg';
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "Space Mono", monospace; font-size: 11px; color: #888; word-break: break-all;';
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "Roboto Condensed", "Arial Narrow", sans-serif; font-size: 11px; color: #888; word-break: break-all;';
infoEl.textContent = text;
output.insertBefore(infoEl, output.firstChild);
}
@@ -5827,7 +5868,7 @@
const errorEl = document.createElement('div');
errorEl.className = 'error-msg';
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "Space Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;';
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "Roboto Condensed", "Arial Narrow", sans-serif; font-size: 11px; color: #ff6688; word-break: break-all;';
errorEl.textContent = '⚠ ' + text;
output.insertBefore(errorEl, output.firstChild);
}
@@ -8481,7 +8522,7 @@
// Draw total in center
ctx.fillStyle = '#fff';
ctx.font = 'bold 16px Space Mono';
ctx.font = 'bold 16px Roboto Condensed';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(total, cx, cy);
@@ -10170,6 +10211,20 @@
let gpsReconnectTimeout = null;
// GPS subscriber callbacks - modules can register to receive GPS stream data
const gpsStreamSubscribers = [];
function addGpsStreamSubscriber(fn) {
if (!gpsStreamSubscribers.includes(fn)) {
gpsStreamSubscribers.push(fn);
}
}
function removeGpsStreamSubscriber(fn) {
const idx = gpsStreamSubscribers.indexOf(fn);
if (idx !== -1) gpsStreamSubscribers.splice(idx, 1);
}
function startGpsStream() {
if (gpsEventSource) {
gpsEventSource.close();
@@ -10187,6 +10242,8 @@
gpsLastPosition = data;
updateLocationFromGps(data);
}
// Dispatch to all subscribers (e.g. GPS mode UI)
gpsStreamSubscribers.forEach(fn => fn(data));
} catch (e) {
console.error('GPS parse error:', e);
}
@@ -10285,7 +10342,7 @@
// Label
if (el > 0) {
ctx.fillStyle = '#444';
ctx.font = '10px Space Mono';
ctx.font = '10px Roboto Condensed';
ctx.textAlign = 'center';
ctx.fillText(el + '°', cx, cy - r + 12);
}
@@ -10358,7 +10415,7 @@
// Label
ctx.fillStyle = '#fff';
ctx.font = '11px Space Mono';
ctx.font = '11px Roboto Condensed';
ctx.fillText(pass.satellite, maxX + 10, maxY - 5);
}
}
@@ -15399,313 +15456,8 @@
<!-- Scanner/Audio code moved to static/js/modes/listening-post.js -->
<!-- Help Modal -->
<div id="helpModal" class="help-modal" onclick="if(event.target === this) hideHelp()">
<div class="help-content">
<button class="help-close" onclick="hideHelp()">×</button>
<h2>iNTERCEPT Help</h2>
{% include 'partials/help-modal.html' %}
<div class="help-tabs">
<button class="help-tab active" data-tab="icons" onclick="switchHelpTab('icons')">Icons</button>
<button class="help-tab" data-tab="modes" onclick="switchHelpTab('modes')">Modes</button>
<button class="help-tab" data-tab="wifi" onclick="switchHelpTab('wifi')">WiFi</button>
<button class="help-tab" data-tab="tips" onclick="switchHelpTab('tips')">Tips</button>
</div>
<!-- Icons Section -->
<div id="help-icons" class="help-section active">
<h3>Stats Bar Icons</h3>
<div class="icon-grid">
<div class="icon-item"><span class="icon">📟</span><span class="desc">POCSAG messages decoded</span>
</div>
<div class="icon-item"><span class="icon">📠</span><span class="desc">FLEX messages decoded</span>
</div>
<div class="icon-item"><span class="icon">📨</span><span class="desc">Total messages received</span>
</div>
<div class="icon-item"><span class="icon">🌡️</span><span class="desc">Unique sensors
detected</span></div>
<div class="icon-item"><span class="icon">📊</span><span class="desc">Device types found</span>
</div>
<div class="icon-item"><span class="icon">🛰️</span><span class="desc">Satellites monitored</span>
</div>
<div class="icon-item"><span class="icon">📡</span><span class="desc">WiFi Access Points</span>
</div>
<div class="icon-item"><span class="icon">👤</span><span class="desc">Connected WiFi clients</span>
</div>
<div class="icon-item"><span class="icon">🤝</span><span class="desc">Captured handshakes</span>
</div>
<div class="icon-item"><span class="icon">🚁</span><span class="desc">Detected drones (click for
details)</span></div>
<div class="icon-item"><span class="icon">⚠️</span><span class="desc">Rogue APs (click for
details)</span></div>
<div class="icon-item"><span class="icon">🔵</span><span class="desc">Bluetooth devices</span></div>
<div class="icon-item"><span class="icon">📍</span><span class="desc">BLE beacons / APRS
stations</span></div>
</div>
<h3>Mode Tab Icons</h3>
<div class="icon-grid">
<div class="icon-item"><span class="icon">📟</span><span class="desc">Pager - POCSAG/FLEX
decoder</span></div>
<div class="icon-item"><span class="icon">📡</span><span class="desc">433MHz - Sensor decoder</span>
</div>
<div class="icon-item"><span class="icon"></span><span class="desc">Meters - Utility meter decoder</span>
</div>
<div class="icon-item"><span class="icon">✈️</span><span class="desc">Aircraft - ADS-B tracking &amp; history</span></div>
<div class="icon-item"><span class="icon">🚢</span><span class="desc">Vessels - AIS &amp; VHF DSC distress</span></div>
<div class="icon-item"><span class="icon">📻</span><span class="desc">Spy Stations - Number stations database</span></div>
<div class="icon-item"><span class="icon">📍</span><span class="desc">APRS - Amateur radio
tracking</span></div>
<div class="icon-item"><span class="icon">🛰️</span><span class="desc">Satellite - Pass
prediction</span></div>
<div class="icon-item"><span class="icon">📶</span><span class="desc">WiFi - Network scanner</span>
</div>
<div class="icon-item"><span class="icon">🔵</span><span class="desc">Bluetooth - BT/BLE
scanner</span></div>
<div class="icon-item"><span class="icon">📻</span><span class="desc">Listening Post - SDR
scanner</span></div>
<div class="icon-item"><span class="icon">🔍</span><span class="desc">TSCM -
Counter-surveillance</span></div>
<div class="icon-item"><span class="icon">📺</span><span class="desc">ISS SSTV - Space station
images</span></div>
<div class="icon-item"><span class="icon">📺</span><span class="desc">HF SSTV - Terrestrial
SSTV images</span></div>
</div>
</div>
<!-- Modes Section -->
<div id="help-modes" class="help-section">
<h3>Pager Mode</h3>
<ul class="tip-list">
<li>Decodes POCSAG and FLEX pager signals using RTL-SDR</li>
<li>Set frequency to local pager frequencies (common: 152-158 MHz)</li>
<li>Messages are displayed in real-time as they're decoded</li>
<li>Use presets for common pager frequencies</li>
</ul>
<h3>433MHz Sensor Mode</h3>
<ul class="tip-list">
<li>Decodes wireless sensors on 433.92 MHz ISM band</li>
<li>Detects temperature, humidity, weather stations, tire pressure monitors</li>
<li>Supports many common protocols (Acurite, LaCrosse, Oregon Scientific, etc.)</li>
<li>Device intelligence builds profiles of recurring devices</li>
</ul>
<h3>Utility Meter Mode</h3>
<ul class="tip-list">
<li>Decodes utility meter transmissions (water, gas, electric) using rtlamr</li>
<li>Supports ERT protocol on 912 MHz (North America) or 868 MHz (Europe)</li>
<li>Displays meter IDs and consumption data in real-time</li>
<li>Supports SCM, SCM+, IDM, NetIDM, and R900 message types</li>
</ul>
<h3>Aircraft (Dashboard)</h3>
<ul class="tip-list">
<li>Opens the dedicated ADS-B Dashboard for aircraft tracking</li>
<li>Features radar scope, map view, airband audio, and ACARS decoding</li>
<li>Optional history mode persists data to Postgres for long-term analysis</li>
<li>Access history dashboard at <code>/adsb/history</code></li>
</ul>
<h3>Vessels (Dashboard)</h3>
<ul class="tip-list">
<li>Opens the AIS Dashboard for maritime vessel tracking</li>
<li>Displays vessel name, MMSI, callsign, destination, and navigation data</li>
<li><strong>VHF DSC Channel 70:</strong> Monitors maritime distress frequency (156.525 MHz)</li>
<li>Decodes DSC messages: Distress, Urgency, Safety, and Routine calls</li>
<li>MMSI country identification via Maritime Identification Digits (MID)</li>
<li>Visual alerts for DISTRESS and URGENCY messages with map markers</li>
</ul>
<h3>Spy Stations</h3>
<ul class="tip-list">
<li>Database of number stations and diplomatic HF networks</li>
<li>Browse stations from priyom.org with frequencies and schedules</li>
<li>Filter by type (number/diplomatic), country, and mode</li>
<li>Famous stations: UVB-76 "The Buzzer", Cuban HM01, Israeli E17z</li>
<li>Click "Tune" to listen via Listening Post mode</li>
</ul>
<h3>APRS Mode</h3>
<ul class="tip-list">
<li>Decodes APRS (Automatic Packet Reporting System) on VHF</li>
<li>Tracks amateur radio operators transmitting position data</li>
<li>Regional frequencies: 144.390 MHz (N. America), 144.800 MHz (Europe)</li>
<li>Uses Direwolf or multimon-ng for packet decoding</li>
<li>Interactive map shows station positions in real-time</li>
</ul>
<h3>Satellite Mode</h3>
<ul class="tip-list">
<li>Track satellites using TLE (Two-Line Element) data</li>
<li>Add satellites manually or fetch from Celestrak by category</li>
<li>Categories: Amateur, Weather, ISS, Starlink, GPS, and more</li>
<li>View next pass predictions with elevation and duration</li>
</ul>
<h3>WiFi Mode</h3>
<ul class="tip-list">
<li>Requires a WiFi adapter capable of monitor mode</li>
<li>Click "Enable Monitor" to put adapter in monitor mode</li>
<li>Scans all channels or lock to a specific channel</li>
<li>Detects drones by SSID patterns and manufacturer OUI</li>
<li>Rogue AP detection flags same SSID on multiple BSSIDs</li>
<li>Click network rows to target for deauth or handshake capture</li>
</ul>
<h3>Bluetooth Mode</h3>
<ul class="tip-list">
<li>Scans for classic Bluetooth and BLE devices</li>
<li>Shows device names, addresses, and signal strength</li>
<li>Manufacturer lookup from MAC address OUI</li>
<li>Radar visualization shows device proximity</li>
</ul>
<h3>Listening Post Mode</h3>
<ul class="tip-list">
<li>Wideband SDR scanner with spectrum visualization</li>
<li>Tune to any frequency supported by your SDR hardware</li>
<li>AM/FM/USB/LSB demodulation modes</li>
<li>Bookmark frequencies for quick recall</li>
<li>Quick tune presets for emergency and marine channels</li>
</ul>
<h3>TSCM Mode</h3>
<ul class="tip-list">
<li>Technical Surveillance Countermeasures sweep</li>
<li>Scans for unknown RF transmitters, WiFi devices, Bluetooth</li>
<li>Baseline comparison to detect new/anomalous devices</li>
<li>Threat classification: Critical, High, Medium, Low</li>
<li>Useful for security audits and bug sweeps</li>
<li><em style="color: var(--text-muted);">Note: This feature is in early development</em></li>
</ul>
<h3>Meshtastic Mode</h3>
<ul class="tip-list">
<li>Integrates with Meshtastic LoRa mesh network devices</li>
<li>Connect Heltec, T-Beam, RAK, or other compatible devices via USB</li>
<li>Real-time message streaming with RSSI and SNR metrics</li>
<li>Configure channels with encryption keys</li>
<li>View connected nodes and message history</li>
<li>Requires: Meshtastic device + <code>pip install meshtastic</code></li>
</ul>
<h3>ISS SSTV Mode</h3>
<ul class="tip-list">
<li>Decode Slow-Scan Television images from the International Space Station</li>
<li>ISS transmits on 145.800 MHz FM during special ARISS events</li>
<li>Real-time ISS tracking map with ground track overlay</li>
<li>Next-pass countdown with elevation and duration predictions</li>
<li>Optional Doppler shift compensation for improved reception</li>
<li>Requires: RTL-SDR (no external decoder needed - built-in Python SSTV decoder)</li>
</ul>
<h3>HF SSTV Mode</h3>
<ul class="tip-list">
<li>Decode terrestrial SSTV images on HF/VHF/UHF amateur radio frequencies</li>
<li>Predefined frequencies: 14.230 MHz USB (20m, most popular), 3.845/7.171 MHz LSB, and more</li>
<li>Supports USB, LSB, and FM demodulation modes</li>
<li>Auto-detects correct modulation when selecting a preset frequency</li>
<li>HF frequencies (below 30 MHz) require an upconverter with RTL-SDR</li>
<li>Requires: RTL-SDR (+ upconverter for HF, no external decoder needed)</li>
</ul>
</div>
<!-- WiFi Section -->
<div id="help-wifi" class="help-section">
<h3>Monitor Mode</h3>
<ul class="tip-list">
<li><strong>Enable Monitor:</strong> Puts WiFi adapter in monitor mode for passive scanning</li>
<li><strong>Kill Processes:</strong> Optional - stops NetworkManager/wpa_supplicant (may drop other
connections)</li>
<li>Some adapters rename when entering monitor mode (e.g., wlan0 → wlan0mon)</li>
</ul>
<h3>Handshake Capture</h3>
<ul class="tip-list">
<li>Click "Capture" on a network to start targeted handshake capture</li>
<li>Status panel shows capture progress and file location</li>
<li>Use deauth to force clients to reconnect (only on authorized networks!)</li>
<li>Handshake files saved to /tmp/intercept_handshake_*.cap</li>
</ul>
<h3>Drone Detection</h3>
<ul class="tip-list">
<li>Drones detected by SSID patterns (DJI, Parrot, Autel, etc.)</li>
<li>Also detected by manufacturer OUI in MAC address</li>
<li>Distance estimated from signal strength (approximate)</li>
<li>Click drone count in stats bar to see all detected drones</li>
</ul>
<h3>Rogue AP Detection</h3>
<ul class="tip-list">
<li>Flags networks where same SSID appears on multiple BSSIDs</li>
<li>Could indicate evil twin attack or legitimate multi-AP setup</li>
<li>Click rogue count to see which SSIDs are flagged</li>
</ul>
<h3>Proximity Alerts</h3>
<ul class="tip-list">
<li>Add MAC addresses to watch list for alerts when detected</li>
<li>Watch list persists in browser localStorage</li>
<li>Useful for tracking specific devices</li>
</ul>
<h3>Client Probe Analysis</h3>
<ul class="tip-list">
<li>Shows what networks client devices are looking for</li>
<li>Orange highlights indicate sensitive/private network names</li>
<li>Reveals user location history (home, work, hotels, airports)</li>
<li>Useful for security awareness and pen test reports</li>
</ul>
</div>
<!-- Tips Section -->
<div id="help-tips" class="help-section">
<h3>General Tips</h3>
<ul class="tip-list">
<li><strong>Collapsible sections:</strong> Click any section header (▼) to collapse/expand</li>
<li><strong>Sound alerts:</strong> Toggle sound on/off in settings for each mode</li>
<li><strong>Export data:</strong> Use export buttons to save captured data as JSON</li>
<li><strong>Device Intelligence:</strong> Tracks device patterns over time</li>
<li><strong>Theme toggle:</strong> Click the theme button in header to switch dark/light mode</li>
<li><strong>Settings:</strong> Click the gear icon in the header to access settings</li>
<li><strong>Offline mode:</strong> Enable in Settings to use local assets without internet</li>
</ul>
<h3>Keyboard Shortcuts</h3>
<ul class="tip-list">
<li><strong>F1</strong> - Open this help page</li>
<li><strong>?</strong> - Open help (when not typing in a field)</li>
<li><strong>Escape</strong> - Close help and modal dialogs</li>
</ul>
<h3>Requirements</h3>
<ul class="tip-list">
<li><strong>Pager:</strong> RTL-SDR, rtl_fm, multimon-ng</li>
<li><strong>433MHz Sensors:</strong> RTL-SDR, rtl_433</li>
<li><strong>Utility Meters:</strong> RTL-SDR, rtl_tcp, rtlamr</li>
<li><strong>Aircraft (ADS-B):</strong> RTL-SDR, dump1090 or rtl_adsb</li>
<li><strong>Aircraft (ACARS):</strong> Second RTL-SDR, acarsdec</li>
<li><strong>Vessels (AIS):</strong> RTL-SDR, AIS-catcher</li>
<li><strong>APRS:</strong> RTL-SDR, direwolf or multimon-ng</li>
<li><strong>Satellite:</strong> Internet for Celestrak (optional), skyfield</li>
<li><strong>WiFi:</strong> Monitor-mode adapter, aircrack-ng suite</li>
<li><strong>Bluetooth:</strong> Bluetooth adapter, bluez (hcitool/bluetoothctl)</li>
<li><strong>Listening Post:</strong> RTL-SDR or SoapySDR-compatible hardware</li>
<li><strong>TSCM:</strong> WiFi adapter, Bluetooth adapter, RTL-SDR (all optional)</li>
<li>Run as root/sudo for full hardware access</li>
</ul>
<h3>Legal Notice</h3>
<ul class="tip-list">
<li>Only use on networks and devices you own or have authorization to test</li>
<li>Passive monitoring may be legal; active attacks require authorization</li>
<li>Check local laws regarding radio frequency monitoring</li>
</ul>
</div>
</div>
</div>
<!-- Satellite Add Modal -->
<div id="satModal" class="help-modal" onclick="if(event.target === this) closeSatModal()">
+1 -1
View File
@@ -10,7 +10,7 @@
{% if offline_settings and offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
{# Core CSS (Design System) #}
+3 -3
View File
@@ -7,7 +7,7 @@
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/agents.css') }}">
@@ -18,8 +18,8 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
--font-sans: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--bg-primary: #0a0c10;
--bg-secondary: #0f1218;
--bg-tertiary: #151a23;
+124 -51
View File
@@ -20,35 +20,43 @@
<div id="help-icons" class="help-section active">
<h3>Stats Bar Icons</h3>
<div class="icon-grid">
<div class="icon-item"><span class="icon">&#128223;</span><span class="desc">POCSAG messages decoded</span></div>
<div class="icon-item"><span class="icon">&#128224;</span><span class="desc">FLEX messages decoded</span></div>
<div class="icon-item"><span class="icon">&#128232;</span><span class="desc">Total messages received</span></div>
<div class="icon-item"><span class="icon">&#127777;&#65039;</span><span class="desc">Unique sensors detected</span></div>
<div class="icon-item"><span class="icon">&#128202;</span><span class="desc">Device types found</span></div>
<div class="icon-item"><span class="icon">&#128752;&#65039;</span><span class="desc">Satellites monitored</span></div>
<div class="icon-item"><span class="icon">&#128225;</span><span class="desc">WiFi Access Points</span></div>
<div class="icon-item"><span class="icon">&#128100;</span><span class="desc">Connected WiFi clients</span></div>
<div class="icon-item"><span class="icon">&#129309;</span><span class="desc">Captured handshakes</span></div>
<div class="icon-item"><span class="icon">&#128641;</span><span class="desc">Detected drones (click for details)</span></div>
<div class="icon-item"><span class="icon">&#9888;&#65039;</span><span class="desc">Rogue APs (click for details)</span></div>
<div class="icon-item"><span class="icon">&#128309;</span><span class="desc">Bluetooth devices</span></div>
<div class="icon-item"><span class="icon">&#128205;</span><span class="desc">BLE beacons / APRS stations</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg></span><span class="desc">POCSAG messages decoded</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg></span><span class="desc">FLEX messages decoded</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg></span><span class="desc">Total messages received</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg></span><span class="desc">Unique sensors detected</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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></span><span class="desc">Device types found</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/></svg></span><span class="desc">Satellites monitored</span></div>
<div class="icon-item"><span class="icon icon--sm"><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></span><span class="desc">WiFi Access Points</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></span><span class="desc">Connected WiFi clients</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m11 17 2 2a1 1 0 1 0 3-3"/><path d="m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4"/><path d="m21 3 1 11h-2"/><path d="M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3"/><path d="M3 4h8"/></svg></span><span class="desc">Captured handshakes</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"/><path d="M3 9a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"/><path d="M17 9a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"/><path d="M3 15a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"/><path d="M17 15a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"/><path d="M9 9l-4 -1"/><path d="M15 9l4 -1"/><path d="M9 15l-4 1"/><path d="M15 15l4 1"/></svg></span><span class="desc">Detected drones (click for details)</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span><span class="desc">Rogue APs (click for details)</span></div>
<div class="icon-item"><span class="icon icon--sm"><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></span><span class="desc">Bluetooth devices</span></div>
<div class="icon-item"><span class="icon icon--sm"><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></span><span class="desc">BLE beacons / APRS stations</span></div>
</div>
<h3>Mode Tab Icons</h3>
<div class="icon-grid">
<div class="icon-item"><span class="icon">&#128223;</span><span class="desc">Pager - POCSAG/FLEX decoder</span></div>
<div class="icon-item"><span class="icon">&#128225;</span><span class="desc">433MHz - Sensor decoder</span></div>
<div class="icon-item"><span class="icon">&#9889;</span><span class="desc">Meters - Utility meter decoder</span></div>
<div class="icon-item"><span class="icon">&#9992;&#65039;</span><span class="desc">Aircraft - ADS-B tracking &amp; history</span></div>
<div class="icon-item"><span class="icon">&#128674;</span><span class="desc">Vessels - AIS &amp; VHF DSC distress</span></div>
<div class="icon-item"><span class="icon">&#128251;</span><span class="desc">Spy Stations - Number stations database</span></div>
<div class="icon-item"><span class="icon">&#128205;</span><span class="desc">APRS - Amateur radio tracking</span></div>
<div class="icon-item"><span class="icon">&#128752;&#65039;</span><span class="desc">Satellite - Pass prediction</span></div>
<div class="icon-item"><span class="icon">&#128246;</span><span class="desc">WiFi - Network scanner</span></div>
<div class="icon-item"><span class="icon">&#128309;</span><span class="desc">Bluetooth - BT/BLE scanner</span></div>
<div class="icon-item"><span class="icon">&#128251;</span><span class="desc">Listening Post - SDR scanner</span></div>
<div class="icon-item"><span class="icon">&#128269;</span><span class="desc">TSCM - Counter-surveillance</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg></span><span class="desc">Pager - POCSAG/FLEX decoder</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg></span><span class="desc">433MHz - Sensor decoder</span></div>
<div class="icon-item"><span class="icon icon--sm"><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></span><span class="desc">Meters - Utility meter decoder</span></div>
<div class="icon-item"><span class="icon icon--sm"><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></span><span class="desc">Aircraft - ADS-B tracking &amp; history</span></div>
<div class="icon-item"><span class="icon icon--sm"><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></span><span class="desc">Vessels - AIS &amp; VHF DSC distress</span></div>
<div class="icon-item"><span class="icon icon--sm"><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></span><span class="desc">APRS - Amateur radio tracking</span></div>
<div class="icon-item"><span class="icon icon--sm"><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></span><span class="desc">Listening Post - SDR scanner</span></div>
<div class="icon-item"><span class="icon icon--sm"><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"/><circle cx="12" cy="12" r="2"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg></span><span class="desc">Spy Stations - Number stations database</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg 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"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg></span><span class="desc">Meshtastic - LoRa mesh networking</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg 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"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span><span class="desc">WebSDR - Remote SDR receivers</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h6l3-9 3 18 3-9h5"/></svg></span><span class="desc">SubGHz - Sub-GHz signal analysis</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span><span class="desc">WiFi - Network scanner</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg></span><span class="desc">Bluetooth - BT/BLE scanner</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/><path d="M9.5 8.5l3 3 2-4-2 4-3 3"/></svg></span><span class="desc">BT Locate - Bluetooth device locator</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span><span class="desc">TSCM - Counter-surveillance</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg></span><span class="desc">Satellite - Pass prediction</span></div>
<div class="icon-item"><span class="icon icon--sm"><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"/><circle cx="12" cy="12" r="3"/><path d="M3 9h2"/><path d="M19 9h2"/><path d="M3 15h2"/><path d="M19 15h2"/></svg></span><span class="desc">ISS SSTV - Space station image decoder</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg 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"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span><span class="desc">Weather Sat - NOAA &amp; Meteor imagery</span></div>
<div class="icon-item"><span class="icon icon--sm"><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"/><circle cx="12" cy="12" r="3"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg></span><span class="desc">HF SSTV - Shortwave image decoder</span></div>
<div class="icon-item"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg></span><span class="desc">GPS - GNSS signal analysis</span></div>
</div>
</div>
@@ -96,15 +104,6 @@
<li>Visual alerts for DISTRESS and URGENCY messages with map markers</li>
</ul>
<h3>Spy Stations</h3>
<ul class="tip-list">
<li>Database of number stations and diplomatic HF networks</li>
<li>Browse stations from priyom.org with frequencies and schedules</li>
<li>Filter by type (number/diplomatic), country, and mode</li>
<li>Famous stations: UVB-76 "The Buzzer", Cuban HM01, Israeli E17z</li>
<li>Click "Tune" to listen via Listening Post mode</li>
</ul>
<h3>APRS Mode</h3>
<ul class="tip-list">
<li>Decodes APRS (Automatic Packet Reporting System) on VHF</li>
@@ -114,6 +113,50 @@
<li>Interactive map shows station positions in real-time</li>
</ul>
<h3>Listening Post Mode</h3>
<ul class="tip-list">
<li>Wideband SDR scanner with spectrum visualization</li>
<li>Tune to any frequency supported by your SDR hardware</li>
<li>AM/FM/USB/LSB demodulation modes</li>
<li>Bookmark frequencies for quick recall</li>
<li>Quick tune presets for emergency and marine channels</li>
</ul>
<h3>Spy Stations</h3>
<ul class="tip-list">
<li>Database of number stations and diplomatic HF networks</li>
<li>Browse stations from priyom.org with frequencies and schedules</li>
<li>Filter by type (number/diplomatic), country, and mode</li>
<li>Famous stations: UVB-76 "The Buzzer", Cuban HM01, Israeli E17z</li>
<li>Click "Tune" to listen via Listening Post mode</li>
</ul>
<h3>Meshtastic Mode</h3>
<ul class="tip-list">
<li>Integrates with Meshtastic LoRa mesh network devices</li>
<li>Connect Heltec, T-Beam, RAK, or other compatible devices via USB</li>
<li>Real-time message streaming with RSSI and SNR metrics</li>
<li>Configure channels with encryption keys</li>
<li>View connected nodes and message history</li>
<li>Requires: Meshtastic device + <code>pip install meshtastic</code></li>
</ul>
<h3>WebSDR Mode</h3>
<ul class="tip-list">
<li>Access remote WebSDR receivers around the world</li>
<li>Listen to shortwave, amateur, and broadcast stations without local SDR hardware</li>
<li>Browse available public WebSDR servers by location and frequency range</li>
<li>Useful for monitoring HF bands from different geographic locations</li>
</ul>
<h3>SubGHz Mode</h3>
<ul class="tip-list">
<li>Analyzes sub-GHz radio signals (common ISM bands: 315, 433, 868, 915 MHz)</li>
<li>Captures and decodes wireless remote controls, key fobs, and IoT devices</li>
<li>Protocol identification and signal analysis</li>
<li>Useful for RF security research and device testing</li>
</ul>
<h3>Satellite Mode</h3>
<ul class="tip-list">
<li>Track satellites using TLE (Two-Line Element) data</li>
@@ -122,6 +165,38 @@
<li>View next pass predictions with elevation and duration</li>
</ul>
<h3>ISS SSTV Mode</h3>
<ul class="tip-list">
<li>Decodes Slow Scan Television (SSTV) images from the International Space Station</li>
<li>Automated ISS pass tracking with Doppler correction on 145.800 MHz</li>
<li>Images decoded in real-time using slowrx</li>
<li>Gallery view with timestamped decoded images</li>
</ul>
<h3>Weather Sat Mode</h3>
<ul class="tip-list">
<li>Receives weather satellite imagery from NOAA APT and Meteor M2 LRPT</li>
<li>Uses SatDump for satellite signal processing and image decoding</li>
<li>Automated pass prediction and scheduling</li>
<li>Decoded images displayed in gallery with pass metadata</li>
</ul>
<h3>HF SSTV Mode</h3>
<ul class="tip-list">
<li>Decodes Slow Scan Television images from HF amateur radio bands</li>
<li>Common SSTV frequencies: 14.230 MHz (20m), 7.171 MHz (40m), 3.730 MHz (80m)</li>
<li>Supports multiple SSTV modes (Martin, Scottie, Robot, etc.)</li>
<li>Real-time image decoding with gallery view</li>
</ul>
<h3>GPS Mode</h3>
<ul class="tip-list">
<li>GNSS signal analysis and satellite constellation tracking</li>
<li>Displays GPS, GLONASS, Galileo, and BeiDou satellite positions</li>
<li>Signal strength visualization and fix quality metrics</li>
<li>Useful for evaluating GNSS reception and interference</li>
</ul>
<h3>WiFi Mode</h3>
<ul class="tip-list">
<li>Requires a WiFi adapter capable of monitor mode</li>
@@ -140,13 +215,12 @@
<li>Radar visualization shows device proximity</li>
</ul>
<h3>Listening Post Mode</h3>
<h3>BT Locate Mode</h3>
<ul class="tip-list">
<li>Wideband SDR scanner with spectrum visualization</li>
<li>Tune to any frequency supported by your SDR hardware</li>
<li>AM/FM/USB/LSB demodulation modes</li>
<li>Bookmark frequencies for quick recall</li>
<li>Quick tune presets for emergency and marine channels</li>
<li>Locate and track specific Bluetooth devices by signal strength</li>
<li>Directional signal strength indicator for physical device finding</li>
<li>Detects known tracker signatures (AirTag, Tile, SmartTag)</li>
<li>Useful for finding lost devices or detecting unwanted trackers</li>
</ul>
<h3>TSCM Mode</h3>
@@ -159,16 +233,6 @@
<li><em style="color: var(--text-muted);">Note: This feature is in early development</em></li>
</ul>
<h3>Meshtastic Mode</h3>
<ul class="tip-list">
<li>Integrates with Meshtastic LoRa mesh network devices</li>
<li>Connect Heltec, T-Beam, RAK, or other compatible devices via USB</li>
<li>Real-time message streaming with RSSI and SNR metrics</li>
<li>Configure channels with encryption keys</li>
<li>View connected nodes and message history</li>
<li>Requires: Meshtastic device + <code>pip install meshtastic</code></li>
</ul>
<h3>Network Monitor</h3>
<ul class="tip-list">
<li>Aggregates data from multiple remote INTERCEPT agents</li>
@@ -256,10 +320,19 @@
<li><strong>Aircraft (ACARS):</strong> Second RTL-SDR, acarsdec</li>
<li><strong>Vessels (AIS):</strong> RTL-SDR, AIS-catcher</li>
<li><strong>APRS:</strong> RTL-SDR, direwolf or multimon-ng</li>
<li><strong>Listening Post:</strong> RTL-SDR or SoapySDR-compatible hardware</li>
<li><strong>Spy Stations:</strong> Internet connection (database lookup)</li>
<li><strong>Meshtastic:</strong> Meshtastic LoRa device, <code>pip install meshtastic</code></li>
<li><strong>WebSDR:</strong> Internet connection (remote receivers)</li>
<li><strong>SubGHz:</strong> RTL-SDR or compatible SDR hardware</li>
<li><strong>Satellite:</strong> Internet for Celestrak (optional), skyfield</li>
<li><strong>ISS SSTV:</strong> RTL-SDR, slowrx</li>
<li><strong>Weather Sat:</strong> RTL-SDR, SatDump</li>
<li><strong>HF SSTV:</strong> RTL-SDR or SoapySDR-compatible hardware, slowrx</li>
<li><strong>GPS:</strong> RTL-SDR or GPS-capable SDR</li>
<li><strong>WiFi:</strong> Monitor-mode adapter, aircrack-ng suite</li>
<li><strong>Bluetooth:</strong> Bluetooth adapter, bluez (hcitool/bluetoothctl)</li>
<li><strong>Listening Post:</strong> RTL-SDR or SoapySDR-compatible hardware</li>
<li><strong>BT Locate:</strong> Bluetooth adapter, bluez</li>
<li><strong>TSCM:</strong> WiFi adapter, Bluetooth adapter, RTL-SDR (all optional)</li>
<li>Run as root/sudo for full hardware access</li>
</ul>
+208
View 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 &amp; 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) &mdash; 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>
+1
View File
@@ -26,6 +26,7 @@
<option value="bleak">Bleak Library</option>
<option value="hcitool">hcitool (Linux)</option>
<option value="bluetoothctl">bluetoothctl (Linux)</option>
<option value="ubertooth" id="btScanModeUbertooth" style="display:none;">Ubertooth One</option>
</select>
</div>
<div class="form-group">
+4 -4
View File
@@ -23,7 +23,7 @@
</div>
<div class="form-group" style="margin-top: 8px;">
<label>Device Serial <span style="color: var(--text-dim); font-weight: normal;">(optional)</span></label>
<input type="text" id="subghzDeviceSerial" placeholder="auto-detect" style="font-family: 'JetBrains Mono', monospace; font-size: 11px;">
<input type="text" id="subghzDeviceSerial" placeholder="auto-detect" style="font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; font-size: 11px;">
</div>
</div>
@@ -55,12 +55,12 @@
<div class="form-group">
<label>LNA Gain (0-40 dB)</label>
<input type="range" id="subghzLnaGain" min="0" max="40" value="24" step="8" oninput="document.getElementById('subghzLnaVal').textContent=this.value">
<span id="subghzLnaVal" style="font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text-secondary);">24</span>
<span id="subghzLnaVal" style="font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; font-size: 11px; color: var(--text-secondary);">24</span>
</div>
<div class="form-group">
<label>VGA Gain (0-62 dB)</label>
<input type="range" id="subghzVgaGain" min="0" max="62" value="20" step="2" oninput="document.getElementById('subghzVgaVal').textContent=this.value">
<span id="subghzVgaVal" style="font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text-secondary);">20</span>
<span id="subghzVgaVal" style="font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; font-size: 11px; color: var(--text-secondary);">20</span>
</div>
<div class="form-group">
<label>Sample Rate</label>
@@ -143,7 +143,7 @@
<div class="form-group">
<label>TX VGA Gain (0-47 dB)</label>
<input type="range" id="subghzTxGain" min="0" max="47" value="20" step="1" oninput="document.getElementById('subghzTxGainVal').textContent=this.value">
<span id="subghzTxGainVal" style="font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text-secondary);">20</span>
<span id="subghzTxGainVal" style="font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; font-size: 11px; color: var(--text-secondary);">20</span>
</div>
<div class="form-group">
<label>Max Duration (seconds)</label>
+228
View 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 &amp; 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) &mdash; 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>
@@ -54,7 +54,7 @@
<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;">V-Dipole (Easiest &mdash; ~$5)</strong>
<div style="margin: 8px 0; padding: 8px; background: var(--bg-tertiary); border-radius: 3px; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text-secondary); white-space: pre; line-height: 1.3; text-align: center;"> coax to SDR
<div style="margin: 8px 0; padding: 8px; background: var(--bg-tertiary); border-radius: 3px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; font-size: 10px; color: var(--text-secondary); white-space: pre; line-height: 1.3; text-align: center;"> coax to SDR
|
===+=== feed point
/ \
@@ -80,7 +80,7 @@
<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;">Turnstile / Crossed Dipole (~$10-15)</strong>
<div style="margin: 8px 0; padding: 8px; background: var(--bg-tertiary); border-radius: 3px; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text-secondary); white-space: pre; line-height: 1.3; text-align: center;"> 53.4cm
<div style="margin: 8px 0; padding: 8px; background: var(--bg-tertiary); border-radius: 3px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; font-size: 10px; color: var(--text-secondary); white-space: pre; line-height: 1.3; text-align: center;"> 53.4cm
&lt;---------&gt;
====+==== dipole 1
|
@@ -105,7 +105,7 @@
<div style="background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; margin-bottom: 10px;">
<strong style="color: #00ff88; font-size: 12px;">QFH &mdash; Quadrifilar Helix (Best &mdash; ~$20-30)</strong>
<div style="margin: 8px 0; padding: 8px; background: var(--bg-tertiary); border-radius: 3px; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text-secondary); white-space: pre; line-height: 1.3; text-align: center;"> ___
<div style="margin: 8px 0; padding: 8px; background: var(--bg-tertiary); border-radius: 3px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; font-size: 10px; color: var(--text-secondary); white-space: pre; line-height: 1.3; text-align: center;"> ___
/ \ two helix loops
| | | twisted 90 deg
| | | around a mast
@@ -200,7 +200,7 @@
</div>
<div class="form-group">
<label>File Path (server-side)</label>
<input type="text" id="wxsatTestFilePath" value="data/weather_sat/samples/noaa_apt_argentina.wav" style="font-family: 'JetBrains Mono', monospace; font-size: 11px;">
<input type="text" id="wxsatTestFilePath" value="data/weather_sat/samples/noaa_apt_argentina.wav" style="font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; font-size: 11px;">
</div>
<div class="form-group">
<label>Sample Rate</label>
@@ -230,7 +230,7 @@
Enable Auto-Capture
</label>
</div>
<div id="wxsatSchedulerStatus" style="font-size: 11px; color: var(--text-dim); font-family: 'JetBrains Mono', monospace; margin-top: 4px;">
<div id="wxsatSchedulerStatus" style="font-size: 11px; color: var(--text-dim); font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; margin-top: 4px;">
Disabled
</div>
</div>
+4
View File
@@ -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('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('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('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('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('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('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>') }}
+2 -2
View File
@@ -54,7 +54,7 @@
<div class="settings-row">
<div class="settings-label">
<span class="settings-label-text">Web Fonts</span>
<span class="settings-label-desc">Space Mono</span>
<span class="settings-label-desc">Roboto Condensed</span>
</div>
<select id="fontsSource" class="settings-select" onchange="Settings.setFontsSource(this.value)">
<option value="cdn">Google Fonts (Online)</option>
@@ -108,7 +108,7 @@
<span class="asset-badge checking" id="statusInter">Checking...</span>
</div>
<div class="asset-status-row">
<span class="asset-name">Space Mono</span>
<span class="asset-name">Roboto Condensed</span>
<span class="asset-badge checking" id="statusJetbrains">Checking...</span>
</div>
</div>
+5 -5
View File
@@ -8,7 +8,7 @@
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
<!-- Leaflet.js - Conditional CDN/Local loading -->
{% if offline_settings.assets_source == 'local' %}
@@ -622,7 +622,7 @@
ctx.stroke();
ctx.fillStyle = 'rgba(0, 212, 255, 0.4)';
ctx.font = '10px Space Mono';
ctx.font = '10px Roboto Condensed';
ctx.fillText(el + '°', cx + 5, cy - r + 12);
}
@@ -990,7 +990,7 @@
ctx.stroke();
ctx.fillStyle = 'rgba(0, 212, 255, 0.4)';
ctx.font = '10px Space Mono';
ctx.font = '10px Roboto Condensed';
ctx.fillText(elRing + '°', cx + 5, cy - r + 12);
}
@@ -1057,7 +1057,7 @@
ctx.textAlign = 'center';
ctx.fillText(satellites[selectedSatellite]?.name || 'SAT', x, y - 20);
ctx.font = '10px Space Mono';
ctx.font = '10px Roboto Condensed';
ctx.fillStyle = el > 0 ? '#00ff88' : '#ff4444';
ctx.fillText(el.toFixed(1) + '°', x, y + 25);
} else {
@@ -1106,7 +1106,7 @@
ctx.textAlign = 'center';
ctx.fillText(satellites[selectedSatellite]?.name || 'SAT', x, y - 20);
ctx.font = '10px Space Mono';
ctx.font = '10px Roboto Condensed';
ctx.fillStyle = el > 0 ? '#00ff88' : '#ff4444';
ctx.fillText(el.toFixed(1) + '°', x, y + 25);
}
+6
View File
@@ -606,6 +606,12 @@ class DeviceAggregator:
return result
def get_fingerprint_mac_count(self, fingerprint_id: str) -> int:
"""Return how many distinct device_ids share a fingerprint."""
with self._lock:
device_ids = self._fingerprint_to_devices.get(fingerprint_id)
return len(device_ids) if device_ids else 0
def prune_ring_buffer(self) -> int:
"""Prune old observations from ring buffer."""
return self._ring_buffer.prune_old()
+57
View File
@@ -101,6 +101,7 @@ ADDRESS_TYPE_RANDOM = 'random'
ADDRESS_TYPE_RANDOM_STATIC = 'random_static'
ADDRESS_TYPE_RPA = 'rpa' # Resolvable Private Address
ADDRESS_TYPE_NRPA = 'nrpa' # Non-Resolvable Private Address
ADDRESS_TYPE_UUID = 'uuid' # CoreBluetooth platform UUID (macOS, no real MAC available)
# =============================================================================
# PROTOCOL TYPES
@@ -278,3 +279,59 @@ MINOR_WEARABLE = {
0x04: 'Helmet',
0x05: 'Glasses',
}
# =============================================================================
# BLE APPEARANCE CODES (GAP Appearance values)
# =============================================================================
BLE_APPEARANCE_NAMES: dict[int, str] = {
0: 'Unknown',
64: 'Phone',
128: 'Computer',
192: 'Watch',
193: 'Sports Watch',
256: 'Clock',
320: 'Display',
384: 'Remote Control',
448: 'Eye Glasses',
512: 'Tag',
576: 'Keyring',
640: 'Media Player',
704: 'Barcode Scanner',
768: 'Thermometer',
832: 'Heart Rate Sensor',
896: 'Blood Pressure',
960: 'HID',
961: 'Keyboard',
962: 'Mouse',
963: 'Joystick',
964: 'Gamepad',
965: 'Digitizer Tablet',
966: 'Card Reader',
967: 'Digital Pen',
968: 'Barcode Scanner (HID)',
1024: 'Glucose Monitor',
1088: 'Running Speed Sensor',
1152: 'Cycling',
1216: 'Control Device',
1280: 'Network Device',
1344: 'Sensor',
1408: 'Light Fixture',
1472: 'Fan',
1536: 'HVAC',
1600: 'Access Control',
1664: 'Motorized Device',
1728: 'Power Device',
1792: 'Light Source',
3136: 'Pulse Oximeter',
3200: 'Weight Scale',
3264: 'Personal Mobility',
5184: 'Outdoor Sports Activity',
}
def get_appearance_name(code: int | None) -> str | None:
"""Look up a human-readable name for a BLE appearance code."""
if code is None:
return None
return BLE_APPEARANCE_NAMES.get(code)
+7 -2
View File
@@ -12,6 +12,7 @@ from typing import Optional
from .constants import (
ADDRESS_TYPE_PUBLIC,
ADDRESS_TYPE_RANDOM_STATIC,
ADDRESS_TYPE_UUID,
)
@@ -46,10 +47,14 @@ def generate_device_key(
if identity_address:
return f"id:{identity_address.upper()}"
# Priority 2: Use public or random_static addresses directly
# Priority 2: Use public or random_static addresses directly (not platform UUIDs)
if address_type in (ADDRESS_TYPE_PUBLIC, ADDRESS_TYPE_RANDOM_STATIC):
return f"mac:{address.upper()}"
# Priority 2b: CoreBluetooth UUIDs are stable per-system, use as identifier
if address_type == ADDRESS_TYPE_UUID:
return f"uuid:{address.upper()}"
# Priority 3: Generate fingerprint hash for random addresses
return _generate_fingerprint_key(address, name, manufacturer_id, service_uuids)
@@ -102,7 +107,7 @@ def is_randomized_mac(address_type: str) -> bool:
Returns:
True if the address is randomized, False otherwise.
"""
return address_type not in (ADDRESS_TYPE_PUBLIC, ADDRESS_TYPE_RANDOM_STATIC)
return address_type not in (ADDRESS_TYPE_PUBLIC, ADDRESS_TYPE_RANDOM_STATIC, ADDRESS_TYPE_UUID)
def extract_key_type(device_key: str) -> str:
+8 -1
View File
@@ -24,8 +24,12 @@ from .constants import (
BLUETOOTHCTL_TIMEOUT,
ADDRESS_TYPE_PUBLIC,
ADDRESS_TYPE_RANDOM,
ADDRESS_TYPE_UUID,
MANUFACTURER_NAMES,
)
# CoreBluetooth UUID pattern: 8-4-4-4-12 hex digits
_CB_UUID_RE = re.compile(r'^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$')
from .models import BTObservation
logger = logging.getLogger(__name__)
@@ -132,7 +136,10 @@ class BleakScanner:
"""Convert bleak device to BTObservation."""
# Determine address type from address format
address_type = ADDRESS_TYPE_PUBLIC
if device.address and ':' in device.address:
if device.address and _CB_UUID_RE.match(device.address):
# macOS CoreBluetooth returns a platform UUID instead of a real MAC
address_type = ADDRESS_TYPE_UUID
elif device.address and ':' in device.address:
# Check if first byte indicates random address
first_byte = int(device.address.split(':')[0], 16)
if (first_byte & 0xC0) == 0xC0: # Random static
+45 -13
View File
@@ -18,6 +18,7 @@ from .constants import (
RANGE_UNKNOWN,
PROTOCOL_BLE,
PROXIMITY_UNKNOWN,
get_appearance_name,
)
# Import tracker types (will be available after tracker_signatures module loads)
@@ -148,10 +149,10 @@ class BTDeviceAggregate:
is_strong_stable: bool = False
has_random_address: bool = False
# Baseline tracking
in_baseline: bool = False
baseline_id: Optional[int] = None
seen_before: bool = False
# Baseline tracking
in_baseline: bool = False
baseline_id: Optional[int] = None
seen_before: bool = False
# Tracker detection fields
is_tracker: bool = False
@@ -165,6 +166,10 @@ class BTDeviceAggregate:
risk_score: float = 0.0 # 0.0 to 1.0
risk_factors: list[str] = field(default_factory=list)
# IRK (Identity Resolving Key) from paired device database
irk_hex: Optional[str] = None # 32-char hex if known
irk_source_name: Optional[str] = None # Name from paired DB
# Payload fingerprint (survives MAC randomization)
payload_fingerprint_id: Optional[str] = None
payload_fingerprint_stability: float = 0.0
@@ -275,10 +280,10 @@ class BTDeviceAggregate:
},
'heuristic_flags': self.heuristic_flags,
# Baseline
'in_baseline': self.in_baseline,
'baseline_id': self.baseline_id,
'seen_before': self.seen_before,
# Baseline
'in_baseline': self.in_baseline,
'baseline_id': self.baseline_id,
'seen_before': self.seen_before,
# Tracker detection
'tracker': {
@@ -296,6 +301,11 @@ class BTDeviceAggregate:
'risk_factors': self.risk_factors,
},
# IRK
'has_irk': self.irk_hex is not None,
'irk_hex': self.irk_hex,
'irk_source_name': self.irk_source_name,
# Fingerprint
'fingerprint': {
'id': self.payload_fingerprint_id,
@@ -319,24 +329,46 @@ class BTDeviceAggregate:
'rssi_current': self.rssi_current,
'rssi_median': round(self.rssi_median, 1) if self.rssi_median else None,
'rssi_ema': round(self.rssi_ema, 1) if self.rssi_ema else None,
'rssi_min': self.rssi_min,
'rssi_max': self.rssi_max,
'rssi_variance': round(self.rssi_variance, 2) if self.rssi_variance else None,
'range_band': self.range_band,
'proximity_band': self.proximity_band,
'estimated_distance_m': round(self.estimated_distance_m, 2) if self.estimated_distance_m else None,
'distance_confidence': round(self.distance_confidence, 2),
'is_randomized_mac': self.is_randomized_mac,
'last_seen': self.last_seen.isoformat(),
'first_seen': self.first_seen.isoformat(),
'age_seconds': self.age_seconds,
'duration_seconds': self.duration_seconds,
'seen_count': self.seen_count,
'heuristic_flags': self.heuristic_flags,
'in_baseline': self.in_baseline,
'seen_before': self.seen_before,
# Tracker info for list view
'is_tracker': self.is_tracker,
'seen_rate': round(self.seen_rate, 2),
'tx_power': self.tx_power,
'manufacturer_id': self.manufacturer_id,
'appearance': self.appearance,
'appearance_name': get_appearance_name(self.appearance),
'is_connectable': self.is_connectable,
'service_uuids': self.service_uuids,
'service_data': {k: v.hex() for k, v in self.service_data.items()},
'manufacturer_bytes': self.manufacturer_bytes.hex() if self.manufacturer_bytes else None,
'heuristic_flags': self.heuristic_flags,
'is_persistent': self.is_persistent,
'is_beacon_like': self.is_beacon_like,
'is_strong_stable': self.is_strong_stable,
'in_baseline': self.in_baseline,
'seen_before': self.seen_before,
# Tracker info for list view
'is_tracker': self.is_tracker,
'tracker_type': self.tracker_type,
'tracker_name': self.tracker_name,
'tracker_confidence': self.tracker_confidence,
'tracker_confidence_score': round(self.tracker_confidence_score, 2),
'tracker_evidence': self.tracker_evidence,
'risk_score': round(self.risk_score, 2),
'risk_factors': self.risk_factors,
'has_irk': self.irk_hex is not None,
'irk_hex': self.irk_hex,
'irk_source_name': self.irk_source_name,
'fingerprint_id': self.payload_fingerprint_id,
}
+76 -1
View File
@@ -24,7 +24,9 @@ from .constants import (
)
from .dbus_scanner import DBusScanner
from .fallback_scanner import FallbackScanner
from .ubertooth_scanner import UbertoothScanner
from .heuristics import HeuristicsEngine
from .irk_extractor import get_paired_irks
from .models import BTDeviceAggregate, BTObservation, ScanStatus, SystemCapabilities
logger = logging.getLogger(__name__)
@@ -57,6 +59,7 @@ class BluetoothScanner:
# Scanner backends
self._dbus_scanner: Optional[DBusScanner] = None
self._fallback_scanner: Optional[FallbackScanner] = None
self._ubertooth_scanner: Optional[UbertoothScanner] = None
self._active_backend: Optional[str] = None
# Event queue for SSE streaming
@@ -113,6 +116,8 @@ class BluetoothScanner:
if mode == 'dbus':
started, backend_used = self._start_dbus(adapter, transport, rssi_threshold)
elif mode == 'ubertooth':
started, backend_used = self._start_ubertooth()
# Fallback: try non-DBus methods if DBus failed or wasn't requested
if not started and (original_mode == 'auto' or mode in ('bleak', 'hcitool', 'bluetoothctl')):
@@ -168,6 +173,18 @@ class BluetoothScanner:
logger.warning(f"DBus scanner failed: {e}")
return False, None
def _start_ubertooth(self) -> tuple[bool, Optional[str]]:
"""Start Ubertooth One scanner."""
try:
self._ubertooth_scanner = UbertoothScanner(
on_observation=self._handle_observation,
)
if self._ubertooth_scanner.start():
return True, 'ubertooth'
except Exception as e:
logger.warning(f"Ubertooth scanner failed: {e}")
return False, None
def _start_fallback(self, adapter: str, preferred: str) -> tuple[bool, Optional[str]]:
"""Start fallback scanner."""
try:
@@ -204,6 +221,10 @@ class BluetoothScanner:
self._fallback_scanner.stop()
self._fallback_scanner = None
if self._ubertooth_scanner:
self._ubertooth_scanner.stop()
self._ubertooth_scanner = None
# Update status
self._status.is_scanning = False
self._active_backend = None
@@ -216,6 +237,47 @@ class BluetoothScanner:
logger.info("Bluetooth scan stopped")
def _match_irk(self, device: BTDeviceAggregate) -> None:
"""Check if a device address resolves against any paired IRK."""
if device.irk_hex is not None:
return # Already matched
address = device.address
if not address or len(address.replace(':', '').replace('-', '')) not in (12, 32):
return
# Only attempt RPA resolution on 6-byte addresses
addr_clean = address.replace(':', '').replace('-', '')
if len(addr_clean) != 12:
return
try:
paired = get_paired_irks()
except Exception:
return
if not paired:
return
try:
from utils.bt_locate import resolve_rpa
except ImportError:
return
for entry in paired:
irk_hex = entry.get('irk_hex', '')
if not irk_hex or len(irk_hex) != 32:
continue
try:
irk = bytes.fromhex(irk_hex)
if resolve_rpa(irk, address):
device.irk_hex = irk_hex
device.irk_source_name = entry.get('name')
logger.debug(f"IRK match for {address}: {entry.get('name', 'unnamed')}")
return
except Exception:
continue
def _handle_observation(self, observation: BTObservation) -> None:
"""Handle incoming observation from scanner backend."""
try:
@@ -225,15 +287,27 @@ class BluetoothScanner:
# Evaluate heuristics
self._heuristics.evaluate(device)
# Check for IRK match
self._match_irk(device)
# Update device count
with self._lock:
self._status.devices_found = self._aggregator.device_count
# Build summary with MAC cluster count
summary = device.to_summary_dict()
if device.payload_fingerprint_id:
summary['mac_cluster_count'] = self._aggregator.get_fingerprint_mac_count(
device.payload_fingerprint_id
)
else:
summary['mac_cluster_count'] = 0
# Queue event
self._queue_event({
'type': 'device',
'action': 'update',
'device': device.to_summary_dict(),
'device': summary,
})
# Callbacks
@@ -398,6 +472,7 @@ class BluetoothScanner:
backend_alive = (
(self._dbus_scanner and self._dbus_scanner.is_scanning)
or (self._fallback_scanner and self._fallback_scanner.is_scanning)
or (self._ubertooth_scanner and self._ubertooth_scanner.is_scanning)
)
if not backend_alive:
self._status.is_scanning = False
+207 -13
View File
@@ -318,6 +318,8 @@ class GPSDClient:
except json.JSONDecodeError:
logger.debug(f"Invalid JSON from gpsd: {line[:50]}")
except Exception as parse_err:
logger.error(f"Error handling gpsd {msg_class} message: {parse_err}")
except socket.timeout:
continue
@@ -371,19 +373,33 @@ class GPSDClient:
self._update_position(position)
def _handle_sky(self, msg: dict) -> None:
"""Handle SKY (satellite sky view) message from gpsd."""
sats = []
for sat in msg.get('satellites', []):
prn = sat.get('PRN', 0)
gnssid = sat.get('gnssid')
sats.append(GPSSatellite(
prn=prn,
elevation=sat.get('el'),
azimuth=sat.get('az'),
snr=sat.get('ss'),
used=sat.get('used', False),
constellation=_classify_constellation(prn, gnssid),
))
"""Handle SKY (satellite sky view) message from gpsd.
gpsd sends multiple SKY messages per cycle: some contain only DOP
values while others include the full satellites array. When a
DOP-only SKY arrives, preserve the most recent satellite list
instead of overwriting it with an empty one.
"""
raw_sats = msg.get('satellites', [])
has_satellites = len(raw_sats) > 0
if has_satellites:
sats = []
for sat in raw_sats:
prn = sat.get('PRN', 0)
gnssid = sat.get('gnssid')
sats.append(GPSSatellite(
prn=prn,
elevation=sat.get('el'),
azimuth=sat.get('az'),
snr=sat.get('ss'),
used=sat.get('used', False),
constellation=_classify_constellation(prn, gnssid),
))
else:
# DOP-only SKY message — keep existing satellites
with self._lock:
sats = list(self._sky.satellites) if self._sky else []
sky_data = GPSSkyData(
satellites=sats,
@@ -483,3 +499,181 @@ def get_current_position() -> GPSPosition | None:
if client:
return client.position
return None
# ============================================
# GPS device detection and gpsd auto-start
# ============================================
_gpsd_process: 'subprocess.Popen | None' = None
_gpsd_process_lock = threading.RLock()
def detect_gps_devices() -> list[dict]:
"""
Detect connected GPS serial devices.
Returns list of dicts with 'path' and 'description' keys.
"""
import glob
import os
import platform
devices: list[dict] = []
system = platform.system()
if system == 'Linux':
# Common USB GPS device paths
patterns = ['/dev/ttyUSB*', '/dev/ttyACM*']
for pattern in patterns:
for path in sorted(glob.glob(pattern)):
desc = _describe_device_linux(path)
devices.append({'path': path, 'description': desc})
# Also check /dev/serial/by-id for descriptive names
serial_dir = '/dev/serial/by-id'
if os.path.isdir(serial_dir):
for name in sorted(os.listdir(serial_dir)):
full = os.path.join(serial_dir, name)
real = os.path.realpath(full)
# Skip if we already found this device
if any(d['path'] == real for d in devices):
# Update description with the more descriptive name
for d in devices:
if d['path'] == real:
d['description'] = name
continue
devices.append({'path': real, 'description': name})
elif system == 'Darwin':
# macOS: USB serial devices (prefer cu. over tty. for outgoing)
patterns = ['/dev/cu.usbmodem*', '/dev/cu.usbserial*']
for pattern in patterns:
for path in sorted(glob.glob(pattern)):
desc = _describe_device_macos(path)
devices.append({'path': path, 'description': desc})
# Sort: devices with GPS-related descriptions first
gps_keywords = ('gps', 'gnss', 'u-blox', 'ublox', 'nmea', 'sirf', 'navigation')
devices.sort(key=lambda d: (
0 if any(k in d['description'].lower() for k in gps_keywords) else 1
))
return devices
def _describe_device_linux(path: str) -> str:
"""Get a human-readable description of a Linux serial device."""
import os
basename = os.path.basename(path)
# Try to read from sysfs
try:
# /sys/class/tty/ttyUSB0/device/../product
sysfs = f'/sys/class/tty/{basename}/device/../product'
if os.path.exists(sysfs):
with open(sysfs) as f:
return f.read().strip()
except Exception:
pass
return basename
def _describe_device_macos(path: str) -> str:
"""Get a description of a macOS serial device."""
import os
return os.path.basename(path)
def is_gpsd_running(host: str = 'localhost', port: int = 2947) -> bool:
"""Check if gpsd is reachable."""
import socket
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1.0)
sock.connect((host, port))
sock.close()
return True
except Exception:
return False
def start_gpsd_daemon(device_path: str, host: str = 'localhost',
port: int = 2947) -> tuple[bool, str]:
"""
Start gpsd daemon pointing at the given device.
Returns (success, message) tuple.
"""
import shutil
import subprocess
global _gpsd_process
with _gpsd_process_lock:
# Already running?
if is_gpsd_running(host, port):
return True, 'gpsd already running'
gpsd_bin = shutil.which('gpsd')
if not gpsd_bin:
return False, 'gpsd not installed'
# Stop any existing managed process
stop_gpsd_daemon()
try:
import os
if not os.path.exists(device_path):
return False, f'Device {device_path} not found'
cmd = [gpsd_bin, '-N', '-n', '-S', str(port), device_path]
logger.info(f"Starting gpsd: {' '.join(cmd)}")
print(f"[GPS] Starting gpsd: {' '.join(cmd)}", flush=True)
_gpsd_process = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
# Give gpsd a moment to start
import time
time.sleep(1.5)
if _gpsd_process.poll() is not None:
stderr = ''
if _gpsd_process.stderr:
stderr = _gpsd_process.stderr.read().decode('utf-8', errors='ignore').strip()
msg = f'gpsd exited with code {_gpsd_process.returncode}'
if stderr:
msg += f': {stderr}'
return False, msg
# Verify it's listening
if is_gpsd_running(host, port):
return True, f'gpsd started on {device_path}'
else:
return False, 'gpsd started but not accepting connections'
except Exception as e:
logger.error(f"Failed to start gpsd: {e}")
return False, str(e)
def stop_gpsd_daemon() -> None:
"""Stop the managed gpsd daemon process."""
global _gpsd_process
with _gpsd_process_lock:
if _gpsd_process and _gpsd_process.poll() is None:
try:
_gpsd_process.terminate()
_gpsd_process.wait(timeout=3.0)
except Exception:
try:
_gpsd_process.kill()
except Exception:
pass
logger.info("Stopped gpsd daemon")
print("[GPS] Stopped gpsd daemon", flush=True)
_gpsd_process = None
+90
View File
@@ -2,11 +2,14 @@ from __future__ import annotations
import atexit
import logging
import os
import platform
import signal
import subprocess
import re
import threading
import time
from pathlib import Path
from typing import Any, Callable
from .dependencies import check_tool
@@ -117,6 +120,93 @@ def cleanup_stale_processes() -> None:
pass
_DUMP1090_PID_FILE = Path(__file__).resolve().parent.parent / 'instance' / 'dump1090.pid'
def write_dump1090_pid(pid: int) -> None:
"""Write the PID of an app-spawned dump1090 process to a PID file."""
try:
_DUMP1090_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
_DUMP1090_PID_FILE.write_text(str(pid))
logger.debug(f"Wrote dump1090 PID file: {pid}")
except OSError as e:
logger.warning(f"Failed to write dump1090 PID file: {e}")
def clear_dump1090_pid() -> None:
"""Remove the dump1090 PID file."""
try:
_DUMP1090_PID_FILE.unlink(missing_ok=True)
logger.debug("Cleared dump1090 PID file")
except OSError as e:
logger.warning(f"Failed to clear dump1090 PID file: {e}")
def _is_dump1090_process(pid: int) -> bool:
"""Check if the given PID is actually a dump1090/readsb process."""
try:
if platform.system() == 'Linux':
cmdline_path = Path(f'/proc/{pid}/cmdline')
if cmdline_path.exists():
cmdline = cmdline_path.read_bytes().replace(b'\x00', b' ').decode('utf-8', errors='ignore')
return 'dump1090' in cmdline or 'readsb' in cmdline
# macOS or fallback
result = subprocess.run(
['ps', '-p', str(pid), '-o', 'comm='],
capture_output=True, text=True, timeout=5
)
comm = result.stdout.strip()
return 'dump1090' in comm or 'readsb' in comm
except Exception:
return False
def cleanup_stale_dump1090() -> None:
"""Kill a stale app-spawned dump1090 using the PID file.
Safe no-op if no PID file exists, process is dead, or PID was reused
by another program.
"""
if not _DUMP1090_PID_FILE.exists():
return
try:
pid = int(_DUMP1090_PID_FILE.read_text().strip())
except (ValueError, OSError) as e:
logger.warning(f"Invalid dump1090 PID file: {e}")
clear_dump1090_pid()
return
# Verify this PID is still a dump1090/readsb process
if not _is_dump1090_process(pid):
logger.debug(f"PID {pid} is not dump1090/readsb (dead or reused), removing stale PID file")
clear_dump1090_pid()
return
# Kill the process group
logger.info(f"Killing stale app-spawned dump1090 (PID {pid})")
try:
pgid = os.getpgid(pid)
os.killpg(pgid, signal.SIGTERM)
# Brief wait for graceful shutdown
for _ in range(10):
try:
os.kill(pid, 0) # Check if still alive
time.sleep(0.2)
except OSError:
break
else:
# Still alive, force kill
try:
os.killpg(pgid, signal.SIGKILL)
except OSError:
pass
except OSError as e:
logger.debug(f"Error killing stale dump1090 PID {pid}: {e}")
clear_dump1090_pid()
def is_valid_mac(mac: str | None) -> bool:
"""Validate MAC address format."""
if not mac: