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:
Smittix
2026-02-23 19:31:10 +00:00
parent 3acdab816a
commit 9705e58691
15 changed files with 647 additions and 448 deletions
+41
View File
@@ -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
View File
@@ -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 \
+5 -7
View File
@@ -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/) |
+20 -1
View File
@@ -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",
-11
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+66
View File
@@ -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;
}
}
+8 -8
View File
@@ -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) {
+39 -12
View File
@@ -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);
+4
View File
@@ -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()">
+21 -3
View File
@@ -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>
+3
View File
@@ -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>
+6
View File
@@ -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
View File
@@ -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: