mirror of
https://github.com/smittix/intercept.git
synced 2026-07-11 19:18:13 -07:00
Robust APRS decoder with deadlock fixes and spectrum scanning
Major improvements to APRS decoding reliability: Process piping fixes (prevent deadlocks): - rtl_fm stderr -> DEVNULL (was blocking on unbuffered stderr) - decoder stderr -> STDOUT (merged, single stream to read) - decoder uses text=True, bufsize=1 for line-buffered reading - Proper EOF detection in stream thread rtl_fm command improvements: - Use -M nfm (narrowband FM) for APRS - Add -E dc (DC blocking filter) for cleaner audio - Add -A fast (fast AGC) for packet bursts - Sample rate 22050 Hz matches direwolf -r 22050 Parsing robustness: - Strip direwolf bracket prefixes like "[0.4] " before parsing - Handle multimon-ng "AFSK1200:" prefix - Better error handling for early process exit New /aprs/spectrum endpoint: - Runs rtl_power to scan around APRS frequency - Returns peak detection, noise floor, signal analysis - Provides advice for antenna/signal debugging - Supports region selection and custom frequency Also added UK to region list (same freq as Europe: 144.800 MHz) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+279
-30
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
@@ -12,6 +13,7 @@ import tempfile
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from subprocess import DEVNULL, PIPE, STDOUT
|
||||||
from typing import Generator, Optional
|
from typing import Generator, Optional
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request, Response
|
from flask import Blueprint, jsonify, request, Response
|
||||||
@@ -33,6 +35,7 @@ aprs_bp = Blueprint('aprs', __name__, url_prefix='/aprs')
|
|||||||
APRS_FREQUENCIES = {
|
APRS_FREQUENCIES = {
|
||||||
'north_america': '144.390',
|
'north_america': '144.390',
|
||||||
'europe': '144.800',
|
'europe': '144.800',
|
||||||
|
'uk': '144.800',
|
||||||
'australia': '145.175',
|
'australia': '145.175',
|
||||||
'new_zealand': '144.575',
|
'new_zealand': '144.575',
|
||||||
'argentina': '144.930',
|
'argentina': '144.930',
|
||||||
@@ -63,6 +66,11 @@ def find_rtl_fm() -> Optional[str]:
|
|||||||
return shutil.which('rtl_fm')
|
return shutil.which('rtl_fm')
|
||||||
|
|
||||||
|
|
||||||
|
def find_rtl_power() -> Optional[str]:
|
||||||
|
"""Find rtl_power binary for spectrum scanning."""
|
||||||
|
return shutil.which('rtl_power')
|
||||||
|
|
||||||
|
|
||||||
# Path to direwolf config file
|
# Path to direwolf config file
|
||||||
DIREWOLF_CONFIG_PATH = os.path.join(tempfile.gettempdir(), 'intercept_direwolf.conf')
|
DIREWOLF_CONFIG_PATH = os.path.join(tempfile.gettempdir(), 'intercept_direwolf.conf')
|
||||||
|
|
||||||
@@ -268,22 +276,32 @@ def parse_weather(data: str) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def stream_aprs_output(rtl_process: subprocess.Popen, decoder_process: subprocess.Popen) -> None:
|
def stream_aprs_output(rtl_process: subprocess.Popen, decoder_process: subprocess.Popen) -> None:
|
||||||
"""Stream decoded APRS packets to queue."""
|
"""Stream decoded APRS packets to queue.
|
||||||
|
|
||||||
|
This function reads from the decoder's stdout (text mode, line-buffered).
|
||||||
|
The decoder's stderr is merged into stdout (STDOUT) to avoid deadlocks.
|
||||||
|
rtl_fm's stderr is sent to DEVNULL for the same reason.
|
||||||
|
"""
|
||||||
global aprs_packet_count, aprs_station_count, aprs_last_packet_time, aprs_stations
|
global aprs_packet_count, aprs_station_count, aprs_last_packet_time, aprs_stations
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app_module.aprs_queue.put({'type': 'status', 'status': 'started'})
|
app_module.aprs_queue.put({'type': 'status', 'status': 'started'})
|
||||||
|
|
||||||
for line in iter(decoder_process.stdout.readline, b''):
|
# Read line-by-line in text mode. Empty string '' signals EOF.
|
||||||
line = line.decode('utf-8', errors='replace').strip()
|
for line in iter(decoder_process.stdout.readline, ''):
|
||||||
|
line = line.strip()
|
||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# direwolf outputs decoded packets, multimon-ng outputs "AFSK1200: ..."
|
# multimon-ng prefixes decoded packets with "AFSK1200: "
|
||||||
if line.startswith('AFSK1200:'):
|
if line.startswith('AFSK1200:'):
|
||||||
line = line[9:].strip()
|
line = line[9:].strip()
|
||||||
|
|
||||||
# Skip non-packet lines
|
# direwolf often prefixes packets with "[0.4] " or similar audio level indicator
|
||||||
|
# Strip any leading bracket prefix like "[0.4] " before parsing
|
||||||
|
line = re.sub(r'^\[\d+\.\d+\]\s*', '', line)
|
||||||
|
|
||||||
|
# Skip non-packet lines (APRS format: CALL>PATH:DATA)
|
||||||
if '>' not in line or ':' not in line:
|
if '>' not in line or ':' not in line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -437,22 +455,27 @@ def start_aprs() -> Response:
|
|||||||
aprs_last_packet_time = None
|
aprs_last_packet_time = None
|
||||||
aprs_stations = {}
|
aprs_stations = {}
|
||||||
|
|
||||||
# Build rtl_fm command
|
# Build rtl_fm command for APRS (narrowband FM at 22050 Hz for AFSK1200)
|
||||||
freq_hz = f"{float(frequency)}M"
|
freq_hz = f"{float(frequency)}M"
|
||||||
rtl_cmd = [
|
rtl_cmd = [
|
||||||
rtl_fm_path,
|
rtl_fm_path,
|
||||||
'-f', freq_hz,
|
'-f', freq_hz,
|
||||||
'-M', 'fm', # FM demodulation
|
'-M', 'nfm', # Narrowband FM for APRS
|
||||||
'-s', '22050', # Sample rate for AFSK1200
|
'-s', '22050', # Sample rate matching direwolf -r 22050
|
||||||
|
'-E', 'dc', # Enable DC blocking filter for cleaner audio
|
||||||
|
'-A', 'fast', # Fast AGC for packet bursts
|
||||||
'-d', str(device),
|
'-d', str(device),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Gain: 0 means auto, otherwise set specific gain
|
||||||
if gain and str(gain) != '0':
|
if gain and str(gain) != '0':
|
||||||
rtl_cmd.extend(['-g', str(gain)])
|
rtl_cmd.extend(['-g', str(gain)])
|
||||||
|
|
||||||
|
# PPM frequency correction
|
||||||
if ppm and str(ppm) != '0':
|
if ppm and str(ppm) != '0':
|
||||||
rtl_cmd.extend(['-p', str(ppm)])
|
rtl_cmd.extend(['-p', str(ppm)])
|
||||||
|
|
||||||
# Explicitly output to stdout
|
# Output raw audio to stdout
|
||||||
rtl_cmd.append('-')
|
rtl_cmd.append('-')
|
||||||
|
|
||||||
# Build decoder command
|
# Build decoder command
|
||||||
@@ -461,59 +484,90 @@ def start_aprs() -> Response:
|
|||||||
config_path = create_direwolf_config()
|
config_path = create_direwolf_config()
|
||||||
|
|
||||||
# direwolf flags for receiving AFSK1200 from stdin:
|
# direwolf flags for receiving AFSK1200 from stdin:
|
||||||
# -c config = config file path
|
# -c config = config file path (must come before other options)
|
||||||
# -n 1 = mono audio channel
|
# -n 1 = mono audio channel
|
||||||
# -r 22050 = sample rate
|
# -r 22050 = sample rate (must match rtl_fm -s)
|
||||||
# -b 16 = 16-bit samples
|
# -b 16 = 16-bit signed samples
|
||||||
# -t 0 = disable text colors (for cleaner parsing)
|
# -t 0 = disable text colors (for cleaner parsing)
|
||||||
# -q h = quiet mode - suppress audio level heard line (keeps packet output)
|
# -q h = quiet: suppress audio level heard line (keeps packet output)
|
||||||
# - = read from stdin
|
# - = read audio from stdin (must be last argument)
|
||||||
decoder_cmd = [direwolf_path, '-c', config_path, '-n', '1', '-r', '22050', '-b', '16', '-t', '0', '-q', 'h', '-']
|
decoder_cmd = [
|
||||||
|
direwolf_path,
|
||||||
|
'-c', config_path,
|
||||||
|
'-n', '1',
|
||||||
|
'-r', '22050',
|
||||||
|
'-b', '16',
|
||||||
|
'-t', '0',
|
||||||
|
'-q', 'h',
|
||||||
|
'-'
|
||||||
|
]
|
||||||
decoder_name = 'direwolf'
|
decoder_name = 'direwolf'
|
||||||
else:
|
else:
|
||||||
|
# Fallback to multimon-ng
|
||||||
decoder_cmd = [multimon_path, '-t', 'raw', '-a', 'AFSK1200', '-']
|
decoder_cmd = [multimon_path, '-t', 'raw', '-a', 'AFSK1200', '-']
|
||||||
decoder_name = 'multimon-ng'
|
decoder_name = 'multimon-ng'
|
||||||
|
|
||||||
logger.info(f"Starting APRS decoder: {' '.join(rtl_cmd)} | {' '.join(decoder_cmd)}")
|
logger.info(f"Starting APRS decoder: {' '.join(rtl_cmd)} | {' '.join(decoder_cmd)}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Start rtl_fm
|
# Start rtl_fm with stdout piped to decoder.
|
||||||
|
# stderr goes to DEVNULL to prevent blocking (rtl_fm logs to stderr).
|
||||||
|
# NOTE: RTL-SDR Blog V4 may show offset-tuned frequency in logs - this is normal.
|
||||||
rtl_process = subprocess.Popen(
|
rtl_process = subprocess.Popen(
|
||||||
rtl_cmd,
|
rtl_cmd,
|
||||||
stdout=subprocess.PIPE,
|
stdout=PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=DEVNULL,
|
||||||
start_new_session=True
|
start_new_session=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start decoder with rtl_fm output
|
# Start decoder with stdin wired to rtl_fm's stdout.
|
||||||
|
# Use text mode with line buffering for reliable line-by-line reading.
|
||||||
|
# Merge stderr into stdout to avoid blocking on unbuffered stderr.
|
||||||
decoder_process = subprocess.Popen(
|
decoder_process = subprocess.Popen(
|
||||||
decoder_cmd,
|
decoder_cmd,
|
||||||
stdin=rtl_process.stdout,
|
stdin=rtl_process.stdout,
|
||||||
stdout=subprocess.PIPE,
|
stdout=PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
start_new_session=True
|
start_new_session=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Allow rtl_fm stdout to be consumed by decoder
|
# Close rtl_fm's stdout in parent so decoder owns it exclusively.
|
||||||
|
# This ensures proper EOF propagation when rtl_fm terminates.
|
||||||
rtl_process.stdout.close()
|
rtl_process.stdout.close()
|
||||||
|
|
||||||
# Wait briefly to check if processes started
|
# Wait briefly to check if processes started successfully
|
||||||
time.sleep(PROCESS_START_WAIT)
|
time.sleep(PROCESS_START_WAIT)
|
||||||
|
|
||||||
if rtl_process.poll() is not None:
|
if rtl_process.poll() is not None:
|
||||||
stderr = rtl_process.stderr.read().decode('utf-8', errors='replace') if rtl_process.stderr else ''
|
# rtl_fm exited early - something went wrong
|
||||||
error_msg = f'rtl_fm failed to start'
|
error_msg = f'rtl_fm failed to start (exit code {rtl_process.returncode})'
|
||||||
if stderr:
|
|
||||||
error_msg += f': {stderr[:200]}'
|
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
decoder_process.kill()
|
try:
|
||||||
|
decoder_process.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return jsonify({'status': 'error', 'message': error_msg}), 500
|
return jsonify({'status': 'error', 'message': error_msg}), 500
|
||||||
|
|
||||||
# Store reference to decoder process (for status checks)
|
if decoder_process.poll() is not None:
|
||||||
|
# Decoder exited early - capture any output
|
||||||
|
error_output = decoder_process.stdout.read()[:500] if decoder_process.stdout else ''
|
||||||
|
error_msg = f'{decoder_name} failed to start'
|
||||||
|
if error_output:
|
||||||
|
error_msg += f': {error_output}'
|
||||||
|
logger.error(error_msg)
|
||||||
|
try:
|
||||||
|
rtl_process.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return jsonify({'status': 'error', 'message': error_msg}), 500
|
||||||
|
|
||||||
|
# Store references for status checks and cleanup
|
||||||
app_module.aprs_process = decoder_process
|
app_module.aprs_process = decoder_process
|
||||||
app_module.aprs_rtl_process = rtl_process
|
app_module.aprs_rtl_process = rtl_process
|
||||||
|
|
||||||
# Start output streaming thread
|
# Start background thread to read decoder output and push to queue
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
target=stream_aprs_output,
|
target=stream_aprs_output,
|
||||||
args=(rtl_process, decoder_process),
|
args=(rtl_process, decoder_process),
|
||||||
@@ -595,3 +649,198 @@ def stream_aprs() -> Response:
|
|||||||
def get_frequencies() -> Response:
|
def get_frequencies() -> Response:
|
||||||
"""Get APRS frequencies by region."""
|
"""Get APRS frequencies by region."""
|
||||||
return jsonify(APRS_FREQUENCIES)
|
return jsonify(APRS_FREQUENCIES)
|
||||||
|
|
||||||
|
|
||||||
|
@aprs_bp.route('/spectrum', methods=['GET', 'POST'])
|
||||||
|
def scan_aprs_spectrum() -> Response:
|
||||||
|
"""Scan spectrum around APRS frequency for signal visibility debugging.
|
||||||
|
|
||||||
|
This endpoint runs rtl_power briefly to detect signal activity near the
|
||||||
|
APRS frequency. Useful for headless/remote debugging to verify antenna
|
||||||
|
and SDR are receiving signals.
|
||||||
|
|
||||||
|
Query params or JSON body:
|
||||||
|
device: SDR device index (default: 0)
|
||||||
|
gain: Gain in dB, 0=auto (default: 0)
|
||||||
|
region: Region for frequency lookup (default: europe)
|
||||||
|
frequency: Override frequency in MHz (optional)
|
||||||
|
duration: Scan duration in seconds (default: 10, max: 60)
|
||||||
|
|
||||||
|
Returns JSON with peak detection and signal analysis.
|
||||||
|
"""
|
||||||
|
rtl_power_path = find_rtl_power()
|
||||||
|
if not rtl_power_path:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'rtl_power not found. Install with: sudo apt install rtl-sdr'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Get parameters from JSON body or query args
|
||||||
|
if request.is_json:
|
||||||
|
data = request.json or {}
|
||||||
|
else:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
device = data.get('device', request.args.get('device', '0'))
|
||||||
|
gain = data.get('gain', request.args.get('gain', '0'))
|
||||||
|
region = data.get('region', request.args.get('region', 'europe'))
|
||||||
|
frequency = data.get('frequency', request.args.get('frequency'))
|
||||||
|
duration = data.get('duration', request.args.get('duration', '10'))
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
try:
|
||||||
|
device = validate_device_index(device)
|
||||||
|
gain = validate_gain(gain)
|
||||||
|
duration = min(max(int(duration), 5), 60) # Clamp 5-60 seconds
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
# Get center frequency
|
||||||
|
if frequency:
|
||||||
|
center_freq_mhz = float(frequency)
|
||||||
|
else:
|
||||||
|
center_freq_mhz = float(APRS_FREQUENCIES.get(region, '144.800'))
|
||||||
|
|
||||||
|
# Scan 20 kHz around center frequency (±10 kHz)
|
||||||
|
start_freq_mhz = center_freq_mhz - 0.010
|
||||||
|
end_freq_mhz = center_freq_mhz + 0.010
|
||||||
|
bin_size_hz = 200 # 200 Hz bins for good resolution
|
||||||
|
|
||||||
|
# Create temp file for rtl_power output
|
||||||
|
tmp_file = os.path.join(tempfile.gettempdir(), f'intercept_rtl_power_{os.getpid()}.csv')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Build rtl_power command
|
||||||
|
# Format: rtl_power -f start:end:bin_size -d device -g gain -i interval -e duration output_file
|
||||||
|
rtl_power_cmd = [
|
||||||
|
rtl_power_path,
|
||||||
|
'-f', f'{start_freq_mhz}M:{end_freq_mhz}M:{bin_size_hz}',
|
||||||
|
'-d', str(device),
|
||||||
|
'-i', '1', # 1 second integration
|
||||||
|
'-e', f'{duration}s',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Gain: 0 means auto
|
||||||
|
if gain and str(gain) != '0':
|
||||||
|
rtl_power_cmd.extend(['-g', str(gain)])
|
||||||
|
|
||||||
|
rtl_power_cmd.append(tmp_file)
|
||||||
|
|
||||||
|
logger.info(f"Running spectrum scan: {' '.join(rtl_power_cmd)}")
|
||||||
|
|
||||||
|
# Run rtl_power with timeout
|
||||||
|
result = subprocess.run(
|
||||||
|
rtl_power_cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=duration + 15 # Allow extra time for startup/shutdown
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
error_msg = result.stderr[:200] if result.stderr else f'Exit code {result.returncode}'
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'rtl_power failed: {error_msg}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# Parse rtl_power CSV output
|
||||||
|
# Format: date, time, start_hz, end_hz, step_hz, samples, db1, db2, db3, ...
|
||||||
|
if not os.path.exists(tmp_file):
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'rtl_power did not produce output file'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
bins = []
|
||||||
|
with open(tmp_file, 'r') as f:
|
||||||
|
reader = csv.reader(f)
|
||||||
|
for row in reader:
|
||||||
|
if len(row) < 7:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
row_start_hz = float(row[2])
|
||||||
|
row_step_hz = float(row[4])
|
||||||
|
# dB values start at column 6
|
||||||
|
for i, db_str in enumerate(row[6:]):
|
||||||
|
db_val = float(db_str.strip())
|
||||||
|
freq_hz = row_start_hz + (i * row_step_hz)
|
||||||
|
bins.append({'freq_hz': freq_hz, 'db': db_val})
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not bins:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'No spectrum data collected. Check SDR connection and antenna.'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# Calculate statistics
|
||||||
|
db_values = [b['db'] for b in bins]
|
||||||
|
avg_db = sum(db_values) / len(db_values)
|
||||||
|
max_bin = max(bins, key=lambda x: x['db'])
|
||||||
|
min_db = min(db_values)
|
||||||
|
|
||||||
|
# Find peak near center frequency (within 5 kHz)
|
||||||
|
center_hz = center_freq_mhz * 1e6
|
||||||
|
near_center_bins = [b for b in bins if abs(b['freq_hz'] - center_hz) < 5000]
|
||||||
|
if near_center_bins:
|
||||||
|
peak_near_center = max(near_center_bins, key=lambda x: x['db'])
|
||||||
|
else:
|
||||||
|
peak_near_center = max_bin
|
||||||
|
|
||||||
|
# Signal analysis
|
||||||
|
peak_above_noise = peak_near_center['db'] - avg_db
|
||||||
|
signal_detected = peak_above_noise > 3 # 3 dB above noise floor
|
||||||
|
|
||||||
|
# Generate advice
|
||||||
|
if peak_above_noise < 1:
|
||||||
|
advice = "No signal detected near APRS frequency. Check antenna connection and orientation."
|
||||||
|
elif peak_above_noise < 3:
|
||||||
|
advice = "Weak signal detected. Consider improving antenna or reducing noise sources."
|
||||||
|
elif peak_above_noise < 6:
|
||||||
|
advice = "Moderate signal detected. Decoding should work for strong stations."
|
||||||
|
else:
|
||||||
|
advice = "Good signal detected. Decoding should work well."
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'scan_params': {
|
||||||
|
'center_freq_mhz': center_freq_mhz,
|
||||||
|
'start_freq_mhz': start_freq_mhz,
|
||||||
|
'end_freq_mhz': end_freq_mhz,
|
||||||
|
'bin_size_hz': bin_size_hz,
|
||||||
|
'duration_seconds': duration,
|
||||||
|
'device': device,
|
||||||
|
'gain': gain,
|
||||||
|
'region': region,
|
||||||
|
},
|
||||||
|
'results': {
|
||||||
|
'total_bins': len(bins),
|
||||||
|
'noise_floor_db': round(avg_db, 1),
|
||||||
|
'min_db': round(min_db, 1),
|
||||||
|
'peak_freq_mhz': round(max_bin['freq_hz'] / 1e6, 6),
|
||||||
|
'peak_db': round(max_bin['db'], 1),
|
||||||
|
'peak_near_aprs_freq_mhz': round(peak_near_center['freq_hz'] / 1e6, 6),
|
||||||
|
'peak_near_aprs_db': round(peak_near_center['db'], 1),
|
||||||
|
'signal_above_noise_db': round(peak_above_noise, 1),
|
||||||
|
'signal_detected': signal_detected,
|
||||||
|
},
|
||||||
|
'advice': advice,
|
||||||
|
})
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Spectrum scan timed out after {duration + 15} seconds'
|
||||||
|
}), 500
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Spectrum scan error: {e}")
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
finally:
|
||||||
|
# Cleanup temp file
|
||||||
|
try:
|
||||||
|
if os.path.exists(tmp_file):
|
||||||
|
os.remove(tmp_file)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user