mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
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:
208
templates/partials/modes/acars.html
Normal file
208
templates/partials/modes/acars.html
Normal 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 & 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) — 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
templates/partials/modes/vdl2.html
Normal file
228
templates/partials/modes/vdl2.html
Normal 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 & 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) — 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>
|
||||
Reference in New Issue
Block a user