mirror of
https://github.com/smittix/intercept.git
synced 2026-07-06 00:28:12 -07:00
fix: stabilize test suite and repair frontend/backend wiring
- meshcore pin >=2.3.0 (EventType.STATS_CORE floor); setup.sh derives optional packages from requirements.txt; Python 3.10 warning - agent-mode wifi clients proxy route + bare-array response handling - remove dead AIS/ACARS/VDL2 SPA wiring and orphaned partials/CSS - agent TLE download to data/tle/ (was littering repo root as gp.php) - gate deferred background init off under pytest (mock-pollution race) - complete Popen mocks (context manager protocol, communicate tuples) - real pipe fds in weather-sat decoder tests (fd 10/11 collision caused 10s SQLite stalls); satellite tests no longer rewrite data/satellites.py - register 'live' pytest marker, excluded by default - update stale test assertions to current APIs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+3
-10
@@ -772,8 +772,6 @@
|
||||
|
||||
{% include 'partials/modes/tscm.html' %}
|
||||
|
||||
{% include 'partials/modes/ais.html' %}
|
||||
|
||||
{% include 'partials/modes/drone.html' %}
|
||||
|
||||
{% include 'partials/modes/radiosonde.html' %}
|
||||
@@ -4462,9 +4460,6 @@
|
||||
sstv_general: () => typeof SSTVGeneral !== 'undefined' && SSTVGeneral.destroy?.(),
|
||||
websdr: () => typeof WebSDR !== 'undefined' && WebSDR.destroy?.(),
|
||||
spystations: () => typeof SpyStations !== 'undefined' && SpyStations.destroy?.(),
|
||||
ais: () => { if (aisEventSource) { aisEventSource.close(); aisEventSource = null; } },
|
||||
acars: () => { if (acarsMainEventSource) { acarsMainEventSource.close(); acarsMainEventSource = null; } },
|
||||
vdl2: () => { if (vdl2MainEventSource) { vdl2MainEventSource.close(); vdl2MainEventSource = null; } },
|
||||
radiosonde: () => { if (radiosondeEventSource) { radiosondeEventSource.close(); radiosondeEventSource = null; } },
|
||||
aprs: () => {
|
||||
if (typeof destroyAprsMode === 'function') {
|
||||
@@ -4820,7 +4815,6 @@
|
||||
document.getElementById('wflMode')?.classList.toggle('active', mode === 'wifi_locate');
|
||||
document.getElementById('aprsMode')?.classList.toggle('active', mode === 'aprs');
|
||||
document.getElementById('tscmMode')?.classList.toggle('active', mode === 'tscm');
|
||||
document.getElementById('aisMode')?.classList.toggle('active', mode === 'ais');
|
||||
document.getElementById('droneMode')?.classList.toggle('active', mode === 'drone');
|
||||
document.getElementById('radiosondeMode')?.classList.toggle('active', mode === 'radiosonde');
|
||||
document.getElementById('spystationsMode')?.classList.toggle('active', mode === 'spystations');
|
||||
@@ -5006,7 +5000,7 @@
|
||||
|
||||
// Show agent selector for modes that support remote agents
|
||||
const agentSection = document.getElementById('agentSection');
|
||||
const agentModes = ['pager', 'sensor', 'rtlamr', 'aprs', 'wifi', 'bluetooth', 'aircraft', 'tscm', 'ais'];
|
||||
const agentModes = ['pager', 'sensor', 'rtlamr', 'aprs', 'wifi', 'bluetooth', 'aircraft', 'tscm'];
|
||||
if (agentSection) agentSection.style.display = agentModes.includes(mode) ? 'block' : 'none';
|
||||
|
||||
// Show RTL-SDR device section for modes that use it
|
||||
@@ -6505,7 +6499,7 @@
|
||||
document.getElementById('capGainRange').textContent = `${caps.gain_min}-${caps.gain_max} dB`;
|
||||
// Update max attribute on all mode gain inputs so constraints match the SDR
|
||||
const gainMax = caps.gain_max;
|
||||
['gain', 'sensorGain', 'aisGainInput', 'acarsGainInput', 'aprsStripGain', 'weatherSatGain'].forEach(id => {
|
||||
['gain', 'sensorGain', 'aprsStripGain', 'weatherSatGain'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.max = gainMax;
|
||||
});
|
||||
@@ -6633,8 +6627,7 @@
|
||||
// Warn if any SDR mode is currently running — bias-T is applied at
|
||||
// start time and cannot be toggled on a running device.
|
||||
const anyRunning = isRunning || isSensorRunning
|
||||
|| (typeof isAdsbRunning !== 'undefined' && isAdsbRunning)
|
||||
|| (typeof isAisRunning !== 'undefined' && isAisRunning);
|
||||
|| (typeof isAdsbRunning !== 'undefined' && isAdsbRunning);
|
||||
if (anyRunning) {
|
||||
showInfo('Bias-T change will take effect after restarting the active SDR mode');
|
||||
}
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
<!-- 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>`;
|
||||
}
|
||||
|
||||
function renderAcarsMainCard(data) {
|
||||
const flight = escapeHtml(data.flight || 'UNKNOWN');
|
||||
const tail = escapeHtml(data.tail || data.reg || '');
|
||||
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 > 200 ? text.substring(0, 200) + '...' : text);
|
||||
const time = typeof InterceptTime !== 'undefined'
|
||||
? InterceptTime.shortTime(new Date()) + InterceptTime.tzSuffix()
|
||||
: 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 ? ' • FL' + escapeHtml(String(p.flight_level)) : ''}${p.destination ? ' → ' + escapeHtml(String(p.destination)) : ''}</div>`;
|
||||
} else if (type === 'engine_data') {
|
||||
const parts = [];
|
||||
Object.keys(p).forEach(k => {
|
||||
const val = typeof p[k] === 'object' ? p[k].value : p[k];
|
||||
parts.push(escapeHtml(k) + ': ' + escapeHtml(String(val)));
|
||||
});
|
||||
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>`;
|
||||
} else if (type === 'weather' && (p.wind_speed || p.temperature)) {
|
||||
const wx = [];
|
||||
if (p.wind_speed) wx.push('Wind ' + escapeHtml(String(p.wind_speed)) + (p.wind_dir ? '/' + escapeHtml(String(p.wind_dir)) : ''));
|
||||
if (p.temperature) wx.push(escapeHtml(String(p.temperature)) + '°C');
|
||||
if (p.turbulence) wx.push('Turb: ' + escapeHtml(String(p.turbulence)));
|
||||
if (wx.length) parsedHtml = `<div style="color:#00d4ff;margin-top:2px;font-size:10px;">${wx.join(' | ')}</div>`;
|
||||
} else if (type === 'cpdlc') {
|
||||
const cpdlcText = p.message || p.text || '';
|
||||
if (cpdlcText) parsedHtml = `<div style="color:#b388ff;margin-top:2px;font-size:10px;font-weight:600;">${escapeHtml(String(cpdlcText))}</div>`;
|
||||
} else if (type === 'squawk' && p.squawk) {
|
||||
parsedHtml = `<div style="color:#ff6b6b;margin-top:2px;font-size:10px;font-weight:600;">Squawk: ${escapeHtml(String(p.squawk))}</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}${tail ? ' <span style="color:var(--text-muted);font-weight:normal;font-size:9px;">(' + tail + ')</span>' : ''}</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>
|
||||
@@ -1,218 +0,0 @@
|
||||
<!-- AIS VESSEL TRACKING MODE -->
|
||||
<div id="aisMode" class="mode-content" style="display: none;">
|
||||
<div class="section">
|
||||
<h3>AIS Vessel Tracking</h3>
|
||||
<div class="info-text" style="margin-bottom: 15px;">
|
||||
Track ships and vessels via AIS (Automatic Identification System) on 161.975 / 162.025 MHz.
|
||||
</div>
|
||||
<a href="/ais/dashboard" target="_blank" class="run-btn" style="display: inline-block; text-decoration: none; text-align: center; margin-bottom: 15px;">
|
||||
Open AIS Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Settings</h3>
|
||||
<div class="form-group">
|
||||
<label>Gain (dB, 0 = auto)</label>
|
||||
<input type="number" id="aisGainInput" value="40" min="0" max="50" placeholder="0-50">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>NMEA UDP Forward</h3>
|
||||
<p class="info-text" style="font-size: 11px; color: var(--text-dim); margin-bottom: 8px;">
|
||||
Forward NMEA 0183 sentences to an external app (e.g. OpenCPN). Leave host blank to disable.
|
||||
</p>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<div style="flex: 2;">
|
||||
<label style="font-size: 10px; color: var(--text-dim);">Host</label>
|
||||
<input type="text" id="aisUdpHost" placeholder="e.g. 192.168.1.10" style="width: 100%;">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label style="font-size: 10px; color: var(--text-dim);">Port</label>
|
||||
<input type="number" id="aisUdpPort" value="10110" min="1" max="65535" style="width: 100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Status</h3>
|
||||
<div id="aisStatusDisplay" class="info-text">
|
||||
<p>Status: <span id="aisStatusText" style="color: var(--accent-yellow);">Standby</span></p>
|
||||
<p>Vessels: <span id="aisVesselCount">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;">
|
||||
Marine VHF band (162 MHz) — stock SDR antenna will NOT work well
|
||||
</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 (Cheapest)</strong>
|
||||
<ul style="margin: 6px 0 0 14px; padding: 0;">
|
||||
<li><strong style="color: var(--text-primary);">Element length:</strong> ~46 cm each (quarter-wave at 162 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 (AIS is vertically polarized)</li>
|
||||
<li><strong style="color: var(--text-primary);">Placement:</strong> As high as possible with clear view of the water/harbor</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<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;">Commercial Options</strong>
|
||||
<ul style="margin: 6px 0 0 14px; padding: 0;">
|
||||
<li><strong style="color: var(--text-primary);">Marine VHF whip:</strong> ~$20–50, designed for 156–163 MHz band</li>
|
||||
<li><strong style="color: var(--text-primary);">Discone:</strong> ~$30–50, wideband coverage including marine VHF</li>
|
||||
<li><strong style="color: var(--text-primary);">Collinear:</strong> Higher gain (~6 dBi), best for coastal monitoring</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<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;">Placement Tips</strong>
|
||||
<ul style="margin: 6px 0 0 14px; padding: 0;">
|
||||
<li><strong style="color: var(--text-primary);">Height is critical:</strong> AIS is line-of-sight. Roof or mast mount is ideal</li>
|
||||
<li><strong style="color: var(--text-primary);">Range:</strong> At 10m height, expect ~25 NM (46 km) range over water</li>
|
||||
<li><strong style="color: var(--text-primary);">LNA:</strong> Nooelec Lana or similar broadband LNA, mount at antenna</li>
|
||||
<li><strong style="color: var(--text-primary);">Coax:</strong> Keep cable short. RG-58 loses ~4 dB per 10m at 162 MHz</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);">AIS Channel A</td>
|
||||
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">161.975 MHz</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||
<td style="padding: 3px 4px; color: var(--text-dim);">AIS Channel B</td>
|
||||
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">162.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;">46 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;">GMSK 9600 baud</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="startAisBtn" onclick="startAisTracking()">
|
||||
Start AIS Tracking
|
||||
</button>
|
||||
<button class="stop-btn" id="stopAisBtn" onclick="stopAisTracking()" style="display: none;">
|
||||
Stop AIS Tracking
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let aisEventSource = null;
|
||||
let aisVessels = {};
|
||||
|
||||
function startAisTracking() {
|
||||
const gain = document.getElementById('aisGainInput').value || '40';
|
||||
const device = document.getElementById('deviceSelect')?.value || '0';
|
||||
const udpHost = document.getElementById('aisUdpHost').value.trim();
|
||||
const udpPort = parseInt(document.getElementById('aisUdpPort').value) || 10110;
|
||||
|
||||
const body = {
|
||||
device, gain,
|
||||
bias_t: typeof getBiasTEnabled === 'function' ? getBiasTEnabled() : false,
|
||||
};
|
||||
if (udpHost) {
|
||||
body.udp_host = udpHost;
|
||||
body.udp_port = udpPort;
|
||||
}
|
||||
|
||||
fetch('/ais/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started' || data.status === 'already_running') {
|
||||
document.getElementById('startAisBtn').style.display = 'none';
|
||||
document.getElementById('stopAisBtn').style.display = 'block';
|
||||
document.getElementById('aisStatusText').textContent = 'Tracking';
|
||||
document.getElementById('aisStatusText').style.color = 'var(--accent-green)';
|
||||
startAisSSE();
|
||||
} else {
|
||||
alert(data.message || 'Failed to start AIS tracking');
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Error: ' + err.message));
|
||||
}
|
||||
|
||||
function stopAisTracking() {
|
||||
fetch('/ais/stop', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(() => {
|
||||
document.getElementById('startAisBtn').style.display = 'block';
|
||||
document.getElementById('stopAisBtn').style.display = 'none';
|
||||
document.getElementById('aisStatusText').textContent = 'Standby';
|
||||
document.getElementById('aisStatusText').style.color = 'var(--accent-yellow)';
|
||||
document.getElementById('aisVesselCount').textContent = '0';
|
||||
if (aisEventSource) {
|
||||
aisEventSource.close();
|
||||
aisEventSource = null;
|
||||
}
|
||||
aisVessels = {};
|
||||
});
|
||||
}
|
||||
|
||||
function startAisSSE() {
|
||||
if (aisEventSource) aisEventSource.close();
|
||||
|
||||
aisEventSource = new EventSource('/ais/stream');
|
||||
aisEventSource.onmessage = function(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'vessel') {
|
||||
aisVessels[data.mmsi] = data;
|
||||
document.getElementById('aisVesselCount').textContent = Object.keys(aisVessels).length;
|
||||
}
|
||||
} catch (err) {}
|
||||
};
|
||||
|
||||
aisEventSource.onerror = function() {
|
||||
setTimeout(() => {
|
||||
const panel = document.getElementById('aisMode');
|
||||
if (panel && panel.classList.contains('active') &&
|
||||
document.getElementById('stopAisBtn').style.display === 'block') {
|
||||
startAisSSE();
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
}
|
||||
|
||||
// Check initial status
|
||||
fetch('/ais/status')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.tracking_active) {
|
||||
document.getElementById('startAisBtn').style.display = 'none';
|
||||
document.getElementById('stopAisBtn').style.display = 'block';
|
||||
document.getElementById('aisStatusText').textContent = 'Tracking';
|
||||
document.getElementById('aisStatusText').style.color = 'var(--accent-green)';
|
||||
document.getElementById('aisVesselCount').textContent = data.vessel_count || 0;
|
||||
startAisSSE();
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
</script>
|
||||
@@ -1,242 +0,0 @@
|
||||
<!-- 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>
|
||||
Reference in New Issue
Block a user