Fix remote VDL2 streaming path and improve decoder reliability

This commit is contained in:
Smittix
2026-02-19 15:57:13 +00:00
parent bcce266dae
commit 0c7e705fee
3 changed files with 81 additions and 101 deletions
+35 -10
View File
@@ -2,7 +2,11 @@
from __future__ import annotations from __future__ import annotations
import io
import json import json
import os
import platform
import pty
import queue import queue
import shutil import shutil
import subprocess import subprocess
@@ -51,15 +55,20 @@ def find_dumpvdl2():
return shutil.which('dumpvdl2') return shutil.which('dumpvdl2')
def stream_vdl2_output(process: subprocess.Popen) -> None: def stream_vdl2_output(process: subprocess.Popen, is_text_mode: bool = False) -> None:
"""Stream dumpvdl2 JSON output to queue.""" """Stream dumpvdl2 JSON output to queue."""
global vdl2_message_count, vdl2_last_message_time global vdl2_message_count, vdl2_last_message_time
try: try:
app_module.vdl2_queue.put({'type': 'status', 'status': 'started'}) app_module.vdl2_queue.put({'type': 'status', 'status': 'started'})
for line in iter(process.stdout.readline, b''): # Use appropriate sentinel based on mode (text mode for pty on macOS)
line = line.decode('utf-8', errors='replace').strip() sentinel = '' if is_text_mode else b''
for line in iter(process.stdout.readline, sentinel):
if is_text_mode:
line = line.strip()
else:
line = line.decode('utf-8', errors='replace').strip()
if not line: if not line:
continue continue
@@ -243,12 +252,28 @@ def start_vdl2() -> Response:
logger.info(f"Starting VDL2 decoder: {' '.join(cmd)}") logger.info(f"Starting VDL2 decoder: {' '.join(cmd)}")
try: try:
process = subprocess.Popen( is_text_mode = False
cmd,
stdout=subprocess.PIPE, # On macOS, use pty to avoid stdout buffering issues
stderr=subprocess.PIPE, if platform.system() == 'Darwin':
start_new_session=True master_fd, slave_fd = pty.openpty()
) process = subprocess.Popen(
cmd,
stdout=slave_fd,
stderr=subprocess.PIPE,
start_new_session=True
)
os.close(slave_fd)
# Wrap master_fd as a text file for line-buffered reading
process.stdout = io.open(master_fd, 'r', buffering=1)
is_text_mode = True
else:
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True
)
# Wait briefly to check if process started # Wait briefly to check if process started
time.sleep(PROCESS_START_WAIT) time.sleep(PROCESS_START_WAIT)
@@ -273,7 +298,7 @@ def start_vdl2() -> Response:
# Start output streaming thread # Start output streaming thread
thread = threading.Thread( thread = threading.Thread(
target=stream_vdl2_output, target=stream_vdl2_output,
args=(process,), args=(process, is_text_mode),
daemon=True daemon=True
) )
thread.start() thread.start()
+2 -2
View File
@@ -485,7 +485,7 @@ async function syncLocalModeStates() {
*/ */
function showAgentModeWarnings(runningModes, modesDetail = {}) { function showAgentModeWarnings(runningModes, modesDetail = {}) {
// SDR modes that can't run simultaneously on same device // SDR modes that can't run simultaneously on same device
const sdrModes = ['sensor', 'pager', 'adsb', 'ais', 'acars', 'aprs', 'rtlamr', 'listening_post', 'tscm', 'dsc']; const sdrModes = ['sensor', 'pager', 'adsb', 'ais', 'acars', 'vdl2', 'aprs', 'rtlamr', 'listening_post', 'tscm', 'dsc'];
const runningSdrModes = runningModes.filter(m => sdrModes.includes(m)); const runningSdrModes = runningModes.filter(m => sdrModes.includes(m));
let warning = document.getElementById('agentModeWarning'); let warning = document.getElementById('agentModeWarning');
@@ -621,7 +621,7 @@ function checkAgentModeConflict(modeToStart, deviceToUse = null) {
return false; return false;
} }
const sdrModes = ['sensor', 'pager', 'adsb', 'ais', 'acars', 'aprs', 'rtlamr', 'listening_post', 'tscm', 'dsc']; const sdrModes = ['sensor', 'pager', 'adsb', 'ais', 'acars', 'vdl2', 'aprs', 'rtlamr', 'listening_post', 'tscm', 'dsc'];
// If we're trying to start an SDR mode // If we're trying to start an SDR mode
if (sdrModes.includes(modeToStart)) { if (sdrModes.includes(modeToStart)) {
+23 -68
View File
@@ -3879,36 +3879,37 @@ sudo make install</code>
function startVdl2Stream(isAgentMode = false) { function startVdl2Stream(isAgentMode = false) {
if (vdl2EventSource) vdl2EventSource.close(); if (vdl2EventSource) vdl2EventSource.close();
// Use different stream endpoint for agent mode // For remote agent mode, stream directly from the selected agent via controller proxy.
const streamUrl = isAgentMode ? '/controller/stream/all' : '/vdl2/stream'; // This does not depend on push ingestion being enabled.
const streamUrl = isAgentMode && vdl2CurrentAgent !== null
? `/controller/agents/${vdl2CurrentAgent}/vdl2/stream`
: '/vdl2/stream';
vdl2EventSource = new EventSource(streamUrl); vdl2EventSource = new EventSource(streamUrl);
vdl2EventSource.onmessage = function(e) { vdl2EventSource.onmessage = function(e) {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
let message = null;
if (isAgentMode) { // Backward compatibility: handle wrapped multi-agent payloads if encountered.
// Handle multi-agent stream format if (data.scan_type === 'vdl2' && data.payload && data.payload.type === 'vdl2') {
if (data.scan_type === 'vdl2' && data.payload) { message = data.payload;
const payload = data.payload; if (isAgentMode) {
if (payload.type === 'vdl2') { message.agent_name = data.agent_name || message.agent_name || 'Remote Agent';
vdl2MessageCount++;
if (typeof stats !== 'undefined') stats.vdl2Messages = (stats.vdl2Messages || 0) + 1;
document.getElementById('vdl2Count').textContent = vdl2MessageCount;
document.getElementById('stripVdl2').textContent = vdl2MessageCount;
payload.agent_name = data.agent_name;
addVdl2Message(payload);
}
} }
} else { } else if (data.type === 'vdl2') {
// Local stream format message = data;
if (data.type === 'vdl2') { if (isAgentMode && !message.agent_name) {
vdl2MessageCount++; message.agent_name = 'Remote Agent';
if (typeof stats !== 'undefined') stats.vdl2Messages = (stats.vdl2Messages || 0) + 1;
document.getElementById('vdl2Count').textContent = vdl2MessageCount;
document.getElementById('stripVdl2').textContent = vdl2MessageCount;
addVdl2Message(data);
} }
} }
if (message) {
vdl2MessageCount++;
if (typeof stats !== 'undefined') stats.vdl2Messages = (stats.vdl2Messages || 0) + 1;
document.getElementById('vdl2Count').textContent = vdl2MessageCount;
document.getElementById('stripVdl2').textContent = vdl2MessageCount;
addVdl2Message(message);
}
}; };
vdl2EventSource.onerror = function() { vdl2EventSource.onerror = function() {
@@ -3919,52 +3920,6 @@ sudo make install</code>
} }
}, 2000); }, 2000);
}; };
// Start polling fallback for agent mode
if (isAgentMode) {
startVdl2Polling();
}
}
// Track last VDL2 message count for polling
let lastVdl2MessageCount = 0;
function startVdl2Polling() {
if (vdl2PollTimer) return;
lastVdl2MessageCount = 0;
const pollInterval = 2000;
vdl2PollTimer = setInterval(async () => {
if (!isVdl2Running || !vdl2CurrentAgent) {
clearInterval(vdl2PollTimer);
vdl2PollTimer = null;
return;
}
try {
const response = await fetch(`/controller/agents/${vdl2CurrentAgent}/vdl2/data`);
if (!response.ok) return;
const data = await response.json();
const result = data.result || data;
const messages = result.data || [];
if (messages.length > lastVdl2MessageCount) {
const newMessages = messages.slice(lastVdl2MessageCount);
newMessages.forEach(msg => {
vdl2MessageCount++;
if (typeof stats !== 'undefined') stats.vdl2Messages = (stats.vdl2Messages || 0) + 1;
document.getElementById('vdl2Count').textContent = vdl2MessageCount;
document.getElementById('stripVdl2').textContent = vdl2MessageCount;
msg.agent_name = result.agent_name || 'Remote Agent';
addVdl2Message(msg);
});
lastVdl2MessageCount = messages.length;
}
} catch (err) {
console.error('VDL2 polling error:', err);
}
}, pollInterval);
} }
function escapeHtml(str) { function escapeHtml(str) {