mirror of
https://github.com/smittix/intercept.git
synced 2026-04-23 22:30:00 -07:00
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>
|