Add Airspy SDR support and persist GPS coordinates

Airspy support:
- Add AIRSPY to SDRType enum and driver mappings
- Create AirspyCommandBuilder using SoapySDR tools (rx_fm, readsb, rtl_433)
- Register in SDRFactory and add to hardware type dropdown
- Supports Airspy R2/Mini (24MHz-1.8GHz) and HF+ devices

GPS coordinate persistence:
- Save observer location to localStorage when manually entered or via geolocation
- Restore saved coordinates on page load in both index.html and adsb_dashboard.html
- Coordinates are shared between both pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-05 20:39:55 +00:00
parent ea54ab4bce
commit 938d021e06
8 changed files with 756 additions and 55 deletions
+81 -11
View File
@@ -141,6 +141,7 @@
<option value="manual">Manual</option>
<option value="browser">Browser</option>
<option value="dongle">USB GPS</option>
<option value="gpsd">gpsd</option>
</select>
</div>
<div class="control-group" id="browserGpsGroup">
@@ -159,6 +160,13 @@
<button class="gps-btn gps-connect-btn" onclick="startGpsDongle()">Connect</button>
<button class="gps-btn gps-disconnect-btn" onclick="stopGpsDongle()" style="display: none; background: rgba(255,0,0,0.2); border-color: #ff4444;">Stop</button>
</div>
<div class="control-group gps-gpsd-controls" style="display: none;">
<input type="text" id="gpsdHost" value="localhost" placeholder="Host" style="width: 80px; font-size: 10px;">
<span style="color: #666;">:</span>
<input type="number" id="gpsdPort" value="2947" min="1" max="65535" style="width: 50px; font-size: 10px;">
<button class="gps-btn gps-connect-btn" onclick="startGpsdClient()">Connect</button>
<button class="gps-btn gps-disconnect-btn" onclick="stopGpsDongle()" style="display: none; background: rgba(255,0,0,0.2); border-color: #ff4444;">Stop</button>
</div>
<div class="control-group">
<label style="display: flex; align-items: center; gap: 4px; font-size: 10px; cursor: pointer;">
<input type="checkbox" id="useRemoteDump1090" onchange="toggleRemoteDump1090()">
@@ -208,8 +216,17 @@
messageTimestamps: []
};
// Observer location and range rings
let observerLocation = { lat: 51.5074, lon: -0.1278 };
// Observer location and range rings (load from localStorage or default to London)
let observerLocation = (function() {
const saved = localStorage.getItem('observerLocation');
if (saved) {
try {
const parsed = JSON.parse(saved);
if (parsed.lat && parsed.lon) return parsed;
} catch (e) {}
}
return { lat: 51.5074, lon: -0.1278 };
})();
let rangeRingsLayer = null;
let observerMarker = null;
@@ -834,6 +851,10 @@
if (!isNaN(lat) && !isNaN(lon) && lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180) {
observerLocation.lat = lat;
observerLocation.lon = lon;
// Save to localStorage for persistence
localStorage.setItem('observerLocation', JSON.stringify(observerLocation));
if (radarMap) {
radarMap.setView([lat, lon], radarMap.getZoom());
}
@@ -858,6 +879,10 @@
(position) => {
observerLocation.lat = position.coords.latitude;
observerLocation.lon = position.coords.longitude;
// Save to localStorage for persistence
localStorage.setItem('observerLocation', JSON.stringify(observerLocation));
document.getElementById('obsLat').value = observerLocation.lat.toFixed(4);
document.getElementById('obsLon').value = observerLocation.lon.toFixed(4);
if (radarMap) {
@@ -881,18 +906,22 @@
const source = document.getElementById('gpsSource').value;
const browserGroup = document.getElementById('browserGpsGroup');
const dongleControls = document.querySelector('.gps-dongle-controls');
const gpsdControls = document.querySelector('.gps-gpsd-controls');
// Hide all first
browserGroup.style.display = 'none';
dongleControls.style.display = 'none';
gpsdControls.style.display = 'none';
if (source === 'dongle') {
browserGroup.style.display = 'none';
dongleControls.style.display = 'flex';
refreshGpsDevices();
} else if (source === 'browser') {
browserGroup.style.display = 'flex';
dongleControls.style.display = 'none';
} else {
browserGroup.style.display = 'none';
dongleControls.style.display = 'none';
} else if (source === 'gpsd') {
gpsdControls.style.display = 'flex';
}
// 'manual' keeps everything hidden
}
async function refreshGpsDevices() {
@@ -935,8 +964,7 @@
if (data.status === 'started') {
gpsConnected = true;
startGpsStream();
document.querySelector('.gps-connect-btn').style.display = 'none';
document.querySelector('.gps-disconnect-btn').style.display = 'block';
updateGpsButtons(true, '.gps-dongle-controls');
} else {
alert('Failed to start GPS: ' + data.message);
}
@@ -945,6 +973,41 @@
}
}
async function startGpsdClient() {
const host = document.getElementById('gpsdHost').value || 'localhost';
const port = parseInt(document.getElementById('gpsdPort').value) || 2947;
try {
const response = await fetch('/gps/gpsd/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ host: host, port: port })
});
const data = await response.json();
if (data.status === 'started') {
gpsConnected = true;
startGpsStream();
updateGpsButtons(true, '.gps-gpsd-controls');
} else {
alert('Failed to connect to gpsd: ' + data.message);
}
} catch (e) {
alert('gpsd connection error: ' + e.message);
}
}
function updateGpsButtons(connected, containerSelector) {
// Update buttons in the specified container
const container = document.querySelector(containerSelector);
if (container) {
const connectBtn = container.querySelector('.gps-connect-btn');
const disconnectBtn = container.querySelector('.gps-disconnect-btn');
if (connectBtn) connectBtn.style.display = connected ? 'none' : 'block';
if (disconnectBtn) disconnectBtn.style.display = connected ? 'block' : 'none';
}
}
async function stopGpsDongle() {
try {
if (gpsEventSource) {
@@ -953,8 +1016,9 @@
}
await fetch('/gps/stop', { method: 'POST' });
gpsConnected = false;
document.querySelector('.gps-connect-btn').style.display = 'block';
document.querySelector('.gps-disconnect-btn').style.display = 'none';
// Reset buttons in both containers
updateGpsButtons(false, '.gps-dongle-controls');
updateGpsButtons(false, '.gps-gpsd-controls');
} catch (e) {
console.warn('GPS stop error:', e);
}
@@ -1027,6 +1091,12 @@
// INITIALIZATION
// ============================================
document.addEventListener('DOMContentLoaded', () => {
// Initialize observer location input fields from saved location
const obsLatInput = document.getElementById('obsLat');
const obsLonInput = document.getElementById('obsLon');
if (obsLatInput) obsLatInput.value = observerLocation.lat.toFixed(4);
if (obsLonInput) obsLonInput.value = observerLocation.lon.toFixed(4);
initMap();
updateClock();
setInterval(updateClock, 1000);
+146 -30
View File
@@ -275,6 +275,7 @@
<option value="rtlsdr">RTL-SDR</option>
<option value="limesdr">LimeSDR</option>
<option value="hackrf">HackRF</option>
<option value="airspy">Airspy</option>
</select>
</div>
<div class="form-group">
@@ -754,22 +755,36 @@
📍 Use Browser Location
</button>
<div class="gps-dongle-section" style="display: none; margin-top: 8px; padding: 8px; background: rgba(0,212,255,0.05); border-radius: 4px;">
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
<select class="gps-device-select" style="flex: 1; font-size: 11px;">
<option value="">Select GPS Device...</option>
<div style="margin-bottom: 5px;">
<select class="gps-source-select" onchange="toggleGpsSourceMode(this)" style="width: 100%; font-size: 11px;">
<option value="serial">Serial Device</option>
<option value="gpsd">gpsd (daemon)</option>
</select>
<button class="preset-btn" onclick="refreshGpsDevices()" style="padding: 2px 6px; font-size: 10px;" title="Refresh">🔄</button>
</div>
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
<select class="gps-baudrate-select" style="flex: 1; font-size: 11px;">
<option value="4800">4800</option>
<option value="9600" selected>9600</option>
<option value="38400">38400</option>
<option value="115200">115200</option>
</select>
<div class="gps-serial-controls">
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
<select class="gps-device-select" style="flex: 1; font-size: 11px;">
<option value="">Select GPS Device...</option>
</select>
<button class="preset-btn" onclick="refreshGpsDevices()" style="padding: 2px 6px; font-size: 10px;" title="Refresh">🔄</button>
</div>
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
<select class="gps-baudrate-select" style="flex: 1; font-size: 11px;">
<option value="4800">4800</option>
<option value="9600" selected>9600</option>
<option value="38400">38400</option>
<option value="115200">115200</option>
</select>
</div>
</div>
<div class="gps-gpsd-controls" style="display: none;">
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
<input type="text" class="gpsd-host-input" value="localhost" placeholder="Host" style="flex: 2; font-size: 11px;">
<input type="number" class="gpsd-port-input" value="2947" placeholder="Port" style="flex: 1; font-size: 11px;">
</div>
</div>
<div style="display: flex; gap: 5px;">
<button class="preset-btn gps-connect-btn" onclick="startGpsDongle(this.closest('.gps-dongle-section').querySelector('.gps-device-select').value, parseInt(this.closest('.gps-dongle-section').querySelector('.gps-baudrate-select').value))" style="flex: 1; font-size: 10px; padding: 4px;">
<button class="preset-btn gps-connect-btn" onclick="startGpsFromSection(this.closest('.gps-dongle-section'))" style="flex: 1; font-size: 10px; padding: 4px;">
Connect
</button>
<button class="preset-btn gps-disconnect-btn" onclick="stopGpsDongle()" style="flex: 1; display: none; font-size: 10px; padding: 4px; background: rgba(255,0,0,0.1); border-color: #ff4444;">
@@ -865,25 +880,44 @@
</button>
<div class="gps-dongle-section" style="display: none; margin-top: 10px; padding: 10px; background: rgba(0,212,255,0.05); border-radius: 4px;">
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 11px;">GPS Device</label>
<div style="display: flex; gap: 5px;">
<select class="gps-device-select" style="flex: 1;">
<option value="">Select GPS Device...</option>
</select>
<button class="preset-btn" onclick="refreshGpsDevices()" style="padding: 4px 8px;" title="Refresh">🔄</button>
</div>
</div>
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 11px;">Baud Rate</label>
<select class="gps-baudrate-select" style="width: 100%;">
<option value="4800">4800</option>
<option value="9600" selected>9600</option>
<option value="38400">38400</option>
<option value="115200">115200</option>
<label style="font-size: 11px;">GPS Source</label>
<select class="gps-source-select" onchange="toggleGpsSourceMode(this)" style="width: 100%;">
<option value="serial">Serial Device</option>
<option value="gpsd">gpsd (daemon)</option>
</select>
</div>
<div class="gps-serial-controls">
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 11px;">GPS Device</label>
<div style="display: flex; gap: 5px;">
<select class="gps-device-select" style="flex: 1;">
<option value="">Select GPS Device...</option>
</select>
<button class="preset-btn" onclick="refreshGpsDevices()" style="padding: 4px 8px;" title="Refresh">🔄</button>
</div>
</div>
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 11px;">Baud Rate</label>
<select class="gps-baudrate-select" style="width: 100%;">
<option value="4800">4800</option>
<option value="9600" selected>9600</option>
<option value="38400">38400</option>
<option value="115200">115200</option>
</select>
</div>
</div>
<div class="gps-gpsd-controls" style="display: none;">
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 11px;">gpsd Host</label>
<input type="text" class="gpsd-host-input" value="localhost" style="width: 100%;">
</div>
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 11px;">gpsd Port</label>
<input type="number" class="gpsd-port-input" value="2947" style="width: 100%;">
</div>
</div>
<div style="display: flex; gap: 5px;">
<button class="preset-btn gps-connect-btn" onclick="startGpsDongle(this.closest('.gps-dongle-section').querySelector('.gps-device-select').value, parseInt(this.closest('.gps-dongle-section').querySelector('.gps-baudrate-select').value))" style="flex: 1;">
<button class="preset-btn gps-connect-btn" onclick="startGpsFromSection(this.closest('.gps-dongle-section'))" style="flex: 1;">
Connect GPS
</button>
<button class="preset-btn gps-disconnect-btn" onclick="stopGpsDongle()" style="flex: 1; display: none; background: rgba(255,0,0,0.1); border-color: #ff4444;">
@@ -1472,8 +1506,17 @@
sessionStart: null // When tracking started
};
// Observer location for distance calculations
let observerLocation = { lat: 51.5074, lon: -0.1278 }; // Default London
// Observer location for distance calculations (load from localStorage or default to London)
let observerLocation = (function() {
const saved = localStorage.getItem('observerLocation');
if (saved) {
try {
const parsed = JSON.parse(saved);
if (parsed.lat && parsed.lon) return parsed;
} catch (e) {}
}
return { lat: 51.5074, lon: -0.1278 };
})();
let rangeRingsLayer = null;
let observerMarkerAdsb = null;
@@ -1804,6 +1847,16 @@
this.parentElement.classList.toggle('collapsed');
});
});
// Initialize observer location input fields from saved location
const adsbLatInput = document.getElementById('adsbObsLat');
const adsbLonInput = document.getElementById('adsbObsLon');
const obsLatInput = document.getElementById('obsLat');
const obsLonInput = document.getElementById('obsLon');
if (adsbLatInput) adsbLatInput.value = observerLocation.lat.toFixed(4);
if (adsbLonInput) adsbLonInput.value = observerLocation.lon.toFixed(4);
if (obsLatInput) obsLatInput.value = observerLocation.lat.toFixed(4);
if (obsLonInput) obsLonInput.value = observerLocation.lon.toFixed(4);
});
// Toggle section collapse
@@ -6025,6 +6078,9 @@
observerLocation.lat = lat;
observerLocation.lon = lon;
// Save to localStorage for persistence
localStorage.setItem('observerLocation', JSON.stringify(observerLocation));
// Center map on location
if (aircraftMap) {
aircraftMap.setView([observerLocation.lat, observerLocation.lon], 8);
@@ -6058,6 +6114,9 @@
observerLocation.lat = position.coords.latitude;
observerLocation.lon = position.coords.longitude;
// Save to localStorage for persistence
localStorage.setItem('observerLocation', JSON.stringify(observerLocation));
// Update input fields
const latInput = document.getElementById('adsbObsLat');
const lonInput = document.getElementById('adsbObsLon');
@@ -6310,6 +6369,63 @@
});
}
function toggleGpsSourceMode(selectElement) {
// Toggle between serial and gpsd controls
const section = selectElement.closest('.gps-dongle-section');
const serialControls = section.querySelector('.gps-serial-controls');
const gpsdControls = section.querySelector('.gps-gpsd-controls');
const source = selectElement.value;
if (source === 'gpsd') {
serialControls.style.display = 'none';
gpsdControls.style.display = 'block';
} else {
serialControls.style.display = 'block';
gpsdControls.style.display = 'none';
}
}
async function startGpsFromSection(section) {
// Start GPS based on the selected source in the section
const sourceSelect = section.querySelector('.gps-source-select');
const source = sourceSelect ? sourceSelect.value : 'serial';
if (source === 'gpsd') {
const host = section.querySelector('.gpsd-host-input').value || 'localhost';
const port = parseInt(section.querySelector('.gpsd-port-input').value) || 2947;
return await startGpsd(host, port);
} else {
const devicePath = section.querySelector('.gps-device-select').value;
const baudrate = parseInt(section.querySelector('.gps-baudrate-select').value) || 9600;
return await startGpsDongle(devicePath, baudrate);
}
}
async function startGpsd(host = 'localhost', port = 2947) {
try {
const response = await fetch('/gps/gpsd/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ host: host, port: port })
});
const data = await response.json();
if (data.status === 'started') {
gpsConnected = true;
startGpsStream();
updateGpsStatus(true);
showInfo(`Connected to gpsd at ${host}:${port}`);
return true;
} else {
showError('Failed to connect to gpsd: ' + data.message);
return false;
}
} catch (e) {
showError('gpsd connection error: ' + e.message);
return false;
}
}
async function startGpsDongle(devicePath, baudrate = 9600) {
if (!devicePath) {
showError('Please select a GPS device');