diff --git a/intercept_agent.py b/intercept_agent.py index de6b7d3..3fd1ea7 100644 --- a/intercept_agent.py +++ b/intercept_agent.py @@ -808,6 +808,7 @@ class ModeManager: elif mode == 'listening_post': info['signal_count'] = len(getattr(self, 'listening_post_activity', [])) info['current_freq'] = getattr(self, 'listening_post_current_freq', 0) + info['freqs_scanned'] = getattr(self, 'listening_post_freqs_scanned', 0) return info return {'running': False} @@ -849,6 +850,8 @@ class ModeManager: data['data'] = { 'activity': getattr(self, 'listening_post_activity', []), 'current_freq': getattr(self, 'listening_post_current_freq', 0), + 'freqs_scanned': getattr(self, 'listening_post_freqs_scanned', 0), + 'signal_count': len(getattr(self, 'listening_post_activity', [])), } elif mode == 'pager': # Return recent pager messages @@ -3267,12 +3270,14 @@ class ModeManager: squelch = params.get('squelch', 20) device = params.get('device', '0') gain = params.get('gain', '40') + dwell_time = params.get('dwell_time', 1.0) rtl_fm_path = self._get_tool_path('rtl_fm') if not rtl_fm_path: return {'status': 'error', 'message': 'rtl_fm not found'} # Quick SDR availability check - try to run rtl_fm briefly + test_proc = None try: test_proc = subprocess.Popen( [rtl_fm_path, '-f', f'{start_freq}M', '-d', str(device), '-g', str(gain)], @@ -3283,9 +3288,21 @@ class ModeManager: if test_proc.poll() is not None: stderr = test_proc.stderr.read().decode('utf-8', errors='ignore') return {'status': 'error', 'message': f'SDR not available: {stderr[:200]}'} + # SDR is available - terminate test process test_proc.terminate() - test_proc.wait(timeout=1) + try: + test_proc.wait(timeout=2) + except subprocess.TimeoutExpired: + test_proc.kill() + test_proc.wait(timeout=1) except Exception as e: + # Ensure test process is killed on any error + if test_proc and test_proc.poll() is None: + test_proc.kill() + try: + test_proc.wait(timeout=1) + except Exception: + pass return {'status': 'error', 'message': f'SDR check failed: {str(e)}'} # Initialize state @@ -3297,7 +3314,7 @@ class ModeManager: thread = threading.Thread( target=self._listening_post_scanner, args=(float(start_freq), float(end_freq), float(step), - modulation, int(squelch), str(device), str(gain)), + modulation, int(squelch), str(device), str(gain), float(dwell_time)), daemon=True ) thread.start() @@ -3310,20 +3327,28 @@ class ModeManager: 'end_freq': end_freq, 'step': step, 'modulation': modulation, + 'dwell_time': dwell_time, 'note': 'Provides signal detection events, not full FFT data', 'gps_enabled': gps_manager.is_running } def _listening_post_scanner(self, start_freq: float, end_freq: float, step: float, modulation: str, squelch: int, - device: str, gain: str): + device: str, gain: str, dwell_time: float = 1.0): """Scan frequency range and report signal detections.""" + import select + import os + import fcntl + mode = 'listening_post' stop_event = self.stop_events.get(mode) rtl_fm_path = self._get_tool_path('rtl_fm') current_freq = start_freq scan_direction = 1 + self.listening_post_freqs_scanned = 0 + + logger.info(f"Listening post scanner starting: {start_freq}-{end_freq} MHz, step {step}, dwell {dwell_time}s") while not (stop_event and stop_event.is_set()): self.listening_post_current_freq = current_freq @@ -3345,31 +3370,45 @@ class ModeManager: stderr=subprocess.PIPE, ) + # Set stdout to non-blocking + fd = proc.stdout.fileno() + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) + signal_detected = False start_time = time.time() - while time.time() - start_time < 1.0: + while time.time() - start_time < dwell_time: if stop_event and stop_event.is_set(): break - data = proc.stdout.read(2205) - if data and len(data) > 10: - # Simple signal detection via audio level + + # Use select for non-blocking read with timeout + ready, _, _ = select.select([proc.stdout], [], [], 0.1) + if ready: try: - samples = [int.from_bytes(data[i:i+2], 'little', signed=True) - for i in range(0, min(len(data)-1, 1000), 2)] - if samples: - rms = (sum(s*s for s in samples) / len(samples)) ** 0.5 - if rms > 500: - signal_detected = True - break - except Exception: + data = proc.stdout.read(2205) + if data and len(data) > 10: + # Simple signal detection via audio level + try: + samples = [int.from_bytes(data[i:i+2], 'little', signed=True) + for i in range(0, min(len(data)-1, 1000), 2)] + if samples: + rms = (sum(s*s for s in samples) / len(samples)) ** 0.5 + if rms > 500: + signal_detected = True + except Exception: + pass + except (IOError, BlockingIOError): pass proc.terminate() try: - proc.wait(timeout=1) + proc.wait(timeout=2) except subprocess.TimeoutExpired: proc.kill() + proc.wait(timeout=1) + + self.listening_post_freqs_scanned += 1 if signal_detected: event = { diff --git a/static/js/core/agents.js b/static/js/core/agents.js index c29779f..cdee596 100644 --- a/static/js/core/agents.js +++ b/static/js/core/agents.js @@ -679,7 +679,8 @@ function syncModeUI(mode, isRunning, agentId = null) { 'adsb': 'setADSBRunning', 'wifi': 'setWiFiRunning', 'bluetooth': 'setBluetoothRunning', - 'acars': 'setAcarsRunning' + 'acars': 'setAcarsRunning', + 'listening_post': 'setListeningPostRunning' }; const setterName = uiSetters[mode]; diff --git a/static/js/modes/listening-post.js b/static/js/modes/listening-post.js index 2e1cb74..3308433 100644 --- a/static/js/modes/listening-post.js +++ b/static/js/modes/listening-post.js @@ -153,6 +153,9 @@ function startScanner() { const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; listeningPostCurrentAgent = isAgentMode ? currentAgent : null; + // Disable listen button for agent mode (audio can't stream over HTTP) + updateListenButtonState(isAgentMode); + if (startFreq >= endFreq) { if (typeof showNotification === 'function') { showNotification('Scanner Error', 'End frequency must be greater than start'); @@ -281,6 +284,9 @@ function stopScanner() { isScannerPaused = false; scannerSignalActive = false; + // Re-enable listen button (will be in local mode after stop) + updateListenButtonState(false); + // Clear polling timer if (listeningPostPollTimer) { clearInterval(listeningPostPollTimer); @@ -461,6 +467,9 @@ function startListeningPostPolling() { if (listeningPostPollTimer) return; lastListeningPostActivityCount = 0; + // Disable listen button for agent mode (audio can't stream over HTTP) + updateListenButtonState(true); + const pollInterval = 2000; listeningPostPollTimer = setInterval(async () => { if (!isScannerRunning || !listeningPostCurrentAgent) { @@ -475,7 +484,9 @@ function startListeningPostPolling() { const data = await response.json(); const result = data.result || data; - const modeData = result.data || {}; + // Controller returns nested structure: data.data.data for agent mode data + const outerData = result.data || {}; + const modeData = outerData.data || outerData; // Process activity from polling response const activity = modeData.activity || []; @@ -502,6 +513,19 @@ function startListeningPostPolling() { frequency: modeData.current_freq }); } + + // Update freqs scanned counter from agent data + if (modeData.freqs_scanned !== undefined) { + const freqsEl = document.getElementById('mainFreqsScanned'); + if (freqsEl) freqsEl.textContent = modeData.freqs_scanned; + scannerFreqsScanned = modeData.freqs_scanned; + } + + // Update signal count from agent data + if (modeData.signal_count !== undefined) { + const signalEl = document.getElementById('mainSignalCount'); + if (signalEl) signalEl.textContent = modeData.signal_count; + } } catch (err) { console.error('Listening Post polling error:', err); } @@ -675,6 +699,27 @@ function handleSignalLost(data) { addScannerLogEntry(logTitle, `${data.frequency.toFixed(3)} MHz`, logType); } +/** + * Update listen button state based on agent mode + * Audio streaming isn't practical over HTTP so disable for remote agents + */ +function updateListenButtonState(isAgentMode) { + const listenBtn = document.getElementById('radioListenBtn'); + if (!listenBtn) return; + + if (isAgentMode) { + listenBtn.disabled = true; + listenBtn.style.opacity = '0.5'; + listenBtn.style.cursor = 'not-allowed'; + listenBtn.title = 'Audio listening not available for remote agents'; + } else { + listenBtn.disabled = false; + listenBtn.style.opacity = '1'; + listenBtn.style.cursor = 'pointer'; + listenBtn.title = 'Listen to current frequency'; + } +} + function updateScannerDisplay(mode, color) { const modeLabel = document.getElementById('scannerModeLabel'); if (modeLabel) { @@ -2385,6 +2430,64 @@ function addSidebarRecentSignal(freq, mod) { // Load bookmarks on init document.addEventListener('DOMContentLoaded', loadFrequencyBookmarks); +/** + * Set listening post running state from external source (agent sync). + * Called by syncModeUI in agents.js when switching to an agent that already has scan running. + */ +function setListeningPostRunning(isRunning, agentId = null) { + console.log(`[ListeningPost] setListeningPostRunning: ${isRunning}, agent: ${agentId}`); + + isScannerRunning = isRunning; + + if (isRunning && agentId !== null && agentId !== 'local') { + // Agent has scan running - sync UI and start polling + listeningPostCurrentAgent = agentId; + + // Update main scan button (radioScanBtn is the actual ID) + const radioScanBtn = document.getElementById('radioScanBtn'); + if (radioScanBtn) { + radioScanBtn.innerHTML = 'STOP'; + radioScanBtn.style.background = 'var(--accent-red)'; + radioScanBtn.style.borderColor = 'var(--accent-red)'; + } + + // Update status display + updateScannerDisplay('SCANNING', 'var(--accent-green)'); + + // Disable listen button (can't stream audio from agent) + updateListenButtonState(true); + + // Start polling for agent data + startListeningPostPolling(); + } else if (!isRunning) { + // Not running - reset UI + listeningPostCurrentAgent = null; + + // Reset scan button + const radioScanBtn = document.getElementById('radioScanBtn'); + if (radioScanBtn) { + radioScanBtn.innerHTML = 'SCAN'; + radioScanBtn.style.background = ''; + radioScanBtn.style.borderColor = ''; + } + + // Update status + updateScannerDisplay('IDLE', 'var(--text-secondary)'); + + // Re-enable listen button + updateListenButtonState(false); + + // Clear polling + if (listeningPostPollTimer) { + clearInterval(listeningPostPollTimer); + listeningPostPollTimer = null; + } + } +} + +// Export for agent sync +window.setListeningPostRunning = setListeningPostRunning; + // Export functions for HTML onclick handlers window.toggleDirectListen = toggleDirectListen; window.startDirectListen = startDirectListen;