mirror of
https://github.com/smittix/intercept.git
synced 2026-06-17 09:59:47 -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_paused = False
|
||||
scanner_current_freq = 0.0
|
||||
scanner_config = {
|
||||
scanner_config = {
|
||||
'start_freq': 88.0,
|
||||
'end_freq': 108.0,
|
||||
'step': 0.1,
|
||||
@@ -57,9 +57,10 @@ scanner_config = {
|
||||
'scan_delay': 0.1, # Seconds between frequency hops (keep low for fast scanning)
|
||||
'device': 0,
|
||||
'gain': 40,
|
||||
'bias_t': False, # Bias-T power for external LNA
|
||||
'sdr_type': 'rtlsdr', # SDR type: rtlsdr, hackrf, airspy, limesdr, sdrplay
|
||||
}
|
||||
'bias_t': False, # Bias-T power for external LNA
|
||||
'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: List[Dict] = []
|
||||
@@ -74,9 +75,14 @@ scanner_queue: queue.Queue = queue.Queue(maxsize=100)
|
||||
# HELPER FUNCTIONS
|
||||
# ============================================
|
||||
|
||||
def find_rtl_fm() -> str | None:
|
||||
"""Find rtl_fm binary."""
|
||||
return shutil.which('rtl_fm')
|
||||
def find_rtl_fm() -> str | None:
|
||||
"""Find rtl_fm binary."""
|
||||
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:
|
||||
@@ -119,7 +125,7 @@ def add_activity_log(event_type: str, frequency: float, details: str = ''):
|
||||
# SCANNER IMPLEMENTATION
|
||||
# ============================================
|
||||
|
||||
def scanner_loop():
|
||||
def scanner_loop():
|
||||
"""Main scanner loop - scans frequencies looking for signals."""
|
||||
global scanner_running, scanner_paused, scanner_current_freq, scanner_skip_signal
|
||||
global audio_process, audio_rtl_process, audio_running, audio_frequency
|
||||
@@ -362,11 +368,184 @@ def scanner_loop():
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Scanner loop error: {e}")
|
||||
finally:
|
||||
scanner_running = False
|
||||
_stop_audio_stream()
|
||||
add_activity_log('scanner_stop', scanner_current_freq, 'Scanner stopped')
|
||||
logger.info("Scanner thread stopped")
|
||||
finally:
|
||||
scanner_running = False
|
||||
_stop_audio_stream()
|
||||
add_activity_log('scanner_stop', scanner_current_freq, 'Scanner 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):
|
||||
@@ -567,12 +746,13 @@ def _stop_audio_stream_internal():
|
||||
# API ENDPOINTS
|
||||
# ============================================
|
||||
|
||||
@listening_post_bp.route('/tools')
|
||||
def check_tools() -> Response:
|
||||
"""Check for required tools."""
|
||||
rtl_fm = find_rtl_fm()
|
||||
rx_fm = find_rx_fm()
|
||||
ffmpeg = find_ffmpeg()
|
||||
@listening_post_bp.route('/tools')
|
||||
def check_tools() -> Response:
|
||||
"""Check for required tools."""
|
||||
rtl_fm = find_rtl_fm()
|
||||
rtl_power = find_rtl_power()
|
||||
rx_fm = find_rx_fm()
|
||||
ffmpeg = find_ffmpeg()
|
||||
|
||||
# Determine which SDR types are supported
|
||||
supported_sdr_types = []
|
||||
@@ -582,19 +762,20 @@ def check_tools() -> Response:
|
||||
# rx_fm from SoapySDR supports these types
|
||||
supported_sdr_types.extend(['hackrf', 'airspy', 'limesdr', 'sdrplay'])
|
||||
|
||||
return jsonify({
|
||||
'rtl_fm': rtl_fm is not None,
|
||||
'rx_fm': rx_fm is not None,
|
||||
'ffmpeg': ffmpeg is not None,
|
||||
'available': (rtl_fm is not None or rx_fm is not None) and ffmpeg is not None,
|
||||
'supported_sdr_types': supported_sdr_types
|
||||
})
|
||||
return jsonify({
|
||||
'rtl_fm': rtl_fm is not None,
|
||||
'rtl_power': rtl_power is not None,
|
||||
'rx_fm': rx_fm is not None,
|
||||
'ffmpeg': ffmpeg is not None,
|
||||
'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'])
|
||||
def start_scanner() -> Response:
|
||||
"""Start the frequency scanner."""
|
||||
global scanner_thread, scanner_running, scanner_config
|
||||
def start_scanner() -> Response:
|
||||
"""Start the frequency scanner."""
|
||||
global scanner_thread, scanner_running, scanner_config
|
||||
|
||||
with scanner_lock:
|
||||
if scanner_running:
|
||||
@@ -607,22 +788,23 @@ def start_scanner() -> Response:
|
||||
|
||||
# Update scanner config
|
||||
try:
|
||||
scanner_config['start_freq'] = float(data.get('start_freq', 88.0))
|
||||
scanner_config['end_freq'] = float(data.get('end_freq', 108.0))
|
||||
scanner_config['step'] = float(data.get('step', 0.1))
|
||||
scanner_config['modulation'] = str(data.get('modulation', 'wfm')).lower()
|
||||
scanner_config['squelch'] = int(data.get('squelch', 20))
|
||||
scanner_config['dwell_time'] = float(data.get('dwell_time', 3.0))
|
||||
scanner_config['scan_delay'] = float(data.get('scan_delay', 0.5))
|
||||
scanner_config['device'] = int(data.get('device', 0))
|
||||
scanner_config['gain'] = int(data.get('gain', 40))
|
||||
scanner_config['bias_t'] = bool(data.get('bias_t', False))
|
||||
scanner_config['sdr_type'] = str(data.get('sdr_type', 'rtlsdr')).lower()
|
||||
except (ValueError, TypeError) as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Invalid parameter: {e}'
|
||||
}), 400
|
||||
scanner_config['start_freq'] = float(data.get('start_freq', 88.0))
|
||||
scanner_config['end_freq'] = float(data.get('end_freq', 108.0))
|
||||
scanner_config['step'] = float(data.get('step', 0.1))
|
||||
scanner_config['modulation'] = str(data.get('modulation', 'wfm')).lower()
|
||||
scanner_config['squelch'] = int(data.get('squelch', 20))
|
||||
scanner_config['dwell_time'] = float(data.get('dwell_time', 3.0))
|
||||
scanner_config['scan_delay'] = float(data.get('scan_delay', 0.5))
|
||||
scanner_config['device'] = int(data.get('device', 0))
|
||||
scanner_config['gain'] = int(data.get('gain', 40))
|
||||
scanner_config['bias_t'] = bool(data.get('bias_t', False))
|
||||
scanner_config['sdr_type'] = str(data.get('sdr_type', 'rtlsdr')).lower()
|
||||
scanner_config['scan_method'] = str(data.get('scan_method', '')).lower().strip()
|
||||
except (ValueError, TypeError) as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Invalid parameter: {e}'
|
||||
}), 400
|
||||
|
||||
# Validate
|
||||
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'
|
||||
}), 400
|
||||
|
||||
# Check tools based on SDR type
|
||||
sdr_type = scanner_config['sdr_type']
|
||||
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
|
||||
|
||||
# Start scanner thread
|
||||
scanner_running = True
|
||||
scanner_thread = threading.Thread(target=scanner_loop, daemon=True)
|
||||
scanner_thread.start()
|
||||
|
||||
return jsonify({
|
||||
'status': 'started',
|
||||
'config': scanner_config
|
||||
})
|
||||
# Decide scan method
|
||||
if not scanner_config['scan_method']:
|
||||
scanner_config['scan_method'] = 'power' if find_rtl_power() else 'classic'
|
||||
|
||||
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'
|
||||
|
||||
# Check tools based on chosen method
|
||||
if scanner_config['scan_method'] == 'power':
|
||||
if not find_rtl_power():
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'rtl_power not found. Install rtl-sdr tools.'
|
||||
}), 503
|
||||
scanner_running = True
|
||||
scanner_thread = threading.Thread(target=scanner_loop_power, daemon=True)
|
||||
scanner_thread.start()
|
||||
else:
|
||||
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'])
|
||||
|
||||
Reference in New Issue
Block a user