feat: Add VDL2 mode and ACARS standalone frontend

Add VDL2 (VHF Digital Link Mode 2) decoding via dumpvdl2 as a new mode,
and promote ACARS from ADS-B-dashboard-only to a first-class standalone
mode in the main SPA. Both aviation datalink modes now have full nav
entries, sidebar partials with region-based frequency selectors, and
SSE streaming. VDL2 also integrated into the ADS-B dashboard as a
collapsible sidebar alongside ACARS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-16 19:48:10 +00:00
parent 126b9ba2ee
commit 2b3f351ff0
12 changed files with 1653 additions and 103 deletions
+451 -87
View File
@@ -92,8 +92,12 @@
<span class="strip-value" id="stripAcars">0</span>
<span class="strip-label">ACARS</span>
</div>
<div class="strip-stat">
<span class="strip-value" id="stripVdl2">0</span>
<span class="strip-label">VDL2</span>
</div>
<div class="strip-stat source-stat" title="Data source (Local or Agent name)">
<span class="strip-value" id="stripSource" style="font-size: 10px;">Local</span>
<span class="strip-value" id="stripSource">Local</span>
<span class="strip-label">SOURCE</span>
</div>
<div class="strip-stat signal-stat" title="Signal quality (messages/errors)">
@@ -106,17 +110,25 @@
</div>
<div class="strip-divider"></div>
<button type="button" class="strip-btn" onclick="showSquawkInfo(null)" title="Squawk Code Reference">
📟 Squawk
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2"/><line x1="6" y1="10" x2="6" y2="14"/><line x1="10" y1="10" x2="10" y2="14"/><line x1="14" y1="10" x2="14" y2="14"/><line x1="18" y1="10" x2="18" y2="14"/></svg>
Squawk
</button>
<button type="button" class="strip-btn" onclick="lookupSelectedFlight()" title="Lookup selected aircraft on FlightAware" id="flightLookupBtn" disabled>
🔗 Lookup
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
Lookup
</button>
<button type="button" class="strip-btn primary" onclick="generateReport()" title="Generate Session Report">
📊 Report
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
Report
</button>
<a class="strip-btn" href="/adsb/history" title="Open History Reporting">
📚 History
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
History
</a>
<button type="button" class="strip-btn" onclick="toggleAntennaGuide()" title="1090 MHz Antenna Guide">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12 L12 2 L22 12"/><line x1="12" y1="2" x2="12" y2="22"/><path d="M4.93 4.93 L12 12"/><path d="M19.07 4.93 L12 12"/></svg>
Antenna
</button>
<div class="strip-divider"></div>
<div class="strip-status">
<div class="status-dot inactive" id="trackingDot"></div>
@@ -176,6 +188,55 @@
</button>
</div>
<!-- VDL2 Panel (left of map, after ACARS) - Collapsible -->
<div class="vdl2-sidebar" id="vdl2Sidebar">
<div class="vdl2-sidebar-content" id="vdl2SidebarContent">
<div class="panel vdl2-panel">
<div class="panel-header">
<span>VDL2 MESSAGES</span>
<div style="display: flex; align-items: center; gap: 8px;">
<span id="vdl2Count" style="font-size: 10px; color: var(--accent-cyan);">0</span>
<div class="panel-indicator" id="vdl2PanelIndicator"></div>
</div>
</div>
<div id="vdl2PanelContent">
<div class="vdl2-info" style="font-size: 9px; color: var(--text-muted); padding: 5px 8px; border-bottom: 1px solid var(--border-color);">
<span style="color: var(--accent-yellow);">&#9888;</span> Requires separate SDR (VHF ~137 MHz)
</div>
<div class="vdl2-controls" style="padding: 8px; border-bottom: 1px solid var(--border-color);">
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
<select id="vdl2DeviceSelect" style="flex: 1; font-size: 10px;">
<option value="0">SDR 0</option>
<option value="1">SDR 1</option>
</select>
<select id="vdl2RegionDashSelect" onchange="updateVdl2FreqCheckboxes()" style="flex: 1; font-size: 10px;">
<option value="na">N. America</option>
<option value="eu">Europe</option>
<option value="ap">Asia-Pac</option>
</select>
</div>
<div id="vdl2FreqSelector" style="display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; font-size: 9px;">
<!-- Frequency checkboxes populated by JS -->
</div>
<button class="vdl2-btn" id="vdl2ToggleBtn" onclick="toggleVdl2()" style="width: 100%;">
&#9654; START VDL2
</button>
</div>
<div class="vdl2-messages" id="vdl2Messages">
<div class="no-aircraft" style="padding: 20px; text-align: center;">
<div style="font-size: 10px; color: var(--text-muted);">No VDL2 messages</div>
<div style="font-size: 9px; color: var(--text-dim); margin-top: 5px;">Start VDL2 to receive digital datalink messages</div>
</div>
</div>
</div>
</div>
</div>
<button class="vdl2-collapse-btn" id="vdl2CollapseBtn" onclick="toggleVdl2Sidebar()" title="Toggle VDL2 Panel">
<span id="vdl2CollapseIcon">&#9664;</span>
<span class="vdl2-collapse-label">VDL2</span>
</button>
</div>
<!-- Main Display (Map or Radar Scope) -->
<div class="main-display">
<div class="display-container">
@@ -223,88 +284,6 @@
</div>
</div>
</div>
<!-- Antenna Guide Panel -->
<div class="panel" id="antennaGuidePanel">
<div class="panel-header" style="cursor: pointer;" onclick="document.getElementById('antennaGuideContent').style.display = document.getElementById('antennaGuideContent').style.display === 'none' ? 'block' : 'none'; this.querySelector('.panel-toggle').textContent = document.getElementById('antennaGuideContent').style.display === 'none' ? '&#9654;' : '&#9660;';">
<span>ANTENNA GUIDE</span>
<span class="panel-toggle" style="font-size: 10px; color: var(--text-muted);">&#9654;</span>
</div>
<div id="antennaGuideContent" style="display: none; padding: 10px; font-size: 11px; color: var(--text-secondary); line-height: 1.5;">
<p style="margin-bottom: 8px; color: var(--accent-cyan); font-weight: 600;">
1090 MHz &mdash; stock SDR antenna can work but is not ideal
</p>
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; padding: 8px; margin-bottom: 8px;">
<strong style="color: var(--accent-cyan); font-size: 11px;">Stock Telescopic Antenna</strong>
<ul style="margin: 4px 0 0 14px; padding: 0; font-size: 10px;">
<li><strong style="color: var(--text-primary);">1090 MHz:</strong> Collapse to ~6.9 cm (quarter-wave). It works for nearby aircraft</li>
<li><strong style="color: var(--text-primary);">Range:</strong> Expect ~50 NM (90 km) indoors, ~100 NM outdoors</li>
</ul>
</div>
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; padding: 8px; margin-bottom: 8px;">
<strong style="color: #00ff88; font-size: 11px;">Recommended: 1090 MHz Collinear (~$10-20 DIY)</strong>
<ul style="margin: 4px 0 0 14px; padding: 0; font-size: 10px;">
<li><strong style="color: var(--text-primary);">Design:</strong> 8 coaxial collinear elements from RG-6 coax cable</li>
<li><strong style="color: var(--text-primary);">Element length:</strong> ~6.9 cm segments soldered alternating center/shield</li>
<li><strong style="color: var(--text-primary);">Gain:</strong> ~5&ndash;7 dBi omnidirectional, ideal for 360&deg; coverage</li>
<li><strong style="color: var(--text-primary);">Range:</strong> 150&ndash;250+ NM depending on height and LOS</li>
</ul>
</div>
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; padding: 8px; margin-bottom: 8px;">
<strong style="color: var(--accent-cyan); font-size: 11px;">Commercial Options</strong>
<ul style="margin: 4px 0 0 14px; padding: 0; font-size: 10px;">
<li><strong style="color: var(--text-primary);">FlightAware antenna:</strong> ~$35, 1090 MHz tuned, 66cm fiberglass whip</li>
<li><strong style="color: var(--text-primary);">ADSBexchange whip:</strong> ~$40, similar performance</li>
<li><strong style="color: var(--text-primary);">Jetvision A3:</strong> ~$50, high-gain 1090 MHz collinear</li>
</ul>
</div>
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; padding: 8px; margin-bottom: 8px;">
<strong style="color: var(--accent-cyan); font-size: 11px;">Placement & LNA</strong>
<ul style="margin: 4px 0 0 14px; padding: 0; font-size: 10px;">
<li><strong style="color: var(--text-primary);">Location:</strong> OUTDOORS, as high as possible. Roof or mast mount</li>
<li><strong style="color: var(--text-primary);">Height:</strong> Every 3m higher adds ~10 NM range (line-of-sight)</li>
<li><strong style="color: var(--text-primary);">LNA:</strong> 1090 MHz filtered LNA at antenna feed (e.g. Uputronics, ~$30)</li>
<li><strong style="color: var(--text-primary);">Filter:</strong> A 1090 MHz bandpass filter removes cell/FM interference</li>
<li><strong style="color: var(--text-primary);">Coax:</strong> Keep short. At 1090 MHz, RG-58 loses ~10 dB per 10m</li>
<li><strong style="color: var(--text-primary);">Bias-T:</strong> Enable Bias-T in controls above if LNA is powered via coax</li>
</ul>
</div>
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; padding: 8px;">
<strong style="color: var(--accent-cyan); font-size: 11px;">Quick Reference</strong>
<table style="width: 100%; margin-top: 4px; font-size: 10px; border-collapse: collapse;">
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 2px 4px; color: var(--text-dim);">ADS-B frequency</td>
<td style="padding: 2px 4px; color: var(--text-primary); text-align: right;">1090 MHz</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 2px 4px; color: var(--text-dim);">Quarter-wave length</td>
<td style="padding: 2px 4px; color: var(--text-primary); text-align: right;">6.9 cm</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 2px 4px; color: var(--text-dim);">Modulation</td>
<td style="padding: 2px 4px; color: var(--text-primary); text-align: right;">PPM (pulse)</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 2px 4px; color: var(--text-dim);">Polarization</td>
<td style="padding: 2px 4px; color: var(--text-primary); text-align: right;">Vertical</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 2px 4px; color: var(--text-dim);">Bandwidth</td>
<td style="padding: 2px 4px; color: var(--text-primary); text-align: right;">~2 MHz</td>
</tr>
<tr>
<td style="padding: 2px 4px; color: var(--text-dim);">Typical range (outdoor)</td>
<td style="padding: 2px 4px; color: var(--text-primary); text-align: right;">100&ndash;250 NM</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<!-- Controls Bar - Reorganized -->
@@ -4115,6 +4094,211 @@ sudo make install</code>
});
});
// ============================================
// VDL2 DATALINK PANEL
// ============================================
let vdl2EventSource = null;
let isVdl2Running = false;
let vdl2MessageCount = 0;
let vdl2SidebarCollapsed = localStorage.getItem('vdl2SidebarCollapsed') !== 'false';
let vdl2Frequencies = {
'na': ['136975000', '136100000', '136650000', '136700000', '136800000'],
'eu': ['136975000', '136675000', '136725000', '136775000', '136825000'],
'ap': ['136975000', '136900000']
};
let vdl2FreqLabels = {
'136975000': '136.975', '136100000': '136.100', '136650000': '136.650',
'136700000': '136.700', '136800000': '136.800', '136675000': '136.675',
'136725000': '136.725', '136775000': '136.775', '136825000': '136.825',
'136900000': '136.900'
};
function toggleVdl2Sidebar() {
const sidebar = document.getElementById('vdl2Sidebar');
vdl2SidebarCollapsed = !vdl2SidebarCollapsed;
sidebar.classList.toggle('collapsed', vdl2SidebarCollapsed);
localStorage.setItem('vdl2SidebarCollapsed', vdl2SidebarCollapsed);
}
document.addEventListener('DOMContentLoaded', () => {
const sidebar = document.getElementById('vdl2Sidebar');
if (sidebar && vdl2SidebarCollapsed) {
sidebar.classList.add('collapsed');
}
updateVdl2FreqCheckboxes();
});
function updateVdl2FreqCheckboxes() {
const region = document.getElementById('vdl2RegionDashSelect').value;
const freqs = vdl2Frequencies[region] || vdl2Frequencies['na'];
const container = document.getElementById('vdl2FreqSelector');
const previouslyChecked = new Set();
container.querySelectorAll('input:checked').forEach(cb => previouslyChecked.add(cb.value));
container.innerHTML = freqs.map(freq => {
const checked = previouslyChecked.size === 0 || previouslyChecked.has(freq) ? 'checked' : '';
const label = vdl2FreqLabels[freq] || freq;
return `
<label style="display: flex; align-items: center; gap: 3px; padding: 2px 6px; background: var(--bg-secondary); border-radius: 3px; cursor: pointer;">
<input type="checkbox" class="vdl2-freq-cb" value="${freq}" ${checked} style="margin: 0; cursor: pointer;">
<span>${label}</span>
</label>
`;
}).join('');
}
function getVdl2RegionFreqs() {
const checkboxes = document.querySelectorAll('.vdl2-freq-cb:checked');
const selectedFreqs = Array.from(checkboxes).map(cb => cb.value);
if (selectedFreqs.length === 0) {
const region = document.getElementById('vdl2RegionDashSelect').value;
return vdl2Frequencies[region] || vdl2Frequencies['na'];
}
return selectedFreqs;
}
function toggleVdl2() {
if (isVdl2Running) {
stopVdl2();
} else {
startVdl2();
}
}
function startVdl2() {
const vdl2Select = document.getElementById('vdl2DeviceSelect');
const device = vdl2Select.value;
const sdr_type = vdl2Select.selectedOptions[0]?.dataset.sdrType || 'rtlsdr';
const frequencies = getVdl2RegionFreqs();
if (isTracking && device === '0') {
const useAnyway = confirm(
'Warning: ADS-B tracking may be using SDR device 0.\n\n' +
'VDL2 uses VHF frequencies (~137 MHz) while ADS-B uses 1090 MHz.\n' +
'You need TWO separate SDR devices to receive both simultaneously.\n\n' +
'Click OK to start VDL2 on device ' + device + ' anyway.'
);
if (!useAnyway) return;
}
fetch('/vdl2/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device, frequencies, gain: '40', sdr_type })
})
.then(r => r.json())
.then(data => {
if (data.status === 'started') {
isVdl2Running = true;
vdl2MessageCount = 0;
document.getElementById('vdl2ToggleBtn').innerHTML = '&#9632; STOP VDL2';
document.getElementById('vdl2ToggleBtn').classList.add('active');
document.getElementById('vdl2PanelIndicator').classList.add('active');
startVdl2Stream();
} else {
alert('VDL2 Error: ' + (data.message || 'Failed to start'));
}
})
.catch(err => alert('VDL2 Error: ' + err));
}
function stopVdl2() {
fetch('/vdl2/stop', { method: 'POST' })
.then(r => r.json())
.then(() => {
isVdl2Running = false;
document.getElementById('vdl2ToggleBtn').innerHTML = '&#9654; START VDL2';
document.getElementById('vdl2ToggleBtn').classList.remove('active');
document.getElementById('vdl2PanelIndicator').classList.remove('active');
if (vdl2EventSource) {
vdl2EventSource.close();
vdl2EventSource = null;
}
});
}
function startVdl2Stream() {
if (vdl2EventSource) vdl2EventSource.close();
vdl2EventSource = new EventSource('/vdl2/stream');
vdl2EventSource.onmessage = function(e) {
const data = JSON.parse(e.data);
if (data.type === 'vdl2') {
vdl2MessageCount++;
if (typeof stats !== 'undefined') stats.vdl2Messages = (stats.vdl2Messages || 0) + 1;
document.getElementById('vdl2Count').textContent = vdl2MessageCount;
document.getElementById('stripVdl2').textContent = vdl2MessageCount;
addVdl2Message(data);
}
};
vdl2EventSource.onerror = function() {
console.error('VDL2 stream error');
};
}
function addVdl2Message(data) {
const container = document.getElementById('vdl2Messages');
const placeholder = container.querySelector('.no-aircraft');
if (placeholder) placeholder.remove();
const msg = document.createElement('div');
msg.className = 'vdl2-message-item';
msg.style.cssText = 'padding: 6px 8px; border-bottom: 1px solid var(--border-color); font-size: 10px;';
const station = data.station || '';
const avlc = data.avlc || {};
const src = avlc.src?.addr || '';
const dst = avlc.dst?.addr || '';
const acars = avlc.acars || {};
const flight = acars.flight || '';
const msgText = acars.msg_text || '';
const time = new Date().toLocaleTimeString();
const freq = data.freq ? (data.freq / 1000000).toFixed(3) : '';
msg.innerHTML = `
<div style="display: flex; justify-content: space-between; margin-bottom: 2px;">
<span style="color: var(--accent-cyan); font-weight: bold;">${flight || src || 'VDL2'}</span>
<span style="color: var(--text-muted);">${time}</span>
</div>
${freq ? `<div style="color: var(--text-dim); font-size: 9px;">${freq} MHz</div>` : ''}
${dst ? `<div style="color: var(--text-muted); font-size: 9px;">To: ${dst}</div>` : ''}
${msgText ? `<div style="color: var(--text-primary); margin-top: 3px; word-break: break-word;">${msgText}</div>` : ''}
`;
container.insertBefore(msg, container.firstChild);
while (container.children.length > 50) {
container.removeChild(container.lastChild);
}
}
// Populate VDL2 device selector
document.addEventListener('DOMContentLoaded', () => {
fetch('/devices')
.then(r => r.json())
.then(devices => {
const select = document.getElementById('vdl2DeviceSelect');
select.innerHTML = '';
if (devices.length === 0) {
select.innerHTML = '<option value="0">No SDR detected</option>';
} else {
devices.forEach((d, i) => {
const opt = document.createElement('option');
opt.value = d.index || i;
opt.dataset.sdrType = d.sdr_type || 'rtlsdr';
opt.textContent = `SDR ${d.index || i}: ${d.name || d.type || 'SDR'}`;
select.appendChild(opt);
});
if (devices.length > 1) {
select.value = '1';
}
}
});
});
// ============================================
// SQUAWK CODE REFERENCE
// ============================================
@@ -4174,6 +4358,18 @@ sudo make install</code>
document.getElementById('squawkModal')?.addEventListener('click', (e) => {
if (e.target.id === 'squawkModal') closeSquawkModal();
});
// ============================================
// ANTENNA GUIDE
// ============================================
function toggleAntennaGuide() {
const modal = document.getElementById('antennaGuideModal');
modal.classList.toggle('active');
}
document.getElementById('antennaGuideModal')?.addEventListener('click', (e) => {
if (e.target.id === 'antennaGuideModal') toggleAntennaGuide();
});
</script>
<!-- Squawk Code Reference Modal -->
@@ -4227,6 +4423,72 @@ sudo make install</code>
</div>
</div>
<!-- Antenna Guide Modal -->
<div id="antennaGuideModal" class="antenna-guide-modal">
<div class="antenna-guide-modal-content">
<div class="antenna-guide-modal-header">
<span>ANTENNA GUIDE &mdash; 1090 MHz ADS-B</span>
<button class="antenna-guide-modal-close" onclick="toggleAntennaGuide()">&times;</button>
</div>
<div class="antenna-guide-modal-body">
<p style="margin-bottom: 10px; color: var(--accent-cyan, #00d4ff); font-weight: 600; font-size: 12px;">
1090 MHz &mdash; stock SDR antenna can work but is not ideal
</p>
<div class="antenna-section">
<strong style="color: var(--accent-cyan, #00d4ff);">Stock Telescopic Antenna</strong>
<ul>
<li><strong>1090 MHz:</strong> Collapse to ~6.9 cm (quarter-wave). It works for nearby aircraft</li>
<li><strong>Range:</strong> Expect ~50 NM (90 km) indoors, ~100 NM outdoors</li>
</ul>
</div>
<div class="antenna-section recommended">
<strong style="color: #00ff88;">Recommended: 1090 MHz Collinear (~$10-20 DIY)</strong>
<ul>
<li><strong>Design:</strong> 8 coaxial collinear elements from RG-6 coax cable</li>
<li><strong>Element length:</strong> ~6.9 cm segments soldered alternating center/shield</li>
<li><strong>Gain:</strong> ~5&ndash;7 dBi omnidirectional, ideal for 360&deg; coverage</li>
<li><strong>Range:</strong> 150&ndash;250+ NM depending on height and LOS</li>
</ul>
</div>
<div class="antenna-section">
<strong style="color: var(--accent-cyan, #00d4ff);">Commercial Options</strong>
<ul>
<li><strong>FlightAware antenna:</strong> ~$35, 1090 MHz tuned, 66cm fiberglass whip</li>
<li><strong>ADSBexchange whip:</strong> ~$40, similar performance</li>
<li><strong>Jetvision A3:</strong> ~$50, high-gain 1090 MHz collinear</li>
</ul>
</div>
<div class="antenna-section">
<strong style="color: var(--accent-cyan, #00d4ff);">Placement &amp; LNA</strong>
<ul>
<li><strong>Location:</strong> OUTDOORS, as high as possible. Roof or mast mount</li>
<li><strong>Height:</strong> Every 3m higher adds ~10 NM range (line-of-sight)</li>
<li><strong>LNA:</strong> 1090 MHz filtered LNA at antenna feed (e.g. Uputronics, ~$30)</li>
<li><strong>Filter:</strong> A 1090 MHz bandpass filter removes cell/FM interference</li>
<li><strong>Coax:</strong> Keep short. At 1090 MHz, RG-58 loses ~10 dB per 10m</li>
<li><strong>Bias-T:</strong> Enable Bias-T in controls above if LNA is powered via coax</li>
</ul>
</div>
<div class="antenna-section">
<strong style="color: var(--accent-cyan, #00d4ff);">Quick Reference</strong>
<table class="antenna-ref-table">
<tr><td>ADS-B frequency</td><td>1090 MHz</td></tr>
<tr><td>Quarter-wave length</td><td>6.9 cm</td></tr>
<tr><td>Modulation</td><td>PPM (pulse)</td></tr>
<tr><td>Polarization</td><td>Vertical</td></tr>
<tr><td>Bandwidth</td><td>~2 MHz</td></tr>
<tr><td>Typical range (outdoor)</td><td>100&ndash;250 NM</td></tr>
</table>
</div>
</div>
</div>
</div>
<style>
.squawk-clickable {
cursor: pointer;
@@ -4374,6 +4636,108 @@ sudo make install</code>
color: #000;
}
/* Antenna Guide Modal */
.antenna-guide-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
justify-content: center;
align-items: center;
}
.antenna-guide-modal.active {
display: flex;
}
.antenna-guide-modal-content {
background: var(--bg-secondary, #1a1a1a);
border: 1px solid var(--border-color, #333);
border-radius: 8px;
width: 90%;
max-width: 480px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.antenna-guide-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--bg-tertiary, #252525);
border-bottom: 1px solid var(--border-color, #333);
font-weight: 600;
font-size: 12px;
letter-spacing: 0.1em;
color: var(--accent-cyan, #00d4ff);
}
.antenna-guide-modal-close {
background: none;
border: none;
color: var(--text-secondary, #888);
font-size: 20px;
cursor: pointer;
padding: 0 4px;
}
.antenna-guide-modal-close:hover {
color: #fff;
}
.antenna-guide-modal-body {
padding: 16px;
overflow-y: auto;
font-size: 11px;
color: var(--text-secondary, #999);
line-height: 1.5;
}
.antenna-section {
background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color, #333);
border-radius: 4px;
padding: 10px;
margin-bottom: 8px;
}
.antenna-section strong {
font-size: 11px;
display: block;
margin-bottom: 4px;
}
.antenna-section ul {
margin: 4px 0 0 14px;
padding: 0;
font-size: 10px;
}
.antenna-section ul li {
margin-bottom: 2px;
}
.antenna-section ul li strong {
display: inline;
color: var(--text-primary, #fff);
font-size: 10px;
}
.antenna-ref-table {
width: 100%;
margin-top: 6px;
font-size: 10px;
border-collapse: collapse;
}
.antenna-ref-table tr {
border-bottom: 1px solid var(--border-color, #333);
}
.antenna-ref-table td {
padding: 3px 4px;
}
.antenna-ref-table td:first-child {
color: var(--text-dim, #666);
}
.antenna-ref-table td:last-child {
color: var(--text-primary, #fff);
text-align: right;
}
/* Watchlist Modal */
.watchlist-modal {
display: none;
+19 -7
View File
@@ -51,6 +51,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/acars.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/vdl2.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/aprs.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/tscm.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-cards.css') }}">
@@ -569,6 +570,10 @@
{% include 'partials/modes/bt_locate.html' %}
{% include 'partials/modes/acars.html' %}
{% include 'partials/modes/vdl2.html' %}
<button class="preset-btn" onclick="killAll()"
style="width: 100%; margin-top: 10px; border-color: #ff3366; color: #ff3366;">
Kill All Processes
@@ -3158,7 +3163,8 @@
const validModes = new Set([
'pager', 'sensor', 'rtlamr', 'aprs', 'listening',
'spystations', 'meshtastic', 'wifi', 'bluetooth', 'bt_locate',
'tscm', 'satellite', 'sstv', 'weathersat', 'sstv_general', 'gps', 'websdr', 'subghz'
'tscm', 'satellite', 'sstv', 'weathersat', 'sstv_general', 'gps', 'websdr', 'subghz',
'acars', 'vdl2'
]);
function getModeFromQuery() {
@@ -3617,7 +3623,7 @@
'wifi': 'wireless', 'bluetooth': 'wireless', 'bt_locate': 'wireless',
'tscm': 'security',
'rtlamr': 'sdr', 'ais': 'sdr', 'spystations': 'sdr',
'meshtastic': 'sdr',
'meshtastic': 'sdr', 'acars': 'sdr', 'vdl2': 'sdr',
'satellite': 'space', 'sstv': 'space', 'weathersat': 'space', 'sstv_general': 'space', 'gps': 'space',
'subghz': 'sdr'
};
@@ -3722,6 +3728,8 @@
document.getElementById('dmrMode')?.classList.toggle('active', mode === 'dmr');
document.getElementById('websdrMode')?.classList.toggle('active', mode === 'websdr');
document.getElementById('subghzMode')?.classList.toggle('active', mode === 'subghz');
document.getElementById('acarsMode')?.classList.toggle('active', mode === 'acars');
document.getElementById('vdl2Mode')?.classList.toggle('active', mode === 'vdl2');
const pagerStats = document.getElementById('pagerStats');
const sensorStats = document.getElementById('sensorStats');
const satelliteStats = document.getElementById('satelliteStats');
@@ -3762,7 +3770,9 @@
'meshtastic': 'MESHTASTIC',
'dmr': 'DIGITAL VOICE',
'websdr': 'WEBSDR',
'subghz': 'SUBGHZ'
'subghz': 'SUBGHZ',
'acars': 'ACARS',
'vdl2': 'VDL2'
};
const activeModeIndicator = document.getElementById('activeModeIndicator');
if (activeModeIndicator) activeModeIndicator.innerHTML = '<span class="pulse-dot"></span>' + (modeNames[mode] || mode.toUpperCase());
@@ -3836,7 +3846,9 @@
'meshtastic': 'Meshtastic Mesh Monitor',
'dmr': 'Digital Voice Decoder',
'websdr': 'HF/Shortwave WebSDR',
'subghz': 'SubGHz Transceiver'
'subghz': 'SubGHz Transceiver',
'acars': 'ACARS Aircraft Messaging',
'vdl2': 'VDL2 Aircraft Datalink'
};
const outputTitle = document.getElementById('outputTitle');
if (outputTitle) outputTitle.textContent = titles[mode] || 'Signal Monitor';
@@ -3854,7 +3866,7 @@
const reconBtn = document.getElementById('reconBtn');
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
const reconPanel = document.getElementById('reconPanel');
if (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'gps' || mode === 'listening' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'dmr' || mode === 'websdr' || mode === 'subghz') {
if (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'gps' || mode === 'listening' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'dmr' || mode === 'websdr' || mode === 'subghz' || mode === 'acars' || mode === 'vdl2') {
if (reconPanel) reconPanel.style.display = 'none';
if (reconBtn) reconBtn.style.display = 'none';
if (intelBtn) intelBtn.style.display = 'none';
@@ -3869,12 +3881,12 @@
// Show agent selector for modes that support remote agents
const agentSection = document.getElementById('agentSection');
const agentModes = ['pager', 'sensor', 'rtlamr', 'listening', 'aprs', 'wifi', 'bluetooth', 'aircraft', 'tscm', 'ais', 'acars', 'dsc'];
const agentModes = ['pager', 'sensor', 'rtlamr', 'listening', 'aprs', 'wifi', 'bluetooth', 'aircraft', 'tscm', 'ais', 'acars', 'vdl2', 'dsc'];
if (agentSection) agentSection.style.display = agentModes.includes(mode) ? 'block' : 'none';
// Show RTL-SDR device section for modes that use it
const rtlDeviceSection = document.getElementById('rtlDeviceSection');
if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'listening' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'dmr') ? 'block' : 'none';
if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'listening' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'dmr' || mode === 'acars' || mode === 'vdl2') ? 'block' : 'none';
// Show waterfall panel if running in listening mode
const waterfallPanel = document.getElementById('waterfallPanel');
+208
View File
@@ -0,0 +1,208 @@
<!-- ACARS AIRCRAFT MESSAGING MODE -->
<div id="acarsMode" class="mode-content" style="display: none;">
<div class="section">
<h3>ACARS Messaging</h3>
<div class="info-text" style="margin-bottom: 15px;">
Decode ACARS (Aircraft Communications Addressing and Reporting System) messages on VHF frequencies (~129-131 MHz). Captures flight data, weather reports, position updates, and operational messages from aircraft.
</div>
</div>
<div class="section">
<h3>Region &amp; Frequencies</h3>
<div class="form-group">
<label>Region</label>
<select id="acarsRegionSelect" onchange="updateAcarsMainFreqs()" style="width: 100%;">
<option value="na">North America</option>
<option value="eu">Europe</option>
<option value="ap">Asia-Pacific</option>
</select>
</div>
<div id="acarsMainFreqSelector" style="display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; font-size: 11px;">
<!-- Populated by JS -->
</div>
<div class="form-group">
<label>Gain (dB, 0 = auto)</label>
<input type="number" id="acarsGainInput" value="40" min="0" max="50" placeholder="0-50">
</div>
</div>
<div class="section">
<h3>Status</h3>
<div id="acarsStatusDisplay" class="info-text">
<p>Status: <span id="acarsStatusText" style="color: var(--accent-yellow);">Standby</span></p>
<p>Messages: <span id="acarsMessageCount">0</span></p>
</div>
</div>
<!-- Antenna Guide -->
<div class="section">
<h3>Antenna Guide</h3>
<div style="font-size: 11px; color: var(--text-dim); line-height: 1.5;">
<p style="margin-bottom: 8px; color: var(--accent-cyan); font-weight: 600;">
VHF Airband (~130 MHz) &mdash; stock SDR antenna may work at close range
</p>
<div style="background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; margin-bottom: 10px;">
<strong style="color: var(--accent-cyan); font-size: 12px;">Simple Dipole</strong>
<ul style="margin: 6px 0 0 14px; padding: 0;">
<li><strong style="color: var(--text-primary);">Element length:</strong> ~57 cm each (quarter-wave at 130 MHz)</li>
<li><strong style="color: var(--text-primary);">Material:</strong> Wire, coat hanger, or copper rod</li>
<li><strong style="color: var(--text-primary);">Orientation:</strong> Vertical (airband is vertically polarized)</li>
<li><strong style="color: var(--text-primary);">Placement:</strong> Outdoors, as high as possible with clear sky view</li>
</ul>
</div>
<div style="background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px;">
<strong style="color: var(--accent-cyan); font-size: 12px;">Quick Reference</strong>
<table style="width: 100%; margin-top: 6px; font-size: 10px; border-collapse: collapse;">
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 3px 4px; color: var(--text-dim);">Primary (worldwide)</td>
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">131.550 MHz</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 3px 4px; color: var(--text-dim);">Quarter-wave length</td>
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">57 cm</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 3px 4px; color: var(--text-dim);">Modulation</td>
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">AM MSK 2400 baud</td>
</tr>
<tr>
<td style="padding: 3px 4px; color: var(--text-dim);">Polarization</td>
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">Vertical</td>
</tr>
</table>
</div>
</div>
</div>
<button class="run-btn" id="startAcarsBtn" onclick="startAcarsMode()">
Start ACARS
</button>
<button class="stop-btn" id="stopAcarsBtn" onclick="stopAcarsMode()" style="display: none;">
Stop ACARS
</button>
</div>
<script>
let acarsMainEventSource = null;
let acarsMainMsgCount = 0;
const acarsMainFrequencies = {
'na': ['129.125', '130.025', '130.450', '131.550'],
'eu': ['131.525', '131.725', '131.550'],
'ap': ['131.550', '131.450']
};
function updateAcarsMainFreqs() {
const region = document.getElementById('acarsRegionSelect').value;
const freqs = acarsMainFrequencies[region] || acarsMainFrequencies['na'];
const container = document.getElementById('acarsMainFreqSelector');
const previouslyChecked = new Set();
container.querySelectorAll('input:checked').forEach(cb => previouslyChecked.add(cb.value));
container.innerHTML = freqs.map(freq => {
const checked = previouslyChecked.size === 0 || previouslyChecked.has(freq) ? 'checked' : '';
return `
<label style="display: flex; align-items: center; gap: 3px; padding: 2px 6px; background: var(--bg-secondary); border-radius: 3px; cursor: pointer;">
<input type="checkbox" class="acars-main-freq-cb" value="${freq}" ${checked} style="margin: 0; cursor: pointer;">
<span>${freq}</span>
</label>
`;
}).join('');
}
function getAcarsMainSelectedFreqs() {
const checkboxes = document.querySelectorAll('.acars-main-freq-cb:checked');
const selected = Array.from(checkboxes).map(cb => cb.value);
if (selected.length === 0) {
const region = document.getElementById('acarsRegionSelect').value;
return acarsMainFrequencies[region] || acarsMainFrequencies['na'];
}
return selected;
}
function startAcarsMode() {
const gain = document.getElementById('acarsGainInput').value || '40';
const device = document.getElementById('deviceSelect')?.value || '0';
const frequencies = getAcarsMainSelectedFreqs();
fetch('/acars/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device, gain, frequencies })
})
.then(r => r.json())
.then(data => {
if (data.status === 'started') {
document.getElementById('startAcarsBtn').style.display = 'none';
document.getElementById('stopAcarsBtn').style.display = 'block';
document.getElementById('acarsStatusText').textContent = 'Listening';
document.getElementById('acarsStatusText').style.color = 'var(--accent-green)';
acarsMainMsgCount = 0;
startAcarsMainSSE();
} else {
alert(data.message || 'Failed to start ACARS');
}
})
.catch(err => alert('Error: ' + err.message));
}
function stopAcarsMode() {
fetch('/acars/stop', { method: 'POST' })
.then(r => r.json())
.then(() => {
document.getElementById('startAcarsBtn').style.display = 'block';
document.getElementById('stopAcarsBtn').style.display = 'none';
document.getElementById('acarsStatusText').textContent = 'Standby';
document.getElementById('acarsStatusText').style.color = 'var(--accent-yellow)';
if (acarsMainEventSource) {
acarsMainEventSource.close();
acarsMainEventSource = null;
}
});
}
function startAcarsMainSSE() {
if (acarsMainEventSource) acarsMainEventSource.close();
acarsMainEventSource = new EventSource('/acars/stream');
acarsMainEventSource.onmessage = function(e) {
try {
const data = JSON.parse(e.data);
if (data.type === 'acars') {
acarsMainMsgCount++;
document.getElementById('acarsMessageCount').textContent = acarsMainMsgCount;
}
} catch (err) {}
};
acarsMainEventSource.onerror = function() {
setTimeout(() => {
if (document.getElementById('stopAcarsBtn').style.display === 'block') {
startAcarsMainSSE();
}
}, 2000);
};
}
// Check initial status
fetch('/acars/status')
.then(r => r.json())
.then(data => {
if (data.running) {
document.getElementById('startAcarsBtn').style.display = 'none';
document.getElementById('stopAcarsBtn').style.display = 'block';
document.getElementById('acarsStatusText').textContent = 'Listening';
document.getElementById('acarsStatusText').style.color = 'var(--accent-green)';
document.getElementById('acarsMessageCount').textContent = data.message_count || 0;
acarsMainMsgCount = data.message_count || 0;
startAcarsMainSSE();
}
})
.catch(() => {});
// Initialize frequency checkboxes
document.addEventListener('DOMContentLoaded', () => updateAcarsMainFreqs());
</script>
+228
View File
@@ -0,0 +1,228 @@
<!-- VDL2 AIRCRAFT DATALINK MODE -->
<div id="vdl2Mode" class="mode-content" style="display: none;">
<div class="section">
<h3>VDL2 Datalink</h3>
<div class="info-text" style="margin-bottom: 15px;">
Decode VDL Mode 2 (VHF Digital Link) messages on ~136 MHz. VDL2 is the digital successor to ACARS, using D8PSK modulation for higher throughput aircraft datalink communications.
</div>
</div>
<div class="section">
<h3>Region &amp; Frequencies</h3>
<div class="form-group">
<label>Region</label>
<select id="vdl2RegionSelect" onchange="updateVdl2MainFreqs()" style="width: 100%;">
<option value="na">North America</option>
<option value="eu">Europe</option>
<option value="ap">Asia-Pacific</option>
</select>
</div>
<div id="vdl2MainFreqSelector" style="display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; font-size: 11px;">
<!-- Populated by JS -->
</div>
<div class="form-group">
<label>Gain (dB, 0 = auto)</label>
<input type="number" id="vdl2GainInput" value="40" min="0" max="50" placeholder="0-50">
</div>
</div>
<div class="section">
<h3>Status</h3>
<div id="vdl2StatusDisplay" class="info-text">
<p>Status: <span id="vdl2StatusText" style="color: var(--accent-yellow);">Standby</span></p>
<p>Messages: <span id="vdl2MessageCount">0</span></p>
</div>
</div>
<!-- Antenna Guide -->
<div class="section">
<h3>Antenna Guide</h3>
<div style="font-size: 11px; color: var(--text-dim); line-height: 1.5;">
<p style="margin-bottom: 8px; color: var(--accent-cyan); font-weight: 600;">
VHF Airband (~137 MHz) &mdash; stock SDR antenna may work at close range
</p>
<div style="background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; margin-bottom: 10px;">
<strong style="color: var(--accent-cyan); font-size: 12px;">Simple Dipole</strong>
<ul style="margin: 6px 0 0 14px; padding: 0;">
<li><strong style="color: var(--text-primary);">Element length:</strong> ~55 cm each (quarter-wave at 137 MHz)</li>
<li><strong style="color: var(--text-primary);">Material:</strong> Wire, coat hanger, or copper rod</li>
<li><strong style="color: var(--text-primary);">Orientation:</strong> Vertical (airband is vertically polarized)</li>
<li><strong style="color: var(--text-primary);">Placement:</strong> Outdoors, as high as possible with clear sky view</li>
</ul>
</div>
<div style="background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px;">
<strong style="color: var(--accent-cyan); font-size: 12px;">Quick Reference</strong>
<table style="width: 100%; margin-top: 6px; font-size: 10px; border-collapse: collapse;">
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 3px 4px; color: var(--text-dim);">Primary (worldwide)</td>
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">136.975 MHz</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 3px 4px; color: var(--text-dim);">Quarter-wave length</td>
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">55 cm</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 3px 4px; color: var(--text-dim);">Modulation</td>
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">D8PSK 31.5 kbps</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 3px 4px; color: var(--text-dim);">Bandwidth</td>
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">25 kHz</td>
</tr>
<tr>
<td style="padding: 3px 4px; color: var(--text-dim);">Polarization</td>
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">Vertical</td>
</tr>
</table>
</div>
</div>
</div>
<button class="run-btn" id="startVdl2Btn" onclick="startVdl2Mode()">
Start VDL2
</button>
<button class="stop-btn" id="stopVdl2Btn" onclick="stopVdl2Mode()" style="display: none;">
Stop VDL2
</button>
</div>
<script>
let vdl2MainEventSource = null;
let vdl2MainMsgCount = 0;
// VDL2 frequencies in Hz (as required by dumpvdl2)
const vdl2MainFrequencies = {
'na': ['136975000', '136100000', '136650000', '136700000', '136800000'],
'eu': ['136975000', '136675000', '136725000', '136775000', '136825000'],
'ap': ['136975000', '136900000']
};
// Display-friendly MHz labels
const vdl2FreqLabels = {
'136975000': '136.975',
'136100000': '136.100',
'136650000': '136.650',
'136700000': '136.700',
'136800000': '136.800',
'136675000': '136.675',
'136725000': '136.725',
'136775000': '136.775',
'136825000': '136.825',
'136900000': '136.900'
};
function updateVdl2MainFreqs() {
const region = document.getElementById('vdl2RegionSelect').value;
const freqs = vdl2MainFrequencies[region] || vdl2MainFrequencies['na'];
const container = document.getElementById('vdl2MainFreqSelector');
const previouslyChecked = new Set();
container.querySelectorAll('input:checked').forEach(cb => previouslyChecked.add(cb.value));
container.innerHTML = freqs.map(freq => {
const checked = previouslyChecked.size === 0 || previouslyChecked.has(freq) ? 'checked' : '';
const label = vdl2FreqLabels[freq] || freq;
return `
<label style="display: flex; align-items: center; gap: 3px; padding: 2px 6px; background: var(--bg-secondary); border-radius: 3px; cursor: pointer;">
<input type="checkbox" class="vdl2-main-freq-cb" value="${freq}" ${checked} style="margin: 0; cursor: pointer;">
<span>${label}</span>
</label>
`;
}).join('');
}
function getVdl2MainSelectedFreqs() {
const checkboxes = document.querySelectorAll('.vdl2-main-freq-cb:checked');
const selected = Array.from(checkboxes).map(cb => cb.value);
if (selected.length === 0) {
const region = document.getElementById('vdl2RegionSelect').value;
return vdl2MainFrequencies[region] || vdl2MainFrequencies['na'];
}
return selected;
}
function startVdl2Mode() {
const gain = document.getElementById('vdl2GainInput').value || '40';
const device = document.getElementById('deviceSelect')?.value || '0';
const frequencies = getVdl2MainSelectedFreqs();
fetch('/vdl2/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device, gain, frequencies })
})
.then(r => r.json())
.then(data => {
if (data.status === 'started') {
document.getElementById('startVdl2Btn').style.display = 'none';
document.getElementById('stopVdl2Btn').style.display = 'block';
document.getElementById('vdl2StatusText').textContent = 'Listening';
document.getElementById('vdl2StatusText').style.color = 'var(--accent-green)';
vdl2MainMsgCount = 0;
startVdl2MainSSE();
} else {
alert(data.message || 'Failed to start VDL2');
}
})
.catch(err => alert('Error: ' + err.message));
}
function stopVdl2Mode() {
fetch('/vdl2/stop', { method: 'POST' })
.then(r => r.json())
.then(() => {
document.getElementById('startVdl2Btn').style.display = 'block';
document.getElementById('stopVdl2Btn').style.display = 'none';
document.getElementById('vdl2StatusText').textContent = 'Standby';
document.getElementById('vdl2StatusText').style.color = 'var(--accent-yellow)';
if (vdl2MainEventSource) {
vdl2MainEventSource.close();
vdl2MainEventSource = null;
}
});
}
function startVdl2MainSSE() {
if (vdl2MainEventSource) vdl2MainEventSource.close();
vdl2MainEventSource = new EventSource('/vdl2/stream');
vdl2MainEventSource.onmessage = function(e) {
try {
const data = JSON.parse(e.data);
if (data.type === 'vdl2') {
vdl2MainMsgCount++;
document.getElementById('vdl2MessageCount').textContent = vdl2MainMsgCount;
}
} catch (err) {}
};
vdl2MainEventSource.onerror = function() {
setTimeout(() => {
if (document.getElementById('stopVdl2Btn').style.display === 'block') {
startVdl2MainSSE();
}
}, 2000);
};
}
// Check initial status
fetch('/vdl2/status')
.then(r => r.json())
.then(data => {
if (data.running) {
document.getElementById('startVdl2Btn').style.display = 'none';
document.getElementById('stopVdl2Btn').style.display = 'block';
document.getElementById('vdl2StatusText').textContent = 'Listening';
document.getElementById('vdl2StatusText').style.color = 'var(--accent-green)';
document.getElementById('vdl2MessageCount').textContent = data.message_count || 0;
vdl2MainMsgCount = data.message_count || 0;
startVdl2MainSSE();
}
})
.catch(() => {});
// Initialize frequency checkboxes
document.addEventListener('DOMContentLoaded', () => updateVdl2MainFreqs());
</script>
+4
View File
@@ -67,6 +67,8 @@
{{ mode_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
{{ mode_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
{{ mode_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
{{ mode_item('acars', 'ACARS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/><line x1="4" y1="4" x2="8" y2="8"/><line x1="20" y1="4" x2="16" y2="8"/></svg>') }}
{{ mode_item('vdl2', 'VDL2', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/><path d="M2 2l4 4M22 2l-4 4M12 12v0"/></svg>') }}
{{ mode_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
{{ mode_item('listening', 'Listening Post', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
{{ mode_item('spystations', 'Spy Stations', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }}
@@ -178,6 +180,8 @@
{{ mobile_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
{{ mobile_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
{{ mobile_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
{{ mobile_item('acars', 'ACARS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>') }}
{{ mobile_item('vdl2', 'VDL2', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>') }}
{{ mobile_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
{{ mobile_item('wifi', 'WiFi', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg>') }}
{{ mobile_item('bluetooth', 'BT', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg>') }}