mirror of
https://github.com/smittix/intercept.git
synced 2026-05-30 00:29:27 -07:00
Fix listening post agent mode and UI sync
Agent scanner fixes: - Use non-blocking I/O with select/fcntl to prevent blocking reads - Pass dwell_time parameter through to scanner function - Add freqs_scanned counter to status and data endpoints - Improve SDR test process cleanup with kill() fallback Frontend listening post fixes: - Add setListeningPostRunning for UI sync when switching to agent - Fix button ID (radioScanBtn not scannerStartBtn) - Handle nested data structure from controller proxy - Update freqs_scanned and signal_count from polling data - Disable listen button for agent mode (audio can't stream over HTTP) Add listening_post to agents.js uiSetters map for mode sync. Live testing completed: - Sensor mode: works via agent - WiFi quick scan: works via agent - Listening post: works via agent (AM airband, WFM broadcast tested) - Signal detection: confirmed working via agent Testing ongoing - modes not yet tested via agent: - Pager, ADS-B, AIS, ACARS, APRS, DSC, RTL-AMR, TSCM, Bluetooth
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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 = '<span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="6" width="12" height="12"/></svg></span>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 = '<span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg></span>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;
|
||||
|
||||
Reference in New Issue
Block a user