mirror of
https://github.com/smittix/intercept.git
synced 2026-07-24 16:58:12 -07:00
Release v2.22.0
Waterfall overhaul, new modes (fingerprint, RF heatmap, SignalID, voice alerts), PWA support, mode stop responsiveness improvements, ADS-B MSG2 surface tracking, WebSDR overhaul, and full documentation audit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,47 @@
|
|||||||
|
|
||||||
All notable changes to iNTERCEPT will be documented in this file.
|
All notable changes to iNTERCEPT will be documented in this file.
|
||||||
|
|
||||||
|
## [2.22.0] - 2026-02-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Waterfall Receiver Overhaul** - WebSocket-based I/Q streaming with server-side FFT, click-to-tune, zoom controls, and auto-scaling
|
||||||
|
- **Voice Alerts** - Configurable text-to-speech event notifications across modes
|
||||||
|
- **Signal Fingerprinting** - RF device identification and pattern analysis mode
|
||||||
|
- **RF Heatmap** - Geographic signal density visualization with Leaflet heatmap overlay
|
||||||
|
- **SignalID** - Automatic signal classification via SigIDWiki API integration
|
||||||
|
- **PWA Support** - Installable web app with service worker caching and manifest
|
||||||
|
- **Real-time Signal Scope** - Live signal visualization for pager, sensor, and SSTV modes
|
||||||
|
- **ADS-B MSG2 Surface Parsing** - Ground vehicle movement tracking from MSG2 frames
|
||||||
|
- **Cheat Sheets** - Quick reference overlays for keyboard shortcuts and mode controls
|
||||||
|
- App icon (SVG) for PWA and browser tab
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **WebSDR overhaul** - Improved receiver management, audio streaming, and UI
|
||||||
|
- **Mode stop responsiveness** - Faster timeout handling and improved WiFi/Bluetooth scanner shutdown
|
||||||
|
- **Mode transitions** - Smoother navigation with performance instrumentation
|
||||||
|
- **BT Locate** - Refactored JS engine with improved trail management and signal smoothing
|
||||||
|
- **Listening Post** - Refactored with cross-module frequency routing
|
||||||
|
- **SSTV decoder** - State machine improvements and partial image streaming
|
||||||
|
- Analytics mode removed; per-mode analytics panels integrated into existing dashboards
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- ADS-B SSE multi-client fanout stability and update flush timing
|
||||||
|
- WiFi scanner robustness and monitor mode teardown reliability
|
||||||
|
- Agent client reliability improvements for remote sensor nodes
|
||||||
|
- SSTV VIS detector state reporting in signal monitor diagnostics
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Complete documentation audit across README, FEATURES, USAGE, help modal, and GitHub Pages
|
||||||
|
- Fixed license badge (MIT → Apache 2.0) to match actual LICENSE file
|
||||||
|
- Fixed tool name `rtl_amr` → `rtlamr` throughout all docs
|
||||||
|
- Fixed incorrect entry point examples (`python app.py` → `sudo -E venv/bin/python intercept.py`)
|
||||||
|
- Removed duplicate AIS Vessel Tracking section from FEATURES.md
|
||||||
|
- Updated SSTV requirements: pure Python decoder, no external `slowrx` needed
|
||||||
|
- Added ACARS and VDL2 mode descriptions to in-app help modal
|
||||||
|
- GitHub Pages site: corrected Docker command, license, and tool name references
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.21.1] - 2026-02-20
|
## [2.21.1] - 2026-02-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+11
-1
@@ -57,7 +57,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
soapysdr-module-airspy \
|
soapysdr-module-airspy \
|
||||||
airspy \
|
airspy \
|
||||||
limesuite \
|
limesuite \
|
||||||
hackrf \
|
|
||||||
# Utilities
|
# Utilities
|
||||||
curl \
|
curl \
|
||||||
procps \
|
procps \
|
||||||
@@ -190,6 +189,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
fi \
|
fi \
|
||||||
&& cd /tmp \
|
&& cd /tmp \
|
||||||
&& rm -rf /tmp/SatDump \
|
&& rm -rf /tmp/SatDump \
|
||||||
|
# Build hackrf CLI tools from source — avoids libhackrf0 version conflict
|
||||||
|
# between the 'hackrf' apt package and soapysdr-module-hackrf's newer libhackrf0
|
||||||
|
&& cd /tmp \
|
||||||
|
&& git clone --depth 1 https://github.com/greatscottgadgets/hackrf.git \
|
||||||
|
&& cd hackrf/host \
|
||||||
|
&& mkdir build && cd build \
|
||||||
|
&& cmake .. \
|
||||||
|
&& make \
|
||||||
|
&& make install \
|
||||||
|
&& ldconfig \
|
||||||
|
&& rm -rf /tmp/hackrf \
|
||||||
# Build rtlamr (utility meter decoder - requires Go)
|
# Build rtlamr (utility meter decoder - requires Go)
|
||||||
&& cd /tmp \
|
&& cd /tmp \
|
||||||
&& curl -fsSL "https://go.dev/dl/go1.22.5.linux-$(dpkg --print-architecture).tar.gz" | tar -C /usr/local -xz \
|
&& curl -fsSL "https://go.dev/dl/go1.22.5.linux-$(dpkg --print-architecture).tar.gz" | tar -C /usr/local -xz \
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="Python 3.9+">
|
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="Python 3.9+">
|
||||||
<img src="https://img.shields.io/badge/license-MIT-green.svg" alt="MIT License">
|
<img src="https://img.shields.io/badge/license-Apache--2.0-green.svg" alt="Apache 2.0 License">
|
||||||
<img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-lightgrey.svg" alt="Platform">
|
<img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-lightgrey.svg" alt="Platform">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ Support the developer of this open-source project
|
|||||||
- **HF SSTV** - Terrestrial SSTV on shortwave frequencies (80m-10m, VHF, UHF)
|
- **HF SSTV** - Terrestrial SSTV on shortwave frequencies (80m-10m, VHF, UHF)
|
||||||
- **APRS** - Amateur packet radio position reports and telemetry via direwolf
|
- **APRS** - Amateur packet radio position reports and telemetry via direwolf
|
||||||
- **Satellite Tracking** - Pass prediction with polar plot and ground track map
|
- **Satellite Tracking** - Pass prediction with polar plot and ground track map
|
||||||
- **Utility Meters** - Electric, gas, and water meter reading via rtl_amr
|
- **Utility Meters** - Electric, gas, and water meter reading via rtlamr
|
||||||
- **ADS-B History** - Persistent aircraft history with reporting dashboard (Postgres optional)
|
- **ADS-B History** - Persistent aircraft history with reporting dashboard (Postgres optional)
|
||||||
- **WiFi Scanning** - Monitor mode reconnaissance via aircrack-ng
|
- **WiFi Scanning** - Monitor mode reconnaissance via aircrack-ng
|
||||||
- **Bluetooth Scanning** - Device discovery and tracker detection (with Ubertooth support)
|
- **Bluetooth Scanning** - Device discovery and tracker detection (with Ubertooth support)
|
||||||
@@ -57,8 +57,6 @@ Support the developer of this open-source project
|
|||||||
|
|
||||||
## Installation / Debian / Ubuntu / MacOS
|
## Installation / Debian / Ubuntu / MacOS
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
**1. Clone and run:**
|
**1. Clone and run:**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/smittix/intercept.git
|
git clone https://github.com/smittix/intercept.git
|
||||||
@@ -150,7 +148,7 @@ Set these as environment variables for either local installs or Docker:
|
|||||||
```bash
|
```bash
|
||||||
INTERCEPT_ADSB_AUTO_START=true \
|
INTERCEPT_ADSB_AUTO_START=true \
|
||||||
INTERCEPT_SHARED_OBSERVER_LOCATION=false \
|
INTERCEPT_SHARED_OBSERVER_LOCATION=false \
|
||||||
python app.py
|
sudo -E venv/bin/python intercept.py
|
||||||
```
|
```
|
||||||
|
|
||||||
**Docker example (.env)**
|
**Docker example (.env)**
|
||||||
@@ -172,7 +170,7 @@ Then open **/adsb/history** for the reporting dashboard.
|
|||||||
|
|
||||||
After starting, open **http://localhost:5050** in your browser. The username and password is <b>admin</b>:<b>admin</b>
|
After starting, open **http://localhost:5050** in your browser. The username and password is <b>admin</b>:<b>admin</b>
|
||||||
|
|
||||||
The credentials can be change in the ADMIN_USERNAME & ADMIN_PASSWORD variables in config.py
|
The credentials can be changed in the ADMIN_USERNAME & ADMIN_PASSWORD variables in config.py
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -245,7 +243,7 @@ Created by **smittix** - [GitHub](https://github.com/smittix)
|
|||||||
[AIS-catcher](https://github.com/jvde-github/AIS-catcher) |
|
[AIS-catcher](https://github.com/jvde-github/AIS-catcher) |
|
||||||
[acarsdec](https://github.com/TLeconte/acarsdec) |
|
[acarsdec](https://github.com/TLeconte/acarsdec) |
|
||||||
[direwolf](https://github.com/wb2osz/direwolf) |
|
[direwolf](https://github.com/wb2osz/direwolf) |
|
||||||
[rtl_amr](https://github.com/bemasher/rtlamr) |
|
[rtlamr](https://github.com/bemasher/rtlamr) |
|
||||||
[dumpvdl2](https://github.com/szpajder/dumpvdl2) |
|
[dumpvdl2](https://github.com/szpajder/dumpvdl2) |
|
||||||
[aircrack-ng](https://www.aircrack-ng.org/) |
|
[aircrack-ng](https://www.aircrack-ng.org/) |
|
||||||
[Leaflet.js](https://leafletjs.com/) |
|
[Leaflet.js](https://leafletjs.com/) |
|
||||||
|
|||||||
@@ -7,10 +7,29 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Application version
|
# Application version
|
||||||
VERSION = "2.21.1"
|
VERSION = "2.22.0"
|
||||||
|
|
||||||
# Changelog - latest release notes (shown on welcome screen)
|
# Changelog - latest release notes (shown on welcome screen)
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "2.22.0",
|
||||||
|
"date": "February 2026",
|
||||||
|
"highlights": [
|
||||||
|
"Waterfall receiver overhaul: WebSocket I/Q streaming with server-side FFT, click-to-tune, and zoom controls",
|
||||||
|
"Voice alerts for configurable event notifications across modes",
|
||||||
|
"Signal fingerprinting mode for RF device identification and pattern analysis",
|
||||||
|
"RF Heatmap for geographic signal density visualization",
|
||||||
|
"SignalID integration via SigIDWiki API for automatic signal classification",
|
||||||
|
"PWA support: installable web app with service worker and manifest",
|
||||||
|
"Mode stop responsiveness improvements with faster timeout handling",
|
||||||
|
"Navigation performance instrumentation and smoother mode transitions",
|
||||||
|
"Pager, sensor, and SSTV real-time signal scope visualization",
|
||||||
|
"ADS-B MSG2 surface movement parsing for ground vehicle tracking",
|
||||||
|
"WebSDR major overhaul with improved receiver management and audio streaming",
|
||||||
|
"Documentation audit: fixed license, tool names, entry points, and SSTV decoder references",
|
||||||
|
"Help modal updated with ACARS and VDL2 mode descriptions",
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "2.21.1",
|
"version": "2.21.1",
|
||||||
"date": "February 2026",
|
"date": "February 2026",
|
||||||
|
|||||||
@@ -24,17 +24,6 @@ Complete feature list for all modules.
|
|||||||
- **Wideband spectrum analysis** with real-time visualization
|
- **Wideband spectrum analysis** with real-time visualization
|
||||||
- **I/Q capture** - record raw samples for offline analysis
|
- **I/Q capture** - record raw samples for offline analysis
|
||||||
|
|
||||||
## AIS Vessel Tracking
|
|
||||||
|
|
||||||
- **Real-time vessel tracking** via AIS-catcher on 161.975/162.025 MHz
|
|
||||||
- **Full-screen dashboard** - dedicated popout with interactive map
|
|
||||||
- **Interactive Leaflet map** with OpenStreetMap tiles (dark-themed)
|
|
||||||
- **Vessel details popup** - name, MMSI, callsign, destination, ETA
|
|
||||||
- **Navigation data** - speed, course, heading, rate of turn
|
|
||||||
- **Ship type classification** - cargo, tanker, passenger, fishing, etc.
|
|
||||||
- **Vessel dimensions** - length, width, draught
|
|
||||||
- **Multi-SDR support** - RTL-SDR, HackRF, LimeSDR, Airspy, SDRplay
|
|
||||||
|
|
||||||
## Spy Stations (Number Stations)
|
## Spy Stations (Number Stations)
|
||||||
|
|
||||||
- **Comprehensive database** of active number stations and diplomatic networks
|
- **Comprehensive database** of active number stations and diplomatic networks
|
||||||
|
|||||||
+1
-1
@@ -172,7 +172,7 @@ Set the following environment variables (Docker recommended):
|
|||||||
```bash
|
```bash
|
||||||
INTERCEPT_ADSB_AUTO_START=true \
|
INTERCEPT_ADSB_AUTO_START=true \
|
||||||
INTERCEPT_SHARED_OBSERVER_LOCATION=false \
|
INTERCEPT_SHARED_OBSERVER_LOCATION=false \
|
||||||
python app.py
|
sudo -E venv/bin/python intercept.py
|
||||||
```
|
```
|
||||||
|
|
||||||
**Docker example (.env)**
|
**Docker example (.env)**
|
||||||
|
|||||||
+3
-3
@@ -110,7 +110,7 @@
|
|||||||
<div class="feature-card" data-category="signals">
|
<div class="feature-card" data-category="signals">
|
||||||
<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>
|
<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>
|
<h3>Utility Meters</h3>
|
||||||
<p>Smart meter monitoring via rtl_amr. Receive electric, gas, and water meter broadcasts in real time.</p>
|
<p>Smart meter monitoring via rtlamr. Receive electric, gas, and water meter broadcasts in real time.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-card" data-category="tracking">
|
<div class="feature-card" data-category="tracking">
|
||||||
<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>
|
<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>
|
||||||
@@ -321,7 +321,7 @@ sudo -E venv/bin/python intercept.py</code></pre>
|
|||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<pre><code>git clone https://github.com/smittix/intercept.git
|
<pre><code>git clone https://github.com/smittix/intercept.git
|
||||||
cd intercept
|
cd intercept
|
||||||
docker compose up -d</code></pre>
|
docker compose --profile basic up -d --build</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<p class="install-note">Requires privileged mode for USB SDR access</p>
|
<p class="install-note">Requires privileged mode for USB SDR access</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -422,7 +422,7 @@ docker compose up -d</code></pre>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-bottom">
|
<div class="footer-bottom">
|
||||||
<p>Created by <a href="https://github.com/smittix" target="_blank">smittix</a> · MIT License</p>
|
<p>Created by <a href="https://github.com/smittix" target="_blank">smittix</a> · Apache 2.0 License</p>
|
||||||
<p class="disclaimer">For educational and authorized testing purposes only.</p>
|
<p class="disclaimer">For educational and authorized testing purposes only.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -560,3 +560,69 @@
|
|||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Crosshair sweep animation ───────────────────────────────────── */
|
||||||
|
.btl-crosshair-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 1200;
|
||||||
|
--btl-crosshair-x-start: 100%;
|
||||||
|
--btl-crosshair-y-start: 100%;
|
||||||
|
--btl-crosshair-x-end: 50%;
|
||||||
|
--btl-crosshair-y-end: 50%;
|
||||||
|
--btl-crosshair-duration: 1500ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btl-crosshair-line {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
background: var(--accent-cyan);
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btl-crosshair-vertical {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 1px;
|
||||||
|
left: 0;
|
||||||
|
transform: translateX(var(--btl-crosshair-x-start));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btl-crosshair-horizontal {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
top: 0;
|
||||||
|
transform: translateY(var(--btl-crosshair-y-start));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btl-crosshair-overlay.active .btl-crosshair-vertical {
|
||||||
|
animation: btlCrosshairSweepX var(--btl-crosshair-duration) cubic-bezier(0.2, 0.85, 0.28, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btl-crosshair-overlay.active .btl-crosshair-horizontal {
|
||||||
|
animation: btlCrosshairSweepY var(--btl-crosshair-duration) cubic-bezier(0.2, 0.85, 0.28, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes btlCrosshairSweepX {
|
||||||
|
0% { transform: translateX(var(--btl-crosshair-x-start)); opacity: 0; }
|
||||||
|
12% { opacity: 1; }
|
||||||
|
85% { opacity: 1; }
|
||||||
|
100% { transform: translateX(var(--btl-crosshair-x-end)); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes btlCrosshairSweepY {
|
||||||
|
0% { transform: translateY(var(--btl-crosshair-y-start)); opacity: 0; }
|
||||||
|
12% { opacity: 1; }
|
||||||
|
85% { opacity: 1; }
|
||||||
|
100% { transform: translateY(var(--btl-crosshair-y-end)); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.btl-crosshair-overlay.active .btl-crosshair-vertical,
|
||||||
|
.btl-crosshair-overlay.active .btl-crosshair-horizontal {
|
||||||
|
animation-duration: 220ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,16 +9,16 @@ const KeyboardShortcuts = (function () {
|
|||||||
if (e.target.matches(GUARD_SELECTOR)) return;
|
if (e.target.matches(GUARD_SELECTOR)) return;
|
||||||
|
|
||||||
if (e.altKey) {
|
if (e.altKey) {
|
||||||
switch (e.key.toLowerCase()) {
|
switch (e.code) {
|
||||||
case 'w': e.preventDefault(); window.switchMode && switchMode('waterfall'); break;
|
case 'KeyW': e.preventDefault(); window.switchMode && switchMode('waterfall'); break;
|
||||||
case 'm': e.preventDefault(); window.VoiceAlerts && VoiceAlerts.toggleMute(); break;
|
case 'KeyM': e.preventDefault(); window.VoiceAlerts && VoiceAlerts.toggleMute(); break;
|
||||||
case 's': e.preventDefault(); _toggleSidebar(); break;
|
case 'KeyS': e.preventDefault(); _toggleSidebar(); break;
|
||||||
case 'k': e.preventDefault(); showHelp(); break;
|
case 'KeyK': e.preventDefault(); showHelp(); break;
|
||||||
case 'c': e.preventDefault(); window.CheatSheets && CheatSheets.showForCurrentMode(); break;
|
case 'KeyC': e.preventDefault(); window.CheatSheets && CheatSheets.showForCurrentMode(); break;
|
||||||
default:
|
default:
|
||||||
if (e.key >= '1' && e.key <= '9') {
|
if (e.code >= 'Digit1' && e.code <= 'Digit9') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
_switchToNthMode(parseInt(e.key) - 1);
|
_switchToNthMode(parseInt(e.code.replace('Digit', '')) - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!e.ctrlKey && !e.metaKey) {
|
} else if (!e.ctrlKey && !e.metaKey) {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const BtLocate = (function() {
|
|||||||
let queuedDetectionTimer = null;
|
let queuedDetectionTimer = null;
|
||||||
let lastDetectionRenderAt = 0;
|
let lastDetectionRenderAt = 0;
|
||||||
let startRequestInFlight = false;
|
let startRequestInFlight = false;
|
||||||
|
let crosshairResetTimer = null;
|
||||||
|
|
||||||
const MAX_HEAT_POINTS = 1200;
|
const MAX_HEAT_POINTS = 1200;
|
||||||
const MAX_TRAIL_POINTS = 1200;
|
const MAX_TRAIL_POINTS = 1200;
|
||||||
@@ -349,19 +350,18 @@ const BtLocate = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stop() {
|
function stop() {
|
||||||
|
// Update UI immediately — don't wait for the backend response.
|
||||||
|
if (queuedDetectionTimer) {
|
||||||
|
clearTimeout(queuedDetectionTimer);
|
||||||
|
queuedDetectionTimer = null;
|
||||||
|
}
|
||||||
|
queuedDetection = null;
|
||||||
|
queuedDetectionOptions = null;
|
||||||
|
showIdleUI();
|
||||||
|
disconnectSSE();
|
||||||
|
stopAudio();
|
||||||
|
// Notify backend asynchronously.
|
||||||
fetch('/bt_locate/stop', { method: 'POST' })
|
fetch('/bt_locate/stop', { method: 'POST' })
|
||||||
.then(r => r.json())
|
|
||||||
.then(() => {
|
|
||||||
if (queuedDetectionTimer) {
|
|
||||||
clearTimeout(queuedDetectionTimer);
|
|
||||||
queuedDetectionTimer = null;
|
|
||||||
}
|
|
||||||
queuedDetection = null;
|
|
||||||
queuedDetectionOptions = null;
|
|
||||||
showIdleUI();
|
|
||||||
disconnectSSE();
|
|
||||||
stopAudio();
|
|
||||||
})
|
|
||||||
.catch(err => console.error('[BtLocate] Stop error:', err));
|
.catch(err => console.error('[BtLocate] Stop error:', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,6 +703,32 @@ const BtLocate = (function() {
|
|||||||
if (gpsCountEl) gpsCountEl.textContent = gpsPoints || 0;
|
if (gpsCountEl) gpsCountEl.textContent = gpsPoints || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function triggerCrosshairAnimation(lat, lon) {
|
||||||
|
if (!map) return;
|
||||||
|
const overlay = document.getElementById('btLocateCrosshairOverlay');
|
||||||
|
if (!overlay) return;
|
||||||
|
const size = map.getSize();
|
||||||
|
const point = map.latLngToContainerPoint([lat, lon]);
|
||||||
|
const targetX = Math.max(0, Math.min(size.x, point.x));
|
||||||
|
const targetY = Math.max(0, Math.min(size.y, point.y));
|
||||||
|
const startX = size.x + 8;
|
||||||
|
const startY = size.y + 8;
|
||||||
|
const duration = 1500;
|
||||||
|
overlay.style.setProperty('--btl-crosshair-x-start', `${startX}px`);
|
||||||
|
overlay.style.setProperty('--btl-crosshair-y-start', `${startY}px`);
|
||||||
|
overlay.style.setProperty('--btl-crosshair-x-end', `${targetX}px`);
|
||||||
|
overlay.style.setProperty('--btl-crosshair-y-end', `${targetY}px`);
|
||||||
|
overlay.style.setProperty('--btl-crosshair-duration', `${duration}ms`);
|
||||||
|
overlay.classList.remove('active');
|
||||||
|
void overlay.offsetWidth;
|
||||||
|
overlay.classList.add('active');
|
||||||
|
if (crosshairResetTimer) clearTimeout(crosshairResetTimer);
|
||||||
|
crosshairResetTimer = setTimeout(() => {
|
||||||
|
overlay.classList.remove('active');
|
||||||
|
crosshairResetTimer = null;
|
||||||
|
}, duration + 100);
|
||||||
|
}
|
||||||
|
|
||||||
function addMapMarker(point, options = {}) {
|
function addMapMarker(point, options = {}) {
|
||||||
if (!map || point.lat == null || point.lon == null) return false;
|
if (!map || point.lat == null || point.lon == null) return false;
|
||||||
const lat = Number(point.lat);
|
const lat = Number(point.lat);
|
||||||
@@ -737,6 +763,7 @@ const BtLocate = (function() {
|
|||||||
'Time: ' + formatPointTimestamp(trailPoint.timestamp) +
|
'Time: ' + formatPointTimestamp(trailPoint.timestamp) +
|
||||||
'</div>'
|
'</div>'
|
||||||
);
|
);
|
||||||
|
marker.on('click', () => triggerCrosshairAnimation(lat, lon));
|
||||||
|
|
||||||
trailPoints.push(trailPoint);
|
trailPoints.push(trailPoint);
|
||||||
mapMarkers.push(marker);
|
mapMarkers.push(marker);
|
||||||
|
|||||||
@@ -1831,6 +1831,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="btl-map-container" style="flex: 1; min-height: 250px; position: relative; overflow: hidden;">
|
<div class="btl-map-container" style="flex: 1; min-height: 250px; position: relative; overflow: hidden;">
|
||||||
<div id="btLocateMap" style="position: absolute; inset: 0;"></div>
|
<div id="btLocateMap" style="position: absolute; inset: 0;"></div>
|
||||||
|
<div id="btLocateCrosshairOverlay" class="btl-crosshair-overlay" aria-hidden="true">
|
||||||
|
<div class="btl-crosshair-line btl-crosshair-vertical"></div>
|
||||||
|
<div class="btl-crosshair-line btl-crosshair-horizontal"></div>
|
||||||
|
</div>
|
||||||
<div class="btl-map-overlay-controls">
|
<div class="btl-map-overlay-controls">
|
||||||
<label class="btl-map-overlay-toggle">
|
<label class="btl-map-overlay-toggle">
|
||||||
<input type="checkbox" id="btLocateHeatmapEnable" onchange="BtLocate.toggleHeatmap()">
|
<input type="checkbox" id="btLocateHeatmapEnable" onchange="BtLocate.toggleHeatmap()">
|
||||||
|
|||||||
@@ -166,11 +166,27 @@
|
|||||||
<li>View next pass predictions with elevation and duration</li>
|
<li>View next pass predictions with elevation and duration</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<h3>ACARS Mode</h3>
|
||||||
|
<ul class="tip-list">
|
||||||
|
<li>Decodes Aircraft Communications Addressing and Reporting System messages via acarsdec</li>
|
||||||
|
<li>Receives operational, weather, and position reports on 129-136 MHz</li>
|
||||||
|
<li>Supports North America, Europe, and Asia-Pacific regional frequency presets</li>
|
||||||
|
<li>Filter by message type, flight ID, or aircraft registration</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>VDL2 Mode</h3>
|
||||||
|
<ul class="tip-list">
|
||||||
|
<li>Decodes VHF Data Link Mode 2 aircraft datalink messages via dumpvdl2</li>
|
||||||
|
<li>Captures ACARS-over-AVLC frames with full signal analysis (SNR, burst length)</li>
|
||||||
|
<li>Monitor multiple VDL2 frequencies simultaneously (136.725, 136.775, 136.975 MHz)</li>
|
||||||
|
<li>Export captured messages to CSV or JSON for offline analysis</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h3>ISS SSTV Mode</h3>
|
<h3>ISS SSTV Mode</h3>
|
||||||
<ul class="tip-list">
|
<ul class="tip-list">
|
||||||
<li>Decodes Slow Scan Television (SSTV) images from the International Space Station</li>
|
<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>Automated ISS pass tracking with Doppler correction on 145.800 MHz</li>
|
||||||
<li>Images decoded in real-time using slowrx</li>
|
<li>Images decoded in real-time using the built-in pure Python decoder</li>
|
||||||
<li>Gallery view with timestamped decoded images</li>
|
<li>Gallery view with timestamped decoded images</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -336,9 +352,11 @@
|
|||||||
<li><strong>WebSDR:</strong> Internet connection (remote receivers)</li>
|
<li><strong>WebSDR:</strong> Internet connection (remote receivers)</li>
|
||||||
<li><strong>SubGHz:</strong> RTL-SDR or compatible SDR hardware</li>
|
<li><strong>SubGHz:</strong> RTL-SDR or compatible SDR hardware</li>
|
||||||
<li><strong>Satellite:</strong> Internet for Celestrak (optional), skyfield</li>
|
<li><strong>Satellite:</strong> Internet for Celestrak (optional), skyfield</li>
|
||||||
<li><strong>ISS SSTV:</strong> RTL-SDR, slowrx</li>
|
<li><strong>ACARS:</strong> RTL-SDR, acarsdec</li>
|
||||||
|
<li><strong>VDL2:</strong> RTL-SDR, dumpvdl2</li>
|
||||||
|
<li><strong>ISS SSTV:</strong> RTL-SDR (pure Python decoder — no external tools needed)</li>
|
||||||
<li><strong>Weather Sat:</strong> RTL-SDR, SatDump</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>HF SSTV:</strong> RTL-SDR or SoapySDR-compatible hardware (pure Python decoder)</li>
|
||||||
<li><strong>GPS:</strong> RTL-SDR or GPS-capable SDR</li>
|
<li><strong>GPS:</strong> RTL-SDR or GPS-capable SDR</li>
|
||||||
<li><strong>Space Weather:</strong> Internet connection (public APIs)</li>
|
<li><strong>Space Weather:</strong> Internet connection (public APIs)</li>
|
||||||
<li><strong>WiFi:</strong> Monitor-mode adapter, aircrack-ng suite</li>
|
<li><strong>WiFi:</strong> Monitor-mode adapter, aircrack-ng suite</li>
|
||||||
|
|||||||
@@ -179,6 +179,9 @@
|
|||||||
<button type="button" class="nav-tool-btn" id="voiceMuteBtn" onclick="window.VoiceAlerts && VoiceAlerts.toggleMute()" title="Toggle voice alerts" aria-label="Toggle voice alerts">
|
<button type="button" class="nav-tool-btn" id="voiceMuteBtn" onclick="window.VoiceAlerts && VoiceAlerts.toggleMute()" title="Toggle voice alerts" aria-label="Toggle voice alerts">
|
||||||
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg></span>
|
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="nav-tool-btn" onclick="window.CheatSheets && CheatSheets.showForCurrentMode()" title="Mode cheat sheet (Alt+C)" aria-label="Mode cheat sheet">
|
||||||
|
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg></span>
|
||||||
|
</button>
|
||||||
<button type="button" class="nav-tool-btn" onclick="window.KeyboardShortcuts && KeyboardShortcuts.showHelp()" title="Keyboard shortcuts (Alt+K)" aria-label="Keyboard shortcuts">
|
<button type="button" class="nav-tool-btn" onclick="window.KeyboardShortcuts && KeyboardShortcuts.showHelp()" title="Keyboard shortcuts (Alt+K)" aria-label="Keyboard shortcuts">
|
||||||
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10"/></svg></span>
|
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10"/></svg></span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ def _load_meta() -> dict[str, Any] | None:
|
|||||||
if os.path.exists(DB_META_FILE):
|
if os.path.exists(DB_META_FILE):
|
||||||
with open(DB_META_FILE, 'r') as f:
|
with open(DB_META_FILE, 'r') as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.warning(f"Corrupt aircraft db meta file, removing: {e}")
|
||||||
|
try:
|
||||||
|
os.remove(DB_META_FILE)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error loading aircraft db meta: {e}")
|
logger.warning(f"Error loading aircraft db meta: {e}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
+29
-11
@@ -637,27 +637,45 @@ def start_locate_session(
|
|||||||
"""Start a new locate session, stopping any existing one."""
|
"""Start a new locate session, stopping any existing one."""
|
||||||
global _session
|
global _session
|
||||||
|
|
||||||
|
# Grab and evict any existing session without holding the lock during stop()
|
||||||
|
# (stop() joins a thread which can block for up to 3 s).
|
||||||
|
old_session = None
|
||||||
with _session_lock:
|
with _session_lock:
|
||||||
if _session and _session.active:
|
if _session and _session.active:
|
||||||
_session.stop()
|
old_session = _session
|
||||||
|
|
||||||
_session = LocateSession(
|
|
||||||
target, environment, custom_exponent, fallback_lat, fallback_lon
|
|
||||||
)
|
|
||||||
if not _session.start():
|
|
||||||
_session = None
|
_session = None
|
||||||
raise RuntimeError("Bluetooth scanner failed to start")
|
|
||||||
return _session
|
if old_session:
|
||||||
|
old_session.stop()
|
||||||
|
|
||||||
|
new_session = LocateSession(
|
||||||
|
target, environment, custom_exponent, fallback_lat, fallback_lon
|
||||||
|
)
|
||||||
|
with _session_lock:
|
||||||
|
_session = new_session
|
||||||
|
|
||||||
|
if not new_session.start():
|
||||||
|
with _session_lock:
|
||||||
|
if _session is new_session:
|
||||||
|
_session = None
|
||||||
|
raise RuntimeError("Bluetooth scanner failed to start")
|
||||||
|
|
||||||
|
return new_session
|
||||||
|
|
||||||
|
|
||||||
def stop_locate_session() -> None:
|
def stop_locate_session() -> None:
|
||||||
"""Stop the active locate session."""
|
"""Stop the active locate session."""
|
||||||
global _session
|
global _session
|
||||||
|
|
||||||
|
# Release the lock before stop() so concurrent status/SSE requests
|
||||||
|
# aren't blocked for up to 3 s while the poll thread is joined.
|
||||||
|
session_to_stop = None
|
||||||
with _session_lock:
|
with _session_lock:
|
||||||
if _session:
|
session_to_stop = _session
|
||||||
_session.stop()
|
_session = None
|
||||||
_session = None
|
|
||||||
|
if session_to_stop:
|
||||||
|
session_to_stop.stop()
|
||||||
|
|
||||||
|
|
||||||
def get_locate_session() -> LocateSession | None:
|
def get_locate_session() -> LocateSession | None:
|
||||||
|
|||||||
Reference in New Issue
Block a user