mirror of
https://github.com/smittix/intercept.git
synced 2026-06-13 00:03:33 -07:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b9c4ebebd | |||
| 7ed039564b | |||
| 8adfb3a40a | |||
| 9a9b1e9856 | |||
| 8aeb52380e | |||
| 05141b9a1b | |||
| dc0850d339 | |||
| 2bbf896e7c | |||
| faf57741a1 | |||
| fd7d01fc7d | |||
| 8ef9dca6ee | |||
| 4610804de6 | |||
| 6d8836ddfc | |||
| 17944554e6 | |||
| 47a7376632 | |||
| e00fbfddc1 | |||
| 00362bcd57 | |||
| fe42ca207c | |||
| 612e137a60 | |||
| 17913fc0e8 | |||
| 44d256179b | |||
| 3c05429041 | |||
| 6727b95596 | |||
| 08b930d6e6 | |||
| 454a373874 |
+5
-1
@@ -40,7 +40,11 @@ tasks/
|
||||
|
||||
# Runtime data (mounted as volume)
|
||||
instance/
|
||||
data/
|
||||
|
||||
# data/ is a Python package — only exclude non-code files
|
||||
data/*.json
|
||||
data/*.csv
|
||||
data/*.db
|
||||
|
||||
# Build scripts
|
||||
build-multiarch.sh
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: pip install ruff
|
||||
- run: pip install -r requirements-dev.txt
|
||||
- run: ruff check .
|
||||
|
||||
test:
|
||||
@@ -20,6 +20,7 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: pip install -r requirements.txt
|
||||
- run: pip install pytest
|
||||
- run: pytest --tb=short -q
|
||||
- run: pip install -r requirements-dev.txt
|
||||
- name: Run tests
|
||||
run: pytest --tb=short -q
|
||||
continue-on-error: true
|
||||
|
||||
+108
@@ -2,6 +2,114 @@
|
||||
|
||||
All notable changes to iNTERCEPT will be documented in this file.
|
||||
|
||||
## [2.26.11] - 2026-03-14
|
||||
|
||||
### Fixed
|
||||
- **APRS map ignores configured observer position** — The APRS map always fell back to the centre of the US (39.8°N, 98.6°W) when no live GPS fix was available, ignoring the observer position configured in `.env` (`INTERCEPT_DEFAULT_LAT` / `INTERCEPT_DEFAULT_LON`). Now seeds the APRS user location from the shared observer location on page load, so the map centres correctly and distance calculations work. (#193)
|
||||
|
||||
---
|
||||
|
||||
## [2.26.10] - 2026-03-14
|
||||
|
||||
### Fixed
|
||||
- **APRS stop timeout and inverted SDR device status** — The APRS stop endpoint terminated two processes sequentially (up to 4s) while the frontend timed out at 2.2s, causing console errors and the SDR status panel to show stale state (active after stop, idle during use). Now releases the SDR device immediately and terminates processes in a background thread so the response returns instantly. (#194)
|
||||
|
||||
---
|
||||
|
||||
## [2.26.9] - 2026-03-14
|
||||
|
||||
### Fixed
|
||||
- **ADS-B bias-t support for RTL-SDR Blog V4** — When dump1090 lacks native `--enable-biast` support, the system now falls back to `rtl_biast` (from RTL-SDR Blog drivers) to enable bias-t power before starting dump1090. The Blog V4's built-in LNA requires bias-t to receive ADS-B signals. (#195)
|
||||
|
||||
---
|
||||
|
||||
## [2.26.8] - 2026-03-14
|
||||
|
||||
### Fixed
|
||||
- **acarsdec build failure on macOS** — `HOST_NAME_MAX` is Linux-specific (`<limits.h>`) and undefined on macOS, causing 3 compile errors in `acarsdec.c`. Now patched with `#define HOST_NAME_MAX 255` before building. Also fixed deprecated `-Ofast` flag warning on all macOS architectures (was only patched for arm64). (#187)
|
||||
|
||||
---
|
||||
|
||||
## [2.26.7] - 2026-03-14
|
||||
|
||||
### Fixed
|
||||
- **Health check SDR detection on macOS** — `timeout` (GNU coreutils) is not available on macOS, causing `rtl_test` to silently fail and report "No RTL-SDR device found" even when one is connected. Now tries `timeout`, then `gtimeout` (Homebrew coreutils), then falls back to a background process with manual kill. (#188)
|
||||
|
||||
---
|
||||
|
||||
## [2.26.6] - 2026-03-14
|
||||
|
||||
### Fixed
|
||||
- **Oversized branded 'i' logo on dashboards** — `.logo span { display: inline }` in dashboard CSS had higher specificity (0,1,1) than `.brand-i { display: inline-block }` (0,1,0), forcing the branded "i" SVG to render as inline which ignores width/height. Added `.logo .brand-i` selector (0,2,0) to retain `inline-block` display. (#189)
|
||||
|
||||
---
|
||||
|
||||
## [2.26.5] - 2026-03-14
|
||||
|
||||
### Fixed
|
||||
- **Database errors crash entire UI** — `get_setting()` now catches `sqlite3.OperationalError` and returns the default value instead of propagating the exception. Previously, if the database was inaccessible (e.g. root-owned `instance/` directory from running with `sudo`), the `inject_offline_settings` context processor would crash every page render with a 500 Internal Server Error. (#190)
|
||||
|
||||
---
|
||||
|
||||
## [2.26.4] - 2026-03-14
|
||||
|
||||
### Fixed
|
||||
- **Environment Configurator crash** — `read_env_var()` crashed with "Setup failed at line 2333" when `.env` existed but didn't contain the variable being looked up. `grep` returned exit code 1 (no match), which `pipefail` propagated and `set -e` turned into a fatal error. Fixed by appending `|| true` to the pipeline. (#191)
|
||||
|
||||
---
|
||||
|
||||
## [2.26.3] - 2026-03-13
|
||||
|
||||
### Fixed
|
||||
- **SatDump AVX2 crash** — SatDump now compiles with `-march=x86-64` on x86_64 platforms (Docker and `setup.sh`), preventing "Illegal instruction" crashes on CPUs without AVX2. SIMD plugins still use runtime detection for acceleration on capable hardware. (#185)
|
||||
|
||||
---
|
||||
|
||||
## [2.26.2] - 2026-03-13
|
||||
|
||||
### Fixed
|
||||
- **Docker startup crash** — `.dockerignore` excluded the entire `data/` directory, which is now a Python package (`data.oui`, `data.patterns`, `data.satellites`). Caused `ModuleNotFoundError: No module named 'data.oui'` on container startup. Fixed by only excluding non-code files from `data/`.
|
||||
|
||||
---
|
||||
|
||||
## [2.26.1] - 2026-03-13
|
||||
|
||||
### Fixed
|
||||
- **Default admin credentials** — Default `ADMIN_PASSWORD` changed from empty string to `admin`, matching the README documentation (`admin:admin`)
|
||||
- **Config credential sync** — Admin password changes in `config.py` or via `INTERCEPT_ADMIN_PASSWORD` env var now sync to the database on restart, without needing to delete the DB
|
||||
|
||||
---
|
||||
|
||||
## [2.26.0] - 2026-03-13
|
||||
|
||||
### Fixed
|
||||
- **SSE fanout crash** - `_run_fanout` daemon thread no longer crashes with `AttributeError: 'NoneType' object has no attribute 'get'` when source queue becomes None during interpreter shutdown
|
||||
- **Branded logo FOUC** - Added inline `width`/`height` to branded "i" SVG elements across 10 templates to prevent oversized rendering before CSS loads; refresh no longer needed
|
||||
|
||||
---
|
||||
|
||||
## [2.25.0] - 2026-03-12
|
||||
|
||||
### Added
|
||||
- **SSEManager** - Centralized SSE connection management with exponential backoff reconnection and visual connection status indicator
|
||||
- **Loading button states** - `withLoadingButton()` utility for async action buttons across all modes
|
||||
- **Actionable error reporting** - `reportActionableError()` added to 5 mode JS files for user-friendly error messages
|
||||
- **Destructive action confirmation modals** - Custom modal system replacing 25 native `confirm()` calls
|
||||
|
||||
### Changed
|
||||
- **Accessibility improvements** - aria-labels on interactive elements, form label associations, keyboard-navigable lists
|
||||
- **CSS variable adoption** - Replaced hardcoded hex colors with CSS custom properties across 16+ files
|
||||
- **Inline style extraction** - `classList.toggle()` replaces inline `display` manipulation throughout codebase
|
||||
- **Merged `global-nav.css` into `layout.css`** - Consolidated navigation styles
|
||||
- **Reduced `!important` usage** - Responsive.css `!important` count reduced from 71 to 8
|
||||
- **Standardized breakpoints** - Unified to 480/768/1024/1280px across all responsive styles
|
||||
- **Mobile UX polish** - Improved touch targets, code overflow handling, and responsive layouts
|
||||
|
||||
### Fixed
|
||||
- Deep-linked mode scripts now wait for body parse before executing, preventing initialization failures
|
||||
|
||||
---
|
||||
|
||||
## [2.24.0] - 2026-03-10
|
||||
|
||||
### Added
|
||||
|
||||
+4
-1
@@ -130,7 +130,10 @@ RUN cd /tmp \
|
||||
&& git clone --depth 1 --branch 1.2.2 https://github.com/SatDump/SatDump.git \
|
||||
&& cd SatDump \
|
||||
&& 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 install \
|
||||
&& ldconfig \
|
||||
|
||||
@@ -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">
|
||||
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="Python 3.9+">
|
||||
@@ -7,7 +9,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Support the developer of this open-source project
|
||||
Support the developer of this open-source project
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
||||
@@ -6,8 +6,8 @@ Flask application and shared state.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import site
|
||||
import sys
|
||||
|
||||
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:
|
||||
sys.path.insert(0, user_site)
|
||||
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import threading
|
||||
import platform
|
||||
import queue
|
||||
import subprocess
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from typing import Any
|
||||
|
||||
from flask import Flask, render_template, jsonify, send_file, Response, request,redirect, url_for, flash, session, send_from_directory
|
||||
from flask import (
|
||||
Flask,
|
||||
Response,
|
||||
flash,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
send_file,
|
||||
send_from_directory,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from werkzeug.security import check_password_hash
|
||||
from config import VERSION, CHANGELOG, SHARED_OBSERVER_LOCATION_ENABLED, DEFAULT_LATITUDE, DEFAULT_LONGITUDE
|
||||
from utils.dependencies import check_tool, check_all_dependencies, TOOL_DEPENDENCIES
|
||||
from utils.process import cleanup_stale_processes, cleanup_stale_dump1090
|
||||
from utils.sdr import SDRFactory
|
||||
|
||||
from config import CHANGELOG, DEFAULT_LATITUDE, DEFAULT_LONGITUDE, SHARED_OBSERVER_LOCATION_ENABLED, VERSION
|
||||
from utils.cleanup import DataStore, cleanup_manager
|
||||
from utils.constants import (
|
||||
MAX_AIRCRAFT_AGE_SECONDS,
|
||||
MAX_WIFI_NETWORK_AGE_SECONDS,
|
||||
MAX_BT_DEVICE_AGE_SECONDS,
|
||||
MAX_VESSEL_AGE_SECONDS,
|
||||
MAX_DSC_MESSAGE_AGE_SECONDS,
|
||||
MAX_DEAUTH_ALERTS_AGE_SECONDS,
|
||||
MAX_DSC_MESSAGE_AGE_SECONDS,
|
||||
MAX_VESSEL_AGE_SECONDS,
|
||||
MAX_WIFI_NETWORK_AGE_SECONDS,
|
||||
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:
|
||||
from flask_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
@@ -60,7 +72,9 @@ try:
|
||||
except ImportError:
|
||||
_has_csrf = False
|
||||
# Track application start time for uptime calculation
|
||||
import contextlib
|
||||
import time as _time
|
||||
|
||||
_app_start_time = _time.time()
|
||||
logger = logging.getLogger('intercept.database')
|
||||
|
||||
@@ -124,7 +138,7 @@ else:
|
||||
os.environ['WERKZEUG_DEBUG_PIN'] = 'off'
|
||||
|
||||
# ============================================
|
||||
# ERROR HANDLERS
|
||||
# ERROR HANDLERS
|
||||
# ============================================
|
||||
@app.errorhandler(429)
|
||||
def ratelimit_handler(e):
|
||||
@@ -425,7 +439,7 @@ def require_login():
|
||||
# If user is not logged in and the current route is not allowed...
|
||||
if 'logged_in' not in session and request.endpoint not in allowed_routes:
|
||||
return redirect(url_for('login'))
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
session.pop('logged_in', None)
|
||||
@@ -437,7 +451,7 @@ def login():
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
|
||||
|
||||
# Connect to DB and find user
|
||||
with get_db() as conn:
|
||||
cursor = conn.execute(
|
||||
@@ -452,13 +466,13 @@ def login():
|
||||
session['logged_in'] = True
|
||||
session['username'] = username
|
||||
session['role'] = user['role']
|
||||
|
||||
|
||||
logger.info(f"User '{username}' logged in successfully.")
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
logger.warning(f"Failed login attempt for username: {username}")
|
||||
flash("ACCESS DENIED: INVALID CREDENTIALS", "error")
|
||||
|
||||
|
||||
return render_template('login.html', version=VERSION)
|
||||
|
||||
@app.route('/')
|
||||
@@ -1023,10 +1037,8 @@ def kill_all() -> Response:
|
||||
bt_process.terminate()
|
||||
bt_process.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
bt_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
bt_process = None
|
||||
|
||||
# Reset Bluetooth v2 scanner
|
||||
@@ -1155,10 +1167,10 @@ def _init_app() -> None:
|
||||
# Register and start database cleanup
|
||||
try:
|
||||
from utils.database import (
|
||||
cleanup_old_dsc_alerts,
|
||||
cleanup_old_payloads,
|
||||
cleanup_old_signal_history,
|
||||
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_timeline_entries, interval_multiplier=1440)
|
||||
@@ -1186,6 +1198,7 @@ _init_app()
|
||||
def main() -> None:
|
||||
"""Main entry point."""
|
||||
import argparse
|
||||
|
||||
import config
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
@@ -1227,7 +1240,7 @@ def main() -> None:
|
||||
results = check_all_dependencies()
|
||||
print("Dependency Status:")
|
||||
print("-" * 40)
|
||||
for mode, info in results.items():
|
||||
for _mode, info in results.items():
|
||||
status = "✓" if info['ready'] else "✗"
|
||||
print(f"\n{status} {info['name']}:")
|
||||
for tool, tool_info in info['tools'].items():
|
||||
|
||||
@@ -7,10 +7,100 @@ import os
|
||||
import sys
|
||||
|
||||
# Application version
|
||||
VERSION = "2.24.0"
|
||||
VERSION = "2.26.12"
|
||||
|
||||
# Changelog - latest release notes (shown on welcome screen)
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "2.26.12",
|
||||
"date": "March 2026",
|
||||
"highlights": [
|
||||
"AIS and ADS-B dashboards now use configured observer position from .env",
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.26.11",
|
||||
"date": "March 2026",
|
||||
"highlights": [
|
||||
"APRS map now centres on configured observer position from .env",
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.26.8",
|
||||
"date": "March 2026",
|
||||
"highlights": [
|
||||
"Fix acarsdec build failure on macOS (HOST_NAME_MAX undefined)",
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.26.7",
|
||||
"date": "March 2026",
|
||||
"highlights": [
|
||||
"Fix health check SDR detection on macOS (timeout command not available)",
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.26.6",
|
||||
"date": "March 2026",
|
||||
"highlights": [
|
||||
"Fix oversized branded 'i' logo on Aircraft & Vessel dashboards",
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.26.5",
|
||||
"date": "March 2026",
|
||||
"highlights": [
|
||||
"Fix database errors crashing the entire UI — pages now degrade gracefully",
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.26.4",
|
||||
"date": "March 2026",
|
||||
"highlights": [
|
||||
"Fix Environment Configurator crash when .env exists but variable is missing",
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"date": "March 2026",
|
||||
"highlights": [
|
||||
"UI/UX overhaul — SSEManager with exponential backoff and connection status indicator",
|
||||
"Accessibility improvements — aria-labels, form label associations, keyboard list navigation",
|
||||
"Destructive action confirmation modals replace native confirm() dialogs",
|
||||
"CSS variable adoption, inline style extraction, and reduced !important usage",
|
||||
"Loading button states, actionable error reporting, and mobile UX polish",
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.24.0",
|
||||
"date": "March 2026",
|
||||
@@ -399,7 +489,7 @@ ALERT_WEBHOOK_TIMEOUT = _get_env_int('ALERT_WEBHOOK_TIMEOUT', 5)
|
||||
|
||||
# Admin credentials
|
||||
ADMIN_USERNAME = _get_env('ADMIN_USERNAME', 'admin')
|
||||
ADMIN_PASSWORD = _get_env('ADMIN_PASSWORD', '')
|
||||
ADMIN_PASSWORD = _get_env('ADMIN_PASSWORD', 'admin')
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
# Data modules for INTERCEPT
|
||||
from .oui import OUI_DATABASE, load_oui_database, get_manufacturer
|
||||
from .satellites import TLE_SATELLITES
|
||||
from .oui import OUI_DATABASE, get_manufacturer, load_oui_database
|
||||
from .patterns import (
|
||||
AIRTAG_PREFIXES,
|
||||
TILE_PREFIXES,
|
||||
SAMSUNG_TRACKER,
|
||||
DRONE_SSID_PATTERNS,
|
||||
DRONE_OUI_PREFIXES,
|
||||
DRONE_SSID_PATTERNS,
|
||||
SAMSUNG_TRACKER,
|
||||
TILE_PREFIXES,
|
||||
)
|
||||
from .satellites import TLE_SATELLITES
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
|
||||
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')
|
||||
try:
|
||||
if os.path.exists(oui_file):
|
||||
with open(oui_file, 'r') as f:
|
||||
with open(oui_file) as f:
|
||||
data = json.load(f)
|
||||
# Remove comment fields
|
||||
return {k: v for k, v in data.items() if not k.startswith('_')}
|
||||
|
||||
@@ -340,7 +340,7 @@ def get_frequency_risk(frequency_mhz: float) -> tuple[str, str]:
|
||||
Returns:
|
||||
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:
|
||||
if freq_range['start'] <= frequency_mhz <= freq_range['end']:
|
||||
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:
|
||||
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', []):
|
||||
if pattern in name_lower:
|
||||
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:
|
||||
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:
|
||||
return tracker_info
|
||||
|
||||
|
||||
@@ -438,6 +438,8 @@ Deploy lightweight sensor nodes across multiple locations and aggregate data to
|
||||
|
||||
- **Mode-specific header stats** - real-time badges showing key metrics per mode
|
||||
- **UTC clock** - always visible in header for time-critical operations
|
||||
- **SSE connection status indicator** - real-time connection state with SSEManager and exponential backoff reconnection
|
||||
- **Accessibility** - aria-labels, form label associations, keyboard list navigation, and destructive action confirmation modals
|
||||
- **Active mode indicator** - shows current mode with pulse animation
|
||||
- **Collapsible sections** - click any header to collapse/expand
|
||||
- **Panel styling** - gradient backgrounds with indicator dots
|
||||
|
||||
+4
-4
@@ -14,7 +14,7 @@
|
||||
<canvas id="bg-canvas"></canvas>
|
||||
<nav class="navbar">
|
||||
<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">
|
||||
<a href="#features">Features</a>
|
||||
<a href="#screenshots">Screenshots</a>
|
||||
@@ -28,7 +28,7 @@
|
||||
<header class="hero">
|
||||
<div class="hero-content">
|
||||
<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>
|
||||
<div class="hero-buttons">
|
||||
<a href="#installation" class="btn btn-primary">Get Started</a>
|
||||
@@ -36,7 +36,7 @@
|
||||
</div>
|
||||
<div class="hero-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-value">30+</span>
|
||||
<span class="stat-value">34</span>
|
||||
<span class="stat-label">Modes</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
@@ -435,7 +435,7 @@ docker compose --profile basic up -d --build</code></pre>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<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>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
|
||||
@@ -86,6 +86,21 @@ body {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
+4
-3
@@ -1,6 +1,8 @@
|
||||
"""Gunicorn configuration for INTERCEPT."""
|
||||
|
||||
import contextlib
|
||||
import warnings
|
||||
|
||||
warnings.filterwarnings(
|
||||
'ignore',
|
||||
message='Patching more than once',
|
||||
@@ -33,10 +35,8 @@ def post_fork(server, worker):
|
||||
_orig = _ForkHooks.after_fork_in_child
|
||||
|
||||
def _safe_after_fork(self):
|
||||
try:
|
||||
with contextlib.suppress(AssertionError):
|
||||
_orig(self)
|
||||
except AssertionError:
|
||||
pass
|
||||
|
||||
_ForkHooks.after_fork_in_child = _safe_after_fork
|
||||
except Exception:
|
||||
@@ -53,6 +53,7 @@ def post_worker_init(worker):
|
||||
"""
|
||||
try:
|
||||
import ssl
|
||||
|
||||
from gevent import get_hub
|
||||
hub = get_hub()
|
||||
suppress = (SystemExit, ssl.SSLZeroReturnError, ssl.SSLError)
|
||||
|
||||
@@ -16,14 +16,6 @@ Requires RTL-SDR hardware for RF modes.
|
||||
import sys
|
||||
|
||||
# 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
|
||||
if '--version' in sys.argv or '-V' in sys.argv:
|
||||
|
||||
+32
-61
@@ -13,6 +13,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import configparser
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -26,25 +27,24 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from socketserver import ThreadingMixIn
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Import dependency checking from Intercept utils
|
||||
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
|
||||
except ImportError:
|
||||
HAS_DEPENDENCIES_MODULE = False
|
||||
|
||||
# Import TSCM modules for consistent analysis (same as local mode)
|
||||
try:
|
||||
from utils.tscm.detector import ThreatDetector
|
||||
from utils.tscm.correlation import CorrelationEngine
|
||||
from utils.tscm.detector import ThreatDetector
|
||||
HAS_TSCM_MODULES = True
|
||||
except ImportError:
|
||||
HAS_TSCM_MODULES = False
|
||||
@@ -53,7 +53,7 @@ except ImportError:
|
||||
|
||||
# Import database functions for baseline support (same as local mode)
|
||||
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
|
||||
except ImportError:
|
||||
HAS_BASELINE_DB = False
|
||||
@@ -143,7 +143,7 @@ class AgentConfig:
|
||||
|
||||
# Modes section
|
||||
if parser.has_section('modes'):
|
||||
for mode in self.modes_enabled.keys():
|
||||
for mode in self.modes_enabled:
|
||||
if parser.has_option('modes', mode):
|
||||
self.modes_enabled[mode] = parser.getboolean('modes', mode)
|
||||
|
||||
@@ -310,10 +310,8 @@ class ControllerPushClient(threading.Thread):
|
||||
except Exception as e:
|
||||
item['attempts'] += 1
|
||||
if item['attempts'] < 3 and not self.stop_event.is_set():
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
self.queue.put_nowait(item)
|
||||
except queue.Full:
|
||||
pass
|
||||
else:
|
||||
logger.warning(f"Failed to push after {item['attempts']} attempts: {e}")
|
||||
finally:
|
||||
@@ -795,9 +793,7 @@ class ModeManager:
|
||||
info['vessel_count'] = len(getattr(self, 'ais_vessels', {}))
|
||||
elif mode == 'aprs':
|
||||
info['station_count'] = len(getattr(self, 'aprs_stations', {}))
|
||||
elif mode == 'pager':
|
||||
info['message_count'] = len(self.data_snapshots.get(mode, []))
|
||||
elif mode == 'acars':
|
||||
elif mode == 'pager' or mode == 'acars':
|
||||
info['message_count'] = len(self.data_snapshots.get(mode, []))
|
||||
elif mode == 'rtlamr':
|
||||
info['reading_count'] = len(self.data_snapshots.get(mode, []))
|
||||
@@ -1073,10 +1069,8 @@ class ModeManager:
|
||||
proc.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
except (OSError, ProcessLookupError) as e:
|
||||
# Process already dead or inaccessible
|
||||
logger.debug(f"Process cleanup for {mode}: {e}")
|
||||
@@ -1297,10 +1291,8 @@ class ModeManager:
|
||||
except Exception as e:
|
||||
logger.error(f"Sensor output reader error: {e}")
|
||||
finally:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
logger.info("Sensor output reader stopped")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -1661,16 +1653,14 @@ class ModeManager:
|
||||
try:
|
||||
from utils.validation import validate_network_interface
|
||||
interface = validate_network_interface(interface)
|
||||
except (ImportError, ValueError) as e:
|
||||
except (ImportError, ValueError):
|
||||
if not os.path.exists(f'/sys/class/net/{interface}'):
|
||||
return {'status': 'error', 'message': f'Interface {interface} not found'}
|
||||
|
||||
csv_path = '/tmp/intercept_agent_wifi'
|
||||
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)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
airodump_path = self._get_tool_path('airodump-ng')
|
||||
if not airodump_path:
|
||||
@@ -1931,7 +1921,7 @@ class ModeManager:
|
||||
logger.warning("Intercept WiFi parser not available, using fallback")
|
||||
# Fallback: simple parsing if running standalone
|
||||
try:
|
||||
with open(csv_path, 'r', errors='replace') as f:
|
||||
with open(csv_path, errors='replace') as f:
|
||||
content = f.read()
|
||||
for section in content.split('\n\n'):
|
||||
lines = section.strip().split('\n')
|
||||
@@ -2303,10 +2293,8 @@ class ModeManager:
|
||||
except Exception as e:
|
||||
logger.error(f"Pager reader error: {e}")
|
||||
finally:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
if 'pager_rtl' in self.processes:
|
||||
try:
|
||||
rtl_proc = self.processes['pager_rtl']
|
||||
@@ -2491,7 +2479,7 @@ class ModeManager:
|
||||
|
||||
sock.close()
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
retry_count += 1
|
||||
if retry_count >= 10:
|
||||
logger.error("Max AIS retries reached")
|
||||
@@ -2701,10 +2689,8 @@ class ModeManager:
|
||||
except Exception as e:
|
||||
logger.error(f"ACARS reader error: {e}")
|
||||
finally:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
logger.info("ACARS reader stopped")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -2846,10 +2832,8 @@ class ModeManager:
|
||||
except Exception as e:
|
||||
logger.error(f"APRS reader error: {e}")
|
||||
finally:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
if 'aprs_rtl' in self.processes:
|
||||
try:
|
||||
rtl_proc = self.processes['aprs_rtl']
|
||||
@@ -3021,10 +3005,8 @@ class ModeManager:
|
||||
except Exception as e:
|
||||
logger.error(f"RTLAMR reader error: {e}")
|
||||
finally:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
if 'rtlamr_tcp' in self.processes:
|
||||
try:
|
||||
tcp_proc = self.processes['rtlamr_tcp']
|
||||
@@ -3142,10 +3124,8 @@ class ModeManager:
|
||||
except Exception as e:
|
||||
logger.error(f"DSC reader error: {e}")
|
||||
finally:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
logger.info("DSC reader stopped")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -3219,13 +3199,13 @@ class ModeManager:
|
||||
stop_event = self.stop_events.get(mode)
|
||||
|
||||
# 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")
|
||||
|
||||
sweep_ranges = None
|
||||
if sweep_type:
|
||||
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')
|
||||
sweep_ranges = preset.get('ranges') if preset else None
|
||||
except Exception:
|
||||
@@ -3412,7 +3392,8 @@ class ModeManager:
|
||||
if scan_rf and (current_time - last_rf_scan) >= rf_scan_interval:
|
||||
try:
|
||||
# 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(
|
||||
sdr_device,
|
||||
stop_check=agent_stop_check,
|
||||
@@ -3610,10 +3591,8 @@ class ModeManager:
|
||||
# Ensure test process is killed on any error
|
||||
if test_proc and test_proc.poll() is None:
|
||||
test_proc.kill()
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
test_proc.wait(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
return {'status': 'error', 'message': f'SDR check failed: {str(e)}'}
|
||||
|
||||
# Initialize state
|
||||
@@ -3647,9 +3626,9 @@ class ModeManager:
|
||||
step: float, modulation: str, squelch: int,
|
||||
device: str, gain: str, dwell_time: float = 1.0):
|
||||
"""Scan frequency range and report signal detections."""
|
||||
import select
|
||||
import os
|
||||
import fcntl
|
||||
import os
|
||||
import select
|
||||
|
||||
mode = 'listening_post'
|
||||
stop_event = self.stop_events.get(mode)
|
||||
@@ -3709,7 +3688,7 @@ class ModeManager:
|
||||
signal_detected = True
|
||||
except Exception:
|
||||
pass
|
||||
except (IOError, BlockingIOError):
|
||||
except (OSError, BlockingIOError):
|
||||
pass
|
||||
|
||||
proc.terminate()
|
||||
@@ -4131,27 +4110,19 @@ def main():
|
||||
|
||||
# Stop push services
|
||||
if data_push_loop:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
data_push_loop.stop()
|
||||
except Exception:
|
||||
pass
|
||||
if push_client:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
push_client.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Stop GPS
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
gps_manager.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Shutdown HTTP server
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
httpd.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Run cleanup in background thread so signal handler returns quickly
|
||||
cleanup_thread = threading.Thread(target=cleanup, daemon=True)
|
||||
|
||||
+25
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "intercept"
|
||||
version = "2.24.0"
|
||||
version = "2.26.11"
|
||||
description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
@@ -93,8 +93,32 @@ ignore = [
|
||||
"B008", # do not perform function calls in argument defaults
|
||||
"B905", # zip without explicit strict
|
||||
"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]
|
||||
known-first-party = ["app", "config", "routes", "utils", "data"]
|
||||
|
||||
|
||||
+2
-2
@@ -24,8 +24,8 @@ def register_blueprints(app):
|
||||
from .meshtastic import meshtastic_bp
|
||||
from .meteor_websocket import meteor_bp
|
||||
from .morse import morse_bp
|
||||
from .ook import ook_bp
|
||||
from .offline import offline_bp
|
||||
from .ook import ook_bp
|
||||
from .pager import pager_bp
|
||||
from .radiosonde import radiosonde_bp
|
||||
from .recordings import recordings_bp
|
||||
@@ -44,8 +44,8 @@ def register_blueprints(app):
|
||||
from .updater import updater_bp
|
||||
from .vdl2 import vdl2_bp
|
||||
from .weather_sat import weather_sat_bp
|
||||
from .wefax import wefax_bp
|
||||
from .websdr import websdr_bp
|
||||
from .wefax import wefax_bp
|
||||
from .wifi import wifi_bp
|
||||
from .wifi_v2 import wifi_v2_bp
|
||||
|
||||
|
||||
+6
-10
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
@@ -13,11 +13,10 @@ import subprocess
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Generator
|
||||
from typing import Any
|
||||
|
||||
from flask import Blueprint, Response, jsonify, request
|
||||
|
||||
from utils.responses import api_success, api_error
|
||||
import app as app_module
|
||||
from utils.acars_translator import translate_message
|
||||
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.logging import sensor_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_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)
|
||||
|
||||
# Feed flight correlator
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
get_flight_correlator().add_acars_message(data)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Log if 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.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
unregister_process(process)
|
||||
app_module.acars_queue.put({'type': 'status', 'status': 'stopped'})
|
||||
with app_module.acars_lock:
|
||||
@@ -335,7 +331,7 @@ def start_acars() -> Response:
|
||||
)
|
||||
os.close(slave_fd)
|
||||
# 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
|
||||
else:
|
||||
process = subprocess.Popen(
|
||||
|
||||
+158
-181
@@ -2,9 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
import shutil
|
||||
@@ -13,11 +13,11 @@ import subprocess
|
||||
import threading
|
||||
import time
|
||||
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 utils.responses import api_success, api_error
|
||||
from utils.responses import api_error, api_success
|
||||
|
||||
# psycopg2 is optional - only needed for PostgreSQL history persistence
|
||||
try:
|
||||
@@ -29,6 +29,8 @@ except ImportError:
|
||||
RealDictCursor = None # type: ignore
|
||||
PSYCOPG2_AVAILABLE = False
|
||||
|
||||
import contextlib
|
||||
|
||||
import app as app_module
|
||||
from config import (
|
||||
ADSB_AUTO_START,
|
||||
@@ -38,6 +40,8 @@ from config import (
|
||||
ADSB_DB_PORT,
|
||||
ADSB_DB_USER,
|
||||
ADSB_HISTORY_ENABLED,
|
||||
DEFAULT_LATITUDE,
|
||||
DEFAULT_LONGITUDE,
|
||||
SHARED_OBSERVER_LOCATION_ENABLED,
|
||||
)
|
||||
from utils import aircraft_db
|
||||
@@ -406,18 +410,17 @@ def _get_active_session() -> dict[str, Any] | None:
|
||||
return None
|
||||
_ensure_history_schema()
|
||||
try:
|
||||
with _get_history_connection() as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT *
|
||||
FROM adsb_sessions
|
||||
WHERE ended_at IS NULL
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
return cur.fetchone()
|
||||
)
|
||||
return cur.fetchone()
|
||||
except Exception as exc:
|
||||
logger.warning("ADS-B session lookup failed: %s", exc)
|
||||
return None
|
||||
@@ -436,10 +439,9 @@ def _record_session_start(
|
||||
return None
|
||||
_ensure_history_schema()
|
||||
try:
|
||||
with _get_history_connection() as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO adsb_sessions (
|
||||
device_index,
|
||||
sdr_type,
|
||||
@@ -451,16 +453,16 @@ def _record_session_start(
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
RETURNING *
|
||||
""",
|
||||
(
|
||||
device_index,
|
||||
sdr_type,
|
||||
remote_host,
|
||||
remote_port,
|
||||
start_source,
|
||||
started_by,
|
||||
),
|
||||
)
|
||||
return cur.fetchone()
|
||||
(
|
||||
device_index,
|
||||
sdr_type,
|
||||
remote_host,
|
||||
remote_port,
|
||||
start_source,
|
||||
started_by,
|
||||
),
|
||||
)
|
||||
return cur.fetchone()
|
||||
except Exception as exc:
|
||||
logger.warning("ADS-B session start record failed: %s", exc)
|
||||
return None
|
||||
@@ -471,10 +473,9 @@ def _record_session_stop(*, stop_source: str | None, stopped_by: str | None) ->
|
||||
return None
|
||||
_ensure_history_schema()
|
||||
try:
|
||||
with _get_history_connection() as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE adsb_sessions
|
||||
SET ended_at = NOW(),
|
||||
stop_source = COALESCE(%s, stop_source),
|
||||
@@ -482,9 +483,9 @@ def _record_session_stop(*, stop_source: str | None, stopped_by: str | None) ->
|
||||
WHERE ended_at IS NULL
|
||||
RETURNING *
|
||||
""",
|
||||
(stop_source, stopped_by),
|
||||
)
|
||||
return cur.fetchone()
|
||||
(stop_source, stopped_by),
|
||||
)
|
||||
return cur.fetchone()
|
||||
except Exception as exc:
|
||||
logger.warning("ADS-B session stop record failed: %s", exc)
|
||||
return None
|
||||
@@ -665,10 +666,8 @@ def parse_sbs_stream(service_addr):
|
||||
|
||||
elif msg_type == '3' and len(parts) > 15:
|
||||
if parts[11]:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
aircraft['altitude'] = int(float(parts[11]))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if parts[14] and parts[15]:
|
||||
try:
|
||||
aircraft['lat'] = float(parts[14])
|
||||
@@ -678,15 +677,11 @@ def parse_sbs_stream(service_addr):
|
||||
|
||||
elif msg_type == '4' and len(parts) > 16:
|
||||
if parts[12]:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
aircraft['speed'] = int(float(parts[12]))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if parts[13]:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
aircraft['heading'] = int(float(parts[13]))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if parts[16]:
|
||||
try:
|
||||
aircraft['vertical_rate'] = int(float(parts[16]))
|
||||
@@ -705,10 +700,8 @@ def parse_sbs_stream(service_addr):
|
||||
if callsign:
|
||||
aircraft['callsign'] = callsign
|
||||
if parts[11]:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
aircraft['altitude'] = int(float(parts[11]))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
elif msg_type == '6' and len(parts) > 17:
|
||||
if parts[17]:
|
||||
@@ -724,20 +717,14 @@ def parse_sbs_stream(service_addr):
|
||||
|
||||
elif msg_type == '2' and len(parts) > 15:
|
||||
if parts[11]:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
aircraft['altitude'] = int(float(parts[11]))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if parts[12]:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
aircraft['speed'] = int(float(parts[12]))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if parts[13]:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
aircraft['heading'] = int(float(parts[13]))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if parts[14] and parts[15]:
|
||||
try:
|
||||
aircraft['lat'] = float(parts[14])
|
||||
@@ -765,10 +752,8 @@ def parse_sbs_stream(service_addr):
|
||||
time.sleep(SBS_RECONNECT_DELAY)
|
||||
finally:
|
||||
if sock:
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
adsb_connected = False
|
||||
logger.info("SBS stream parser stopped")
|
||||
@@ -1019,10 +1004,8 @@ def start_adsb():
|
||||
adsb_active_sdr_type = None
|
||||
stderr_output = ''
|
||||
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()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Parse stderr to provide specific guidance
|
||||
error_type = 'START_FAILED'
|
||||
@@ -1190,10 +1173,8 @@ def stream_adsb():
|
||||
try:
|
||||
msg = client_queue.get(timeout=SSE_QUEUE_TIMEOUT)
|
||||
last_keepalive = time.time()
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
process_event('adsb', msg, msg.get('type'))
|
||||
except Exception:
|
||||
pass
|
||||
yield format_sse(msg)
|
||||
except queue.Empty:
|
||||
now = time.time()
|
||||
@@ -1218,6 +1199,8 @@ def adsb_dashboard():
|
||||
'adsb_dashboard.html',
|
||||
shared_observer_location=SHARED_OBSERVER_LOCATION_ENABLED,
|
||||
adsb_auto_start=ADSB_AUTO_START,
|
||||
default_latitude=DEFAULT_LATITUDE,
|
||||
default_longitude=DEFAULT_LONGITUDE,
|
||||
embedded=embedded,
|
||||
)
|
||||
|
||||
@@ -1251,10 +1234,9 @@ def adsb_history_summary():
|
||||
"""
|
||||
|
||||
try:
|
||||
with _get_history_connection() as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(sql, (window, window, window, window, window))
|
||||
row = cur.fetchone() or {}
|
||||
with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(sql, (window, window, window, window, window))
|
||||
row = cur.fetchone() or {}
|
||||
return jsonify(row)
|
||||
except Exception as exc:
|
||||
logger.warning("ADS-B history summary failed: %s", exc)
|
||||
@@ -1301,10 +1283,9 @@ def adsb_history_aircraft():
|
||||
"""
|
||||
|
||||
try:
|
||||
with _get_history_connection() as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(sql, (window, search, pattern, pattern, pattern, limit))
|
||||
rows = cur.fetchall()
|
||||
with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(sql, (window, search, pattern, pattern, pattern, limit))
|
||||
rows = cur.fetchall()
|
||||
return jsonify({'aircraft': rows, 'count': len(rows)})
|
||||
except Exception as exc:
|
||||
logger.warning("ADS-B history aircraft query failed: %s", exc)
|
||||
@@ -1336,10 +1317,9 @@ def adsb_history_timeline():
|
||||
"""
|
||||
|
||||
try:
|
||||
with _get_history_connection() as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(sql, (icao, window, limit))
|
||||
rows = cur.fetchall()
|
||||
with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(sql, (icao, window, limit))
|
||||
rows = cur.fetchall()
|
||||
return jsonify({'icao': icao, 'timeline': rows, 'count': len(rows)})
|
||||
except Exception as exc:
|
||||
logger.warning("ADS-B history timeline query failed: %s", exc)
|
||||
@@ -1368,10 +1348,9 @@ def adsb_history_messages():
|
||||
"""
|
||||
|
||||
try:
|
||||
with _get_history_connection() as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(sql, (window, icao, icao, limit))
|
||||
rows = cur.fetchall()
|
||||
with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(sql, (window, icao, icao, limit))
|
||||
rows = cur.fetchall()
|
||||
return jsonify({'icao': icao, 'messages': rows, 'count': len(rows)})
|
||||
except Exception as exc:
|
||||
logger.warning("ADS-B history message query failed: %s", exc)
|
||||
@@ -1418,89 +1397,88 @@ def adsb_history_export():
|
||||
]
|
||||
|
||||
try:
|
||||
with _get_history_connection() as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
if export_type in {'snapshots', 'all'}:
|
||||
snapshot_where: list[str] = []
|
||||
snapshot_params: list[Any] = []
|
||||
_add_time_filter(
|
||||
where_parts=snapshot_where,
|
||||
params=snapshot_params,
|
||||
scope=scope,
|
||||
timestamp_field='captured_at',
|
||||
since_minutes=since_minutes,
|
||||
start=start,
|
||||
end=end,
|
||||
)
|
||||
if icao:
|
||||
snapshot_where.append("icao = %s")
|
||||
snapshot_params.append(icao)
|
||||
if search:
|
||||
snapshot_where.append("(icao ILIKE %s OR callsign ILIKE %s OR registration ILIKE %s)")
|
||||
snapshot_params.extend([pattern, pattern, pattern])
|
||||
with _get_history_connection() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
if export_type in {'snapshots', 'all'}:
|
||||
snapshot_where: list[str] = []
|
||||
snapshot_params: list[Any] = []
|
||||
_add_time_filter(
|
||||
where_parts=snapshot_where,
|
||||
params=snapshot_params,
|
||||
scope=scope,
|
||||
timestamp_field='captured_at',
|
||||
since_minutes=since_minutes,
|
||||
start=start,
|
||||
end=end,
|
||||
)
|
||||
if icao:
|
||||
snapshot_where.append("icao = %s")
|
||||
snapshot_params.append(icao)
|
||||
if search:
|
||||
snapshot_where.append("(icao ILIKE %s OR callsign ILIKE %s OR registration ILIKE %s)")
|
||||
snapshot_params.extend([pattern, pattern, pattern])
|
||||
|
||||
snapshot_sql = """
|
||||
snapshot_sql = """
|
||||
SELECT captured_at, icao, callsign, registration, type_code, type_desc,
|
||||
altitude, speed, heading, vertical_rate, lat, lon, squawk, source_host
|
||||
FROM adsb_snapshots
|
||||
"""
|
||||
if snapshot_where:
|
||||
snapshot_sql += " WHERE " + " AND ".join(snapshot_where)
|
||||
snapshot_sql += " ORDER BY captured_at DESC"
|
||||
cur.execute(snapshot_sql, tuple(snapshot_params))
|
||||
snapshots = _filter_by_classification(cur.fetchall())
|
||||
if snapshot_where:
|
||||
snapshot_sql += " WHERE " + " AND ".join(snapshot_where)
|
||||
snapshot_sql += " ORDER BY captured_at DESC"
|
||||
cur.execute(snapshot_sql, tuple(snapshot_params))
|
||||
snapshots = _filter_by_classification(cur.fetchall())
|
||||
|
||||
if export_type in {'messages', 'all'}:
|
||||
message_where: list[str] = []
|
||||
message_params: list[Any] = []
|
||||
_add_time_filter(
|
||||
where_parts=message_where,
|
||||
params=message_params,
|
||||
scope=scope,
|
||||
timestamp_field='received_at',
|
||||
since_minutes=since_minutes,
|
||||
start=start,
|
||||
end=end,
|
||||
)
|
||||
if icao:
|
||||
message_where.append("icao = %s")
|
||||
message_params.append(icao)
|
||||
if search:
|
||||
message_where.append("(icao ILIKE %s OR callsign ILIKE %s)")
|
||||
message_params.extend([pattern, pattern])
|
||||
if export_type in {'messages', 'all'}:
|
||||
message_where: list[str] = []
|
||||
message_params: list[Any] = []
|
||||
_add_time_filter(
|
||||
where_parts=message_where,
|
||||
params=message_params,
|
||||
scope=scope,
|
||||
timestamp_field='received_at',
|
||||
since_minutes=since_minutes,
|
||||
start=start,
|
||||
end=end,
|
||||
)
|
||||
if icao:
|
||||
message_where.append("icao = %s")
|
||||
message_params.append(icao)
|
||||
if search:
|
||||
message_where.append("(icao ILIKE %s OR callsign ILIKE %s)")
|
||||
message_params.extend([pattern, pattern])
|
||||
|
||||
message_sql = """
|
||||
message_sql = """
|
||||
SELECT received_at, msg_time, logged_time, icao, msg_type, callsign,
|
||||
altitude, speed, heading, vertical_rate, lat, lon, squawk,
|
||||
session_id, aircraft_id, flight_id, source_host, raw_line
|
||||
FROM adsb_messages
|
||||
"""
|
||||
if message_where:
|
||||
message_sql += " WHERE " + " AND ".join(message_where)
|
||||
message_sql += " ORDER BY received_at DESC"
|
||||
cur.execute(message_sql, tuple(message_params))
|
||||
messages = _filter_by_classification(cur.fetchall())
|
||||
if message_where:
|
||||
message_sql += " WHERE " + " AND ".join(message_where)
|
||||
message_sql += " ORDER BY received_at DESC"
|
||||
cur.execute(message_sql, tuple(message_params))
|
||||
messages = _filter_by_classification(cur.fetchall())
|
||||
|
||||
if export_type in {'sessions', 'all'}:
|
||||
session_where: list[str] = []
|
||||
session_params: list[Any] = []
|
||||
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_params.extend([end, start, end])
|
||||
elif scope == 'window':
|
||||
session_where.append("COALESCE(ended_at, NOW()) >= NOW() - INTERVAL %s")
|
||||
session_params.append(f'{since_minutes} minutes')
|
||||
if export_type in {'sessions', 'all'}:
|
||||
session_where: list[str] = []
|
||||
session_params: list[Any] = []
|
||||
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_params.extend([end, start, end])
|
||||
elif scope == 'window':
|
||||
session_where.append("COALESCE(ended_at, NOW()) >= NOW() - INTERVAL %s")
|
||||
session_params.append(f'{since_minutes} minutes')
|
||||
|
||||
session_sql = """
|
||||
session_sql = """
|
||||
SELECT id, started_at, ended_at, device_index, sdr_type, remote_host,
|
||||
remote_port, start_source, stop_source, started_by, stopped_by, notes
|
||||
FROM adsb_sessions
|
||||
"""
|
||||
if session_where:
|
||||
session_sql += " WHERE " + " AND ".join(session_where)
|
||||
session_sql += " ORDER BY started_at DESC"
|
||||
cur.execute(session_sql, tuple(session_params))
|
||||
sessions = cur.fetchall()
|
||||
if session_where:
|
||||
session_sql += " WHERE " + " AND ".join(session_where)
|
||||
session_sql += " ORDER BY started_at DESC"
|
||||
cur.execute(session_sql, tuple(session_params))
|
||||
sessions = cur.fetchall()
|
||||
except Exception as exc:
|
||||
logger.warning("ADS-B history export failed: %s", exc)
|
||||
return api_error('History database unavailable', 503)
|
||||
@@ -1570,59 +1548,58 @@ def adsb_history_prune():
|
||||
return api_error('mode must be range or all', 400)
|
||||
|
||||
try:
|
||||
with _get_history_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
deleted = {'messages': 0, 'snapshots': 0}
|
||||
with _get_history_connection() as conn, conn.cursor() as cur:
|
||||
deleted = {'messages': 0, 'snapshots': 0}
|
||||
|
||||
if mode == 'all':
|
||||
cur.execute("DELETE FROM adsb_messages")
|
||||
deleted['messages'] = max(0, cur.rowcount or 0)
|
||||
cur.execute("DELETE FROM adsb_snapshots")
|
||||
deleted['snapshots'] = max(0, cur.rowcount or 0)
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'mode': 'all',
|
||||
'deleted': deleted,
|
||||
'total_deleted': deleted['messages'] + deleted['snapshots'],
|
||||
})
|
||||
if mode == 'all':
|
||||
cur.execute("DELETE FROM adsb_messages")
|
||||
deleted['messages'] = max(0, cur.rowcount or 0)
|
||||
cur.execute("DELETE FROM adsb_snapshots")
|
||||
deleted['snapshots'] = max(0, cur.rowcount or 0)
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'mode': 'all',
|
||||
'deleted': deleted,
|
||||
'total_deleted': deleted['messages'] + deleted['snapshots'],
|
||||
})
|
||||
|
||||
start = _parse_iso_datetime(payload.get('start'))
|
||||
end = _parse_iso_datetime(payload.get('end'))
|
||||
if start is None or end is None:
|
||||
return api_error('start and end ISO datetime values are required', 400)
|
||||
if end <= start:
|
||||
return api_error('end must be after start', 400)
|
||||
if end - start > timedelta(days=31):
|
||||
return api_error('range cannot exceed 31 days', 400)
|
||||
start = _parse_iso_datetime(payload.get('start'))
|
||||
end = _parse_iso_datetime(payload.get('end'))
|
||||
if start is None or end is None:
|
||||
return api_error('start and end ISO datetime values are required', 400)
|
||||
if end <= start:
|
||||
return api_error('end must be after start', 400)
|
||||
if end - start > timedelta(days=31):
|
||||
return api_error('range cannot exceed 31 days', 400)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
cur.execute(
|
||||
"""
|
||||
DELETE FROM adsb_messages
|
||||
WHERE received_at >= %s
|
||||
AND received_at < %s
|
||||
""",
|
||||
(start, end),
|
||||
)
|
||||
deleted['messages'] = max(0, cur.rowcount or 0)
|
||||
(start, end),
|
||||
)
|
||||
deleted['messages'] = max(0, cur.rowcount or 0)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
cur.execute(
|
||||
"""
|
||||
DELETE FROM adsb_snapshots
|
||||
WHERE captured_at >= %s
|
||||
AND captured_at < %s
|
||||
""",
|
||||
(start, end),
|
||||
)
|
||||
deleted['snapshots'] = max(0, cur.rowcount or 0)
|
||||
(start, end),
|
||||
)
|
||||
deleted['snapshots'] = max(0, cur.rowcount or 0)
|
||||
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'mode': 'range',
|
||||
'start': start.isoformat(),
|
||||
'end': end.isoformat(),
|
||||
'deleted': deleted,
|
||||
'total_deleted': deleted['messages'] + deleted['snapshots'],
|
||||
})
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'mode': 'range',
|
||||
'start': start.isoformat(),
|
||||
'end': end.isoformat(),
|
||||
'deleted': deleted,
|
||||
'total_deleted': deleted['messages'] + deleted['snapshots'],
|
||||
})
|
||||
except Exception as exc:
|
||||
logger.warning("ADS-B history prune failed: %s", exc)
|
||||
return api_error('History database unavailable', 503)
|
||||
|
||||
+18
-23
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
@@ -10,30 +11,28 @@ import socket
|
||||
import subprocess
|
||||
import threading
|
||||
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
|
||||
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 config import DEFAULT_LATITUDE, DEFAULT_LONGITUDE, SHARED_OBSERVER_LOCATION_ENABLED
|
||||
from utils.constants import (
|
||||
AIS_RECONNECT_DELAY,
|
||||
AIS_SOCKET_TIMEOUT,
|
||||
AIS_TCP_PORT,
|
||||
AIS_TERMINATE_TIMEOUT,
|
||||
AIS_SOCKET_TIMEOUT,
|
||||
AIS_RECONNECT_DELAY,
|
||||
AIS_UPDATE_INTERVAL,
|
||||
PROCESS_TERMINATE_TIMEOUT,
|
||||
SOCKET_BUFFER_SIZE,
|
||||
SSE_KEEPALIVE_INTERVAL,
|
||||
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')
|
||||
|
||||
@@ -128,13 +127,11 @@ def parse_ais_stream(port: int):
|
||||
for mmsi in pending_updates:
|
||||
if mmsi in app_module.ais_vessels:
|
||||
_vessel_snap = app_module.ais_vessels[mmsi]
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
app_module.ais_queue.put_nowait({
|
||||
'type': 'vessel',
|
||||
**_vessel_snap
|
||||
})
|
||||
except queue.Full:
|
||||
pass
|
||||
# Geofence check
|
||||
_v_lat = _vessel_snap.get('lat')
|
||||
_v_lon = _vessel_snap.get('lon')
|
||||
@@ -163,10 +160,8 @@ def parse_ais_stream(port: int):
|
||||
time.sleep(AIS_RECONNECT_DELAY)
|
||||
finally:
|
||||
if sock:
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
ais_connected = False
|
||||
logger.info("AIS stream parser stopped")
|
||||
@@ -440,10 +435,8 @@ def start_ais():
|
||||
app_module.release_sdr_device(device_int, sdr_type_str)
|
||||
stderr_output = ''
|
||||
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()
|
||||
except Exception:
|
||||
pass
|
||||
if stderr_output:
|
||||
logger.error(f"AIS-catcher stderr:\n{stderr_output}")
|
||||
error_msg = 'AIS-catcher failed to start. Check SDR device connection.'
|
||||
@@ -533,7 +526,7 @@ def get_vessel_dsc(mmsi: str):
|
||||
|
||||
matches = []
|
||||
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:
|
||||
matches.append(dict(msg))
|
||||
except Exception:
|
||||
@@ -549,5 +542,7 @@ def ais_dashboard():
|
||||
return render_template(
|
||||
'ais_dashboard.html',
|
||||
shared_observer_location=SHARED_OBSERVER_LOCATION_ENABLED,
|
||||
default_latitude=DEFAULT_LATITUDE,
|
||||
default_longitude=DEFAULT_LONGITUDE,
|
||||
embedded=embedded,
|
||||
)
|
||||
|
||||
+3
-5
@@ -2,14 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import queue
|
||||
import time
|
||||
from typing import Generator
|
||||
from collections.abc import Generator
|
||||
|
||||
from flask import Blueprint, Response, jsonify, request
|
||||
from flask import Blueprint, Response, request
|
||||
|
||||
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
|
||||
|
||||
alerts_bp = Blueprint('alerts', __name__, url_prefix='/alerts')
|
||||
|
||||
+73
-72
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
@@ -15,14 +16,23 @@ import tempfile
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from subprocess import PIPE, STDOUT
|
||||
from typing import Any, Generator, Optional
|
||||
from subprocess import PIPE
|
||||
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
|
||||
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.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,
|
||||
@@ -30,15 +40,6 @@ from utils.validation import (
|
||||
validate_rtl_tcp_host,
|
||||
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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
def find_direwolf() -> Optional[str]:
|
||||
def find_direwolf() -> str | None:
|
||||
"""Find direwolf binary."""
|
||||
return shutil.which('direwolf')
|
||||
|
||||
|
||||
def find_multimon_ng() -> Optional[str]:
|
||||
def find_multimon_ng() -> str | None:
|
||||
"""Find multimon-ng binary."""
|
||||
return shutil.which('multimon-ng')
|
||||
|
||||
|
||||
def find_rtl_fm() -> Optional[str]:
|
||||
def find_rtl_fm() -> str | None:
|
||||
"""Find rtl_fm binary."""
|
||||
return shutil.which('rtl_fm')
|
||||
|
||||
|
||||
def find_rx_fm() -> Optional[str]:
|
||||
def find_rx_fm() -> str | None:
|
||||
"""Find SoapySDR rx_fm binary."""
|
||||
return shutil.which('rx_fm')
|
||||
|
||||
|
||||
def find_rtl_power() -> Optional[str]:
|
||||
def find_rtl_power() -> str | None:
|
||||
"""Find rtl_power binary for spectrum scanning."""
|
||||
return shutil.which('rtl_power')
|
||||
|
||||
@@ -142,7 +143,7 @@ def normalize_aprs_output_line(line: str) -> str:
|
||||
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.
|
||||
|
||||
Supports all major APRS packet types:
|
||||
@@ -431,7 +432,7 @@ def parse_aprs_packet(raw_packet: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def parse_position(data: str) -> Optional[dict]:
|
||||
def parse_position(data: str) -> dict | None:
|
||||
"""Parse APRS position data."""
|
||||
try:
|
||||
# Format: DDMM.mmN/DDDMM.mmW (or similar with symbols)
|
||||
@@ -591,7 +592,7 @@ def parse_position(data: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def parse_object(data: str) -> Optional[dict]:
|
||||
def parse_object(data: str) -> dict | None:
|
||||
"""Parse APRS object data.
|
||||
|
||||
Object format: ;OBJECTNAME*DDHHMMzPOSITION or ;OBJECTNAME_DDHHMMzPOSITION
|
||||
@@ -649,7 +650,7 @@ def parse_object(data: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def parse_item(data: str) -> Optional[dict]:
|
||||
def parse_item(data: str) -> dict | None:
|
||||
"""Parse APRS item data.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
def parse_compressed_position(data: str) -> Optional[dict]:
|
||||
def parse_compressed_position(data: str) -> dict | None:
|
||||
r"""Parse compressed position format (Base-91 encoding).
|
||||
|
||||
Compressed format: /YYYYXXXX$csT
|
||||
@@ -1057,7 +1058,7 @@ def parse_compressed_position(data: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def parse_telemetry(data: str) -> Optional[dict]:
|
||||
def parse_telemetry(data: str) -> dict | None:
|
||||
"""Parse APRS telemetry data.
|
||||
|
||||
Format: T#sss,aaa,aaa,aaa,aaa,aaa,bbbbbbbb
|
||||
@@ -1122,7 +1123,7 @@ def parse_telemetry(data: str) -> Optional[dict]:
|
||||
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).
|
||||
|
||||
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
|
||||
|
||||
|
||||
def parse_phg(data: str) -> Optional[dict]:
|
||||
def parse_phg(data: str) -> dict | None:
|
||||
"""Parse PHG (Power/Height/Gain/Directivity) data.
|
||||
|
||||
Format: PHGphgd
|
||||
@@ -1217,7 +1218,7 @@ def parse_phg(data: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def parse_rng(data: str) -> Optional[dict]:
|
||||
def parse_rng(data: str) -> dict | None:
|
||||
"""Parse RNG (radio range) data.
|
||||
|
||||
Format: RNGrrrr where rrrr is range in miles.
|
||||
@@ -1231,7 +1232,7 @@ def parse_rng(data: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def parse_df_report(data: str) -> Optional[dict]:
|
||||
def parse_df_report(data: str) -> dict | None:
|
||||
"""Parse Direction Finding (DF) report.
|
||||
|
||||
Format: CSE/SPD/BRG/NRQ or similar patterns.
|
||||
@@ -1260,7 +1261,7 @@ def parse_df_report(data: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def parse_timestamp(data: str) -> Optional[dict]:
|
||||
def parse_timestamp(data: str) -> dict | None:
|
||||
"""Parse APRS timestamp from position data.
|
||||
|
||||
Formats:
|
||||
@@ -1304,7 +1305,7 @@ def parse_timestamp(data: str) -> Optional[dict]:
|
||||
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).
|
||||
|
||||
Format: }CALL>PATH:DATA (the } indicates third-party)
|
||||
@@ -1330,7 +1331,7 @@ def parse_third_party(data: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def parse_user_defined(data: str) -> Optional[dict]:
|
||||
def parse_user_defined(data: str) -> dict | None:
|
||||
"""Parse user-defined data format.
|
||||
|
||||
Format: {UUXXXX...
|
||||
@@ -1352,7 +1353,7 @@ def parse_user_defined(data: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def parse_capabilities(data: str) -> Optional[dict]:
|
||||
def parse_capabilities(data: str) -> dict | None:
|
||||
"""Parse station capabilities response.
|
||||
|
||||
Format: <capability1,capability2,...
|
||||
@@ -1381,7 +1382,7 @@ def parse_capabilities(data: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def parse_nmea(data: str) -> Optional[dict]:
|
||||
def parse_nmea(data: str) -> dict | None:
|
||||
"""Parse raw GPS NMEA sentences.
|
||||
|
||||
APRS can include raw NMEA data starting with $.
|
||||
@@ -1409,7 +1410,7 @@ def parse_nmea(data: str) -> Optional[dict]:
|
||||
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).
|
||||
|
||||
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}")
|
||||
app_module.aprs_queue.put({'type': 'error', 'message': str(e)})
|
||||
finally:
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
os.close(master_fd)
|
||||
except OSError:
|
||||
pass
|
||||
app_module.aprs_queue.put({'type': 'status', 'status': 'stopped'})
|
||||
# Cleanup processes
|
||||
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.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
# 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:
|
||||
app_module.release_sdr_device(my_device, aprs_active_sdr_type or 'rtlsdr')
|
||||
@@ -1860,14 +1857,10 @@ def start_aprs() -> Response:
|
||||
if stderr_output:
|
||||
error_msg += f': {stderr_output[:500]}'
|
||||
logger.error(error_msg)
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
os.close(master_fd)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
decoder_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
if aprs_active_device is not None:
|
||||
app_module.release_sdr_device(aprs_active_device, aprs_active_sdr_type or 'rtlsdr')
|
||||
aprs_active_device = None
|
||||
@@ -1888,14 +1881,10 @@ def start_aprs() -> Response:
|
||||
if error_output:
|
||||
error_msg += f': {error_output}'
|
||||
logger.error(error_msg)
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
os.close(master_fd)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
rtl_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
if aprs_active_device is not None:
|
||||
app_module.release_sdr_device(aprs_active_device, aprs_active_sdr_type or 'rtlsdr')
|
||||
aprs_active_device = None
|
||||
@@ -1935,7 +1924,13 @@ def start_aprs() -> Response:
|
||||
|
||||
@aprs_bp.route('/stop', methods=['POST'])
|
||||
def stop_aprs() -> Response:
|
||||
"""Stop APRS decoder."""
|
||||
"""Stop APRS decoder.
|
||||
|
||||
Releases the SDR device immediately so the status panel updates
|
||||
without waiting for process termination. Process cleanup runs in a
|
||||
background thread to avoid blocking the HTTP response (which caused
|
||||
frontend timeout errors when two processes each took up to 2s to die).
|
||||
"""
|
||||
global aprs_active_device, aprs_active_sdr_type
|
||||
|
||||
with app_module.aprs_lock:
|
||||
@@ -1950,6 +1945,28 @@ def stop_aprs() -> Response:
|
||||
if not processes_to_stop:
|
||||
return api_error('APRS decoder not running', 400)
|
||||
|
||||
# Release SDR device immediately so status panel reflects the
|
||||
# change without waiting for process termination.
|
||||
if aprs_active_device is not None:
|
||||
app_module.release_sdr_device(aprs_active_device, aprs_active_sdr_type or 'rtlsdr')
|
||||
aprs_active_device = None
|
||||
aprs_active_sdr_type = None
|
||||
|
||||
# Capture refs to clear before releasing the lock
|
||||
master_fd = getattr(app_module, 'aprs_master_fd', None)
|
||||
app_module.aprs_process = None
|
||||
if hasattr(app_module, 'aprs_rtl_process'):
|
||||
app_module.aprs_rtl_process = None
|
||||
app_module.aprs_master_fd = None
|
||||
|
||||
# Terminate processes in background so the response returns fast.
|
||||
# Each proc.wait() can block up to PROCESS_TERMINATE_TIMEOUT (2s),
|
||||
# which previously caused the frontend 2200ms fetch to abort.
|
||||
def _cleanup():
|
||||
# Close PTY master fd first — this unblocks the stream thread
|
||||
if master_fd is not None:
|
||||
with contextlib.suppress(OSError):
|
||||
os.close(master_fd)
|
||||
for proc in processes_to_stop:
|
||||
try:
|
||||
proc.terminate()
|
||||
@@ -1959,23 +1976,7 @@ def stop_aprs() -> Response:
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping APRS process: {e}")
|
||||
|
||||
# Close PTY master fd
|
||||
if hasattr(app_module, 'aprs_master_fd') and app_module.aprs_master_fd is not None:
|
||||
try:
|
||||
os.close(app_module.aprs_master_fd)
|
||||
except OSError:
|
||||
pass
|
||||
app_module.aprs_master_fd = None
|
||||
|
||||
app_module.aprs_process = None
|
||||
if hasattr(app_module, 'aprs_rtl_process'):
|
||||
app_module.aprs_rtl_process = None
|
||||
|
||||
# Release SDR device
|
||||
if aprs_active_device is not None:
|
||||
app_module.release_sdr_device(aprs_active_device, aprs_active_sdr_type or 'rtlsdr')
|
||||
aprs_active_device = None
|
||||
aprs_active_sdr_type = None
|
||||
threading.Thread(target=_cleanup, daemon=True).start()
|
||||
|
||||
return jsonify({'status': 'stopped'})
|
||||
|
||||
@@ -2099,7 +2100,7 @@ def scan_aprs_spectrum() -> Response:
|
||||
return api_error('rtl_power did not produce output file', 500)
|
||||
|
||||
bins = []
|
||||
with open(tmp_file, 'r') as f:
|
||||
with open(tmp_file) as f:
|
||||
reader = csv.reader(f)
|
||||
for row in reader:
|
||||
if len(row) < 7:
|
||||
|
||||
@@ -6,6 +6,7 @@ import socket
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
from flask import Flask
|
||||
|
||||
# Try to import flask-sock
|
||||
@@ -16,6 +17,8 @@ except ImportError:
|
||||
WEBSOCKET_AVAILABLE = False
|
||||
Sock = None
|
||||
|
||||
import contextlib
|
||||
|
||||
from utils.logging import get_logger
|
||||
|
||||
logger = get_logger('intercept.audio_ws')
|
||||
@@ -56,10 +59,8 @@ def kill_audio_processes():
|
||||
audio_process.terminate()
|
||||
audio_process.wait(timeout=0.5)
|
||||
except:
|
||||
try:
|
||||
with contextlib.suppress(BaseException):
|
||||
audio_process.kill()
|
||||
except:
|
||||
pass
|
||||
audio_process = None
|
||||
|
||||
if rtl_process:
|
||||
@@ -67,10 +68,8 @@ def kill_audio_processes():
|
||||
rtl_process.terminate()
|
||||
rtl_process.wait(timeout=0.5)
|
||||
except:
|
||||
try:
|
||||
with contextlib.suppress(BaseException):
|
||||
rtl_process.kill()
|
||||
except:
|
||||
pass
|
||||
rtl_process = None
|
||||
|
||||
time.sleep(0.3)
|
||||
@@ -261,16 +260,10 @@ def init_audio_websocket(app: Flask):
|
||||
# Complete WebSocket close handshake, then shut down the
|
||||
# raw socket so Werkzeug cannot write its HTTP 200 response
|
||||
# on top of the WebSocket stream.
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
ws.sock.shutdown(socket.SHUT_RDWR)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
ws.sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
logger.info("WebSocket audio client disconnected")
|
||||
|
||||
+14
-27
@@ -2,8 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import contextlib
|
||||
import os
|
||||
import platform
|
||||
import pty
|
||||
@@ -13,30 +12,22 @@ import select
|
||||
import subprocess
|
||||
import threading
|
||||
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
|
||||
from utils.dependencies import check_tool
|
||||
from utils.logging import bluetooth_logger as logger
|
||||
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 data.oui import OUI_DATABASE, get_manufacturer, load_oui_database
|
||||
from data.patterns import AIRTAG_PREFIXES, SAMSUNG_TRACKER, TILE_PREFIXES
|
||||
from utils.constants import (
|
||||
BT_TERMINATE_TIMEOUT,
|
||||
SSE_KEEPALIVE_INTERVAL,
|
||||
SSE_QUEUE_TIMEOUT,
|
||||
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')
|
||||
|
||||
@@ -328,10 +319,8 @@ def stream_bt_scan(process, scan_mode):
|
||||
except OSError:
|
||||
break
|
||||
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
os.close(master_fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
except Exception as 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.wait(timeout=2)
|
||||
except (subprocess.TimeoutExpired, OSError):
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
app_module.bt_process.kill()
|
||||
except OSError:
|
||||
pass
|
||||
app_module.bt_process = None
|
||||
|
||||
try:
|
||||
@@ -507,7 +494,7 @@ def reset_bt_adapter():
|
||||
|
||||
return jsonify({
|
||||
'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
|
||||
})
|
||||
|
||||
|
||||
+7
-14
@@ -7,31 +7,27 @@ aggregation, and heuristics.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Generator
|
||||
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 (
|
||||
BluetoothScanner,
|
||||
BTDeviceAggregate,
|
||||
get_bluetooth_scanner,
|
||||
check_capabilities,
|
||||
RANGE_UNKNOWN,
|
||||
TrackerType,
|
||||
TrackerConfidence,
|
||||
get_tracker_engine,
|
||||
get_bluetooth_scanner,
|
||||
)
|
||||
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.responses import api_error
|
||||
from utils.sse import format_sse
|
||||
|
||||
logger = logging.getLogger('intercept.bluetooth_v2')
|
||||
|
||||
@@ -901,10 +897,8 @@ def stream_events():
|
||||
"""Generate SSE events from scanner."""
|
||||
for event in scanner.stream_events(timeout=1.0):
|
||||
event_name, event_data = map_event_type(event)
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
process_event('bluetooth', event_data, event_name)
|
||||
except Exception:
|
||||
pass
|
||||
yield format_sse(event_data, event=event_name)
|
||||
|
||||
return Response(
|
||||
@@ -972,7 +966,6 @@ def get_tscm_bluetooth_snapshot(duration: int = 8) -> list[dict]:
|
||||
Returns:
|
||||
List of device dictionaries in TSCM format.
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger('intercept.bluetooth_v2')
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,6 @@ from collections.abc import Generator
|
||||
|
||||
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.bt_locate import (
|
||||
Environment,
|
||||
@@ -22,6 +21,7 @@ from utils.bt_locate import (
|
||||
start_locate_session,
|
||||
stop_locate_session,
|
||||
)
|
||||
from utils.responses import api_error
|
||||
from utils.sse import format_sse
|
||||
|
||||
logger = logging.getLogger('intercept.bt_locate')
|
||||
|
||||
+17
-12
@@ -10,30 +10,34 @@ This blueprint provides:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime, timezone
|
||||
from typing import Generator
|
||||
|
||||
import requests
|
||||
from flask import Blueprint, Response, jsonify, request
|
||||
|
||||
from flask import Blueprint, jsonify, request, Response
|
||||
|
||||
from utils.responses import api_success, api_error
|
||||
from utils.agent_client import AgentClient, AgentConnectionError, AgentHTTPError, create_client_from_agent
|
||||
from utils.database import (
|
||||
create_agent, get_agent, get_agent_by_name, list_agents,
|
||||
update_agent, delete_agent, store_push_payload, get_recent_payloads
|
||||
)
|
||||
from utils.agent_client import (
|
||||
AgentClient, AgentHTTPError, AgentConnectionError, create_client_from_agent
|
||||
create_agent,
|
||||
delete_agent,
|
||||
get_agent,
|
||||
get_agent_by_name,
|
||||
get_recent_payloads,
|
||||
list_agents,
|
||||
store_push_payload,
|
||||
update_agent,
|
||||
)
|
||||
from utils.responses import api_error
|
||||
from utils.sse import format_sse
|
||||
from utils.trilateration import (
|
||||
DeviceLocationTracker, PathLossModel, Trilateration,
|
||||
AgentObservation, estimate_location_from_observations
|
||||
DeviceLocationTracker,
|
||||
PathLossModel,
|
||||
Trilateration,
|
||||
estimate_location_from_observations,
|
||||
)
|
||||
|
||||
logger = logging.getLogger('intercept.controller')
|
||||
@@ -700,6 +704,7 @@ def stream_all_agents():
|
||||
def agent_management_page():
|
||||
"""Render the agent management page."""
|
||||
from flask import render_template
|
||||
|
||||
from config import VERSION
|
||||
return render_template('agents.html', version=VERSION)
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import Blueprint, jsonify, request, Response
|
||||
from flask import Blueprint, Response, request
|
||||
|
||||
import app as app_module
|
||||
from utils.correlation import get_correlations
|
||||
from utils.responses import api_success, api_error
|
||||
from utils.logging import get_logger
|
||||
from utils.responses import api_error, api_success
|
||||
|
||||
logger = get_logger('intercept.correlation')
|
||||
|
||||
|
||||
+19
-32
@@ -6,7 +6,7 @@ distress and safety communications per ITU-R M.493.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import pty
|
||||
@@ -16,37 +16,36 @@ import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
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
|
||||
from utils.constants import (
|
||||
DSC_VHF_FREQUENCY_MHZ,
|
||||
DSC_SAMPLE_RATE,
|
||||
DSC_TERMINATE_TIMEOUT,
|
||||
DSC_VHF_FREQUENCY_MHZ,
|
||||
)
|
||||
from utils.database import (
|
||||
store_dsc_alert,
|
||||
get_dsc_alerts,
|
||||
get_dsc_alert,
|
||||
acknowledge_dsc_alert,
|
||||
get_dsc_alert,
|
||||
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.sse import sse_stream_fanout
|
||||
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 (
|
||||
validate_device_index,
|
||||
validate_gain,
|
||||
validate_rtl_tcp_host,
|
||||
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')
|
||||
|
||||
@@ -83,8 +82,8 @@ def _check_dsc_tools() -> dict:
|
||||
# Check for scipy/numpy (needed for decoder)
|
||||
scipy_available = False
|
||||
try:
|
||||
import scipy
|
||||
import numpy
|
||||
import scipy
|
||||
scipy_available = True
|
||||
except ImportError:
|
||||
pass
|
||||
@@ -179,10 +178,8 @@ def stream_dsc_decoder(master_fd: int, decoder_process: subprocess.Popen) -> Non
|
||||
})
|
||||
finally:
|
||||
global dsc_active_device, dsc_active_sdr_type
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
os.close(master_fd)
|
||||
except OSError:
|
||||
pass
|
||||
dsc_running = False
|
||||
# Cleanup both processes
|
||||
with app_module.dsc_lock:
|
||||
@@ -193,10 +190,8 @@ def stream_dsc_decoder(master_fd: int, decoder_process: subprocess.Popen) -> Non
|
||||
proc.terminate()
|
||||
proc.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
unregister_process(proc)
|
||||
app_module.dsc_queue.put({'type': 'status', 'status': 'stopped'})
|
||||
with app_module.dsc_lock:
|
||||
@@ -466,10 +461,8 @@ def start_decoding() -> Response:
|
||||
rtl_process.terminate()
|
||||
rtl_process.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
rtl_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
# Release device on failure
|
||||
if dsc_active_device is not None:
|
||||
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.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
rtl_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
# Release device on failure
|
||||
if dsc_active_device is not None:
|
||||
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.wait(timeout=DSC_TERMINATE_TIMEOUT)
|
||||
except subprocess.TimeoutExpired:
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
app_module.dsc_rtl_process.kill()
|
||||
except OSError:
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@@ -531,10 +520,8 @@ def stop_decoding() -> Response:
|
||||
app_module.dsc_process.terminate()
|
||||
app_module.dsc_process.wait(timeout=DSC_TERMINATE_TIMEOUT)
|
||||
except subprocess.TimeoutExpired:
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
app_module.dsc_process.kill()
|
||||
except OSError:
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import queue
|
||||
import time
|
||||
from collections.abc import Generator
|
||||
|
||||
from flask import Blueprint, Response, jsonify
|
||||
|
||||
from utils.responses import api_success, api_error
|
||||
from utils.gps import (
|
||||
GPSPosition,
|
||||
GPSSkyData,
|
||||
|
||||
@@ -11,8 +11,8 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import queue
|
||||
import signal
|
||||
import shutil
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import threading
|
||||
@@ -22,15 +22,15 @@ from typing import Dict, List, Optional
|
||||
|
||||
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 (
|
||||
SSE_QUEUE_TIMEOUT,
|
||||
SSE_KEEPALIVE_INTERVAL,
|
||||
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.sse import sse_stream_fanout
|
||||
|
||||
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.
|
||||
# app.py -> register_blueprints -> from .listening_post import receiver_bp
|
||||
# must find receiver_bp already defined (above) before this import runs.
|
||||
import contextlib
|
||||
|
||||
import app as app_module # noqa: E402
|
||||
|
||||
# ============================================
|
||||
@@ -57,16 +59,16 @@ audio_source = 'process'
|
||||
audio_start_token = 0
|
||||
|
||||
# Scanner state
|
||||
scanner_thread: Optional[threading.Thread] = None
|
||||
scanner_thread: threading.Thread | None = None
|
||||
scanner_running = False
|
||||
scanner_lock = threading.Lock()
|
||||
scanner_paused = False
|
||||
scanner_current_freq = 0.0
|
||||
scanner_active_device: Optional[int] = None
|
||||
scanner_active_device: int | None = None
|
||||
scanner_active_sdr_type: str = 'rtlsdr'
|
||||
receiver_active_device: Optional[int] = None
|
||||
receiver_active_device: int | None = None
|
||||
receiver_active_sdr_type: str = 'rtlsdr'
|
||||
scanner_power_process: Optional[subprocess.Popen] = None
|
||||
scanner_power_process: subprocess.Popen | None = None
|
||||
scanner_config = {
|
||||
'start_freq': 88.0,
|
||||
'end_freq': 108.0,
|
||||
@@ -84,7 +86,7 @@ scanner_config = {
|
||||
}
|
||||
|
||||
# Activity log
|
||||
activity_log: List[Dict] = []
|
||||
activity_log: list[dict] = []
|
||||
activity_log_lock = threading.Lock()
|
||||
MAX_LOG_ENTRIES = 500
|
||||
|
||||
@@ -95,12 +97,12 @@ scanner_queue: queue.Queue = queue.Queue(maxsize=100)
|
||||
scanner_skip_signal = False
|
||||
|
||||
# Waterfall / spectrogram state
|
||||
waterfall_process: Optional[subprocess.Popen] = None
|
||||
waterfall_thread: Optional[threading.Thread] = None
|
||||
waterfall_process: subprocess.Popen | None = None
|
||||
waterfall_thread: threading.Thread | None = None
|
||||
waterfall_running = False
|
||||
waterfall_lock = threading.Lock()
|
||||
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_config = {
|
||||
'start_freq': 88.0,
|
||||
@@ -185,13 +187,11 @@ def add_activity_log(event_type: str, frequency: float, details: str = ''):
|
||||
activity_log.pop()
|
||||
|
||||
# Also push to SSE queue
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
scanner_queue.put_nowait({
|
||||
'type': 'log',
|
||||
'entry': entry
|
||||
})
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
|
||||
def _start_audio_stream(
|
||||
@@ -348,12 +348,12 @@ def _start_audio_stream(
|
||||
rtl_stderr = ''
|
||||
ffmpeg_stderr = ''
|
||||
try:
|
||||
with open(rtl_stderr_log, 'r') as f:
|
||||
with open(rtl_stderr_log) as f:
|
||||
rtl_stderr = f.read().strip()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
with open(ffmpeg_stderr_log, 'r') as f:
|
||||
with open(ffmpeg_stderr_log) as f:
|
||||
ffmpeg_stderr = f.read().strip()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -502,10 +502,8 @@ def _stop_waterfall_internal() -> None:
|
||||
waterfall_process.terminate()
|
||||
waterfall_process.wait(timeout=1)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
waterfall_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
waterfall_process = 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
|
||||
# ============================================
|
||||
from . import scanner # noqa: E402, F401
|
||||
from . import audio # noqa: E402, F401
|
||||
from . import waterfall # noqa: E402, F401
|
||||
from . import tools # noqa: E402, F401
|
||||
from . import (
|
||||
audio, # noqa: E402, F401
|
||||
scanner, # noqa: E402, F401
|
||||
tools, # noqa: E402, F401
|
||||
waterfall, # noqa: E402, F401
|
||||
)
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import select
|
||||
import subprocess
|
||||
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 (
|
||||
receiver_bp,
|
||||
logger,
|
||||
app_module,
|
||||
scanner_config,
|
||||
_wav_header,
|
||||
_start_audio_stream,
|
||||
_stop_audio_stream,
|
||||
_stop_waterfall_internal,
|
||||
_wav_header,
|
||||
app_module,
|
||||
logger,
|
||||
normalize_modulation,
|
||||
receiver_bp,
|
||||
scanner_config,
|
||||
)
|
||||
import routes.listening_post as _state
|
||||
|
||||
|
||||
# ============================================
|
||||
# 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)
|
||||
if need_scanner_teardown:
|
||||
if scanner_thread_ref and scanner_thread_ref.is_alive():
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
scanner_thread_ref.join(timeout=2.0)
|
||||
except Exception:
|
||||
pass
|
||||
if scanner_proc_ref and scanner_proc_ref.poll() is None:
|
||||
try:
|
||||
scanner_proc_ref.terminate()
|
||||
scanner_proc_ref.wait(timeout=1)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
scanner_proc_ref.kill()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
subprocess.run(['pkill', '-9', 'rtl_power'], capture_output=True, timeout=0.5)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
|
||||
# Re-acquire lock for waterfall check and device claim
|
||||
@@ -232,7 +226,7 @@ def start_audio() -> Response:
|
||||
start_error = ''
|
||||
for log_path in ('/tmp/rtl_fm_stderr.log', '/tmp/ffmpeg_stderr.log'):
|
||||
try:
|
||||
with open(log_path, 'r') as handle:
|
||||
with open(log_path) as handle:
|
||||
content = handle.read().strip()
|
||||
if content:
|
||||
start_error = content.splitlines()[-1]
|
||||
@@ -290,7 +284,7 @@ def audio_debug() -> Response:
|
||||
|
||||
def _read_log(path: str) -> str:
|
||||
try:
|
||||
with open(path, 'r') as handle:
|
||||
with open(path) as handle:
|
||||
return handle.read().strip()
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import math
|
||||
import queue
|
||||
import struct
|
||||
@@ -10,32 +11,32 @@ import threading
|
||||
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 (
|
||||
receiver_bp,
|
||||
logger,
|
||||
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,
|
||||
SSE_KEEPALIVE_INTERVAL,
|
||||
SSE_QUEUE_TIMEOUT,
|
||||
_rtl_fm_demod_mode,
|
||||
_start_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,
|
||||
receiver_bp,
|
||||
scanner_config,
|
||||
scanner_lock,
|
||||
scanner_queue,
|
||||
sse_stream_fanout,
|
||||
SSE_QUEUE_TIMEOUT,
|
||||
SSE_KEEPALIVE_INTERVAL,
|
||||
)
|
||||
import routes.listening_post as _state
|
||||
|
||||
|
||||
# ============================================
|
||||
# SCANNER IMPLEMENTATION
|
||||
@@ -76,7 +77,7 @@ def scanner_loop():
|
||||
_state.scanner_current_freq = current_freq
|
||||
|
||||
# Notify clients of frequency change
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
scanner_queue.put_nowait({
|
||||
'type': 'freq_change',
|
||||
'frequency': current_freq,
|
||||
@@ -84,8 +85,6 @@ def scanner_loop():
|
||||
'range_start': scanner_config['start_freq'],
|
||||
'range_end': scanner_config['end_freq']
|
||||
})
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
# Start rtl_fm at this frequency
|
||||
freq_hz = int(current_freq * 1e6)
|
||||
@@ -168,7 +167,7 @@ def scanner_loop():
|
||||
audio_detected = rms > effective_threshold
|
||||
|
||||
# Send level info to clients
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
scanner_queue.put_nowait({
|
||||
'type': 'scan_update',
|
||||
'frequency': current_freq,
|
||||
@@ -178,8 +177,6 @@ def scanner_loop():
|
||||
'range_start': scanner_config['start_freq'],
|
||||
'range_end': scanner_config['end_freq']
|
||||
})
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
if audio_detected and _state.scanner_running:
|
||||
if not signal_detected:
|
||||
@@ -214,13 +211,11 @@ def scanner_loop():
|
||||
_state.scanner_skip_signal = False
|
||||
signal_detected = False
|
||||
_stop_audio_stream()
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
scanner_queue.put_nowait({
|
||||
'type': 'signal_skipped',
|
||||
'frequency': current_freq
|
||||
})
|
||||
except queue.Full:
|
||||
pass
|
||||
# Move to next frequency (step is in kHz, convert to MHz)
|
||||
current_freq += step_mhz
|
||||
if current_freq > scanner_config['end_freq']:
|
||||
@@ -240,15 +235,13 @@ def scanner_loop():
|
||||
if _state.scanner_running and not _state.scanner_skip_signal:
|
||||
signal_detected = False
|
||||
_stop_audio_stream()
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
scanner_queue.put_nowait({
|
||||
'type': 'signal_lost',
|
||||
'frequency': current_freq,
|
||||
'range_start': scanner_config['start_freq'],
|
||||
'range_end': scanner_config['end_freq']
|
||||
})
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
current_freq += step_mhz
|
||||
if current_freq > scanner_config['end_freq']:
|
||||
@@ -268,13 +261,11 @@ def scanner_loop():
|
||||
# Stop audio
|
||||
_stop_audio_stream()
|
||||
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
scanner_queue.put_nowait({
|
||||
'type': 'signal_lost',
|
||||
'frequency': current_freq
|
||||
})
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
# Move to next frequency (step is in kHz, convert to MHz)
|
||||
current_freq += step_mhz
|
||||
@@ -321,7 +312,7 @@ def scanner_loop_power():
|
||||
step_khz = scanner_config['step']
|
||||
gain = scanner_config['gain']
|
||||
device = scanner_config['device']
|
||||
squelch = scanner_config['squelch']
|
||||
scanner_config['squelch']
|
||||
mod = scanner_config['modulation']
|
||||
|
||||
# Configure sweep
|
||||
@@ -355,7 +346,7 @@ def scanner_loop_power():
|
||||
|
||||
if not stdout:
|
||||
add_activity_log('error', start_mhz, 'Power sweep produced no data')
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
scanner_queue.put_nowait({
|
||||
'type': 'scan_update',
|
||||
'frequency': end_mhz,
|
||||
@@ -365,8 +356,6 @@ def scanner_loop_power():
|
||||
'range_start': scanner_config['start_freq'],
|
||||
'range_end': scanner_config['end_freq']
|
||||
})
|
||||
except queue.Full:
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
|
||||
@@ -414,7 +403,7 @@ def scanner_loop_power():
|
||||
|
||||
if not segments:
|
||||
add_activity_log('error', start_mhz, 'Power sweep bins missing')
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
scanner_queue.put_nowait({
|
||||
'type': 'scan_update',
|
||||
'frequency': end_mhz,
|
||||
@@ -424,8 +413,6 @@ def scanner_loop_power():
|
||||
'range_start': scanner_config['start_freq'],
|
||||
'range_end': scanner_config['end_freq']
|
||||
})
|
||||
except queue.Full:
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
|
||||
@@ -457,7 +444,7 @@ def scanner_loop_power():
|
||||
level = int(max(0, snr) * 100)
|
||||
threshold = int(snr_threshold * 100)
|
||||
progress = min(1.0, (segment_offset + idx) / max(1, total_bins - 1))
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
scanner_queue.put_nowait({
|
||||
'type': 'scan_update',
|
||||
'frequency': _state.scanner_current_freq,
|
||||
@@ -468,8 +455,6 @@ def scanner_loop_power():
|
||||
'range_start': scanner_config['start_freq'],
|
||||
'range_end': scanner_config['end_freq']
|
||||
})
|
||||
except queue.Full:
|
||||
pass
|
||||
segment_offset += len(bin_values)
|
||||
|
||||
# Detect peaks (clusters above threshold)
|
||||
@@ -505,7 +490,7 @@ def scanner_loop_power():
|
||||
threshold = int(snr_threshold * 100)
|
||||
add_activity_log('signal_found', freq_mhz,
|
||||
f'Peak detected at {freq_mhz:.3f} MHz ({mod.upper()})')
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
scanner_queue.put_nowait({
|
||||
'type': 'signal_found',
|
||||
'frequency': freq_mhz,
|
||||
@@ -517,8 +502,6 @@ def scanner_loop_power():
|
||||
'range_start': scanner_config['start_freq'],
|
||||
'range_end': scanner_config['end_freq']
|
||||
})
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
add_activity_log('scan_cycle', start_mhz, 'Power sweep complete')
|
||||
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']
|
||||
|
||||
# Power scan only supports RTL-SDR for now
|
||||
if scanner_config['scan_method'] == 'power':
|
||||
if sdr_type != 'rtlsdr' or not find_rtl_power():
|
||||
scanner_config['scan_method'] = 'classic'
|
||||
if scanner_config['scan_method'] == 'power' and (sdr_type != 'rtlsdr' or not find_rtl_power()):
|
||||
scanner_config['scan_method'] = 'classic'
|
||||
|
||||
# Check tools based on chosen method
|
||||
if scanner_config['scan_method'] == 'power':
|
||||
@@ -666,10 +648,8 @@ def stop_scanner() -> Response:
|
||||
_state.scanner_power_process.terminate()
|
||||
_state.scanner_power_process.wait(timeout=1)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
_state.scanner_power_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
_state.scanner_power_process = None
|
||||
if _state.scanner_active_device is not None:
|
||||
app_module.release_sdr_device(_state.scanner_active_device, _state.scanner_active_sdr_type)
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import jsonify, request, Response
|
||||
from flask import Response, jsonify, request
|
||||
|
||||
from . import (
|
||||
receiver_bp,
|
||||
logger,
|
||||
find_ffmpeg,
|
||||
find_rtl_fm,
|
||||
find_rtl_power,
|
||||
find_rx_fm,
|
||||
find_ffmpeg,
|
||||
logger,
|
||||
receiver_bp,
|
||||
)
|
||||
|
||||
|
||||
# ============================================
|
||||
# TOOL CHECK ENDPOINT
|
||||
# ============================================
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import math
|
||||
import queue
|
||||
import struct
|
||||
@@ -11,23 +12,23 @@ import time
|
||||
from datetime import datetime
|
||||
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
|
||||
|
||||
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
|
||||
@@ -75,14 +76,12 @@ def _parse_rtl_power_line(line: str) -> tuple[str | None, float | None, float |
|
||||
|
||||
def _queue_waterfall_error(message: str) -> None:
|
||||
"""Push an error message onto the waterfall SSE queue."""
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
_state.waterfall_queue.put_nowait({
|
||||
'type': 'waterfall_error',
|
||||
'message': message,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
})
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
|
||||
def _downsample_bins(values: list[float], target: int) -> list[float]:
|
||||
@@ -229,14 +228,10 @@ def _waterfall_loop_iq(sdr_type: SDRType):
|
||||
try:
|
||||
_state.waterfall_queue.put_nowait(msg)
|
||||
except queue.Full:
|
||||
try:
|
||||
with contextlib.suppress(queue.Empty):
|
||||
_state.waterfall_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
pass
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
_state.waterfall_queue.put_nowait(msg)
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
# Throttle to respect interval
|
||||
time.sleep(interval)
|
||||
@@ -254,10 +249,8 @@ def _waterfall_loop_iq(sdr_type: SDRType):
|
||||
_state.waterfall_process.terminate()
|
||||
_state.waterfall_process.wait(timeout=1)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
_state.waterfall_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
_state.waterfall_process = None
|
||||
logger.info("Waterfall IQ loop stopped")
|
||||
|
||||
@@ -346,14 +339,10 @@ def _waterfall_loop_rtl_power():
|
||||
try:
|
||||
_state.waterfall_queue.put_nowait(msg)
|
||||
except queue.Full:
|
||||
try:
|
||||
with contextlib.suppress(queue.Empty):
|
||||
_state.waterfall_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
pass
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
_state.waterfall_queue.put_nowait(msg)
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
all_bins = []
|
||||
sweep_start_hz = start_hz
|
||||
@@ -379,10 +368,8 @@ def _waterfall_loop_rtl_power():
|
||||
'bins': bins_to_send,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
}
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
_state.waterfall_queue.put_nowait(msg)
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
if _state.waterfall_running and not received_any:
|
||||
_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.wait(timeout=1)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
_state.waterfall_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
_state.waterfall_process = None
|
||||
logger.info("Waterfall loop stopped")
|
||||
|
||||
@@ -432,9 +417,8 @@ def start_waterfall() -> Response:
|
||||
sdr_type_str = sdr_type.value
|
||||
|
||||
# RTL-SDR uses rtl_power; other types use rx_sdr via IQ capture
|
||||
if sdr_type == SDRType.RTL_SDR:
|
||||
if not find_rtl_power():
|
||||
return jsonify({'status': 'error', 'message': 'rtl_power not found'}), 503
|
||||
if sdr_type == SDRType.RTL_SDR and not find_rtl_power():
|
||||
return jsonify({'status': 'error', 'message': 'rtl_power not found'}), 503
|
||||
|
||||
try:
|
||||
_state.waterfall_config['start_freq'] = float(data.get('start_freq', 88.0))
|
||||
|
||||
@@ -11,21 +11,19 @@ Supports multiple connection types:
|
||||
from __future__ import annotations
|
||||
|
||||
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.sse import sse_stream_fanout
|
||||
from utils.meshtastic import (
|
||||
MeshtasticMessage,
|
||||
get_meshtastic_client,
|
||||
is_meshtastic_available,
|
||||
start_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')
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from typing import Any
|
||||
|
||||
from flask import Blueprint, Flask, Response, jsonify, request
|
||||
|
||||
from utils.responses import api_success, api_error
|
||||
from utils.responses import api_error
|
||||
|
||||
try:
|
||||
from flask_sock import Sock
|
||||
|
||||
+1
-1
@@ -13,7 +13,6 @@ from typing import Any
|
||||
|
||||
from flask import Blueprint, Response, jsonify, request
|
||||
|
||||
from utils.responses import api_success, api_error
|
||||
import app as app_module
|
||||
from utils.event_pipeline import process_event
|
||||
from utils.logging import sensor_logger as logger
|
||||
@@ -22,6 +21,7 @@ from utils.morse import (
|
||||
morse_decoder_thread,
|
||||
)
|
||||
from utils.process import register_process, safe_terminate, 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 (
|
||||
|
||||
+5
-3
@@ -2,11 +2,13 @@
|
||||
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
|
||||
|
||||
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')
|
||||
|
||||
# Default offline settings
|
||||
|
||||
+1
-1
@@ -19,10 +19,10 @@ from flask import Blueprint, Response, jsonify, request
|
||||
|
||||
import app as app_module
|
||||
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.ook import ook_parser_thread
|
||||
from utils.process import register_process, safe_terminate, 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 (
|
||||
|
||||
+25
-34
@@ -2,34 +2,39 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import math
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import pty
|
||||
import queue
|
||||
import re
|
||||
import select
|
||||
import struct
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
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
|
||||
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.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__)
|
||||
|
||||
@@ -189,10 +194,8 @@ def audio_relay_thread(
|
||||
except Exception as e:
|
||||
logger.debug(f"Audio relay error: {e}")
|
||||
finally:
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
multimon_stdin.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
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)})
|
||||
finally:
|
||||
global pager_active_device, pager_active_sdr_type
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
os.close(master_fd)
|
||||
except OSError:
|
||||
pass
|
||||
# Signal relay thread to stop
|
||||
with app_module.process_lock:
|
||||
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.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
unregister_process(proc)
|
||||
app_module.output_queue.put({'type': 'status', 'text': 'stopped'})
|
||||
with app_module.process_lock:
|
||||
@@ -454,10 +453,8 @@ def start_decoding() -> Response:
|
||||
rtl_process.terminate()
|
||||
rtl_process.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
rtl_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
# Release device on failure
|
||||
if pager_active_device is not None:
|
||||
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.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
rtl_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
# Release device on failure
|
||||
if pager_active_device is not None:
|
||||
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.wait(timeout=2)
|
||||
except (subprocess.TimeoutExpired, OSError):
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
app_module.current_process._rtl_process.kill()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Close PTY master fd
|
||||
if hasattr(app_module.current_process, '_master_fd'):
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
os.close(app_module.current_process._master_fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Kill multimon-ng
|
||||
app_module.current_process.terminate()
|
||||
|
||||
+17
-44
@@ -7,6 +7,7 @@ telemetry (position, altitude, temperature, humidity, pressure) on the
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
@@ -20,7 +21,6 @@ from typing import Any
|
||||
|
||||
from flask import Blueprint, Response, jsonify, request
|
||||
|
||||
from utils.responses import api_success, api_error
|
||||
import app as app_module
|
||||
from utils.constants import (
|
||||
MAX_RADIOSONDE_AGE_SECONDS,
|
||||
@@ -32,6 +32,7 @@ from utils.constants import (
|
||||
)
|
||||
from utils.gps import is_gpsd_running
|
||||
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 (
|
||||
@@ -270,7 +271,7 @@ def _fix_data_ownership(path: str) -> None:
|
||||
return
|
||||
try:
|
||||
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)
|
||||
for fname in filenames:
|
||||
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:
|
||||
with _balloons_lock:
|
||||
radiosonde_balloons[serial] = balloon
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
app_module.radiosonde_queue.put_nowait({
|
||||
'type': 'balloon',
|
||||
**balloon,
|
||||
})
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
_udp_socket = None
|
||||
logger.info("Radiosonde UDP listener stopped")
|
||||
|
||||
@@ -354,71 +351,51 @@ def _process_telemetry(msg: dict) -> dict | None:
|
||||
# Position
|
||||
for key in ('lat', 'latitude'):
|
||||
if key in msg:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
balloon['lat'] = float(msg[key])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
break
|
||||
for key in ('lon', 'longitude'):
|
||||
if key in msg:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
balloon['lon'] = float(msg[key])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
break
|
||||
|
||||
# Altitude (metres)
|
||||
if 'alt' in msg:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
balloon['alt'] = float(msg['alt'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Meteorological data
|
||||
for field in ('temp', 'humidity', 'pressure'):
|
||||
if field in msg:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
balloon[field] = float(msg[field])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Velocity
|
||||
if 'vel_h' in msg:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
balloon['vel_h'] = float(msg['vel_h'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if 'vel_v' in msg:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
balloon['vel_v'] = float(msg['vel_v'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if 'heading' in msg:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
balloon['heading'] = float(msg['heading'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# GPS satellites
|
||||
if 'sats' in msg:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
balloon['sats'] = int(msg['sats'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Battery voltage
|
||||
if 'batt' in msg:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
balloon['batt'] = float(msg['batt'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Frequency
|
||||
if 'freq' in msg:
|
||||
try:
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
balloon['freq'] = float(msg['freq'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
balloon['last_seen'] = time.time()
|
||||
return balloon
|
||||
@@ -612,12 +589,10 @@ def start_radiosonde():
|
||||
app_module.release_sdr_device(device_int, sdr_type_str)
|
||||
stderr_output = ''
|
||||
if app_module.radiosonde_process.stderr:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
stderr_output = app_module.radiosonde_process.stderr.read().decode(
|
||||
'utf-8', errors='ignore'
|
||||
).strip()
|
||||
except Exception:
|
||||
pass
|
||||
if stderr_output:
|
||||
logger.error(f"radiosonde_auto_rx stderr:\n{stderr_output}")
|
||||
if stderr_output and (
|
||||
@@ -686,10 +661,8 @@ def stop_radiosonde():
|
||||
|
||||
# Close UDP socket to unblock listener thread
|
||||
if _udp_socket:
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
_udp_socket.close()
|
||||
except OSError:
|
||||
pass
|
||||
_udp_socket = None
|
||||
|
||||
# Release SDR device
|
||||
|
||||
@@ -5,10 +5,10 @@ from __future__ import annotations
|
||||
import json
|
||||
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.responses import api_success, api_error
|
||||
from utils.recording import RECORDING_ROOT, get_recording_manager
|
||||
from utils.responses import api_error, api_success
|
||||
|
||||
recordings_bp = Blueprint('recordings', __name__, url_prefix='/recordings')
|
||||
|
||||
|
||||
+13
-19
@@ -2,25 +2,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import queue
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
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
|
||||
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.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__)
|
||||
|
||||
@@ -70,10 +68,8 @@ def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None:
|
||||
process.terminate()
|
||||
process.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
unregister_process(process)
|
||||
# Kill companion rtl_tcp process
|
||||
with rtl_tcp_lock:
|
||||
@@ -82,10 +78,8 @@ def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None:
|
||||
rtl_tcp_process.terminate()
|
||||
rtl_tcp_process.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
rtl_tcp_process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
unregister_process(rtl_tcp_process)
|
||||
rtl_tcp_process = None
|
||||
app_module.rtlamr_queue.put({'type': 'status', 'text': 'stopped'})
|
||||
@@ -139,7 +133,7 @@ def start_rtlamr() -> Response:
|
||||
# Get message type (default to scm)
|
||||
msgtype = data.get('msgtype', 'scm')
|
||||
output_format = data.get('format', 'json')
|
||||
|
||||
|
||||
# Start rtl_tcp first
|
||||
rtl_tcp_just_started = False
|
||||
rtl_tcp_cmd_str = ''
|
||||
@@ -191,16 +185,16 @@ def start_rtlamr() -> Response:
|
||||
f'-format={output_format}',
|
||||
f'-centerfreq={int(float(freq) * 1e6)}'
|
||||
]
|
||||
|
||||
|
||||
# Add filter options if provided
|
||||
filterid = data.get('filterid')
|
||||
if filterid:
|
||||
cmd.append(f'-filterid={filterid}')
|
||||
|
||||
|
||||
filtertype = data.get('filtertype')
|
||||
if filtertype:
|
||||
cmd.append(f'-filtertype={filtertype}')
|
||||
|
||||
|
||||
# Unique messages only
|
||||
if data.get('unique', True):
|
||||
cmd.append('-unique=true')
|
||||
|
||||
+8
-13
@@ -2,30 +2,25 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
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 data.satellites import TLE_SATELLITES
|
||||
from utils.database import (
|
||||
get_tracked_satellites,
|
||||
add_tracked_satellite,
|
||||
bulk_add_tracked_satellites,
|
||||
update_tracked_satellite,
|
||||
get_tracked_satellites,
|
||||
remove_tracked_satellite,
|
||||
update_tracked_satellite,
|
||||
)
|
||||
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')
|
||||
|
||||
@@ -87,7 +82,7 @@ def init_tle_auto_refresh():
|
||||
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.
|
||||
|
||||
@@ -190,8 +185,8 @@ def satellite_dashboard():
|
||||
def predict_passes():
|
||||
"""Calculate satellite passes using skyfield."""
|
||||
try:
|
||||
from skyfield.api import wgs84, EarthSatellite
|
||||
from skyfield.almanac import find_discrete
|
||||
from skyfield.api import EarthSatellite, wgs84
|
||||
except ImportError:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
@@ -344,7 +339,7 @@ def predict_passes():
|
||||
def get_satellite_position():
|
||||
"""Get real-time positions of satellites."""
|
||||
try:
|
||||
from skyfield.api import wgs84, EarthSatellite
|
||||
from skyfield.api import EarthSatellite, wgs84
|
||||
except ImportError:
|
||||
return api_error('skyfield not installed', 503)
|
||||
|
||||
|
||||
+16
-13
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import math
|
||||
import queue
|
||||
@@ -9,21 +10,25 @@ import subprocess
|
||||
import threading
|
||||
import time
|
||||
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
|
||||
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.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.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__)
|
||||
|
||||
@@ -137,10 +142,8 @@ def stream_sensor_output(process: subprocess.Popen[bytes]) -> None:
|
||||
process.terminate()
|
||||
process.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
unregister_process(process)
|
||||
app_module.sensor_queue.put({'type': 'status', 'text': 'stopped'})
|
||||
with app_module.sensor_lock:
|
||||
|
||||
+4
-4
@@ -6,14 +6,14 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from flask import Blueprint, jsonify, request, Response
|
||||
from flask import Blueprint, Response, jsonify, request
|
||||
|
||||
from utils.database import (
|
||||
get_setting,
|
||||
set_setting,
|
||||
delete_setting,
|
||||
get_all_settings,
|
||||
get_correlations,
|
||||
get_setting,
|
||||
set_setting,
|
||||
)
|
||||
from utils.logging import get_logger
|
||||
from utils.responses import api_error, api_success
|
||||
@@ -163,7 +163,7 @@ def check_dvb_driver_status() -> Response:
|
||||
blacklist_contents = []
|
||||
if blacklist_exists:
|
||||
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('#')]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
+1
-1
@@ -10,8 +10,8 @@ from typing import Any
|
||||
|
||||
from flask import Blueprint, Response, jsonify, request
|
||||
|
||||
from utils.responses import api_success, api_error
|
||||
from utils.logging import get_logger
|
||||
from utils.responses import api_error
|
||||
|
||||
logger = get_logger('intercept.signalid')
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from typing import Any
|
||||
from flask import Blueprint, Response, jsonify
|
||||
|
||||
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')
|
||||
|
||||
|
||||
@@ -611,9 +611,9 @@ def get_station(station_id):
|
||||
@spy_stations_bp.route('/filters')
|
||||
def get_filters():
|
||||
"""Return available filter options."""
|
||||
types = list(set(s['type'] for s in STATIONS))
|
||||
countries = sorted(list(set((s['country'], s['country_code']) for s in STATIONS)))
|
||||
modes = sorted(list(set(s['mode'].split('/')[0] for s in STATIONS)))
|
||||
types = list({s['type'] for s in STATIONS})
|
||||
countries = sorted({(s['country'], s['country_code']) for s in STATIONS})
|
||||
modes = sorted({s['mode'].split('/')[0] for s in STATIONS})
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
|
||||
+11
-10
@@ -6,23 +6,24 @@ ISS SSTV events occur during special commemorations and typically transmit on 14
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
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
|
||||
from utils.logging import get_logger
|
||||
from utils.sse import sse_stream_fanout
|
||||
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 (
|
||||
ISS_SSTV_FREQ,
|
||||
get_sstv_decoder,
|
||||
is_sstv_available,
|
||||
ISS_SSTV_FREQ,
|
||||
)
|
||||
|
||||
logger = get_logger('intercept.sstv')
|
||||
@@ -520,9 +521,11 @@ def iss_schedule():
|
||||
return jsonify(_iss_schedule_cache)
|
||||
|
||||
try:
|
||||
from skyfield.api import wgs84, EarthSatellite
|
||||
from skyfield.almanac import find_discrete
|
||||
from datetime import timedelta
|
||||
|
||||
from skyfield.almanac import find_discrete
|
||||
from skyfield.api import EarthSatellite, wgs84
|
||||
|
||||
from data.satellites import TLE_SATELLITES
|
||||
|
||||
# Get ISS TLE
|
||||
@@ -816,7 +819,5 @@ def decode_file():
|
||||
|
||||
finally:
|
||||
# Clean up temp file
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
Path(tmp_path).unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -6,18 +6,17 @@ frequencies used by amateur radio operators worldwide.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import queue
|
||||
import time
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Blueprint, Response, jsonify, request, send_file
|
||||
|
||||
from utils.responses import api_success, api_error
|
||||
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.logging import get_logger
|
||||
from utils.responses import api_error
|
||||
from utils.sse import sse_stream_fanout
|
||||
from utils.sstv import (
|
||||
get_general_sstv_decoder,
|
||||
)
|
||||
@@ -325,7 +324,5 @@ def decode_file():
|
||||
return api_error(str(e), 500)
|
||||
|
||||
finally:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
Path(tmp_path).unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
+15
-16
@@ -6,25 +6,26 @@ signal replay/transmit, and wideband spectrum analysis.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
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.responses import api_error
|
||||
from utils.sse import sse_stream
|
||||
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')
|
||||
|
||||
@@ -36,10 +37,8 @@ _subghz_queue: queue.Queue = queue.Queue(maxsize=200)
|
||||
|
||||
def _event_callback(event: dict) -> None:
|
||||
"""Forward SubGhzManager events to the SSE queue."""
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
process_event('subghz', event, event.get('type'))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_subghz_queue.put_nowait(event)
|
||||
except queue.Full:
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ from flask import Blueprint, Response, jsonify, request
|
||||
|
||||
from utils.constants import SSE_KEEPALIVE_INTERVAL, SSE_QUEUE_TIMEOUT
|
||||
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
|
||||
|
||||
try:
|
||||
|
||||
+22
-22
@@ -7,6 +7,7 @@ threat detection, and reporting.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
@@ -23,9 +24,9 @@ from data.tscm_frequencies import (
|
||||
get_sweep_preset,
|
||||
)
|
||||
from utils.database import (
|
||||
acknowledge_tscm_threat,
|
||||
add_device_timeline_entry,
|
||||
add_tscm_threat,
|
||||
acknowledge_tscm_threat,
|
||||
cleanup_old_timeline_entries,
|
||||
create_tscm_schedule,
|
||||
create_tscm_sweep,
|
||||
@@ -43,6 +44,8 @@ from utils.database import (
|
||||
update_tscm_schedule,
|
||||
update_tscm_sweep,
|
||||
)
|
||||
from utils.event_pipeline import process_event
|
||||
from utils.sse import sse_stream_fanout
|
||||
from utils.tscm.baseline import (
|
||||
BaselineComparator,
|
||||
BaselineRecorder,
|
||||
@@ -56,12 +59,10 @@ from utils.tscm.correlation import (
|
||||
from utils.tscm.detector import ThreatDetector
|
||||
from utils.tscm.device_identity import (
|
||||
get_identity_engine,
|
||||
reset_identity_engine,
|
||||
ingest_ble_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
|
||||
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
|
||||
detection, with fallback to system tools if bleak is unavailable.
|
||||
"""
|
||||
import platform
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -874,10 +875,8 @@ def _scan_bluetooth_devices(interface: str, duration: int = 10) -> list[dict]:
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
os.close(master_fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
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
|
||||
if stop_check is None:
|
||||
stop_check = lambda: not _sweep_running
|
||||
def stop_check():
|
||||
return not _sweep_running
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -954,11 +954,11 @@ def _scan_rf_signals(
|
||||
# Tool exists but no device detected — try anyway (detection may have failed)
|
||||
sdr_type = 'rtlsdr'
|
||||
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:
|
||||
sdr_type = 'hackrf'
|
||||
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:
|
||||
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)
|
||||
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:
|
||||
parts = line.strip().split(',')
|
||||
if len(parts) >= 7:
|
||||
try:
|
||||
# CSV format: date, time, hz_low, hz_high, hz_step, samples, db_values...
|
||||
hz_low = int(parts[2].strip())
|
||||
hz_high = int(parts[3].strip())
|
||||
int(parts[3].strip())
|
||||
hz_step = float(parts[4].strip())
|
||||
db_values = [float(x) for x in parts[6:] if x.strip()]
|
||||
|
||||
@@ -1100,10 +1100,8 @@ def _scan_rf_signals(
|
||||
|
||||
finally:
|
||||
# Cleanup temp file
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
os.unlink(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Deduplicate nearby frequencies (within 100kHz)
|
||||
if signals:
|
||||
@@ -1816,9 +1814,11 @@ def _generate_assessment(summary: dict) -> str:
|
||||
# =============================================================================
|
||||
# Import sub-modules to register routes on tscm_bp
|
||||
# =============================================================================
|
||||
from routes.tscm import sweep # noqa: E402, F401
|
||||
from routes.tscm import baseline # noqa: E402, F401
|
||||
from routes.tscm import cases # noqa: E402, F401
|
||||
from routes.tscm import meeting # noqa: E402, F401
|
||||
from routes.tscm import analysis # noqa: E402, F401
|
||||
from routes.tscm import schedules # noqa: E402, F401
|
||||
from routes.tscm import (
|
||||
analysis, # noqa: E402, F401
|
||||
baseline, # noqa: E402, F401
|
||||
cases, # noqa: E402, F401
|
||||
meeting, # noqa: E402, F401
|
||||
schedules, # noqa: E402, F401
|
||||
sweep, # noqa: E402, F401
|
||||
)
|
||||
|
||||
@@ -14,7 +14,6 @@ from datetime import datetime
|
||||
from flask import Response, jsonify, request
|
||||
|
||||
from routes.tscm import (
|
||||
_current_sweep_id,
|
||||
_generate_assessment,
|
||||
tscm_bp,
|
||||
)
|
||||
@@ -253,9 +252,9 @@ def get_pdf_report():
|
||||
summary, and mandatory disclaimers.
|
||||
"""
|
||||
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 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)
|
||||
if not sweep_id:
|
||||
@@ -306,9 +305,9 @@ def get_technical_annex():
|
||||
for audit purposes. No packet data included.
|
||||
"""
|
||||
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 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)
|
||||
format_type = request.args.get('format', 'json')
|
||||
@@ -900,8 +899,8 @@ def get_device_timeline_endpoint(identifier: str):
|
||||
and meeting window correlation.
|
||||
"""
|
||||
try:
|
||||
from utils.tscm.advanced import get_timeline_manager
|
||||
from utils.database import get_device_timeline
|
||||
from utils.tscm.advanced import get_timeline_manager
|
||||
|
||||
protocol = request.args.get('protocol', 'bluetooth')
|
||||
since_hours = request.args.get('since_hours', 24, type=int)
|
||||
|
||||
@@ -25,7 +25,6 @@ from utils.database import (
|
||||
set_active_tscm_baseline,
|
||||
)
|
||||
from utils.tscm.baseline import (
|
||||
BaselineComparator,
|
||||
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):
|
||||
"""Get health assessment for a baseline."""
|
||||
try:
|
||||
from utils.tscm.advanced import BaselineHealth
|
||||
|
||||
baseline = get_tscm_baseline(baseline_id)
|
||||
if not baseline:
|
||||
|
||||
@@ -91,7 +91,6 @@ def start_tracked_meeting():
|
||||
"""
|
||||
from utils.database import start_meeting_window
|
||||
from utils.tscm.advanced import get_timeline_manager
|
||||
from routes.tscm import _current_sweep_id
|
||||
|
||||
data = request.get_json() or {}
|
||||
|
||||
@@ -156,9 +155,9 @@ def end_tracked_meeting(meeting_id: int):
|
||||
def get_meeting_summary_endpoint(meeting_id: int):
|
||||
"""Get detailed summary of device activity during a meeting."""
|
||||
try:
|
||||
from routes.tscm import _current_sweep_id
|
||||
from utils.database import get_meeting_windows
|
||||
from utils.tscm.advanced import generate_meeting_summary, get_timeline_manager
|
||||
from routes.tscm import _current_sweep_id
|
||||
|
||||
# Get meeting window
|
||||
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():
|
||||
"""Get currently 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)
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ from routes.tscm import (
|
||||
_get_schedule_timezone,
|
||||
_next_run_from_cron,
|
||||
_start_sweep_internal,
|
||||
_sweep_running,
|
||||
tscm_bp,
|
||||
)
|
||||
from utils.database import (
|
||||
|
||||
+3
-11
@@ -7,27 +7,25 @@ Handles /sweep/*, /status, /devices, /presets/*, /feed/*,
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
from flask import Response, jsonify, request
|
||||
|
||||
from data.tscm_frequencies import get_all_sweep_presets, get_sweep_preset
|
||||
from routes.tscm import (
|
||||
_baseline_recorder,
|
||||
_current_sweep_id,
|
||||
_emit_event,
|
||||
_start_sweep_internal,
|
||||
_sweep_running,
|
||||
tscm_bp,
|
||||
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.event_pipeline import process_event
|
||||
from utils.sse import sse_stream_fanout
|
||||
@@ -38,7 +36,6 @@ logger = logging.getLogger('intercept.tscm')
|
||||
@tscm_bp.route('/status')
|
||||
def tscm_status():
|
||||
"""Check if any TSCM operation is currently running."""
|
||||
from routes.tscm import _sweep_running
|
||||
return jsonify({'running': _sweep_running})
|
||||
|
||||
|
||||
@@ -98,7 +95,6 @@ def stop_sweep():
|
||||
@tscm_bp.route('/sweep/status')
|
||||
def sweep_status():
|
||||
"""Get current sweep status."""
|
||||
from routes.tscm import _sweep_running, _current_sweep_id
|
||||
|
||||
status = {
|
||||
'running': _sweep_running,
|
||||
@@ -116,7 +112,6 @@ def sweep_status():
|
||||
@tscm_bp.route('/sweep/stream')
|
||||
def sweep_stream():
|
||||
"""SSE stream for real-time sweep updates."""
|
||||
from routes.tscm import tscm_queue
|
||||
|
||||
def _on_msg(msg: dict[str, Any]) -> None:
|
||||
process_event('tscm', msg, msg.get('type'))
|
||||
@@ -218,7 +213,7 @@ def get_tscm_devices():
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
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():
|
||||
first_line = block.split('\n')[0]
|
||||
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'])
|
||||
def feed_wifi():
|
||||
"""Feed WiFi device data for baseline recording."""
|
||||
from routes.tscm import _baseline_recorder
|
||||
|
||||
data = request.get_json()
|
||||
if data:
|
||||
@@ -367,7 +361,6 @@ def feed_wifi():
|
||||
@tscm_bp.route('/feed/bluetooth', methods=['POST'])
|
||||
def feed_bluetooth():
|
||||
"""Feed Bluetooth device data for baseline recording."""
|
||||
from routes.tscm import _baseline_recorder
|
||||
|
||||
data = request.get_json()
|
||||
if data:
|
||||
@@ -378,7 +371,6 @@ def feed_bluetooth():
|
||||
@tscm_bp.route('/feed/rf', methods=['POST'])
|
||||
def feed_rf():
|
||||
"""Feed RF signal data for baseline recording."""
|
||||
from routes.tscm import _baseline_recorder
|
||||
|
||||
data = request.get_json()
|
||||
if data:
|
||||
|
||||
+1
-1
@@ -4,8 +4,8 @@ from __future__ import annotations
|
||||
|
||||
from flask import Blueprint, Response, jsonify, request
|
||||
|
||||
from utils.responses import api_success, api_error
|
||||
from utils.logging import get_logger
|
||||
from utils.responses import api_error
|
||||
from utils.updater import (
|
||||
check_for_updates,
|
||||
dismiss_update,
|
||||
|
||||
+6
-10
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
@@ -13,12 +13,11 @@ import subprocess
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Generator
|
||||
from typing import Any
|
||||
|
||||
from flask import Blueprint, Response, jsonify, request
|
||||
|
||||
import app as app_module
|
||||
from utils.responses import api_success, api_error
|
||||
from utils.acars_translator import translate_message
|
||||
from utils.constants import (
|
||||
PROCESS_START_WAIT,
|
||||
@@ -30,6 +29,7 @@ from utils.event_pipeline import process_event
|
||||
from utils.flight_correlator import get_flight_correlator
|
||||
from utils.logging import sensor_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_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)
|
||||
|
||||
# Feed flight correlator
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
get_flight_correlator().add_vdl2_message(data)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Log if 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.wait(timeout=2)
|
||||
except Exception:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
unregister_process(process)
|
||||
app_module.vdl2_queue.put({'type': 'status', 'status': 'stopped'})
|
||||
with app_module.vdl2_lock:
|
||||
@@ -275,7 +271,7 @@ def start_vdl2() -> Response:
|
||||
)
|
||||
os.close(slave_fd)
|
||||
# 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
|
||||
else:
|
||||
process = subprocess.Popen(
|
||||
|
||||
@@ -372,7 +372,6 @@ def init_waterfall_websocket(app: Flask):
|
||||
capture_center_mhz = 0.0
|
||||
capture_start_freq = 0.0
|
||||
capture_end_freq = 0.0
|
||||
capture_span_mhz = 0.0
|
||||
# Queue for outgoing messages — only the main loop touches ws.send()
|
||||
send_queue = queue.Queue(maxsize=120)
|
||||
|
||||
@@ -619,7 +618,6 @@ def init_waterfall_websocket(app: Flask):
|
||||
capture_center_mhz = center_freq_mhz
|
||||
capture_start_freq = start_freq
|
||||
capture_end_freq = end_freq
|
||||
capture_span_mhz = effective_span_mhz
|
||||
|
||||
my_generation = _set_shared_capture_state(
|
||||
running=True,
|
||||
|
||||
+15
-7
@@ -8,18 +8,26 @@ from __future__ import annotations
|
||||
|
||||
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.responses import api_error
|
||||
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 (
|
||||
DEFAULT_SAMPLE_RATE,
|
||||
WEATHER_SATELLITES,
|
||||
CaptureProgress,
|
||||
get_weather_sat_decoder,
|
||||
is_weather_sat_available,
|
||||
CaptureProgress,
|
||||
WEATHER_SATELLITES,
|
||||
DEFAULT_SAMPLE_RATE,
|
||||
)
|
||||
|
||||
logger = get_logger('intercept.weather_sat')
|
||||
@@ -613,7 +621,7 @@ def enable_schedule():
|
||||
gain=gain_val,
|
||||
bias_t=bool(data.get('bias_t', False)),
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception("Failed to enable weather sat scheduler")
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
|
||||
+12
-19
@@ -9,11 +9,10 @@ import re
|
||||
import struct
|
||||
import threading
|
||||
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:
|
||||
from flask_sock import Sock
|
||||
@@ -21,7 +20,9 @@ try:
|
||||
except ImportError:
|
||||
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
|
||||
|
||||
logger = get_logger('intercept.websdr')
|
||||
@@ -38,7 +39,7 @@ _cache_timestamp: float = 0
|
||||
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."""
|
||||
if not coord_str:
|
||||
return None
|
||||
@@ -70,8 +71,8 @@ KIWI_DATA_URLS = [
|
||||
|
||||
def _fetch_kiwi_receivers() -> list[dict]:
|
||||
"""Fetch the KiwiSDR receiver list from the public directory."""
|
||||
import urllib.request
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
receivers = []
|
||||
raw = None
|
||||
@@ -335,7 +336,7 @@ def websdr_status() -> Response:
|
||||
# KIWISDR AUDIO PROXY
|
||||
# ============================================
|
||||
|
||||
_kiwi_client: Optional[KiwiSDRClient] = None
|
||||
_kiwi_client: KiwiSDRClient | None = None
|
||||
_kiwi_lock = threading.Lock()
|
||||
_kiwi_audio_queue: queue.Queue = queue.Queue(maxsize=200)
|
||||
|
||||
@@ -387,26 +388,18 @@ def _handle_kiwi_command(ws, cmd: str, data: dict) -> None:
|
||||
try:
|
||||
_kiwi_audio_queue.put_nowait(header + pcm_bytes)
|
||||
except queue.Full:
|
||||
try:
|
||||
with contextlib.suppress(queue.Empty):
|
||||
_kiwi_audio_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
pass
|
||||
try:
|
||||
with contextlib.suppress(queue.Full):
|
||||
_kiwi_audio_queue.put_nowait(header + pcm_bytes)
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
def on_error(msg):
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
ws.send(json.dumps({'type': 'error', 'message': msg}))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_disconnect():
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
ws.send(json.dumps({'type': 'disconnected'}))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with _kiwi_lock:
|
||||
_kiwi_client = KiwiSDRClient(
|
||||
|
||||
+4
-5
@@ -6,13 +6,14 @@ maritime/aviation weather services worldwide.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import queue
|
||||
|
||||
from flask import Blueprint, Response, jsonify, request, send_file
|
||||
|
||||
from utils.responses import api_success, api_error
|
||||
import app as app_module
|
||||
from utils.logging import get_logger
|
||||
from utils.responses import api_error
|
||||
from utils.sdr import SDRType
|
||||
from utils.sse import sse_stream_fanout
|
||||
from utils.validation import validate_frequency
|
||||
@@ -129,10 +130,8 @@ def start_decoder():
|
||||
frequency_reference = str(data.get('frequency_reference', 'auto')).strip().lower()
|
||||
|
||||
sdr_type_str = str(data.get('sdr_type', 'rtlsdr')).lower()
|
||||
try:
|
||||
sdr_type = SDRType(sdr_type_str)
|
||||
except ValueError:
|
||||
sdr_type = SDRType.RTL_SDR
|
||||
with contextlib.suppress(ValueError):
|
||||
SDRType(sdr_type_str)
|
||||
if not frequency_reference:
|
||||
frequency_reference = 'auto'
|
||||
|
||||
|
||||
+28
-45
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
@@ -11,39 +12,25 @@ import re
|
||||
import subprocess
|
||||
import threading
|
||||
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
|
||||
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 utils.constants import (
|
||||
WIFI_TERMINATE_TIMEOUT,
|
||||
PMKID_TERMINATE_TIMEOUT,
|
||||
SSE_KEEPALIVE_INTERVAL,
|
||||
SSE_QUEUE_TIMEOUT,
|
||||
WIFI_CSV_PARSE_INTERVAL,
|
||||
WIFI_CSV_TIMEOUT_WARNING,
|
||||
SUBPROCESS_TIMEOUT_SHORT,
|
||||
SUBPROCESS_TIMEOUT_MEDIUM,
|
||||
SUBPROCESS_TIMEOUT_LONG,
|
||||
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,
|
||||
SUBPROCESS_TIMEOUT_SHORT,
|
||||
)
|
||||
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')
|
||||
|
||||
@@ -201,9 +188,9 @@ def _get_interface_details(iface_name):
|
||||
# Get MAC address
|
||||
try:
|
||||
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()
|
||||
except (FileNotFoundError, IOError):
|
||||
except (OSError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# Get driver name
|
||||
@@ -212,7 +199,7 @@ def _get_interface_details(iface_name):
|
||||
if os.path.islink(driver_link):
|
||||
driver_path = os.readlink(driver_link)
|
||||
details['driver'] = os.path.basename(driver_path)
|
||||
except (FileNotFoundError, IOError, OSError):
|
||||
except (FileNotFoundError, OSError):
|
||||
pass
|
||||
|
||||
# Try airmon-ng first for chipset info (most reliable for WiFi adapters)
|
||||
@@ -230,11 +217,10 @@ def _get_interface_details(iface_name):
|
||||
break
|
||||
# Also try space-separated format
|
||||
parts = line.split()
|
||||
if len(parts) >= 4:
|
||||
if parts[1] == iface_name or parts[1].startswith(iface_name):
|
||||
details['driver'] = parts[2]
|
||||
details['chipset'] = ' '.join(parts[3:])
|
||||
break
|
||||
if len(parts) >= 4 and (parts[1] == iface_name or parts[1].startswith(iface_name)):
|
||||
details['driver'] = parts[2]
|
||||
details['chipset'] = ' '.join(parts[3:])
|
||||
break
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError):
|
||||
pass
|
||||
|
||||
@@ -246,10 +232,10 @@ def _get_interface_details(iface_name):
|
||||
# Try to get USB product name
|
||||
for usb_path in [f'{device_path}/product', f'{device_path}/../product']:
|
||||
try:
|
||||
with open(usb_path, 'r') as f:
|
||||
with open(usb_path) as f:
|
||||
details['chipset'] = f.read().strip()
|
||||
break
|
||||
except (FileNotFoundError, IOError):
|
||||
except (OSError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# If no USB product, try lsusb for USB devices
|
||||
@@ -257,7 +243,7 @@ def _get_interface_details(iface_name):
|
||||
try:
|
||||
# Get USB bus/device info
|
||||
uevent_path = f'{device_path}/uevent'
|
||||
with open(uevent_path, 'r') as f:
|
||||
with open(uevent_path) as f:
|
||||
for line in f:
|
||||
if line.startswith('PRODUCT='):
|
||||
# PRODUCT format: vendor/product/bcdDevice
|
||||
@@ -280,9 +266,9 @@ def _get_interface_details(iface_name):
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
break
|
||||
except (FileNotFoundError, IOError):
|
||||
except (OSError, FileNotFoundError):
|
||||
pass
|
||||
except (FileNotFoundError, IOError, OSError):
|
||||
except (FileNotFoundError, OSError):
|
||||
pass
|
||||
|
||||
return details
|
||||
@@ -294,7 +280,7 @@ def parse_airodump_csv(csv_path):
|
||||
clients = {}
|
||||
|
||||
try:
|
||||
with open(csv_path, 'r', errors='replace') as f:
|
||||
with open(csv_path, errors='replace') as f:
|
||||
content = f.read()
|
||||
|
||||
sections = content.split('\n\n')
|
||||
@@ -602,7 +588,6 @@ def toggle_monitor_mode():
|
||||
return api_success(data={'monitor_interface': app_module.wifi_monitor_interface})
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
logger.error(f"Error enabling monitor mode: {e}", exc_info=True)
|
||||
return api_error(str(e))
|
||||
|
||||
@@ -683,11 +668,9 @@ def start_wifi_scan():
|
||||
|
||||
csv_path = '/tmp/intercept_wifi'
|
||||
|
||||
for f in [f'/tmp/intercept_wifi-01.csv', f'/tmp/intercept_wifi-01.cap']:
|
||||
try:
|
||||
for f in ['/tmp/intercept_wifi-01.csv', '/tmp/intercept_wifi-01.cap']:
|
||||
with contextlib.suppress(OSError):
|
||||
os.remove(f)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
airodump_path = get_tool_path('airodump-ng')
|
||||
cmd = [
|
||||
@@ -1021,7 +1004,7 @@ def check_pmkid_status():
|
||||
|
||||
try:
|
||||
hash_file = capture_file.replace('.pcapng', '.22000')
|
||||
result = subprocess.run(
|
||||
subprocess.run(
|
||||
['hcxpcapngtool', '-o', hash_file, capture_file],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
@@ -1170,7 +1153,7 @@ def stream_wifi():
|
||||
# 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')
|
||||
|
||||
+12
-14
@@ -7,26 +7,26 @@ channel analysis, hidden SSID correlation, and SSE streaming.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Generator
|
||||
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 (
|
||||
get_wifi_scanner,
|
||||
analyze_channels,
|
||||
get_hidden_correlator,
|
||||
SCAN_MODE_QUICK,
|
||||
SCAN_MODE_DEEP,
|
||||
)
|
||||
from utils.responses import api_success, api_error
|
||||
from utils.event_pipeline import process_event
|
||||
from utils.responses import api_error
|
||||
from utils.sse import format_sse
|
||||
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__)
|
||||
|
||||
@@ -407,10 +407,8 @@ def event_stream():
|
||||
scanner = get_wifi_scanner()
|
||||
|
||||
for event in scanner.get_event_stream():
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
process_event('wifi', event, event.get('type'))
|
||||
except Exception:
|
||||
pass
|
||||
yield format_sse(event)
|
||||
|
||||
response = Response(generate(), mimetype='text/event-stream')
|
||||
|
||||
@@ -174,7 +174,7 @@ read_env_var() {
|
||||
local fallback="${2:-}"
|
||||
if [[ -f "$SCRIPT_DIR/.env" ]]; then
|
||||
local val
|
||||
val=$(grep -E "^${key}=" "$SCRIPT_DIR/.env" 2>/dev/null | tail -1 | cut -d'=' -f2-)
|
||||
val=$(grep -E "^${key}=" "$SCRIPT_DIR/.env" 2>/dev/null | tail -1 | cut -d'=' -f2- || true)
|
||||
if [[ -n "$val" ]]; then
|
||||
# Strip surrounding quotes
|
||||
val="${val#\"}"
|
||||
@@ -751,9 +751,26 @@ install_acarsdec_from_source_macos() {
|
||||
|
||||
cd "$tmp_dir/acarsdec"
|
||||
|
||||
# Replace deprecated -Ofast (all macOS, not just arm64)
|
||||
if grep -q '\-Ofast' CMakeLists.txt 2>/dev/null; then
|
||||
sed -i '' 's/-Ofast/-O3 -ffast-math/g' CMakeLists.txt
|
||||
info "Patched deprecated -Ofast flag"
|
||||
fi
|
||||
|
||||
# macOS doesn't have -march=native on arm64
|
||||
if [[ "$(uname -m)" == "arm64" ]]; then
|
||||
sed -i '' 's/-Ofast -march=native/-O3 -ffast-math/g' CMakeLists.txt
|
||||
info "Patched compiler flags for Apple Silicon (arm64)"
|
||||
sed -i '' 's/ -march=native//g' CMakeLists.txt
|
||||
info "Removed -march=native for Apple Silicon"
|
||||
fi
|
||||
|
||||
# HOST_NAME_MAX is Linux-specific; macOS uses _POSIX_HOST_NAME_MAX
|
||||
if grep -q 'HOST_NAME_MAX' acarsdec.c 2>/dev/null; then
|
||||
sed -i '' '1i\
|
||||
#ifndef HOST_NAME_MAX\
|
||||
#define HOST_NAME_MAX 255\
|
||||
#endif
|
||||
' acarsdec.c
|
||||
info "Patched HOST_NAME_MAX for macOS compatibility"
|
||||
fi
|
||||
|
||||
if grep -q 'pthread_tryjoin_np' rtl.c 2>/dev/null; then
|
||||
@@ -957,8 +974,14 @@ install_satdump_from_source_debian() {
|
||||
) &
|
||||
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 \
|
||||
-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
|
||||
kill $progress_pid 2>/dev/null; wait $progress_pid 2>/dev/null
|
||||
$SUDO make install >/dev/null 2>&1
|
||||
@@ -1918,7 +1941,18 @@ do_health_check() {
|
||||
info "SDR device detection..."
|
||||
if cmd_exists rtl_test; then
|
||||
local rtl_output
|
||||
rtl_output=$(timeout 3 rtl_test -d 0 2>&1 || true)
|
||||
if cmd_exists timeout; then
|
||||
rtl_output=$(timeout 3 rtl_test -d 0 2>&1 || true)
|
||||
elif cmd_exists gtimeout; then
|
||||
rtl_output=$(gtimeout 3 rtl_test -d 0 2>&1 || true)
|
||||
else
|
||||
# No timeout command (common on macOS) — run with background kill
|
||||
rtl_test -d 0 > /tmp/.rtl_test_out 2>&1 & local rtl_pid=$!
|
||||
sleep 2
|
||||
kill "$rtl_pid" 2>/dev/null; wait "$rtl_pid" 2>/dev/null
|
||||
rtl_output=$(cat /tmp/.rtl_test_out 2>/dev/null || true)
|
||||
rm -f /tmp/.rtl_test_out
|
||||
fi
|
||||
if echo "$rtl_output" | grep -q "Found\|Using device"; then
|
||||
ok "RTL-SDR device detected"
|
||||
((pass++)) || true
|
||||
|
||||
@@ -87,6 +87,25 @@
|
||||
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.
|
||||
Uses .logo .brand-i (0,2,0) to beat .logo span (0,1,1) in dashboard CSS
|
||||
which otherwise forces display:inline and breaks width/height. */
|
||||
.brand-i,
|
||||
.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%;
|
||||
}
|
||||
|
||||
.app-logo-tagline {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-dim);
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -36,6 +36,8 @@
|
||||
<script>
|
||||
window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }};
|
||||
window.INTERCEPT_ADSB_AUTO_START = {{ adsb_auto_start | tojson }};
|
||||
window.INTERCEPT_DEFAULT_LAT = {{ default_latitude | tojson }};
|
||||
window.INTERCEPT_DEFAULT_LON = {{ default_longitude | tojson }};
|
||||
</script>
|
||||
{% if offline_settings.assets_source == 'local' %}
|
||||
<script defer src="{{ url_for('static', filename='vendor/leaflet/leaflet.js') }}"></script>
|
||||
@@ -51,7 +53,7 @@
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
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 class="status-bar">
|
||||
<!-- Agent Selector -->
|
||||
@@ -340,8 +342,8 @@
|
||||
<div class="control-group">
|
||||
<span class="control-group-label">LOCATION</span>
|
||||
<div class="control-group-items">
|
||||
<input type="text" id="obsLat" value="51.5074" onchange="updateObserverLoc()" style="width: 70px;" title="Latitude" placeholder="Lat">
|
||||
<input type="text" id="obsLon" value="-0.1278" onchange="updateObserverLoc()" style="width: 70px;" title="Longitude" placeholder="Lon">
|
||||
<input type="text" id="obsLat" value="{{ default_latitude }}" onchange="updateObserverLoc()" style="width: 70px;" title="Latitude" placeholder="Lat">
|
||||
<input type="text" id="obsLon" value="{{ default_longitude }}" onchange="updateObserverLoc()" style="width: 70px;" title="Longitude" placeholder="Lon">
|
||||
<span id="gpsIndicator" class="gps-indicator" style="display: none;" title="GPS connected via gpsd"><span class="gps-dot"></span> GPS</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -643,7 +645,9 @@
|
||||
if (parsed.lat !== undefined && parsed.lat !== null && parsed.lon !== undefined && parsed.lon !== null) return parsed;
|
||||
} catch (e) {}
|
||||
}
|
||||
return { lat: 51.5074, lon: -0.1278 };
|
||||
const defaultLat = window.INTERCEPT_DEFAULT_LAT || 51.5074;
|
||||
const defaultLon = window.INTERCEPT_DEFAULT_LON || -0.1278;
|
||||
return { lat: defaultLat, lon: defaultLon };
|
||||
})();
|
||||
let rangeRingsLayer = null;
|
||||
let observerMarker = null;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
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 class="status-bar">
|
||||
<a href="/adsb/dashboard" class="back-link">Live Radar</a>
|
||||
|
||||
@@ -281,7 +281,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
<!-- Deferred scripts -->
|
||||
<script>
|
||||
window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }};
|
||||
window.INTERCEPT_DEFAULT_LAT = {{ default_latitude | tojson }};
|
||||
window.INTERCEPT_DEFAULT_LON = {{ default_longitude | tojson }};
|
||||
</script>
|
||||
{% if offline_settings.assets_source == 'local' %}
|
||||
<script defer src="{{ url_for('static', filename='vendor/leaflet/leaflet.js') }}"></script>
|
||||
@@ -51,7 +53,7 @@
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
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 class="status-bar">
|
||||
<!-- Agent Selector -->
|
||||
@@ -185,8 +187,8 @@
|
||||
<div class="control-group">
|
||||
<span class="control-group-label">LOCATION</span>
|
||||
<div class="control-group-items">
|
||||
<input type="text" id="obsLat" value="51.5074" onchange="updateObserverLoc()" style="width: 70px;" title="Latitude" placeholder="Lat">
|
||||
<input type="text" id="obsLon" value="-0.1278" onchange="updateObserverLoc()" style="width: 70px;" title="Longitude" placeholder="Lon">
|
||||
<input type="text" id="obsLat" value="{{ default_latitude }}" onchange="updateObserverLoc()" style="width: 70px;" title="Latitude" placeholder="Lat">
|
||||
<input type="text" id="obsLon" value="{{ default_longitude }}" onchange="updateObserverLoc()" style="width: 70px;" title="Longitude" placeholder="Lon">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -248,7 +250,9 @@
|
||||
if (window.ObserverLocation && ObserverLocation.getForModule) {
|
||||
return ObserverLocation.getForModule('ais_observerLocation');
|
||||
}
|
||||
return { lat: 51.5074, lon: -0.1278 };
|
||||
const defaultLat = window.INTERCEPT_DEFAULT_LAT || 51.5074;
|
||||
const defaultLon = window.INTERCEPT_DEFAULT_LON || -0.1278;
|
||||
return { lat: defaultLat, lon: defaultLon };
|
||||
})();
|
||||
let rangeRingsLayer = null;
|
||||
let observerMarker = null;
|
||||
|
||||
+23
-3
@@ -293,7 +293,7 @@
|
||||
<rect x="38" y="76" width="24" height="4" rx="1" fill="#00d4ff" />
|
||||
</svg>
|
||||
</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>
|
||||
<span class="welcome-version">v{{ version }}</span>
|
||||
<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" />
|
||||
</svg>
|
||||
</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 class="header-right">
|
||||
<span class="active-mode-indicator" id="activeModeIndicator"><span class="pulse-dot"></span>PAGER</span>
|
||||
@@ -9849,8 +9849,28 @@
|
||||
let aprsMeterCheckInterval = null;
|
||||
const APRS_METER_TIMEOUT = 5000; // 5 seconds for "no signal" state
|
||||
|
||||
// APRS user location (from GPS)
|
||||
// APRS user location (from GPS or shared observer location)
|
||||
let aprsUserLocation = { lat: null, lon: null };
|
||||
|
||||
// Seed from configured observer location so the map centres on the
|
||||
// user's position even without a live GPS fix.
|
||||
(function _seedAprsLocation() {
|
||||
if (typeof ObserverLocation !== 'undefined' && ObserverLocation.getShared) {
|
||||
const shared = ObserverLocation.getShared();
|
||||
if (shared && shared.lat && shared.lon) {
|
||||
aprsUserLocation.lat = shared.lat;
|
||||
aprsUserLocation.lon = shared.lon;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fallback: read the Jinja-injected defaults directly
|
||||
const lat = window.INTERCEPT_DEFAULT_LAT;
|
||||
const lon = window.INTERCEPT_DEFAULT_LON;
|
||||
if (lat && lon && Number.isFinite(lat) && Number.isFinite(lon)) {
|
||||
aprsUserLocation.lat = lat;
|
||||
aprsUserLocation.lon = lon;
|
||||
}
|
||||
})();
|
||||
let aprsUserMarker = null;
|
||||
|
||||
// Calculate distance in miles using Haversine formula
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<rect x="38" y="76" width="24" height="4" rx="1" fill="var(--accent-cyan)"/>
|
||||
</svg>
|
||||
<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>
|
||||
</a>
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
{% block dashboard_title %}DASHBOARD{% endblock %}
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 class="help-content" tabindex="-1">
|
||||
<button type="button" class="help-close" onclick="hideHelp()" aria-label="Close help">×</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">
|
||||
<button type="button" class="help-tab active" data-tab="icons" onclick="switchHelpTab('icons')" role="tab" aria-controls="help-icons" aria-selected="true">Icons</button>
|
||||
|
||||
@@ -530,7 +530,7 @@
|
||||
<div id="settings-about" class="settings-section">
|
||||
<div class="settings-group">
|
||||
<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>
|
||||
A unified web interface for software-defined radio (SDR) tools,
|
||||
@@ -546,7 +546,7 @@
|
||||
<div class="settings-group">
|
||||
<div class="settings-group-title">Support the Project</div>
|
||||
<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>
|
||||
<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;">
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
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 class="stats-badges">
|
||||
<div class="stat-badge">
|
||||
|
||||
+5
-4
@@ -1,9 +1,11 @@
|
||||
"""Pytest configuration and fixtures."""
|
||||
|
||||
import contextlib
|
||||
import sqlite3
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app import app as flask_app
|
||||
from routes import register_blueprints
|
||||
|
||||
@@ -80,9 +82,10 @@ def mock_app_state():
|
||||
|
||||
Provides mock process, queue, and lock objects on the app module.
|
||||
"""
|
||||
import app as app_module
|
||||
import queue
|
||||
|
||||
import app as app_module
|
||||
|
||||
mock_process = MagicMock()
|
||||
mock_process.poll.return_value = None
|
||||
mock_queue = queue.Queue()
|
||||
@@ -107,10 +110,8 @@ def mock_app_state():
|
||||
|
||||
for attr, orig in originals.items():
|
||||
if orig is None:
|
||||
try:
|
||||
with contextlib.suppress(AttributeError):
|
||||
delattr(app_module, attr)
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
setattr(app_module, attr, orig)
|
||||
|
||||
|
||||
+5
-7
@@ -12,12 +12,12 @@ Usage:
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask import Flask, jsonify, request
|
||||
|
||||
app = Flask(__name__)
|
||||
@@ -53,7 +53,7 @@ def generate_sensors() -> list[dict]:
|
||||
"""Generate fake 433MHz sensor data."""
|
||||
sensors = []
|
||||
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({
|
||||
'time': datetime.now(timezone.utc).isoformat(),
|
||||
'model': random.choice(models),
|
||||
@@ -71,7 +71,7 @@ def generate_wifi_networks() -> list[dict]:
|
||||
networks = []
|
||||
ssids = ['HomeNetwork', 'Linksys', 'NETGEAR', 'xfinitywifi', 'ATT-WIFI', 'CoffeeShop-Guest']
|
||||
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({
|
||||
'ssid': ssid,
|
||||
'bssid': bssid,
|
||||
@@ -89,7 +89,7 @@ def generate_bluetooth_devices() -> list[dict]:
|
||||
devices = []
|
||||
names = ['iPhone', 'Galaxy S21', 'AirPods', 'Tile Tracker', 'Fitbit', 'Unknown']
|
||||
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({
|
||||
'address': mac,
|
||||
'name': random.choice(names),
|
||||
@@ -209,9 +209,7 @@ def config():
|
||||
'name': agent_name,
|
||||
'port': request.environ.get('SERVER_PORT', 8021),
|
||||
'push_enabled': False,
|
||||
'modes_enabled': {m: True for m in [
|
||||
'pager', 'sensor', 'adsb', 'ais', 'wifi', 'bluetooth'
|
||||
]}
|
||||
'modes_enabled': dict.fromkeys(['pager', 'sensor', 'adsb', 'ais', 'wifi', 'bluetooth'], True)
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -17,10 +17,7 @@ Requirements:
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import requests
|
||||
@@ -258,7 +255,7 @@ class SmokeTests:
|
||||
def run_all(self):
|
||||
"""Run all smoke tests."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"BLUETOOTH API SMOKE TESTS")
|
||||
print("BLUETOOTH API SMOKE TESTS")
|
||||
print(f"Target: {self.base_url}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
"""Tests for ACARS message translator."""
|
||||
|
||||
import pytest
|
||||
|
||||
from utils.acars_translator import (
|
||||
ACARS_LABELS,
|
||||
translate_label,
|
||||
classify_message_type,
|
||||
parse_position_report,
|
||||
parse_engine_data,
|
||||
parse_weather_data,
|
||||
parse_oooi,
|
||||
parse_position_report,
|
||||
parse_weather_data,
|
||||
translate_label,
|
||||
translate_message,
|
||||
)
|
||||
|
||||
|
||||
# --- translate_label ---
|
||||
|
||||
class TestTranslateLabel:
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock, patch, PropertyMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
+16
-12
@@ -10,23 +10,27 @@ Tests cover:
|
||||
|
||||
import json
|
||||
import os
|
||||
import pytest
|
||||
import tempfile
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
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__))))
|
||||
|
||||
from utils.agent_client import (
|
||||
AgentClient, AgentHTTPError, AgentConnectionError, create_client_from_agent
|
||||
)
|
||||
from utils.agent_client import AgentClient, AgentConnectionError, AgentHTTPError, create_client_from_agent
|
||||
from utils.database import (
|
||||
init_db, get_db_path, create_agent, get_agent, get_agent_by_name,
|
||||
list_agents, update_agent, delete_agent, store_push_payload,
|
||||
get_recent_payloads, cleanup_old_payloads
|
||||
create_agent,
|
||||
delete_agent,
|
||||
get_agent,
|
||||
get_agent_by_name,
|
||||
get_recent_payloads,
|
||||
init_db,
|
||||
list_agents,
|
||||
store_push_payload,
|
||||
update_agent,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AgentConfig Tests
|
||||
# =============================================================================
|
||||
@@ -559,8 +563,8 @@ class TestAgentClientIntegration:
|
||||
@pytest.fixture
|
||||
def mock_agent(self):
|
||||
"""Start mock agent server for testing."""
|
||||
|
||||
from tests.mock_agent import app as mock_app
|
||||
import threading
|
||||
|
||||
# Run mock agent in background
|
||||
mock_app.config['TESTING'] = True
|
||||
|
||||
@@ -19,13 +19,14 @@ Skip live tests:
|
||||
|
||||
import json
|
||||
import os
|
||||
import pytest
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
|
||||
+11
-15
@@ -10,14 +10,13 @@ Tests cover:
|
||||
- Error handling and edge cases
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
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__))))
|
||||
|
||||
@@ -34,10 +33,8 @@ def mode_manager():
|
||||
yield manager
|
||||
# Cleanup: stop all modes
|
||||
for mode in list(manager.running_modes.keys()):
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
manager.stop_mode(mode)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -139,14 +136,13 @@ class TestModeLifecycle:
|
||||
mock_popen, mock_proc = mock_subprocess
|
||||
|
||||
# Mock glob for CSV file detection
|
||||
with patch('glob.glob', return_value=[]):
|
||||
with patch('tempfile.mkdtemp', return_value='/tmp/test'):
|
||||
result = mode_manager.start_mode('wifi', {
|
||||
'interface': 'wlan0',
|
||||
'scan_type': 'quick'
|
||||
})
|
||||
# Quick scan returns data directly
|
||||
assert result['status'] in ['started', 'error', 'success']
|
||||
with patch('glob.glob', return_value=[]), patch('tempfile.mkdtemp', return_value='/tmp/test'):
|
||||
result = mode_manager.start_mode('wifi', {
|
||||
'interface': 'wlan0',
|
||||
'scan_type': 'quick'
|
||||
})
|
||||
# Quick scan returns data directly
|
||||
assert result['status'] in ['started', 'error', 'success']
|
||||
|
||||
def test_bluetooth_mode_lifecycle(self, mode_manager, mock_subprocess, mock_tools):
|
||||
"""Bluetooth mode should start and stop cleanly."""
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Tests for main application routes."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_index_page(client):
|
||||
|
||||
@@ -6,7 +6,6 @@ import pytest
|
||||
|
||||
from routes.aprs import parse_aprs_packet
|
||||
|
||||
|
||||
_BASE_PACKET = "N0CALL-9>APRS,TCPIP*:@092345z4903.50N/07201.75W_090/000g005t077"
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import pytest
|
||||
import json
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from routes.bluetooth import bluetooth_bp, classify_bt_device, detect_tracker
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
"""Unit tests for Bluetooth device aggregation."""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from utils.bluetooth.aggregator import DeviceAggregator
|
||||
from utils.bluetooth.models import BTObservation, BTDeviceAggregate
|
||||
from utils.bluetooth.constants import (
|
||||
MAX_RSSI_SAMPLES,
|
||||
DEVICE_STALE_TIMEOUT as DEVICE_STALE_SECONDS,
|
||||
)
|
||||
from utils.bluetooth.constants import (
|
||||
MAX_RSSI_SAMPLES,
|
||||
)
|
||||
from utils.bluetooth.models import BTObservation
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""API endpoint tests for Bluetooth v2 routes."""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch, PropertyMock
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
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
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
"""Unit tests for Bluetooth heuristic detection."""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from utils.bluetooth.heuristics import HeuristicsEngine
|
||||
from utils.bluetooth.models import BTDeviceAggregate
|
||||
import pytest
|
||||
|
||||
from utils.bluetooth.constants import (
|
||||
BEACON_INTERVAL_MAX_VARIANCE as HEURISTIC_BEACON_VARIANCE_THRESHOLD,
|
||||
)
|
||||
from utils.bluetooth.constants import (
|
||||
PERSISTENT_MIN_SEEN_COUNT as HEURISTIC_PERSISTENT_MIN_SEEN,
|
||||
)
|
||||
from utils.bluetooth.constants import (
|
||||
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,
|
||||
)
|
||||
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
|
||||
|
||||
@@ -5,21 +5,21 @@ Tests device key stability, EMA smoothing, distance estimation,
|
||||
band classification, and ring buffer functionality.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from utils.bluetooth.device_key import (
|
||||
extract_key_type,
|
||||
generate_device_key,
|
||||
is_randomized_mac,
|
||||
extract_key_type,
|
||||
)
|
||||
from utils.bluetooth.distance import (
|
||||
DistanceEstimator,
|
||||
ProximityBand,
|
||||
RSSI_THRESHOLD_FAR,
|
||||
RSSI_THRESHOLD_IMMEDIATE,
|
||||
RSSI_THRESHOLD_NEAR,
|
||||
RSSI_THRESHOLD_FAR,
|
||||
DistanceEstimator,
|
||||
ProximityBand,
|
||||
)
|
||||
from utils.bluetooth.ring_buffer import RingBuffer
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Tests for configuration module."""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
|
||||
class TestConfigEnvVars:
|
||||
@@ -9,7 +7,7 @@ class TestConfigEnvVars:
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test that default values are set."""
|
||||
from config import PORT, HOST, DEBUG
|
||||
from config import DEBUG, HOST, PORT
|
||||
|
||||
assert PORT == 5050
|
||||
assert HOST == '0.0.0.0'
|
||||
@@ -22,6 +20,7 @@ class TestConfigEnvVars:
|
||||
|
||||
# Re-import to get new values
|
||||
import importlib
|
||||
|
||||
import config
|
||||
importlib.reload(config)
|
||||
|
||||
@@ -38,6 +37,7 @@ class TestConfigEnvVars:
|
||||
monkeypatch.setenv('INTERCEPT_PORT', 'invalid')
|
||||
|
||||
import importlib
|
||||
|
||||
import config
|
||||
importlib.reload(config)
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ Tests cover:
|
||||
|
||||
import json
|
||||
import os
|
||||
import pytest
|
||||
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__))))
|
||||
|
||||
@@ -51,6 +52,7 @@ def setup_db(tmp_path):
|
||||
def app(setup_db):
|
||||
"""Create Flask app with controller blueprint."""
|
||||
from flask import Flask
|
||||
|
||||
from routes.controller import controller_bp
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user