When dump1090 lacks native --enable-biast support, the system now falls
back to rtl_biast (RTL-SDR Blog drivers) to enable bias-t power before
starting dump1090. The Blog V4's built-in LNA requires bias-t to
receive ADS-B signals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HOST_NAME_MAX is Linux-specific and undefined on macOS, causing 3
compile errors in acarsdec.c. Now patched with #define HOST_NAME_MAX 255
before building. Also fixed deprecated -Ofast flag on all macOS archs
(was only patched for arm64).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
timeout (GNU coreutils) is not available on macOS, causing rtl_test to
silently fail and report no SDR device found. Now tries timeout, then
gtimeout (Homebrew coreutils), then falls back to background process
with manual kill.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
.logo span { display: inline } in dashboard CSS had specificity (0,1,1),
overriding .brand-i { display: inline-block } at (0,1,0). Inline elements
ignore width/height, so the SVG rendered at intrinsic size (~80px tall).
Added .logo .brand-i selector at (0,2,0) to retain inline-block display.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
get_setting() now catches sqlite3.OperationalError and returns the
default value. Previously, an inaccessible database (e.g. root-owned
instance/ from sudo) caused inject_offline_settings to crash every
page render with 500 Internal Server Error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
read_env_var() grep pipeline failed under set -euo pipefail when .env
existed but didn't contain the requested key. grep returned 1 (no match),
pipefail propagated it, and set -e killed the script.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On x86_64, explicitly pass -march=x86-64 so the compiler emits only
baseline instructions. SatDump's SIMD plugins still compile with their
own per-target flags and do runtime CPU detection, so AVX2 acceleration
remains available on capable hardware. ARM builds are unaffected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The data/ directory became a Python package (oui.py, patterns.py, satellites.py)
in v2.26.0, but .dockerignore still blanket-excluded it as runtime data.
This caused ModuleNotFoundError: No module named 'data.oui' on container startup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Apply the branded SVG "i" glyph to nav logo, hero heading, and footer
on the GitHub Pages landing page, matching the main app's branding.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Patch release for #186 — default ADMIN_PASSWORD now matches README,
and credential changes in config.py sync to DB on restart.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The default ADMIN_PASSWORD was an empty string, triggering random
password generation on first run — contradicting the README which
states admin:admin. Additionally, editing config.py after first run
had no effect since init_db() only seeded users on an empty table.
- Change default ADMIN_PASSWORD from '' to 'admin'
- Sync admin credentials from config on every startup so that
changes to config.py or env vars take effect without wiping the DB
Fixes#186
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix SSE fanout thread AttributeError when source queue is None during
interpreter shutdown by snapshotting to local variable with null guard
- Fix branded "i" logo rendering oversized on first page load (FOUC) by
adding inline width/height to SVG elements across 10 templates
- Bump version to 2.26.0 in config.py, pyproject.toml, and CHANGELOG.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scale down the branded "i" to sit as a proper lowercase glyph beside
the uppercase "NTERCEPT" text, with the stem bottom on the baseline
and the dot just above cap height.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scale the branded "i" glyph proportionally to each SVG's font size
(scale 0.94 for 64px, 1.24 for 84px) and align the stem bottom to
the text baseline so the glyph sits naturally beside "NTERCEPT".
Also adds brand-pack.html (logos, profiles, banners, stickers, release
templates) and wallpapers.html (12 themes, 8 resolutions, PNG export).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the plain cyan text "i" with the logo-style SVG glyph (green dot
+ cyan stem/bars) in both the README banner and social preview images.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The CSS ::after dot positioning was unreliable across fonts and sizes.
Switch to an inline SVG of the "i" glyph (green dot + cyan stem/bars)
extracted from the logo — renders pixel-perfect at any size.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The dotless i (ı) wasn't rendering in all fonts. Switch to a regular "i"
with the green dot CSS overlay positioned on top of the native dot.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Matches the logo icon — the "i" in iNTERCEPT now renders with a cyan
letter and green dot via CSS, consistent across the main header, welcome
card, dashboard headers, help modal, settings modal, and all popout pages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ensureModeScript() used document.body.appendChild() to load lazy mode
scripts, but the preload for ?mode= query params runs in <head> before
<body> exists, causing all deep-linked modes to silently fail.
Also fix cross-mode handoffs (BT→BT Locate, WiFi→WiFi Locate,
Spy Stations→Waterfall) that assumed target module was already loaded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend sends rssi_current but frontend was reading net.signal || net.rssi,
causing RSSI to parse as NaN and silently skipping all meter/audio updates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
acarsdec is not available in apt repos, so the apt_install attempt
always failed with a confusing error message before falling through
to the source build. Skip the apt attempt and go straight to compiling
from source on Linux.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add WiFi Locate mode for locating access points by BSSID with real-time
signal meter, distance estimation, RSSI history chart, and audio
proximity tones. Includes hand-off from WiFi detail drawer, environment
presets (Free Space/Outdoor/Indoor), and signal-lost detection.
Also includes:
- Mobile navigation reorganized into labeled groups (SIG/TRK/SPC/WIFI/INTEL/SYS)
- flask-limiter made optional with graceful degradation
- Fix radiosonde setup missing semver Python dependency
- Documentation updates (FEATURES, USAGE, UI_GUIDE, GitHub Pages site)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The OOK subprocess was spawned without start_new_session=True, so
process.terminate() only signalled the parent — child processes kept
running. Now uses os.killpg() to terminate the entire process group,
matching the pattern used by all other routes (ADS-B, AIS, ACARS, etc.).
Also fixes silent error swallowing in the frontend stop handler so the
UI resets even if the backend request fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The airband start function was calling parseInt() directly on composite
device selector values like "rtlsdr:0", which always returned NaN and
fell back to device 0. This also meant sdr_type was never sent to the
backend, and could result in int(None) TypeError on the server.
Now properly splits the composite value (matching ADS-B/ACARS/VDL2
pattern) and sends both device index and sdr_type. Also hardened
backend int() parsing to use explicit None checks.
Fixes: "Airband Error: Invalid parameter: int() argument must be a
string, a bytes-like object or a real number, not 'NoneType'"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses final upstream review — all backend-derived values (timestamp,
bit_count, rssi, hex, ascii) now use DOM methods instead of innerHTML
interpolation, closing the last XSS surface. Bumps cache-buster to ook2.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SatDump v1.2.2 has multiple GCC 15 build failures (sol2 templates,
libacars incompatible pointer types) that are difficult to patch
exhaustively. On distros where SatDump is available as a system
package (Ubuntu 24.10+, Debian Trixie+), install via apt instead
of building from source. Falls back to source build on older systems.
Closes#180
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Detect crashed rtl_433 process via poll() and clean up stale state
instead of permanently blocking restarts with 409
- Replace innerHTML+onclick preset rendering with createElement/addEventListener
to prevent XSS via crafted localStorage frequency values
- Normalize preset frequencies to toFixed(3) on save and render
- Add try/catch + shape validation to loadPresets() for corrupted localStorage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical:
- Pass sdr_type_str to claim/release_sdr_device (was missing 3rd arg)
- Add ook_active_sdr_type module-level var for proper device registry tracking
- Add server-side range validation on all timing params via validate_positive_int
Major:
- Extract cleanup_ook() function for full teardown (stop_event, pipes, process,
SDR release) — called from both stop_ook() and kill_all()
- Replace Popen monkey-patching with module-level _ook_stop_event/_ook_parser_thread
- Fix XSS: define local _esc() fallback in ook.js, never use raw innerHTML
- Remove dead inversion code path in utils/ook.py (bytes.fromhex on same
string that already failed decode — could never produce a result)
Minor:
- Status event key 'status' → 'text' for consistency with other modules
- Parser thread logging: debug → warning for missing code field and errors
- Parser thread emits status:stopped on exit (normal EOF or crash)
- Add cache-busting ?v={{ version }}&r=ook1 to ook.js script include
- Fix gain/ppm comparison: != '0' (string) → != 0 (number)
Tests: 22 → 33 (added start success, stop with process, SSE stream,
timing range validation, stopped-on-exit event)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add client-side and server-side military aircraft detection using ICAO
hex ranges and callsign prefixes (matching live dashboard logic). History
table shows MIL/CIV badges with filtering dropdown, and exports respect
the classification filter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add date range filtering, CSV export, and enhanced history page styling
for the ADS-B aircraft tracking history feature.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users reported setup.sh appearing stuck during dump1090 installation on
Ubuntu 25.10. Added progress messages before APT package checks, build
dependency installation, and fallback clone steps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add -Wno-template-body to CMAKE_CXX_FLAGS to suppress GCC 15's
-Wtemplate-body warning that breaks SatDump's bundled sol2/sol.hpp.
The flag is silently ignored by older GCC versions.
Closes#180
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add kill_all() handler for OOK process cleanup on global reset
- Fix stop_ook() to close pipes and join parser thread (prevents hangs)
- Add ook.css with CSS classes, replace inline styles in ook.html
- Register ook.css in lazy-load style map (INTERCEPT_MODE_STYLE_MAP)
- Fix frontend frequency min=24 to match backend validation
- Add 22 unit tests for decode_ook_frame, ook_parser_thread, and routes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Parse hackrf_info stderr (newer firmware) and handle non-zero exit codes
- Fix gain_max from 62 to 102 (combined LNA 40 + VGA 62)
- Apply resolved readsb binary path for all SDR types, not just RTL-SDR
- Add HackRF/SoapySDR-specific error messages in ADS-B startup
- Add HackRF waterfall support via rx_sdr IQ capture + FFT
- Add 17 tests for HackRF detection and command builder
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix XSS: escape ASCII output in innerHTML via escapeHtml()
- Fix deadlock: use put_nowait() for queue ops under ook_lock
- Fix SSE leak: add ook to moduleDestroyMap so switching modes
closes the EventSource
- Fix RSSI: explicit null check preserves valid zero values in
JSON export
- Add frame cap: trim oldest frames at 5000 to prevent unbounded
memory growth on busy bands
- Validate timing params: wrap int() casts in try/except, return
400 instead of 500 on invalid input
- Fix PWM hint: correct to short=0/long=1 matching rtl_433
OOK_PWM convention (UI, JS hints, and cheat sheet)
- Fix inversion docstring: clarify fallback only applies when
primary hex parse fails, not for valid decoded frames
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded frequency buttons with localStorage-backed presets.
Default presets are standard ISM frequencies (433.920, 315, 868, 915 MHz).
Users can add custom frequencies, right-click to remove, and reset to
defaults — matching the pager module pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers identifying modulation type (PWM/PPM/Manchester), finding
pulse timing via rtl_433 -A, common ISM frequencies and timings,
and troubleshooting tips for tolerance and bit order.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix double-scroll by switching ookOutputPanel to flex layout
- Keep decoded frames visible after stopping (persist for review)
- Wire global Clear/CSV/JSON status bar buttons to OOK functions
- Hide default output pane in OOK mode (uses own panel)
- Add command display showing the active rtl_433 command
- Add JSON export and auto-scroll support
- Fix 0x prefix stripping in OOK hex decoder
- Fix PWM encoding hint text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Timing presets: five quick-fill buttons (300/600, 300/900, 400/800, 500/1500, 500 MC)
that populate all six pulse-timing fields at once — maps to CTF flag timing profiles
- RSSI per frame: add -M level to rtl_433 command; parse snr/rssi/level from JSON;
display dB SNR inline with each frame; include rssi_db column in CSV export
- Auto bit-order suggest: "Suggest" button counts printable chars across all stored
frames for MSB vs LSB, selects the winner, shows count — no decoder restart needed
- Pattern filter: live hex/ASCII filter input above the frame log; hides non-matching
frames and highlights matches in green; respects current bit order
- TSCM integration: "Decode (OOK)" button in RF signal device details panel switches
to OOK mode and pre-fills frequency — frontend-only, no backend changes needed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users had no visibility into what was happening during silent apt/pip
installs. Added info messages before Python package installs, APT
package lists update, and PostgreSQL installation.
Replace the linear setup.sh with an interactive menu-driven installer:
- First-time wizard with OS detection and profile selection
- Install profiles: Core SIGINT, Maritime, Weather, RF Security, Full, Custom
- System health check (tools, SDR devices, ports, permissions, venv, PostgreSQL)
- Automated PostgreSQL setup for ADS-B history (creates DB, user, tables, indexes)
- Environment configurator for interactive INTERCEPT_* variable editing
- Update tools (rebuild source-built binaries)
- Uninstall/cleanup with granular options and double-confirm for destructive ops
- View status table of all tools with installed/missing state
- CLI flags: --non-interactive, --profile=, --health-check, --postgres-setup, --menu
- .env file helpers (read/write) with start.sh auto-sourcing
- Bash 3.2 compatible (no associative arrays) for macOS support
Update all documentation to reflect the new menu system:
- README.md: installation section with profiles, CLI flags, env config, health check
- CLAUDE.md: entry points and local setup commands
- docs/index.html: GitHub Pages install cards with profile mentions
- docs/HARDWARE.md: setup script section with profile table
- docs/TROUBLESHOOTING.md: health check and profile-based install guidance
- docs/DISTRIBUTED_AGENTS.md: controller quick start
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Runtime data (station config, logs) should not be tracked in version control.
Also removes duplicate "Local data" block in .gitignore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SSE EventSources and running processes were not cleaned up during
dashboard navigation, saturating the browser's per-origin connection
limit. Extract moduleDestroyMap into shared getModuleDestroyFn() and
call destroyCurrentMode() before navigation. Also expand
stopActiveLocalScansForNavigation() to cover wefax, weathersat, sstv,
subghz, meshtastic, and gps modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Several modes didn't pass sdr_type to claim_sdr_device(), defaulting to
'rtlsdr' and triggering an rtl_test USB probe that fails for HackRF with
a confusing "check that the RTL-SDR is connected" message.
- Add sdr_type to frontend start requests for rtlamr, weather-sat, sstv-general
- Read sdr_type in backend routes and pass to claim/release_sdr_device()
- Add early guard returning clear "not yet supported" error for non-RTL-SDR
hardware in modes that are hardcoded to RTL-SDR tools
- Make probe_rtlsdr_device error message device-type-agnostic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The "Mute" button on pager cards persists muted addresses to
localStorage with no visible indicator, making it easy to
accidentally hide an address and forget about it. This caused
flag fragment messages on RIC 1337 to silently disappear.
- Add "X muted source(s) — Unmute All" indicator to sidebar
- Stop persisting hideToneOnly filter across sessions so the
default (show all) always applies on page load
- Remove default checked state from Tone Only filter checkbox
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Safety net for Windows developers whose git config (core.autocrlf=true)
converts LF to CRLF on checkout. Even with .gitattributes forcing eol=lf,
some git configurations can still produce CRLF working copies. The sed
pass after COPY ensures start.sh and other scripts always have Unix
line endings inside the container.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Docker containers crash on startup when shell scripts have CRLF line
endings (from Windows git checkout with core.autocrlf=true). The
start.sh gunicorn entrypoint fails with "$'\r': command not found".
Add .gitattributes forcing eol=lf for *.sh and Dockerfile so Docker
builds work regardless of the developer's git line ending config.
Also normalizes two scripts that were committed with CRLF.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover all parse_multimon_output code paths:
- Alpha and Numeric content types across POCSAG baud rates
- Empty content and special characters (base64, punctuation)
- Catch-all pattern for non-standard content type labels
- Address-only (Tone) messages with trailing whitespace
- FLEX simple format and unrecognized input lines
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three issues caused POCSAG messages to be incorrectly hidden or
misclassified in the Device Intelligence panel:
1. detectEncryption used a narrow character class ([a-zA-Z0-9\s.,!?-])
to measure "printable ratio". Messages containing common printable
ASCII characters like : = / + @ fell below the 0.8 threshold and
returned null ("Unknown") instead of false ("Plaintext"). Simplified
to check all printable ASCII (\x20-\x7E) which correctly classifies
base64, structured data, and punctuation-heavy content.
2. The default hideToneOnly filter was true, hiding all address-only
(Tone) pager messages. When RF conditions cause multimon-ng to decode
the address but not the message content, the resulting Tone card was
silently filtered. Changed default to false so users see all traffic
and can opt-in to filtering.
3. The multimon-ng output parser only recognized "Alpha" and "Numeric"
content type labels. Added a catch-all pattern to capture any
additional content type labels that future multimon-ng versions or
forks might emit, rather than dropping them to raw output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Radiosonde route now runs a quick import check before launching the full
subprocess, catching missing Python dependencies immediately with a clear
message instead of a truncated traceback. Error messages are context-aware:
import errors suggest re-running setup.sh rather than checking SDR connections.
Increased stderr truncation limit from 200 to 500 chars and added full stderr
logging via logger.error() across all affected routes (radiosonde, ais, aprs,
acars, vdl2) for easier debugging.
Closes#173
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the /devices fetch hasn't completed or fails, parseInt on an empty
select returns NaN which JSON-serializes to null. The backend then calls
int(None) and raises TypeError. Fix both layers: frontend falls back to
0 on NaN, backend uses `or` defaults so null values don't bypass the
fallback.
Also adds a short TTL cache to detect_all_devices() so multiple
concurrent callers on the same page load don't each spawn blocking
subprocess probes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three compounding bugs prevented flask-sock (and other C-extension
packages) from installing and hid the actual errors:
- Add python3-dev to Debian apt installs so Python.h is available for
building gevent, cryptography, etc.
- Remove 2>/dev/null from optional packages pip loop so install errors
are visible and diagnosable
- Surface pip/setuptools/wheel upgrade failures with a warning instead
of silently swallowing them
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add dropout tolerance (2 blocks ~40ms) to bridge brief signal gaps that
caused the state machine to chop dahs into multiple dits. Also fix scope
SNR display to use actual noise_ref instead of noise_floor.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add theme-aware severity/neon CSS variables and replace hardcoded hex
colors (#fff, #000, #00ff88, #ffcc00, etc.) with var() references
across 26 files so text remains readable in both dark and light themes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nav active labels used color: var(--bg-primary) which resolved to
near-white on light backgrounds. Run-state chips and buttons had
hardcoded dark RGBA backgrounds. Added light-theme overrides for
readable text and appropriate light backgrounds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use postMessage from parent page to notify the satellite dashboard
iframe of visibility changes, preventing unnecessary POST requests
to /satellite/position when the user isn't viewing satellite mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend: Add /prefetch-images endpoint that warms the image cache in
parallel using a thread pool, skipping already-cached images.
Frontend: Trigger prefetch on mode init so images load instantly.
Replace per-request Date.now() cache-bust with a 5-minute rotating
key to allow browser caching aligned with backend max-age.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When navigating from another mode (e.g. pager) to the ADS-B dashboard,
the old process could still hold the USB device. Two fixes:
1. routes/adsb.py: If dump1090 starts but SBS port never comes up,
kill the process and return a DEVICE_BUSY error instead of silently
claiming success with no data.
2. templates/adsb_dashboard.html: Pre-flight conflict check in
toggleTracking() queries /devices/status and auto-stops any
conflicting mode before starting ADS-B, with a 1.5s USB release
delay.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stock rtl-sdr packages don't support the -T bias-tee flag (only
RTL-SDR Blog builds do). Passing -T to stock rtl_sdr causes an
immediate exit, breaking meteor scatter and waterfall modes.
Now probes the tool's --help output before adding -T, with a regex
that avoids false-matching "DVB-T" in the description text.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Meteor: onopen callback used closure variable _ws instead of `this`,
so a double-click during CONNECTING state sent on the wrong socket.
Also clean up any in-progress connection on re-start, not just running ones.
Setup: make apt-get update non-fatal so third-party repo errors
(e.g. stale PPAs on Debian) don't abort the entire install.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Match gunicorn's patch_all() args exactly (remove subprocess=False),
filter the MonkeyPatchWarning from the unavoidable double-patch, and
wrap gevent's _ForkHooks.after_fork_in_child to catch the spurious
AssertionError that fires when subprocesses fork after double-patching.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gunicorn's gevent worker deadlocks during init_process() on Raspberry Pi
(ARM) before it can apply its own monkey-patching. Patching in post_fork
runs immediately after fork and before worker init, avoiding the race.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full-stack meteor scatter monitoring mode that captures IQ data from
an RTL-SDR, computes FFT waterfall frames via WebSocket, and runs a
real-time detection engine to identify transient VHF reflections from
meteor ionization trails (e.g. GRAVES radar at 143.050 MHz).
Backend: MeteorDetector with EMA noise floor, SNR threshold state
machine (IDLE/DETECTING/ACTIVE/COOLDOWN), hysteresis, and CSV/JSON
export. WebSocket at /ws/meteor for binary waterfall frames, SSE at
/meteor/stream for detection events and stats.
Frontend: spectrum + waterfall + timeline canvases, event table with
SNR/duration/confidence, stats strip, turbo colour LUT. Uses shared
SDR device selection panel with conflict tracking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SSTV mode was slow to populate next-pass countdown and ISS location map
due to uncached skyfield computation and sequential JS API calls.
- Cache ISS position (10s TTL) and schedule (15min TTL, keyed by rounded lat/lon)
- Cache skyfield timescale object (expensive to create on every request)
- Reduce external API timeouts from 5s to 3s
- Fire checkStatus, loadImages, loadIssSchedule, updateIssPosition in parallel via Promise.all
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The /space-weather/data endpoint made 13 sequential HTTP requests, each
with a 15s timeout, causing 30-195s load times on cold cache. Now uses
ThreadPoolExecutor to fetch all sources concurrently, reducing worst-case
latency to ~15s (single slowest request).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add flex-shrink: 0 so the strip holds its intrinsic height instead of
being distorted by the parent flex container.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move tracking state, balloon count, last update, and waveform from the
sidebar into a stats strip above the map, matching the APRS strip pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reverts the incorrect assumption from f8e5d61 that -c expects a
directory. The auto_rx -c flag expects the full path to station.cfg.
Passing the directory caused "Config file ... does not exist!" on start.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Running via sudo creates data/radiosonde/ as root. On next run the
config write fails with an unhandled OSError, Flask returns an HTML 500,
and the frontend shows a cryptic JSON parse error.
Three-layer fix:
- start.sh: pre-create known data dirs before chown, add certs/ to the
list, export INTERCEPT_SUDO_UID/GID for runtime use
- generate_station_cfg: catch OSError with actionable message, chown
newly created files to the real user via _fix_data_ownership()
- start_radiosonde: wrap config generation in try/except so it returns
JSON instead of letting Flask emit an HTML error page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reusable SVG bar waveform (SignalWaveform.Live) that animates in response
to incoming SSE data — idle breathing when stopped, active oscillation
proportional to telemetry update frequency, smooth decay on signal loss.
Integrated into radiosonde Status section with ping() on each balloon
message and stop() on tracking stop. Also hardens the fetch error path
to show a readable message instead of a JSON parse error when the server
returns HTML.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The -c flag expects a directory containing station.cfg, but we were
passing the full file path, so auto_rx could never find its config.
Also fix sonde_type priority to prefer subtype over type.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The visual refresh layer hardcoded dark rgba() gradients that overrode
variable-based backgrounds. Added [data-theme="light"] overrides for
visual refresh CSS variables and comprehensive component backgrounds
in index.css and global-nav.css.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move flask-sock and websocket-client from the batch core install (where
failures are silently swallowed) to the optional packages loop so users
see a clear warning if either package fails to build on ARM.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove section hover shift, fix broken NOAA PDF link, reorder sections
to match Weather Satellite pattern, and fix text alignment spacing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SSE EventSource connections for AIS, ACARS, VDL2, and radiosonde were
not closed when switching modes, causing fd exhaustion after repeated
switches. Also fixes socket leaks on exception paths in AIS/ADS-B
stream parsers, closes subprocess pipes in safe_terminate/cleanup, and
caches skyfield timescale at module level to avoid per-request fd churn.
Closes#169
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
METEOR-M2-4 was defined as an active weather satellite but had no
orbital data, so pass predictions always returned empty. Added TLE
entry and CelesTrak name mapping for automatic refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add SSLZeroReturnError and SSLError to gevent's NOT_ERROR list so
dropped TLS handshakes (browser preflight, plain HTTP to HTTPS port)
don't print scary tracebacks to the console.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolves the machine's LAN IP via hostname -I so users see a
clickable URL they can use from other devices on the network.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sqlite3.connect() opens read-only files without error — the failure
only surfaces on the first write (INSERT). Add an upfront os.access()
check on both the directory and file, with a clear error showing the
owner and the exact chown command to fix it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When start.sh runs via sudo, chown instance/ and data/ back to the
invoking user so the SQLite DB stays accessible without sudo. Also
adds a clear error message in get_connection() when the DB can't be
opened due to permissions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The FFTW3 dev package was listed twice in the build stage and both
copies were removed during cleanup, taking the runtime .so with them.
Switching the duplicate to libfftw3-bin ensures libfftw3f.so.3 persists.
Fixes#166
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Escape ac.icao, callsign, typeCode with escapeHtml() in aircraft card (XSS)
- Add linking comments between duplicated IATA_TO_ICAO mappings
- VDL2 sidebar: single-click selects aircraft, double-click opens modal
- Remove stale ICAOs from acarsAircraftIcaos in cleanupOldAircraft()
- Add null guard to drawPolarPlot() in weather-satellite.js
- Move deferred imports (translate_message, get_flight_correlator) to module level
- Check all frequency checkboxes by default on initial load
- Remove extra blank lines and uncertain MC/MCO airline code entry
- Add TODO comments linking duplicated renderAcarsCard implementations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extends the rtl_tcp support (added in c1339b6 for APRS, Morse, DSC) to
the weather satellite mode. When a remote SDR host is provided, SatDump
uses --source rtltcp instead of --source rtlsdr, local device claiming
is skipped, and the frontend sends rtl_tcp params via getRemoteSDRConfig().
Closes#166
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When stopping gunicorn with Ctrl+C, the gevent worker's handle_quit()
calls sys.exit(0) inside a greenlet, causing gevent to print a
SystemExit traceback. Add a gunicorn config with post_worker_init hook
that marks SystemExit as a non-error in gevent's hub.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes#164. Only pager and sensor routes supported rtl_tcp connections.
Now aprs, morse, and dsc routes follow the same pattern: extract
rtl_tcp_host/port from the request, skip local device claiming for
remote connections, and use SDRFactory.create_network_device(). DSC also
refactored from manual rtl_fm command building to use SDRFactory's
builder abstraction. Frontend wired up for all three modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Blueprint registration and database init run synchronously (essential
for routing). Process cleanup, database cleanup scheduling, and TLE
satellite updates are deferred to a background thread with a 1-second
delay so the gevent worker can start serving HTTP requests right away.
Previously all init ran synchronously during module import, blocking
the single gevent worker for minutes while TLE data was fetched from
CelesTrak.
Also removes duplicate TLE update — init_tle_auto_refresh() already
schedules its own background fetch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All other docs already reference sudo ./start.sh but the inline usage
comments in start.sh itself and the --help example in USAGE.md were
missing it, which could lead users to run without root privileges.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The custom SIGINT/SIGTERM handler in utils/process.py overrode
gunicorn's own signal management, causing KeyboardInterrupt to fire
inside the gevent worker on Ctrl+C instead of allowing gunicorn's
graceful shutdown. Now detects if another signal manager (gunicorn)
has already installed handlers and defers to it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Blueprint registration, database init, cleanup, and websocket setup
were all inside main() which only runs via 'python intercept.py'.
When gunicorn imports app:app, it got a bare Flask app with no routes,
causing every endpoint to return 404.
Extracted initialization into _init_app() called at module level with
a guard to prevent double-init when main() is also used.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rate limiting on login is a security requirement, not optional.
Reverts the no-op fallback — if flask-limiter is missing, the app
will fail fast with a clear import error rather than silently
running without rate limiting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- psutil was in requirements.txt but missing from setup.sh optional list
- Verification check no longer hard-fails on flask-limiter since app.py
now handles it as optional with a no-op fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
flask-limiter may not be installed (e.g. RPi venv). The hard import
crashed the gunicorn gevent worker on startup, causing all routes to
return 404 with no visible error. Now falls back to a no-op limiter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Long-lived SSE connections prevent the gevent worker from exiting on
SIGINT. --graceful-timeout 5 force-kills the worker after 5 seconds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gunicorn's gevent worker (-k gevent) handles monkey-patching internally.
The manual patch_all() in app.py ran in the master process before worker
fork, preventing the worker from booting (no 'Booting worker' log line,
server unreachable).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolves ModuleNotFoundError when running outside a venv by auto-detecting
the venv/bin/python relative to the script, falling back to VIRTUAL_ENV
or system python3.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add start.sh as the recommended production entry point with:
- gunicorn + gevent worker for concurrent SSE/WebSocket handling
- CLI flags for port, host, debug, HTTPS, and dependency checks
- Auto-fallback to Flask dev server if gunicorn not installed
- Conditional gevent monkey-patch in app.py via INTERCEPT_USE_GEVENT env var
- Docker CMD updated to use start.sh
- Updated all docs, setup.sh, and requirements.txt accordingly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change 'already_running' to 'already_scanning' status in bluetooth_v2
so frontend recognizes the response and connects the SSE stream
- Hide pagerScopePanel and sensorScopePanel in switchMode() to prevent
audio waveform bars leaking into other modes
- Clear devices Map, pendingDeviceIds Set, and UI in BluetoothMode.destroy()
to prevent memory accumulation on repeated mode switches
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously all HackRF devices were hardcoded as "HackRF One" regardless
of actual hardware variant. Now parses the Board ID line from hackrf_info
to correctly identify HackRF Pro, HackRF One, and other variants.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pass observer location and gpsd status to radiosonde_auto_rx station config
- Add station marker on radiosonde map with GPS live position updates
- Display distance from station to each balloon in cards and popups
- Update aircraft database
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolved conflicts:
- routes/acars.py: keep /messages and /clear endpoints for history reload
- routes/vdl2.py: keep /messages and /clear endpoints for history reload
- templates/adsb_dashboard.html: keep removal of hardcoded device-1
defaults for ACARS/VDL2 selectors (users pick their own device)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mode modules were leaking EventSource connections, setInterval timers,
and setTimeout timers on every mode switch, causing progressive browser
sluggishness. Added destroy() to 8 modules missing it (meshtastic,
bluetooth, wifi, bt_locate, sstv, sstv-general, websdr, spy-stations)
and centralized all destroy calls in switchMode() via a moduleDestroyMap
that cleanly tears down only the previous mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Re-implements envelope detection on top of the rewritten Morse decoder.
Addresses PR #160 review feedback:
- Rebase: rebuilt on current upstream/main (lifecycle state machine)
- Gap thresholds: 2.0/5.0 for envelope only; goertzel keeps 2.6/6.0
- Frequency validation: max_mhz=1766 for envelope, 30 for goertzel
- Tests: EnvelopeDetector unit tests + envelope-mode decoder test
- Envelope uses direct magnitude threshold (no SNR/noise ref)
- Goertzel path completely unchanged
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rtl_test opens the USB device during probing. After killing the
process, the kernel may not release the USB interface immediately.
dump1090 then fails with usb_claim_interface error -6. Add a 0.5s
delay after probe cleanup to allow the kernel to fully release the
device before the actual decoder opens it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rtl_test -t often exits non-zero after finding a device (e.g.
"No E4000 tuner found, aborting" with R820T tuners). The return
code fallback was firing even when the "Found N device(s)" success
message had already been matched. Track device_found separately
and only use return code as fallback when no success was seen.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The success check ('Found' in line and 'device' in line) matched
"No supported devices found" since both keywords appear. Add a
pre-check for negative device messages, a return code fallback,
and a clearer error message.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The stop button appeared unresponsive because UI updates waited for the
server response. If the fetch hung or errored, the user saw nothing.
Now the UI updates immediately (matching the pager stop pattern) and
the server request happens in the background.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Auto_rx reads many config keys without defaults and crashes if they're
missing, even for disabled features like email. Include every section
and key from the example config to prevent missing-key errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The config format changed significantly: SDR settings moved to [sdr_1],
[positioning] became [location], and many sections are now required.
Also enable payload_summary UDP output so telemetry reaches our listener.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pass config file path (not directory) to auto_rx -c flag
- Use absolute paths in generated station.cfg since auto_rx runs
with cwd set to its install directory
- Teach dependency checker about auto_rx.py at /opt install path
so the "missing dependency" banner no longer appears
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The setup.sh skip check only looked for auto_rx.py, so a previous
incomplete install (Python files but no compiled binaries) would be
treated as fully installed. Now also checks for dft_detect binary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
setup.sh and Dockerfile were installing the Python package and copying
files but skipping the build.sh step that compiles the C decoders.
This caused "Binary dft_detect does not exist" at runtime.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The subprocess was launched with bare 'python' which on Debian doesn't
exist (python3 only) and wouldn't have access to the venv-installed
radiosonde dependencies anyway. Using sys.executable ensures the same
interpreter (with all installed packages) is used.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move adsb_active_device/sdr_type assignment to immediately after
claim_sdr_device so stop_adsb() can always release the device, even
during startup. Sync sdr_type_str after SDRType fallback to prevent
claim/release key mismatch. Clear active device on all error paths.
Replace blind 3s sleep for dump1090 readiness with port-polling loop
(100ms intervals, 3s max). Replace subprocess.run() in rtl_test probe
with Popen + select-based early termination on success/error detection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Avoids PEP 668 externally-managed-environment error on Debian Bookworm
by using the project's venv/bin/pip instead of system pip3.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Integrate radiosonde_auto_rx for automatic weather balloon detection and
decoding on 400-406 MHz. Includes UDP telemetry parsing, Leaflet map with
altitude-colored markers and trajectory tracks, SDR device registry
integration, setup script installation, and Docker support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The registry used plain int keys (device index), so HackRF at index 0
and RTL-SDR at index 0 would collide. Changed to composite string keys
("sdr_type:index") so each SDR type+index pair is tracked independently.
Updated all route callers, frontend device selectors, and session restore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch direwolf subprocess output from PIPE to PTY (pseudo-terminal),
forcing line-buffered output so packets arrive immediately instead of
waiting for a 4-8KB pipe buffer to fill. Matches the proven pattern
used by pager mode.
Also enhances direwolf config with FIX_BITS error correction and
disables unused AGWPE/KISS server ports.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The globe wasn't rendering because initGlobe() used setTimeout(100)
which can race with the display:none removal by switchMode(). Both
GPS and WebSDR modes use requestAnimationFrame to wait for the browser
to compute layout before initializing Globe.gl.
- Replace setTimeout with RAF-based retry loop (up to 8 frames)
- Add try-catch around Globe() init with fallback message
- Match the proven pattern from GPS and WebSDR modes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix globe destroyed on re-render by preserving canvas DOM node across
renderLocationCard() calls instead of recreating from scratch
- Reduce globe.gl camera minDistance (180->120) so globe is visible in
200px container
- Clear stale globeInstance ref when canvas is gone
- Enlarge CPU gauge (90->110px), percentage label (18->22px), core bars
(24->48px height), and detail text (11->12px)
- JS fetchLocation() now supplements server response with client-side
ObserverLocation.getShared() from localStorage when server returns
'default' or 'none', picking up manual coordinates from settings modal
- Location priority: GPS > config env vars > manual (localStorage) > default
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add third location fallback to utils/constants (London 51.5074/-0.1278)
so location always resolves even without GPS or env vars configured
- Remove min-height from sys-card to eliminate wasted space
- Switch System Info to vertical key-value layout filling the card
- Clean up OS string (strip glibc suffix), use locale date for boot time
- Bump info grid font size from 11px to 12px for readability
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace broken app.gps_state lookup with utils.gps.get_current_position()
and return GPS metadata (fix quality, satellites, accuracy). Shrink location
card to single-column with 200px globe, move System Info into row 2.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add SVG arc gauge, per-core CPU bars, temperature sparkline, network
interface monitoring with bandwidth deltas, disk I/O rates, 3D globe
with observer location, weather overlay, battery/fan/throttle support,
and process grid layout. New /system/location and /system/weather endpoints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Clear sidebar highlights and ACARS message timer when stale selected
aircraft is removed in cleanupOldAircraft()
- Escape all user-controlled strings in renderAcarsCard(),
addAcarsMessage(), and renderAcarsMainCard() before innerHTML insertion
- Remove dead duplicate H1 check in classify_message_type
- Move _d label from link_test set to handshake return path
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Real-time dashboard for host metrics (CPU, memory, disk, temperatures),
active decoder process status, and SDR device enumeration via SSE streaming.
Auto-connects when entering the mode with graceful psutil fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch decoder subprocess from text mode to binary mode and decode
each line with errors='replace' so corrupted radio bytes (e.g. 0xf7)
are substituted instead of raising UnicodeDecodeError after long runs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The satellite dropdown had no change listener, so selecting a different
satellite never updated the pass list, timeline, countdown, or polar plot.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The .btn, .btn-sm, and .btn-ghost classes used by morse mode buttons
(TXT, CSV, Copy, Clear) were defined in core/components.css but the
stylesheet was never loaded in index.html, causing unstyled buttons.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the stop POST timed out (5s), lifecycle was set to 'idle' on error,
allowing checkStatus to see running=true and reconnect SSE. Now:
- stop .then() stays in 'stopping' on timeout/error instead of going idle
- checkStatus skips reconnect when lifecycle is 'stopping' post-timeout
but still transitions to idle when server confirms running=false
- LOCAL_STOP_TIMEOUT_MS raised from 5s to 12s to match server cleanup time
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous stopPromise guard only prevented new polls from being
dispatched. Polls already in-flight before stop was clicked could still
return with running=true and override the stopping lifecycle, causing
SSE reconnection and an apparent restart loop. Add a second guard in
the .then() handler to check stopPromise/lifecycle before acting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Guard checkStatus() against in-flight stop to prevent status poller
from overriding stopping state and reconnecting SSE. Lower SNR floor
from 1.3 to 1.15 to accommodate weaker CW signals. Add SNR/noise_ref
to scope events and metrics for real-time threshold debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Widen noise detector offset from ±100Hz to ±200Hz to reduce spectral
leakage into the noise reference, and scale threshold_multiplier for
SNR space (2.8 → 1.54) so real CW signals reliably trigger tone
detection instead of producing all-E's at 60 WPM.
Fix misleading "decoder startup" timeout message on stop requests and
increase stop timeout from 2.2s to 5s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous magnitude-based threshold couldn't distinguish CW tone from
AGC-amplified inter-element silence — the Goertzel level stayed above
threshold permanently, preventing any tone OFF transitions and thus zero
character decodes.
Switch tone detection to use SNR (tone_mag / adjacent_band_noise_ref).
Both bands are equally amplified by AGC, so the ratio is gain-invariant.
Also replace the conditional noise_ref guard with unconditional blending
so the noise floor tracks actual ambient levels continuously.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Filter decoder-thread 'stopped' status events that race with the route
lifecycle, causing the frontend to drop back to idle on first start.
Pull noise floor upward using adjacent-frequency Goertzel reference when
warmup calibration runs before AGC converges, preventing permanent
tone-on with zero character decodes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The multimon-ng MORSE_CW decoder never reliably decoded characters.
Switch live decode to use the existing morse_decoder_thread() which
wraps MorseDecoder with Goertzel tone detection, adaptive thresholds,
and proper timing estimation — eliminating multimon-ng, PTY plumbing,
and the relay thread from the CW pipeline entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Step-by-step instructions for running multiple RTL-SDR dongles:
serial burning, udev symlinks, USB power, and Docker passthrough.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add bufsize=0 to Popen for raw FileIO instead of BufferedReader, and
start decoder/stderr threads immediately before sleep+poll so stdout
is read without delay — matching the working pager pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BufferedReader.read(n) on non-interactive streams (Python 3.14) blocks
until the full n bytes accumulate, starving the decoder of real-time
PCM data. Use os.read() on the raw file descriptor instead, which
returns as soon as any data is available. Falls back to .read() for
file-like objects without fileno() (e.g. BytesIO in tests).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace select.select()+os.read() with a blocking reader thread feeding
a queue, matching pager's working pattern. The select() approach fails
to detect available data on Python 3.14's BufferedReader-wrapped pipes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rtl_fm prints device info, tuning, and errors to stderr but the morse
route only logged these server-side. Now stderr lines are forwarded to
the morse queue as info events, displayed in a compact diagnostic log
below the scope canvas. After 10s with no audio data, the scope text
escalates to prompt the user to check the SDR log.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace blocking rtl_stdout.read() with select()+os.read() so the
decoder thread emits diagnostic heartbeat scope events when rtl_fm
produces no PCM data (common in direct sampling mode). Add waiting-state
rendering in the scope canvas and hide the generic placeholder/status
bar for morse mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add automatic gain control (AGC) before Goertzel processing to normalize
quiet audio from direct sampling mode where the -g gain flag has no effect.
Fix broken adaptive threshold bootstrap by adding a 50-block warm-up phase
that collects magnitude statistics before seeding noise floor and signal peak.
Lower threshold ratio from 50% to 30% for better weak-CW sensitivity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The -D flag is only available in newer rtl_fm builds. Docker and distro
packages use the older -E direct / -E direct2 flags instead, which are
universally supported.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enable direct sampling (-D 2) for RTL-SDR at HF frequencies below 24 MHz
so rtl_fm can actually receive CW signals. Add startup health check to
detect immediate rtl_fm failures. Push stopped status event from decoder
thread on EOF so the frontend auto-resets. Add frequency placeholder and
help text. Fix stop button silently swallowing errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
validate_frequency() defaults to 24-1766 MHz (VHF/UHF range), but Morse/CW
operates on HF bands (0.5-30 MHz). Pass explicit min/max to allow HF frequencies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolved conflict in static/js/modes/weather-satellite.js:
- Kept allPasses state variable and applyPassFilter() for satellite pass filtering
- Kept satellite select dropdown listener for filter feature
- Adopted upstream's optimistic stop() UI pattern for better responsiveness
- Kept optional chaining (pass?.trajectory) since drawPolarPlot can receive null
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes#155 — users can now access settings directly from the welcome
screen without entering a mode first.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix "What's New" section shifting up/down on smaller screens (#157) by
isolating the logo pulse animation to its own compositing layer, stabilizing
the scrollbar gutter, and pinning the welcome container dimensions.
Morse mode improvements: relocate scope and decoded output panels to the
main content area, use shared SDR device controls, and reduce panel heights
for better layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Correct modulation parameters (1200 bps, 2100/1300 Hz tones), replace
invented format codes with the six ITU-defined specifiers {102, 112,
114, 116, 120, 123}, accept all valid EOS symbols (117, 122, 127),
add parser validation (format, MMSI, raw field, telecommand range),
and fix truthiness bugs that dropped zero-valued fields.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add synchronous startup verification after Popen() — sleep 0.5s and poll
the process before returning to the caller. If SatDump exits immediately
(missing device, bad args), raise RuntimeError with the actual error
message instead of returning status: 'started'. Keep a shorter (2s) async
backup check for slower failures.
Also fix --source_id handling: omit the flag entirely when no serial number
is found instead of passing "0" which SatDump may reject. Change start()
and start_from_file() to return (bool, str|None) tuples so error messages
propagate through to the HTTP response.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use run-btn/stop-btn classes and bottom placement instead of
btn-primary/btn-danger in a flex section, and preset-btn class
for band presets. Aligns with all other mode panels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TSCM RF scan now auto-detects HackRF via SDRFactory and uses
hackrf_sweep as an alternative to rtl_power. Also includes
improvements to listening post, rtlamr, weather satellite,
SubGHz, Meshtastic, SSTV, WeFax, and process monitor modules.
Fixes#154
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New signal mode for decoding Morse code (CW) transmissions via SDR.
Includes route blueprint, utility decoder, frontend UI, and tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an error occurred with an out-of-range span (e.g. 30 MHz on
RTL-SDR), the span input kept the invalid value. Track the last
effective span from successful starts and reset the input on error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded "rtl_fm" references in wefax.py with the actual SDR
tool name so error messages correctly show "rx_fm" for non-RTL devices.
Use get_tool_path('rx_fm') in all SoapySDR command builders to match
the pattern already used for rx_sdr.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add pre-flight check for I/Q capture binary before spawning process
- Capture stderr from I/Q process for better error diagnostics
- Sync effective span value back to UI when backend adjusts it
- Use get_tool_path('rx_sdr') in Airspy, HackRF, LimeSDR, and SDRPlay
command builders to support custom install locations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Make stopAprs() async and await backend stop completion before
re-enabling the Start button, preventing race where a late stop
request kills newly started processes
- Add cache-buster param to EventSource URL to prevent browser
SSE connection reuse between stop/start cycles
- Capture aprs_active_device locally in stream_aprs_output so the
old thread's finally block doesn't release a device claimed by
a new session
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded rtl_fm with SDRFactory abstraction layer so WeFax works
with any supported SDR hardware, matching the pattern used by APRS and
other modes. RTL-SDR direct sampling flag preserved for HF reception.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Number(null) evaluates to 0 which passes Number.isFinite(),
causing aprsHasValidCoordinates(null, null) to return true.
This made initAprsMap() center the map at [0,0] (Gulf of Guinea)
at zoom 8 instead of the US default, hiding all station markers
off-screen.
Add null guards (lat != null && lon != null) to reject null/undefined
while still accepting 0 as a valid equator coordinate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add flex-shrink: 0 to .section, .run-btn, and .stop-btn so flex
children maintain natural height and the sidebar scrolls instead
of compressing content on 1080p displays.
Fixes#151
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The default 1 MHz sample rate was too low for SatDump's meteor_m2-x_lrpt
pipeline, causing NOSYNC and 0.000dB SNR. Bumped to 2.4 MHz (SatDump
recommendation) and wired up the WEATHER_SAT_SAMPLE_RATE config value
so it actually gets passed to decoder.start() from both the auto-scheduler
and manual start route.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Timer threads now log on fire and catch all exceptions so scheduled
captures never die silently. Frontend connects SSE when the scheduler
is enabled (not only on manual Start) and polls /wefax/status every 10s
as a fallback so the UI stays in sync with auto-fired captures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace window.open() with a fullscreen modal matching the SSTV
pattern: toolbar with download/delete SVG buttons, close button,
click-outside-to-close, and confirmation before delete.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
stop() was returning before the decode thread could save any partial
image to disk, so the frontend loadImages() call found nothing new.
Join the decode thread (2s timeout) before returning — with select()-
based reads the thread exits within ~0.5s so this stays responsive.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace blocking stdout.read() with select()-based non-blocking reads
so the decode thread responds to stop within 0.5s
- Make stop() non-blocking by releasing the lock before terminating the
process and removing the redundant wait()
- Move initial scanning SSE event from start() into the decode thread so
it fires after the frontend EventSource connects
- Update frontend stop() to give immediate UI feedback before the fetch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rtl_fm subprocess failures (missing tool, no SDR hardware) were silent —
add tool-path check and post-spawn health check in _start_pipeline(),
show errors prominently in the strip status bar (red text + red dot),
and include error detail in scheduler skip events.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Flash the Start button itself with amber pulse when clicked without a
station selected, and show "Select Station" in the strip status text
right next to the button so the error is immediately visible.
Add a 24-hour timeline bar with broadcast window markers, red UTC time
cursor, and countdown boxes (HRS/MIN/SEC) that tick down to the next
broadcast. Broadcasts show as amber blocks on the timeline track with
imminent/active visual states matching the weather satellite pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix silent failure when starting without station/frequency selected by
flashing amber on status text and dropdowns. Add auto-capture scheduler
that uses fixed UTC broadcast schedules from station data to
automatically start/stop WeFax decoding at broadcast times.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement HF radiofax decoding with custom Python DSP pipeline
(rtl_fm USB → Goertzel/Hilbert demodulation), 33-station database
with broadcast schedules, audio waveform scope, live image preview,
and decoded image gallery. Amber/gold UI theme for HF distinction.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove profiles: [basic] from intercept service so docker compose up -d
works without --profile flag (fixes breaking change for existing deployments)
- Add missing Any import to routes/acars.py and routes/vdl2.py
- Reset last_message_time to None in ACARS and VDL2 clear endpoints
- Restore 131.725 and 131.825 to default ACARS frequencies (major US carriers)
- Copy VDL2 ACARS enrichment fields to top-level data dict instead of mutating
nested acars_payload (consistent with ACARS route pattern)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The shared audio queue (maxsize reduced from 80 to 20) was not flushed
when the monitor frequency changed — only when the monitor was disabled.
This caused up to 4 seconds of stale old-frequency audio to play after
clicking to tune, making click-to-tune appear non-functional.
Now flushes the queue whenever the VFO frequency changes, so audio at
the new frequency begins within ~50ms (one FFT frame).
1. Stop Monitor button was disabled during shared monitor retunes
because _syncMonitorButtons disabled the button whenever
_startingMonitor was true, even if the monitor was already active.
Now only disables during initial start (not retunes).
2. Click-to-tune was inconsistent because the shared monitor retune
(rearm after capture restart) captured the center frequency early
in _startMonitorInternal, then sent it via POST to /audio/start.
If the user clicked a new frequency during the async reconnect,
the POST carried the stale frequency and could override the click.
Now retunes use the live _monitorFreqMhz and send a WS tune sync
after reconnecting to ensure the backend has the latest VFO.
Two root causes for the waterfall/monitor lockup when scrolling past the
2.4 MHz RTL-SDR span:
1. safe_terminate() sent SIGKILL but never called wait(), leaving a
zombie process that kept the USB device handle open. The subsequent
capture restart failed the USB probe and the monitor could not use
the shared IQ path, falling back to a process-based monitor that
stole the SDR from the waterfall.
2. When the frontend created a new WebSocket after a failure, the old
handler's finally block called _set_shared_capture_state(running=False)
which could race with the new handler's running=True, making the
shared monitor path unavailable. Added a generation counter so only
the owning handler can clear the shared state.
When restarting capture for a new frequency, the USB handle from the
just-killed process may not be released by the kernel in time for the
rtl_test probe inside claim_sdr_device. Add retry logic (up to 4
attempts with 0.4s backoff) matching the pattern already used by the
audio start endpoint.
Also clean up stale shared-monitor state in the frontend error handler
so the monitor button is not left disabled when the capture restart
fails.
When changing frequency with shared monitor active, the monitor retune
could be silently dropped if a previous retune was still in-flight,
leaving the UI stuck on "Starting <freq>". After stopping and restarting
the waterfall, the monitor button could remain disabled because
_startingMonitor was never reset and _monitorRetuneTimer was not cleared.
- Cancel in-flight monitor start when queuing a new retune
- Always clear _pendingSharedMonitorRearm in started handler
- Clear _monitorRetuneTimer and reset _startingMonitor in stop()
Auto-generates a self-signed certificate into data/certs/ when
INTERCEPT_HTTPS=true, or accepts custom cert/key paths via
INTERCEPT_SSL_CERT and INTERCEPT_SSL_KEY. Resolves 400 errors
from browsers sending TLS ClientHello to the plain HTTP server.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
stop() sets _ws = null before the async onclose fires, so the handler
now early-returns when _ws is null instead of showing the misleading
"WebSocket closed before ready" retry message.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- _waitForPlayback now only succeeds on playing/timeupdate events, not
loadeddata/canplay which fire from just the WAV header before real
audio arrives
- stopMonitor() pauses audio and updates UI immediately instead of
blocking on the backend stop request (1+ second delay)
- Reduced backend audio stop sleep from 1.0s to 0.15s; the start
retry loop already handles USB contention
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Waterfall: load waterfall.css eagerly in <head> instead of lazily on
mode switch; the lazy inject raced with the panel becoming visible,
leaving unstyled HTML for up to 20 s on cold cache
- WebSDR: await a requestAnimationFrame before calling Globe()(mapEl) so
the browser has committed the display:flex layout and clientWidth/
clientHeight are non-zero; previously the globe WebGL renderer was
created at 0×0 (especially on warm-cache refreshes) and could not
recover via the deferred resize calls
- Bump version to 2.22.2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Regenerates icon-192.png, icon-512.png, apple-touch-icon.png, and
favicon-32.png from the official iNTERCEPT logo (favicon.svg) instead
of the placeholder icon.svg. Also replaces icon.svg with the official
logo so the SVG manifest entry is consistent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Browsers require PNG icons (192x192, 512x512) in the manifest to show
the install prompt. SVG-only manifests are not sufficient. Also adds the
180x180 apple-touch-icon PNG for iOS home screen, bumps SW cache to v3,
and adds scope to the manifest.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When dashboards (satellite, ADS-B, AIS) are loaded via iframe with
?embedded=true, the full navigation bar was still rendered, creating
a "UI in UI" effect. Pass the embedded query param from route handlers
to templates and conditionally skip the nav include.
Fixes#144
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Hit area: was Math.max(dotSize * 2, 15) — up to 24px radius around a 4px
dot. Now the CSS hover-flicker is fixed the large hit area is unnecessary
and was the reason dots activated when merely nearby. Changed to dotSize + 4
(proportional, 4px padding around the visual circle).
Overlap spread: compute all band positions first, then run an iterative
push-apart pass (spreadOverlappingDots) that nudges any two dots whose
arc gap is smaller than 2 * maxHitArea + 2px apart. Positions within a
band are stable across renders (same hash angle, same band = same output
before spreading) so dots don't shuffle on every update.
Z-order: sort visible devices by rssi_current ascending before rendering
so the strongest signal lands last in SVG order and receives clicks when
dots stack.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The root cause was in proximity-viz.css, not the JS:
.radar-device:hover { transform: scale(1.2); }
When the cursor entered a .radar-device, the 1.2x scale physically moved
the hit-area boundary, pushing the cursor outside it. The browser then
fired mouseout, the scale reverted, the cursor was back inside, mouseover
fired again, and the scale reapplied — a rapid enter/exit loop that looked
like the dot jumping and dancing.
Replace the geometry-changing scale with a brightness filter on the dot
circle only. filter: brightness() does not affect pointer-event hit testing
so there is no feedback loop, and the hover still gives clear visual
feedback. Also removes the transition: transform rule that was animating
the scale and contributing to the flicker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace continuous estimated_distance_m-based radius with proximity band
snapping (immediate/near/far/unknown → fixed radius ratios of 0.15/0.40/
0.70/0.90). The proximity_band is computed server-side from rssi_ema which
is already smoothed, so it changes infrequently — dots now only move when
a device genuinely crosses a band boundary rather than on every RSSI
fluctuation.
Also removes the client-side EMA and positionCache added in the previous
commit, and reverts CSS style.transform back to SVG transform attribute to
avoid coordinate-system mismatch when the SVG is displayed at a scaled size.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The remaining jitter after the in-place DOM rewrite was caused by RSSI
fluctuations propagating directly into dot positions on every 200ms
update cycle.
Two fixes:
1. Client-side EMA (alpha=0.25) on x/y coordinates per device. Each
render blends 25% toward the new raw position and retains 75% of the
smoothed position, filtering high-frequency RSSI noise without hiding
genuine distance changes. positionCache is keyed by device_key and
cleared on device removal or radar reset.
2. CSS transition (transform 0.6s ease-out) on each wrapper element.
Switching from SVG transform attribute to style.transform enables
native CSS transitions, so any remaining position change (e.g. a band
crossing) animates smoothly rather than snapping.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of rebuilding devicesGroup.innerHTML on every render, mutate
existing SVG elements in-place (update transforms, attributes, class
names) and only create/remove elements when devices genuinely appear
or disappear from the visible set.
This eliminates the root cause of both the jitter and the blank-radar
regression: hover state can never be disrupted by a render because the
DOM elements under the cursor are never destroyed. The isHovered /
renderPending / interactionLockUntil state machine and its associated
mouseover/mouseout listeners are removed entirely — they are no longer
needed. A shared buildSelectRing() helper deduplicates the animated
selection ring construction used by renderDevices() and
applySelectionToElement(). Closes#143.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace capture-phase mouseenter/mouseleave with bubbling mouseover/mouseout
for tracking hover state in the ProximityRadar component.
The capture-phase approach caused two problems:
1. Moving between sibling child elements (hit-area → dot circle) fired
mouseleave, prematurely clearing isHovered and triggering a DOM rebuild
that caused visible jitter.
2. When renderDevices() rebuilt innerHTML, the browser fired mouseleave for
the destroyed element with relatedTarget pointing at the newly created
element at the same position, leaving isHovered permanently stuck at true
and suppressing all future renders.
The fix uses mouseover/mouseout (which bubble) with devicesGroup.contains()
to reliably detect whether the cursor genuinely left the device group, immune
to innerHTML rebuilds. Fixes both WiFi and Bluetooth proximity radars as they
share this component. Closes#143.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Persist ACARS/VDL2 messages across page refresh via new /acars/messages
and /vdl2/messages endpoints backed by FlightCorrelator
- Add clear buttons to ACARS/VDL2 sidebars and right-panel datalink section
with /acars/clear and /vdl2/clear endpoints
- Fix right-panel DATALINK MESSAGES flickering by diffing innerHTML before
updating, with opacity transition for smooth refreshes
- Add aircraft deselect toggle (click selected aircraft again to deselect)
- Enrich VDL2 messages with ACARS label translation (label_description,
message_type, parsed fields) matching existing ACARS translator
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ACARS and VDL2 conflict warnings were hardcoded to check device === '0'
instead of comparing against the actual ADS-B device (adsbActiveDevice).
This caused false warnings when ADS-B used a different device index.
Also removes hardcoded device-1 defaults for ACARS/VDL2 selectors —
users should pick their own device based on their antenna setup.
Adds profiles: [basic] to the intercept service in docker-compose so it
doesn't port-conflict with intercept-history when using --profile history.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ACARS messages use IATA codes (e.g. UA2412) while ADS-B uses ICAO
callsigns (e.g. UAL2412). Add a translation layer so the two can
match, enabling click-to-highlight and datalink message correlation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Click an ACARS message in the left sidebar to zoom the map to the
matching aircraft and open its detail panel. Aircraft with ACARS
activity show a DLK badge in the tracked list. Default NA frequency
changed to only check 131.550 on initial load.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ACARS label translation, message classification, and field parsers
so decoded messages show human-readable descriptions instead of raw
label codes (H1, DF, _d, 5Z, etc.). Integrate translated ACARS
messages into the ADS-B aircraft detail panel and add a live message
feed to the standalone ACARS mode.
- New utils/acars_translator.py with ~50 label codes, type classifier,
and parsers for position reports, engine data, weather, and OOOI
- Enrich messages at ingest in routes/acars.py with translation fields
- Backfill translation in /adsb/aircraft/<icao>/messages endpoint
- ADS-B dashboard: DATALINK MESSAGES section in aircraft detail panel
with auto-refresh, color-coded type badges, and parsed field display
- Standalone ACARS mode: scrollable live message feed (max 30 cards)
- Fix default N. America ACARS frequencies to 131.550/130.025/129.125
- Unit tests covering all translator functions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The slant correction was severely under-correcting because bwd=50 caused
the sync deviation measurements to saturate after only ~25 lines (for a
2-sample/line SDR clock drift). Lines 25-256 all reported deviation=-50,
pulling the linear regression slope toward zero.
Increase bwd and fwd to 800 samples each — sufficient to track cumulative
drift from up to ~±200 ppm SDR clock offset across the full 256-line image.
Also use a full-sync-length (432-sample) Goertzel window instead of 1/3
length, giving ~111 Hz frequency resolution to cleanly separate the 1200 Hz
sync tone from 1500 Hz pixel data. Search is stepped at 5 samples (~0.1 ms)
for efficiency, keeping the goertzel_batch batch size at ~320 windows/line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous attempts to correct slant by altering R-channel placement and
buffer consumption caused cascading failures: a false positive in B pixel
data would misplace R, then the wrong consumed value misaligned the next
line's G, and the error compounded across all 256 lines.
New approach (safe by design):
- Sync search is measurement-only: never touches pos or consumed, so
a noisy or wrong measurement cannot corrupt the current or future lines.
- Per-line deviation (measured sync position minus expected) is recorded
in self._sync_deviations throughout the decode.
- get_image() fits a line through the deviations (linear regression) to
estimate the per-line SDR clock drift rate, then applies a horizontal
shear to the assembled PIL image: each row is shifted by
-round(row × drift_rate × width / channel_samples) pixels.
- Worst case (all measurements fail): no correction applied, image
quality identical to the pre-change baseline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The step-49 coarse scan introduced up to ±24 sample uncertainty in R
channel placement. When accumulated SDR clock drift pushed the actual
sync 35+ samples early in the search region, the step-49 windows could
land on the B-pixel tail and return position 0, misplacing R by ~50
samples (~16 pixel colour shift) — worse than no correction at all.
Replace with a vectorised goertzel_batch sliding-window scan at step=1
over a short window (sync_duration / 3 ≈ 3 ms), giving single-sample
accuracy. Use consumed=pos (instead of max(pos,line_samples)) when the
sync is found, so the next line starts at its correct separator and
per-line timing errors stop accumulating entirely.
Falls back to the fixed-offset path whenever the sync is not found
(e.g. noisy signal), preserving the pre-change baseline quality.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous sync search used search_margin = line_samples/10 (~306
samples for Scottie2), reaching deep into B channel pixel data behind
pos and well past the expected sync end ahead of pos.
When _find_sync returned a position in the late portion of that wide
region, pos + R_channel_samples exceeded the buffer length. The
buffer-too-short guard in _decode_line then returned early without
consuming data or advancing the line counter, causing the stall guard
in feed() to permanently break the decode loop.
Fix: use a 50-sample backward margin (covers >130 ppm SDR drift) and
a forward margin capped to whatever the current buffer can safely
support for the R channel. A final candidate-position check before
committing pos ensures no overflow is possible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scottie modes place their horizontal sync pulse between the Blue and Red
channels. The decoder was using a fixed offset to skip over it, so any
SDR clock error accumulated line-by-line and produced a visible diagonal
slant in the decoded image.
Fix: search for the actual 1200 Hz sync pulse in a ±10% window around
the expected position before decoding the Red channel, then align to the
real pulse. This resets accumulated clock drift on every scanline, the
same way Martin and Robot modes already handle their front-of-line sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix wrong VIS codes for PD90 (96→99), PD120 (93→95), PD180 (95→97),
PD240 (113→96), and ScottieDX (55→76). This caused PD180 to be detected
as PD90 and PD120 to fail entirely.
Replace batch Goertzel pixel decoding with analytic signal (Hilbert
transform) FM demodulation. The Goertzel approach used 96-sample windows
with ~500 Hz resolution — wider than the 800 Hz pixel frequency range —
making accurate pixel decoding impossible for fast modes like Martin2
and Scottie2. The Hilbert method computes per-sample instantaneous
frequency, matching the approach used by QSSTV and other professional
SSTV decoders.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tolerate intermittent ambiguous windows during leader detection (up to
3 consecutive misses), use energy-based break detection when tone
classification fails at leader-break boundary, and add single-bit VIS
error correction for parity-bit and data-bit corruption on noisy HF.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VIS detection recognized these modes but ALL_MODES had no decoder specs,
causing silent decode failures on common HF frequencies like 14.230 MHz.
Also emit a user-visible SSE event when an unsupported VIS code is detected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous fix (f29ae3d) introduced a regression: when VIS parity
check failed or the VIS code was unrecognized, the detector entered
DETECTED state permanently and never resumed scanning. Now it resets
to IDLE on validation failure and only enters DETECTED on success.
Also resets partial image progress counter between consecutive decodes
and adds SDR device claiming to general SSTV route to prevent conflicts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VISDetector._process_window() was calling self.reset() inside the
STOP_BIT handler, wiping self._buffer before feed() could advance
past the triggering window. All audio samples buffered after the
VIS STOP_BIT (the start of the first scan line) were silently
discarded, causing the image decoder to begin decoding mid-stream
with no alignment reference. The result was every scan line being
desynchronised from the first, producing the diagonal stripes and
scrambled colours seen in decoded images.
Fix: remove the premature reset() from _process_window(). The
STOP_BIT handler now sets state=DETECTED and returns the result.
A new remaining_buffer property exposes the post-VIS samples.
_decode_audio_stream() and decode_file() capture those samples
before calling reset(), then immediately feed them into the newly
created SSTVImageDecoder so decoding begins from sample 0 of
the first sync pulse.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If dump1090-mutability was installed by a previous run and later
removed (e.g. by apt removing it as a reverse dep), the symlink at
/usr/local/sbin/dump1090 is left pointing at a non-existent target.
cmd_exists finds the broken symlink and treats dump1090 as installed,
so the real install is skipped and running dump1090 gives
"No such file or directory".
Before the install check, resolve the command path and delete it if
it exists in PATH but is not executable (broken symlink).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The apt-removal approach caused cascading failures: removing librtlsdr0
swept out dump1090-mutability and other reverse deps, then source builds
reinstalled librtlsdr-dev (pulling librtlsdr0 back), and the dump1090
subshell crashed because kill "" (empty progress_pid after progress_pid=)
returned non-zero and fired the global ERR trap.
Switch to a targeted ldconfig priority file instead:
- Write /etc/ld.so.conf.d/00-local-first.conf containing /usr/local/lib
- Files named 00-* sort before aarch64-linux-gnu.conf alphabetically,
so ldconfig lists /usr/local/lib/librtlsdr.so.0 (Blog) first
- apt librtlsdr0, rtl-sdr, dump1090-mutability etc. are never touched
- Source build functions keep their unconditional apt_install librtlsdr-dev
Also fix the dump1090 EXIT trap: guard kill/wait against empty
progress_pid so it does not fire the ERR trap after a clean exit 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The global ERR trap (trap 'on_error $LINENO' ERR) fires on any non-zero
exit. After `kill $progress_pid`, `wait $progress_pid` returns 143
(128+SIGTERM), triggering the trap and aborting the build even when
make itself succeeded. Add `|| true` to all five wait calls in
install_dump1090_from_source_debian (inline and EXIT trap).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Blog drivers are installed, apt rtl-sdr/librtlsdr0/librtlsdr-dev
are removed to ensure the Blog library in /usr/local/lib is the only
one ldconfig sees. But four source-build functions each called
`apt_install librtlsdr-dev`, which re-pulled librtlsdr0 from apt and
immediately re-shadowed the Blog library.
Fix: each function now checks `pkg-config --exists librtlsdr` first;
if the Blog drivers (or any other /usr/local install) already provide
the headers and .pc file the apt install is skipped entirely.
Also add a post-removal guard in install_rtlsdr_blog_drivers_debian:
after apt removes librtlsdr0 it may silently sweep out dump1090-mutability
as a reverse dep. The guard detects this and rebuilds dump1090 from
source immediately, using the Blog drivers' headers via pkg-config.
Affected functions:
- install_dump1090_from_source_debian
- install_acarsdec_from_source_debian
- install_dumpvdl2_from_source_debian
- install_aiscatcher_from_source_debian
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removing only the rtl-sdr binary package left librtlsdr0 (the library)
installed at /lib/aarch64-linux-gnu/librtlsdr.so.0. ldconfig lists the
multiarch path before /usr/local/lib, so even the Blog driver binary
(/usr/local/bin/rtl_test) was loading the old apt library — which has
no R828D/V4 tuner support — causing the PLL-not-locked / deaf dongle
symptom.
Now remove rtl-sdr, librtlsdr0, and librtlsdr-dev together so the only
librtlsdr.so.0 in the ldconfig cache is the Blog drivers' copy in
/usr/local/lib.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs caused RTL-SDR dongles to be deaf after setup on Raspberry Pi:
1. The apt `rtl-sdr` package was left installed alongside the Blog
drivers, creating a binary/library ambiguity. Anything linking or
calling the apt binaries in /usr/bin used the non-V4-aware library
from /usr/lib instead of the Blog drivers in /usr/local. Fix: remove
the apt package immediately after a successful Blog driver build.
2. `blacklist_kernel_drivers_debian` returned early with "already
present" without ever running `modprobe -r`, so dvb_usb_rtl28xxu
could remain loaded and hold the device in DVB mode (rtl_test sees
the USB device but the tuner is unconfigured). Fix: always run the
module unload loop regardless of whether the blacklist file is new.
Also add `update-initramfs -u` so the blacklist survives reboots.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous logic installed rtl-sdr via apt first, then gated the Blog
driver install on cmd_exists rtl_test — which was always true, so V4
drivers were never installed. Replace with a yes/no prompt (default y,
backward-compatible) guarded by IS_DRAGONOS for pre-configured distros.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add missing setThemePreference() and setAnimationsEnabled() functions
to settings-manager.js; sync theme/animations dropdowns in _updateUI
- Fix base.html toggleTheme() saving to wrong localStorage key ('theme'
instead of 'intercept-theme'), causing theme not to persist in ADS-B
and AIS dashboards; also sync button icon and persist to server
- Add [data-theme="light"] CSS variable overrides to adsb_dashboard.css
and ais_dashboard.css so the dashboards respond to light theme
- Fix GPS sky view canvas (gps.js) to read grid/label colours from CSS
variables instead of hardcoded dark hex values; add MutationObserver
to redraw immediately on theme change
- Fix satellite_dashboard.html polar plot functions to read background,
accent and text colours from CSS variables
Closes#139
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The .icon base class (global-nav.css) forces display:inline-flex and
width/height of 18-20px, overriding the intended 48px size and causing
the SVG to render inline inside the h2 rather than as a block above it.
Override with display:block, explicit 48px dimensions, and auto margins
so the icon renders centred above the DISCLAIMER heading.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the | tail -5 filter with pip --quiet and 2>/dev/null to
silence 'Requirement already satisfied' lines and the harmless
send2trash metadata warning that were leaking to the terminal.
The import verification step still catches real install failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If lua_utils.cpp has no trailing newline the closing pragma was appended
directly to the last line (};#pragma GCC diagnostic pop), causing a
stray '#' compile error on GCC 13+ / Raspberry Pi OS Bookworm.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Raspberry Pi OS Bookworm the package is libvolk2-dev, not libvolk-dev.
Also soft-fail optional SDR hardware libs (libjemalloc, libnng, SoapySDR,
HackRF, LimeSuite) so a missing package no longer aborts the SatDump build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Weather Satellite:
- Fix duplicate event listeners on mode re-entry via locationListenersAttached guard
- Add suspend() to stop countdown/SSE stream when switching away from the mode
- Call WeatherSat.suspend() in switchMode() when leaving weathersat
- Fix toggleScheduler() to take the checkbox element as source of truth,
preventing both checkboxes fighting each other
- Reset isRunning/UI state after auto-capture completes (scheduler path)
- Always re-select first pass and reset selectedPassIndex after loadPasses()
- Keep timeline cursor in sync inside selectPass()
- Add seconds to pass ID format to avoid collisions on concurrent passes
- Improve predict_passes() comment clarity; fix trajectory comment
ADS-B dashboard:
- Batch altitude-colour trail segments into runs of same-colour polylines,
reducing Leaflet layer count from O(trail length) to O(colour changes)
for significantly better rendering performance with many aircraft
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single-threaded make on a Raspberry Pi 5 could take 5-10+ minutes
with no output, making the setup appear hung. Now uses all available
CPU cores and prints a "still compiling" heartbeat every 20s.
Also prints build log tail on failure for easier debugging.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the old SDR/RF, Wireless, Security, Space layout with a cleaner
five-group structure. Tracking (Aircraft, Vessels, APRS, GPS) becomes its
own top-level group; Meshtastic moves to Wireless; WebSDR and Spy Stations
move to Intel. Also fixes BT Locate overflow/min-height CSS.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New mode providing real-time space weather data from NOAA SWPC, NASA SDO,
and HamQSL APIs. Includes Kp index, solar wind, X-ray flux charts, HF band
conditions, D-RAP absorption maps, aurora forecast, solar imagery, flare
probability, and active solar regions. No SDR hardware required.
Bumps version to 2.20.0. Updates all documentation including README, FEATURES,
USAGE, UI_GUIDE, help modal, and GitHub Pages site.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update default ACARS frequencies for North America to 131.725/131.825 MHz and add ISS (145.825 MHz) as a selectable APRS frequency region.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix SatDump crash reported as "Capture complete" by collecting exit
status via process.wait() before checking returncode
- Fix PTY file descriptor double-close race between stop() and reader
thread by adding thread-safe _close_pty() helper with dedicated lock
- Fix image watcher missing final images by doing post-exit scans after
SatDump process ends, using threading.Event for fast wakeup
- Fix failed image copy permanently skipping file by only marking as
known after successful copy
- Fix frontend error handler not resetting isRunning, preventing new
captures after a crash
- Fix console auto-hide timer leak on rapid complete/error events
- Fix ground track and auto-scheduler ignoring shared ObserverLocation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The button is unnecessary since analytics expands the sidebar to
full width with no output panel to reveal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sidebar sections are collapsed by default on DOMContentLoaded. When
switching to analytics mode, expand all its sections so the dashboard
content is visible immediately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Analytics is a sidebar-only mode with no visuals container, so the
output panel was rendering on top of the analytics content. Add
analytics-active class to expand the sidebar full-width and hide
the output panel when in analytics mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The decoder output panel was not being hidden when entering analytics
mode, causing it to render on top of the analytics dashboard.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend cross-mode analytics to include ACARS/VDL2 message counts, APRS
stations, and Meshtastic messages. Refactor count helpers into reusable
_safe_len() and _safe_route_attr() utilities. Add health checks for
rtlamr, dmr, and meshtastic modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The analytics summary, health, and export were only reading from legacy
DataStores (app_module.wifi_networks, bt_devices) which the v2 WiFi and
Bluetooth scanners don't populate. Now checks v2 scanner singletons
first and falls back to legacy stores.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a unified analytics mode under the Security nav group that aggregates
data across all signal modes. Includes emergency squawk alerting (7700/7600/7500),
vertical rate anomaly detection, ACARS/VDL2-to-ADS-B flight correlation,
geofence zones with enter/exit detection for aircraft/vessels/APRS stations,
temporal pattern detection, RSSI history tracking, Meshtastic topology mapping,
and JSON/CSV data export.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When navigating away from the dashboard and back, the page reloads with
no knowledge of running decoders. Add status checks on page load to sync
UI state and reconnect SSE streams. Also add auto-reconnect on SSE error
with guard conditions to prevent loops when intentionally stopped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Check process exit code when SatDump terminates — non-zero exit now
emits an error status with the exit code instead of falsely reporting
a successful capture completion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
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>
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>
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>
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>
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>
New modes:
- BT Locate: SAR Bluetooth device location with GPS-tagged signal trail,
RSSI-based proximity bands, audio alerts, and IRK auto-extraction from
paired devices (macOS plist / Linux BlueZ)
- GPS: Real-time position tracking with live map, speed, heading, altitude,
satellite info, and track recording via gpsd
Bug fixes:
- Fix ABBA deadlock between session lock and aggregator lock in BT Locate
- Fix bleak scan lifecycle tracking in BluetoothScanner (is_scanning property
now cross-checks backend state)
- Fix map tile persistence when switching modes
- Use 15s max_age window for fresh detections in BT Locate poll loop
Documentation:
- Update README, FEATURES.md, USAGE.md, and GitHub Pages with new modes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SatDump is a large C++ project that can take 10-30 minutes to compile.
Previously all build output was sent to /dev/null, making it appear
hung. Now shows a progress message every 30 seconds, sets time
expectations upfront, and displays the build log on failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The upstream acarsdec uses pthread_tryjoin_np (a Linux-only GNU
extension) and has broken libacars linking on macOS. The setup script
now patches both issues at build time, along with the existing compiler
flag fix for ARM64.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Strip ITU-R M.493 phasing symbols (120-126) after dot pattern sync before
decoding message content. Fix MMSI BCD digit trimming direction and correct
test symbol encodings for position and MMSI edge cases.
Satellites added via CelesTrak import or TLE paste are now stored in
SQLite and survive page reloads and app restarts. Adds CRUD API
endpoints and wires frontend sidebar + dashboard to use them.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update README, FEATURES.md, USAGE.md, and GitHub Pages index.html with
all current modes including Sub-GHz analyzer, APRS, utility meters,
DMR digital voice, listening post, weather satellites, WebSDR, HF SSTV,
and AIS vessel tracking. Update mode count from 15+ to 20+.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add .gitignore entry for data/subghz/captures/ to prevent large
IQ recording files from being committed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The partially-added MLAT support was out of sync between config and
routes, causing an ImportError when importing adsb_bp. Remove all MLAT
additions from config, template UI/JS, and docs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Throttle audio waterfall rendering (50ms→200ms), eliminate per-frame
Array.from() allocation, drain stale pipe buffer before streaming,
increase chunk size to 8192, and remove debug logging from animation
hot paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Weather satellite decoding (NOAA APT & Meteor LRPT) was added in the
Dockerfile but setup.sh had no SatDump support, leaving local installs
with a broken weather satellite mode. Adds build-from-source functions
for both Debian and macOS, a check_optional entry, and prompted install
steps in both platform installers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds libgtk-3-dev to the apt-get remove list so it doesn't remain
in the final image. Runtime GTK libs stay for slowrx.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Created test_weather_sat_routes.py with 42 tests for all endpoints
- Created test_weather_sat_decoder.py with 47 tests for WeatherSatDecoder class
- Created test_weather_sat_predict.py with 14 tests for pass prediction
- Created test_weather_sat_scheduler.py with 31 tests for auto-scheduler
- Total: 134 test functions across 14 test classes
- All tests follow existing patterns (mocking, fixtures, docstrings)
- Tests cover happy paths, error handling, and edge cases
- Mock all external subprocess calls and HTTP requests
Co-authored-by: mitchross <6330506+mitchross@users.noreply.github.com>
Starting ffmpeg at decoder launch caused a pipe-buffer deadlock: ffmpeg
stdout filled up (~64KB on Linux) before the browser connected to the
audio stream, back-pressuring the entire pipeline and freezing dsd-fme
stderr output (no text data, no syncs, no calls).
New architecture: a mux thread always drains dsd-fme stdout to keep the
pipeline flowing. ffmpeg starts lazily per-client when /dmr/audio/stream
is requested (matching the listening post pattern). The mux forwards
decoded audio to the active ffmpeg with silence fill during voice gaps,
and discards audio when no client is connected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Digital voice is intermittent — dsd-fme only outputs PCM during active
voice transmissions. Without input, ffmpeg never wrote the WAV header
and the browser got an empty response. Add an audio bridge thread that
feeds 100ms silence chunks during voice gaps so ffmpeg always has input
and the browser receives a continuous WAV stream. Add auto-reconnect
on the frontend if the audio stream drops while the decoder is running.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stream decoded digital voice audio to the browser via ffmpeg pipeline
(dsd-fme 8kHz PCM → ffmpeg → 44.1kHz WAV → chunked HTTP). Persist
frequency/protocol/gain/ppm settings in localStorage so they survive
page navigation. Add bookmark system for saving and recalling frequencies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wait for server-side WebSocket stop confirmation before closing the
connection, ensuring the IQ process is fully terminated and the USB
device released. Add retry logic with back-off in the audio start
endpoint as defense-in-depth for any remaining timing gaps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The box-drawing character filter was dropping ANY line containing │ or ─,
including dsd-fme data lines that use these as column separators (e.g.
"DMR BS │ Slot 1 │ TG: 12345 │ SRC: 67890"). Now only filters lines
that are purely decorative (no alphanumeric content).
Also adds -J /dev/stderr so dsd-fme writes its event log to stderr
where we capture it, and debug logging of raw stderr lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
-fd means D-STAR in dsd-fme, not DMR — causing sync detection
(shared C4FM modulation) but no decoded data. DMR Simplex is -fs.
Also fix -o - (invalid in dsd-fme) to -o null for headless servers,
add D-STAR flag mapping, and handle TGT/SRC output format in parser.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dsd-fme remapped several flags from classic DSD: -fp is ProVoice (not
P25), -fi is NXDN48 (not D-Star), -fv doesn't exist. This caused P25
to trigger ProVoice decoding and D-Star to trigger NXDN48. Corrected
flag table and added C4FM modulation hints for better sync reliability.
Also fixes: device panel showing "DMR" regardless of protocol, signal
activity status flip-flopping between LISTENING and IDLE, and rtl_fm
squelch chopping the bitstream mid-frame. Adds PPM correction and
relax CRC controls for fine-tuning on marginal signals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Security: replace path traversal-vulnerable str().startswith() with
is_relative_to(), anchor path checks to app root, strip filesystem
paths from error responses, add decoder-level path validation.
Architecture: use safe_terminate/register_process for subprocess
lifecycle, replace custom SSE generator with sse_stream(), use
centralized validate_* functions, remove unused app.py declarations.
Bugs: add thread-safe singleton locks, protect _images list across
threads, move blocking process.wait() to async daemon thread, fix
timezone handling for tz-aware datetimes, use full path for image
deduplication, guard TLE auto-refresh during tests, validate
scheduler parameters to avoid 500 errors.
Docker: pin SatDump to v1.2.2 and slowrx to ca6d7012, document
INTERCEPT_IMAGE fallback pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve conflicts keeping local GSM tools in kill_all() process list
and weather satellite config settings while merging upstream changes
including GSM spy removal, DMR fixes, USB device probe, APRS crash
fix, and cross-module frequency routing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the backend has an active DMR session but the frontend lost track
(page refresh, broken flags causing silent running), clicking Start
returned 409 with no recovery path. Now the frontend resyncs on
"Already running" responses and checks backend status on tab activation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The _DSD_FME_PROTOCOL_FLAGS dictionary had every protocol flag wrong,
causing dsd-fme (the preferred binary) to receive invalid or mismatched
-f flags. Also fix orphaned process leak on startup failure and add
centralized input validation for frequency/gain/device.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enable sending discovered frequencies from the Listening Post scanner,
signal identification panel, and waterfall display directly to Pager,
433 Sensor, or RTLAMR decoder modes with one click.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Constrain modal height to viewport and make tab content scrollable
so the modal no longer falls off the bottom of the screen.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an external process (or stale handle from a crash) holds an SDR
device, claim_sdr_device() registry check passes but rtl_fm fails with
usb_claim_interface error -6. This adds a quick rtl_test probe inside
claim_sdr_device() so all modes get a clear error message before the
decoder pipeline is launched.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix infinite loop in updateAprsStationList: querySelectorAll returns a
static NodeList so the while(cards.length > 50) loop never terminated,
crashing the page. Use live childElementCount instead.
- Fix station list pushing map off-screen by adding overflow:hidden and
min-height:0 to flex containers so only the station list scrolls.
- Cap backend aprs_stations dict at 500 entries with oldest-eviction to
prevent unbounded memory growth.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove all GSM cellular intelligence features including tower scanning,
signal monitoring, rogue detection, crowd density analysis, and
OpenCellID integration across routes, templates, utils, tests, and
build configuration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove logging and cleanup_all_processes() from signal handler to
prevent deadlocks when another thread holds the logging or process lock.
Process cleanup is handled by the atexit handler instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The monitored tower may have CID=0 (partially decoded cell) which
OpenCellID can't geocode, leaving it without coordinates. The heatmap
now falls back through: monitored tower by ARFCN, any geocoded tower,
then observer location. Also tracks the monitored ARFCN so the fallback
can find the right tower even when CID matching fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The heatmap silently failed when: CID types mismatched (string vs number),
LAC wasn't checked (wrong tower matched), or no data existed yet (button
showed ON with no layer). Now coerces CID/LAC to Number for comparison,
validates coordinates with parseFloat, logs match diagnostics to console,
and only shows ON when the layer is actually rendered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a toggleable heatmap layer that visualizes crowd density data from
the existing /gsm_spy/crowd_density endpoint as a gradient overlay on the
map, with auto-refresh every 30s during active monitoring.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add explicit band name mapping from internal names to grgsm_scanner's
accepted -b values (GSM900, GSM850, DCS1800, PCS1900). Bands without
a valid grgsm_scanner equivalent (GSM800, EGSM900_EXT) are skipped
with a log message instead of crashing the scanner. Remove GSM800
from the dashboard band selector since it can't be scanned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Parse tshark GSM field values with int(value, 0) instead of int(value)
to auto-detect hex 0x-prefixed output (e.g. 0x039e for TMSI/LAC/CID).
Without this, every tshark line with hex values fails to parse, causing
0 devices to be captured during monitoring.
Also add API Keys tab to Settings modal for configuring OpenCellID key
via the UI (in addition to env var), with status display and usage bar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire SIGNALS/DEVICES/CROWD counters to monitor_heartbeat SSE data so
they update in real-time during monitoring. Redesign device list items
as richer cards with type badges, TA/distance, and observation counts.
Add clickable device detail modal with full device info and copy
support. Improve tower list with signal strength bars. Widen right
sidebar and bump list font sizes for readability.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Cache lookup now requires non-NULL lat/lon — previously a row with
NULL coordinates counted as a cache hit, returning {lat: None, lon: None}
which the frontend silently ignored (tower in list but no map pin)
- API response handler validates lat/lon exist before caching, preventing
error responses (status 200 with error body) from poisoning the cache
- On geocoding worker start, delete any existing poisoned cache rows
- Geocoding worker now logs "API key not configured" vs "rate limit
reached" so the actual problem is visible in logs
- API error responses now log the response body for easier debugging
The EGSM900 band table had start=925e6 but ARFCNs 0-124 use downlink
frequencies starting at 935 MHz (DL = 935 + 0.2*ARFCN). The 925 MHz
value is the E-GSM extension band (ARFCNs 975-1023).
This caused grgsm_livemon to tune 10 MHz too low — ARFCN 22 tuned to
929.4 MHz instead of 939.4 MHz, receiving no GSM frames and producing
zero GSMTAP packets for tshark to capture.
Also adds EGSM900_EXT band (ARFCNs 975-1023, DL 925.2-934.8 MHz)
and diagnostic logging in the monitor thread to track raw tshark
line counts vs parsed packets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- /lookup_cell and /detect_rogue rejected CID=0 towers because
`all([..., cid])` is falsy when cid=0; use `is not None` checks
- can_use_api() now returns False when GSM_OPENCELLID_API_KEY is empty,
preventing the geocoding worker from wasting daily quota on doomed calls
- /lookup_cell returns 503 with clear message when API key not configured
- parse_tshark_output uses rstrip('\n\r') instead of strip() to preserve
leading empty tab-separated fields (strip() ate leading tabs, shifting
all columns when the first field was empty)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The display filter `gsm_a.tmsi || e212.imsi` was too restrictive —
paging requests use different field paths for TMSI so nothing matched.
The capture filter (-f 'udp port 4729') already limits to GSMTAP, and
the parser discards rows without TMSI/IMSI identifiers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When tshark field discovery finds no valid candidate for a logical field
(e.g. timing_advance, cellid), the old code fell back to the first
candidate name even though it was known to be invalid. This caused tshark
to exit immediately with "Some fields aren't valid".
Now fields resolve to None when no valid candidate exists, and the tshark
command is built using only validated fields. The parser dynamically maps
columns via field_order instead of assuming a fixed 5-column layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tshark -G fields lists fields that exist in the protocol tree but
aren't all valid for -T fields -e extraction. Changed discovery to
actually test candidates by running tshark -T fields -e <field> -r
/dev/null and parsing stderr for invalid field names. This correctly
identifies which fields work for extraction on the installed version.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tshark field names differ between Wireshark versions (3.x vs 4.x):
- 3.x: gsm_a.rr.timing_advance, gsm_a.tmsi, gsm_a.cellid
- 4.x: gsm_a_rr.timing_adv, gsm_a_dtap.tmsi, e212.ci
Added _discover_tshark_fields() that queries `tshark -G fields` to
find which field names are available on the installed version, then
uses the correct ones for the capture filter and field extraction.
Results are cached after first discovery.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend: monitor_thread sends periodic monitor_heartbeat events (every
5s) with elapsed time, packet count, and device count so the frontend
knows monitoring is active.
Frontend: new monitoring overlay replaces scan progress bar when
auto-monitor starts. Shows pulsing green indicator, ARFCN being
monitored, live elapsed timer, packet/device counts, and
"Listening..."/"Capturing" activity state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add capture filter (-f 'udp port 4729') to only capture GSMTAP packets
- Add -l flag for line-buffered output on live capture
- Add early exit detection for tshark with stderr capture
- Add stderr reader thread in monitor_thread for ongoing tshark diagnostics
- Clean up grgsm_livemon if tshark fails to start
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Set QT_QPA_PLATFORM=offscreen for both grgsm_livemon and
grgsm_scanner to prevent SIGABRT when no X11 display is available.
grgsm_livemon uses GNU Radio which loads Qt plugins — without a
display, Qt aborts with "could not connect to display".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Repositioned progress indicator from right sidebar to a full-width
overlay at the top of the map panel
- Added animated spinning icon, glowing progress bar, blurred backdrop
- Centered layout with max-width constraint for readability
- Progress bar and status text more visible during active scans
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add pre-flight checks (shutil.which) for grgsm_livemon and tshark
- Capture stderr when grgsm_livemon exits immediately (exit code 1)
- Start background stderr reader thread for ongoing livemon diagnostics
- Add idle_count grace period in SSE stream to handle scanner→monitor
transition without premature disconnect
- Forward monitor failure errors to SSE for frontend display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The scanner and monitor are mutually exclusive (both need the SDR).
Previously auto-monitor tried to start mid-scan (causing device
conflicts) and required 3 towers (rarely achieved with weak signals).
Now after the first scan completes:
- If any towers were found, automatically stop scanner and start
grgsm_livemon + tshark on the strongest tower's ARFCN
- SDR handoff is clean (scanner process has already exited)
- If monitor fails to start, scanner loop resumes
- Scanner thread's finally block preserves SDR allocation when
monitor has taken over
- Frontend shows "Monitoring ARFCN X for devices..." status
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Forward scanner progress (%) and status to SSE stream
- Show progress bar and scan status in TRACKED TOWERS panel
- Send scan_complete event with tower count and duration
- Fix Europe BAND_CONFIG: only EGSM900 is recommended (GSM850/GSM800
are rarely used in Europe and waste scan time)
- DCS1800 available but not recommended (RTL-SDR sensitivity is lower)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CID=0 with valid MCC/MNC means the scanner found the cell but didn't
decode System Information 3/4 (which carries the Cell ID). These are
still valid towers worth displaying. Only filter when MCC=0 AND MNC=0
(truly unidentified signals).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend:
- Filter out CID=0 and MCC=0 entries (ARFCNs with no decoded cell identity)
Frontend:
- Move stats update before coordinate check so towers always counted
- Fix signal_strength display using null check instead of || (0 is falsy)
- Show operator name, frequency, and status in tower detail panel
- Show "Located" indicator in tower list for geocoded towers
- Fix selectTower crash when tower has no coordinates
- Update placeholder text to "Select a tower from the list"
- Add try/catch to selectTower for error resilience
Tests:
- Add tests for CID=0 and MCC=0 filtering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Send all existing towers on SSE connect (fixes data loss on reconnect)
- Fix tower.signal -> tower.signal_strength field name in frontend
- Fix TypeError crash in selectTower when tower has no coordinates
- Add Connection: keep-alive header to SSE response
- Add comprehensive console.log debugging for SSE data flow
- Handle error/disconnected SSE event types in frontend
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
grgsm_scanner is a Python/GNU Radio script, so stdbuf has no effect.
Setting PYTHONUNBUFFERED=1 in the subprocess env forces Python to
flush stdout on every write, enabling real-time scan output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
grgsm_scanner fully buffers stdout when piped, so scan results never
reach Python until the buffer fills or process exits. Wrapping with
stdbuf -oL forces line-buffered output for real-time data streaming.
Also increased scan timeout from 120s to 300s since scanning 4 bands
legitimately takes 2-3 minutes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The ptrkrysik/gr-gsm repo uses SWIG which is incompatible with
GNU Radio 3.10+. The bkerler fork supports modern GNU Radio and
builds successfully on current Ubuntu/Debian systems.
Updated all references in Dockerfile, setup.sh, dependencies.py,
and error messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
apt-packaged gr-gsm aborts with SIGABRT (-6) due to duplicate FSM
registration in libosmocore. Setting this env var suppresses the
fatal assertion, allowing grgsm_scanner to run normally.
Applied to both scanner and livemon subprocess spawns.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
grgsm_scanner exits in <300ms with osmo_fsm assertion error due to
libosmocore incompatibility. Added crash detection: if process exits
in <5s with non-zero code, counts as crash. After 3 crashes, stops
retrying and sends error to SSE stream so the UI can display it.
Also drains remaining queue items after process exits and logs exit
code and scan duration for diagnostics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Global LOG_LEVEL defaults to WARNING, silencing all INFO/DEBUG logs.
GSM Spy needs verbose logging for scanner diagnostics. Override the
module logger level to DEBUG so scanner output is always visible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gsm_spy.py used logging.getLogger() directly which returns a bare
logger with no handler. The parent 'intercept' logger has
propagate=False, so all GSM Spy logs were silently dropped.
Now uses utils.logging.get_logger() which adds a stderr handler
and sets the log level, matching all other route modules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
grgsm_scanner (like many GNU Radio tools) writes scan results to
stderr, not stdout. The stderr reader was only logging at debug
level and discarding lines. Now feeds stderr into the parse queue.
Also added info-level logging for all scanner output lines to aid
debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Parser expected pipe-delimited table rows but grgsm_scanner outputs
comma-separated key-value pairs like:
ARFCN: 975, Freq: 925.2M, CID: 13522, LAC: 38722, MCC: 262, MNC: 1, Pwr: -58
This was the root cause of no data appearing in GSM Spy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
stop_scanner() cleared gsm_spy_active_device without calling
release_sdr_device(), so the device stayed claimed in the registry.
The scanner thread's finally block then saw None and skipped release.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
UI was sending GSM900 but backend REGIONAL_BANDS expects EGSM900
for Europe and Asia regions, causing validation rejection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GSM Spy was failing with FileNotFoundError because grgsm_scanner
wasn't installed. These tools are now installed automatically by
setup.sh (both Debian and macOS) and included in the Dockerfile,
matching how other tools like multimon-ng and ffmpeg are handled.
- setup.sh: Remove ask_yes_no prompts for gr-gsm and tshark, install
unconditionally; add check_recommended tier for final summary
- Dockerfile: Add tshark to apt layer, add gr-gsm RUN layer with
apt-then-source-build fallback, preseed debconf for tshark
- gsm_spy.py: Add shutil.which pre-check in start_scanner route,
catch FileNotFoundError in scanner_thread to stop retry loop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- kill_all() now resets gsm_spy_scanner_running and related state so
the scanner thread stops after killall
- scanner_thread sets flag to False instead of None on exit
- Restore alert_rules, alert_events, recording_sessions tables and
wifi_clients column removed by PR in database.py
- Escape all server-sourced values in analysis modals with escapeHtml()
- Reset gsm_towers_found/gsm_devices_tracked on stop to prevent
counter drift across sessions
- Replace raw terminate/kill with safe_terminate() in scanner_thread
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The function-strip CSS was never linked in index.html, causing all
strip items to render as unstyled stacked elements instead of a
horizontal flex bar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move waterfall controls from the sidebar into a function-strip bar inside
#listeningPostVisuals so they sit directly above the waterfall canvas.
Also fix the "SDR device in use" error when clicking a waterfall frequency
to listen — the WebSocket waterfall's device claim wasn't being released
before the audio start request because the backend cleanup hadn't finished.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Zooming caused "I/Q capture process exited immediately" because the client
closed the WebSocket and opened a new one, racing with the old rtl_sdr
process releasing the USB device. Now zoom/retune sends a start command on
the existing WebSocket, and the server adds a USB release delay plus retry
loop when restarting capture within the same connection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The FFT pipeline produces power values in the ~0-60 dB range for
normalized IQ data, but quantize_to_uint8 used a hardcoded range
of -90 to -20 dB. Every bin saturated to 255, producing a uniform
yellow waterfall with no signal differentiation.
Now auto-scales to the actual min/max of each frame so the full
colour palette is always used.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After a WebSocket handler exits, flask-sock returns a Response to
Werkzeug which writes "HTTP/1.1 200 OK..." on the still-open socket.
Browsers see these HTTP bytes as a malformed WebSocket frame, causing
"Invalid frame header".
Now the handler explicitly closes the raw TCP socket after the
WebSocket close handshake, so Werkzeug's write harmlessly fails.
Applied to both waterfall and audio WebSocket handlers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
simple-websocket 1.1.0's receive(timeout=N) returns None on timeout
instead of raising TimeoutError. The handler treated None as
"connection closed" and broke out of the loop, causing Werkzeug to
write its HTTP 200 response on the still-open WebSocket socket.
The browser saw those HTTP bytes as an invalid WebSocket frame.
Now checks ws.connected to distinguish timeout (None + connected)
from actual close (None + not connected).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
agents.js syncLocalModeStates() expects these endpoints to check
whether each mode is running locally. Both were missing, causing
404 errors on mode switch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The before_request require_login hook was returning a 302 redirect
for WebSocket upgrade requests, which browsers report as "Invalid
frame header". WebSocket requests don't always carry session cookies
reliably. Allow /ws/ paths through the login check since the page
that initiates these connections already requires authentication.
Also keeps the prior fix: serialize WebSocket sends through a queue
to avoid concurrent read/write on the non-thread-safe simple-websocket.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The fft_reader thread was calling ws.send() concurrently with
ws.receive() in the main loop. simple-websocket is not thread-safe
for simultaneous read/write, corrupting frame headers. Now the reader
thread enqueues frames and only the main loop touches the WebSocket.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the batch rtl_power SSE pipeline with continuous I/Q streaming
via WebSocket for smooth ~25fps waterfall display. The server captures
raw I/Q samples (rtl_sdr/rx_sdr), computes Hann-windowed FFT, and
sends compact binary frames (1035 bytes vs ~15KB JSON, 93% reduction).
Client falls back to existing SSE path if WebSocket is unavailable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Waterfall clicks now auto-select the correct modulation for the frequency
band (e.g., WFM for FM broadcast, AM for airband) instead of using whatever
modulation was last selected. Adds a hover tooltip showing frequency and
suggested modulation. Fixes the kill-all notification to show a clean
"All processes stopped" message instead of listing "bluetooth_scanner".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a phosphor-persistence waveform scope showing audio RMS/peak
levels during ISS SSTV and General SSTV decoding, matching the
existing pager scope pattern with a purple color scheme.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enable -M level on rtl_433 to include RSSI/SNR in decoded JSON, extract
signal levels and push scope events to the SSE stream. Renders a green-
themed canvas oscilloscope showing signal strength pulses on packet decode
with amber SNR indicator and decay between packets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tap the rtl_fm → multimon-ng audio pipeline via a relay thread to extract
RMS/peak amplitude levels and render a 60fps canvas oscilloscope during
pager decoding, giving visual feedback of RF activity before messages are
fully decoded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reverts IQ pipeline and removes syncWaterfallToFrequency calls from
pager, sensor, rtlamr, DMR, SSTV, and SSTV general modes. Waterfall
is now exclusive to listening post mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace rtl_fm/rtl_433 with rtl_sdr for raw IQ capture when available,
enabling a Python IQ processor to compute FFT for the waterfall while
simultaneously feeding decoded data to multimon-ng (pager) or rtl_433
(sensor). Falls back to the legacy pipeline when rtl_sdr is unavailable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. utils/weather_sat.py — Added delete_all_images() method that globs for *.png, *.jpg, *.jpeg in the output dir, unlinks each, clears _images list, and returns the
count.
2. routes/weather_sat.py — Added DELETE /weather-sat/images route that calls decoder.delete_all_images() and returns {'status': 'ok', 'deleted': count}.
3. static/js/modes/weather-satellite.js:
- Added currentModalFilename state variable
- renderGallery() now sorts images by timestamp descending, groups by date using toLocaleDateString(), renders date headers spanning the grid, and adds a delete
overlay button on each card
- showImage() accepts a filename param, stores it in currentModalFilename, and creates a modal toolbar with a delete button
- Added deleteImage(filename) — confirm dialog → DELETE /weather-sat/images/{filename} → filter from array → re-render + close modal
- Added deleteAllImages() — confirm dialog → DELETE /weather-sat/images → clear array → re-render
- Exposed deleteImage, deleteAllImages, and _getModalFilename in public API
4. static/css/modes/weather-satellite.css:
- Added position: relative to .wxsat-image-card
- .wxsat-image-actions — absolute top-right overlay, hidden by default, appears on card hover
- .wxsat-image-actions button — dark background, turns red on hover
- .wxsat-date-header — full-grid-width date separator with dimmed uppercase text
- .wxsat-modal-toolbar — absolute top-left in modal for the delete button
- .wxsat-modal-btn.delete — turns red on hover
- .wxsat-gallery-clear-btn — subtle icon button, pushed right via margin-left: auto, turns red on hover
- Updated .wxsat-gallery-header from justify-content: space-between to gap: 8px for proper 3-child layout
5. templates/index.html — Added clear-all trash button with SVG icon in the gallery header, wired to WeatherSat.deleteAllImages().
The reader thread loop checks self._running but it was being set to
True after _start_satdump() returned, which is after the thread
already started. The thread would see _running=False and exit
immediately without reading any SatDump output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SatDump writes to stderr via fwrite() with its custom logger. When
stderr is redirected to a pipe, C runtime fully buffers it. Neither
stdbuf nor bufsize settings help since SatDump doesn't use stdio for
output.
PTY (pseudo-terminal) makes SatDump think it's writing to a real
terminal, which disables buffering. Also strips ANSI escape codes
from the output and properly handles \r progress lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merges upstream changes into fork while preserving weather satellite
(NOAA APT/Meteor LRPT via SatDump), rtlamr, multi-arch build, and
decoder console features from our branch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Kill audio pipeline when startup produces no data instead of leaving
zombie processes running
- Skip unnecessary 1s USB release delay when no processes were active
- Remove racy fresh=1 pipeline restart from stream endpoint
- Await stopScanner() before starting direct listen to prevent race
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allow users to override the pgdata volume mount via PGDATA_PATH env var,
enabling external storage (e.g. USB) for ADS-B history. Defaults to
./pgdata for backwards compatibility.
Based on PR #88 by JamesIOmete, rebased cleanly onto main.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stream threads for sensor, pager, acars, rtlamr, dmr, and dsc modes
never called release_sdr_device() when their SDR process crashed,
leaving devices permanently locked in the registry. Also fixes orphaned
companion processes (rtl_fm, rtl_tcp) not being killed on crash, start
path failures leaking processes, DMR stop handler missing lock, and
listening post/audio websocket pkill nuking all system-wide rtl_fm
processes. Wires up register_process()/unregister_process() so the
atexit/signal cleanup safety net actually works, and adds rtl_tcp,
rtl_power, rtlamr, ffmpeg to the killall endpoint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The RiskLevel.NEEDS_REVIEW enum value was 'review' but the
devices_by_risk dict and all summary keys used 'needs_review',
causing a KeyError during sweep correlation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix infinite CPU spin in PD120 decoding caused by a 1-sample rounding
mismatch between line_samples (24407) and the sum of sub-component
samples (24408). The feed() while loop would re-enter _decode_line()
endlessly when the buffer was too short by 1 sample. Added a stall
guard that breaks the loop when no progress is made.
Fix false "leader tone detected" in the signal monitor by requiring
the detected tone to dominate the other tone by 2x, matching the
approach already used by the VIS detector.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the WiFi interface is down (e.g. USB adapter not activated),
scanning fails with "Network is down" errors. Now the scanner
proactively checks interface state via /sys/class/net and brings
it up using ip link (or ifconfig fallback) before attempting scans,
with a retry loop if the initial scan still fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Receiver Count section had no <h3> so it didn't get collapsible
panel styling, rendering as a small out-of-place rectangle. The count
is already shown in the main receiver list panel so this was redundant.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Static minZoom: 2 wasn't enough for tall containers. Now calculate
minZoom from actual container height so tiles always cover the visible
area. Also set map background to match CartoDB dark tile ocean color
so any remaining edge at extreme latitudes blends seamlessly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add maxBounds to limit vertical panning to ±85° latitude and set
minZoom to 2 so tiles always cover the visible area. Prevents the
large black bands above and below the map tiles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The dmrRawOutput div was rendering garbled box-drawing characters from
the dsd-fme ASCII art banner below the signal activity canvas. Remove
the div and filter banner lines (box-drawing chars, version info) in
the parser so they never become events.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The stream thread used a blocking readline() with no timeout, so once
DSD finished outputting its startup banner there were no more events
until actual signal activity. The frontend decayed to zero and appeared
dead. If DSD crashed, the synthesizer state never transitioned to
'stopped' so there was no visual or textual indication of failure.
- Use select() with 1s timeout on DSD stderr to avoid indefinite block
- Send heartbeat events every 3s while decoder is alive but idle
- Detect DSD crashes: capture exit code and remaining stderr, send as
'crashed' status with details and show notification to user
- Frontend properly transitions synthesizer to 'stopped' on process
death (was only happening on user-initiated stop)
- Increase idle breathing amplitude so LISTENING state is clearly
visible (0.12 +/- 0.06 vs old 0.05 +/- 0.035)
- Release device reservation on crash, not just user stop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DMR was missing checkDeviceAvailability/reserveDevice/releaseDevice
calls that other modes (SSTV, listening post) use, so the device
dropdown showed device 0 as available even when another process held
it. Also detect USB claim errors from rtl_fm and surface a clear
message telling the user to pick a different device.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The DSD stderr parser had regex ordering bugs that swallowed voice and
call events as bare slot events, and only matched classic dsd output
format (not dsd-fme). Unmatched lines were silently dropped, leaving
the signal activity panel with nothing to display.
- Reorder regex checks: TG/Src before voice before slot
- Support dsd-fme comma-separated format (TG: x, Src: y)
- Make bare slot regex strict (only standalone "Slot N" lines)
- Forward unmatched DSD lines as raw events for diagnostics
- Add LISTENING state to signal activity panel for raw output
- Show raw decoder output text below synthesizer canvas
- Fix test mocks for find_dsd() tuple return value
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The APRS route called app_module.reserve_sdr_device() which does not
exist, causing an AttributeError that Flask returned as an HTML error
page. The frontend then failed to parse it as JSON, showing
"Unexpected token '<'" to the user. Fixed to use claim_sdr_device()
which is the correct function used by all other modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SSTV was not claiming/releasing SDR devices through the centralized
registry, so the device state panel always showed the device as idle
during SSTV use. Added claim_sdr_device/release_sdr_device on the
backend and reserveDevice/releaseDevice on the frontend, matching the
pattern used by all other modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users can now manage decoded SSTV images with download and delete actions
accessible from hover overlays on gallery cards, the full-size image modal
toolbar, and a "Clear All" button in the gallery header. Both ISS and
General SSTV modes are supported.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The decode canvas was always black because nothing drew on it. Now the
backend encodes partial JPEG snapshots every 5% progress and the frontend
uses an <img> tag with in-place DOM updates instead of recreating innerHTML
on every SSE event.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shows the current VIS detection state machine position (Idle, Leader,
Break, Start bit, Data bits, etc.) in the signal monitor. This helps
diagnose why decoding may not be starting - e.g. if the VIS detector
is stuck in Idle despite a leader tone being present, the signal may
not contain a valid VIS header.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Doppler tracking thread emits detecting events every 5s from a
separate thread, unaware of decode state. The previous to_dict() change
included signal_level for ALL detecting events, causing the frontend to
replace the decode progress canvas with the signal monitor mid-decode.
Fix: use None as default for signal_level so only signal-metrics events
(which explicitly set the value) include the field. Also add a frontend
guard to ignore detecting events while the UI is in decoding state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The to_dict() method was skipping signal_level when it was 0, so the
frontend never received the field and never rendered the monitor.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shows RMS audio level bar and SSTV tone classification (leader/sync/noise)
via SSE during detecting mode, replacing the static "Listening..." state
with actionable signal feedback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When rtl_fm exits unexpectedly, read its stderr output to diagnose
the failure (no device, permission denied, etc.) and include the
error message in both the server log and the SSE progress event
sent to the browser.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three bugs preventing the live SSTV pipeline from working:
1. Race condition: self._running was set AFTER starting the decode
thread, so the thread checked the flag, found it False, and exited
immediately without ever processing audio.
2. Ghost running state: when the decode thread exited (e.g. rtl_fm
died), self._running stayed True. The decoder reported as running
but was dead, and subsequent start() calls returned without doing
anything - permanently stuck until app restart.
3. VIS detection fragility: unclassifiable windows at tone transition
boundaries (mixed energy from two tones) caused the state machine
to reset from LEADER/BREAK states back to IDLE, dropping valid
VIS headers on real signals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
slowrx is a GTK GUI app that doesn't support CLI usage, so the SSTV
decoder was silently failing. This replaces it with a pure Python
implementation using numpy and Pillow that supports Robot36/72,
Martin1/2, Scottie1/2, and PD120/180 modes via VIS header auto-detection.
Key implementation details:
- Generalized Goertzel (DTFT) for exact-frequency tone detection
- Vectorized batch Goertzel for real-time pixel decoding performance
- Overlapping analysis windows for short-window frequency estimation
- VIS header detection state machine with parity validation
- Per-line sync re-synchronization for drift tolerance
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
getBoundingClientRect on the canvas itself (sized via CSS width:100%)
instead of parentElement with arbitrary offset, preventing zero-width
canvas when flex layout timing varies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Event-driven spring-physics bar visualization reacting to SSE events
(sync/call/voice) with HSL color coding and center-outward ripple effects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rtl_fm stderr was sent to DEVNULL, hiding the actual failure reason
(rc=1). Now captured and surfaced in the error response. Also drains
rtl_fm stderr during normal operation to prevent pipe blocking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use -o - (stdout) instead of -o /dev/null for audio output, as
dsd-fme expects specific output targets. Remove -N flag which may
cause issues in headless mode. Add stderr capture on pipeline
failure for better error messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dsd-fme uses different protocol flags than classic dsd (e.g. -fs for
DMR instead of -fd, -f1 for P25 instead of -fp). Add -N flag to
disable ncurses terminal which is required when reading from stdin pipe.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Check for dsd-fme binary (common fork) before falling back to dsd.
Disable audio output with -o /dev/null to prevent PulseAudio
connection failures when running under sudo.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- DMR/P25 digital voice decoder mode with DSD-FME integration
- WebSDR mode with KiwiSDR audio proxy and websocket-client support
- Listening post waterfall/spectrogram visualization and audio streaming
- Dockerfile updates for mbelib and DSD-FME build dependencies
- New tests for DMR, WebSDR, KiwiSDR, waterfall, and signal guess API
- Chart.js date adapter for time-scale axes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a general-purpose SSTV decoder alongside the existing ISS SSTV mode,
supporting USB/LSB/FM modulation on common amateur radio HF/VHF/UHF
frequencies (14.230 MHz USB, 3.845 MHz LSB, etc.) with auto-detection
of modulation from preset frequency table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add SDR device reservation to prevent conflicts with other modes, and
capture rtl_fm stderr so actual error messages are reported to the user
instead of a generic exit code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix SDR device stuck claimed on capture failure via on_complete callback
- Improve SatDump output parsing to emit all lines (throttled 2s) for real-time feedback
- Extract shared pass prediction into utils/weather_sat_predict.py with trajectory/ground track support
- Add auto-scheduler (utils/weather_sat_scheduler.py) using threading.Timer for unattended captures
- Add scheduler API endpoints (enable/disable/status/passes/skip) with SSE event notifications
- Add countdown timer (D/H/M/S) with imminent/active glow states
- Add 24h timeline bar with colored pass markers and current-time cursor
- Add canvas polar plot showing az/el trajectory arc with cardinal directions
- Add Leaflet ground track map with satellite path and observer marker
- Restructure to 3-column layout (passes | polar+map | gallery) with responsive stacking
- Add auto-schedule toggle in strip bar and sidebar
- Add rtlamr (Go utility meter decoder) to Dockerfile
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each SDR mode now includes frequency-specific antenna guidance:
- Pager: VHF/UHF dipole info for 153/929 MHz bands
- 433 MHz Sensors: quarter-wave ground plane for ISM band
- Utility Meters: 912 MHz stock antenna tips and upgrades
- APRS: 2m band dipole and commercial options for 144.39 MHz
- SSTV: V-dipole for ISS reception at 145.800 MHz
- AIS: marine VHF antenna for 162 MHz vessel tracking
- Listening Post: wideband discone recommendation with band table
- Meshtastic: LoRa 915/868 MHz antenna upgrades and placement
- ADS-B: 1090 MHz collinear, commercial options, LNA/placement
Each guide includes antenna type, element lengths, placement tips,
and a quick reference table with key specs for the mode.
https://claude.ai/code/session_01FjLTkyELaqh27U1wEXngFQ
Uncomment and enable the ./data:/app/data volume mount on both the
basic and history service profiles. This persists decoded weather
satellite images, the SQLite database, and other data across
container rebuilds. Critical for Docker-only deployments.
https://claude.ai/code/session_01FjLTkyELaqh27U1wEXngFQ
Add defensive typeof checks before referencing the Updater global in
loadUpdateStatus() and checkForUpdatesManual() so the settings panel
shows a helpful message instead of crashing. Also swap script load
order so updater.js loads before settings-manager.js.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add slowrx SSTV decoder build with required deps (libsndfile1,
libgtk-3-dev, libasound2-dev, libfftw3-dev) for arm64/RPi5 support
- Enable USB device passthrough (/dev/bus/usb) on both service profiles
- Add 'basic' profile to main intercept service for explicit selection
- Fix intercept-history container_name conflict (was duplicating 'intercept')
https://claude.ai/code/session_01FjLTkyELaqh27U1wEXngFQ
Replace fragile platform-specific WiFi detection with the same
scanner._detect_interfaces() used by the actual scanning code,
eliminating false "No wireless interfaces found" warnings.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use networksetup instead of deprecated airport utility for macOS WiFi detection
- Fix SDRDevice attribute access (use getattr instead of dict .get())
- Move Detected Threats panel next to RF Signals in 2-column grid
- Always run correlation/identity analysis at sweep end, even if stopped by user
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The sweep loop's WiFi/BT/RF scan processing had unprotected
timeline_manager.add_observation() calls that could crash an entire
scan iteration, silently preventing all device events from reaching
the frontend. Additionally, scan interval timestamps were only updated
at the end of processing, causing tight retry loops on persistent errors.
- Wrap timeline observation calls in try/except for all three protocols
- Move last_*_scan timestamp updates immediately after scan completes
- Add per-device try/except so one bad device doesn't block others
- Emit sweep_progress after WiFi scan for real-time status visibility
- Log warning when WiFi scan returns 0 networks for easier diagnosis
- Add known_device and score_modifier fields to correlation engine
- Add TSCM scheduling, cases, known devices, and advanced WiFi indicators
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Open Notify API (api.open-notify.org) is frequently unreliable,
causing 5-second timeout delays on every ISS position request.
Promote wheretheiss.at as the primary API in both satellite.py
and sstv.py, demoting Open Notify to fallback.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The innerHTML rebuild on every SSE event was destroying and recreating
DOM elements under the cursor, causing rapid mouseenter/mouseleave
cycling. Now defers DOM rebuilds while hovering and debounces rapid
update calls with a 200ms window.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Separate SVG translate positioning from CSS hover scale by nesting
device elements in two groups, preventing the CSS transform from
overriding the position and causing rapid mouseenter/mouseleave cycling.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add missing entries for v2.12.1, v2.13.0, and v2.13.1 to
CHANGELOG.md. Update config.py CHANGELOG highlights to reflect
UI overhaul, signal scanner rewrite, and WiFi client fix.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The /wifi/v2/clients endpoint was returning all clients regardless
of query parameters, because a duplicate route in wifi.py took
precedence over the filtered one in wifi_v2.py. Added bssid,
associated, and min_rssi filtering to the active route.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add retry mechanism (3 attempts) for usb_claim_interface errors when
the SDR device hasn't been fully released by a previous process. Also
kill rtl_power alongside rtl_fm during cleanup and increase the USB
release delay.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
More robust approach:
- align-items: stretch !important on controls-bar
- margin-top: auto on control-group-items to push to bottom
- Specific selector for controls-bar > control-group
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use align-items: stretch on controls-bar to make all control
groups the same height, and justify-content: space-between on
control-group to push content to top/bottom within each box.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change from stretch to flex-end to ensure control group
bottom edges stay aligned regardless of varying heights.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change align-items from center to stretch so control groups
of varying heights align at top and bottom instead of floating.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduces an 'optionals' dependency group in pyproject.toml. There was a discrepancy because they had been added to requirements.txt at some point during the last few commits but not to .toml. Update on test_requirements.py to include and validate these optional dependencies. Enhances test logic to ensure all main, dev, and optional dependencies are checked for environment consistency.
2026-02-01 17:03:00 +01:00
413 changed files with 108605 additions and 19286 deletions
All notable changes to iNTERCEPT will be documented in this file.
## [2.26.9] - 2026-03-14
### Fixed
- **ADS-B bias-t support for RTL-SDR Blog V4** — When dump1090 lacks native `--enable-biast` support, the system now falls back to `rtl_biast` (from RTL-SDR Blog drivers) to enable bias-t power before starting dump1090. The Blog V4's built-in LNA requires bias-t to receive ADS-B signals. (#195)
---
## [2.26.8] - 2026-03-14
### Fixed
- **acarsdec build failure on macOS** — `HOST_NAME_MAX` is Linux-specific (`<limits.h>`) and undefined on macOS, causing 3 compile errors in `acarsdec.c`. Now patched with `#define HOST_NAME_MAX 255` before building. Also fixed deprecated `-Ofast` flag warning on all macOS architectures (was only patched for arm64). (#187)
---
## [2.26.7] - 2026-03-14
### Fixed
- **Health check SDR detection on macOS** — `timeout` (GNU coreutils) is not available on macOS, causing `rtl_test` to silently fail and report "No RTL-SDR device found" even when one is connected. Now tries `timeout`, then `gtimeout` (Homebrew coreutils), then falls back to a background process with manual kill. (#188)
---
## [2.26.6] - 2026-03-14
### Fixed
- **Oversized branded 'i' logo on dashboards** — `.logo span { display: inline }` in dashboard CSS had higher specificity (0,1,1) than `.brand-i { display: inline-block }` (0,1,0), forcing the branded "i" SVG to render as inline which ignores width/height. Added `.logo .brand-i` selector (0,2,0) to retain `inline-block` display. (#189)
---
## [2.26.5] - 2026-03-14
### Fixed
- **Database errors crash entire UI** — `get_setting()` now catches `sqlite3.OperationalError` and returns the default value instead of propagating the exception. Previously, if the database was inaccessible (e.g. root-owned `instance/` directory from running with `sudo`), the `inject_offline_settings` context processor would crash every page render with a 500 Internal Server Error. (#190)
---
## [2.26.4] - 2026-03-14
### Fixed
- **Environment Configurator crash** — `read_env_var()` crashed with "Setup failed at line 2333" when `.env` existed but didn't contain the variable being looked up. `grep` returned exit code 1 (no match), which `pipefail` propagated and `set -e` turned into a fatal error. Fixed by appending `|| true` to the pipeline. (#191)
---
## [2.26.3] - 2026-03-13
### Fixed
- **SatDump AVX2 crash** — SatDump now compiles with `-march=x86-64` on x86_64 platforms (Docker and `setup.sh`), preventing "Illegal instruction" crashes on CPUs without AVX2. SIMD plugins still use runtime detection for acceleration on capable hardware. (#185)
---
## [2.26.2] - 2026-03-13
### Fixed
- **Docker startup crash** — `.dockerignore` excluded the entire `data/` directory, which is now a Python package (`data.oui`, `data.patterns`, `data.satellites`). Caused `ModuleNotFoundError: No module named 'data.oui'` on container startup. Fixed by only excluding non-code files from `data/`.
---
## [2.26.1] - 2026-03-13
### Fixed
- **Default admin credentials** — Default `ADMIN_PASSWORD` changed from empty string to `admin`, matching the README documentation (`admin:admin`)
- **Config credential sync** — Admin password changes in `config.py` or via `INTERCEPT_ADMIN_PASSWORD` env var now sync to the database on restart, without needing to delete the DB
---
## [2.26.0] - 2026-03-13
### Fixed
- **SSE fanout crash** - `_run_fanout` daemon thread no longer crashes with `AttributeError: 'NoneType' object has no attribute 'get'` when source queue becomes None during interpreter shutdown
- **Branded logo FOUC** - Added inline `width`/`height` to branded "i" SVG elements across 10 templates to prevent oversized rendering before CSS loads; refresh no longer needed
---
## [2.25.0] - 2026-03-12
### Added
- **SSEManager** - Centralized SSE connection management with exponential backoff reconnection and visual connection status indicator
- **Loading button states** - `withLoadingButton()` utility for async action buttons across all modes
- **Actionable error reporting** - `reportActionableError()` added to 5 mode JS files for user-friendly error messages
- SSE fanout backlog causing delayed updates across all modes
- SSE reconnect packet loss during client reconnection
- Waterfall monitor tuning race conditions
- Mode FOUC (flash of unstyled content) on initial navigation
- Various Morse decoder stability and lifecycle fixes
---
## [2.22.3] - 2026-02-23
### Fixed
- Waterfall control panel rendered as unstyled text for up to 20 seconds on first visit — CSS is now loaded eagerly with the rest of the page assets
- WebSDR globe failed to render on first page load — initialization now waits for a layout frame before mounting the WebGL renderer, ensuring the container has non-zero dimensions
- Waterfall monitor audio took minutes to start — `_waitForPlayback` now only reports success on actual audio playback (`playing`/`timeupdate`), not from the WAV header alone (`loadeddata`/`canplay`)
- Waterfall monitor could not be stopped — `stopMonitor()` now pauses audio and updates the UI immediately instead of waiting for the backend stop request (which blocked for 1+ seconds during SDR process cleanup)
- Stopping the waterfall no longer shows a stale "WebSocket closed before ready" message — the `onclose` handler now detects intentional closes
---
## [2.22.1] - 2026-02-23
### Fixed
- PWA install prompt not appearing — manifest now includes required PNG icons (192×192, 512×512)
- Apple touch icon updated to PNG for iOS Safari compatibility
- Service worker cache bumped to bust stale cached assets
---
## [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
@@ -4,21 +4,46 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
INTERCEPT is a web-based Signal Intelligence (SIGINT) platform providing a unified Flask interface for software-defined radio (SDR) tools. It supports pager decoding, 433MHz sensors, ADS-B aircraft tracking, ACARS messaging, WiFi/Bluetooth scanning, and satellite tracking.
INTERCEPT is a web-based Signal Intelligence (SIGINT) platform providing a unified Flask interface for software-defined radio (SDR) tools. It supports pager decoding, 433MHz sensors, ADS-B aircraft tracking, ACARS messaging, WiFi/Bluetooth scanning, satellite tracking, ISS SSTV decoding, AIS vessel tracking, weather satellite imagery (NOAA APT & Meteor LRPT), and Meshtastic mesh networking.
## Common Commands
### Setup and Running
### Docker (Primary)
```bash
# Initial setup (installs dependencies and configures SDR tools)
# Build and run (basic profile)
docker compose --profile basic up -d
# Build and run with ADS-B history (Postgres)
docker compose --profile history up -d
# Rebuild after code changes
docker compose --profile basic up -d --build
# Multi-arch build (amd64 + arm64 for RPi)
./build-multiarch.sh
```
### Local Setup (Alternative)
```bash
# First-time setup (interactive wizard with install profiles)
./setup.sh
# Run the application (requires sudo for SDR/network access)
# Or headless full install
./setup.sh --non-interactive
# Or install specific profiles
./setup.sh --profile=core,weather
# Run with production server (gunicorn + gevent, handles concurrent SSE/WebSocket)
sudo ./start.sh
# Or for quick local dev (Flask dev server)
sudo -E venv/bin/python intercept.py
# Or activate venv first
source venv/bin/activate
sudo -E python intercept.py
# Other setup utilities
./setup.sh --health-check # Verify installation
./setup.sh --postgres-setup # Set up ADS-B history database
./setup.sh --menu # Force interactive menu
```
### Testing
@@ -54,8 +79,10 @@ mypy .
## Architecture
### Entry Points
-`intercept.py` - Main entry point script
-`app.py` - Flask application initialization, global state management, process lifecycle, SSE streaming infrastructure
-`setup.sh` - Menu-driven installer with profile system (wizard, health check, PostgreSQL setup, env configurator, update, uninstall). Sources `.env` on startup via `start.sh`.
-`start.sh` - Production startup script (gunicorn + gevent auto-detection, CLI flags, HTTPS, `.env` sourcing, fallback to Flask dev server)
-`intercept.py` - Direct Flask dev server entry point (quick local development)
-`app.py` - Flask application initialization, global state management, process lifecycle, SSE streaming infrastructure, conditional gevent monkey-patch
### Route Blueprints (routes/)
Each signal type has its own Flask blueprint:
@@ -66,8 +93,12 @@ Each signal type has its own Flask blueprint:
-`wifi.py`, `wifi_v2.py` - WiFi scanning (legacy and unified APIs)
-`bluetooth.py`, `bluetooth_v2.py` - Bluetooth scanning (legacy and unified APIs)
-`satellite.py` - Pass prediction using TLE data
-`sstv.py` - ISS SSTV image decoding via slowrx
-`weather_sat.py` - NOAA APT & Meteor LRPT via SatDump
-`ais.py` - AIS vessel tracking and VHF DSC distress monitoring
@@ -91,9 +122,18 @@ Each signal type has its own Flask blueprint:
- Platform-agnostic scanner with parsers for airodump-ng, nmcli, iw, iwlist, airport (macOS)
-`channel_analyzer.py` - Frequency band analysis
**Weather Satellite** (`utils/weather_sat.py`):
- Singleton `WeatherSatDecoder` using SatDump CLI for NOAA APT and Meteor LRPT
- Subprocess management with stdout parsing, image watcher via rglob
- Pass prediction using skyfield TLE data
**SSTV Decoder** (`utils/sstv.py`):
- ISS SSTV reception via slowrx with Doppler tracking
- Singleton pattern, image gallery with timestamped filenames
### Key Patterns
**Server-Sent Events (SSE)**: All real-time features stream via SSE endpoints (`/stream_pager`, `/stream_sensor`, etc.). Pattern uses `queue.Queue` with timeout and keepalive messages.
**Server-Sent Events (SSE)**: All real-time features stream via SSE endpoints (`/stream_pager`, `/stream_sensor`, etc.). Pattern uses `queue.Queue` with timeout and keepalive messages. Under gunicorn + gevent, each SSE connection is a lightweight greenlet instead of an OS thread.
**Process Management**: External decoders run as subprocesses with output threads feeding queues. Use `safe_terminate()` for cleanup. Global locks prevent race conditions.
@@ -112,9 +152,25 @@ Each signal type has its own Flask blueprint:
- **Bandwidth**: `200 Hz` (use `100 Hz` for crowded bands, `400 Hz` for drifting signals)
- **Threshold Mode**: `Auto`
- **WPM Mode**: `Auto`
### Docker (Alternative)
Auto Tone Track behavior:
- Continuously measures nearby tone energy around the configured CW pitch.
- Steers the detector toward the strongest valid CW tone when signal-to-noise is sufficient.
- Use **Hold Tone Lock** to freeze tracking once the desired signal is centered.
Troubleshooting (no decode / noisy decode):
- Confirm demod path is **USB/CW-compatible** and frequency is tuned correctly.
- If multiple SDRs are connected and the selected one has no PCM output, Morse startup now auto-tries other detected SDR devices and reports the active device/serial in status logs.
- Match **tone** and **bandwidth** to the actual sidetone/pitch.
- Try **Threshold Auto** first; if needed, switch to manual threshold and recalibrate.
- Use **Reset/Calibrate** after major frequency or band condition changes.
- Raise **Minimum Signal Gate** to suppress random noise keying.
To store Postgres data on external storage, set `PGDATA_PATH` (defaults to `./pgdata`):
```bash
PGDATA_PATH=/mnt/usbpi1/intercept/pgdata
```
Then open **/adsb/history** for the reporting dashboard.
On first run, `setup.sh` launches a **guided wizard** that detects your OS, lets you choose install profiles, sets up the Python environment, and optionally configures environment variables and PostgreSQL.
On subsequent runs, it opens an **interactive menu**:
```
INTERCEPT Setup Menu
════════════════════════════════════════
1) Install / Add Modules
2) System Health Check
3) Database Setup (ADS-B History)
4) Update Tools
5) Environment Configurator
6) Uninstall / Cleanup
7) View Status
0) Exit
```
> **Production vs Dev server:** `start.sh` auto-detects gunicorn + gevent and runs a production server with cooperative greenlets — handles multiple SSE/WebSocket clients without blocking. Falls back to Flask dev server if gunicorn is not installed. For quick local development, you can still use `sudo -E venv/bin/python intercept.py` directly.
### Install Profiles
Choose what to install during the wizard or via menu option 1:
If you've pushed to a registry, you can skip building entirely on the target machine:
```bash
# Set in .env or export
INTERCEPT_IMAGE=ghcr.io/youruser/intercept:latest
# Then just run
docker compose --profile basic up -d
```
### Environment Configuration
Use the **Environment Configurator** (menu option 5) to interactively set any `INTERCEPT_*` variable. Settings are saved to a `.env` file that `start.sh` sources automatically on startup.
You can also create or edit `.env` manually:
```bash
# .env (auto-loaded by start.sh)
INTERCEPT_PORT=5050
INTERCEPT_ADSB_AUTO_START=true
INTERCEPT_DEFAULT_LAT=51.5074
INTERCEPT_DEFAULT_LON=-0.1278
```
### ADS-B History (Optional)
The ADS-B history feature persists aircraft messages to PostgreSQL for long-term analysis.
**Automated setup (local install):**
```bash
./setup.sh --postgres-setup
# Or use menu option 3: Database Setup
```
This will install PostgreSQL if needed, create the database/user/tables, and write the connection settings to `.env`.
**Docker:**
```bash
docker compose --profile history up -d
```
Set the following environment variables (in `.env`):
```bash
INTERCEPT_ADSB_HISTORY_ENABLED=true
INTERCEPT_ADSB_DB_HOST=adsb_db
INTERCEPT_ADSB_DB_PORT=5432
INTERCEPT_ADSB_DB_NAME=intercept_adsb
INTERCEPT_ADSB_DB_USER=intercept
INTERCEPT_ADSB_DB_PASSWORD=intercept
```
To store Postgres data on external storage, set `PGDATA_PATH` (defaults to `./pgdata`):
```bash
PGDATA_PATH=/mnt/usbpi1/intercept/pgdata
```
Then open **/adsb/history** for the reporting dashboard.
### System Health Check
Verify your installation is complete and working:
```bash
./setup.sh --health-check
# Or use menu option 2
```
Checks installed tools, SDR devices, port availability, permissions, Python venv, `.env` configuration, and PostgreSQL connectivity.
### Open the Interface
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
---
@@ -179,7 +313,7 @@ This project was developed using AI as a coding partner, combining human directi
## License
MIT License - see [LICENSE](LICENSE)
Apache 2.0 License - see [LICENSE](LICENSE)
## Author
@@ -193,8 +327,12 @@ Created by **smittix** - [GitHub](https://github.com/smittix)
If you're running two (or more) RTL-SDR dongles on the same machine, they ship with the same default serial number so Linux can't tell them apart reliably. Follow these steps to give each a unique identity.
### Step 1: Blacklist the DVB-T driver
Already covered above, but make sure this is done first — the kernel's DVB driver will grab the dongles before librtlsdr can:
```bash
echo "blacklist dvb_usb_rtl28xxu" | sudo tee /etc/modprobe.d/blacklist-rtl.conf
sudo modprobe -r dvb_usb_rtl28xxu
```
### Step 2: Burn unique serial numbers
Each dongle has an EEPROM that stores a serial number. By default they're all `00000001`. You need to give each one a unique serial.
**Plug in only the first dongle**, then:
```bash
rtl_eeprom -d 0 -s 00000001
```
**Unplug it, plug in the second dongle**, then:
```bash
rtl_eeprom -d 0 -s 00000002
```
> Pick any 8-digit hex serials you like. The `-d 0` means "device index 0" (the only one plugged in).
Unplug and replug both dongles after writing.
### Step 3: Verify
With both plugged in:
```bash
rtl_test -t
```
You should see:
```
0: Realtek, RTL2838UHIDIR, SN: 00000001
1: Realtek, RTL2838UHIDIR, SN: 00000002
```
**Tip:** If you don't know which physical dongle has which serial, unplug one and run `rtl_test -t` — the one still detected is the one still plugged in.
### Step 4: Udev rules with stable symlinks
Create rules that give each dongle a persistent name based on its serial:
The setup menu also includes a **System Health Check** (option 2) that verifies all tools, SDR devices, ports, permissions, and Python packages — useful for diagnosing installation problems.
### Interface not found after enabling monitor mode
@@ -373,7 +376,14 @@ sudo usermod -a -G bluetooth $USER
### Cannot install dump1090 in Debian (ADS-B mode)
On newer Debian versions, dump1090 may not be in repositories. The recommended action is to build from source or use the setup.sh script which will do it for you.
On newer Debian versions, dump1090 may not be in repositories. Use the setup script which builds it from source automatically:
```bash
./setup.sh # Select Core SIGINT profile, or
./setup.sh --profile=core # Install core tools including dump1090
```
The setup menu's **Install / Add Modules** option also lets you install dump1090 individually via the Custom tool checklist.
- **Detect IRKs** - Click "Detect" to auto-extract IRKs from paired devices
2. **Choose Environment** - Select the RF environment preset:
- **Open Field** (n=2.0) - Best for open areas with line-of-sight
- **Outdoor** (n=2.2) - Default, works well in most outdoor settings
- **Indoor** (n=3.0) - For buildings with walls and obstacles
3. **Start Locate** - Click "Start Locate" to begin tracking
4. **Monitor HUD** - The proximity display shows:
- Proximity band (IMMEDIATE / NEAR / FAR)
- Estimated distance in meters
- Raw RSSI and smoothed RSSI average
- Detection count and GPS-tagged points
5. **Follow the Signal** - Move towards stronger signal (higher RSSI / closer distance)
6. **Audio Alerts** - Enable audio for proximity tones that increase in pitch as you get closer
7. **Review Trail** - Check the map for GPS-tagged detection trail
### Hand-off from Bluetooth Mode
1. Open Bluetooth scanning mode and find the target device
2. Click the "Locate" button on the device card
3. BT Locate opens with the device pre-filled
4. Click "Start Locate" to begin tracking
### Tips
- For devices with address randomization (iPhones, modern Android), use the IRK method
- Click "Detect" next to the IRK field to auto-extract IRKs from paired devices
- The RSSI chart shows signal trend over time — use it to determine if you're getting closer
- Clear the trail when starting a new search area
## WiFi Locate Mode
1. **Set Target** - Enter a BSSID (MAC address) in AA:BB:CC:DD:EE:FF format, or hand off from WiFi mode
2. **Choose Environment** - Select the RF environment preset:
- **Open Field** (n=2.0) - Best for open areas with line-of-sight
- **Outdoor** (n=2.8) - Default, works well in most outdoor settings
- **Indoor** (n=3.5) - For buildings with walls and obstacles
3. **Start Locate** - Click "Start Locate" to begin tracking
4. **Monitor Signal** - The HUD shows:
- Large dBm reading with color coding (green/yellow/red)
- 20-segment signal bar for quick visual reference
- Estimated distance based on path loss model
- RSSI history chart for trend analysis
- Current/min/max/average statistics
5. **Follow the Signal** - Move towards stronger signal (higher RSSI / closer distance)
6. **Audio Alerts** - Enable audio for proximity tones that speed up as signal strengthens
### Hand-off from WiFi Mode
1. Open WiFi scanning mode and start a deep scan
2. Click any network to open the detail drawer
3. Click the "Locate" button in the drawer header
4. WiFi Locate opens with the BSSID and SSID pre-filled
5. Click "Start Locate" to begin tracking
### Tips
- Deep scan is required for continuous RSSI updates — WiFi Locate auto-starts it if needed
- The WiFi scan is preserved when switching between WiFi and WiFi Locate modes
- Signal lost overlay appears after 30 seconds without an update from the target
- The distance estimate is approximate — environment preset significantly affects accuracy
- Indoor environments with walls attenuate signal more than open field
## GPS Mode
1. **Start GPS** - Click "Start" to connect to gpsd and begin position tracking
2. **View Map** - Your position appears on the interactive map with a track trail
3. **Monitor Stats** - Speed, heading, altitude, and satellite count displayed in real-time
4. **Record Track** - Enable track recording to save your path
### Tips
- Ensure gpsd is running: `sudo gpsd /dev/ttyUSB0 -F /var/run/gpsd.sock`
- 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
-H, --host HOST Host to bind to (default: 0.0.0.0)
-d, --debug Run in debug mode (Flask dev server)
--https Enable HTTPS with self-signed certificate
--check-deps Check dependencies and exit
```
> **Note:**`sudo` is required for SDR hardware access, WiFi monitor mode, and Bluetooth low-level operations.
`start.sh` auto-detects gunicorn + gevent and runs a production WSGI server with cooperative greenlets — this handles multiple SSE streams and WebSocket connections concurrently without blocking. Falls back to the Flask dev server if gunicorn is not installed.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.