mirror of
https://github.com/smittix/intercept.git
synced 2026-05-31 02:03: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>
285 lines
14 KiB
HTML
285 lines
14 KiB
HTML
<!-- 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 (N. America)</td>
|
|
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">131.550 / 130.025 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>
|
|
|
|
<!-- Live Message Feed -->
|
|
<div class="section" id="acarsMessageFeedSection" style="margin-top: 15px;">
|
|
<h3>Message Feed</h3>
|
|
<div id="acarsMessageFeed" class="acars-message-feed" style="max-height: 400px; overflow-y: auto; font-size: 11px;">
|
|
<div style="color: var(--text-muted); font-style: italic; padding: 10px 0;">Start ACARS to see live messages</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let acarsMainEventSource = null;
|
|
let acarsMainMsgCount = 0;
|
|
|
|
const acarsMainFrequencies = {
|
|
'na': ['131.550', '130.025', '129.125'],
|
|
'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, i) => {
|
|
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 acarsMainTypeBadge(type) {
|
|
const colors = {
|
|
position: '#00ff88', engine_data: '#ff9500', weather: '#00d4ff',
|
|
ats: '#ffdd00', cpdlc: '#b388ff', oooi: '#4fc3f7', squawk: '#ff6b6b',
|
|
link_test: '#666', handshake: '#555', other: '#888'
|
|
};
|
|
const labels = {
|
|
position: 'POS', engine_data: 'ENG', weather: 'WX', ats: 'ATS',
|
|
cpdlc: 'CPDLC', oooi: 'OOOI', squawk: 'SQK', link_test: 'LINK',
|
|
handshake: 'HSHK', other: 'MSG'
|
|
};
|
|
const color = colors[type] || '#888';
|
|
const lbl = labels[type] || 'MSG';
|
|
return `<span style="display:inline-block;padding:1px 5px;border-radius:3px;font-size:8px;font-weight:700;color:#000;background:${color};">${lbl}</span>`;
|
|
}
|
|
|
|
// TODO: Similar to renderAcarsCard in templates/adsb_dashboard.html — consider unifying
|
|
function renderAcarsMainCard(data) {
|
|
const flight = escapeHtml(data.flight || 'UNKNOWN');
|
|
const type = data.message_type || 'other';
|
|
const badge = acarsMainTypeBadge(type);
|
|
const desc = escapeHtml(data.label_description || (data.label ? 'Label: ' + data.label : ''));
|
|
const text = data.text || data.msg || '';
|
|
const truncText = escapeHtml(text.length > 150 ? text.substring(0, 150) + '...' : text);
|
|
const time = new Date().toLocaleTimeString();
|
|
|
|
let parsedHtml = '';
|
|
if (data.parsed) {
|
|
const p = data.parsed;
|
|
if (type === 'position' && p.lat !== undefined) {
|
|
parsedHtml = `<div style="color:var(--accent-green);margin-top:2px;font-size:10px;">${p.lat.toFixed(4)}, ${p.lon.toFixed(4)}${p.flight_level ? ' • ' + escapeHtml(String(p.flight_level)) : ''}${p.destination ? ' → ' + escapeHtml(String(p.destination)) : ''}</div>`;
|
|
} else if (type === 'engine_data') {
|
|
const parts = [];
|
|
Object.keys(p).forEach(k => parts.push(escapeHtml(k) + ': ' + escapeHtml(String(p[k].value))));
|
|
if (parts.length) parsedHtml = `<div style="color:#ff9500;margin-top:2px;font-size:10px;">${parts.slice(0, 4).join(' | ')}</div>`;
|
|
} else if (type === 'oooi' && p.origin) {
|
|
parsedHtml = `<div style="color:var(--accent-cyan);margin-top:2px;font-size:10px;">${escapeHtml(String(p.origin))} → ${escapeHtml(String(p.destination))}${p.out ? ' | OUT ' + escapeHtml(String(p.out)) : ''}${p.off ? ' OFF ' + escapeHtml(String(p.off)) : ''}${p.on ? ' ON ' + escapeHtml(String(p.on)) : ''}${p['in'] ? ' IN ' + escapeHtml(String(p['in'])) : ''}</div>`;
|
|
}
|
|
}
|
|
|
|
return `<div class="acars-feed-card" style="padding:6px 8px;border-bottom:1px solid var(--border-color);animation:fadeInMsg 0.3s ease;">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:2px;">
|
|
<span style="color:var(--accent-cyan);font-weight:bold;">${flight}</span>
|
|
<span style="color:var(--text-muted);font-size:9px;">${time}</span>
|
|
</div>
|
|
<div style="margin-top:2px;">${badge} <span style="color:var(--text-primary);">${desc}</span></div>
|
|
${parsedHtml}
|
|
${truncText && type !== 'link_test' && type !== 'handshake' ? `<div style="color:var(--text-dim);font-family:var(--font-mono);font-size:9px;margin-top:3px;word-break:break-all;">${truncText}</div>` : ''}
|
|
</div>`;
|
|
}
|
|
|
|
function startAcarsMainSSE() {
|
|
if (acarsMainEventSource) acarsMainEventSource.close();
|
|
|
|
const feed = document.getElementById('acarsMessageFeed');
|
|
if (feed && feed.querySelector('[style*="font-style: italic"]')) {
|
|
feed.innerHTML = '';
|
|
}
|
|
|
|
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;
|
|
|
|
// Add to message feed
|
|
const feed = document.getElementById('acarsMessageFeed');
|
|
if (feed) {
|
|
feed.insertAdjacentHTML('afterbegin', renderAcarsMainCard(data));
|
|
// Keep max 30 messages for RPi performance
|
|
while (feed.children.length > 30) {
|
|
feed.removeChild(feed.lastChild);
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {}
|
|
};
|
|
|
|
acarsMainEventSource.onerror = function() {
|
|
setTimeout(() => {
|
|
const panel = document.getElementById('acarsMode');
|
|
if (panel && panel.classList.contains('active') &&
|
|
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>
|