Merge upstream/main and resolve acars, vdl2, dashboard conflicts

Resolved conflicts:
- routes/acars.py: keep /messages and /clear endpoints for history reload
- routes/vdl2.py: keep /messages and /clear endpoints for history reload
- templates/adsb_dashboard.html: keep removal of hardcoded device-1
  defaults for ACARS/VDL2 selectors (users pick their own device)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mitchross
2026-02-27 14:47:57 -05:00
41 changed files with 5065 additions and 1951 deletions
+49 -27
View File
@@ -1762,31 +1762,37 @@ ACARS: ${r.statistics.acarsMessages} messages`;
airbandSelect.innerHTML = '';
if (devices.length === 0) {
adsbSelect.innerHTML = '<option value="0">No SDR found</option>';
airbandSelect.innerHTML = '<option value="0">No SDR found</option>';
adsbSelect.innerHTML = '<option value="rtlsdr:0">No SDR found</option>';
airbandSelect.innerHTML = '<option value="rtlsdr:0">No SDR found</option>';
airbandSelect.disabled = true;
} else {
devices.forEach((dev, i) => {
const idx = dev.index !== undefined ? dev.index : i;
const sdrType = dev.sdr_type || 'rtlsdr';
const compositeVal = `${sdrType}:${idx}`;
const displayName = `SDR ${idx}: ${dev.name}`;
// Add to ADS-B selector
const adsbOpt = document.createElement('option');
adsbOpt.value = idx;
adsbOpt.value = compositeVal;
adsbOpt.dataset.sdrType = sdrType;
adsbOpt.dataset.index = idx;
adsbOpt.textContent = displayName;
adsbSelect.appendChild(adsbOpt);
// Add to Airband selector
const airbandOpt = document.createElement('option');
airbandOpt.value = idx;
airbandOpt.value = compositeVal;
airbandOpt.dataset.sdrType = sdrType;
airbandOpt.dataset.index = idx;
airbandOpt.textContent = displayName;
airbandSelect.appendChild(airbandOpt);
});
// Default: ADS-B uses first device, Airband uses second (if available)
adsbSelect.value = devices[0].index !== undefined ? devices[0].index : 0;
adsbSelect.value = adsbSelect.options[0]?.value || 'rtlsdr:0';
if (devices.length > 1) {
airbandSelect.value = devices[1].index !== undefined ? devices[1].index : 1;
airbandSelect.value = airbandSelect.options[1]?.value || airbandSelect.options[0]?.value || 'rtlsdr:0';
}
// Show warning if only one device
@@ -1797,8 +1803,8 @@ ACARS: ${r.statistics.acarsMessages} messages`;
}
})
.catch(() => {
document.getElementById('adsbDeviceSelect').innerHTML = '<option value="0">Error</option>';
document.getElementById('airbandDeviceSelect').innerHTML = '<option value="0">Error</option>';
document.getElementById('adsbDeviceSelect').innerHTML = '<option value="rtlsdr:0">Error</option>';
document.getElementById('airbandDeviceSelect').innerHTML = '<option value="rtlsdr:0">Error</option>';
});
}
@@ -2161,11 +2167,14 @@ sudo make install</code>
}
}
// Get selected ADS-B device
const adsbDevice = parseInt(document.getElementById('adsbDeviceSelect').value) || 0;
// Get selected ADS-B device (composite value "sdr_type:index")
const adsbSelectVal = document.getElementById('adsbDeviceSelect').value || 'rtlsdr:0';
const [adsbSdrType, adsbDeviceIdx] = adsbSelectVal.includes(':') ? adsbSelectVal.split(':') : ['rtlsdr', adsbSelectVal];
const adsbDevice = parseInt(adsbDeviceIdx) || 0;
const requestBody = {
device: adsbDevice,
sdr_type: adsbSdrType,
bias_t: getBiasTEnabled()
};
if (remoteConfig) {
@@ -2316,11 +2325,13 @@ sudo make install</code>
}
const sessionDevice = session.device_index;
const sessionSdrType = session.sdr_type || 'rtlsdr';
if (sessionDevice !== null && sessionDevice !== undefined) {
adsbActiveDevice = sessionDevice;
const adsbSelect = document.getElementById('adsbDeviceSelect');
if (adsbSelect) {
adsbSelect.value = sessionDevice;
// Use composite value to select the correct device+type
adsbSelect.value = `${sessionSdrType}:${sessionDevice}`;
}
}
@@ -3834,8 +3845,9 @@ sudo make install</code>
function startAcars() {
const acarsSelect = document.getElementById('acarsDeviceSelect');
const device = acarsSelect.value;
const sdr_type = acarsSelect.selectedOptions[0]?.dataset.sdrType || 'rtlsdr';
const compositeVal = acarsSelect.value || 'rtlsdr:0';
const [sdr_type, deviceIdx] = compositeVal.includes(':') ? compositeVal.split(':') : ['rtlsdr', compositeVal];
const device = deviceIdx;
const frequencies = getAcarsRegionFreqs();
// Check if using agent mode
@@ -4179,13 +4191,16 @@ sudo make install</code>
const select = document.getElementById('acarsDeviceSelect');
select.innerHTML = '';
if (devices.length === 0) {
select.innerHTML = '<option value="0">No SDR detected</option>';
select.innerHTML = '<option value="rtlsdr: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'}`;
const sdrType = d.sdr_type || 'rtlsdr';
const idx = d.index !== undefined ? d.index : i;
opt.value = `${sdrType}:${idx}`;
opt.dataset.sdrType = sdrType;
opt.dataset.index = idx;
opt.textContent = `SDR ${idx}: ${d.name || d.type || 'SDR'}`;
select.appendChild(opt);
});
}
@@ -4277,8 +4292,9 @@ sudo make install</code>
function startVdl2() {
const vdl2Select = document.getElementById('vdl2DeviceSelect');
const device = vdl2Select.value;
const sdr_type = vdl2Select.selectedOptions[0]?.dataset.sdrType || 'rtlsdr';
const compositeVal = vdl2Select.value || 'rtlsdr:0';
const [sdr_type, deviceIdx] = compositeVal.includes(':') ? compositeVal.split(':') : ['rtlsdr', compositeVal];
const device = deviceIdx;
const frequencies = getVdl2RegionFreqs();
// Check if using agent mode
@@ -4723,13 +4739,16 @@ sudo make install</code>
const select = document.getElementById('vdl2DeviceSelect');
select.innerHTML = '';
if (devices.length === 0) {
select.innerHTML = '<option value="0">No SDR detected</option>';
select.innerHTML = '<option value="rtlsdr: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'}`;
const sdrType = d.sdr_type || 'rtlsdr';
const idx = d.index !== undefined ? d.index : i;
opt.value = `${sdrType}:${idx}`;
opt.dataset.sdrType = sdrType;
opt.dataset.index = idx;
opt.textContent = `SDR ${idx}: ${d.name || d.type || 'SDR'}`;
select.appendChild(opt);
});
}
@@ -5715,13 +5734,16 @@ sudo make install</code>
select.innerHTML = '';
if (devices.length === 0) {
select.innerHTML = '<option value="0">No SDR found</option>';
select.innerHTML = '<option value="rtlsdr:0">No SDR found</option>';
} else {
devices.forEach(device => {
const opt = document.createElement('option');
opt.value = device.index;
opt.dataset.sdrType = device.sdr_type || 'rtlsdr';
opt.textContent = `SDR ${device.index}: ${device.name || device.type || 'SDR'}`;
const sdrType = device.sdr_type || 'rtlsdr';
const idx = device.index !== undefined ? device.index : 0;
opt.value = `${sdrType}:${idx}`;
opt.dataset.sdrType = sdrType;
opt.dataset.index = idx;
opt.textContent = `SDR ${idx}: ${device.name || device.type || 'SDR'}`;
select.appendChild(opt);
});
}
+90 -49
View File
@@ -83,6 +83,7 @@
spaceweather: "{{ url_for('static', filename='css/modes/space-weather.css') }}",
wefax: "{{ url_for('static', filename='css/modes/wefax.css') }}",
morse: "{{ url_for('static', filename='css/modes/morse.css') }}",
radiosonde: "{{ url_for('static', filename='css/modes/radiosonde.css') }}",
system: "{{ url_for('static', filename='css/modes/system.css') }}"
};
window.INTERCEPT_MODE_STYLE_LOADED = {};
@@ -307,6 +308,10 @@
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg></span>
<span class="mode-name">GPS</span>
</button>
<button class="mode-card mode-card-sm" onclick="selectMode('radiosonde')">
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v6"/><circle cx="12" cy="12" r="4"/><path d="M12 16v6"/><path d="M4.93 4.93l4.24 4.24"/><path d="M14.83 14.83l4.24 4.24"/></svg></span>
<span class="mode-name">Radiosonde</span>
</button>
</div>
</div>
@@ -696,6 +701,8 @@
{% include 'partials/modes/ais.html' %}
{% include 'partials/modes/radiosonde.html' %}
{% include 'partials/modes/spy-stations.html' %}
{% include 'partials/modes/meshtastic.html' %}
@@ -3127,9 +3134,17 @@
</div>
</div>
<!-- Radiosonde Visuals -->
<div id="radiosondeVisuals" class="radiosonde-visuals-container" style="display: none;">
<div id="radiosondeMapContainer" style="flex: 1; min-height: 300px; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-primary);"></div>
<div id="radiosondeCardContainer" class="radiosonde-card-container"></div>
</div>
<!-- System Health Visuals -->
<div id="systemVisuals" class="sys-visuals-container" style="display: none;">
<div class="sys-dashboard">
<!-- Row 1: COMPUTE -->
<div class="sys-group-header">Compute</div>
<div class="sys-card" id="sysCardCpu">
<div class="sys-card-header">CPU</div>
<div class="sys-card-body"><span class="sys-metric-na">Connecting&hellip;</span></div>
@@ -3138,8 +3153,30 @@
<div class="sys-card-header">Memory</div>
<div class="sys-card-body"><span class="sys-metric-na">Connecting&hellip;</span></div>
</div>
<div class="sys-card" id="sysCardTemp">
<div class="sys-card-header">Temperature &amp; Power</div>
<div class="sys-card-body"><span class="sys-metric-na">Connecting&hellip;</span></div>
</div>
<!-- Row 2: NETWORK & LOCATION -->
<div class="sys-group-header">Network &amp; Location</div>
<div class="sys-card" id="sysCardNetwork">
<div class="sys-card-header">Network</div>
<div class="sys-card-body"><span class="sys-metric-na">Connecting&hellip;</span></div>
</div>
<div class="sys-card" id="sysCardLocation">
<div class="sys-card-header">Location &amp; Weather</div>
<div class="sys-card-body"><span class="sys-metric-na">Loading&hellip;</span></div>
</div>
<div class="sys-card" id="sysCardInfo">
<div class="sys-card-header">System Info</div>
<div class="sys-card-body"><span class="sys-metric-na">Connecting&hellip;</span></div>
</div>
<!-- Row 3: EQUIPMENT & OPERATIONS -->
<div class="sys-group-header">Equipment &amp; Operations</div>
<div class="sys-card" id="sysCardDisk">
<div class="sys-card-header">Disk</div>
<div class="sys-card-header">Disk &amp; Storage</div>
<div class="sys-card-body"><span class="sys-metric-na">Connecting&hellip;</span></div>
</div>
<div class="sys-card" id="sysCardSdr">
@@ -3147,11 +3184,7 @@
<div class="sys-card-body"><span class="sys-metric-na">Scanning&hellip;</span></div>
</div>
<div class="sys-card" id="sysCardProcesses">
<div class="sys-card-header">Processes</div>
<div class="sys-card-body"><span class="sys-metric-na">Connecting&hellip;</span></div>
</div>
<div class="sys-card" id="sysCardInfo">
<div class="sys-card-header">System Info</div>
<div class="sys-card-header">Active Processes</div>
<div class="sys-card-body"><span class="sys-metric-na">Connecting&hellip;</span></div>
</div>
</div>
@@ -3367,6 +3400,7 @@
subghz: { label: 'SubGHz', indicator: 'SUBGHZ', outputTitle: 'SubGHz Transceiver', group: 'signals' },
aprs: { label: 'APRS', indicator: 'APRS', outputTitle: 'APRS Tracker', group: 'tracking' },
gps: { label: 'GPS', indicator: 'GPS', outputTitle: 'GPS Receiver', group: 'tracking' },
radiosonde: { label: 'Radiosonde', indicator: 'SONDE', outputTitle: 'Radiosonde Decoder', group: 'tracking' },
satellite: { label: 'Satellite', indicator: 'SATELLITE', outputTitle: 'Satellite Monitor', group: 'space' },
sstv: { label: 'ISS SSTV', indicator: 'ISS SSTV', outputTitle: 'ISS SSTV Decoder', group: 'space' },
weathersat: { label: 'Weather Sat', indicator: 'WEATHER SAT', outputTitle: 'Weather Satellite Decoder', group: 'space' },
@@ -4106,12 +4140,27 @@
const stopPhaseMs = Math.round(performance.now() - stopPhaseStartMs);
await styleReadyPromise;
// Clean up SubGHz SSE connection when leaving the mode
if (typeof SubGhz !== 'undefined' && currentMode === 'subghz' && mode !== 'subghz') {
SubGhz.destroy();
}
if (typeof MorseMode !== 'undefined' && currentMode === 'morse' && mode !== 'morse' && typeof MorseMode.destroy === 'function') {
MorseMode.destroy();
// Generic module cleanup — destroy previous mode's timers, SSE, etc.
const moduleDestroyMap = {
subghz: () => typeof SubGhz !== 'undefined' && SubGhz.destroy(),
morse: () => typeof MorseMode !== 'undefined' && MorseMode.destroy?.(),
spaceweather: () => typeof SpaceWeather !== 'undefined' && SpaceWeather.destroy?.(),
weathersat: () => typeof WeatherSat !== 'undefined' && WeatherSat.suspend?.(),
wefax: () => typeof WeFax !== 'undefined' && WeFax.destroy?.(),
system: () => typeof SystemHealth !== 'undefined' && SystemHealth.destroy?.(),
waterfall: () => typeof Waterfall !== 'undefined' && Waterfall.destroy?.(),
gps: () => typeof GPS !== 'undefined' && GPS.destroy?.(),
meshtastic: () => typeof Meshtastic !== 'undefined' && Meshtastic.destroy?.(),
bluetooth: () => typeof BluetoothMode !== 'undefined' && BluetoothMode.destroy?.(),
wifi: () => typeof WiFiMode !== 'undefined' && WiFiMode.destroy?.(),
bt_locate: () => typeof BtLocate !== 'undefined' && BtLocate.destroy?.(),
sstv: () => typeof SSTV !== 'undefined' && SSTV.destroy?.(),
sstv_general: () => typeof SSTVGeneral !== 'undefined' && SSTVGeneral.destroy?.(),
websdr: () => typeof WebSDR !== 'undefined' && WebSDR.destroy?.(),
spystations: () => typeof SpyStations !== 'undefined' && SpyStations.destroy?.(),
};
if (previousMode && previousMode !== mode && moduleDestroyMap[previousMode]) {
try { moduleDestroyMap[previousMode](); } catch(e) { console.warn(`[switchMode] destroy ${previousMode} failed:`, e); }
}
currentMode = mode;
@@ -4155,6 +4204,7 @@
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('radiosondeMode')?.classList.toggle('active', mode === 'radiosonde');
document.getElementById('spystationsMode')?.classList.toggle('active', mode === 'spystations');
document.getElementById('meshtasticMode')?.classList.toggle('active', mode === 'meshtastic');
document.getElementById('websdrMode')?.classList.toggle('active', mode === 'websdr');
@@ -4204,6 +4254,7 @@
const wefaxVisuals = document.getElementById('wefaxVisuals');
const spaceWeatherVisuals = document.getElementById('spaceWeatherVisuals');
const waterfallVisuals = document.getElementById('waterfallVisuals');
const radiosondeVisuals = document.getElementById('radiosondeVisuals');
const systemVisuals = document.getElementById('systemVisuals');
if (wifiLayoutContainer) wifiLayoutContainer.style.display = mode === 'wifi' ? 'flex' : 'none';
if (btLayoutContainer) btLayoutContainer.style.display = mode === 'bluetooth' ? 'flex' : 'none';
@@ -4222,6 +4273,7 @@
if (wefaxVisuals) wefaxVisuals.style.display = mode === 'wefax' ? 'flex' : 'none';
if (spaceWeatherVisuals) spaceWeatherVisuals.style.display = mode === 'spaceweather' ? 'flex' : 'none';
if (waterfallVisuals) waterfallVisuals.style.display = mode === 'waterfall' ? 'flex' : 'none';
if (radiosondeVisuals) radiosondeVisuals.style.display = mode === 'radiosonde' ? 'flex' : 'none';
if (systemVisuals) systemVisuals.style.display = mode === 'system' ? 'flex' : 'none';
// Prevent Leaflet heatmap redraws on hidden BT Locate map containers.
@@ -4264,25 +4316,7 @@
refreshTscmDevices();
}
// Initialize/destroy Space Weather mode
if (mode !== 'spaceweather') {
if (typeof SpaceWeather !== 'undefined' && SpaceWeather.destroy) SpaceWeather.destroy();
}
// Suspend Weather Satellite background timers/streams when leaving the mode
if (mode !== 'weathersat') {
if (typeof WeatherSat !== 'undefined' && WeatherSat.suspend) WeatherSat.suspend();
}
// Suspend WeFax background streams when leaving the mode
if (mode !== 'wefax') {
if (typeof WeFax !== 'undefined' && WeFax.destroy) WeFax.destroy();
}
// Disconnect System Health SSE when leaving the mode
if (mode !== 'system') {
if (typeof SystemHealth !== 'undefined' && SystemHealth.destroy) SystemHealth.destroy();
}
// Module destroy is now handled by moduleDestroyMap above.
// Show/hide Device Intelligence for modes that use it (not for satellite/aircraft/tscm)
const reconBtn = document.getElementById('reconBtn');
@@ -4309,7 +4343,7 @@
// Show RTL-SDR device section for modes that use it
const rtlDeviceSection = document.getElementById('rtlDeviceSection');
if (rtlDeviceSection) {
rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'wefax' || mode === 'morse') ? 'block' : 'none';
rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'wefax' || mode === 'morse' || mode === 'radiosonde') ? 'block' : 'none';
// Save original sidebar position of SDR device section (once)
if (!rtlDeviceSection._origParent) {
rtlDeviceSection._origParent = rtlDeviceSection.parentNode;
@@ -4414,14 +4448,16 @@
if (typeof Waterfall !== 'undefined') Waterfall.init();
} else if (mode === 'morse') {
MorseMode.init();
} else if (mode === 'radiosonde') {
initRadiosondeMap();
setTimeout(() => {
if (radiosondeMap) radiosondeMap.invalidateSize();
}, 100);
} else if (mode === 'system') {
SystemHealth.init();
}
// Destroy Waterfall WebSocket when leaving SDR receiver modes
if (mode !== 'waterfall' && typeof Waterfall !== 'undefined' && Waterfall.destroy) {
Promise.resolve(Waterfall.destroy()).catch(() => {});
}
// Waterfall destroy is now handled by moduleDestroyMap above.
const totalMs = Math.round(performance.now() - switchStartMs);
console.info(
@@ -5626,37 +5662,41 @@
let currentDeviceList = [];
// SDR Device Usage Tracking
// Tracks which mode is using which device index
// Tracks which mode is using which device (keyed by "sdr_type:index")
const sdrDeviceUsage = {
// deviceIndex: 'modeName' (e.g., 0: 'pager', 1: 'scanner')
// "sdr_type:index": 'modeName' (e.g., "rtlsdr:0": 'pager', "hackrf:0": 'scanner')
};
function getDeviceInUseBy(deviceIndex) {
return sdrDeviceUsage[deviceIndex] || null;
function getDeviceInUseBy(deviceIndex, sdrType) {
const key = `${sdrType || getSelectedSDRType()}:${deviceIndex}`;
return sdrDeviceUsage[key] || null;
}
function isDeviceInUse(deviceIndex) {
return sdrDeviceUsage[deviceIndex] !== undefined;
function isDeviceInUse(deviceIndex, sdrType) {
const key = `${sdrType || getSelectedSDRType()}:${deviceIndex}`;
return sdrDeviceUsage[key] !== undefined;
}
function reserveDevice(deviceIndex, modeName) {
sdrDeviceUsage[deviceIndex] = modeName;
function reserveDevice(deviceIndex, modeName, sdrType) {
const key = `${sdrType || getSelectedSDRType()}:${deviceIndex}`;
sdrDeviceUsage[key] = modeName;
updateDeviceSelectStatus();
}
function releaseDevice(modeName) {
for (const [idx, mode] of Object.entries(sdrDeviceUsage)) {
for (const [key, mode] of Object.entries(sdrDeviceUsage)) {
if (mode === modeName) {
delete sdrDeviceUsage[idx];
delete sdrDeviceUsage[key];
}
}
updateDeviceSelectStatus();
}
function getAvailableDevice() {
// Find first device not in use
// Find first device not in use (within selected SDR type)
const sdrType = getSelectedSDRType();
for (const device of currentDeviceList) {
if (!isDeviceInUse(device.index)) {
if ((device.sdr_type || 'rtlsdr') === sdrType && !isDeviceInUse(device.index, sdrType)) {
return device.index;
}
}
@@ -5668,10 +5708,11 @@
const select = document.getElementById('deviceSelect');
if (!select) return;
const sdrType = getSelectedSDRType();
const options = select.querySelectorAll('option');
options.forEach(opt => {
const idx = parseInt(opt.value);
const usedBy = getDeviceInUseBy(idx);
const usedBy = getDeviceInUseBy(idx, sdrType);
const baseName = opt.textContent.replace(/ \[.*\]$/, ''); // Remove existing status
if (usedBy) {
opt.textContent = `${baseName} [${usedBy.toUpperCase()}]`;
+383
View File
@@ -0,0 +1,383 @@
<!-- RADIOSONDE WEATHER BALLOON TRACKING MODE -->
<div id="radiosondeMode" class="mode-content">
<div class="section">
<h3>Radiosonde Decoder</h3>
<div class="info-text" style="margin-bottom: 15px;">
Track weather balloons via radiosonde telemetry on 400&ndash;406 MHz. Decodes position, altitude, temperature, humidity, and pressure.
</div>
</div>
<div class="section">
<h3>Settings</h3>
<div class="form-group">
<label>Region / Frequency Band</label>
<select id="radiosondeRegionSelect" onchange="updateRadiosondeFreqRange()">
<option value="global" selected>Global (400&ndash;406 MHz)</option>
<option value="eu">Europe (400&ndash;403 MHz)</option>
<option value="us">US (400&ndash;406 MHz)</option>
<option value="au">Australia (400&ndash;403 MHz)</option>
<option value="custom">Custom&hellip;</option>
</select>
</div>
<div class="form-group" id="radiosondeCustomFreqGroup" style="display: none;">
<label>Frequency Range (MHz)</label>
<div style="display: flex; gap: 8px; align-items: center;">
<input type="number" id="radiosondeFreqMin" value="400.0" min="380" max="410" step="0.1" style="width: 50%;" placeholder="Min">
<span style="color: var(--text-dim);">&ndash;</span>
<input type="number" id="radiosondeFreqMax" value="406.0" min="380" max="410" step="0.1" style="width: 50%;" placeholder="Max">
</div>
</div>
<div class="form-group">
<label>Gain (dB, 0 = auto)</label>
<input type="number" id="radiosondeGainInput" value="40" min="0" max="50" placeholder="0-50">
</div>
</div>
<div class="section">
<h3>Status</h3>
<div id="radiosondeStatusDisplay" class="info-text">
<p>Status: <span id="radiosondeStatusText" style="color: var(--accent-yellow);">Standby</span></p>
<p>Balloons: <span id="radiosondeBalloonCount">0</span></p>
<p>Last update: <span id="radiosondeLastUpdate">&mdash;</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;">
400 MHz meteorological band &mdash; stock SDR antenna may work for nearby launches
</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 Quarter-Wave</strong>
<ul style="margin: 6px 0 0 14px; padding: 0;">
<li><strong style="color: var(--text-primary);">Element length:</strong> ~18.7 cm (quarter-wave at 400 MHz)</li>
<li><strong style="color: var(--text-primary);">Material:</strong> Wire or copper rod</li>
<li><strong style="color: var(--text-primary);">Orientation:</strong> Vertical</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; margin-bottom: 10px;">
<strong style="color: var(--accent-cyan); font-size: 12px;">Tips</strong>
<ul style="margin: 6px 0 0 14px; padding: 0;">
<li><strong style="color: var(--text-primary);">Range:</strong> 200+ km with LNA and good antenna placement</li>
<li><strong style="color: var(--text-primary);">LNA:</strong> Recommended &mdash; mount near antenna for best results</li>
<li><strong style="color: var(--text-primary);">Launches:</strong> Typically 2&times;/day at 00Z and 12Z from weather stations</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);">Frequency band</td>
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">400&ndash;406 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;">18.7 cm</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 3px 4px; color: var(--text-dim);">Common types</td>
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">RS41, RS92, DFM, M10</td>
</tr>
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 3px 4px; color: var(--text-dim);">Max altitude</td>
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">~35 km (115,000 ft)</td>
</tr>
<tr>
<td style="padding: 3px 4px; color: var(--text-dim);">Flight duration</td>
<td style="padding: 3px 4px; color: var(--text-primary); text-align: right;">~90 min ascent</td>
</tr>
</table>
</div>
</div>
</div>
<button class="run-btn" id="startRadiosondeBtn" onclick="startRadiosondeTracking()">
Start Radiosonde Tracking
</button>
<button class="stop-btn" id="stopRadiosondeBtn" onclick="stopRadiosondeTracking()" style="display: none;">
Stop Radiosonde Tracking
</button>
</div>
<script>
let radiosondeEventSource = null;
let radiosondeBalloons = {};
function updateRadiosondeFreqRange() {
const region = document.getElementById('radiosondeRegionSelect').value;
const customGroup = document.getElementById('radiosondeCustomFreqGroup');
const minInput = document.getElementById('radiosondeFreqMin');
const maxInput = document.getElementById('radiosondeFreqMax');
const presets = {
global: [400.0, 406.0],
eu: [400.0, 403.0],
us: [400.0, 406.0],
au: [400.0, 403.0],
};
if (region === 'custom') {
customGroup.style.display = 'block';
} else {
customGroup.style.display = 'none';
if (presets[region]) {
minInput.value = presets[region][0];
maxInput.value = presets[region][1];
}
}
}
function startRadiosondeTracking() {
const gain = document.getElementById('radiosondeGainInput').value || '40';
const device = document.getElementById('deviceSelect')?.value || '0';
const freqMin = parseFloat(document.getElementById('radiosondeFreqMin').value) || 400.0;
const freqMax = parseFloat(document.getElementById('radiosondeFreqMax').value) || 406.0;
fetch('/radiosonde/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
device,
gain,
freq_min: freqMin,
freq_max: freqMax,
bias_t: typeof getBiasTEnabled === 'function' ? getBiasTEnabled() : false,
})
})
.then(r => r.json())
.then(data => {
if (data.status === 'started' || data.status === 'already_running') {
document.getElementById('startRadiosondeBtn').style.display = 'none';
document.getElementById('stopRadiosondeBtn').style.display = 'block';
document.getElementById('radiosondeStatusText').textContent = 'Tracking';
document.getElementById('radiosondeStatusText').style.color = 'var(--accent-green)';
startRadiosondeSSE();
} else {
alert(data.message || 'Failed to start radiosonde tracking');
}
})
.catch(err => alert('Error: ' + err.message));
}
function stopRadiosondeTracking() {
// Update UI immediately so the user sees feedback
document.getElementById('startRadiosondeBtn').style.display = 'block';
document.getElementById('stopRadiosondeBtn').style.display = 'none';
document.getElementById('radiosondeStatusText').textContent = 'Stopping...';
document.getElementById('radiosondeStatusText').style.color = 'var(--accent-yellow)';
if (radiosondeEventSource) {
radiosondeEventSource.close();
radiosondeEventSource = null;
}
fetch('/radiosonde/stop', { method: 'POST' })
.then(r => r.json())
.then(() => {
document.getElementById('radiosondeStatusText').textContent = 'Standby';
document.getElementById('radiosondeBalloonCount').textContent = '0';
document.getElementById('radiosondeLastUpdate').textContent = '\u2014';
radiosondeBalloons = {};
// Clear map markers
if (typeof radiosondeMap !== 'undefined' && radiosondeMap) {
radiosondeMarkers.forEach(m => radiosondeMap.removeLayer(m));
radiosondeMarkers.clear();
radiosondeTracks.forEach(t => radiosondeMap.removeLayer(t));
radiosondeTracks.clear();
}
})
.catch(() => {
document.getElementById('radiosondeStatusText').textContent = 'Standby';
});
}
function startRadiosondeSSE() {
if (radiosondeEventSource) radiosondeEventSource.close();
radiosondeEventSource = new EventSource('/radiosonde/stream');
radiosondeEventSource.onmessage = function(e) {
try {
const data = JSON.parse(e.data);
if (data.type === 'balloon') {
radiosondeBalloons[data.id] = data;
document.getElementById('radiosondeBalloonCount').textContent = Object.keys(radiosondeBalloons).length;
const now = new Date();
document.getElementById('radiosondeLastUpdate').textContent =
now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
updateRadiosondeMap(data);
updateRadiosondeCards();
}
} catch (err) {}
};
radiosondeEventSource.onerror = function() {
setTimeout(() => {
if (document.getElementById('stopRadiosondeBtn').style.display === 'block') {
startRadiosondeSSE();
}
}, 2000);
};
}
// Map management
let radiosondeMap = null;
let radiosondeMarkers = new Map();
let radiosondeTracks = new Map();
let radiosondeTrackPoints = new Map();
function initRadiosondeMap() {
if (radiosondeMap) return;
const container = document.getElementById('radiosondeMapContainer');
if (!container) return;
radiosondeMap = L.map('radiosondeMapContainer', {
center: [40, -95],
zoom: 4,
zoomControl: true,
});
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; OpenStreetMap &copy; CARTO',
maxZoom: 18,
}).addTo(radiosondeMap);
}
function updateRadiosondeMap(balloon) {
if (!radiosondeMap || !balloon.lat || !balloon.lon) return;
const id = balloon.id;
const latlng = [balloon.lat, balloon.lon];
// Altitude-based colour coding
const alt = balloon.alt || 0;
let colour;
if (alt < 5000) colour = '#00ff88';
else if (alt < 15000) colour = '#00ccff';
else if (alt < 25000) colour = '#ff9900';
else colour = '#ff3366';
// Update or create marker
if (radiosondeMarkers.has(id)) {
radiosondeMarkers.get(id).setLatLng(latlng);
} else {
const marker = L.circleMarker(latlng, {
radius: 7,
color: colour,
fillColor: colour,
fillOpacity: 0.8,
weight: 2,
}).addTo(radiosondeMap);
radiosondeMarkers.set(id, marker);
}
// Update marker colour based on altitude
radiosondeMarkers.get(id).setStyle({ color: colour, fillColor: colour });
// Build popup content
const altStr = alt ? `${Math.round(alt).toLocaleString()} m` : '--';
const tempStr = balloon.temp != null ? `${balloon.temp.toFixed(1)} °C` : '--';
const humStr = balloon.humidity != null ? `${balloon.humidity.toFixed(0)}%` : '--';
const velStr = balloon.vel_v != null ? `${balloon.vel_v.toFixed(1)} m/s` : '--';
radiosondeMarkers.get(id).bindPopup(
`<strong>${id}</strong><br>` +
`Type: ${balloon.sonde_type || '--'}<br>` +
`Alt: ${altStr}<br>` +
`Temp: ${tempStr} | Hum: ${humStr}<br>` +
`Vert: ${velStr}<br>` +
(balloon.freq ? `Freq: ${balloon.freq.toFixed(3)} MHz` : '')
);
// Track polyline
if (!radiosondeTrackPoints.has(id)) {
radiosondeTrackPoints.set(id, []);
}
radiosondeTrackPoints.get(id).push(latlng);
if (radiosondeTracks.has(id)) {
radiosondeTracks.get(id).setLatLngs(radiosondeTrackPoints.get(id));
} else {
const track = L.polyline(radiosondeTrackPoints.get(id), {
color: colour,
weight: 2,
opacity: 0.6,
dashArray: '4 4',
}).addTo(radiosondeMap);
radiosondeTracks.set(id, track);
}
// Auto-centre on first balloon
if (radiosondeMarkers.size === 1) {
radiosondeMap.setView(latlng, 8);
}
}
function updateRadiosondeCards() {
const container = document.getElementById('radiosondeCardContainer');
if (!container) return;
const sorted = Object.values(radiosondeBalloons).sort((a, b) => (b.alt || 0) - (a.alt || 0));
container.innerHTML = sorted.map(b => {
const alt = b.alt ? `${Math.round(b.alt).toLocaleString()} m` : '--';
const temp = b.temp != null ? `${b.temp.toFixed(1)}°C` : '--';
const hum = b.humidity != null ? `${b.humidity.toFixed(0)}%` : '--';
const press = b.pressure != null ? `${b.pressure.toFixed(1)} hPa` : '--';
const vel = b.vel_v != null ? `${b.vel_v > 0 ? '+' : ''}${b.vel_v.toFixed(1)} m/s` : '--';
const freq = b.freq ? `${b.freq.toFixed(3)} MHz` : '--';
return `
<div class="radiosonde-card" onclick="radiosondeMap && radiosondeMap.setView([${b.lat || 0}, ${b.lon || 0}], 10)">
<div class="radiosonde-card-header">
<span class="radiosonde-serial">${b.id}</span>
<span class="radiosonde-type">${b.sonde_type || '??'}</span>
</div>
<div class="radiosonde-stats">
<div class="radiosonde-stat">
<span class="radiosonde-stat-value">${alt}</span>
<span class="radiosonde-stat-label">ALT</span>
</div>
<div class="radiosonde-stat">
<span class="radiosonde-stat-value">${temp}</span>
<span class="radiosonde-stat-label">TEMP</span>
</div>
<div class="radiosonde-stat">
<span class="radiosonde-stat-value">${hum}</span>
<span class="radiosonde-stat-label">HUM</span>
</div>
<div class="radiosonde-stat">
<span class="radiosonde-stat-value">${press}</span>
<span class="radiosonde-stat-label">PRESS</span>
</div>
<div class="radiosonde-stat">
<span class="radiosonde-stat-value">${vel}</span>
<span class="radiosonde-stat-label">VERT</span>
</div>
<div class="radiosonde-stat">
<span class="radiosonde-stat-value">${freq}</span>
<span class="radiosonde-stat-label">FREQ</span>
</div>
</div>
</div>
`;
}).join('');
}
// Check initial status on load
fetch('/radiosonde/status')
.then(r => r.json())
.then(data => {
if (data.tracking_active) {
document.getElementById('startRadiosondeBtn').style.display = 'none';
document.getElementById('stopRadiosondeBtn').style.display = 'block';
document.getElementById('radiosondeStatusText').textContent = 'Tracking';
document.getElementById('radiosondeStatusText').style.color = 'var(--accent-green)';
document.getElementById('radiosondeBalloonCount').textContent = data.balloon_count || 0;
startRadiosondeSSE();
}
})
.catch(() => {});
</script>
+18
View File
@@ -31,6 +31,24 @@
</div>
</div>
<!-- Network & Location -->
<div class="section">
<h3>Network</h3>
<div id="sysQuickNet" style="font-size: 11px; color: var(--text-dim);">--</div>
</div>
<!-- Battery (shown only when available) -->
<div class="section" id="sysQuickBatterySection" style="display: none;">
<h3>Battery</h3>
<div id="sysQuickBattery" style="font-size: 11px; color: var(--text-dim);">--</div>
</div>
<!-- Location -->
<div class="section">
<h3>Location</h3>
<div id="sysQuickLocation" style="font-size: 11px; color: var(--text-dim);">--</div>
</div>
<!-- SDR Devices -->
<div class="section">
<h3>SDR Devices</h3>
+1
View File
@@ -84,6 +84,7 @@
{{ mode_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
{{ mode_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
{{ mode_item('gps', 'GPS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>') }}
{{ mode_item('radiosonde', 'Radiosonde', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v6"/><circle cx="12" cy="12" r="4"/><path d="M12 16v6"/><path d="M4.93 4.93l4.24 4.24"/><path d="M14.83 14.83l4.24 4.24"/></svg>') }}
</div>
</div>