mirror of
https://github.com/smittix/intercept.git
synced 2026-07-18 06:18:11 -07:00
Update listening post handling
This commit is contained in:
+164
-94
@@ -96,16 +96,27 @@ def find_rx_fm() -> str | None:
|
|||||||
return shutil.which('rx_fm')
|
return shutil.which('rx_fm')
|
||||||
|
|
||||||
|
|
||||||
def find_ffmpeg() -> str | None:
|
def find_ffmpeg() -> str | None:
|
||||||
"""Find ffmpeg for audio encoding."""
|
"""Find ffmpeg for audio encoding."""
|
||||||
return shutil.which('ffmpeg')
|
return shutil.which('ffmpeg')
|
||||||
|
|
||||||
|
|
||||||
|
VALID_MODULATIONS = ['fm', 'wfm', 'am', 'usb', 'lsb']
|
||||||
|
|
||||||
def add_activity_log(event_type: str, frequency: float, details: str = ''):
|
|
||||||
"""Add entry to activity log."""
|
def normalize_modulation(value: str) -> str:
|
||||||
with activity_log_lock:
|
"""Normalize and validate modulation string."""
|
||||||
|
mod = str(value or '').lower().strip()
|
||||||
|
if mod not in VALID_MODULATIONS:
|
||||||
|
raise ValueError(f'Invalid modulation. Use: {", ".join(VALID_MODULATIONS)}')
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def add_activity_log(event_type: str, frequency: float, details: str = ''):
|
||||||
|
"""Add entry to activity log."""
|
||||||
|
with activity_log_lock:
|
||||||
entry = {
|
entry = {
|
||||||
'timestamp': datetime.utcnow().isoformat() + 'Z',
|
'timestamp': datetime.utcnow().isoformat() + 'Z',
|
||||||
'type': event_type,
|
'type': event_type,
|
||||||
@@ -723,56 +734,106 @@ def _start_audio_stream(frequency: float, modulation: str):
|
|||||||
'pipe:1'
|
'pipe:1'
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Use shell pipe for reliable streaming
|
# Use subprocess piping for reliable streaming.
|
||||||
# Log stderr to temp files for error diagnosis
|
# Log stderr to temp files for error diagnosis.
|
||||||
rtl_stderr_log = '/tmp/rtl_fm_stderr.log'
|
rtl_stderr_log = '/tmp/rtl_fm_stderr.log'
|
||||||
ffmpeg_stderr_log = '/tmp/ffmpeg_stderr.log'
|
ffmpeg_stderr_log = '/tmp/ffmpeg_stderr.log'
|
||||||
shell_cmd = f"{' '.join(sdr_cmd)} 2>{rtl_stderr_log} | {' '.join(encoder_cmd)} 2>{ffmpeg_stderr_log}"
|
logger.info(f"Starting audio: {frequency} MHz, mod={modulation}, device={scanner_config['device']}")
|
||||||
logger.info(f"Starting audio: {frequency} MHz, mod={modulation}, device={scanner_config['device']}")
|
|
||||||
|
# Retry loop for USB device contention (device may not be
|
||||||
# Retry loop for USB device contention (device may not be
|
# released immediately after a previous process exits)
|
||||||
# released immediately after a previous process exits)
|
max_attempts = 3
|
||||||
max_attempts = 3
|
for attempt in range(max_attempts):
|
||||||
for attempt in range(max_attempts):
|
audio_rtl_process = None
|
||||||
audio_rtl_process = None # Not used in shell mode
|
audio_process = None
|
||||||
audio_process = subprocess.Popen(
|
rtl_err_handle = None
|
||||||
shell_cmd,
|
ffmpeg_err_handle = None
|
||||||
shell=True,
|
try:
|
||||||
stdout=subprocess.PIPE,
|
rtl_err_handle = open(rtl_stderr_log, 'w')
|
||||||
stderr=subprocess.PIPE,
|
ffmpeg_err_handle = open(ffmpeg_stderr_log, 'w')
|
||||||
bufsize=0,
|
audio_rtl_process = subprocess.Popen(
|
||||||
start_new_session=True # Create new process group for clean shutdown
|
sdr_cmd,
|
||||||
)
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=rtl_err_handle,
|
||||||
# Brief delay to check if process started successfully
|
bufsize=0,
|
||||||
time.sleep(0.3)
|
start_new_session=True # Create new process group for clean shutdown
|
||||||
|
)
|
||||||
if audio_process.poll() is not None:
|
audio_process = subprocess.Popen(
|
||||||
# Read stderr from temp files
|
encoder_cmd,
|
||||||
rtl_stderr = ''
|
stdin=audio_rtl_process.stdout,
|
||||||
ffmpeg_stderr = ''
|
stdout=subprocess.PIPE,
|
||||||
try:
|
stderr=ffmpeg_err_handle,
|
||||||
with open(rtl_stderr_log, 'r') as f:
|
bufsize=0,
|
||||||
rtl_stderr = f.read().strip()
|
start_new_session=True # Create new process group for clean shutdown
|
||||||
except Exception:
|
)
|
||||||
pass
|
if audio_rtl_process.stdout:
|
||||||
try:
|
audio_rtl_process.stdout.close()
|
||||||
with open(ffmpeg_stderr_log, 'r') as f:
|
finally:
|
||||||
ffmpeg_stderr = f.read().strip()
|
if rtl_err_handle:
|
||||||
except Exception:
|
rtl_err_handle.close()
|
||||||
pass
|
if ffmpeg_err_handle:
|
||||||
|
ffmpeg_err_handle.close()
|
||||||
if 'usb_claim_interface' in rtl_stderr and attempt < max_attempts - 1:
|
|
||||||
logger.warning(f"USB device busy (attempt {attempt + 1}/{max_attempts}), waiting for release...")
|
# Brief delay to check if process started successfully
|
||||||
time.sleep(1.0)
|
time.sleep(0.3)
|
||||||
continue
|
|
||||||
|
if (audio_rtl_process and audio_rtl_process.poll() is not None) or (
|
||||||
logger.error(f"Audio pipeline exited immediately. rtl_fm stderr: {rtl_stderr}, ffmpeg stderr: {ffmpeg_stderr}")
|
audio_process and audio_process.poll() is not None
|
||||||
return
|
):
|
||||||
|
# Read stderr from temp files
|
||||||
# Pipeline started successfully
|
rtl_stderr = ''
|
||||||
break
|
ffmpeg_stderr = ''
|
||||||
|
try:
|
||||||
|
with open(rtl_stderr_log, 'r') as f:
|
||||||
|
rtl_stderr = f.read().strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
with open(ffmpeg_stderr_log, 'r') as f:
|
||||||
|
ffmpeg_stderr = f.read().strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'usb_claim_interface' in rtl_stderr and attempt < max_attempts - 1:
|
||||||
|
logger.warning(f"USB device busy (attempt {attempt + 1}/{max_attempts}), waiting for release...")
|
||||||
|
if audio_process:
|
||||||
|
try:
|
||||||
|
audio_process.terminate()
|
||||||
|
audio_process.wait(timeout=0.5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if audio_rtl_process:
|
||||||
|
try:
|
||||||
|
audio_rtl_process.terminate()
|
||||||
|
audio_rtl_process.wait(timeout=0.5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(1.0)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if audio_process and audio_process.poll() is None:
|
||||||
|
try:
|
||||||
|
audio_process.terminate()
|
||||||
|
audio_process.wait(timeout=0.5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if audio_rtl_process and audio_rtl_process.poll() is None:
|
||||||
|
try:
|
||||||
|
audio_rtl_process.terminate()
|
||||||
|
audio_rtl_process.wait(timeout=0.5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
audio_process = None
|
||||||
|
audio_rtl_process = None
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
f"Audio pipeline exited immediately. rtl_fm stderr: {rtl_stderr}, ffmpeg stderr: {ffmpeg_stderr}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Pipeline started successfully
|
||||||
|
break
|
||||||
|
|
||||||
# Validate that audio is producing data quickly
|
# Validate that audio is producing data quickly
|
||||||
try:
|
try:
|
||||||
@@ -797,28 +858,38 @@ def _stop_audio_stream():
|
|||||||
_stop_audio_stream_internal()
|
_stop_audio_stream_internal()
|
||||||
|
|
||||||
|
|
||||||
def _stop_audio_stream_internal():
|
def _stop_audio_stream_internal():
|
||||||
"""Internal stop (must hold lock)."""
|
"""Internal stop (must hold lock)."""
|
||||||
global audio_process, audio_rtl_process, audio_running, audio_frequency
|
global audio_process, audio_rtl_process, audio_running, audio_frequency
|
||||||
|
|
||||||
# Set flag first to stop any streaming
|
# Set flag first to stop any streaming
|
||||||
audio_running = False
|
audio_running = False
|
||||||
audio_frequency = 0.0
|
audio_frequency = 0.0
|
||||||
|
|
||||||
# Kill the shell process and its children
|
# Kill the pipeline processes and their groups
|
||||||
if audio_process:
|
if audio_process:
|
||||||
try:
|
try:
|
||||||
# Kill entire process group (rtl_fm, ffmpeg, shell)
|
# Kill entire process group (SDR demod + ffmpeg)
|
||||||
try:
|
try:
|
||||||
os.killpg(os.getpgid(audio_process.pid), signal.SIGKILL)
|
os.killpg(os.getpgid(audio_process.pid), signal.SIGKILL)
|
||||||
except (ProcessLookupError, PermissionError):
|
except (ProcessLookupError, PermissionError):
|
||||||
audio_process.kill()
|
audio_process.kill()
|
||||||
audio_process.wait(timeout=0.5)
|
audio_process.wait(timeout=0.5)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
audio_process = None
|
if audio_rtl_process:
|
||||||
audio_rtl_process = None
|
try:
|
||||||
|
try:
|
||||||
|
os.killpg(os.getpgid(audio_rtl_process.pid), signal.SIGKILL)
|
||||||
|
except (ProcessLookupError, PermissionError):
|
||||||
|
audio_rtl_process.kill()
|
||||||
|
audio_rtl_process.wait(timeout=0.5)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
audio_process = None
|
||||||
|
audio_rtl_process = None
|
||||||
|
|
||||||
# Kill any orphaned rtl_fm, rtl_power, and ffmpeg processes
|
# Kill any orphaned rtl_fm, rtl_power, and ffmpeg processes
|
||||||
for proc_pattern in ['rtl_fm', 'rtl_power']:
|
for proc_pattern in ['rtl_fm', 'rtl_power']:
|
||||||
@@ -891,7 +962,7 @@ def start_scanner() -> Response:
|
|||||||
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'] = normalize_modulation(data.get('modulation', 'wfm'))
|
||||||
scanner_config['squelch'] = int(data.get('squelch', 0))
|
scanner_config['squelch'] = int(data.get('squelch', 0))
|
||||||
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))
|
||||||
@@ -1073,9 +1144,15 @@ def update_scanner_config() -> Response:
|
|||||||
scanner_config['dwell_time'] = int(data['dwell_time'])
|
scanner_config['dwell_time'] = int(data['dwell_time'])
|
||||||
updated.append(f"dwell={data['dwell_time']}s")
|
updated.append(f"dwell={data['dwell_time']}s")
|
||||||
|
|
||||||
if 'modulation' in data:
|
if 'modulation' in data:
|
||||||
scanner_config['modulation'] = str(data['modulation']).lower()
|
try:
|
||||||
updated.append(f"mod={data['modulation']}")
|
scanner_config['modulation'] = normalize_modulation(data['modulation'])
|
||||||
|
updated.append(f"mod={data['modulation']}")
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': str(e)
|
||||||
|
}), 400
|
||||||
|
|
||||||
if updated:
|
if updated:
|
||||||
logger.info(f"Scanner config updated: {', '.join(updated)}")
|
logger.info(f"Scanner config updated: {', '.join(updated)}")
|
||||||
@@ -1197,7 +1274,7 @@ def start_audio() -> Response:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
frequency = float(data.get('frequency', 0))
|
frequency = float(data.get('frequency', 0))
|
||||||
modulation = str(data.get('modulation', 'wfm')).lower()
|
modulation = normalize_modulation(data.get('modulation', 'wfm'))
|
||||||
squelch = int(data.get('squelch', 0))
|
squelch = int(data.get('squelch', 0))
|
||||||
gain = int(data.get('gain', 40))
|
gain = int(data.get('gain', 40))
|
||||||
device = int(data.get('device', 0))
|
device = int(data.get('device', 0))
|
||||||
@@ -1214,13 +1291,6 @@ def start_audio() -> Response:
|
|||||||
'message': 'frequency is required'
|
'message': 'frequency is required'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
valid_mods = ['fm', 'wfm', 'am', 'usb', 'lsb']
|
|
||||||
if modulation not in valid_mods:
|
|
||||||
return jsonify({
|
|
||||||
'status': 'error',
|
|
||||||
'message': f'Invalid modulation. Use: {", ".join(valid_mods)}'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
valid_sdr_types = ['rtlsdr', 'hackrf', 'airspy', 'limesdr', 'sdrplay']
|
valid_sdr_types = ['rtlsdr', 'hackrf', 'airspy', 'limesdr', 'sdrplay']
|
||||||
if sdr_type not in valid_sdr_types:
|
if sdr_type not in valid_sdr_types:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|||||||
Reference in New Issue
Block a user