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
+56 -31
View File
@@ -1,13 +1,17 @@
"""VDL2 aircraft datalink routes.""" """VDL2 aircraft datalink routes."""
from __future__ import annotations from __future__ import annotations
import json import io
import queue import json
import shutil import os
import subprocess import platform
import threading import pty
import time import queue
import shutil
import subprocess
import threading
import time
from datetime import datetime from datetime import datetime
from typing import Generator from typing import Generator
@@ -51,17 +55,22 @@ 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''
if not line: for line in iter(process.stdout.readline, sentinel):
continue if is_text_mode:
line = line.strip()
else:
line = line.decode('utf-8', errors='replace').strip()
if not line:
continue
try: try:
data = json.loads(line) data = json.loads(line)
@@ -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)
@@ -270,12 +295,12 @@ def start_vdl2() -> Response:
app_module.vdl2_process = process app_module.vdl2_process = process
register_process(process) register_process(process)
# 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()
return jsonify({ return jsonify({
+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) {