mirror of
https://github.com/smittix/intercept.git
synced 2026-07-29 02:58:11 -07:00
Switch scanner to rtl_power sweep
This commit is contained in:
+270
-69
@@ -47,7 +47,7 @@ scanner_running = False
|
|||||||
scanner_lock = threading.Lock()
|
scanner_lock = threading.Lock()
|
||||||
scanner_paused = False
|
scanner_paused = False
|
||||||
scanner_current_freq = 0.0
|
scanner_current_freq = 0.0
|
||||||
scanner_config = {
|
scanner_config = {
|
||||||
'start_freq': 88.0,
|
'start_freq': 88.0,
|
||||||
'end_freq': 108.0,
|
'end_freq': 108.0,
|
||||||
'step': 0.1,
|
'step': 0.1,
|
||||||
@@ -57,9 +57,10 @@ scanner_config = {
|
|||||||
'scan_delay': 0.1, # Seconds between frequency hops (keep low for fast scanning)
|
'scan_delay': 0.1, # Seconds between frequency hops (keep low for fast scanning)
|
||||||
'device': 0,
|
'device': 0,
|
||||||
'gain': 40,
|
'gain': 40,
|
||||||
'bias_t': False, # Bias-T power for external LNA
|
'bias_t': False, # Bias-T power for external LNA
|
||||||
'sdr_type': 'rtlsdr', # SDR type: rtlsdr, hackrf, airspy, limesdr, sdrplay
|
'sdr_type': 'rtlsdr', # SDR type: rtlsdr, hackrf, airspy, limesdr, sdrplay
|
||||||
}
|
'scan_method': 'power', # power (rtl_power) or classic (rtl_fm hop)
|
||||||
|
}
|
||||||
|
|
||||||
# Activity log
|
# Activity log
|
||||||
activity_log: List[Dict] = []
|
activity_log: List[Dict] = []
|
||||||
@@ -74,9 +75,14 @@ scanner_queue: queue.Queue = queue.Queue(maxsize=100)
|
|||||||
# HELPER FUNCTIONS
|
# HELPER FUNCTIONS
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
def find_rtl_fm() -> str | None:
|
def find_rtl_fm() -> str | None:
|
||||||
"""Find rtl_fm binary."""
|
"""Find rtl_fm binary."""
|
||||||
return shutil.which('rtl_fm')
|
return shutil.which('rtl_fm')
|
||||||
|
|
||||||
|
|
||||||
|
def find_rtl_power() -> str | None:
|
||||||
|
"""Find rtl_power binary."""
|
||||||
|
return shutil.which('rtl_power')
|
||||||
|
|
||||||
|
|
||||||
def find_rx_fm() -> str | None:
|
def find_rx_fm() -> str | None:
|
||||||
@@ -119,7 +125,7 @@ def add_activity_log(event_type: str, frequency: float, details: str = ''):
|
|||||||
# SCANNER IMPLEMENTATION
|
# SCANNER IMPLEMENTATION
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
def scanner_loop():
|
def scanner_loop():
|
||||||
"""Main scanner loop - scans frequencies looking for signals."""
|
"""Main scanner loop - scans frequencies looking for signals."""
|
||||||
global scanner_running, scanner_paused, scanner_current_freq, scanner_skip_signal
|
global scanner_running, scanner_paused, scanner_current_freq, scanner_skip_signal
|
||||||
global audio_process, audio_rtl_process, audio_running, audio_frequency
|
global audio_process, audio_rtl_process, audio_running, audio_frequency
|
||||||
@@ -362,11 +368,184 @@ def scanner_loop():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Scanner loop error: {e}")
|
logger.error(f"Scanner loop error: {e}")
|
||||||
finally:
|
finally:
|
||||||
scanner_running = False
|
scanner_running = False
|
||||||
_stop_audio_stream()
|
_stop_audio_stream()
|
||||||
add_activity_log('scanner_stop', scanner_current_freq, 'Scanner stopped')
|
add_activity_log('scanner_stop', scanner_current_freq, 'Scanner stopped')
|
||||||
logger.info("Scanner thread stopped")
|
logger.info("Scanner thread stopped")
|
||||||
|
|
||||||
|
|
||||||
|
def scanner_loop_power():
|
||||||
|
"""Power sweep scanner using rtl_power to detect peaks."""
|
||||||
|
global scanner_running, scanner_paused, scanner_current_freq
|
||||||
|
|
||||||
|
logger.info("Power sweep scanner thread started")
|
||||||
|
add_activity_log('scanner_start', scanner_config['start_freq'],
|
||||||
|
f"Power sweep {scanner_config['start_freq']}-{scanner_config['end_freq']} MHz")
|
||||||
|
|
||||||
|
rtl_power_path = find_rtl_power()
|
||||||
|
if not rtl_power_path:
|
||||||
|
logger.error("rtl_power not found")
|
||||||
|
add_activity_log('error', 0, 'rtl_power not found')
|
||||||
|
scanner_running = False
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
while scanner_running:
|
||||||
|
if scanner_paused:
|
||||||
|
time.sleep(0.1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
start_mhz = scanner_config['start_freq']
|
||||||
|
end_mhz = scanner_config['end_freq']
|
||||||
|
step_khz = scanner_config['step']
|
||||||
|
gain = scanner_config['gain']
|
||||||
|
device = scanner_config['device']
|
||||||
|
squelch = scanner_config['squelch']
|
||||||
|
mod = scanner_config['modulation']
|
||||||
|
|
||||||
|
# Configure sweep
|
||||||
|
bin_hz = max(1000, int(step_khz * 1000))
|
||||||
|
start_hz = int(start_mhz * 1e6)
|
||||||
|
end_hz = int(end_mhz * 1e6)
|
||||||
|
# Integration time per sweep (seconds)
|
||||||
|
integration = max(0.3, min(1.0, scanner_config.get('scan_delay', 0.5)))
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
rtl_power_path,
|
||||||
|
'-f', f'{start_hz}:{end_hz}:{bin_hz}',
|
||||||
|
'-i', f'{integration}',
|
||||||
|
'-1',
|
||||||
|
'-g', str(gain),
|
||||||
|
'-d', str(device),
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
||||||
|
stdout, _ = proc.communicate(timeout=15)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
proc.kill()
|
||||||
|
stdout = b''
|
||||||
|
|
||||||
|
if not stdout:
|
||||||
|
time.sleep(0.2)
|
||||||
|
continue
|
||||||
|
|
||||||
|
lines = stdout.decode(errors='ignore').splitlines()
|
||||||
|
for line in lines:
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
parts = [p.strip() for p in line.split(',')]
|
||||||
|
# Find start_hz token
|
||||||
|
start_idx = None
|
||||||
|
for i, tok in enumerate(parts):
|
||||||
|
try:
|
||||||
|
val = float(tok)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if val > 1e5:
|
||||||
|
start_idx = i
|
||||||
|
break
|
||||||
|
if start_idx is None or len(parts) < start_idx + 6:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
sweep_start = float(parts[start_idx])
|
||||||
|
sweep_end = float(parts[start_idx + 1])
|
||||||
|
sweep_bin = float(parts[start_idx + 2])
|
||||||
|
bin_values = [float(v) for v in parts[start_idx + 4:] if v]
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not bin_values:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Noise floor (median)
|
||||||
|
sorted_vals = sorted(bin_values)
|
||||||
|
mid = len(sorted_vals) // 2
|
||||||
|
noise_floor = sorted_vals[mid]
|
||||||
|
|
||||||
|
# SNR threshold (dB) based on squelch
|
||||||
|
snr_threshold = 8 + (squelch * 0.3)
|
||||||
|
|
||||||
|
# Emit progress updates (throttled)
|
||||||
|
emit_stride = max(1, len(bin_values) // 60)
|
||||||
|
for idx, val in enumerate(bin_values):
|
||||||
|
if idx % emit_stride != 0 and idx != len(bin_values) - 1:
|
||||||
|
continue
|
||||||
|
freq_hz = sweep_start + sweep_bin * idx
|
||||||
|
scanner_current_freq = freq_hz / 1e6
|
||||||
|
snr = val - noise_floor
|
||||||
|
level = int(max(0, snr) * 100)
|
||||||
|
threshold = int(snr_threshold * 100)
|
||||||
|
try:
|
||||||
|
scanner_queue.put_nowait({
|
||||||
|
'type': 'scan_update',
|
||||||
|
'frequency': scanner_current_freq,
|
||||||
|
'level': level,
|
||||||
|
'threshold': threshold,
|
||||||
|
'detected': snr >= snr_threshold
|
||||||
|
})
|
||||||
|
except queue.Full:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Detect peaks (clusters above threshold)
|
||||||
|
peaks = []
|
||||||
|
in_cluster = False
|
||||||
|
peak_idx = None
|
||||||
|
peak_val = None
|
||||||
|
cluster_start = 0
|
||||||
|
for idx, val in enumerate(bin_values):
|
||||||
|
snr = val - noise_floor
|
||||||
|
if snr >= snr_threshold:
|
||||||
|
if not in_cluster:
|
||||||
|
in_cluster = True
|
||||||
|
cluster_start = idx
|
||||||
|
peak_idx = idx
|
||||||
|
peak_val = val
|
||||||
|
else:
|
||||||
|
if val > peak_val:
|
||||||
|
peak_val = val
|
||||||
|
peak_idx = idx
|
||||||
|
else:
|
||||||
|
if in_cluster and peak_idx is not None:
|
||||||
|
peaks.append((peak_idx, peak_val))
|
||||||
|
in_cluster = False
|
||||||
|
peak_idx = None
|
||||||
|
peak_val = None
|
||||||
|
if in_cluster and peak_idx is not None:
|
||||||
|
peaks.append((peak_idx, peak_val))
|
||||||
|
|
||||||
|
for idx, val in peaks:
|
||||||
|
freq_hz = sweep_start + sweep_bin * (idx + 0.5)
|
||||||
|
freq_mhz = freq_hz / 1e6
|
||||||
|
snr = val - noise_floor
|
||||||
|
level = int(max(0, snr) * 100)
|
||||||
|
threshold = int(snr_threshold * 100)
|
||||||
|
add_activity_log('signal_found', freq_mhz,
|
||||||
|
f'Peak detected at {freq_mhz:.3f} MHz ({mod.upper()})')
|
||||||
|
try:
|
||||||
|
scanner_queue.put_nowait({
|
||||||
|
'type': 'signal_found',
|
||||||
|
'frequency': freq_mhz,
|
||||||
|
'modulation': mod,
|
||||||
|
'audio_streaming': False,
|
||||||
|
'level': level,
|
||||||
|
'threshold': threshold
|
||||||
|
})
|
||||||
|
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)))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Power sweep scanner error: {e}")
|
||||||
|
finally:
|
||||||
|
scanner_running = False
|
||||||
|
add_activity_log('scanner_stop', scanner_current_freq, 'Scanner stopped')
|
||||||
|
logger.info("Power sweep scanner thread stopped")
|
||||||
|
|
||||||
|
|
||||||
def _start_audio_stream(frequency: float, modulation: str):
|
def _start_audio_stream(frequency: float, modulation: str):
|
||||||
@@ -567,12 +746,13 @@ def _stop_audio_stream_internal():
|
|||||||
# API ENDPOINTS
|
# API ENDPOINTS
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
@listening_post_bp.route('/tools')
|
@listening_post_bp.route('/tools')
|
||||||
def check_tools() -> Response:
|
def check_tools() -> Response:
|
||||||
"""Check for required tools."""
|
"""Check for required tools."""
|
||||||
rtl_fm = find_rtl_fm()
|
rtl_fm = find_rtl_fm()
|
||||||
rx_fm = find_rx_fm()
|
rtl_power = find_rtl_power()
|
||||||
ffmpeg = find_ffmpeg()
|
rx_fm = find_rx_fm()
|
||||||
|
ffmpeg = find_ffmpeg()
|
||||||
|
|
||||||
# Determine which SDR types are supported
|
# Determine which SDR types are supported
|
||||||
supported_sdr_types = []
|
supported_sdr_types = []
|
||||||
@@ -582,19 +762,20 @@ def check_tools() -> Response:
|
|||||||
# rx_fm from SoapySDR supports these types
|
# rx_fm from SoapySDR supports these types
|
||||||
supported_sdr_types.extend(['hackrf', 'airspy', 'limesdr', 'sdrplay'])
|
supported_sdr_types.extend(['hackrf', 'airspy', 'limesdr', 'sdrplay'])
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'rtl_fm': rtl_fm is not None,
|
'rtl_fm': rtl_fm is not None,
|
||||||
'rx_fm': rx_fm is not None,
|
'rtl_power': rtl_power is not None,
|
||||||
'ffmpeg': ffmpeg is not None,
|
'rx_fm': rx_fm is not None,
|
||||||
'available': (rtl_fm is not None or rx_fm is not None) and ffmpeg is not None,
|
'ffmpeg': ffmpeg is not None,
|
||||||
'supported_sdr_types': supported_sdr_types
|
'available': (rtl_fm is not None or rx_fm is not None) and ffmpeg is not None,
|
||||||
})
|
'supported_sdr_types': supported_sdr_types
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@listening_post_bp.route('/scanner/start', methods=['POST'])
|
@listening_post_bp.route('/scanner/start', methods=['POST'])
|
||||||
def start_scanner() -> Response:
|
def start_scanner() -> Response:
|
||||||
"""Start the frequency scanner."""
|
"""Start the frequency scanner."""
|
||||||
global scanner_thread, scanner_running, scanner_config
|
global scanner_thread, scanner_running, scanner_config
|
||||||
|
|
||||||
with scanner_lock:
|
with scanner_lock:
|
||||||
if scanner_running:
|
if scanner_running:
|
||||||
@@ -607,22 +788,23 @@ def start_scanner() -> Response:
|
|||||||
|
|
||||||
# Update scanner config
|
# Update scanner config
|
||||||
try:
|
try:
|
||||||
scanner_config['start_freq'] = float(data.get('start_freq', 88.0))
|
scanner_config['start_freq'] = float(data.get('start_freq', 88.0))
|
||||||
scanner_config['end_freq'] = float(data.get('end_freq', 108.0))
|
scanner_config['end_freq'] = float(data.get('end_freq', 108.0))
|
||||||
scanner_config['step'] = float(data.get('step', 0.1))
|
scanner_config['step'] = float(data.get('step', 0.1))
|
||||||
scanner_config['modulation'] = str(data.get('modulation', 'wfm')).lower()
|
scanner_config['modulation'] = str(data.get('modulation', 'wfm')).lower()
|
||||||
scanner_config['squelch'] = int(data.get('squelch', 20))
|
scanner_config['squelch'] = int(data.get('squelch', 20))
|
||||||
scanner_config['dwell_time'] = float(data.get('dwell_time', 3.0))
|
scanner_config['dwell_time'] = float(data.get('dwell_time', 3.0))
|
||||||
scanner_config['scan_delay'] = float(data.get('scan_delay', 0.5))
|
scanner_config['scan_delay'] = float(data.get('scan_delay', 0.5))
|
||||||
scanner_config['device'] = int(data.get('device', 0))
|
scanner_config['device'] = int(data.get('device', 0))
|
||||||
scanner_config['gain'] = int(data.get('gain', 40))
|
scanner_config['gain'] = int(data.get('gain', 40))
|
||||||
scanner_config['bias_t'] = bool(data.get('bias_t', False))
|
scanner_config['bias_t'] = bool(data.get('bias_t', False))
|
||||||
scanner_config['sdr_type'] = str(data.get('sdr_type', 'rtlsdr')).lower()
|
scanner_config['sdr_type'] = str(data.get('sdr_type', 'rtlsdr')).lower()
|
||||||
except (ValueError, TypeError) as e:
|
scanner_config['scan_method'] = str(data.get('scan_method', '')).lower().strip()
|
||||||
return jsonify({
|
except (ValueError, TypeError) as e:
|
||||||
'status': 'error',
|
return jsonify({
|
||||||
'message': f'Invalid parameter: {e}'
|
'status': 'error',
|
||||||
}), 400
|
'message': f'Invalid parameter: {e}'
|
||||||
|
}), 400
|
||||||
|
|
||||||
# Validate
|
# Validate
|
||||||
if scanner_config['start_freq'] >= scanner_config['end_freq']:
|
if scanner_config['start_freq'] >= scanner_config['end_freq']:
|
||||||
@@ -631,30 +813,49 @@ def start_scanner() -> Response:
|
|||||||
'message': 'start_freq must be less than end_freq'
|
'message': 'start_freq must be less than end_freq'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# Check tools based on SDR type
|
# Decide scan method
|
||||||
sdr_type = scanner_config['sdr_type']
|
if not scanner_config['scan_method']:
|
||||||
if sdr_type == 'rtlsdr':
|
scanner_config['scan_method'] = 'power' if find_rtl_power() else 'classic'
|
||||||
if not find_rtl_fm():
|
|
||||||
return jsonify({
|
sdr_type = scanner_config['sdr_type']
|
||||||
'status': 'error',
|
|
||||||
'message': 'rtl_fm not found. Install rtl-sdr tools.'
|
# Power scan only supports RTL-SDR for now
|
||||||
}), 503
|
if scanner_config['scan_method'] == 'power':
|
||||||
else:
|
if sdr_type != 'rtlsdr' or not find_rtl_power():
|
||||||
if not find_rx_fm():
|
scanner_config['scan_method'] = 'classic'
|
||||||
return jsonify({
|
|
||||||
'status': 'error',
|
# Check tools based on chosen method
|
||||||
'message': f'rx_fm not found. Install SoapySDR utilities for {sdr_type}.'
|
if scanner_config['scan_method'] == 'power':
|
||||||
}), 503
|
if not find_rtl_power():
|
||||||
|
return jsonify({
|
||||||
# Start scanner thread
|
'status': 'error',
|
||||||
scanner_running = True
|
'message': 'rtl_power not found. Install rtl-sdr tools.'
|
||||||
scanner_thread = threading.Thread(target=scanner_loop, daemon=True)
|
}), 503
|
||||||
scanner_thread.start()
|
scanner_running = True
|
||||||
|
scanner_thread = threading.Thread(target=scanner_loop_power, daemon=True)
|
||||||
return jsonify({
|
scanner_thread.start()
|
||||||
'status': 'started',
|
else:
|
||||||
'config': scanner_config
|
if sdr_type == 'rtlsdr':
|
||||||
})
|
if not find_rtl_fm():
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'rtl_fm not found. Install rtl-sdr tools.'
|
||||||
|
}), 503
|
||||||
|
else:
|
||||||
|
if not find_rx_fm():
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'rx_fm not found. Install SoapySDR utilities for {sdr_type}.'
|
||||||
|
}), 503
|
||||||
|
|
||||||
|
scanner_running = True
|
||||||
|
scanner_thread = threading.Thread(target=scanner_loop, daemon=True)
|
||||||
|
scanner_thread.start()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'started',
|
||||||
|
'config': scanner_config
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@listening_post_bp.route('/scanner/stop', methods=['POST'])
|
@listening_post_bp.route('/scanner/stop', methods=['POST'])
|
||||||
|
|||||||
Reference in New Issue
Block a user