// FlipperDroid WebUI — Bridge Controller let config = {}; let currentTab = 'dashboard'; let toastTimer = null; let eventPollInterval = null; // ---- API ---- async function api(path, opts = {}) { try { const res = await fetch(path, opts); if (path.endsWith('.css') || path.endsWith('.js') || path.endsWith('.html')) return res; const ct = res.headers.get('content-type') || ''; if (ct.includes('json')) return await res.json(); return await res.text(); } catch (e) { showToast('Connection error', 'error'); return null; } } const post = (path, body) => api(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); // ---- Toast ---- function showToast(msg, type = 'success') { const el = document.getElementById('toast'); el.textContent = msg; el.className = 'toast ' + type + ' show'; clearTimeout(toastTimer); toastTimer = setTimeout(() => el.className = 'toast', 2500); } // ---- Tabs ---- function switchTab(tab) { currentTab = tab; document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active')); document.getElementById('page-' + tab).classList.add('active'); document.querySelector(`[data-tab="${tab}"]`).classList.add('active'); if (tab === 'events') loadEvents(); if (tab === 'gpio') initGpioGrid(); if (tab === 'stealth') stealthStatus(); } // ---- Dashboard ---- async function loadStatus() { const data = await api('/api/status'); if (!data) return; config = data; setText('info-status', data.status || 'unknown'); setText('info-transport', data.conn_type || 'none'); setText('info-device', data.device || '-'); setText('info-product', data.product || '-'); setText('info-serial', data.serial || '-'); setText('info-firmware', data.firmware_version || '-'); const badge = document.getElementById('status-badge'); if (data.status === 'connected') { badge.textContent = `Connected — ${data.conn_type?.toUpperCase() || 'USB'} — ${data.product || 'Flipper Zero'}`; } else { badge.textContent = 'Disconnected'; } // Status chips setChip('chip-conn', data.status === 'connected'); setChip('chip-gpio', data.status === 'connected' && data.config?.enable_gpio); setChip('chip-rf', data.status === 'connected' && data.config?.enable_subghz); setChip('chip-nfc', data.status === 'connected' && data.config?.enable_nfc); setChip('chip-cpu', data.status === 'connected' && data.config?.cpu_share); // Toggles if (data.config) { setToggle('toggle-autoconnect', data.config.auto_connect); setToggle('toggle-cpushare', data.config.cpu_share); } } function setChip(id, on) { const chip = document.getElementById(id); if (!chip) return; const dot = chip.querySelector('.dot'); if (dot) { dot.className = on ? 'dot' : 'dot off'; } } function setText(id, val) { const el = document.getElementById(id); if (el) el.textContent = val; } function setToggle(id, val) { const el = document.getElementById(id); if (el) el.checked = !!val; } async function setConfig(key, value) { const r = await post('/api/config', { key, value: String(value) }); if (r && r.ok) showToast('Config updated'); else showToast('Config update failed', 'error'); } async function reconnect() { await post('/api/reconnect'); showToast('Reconnecting...'); setTimeout(loadStatus, 3000); } async function pingFlipper() { const r = await post('/api/system/ping'); if (r && r.result && r.result.startsWith('OK')) showToast('Pong!'); else showToast('Ping failed: ' + (r?.result || 'no response'), 'error'); } // ---- GPIO ---- const GPIO_PINS = [ { num: 2, name: 'PA2' }, { num: 3, name: 'PA3' }, { num: 4, name: 'PA4' }, { num: 6, name: 'PA6' }, { num: 7, name: 'PA7' }, { num: 13, name: 'PB2' }, { num: 14, name: 'PB3' }, { num: 15, name: 'PB13' }, { num: 16, name: 'PB14' }, { num: 17, name: 'PC0' }, { num: 18, name: 'PC1' }, { num: 19, name: 'PC3' } ]; function initGpioGrid() { const grid = document.getElementById('gpio-grid'); if (!grid || grid.children.length > 0) return; GPIO_PINS.forEach(pin => { const div = document.createElement('div'); div.className = 'gpio-pin'; div.id = 'gpio-pin-' + pin.num; div.innerHTML = `
${pin.name}
-
`; div.onclick = () => quickGpioRead(pin.num); grid.appendChild(div); }); } async function quickGpioRead(pinNum) { const r = await post('/api/gpio/read', { pin: pinNum }); const el = document.getElementById('gpio-pin-' + pinNum); if (r && r.result && r.result.startsWith('OK:')) { const val = r.result.replace('OK:', ''); const valEl = el.querySelector('.pin-val'); valEl.textContent = val === '01' || val === '1' ? 'HIGH' : 'LOW'; el.className = 'gpio-pin ' + (val === '01' || val === '1' ? 'high' : 'low'); } } async function gpioInit() { const pin = parseInt(document.getElementById('gpio-pin').value); const mode = parseInt(document.getElementById('gpio-mode').value); const r = await post('/api/gpio/init', { pin, mode }); showToast(r?.result || 'No response'); } async function gpioRead() { const pin = parseInt(document.getElementById('gpio-pin').value); const r = await post('/api/gpio/read', { pin }); document.getElementById('gpio-result').textContent = r?.result || '-'; } async function gpioWrite(val) { const pin = parseInt(document.getElementById('gpio-pin').value); const r = await post('/api/gpio/write', { pin, value: val }); showToast(r?.result || 'No response'); } async function gpioPwm() { const pin = parseInt(document.getElementById('gpio-pin').value); const freq = parseInt(document.getElementById('gpio-pwm-freq').value); const duty = parseInt(document.getElementById('gpio-pwm-duty').value); const r = await post('/api/gpio/pwm', { pin, freq, duty }); showToast(r?.result || 'No response'); } async function gpioAdc() { const pin = parseInt(document.getElementById('gpio-pin').value); const r = await post('/api/gpio/adc', { pin }); document.getElementById('gpio-adc-result').textContent = r?.result || '-'; } // ---- SubGHz ---- async function subghzSetFreq() { const freq = parseInt(document.getElementById('subghz-freq').value); const r = await post('/api/subghz/set_freq', { freq }); showToast(r?.result || 'No response'); } async function subghzTx() { const data = document.getElementById('subghz-tx-data').value; const r = await post('/api/subghz/tx', { data }); showToast(r?.result || 'No response'); } async function subghzRxStart() { const r = await post('/api/subghz/rx_start'); showToast(r?.result || 'No response'); } async function subghzRxStop() { const r = await post('/api/subghz/rx_stop'); showToast(r?.result || 'No response'); } async function subghzGetRssi() { const r = await post('/api/subghz/get_rssi'); document.getElementById('subghz-rssi').textContent = r?.result || '-'; } async function subghzReplay() { const slot = parseInt(document.getElementById('subghz-replay-slot').value); const r = await post('/api/subghz/replay', { slot }); showToast(r?.result || 'No response'); } // ---- NFC ---- async function nfcPoll() { appendLog('nfc-log', 'Polling for NFC card...'); const r = await post('/api/nfc/poll'); appendLog('nfc-log', r?.result || 'No response'); } async function nfcReadFull() { appendLog('nfc-log', 'Reading full card...'); const r = await post('/api/nfc/read_full'); appendLog('nfc-log', r?.result || 'No response'); } async function nfcRelayStart() { const r = await post('/api/nfc/relay_start'); showToast(r?.result || 'No response'); } async function nfcRelayStop() { const r = await post('/api/nfc/relay_stop'); showToast(r?.result || 'No response'); } // ---- RFID ---- async function rfidRead() { appendLog('rfid-log', 'Reading RFID tag...'); const r = await post('/api/rfid/read'); appendLog('rfid-log', r?.result || 'No response'); } async function rfidEmulate() { const data = document.getElementById('rfid-emu-data').value; const r = await post('/api/rfid/emulate', { data }); showToast(r?.result || 'No response'); } // ---- IR ---- async function irTx() { const protocol = parseInt(document.getElementById('ir-proto').value); const address = parseInt(document.getElementById('ir-addr').value); const command = parseInt(document.getElementById('ir-cmd').value); const r = await post('/api/ir/tx', { protocol, address, command }); showToast(r?.result || 'No response'); } async function irRxStart() { const r = await post('/api/ir/rx_start'); showToast(r?.result || 'No response'); } async function irRxStop() { const r = await post('/api/ir/rx_stop'); showToast(r?.result || 'No response'); } async function irReplay() { const slot = parseInt(document.getElementById('ir-replay-slot').value); const r = await post('/api/ir/replay', { slot }); showToast(r?.result || 'No response'); } // ---- Stealth ---- async function stealthStatus() { const r = await api('/api/stealth/status'); const el = document.getElementById('stealth-status-text'); const countEl = document.getElementById('stealth-mount-count'); const logEl = document.getElementById('stealth-log'); if (r && typeof r === 'object') { el.textContent = r.active_bind_mounts > 0 ? 'ACTIVE' : 'INACTIVE'; el.style.color = r.active_bind_mounts > 0 ? 'var(--success)' : 'var(--text-muted)'; countEl.textContent = r.active_bind_mounts || '0'; if (logEl) { logEl.textContent = JSON.stringify(r, null, 2); } } else { el.textContent = 'unavailable'; } } async function stealthApply() { const r = await post('/api/stealth/apply'); showToast(r?.message || 'Stealth applied'); setTimeout(stealthStatus, 1000); } async function stealthTeardown() { const r = await post('/api/stealth/teardown'); showToast(r?.message || 'Stealth removed'); setTimeout(stealthStatus, 1000); } async function stealthHideDev() { const r = await post('/api/stealth/hide'); showToast(r?.message || 'Device hidden'); setTimeout(stealthStatus, 1000); } async function stealthShowDev() { const r = await post('/api/stealth/show'); showToast(r?.message || 'Device visible'); setTimeout(stealthStatus, 1000); } // ---- Events ---- async function loadEvents() { const data = await api('/api/events'); const log = document.getElementById('events-log'); if (!log) return; if (Array.isArray(data)) { log.textContent = data.map(e => `[${new Date(e.timestamp * 1000).toLocaleTimeString()}] ${e.type}: ${e.data} ${e.extra || ''}` ).join('\n') || 'No events yet'; } else { log.textContent = data || 'No events'; } } async function loadSysLog() { const data = await api('/api/log'); const log = document.getElementById('sys-log'); if (log) log.textContent = data || 'No log data'; } // ---- Helpers ---- function appendLog(id, msg) { const el = document.getElementById(id); if (!el) return; const ts = new Date().toLocaleTimeString(); el.textContent += `[${ts}] ${msg}\n`; el.scrollTop = el.scrollHeight; } // ---- Init ---- document.addEventListener('DOMContentLoaded', () => { loadStatus(); setInterval(loadStatus, 5000); // Poll events when on events tab setInterval(() => { if (currentTab === 'events') loadEvents(); }, 3000); // Poll SubGHz log when on subghz tab setInterval(async () => { if (currentTab === 'subghz') { const data = await api('/api/events'); if (Array.isArray(data)) { const rfEvents = data.filter(e => e.type === 'subghz_rx'); if (rfEvents.length > 0) { const log = document.getElementById('subghz-log'); if (log) { log.textContent = rfEvents.map(e => `[${new Date(e.timestamp * 1000).toLocaleTimeString()}] RX: ${e.data} (${e.extra})` ).join('\n'); } } } } }, 2000); });