feat(drone): replace freeform inputs with populated device selects

Add /drone/devices endpoint that enumerates available WiFi interfaces
(via iw/iwconfig) and RTL-SDR devices (via SDRFactory.detect_devices),
matching the pattern used by TSCM.

Sidebar WiFi interface and RTL-SDR inputs are now <select> elements
populated on init() from /drone/devices, consistent with how other
modes expose hardware selection. HackRF checkbox remains as a toggle
since it's a binary capability rather than an enumerated device list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
James Smith
2026-05-13 09:25:21 +01:00
parent 6523686aca
commit 4ba8a40af9
3 changed files with 140 additions and 5 deletions
+56 -2
View File
@@ -10,11 +10,63 @@
function init() {
document.getElementById('droneStartBtn')?.addEventListener('click', _start);
document.getElementById('droneStopBtn')?.addEventListener('click', _stop);
_refreshDevices();
_initMap();
_connectSSE();
_refreshStatus();
}
function _refreshDevices() {
fetch('/drone/devices')
.then(function (r) { return r.json(); })
.then(function (data) {
const devs = data.devices || {};
_populateSelect(
'droneWifiIface',
devs.wifi_interfaces || [],
function (i) { return i.name; },
function (i) { return i.display_name || i.name; },
'No WiFi interfaces found'
);
_populateSelect(
'droneRtlIndex',
devs.sdr_devices || [],
function (d) { return d.index; },
function (d) { return d.display_name || d.name; },
'No SDR devices found'
);
})
.catch(function () {
_setSelectError('droneWifiIface', 'Failed to load interfaces');
_setSelectError('droneRtlIndex', 'Failed to load devices');
});
}
function _populateSelect(id, items, valFn, labelFn, emptyMsg) {
const sel = document.getElementById(id);
if (!sel) return;
sel.innerHTML = '';
if (!items.length) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = emptyMsg;
sel.appendChild(opt);
return;
}
items.forEach(function (item) {
const opt = document.createElement('option');
opt.value = valFn(item);
opt.textContent = labelFn(item);
sel.appendChild(opt);
});
}
function _setSelectError(id, msg) {
const sel = document.getElementById(id);
if (!sel) return;
sel.innerHTML = '<option value="">' + msg + '</option>';
}
function _initMap() {
if (_map) return;
const mapEl = document.getElementById('droneMainMap');
@@ -163,8 +215,10 @@
}
function _start() {
const iface = document.getElementById('droneWifiIface')?.value.trim() || null;
const rtlIndex = parseInt(document.getElementById('droneRtlIndex')?.value, 10) || 0;
const ifaceVal = document.getElementById('droneWifiIface')?.value || '';
const iface = ifaceVal || null;
const rtlVal = document.getElementById('droneRtlIndex')?.value;
const rtlIndex = rtlVal !== '' && rtlVal != null ? parseInt(rtlVal, 10) : 0;
const useHackrf = document.getElementById('droneUseHackrf')?.checked ?? true;
fetch('/drone/start', {
method: 'POST',