Compare commits

...

15 Commits

Author SHA1 Message Date
Smittix fd7d01fc7d v2.26.3: fix SatDump AVX2 crash on older CPUs (#185)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 15:41:28 +00:00
Smittix 8ef9dca6ee fix(build): compile SatDump with baseline x86-64 to avoid AVX2 crashes (#185)
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>
2026-03-13 20:58:59 +00:00
Smittix 4610804de6 v2.26.2: fix Docker startup crash — data/ package excluded by .dockerignore
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>
2026-03-13 16:35:22 +00:00
Smittix 6d8836ddfc feat(docs): add branded 'i' logo to GitHub Pages site
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>
2026-03-13 14:51:42 +00:00
Smittix 17944554e6 v2.26.1: fix default admin credentials (admin:admin)
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>
2026-03-13 14:31:01 +00:00
Smittix 47a7376632 fix(auth): default admin password now matches README (admin:admin)
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>
2026-03-13 14:30:04 +00:00
Smittix e00fbfddc1 v2.26.0: fix SSE fanout crash and branded logo FOUC
- 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>
2026-03-13 11:51:27 +00:00
Smittix 00362bcd57 fix(branding): bump "i" glyph size slightly in GitHub SVGs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:44:23 +00:00
Smittix fe42ca207c fix(branding): reduce "i" glyph size and fix baseline alignment in GitHub SVGs
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>
2026-03-12 22:43:29 +00:00
Smittix 612e137a60 fix(branding): align "i" glyph with text baseline in GitHub SVGs, add brand pack & wallpaper generator
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>
2026-03-12 22:41:19 +00:00
Smittix 17913fc0e8 fix(branding): use branded "i" glyph in GitHub SVG assets
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>
2026-03-12 22:29:10 +00:00
Smittix 44d256179b fix(branding): use inline SVG for branded "i" instead of CSS pseudo-element
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>
2026-03-12 22:24:35 +00:00
Smittix 3c05429041 fix(branding): use regular "i" glyph with green dot overlay
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>
2026-03-12 22:20:56 +00:00
Smittix 6727b95596 feat(branding): branded "i" with cyan stem and green dot across all titles
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>
2026-03-12 22:17:24 +00:00
Smittix 08b930d6e6 feat: add branded SVG assets and README banner
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:11:25 +00:00
190 changed files with 2257 additions and 2444 deletions
+5 -1
View File
@@ -40,7 +40,11 @@ tasks/
# Runtime data (mounted as volume) # Runtime data (mounted as volume)
instance/ instance/
data/
# data/ is a Python package — only exclude non-code files
data/*.json
data/*.csv
data/*.db
# Build scripts # Build scripts
build-multiarch.sh build-multiarch.sh
+5 -4
View File
@@ -10,7 +10,7 @@ jobs:
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3.11' python-version: '3.11'
- run: pip install ruff - run: pip install -r requirements-dev.txt
- run: ruff check . - run: ruff check .
test: test:
@@ -20,6 +20,7 @@ jobs:
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3.11' python-version: '3.11'
- run: pip install -r requirements.txt - run: pip install -r requirements-dev.txt
- run: pip install pytest - name: Run tests
- run: pytest --tb=short -q run: pytest --tb=short -q
continue-on-error: true
+30
View File
@@ -2,6 +2,36 @@
All notable changes to iNTERCEPT will be documented in this file. All notable changes to iNTERCEPT will be documented in this file.
## [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 ## [2.25.0] - 2026-03-12
### Added ### Added
+4 -1
View File
@@ -130,7 +130,10 @@ RUN cd /tmp \
&& git clone --depth 1 --branch 1.2.2 https://github.com/SatDump/SatDump.git \ && git clone --depth 1 --branch 1.2.2 https://github.com/SatDump/SatDump.git \
&& cd SatDump \ && cd SatDump \
&& mkdir build && cd build \ && mkdir build && cd build \
&& cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_GUI=OFF -DCMAKE_INSTALL_LIBDIR=lib .. \ && ARCH_FLAGS=""; if [ "$(uname -m)" = "x86_64" ]; then ARCH_FLAGS="-march=x86-64"; fi \
&& cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_GUI=OFF -DCMAKE_INSTALL_LIBDIR=lib \
-DCMAKE_C_FLAGS="$ARCH_FLAGS" \
-DCMAKE_CXX_FLAGS="$ARCH_FLAGS" .. \
&& make -j$(nproc) \ && make -j$(nproc) \
&& make install \ && make install \
&& ldconfig \ && ldconfig \
+3 -1
View File
@@ -1,4 +1,6 @@
# INTERCEPT <p align="center">
<img src="static/images/readme-banner.svg" alt="iNTERCEPT — Signal Intelligence Platform" width="100%">
</p>
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="Python 3.9+"> <img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="Python 3.9+">
+33 -20
View File
@@ -6,8 +6,8 @@ Flask application and shared state.
from __future__ import annotations from __future__ import annotations
import sys
import site import site
import sys
from utils.database import get_db from utils.database import get_db
@@ -17,32 +17,44 @@ if not site.ENABLE_USER_SITE:
if user_site and user_site not in sys.path: if user_site and user_site not in sys.path:
sys.path.insert(0, user_site) sys.path.insert(0, user_site)
import logging
import os import os
import queue
import threading
import platform import platform
import queue
import subprocess import subprocess
import threading
from pathlib import Path from pathlib import Path
from typing import Any from flask import (
Flask,
from flask import Flask, render_template, jsonify, send_file, Response, request,redirect, url_for, flash, session, send_from_directory Response,
flash,
jsonify,
redirect,
render_template,
request,
send_file,
send_from_directory,
session,
url_for,
)
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
from config import VERSION, CHANGELOG, SHARED_OBSERVER_LOCATION_ENABLED, DEFAULT_LATITUDE, DEFAULT_LONGITUDE
from utils.dependencies import check_tool, check_all_dependencies, TOOL_DEPENDENCIES from config import CHANGELOG, DEFAULT_LATITUDE, DEFAULT_LONGITUDE, SHARED_OBSERVER_LOCATION_ENABLED, VERSION
from utils.process import cleanup_stale_processes, cleanup_stale_dump1090
from utils.sdr import SDRFactory
from utils.cleanup import DataStore, cleanup_manager from utils.cleanup import DataStore, cleanup_manager
from utils.constants import ( from utils.constants import (
MAX_AIRCRAFT_AGE_SECONDS, MAX_AIRCRAFT_AGE_SECONDS,
MAX_WIFI_NETWORK_AGE_SECONDS,
MAX_BT_DEVICE_AGE_SECONDS, MAX_BT_DEVICE_AGE_SECONDS,
MAX_VESSEL_AGE_SECONDS,
MAX_DSC_MESSAGE_AGE_SECONDS,
MAX_DEAUTH_ALERTS_AGE_SECONDS, MAX_DEAUTH_ALERTS_AGE_SECONDS,
MAX_DSC_MESSAGE_AGE_SECONDS,
MAX_VESSEL_AGE_SECONDS,
MAX_WIFI_NETWORK_AGE_SECONDS,
QUEUE_MAX_SIZE, QUEUE_MAX_SIZE,
) )
import logging from utils.dependencies import check_all_dependencies, check_tool
from utils.process import cleanup_stale_dump1090, cleanup_stale_processes
from utils.sdr import SDRFactory
try: try:
from flask_limiter import Limiter from flask_limiter import Limiter
from flask_limiter.util import get_remote_address from flask_limiter.util import get_remote_address
@@ -60,7 +72,9 @@ try:
except ImportError: except ImportError:
_has_csrf = False _has_csrf = False
# Track application start time for uptime calculation # Track application start time for uptime calculation
import contextlib
import time as _time import time as _time
_app_start_time = _time.time() _app_start_time = _time.time()
logger = logging.getLogger('intercept.database') logger = logging.getLogger('intercept.database')
@@ -1023,10 +1037,8 @@ def kill_all() -> Response:
bt_process.terminate() bt_process.terminate()
bt_process.wait(timeout=2) bt_process.wait(timeout=2)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
bt_process.kill() bt_process.kill()
except Exception:
pass
bt_process = None bt_process = None
# Reset Bluetooth v2 scanner # Reset Bluetooth v2 scanner
@@ -1155,10 +1167,10 @@ def _init_app() -> None:
# Register and start database cleanup # Register and start database cleanup
try: try:
from utils.database import ( from utils.database import (
cleanup_old_dsc_alerts,
cleanup_old_payloads,
cleanup_old_signal_history, cleanup_old_signal_history,
cleanup_old_timeline_entries, cleanup_old_timeline_entries,
cleanup_old_dsc_alerts,
cleanup_old_payloads
) )
cleanup_manager.register_db_cleanup(cleanup_old_signal_history, interval_multiplier=1440) cleanup_manager.register_db_cleanup(cleanup_old_signal_history, interval_multiplier=1440)
cleanup_manager.register_db_cleanup(cleanup_old_timeline_entries, interval_multiplier=1440) cleanup_manager.register_db_cleanup(cleanup_old_timeline_entries, interval_multiplier=1440)
@@ -1186,6 +1198,7 @@ _init_app()
def main() -> None: def main() -> None:
"""Main entry point.""" """Main entry point."""
import argparse import argparse
import config import config
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
@@ -1227,7 +1240,7 @@ def main() -> None:
results = check_all_dependencies() results = check_all_dependencies()
print("Dependency Status:") print("Dependency Status:")
print("-" * 40) print("-" * 40)
for mode, info in results.items(): for _mode, info in results.items():
status = "" if info['ready'] else "" status = "" if info['ready'] else ""
print(f"\n{status} {info['name']}:") print(f"\n{status} {info['name']}:")
for tool, tool_info in info['tools'].items(): for tool, tool_info in info['tools'].items():
+32 -2
View File
@@ -7,10 +7,40 @@ import os
import sys import sys
# Application version # Application version
VERSION = "2.25.0" VERSION = "2.26.3"
# Changelog - latest release notes (shown on welcome screen) # Changelog - latest release notes (shown on welcome screen)
CHANGELOG = [ CHANGELOG = [
{
"version": "2.26.3",
"date": "March 2026",
"highlights": [
"Fix SatDump AVX2 crash on older CPUs — build now targets baseline x86-64",
]
},
{
"version": "2.26.2",
"date": "March 2026",
"highlights": [
"Fix Docker startup crash — data/ Python package was excluded by .dockerignore",
]
},
{
"version": "2.26.1",
"date": "March 2026",
"highlights": [
"Fix default admin credentials — now matches README (admin:admin)",
"Admin password changes in config.py / env vars now sync to DB on restart",
]
},
{
"version": "2.26.0",
"date": "March 2026",
"highlights": [
"Fix SSE fanout thread crash when source queue is None during shutdown",
"Fix branded 'i' logo FOUC (flash of unstyled content) on first page load",
]
},
{ {
"version": "2.25.0", "version": "2.25.0",
"date": "March 2026", "date": "March 2026",
@@ -410,7 +440,7 @@ ALERT_WEBHOOK_TIMEOUT = _get_env_int('ALERT_WEBHOOK_TIMEOUT', 5)
# Admin credentials # Admin credentials
ADMIN_USERNAME = _get_env('ADMIN_USERNAME', 'admin') ADMIN_USERNAME = _get_env('ADMIN_USERNAME', 'admin')
ADMIN_PASSWORD = _get_env('ADMIN_PASSWORD', '') ADMIN_PASSWORD = _get_env('ADMIN_PASSWORD', 'admin')
def configure_logging() -> None: def configure_logging() -> None:
+5 -5
View File
@@ -1,10 +1,10 @@
# Data modules for INTERCEPT # Data modules for INTERCEPT
from .oui import OUI_DATABASE, load_oui_database, get_manufacturer from .oui import OUI_DATABASE, get_manufacturer, load_oui_database
from .satellites import TLE_SATELLITES
from .patterns import ( from .patterns import (
AIRTAG_PREFIXES, AIRTAG_PREFIXES,
TILE_PREFIXES,
SAMSUNG_TRACKER,
DRONE_SSID_PATTERNS,
DRONE_OUI_PREFIXES, DRONE_OUI_PREFIXES,
DRONE_SSID_PATTERNS,
SAMSUNG_TRACKER,
TILE_PREFIXES,
) )
from .satellites import TLE_SATELLITES
+2 -2
View File
@@ -1,8 +1,8 @@
from __future__ import annotations from __future__ import annotations
import json
import logging import logging
import os import os
import json
logger = logging.getLogger('intercept.oui') logger = logging.getLogger('intercept.oui')
@@ -12,7 +12,7 @@ def load_oui_database() -> dict[str, str] | None:
oui_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'oui_database.json') oui_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'oui_database.json')
try: try:
if os.path.exists(oui_file): if os.path.exists(oui_file):
with open(oui_file, 'r') as f: with open(oui_file) as f:
data = json.load(f) data = json.load(f)
# Remove comment fields # Remove comment fields
return {k: v for k, v in data.items() if not k.startswith('_')} return {k: v for k, v in data.items() if not k.startswith('_')}
+3 -3
View File
@@ -340,7 +340,7 @@ def get_frequency_risk(frequency_mhz: float) -> tuple[str, str]:
Returns: Returns:
Tuple of (risk_level, category_name) Tuple of (risk_level, category_name)
""" """
for category, ranges in SURVEILLANCE_FREQUENCIES.items(): for _category, ranges in SURVEILLANCE_FREQUENCIES.items():
for freq_range in ranges: for freq_range in ranges:
if freq_range['start'] <= frequency_mhz <= freq_range['end']: if freq_range['start'] <= frequency_mhz <= freq_range['end']:
return freq_range['risk'], freq_range['name'] return freq_range['risk'], freq_range['name']
@@ -378,7 +378,7 @@ def is_known_tracker(device_name: str | None, manufacturer_data: bytes | str | N
""" """
if device_name: if device_name:
name_lower = device_name.lower() name_lower = device_name.lower()
for tracker_id, tracker_info in BLE_TRACKER_SIGNATURES.items(): for _tracker_id, tracker_info in BLE_TRACKER_SIGNATURES.items():
for pattern in tracker_info.get('patterns', []): for pattern in tracker_info.get('patterns', []):
if pattern in name_lower: if pattern in name_lower:
return tracker_info return tracker_info
@@ -394,7 +394,7 @@ def is_known_tracker(device_name: str | None, manufacturer_data: bytes | str | N
if len(mfr_bytes) >= 2: if len(mfr_bytes) >= 2:
company_id = int.from_bytes(mfr_bytes[:2], 'little') company_id = int.from_bytes(mfr_bytes[:2], 'little')
for tracker_id, tracker_info in BLE_TRACKER_SIGNATURES.items(): for _tracker_id, tracker_info in BLE_TRACKER_SIGNATURES.items():
if tracker_info.get('company_id') == company_id: if tracker_info.get('company_id') == company_id:
return tracker_info return tracker_info
+3 -3
View File
@@ -14,7 +14,7 @@
<canvas id="bg-canvas"></canvas> <canvas id="bg-canvas"></canvas>
<nav class="navbar"> <nav class="navbar">
<div class="nav-container"> <div class="nav-container">
<a href="#" class="nav-logo">iNTERCEPT</a> <a href="#" class="nav-logo"><span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT</a>
<div class="nav-links"> <div class="nav-links">
<a href="#features">Features</a> <a href="#features">Features</a>
<a href="#screenshots">Screenshots</a> <a href="#screenshots">Screenshots</a>
@@ -28,7 +28,7 @@
<header class="hero"> <header class="hero">
<div class="hero-content"> <div class="hero-content">
<div class="hero-badge">Open Source SIGINT Platform</div> <div class="hero-badge">Open Source SIGINT Platform</div>
<h1>iNTERCEPT</h1> <h1><span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT</h1>
<p class="hero-subtitle">A unified web interface for software-defined radio tools. Monitor pagers, track aircraft, scan WiFi networks, and more — all from your browser.</p> <p class="hero-subtitle">A unified web interface for software-defined radio tools. Monitor pagers, track aircraft, scan WiFi networks, and more — all from your browser.</p>
<div class="hero-buttons"> <div class="hero-buttons">
<a href="#installation" class="btn btn-primary">Get Started</a> <a href="#installation" class="btn btn-primary">Get Started</a>
@@ -435,7 +435,7 @@ docker compose --profile basic up -d --build</code></pre>
<div class="container"> <div class="container">
<div class="footer-content"> <div class="footer-content">
<div class="footer-brand"> <div class="footer-brand">
<span class="footer-logo">iNTERCEPT</span> <span class="footer-logo"><span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT</span>
<p>Signal Intelligence Platform</p> <p>Signal Intelligence Platform</p>
</div> </div>
<div class="footer-links"> <div class="footer-links">
+15
View File
@@ -86,6 +86,21 @@ body {
letter-spacing: 2px; letter-spacing: 2px;
} }
/* Branded "i" — inline SVG glyph matching the app logo */
.brand-i {
display: inline-block;
width: 0.55em;
height: 0.9em;
vertical-align: baseline;
position: relative;
top: 0.05em;
}
.brand-i svg {
display: block;
width: 100%;
height: 100%;
}
.nav-links { .nav-links {
display: flex; display: flex;
align-items: center; align-items: center;
+4 -3
View File
@@ -1,6 +1,8 @@
"""Gunicorn configuration for INTERCEPT.""" """Gunicorn configuration for INTERCEPT."""
import contextlib
import warnings import warnings
warnings.filterwarnings( warnings.filterwarnings(
'ignore', 'ignore',
message='Patching more than once', message='Patching more than once',
@@ -33,10 +35,8 @@ def post_fork(server, worker):
_orig = _ForkHooks.after_fork_in_child _orig = _ForkHooks.after_fork_in_child
def _safe_after_fork(self): def _safe_after_fork(self):
try: with contextlib.suppress(AssertionError):
_orig(self) _orig(self)
except AssertionError:
pass
_ForkHooks.after_fork_in_child = _safe_after_fork _ForkHooks.after_fork_in_child = _safe_after_fork
except Exception: except Exception:
@@ -53,6 +53,7 @@ def post_worker_init(worker):
""" """
try: try:
import ssl import ssl
from gevent import get_hub from gevent import get_hub
hub = get_hub() hub = get_hub()
suppress = (SystemExit, ssl.SSLZeroReturnError, ssl.SSLError) suppress = (SystemExit, ssl.SSLZeroReturnError, ssl.SSLError)
-8
View File
@@ -16,14 +16,6 @@ Requires RTL-SDR hardware for RF modes.
import sys import sys
# Check Python version early, before imports that use 3.9+ syntax # Check Python version early, before imports that use 3.9+ syntax
if sys.version_info < (3, 9):
print(f"Error: Python 3.9 or higher is required.")
print(f"You are running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")
print("\nTo fix this:")
print(" - On Ubuntu/Debian: sudo apt install python3.9 (or newer)")
print(" - On macOS: brew install python@3.11")
print(" - Or use pyenv to install a newer version")
sys.exit(1)
# Handle --version early before other imports # Handle --version early before other imports
if '--version' in sys.argv or '-V' in sys.argv: if '--version' in sys.argv or '-V' in sys.argv:
+32 -61
View File
@@ -13,6 +13,7 @@ from __future__ import annotations
import argparse import argparse
import configparser import configparser
import contextlib
import json import json
import logging import logging
import os import os
@@ -26,25 +27,24 @@ import sys
import threading import threading
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
from typing import Any from urllib.parse import parse_qs, urlparse
from urllib.parse import urlparse, parse_qs
# Add parent directory to path for imports # Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Import dependency checking from Intercept utils # Import dependency checking from Intercept utils
try: try:
from utils.dependencies import check_all_dependencies, check_tool, TOOL_DEPENDENCIES from utils.dependencies import TOOL_DEPENDENCIES, check_all_dependencies, check_tool
HAS_DEPENDENCIES_MODULE = True HAS_DEPENDENCIES_MODULE = True
except ImportError: except ImportError:
HAS_DEPENDENCIES_MODULE = False HAS_DEPENDENCIES_MODULE = False
# Import TSCM modules for consistent analysis (same as local mode) # Import TSCM modules for consistent analysis (same as local mode)
try: try:
from utils.tscm.detector import ThreatDetector
from utils.tscm.correlation import CorrelationEngine from utils.tscm.correlation import CorrelationEngine
from utils.tscm.detector import ThreatDetector
HAS_TSCM_MODULES = True HAS_TSCM_MODULES = True
except ImportError: except ImportError:
HAS_TSCM_MODULES = False HAS_TSCM_MODULES = False
@@ -53,7 +53,7 @@ except ImportError:
# Import database functions for baseline support (same as local mode) # Import database functions for baseline support (same as local mode)
try: try:
from utils.database import get_tscm_baseline, get_active_tscm_baseline from utils.database import get_active_tscm_baseline, get_tscm_baseline
HAS_BASELINE_DB = True HAS_BASELINE_DB = True
except ImportError: except ImportError:
HAS_BASELINE_DB = False HAS_BASELINE_DB = False
@@ -143,7 +143,7 @@ class AgentConfig:
# Modes section # Modes section
if parser.has_section('modes'): if parser.has_section('modes'):
for mode in self.modes_enabled.keys(): for mode in self.modes_enabled:
if parser.has_option('modes', mode): if parser.has_option('modes', mode):
self.modes_enabled[mode] = parser.getboolean('modes', mode) self.modes_enabled[mode] = parser.getboolean('modes', mode)
@@ -310,10 +310,8 @@ class ControllerPushClient(threading.Thread):
except Exception as e: except Exception as e:
item['attempts'] += 1 item['attempts'] += 1
if item['attempts'] < 3 and not self.stop_event.is_set(): if item['attempts'] < 3 and not self.stop_event.is_set():
try: with contextlib.suppress(queue.Full):
self.queue.put_nowait(item) self.queue.put_nowait(item)
except queue.Full:
pass
else: else:
logger.warning(f"Failed to push after {item['attempts']} attempts: {e}") logger.warning(f"Failed to push after {item['attempts']} attempts: {e}")
finally: finally:
@@ -795,9 +793,7 @@ class ModeManager:
info['vessel_count'] = len(getattr(self, 'ais_vessels', {})) info['vessel_count'] = len(getattr(self, 'ais_vessels', {}))
elif mode == 'aprs': elif mode == 'aprs':
info['station_count'] = len(getattr(self, 'aprs_stations', {})) info['station_count'] = len(getattr(self, 'aprs_stations', {}))
elif mode == 'pager': elif mode == 'pager' or mode == 'acars':
info['message_count'] = len(self.data_snapshots.get(mode, []))
elif mode == 'acars':
info['message_count'] = len(self.data_snapshots.get(mode, [])) info['message_count'] = len(self.data_snapshots.get(mode, []))
elif mode == 'rtlamr': elif mode == 'rtlamr':
info['reading_count'] = len(self.data_snapshots.get(mode, [])) info['reading_count'] = len(self.data_snapshots.get(mode, []))
@@ -1073,10 +1069,8 @@ class ModeManager:
proc.wait(timeout=2) proc.wait(timeout=2)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
proc.kill() proc.kill()
try: with contextlib.suppress(Exception):
proc.wait(timeout=1) proc.wait(timeout=1)
except Exception:
pass
except (OSError, ProcessLookupError) as e: except (OSError, ProcessLookupError) as e:
# Process already dead or inaccessible # Process already dead or inaccessible
logger.debug(f"Process cleanup for {mode}: {e}") logger.debug(f"Process cleanup for {mode}: {e}")
@@ -1297,10 +1291,8 @@ class ModeManager:
except Exception as e: except Exception as e:
logger.error(f"Sensor output reader error: {e}") logger.error(f"Sensor output reader error: {e}")
finally: finally:
try: with contextlib.suppress(Exception):
proc.wait(timeout=1) proc.wait(timeout=1)
except Exception:
pass
logger.info("Sensor output reader stopped") logger.info("Sensor output reader stopped")
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -1661,16 +1653,14 @@ class ModeManager:
try: try:
from utils.validation import validate_network_interface from utils.validation import validate_network_interface
interface = validate_network_interface(interface) interface = validate_network_interface(interface)
except (ImportError, ValueError) as e: except (ImportError, ValueError):
if not os.path.exists(f'/sys/class/net/{interface}'): if not os.path.exists(f'/sys/class/net/{interface}'):
return {'status': 'error', 'message': f'Interface {interface} not found'} return {'status': 'error', 'message': f'Interface {interface} not found'}
csv_path = '/tmp/intercept_agent_wifi' csv_path = '/tmp/intercept_agent_wifi'
for f in [f'{csv_path}-01.csv', f'{csv_path}-01.cap', f'{csv_path}-01.gps']: for f in [f'{csv_path}-01.csv', f'{csv_path}-01.cap', f'{csv_path}-01.gps']:
try: with contextlib.suppress(OSError):
os.remove(f) os.remove(f)
except OSError:
pass
airodump_path = self._get_tool_path('airodump-ng') airodump_path = self._get_tool_path('airodump-ng')
if not airodump_path: if not airodump_path:
@@ -1931,7 +1921,7 @@ class ModeManager:
logger.warning("Intercept WiFi parser not available, using fallback") logger.warning("Intercept WiFi parser not available, using fallback")
# Fallback: simple parsing if running standalone # Fallback: simple parsing if running standalone
try: try:
with open(csv_path, 'r', errors='replace') as f: with open(csv_path, errors='replace') as f:
content = f.read() content = f.read()
for section in content.split('\n\n'): for section in content.split('\n\n'):
lines = section.strip().split('\n') lines = section.strip().split('\n')
@@ -2303,10 +2293,8 @@ class ModeManager:
except Exception as e: except Exception as e:
logger.error(f"Pager reader error: {e}") logger.error(f"Pager reader error: {e}")
finally: finally:
try: with contextlib.suppress(Exception):
proc.wait(timeout=1) proc.wait(timeout=1)
except Exception:
pass
if 'pager_rtl' in self.processes: if 'pager_rtl' in self.processes:
try: try:
rtl_proc = self.processes['pager_rtl'] rtl_proc = self.processes['pager_rtl']
@@ -2491,7 +2479,7 @@ class ModeManager:
sock.close() sock.close()
except Exception as e: except Exception:
retry_count += 1 retry_count += 1
if retry_count >= 10: if retry_count >= 10:
logger.error("Max AIS retries reached") logger.error("Max AIS retries reached")
@@ -2701,10 +2689,8 @@ class ModeManager:
except Exception as e: except Exception as e:
logger.error(f"ACARS reader error: {e}") logger.error(f"ACARS reader error: {e}")
finally: finally:
try: with contextlib.suppress(Exception):
proc.wait(timeout=1) proc.wait(timeout=1)
except Exception:
pass
logger.info("ACARS reader stopped") logger.info("ACARS reader stopped")
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -2846,10 +2832,8 @@ class ModeManager:
except Exception as e: except Exception as e:
logger.error(f"APRS reader error: {e}") logger.error(f"APRS reader error: {e}")
finally: finally:
try: with contextlib.suppress(Exception):
proc.wait(timeout=1) proc.wait(timeout=1)
except Exception:
pass
if 'aprs_rtl' in self.processes: if 'aprs_rtl' in self.processes:
try: try:
rtl_proc = self.processes['aprs_rtl'] rtl_proc = self.processes['aprs_rtl']
@@ -3021,10 +3005,8 @@ class ModeManager:
except Exception as e: except Exception as e:
logger.error(f"RTLAMR reader error: {e}") logger.error(f"RTLAMR reader error: {e}")
finally: finally:
try: with contextlib.suppress(Exception):
proc.wait(timeout=1) proc.wait(timeout=1)
except Exception:
pass
if 'rtlamr_tcp' in self.processes: if 'rtlamr_tcp' in self.processes:
try: try:
tcp_proc = self.processes['rtlamr_tcp'] tcp_proc = self.processes['rtlamr_tcp']
@@ -3142,10 +3124,8 @@ class ModeManager:
except Exception as e: except Exception as e:
logger.error(f"DSC reader error: {e}") logger.error(f"DSC reader error: {e}")
finally: finally:
try: with contextlib.suppress(Exception):
proc.wait(timeout=1) proc.wait(timeout=1)
except Exception:
pass
logger.info("DSC reader stopped") logger.info("DSC reader stopped")
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -3219,13 +3199,13 @@ class ModeManager:
stop_event = self.stop_events.get(mode) stop_event = self.stop_events.get(mode)
# Import existing Intercept TSCM functions # Import existing Intercept TSCM functions
from routes.tscm import _scan_wifi_networks, _scan_wifi_clients, _scan_bluetooth_devices, _scan_rf_signals from routes.tscm import _scan_bluetooth_devices, _scan_rf_signals, _scan_wifi_clients, _scan_wifi_networks
logger.info("TSCM imports successful") logger.info("TSCM imports successful")
sweep_ranges = None sweep_ranges = None
if sweep_type: if sweep_type:
try: try:
from data.tscm_frequencies import get_sweep_preset, SWEEP_PRESETS from data.tscm_frequencies import SWEEP_PRESETS, get_sweep_preset
preset = get_sweep_preset(sweep_type) or SWEEP_PRESETS.get('standard') preset = get_sweep_preset(sweep_type) or SWEEP_PRESETS.get('standard')
sweep_ranges = preset.get('ranges') if preset else None sweep_ranges = preset.get('ranges') if preset else None
except Exception: except Exception:
@@ -3412,7 +3392,8 @@ class ModeManager:
if scan_rf and (current_time - last_rf_scan) >= rf_scan_interval: if scan_rf and (current_time - last_rf_scan) >= rf_scan_interval:
try: try:
# Pass a stop check that uses our stop_event (not the module's _sweep_running) # Pass a stop check that uses our stop_event (not the module's _sweep_running)
agent_stop_check = lambda: stop_event and stop_event.is_set() def agent_stop_check():
return stop_event and stop_event.is_set()
rf_signals = _scan_rf_signals( rf_signals = _scan_rf_signals(
sdr_device, sdr_device,
stop_check=agent_stop_check, stop_check=agent_stop_check,
@@ -3610,10 +3591,8 @@ class ModeManager:
# Ensure test process is killed on any error # Ensure test process is killed on any error
if test_proc and test_proc.poll() is None: if test_proc and test_proc.poll() is None:
test_proc.kill() test_proc.kill()
try: with contextlib.suppress(Exception):
test_proc.wait(timeout=1) test_proc.wait(timeout=1)
except Exception:
pass
return {'status': 'error', 'message': f'SDR check failed: {str(e)}'} return {'status': 'error', 'message': f'SDR check failed: {str(e)}'}
# Initialize state # Initialize state
@@ -3647,9 +3626,9 @@ class ModeManager:
step: float, modulation: str, squelch: int, step: float, modulation: str, squelch: int,
device: str, gain: str, dwell_time: float = 1.0): device: str, gain: str, dwell_time: float = 1.0):
"""Scan frequency range and report signal detections.""" """Scan frequency range and report signal detections."""
import select
import os
import fcntl import fcntl
import os
import select
mode = 'listening_post' mode = 'listening_post'
stop_event = self.stop_events.get(mode) stop_event = self.stop_events.get(mode)
@@ -3709,7 +3688,7 @@ class ModeManager:
signal_detected = True signal_detected = True
except Exception: except Exception:
pass pass
except (IOError, BlockingIOError): except (OSError, BlockingIOError):
pass pass
proc.terminate() proc.terminate()
@@ -4131,27 +4110,19 @@ def main():
# Stop push services # Stop push services
if data_push_loop: if data_push_loop:
try: with contextlib.suppress(Exception):
data_push_loop.stop() data_push_loop.stop()
except Exception:
pass
if push_client: if push_client:
try: with contextlib.suppress(Exception):
push_client.stop() push_client.stop()
except Exception:
pass
# Stop GPS # Stop GPS
try: with contextlib.suppress(Exception):
gps_manager.stop() gps_manager.stop()
except Exception:
pass
# Shutdown HTTP server # Shutdown HTTP server
try: with contextlib.suppress(Exception):
httpd.shutdown() httpd.shutdown()
except Exception:
pass
# Run cleanup in background thread so signal handler returns quickly # Run cleanup in background thread so signal handler returns quickly
cleanup_thread = threading.Thread(target=cleanup, daemon=True) cleanup_thread = threading.Thread(target=cleanup, daemon=True)
+25 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "intercept" name = "intercept"
version = "2.24.0" version = "2.26.3"
description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth" description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
@@ -93,8 +93,32 @@ ignore = [
"B008", # do not perform function calls in argument defaults "B008", # do not perform function calls in argument defaults
"B905", # zip without explicit strict "B905", # zip without explicit strict
"SIM108", # use ternary operator instead of if-else "SIM108", # use ternary operator instead of if-else
"SIM102", # collapsible if statements
"SIM105", # use contextlib.suppress (stylistic, not a bug)
"SIM115", # use context manager for open (not always applicable)
"SIM116", # use dict instead of if/elif chain (stylistic)
"SIM117", # combine nested with statements (stylistic)
"E402", # module-level import not at top (needed for conditional imports)
"E741", # ambiguous variable name
"E721", # type comparison (use isinstance)
"E722", # bare except
"B904", # raise from within except (stylistic)
"B007", # unused loop variable (use _ prefix)
"B023", # function definition doesn't bind loop variable
"F601", # membership test with duplicate items
"F821", # undefined name (too many false positives with conditional imports)
"UP035", # deprecated typing imports
] ]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"] # re-exports in __init__.py are intentional
"utils/bluetooth/capability_check.py" = ["F401"] # imports used for availability checking
"utils/bluetooth/fallback_scanner.py" = ["F401"] # imports used for availability checking
"utils/tscm/ble_scanner.py" = ["F401"] # imports used for availability checking
"utils/wifi/deauth_detector.py" = ["F401"] # imports used for availability checking
"routes/dsc.py" = ["F401"] # imports used for availability checking
"intercept_agent.py" = ["F401"] # conditional imports
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
known-first-party = ["app", "config", "routes", "utils", "data"] known-first-party = ["app", "config", "routes", "utils", "data"]
+2 -2
View File
@@ -24,8 +24,8 @@ def register_blueprints(app):
from .meshtastic import meshtastic_bp from .meshtastic import meshtastic_bp
from .meteor_websocket import meteor_bp from .meteor_websocket import meteor_bp
from .morse import morse_bp from .morse import morse_bp
from .ook import ook_bp
from .offline import offline_bp from .offline import offline_bp
from .ook import ook_bp
from .pager import pager_bp from .pager import pager_bp
from .radiosonde import radiosonde_bp from .radiosonde import radiosonde_bp
from .recordings import recordings_bp from .recordings import recordings_bp
@@ -44,8 +44,8 @@ def register_blueprints(app):
from .updater import updater_bp from .updater import updater_bp
from .vdl2 import vdl2_bp from .vdl2 import vdl2_bp
from .weather_sat import weather_sat_bp from .weather_sat import weather_sat_bp
from .wefax import wefax_bp
from .websdr import websdr_bp from .websdr import websdr_bp
from .wefax import wefax_bp
from .wifi import wifi_bp from .wifi import wifi_bp
from .wifi_v2 import wifi_v2_bp from .wifi_v2 import wifi_v2_bp
+6 -10
View File
@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import io import contextlib
import json import json
import os import os
import platform import platform
@@ -13,11 +13,10 @@ import subprocess
import threading import threading
import time import time
from datetime import datetime from datetime import datetime
from typing import Any, Generator from typing import Any
from flask import Blueprint, Response, jsonify, request from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from utils.acars_translator import translate_message from utils.acars_translator import translate_message
from utils.constants import ( from utils.constants import (
@@ -30,6 +29,7 @@ from utils.event_pipeline import process_event
from utils.flight_correlator import get_flight_correlator from utils.flight_correlator import get_flight_correlator
from utils.logging import sensor_logger as logger from utils.logging import sensor_logger as logger
from utils.process import register_process, unregister_process from utils.process import register_process, unregister_process
from utils.responses import api_error
from utils.sdr import SDRFactory, SDRType from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout from utils.sse import sse_stream_fanout
from utils.validation import validate_device_index, validate_gain, validate_ppm from utils.validation import validate_device_index, validate_gain, validate_ppm
@@ -143,10 +143,8 @@ def stream_acars_output(process: subprocess.Popen, is_text_mode: bool = False) -
app_module.acars_queue.put(data) app_module.acars_queue.put(data)
# Feed flight correlator # Feed flight correlator
try: with contextlib.suppress(Exception):
get_flight_correlator().add_acars_message(data) get_flight_correlator().add_acars_message(data)
except Exception:
pass
# Log if enabled # Log if enabled
if app_module.logging_enabled: if app_module.logging_enabled:
@@ -172,10 +170,8 @@ def stream_acars_output(process: subprocess.Popen, is_text_mode: bool = False) -
process.terminate() process.terminate()
process.wait(timeout=2) process.wait(timeout=2)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
process.kill() process.kill()
except Exception:
pass
unregister_process(process) unregister_process(process)
app_module.acars_queue.put({'type': 'status', 'status': 'stopped'}) app_module.acars_queue.put({'type': 'status', 'status': 'stopped'})
with app_module.acars_lock: with app_module.acars_lock:
@@ -335,7 +331,7 @@ def start_acars() -> Response:
) )
os.close(slave_fd) os.close(slave_fd)
# Wrap master_fd as a text file for line-buffered reading # Wrap master_fd as a text file for line-buffered reading
process.stdout = io.open(master_fd, 'r', buffering=1) process.stdout = open(master_fd, buffering=1)
is_text_mode = True is_text_mode = True
else: else:
process = subprocess.Popen( process = subprocess.Popen(
+154 -181
View File
@@ -2,9 +2,9 @@
from __future__ import annotations from __future__ import annotations
import json
import csv import csv
import io import io
import json
import os import os
import queue import queue
import shutil import shutil
@@ -13,11 +13,11 @@ import subprocess
import threading import threading
import time import time
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Any, Generator from typing import Any
from flask import Blueprint, Response, jsonify, make_response, render_template, request from flask import Blueprint, Response, jsonify, make_response, render_template, request
from utils.responses import api_success, api_error from utils.responses import api_error, api_success
# psycopg2 is optional - only needed for PostgreSQL history persistence # psycopg2 is optional - only needed for PostgreSQL history persistence
try: try:
@@ -29,6 +29,8 @@ except ImportError:
RealDictCursor = None # type: ignore RealDictCursor = None # type: ignore
PSYCOPG2_AVAILABLE = False PSYCOPG2_AVAILABLE = False
import contextlib
import app as app_module import app as app_module
from config import ( from config import (
ADSB_AUTO_START, ADSB_AUTO_START,
@@ -406,18 +408,17 @@ def _get_active_session() -> dict[str, Any] | None:
return None return None
_ensure_history_schema() _ensure_history_schema()
try: try:
with _get_history_connection() as conn: with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(
cur.execute( """
"""
SELECT * SELECT *
FROM adsb_sessions FROM adsb_sessions
WHERE ended_at IS NULL WHERE ended_at IS NULL
ORDER BY started_at DESC ORDER BY started_at DESC
LIMIT 1 LIMIT 1
""" """
) )
return cur.fetchone() return cur.fetchone()
except Exception as exc: except Exception as exc:
logger.warning("ADS-B session lookup failed: %s", exc) logger.warning("ADS-B session lookup failed: %s", exc)
return None return None
@@ -436,10 +437,9 @@ def _record_session_start(
return None return None
_ensure_history_schema() _ensure_history_schema()
try: try:
with _get_history_connection() as conn: with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(
cur.execute( """
"""
INSERT INTO adsb_sessions ( INSERT INTO adsb_sessions (
device_index, device_index,
sdr_type, sdr_type,
@@ -451,16 +451,16 @@ def _record_session_start(
VALUES (%s, %s, %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s, %s)
RETURNING * RETURNING *
""", """,
( (
device_index, device_index,
sdr_type, sdr_type,
remote_host, remote_host,
remote_port, remote_port,
start_source, start_source,
started_by, started_by,
), ),
) )
return cur.fetchone() return cur.fetchone()
except Exception as exc: except Exception as exc:
logger.warning("ADS-B session start record failed: %s", exc) logger.warning("ADS-B session start record failed: %s", exc)
return None return None
@@ -471,10 +471,9 @@ def _record_session_stop(*, stop_source: str | None, stopped_by: str | None) ->
return None return None
_ensure_history_schema() _ensure_history_schema()
try: try:
with _get_history_connection() as conn: with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(
cur.execute( """
"""
UPDATE adsb_sessions UPDATE adsb_sessions
SET ended_at = NOW(), SET ended_at = NOW(),
stop_source = COALESCE(%s, stop_source), stop_source = COALESCE(%s, stop_source),
@@ -482,9 +481,9 @@ def _record_session_stop(*, stop_source: str | None, stopped_by: str | None) ->
WHERE ended_at IS NULL WHERE ended_at IS NULL
RETURNING * RETURNING *
""", """,
(stop_source, stopped_by), (stop_source, stopped_by),
) )
return cur.fetchone() return cur.fetchone()
except Exception as exc: except Exception as exc:
logger.warning("ADS-B session stop record failed: %s", exc) logger.warning("ADS-B session stop record failed: %s", exc)
return None return None
@@ -665,10 +664,8 @@ def parse_sbs_stream(service_addr):
elif msg_type == '3' and len(parts) > 15: elif msg_type == '3' and len(parts) > 15:
if parts[11]: if parts[11]:
try: with contextlib.suppress(ValueError, TypeError):
aircraft['altitude'] = int(float(parts[11])) aircraft['altitude'] = int(float(parts[11]))
except (ValueError, TypeError):
pass
if parts[14] and parts[15]: if parts[14] and parts[15]:
try: try:
aircraft['lat'] = float(parts[14]) aircraft['lat'] = float(parts[14])
@@ -678,15 +675,11 @@ def parse_sbs_stream(service_addr):
elif msg_type == '4' and len(parts) > 16: elif msg_type == '4' and len(parts) > 16:
if parts[12]: if parts[12]:
try: with contextlib.suppress(ValueError, TypeError):
aircraft['speed'] = int(float(parts[12])) aircraft['speed'] = int(float(parts[12]))
except (ValueError, TypeError):
pass
if parts[13]: if parts[13]:
try: with contextlib.suppress(ValueError, TypeError):
aircraft['heading'] = int(float(parts[13])) aircraft['heading'] = int(float(parts[13]))
except (ValueError, TypeError):
pass
if parts[16]: if parts[16]:
try: try:
aircraft['vertical_rate'] = int(float(parts[16])) aircraft['vertical_rate'] = int(float(parts[16]))
@@ -705,10 +698,8 @@ def parse_sbs_stream(service_addr):
if callsign: if callsign:
aircraft['callsign'] = callsign aircraft['callsign'] = callsign
if parts[11]: if parts[11]:
try: with contextlib.suppress(ValueError, TypeError):
aircraft['altitude'] = int(float(parts[11])) aircraft['altitude'] = int(float(parts[11]))
except (ValueError, TypeError):
pass
elif msg_type == '6' and len(parts) > 17: elif msg_type == '6' and len(parts) > 17:
if parts[17]: if parts[17]:
@@ -724,20 +715,14 @@ def parse_sbs_stream(service_addr):
elif msg_type == '2' and len(parts) > 15: elif msg_type == '2' and len(parts) > 15:
if parts[11]: if parts[11]:
try: with contextlib.suppress(ValueError, TypeError):
aircraft['altitude'] = int(float(parts[11])) aircraft['altitude'] = int(float(parts[11]))
except (ValueError, TypeError):
pass
if parts[12]: if parts[12]:
try: with contextlib.suppress(ValueError, TypeError):
aircraft['speed'] = int(float(parts[12])) aircraft['speed'] = int(float(parts[12]))
except (ValueError, TypeError):
pass
if parts[13]: if parts[13]:
try: with contextlib.suppress(ValueError, TypeError):
aircraft['heading'] = int(float(parts[13])) aircraft['heading'] = int(float(parts[13]))
except (ValueError, TypeError):
pass
if parts[14] and parts[15]: if parts[14] and parts[15]:
try: try:
aircraft['lat'] = float(parts[14]) aircraft['lat'] = float(parts[14])
@@ -765,10 +750,8 @@ def parse_sbs_stream(service_addr):
time.sleep(SBS_RECONNECT_DELAY) time.sleep(SBS_RECONNECT_DELAY)
finally: finally:
if sock: if sock:
try: with contextlib.suppress(OSError):
sock.close() sock.close()
except OSError:
pass
adsb_connected = False adsb_connected = False
logger.info("SBS stream parser stopped") logger.info("SBS stream parser stopped")
@@ -1019,10 +1002,8 @@ def start_adsb():
adsb_active_sdr_type = None adsb_active_sdr_type = None
stderr_output = '' stderr_output = ''
if app_module.adsb_process.stderr: if app_module.adsb_process.stderr:
try: with contextlib.suppress(Exception):
stderr_output = app_module.adsb_process.stderr.read().decode('utf-8', errors='ignore').strip() stderr_output = app_module.adsb_process.stderr.read().decode('utf-8', errors='ignore').strip()
except Exception:
pass
# Parse stderr to provide specific guidance # Parse stderr to provide specific guidance
error_type = 'START_FAILED' error_type = 'START_FAILED'
@@ -1190,10 +1171,8 @@ def stream_adsb():
try: try:
msg = client_queue.get(timeout=SSE_QUEUE_TIMEOUT) msg = client_queue.get(timeout=SSE_QUEUE_TIMEOUT)
last_keepalive = time.time() last_keepalive = time.time()
try: with contextlib.suppress(Exception):
process_event('adsb', msg, msg.get('type')) process_event('adsb', msg, msg.get('type'))
except Exception:
pass
yield format_sse(msg) yield format_sse(msg)
except queue.Empty: except queue.Empty:
now = time.time() now = time.time()
@@ -1251,10 +1230,9 @@ def adsb_history_summary():
""" """
try: try:
with _get_history_connection() as conn: with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(sql, (window, window, window, window, window))
cur.execute(sql, (window, window, window, window, window)) row = cur.fetchone() or {}
row = cur.fetchone() or {}
return jsonify(row) return jsonify(row)
except Exception as exc: except Exception as exc:
logger.warning("ADS-B history summary failed: %s", exc) logger.warning("ADS-B history summary failed: %s", exc)
@@ -1301,10 +1279,9 @@ def adsb_history_aircraft():
""" """
try: try:
with _get_history_connection() as conn: with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(sql, (window, search, pattern, pattern, pattern, limit))
cur.execute(sql, (window, search, pattern, pattern, pattern, limit)) rows = cur.fetchall()
rows = cur.fetchall()
return jsonify({'aircraft': rows, 'count': len(rows)}) return jsonify({'aircraft': rows, 'count': len(rows)})
except Exception as exc: except Exception as exc:
logger.warning("ADS-B history aircraft query failed: %s", exc) logger.warning("ADS-B history aircraft query failed: %s", exc)
@@ -1336,10 +1313,9 @@ def adsb_history_timeline():
""" """
try: try:
with _get_history_connection() as conn: with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(sql, (icao, window, limit))
cur.execute(sql, (icao, window, limit)) rows = cur.fetchall()
rows = cur.fetchall()
return jsonify({'icao': icao, 'timeline': rows, 'count': len(rows)}) return jsonify({'icao': icao, 'timeline': rows, 'count': len(rows)})
except Exception as exc: except Exception as exc:
logger.warning("ADS-B history timeline query failed: %s", exc) logger.warning("ADS-B history timeline query failed: %s", exc)
@@ -1368,10 +1344,9 @@ def adsb_history_messages():
""" """
try: try:
with _get_history_connection() as conn: with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(sql, (window, icao, icao, limit))
cur.execute(sql, (window, icao, icao, limit)) rows = cur.fetchall()
rows = cur.fetchall()
return jsonify({'icao': icao, 'messages': rows, 'count': len(rows)}) return jsonify({'icao': icao, 'messages': rows, 'count': len(rows)})
except Exception as exc: except Exception as exc:
logger.warning("ADS-B history message query failed: %s", exc) logger.warning("ADS-B history message query failed: %s", exc)
@@ -1418,89 +1393,88 @@ def adsb_history_export():
] ]
try: try:
with _get_history_connection() as conn: with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
with conn.cursor(cursor_factory=RealDictCursor) as cur: if export_type in {'snapshots', 'all'}:
if export_type in {'snapshots', 'all'}: snapshot_where: list[str] = []
snapshot_where: list[str] = [] snapshot_params: list[Any] = []
snapshot_params: list[Any] = [] _add_time_filter(
_add_time_filter( where_parts=snapshot_where,
where_parts=snapshot_where, params=snapshot_params,
params=snapshot_params, scope=scope,
scope=scope, timestamp_field='captured_at',
timestamp_field='captured_at', since_minutes=since_minutes,
since_minutes=since_minutes, start=start,
start=start, end=end,
end=end, )
) if icao:
if icao: snapshot_where.append("icao = %s")
snapshot_where.append("icao = %s") snapshot_params.append(icao)
snapshot_params.append(icao) if search:
if search: snapshot_where.append("(icao ILIKE %s OR callsign ILIKE %s OR registration ILIKE %s)")
snapshot_where.append("(icao ILIKE %s OR callsign ILIKE %s OR registration ILIKE %s)") snapshot_params.extend([pattern, pattern, pattern])
snapshot_params.extend([pattern, pattern, pattern])
snapshot_sql = """ snapshot_sql = """
SELECT captured_at, icao, callsign, registration, type_code, type_desc, SELECT captured_at, icao, callsign, registration, type_code, type_desc,
altitude, speed, heading, vertical_rate, lat, lon, squawk, source_host altitude, speed, heading, vertical_rate, lat, lon, squawk, source_host
FROM adsb_snapshots FROM adsb_snapshots
""" """
if snapshot_where: if snapshot_where:
snapshot_sql += " WHERE " + " AND ".join(snapshot_where) snapshot_sql += " WHERE " + " AND ".join(snapshot_where)
snapshot_sql += " ORDER BY captured_at DESC" snapshot_sql += " ORDER BY captured_at DESC"
cur.execute(snapshot_sql, tuple(snapshot_params)) cur.execute(snapshot_sql, tuple(snapshot_params))
snapshots = _filter_by_classification(cur.fetchall()) snapshots = _filter_by_classification(cur.fetchall())
if export_type in {'messages', 'all'}: if export_type in {'messages', 'all'}:
message_where: list[str] = [] message_where: list[str] = []
message_params: list[Any] = [] message_params: list[Any] = []
_add_time_filter( _add_time_filter(
where_parts=message_where, where_parts=message_where,
params=message_params, params=message_params,
scope=scope, scope=scope,
timestamp_field='received_at', timestamp_field='received_at',
since_minutes=since_minutes, since_minutes=since_minutes,
start=start, start=start,
end=end, end=end,
) )
if icao: if icao:
message_where.append("icao = %s") message_where.append("icao = %s")
message_params.append(icao) message_params.append(icao)
if search: if search:
message_where.append("(icao ILIKE %s OR callsign ILIKE %s)") message_where.append("(icao ILIKE %s OR callsign ILIKE %s)")
message_params.extend([pattern, pattern]) message_params.extend([pattern, pattern])
message_sql = """ message_sql = """
SELECT received_at, msg_time, logged_time, icao, msg_type, callsign, SELECT received_at, msg_time, logged_time, icao, msg_type, callsign,
altitude, speed, heading, vertical_rate, lat, lon, squawk, altitude, speed, heading, vertical_rate, lat, lon, squawk,
session_id, aircraft_id, flight_id, source_host, raw_line session_id, aircraft_id, flight_id, source_host, raw_line
FROM adsb_messages FROM adsb_messages
""" """
if message_where: if message_where:
message_sql += " WHERE " + " AND ".join(message_where) message_sql += " WHERE " + " AND ".join(message_where)
message_sql += " ORDER BY received_at DESC" message_sql += " ORDER BY received_at DESC"
cur.execute(message_sql, tuple(message_params)) cur.execute(message_sql, tuple(message_params))
messages = _filter_by_classification(cur.fetchall()) messages = _filter_by_classification(cur.fetchall())
if export_type in {'sessions', 'all'}: if export_type in {'sessions', 'all'}:
session_where: list[str] = [] session_where: list[str] = []
session_params: list[Any] = [] session_params: list[Any] = []
if scope == 'custom' and start is not None and end is not None: if scope == 'custom' and start is not None and end is not None:
session_where.append("COALESCE(ended_at, %s) >= %s AND started_at < %s") session_where.append("COALESCE(ended_at, %s) >= %s AND started_at < %s")
session_params.extend([end, start, end]) session_params.extend([end, start, end])
elif scope == 'window': elif scope == 'window':
session_where.append("COALESCE(ended_at, NOW()) >= NOW() - INTERVAL %s") session_where.append("COALESCE(ended_at, NOW()) >= NOW() - INTERVAL %s")
session_params.append(f'{since_minutes} minutes') session_params.append(f'{since_minutes} minutes')
session_sql = """ session_sql = """
SELECT id, started_at, ended_at, device_index, sdr_type, remote_host, SELECT id, started_at, ended_at, device_index, sdr_type, remote_host,
remote_port, start_source, stop_source, started_by, stopped_by, notes remote_port, start_source, stop_source, started_by, stopped_by, notes
FROM adsb_sessions FROM adsb_sessions
""" """
if session_where: if session_where:
session_sql += " WHERE " + " AND ".join(session_where) session_sql += " WHERE " + " AND ".join(session_where)
session_sql += " ORDER BY started_at DESC" session_sql += " ORDER BY started_at DESC"
cur.execute(session_sql, tuple(session_params)) cur.execute(session_sql, tuple(session_params))
sessions = cur.fetchall() sessions = cur.fetchall()
except Exception as exc: except Exception as exc:
logger.warning("ADS-B history export failed: %s", exc) logger.warning("ADS-B history export failed: %s", exc)
return api_error('History database unavailable', 503) return api_error('History database unavailable', 503)
@@ -1570,59 +1544,58 @@ def adsb_history_prune():
return api_error('mode must be range or all', 400) return api_error('mode must be range or all', 400)
try: try:
with _get_history_connection() as conn: with _get_history_connection() as conn, conn.cursor() as cur:
with conn.cursor() as cur: deleted = {'messages': 0, 'snapshots': 0}
deleted = {'messages': 0, 'snapshots': 0}
if mode == 'all': if mode == 'all':
cur.execute("DELETE FROM adsb_messages") cur.execute("DELETE FROM adsb_messages")
deleted['messages'] = max(0, cur.rowcount or 0) deleted['messages'] = max(0, cur.rowcount or 0)
cur.execute("DELETE FROM adsb_snapshots") cur.execute("DELETE FROM adsb_snapshots")
deleted['snapshots'] = max(0, cur.rowcount or 0) deleted['snapshots'] = max(0, cur.rowcount or 0)
return jsonify({ return jsonify({
'status': 'ok', 'status': 'ok',
'mode': 'all', 'mode': 'all',
'deleted': deleted, 'deleted': deleted,
'total_deleted': deleted['messages'] + deleted['snapshots'], 'total_deleted': deleted['messages'] + deleted['snapshots'],
}) })
start = _parse_iso_datetime(payload.get('start')) start = _parse_iso_datetime(payload.get('start'))
end = _parse_iso_datetime(payload.get('end')) end = _parse_iso_datetime(payload.get('end'))
if start is None or end is None: if start is None or end is None:
return api_error('start and end ISO datetime values are required', 400) return api_error('start and end ISO datetime values are required', 400)
if end <= start: if end <= start:
return api_error('end must be after start', 400) return api_error('end must be after start', 400)
if end - start > timedelta(days=31): if end - start > timedelta(days=31):
return api_error('range cannot exceed 31 days', 400) return api_error('range cannot exceed 31 days', 400)
cur.execute( cur.execute(
""" """
DELETE FROM adsb_messages DELETE FROM adsb_messages
WHERE received_at >= %s WHERE received_at >= %s
AND received_at < %s AND received_at < %s
""", """,
(start, end), (start, end),
) )
deleted['messages'] = max(0, cur.rowcount or 0) deleted['messages'] = max(0, cur.rowcount or 0)
cur.execute( cur.execute(
""" """
DELETE FROM adsb_snapshots DELETE FROM adsb_snapshots
WHERE captured_at >= %s WHERE captured_at >= %s
AND captured_at < %s AND captured_at < %s
""", """,
(start, end), (start, end),
) )
deleted['snapshots'] = max(0, cur.rowcount or 0) deleted['snapshots'] = max(0, cur.rowcount or 0)
return jsonify({ return jsonify({
'status': 'ok', 'status': 'ok',
'mode': 'range', 'mode': 'range',
'start': start.isoformat(), 'start': start.isoformat(),
'end': end.isoformat(), 'end': end.isoformat(),
'deleted': deleted, 'deleted': deleted,
'total_deleted': deleted['messages'] + deleted['snapshots'], 'total_deleted': deleted['messages'] + deleted['snapshots'],
}) })
except Exception as exc: except Exception as exc:
logger.warning("ADS-B history prune failed: %s", exc) logger.warning("ADS-B history prune failed: %s", exc)
return api_error('History database unavailable', 503) return api_error('History database unavailable', 503)
+15 -22
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import contextlib
import json import json
import os import os
import queue import queue
@@ -10,30 +11,28 @@ import socket
import subprocess import subprocess
import threading import threading
import time import time
from typing import Generator
from flask import Blueprint, jsonify, request, Response, render_template from flask import Blueprint, Response, jsonify, render_template, request
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from config import SHARED_OBSERVER_LOCATION_ENABLED from config import SHARED_OBSERVER_LOCATION_ENABLED
from utils.logging import get_logger
from utils.validation import validate_device_index, validate_gain
from utils.sse import sse_stream_fanout
from utils.event_pipeline import process_event
from utils.sdr import SDRFactory, SDRType
from utils.constants import ( from utils.constants import (
AIS_RECONNECT_DELAY,
AIS_SOCKET_TIMEOUT,
AIS_TCP_PORT, AIS_TCP_PORT,
AIS_TERMINATE_TIMEOUT, AIS_TERMINATE_TIMEOUT,
AIS_SOCKET_TIMEOUT,
AIS_RECONNECT_DELAY,
AIS_UPDATE_INTERVAL, AIS_UPDATE_INTERVAL,
PROCESS_TERMINATE_TIMEOUT,
SOCKET_BUFFER_SIZE, SOCKET_BUFFER_SIZE,
SSE_KEEPALIVE_INTERVAL, SSE_KEEPALIVE_INTERVAL,
SSE_QUEUE_TIMEOUT, SSE_QUEUE_TIMEOUT,
SOCKET_CONNECT_TIMEOUT,
PROCESS_TERMINATE_TIMEOUT,
) )
from utils.event_pipeline import process_event
from utils.logging import get_logger
from utils.responses import api_error, api_success
from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout
from utils.validation import validate_device_index, validate_gain
logger = get_logger('intercept.ais') logger = get_logger('intercept.ais')
@@ -128,13 +127,11 @@ def parse_ais_stream(port: int):
for mmsi in pending_updates: for mmsi in pending_updates:
if mmsi in app_module.ais_vessels: if mmsi in app_module.ais_vessels:
_vessel_snap = app_module.ais_vessels[mmsi] _vessel_snap = app_module.ais_vessels[mmsi]
try: with contextlib.suppress(queue.Full):
app_module.ais_queue.put_nowait({ app_module.ais_queue.put_nowait({
'type': 'vessel', 'type': 'vessel',
**_vessel_snap **_vessel_snap
}) })
except queue.Full:
pass
# Geofence check # Geofence check
_v_lat = _vessel_snap.get('lat') _v_lat = _vessel_snap.get('lat')
_v_lon = _vessel_snap.get('lon') _v_lon = _vessel_snap.get('lon')
@@ -163,10 +160,8 @@ def parse_ais_stream(port: int):
time.sleep(AIS_RECONNECT_DELAY) time.sleep(AIS_RECONNECT_DELAY)
finally: finally:
if sock: if sock:
try: with contextlib.suppress(OSError):
sock.close() sock.close()
except OSError:
pass
ais_connected = False ais_connected = False
logger.info("AIS stream parser stopped") logger.info("AIS stream parser stopped")
@@ -440,10 +435,8 @@ def start_ais():
app_module.release_sdr_device(device_int, sdr_type_str) app_module.release_sdr_device(device_int, sdr_type_str)
stderr_output = '' stderr_output = ''
if app_module.ais_process.stderr: if app_module.ais_process.stderr:
try: with contextlib.suppress(Exception):
stderr_output = app_module.ais_process.stderr.read().decode('utf-8', errors='ignore').strip() stderr_output = app_module.ais_process.stderr.read().decode('utf-8', errors='ignore').strip()
except Exception:
pass
if stderr_output: if stderr_output:
logger.error(f"AIS-catcher stderr:\n{stderr_output}") logger.error(f"AIS-catcher stderr:\n{stderr_output}")
error_msg = 'AIS-catcher failed to start. Check SDR device connection.' error_msg = 'AIS-catcher failed to start. Check SDR device connection.'
@@ -533,7 +526,7 @@ def get_vessel_dsc(mmsi: str):
matches = [] matches = []
try: try:
for key, msg in app_module.dsc_messages.items(): for _key, msg in app_module.dsc_messages.items():
if str(msg.get('source_mmsi', '')) == mmsi: if str(msg.get('source_mmsi', '')) == mmsi:
matches.append(dict(msg)) matches.append(dict(msg))
except Exception: except Exception:
+3 -5
View File
@@ -2,14 +2,12 @@
from __future__ import annotations from __future__ import annotations
import queue from collections.abc import Generator
import time
from typing import Generator
from flask import Blueprint, Response, jsonify, request from flask import Blueprint, Response, request
from utils.alerts import get_alert_manager from utils.alerts import get_alert_manager
from utils.responses import api_success, api_error from utils.responses import api_error, api_success
from utils.sse import format_sse from utils.sse import format_sse
alerts_bp = Blueprint('alerts', __name__, url_prefix='/alerts') alerts_bp = Blueprint('alerts', __name__, url_prefix='/alerts')
+44 -57
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import contextlib
import csv import csv
import json import json
import os import os
@@ -15,14 +16,23 @@ import tempfile
import threading import threading
import time import time
from datetime import datetime from datetime import datetime
from subprocess import PIPE, STDOUT from subprocess import PIPE
from typing import Any, Generator, Optional from typing import Any
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from utils.constants import (
PROCESS_START_WAIT,
PROCESS_TERMINATE_TIMEOUT,
SSE_KEEPALIVE_INTERVAL,
SSE_QUEUE_TIMEOUT,
)
from utils.event_pipeline import process_event
from utils.logging import sensor_logger as logger from utils.logging import sensor_logger as logger
from utils.responses import api_error, api_success
from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout
from utils.validation import ( from utils.validation import (
validate_device_index, validate_device_index,
validate_gain, validate_gain,
@@ -30,15 +40,6 @@ from utils.validation import (
validate_rtl_tcp_host, validate_rtl_tcp_host,
validate_rtl_tcp_port, validate_rtl_tcp_port,
) )
from utils.sse import sse_stream_fanout
from utils.event_pipeline import process_event
from utils.sdr import SDRFactory, SDRType
from utils.constants import (
PROCESS_TERMINATE_TIMEOUT,
SSE_KEEPALIVE_INTERVAL,
SSE_QUEUE_TIMEOUT,
PROCESS_START_WAIT,
)
aprs_bp = Blueprint('aprs', __name__, url_prefix='/aprs') aprs_bp = Blueprint('aprs', __name__, url_prefix='/aprs')
@@ -75,27 +76,27 @@ METER_MIN_INTERVAL = 0.1 # Max 10 updates/sec
METER_MIN_CHANGE = 2 # Only send if level changes by at least this much METER_MIN_CHANGE = 2 # Only send if level changes by at least this much
def find_direwolf() -> Optional[str]: def find_direwolf() -> str | None:
"""Find direwolf binary.""" """Find direwolf binary."""
return shutil.which('direwolf') return shutil.which('direwolf')
def find_multimon_ng() -> Optional[str]: def find_multimon_ng() -> str | None:
"""Find multimon-ng binary.""" """Find multimon-ng binary."""
return shutil.which('multimon-ng') return shutil.which('multimon-ng')
def find_rtl_fm() -> Optional[str]: def find_rtl_fm() -> str | None:
"""Find rtl_fm binary.""" """Find rtl_fm binary."""
return shutil.which('rtl_fm') return shutil.which('rtl_fm')
def find_rx_fm() -> Optional[str]: def find_rx_fm() -> str | None:
"""Find SoapySDR rx_fm binary.""" """Find SoapySDR rx_fm binary."""
return shutil.which('rx_fm') return shutil.which('rx_fm')
def find_rtl_power() -> Optional[str]: def find_rtl_power() -> str | None:
"""Find rtl_power binary for spectrum scanning.""" """Find rtl_power binary for spectrum scanning."""
return shutil.which('rtl_power') return shutil.which('rtl_power')
@@ -142,7 +143,7 @@ def normalize_aprs_output_line(line: str) -> str:
return normalized return normalized
def parse_aprs_packet(raw_packet: str) -> Optional[dict]: def parse_aprs_packet(raw_packet: str) -> dict | None:
"""Parse APRS packet into structured data. """Parse APRS packet into structured data.
Supports all major APRS packet types: Supports all major APRS packet types:
@@ -431,7 +432,7 @@ def parse_aprs_packet(raw_packet: str) -> Optional[dict]:
return None return None
def parse_position(data: str) -> Optional[dict]: def parse_position(data: str) -> dict | None:
"""Parse APRS position data.""" """Parse APRS position data."""
try: try:
# Format: DDMM.mmN/DDDMM.mmW (or similar with symbols) # Format: DDMM.mmN/DDDMM.mmW (or similar with symbols)
@@ -591,7 +592,7 @@ def parse_position(data: str) -> Optional[dict]:
return None return None
def parse_object(data: str) -> Optional[dict]: def parse_object(data: str) -> dict | None:
"""Parse APRS object data. """Parse APRS object data.
Object format: ;OBJECTNAME*DDHHMMzPOSITION or ;OBJECTNAME_DDHHMMzPOSITION Object format: ;OBJECTNAME*DDHHMMzPOSITION or ;OBJECTNAME_DDHHMMzPOSITION
@@ -649,7 +650,7 @@ def parse_object(data: str) -> Optional[dict]:
return None return None
def parse_item(data: str) -> Optional[dict]: def parse_item(data: str) -> dict | None:
"""Parse APRS item data. """Parse APRS item data.
Item format: )ITEMNAME!POSITION or )ITEMNAME_POSITION Item format: )ITEMNAME!POSITION or )ITEMNAME_POSITION
@@ -830,7 +831,7 @@ MIC_E_MESSAGE_TYPES = {
} }
def parse_mic_e(dest: str, data: str) -> Optional[dict]: def parse_mic_e(dest: str, data: str) -> dict | None:
"""Parse Mic-E encoded position from destination and data fields. """Parse Mic-E encoded position from destination and data fields.
Mic-E is a highly compressed format that encodes: Mic-E is a highly compressed format that encodes:
@@ -973,7 +974,7 @@ def parse_mic_e(dest: str, data: str) -> Optional[dict]:
return None return None
def parse_compressed_position(data: str) -> Optional[dict]: def parse_compressed_position(data: str) -> dict | None:
r"""Parse compressed position format (Base-91 encoding). r"""Parse compressed position format (Base-91 encoding).
Compressed format: /YYYYXXXX$csT Compressed format: /YYYYXXXX$csT
@@ -1057,7 +1058,7 @@ def parse_compressed_position(data: str) -> Optional[dict]:
return None return None
def parse_telemetry(data: str) -> Optional[dict]: def parse_telemetry(data: str) -> dict | None:
"""Parse APRS telemetry data. """Parse APRS telemetry data.
Format: T#sss,aaa,aaa,aaa,aaa,aaa,bbbbbbbb Format: T#sss,aaa,aaa,aaa,aaa,aaa,bbbbbbbb
@@ -1122,7 +1123,7 @@ def parse_telemetry(data: str) -> Optional[dict]:
return None return None
def parse_telemetry_definition(callsign: str, msg_type: str, content: str) -> Optional[dict]: def parse_telemetry_definition(callsign: str, msg_type: str, content: str) -> dict | None:
"""Parse telemetry definition messages (PARM, UNIT, EQNS, BITS). """Parse telemetry definition messages (PARM, UNIT, EQNS, BITS).
These messages define the meaning of telemetry values for a station. These messages define the meaning of telemetry values for a station.
@@ -1174,7 +1175,7 @@ def parse_telemetry_definition(callsign: str, msg_type: str, content: str) -> Op
return None return None
def parse_phg(data: str) -> Optional[dict]: def parse_phg(data: str) -> dict | None:
"""Parse PHG (Power/Height/Gain/Directivity) data. """Parse PHG (Power/Height/Gain/Directivity) data.
Format: PHGphgd Format: PHGphgd
@@ -1217,7 +1218,7 @@ def parse_phg(data: str) -> Optional[dict]:
return None return None
def parse_rng(data: str) -> Optional[dict]: def parse_rng(data: str) -> dict | None:
"""Parse RNG (radio range) data. """Parse RNG (radio range) data.
Format: RNGrrrr where rrrr is range in miles. Format: RNGrrrr where rrrr is range in miles.
@@ -1231,7 +1232,7 @@ def parse_rng(data: str) -> Optional[dict]:
return None return None
def parse_df_report(data: str) -> Optional[dict]: def parse_df_report(data: str) -> dict | None:
"""Parse Direction Finding (DF) report. """Parse Direction Finding (DF) report.
Format: CSE/SPD/BRG/NRQ or similar patterns. Format: CSE/SPD/BRG/NRQ or similar patterns.
@@ -1260,7 +1261,7 @@ def parse_df_report(data: str) -> Optional[dict]:
return None return None
def parse_timestamp(data: str) -> Optional[dict]: def parse_timestamp(data: str) -> dict | None:
"""Parse APRS timestamp from position data. """Parse APRS timestamp from position data.
Formats: Formats:
@@ -1304,7 +1305,7 @@ def parse_timestamp(data: str) -> Optional[dict]:
return None return None
def parse_third_party(data: str) -> Optional[dict]: def parse_third_party(data: str) -> dict | None:
"""Parse third-party traffic (packets relayed from another network). """Parse third-party traffic (packets relayed from another network).
Format: }CALL>PATH:DATA (the } indicates third-party) Format: }CALL>PATH:DATA (the } indicates third-party)
@@ -1330,7 +1331,7 @@ def parse_third_party(data: str) -> Optional[dict]:
return None return None
def parse_user_defined(data: str) -> Optional[dict]: def parse_user_defined(data: str) -> dict | None:
"""Parse user-defined data format. """Parse user-defined data format.
Format: {UUXXXX... Format: {UUXXXX...
@@ -1352,7 +1353,7 @@ def parse_user_defined(data: str) -> Optional[dict]:
return None return None
def parse_capabilities(data: str) -> Optional[dict]: def parse_capabilities(data: str) -> dict | None:
"""Parse station capabilities response. """Parse station capabilities response.
Format: <capability1,capability2,... Format: <capability1,capability2,...
@@ -1381,7 +1382,7 @@ def parse_capabilities(data: str) -> Optional[dict]:
return None return None
def parse_nmea(data: str) -> Optional[dict]: def parse_nmea(data: str) -> dict | None:
"""Parse raw GPS NMEA sentences. """Parse raw GPS NMEA sentences.
APRS can include raw NMEA data starting with $. APRS can include raw NMEA data starting with $.
@@ -1409,7 +1410,7 @@ def parse_nmea(data: str) -> Optional[dict]:
return None return None
def parse_audio_level(line: str) -> Optional[int]: def parse_audio_level(line: str) -> int | None:
"""Parse direwolf audio level line and return normalized level (0-100). """Parse direwolf audio level line and return normalized level (0-100).
Direwolf outputs lines like: Direwolf outputs lines like:
@@ -1579,10 +1580,8 @@ def stream_aprs_output(master_fd: int, rtl_process: subprocess.Popen, decoder_pr
logger.error(f"APRS stream error: {e}") logger.error(f"APRS stream error: {e}")
app_module.aprs_queue.put({'type': 'error', 'message': str(e)}) app_module.aprs_queue.put({'type': 'error', 'message': str(e)})
finally: finally:
try: with contextlib.suppress(OSError):
os.close(master_fd) os.close(master_fd)
except OSError:
pass
app_module.aprs_queue.put({'type': 'status', 'status': 'stopped'}) app_module.aprs_queue.put({'type': 'status', 'status': 'stopped'})
# Cleanup processes # Cleanup processes
for proc in [rtl_process, decoder_process]: for proc in [rtl_process, decoder_process]:
@@ -1590,10 +1589,8 @@ def stream_aprs_output(master_fd: int, rtl_process: subprocess.Popen, decoder_pr
proc.terminate() proc.terminate()
proc.wait(timeout=2) proc.wait(timeout=2)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
proc.kill() proc.kill()
except Exception:
pass
# Release SDR device — only if it's still ours (not reclaimed by a new start) # Release SDR device — only if it's still ours (not reclaimed by a new start)
if my_device is not None and aprs_active_device == my_device: if my_device is not None and aprs_active_device == my_device:
app_module.release_sdr_device(my_device, aprs_active_sdr_type or 'rtlsdr') app_module.release_sdr_device(my_device, aprs_active_sdr_type or 'rtlsdr')
@@ -1860,14 +1857,10 @@ def start_aprs() -> Response:
if stderr_output: if stderr_output:
error_msg += f': {stderr_output[:500]}' error_msg += f': {stderr_output[:500]}'
logger.error(error_msg) logger.error(error_msg)
try: with contextlib.suppress(OSError):
os.close(master_fd) os.close(master_fd)
except OSError: with contextlib.suppress(Exception):
pass
try:
decoder_process.kill() decoder_process.kill()
except Exception:
pass
if aprs_active_device is not None: if aprs_active_device is not None:
app_module.release_sdr_device(aprs_active_device, aprs_active_sdr_type or 'rtlsdr') app_module.release_sdr_device(aprs_active_device, aprs_active_sdr_type or 'rtlsdr')
aprs_active_device = None aprs_active_device = None
@@ -1888,14 +1881,10 @@ def start_aprs() -> Response:
if error_output: if error_output:
error_msg += f': {error_output}' error_msg += f': {error_output}'
logger.error(error_msg) logger.error(error_msg)
try: with contextlib.suppress(OSError):
os.close(master_fd) os.close(master_fd)
except OSError: with contextlib.suppress(Exception):
pass
try:
rtl_process.kill() rtl_process.kill()
except Exception:
pass
if aprs_active_device is not None: if aprs_active_device is not None:
app_module.release_sdr_device(aprs_active_device, aprs_active_sdr_type or 'rtlsdr') app_module.release_sdr_device(aprs_active_device, aprs_active_sdr_type or 'rtlsdr')
aprs_active_device = None aprs_active_device = None
@@ -1961,10 +1950,8 @@ def stop_aprs() -> Response:
# Close PTY master fd # Close PTY master fd
if hasattr(app_module, 'aprs_master_fd') and app_module.aprs_master_fd is not None: if hasattr(app_module, 'aprs_master_fd') and app_module.aprs_master_fd is not None:
try: with contextlib.suppress(OSError):
os.close(app_module.aprs_master_fd) os.close(app_module.aprs_master_fd)
except OSError:
pass
app_module.aprs_master_fd = None app_module.aprs_master_fd = None
app_module.aprs_process = None app_module.aprs_process = None
@@ -2099,7 +2086,7 @@ def scan_aprs_spectrum() -> Response:
return api_error('rtl_power did not produce output file', 500) return api_error('rtl_power did not produce output file', 500)
bins = [] bins = []
with open(tmp_file, 'r') as f: with open(tmp_file) as f:
reader = csv.reader(f) reader = csv.reader(f)
for row in reader: for row in reader:
if len(row) < 7: if len(row) < 7:
+8 -15
View File
@@ -6,6 +6,7 @@ import socket
import subprocess import subprocess
import threading import threading
import time import time
from flask import Flask from flask import Flask
# Try to import flask-sock # Try to import flask-sock
@@ -16,6 +17,8 @@ except ImportError:
WEBSOCKET_AVAILABLE = False WEBSOCKET_AVAILABLE = False
Sock = None Sock = None
import contextlib
from utils.logging import get_logger from utils.logging import get_logger
logger = get_logger('intercept.audio_ws') logger = get_logger('intercept.audio_ws')
@@ -56,10 +59,8 @@ def kill_audio_processes():
audio_process.terminate() audio_process.terminate()
audio_process.wait(timeout=0.5) audio_process.wait(timeout=0.5)
except: except:
try: with contextlib.suppress(BaseException):
audio_process.kill() audio_process.kill()
except:
pass
audio_process = None audio_process = None
if rtl_process: if rtl_process:
@@ -67,10 +68,8 @@ def kill_audio_processes():
rtl_process.terminate() rtl_process.terminate()
rtl_process.wait(timeout=0.5) rtl_process.wait(timeout=0.5)
except: except:
try: with contextlib.suppress(BaseException):
rtl_process.kill() rtl_process.kill()
except:
pass
rtl_process = None rtl_process = None
time.sleep(0.3) time.sleep(0.3)
@@ -261,16 +260,10 @@ def init_audio_websocket(app: Flask):
# Complete WebSocket close handshake, then shut down the # Complete WebSocket close handshake, then shut down the
# raw socket so Werkzeug cannot write its HTTP 200 response # raw socket so Werkzeug cannot write its HTTP 200 response
# on top of the WebSocket stream. # on top of the WebSocket stream.
try: with contextlib.suppress(Exception):
ws.close() ws.close()
except Exception: with contextlib.suppress(Exception):
pass
try:
ws.sock.shutdown(socket.SHUT_RDWR) ws.sock.shutdown(socket.SHUT_RDWR)
except Exception: with contextlib.suppress(Exception):
pass
try:
ws.sock.close() ws.sock.close()
except Exception:
pass
logger.info("WebSocket audio client disconnected") logger.info("WebSocket audio client disconnected")
+14 -27
View File
@@ -2,8 +2,7 @@
from __future__ import annotations from __future__ import annotations
import fcntl import contextlib
import json
import os import os
import platform import platform
import pty import pty
@@ -13,30 +12,22 @@ import select
import subprocess import subprocess
import threading import threading
import time import time
from typing import Any, Generator from typing import Any
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from utils.dependencies import check_tool from data.oui import OUI_DATABASE, get_manufacturer, load_oui_database
from utils.logging import bluetooth_logger as logger from data.patterns import AIRTAG_PREFIXES, SAMSUNG_TRACKER, TILE_PREFIXES
from utils.sse import sse_stream_fanout
from utils.event_pipeline import process_event
from utils.validation import validate_bluetooth_interface
from data.oui import OUI_DATABASE, load_oui_database, get_manufacturer
from data.patterns import AIRTAG_PREFIXES, TILE_PREFIXES, SAMSUNG_TRACKER
from utils.constants import ( from utils.constants import (
BT_TERMINATE_TIMEOUT,
SSE_KEEPALIVE_INTERVAL,
SSE_QUEUE_TIMEOUT,
SUBPROCESS_TIMEOUT_SHORT, SUBPROCESS_TIMEOUT_SHORT,
SERVICE_ENUM_TIMEOUT,
PROCESS_START_WAIT,
BT_RESET_DELAY,
BT_ADAPTER_DOWN_WAIT,
PROCESS_TERMINATE_TIMEOUT,
) )
from utils.dependencies import check_tool
from utils.event_pipeline import process_event
from utils.logging import bluetooth_logger as logger
from utils.responses import api_error, api_success
from utils.sse import sse_stream_fanout
from utils.validation import validate_bluetooth_interface
bluetooth_bp = Blueprint('bluetooth', __name__, url_prefix='/bt') bluetooth_bp = Blueprint('bluetooth', __name__, url_prefix='/bt')
@@ -328,10 +319,8 @@ def stream_bt_scan(process, scan_mode):
except OSError: except OSError:
break break
try: with contextlib.suppress(OSError):
os.close(master_fd) os.close(master_fd)
except OSError:
pass
except Exception as e: except Exception as e:
app_module.bt_queue.put({'type': 'error', 'text': str(e)}) app_module.bt_queue.put({'type': 'error', 'text': str(e)})
@@ -485,10 +474,8 @@ def reset_bt_adapter():
app_module.bt_process.terminate() app_module.bt_process.terminate()
app_module.bt_process.wait(timeout=2) app_module.bt_process.wait(timeout=2)
except (subprocess.TimeoutExpired, OSError): except (subprocess.TimeoutExpired, OSError):
try: with contextlib.suppress(OSError):
app_module.bt_process.kill() app_module.bt_process.kill()
except OSError:
pass
app_module.bt_process = None app_module.bt_process = None
try: try:
@@ -507,7 +494,7 @@ def reset_bt_adapter():
return jsonify({ return jsonify({
'status': 'success' if is_up else 'warning', 'status': 'success' if is_up else 'warning',
'message': f'Adapter {interface} reset' if is_up else f'Reset attempted but adapter may still be down', 'message': f'Adapter {interface} reset' if is_up else 'Reset attempted but adapter may still be down',
'is_up': is_up 'is_up': is_up
}) })
+7 -14
View File
@@ -7,31 +7,27 @@ aggregation, and heuristics.
from __future__ import annotations from __future__ import annotations
import contextlib
import csv import csv
import io import io
import json import json
import logging import logging
import threading import threading
import time import time
from collections.abc import Generator
from datetime import datetime from datetime import datetime
from typing import Generator
from flask import Blueprint, Response, jsonify, request, session from flask import Blueprint, Response, jsonify, request
from utils.bluetooth import ( from utils.bluetooth import (
BluetoothScanner,
BTDeviceAggregate, BTDeviceAggregate,
get_bluetooth_scanner,
check_capabilities, check_capabilities,
RANGE_UNKNOWN, get_bluetooth_scanner,
TrackerType,
TrackerConfidence,
get_tracker_engine,
) )
from utils.database import get_db from utils.database import get_db
from utils.responses import api_success, api_error
from utils.sse import format_sse
from utils.event_pipeline import process_event from utils.event_pipeline import process_event
from utils.responses import api_error
from utils.sse import format_sse
logger = logging.getLogger('intercept.bluetooth_v2') logger = logging.getLogger('intercept.bluetooth_v2')
@@ -901,10 +897,8 @@ def stream_events():
"""Generate SSE events from scanner.""" """Generate SSE events from scanner."""
for event in scanner.stream_events(timeout=1.0): for event in scanner.stream_events(timeout=1.0):
event_name, event_data = map_event_type(event) event_name, event_data = map_event_type(event)
try: with contextlib.suppress(Exception):
process_event('bluetooth', event_data, event_name) process_event('bluetooth', event_data, event_name)
except Exception:
pass
yield format_sse(event_data, event=event_name) yield format_sse(event_data, event=event_name)
return Response( return Response(
@@ -972,7 +966,6 @@ def get_tscm_bluetooth_snapshot(duration: int = 8) -> list[dict]:
Returns: Returns:
List of device dictionaries in TSCM format. List of device dictionaries in TSCM format.
""" """
import time
import logging import logging
logger = logging.getLogger('intercept.bluetooth_v2') logger = logging.getLogger('intercept.bluetooth_v2')
+1 -1
View File
@@ -12,7 +12,6 @@ from collections.abc import Generator
from flask import Blueprint, Response, jsonify, request from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
from utils.bluetooth.irk_extractor import get_paired_irks from utils.bluetooth.irk_extractor import get_paired_irks
from utils.bt_locate import ( from utils.bt_locate import (
Environment, Environment,
@@ -22,6 +21,7 @@ from utils.bt_locate import (
start_locate_session, start_locate_session,
stop_locate_session, stop_locate_session,
) )
from utils.responses import api_error
from utils.sse import format_sse from utils.sse import format_sse
logger = logging.getLogger('intercept.bt_locate') logger = logging.getLogger('intercept.bt_locate')
+17 -12
View File
@@ -10,30 +10,34 @@ This blueprint provides:
from __future__ import annotations from __future__ import annotations
import json
import logging import logging
import queue import queue
import threading import threading
import time import time
from collections.abc import Generator
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Generator
import requests import requests
from flask import Blueprint, Response, jsonify, request
from flask import Blueprint, jsonify, request, Response from utils.agent_client import AgentClient, AgentConnectionError, AgentHTTPError, create_client_from_agent
from utils.responses import api_success, api_error
from utils.database import ( from utils.database import (
create_agent, get_agent, get_agent_by_name, list_agents, create_agent,
update_agent, delete_agent, store_push_payload, get_recent_payloads delete_agent,
) get_agent,
from utils.agent_client import ( get_agent_by_name,
AgentClient, AgentHTTPError, AgentConnectionError, create_client_from_agent get_recent_payloads,
list_agents,
store_push_payload,
update_agent,
) )
from utils.responses import api_error
from utils.sse import format_sse from utils.sse import format_sse
from utils.trilateration import ( from utils.trilateration import (
DeviceLocationTracker, PathLossModel, Trilateration, DeviceLocationTracker,
AgentObservation, estimate_location_from_observations PathLossModel,
Trilateration,
estimate_location_from_observations,
) )
logger = logging.getLogger('intercept.controller') logger = logging.getLogger('intercept.controller')
@@ -700,6 +704,7 @@ def stream_all_agents():
def agent_management_page(): def agent_management_page():
"""Render the agent management page.""" """Render the agent management page."""
from flask import render_template from flask import render_template
from config import VERSION from config import VERSION
return render_template('agents.html', version=VERSION) return render_template('agents.html', version=VERSION)
+2 -2
View File
@@ -2,12 +2,12 @@
from __future__ import annotations from __future__ import annotations
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, request
import app as app_module import app as app_module
from utils.correlation import get_correlations from utils.correlation import get_correlations
from utils.responses import api_success, api_error
from utils.logging import get_logger from utils.logging import get_logger
from utils.responses import api_error, api_success
logger = get_logger('intercept.correlation') logger = get_logger('intercept.correlation')
+19 -32
View File
@@ -6,7 +6,7 @@ distress and safety communications per ITU-R M.493.
from __future__ import annotations from __future__ import annotations
import json import contextlib
import logging import logging
import os import os
import pty import pty
@@ -16,37 +16,36 @@ import shutil
import subprocess import subprocess
import threading import threading
import time import time
from datetime import datetime from typing import Any
from typing import Any, Generator
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from utils.constants import ( from utils.constants import (
DSC_VHF_FREQUENCY_MHZ,
DSC_SAMPLE_RATE, DSC_SAMPLE_RATE,
DSC_TERMINATE_TIMEOUT, DSC_TERMINATE_TIMEOUT,
DSC_VHF_FREQUENCY_MHZ,
) )
from utils.database import ( from utils.database import (
store_dsc_alert,
get_dsc_alerts,
get_dsc_alert,
acknowledge_dsc_alert, acknowledge_dsc_alert,
get_dsc_alert,
get_dsc_alert_summary, get_dsc_alert_summary,
get_dsc_alerts,
store_dsc_alert,
) )
from utils.dependencies import get_tool_path
from utils.dsc.parser import parse_dsc_message from utils.dsc.parser import parse_dsc_message
from utils.sse import sse_stream_fanout
from utils.event_pipeline import process_event from utils.event_pipeline import process_event
from utils.process import register_process, unregister_process
from utils.responses import api_error
from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout
from utils.validation import ( from utils.validation import (
validate_device_index, validate_device_index,
validate_gain, validate_gain,
validate_rtl_tcp_host, validate_rtl_tcp_host,
validate_rtl_tcp_port, validate_rtl_tcp_port,
) )
from utils.sdr import SDRFactory, SDRType
from utils.dependencies import get_tool_path
from utils.process import register_process, unregister_process
logger = logging.getLogger('intercept.dsc') logger = logging.getLogger('intercept.dsc')
@@ -83,8 +82,8 @@ def _check_dsc_tools() -> dict:
# Check for scipy/numpy (needed for decoder) # Check for scipy/numpy (needed for decoder)
scipy_available = False scipy_available = False
try: try:
import scipy
import numpy import numpy
import scipy
scipy_available = True scipy_available = True
except ImportError: except ImportError:
pass pass
@@ -179,10 +178,8 @@ def stream_dsc_decoder(master_fd: int, decoder_process: subprocess.Popen) -> Non
}) })
finally: finally:
global dsc_active_device, dsc_active_sdr_type global dsc_active_device, dsc_active_sdr_type
try: with contextlib.suppress(OSError):
os.close(master_fd) os.close(master_fd)
except OSError:
pass
dsc_running = False dsc_running = False
# Cleanup both processes # Cleanup both processes
with app_module.dsc_lock: with app_module.dsc_lock:
@@ -193,10 +190,8 @@ def stream_dsc_decoder(master_fd: int, decoder_process: subprocess.Popen) -> Non
proc.terminate() proc.terminate()
proc.wait(timeout=2) proc.wait(timeout=2)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
proc.kill() proc.kill()
except Exception:
pass
unregister_process(proc) unregister_process(proc)
app_module.dsc_queue.put({'type': 'status', 'status': 'stopped'}) app_module.dsc_queue.put({'type': 'status', 'status': 'stopped'})
with app_module.dsc_lock: with app_module.dsc_lock:
@@ -466,10 +461,8 @@ def start_decoding() -> Response:
rtl_process.terminate() rtl_process.terminate()
rtl_process.wait(timeout=2) rtl_process.wait(timeout=2)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
rtl_process.kill() rtl_process.kill()
except Exception:
pass
# Release device on failure # Release device on failure
if dsc_active_device is not None: if dsc_active_device is not None:
app_module.release_sdr_device(dsc_active_device, dsc_active_sdr_type or 'rtlsdr') app_module.release_sdr_device(dsc_active_device, dsc_active_sdr_type or 'rtlsdr')
@@ -485,10 +478,8 @@ def start_decoding() -> Response:
rtl_process.terminate() rtl_process.terminate()
rtl_process.wait(timeout=2) rtl_process.wait(timeout=2)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
rtl_process.kill() rtl_process.kill()
except Exception:
pass
# Release device on failure # Release device on failure
if dsc_active_device is not None: if dsc_active_device is not None:
app_module.release_sdr_device(dsc_active_device, dsc_active_sdr_type or 'rtlsdr') app_module.release_sdr_device(dsc_active_device, dsc_active_sdr_type or 'rtlsdr')
@@ -518,10 +509,8 @@ def stop_decoding() -> Response:
app_module.dsc_rtl_process.terminate() app_module.dsc_rtl_process.terminate()
app_module.dsc_rtl_process.wait(timeout=DSC_TERMINATE_TIMEOUT) app_module.dsc_rtl_process.wait(timeout=DSC_TERMINATE_TIMEOUT)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
try: with contextlib.suppress(OSError):
app_module.dsc_rtl_process.kill() app_module.dsc_rtl_process.kill()
except OSError:
pass
except OSError: except OSError:
pass pass
@@ -531,10 +520,8 @@ def stop_decoding() -> Response:
app_module.dsc_process.terminate() app_module.dsc_process.terminate()
app_module.dsc_process.wait(timeout=DSC_TERMINATE_TIMEOUT) app_module.dsc_process.wait(timeout=DSC_TERMINATE_TIMEOUT)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
try: with contextlib.suppress(OSError):
app_module.dsc_process.kill() app_module.dsc_process.kill()
except OSError:
pass
except OSError: except OSError:
pass pass
-3
View File
@@ -3,12 +3,9 @@
from __future__ import annotations from __future__ import annotations
import queue import queue
import time
from collections.abc import Generator
from flask import Blueprint, Response, jsonify from flask import Blueprint, Response, jsonify
from utils.responses import api_success, api_error
from utils.gps import ( from utils.gps import (
GPSPosition, GPSPosition,
GPSSkyData, GPSSkyData,
+26 -26
View File
@@ -11,8 +11,8 @@ from __future__ import annotations
import os import os
import queue import queue
import signal
import shutil import shutil
import signal
import struct import struct
import subprocess import subprocess
import threading import threading
@@ -22,15 +22,15 @@ from typing import Dict, List, Optional
from flask import Blueprint from flask import Blueprint
from utils.logging import get_logger
from utils.sse import sse_stream_fanout
from utils.event_pipeline import process_event
from utils.constants import ( from utils.constants import (
SSE_QUEUE_TIMEOUT,
SSE_KEEPALIVE_INTERVAL,
PROCESS_TERMINATE_TIMEOUT, PROCESS_TERMINATE_TIMEOUT,
SSE_KEEPALIVE_INTERVAL,
SSE_QUEUE_TIMEOUT,
) )
from utils.event_pipeline import process_event
from utils.logging import get_logger
from utils.sdr import SDRFactory, SDRType from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout
logger = get_logger('intercept.receiver') logger = get_logger('intercept.receiver')
@@ -39,6 +39,8 @@ receiver_bp = Blueprint('receiver', __name__, url_prefix='/receiver')
# Deferred import to avoid circular import at module load time. # Deferred import to avoid circular import at module load time.
# app.py -> register_blueprints -> from .listening_post import receiver_bp # app.py -> register_blueprints -> from .listening_post import receiver_bp
# must find receiver_bp already defined (above) before this import runs. # must find receiver_bp already defined (above) before this import runs.
import contextlib
import app as app_module # noqa: E402 import app as app_module # noqa: E402
# ============================================ # ============================================
@@ -57,16 +59,16 @@ audio_source = 'process'
audio_start_token = 0 audio_start_token = 0
# Scanner state # Scanner state
scanner_thread: Optional[threading.Thread] = None scanner_thread: threading.Thread | None = None
scanner_running = False scanner_running = False
scanner_lock = threading.Lock() scanner_lock = threading.Lock()
scanner_paused = False scanner_paused = False
scanner_current_freq = 0.0 scanner_current_freq = 0.0
scanner_active_device: Optional[int] = None scanner_active_device: int | None = None
scanner_active_sdr_type: str = 'rtlsdr' scanner_active_sdr_type: str = 'rtlsdr'
receiver_active_device: Optional[int] = None receiver_active_device: int | None = None
receiver_active_sdr_type: str = 'rtlsdr' receiver_active_sdr_type: str = 'rtlsdr'
scanner_power_process: Optional[subprocess.Popen] = None scanner_power_process: subprocess.Popen | None = None
scanner_config = { scanner_config = {
'start_freq': 88.0, 'start_freq': 88.0,
'end_freq': 108.0, 'end_freq': 108.0,
@@ -84,7 +86,7 @@ scanner_config = {
} }
# Activity log # Activity log
activity_log: List[Dict] = [] activity_log: list[dict] = []
activity_log_lock = threading.Lock() activity_log_lock = threading.Lock()
MAX_LOG_ENTRIES = 500 MAX_LOG_ENTRIES = 500
@@ -95,12 +97,12 @@ scanner_queue: queue.Queue = queue.Queue(maxsize=100)
scanner_skip_signal = False scanner_skip_signal = False
# Waterfall / spectrogram state # Waterfall / spectrogram state
waterfall_process: Optional[subprocess.Popen] = None waterfall_process: subprocess.Popen | None = None
waterfall_thread: Optional[threading.Thread] = None waterfall_thread: threading.Thread | None = None
waterfall_running = False waterfall_running = False
waterfall_lock = threading.Lock() waterfall_lock = threading.Lock()
waterfall_queue: queue.Queue = queue.Queue(maxsize=200) waterfall_queue: queue.Queue = queue.Queue(maxsize=200)
waterfall_active_device: Optional[int] = None waterfall_active_device: int | None = None
waterfall_active_sdr_type: str = 'rtlsdr' waterfall_active_sdr_type: str = 'rtlsdr'
waterfall_config = { waterfall_config = {
'start_freq': 88.0, 'start_freq': 88.0,
@@ -185,13 +187,11 @@ def add_activity_log(event_type: str, frequency: float, details: str = ''):
activity_log.pop() activity_log.pop()
# Also push to SSE queue # Also push to SSE queue
try: with contextlib.suppress(queue.Full):
scanner_queue.put_nowait({ scanner_queue.put_nowait({
'type': 'log', 'type': 'log',
'entry': entry 'entry': entry
}) })
except queue.Full:
pass
def _start_audio_stream( def _start_audio_stream(
@@ -348,12 +348,12 @@ def _start_audio_stream(
rtl_stderr = '' rtl_stderr = ''
ffmpeg_stderr = '' ffmpeg_stderr = ''
try: try:
with open(rtl_stderr_log, 'r') as f: with open(rtl_stderr_log) as f:
rtl_stderr = f.read().strip() rtl_stderr = f.read().strip()
except Exception: except Exception:
pass pass
try: try:
with open(ffmpeg_stderr_log, 'r') as f: with open(ffmpeg_stderr_log) as f:
ffmpeg_stderr = f.read().strip() ffmpeg_stderr = f.read().strip()
except Exception: except Exception:
pass pass
@@ -502,10 +502,8 @@ def _stop_waterfall_internal() -> None:
waterfall_process.terminate() waterfall_process.terminate()
waterfall_process.wait(timeout=1) waterfall_process.wait(timeout=1)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
waterfall_process.kill() waterfall_process.kill()
except Exception:
pass
waterfall_process = None waterfall_process = None
if waterfall_active_device is not None: if waterfall_active_device is not None:
@@ -517,7 +515,9 @@ def _stop_waterfall_internal() -> None:
# ============================================ # ============================================
# Import sub-modules to register routes on receiver_bp # Import sub-modules to register routes on receiver_bp
# ============================================ # ============================================
from . import scanner # noqa: E402, F401 from . import (
from . import audio # noqa: E402, F401 audio, # noqa: E402, F401
from . import waterfall # noqa: E402, F401 scanner, # noqa: E402, F401
from . import tools # noqa: E402, F401 tools, # noqa: E402, F401
waterfall, # noqa: E402, F401
)
+14 -20
View File
@@ -2,27 +2,27 @@
from __future__ import annotations from __future__ import annotations
import contextlib
import os import os
import select import select
import subprocess import subprocess
import time import time
from typing import Any
from flask import jsonify, request, Response from flask import Response, jsonify, request
import routes.listening_post as _state
from . import ( from . import (
receiver_bp,
logger,
app_module,
scanner_config,
_wav_header,
_start_audio_stream, _start_audio_stream,
_stop_audio_stream, _stop_audio_stream,
_stop_waterfall_internal, _stop_waterfall_internal,
_wav_header,
app_module,
logger,
normalize_modulation, normalize_modulation,
receiver_bp,
scanner_config,
) )
import routes.listening_post as _state
# ============================================ # ============================================
# MANUAL AUDIO ENDPOINTS (for direct listening) # MANUAL AUDIO ENDPOINTS (for direct listening)
@@ -106,23 +106,17 @@ def start_audio() -> Response:
# Scanner teardown outside lock (blocking: thread join, process wait, pkill, sleep) # Scanner teardown outside lock (blocking: thread join, process wait, pkill, sleep)
if need_scanner_teardown: if need_scanner_teardown:
if scanner_thread_ref and scanner_thread_ref.is_alive(): if scanner_thread_ref and scanner_thread_ref.is_alive():
try: with contextlib.suppress(Exception):
scanner_thread_ref.join(timeout=2.0) scanner_thread_ref.join(timeout=2.0)
except Exception:
pass
if scanner_proc_ref and scanner_proc_ref.poll() is None: if scanner_proc_ref and scanner_proc_ref.poll() is None:
try: try:
scanner_proc_ref.terminate() scanner_proc_ref.terminate()
scanner_proc_ref.wait(timeout=1) scanner_proc_ref.wait(timeout=1)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
scanner_proc_ref.kill() scanner_proc_ref.kill()
except Exception: with contextlib.suppress(Exception):
pass
try:
subprocess.run(['pkill', '-9', 'rtl_power'], capture_output=True, timeout=0.5) subprocess.run(['pkill', '-9', 'rtl_power'], capture_output=True, timeout=0.5)
except Exception:
pass
time.sleep(0.5) time.sleep(0.5)
# Re-acquire lock for waterfall check and device claim # Re-acquire lock for waterfall check and device claim
@@ -232,7 +226,7 @@ def start_audio() -> Response:
start_error = '' start_error = ''
for log_path in ('/tmp/rtl_fm_stderr.log', '/tmp/ffmpeg_stderr.log'): for log_path in ('/tmp/rtl_fm_stderr.log', '/tmp/ffmpeg_stderr.log'):
try: try:
with open(log_path, 'r') as handle: with open(log_path) as handle:
content = handle.read().strip() content = handle.read().strip()
if content: if content:
start_error = content.splitlines()[-1] start_error = content.splitlines()[-1]
@@ -290,7 +284,7 @@ def audio_debug() -> Response:
def _read_log(path: str) -> str: def _read_log(path: str) -> str:
try: try:
with open(path, 'r') as handle: with open(path) as handle:
return handle.read().strip() return handle.read().strip()
except Exception: except Exception:
return '' return ''
+32 -52
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import contextlib
import math import math
import queue import queue
import struct import struct
@@ -10,32 +11,32 @@ import threading
import time import time
from typing import Any from typing import Any
from flask import jsonify, request, Response from flask import Response, jsonify, request
import routes.listening_post as _state
from . import ( from . import (
receiver_bp, SSE_KEEPALIVE_INTERVAL,
logger, SSE_QUEUE_TIMEOUT,
app_module,
scanner_queue,
scanner_config,
scanner_lock,
activity_log,
activity_log_lock,
add_activity_log,
find_rtl_fm,
find_rtl_power,
find_rx_fm,
normalize_modulation,
_rtl_fm_demod_mode, _rtl_fm_demod_mode,
_start_audio_stream, _start_audio_stream,
_stop_audio_stream, _stop_audio_stream,
activity_log,
activity_log_lock,
add_activity_log,
app_module,
find_rtl_fm,
find_rtl_power,
find_rx_fm,
logger,
normalize_modulation,
process_event, process_event,
receiver_bp,
scanner_config,
scanner_lock,
scanner_queue,
sse_stream_fanout, sse_stream_fanout,
SSE_QUEUE_TIMEOUT,
SSE_KEEPALIVE_INTERVAL,
) )
import routes.listening_post as _state
# ============================================ # ============================================
# SCANNER IMPLEMENTATION # SCANNER IMPLEMENTATION
@@ -76,7 +77,7 @@ def scanner_loop():
_state.scanner_current_freq = current_freq _state.scanner_current_freq = current_freq
# Notify clients of frequency change # Notify clients of frequency change
try: with contextlib.suppress(queue.Full):
scanner_queue.put_nowait({ scanner_queue.put_nowait({
'type': 'freq_change', 'type': 'freq_change',
'frequency': current_freq, 'frequency': current_freq,
@@ -84,8 +85,6 @@ def scanner_loop():
'range_start': scanner_config['start_freq'], 'range_start': scanner_config['start_freq'],
'range_end': scanner_config['end_freq'] 'range_end': scanner_config['end_freq']
}) })
except queue.Full:
pass
# Start rtl_fm at this frequency # Start rtl_fm at this frequency
freq_hz = int(current_freq * 1e6) freq_hz = int(current_freq * 1e6)
@@ -168,7 +167,7 @@ def scanner_loop():
audio_detected = rms > effective_threshold audio_detected = rms > effective_threshold
# Send level info to clients # Send level info to clients
try: with contextlib.suppress(queue.Full):
scanner_queue.put_nowait({ scanner_queue.put_nowait({
'type': 'scan_update', 'type': 'scan_update',
'frequency': current_freq, 'frequency': current_freq,
@@ -178,8 +177,6 @@ def scanner_loop():
'range_start': scanner_config['start_freq'], 'range_start': scanner_config['start_freq'],
'range_end': scanner_config['end_freq'] 'range_end': scanner_config['end_freq']
}) })
except queue.Full:
pass
if audio_detected and _state.scanner_running: if audio_detected and _state.scanner_running:
if not signal_detected: if not signal_detected:
@@ -214,13 +211,11 @@ def scanner_loop():
_state.scanner_skip_signal = False _state.scanner_skip_signal = False
signal_detected = False signal_detected = False
_stop_audio_stream() _stop_audio_stream()
try: with contextlib.suppress(queue.Full):
scanner_queue.put_nowait({ scanner_queue.put_nowait({
'type': 'signal_skipped', 'type': 'signal_skipped',
'frequency': current_freq 'frequency': current_freq
}) })
except queue.Full:
pass
# Move to next frequency (step is in kHz, convert to MHz) # Move to next frequency (step is in kHz, convert to MHz)
current_freq += step_mhz current_freq += step_mhz
if current_freq > scanner_config['end_freq']: if current_freq > scanner_config['end_freq']:
@@ -240,15 +235,13 @@ def scanner_loop():
if _state.scanner_running and not _state.scanner_skip_signal: if _state.scanner_running and not _state.scanner_skip_signal:
signal_detected = False signal_detected = False
_stop_audio_stream() _stop_audio_stream()
try: with contextlib.suppress(queue.Full):
scanner_queue.put_nowait({ scanner_queue.put_nowait({
'type': 'signal_lost', 'type': 'signal_lost',
'frequency': current_freq, 'frequency': current_freq,
'range_start': scanner_config['start_freq'], 'range_start': scanner_config['start_freq'],
'range_end': scanner_config['end_freq'] 'range_end': scanner_config['end_freq']
}) })
except queue.Full:
pass
current_freq += step_mhz current_freq += step_mhz
if current_freq > scanner_config['end_freq']: if current_freq > scanner_config['end_freq']:
@@ -268,13 +261,11 @@ def scanner_loop():
# Stop audio # Stop audio
_stop_audio_stream() _stop_audio_stream()
try: with contextlib.suppress(queue.Full):
scanner_queue.put_nowait({ scanner_queue.put_nowait({
'type': 'signal_lost', 'type': 'signal_lost',
'frequency': current_freq 'frequency': current_freq
}) })
except queue.Full:
pass
# Move to next frequency (step is in kHz, convert to MHz) # Move to next frequency (step is in kHz, convert to MHz)
current_freq += step_mhz current_freq += step_mhz
@@ -321,7 +312,7 @@ def scanner_loop_power():
step_khz = scanner_config['step'] step_khz = scanner_config['step']
gain = scanner_config['gain'] gain = scanner_config['gain']
device = scanner_config['device'] device = scanner_config['device']
squelch = scanner_config['squelch'] scanner_config['squelch']
mod = scanner_config['modulation'] mod = scanner_config['modulation']
# Configure sweep # Configure sweep
@@ -355,7 +346,7 @@ def scanner_loop_power():
if not stdout: if not stdout:
add_activity_log('error', start_mhz, 'Power sweep produced no data') add_activity_log('error', start_mhz, 'Power sweep produced no data')
try: with contextlib.suppress(queue.Full):
scanner_queue.put_nowait({ scanner_queue.put_nowait({
'type': 'scan_update', 'type': 'scan_update',
'frequency': end_mhz, 'frequency': end_mhz,
@@ -365,8 +356,6 @@ def scanner_loop_power():
'range_start': scanner_config['start_freq'], 'range_start': scanner_config['start_freq'],
'range_end': scanner_config['end_freq'] 'range_end': scanner_config['end_freq']
}) })
except queue.Full:
pass
time.sleep(0.2) time.sleep(0.2)
continue continue
@@ -414,7 +403,7 @@ def scanner_loop_power():
if not segments: if not segments:
add_activity_log('error', start_mhz, 'Power sweep bins missing') add_activity_log('error', start_mhz, 'Power sweep bins missing')
try: with contextlib.suppress(queue.Full):
scanner_queue.put_nowait({ scanner_queue.put_nowait({
'type': 'scan_update', 'type': 'scan_update',
'frequency': end_mhz, 'frequency': end_mhz,
@@ -424,8 +413,6 @@ def scanner_loop_power():
'range_start': scanner_config['start_freq'], 'range_start': scanner_config['start_freq'],
'range_end': scanner_config['end_freq'] 'range_end': scanner_config['end_freq']
}) })
except queue.Full:
pass
time.sleep(0.2) time.sleep(0.2)
continue continue
@@ -457,7 +444,7 @@ def scanner_loop_power():
level = int(max(0, snr) * 100) level = int(max(0, snr) * 100)
threshold = int(snr_threshold * 100) threshold = int(snr_threshold * 100)
progress = min(1.0, (segment_offset + idx) / max(1, total_bins - 1)) progress = min(1.0, (segment_offset + idx) / max(1, total_bins - 1))
try: with contextlib.suppress(queue.Full):
scanner_queue.put_nowait({ scanner_queue.put_nowait({
'type': 'scan_update', 'type': 'scan_update',
'frequency': _state.scanner_current_freq, 'frequency': _state.scanner_current_freq,
@@ -468,8 +455,6 @@ def scanner_loop_power():
'range_start': scanner_config['start_freq'], 'range_start': scanner_config['start_freq'],
'range_end': scanner_config['end_freq'] 'range_end': scanner_config['end_freq']
}) })
except queue.Full:
pass
segment_offset += len(bin_values) segment_offset += len(bin_values)
# Detect peaks (clusters above threshold) # Detect peaks (clusters above threshold)
@@ -505,7 +490,7 @@ def scanner_loop_power():
threshold = int(snr_threshold * 100) threshold = int(snr_threshold * 100)
add_activity_log('signal_found', freq_mhz, add_activity_log('signal_found', freq_mhz,
f'Peak detected at {freq_mhz:.3f} MHz ({mod.upper()})') f'Peak detected at {freq_mhz:.3f} MHz ({mod.upper()})')
try: with contextlib.suppress(queue.Full):
scanner_queue.put_nowait({ scanner_queue.put_nowait({
'type': 'signal_found', 'type': 'signal_found',
'frequency': freq_mhz, 'frequency': freq_mhz,
@@ -517,8 +502,6 @@ def scanner_loop_power():
'range_start': scanner_config['start_freq'], 'range_start': scanner_config['start_freq'],
'range_end': scanner_config['end_freq'] 'range_end': scanner_config['end_freq']
}) })
except queue.Full:
pass
add_activity_log('scan_cycle', start_mhz, 'Power sweep complete') add_activity_log('scan_cycle', start_mhz, 'Power sweep complete')
time.sleep(max(0.1, scanner_config.get('scan_delay', 0.5))) time.sleep(max(0.1, scanner_config.get('scan_delay', 0.5)))
@@ -590,9 +573,8 @@ def start_scanner() -> Response:
sdr_type = scanner_config['sdr_type'] sdr_type = scanner_config['sdr_type']
# Power scan only supports RTL-SDR for now # Power scan only supports RTL-SDR for now
if scanner_config['scan_method'] == 'power': if scanner_config['scan_method'] == 'power' and (sdr_type != 'rtlsdr' or not find_rtl_power()):
if sdr_type != 'rtlsdr' or not find_rtl_power(): scanner_config['scan_method'] = 'classic'
scanner_config['scan_method'] = 'classic'
# Check tools based on chosen method # Check tools based on chosen method
if scanner_config['scan_method'] == 'power': if scanner_config['scan_method'] == 'power':
@@ -666,10 +648,8 @@ def stop_scanner() -> Response:
_state.scanner_power_process.terminate() _state.scanner_power_process.terminate()
_state.scanner_power_process.wait(timeout=1) _state.scanner_power_process.wait(timeout=1)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
_state.scanner_power_process.kill() _state.scanner_power_process.kill()
except Exception:
pass
_state.scanner_power_process = None _state.scanner_power_process = None
if _state.scanner_active_device is not None: if _state.scanner_active_device is not None:
app_module.release_sdr_device(_state.scanner_active_device, _state.scanner_active_sdr_type) app_module.release_sdr_device(_state.scanner_active_device, _state.scanner_active_sdr_type)
+4 -5
View File
@@ -2,18 +2,17 @@
from __future__ import annotations from __future__ import annotations
from flask import jsonify, request, Response from flask import Response, jsonify, request
from . import ( from . import (
receiver_bp, find_ffmpeg,
logger,
find_rtl_fm, find_rtl_fm,
find_rtl_power, find_rtl_power,
find_rx_fm, find_rx_fm,
find_ffmpeg, logger,
receiver_bp,
) )
# ============================================ # ============================================
# TOOL CHECK ENDPOINT # TOOL CHECK ENDPOINT
# ============================================ # ============================================
+25 -41
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import contextlib
import math import math
import queue import queue
import struct import struct
@@ -11,23 +12,23 @@ import time
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from flask import jsonify, request, Response from flask import Response, jsonify, request
from . import (
receiver_bp,
logger,
app_module,
_stop_waterfall_internal,
process_event,
sse_stream_fanout,
SSE_QUEUE_TIMEOUT,
SSE_KEEPALIVE_INTERVAL,
find_rtl_power,
SDRFactory,
SDRType,
)
import routes.listening_post as _state import routes.listening_post as _state
from . import (
SSE_KEEPALIVE_INTERVAL,
SSE_QUEUE_TIMEOUT,
SDRFactory,
SDRType,
_stop_waterfall_internal,
app_module,
find_rtl_power,
logger,
process_event,
receiver_bp,
sse_stream_fanout,
)
# ============================================ # ============================================
# WATERFALL HELPER FUNCTIONS # WATERFALL HELPER FUNCTIONS
@@ -75,14 +76,12 @@ def _parse_rtl_power_line(line: str) -> tuple[str | None, float | None, float |
def _queue_waterfall_error(message: str) -> None: def _queue_waterfall_error(message: str) -> None:
"""Push an error message onto the waterfall SSE queue.""" """Push an error message onto the waterfall SSE queue."""
try: with contextlib.suppress(queue.Full):
_state.waterfall_queue.put_nowait({ _state.waterfall_queue.put_nowait({
'type': 'waterfall_error', 'type': 'waterfall_error',
'message': message, 'message': message,
'timestamp': datetime.now().isoformat(), 'timestamp': datetime.now().isoformat(),
}) })
except queue.Full:
pass
def _downsample_bins(values: list[float], target: int) -> list[float]: def _downsample_bins(values: list[float], target: int) -> list[float]:
@@ -229,14 +228,10 @@ def _waterfall_loop_iq(sdr_type: SDRType):
try: try:
_state.waterfall_queue.put_nowait(msg) _state.waterfall_queue.put_nowait(msg)
except queue.Full: except queue.Full:
try: with contextlib.suppress(queue.Empty):
_state.waterfall_queue.get_nowait() _state.waterfall_queue.get_nowait()
except queue.Empty: with contextlib.suppress(queue.Full):
pass
try:
_state.waterfall_queue.put_nowait(msg) _state.waterfall_queue.put_nowait(msg)
except queue.Full:
pass
# Throttle to respect interval # Throttle to respect interval
time.sleep(interval) time.sleep(interval)
@@ -254,10 +249,8 @@ def _waterfall_loop_iq(sdr_type: SDRType):
_state.waterfall_process.terminate() _state.waterfall_process.terminate()
_state.waterfall_process.wait(timeout=1) _state.waterfall_process.wait(timeout=1)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
_state.waterfall_process.kill() _state.waterfall_process.kill()
except Exception:
pass
_state.waterfall_process = None _state.waterfall_process = None
logger.info("Waterfall IQ loop stopped") logger.info("Waterfall IQ loop stopped")
@@ -346,14 +339,10 @@ def _waterfall_loop_rtl_power():
try: try:
_state.waterfall_queue.put_nowait(msg) _state.waterfall_queue.put_nowait(msg)
except queue.Full: except queue.Full:
try: with contextlib.suppress(queue.Empty):
_state.waterfall_queue.get_nowait() _state.waterfall_queue.get_nowait()
except queue.Empty: with contextlib.suppress(queue.Full):
pass
try:
_state.waterfall_queue.put_nowait(msg) _state.waterfall_queue.put_nowait(msg)
except queue.Full:
pass
all_bins = [] all_bins = []
sweep_start_hz = start_hz sweep_start_hz = start_hz
@@ -379,10 +368,8 @@ def _waterfall_loop_rtl_power():
'bins': bins_to_send, 'bins': bins_to_send,
'timestamp': datetime.now().isoformat(), 'timestamp': datetime.now().isoformat(),
} }
try: with contextlib.suppress(queue.Full):
_state.waterfall_queue.put_nowait(msg) _state.waterfall_queue.put_nowait(msg)
except queue.Full:
pass
if _state.waterfall_running and not received_any: if _state.waterfall_running and not received_any:
_queue_waterfall_error('No waterfall FFT data received from rtl_power') _queue_waterfall_error('No waterfall FFT data received from rtl_power')
@@ -397,10 +384,8 @@ def _waterfall_loop_rtl_power():
_state.waterfall_process.terminate() _state.waterfall_process.terminate()
_state.waterfall_process.wait(timeout=1) _state.waterfall_process.wait(timeout=1)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
_state.waterfall_process.kill() _state.waterfall_process.kill()
except Exception:
pass
_state.waterfall_process = None _state.waterfall_process = None
logger.info("Waterfall loop stopped") logger.info("Waterfall loop stopped")
@@ -432,9 +417,8 @@ def start_waterfall() -> Response:
sdr_type_str = sdr_type.value sdr_type_str = sdr_type.value
# RTL-SDR uses rtl_power; other types use rx_sdr via IQ capture # RTL-SDR uses rtl_power; other types use rx_sdr via IQ capture
if sdr_type == SDRType.RTL_SDR: if sdr_type == SDRType.RTL_SDR and not find_rtl_power():
if not find_rtl_power(): return jsonify({'status': 'error', 'message': 'rtl_power not found'}), 503
return jsonify({'status': 'error', 'message': 'rtl_power not found'}), 503
try: try:
_state.waterfall_config['start_freq'] = float(data.get('start_freq', 88.0)) _state.waterfall_config['start_freq'] = float(data.get('start_freq', 88.0))
+5 -7
View File
@@ -11,21 +11,19 @@ Supports multiple connection types:
from __future__ import annotations from __future__ import annotations
import queue import queue
import time
from typing import Generator
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
from utils.logging import get_logger from utils.logging import get_logger
from utils.sse import sse_stream_fanout
from utils.meshtastic import ( from utils.meshtastic import (
MeshtasticMessage,
get_meshtastic_client, get_meshtastic_client,
is_meshtastic_available,
start_meshtastic, start_meshtastic,
stop_meshtastic, stop_meshtastic,
is_meshtastic_available,
MeshtasticMessage,
) )
from utils.responses import api_error
from utils.sse import sse_stream_fanout
logger = get_logger('intercept.meshtastic') logger = get_logger('intercept.meshtastic')
+1 -1
View File
@@ -20,7 +20,7 @@ from typing import Any
from flask import Blueprint, Flask, Response, jsonify, request from flask import Blueprint, Flask, Response, jsonify, request
from utils.responses import api_success, api_error from utils.responses import api_error
try: try:
from flask_sock import Sock from flask_sock import Sock
+1 -1
View File
@@ -13,7 +13,6 @@ from typing import Any
from flask import Blueprint, Response, jsonify, request from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from utils.event_pipeline import process_event from utils.event_pipeline import process_event
from utils.logging import sensor_logger as logger from utils.logging import sensor_logger as logger
@@ -22,6 +21,7 @@ from utils.morse import (
morse_decoder_thread, morse_decoder_thread,
) )
from utils.process import register_process, safe_terminate, unregister_process from utils.process import register_process, safe_terminate, unregister_process
from utils.responses import api_error
from utils.sdr import SDRFactory, SDRType from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout from utils.sse import sse_stream_fanout
from utils.validation import ( from utils.validation import (
+5 -3
View File
@@ -2,11 +2,13 @@
Offline mode routes - Asset management and settings for offline operation. Offline mode routes - Asset management and settings for offline operation.
""" """
from flask import Blueprint, jsonify, request
from utils.database import get_setting, set_setting
from utils.responses import api_success, api_error
import os import os
from flask import Blueprint, request
from utils.database import get_setting, set_setting
from utils.responses import api_error, api_success
offline_bp = Blueprint('offline', __name__, url_prefix='/offline') offline_bp = Blueprint('offline', __name__, url_prefix='/offline')
# Default offline settings # Default offline settings
+1 -1
View File
@@ -19,10 +19,10 @@ from flask import Blueprint, Response, jsonify, request
import app as app_module import app as app_module
from utils.event_pipeline import process_event from utils.event_pipeline import process_event
from utils.responses import api_success, api_error
from utils.logging import sensor_logger as logger from utils.logging import sensor_logger as logger
from utils.ook import ook_parser_thread from utils.ook import ook_parser_thread
from utils.process import register_process, safe_terminate, unregister_process from utils.process import register_process, safe_terminate, unregister_process
from utils.responses import api_error
from utils.sdr import SDRFactory, SDRType from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout from utils.sse import sse_stream_fanout
from utils.validation import ( from utils.validation import (
+25 -34
View File
@@ -2,34 +2,39 @@
from __future__ import annotations from __future__ import annotations
import contextlib
import math import math
import os import os
import pathlib import pathlib
import re
import pty import pty
import queue import queue
import re
import select import select
import struct import struct
import subprocess import subprocess
import threading import threading
import time import time
from datetime import datetime from datetime import datetime
from typing import Any, Generator from typing import Any
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from utils.logging import pager_logger as logger
from utils.validation import (
validate_frequency, validate_device_index, validate_gain, validate_ppm,
validate_rtl_tcp_host, validate_rtl_tcp_port
)
from utils.sse import sse_stream_fanout
from utils.event_pipeline import process_event
from utils.process import safe_terminate, register_process, unregister_process
from utils.sdr import SDRFactory, SDRType, SDRValidationError
from utils.dependencies import get_tool_path from utils.dependencies import get_tool_path
from utils.event_pipeline import process_event
from utils.logging import pager_logger as logger
from utils.process import register_process, unregister_process
from utils.responses import api_error
from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout
from utils.validation import (
validate_device_index,
validate_frequency,
validate_gain,
validate_ppm,
validate_rtl_tcp_host,
validate_rtl_tcp_port,
)
pager_bp = Blueprint('pager', __name__) pager_bp = Blueprint('pager', __name__)
@@ -189,10 +194,8 @@ def audio_relay_thread(
except Exception as e: except Exception as e:
logger.debug(f"Audio relay error: {e}") logger.debug(f"Audio relay error: {e}")
finally: finally:
try: with contextlib.suppress(OSError):
multimon_stdin.close() multimon_stdin.close()
except OSError:
pass
def stream_decoder(master_fd: int, process: subprocess.Popen[bytes]) -> None: def stream_decoder(master_fd: int, process: subprocess.Popen[bytes]) -> None:
@@ -237,10 +240,8 @@ def stream_decoder(master_fd: int, process: subprocess.Popen[bytes]) -> None:
app_module.output_queue.put({'type': 'error', 'text': str(e)}) app_module.output_queue.put({'type': 'error', 'text': str(e)})
finally: finally:
global pager_active_device, pager_active_sdr_type global pager_active_device, pager_active_sdr_type
try: with contextlib.suppress(OSError):
os.close(master_fd) os.close(master_fd)
except OSError:
pass
# Signal relay thread to stop # Signal relay thread to stop
with app_module.process_lock: with app_module.process_lock:
stop_relay = getattr(app_module.current_process, '_stop_relay', None) stop_relay = getattr(app_module.current_process, '_stop_relay', None)
@@ -255,10 +256,8 @@ def stream_decoder(master_fd: int, process: subprocess.Popen[bytes]) -> None:
proc.terminate() proc.terminate()
proc.wait(timeout=2) proc.wait(timeout=2)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
proc.kill() proc.kill()
except Exception:
pass
unregister_process(proc) unregister_process(proc)
app_module.output_queue.put({'type': 'status', 'text': 'stopped'}) app_module.output_queue.put({'type': 'status', 'text': 'stopped'})
with app_module.process_lock: with app_module.process_lock:
@@ -454,10 +453,8 @@ def start_decoding() -> Response:
rtl_process.terminate() rtl_process.terminate()
rtl_process.wait(timeout=2) rtl_process.wait(timeout=2)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
rtl_process.kill() rtl_process.kill()
except Exception:
pass
# Release device on failure # Release device on failure
if pager_active_device is not None: if pager_active_device is not None:
app_module.release_sdr_device(pager_active_device, pager_active_sdr_type or 'rtlsdr') app_module.release_sdr_device(pager_active_device, pager_active_sdr_type or 'rtlsdr')
@@ -470,10 +467,8 @@ def start_decoding() -> Response:
rtl_process.terminate() rtl_process.terminate()
rtl_process.wait(timeout=2) rtl_process.wait(timeout=2)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
rtl_process.kill() rtl_process.kill()
except Exception:
pass
# Release device on failure # Release device on failure
if pager_active_device is not None: if pager_active_device is not None:
app_module.release_sdr_device(pager_active_device, pager_active_sdr_type or 'rtlsdr') app_module.release_sdr_device(pager_active_device, pager_active_sdr_type or 'rtlsdr')
@@ -498,17 +493,13 @@ def stop_decoding() -> Response:
app_module.current_process._rtl_process.terminate() app_module.current_process._rtl_process.terminate()
app_module.current_process._rtl_process.wait(timeout=2) app_module.current_process._rtl_process.wait(timeout=2)
except (subprocess.TimeoutExpired, OSError): except (subprocess.TimeoutExpired, OSError):
try: with contextlib.suppress(OSError):
app_module.current_process._rtl_process.kill() app_module.current_process._rtl_process.kill()
except OSError:
pass
# Close PTY master fd # Close PTY master fd
if hasattr(app_module.current_process, '_master_fd'): if hasattr(app_module.current_process, '_master_fd'):
try: with contextlib.suppress(OSError):
os.close(app_module.current_process._master_fd) os.close(app_module.current_process._master_fd)
except OSError:
pass
# Kill multimon-ng # Kill multimon-ng
app_module.current_process.terminate() app_module.current_process.terminate()
+17 -44
View File
@@ -7,6 +7,7 @@ telemetry (position, altitude, temperature, humidity, pressure) on the
from __future__ import annotations from __future__ import annotations
import contextlib
import json import json
import os import os
import queue import queue
@@ -20,7 +21,6 @@ from typing import Any
from flask import Blueprint, Response, jsonify, request from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from utils.constants import ( from utils.constants import (
MAX_RADIOSONDE_AGE_SECONDS, MAX_RADIOSONDE_AGE_SECONDS,
@@ -32,6 +32,7 @@ from utils.constants import (
) )
from utils.gps import is_gpsd_running from utils.gps import is_gpsd_running
from utils.logging import get_logger from utils.logging import get_logger
from utils.responses import api_error, api_success
from utils.sdr import SDRFactory, SDRType from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout from utils.sse import sse_stream_fanout
from utils.validation import ( from utils.validation import (
@@ -270,7 +271,7 @@ def _fix_data_ownership(path: str) -> None:
return return
try: try:
uid_int, gid_int = int(uid), int(gid) uid_int, gid_int = int(uid), int(gid)
for dirpath, dirnames, filenames in os.walk(path): for dirpath, _dirnames, filenames in os.walk(path):
os.chown(dirpath, uid_int, gid_int) os.chown(dirpath, uid_int, gid_int)
for fname in filenames: for fname in filenames:
os.chown(os.path.join(dirpath, fname), uid_int, gid_int) os.chown(os.path.join(dirpath, fname), uid_int, gid_int)
@@ -315,18 +316,14 @@ def parse_radiosonde_udp(udp_port: int) -> None:
if serial: if serial:
with _balloons_lock: with _balloons_lock:
radiosonde_balloons[serial] = balloon radiosonde_balloons[serial] = balloon
try: with contextlib.suppress(queue.Full):
app_module.radiosonde_queue.put_nowait({ app_module.radiosonde_queue.put_nowait({
'type': 'balloon', 'type': 'balloon',
**balloon, **balloon,
}) })
except queue.Full:
pass
try: with contextlib.suppress(OSError):
sock.close() sock.close()
except OSError:
pass
_udp_socket = None _udp_socket = None
logger.info("Radiosonde UDP listener stopped") logger.info("Radiosonde UDP listener stopped")
@@ -354,71 +351,51 @@ def _process_telemetry(msg: dict) -> dict | None:
# Position # Position
for key in ('lat', 'latitude'): for key in ('lat', 'latitude'):
if key in msg: if key in msg:
try: with contextlib.suppress(ValueError, TypeError):
balloon['lat'] = float(msg[key]) balloon['lat'] = float(msg[key])
except (ValueError, TypeError):
pass
break break
for key in ('lon', 'longitude'): for key in ('lon', 'longitude'):
if key in msg: if key in msg:
try: with contextlib.suppress(ValueError, TypeError):
balloon['lon'] = float(msg[key]) balloon['lon'] = float(msg[key])
except (ValueError, TypeError):
pass
break break
# Altitude (metres) # Altitude (metres)
if 'alt' in msg: if 'alt' in msg:
try: with contextlib.suppress(ValueError, TypeError):
balloon['alt'] = float(msg['alt']) balloon['alt'] = float(msg['alt'])
except (ValueError, TypeError):
pass
# Meteorological data # Meteorological data
for field in ('temp', 'humidity', 'pressure'): for field in ('temp', 'humidity', 'pressure'):
if field in msg: if field in msg:
try: with contextlib.suppress(ValueError, TypeError):
balloon[field] = float(msg[field]) balloon[field] = float(msg[field])
except (ValueError, TypeError):
pass
# Velocity # Velocity
if 'vel_h' in msg: if 'vel_h' in msg:
try: with contextlib.suppress(ValueError, TypeError):
balloon['vel_h'] = float(msg['vel_h']) balloon['vel_h'] = float(msg['vel_h'])
except (ValueError, TypeError):
pass
if 'vel_v' in msg: if 'vel_v' in msg:
try: with contextlib.suppress(ValueError, TypeError):
balloon['vel_v'] = float(msg['vel_v']) balloon['vel_v'] = float(msg['vel_v'])
except (ValueError, TypeError):
pass
if 'heading' in msg: if 'heading' in msg:
try: with contextlib.suppress(ValueError, TypeError):
balloon['heading'] = float(msg['heading']) balloon['heading'] = float(msg['heading'])
except (ValueError, TypeError):
pass
# GPS satellites # GPS satellites
if 'sats' in msg: if 'sats' in msg:
try: with contextlib.suppress(ValueError, TypeError):
balloon['sats'] = int(msg['sats']) balloon['sats'] = int(msg['sats'])
except (ValueError, TypeError):
pass
# Battery voltage # Battery voltage
if 'batt' in msg: if 'batt' in msg:
try: with contextlib.suppress(ValueError, TypeError):
balloon['batt'] = float(msg['batt']) balloon['batt'] = float(msg['batt'])
except (ValueError, TypeError):
pass
# Frequency # Frequency
if 'freq' in msg: if 'freq' in msg:
try: with contextlib.suppress(ValueError, TypeError):
balloon['freq'] = float(msg['freq']) balloon['freq'] = float(msg['freq'])
except (ValueError, TypeError):
pass
balloon['last_seen'] = time.time() balloon['last_seen'] = time.time()
return balloon return balloon
@@ -612,12 +589,10 @@ def start_radiosonde():
app_module.release_sdr_device(device_int, sdr_type_str) app_module.release_sdr_device(device_int, sdr_type_str)
stderr_output = '' stderr_output = ''
if app_module.radiosonde_process.stderr: if app_module.radiosonde_process.stderr:
try: with contextlib.suppress(Exception):
stderr_output = app_module.radiosonde_process.stderr.read().decode( stderr_output = app_module.radiosonde_process.stderr.read().decode(
'utf-8', errors='ignore' 'utf-8', errors='ignore'
).strip() ).strip()
except Exception:
pass
if stderr_output: if stderr_output:
logger.error(f"radiosonde_auto_rx stderr:\n{stderr_output}") logger.error(f"radiosonde_auto_rx stderr:\n{stderr_output}")
if stderr_output and ( if stderr_output and (
@@ -686,10 +661,8 @@ def stop_radiosonde():
# Close UDP socket to unblock listener thread # Close UDP socket to unblock listener thread
if _udp_socket: if _udp_socket:
try: with contextlib.suppress(OSError):
_udp_socket.close() _udp_socket.close()
except OSError:
pass
_udp_socket = None _udp_socket = None
# Release SDR device # Release SDR device
+3 -3
View File
@@ -5,10 +5,10 @@ from __future__ import annotations
import json import json
from pathlib import Path from pathlib import Path
from flask import Blueprint, jsonify, request, send_file from flask import Blueprint, request, send_file
from utils.recording import get_recording_manager, RECORDING_ROOT from utils.recording import RECORDING_ROOT, get_recording_manager
from utils.responses import api_success, api_error from utils.responses import api_error, api_success
recordings_bp = Blueprint('recordings', __name__, url_prefix='/recordings') recordings_bp = Blueprint('recordings', __name__, url_prefix='/recordings')
+9 -15
View File
@@ -2,25 +2,23 @@
from __future__ import annotations from __future__ import annotations
import contextlib
import json import json
import queue import queue
import subprocess import subprocess
import threading import threading
import time import time
from datetime import datetime from datetime import datetime
from typing import Generator
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from utils.logging import sensor_logger as logger
from utils.validation import (
validate_frequency, validate_device_index, validate_gain, validate_ppm
)
from utils.sse import sse_stream_fanout
from utils.event_pipeline import process_event from utils.event_pipeline import process_event
from utils.process import safe_terminate, register_process, unregister_process from utils.logging import sensor_logger as logger
from utils.process import register_process, unregister_process
from utils.responses import api_error
from utils.sse import sse_stream_fanout
from utils.validation import validate_device_index, validate_frequency, validate_gain, validate_ppm
rtlamr_bp = Blueprint('rtlamr', __name__) rtlamr_bp = Blueprint('rtlamr', __name__)
@@ -70,10 +68,8 @@ def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None:
process.terminate() process.terminate()
process.wait(timeout=2) process.wait(timeout=2)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
process.kill() process.kill()
except Exception:
pass
unregister_process(process) unregister_process(process)
# Kill companion rtl_tcp process # Kill companion rtl_tcp process
with rtl_tcp_lock: with rtl_tcp_lock:
@@ -82,10 +78,8 @@ def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None:
rtl_tcp_process.terminate() rtl_tcp_process.terminate()
rtl_tcp_process.wait(timeout=2) rtl_tcp_process.wait(timeout=2)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
rtl_tcp_process.kill() rtl_tcp_process.kill()
except Exception:
pass
unregister_process(rtl_tcp_process) unregister_process(rtl_tcp_process)
rtl_tcp_process = None rtl_tcp_process = None
app_module.rtlamr_queue.put({'type': 'status', 'text': 'stopped'}) app_module.rtlamr_queue.put({'type': 'status', 'text': 'stopped'})
+8 -13
View File
@@ -2,30 +2,25 @@
from __future__ import annotations from __future__ import annotations
import json
import math import math
import urllib.request import urllib.request
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Optional
from urllib.parse import urlparse
import requests import requests
from flask import Blueprint, jsonify, render_template, request
from flask import Blueprint, jsonify, request, render_template, Response
from utils.responses import api_success, api_error
from config import SHARED_OBSERVER_LOCATION_ENABLED from config import SHARED_OBSERVER_LOCATION_ENABLED
from data.satellites import TLE_SATELLITES from data.satellites import TLE_SATELLITES
from utils.database import ( from utils.database import (
get_tracked_satellites,
add_tracked_satellite, add_tracked_satellite,
bulk_add_tracked_satellites, bulk_add_tracked_satellites,
update_tracked_satellite, get_tracked_satellites,
remove_tracked_satellite, remove_tracked_satellite,
update_tracked_satellite,
) )
from utils.logging import satellite_logger as logger from utils.logging import satellite_logger as logger
from utils.validation import validate_latitude, validate_longitude, validate_hours, validate_elevation from utils.responses import api_error
from utils.validation import validate_elevation, validate_hours, validate_latitude, validate_longitude
satellite_bp = Blueprint('satellite', __name__, url_prefix='/satellite') satellite_bp = Blueprint('satellite', __name__, url_prefix='/satellite')
@@ -87,7 +82,7 @@ def init_tle_auto_refresh():
logger.info("TLE auto-refresh scheduled") logger.info("TLE auto-refresh scheduled")
def _fetch_iss_realtime(observer_lat: Optional[float] = None, observer_lon: Optional[float] = None) -> Optional[dict]: def _fetch_iss_realtime(observer_lat: float | None = None, observer_lon: float | None = None) -> dict | None:
""" """
Fetch real-time ISS position from external APIs. Fetch real-time ISS position from external APIs.
@@ -190,8 +185,8 @@ def satellite_dashboard():
def predict_passes(): def predict_passes():
"""Calculate satellite passes using skyfield.""" """Calculate satellite passes using skyfield."""
try: try:
from skyfield.api import wgs84, EarthSatellite
from skyfield.almanac import find_discrete from skyfield.almanac import find_discrete
from skyfield.api import EarthSatellite, wgs84
except ImportError: except ImportError:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
@@ -344,7 +339,7 @@ def predict_passes():
def get_satellite_position(): def get_satellite_position():
"""Get real-time positions of satellites.""" """Get real-time positions of satellites."""
try: try:
from skyfield.api import wgs84, EarthSatellite from skyfield.api import EarthSatellite, wgs84
except ImportError: except ImportError:
return api_error('skyfield not installed', 503) return api_error('skyfield not installed', 503)
+16 -13
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import contextlib
import json import json
import math import math
import queue import queue
@@ -9,21 +10,25 @@ import subprocess
import threading import threading
import time import time
from datetime import datetime from datetime import datetime
from typing import Any, Generator from typing import Any
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from utils.logging import sensor_logger as logger
from utils.validation import (
validate_frequency, validate_device_index, validate_gain, validate_ppm,
validate_rtl_tcp_host, validate_rtl_tcp_port
)
from utils.sse import sse_stream_fanout
from utils.event_pipeline import process_event from utils.event_pipeline import process_event
from utils.process import safe_terminate, register_process, unregister_process from utils.logging import sensor_logger as logger
from utils.process import register_process, unregister_process
from utils.responses import api_error, api_success
from utils.sdr import SDRFactory, SDRType from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout
from utils.validation import (
validate_device_index,
validate_frequency,
validate_gain,
validate_ppm,
validate_rtl_tcp_host,
validate_rtl_tcp_port,
)
sensor_bp = Blueprint('sensor', __name__) sensor_bp = Blueprint('sensor', __name__)
@@ -137,10 +142,8 @@ def stream_sensor_output(process: subprocess.Popen[bytes]) -> None:
process.terminate() process.terminate()
process.wait(timeout=2) process.wait(timeout=2)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
process.kill() process.kill()
except Exception:
pass
unregister_process(process) unregister_process(process)
app_module.sensor_queue.put({'type': 'status', 'text': 'stopped'}) app_module.sensor_queue.put({'type': 'status', 'text': 'stopped'})
with app_module.sensor_lock: with app_module.sensor_lock:
+4 -4
View File
@@ -6,14 +6,14 @@ import os
import subprocess import subprocess
import sys import sys
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, jsonify, request
from utils.database import ( from utils.database import (
get_setting,
set_setting,
delete_setting, delete_setting,
get_all_settings, get_all_settings,
get_correlations, get_correlations,
get_setting,
set_setting,
) )
from utils.logging import get_logger from utils.logging import get_logger
from utils.responses import api_error, api_success from utils.responses import api_error, api_success
@@ -163,7 +163,7 @@ def check_dvb_driver_status() -> Response:
blacklist_contents = [] blacklist_contents = []
if blacklist_exists: if blacklist_exists:
try: try:
with open(BLACKLIST_FILE, 'r') as f: with open(BLACKLIST_FILE) as f:
blacklist_contents = [line.strip() for line in f if line.strip() and not line.startswith('#')] blacklist_contents = [line.strip() for line in f if line.strip() and not line.startswith('#')]
except Exception: except Exception:
pass pass
+1 -1
View File
@@ -10,8 +10,8 @@ from typing import Any
from flask import Blueprint, Response, jsonify, request from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
from utils.logging import get_logger from utils.logging import get_logger
from utils.responses import api_error
logger = get_logger('intercept.signalid') logger = get_logger('intercept.signalid')
+1 -1
View File
@@ -13,7 +13,7 @@ from typing import Any
from flask import Blueprint, Response, jsonify from flask import Blueprint, Response, jsonify
from utils.logging import get_logger from utils.logging import get_logger
from utils.responses import api_success, api_error from utils.responses import api_error
logger = get_logger('intercept.space_weather') logger = get_logger('intercept.space_weather')
+3 -3
View File
@@ -611,9 +611,9 @@ def get_station(station_id):
@spy_stations_bp.route('/filters') @spy_stations_bp.route('/filters')
def get_filters(): def get_filters():
"""Return available filter options.""" """Return available filter options."""
types = list(set(s['type'] for s in STATIONS)) types = list({s['type'] for s in STATIONS})
countries = sorted(list(set((s['country'], s['country_code']) for s in STATIONS))) countries = sorted({(s['country'], s['country_code']) for s in STATIONS})
modes = sorted(list(set(s['mode'].split('/')[0] for s in STATIONS))) modes = sorted({s['mode'].split('/')[0] for s in STATIONS})
return jsonify({ return jsonify({
'status': 'success', 'status': 'success',
+11 -10
View File
@@ -6,23 +6,24 @@ ISS SSTV events occur during special commemorations and typically transmit on 14
from __future__ import annotations from __future__ import annotations
import contextlib
import queue import queue
import threading import threading
import time import time
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from flask import Blueprint, jsonify, request, Response, send_file from flask import Blueprint, Response, jsonify, request, send_file
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from utils.logging import get_logger
from utils.sse import sse_stream_fanout
from utils.event_pipeline import process_event from utils.event_pipeline import process_event
from utils.logging import get_logger
from utils.responses import api_error
from utils.sse import sse_stream_fanout
from utils.sstv import ( from utils.sstv import (
ISS_SSTV_FREQ,
get_sstv_decoder, get_sstv_decoder,
is_sstv_available, is_sstv_available,
ISS_SSTV_FREQ,
) )
logger = get_logger('intercept.sstv') logger = get_logger('intercept.sstv')
@@ -520,9 +521,11 @@ def iss_schedule():
return jsonify(_iss_schedule_cache) return jsonify(_iss_schedule_cache)
try: try:
from skyfield.api import wgs84, EarthSatellite
from skyfield.almanac import find_discrete
from datetime import timedelta from datetime import timedelta
from skyfield.almanac import find_discrete
from skyfield.api import EarthSatellite, wgs84
from data.satellites import TLE_SATELLITES from data.satellites import TLE_SATELLITES
# Get ISS TLE # Get ISS TLE
@@ -816,7 +819,5 @@ def decode_file():
finally: finally:
# Clean up temp file # Clean up temp file
try: with contextlib.suppress(Exception):
Path(tmp_path).unlink() Path(tmp_path).unlink()
except Exception:
pass
+5 -8
View File
@@ -6,18 +6,17 @@ frequencies used by amateur radio operators worldwide.
from __future__ import annotations from __future__ import annotations
import contextlib
import queue import queue
import time
from collections.abc import Generator
from pathlib import Path from pathlib import Path
from flask import Blueprint, Response, jsonify, request, send_file from flask import Blueprint, Response, jsonify, request, send_file
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from utils.logging import get_logger
from utils.sse import sse_stream_fanout
from utils.event_pipeline import process_event from utils.event_pipeline import process_event
from utils.logging import get_logger
from utils.responses import api_error
from utils.sse import sse_stream_fanout
from utils.sstv import ( from utils.sstv import (
get_general_sstv_decoder, get_general_sstv_decoder,
) )
@@ -325,7 +324,5 @@ def decode_file():
return api_error(str(e), 500) return api_error(str(e), 500)
finally: finally:
try: with contextlib.suppress(Exception):
Path(tmp_path).unlink() Path(tmp_path).unlink()
except Exception:
pass
+15 -16
View File
@@ -6,25 +6,26 @@ signal replay/transmit, and wideband spectrum analysis.
from __future__ import annotations from __future__ import annotations
import contextlib
import queue import queue
from flask import Blueprint, jsonify, request, Response, send_file from flask import Blueprint, Response, jsonify, request, send_file
from utils.responses import api_success, api_error from utils.constants import (
SUBGHZ_FREQ_MAX_MHZ,
SUBGHZ_FREQ_MIN_MHZ,
SUBGHZ_LNA_GAIN_MAX,
SUBGHZ_PRESETS,
SUBGHZ_SAMPLE_RATES,
SUBGHZ_TX_MAX_DURATION,
SUBGHZ_TX_VGA_GAIN_MAX,
SUBGHZ_VGA_GAIN_MAX,
)
from utils.event_pipeline import process_event
from utils.logging import get_logger from utils.logging import get_logger
from utils.responses import api_error
from utils.sse import sse_stream from utils.sse import sse_stream
from utils.subghz import get_subghz_manager from utils.subghz import get_subghz_manager
from utils.event_pipeline import process_event
from utils.constants import (
SUBGHZ_FREQ_MIN_MHZ,
SUBGHZ_FREQ_MAX_MHZ,
SUBGHZ_LNA_GAIN_MAX,
SUBGHZ_VGA_GAIN_MAX,
SUBGHZ_TX_VGA_GAIN_MAX,
SUBGHZ_TX_MAX_DURATION,
SUBGHZ_SAMPLE_RATES,
SUBGHZ_PRESETS,
)
logger = get_logger('intercept.subghz') logger = get_logger('intercept.subghz')
@@ -36,10 +37,8 @@ _subghz_queue: queue.Queue = queue.Queue(maxsize=200)
def _event_callback(event: dict) -> None: def _event_callback(event: dict) -> None:
"""Forward SubGhzManager events to the SSE queue.""" """Forward SubGhzManager events to the SSE queue."""
try: with contextlib.suppress(Exception):
process_event('subghz', event, event.get('type')) process_event('subghz', event, event.get('type'))
except Exception:
pass
try: try:
_subghz_queue.put_nowait(event) _subghz_queue.put_nowait(event)
except queue.Full: except queue.Full:
+1 -1
View File
@@ -22,7 +22,7 @@ from flask import Blueprint, Response, jsonify, request
from utils.constants import SSE_KEEPALIVE_INTERVAL, SSE_QUEUE_TIMEOUT from utils.constants import SSE_KEEPALIVE_INTERVAL, SSE_QUEUE_TIMEOUT
from utils.logging import sensor_logger as logger from utils.logging import sensor_logger as logger
from utils.responses import api_success, api_error from utils.responses import api_error
from utils.sse import sse_stream_fanout from utils.sse import sse_stream_fanout
try: try:
+22 -22
View File
@@ -7,6 +7,7 @@ threat detection, and reporting.
from __future__ import annotations from __future__ import annotations
import contextlib
import json import json
import logging import logging
import queue import queue
@@ -23,9 +24,9 @@ from data.tscm_frequencies import (
get_sweep_preset, get_sweep_preset,
) )
from utils.database import ( from utils.database import (
acknowledge_tscm_threat,
add_device_timeline_entry, add_device_timeline_entry,
add_tscm_threat, add_tscm_threat,
acknowledge_tscm_threat,
cleanup_old_timeline_entries, cleanup_old_timeline_entries,
create_tscm_schedule, create_tscm_schedule,
create_tscm_sweep, create_tscm_sweep,
@@ -43,6 +44,8 @@ from utils.database import (
update_tscm_schedule, update_tscm_schedule,
update_tscm_sweep, update_tscm_sweep,
) )
from utils.event_pipeline import process_event
from utils.sse import sse_stream_fanout
from utils.tscm.baseline import ( from utils.tscm.baseline import (
BaselineComparator, BaselineComparator,
BaselineRecorder, BaselineRecorder,
@@ -56,12 +59,10 @@ from utils.tscm.correlation import (
from utils.tscm.detector import ThreatDetector from utils.tscm.detector import ThreatDetector
from utils.tscm.device_identity import ( from utils.tscm.device_identity import (
get_identity_engine, get_identity_engine,
reset_identity_engine,
ingest_ble_dict, ingest_ble_dict,
ingest_wifi_dict, ingest_wifi_dict,
reset_identity_engine,
) )
from utils.event_pipeline import process_event
from utils.sse import sse_stream_fanout
# Import unified Bluetooth scanner helper for TSCM integration # Import unified Bluetooth scanner helper for TSCM integration
try: try:
@@ -659,8 +660,8 @@ def _scan_bluetooth_devices(interface: str, duration: int = 10) -> list[dict]:
Uses the BLE scanner module (bleak library) for proper manufacturer ID Uses the BLE scanner module (bleak library) for proper manufacturer ID
detection, with fallback to system tools if bleak is unavailable. detection, with fallback to system tools if bleak is unavailable.
""" """
import platform
import os import os
import platform
import re import re
import shutil import shutil
import subprocess import subprocess
@@ -874,10 +875,8 @@ def _scan_bluetooth_devices(interface: str, duration: int = 10) -> list[dict]:
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
process.kill() process.kill()
try: with contextlib.suppress(OSError):
os.close(master_fd) os.close(master_fd)
except OSError:
pass
logger.info(f"bluetoothctl scan found {len(devices)} devices") logger.info(f"bluetoothctl scan found {len(devices)} devices")
@@ -914,7 +913,8 @@ def _scan_rf_signals(
""" """
# Default stop check uses module-level _sweep_running # Default stop check uses module-level _sweep_running
if stop_check is None: if stop_check is None:
stop_check = lambda: not _sweep_running def stop_check():
return not _sweep_running
import os import os
import shutil import shutil
import subprocess import subprocess
@@ -954,11 +954,11 @@ def _scan_rf_signals(
# Tool exists but no device detected — try anyway (detection may have failed) # Tool exists but no device detected — try anyway (detection may have failed)
sdr_type = 'rtlsdr' sdr_type = 'rtlsdr'
sweep_tool_path = rtl_power_path sweep_tool_path = rtl_power_path
logger.info(f"No SDR detected but rtl_power found, attempting RTL-SDR scan") logger.info("No SDR detected but rtl_power found, attempting RTL-SDR scan")
elif hackrf_sweep_path: elif hackrf_sweep_path:
sdr_type = 'hackrf' sdr_type = 'hackrf'
sweep_tool_path = hackrf_sweep_path sweep_tool_path = hackrf_sweep_path
logger.info(f"No SDR detected but hackrf_sweep found, attempting HackRF scan") logger.info("No SDR detected but hackrf_sweep found, attempting HackRF scan")
if not sweep_tool_path: if not sweep_tool_path:
logger.warning("No supported sweep tool found (rtl_power or hackrf_sweep)") logger.warning("No supported sweep tool found (rtl_power or hackrf_sweep)")
@@ -1059,14 +1059,14 @@ def _scan_rf_signals(
# Parse the CSV output (same format for both rtl_power and hackrf_sweep) # Parse the CSV output (same format for both rtl_power and hackrf_sweep)
if os.path.exists(tmp_path) and os.path.getsize(tmp_path) > 0: if os.path.exists(tmp_path) and os.path.getsize(tmp_path) > 0:
with open(tmp_path, 'r') as f: with open(tmp_path) as f:
for line in f: for line in f:
parts = line.strip().split(',') parts = line.strip().split(',')
if len(parts) >= 7: if len(parts) >= 7:
try: try:
# CSV format: date, time, hz_low, hz_high, hz_step, samples, db_values... # CSV format: date, time, hz_low, hz_high, hz_step, samples, db_values...
hz_low = int(parts[2].strip()) hz_low = int(parts[2].strip())
hz_high = int(parts[3].strip()) int(parts[3].strip())
hz_step = float(parts[4].strip()) hz_step = float(parts[4].strip())
db_values = [float(x) for x in parts[6:] if x.strip()] db_values = [float(x) for x in parts[6:] if x.strip()]
@@ -1100,10 +1100,8 @@ def _scan_rf_signals(
finally: finally:
# Cleanup temp file # Cleanup temp file
try: with contextlib.suppress(OSError):
os.unlink(tmp_path) os.unlink(tmp_path)
except OSError:
pass
# Deduplicate nearby frequencies (within 100kHz) # Deduplicate nearby frequencies (within 100kHz)
if signals: if signals:
@@ -1816,9 +1814,11 @@ def _generate_assessment(summary: dict) -> str:
# ============================================================================= # =============================================================================
# Import sub-modules to register routes on tscm_bp # Import sub-modules to register routes on tscm_bp
# ============================================================================= # =============================================================================
from routes.tscm import sweep # noqa: E402, F401 from routes.tscm import (
from routes.tscm import baseline # noqa: E402, F401 analysis, # noqa: E402, F401
from routes.tscm import cases # noqa: E402, F401 baseline, # noqa: E402, F401
from routes.tscm import meeting # noqa: E402, F401 cases, # noqa: E402, F401
from routes.tscm import analysis # noqa: E402, F401 meeting, # noqa: E402, F401
from routes.tscm import schedules # noqa: E402, F401 schedules, # noqa: E402, F401
sweep, # noqa: E402, F401
)
+5 -6
View File
@@ -14,7 +14,6 @@ from datetime import datetime
from flask import Response, jsonify, request from flask import Response, jsonify, request
from routes.tscm import ( from routes.tscm import (
_current_sweep_id,
_generate_assessment, _generate_assessment,
tscm_bp, tscm_bp,
) )
@@ -253,9 +252,9 @@ def get_pdf_report():
summary, and mandatory disclaimers. summary, and mandatory disclaimers.
""" """
try: try:
from utils.tscm.reports import generate_report, get_pdf_report
from utils.tscm.advanced import detect_sweep_capabilities, get_timeline_manager
from routes.tscm import _current_sweep_id from routes.tscm import _current_sweep_id
from utils.tscm.advanced import detect_sweep_capabilities, get_timeline_manager
from utils.tscm.reports import generate_report, get_pdf_report
sweep_id = request.args.get('sweep_id', _current_sweep_id, type=int) sweep_id = request.args.get('sweep_id', _current_sweep_id, type=int)
if not sweep_id: if not sweep_id:
@@ -306,9 +305,9 @@ def get_technical_annex():
for audit purposes. No packet data included. for audit purposes. No packet data included.
""" """
try: try:
from utils.tscm.reports import generate_report, get_json_annex, get_csv_annex
from utils.tscm.advanced import detect_sweep_capabilities, get_timeline_manager
from routes.tscm import _current_sweep_id from routes.tscm import _current_sweep_id
from utils.tscm.advanced import detect_sweep_capabilities, get_timeline_manager
from utils.tscm.reports import generate_report, get_csv_annex, get_json_annex
sweep_id = request.args.get('sweep_id', _current_sweep_id, type=int) sweep_id = request.args.get('sweep_id', _current_sweep_id, type=int)
format_type = request.args.get('format', 'json') format_type = request.args.get('format', 'json')
@@ -900,8 +899,8 @@ def get_device_timeline_endpoint(identifier: str):
and meeting window correlation. and meeting window correlation.
""" """
try: try:
from utils.tscm.advanced import get_timeline_manager
from utils.database import get_device_timeline from utils.database import get_device_timeline
from utils.tscm.advanced import get_timeline_manager
protocol = request.args.get('protocol', 'bluetooth') protocol = request.args.get('protocol', 'bluetooth')
since_hours = request.args.get('since_hours', 24, type=int) since_hours = request.args.get('since_hours', 24, type=int)
-2
View File
@@ -25,7 +25,6 @@ from utils.database import (
set_active_tscm_baseline, set_active_tscm_baseline,
) )
from utils.tscm.baseline import ( from utils.tscm.baseline import (
BaselineComparator,
get_comparison_for_active_baseline, get_comparison_for_active_baseline,
) )
@@ -213,7 +212,6 @@ def get_baseline_diff(baseline_id: int, sweep_id: int):
def get_baseline_health(baseline_id: int): def get_baseline_health(baseline_id: int):
"""Get health assessment for a baseline.""" """Get health assessment for a baseline."""
try: try:
from utils.tscm.advanced import BaselineHealth
baseline = get_tscm_baseline(baseline_id) baseline = get_tscm_baseline(baseline_id)
if not baseline: if not baseline:
+1 -3
View File
@@ -91,7 +91,6 @@ def start_tracked_meeting():
""" """
from utils.database import start_meeting_window from utils.database import start_meeting_window
from utils.tscm.advanced import get_timeline_manager from utils.tscm.advanced import get_timeline_manager
from routes.tscm import _current_sweep_id
data = request.get_json() or {} data = request.get_json() or {}
@@ -156,9 +155,9 @@ def end_tracked_meeting(meeting_id: int):
def get_meeting_summary_endpoint(meeting_id: int): def get_meeting_summary_endpoint(meeting_id: int):
"""Get detailed summary of device activity during a meeting.""" """Get detailed summary of device activity during a meeting."""
try: try:
from routes.tscm import _current_sweep_id
from utils.database import get_meeting_windows from utils.database import get_meeting_windows
from utils.tscm.advanced import generate_meeting_summary, get_timeline_manager from utils.tscm.advanced import generate_meeting_summary, get_timeline_manager
from routes.tscm import _current_sweep_id
# Get meeting window # Get meeting window
windows = get_meeting_windows(_current_sweep_id or 0) windows = get_meeting_windows(_current_sweep_id or 0)
@@ -194,7 +193,6 @@ def get_meeting_summary_endpoint(meeting_id: int):
def get_active_meeting(): def get_active_meeting():
"""Get currently active meeting window.""" """Get currently active meeting window."""
from utils.database import get_active_meeting_window from utils.database import get_active_meeting_window
from routes.tscm import _current_sweep_id
meeting = get_active_meeting_window(_current_sweep_id) meeting = get_active_meeting_window(_current_sweep_id)
-1
View File
@@ -16,7 +16,6 @@ from routes.tscm import (
_get_schedule_timezone, _get_schedule_timezone,
_next_run_from_cron, _next_run_from_cron,
_start_sweep_internal, _start_sweep_internal,
_sweep_running,
tscm_bp, tscm_bp,
) )
from utils.database import ( from utils.database import (
+3 -11
View File
@@ -7,27 +7,25 @@ Handles /sweep/*, /status, /devices, /presets/*, /feed/*,
from __future__ import annotations from __future__ import annotations
import json
import logging import logging
import os import os
import platform import platform
import re import re
import shutil
import subprocess import subprocess
from typing import Any from typing import Any
from flask import Response, jsonify, request from flask import Response, jsonify, request
from data.tscm_frequencies import get_all_sweep_presets, get_sweep_preset
from routes.tscm import ( from routes.tscm import (
_baseline_recorder,
_current_sweep_id, _current_sweep_id,
_emit_event, _emit_event,
_start_sweep_internal, _start_sweep_internal,
_sweep_running, _sweep_running,
tscm_bp, tscm_bp,
tscm_queue, tscm_queue,
_baseline_recorder,
) )
from data.tscm_frequencies import get_all_sweep_presets, get_sweep_preset
from utils.database import get_tscm_sweep, update_tscm_sweep from utils.database import get_tscm_sweep, update_tscm_sweep
from utils.event_pipeline import process_event from utils.event_pipeline import process_event
from utils.sse import sse_stream_fanout from utils.sse import sse_stream_fanout
@@ -38,7 +36,6 @@ logger = logging.getLogger('intercept.tscm')
@tscm_bp.route('/status') @tscm_bp.route('/status')
def tscm_status(): def tscm_status():
"""Check if any TSCM operation is currently running.""" """Check if any TSCM operation is currently running."""
from routes.tscm import _sweep_running
return jsonify({'running': _sweep_running}) return jsonify({'running': _sweep_running})
@@ -98,7 +95,6 @@ def stop_sweep():
@tscm_bp.route('/sweep/status') @tscm_bp.route('/sweep/status')
def sweep_status(): def sweep_status():
"""Get current sweep status.""" """Get current sweep status."""
from routes.tscm import _sweep_running, _current_sweep_id
status = { status = {
'running': _sweep_running, 'running': _sweep_running,
@@ -116,7 +112,6 @@ def sweep_status():
@tscm_bp.route('/sweep/stream') @tscm_bp.route('/sweep/stream')
def sweep_stream(): def sweep_stream():
"""SSE stream for real-time sweep updates.""" """SSE stream for real-time sweep updates."""
from routes.tscm import tscm_queue
def _on_msg(msg: dict[str, Any]) -> None: def _on_msg(msg: dict[str, Any]) -> None:
process_event('tscm', msg, msg.get('type')) process_event('tscm', msg, msg.get('type'))
@@ -218,7 +213,7 @@ def get_tscm_devices():
capture_output=True, text=True, timeout=5 capture_output=True, text=True, timeout=5
) )
blocks = re.split(r'(?=^hci\d+:)', result.stdout, flags=re.MULTILINE) blocks = re.split(r'(?=^hci\d+:)', result.stdout, flags=re.MULTILINE)
for idx, block in enumerate(blocks): for _idx, block in enumerate(blocks):
if block.strip(): if block.strip():
first_line = block.split('\n')[0] first_line = block.split('\n')[0]
match = re.match(r'(hci\d+):', first_line) match = re.match(r'(hci\d+):', first_line)
@@ -353,7 +348,6 @@ def get_preset(preset_name: str):
@tscm_bp.route('/feed/wifi', methods=['POST']) @tscm_bp.route('/feed/wifi', methods=['POST'])
def feed_wifi(): def feed_wifi():
"""Feed WiFi device data for baseline recording.""" """Feed WiFi device data for baseline recording."""
from routes.tscm import _baseline_recorder
data = request.get_json() data = request.get_json()
if data: if data:
@@ -367,7 +361,6 @@ def feed_wifi():
@tscm_bp.route('/feed/bluetooth', methods=['POST']) @tscm_bp.route('/feed/bluetooth', methods=['POST'])
def feed_bluetooth(): def feed_bluetooth():
"""Feed Bluetooth device data for baseline recording.""" """Feed Bluetooth device data for baseline recording."""
from routes.tscm import _baseline_recorder
data = request.get_json() data = request.get_json()
if data: if data:
@@ -378,7 +371,6 @@ def feed_bluetooth():
@tscm_bp.route('/feed/rf', methods=['POST']) @tscm_bp.route('/feed/rf', methods=['POST'])
def feed_rf(): def feed_rf():
"""Feed RF signal data for baseline recording.""" """Feed RF signal data for baseline recording."""
from routes.tscm import _baseline_recorder
data = request.get_json() data = request.get_json()
if data: if data:
+1 -1
View File
@@ -4,8 +4,8 @@ from __future__ import annotations
from flask import Blueprint, Response, jsonify, request from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
from utils.logging import get_logger from utils.logging import get_logger
from utils.responses import api_error
from utils.updater import ( from utils.updater import (
check_for_updates, check_for_updates,
dismiss_update, dismiss_update,
+6 -10
View File
@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import io import contextlib
import json import json
import os import os
import platform import platform
@@ -13,12 +13,11 @@ import subprocess
import threading import threading
import time import time
from datetime import datetime from datetime import datetime
from typing import Any, Generator from typing import Any
from flask import Blueprint, Response, jsonify, request from flask import Blueprint, Response, jsonify, request
import app as app_module import app as app_module
from utils.responses import api_success, api_error
from utils.acars_translator import translate_message from utils.acars_translator import translate_message
from utils.constants import ( from utils.constants import (
PROCESS_START_WAIT, PROCESS_START_WAIT,
@@ -30,6 +29,7 @@ from utils.event_pipeline import process_event
from utils.flight_correlator import get_flight_correlator from utils.flight_correlator import get_flight_correlator
from utils.logging import sensor_logger as logger from utils.logging import sensor_logger as logger
from utils.process import register_process, unregister_process from utils.process import register_process, unregister_process
from utils.responses import api_error
from utils.sdr import SDRFactory, SDRType from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout from utils.sse import sse_stream_fanout
from utils.validation import validate_device_index, validate_gain, validate_ppm from utils.validation import validate_device_index, validate_gain, validate_ppm
@@ -105,10 +105,8 @@ def stream_vdl2_output(process: subprocess.Popen, is_text_mode: bool = False) ->
app_module.vdl2_queue.put(data) app_module.vdl2_queue.put(data)
# Feed flight correlator # Feed flight correlator
try: with contextlib.suppress(Exception):
get_flight_correlator().add_vdl2_message(data) get_flight_correlator().add_vdl2_message(data)
except Exception:
pass
# Log if enabled # Log if enabled
if app_module.logging_enabled: if app_module.logging_enabled:
@@ -134,10 +132,8 @@ def stream_vdl2_output(process: subprocess.Popen, is_text_mode: bool = False) ->
process.terminate() process.terminate()
process.wait(timeout=2) process.wait(timeout=2)
except Exception: except Exception:
try: with contextlib.suppress(Exception):
process.kill() process.kill()
except Exception:
pass
unregister_process(process) unregister_process(process)
app_module.vdl2_queue.put({'type': 'status', 'status': 'stopped'}) app_module.vdl2_queue.put({'type': 'status', 'status': 'stopped'})
with app_module.vdl2_lock: with app_module.vdl2_lock:
@@ -275,7 +271,7 @@ def start_vdl2() -> Response:
) )
os.close(slave_fd) os.close(slave_fd)
# Wrap master_fd as a text file for line-buffered reading # Wrap master_fd as a text file for line-buffered reading
process.stdout = io.open(master_fd, 'r', buffering=1) process.stdout = open(master_fd, buffering=1)
is_text_mode = True is_text_mode = True
else: else:
process = subprocess.Popen( process = subprocess.Popen(
-2
View File
@@ -372,7 +372,6 @@ def init_waterfall_websocket(app: Flask):
capture_center_mhz = 0.0 capture_center_mhz = 0.0
capture_start_freq = 0.0 capture_start_freq = 0.0
capture_end_freq = 0.0 capture_end_freq = 0.0
capture_span_mhz = 0.0
# Queue for outgoing messages — only the main loop touches ws.send() # Queue for outgoing messages — only the main loop touches ws.send()
send_queue = queue.Queue(maxsize=120) send_queue = queue.Queue(maxsize=120)
@@ -619,7 +618,6 @@ def init_waterfall_websocket(app: Flask):
capture_center_mhz = center_freq_mhz capture_center_mhz = center_freq_mhz
capture_start_freq = start_freq capture_start_freq = start_freq
capture_end_freq = end_freq capture_end_freq = end_freq
capture_span_mhz = effective_span_mhz
my_generation = _set_shared_capture_state( my_generation = _set_shared_capture_state(
running=True, running=True,
+15 -7
View File
@@ -8,18 +8,26 @@ from __future__ import annotations
import queue import queue
from flask import Blueprint, jsonify, request, Response, send_file from flask import Blueprint, Response, jsonify, request, send_file
from utils.responses import api_success, api_error
from utils.logging import get_logger from utils.logging import get_logger
from utils.responses import api_error
from utils.sse import sse_stream from utils.sse import sse_stream
from utils.validation import validate_device_index, validate_gain, validate_latitude, validate_longitude, validate_elevation, validate_rtl_tcp_host, validate_rtl_tcp_port from utils.validation import (
validate_device_index,
validate_elevation,
validate_gain,
validate_latitude,
validate_longitude,
validate_rtl_tcp_host,
validate_rtl_tcp_port,
)
from utils.weather_sat import ( from utils.weather_sat import (
DEFAULT_SAMPLE_RATE,
WEATHER_SATELLITES,
CaptureProgress,
get_weather_sat_decoder, get_weather_sat_decoder,
is_weather_sat_available, is_weather_sat_available,
CaptureProgress,
WEATHER_SATELLITES,
DEFAULT_SAMPLE_RATE,
) )
logger = get_logger('intercept.weather_sat') logger = get_logger('intercept.weather_sat')
@@ -613,7 +621,7 @@ def enable_schedule():
gain=gain_val, gain=gain_val,
bias_t=bool(data.get('bias_t', False)), bias_t=bool(data.get('bias_t', False)),
) )
except Exception as e: except Exception:
logger.exception("Failed to enable weather sat scheduler") logger.exception("Failed to enable weather sat scheduler")
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
+12 -19
View File
@@ -9,11 +9,10 @@ import re
import struct import struct
import threading import threading
import time import time
from typing import Optional
from flask import Blueprint, Flask, jsonify, request, Response from flask import Blueprint, Flask, Response, jsonify, request
from utils.responses import api_success, api_error from utils.responses import api_error, api_success
try: try:
from flask_sock import Sock from flask_sock import Sock
@@ -21,7 +20,9 @@ try:
except ImportError: except ImportError:
WEBSOCKET_AVAILABLE = False WEBSOCKET_AVAILABLE = False
from utils.kiwisdr import KiwiSDRClient, KIWI_SAMPLE_RATE, VALID_MODES, parse_host_port import contextlib
from utils.kiwisdr import KIWI_SAMPLE_RATE, VALID_MODES, KiwiSDRClient, parse_host_port
from utils.logging import get_logger from utils.logging import get_logger
logger = get_logger('intercept.websdr') logger = get_logger('intercept.websdr')
@@ -38,7 +39,7 @@ _cache_timestamp: float = 0
CACHE_TTL = 3600 # 1 hour CACHE_TTL = 3600 # 1 hour
def _parse_gps_coord(coord_str: str) -> Optional[float]: def _parse_gps_coord(coord_str: str) -> float | None:
"""Parse a GPS coordinate string like '51.5074' or '(-33.87)' into a float.""" """Parse a GPS coordinate string like '51.5074' or '(-33.87)' into a float."""
if not coord_str: if not coord_str:
return None return None
@@ -70,8 +71,8 @@ KIWI_DATA_URLS = [
def _fetch_kiwi_receivers() -> list[dict]: def _fetch_kiwi_receivers() -> list[dict]:
"""Fetch the KiwiSDR receiver list from the public directory.""" """Fetch the KiwiSDR receiver list from the public directory."""
import urllib.request
import json import json
import urllib.request
receivers = [] receivers = []
raw = None raw = None
@@ -335,7 +336,7 @@ def websdr_status() -> Response:
# KIWISDR AUDIO PROXY # KIWISDR AUDIO PROXY
# ============================================ # ============================================
_kiwi_client: Optional[KiwiSDRClient] = None _kiwi_client: KiwiSDRClient | None = None
_kiwi_lock = threading.Lock() _kiwi_lock = threading.Lock()
_kiwi_audio_queue: queue.Queue = queue.Queue(maxsize=200) _kiwi_audio_queue: queue.Queue = queue.Queue(maxsize=200)
@@ -387,26 +388,18 @@ def _handle_kiwi_command(ws, cmd: str, data: dict) -> None:
try: try:
_kiwi_audio_queue.put_nowait(header + pcm_bytes) _kiwi_audio_queue.put_nowait(header + pcm_bytes)
except queue.Full: except queue.Full:
try: with contextlib.suppress(queue.Empty):
_kiwi_audio_queue.get_nowait() _kiwi_audio_queue.get_nowait()
except queue.Empty: with contextlib.suppress(queue.Full):
pass
try:
_kiwi_audio_queue.put_nowait(header + pcm_bytes) _kiwi_audio_queue.put_nowait(header + pcm_bytes)
except queue.Full:
pass
def on_error(msg): def on_error(msg):
try: with contextlib.suppress(Exception):
ws.send(json.dumps({'type': 'error', 'message': msg})) ws.send(json.dumps({'type': 'error', 'message': msg}))
except Exception:
pass
def on_disconnect(): def on_disconnect():
try: with contextlib.suppress(Exception):
ws.send(json.dumps({'type': 'disconnected'})) ws.send(json.dumps({'type': 'disconnected'}))
except Exception:
pass
with _kiwi_lock: with _kiwi_lock:
_kiwi_client = KiwiSDRClient( _kiwi_client = KiwiSDRClient(
+4 -5
View File
@@ -6,13 +6,14 @@ maritime/aviation weather services worldwide.
from __future__ import annotations from __future__ import annotations
import contextlib
import queue import queue
from flask import Blueprint, Response, jsonify, request, send_file from flask import Blueprint, Response, jsonify, request, send_file
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from utils.logging import get_logger from utils.logging import get_logger
from utils.responses import api_error
from utils.sdr import SDRType from utils.sdr import SDRType
from utils.sse import sse_stream_fanout from utils.sse import sse_stream_fanout
from utils.validation import validate_frequency from utils.validation import validate_frequency
@@ -129,10 +130,8 @@ def start_decoder():
frequency_reference = str(data.get('frequency_reference', 'auto')).strip().lower() frequency_reference = str(data.get('frequency_reference', 'auto')).strip().lower()
sdr_type_str = str(data.get('sdr_type', 'rtlsdr')).lower() sdr_type_str = str(data.get('sdr_type', 'rtlsdr')).lower()
try: with contextlib.suppress(ValueError):
sdr_type = SDRType(sdr_type_str) SDRType(sdr_type_str)
except ValueError:
sdr_type = SDRType.RTL_SDR
if not frequency_reference: if not frequency_reference:
frequency_reference = 'auto' frequency_reference = 'auto'
+28 -45
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import contextlib
import fcntl import fcntl
import json import json
import os import os
@@ -11,39 +12,25 @@ import re
import subprocess import subprocess
import threading import threading
import time import time
from typing import Any, Generator from typing import Any
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
import app as app_module import app as app_module
from utils.dependencies import check_tool, get_tool_path
from utils.logging import wifi_logger as logger
from utils.process import is_valid_mac, is_valid_channel
from utils.validation import validate_wifi_channel, validate_mac_address, validate_network_interface
from utils.sse import format_sse, sse_stream_fanout
from utils.event_pipeline import process_event
from data.oui import get_manufacturer from data.oui import get_manufacturer
from utils.constants import ( from utils.constants import (
WIFI_TERMINATE_TIMEOUT,
PMKID_TERMINATE_TIMEOUT,
SSE_KEEPALIVE_INTERVAL, SSE_KEEPALIVE_INTERVAL,
SSE_QUEUE_TIMEOUT, SSE_QUEUE_TIMEOUT,
WIFI_CSV_PARSE_INTERVAL,
WIFI_CSV_TIMEOUT_WARNING,
SUBPROCESS_TIMEOUT_SHORT,
SUBPROCESS_TIMEOUT_MEDIUM, SUBPROCESS_TIMEOUT_MEDIUM,
SUBPROCESS_TIMEOUT_LONG, SUBPROCESS_TIMEOUT_SHORT,
DEAUTH_TIMEOUT,
MIN_DEAUTH_COUNT,
MAX_DEAUTH_COUNT,
DEFAULT_DEAUTH_COUNT,
PROCESS_START_WAIT,
MONITOR_MODE_DELAY,
WIFI_CAPTURE_PATH_PREFIX,
HANDSHAKE_CAPTURE_PATH_PREFIX,
PMKID_CAPTURE_PATH_PREFIX,
) )
from utils.dependencies import check_tool, get_tool_path
from utils.event_pipeline import process_event
from utils.logging import wifi_logger as logger
from utils.process import is_valid_channel, is_valid_mac
from utils.responses import api_error, api_success
from utils.sse import format_sse, sse_stream_fanout
from utils.validation import validate_network_interface, validate_wifi_channel
wifi_bp = Blueprint('wifi', __name__, url_prefix='/wifi') wifi_bp = Blueprint('wifi', __name__, url_prefix='/wifi')
@@ -201,9 +188,9 @@ def _get_interface_details(iface_name):
# Get MAC address # Get MAC address
try: try:
mac_path = f'/sys/class/net/{iface_name}/address' mac_path = f'/sys/class/net/{iface_name}/address'
with open(mac_path, 'r') as f: with open(mac_path) as f:
details['mac'] = f.read().strip().upper() details['mac'] = f.read().strip().upper()
except (FileNotFoundError, IOError): except (OSError, FileNotFoundError):
pass pass
# Get driver name # Get driver name
@@ -212,7 +199,7 @@ def _get_interface_details(iface_name):
if os.path.islink(driver_link): if os.path.islink(driver_link):
driver_path = os.readlink(driver_link) driver_path = os.readlink(driver_link)
details['driver'] = os.path.basename(driver_path) details['driver'] = os.path.basename(driver_path)
except (FileNotFoundError, IOError, OSError): except (FileNotFoundError, OSError):
pass pass
# Try airmon-ng first for chipset info (most reliable for WiFi adapters) # Try airmon-ng first for chipset info (most reliable for WiFi adapters)
@@ -230,11 +217,10 @@ def _get_interface_details(iface_name):
break break
# Also try space-separated format # Also try space-separated format
parts = line.split() parts = line.split()
if len(parts) >= 4: if len(parts) >= 4 and (parts[1] == iface_name or parts[1].startswith(iface_name)):
if parts[1] == iface_name or parts[1].startswith(iface_name): details['driver'] = parts[2]
details['driver'] = parts[2] details['chipset'] = ' '.join(parts[3:])
details['chipset'] = ' '.join(parts[3:]) break
break
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError): except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError):
pass pass
@@ -246,10 +232,10 @@ def _get_interface_details(iface_name):
# Try to get USB product name # Try to get USB product name
for usb_path in [f'{device_path}/product', f'{device_path}/../product']: for usb_path in [f'{device_path}/product', f'{device_path}/../product']:
try: try:
with open(usb_path, 'r') as f: with open(usb_path) as f:
details['chipset'] = f.read().strip() details['chipset'] = f.read().strip()
break break
except (FileNotFoundError, IOError): except (OSError, FileNotFoundError):
pass pass
# If no USB product, try lsusb for USB devices # If no USB product, try lsusb for USB devices
@@ -257,7 +243,7 @@ def _get_interface_details(iface_name):
try: try:
# Get USB bus/device info # Get USB bus/device info
uevent_path = f'{device_path}/uevent' uevent_path = f'{device_path}/uevent'
with open(uevent_path, 'r') as f: with open(uevent_path) as f:
for line in f: for line in f:
if line.startswith('PRODUCT='): if line.startswith('PRODUCT='):
# PRODUCT format: vendor/product/bcdDevice # PRODUCT format: vendor/product/bcdDevice
@@ -280,9 +266,9 @@ def _get_interface_details(iface_name):
except (FileNotFoundError, subprocess.TimeoutExpired): except (FileNotFoundError, subprocess.TimeoutExpired):
pass pass
break break
except (FileNotFoundError, IOError): except (OSError, FileNotFoundError):
pass pass
except (FileNotFoundError, IOError, OSError): except (FileNotFoundError, OSError):
pass pass
return details return details
@@ -294,7 +280,7 @@ def parse_airodump_csv(csv_path):
clients = {} clients = {}
try: try:
with open(csv_path, 'r', errors='replace') as f: with open(csv_path, errors='replace') as f:
content = f.read() content = f.read()
sections = content.split('\n\n') sections = content.split('\n\n')
@@ -602,7 +588,6 @@ def toggle_monitor_mode():
return api_success(data={'monitor_interface': app_module.wifi_monitor_interface}) return api_success(data={'monitor_interface': app_module.wifi_monitor_interface})
except Exception as e: except Exception as e:
import traceback
logger.error(f"Error enabling monitor mode: {e}", exc_info=True) logger.error(f"Error enabling monitor mode: {e}", exc_info=True)
return api_error(str(e)) return api_error(str(e))
@@ -683,11 +668,9 @@ def start_wifi_scan():
csv_path = '/tmp/intercept_wifi' csv_path = '/tmp/intercept_wifi'
for f in [f'/tmp/intercept_wifi-01.csv', f'/tmp/intercept_wifi-01.cap']: for f in ['/tmp/intercept_wifi-01.csv', '/tmp/intercept_wifi-01.cap']:
try: with contextlib.suppress(OSError):
os.remove(f) os.remove(f)
except OSError:
pass
airodump_path = get_tool_path('airodump-ng') airodump_path = get_tool_path('airodump-ng')
cmd = [ cmd = [
@@ -1021,7 +1004,7 @@ def check_pmkid_status():
try: try:
hash_file = capture_file.replace('.pcapng', '.22000') hash_file = capture_file.replace('.pcapng', '.22000')
result = subprocess.run( subprocess.run(
['hcxpcapngtool', '-o', hash_file, capture_file], ['hcxpcapngtool', '-o', hash_file, capture_file],
capture_output=True, text=True, timeout=10 capture_output=True, text=True, timeout=10
) )
@@ -1170,7 +1153,7 @@ def stream_wifi():
# V2 API Endpoints - Using unified WiFi scanner # V2 API Endpoints - Using unified WiFi scanner
# ============================================================================= # =============================================================================
from utils.wifi.scanner import get_wifi_scanner, reset_wifi_scanner from utils.wifi.scanner import get_wifi_scanner
@wifi_bp.route('/v2/capabilities') @wifi_bp.route('/v2/capabilities')
+12 -14
View File
@@ -7,26 +7,26 @@ channel analysis, hidden SSID correlation, and SSE streaming.
from __future__ import annotations from __future__ import annotations
import contextlib
import csv import csv
import io import io
import json import json
import logging import logging
from collections.abc import Generator
from datetime import datetime from datetime import datetime
from typing import Generator
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, jsonify, request
from utils.wifi import ( from utils.event_pipeline import process_event
get_wifi_scanner, from utils.responses import api_error
analyze_channels,
get_hidden_correlator,
SCAN_MODE_QUICK,
SCAN_MODE_DEEP,
)
from utils.responses import api_success, api_error
from utils.sse import format_sse from utils.sse import format_sse
from utils.validation import validate_wifi_channel from utils.validation import validate_wifi_channel
from utils.event_pipeline import process_event from utils.wifi import (
SCAN_MODE_DEEP,
analyze_channels,
get_hidden_correlator,
get_wifi_scanner,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -407,10 +407,8 @@ def event_stream():
scanner = get_wifi_scanner() scanner = get_wifi_scanner()
for event in scanner.get_event_stream(): for event in scanner.get_event_stream():
try: with contextlib.suppress(Exception):
process_event('wifi', event, event.get('type')) process_event('wifi', event, event.get('type'))
except Exception:
pass
yield format_sse(event) yield format_sse(event)
response = Response(generate(), mimetype='text/event-stream') response = Response(generate(), mimetype='text/event-stream')
+7 -1
View File
@@ -957,8 +957,14 @@ install_satdump_from_source_debian() {
) & ) &
progress_pid=$! progress_pid=$!
local arch_flags=""
if [[ "$(uname -m)" == "x86_64" ]]; then
arch_flags="-march=x86-64"
fi
if cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_GUI=OFF -DCMAKE_INSTALL_LIBDIR=lib \ if cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_GUI=OFF -DCMAKE_INSTALL_LIBDIR=lib \
-DCMAKE_CXX_FLAGS="-Wno-template-body" .. >"$build_log" 2>&1 \ -DCMAKE_C_FLAGS="$arch_flags" \
-DCMAKE_CXX_FLAGS="$arch_flags -Wno-template-body" .. >"$build_log" 2>&1 \
&& make -j "$(nproc)" >>"$build_log" 2>&1; then && make -j "$(nproc)" >>"$build_log" 2>&1; then
kill $progress_pid 2>/dev/null; wait $progress_pid 2>/dev/null kill $progress_pid 2>/dev/null; wait $progress_pid 2>/dev/null
$SUDO make install >/dev/null 2>&1 $SUDO make install >/dev/null 2>&1
+16
View File
@@ -87,6 +87,22 @@
color: var(--text-primary); color: var(--text-primary);
} }
/* Branded "i" inline SVG that matches the logo icon.
Sized to 0.9em so it sits naturally alongside text at any font-size. */
.brand-i {
display: inline-block;
width: 0.55em;
height: 0.9em;
vertical-align: baseline;
position: relative;
top: 0.05em;
}
.brand-i svg {
display: block;
width: 100%;
height: 100%;
}
.app-logo-tagline { .app-logo-tagline {
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--text-dim); color: var(--text-dim);
+59
View File
@@ -0,0 +1,59 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 300" width="1200" height="300">
<defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(0,212,255,0.025)" stroke-width="1"/>
</pattern>
<radialGradient id="glow1" cx="75%" cy="30%" r="35%">
<stop offset="0%" stop-color="#00d4ff" stop-opacity="0.05"/>
<stop offset="100%" stop-color="#00d4ff" stop-opacity="0"/>
</radialGradient>
<radialGradient id="glow2" cx="25%" cy="70%" r="30%">
<stop offset="0%" stop-color="#00ff88" stop-opacity="0.03"/>
<stop offset="100%" stop-color="#00ff88" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Background -->
<rect width="1200" height="300" fill="#0a0a0f"/>
<rect width="1200" height="300" fill="url(#grid)"/>
<rect width="1200" height="300" fill="url(#glow1)"/>
<rect width="1200" height="300" fill="url(#glow2)"/>
<!-- Logo -->
<g transform="translate(340, 60) scale(1.0)">
<path d="M15 30 Q5 50, 15 70" stroke="#00d4ff" stroke-width="4" fill="none" stroke-linecap="round" opacity="0.5"/>
<path d="M22 35 Q14 50, 22 65" stroke="#00d4ff" stroke-width="3.5" fill="none" stroke-linecap="round" opacity="0.7"/>
<path d="M29 40 Q23 50, 29 60" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M85 30 Q95 50, 85 70" stroke="#00d4ff" stroke-width="4" fill="none" stroke-linecap="round" opacity="0.5"/>
<path d="M78 35 Q86 50, 78 65" stroke="#00d4ff" stroke-width="3.5" fill="none" stroke-linecap="round" opacity="0.7"/>
<path d="M71 40 Q77 50, 71 60" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round"/>
<circle cx="50" cy="22" r="7" fill="#00ff88"/>
<rect x="43" y="35" width="14" height="45" rx="2" fill="#00d4ff"/>
<rect x="36" y="35" width="28" height="5" rx="1" fill="#00d4ff"/>
<rect x="36" y="75" width="28" height="5" rx="1" fill="#00d4ff"/>
</g>
<!-- Title: branded "i" glyph + NTERCEPT text -->
<g transform="translate(476, 78) scale(0.76)">
<circle cx="50" cy="18" r="6" fill="#00ff88"/>
<rect x="44" y="30" width="12" height="45" rx="2" fill="#00d4ff"/>
<rect x="38" y="30" width="24" height="4" rx="1" fill="#00d4ff"/>
<rect x="38" y="71" width="24" height="4" rx="1" fill="#00d4ff"/>
</g>
<text x="524" y="135" font-family="'Segoe UI','Helvetica Neue',Arial,sans-serif" font-size="64" font-weight="800" letter-spacing="-1.5" fill="white">NTERCEPT</text>
<!-- Subtitle -->
<text x="490" y="170" font-family="'Segoe UI','Helvetica Neue',Arial,sans-serif" font-size="18" font-weight="300" fill="rgba(255,255,255,0.35)">
Web-Based Signal Intelligence Platform
</text>
<!-- Feature dots -->
<g font-family="'Courier New',monospace" font-size="11" fill="rgba(0,212,255,0.4)" letter-spacing="1.5">
<circle cx="492" cy="206" r="2" fill="#00ff88" opacity="0.6"/>
<text x="500" y="210">34 Signal Modes</text>
<circle cx="492" cy="228" r="2" fill="#00ff88" opacity="0.6"/>
<text x="500" y="232">Multi-SDR Support</text>
<circle cx="492" cy="250" r="2" fill="#00ff88" opacity="0.6"/>
<text x="500" y="254">Open Source</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+68
View File
@@ -0,0 +1,68 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 640" width="1280" height="640">
<defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(0,212,255,0.025)" stroke-width="1"/>
</pattern>
<radialGradient id="glow1" cx="80%" cy="20%" r="40%">
<stop offset="0%" stop-color="#00d4ff" stop-opacity="0.06"/>
<stop offset="100%" stop-color="#00d4ff" stop-opacity="0"/>
</radialGradient>
<radialGradient id="glow2" cx="20%" cy="80%" r="30%">
<stop offset="0%" stop-color="#00ff88" stop-opacity="0.04"/>
<stop offset="100%" stop-color="#00ff88" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Background -->
<rect width="1280" height="640" fill="#0a0a0f"/>
<rect width="1280" height="640" fill="url(#grid)"/>
<rect width="1280" height="640" fill="url(#glow1)"/>
<rect width="1280" height="640" fill="url(#glow2)"/>
<!-- Corner brackets -->
<g stroke="rgba(0,212,255,0.2)" stroke-width="1.5" fill="none">
<polyline points="24,48 24,24 48,24"/>
<polyline points="1232,24 1256,24 1256,48"/>
<polyline points="24,592 24,616 48,616"/>
<polyline points="1232,616 1256,616 1256,592"/>
</g>
<!-- Logo -->
<g transform="translate(560, 140) scale(1.4)">
<path d="M15 30 Q5 50, 15 70" stroke="#00d4ff" stroke-width="4" fill="none" stroke-linecap="round" opacity="0.5"/>
<path d="M22 35 Q14 50, 22 65" stroke="#00d4ff" stroke-width="3.5" fill="none" stroke-linecap="round" opacity="0.7"/>
<path d="M29 40 Q23 50, 29 60" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M85 30 Q95 50, 85 70" stroke="#00d4ff" stroke-width="4" fill="none" stroke-linecap="round" opacity="0.5"/>
<path d="M78 35 Q86 50, 78 65" stroke="#00d4ff" stroke-width="3.5" fill="none" stroke-linecap="round" opacity="0.7"/>
<path d="M71 40 Q77 50, 71 60" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round"/>
<circle cx="50" cy="22" r="7" fill="#00ff88"/>
<rect x="43" y="35" width="14" height="45" rx="2" fill="#00d4ff"/>
<rect x="36" y="35" width="28" height="5" rx="1" fill="#00d4ff"/>
<rect x="36" y="75" width="28" height="5" rx="1" fill="#00d4ff"/>
</g>
<!-- Title: branded "i" glyph + NTERCEPT text -->
<g transform="translate(337, 304) scale(1.0)">
<circle cx="50" cy="18" r="6" fill="#00ff88"/>
<rect x="44" y="30" width="12" height="45" rx="2" fill="#00d4ff"/>
<rect x="38" y="30" width="24" height="4" rx="1" fill="#00d4ff"/>
<rect x="38" y="71" width="24" height="4" rx="1" fill="#00d4ff"/>
</g>
<text x="400" y="380" font-family="'Segoe UI','Helvetica Neue',Arial,sans-serif" font-size="84" font-weight="800" letter-spacing="-2" fill="white">NTERCEPT</text>
<!-- Subtitle -->
<text x="640" y="425" text-anchor="middle" font-family="'Segoe UI','Helvetica Neue',Arial,sans-serif" font-size="24" font-weight="300" fill="rgba(255,255,255,0.4)" letter-spacing="0.5">
Web-Based Signal Intelligence Platform
</text>
<!-- Tags -->
<g font-family="'Courier New',monospace" font-size="12" fill="rgba(0,212,255,0.5)" letter-spacing="2">
<text x="440" y="480" text-anchor="middle">SDR</text>
<text x="520" y="480" text-anchor="middle" fill="rgba(255,255,255,0.15)">|</text>
<text x="600" y="480" text-anchor="middle">RF ANALYSIS</text>
<text x="698" y="480" text-anchor="middle" fill="rgba(255,255,255,0.15)">|</text>
<text x="778" y="480" text-anchor="middle">34 MODES</text>
<text x="858" y="480" text-anchor="middle" fill="rgba(255,255,255,0.15)">|</text>
<text x="950" y="480" text-anchor="middle">OPEN SOURCE</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+1 -1
View File
@@ -51,7 +51,7 @@
<header class="header"> <header class="header">
<div class="logo"> <div class="logo">
AIRCRAFT RADAR AIRCRAFT RADAR
<span>// INTERCEPT - See the Invisible</span> <span>// <span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT - See the Invisible</span>
</div> </div>
<div class="status-bar"> <div class="status-bar">
<!-- Agent Selector --> <!-- Agent Selector -->
+1 -1
View File
@@ -22,7 +22,7 @@
<header class="header"> <header class="header">
<div class="logo"> <div class="logo">
ADS-B HISTORY ADS-B HISTORY
<span>// INTERCEPT REPORTING</span> <span>// <span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT REPORTING</span>
</div> </div>
<div class="status-bar"> <div class="status-bar">
<a href="/adsb/dashboard" class="back-link">Live Radar</a> <a href="/adsb/dashboard" class="back-link">Live Radar</a>
+1 -1
View File
@@ -281,7 +281,7 @@
</svg> </svg>
</div> </div>
<h1 style="margin: 0;"> <h1 style="margin: 0;">
iNTERCEPT <span class="tagline">// Remote Agents</span> <span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT <span class="tagline">// Remote Agents</span>
</h1> </h1>
</header> </header>
+1 -1
View File
@@ -51,7 +51,7 @@
<header class="header"> <header class="header">
<div class="logo"> <div class="logo">
VESSEL RADAR VESSEL RADAR
<span>// INTERCEPT - AIS Tracking</span> <span>// <span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT - AIS Tracking</span>
</div> </div>
<div class="status-bar"> <div class="status-bar">
<!-- Agent Selector --> <!-- Agent Selector -->
+2 -2
View File
@@ -293,7 +293,7 @@
<rect x="38" y="76" width="24" height="4" rx="1" fill="#00d4ff" /> <rect x="38" y="76" width="24" height="4" rx="1" fill="#00d4ff" />
</svg> </svg>
</div> </div>
<h1 class="welcome-title">iNTERCEPT</h1> <h1 class="welcome-title"><span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT</h1>
<p class="welcome-tagline">// See the Invisible</p> <p class="welcome-tagline">// See the Invisible</p>
<span class="welcome-version">v{{ version }}</span> <span class="welcome-version">v{{ version }}</span>
<button type="button" class="welcome-settings-btn" onclick="showSettings()" title="Settings" aria-label="Open settings"> <button type="button" class="welcome-settings-btn" onclick="showSettings()" title="Settings" aria-label="Open settings">
@@ -589,7 +589,7 @@
<rect x="38" y="76" width="24" height="4" rx="1" fill="#00d4ff" /> <rect x="38" y="76" width="24" height="4" rx="1" fill="#00d4ff" />
</svg> </svg>
</a> </a>
<h1>iNTERCEPT <span class="tagline">// See the Invisible</span></h1> <h1><span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT <span class="tagline">// See the Invisible</span></h1>
</div> </div>
<div class="header-right"> <div class="header-right">
<span class="active-mode-indicator" id="activeModeIndicator"><span class="pulse-dot"></span>PAGER</span> <span class="active-mode-indicator" id="activeModeIndicator"><span class="pulse-dot"></span>PAGER</span>
+1 -1
View File
@@ -53,7 +53,7 @@
<rect x="38" y="76" width="24" height="4" rx="1" fill="var(--accent-cyan)"/> <rect x="38" y="76" width="24" height="4" rx="1" fill="var(--accent-cyan)"/>
</svg> </svg>
<span class="app-logo-text"> <span class="app-logo-text">
<span class="app-logo-title">iNTERCEPT</span> <span class="app-logo-title"><span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT</span>
<span class="app-logo-tagline">// See the Invisible</span> <span class="app-logo-tagline">// See the Invisible</span>
</span> </span>
</a> </a>
+1 -1
View File
@@ -151,7 +151,7 @@
{% block dashboard_title %}DASHBOARD{% endblock %} {% block dashboard_title %}DASHBOARD{% endblock %}
</span> </span>
<span style="font-size: var(--text-sm); color: var(--text-dim); margin-left: var(--space-2);"> <span style="font-size: var(--text-sm); color: var(--text-dim); margin-left: var(--space-2);">
// iNTERCEPT // <span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT
</span> </span>
</div> </div>
</div> </div>
+1 -1
View File
@@ -7,7 +7,7 @@
<div id="helpModal" class="help-modal" role="dialog" aria-modal="true" aria-hidden="true" aria-labelledby="helpModalTitle" onclick="if(event.target === this) hideHelp()"> <div id="helpModal" class="help-modal" role="dialog" aria-modal="true" aria-hidden="true" aria-labelledby="helpModalTitle" onclick="if(event.target === this) hideHelp()">
<div class="help-content" tabindex="-1"> <div class="help-content" tabindex="-1">
<button type="button" class="help-close" onclick="hideHelp()" aria-label="Close help">&times;</button> <button type="button" class="help-close" onclick="hideHelp()" aria-label="Close help">&times;</button>
<h2 id="helpModalTitle">iNTERCEPT Help</h2> <h2 id="helpModalTitle"><span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT Help</h2>
<div class="help-tabs" role="tablist" aria-label="Help sections"> <div class="help-tabs" role="tablist" aria-label="Help sections">
<button type="button" class="help-tab active" data-tab="icons" onclick="switchHelpTab('icons')" role="tab" aria-controls="help-icons" aria-selected="true">Icons</button> <button type="button" class="help-tab active" data-tab="icons" onclick="switchHelpTab('icons')" role="tab" aria-controls="help-icons" aria-selected="true">Icons</button>
+2 -2
View File
@@ -530,7 +530,7 @@
<div id="settings-about" class="settings-section"> <div id="settings-about" class="settings-section">
<div class="settings-group"> <div class="settings-group">
<div class="about-info"> <div class="about-info">
<p><strong>iNTERCEPT</strong> - Signal Intelligence Platform</p> <p><strong><span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT</strong> - Signal Intelligence Platform</p>
<p>Version: <span class="about-version">{{ version }}</span></p> <p>Version: <span class="about-version">{{ version }}</span></p>
<p> <p>
A unified web interface for software-defined radio (SDR) tools, A unified web interface for software-defined radio (SDR) tools,
@@ -546,7 +546,7 @@
<div class="settings-group"> <div class="settings-group">
<div class="settings-group-title">Support the Project</div> <div class="settings-group-title">Support the Project</div>
<p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;"> <p style="color: var(--text-dim); margin-bottom: 15px; font-size: 12px;">
If you find iNTERCEPT useful, consider supporting its development. If you find <span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT useful, consider supporting its development.
</p> </p>
<a href="https://buymeacoffee.com/smittix" target="_blank" rel="noopener noreferrer" class="donate-btn"> <a href="https://buymeacoffee.com/smittix" target="_blank" rel="noopener noreferrer" class="donate-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 18px; height: 18px; vertical-align: -3px; margin-right: 8px;"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 18px; height: 18px; vertical-align: -3px; margin-right: 8px;">
+1 -1
View File
@@ -37,7 +37,7 @@
<header class="header"> <header class="header">
<div class="logo"> <div class="logo">
SATELLITE COMMAND SATELLITE COMMAND
<span>// iNTERCEPT - See the Invisible</span> <span>// <span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT - See the Invisible</span>
</div> </div>
<div class="stats-badges"> <div class="stats-badges">
<div class="stat-badge"> <div class="stat-badge">
+5 -4
View File
@@ -1,9 +1,11 @@
"""Pytest configuration and fixtures.""" """Pytest configuration and fixtures."""
import contextlib
import sqlite3 import sqlite3
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
from app import app as flask_app from app import app as flask_app
from routes import register_blueprints from routes import register_blueprints
@@ -80,9 +82,10 @@ def mock_app_state():
Provides mock process, queue, and lock objects on the app module. Provides mock process, queue, and lock objects on the app module.
""" """
import app as app_module
import queue import queue
import app as app_module
mock_process = MagicMock() mock_process = MagicMock()
mock_process.poll.return_value = None mock_process.poll.return_value = None
mock_queue = queue.Queue() mock_queue = queue.Queue()
@@ -107,10 +110,8 @@ def mock_app_state():
for attr, orig in originals.items(): for attr, orig in originals.items():
if orig is None: if orig is None:
try: with contextlib.suppress(AttributeError):
delattr(app_module, attr) delattr(app_module, attr)
except AttributeError:
pass
else: else:
setattr(app_module, attr, orig) setattr(app_module, attr, orig)
+5 -7
View File
@@ -12,12 +12,12 @@ Usage:
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import json
import random import random
import string import string
import threading import threading
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from flask import Flask, jsonify, request from flask import Flask, jsonify, request
app = Flask(__name__) app = Flask(__name__)
@@ -53,7 +53,7 @@ def generate_sensors() -> list[dict]:
"""Generate fake 433MHz sensor data.""" """Generate fake 433MHz sensor data."""
sensors = [] sensors = []
models = ['Acurite-Tower', 'Oregon-THGR122N', 'LaCrosse-TX141W', 'Ambient-F007TH'] models = ['Acurite-Tower', 'Oregon-THGR122N', 'LaCrosse-TX141W', 'Ambient-F007TH']
for i in range(random.randint(2, 5)): for _i in range(random.randint(2, 5)):
sensors.append({ sensors.append({
'time': datetime.now(timezone.utc).isoformat(), 'time': datetime.now(timezone.utc).isoformat(),
'model': random.choice(models), 'model': random.choice(models),
@@ -71,7 +71,7 @@ def generate_wifi_networks() -> list[dict]:
networks = [] networks = []
ssids = ['HomeNetwork', 'Linksys', 'NETGEAR', 'xfinitywifi', 'ATT-WIFI', 'CoffeeShop-Guest'] ssids = ['HomeNetwork', 'Linksys', 'NETGEAR', 'xfinitywifi', 'ATT-WIFI', 'CoffeeShop-Guest']
for ssid in random.sample(ssids, random.randint(3, 6)): for ssid in random.sample(ssids, random.randint(3, 6)):
bssid = ':'.join(['%02X' % random.randint(0, 255) for _ in range(6)]) bssid = ':'.join([f'{random.randint(0, 255):02X}' for _ in range(6)])
networks.append({ networks.append({
'ssid': ssid, 'ssid': ssid,
'bssid': bssid, 'bssid': bssid,
@@ -89,7 +89,7 @@ def generate_bluetooth_devices() -> list[dict]:
devices = [] devices = []
names = ['iPhone', 'Galaxy S21', 'AirPods', 'Tile Tracker', 'Fitbit', 'Unknown'] names = ['iPhone', 'Galaxy S21', 'AirPods', 'Tile Tracker', 'Fitbit', 'Unknown']
for _ in range(random.randint(2, 8)): for _ in range(random.randint(2, 8)):
mac = ':'.join(['%02X' % random.randint(0, 255) for _ in range(6)]) mac = ':'.join([f'{random.randint(0, 255):02X}' for _ in range(6)])
devices.append({ devices.append({
'address': mac, 'address': mac,
'name': random.choice(names), 'name': random.choice(names),
@@ -209,9 +209,7 @@ def config():
'name': agent_name, 'name': agent_name,
'port': request.environ.get('SERVER_PORT', 8021), 'port': request.environ.get('SERVER_PORT', 8021),
'push_enabled': False, 'push_enabled': False,
'modes_enabled': {m: True for m in [ 'modes_enabled': dict.fromkeys(['pager', 'sensor', 'adsb', 'ais', 'wifi', 'bluetooth'], True)
'pager', 'sensor', 'adsb', 'ais', 'wifi', 'bluetooth'
]}
}) })
+1 -4
View File
@@ -17,10 +17,7 @@ Requirements:
""" """
import argparse import argparse
import json
import sys import sys
import time
from typing import Any
try: try:
import requests import requests
@@ -258,7 +255,7 @@ class SmokeTests:
def run_all(self): def run_all(self):
"""Run all smoke tests.""" """Run all smoke tests."""
print(f"\n{'='*60}") print(f"\n{'='*60}")
print(f"BLUETOOTH API SMOKE TESTS") print("BLUETOOTH API SMOKE TESTS")
print(f"Target: {self.base_url}") print(f"Target: {self.base_url}")
print(f"{'='*60}") print(f"{'='*60}")
+3 -6
View File
@@ -1,19 +1,16 @@
"""Tests for ACARS message translator.""" """Tests for ACARS message translator."""
import pytest
from utils.acars_translator import ( from utils.acars_translator import (
ACARS_LABELS,
translate_label,
classify_message_type, classify_message_type,
parse_position_report,
parse_engine_data, parse_engine_data,
parse_weather_data,
parse_oooi, parse_oooi,
parse_position_report,
parse_weather_data,
translate_label,
translate_message, translate_message,
) )
# --- translate_label --- # --- translate_label ---
class TestTranslateLabel: class TestTranslateLabel:
+1 -2
View File
@@ -2,9 +2,8 @@
import queue import queue
import threading import threading
import time
from datetime import datetime, timezone from datetime import datetime, timezone
from unittest.mock import MagicMock, patch, PropertyMock from unittest.mock import MagicMock, patch
import pytest import pytest
+16 -12
View File
@@ -10,23 +10,27 @@ Tests cover:
import json import json
import os import os
import pytest
import tempfile
from unittest.mock import Mock, patch, MagicMock
import sys import sys
import tempfile
from unittest.mock import Mock, patch
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils.agent_client import ( from utils.agent_client import AgentClient, AgentConnectionError, AgentHTTPError, create_client_from_agent
AgentClient, AgentHTTPError, AgentConnectionError, create_client_from_agent
)
from utils.database import ( from utils.database import (
init_db, get_db_path, create_agent, get_agent, get_agent_by_name, create_agent,
list_agents, update_agent, delete_agent, store_push_payload, delete_agent,
get_recent_payloads, cleanup_old_payloads get_agent,
get_agent_by_name,
get_recent_payloads,
init_db,
list_agents,
store_push_payload,
update_agent,
) )
# ============================================================================= # =============================================================================
# AgentConfig Tests # AgentConfig Tests
# ============================================================================= # =============================================================================
@@ -559,8 +563,8 @@ class TestAgentClientIntegration:
@pytest.fixture @pytest.fixture
def mock_agent(self): def mock_agent(self):
"""Start mock agent server for testing.""" """Start mock agent server for testing."""
from tests.mock_agent import app as mock_app from tests.mock_agent import app as mock_app
import threading
# Run mock agent in background # Run mock agent in background
mock_app.config['TESTING'] = True mock_app.config['TESTING'] = True
+2 -1
View File
@@ -19,13 +19,14 @@ Skip live tests:
import json import json
import os import os
import pytest
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import time import time
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+11 -15
View File
@@ -10,14 +10,13 @@ Tests cover:
- Error handling and edge cases - Error handling and edge cases
""" """
import contextlib
import os import os
import sys import sys
import json
import time import time
from unittest.mock import MagicMock, patch
import pytest import pytest
import threading
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime, timezone
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -34,10 +33,8 @@ def mode_manager():
yield manager yield manager
# Cleanup: stop all modes # Cleanup: stop all modes
for mode in list(manager.running_modes.keys()): for mode in list(manager.running_modes.keys()):
try: with contextlib.suppress(Exception):
manager.stop_mode(mode) manager.stop_mode(mode)
except Exception:
pass
@pytest.fixture @pytest.fixture
@@ -139,14 +136,13 @@ class TestModeLifecycle:
mock_popen, mock_proc = mock_subprocess mock_popen, mock_proc = mock_subprocess
# Mock glob for CSV file detection # Mock glob for CSV file detection
with patch('glob.glob', return_value=[]): with patch('glob.glob', return_value=[]), patch('tempfile.mkdtemp', return_value='/tmp/test'):
with patch('tempfile.mkdtemp', return_value='/tmp/test'): result = mode_manager.start_mode('wifi', {
result = mode_manager.start_mode('wifi', { 'interface': 'wlan0',
'interface': 'wlan0', 'scan_type': 'quick'
'scan_type': 'quick' })
}) # Quick scan returns data directly
# Quick scan returns data directly assert result['status'] in ['started', 'error', 'success']
assert result['status'] in ['started', 'error', 'success']
def test_bluetooth_mode_lifecycle(self, mode_manager, mock_subprocess, mock_tools): def test_bluetooth_mode_lifecycle(self, mode_manager, mock_subprocess, mock_tools):
"""Bluetooth mode should start and stop cleanly.""" """Bluetooth mode should start and stop cleanly."""
-1
View File
@@ -1,6 +1,5 @@
"""Tests for main application routes.""" """Tests for main application routes."""
import pytest
def test_index_page(client): def test_index_page(client):
-1
View File
@@ -6,7 +6,6 @@ import pytest
from routes.aprs import parse_aprs_packet from routes.aprs import parse_aprs_packet
_BASE_PACKET = "N0CALL-9>APRS,TCPIP*:@092345z4903.50N/07201.75W_090/000g005t077" _BASE_PACKET = "N0CALL-9>APRS,TCPIP*:@092345z4903.50N/07201.75W_090/000g005t077"
+3 -3
View File
@@ -1,8 +1,8 @@
import pytest
import json
import subprocess
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest
from flask import Flask from flask import Flask
from routes.bluetooth import bluetooth_bp, classify_bt_device, detect_tracker from routes.bluetooth import bluetooth_bp, classify_bt_device, detect_tracker
+6 -4
View File
@@ -1,15 +1,17 @@
"""Unit tests for Bluetooth device aggregation.""" """Unit tests for Bluetooth device aggregation."""
import pytest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch
import pytest
from utils.bluetooth.aggregator import DeviceAggregator from utils.bluetooth.aggregator import DeviceAggregator
from utils.bluetooth.models import BTObservation, BTDeviceAggregate
from utils.bluetooth.constants import ( from utils.bluetooth.constants import (
MAX_RSSI_SAMPLES,
DEVICE_STALE_TIMEOUT as DEVICE_STALE_SECONDS, DEVICE_STALE_TIMEOUT as DEVICE_STALE_SECONDS,
) )
from utils.bluetooth.constants import (
MAX_RSSI_SAMPLES,
)
from utils.bluetooth.models import BTObservation
@pytest.fixture @pytest.fixture
+4 -4
View File
@@ -1,13 +1,13 @@
"""API endpoint tests for Bluetooth v2 routes.""" """API endpoint tests for Bluetooth v2 routes."""
import pytest
import json
from unittest.mock import MagicMock, patch, PropertyMock
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock, patch
import pytest
from flask import Flask from flask import Flask
from routes.bluetooth_v2 import bluetooth_v2_bp from routes.bluetooth_v2 import bluetooth_v2_bp
from utils.bluetooth.models import BTDeviceAggregate, ScanStatus, SystemCapabilities from utils.bluetooth.models import BTDeviceAggregate, SystemCapabilities
@pytest.fixture @pytest.fixture
+14 -6
View File
@@ -1,18 +1,26 @@
"""Unit tests for Bluetooth heuristic detection.""" """Unit tests for Bluetooth heuristic detection."""
import pytest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import MagicMock
from utils.bluetooth.heuristics import HeuristicsEngine import pytest
from utils.bluetooth.models import BTDeviceAggregate
from utils.bluetooth.constants import (
BEACON_INTERVAL_MAX_VARIANCE as HEURISTIC_BEACON_VARIANCE_THRESHOLD,
)
from utils.bluetooth.constants import ( from utils.bluetooth.constants import (
PERSISTENT_MIN_SEEN_COUNT as HEURISTIC_PERSISTENT_MIN_SEEN, PERSISTENT_MIN_SEEN_COUNT as HEURISTIC_PERSISTENT_MIN_SEEN,
)
from utils.bluetooth.constants import (
PERSISTENT_WINDOW_SECONDS as HEURISTIC_PERSISTENT_WINDOW_SECONDS, PERSISTENT_WINDOW_SECONDS as HEURISTIC_PERSISTENT_WINDOW_SECONDS,
BEACON_INTERVAL_MAX_VARIANCE as HEURISTIC_BEACON_VARIANCE_THRESHOLD, )
STRONG_RSSI_THRESHOLD as HEURISTIC_STRONG_STABLE_RSSI, from utils.bluetooth.constants import (
STABLE_VARIANCE_THRESHOLD as HEURISTIC_STRONG_STABLE_VARIANCE, STABLE_VARIANCE_THRESHOLD as HEURISTIC_STRONG_STABLE_VARIANCE,
) )
from utils.bluetooth.constants import (
STRONG_RSSI_THRESHOLD as HEURISTIC_STRONG_STABLE_RSSI,
)
from utils.bluetooth.heuristics import HeuristicsEngine
from utils.bluetooth.models import BTDeviceAggregate
@pytest.fixture @pytest.fixture
+6 -6
View File
@@ -5,21 +5,21 @@ Tests device key stability, EMA smoothing, distance estimation,
band classification, and ring buffer functionality. band classification, and ring buffer functionality.
""" """
import pytest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import patch
import pytest
from utils.bluetooth.device_key import ( from utils.bluetooth.device_key import (
extract_key_type,
generate_device_key, generate_device_key,
is_randomized_mac, is_randomized_mac,
extract_key_type,
) )
from utils.bluetooth.distance import ( from utils.bluetooth.distance import (
DistanceEstimator, RSSI_THRESHOLD_FAR,
ProximityBand,
RSSI_THRESHOLD_IMMEDIATE, RSSI_THRESHOLD_IMMEDIATE,
RSSI_THRESHOLD_NEAR, RSSI_THRESHOLD_NEAR,
RSSI_THRESHOLD_FAR, DistanceEstimator,
ProximityBand,
) )
from utils.bluetooth.ring_buffer import RingBuffer from utils.bluetooth.ring_buffer import RingBuffer
+3 -3
View File
@@ -1,7 +1,5 @@
"""Tests for configuration module.""" """Tests for configuration module."""
import os
import pytest
class TestConfigEnvVars: class TestConfigEnvVars:
@@ -9,7 +7,7 @@ class TestConfigEnvVars:
def test_default_values(self): def test_default_values(self):
"""Test that default values are set.""" """Test that default values are set."""
from config import PORT, HOST, DEBUG from config import DEBUG, HOST, PORT
assert PORT == 5050 assert PORT == 5050
assert HOST == '0.0.0.0' assert HOST == '0.0.0.0'
@@ -22,6 +20,7 @@ class TestConfigEnvVars:
# Re-import to get new values # Re-import to get new values
import importlib import importlib
import config import config
importlib.reload(config) importlib.reload(config)
@@ -38,6 +37,7 @@ class TestConfigEnvVars:
monkeypatch.setenv('INTERCEPT_PORT', 'invalid') monkeypatch.setenv('INTERCEPT_PORT', 'invalid')
import importlib import importlib
import config import config
importlib.reload(config) importlib.reload(config)
+4 -2
View File
@@ -11,9 +11,10 @@ Tests cover:
import json import json
import os import os
import pytest
import sys import sys
from unittest.mock import Mock, patch, MagicMock from unittest.mock import Mock, patch
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -51,6 +52,7 @@ def setup_db(tmp_path):
def app(setup_db): def app(setup_db):
"""Create Flask app with controller blueprint.""" """Create Flask app with controller blueprint."""
from flask import Flask from flask import Flask
from routes.controller import controller_bp from routes.controller import controller_bp
app = Flask(__name__) app = Flask(__name__)
+1 -2
View File
@@ -1,8 +1,7 @@
"""Tests for device correlation engine.""" """Tests for device correlation engine."""
import pytest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import patch, MagicMock from unittest.mock import patch
class TestDeviceCorrelator: class TestDeviceCorrelator:

Some files were not shown because too many files have changed in this diff Show More