mirror of
https://github.com/smittix/intercept.git
synced 2026-07-08 01:28:13 -07:00
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:
@@ -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
@@ -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…</span></div>
|
||||
@@ -3138,8 +3153,30 @@
|
||||
<div class="sys-card-header">Memory</div>
|
||||
<div class="sys-card-body"><span class="sys-metric-na">Connecting…</span></div>
|
||||
</div>
|
||||
<div class="sys-card" id="sysCardTemp">
|
||||
<div class="sys-card-header">Temperature & Power</div>
|
||||
<div class="sys-card-body"><span class="sys-metric-na">Connecting…</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: NETWORK & LOCATION -->
|
||||
<div class="sys-group-header">Network & 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…</span></div>
|
||||
</div>
|
||||
<div class="sys-card" id="sysCardLocation">
|
||||
<div class="sys-card-header">Location & Weather</div>
|
||||
<div class="sys-card-body"><span class="sys-metric-na">Loading…</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…</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Row 3: EQUIPMENT & OPERATIONS -->
|
||||
<div class="sys-group-header">Equipment & Operations</div>
|
||||
<div class="sys-card" id="sysCardDisk">
|
||||
<div class="sys-card-header">Disk</div>
|
||||
<div class="sys-card-header">Disk & Storage</div>
|
||||
<div class="sys-card-body"><span class="sys-metric-na">Connecting…</span></div>
|
||||
</div>
|
||||
<div class="sys-card" id="sysCardSdr">
|
||||
@@ -3147,11 +3184,7 @@
|
||||
<div class="sys-card-body"><span class="sys-metric-na">Scanning…</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…</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…</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()}]`;
|
||||
|
||||
@@ -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–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–406 MHz)</option>
|
||||
<option value="eu">Europe (400–403 MHz)</option>
|
||||
<option value="us">US (400–406 MHz)</option>
|
||||
<option value="au">Australia (400–403 MHz)</option>
|
||||
<option value="custom">Custom…</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);">–</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">—</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 — 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 — mount near antenna for best results</li>
|
||||
<li><strong style="color: var(--text-primary);">Launches:</strong> Typically 2×/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–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: '© OpenStreetMap © 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user