// HMC472A Attenuator Web UI const API_BASE = ''; // Same origin let pollInterval = null; let sweepRunning = false; // DOM elements const dbValue = document.getElementById('db-value'); const stepValue = document.getElementById('step-value'); const dbSlider = document.getElementById('db-slider'); const presetBtns = document.querySelectorAll('.preset-btn'); const pins = { v1: document.getElementById('pin-v1'), v2: document.getElementById('pin-v2'), v3: document.getElementById('pin-v3'), v4: document.getElementById('pin-v4'), v5: document.getElementById('pin-v5'), v6: document.getElementById('pin-v6') }; const sweepUpBtn = document.getElementById('sweep-up'); const sweepDownBtn = document.getElementById('sweep-down'); const sweepStopBtn = document.getElementById('sweep-stop'); const sweepDwellInput = document.getElementById('sweep-dwell'); const sweepStatus = document.getElementById('sweep-status'); const hostname = document.getElementById('hostname'); const ipAddress = document.getElementById('ip-address'); const rssi = document.getElementById('rssi'); const version = document.getElementById('version'); const otaBtn = document.getElementById('ota-btn'); // --- API Functions --- async function fetchStatus() { try { const response = await fetch(`${API_BASE}/status`); if (!response.ok) throw new Error('Failed to fetch status'); return await response.json(); } catch (error) { console.error('Status fetch error:', error); return null; } } async function setAttenuation(db) { try { const response = await fetch(`${API_BASE}/set`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ attenuation_db: db }) }); if (!response.ok) throw new Error('Failed to set attenuation'); return await response.json(); } catch (error) { console.error('Set attenuation error:', error); return null; } } async function startSweep(direction) { try { const dwellMs = parseInt(sweepDwellInput.value) || 500; const response = await fetch(`${API_BASE}/sweep`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ direction, dwell_ms: dwellMs }) }); if (!response.ok) throw new Error('Failed to start sweep'); return await response.json(); } catch (error) { console.error('Start sweep error:', error); return null; } } async function stopSweep() { try { const response = await fetch(`${API_BASE}/sweep/stop`, { method: 'POST' }); if (!response.ok) throw new Error('Failed to stop sweep'); return await response.json(); } catch (error) { console.error('Stop sweep error:', error); return null; } } async function enableOTA() { try { const response = await fetch(`${API_BASE}/ota`, { method: 'POST' }); if (!response.ok) throw new Error('Failed to enable OTA'); return await response.json(); } catch (error) { console.error('Enable OTA error:', error); return null; } } async function fetchConfig() { try { const response = await fetch(`${API_BASE}/config`); if (!response.ok) throw new Error('Failed to fetch config'); return await response.json(); } catch (error) { console.error('Config fetch error:', error); return null; } } // --- UI Update Functions --- function updateUI(status) { if (!status) return; // Update display dbValue.textContent = status.attenuation_db.toFixed(1); stepValue.textContent = status.step; dbSlider.value = status.step; // Update pin indicators const pinKeys = ['V1', 'V2', 'V3', 'V4', 'V5', 'V6']; pinKeys.forEach((key, i) => { const pinEl = pins[key.toLowerCase()]; const pinData = status.pins[key]; if (pinEl && pinData) { if (pinData.state === 'LOW') { pinEl.classList.add('low'); } else { pinEl.classList.remove('low'); } } }); // Update preset button states presetBtns.forEach(btn => { const presetDb = parseFloat(btn.dataset.db); if (Math.abs(status.attenuation_db - presetDb) < 0.01) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); // Update sweep status if (status.sweep) { sweepRunning = status.sweep.running; if (sweepRunning) { sweepStatus.textContent = `Sweeping ${status.sweep.direction}...`; sweepUpBtn.classList.toggle('active', status.sweep.direction === 'up'); sweepDownBtn.classList.toggle('active', status.sweep.direction === 'down'); } else { sweepStatus.textContent = ''; sweepUpBtn.classList.remove('active'); sweepDownBtn.classList.remove('active'); } } // Update footer status rssi.textContent = `${status.wifi_rssi} dBm`; version.textContent = `v${status.version}`; } function updateConfig(config) { if (!config) return; hostname.textContent = `${config.hostname}.local`; ipAddress.textContent = config.ip; version.textContent = `v${config.version}`; if (config.ota_enabled) { otaBtn.textContent = 'OTA Enabled'; otaBtn.classList.add('enabled'); otaBtn.disabled = true; } } // --- Event Handlers --- dbSlider.addEventListener('input', async () => { const step = parseInt(dbSlider.value); const db = step * 0.5; // Optimistic UI update dbValue.textContent = db.toFixed(1); stepValue.textContent = step; // Send to API (debounced by slider behavior) const status = await setAttenuation(db); if (status) updateUI(status); }); presetBtns.forEach(btn => { btn.addEventListener('click', async () => { const db = parseFloat(btn.dataset.db); const status = await setAttenuation(db); if (status) updateUI(status); }); }); sweepUpBtn.addEventListener('click', async () => { if (sweepRunning) return; await startSweep('up'); startPolling(100); // Poll faster during sweep }); sweepDownBtn.addEventListener('click', async () => { if (sweepRunning) return; await startSweep('down'); startPolling(100); }); sweepStopBtn.addEventListener('click', async () => { await stopSweep(); startPolling(1000); // Return to normal polling }); otaBtn.addEventListener('click', async () => { const result = await enableOTA(); if (result) { otaBtn.textContent = 'OTA Enabled'; otaBtn.classList.add('enabled'); otaBtn.disabled = true; } }); // --- Polling --- function startPolling(intervalMs = 1000) { if (pollInterval) { clearInterval(pollInterval); } pollInterval = setInterval(async () => { const status = await fetchStatus(); if (status) { updateUI(status); // Slow down polling if sweep stopped if (!status.sweep?.running && intervalMs < 1000) { startPolling(1000); } } }, intervalMs); } // --- Initialization --- async function init() { // Fetch initial status and config const [status, config] = await Promise.all([ fetchStatus(), fetchConfig() ]); if (status) updateUI(status); if (config) updateConfig(config); // Start background polling startPolling(1000); } // Start when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }