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;