mirror of
https://github.com/smittix/intercept.git
synced 2026-05-31 10:13:37 -07:00
8379f42ec3
SSE EventSource connections for AIS, ACARS, VDL2, and radiosonde were not closed when switching modes, causing fd exhaustion after repeated switches. Also fixes socket leaks on exception paths in AIS/ADS-B stream parsers, closes subprocess pipes in safe_terminate/cleanup, and caches skyfield timescale at module level to avoid per-request fd churn. Closes #169 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
243 lines
11 KiB
HTML
243 lines
11 KiB
HTML
<!-- 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',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ source: 'vdl2_mode' })
|
|
})
|
|
.then(async (r) => {
|
|
const text = await r.text();
|
|
const data = text ? JSON.parse(text) : {};
|
|
if (!r.ok || (data.status !== 'stopped' && data.status !== 'success')) {
|
|
throw new Error(data.message || `HTTP ${r.status}`);
|
|
}
|
|
return data;
|
|
})
|
|
.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;
|
|
}
|
|
})
|
|
.catch(err => alert('Failed to stop VDL2: ' + err.message));
|
|
}
|
|
|
|
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(() => {
|
|
const panel = document.getElementById('vdl2Mode');
|
|
if (panel && panel.classList.contains('active') &&
|
|
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>
|