v2.26.0: fix SSE fanout crash and branded logo FOUC

- Fix SSE fanout thread AttributeError when source queue is None during
  interpreter shutdown by snapshotting to local variable with null guard
- Fix branded "i" logo rendering oversized on first page load (FOUC) by
  adding inline width/height to SVG elements across 10 templates
- Bump version to 2.26.0 in config.py, pyproject.toml, and CHANGELOG.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-03-13 11:51:27 +00:00
parent 00362bcd57
commit e00fbfddc1
183 changed files with 2006 additions and 4243 deletions

View File

@@ -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

View File

@@ -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(

View File

@@ -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,
@@ -406,18 +408,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 +437,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 +451,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 +471,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 +481,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 +664,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 +675,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 +698,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 +715,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 +750,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 +1002,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 +1171,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()
@@ -1251,10 +1230,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 +1279,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 +1313,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 +1344,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 +1393,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 +1544,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)

View File

@@ -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 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:

View File

@@ -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')

View File

@@ -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
@@ -1961,10 +1950,8 @@ def stop_aprs() -> Response:
# Close PTY master fd
if hasattr(app_module, 'aprs_master_fd') and app_module.aprs_master_fd is not None:
try:
with contextlib.suppress(OSError):
os.close(app_module.aprs_master_fd)
except OSError:
pass
app_module.aprs_master_fd = None
app_module.aprs_process = None
@@ -2099,7 +2086,7 @@ def scan_aprs_spectrum() -> Response:
return api_error('rtl_power did not produce output file', 500)
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:

View File

@@ -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")

View File

@@ -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
})

View File

@@ -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')

View File

@@ -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')

View File

@@ -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)

View File

@@ -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')

View File

@@ -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

View File

@@ -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,

View File

@@ -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
)

View File

@@ -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 ''

View File

@@ -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)

View File

@@ -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
# ============================================

View File

@@ -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))

View File

@@ -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')

View File

@@ -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

View File

@@ -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 (

View File

@@ -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

View File

@@ -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 (

View File

@@ -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()

View File

@@ -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

View File

@@ -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')

View File

@@ -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')

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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')

View File

@@ -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')

View File

@@ -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',

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -22,7 +22,7 @@ from flask import Blueprint, Response, jsonify, request
from utils.constants import SSE_KEEPALIVE_INTERVAL, SSE_QUEUE_TIMEOUT
from utils.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:

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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)

View File

@@ -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 (

View File

@@ -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:

View File

@@ -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,

View File

@@ -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(

View File

@@ -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,

View File

@@ -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',

View File

@@ -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(

View File

@@ -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'

View File

@@ -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')

View File

@@ -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')