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:
@@ -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);">⚠</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%;">
|
||||
▶ 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">◀</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' ? '▶' : '▼';">
|
||||
<span>ANTENNA GUIDE</span>
|
||||
<span class="panel-toggle" style="font-size: 10px; color: var(--text-muted);">▶</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 — 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–7 dBi omnidirectional, ideal for 360° coverage</li>
|
||||
<li><strong style="color: var(--text-primary);">Range:</strong> 150–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–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 = '■ 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 = '▶ 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 — 1090 MHz ADS-B</span>
|
||||
<button class="antenna-guide-modal-close" onclick="toggleAntennaGuide()">×</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 — 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–7 dBi omnidirectional, ideal for 360° coverage</li>
|
||||
<li><strong>Range:</strong> 150–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 & 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–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;
|
||||
|
||||
Reference in New Issue
Block a user