Initial commit — FlipperDroid v0.1.0-poc

KernelSU module + Flipper Zero FAP that bridges both devices into a
unified pentesting platform over USB CDC serial / BT rfcomm.

Android side: bridge daemon, WebUI (:8089), bind mount namespace
isolation stealth engine. Flipper side: proper FAP with 4-view GUI,
GPIO/SubGHz/IR/file command handlers, async event streaming.
This commit is contained in:
sssnake
2026-03-31 21:26:58 -07:00
commit be81a92d44
22 changed files with 4191 additions and 0 deletions

418
webroot/css/style.css Normal file
View File

@@ -0,0 +1,418 @@
:root {
--bg-primary: #0a0e17;
--bg-card: #111827;
--bg-card-hover: #1a2332;
--bg-input: #1e293b;
--border: #1e3a5f;
--border-active: #f97316;
--text-primary: #e2e8f0;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent: #f97316;
--accent-glow: rgba(249, 115, 22, 0.3);
--success: #22c55e;
--success-bg: rgba(34, 197, 94, 0.1);
--warning: #f59e0b;
--warning-bg: rgba(245, 158, 11, 0.1);
--danger: #ef4444;
--danger-bg: rgba(239, 68, 68, 0.1);
--radius: 12px;
--radius-sm: 8px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse at 20% 50%, rgba(249,115,22,0.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(239,68,68,0.06) 0%, transparent 50%),
linear-gradient(180deg, rgba(249,115,22,0.02) 0%, transparent 40%);
pointer-events: none;
z-index: 0;
}
.app {
position: relative;
z-index: 1;
max-width: 540px;
margin: 0 auto;
padding: 16px;
padding-bottom: 80px;
}
/* Header */
.header {
text-align: center;
padding: 24px 0 20px;
}
.header-icon {
width: 56px;
height: 56px;
border-radius: 16px;
background: linear-gradient(135deg, #f97316, #ef4444);
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
box-shadow: 0 8px 32px rgba(249,115,22,0.3);
}
.header-icon svg {
width: 28px;
height: 28px;
fill: white;
}
.header h1 {
font-size: 22px;
font-weight: 700;
letter-spacing: -0.5px;
background: linear-gradient(135deg, #f97316, #ef4444);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header p {
font-size: 13px;
color: var(--text-muted);
margin-top: 4px;
}
/* Status bar */
.status-bar {
display: flex;
gap: 8px;
margin-bottom: 20px;
overflow-x: auto;
scrollbar-width: none;
padding: 2px;
}
.status-bar::-webkit-scrollbar { display: none; }
.status-chip {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
background: var(--bg-card);
border: 1px solid var(--border);
}
.status-chip .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--success);
box-shadow: 0 0 6px var(--success);
}
.status-chip .dot.warn { background: var(--warning); box-shadow: 0 0 6px var(--warning); }
.status-chip .dot.off { background: var(--text-muted); box-shadow: none; }
/* Section */
.section { margin-bottom: 16px; }
.section-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text-muted);
padding: 0 4px;
margin-bottom: 8px;
}
/* Cards */
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
margin-bottom: 8px;
padding: 12px 16px;
transition: border-color 0.2s;
}
.card:hover { border-color: rgba(249,115,22,0.3); }
.card-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
gap: 8px;
flex-wrap: wrap;
}
.card-row + .card-row { border-top: 1px solid var(--border); }
.card-row-info { flex: 1; min-width: 0; }
.card-row-label { font-size: 14px; font-weight: 500; color: var(--text-primary); }
.card-row-desc { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
/* Toggle switch */
.toggle {
position: relative;
width: 44px;
height: 26px;
flex-shrink: 0;
margin-left: 12px;
}
.toggle input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
position: absolute;
inset: 0;
background: var(--bg-input);
border-radius: 13px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid var(--border);
}
.toggle-slider::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
left: 2px;
bottom: 2px;
background: var(--text-muted);
border-radius: 50%;
transition: all 0.3s;
}
.toggle input:checked + .toggle-slider {
background: var(--accent);
border-color: var(--accent);
}
.toggle input:checked + .toggle-slider::before {
transform: translateX(18px);
background: white;
}
/* Info grid */
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
background: var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.info-cell {
background: var(--bg-card);
padding: 12px 14px;
}
.info-label {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.info-value {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
margin-top: 4px;
word-break: break-all;
}
/* Buttons */
.btn {
padding: 10px 16px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-primary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
text-align: center;
display: inline-block;
margin: 4px;
}
.btn:hover { border-color: var(--accent); background: var(--bg-card-hover); }
.btn-danger { border-color: rgba(239,68,68,0.3); color: var(--danger); }
.btn-danger:hover { background: var(--danger-bg); border-color: var(--danger); }
.btn-secondary { border-color: rgba(139,92,246,0.3); color: #8b5cf6; }
.btn-secondary:hover { background: rgba(139,92,246,0.1); border-color: #8b5cf6; }
.btn-small { padding: 6px 12px; font-size: 12px; }
/* Input fields */
.input-text {
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 12px;
color: var(--text-primary);
font-size: 13px;
outline: none;
transition: border-color 0.2s;
width: auto;
min-width: 80px;
}
.input-text:focus { border-color: var(--accent); }
.input-select {
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 12px;
color: var(--text-primary);
font-size: 13px;
outline: none;
}
/* Log output */
.log-output {
background: #030712;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 12px;
max-height: 300px;
overflow-y: auto;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 11px;
line-height: 1.6;
color: var(--text-secondary);
margin-top: 8px;
white-space: pre-wrap;
word-break: break-all;
}
.mono {
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 12px;
color: var(--accent);
}
/* GPIO grid */
.gpio-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
}
.gpio-pin {
padding: 8px 4px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--bg-input);
text-align: center;
font-size: 10px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
transition: all 0.2s;
}
.gpio-pin.active { border-color: var(--accent); color: var(--accent); background: rgba(249,115,22,0.1); }
.gpio-pin.high { border-color: var(--success); color: var(--success); background: var(--success-bg); }
.gpio-pin.low { border-color: var(--danger); color: var(--danger); background: var(--danger-bg); }
.gpio-pin .pin-name { font-weight: 600; font-size: 11px; }
.gpio-pin .pin-val { font-size: 9px; margin-top: 2px; }
/* Toast */
.toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%) translateY(80px);
padding: 12px 20px;
border-radius: var(--radius-sm);
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-primary);
font-size: 13px;
font-weight: 500;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
opacity: 0;
transition: all 0.3s ease;
z-index: 100;
white-space: nowrap;
}
.toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }
.toast.success { border-color: var(--success); }
.toast.error { border-color: var(--danger); }
/* Tab bar */
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(10,14,23,0.95);
backdrop-filter: blur(20px);
border-top: 1px solid var(--border);
display: flex;
justify-content: center;
gap: 4px;
padding: 8px 16px;
padding-bottom: max(8px, env(safe-area-inset-bottom));
z-index: 50;
}
.tab-item {
flex: 1;
max-width: 80px;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 6px 4px;
border-radius: var(--radius-sm);
color: var(--text-muted);
font-size: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
background: none;
}
.tab-item:hover { color: var(--text-secondary); }
.tab-item.active { color: var(--accent); }
.tab-item svg { width: 20px; height: 20px; fill: currentColor; }
/* Pages */
.page { display: none; }
.page.active { display: block; }
label {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
}
@media (max-width: 380px) {
.info-grid { grid-template-columns: 1fr; }
.gpio-grid { grid-template-columns: repeat(3, 1fr); }
}

305
webroot/index.html Normal file
View File

@@ -0,0 +1,305 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#0a0e17">
<title>FlipperDroid</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="app">
<div class="header">
<div class="header-icon">
<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8 2c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>
</div>
<h1>FlipperDroid</h1>
<p id="status-badge">Detecting Flipper...</p>
</div>
<div class="status-bar">
<div class="status-chip" id="chip-conn"><span class="dot off"></span> LINK</div>
<div class="status-chip" id="chip-gpio"><span class="dot off"></span> GPIO</div>
<div class="status-chip" id="chip-rf"><span class="dot off"></span> RF</div>
<div class="status-chip" id="chip-nfc"><span class="dot off"></span> NFC</div>
<div class="status-chip" id="chip-cpu"><span class="dot off"></span> CPU</div>
</div>
<!-- ==================== DASHBOARD ==================== -->
<div class="page active" id="page-dashboard">
<div class="section">
<div class="section-title">Connection</div>
<div class="card">
<div class="info-grid">
<div class="info-cell"><div class="info-label">Status</div><div class="info-value" id="info-status">-</div></div>
<div class="info-cell"><div class="info-label">Transport</div><div class="info-value" id="info-transport">-</div></div>
<div class="info-cell"><div class="info-label">Device</div><div class="info-value" id="info-device">-</div></div>
<div class="info-cell"><div class="info-label">Product</div><div class="info-value" id="info-product">-</div></div>
<div class="info-cell"><div class="info-label">Serial</div><div class="info-value" id="info-serial">-</div></div>
<div class="info-cell"><div class="info-label">Firmware</div><div class="info-value" id="info-firmware">-</div></div>
</div>
</div>
<div class="card" style="margin-top:12px">
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Auto-Connect</div>
<div class="card-row-desc">Reconnect to Flipper on boot and disconnect</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-autoconnect" onchange="setConfig('AUTO_CONNECT', this.checked?1:0)"><span class="toggle-slider"></span></label>
</div>
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">CPU Sharing</div>
<div class="card-row-desc">Let Flipper offload compute to phone CPU</div>
</div>
<label class="toggle"><input type="checkbox" id="toggle-cpushare" onchange="setConfig('CPU_SHARE', this.checked?1:0)"><span class="toggle-slider"></span></label>
</div>
</div>
<button class="btn" onclick="reconnect()">Reconnect</button>
<button class="btn btn-secondary" onclick="pingFlipper()">Ping</button>
</div>
</div>
<!-- ==================== GPIO ==================== -->
<div class="page" id="page-gpio">
<div class="section">
<div class="section-title">GPIO Control</div>
<div class="card">
<div class="gpio-grid" id="gpio-grid"></div>
</div>
<div class="card" style="margin-top:12px">
<div class="section-title">Quick Actions</div>
<div class="card-row">
<select id="gpio-pin" class="input-select">
<option value="2">PA2</option><option value="3">PA3</option>
<option value="4">PA4</option><option value="6">PA6</option>
<option value="7">PA7</option><option value="13">PB2</option>
<option value="14">PB3</option><option value="15">PB13</option>
<option value="16">PB14</option><option value="17">PC0</option>
<option value="18">PC1</option><option value="19">PC3</option>
</select>
<select id="gpio-mode" class="input-select">
<option value="0">Input</option>
<option value="1">Output Push-Pull</option>
<option value="2">Output Open-Drain</option>
<option value="3">Analog</option>
</select>
<button class="btn btn-small" onclick="gpioInit()">Init</button>
</div>
<div class="card-row">
<button class="btn btn-small" onclick="gpioRead()">Read</button>
<button class="btn btn-small" onclick="gpioWrite(1)">HIGH</button>
<button class="btn btn-small btn-danger" onclick="gpioWrite(0)">LOW</button>
<span id="gpio-result" class="mono"></span>
</div>
<div class="card-row">
<label>PWM Freq:</label>
<input type="number" id="gpio-pwm-freq" class="input-text" value="1000" min="1" max="1000000">
<label>Duty:</label>
<input type="number" id="gpio-pwm-duty" class="input-text" value="50" min="0" max="100">
<button class="btn btn-small" onclick="gpioPwm()">Set PWM</button>
</div>
<div class="card-row">
<button class="btn btn-small" onclick="gpioAdc()">Read ADC</button>
<span id="gpio-adc-result" class="mono"></span>
</div>
</div>
</div>
</div>
<!-- ==================== SubGHz ==================== -->
<div class="page" id="page-subghz">
<div class="section">
<div class="section-title">Sub-GHz Radio</div>
<div class="card">
<div class="card-row">
<label>Frequency (Hz):</label>
<input type="number" id="subghz-freq" class="input-text" value="433920000">
<button class="btn btn-small" onclick="subghzSetFreq()">Set</button>
</div>
<div class="card-row">
<button class="btn" onclick="subghzRxStart()">Start RX</button>
<button class="btn btn-danger" onclick="subghzRxStop()">Stop RX</button>
<button class="btn btn-secondary" onclick="subghzGetRssi()">RSSI</button>
<span id="subghz-rssi" class="mono"></span>
</div>
<div class="card-row">
<label>TX Data (hex):</label>
<input type="text" id="subghz-tx-data" class="input-text" placeholder="deadbeef">
<button class="btn btn-small" onclick="subghzTx()">Transmit</button>
</div>
<div class="card-row">
<label>Replay slot:</label>
<input type="number" id="subghz-replay-slot" class="input-text" value="0" min="0" max="9">
<button class="btn btn-small" onclick="subghzReplay()">Replay</button>
</div>
</div>
<div class="section-title" style="margin-top:16px">Captured Signals</div>
<div class="card">
<div class="log-output" id="subghz-log"></div>
</div>
</div>
</div>
<!-- ==================== NFC/RFID ==================== -->
<div class="page" id="page-nfc">
<div class="section">
<div class="section-title">NFC (13.56 MHz)</div>
<div class="card">
<button class="btn" onclick="nfcPoll()">Poll Card</button>
<button class="btn" onclick="nfcReadFull()">Full Read</button>
<button class="btn btn-secondary" onclick="nfcRelayStart()">Start Relay</button>
<button class="btn btn-danger" onclick="nfcRelayStop()">Stop Relay</button>
<div class="log-output" id="nfc-log"></div>
</div>
<div class="section-title" style="margin-top:16px">RFID (125 kHz)</div>
<div class="card">
<button class="btn" onclick="rfidRead()">Read Tag</button>
<div class="card-row">
<label>Emulate UID (hex):</label>
<input type="text" id="rfid-emu-data" class="input-text" placeholder="0102030405">
<button class="btn btn-small" onclick="rfidEmulate()">Emulate</button>
</div>
<div class="log-output" id="rfid-log"></div>
</div>
</div>
</div>
<!-- ==================== IR ==================== -->
<div class="page" id="page-ir">
<div class="section">
<div class="section-title">Infrared</div>
<div class="card">
<div class="card-row">
<label>Protocol:</label>
<select id="ir-proto" class="input-select">
<option value="0">NEC</option><option value="1">Samsung</option>
<option value="2">RC5</option><option value="3">RC6</option>
<option value="4">SIRC</option><option value="5">Kaseikyo</option>
</select>
<label>Addr:</label>
<input type="number" id="ir-addr" class="input-text" value="0">
<label>Cmd:</label>
<input type="number" id="ir-cmd" class="input-text" value="0">
<button class="btn btn-small" onclick="irTx()">Send</button>
</div>
<div class="card-row">
<button class="btn" onclick="irRxStart()">Start Learn</button>
<button class="btn btn-danger" onclick="irRxStop()">Stop</button>
<label>Replay:</label>
<input type="number" id="ir-replay-slot" class="input-text" value="0" min="0" max="9">
<button class="btn btn-small" onclick="irReplay()">Replay</button>
</div>
<div class="log-output" id="ir-log"></div>
</div>
</div>
</div>
<!-- ==================== EVENTS ==================== -->
<div class="page" id="page-events">
<div class="section">
<div class="section-title">Live Events</div>
<div class="card">
<button class="btn btn-small" onclick="loadEvents()">Refresh</button>
<div class="log-output" id="events-log" style="max-height:60vh"></div>
</div>
<div class="section-title" style="margin-top:16px">System Log</div>
<div class="card">
<button class="btn btn-small" onclick="loadSysLog()">Load Log</button>
<div class="log-output" id="sys-log" style="max-height:40vh"></div>
</div>
</div>
</div>
<!-- ==================== STEALTH ==================== -->
<div class="page" id="page-stealth">
<div class="section">
<div class="section-title">Namespace Isolation</div>
<div class="card">
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Stealth Status</div>
<div class="card-row-desc">Bind mounts, port firewall, config hiding</div>
</div>
<span id="stealth-status-text" class="mono">-</span>
</div>
<div class="card-row">
<button class="btn" onclick="stealthApply()">Apply Stealth</button>
<button class="btn btn-danger" onclick="stealthTeardown()">Teardown</button>
<button class="btn btn-secondary" onclick="stealthStatus()">Status</button>
</div>
</div>
<div class="section-title" style="margin-top:16px">Quick Controls</div>
<div class="card">
<div class="card-row">
<div class="card-row-info">
<div class="card-row-label">Hide Device + Port</div>
<div class="card-row-desc">Firewall WebUI to localhost, restrict config perms</div>
</div>
<button class="btn btn-small" onclick="stealthHideDev()">Hide</button>
<button class="btn btn-small" onclick="stealthShowDev()">Show</button>
</div>
</div>
<div class="section-title" style="margin-top:16px">Stealth Map</div>
<div class="card">
<div class="card-row-desc" style="padding:8px">
Edit <code>/data/adb/flipperdroid/stealth_map.conf</code> to configure per-process bind mount isolation.
Stock files stay untouched. Only target processes see modifications.
</div>
<div class="card-row">
<label>Active bind mounts:</label>
<span id="stealth-mount-count" class="mono">-</span>
</div>
<div class="log-output" id="stealth-log"></div>
</div>
</div>
</div>
<!-- Toast -->
<div id="toast" class="toast"></div>
<!-- Tab bar -->
<div class="tab-bar">
<div class="tab-item active" data-tab="dashboard" onclick="switchTab('dashboard')">
<svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
<span>Home</span>
</div>
<div class="tab-item" data-tab="gpio" onclick="switchTab('gpio')">
<svg viewBox="0 0 24 24"><path d="M7 2v11h3v9l7-12h-4l4-8z"/></svg>
<span>GPIO</span>
</div>
<div class="tab-item" data-tab="subghz" onclick="switchTab('subghz')">
<svg viewBox="0 0 24 24"><path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/></svg>
<span>SubGHz</span>
</div>
<div class="tab-item" data-tab="nfc" onclick="switchTab('nfc')">
<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 18H4V4h16v16zM6 6h2v2H6zm0 4h2v2H6zm0 4h2v2H6zm4-8h8v2h-8zm0 4h8v2h-8zm0 4h8v2h-8z"/></svg>
<span>NFC</span>
</div>
<div class="tab-item" data-tab="ir" onclick="switchTab('ir')">
<svg viewBox="0 0 24 24"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
<span>IR</span>
</div>
<div class="tab-item" data-tab="stealth" onclick="switchTab('stealth')">
<svg viewBox="0 0 24 24"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg>
<span>Stealth</span>
</div>
<div class="tab-item" data-tab="events" onclick="switchTab('events')">
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
<span>Log</span>
</div>
</div>
</div>
<script src="/js/app.js"></script>
</body>
</html>

376
webroot/js/app.js Normal file
View File

@@ -0,0 +1,376 @@
// 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 = `<div class="pin-name">${pin.name}</div><div class="pin-val">-</div>`;
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);
});