Port to ESP32
This commit is contained in:
322
webapp/app/app.js
Normal file
322
webapp/app/app.js
Normal file
@ -0,0 +1,322 @@
|
||||
/**
|
||||
* LED Controller Web-BLE Application
|
||||
* Communicates with ESP32 LED Controller via Bluetooth Low Energy
|
||||
*/
|
||||
|
||||
// BLE Service and Characteristic UUIDs
|
||||
const SERVICE_UUID = 0x00FF;
|
||||
const CHAR_CONFIG_UUID = 0xFF01;
|
||||
const CHAR_MODE_UUID = 0xFF02;
|
||||
const CHAR_PWM_UUID = 0xFF03;
|
||||
const CHAR_OTA_UUID = 0xFF04;
|
||||
|
||||
// Animation mode names
|
||||
const MODE_NAMES = [
|
||||
'Black', 'Red', 'Blue', 'Green', 'White',
|
||||
'Rainbow', 'Rainbow Glitter', 'Confetti', 'Sinelon',
|
||||
'BPM Pulse', 'Navigation', 'Chase (Red)', 'Chase (RGB)', 'Random'
|
||||
];
|
||||
|
||||
class LEDController {
|
||||
constructor() {
|
||||
this.device = null;
|
||||
this.server = null;
|
||||
this.service = null;
|
||||
this.characteristics = {
|
||||
config: null,
|
||||
mode: null,
|
||||
pwm: null,
|
||||
ota: null
|
||||
};
|
||||
this.connected = false;
|
||||
|
||||
this.initUI();
|
||||
}
|
||||
|
||||
initUI() {
|
||||
// Connect button
|
||||
document.getElementById('connect-btn').addEventListener('click', () => this.connect());
|
||||
|
||||
// Save configuration button
|
||||
document.getElementById('save-config-btn').addEventListener('click', () => this.saveConfig());
|
||||
|
||||
// PWM emulation button
|
||||
document.getElementById('pwm-emulate-btn').addEventListener('click', () => this.emulatePWM());
|
||||
|
||||
// Mode selection buttons
|
||||
document.querySelectorAll('.btn-mode').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const mode = parseInt(e.target.dataset.mode);
|
||||
this.setMode(mode);
|
||||
});
|
||||
});
|
||||
|
||||
// Firmware file selection
|
||||
document.getElementById('firmware-file').addEventListener('change', (e) => {
|
||||
const uploadBtn = document.getElementById('ota-upload-btn');
|
||||
uploadBtn.disabled = !e.target.files.length;
|
||||
});
|
||||
|
||||
// OTA upload button
|
||||
document.getElementById('ota-upload-btn').addEventListener('click', () => this.uploadFirmware());
|
||||
}
|
||||
|
||||
async connect() {
|
||||
try {
|
||||
console.log('Requesting Bluetooth Device...');
|
||||
|
||||
this.device = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ name: 'LED-Controller' }],
|
||||
optionalServices: [SERVICE_UUID]
|
||||
});
|
||||
|
||||
console.log('Connecting to GATT Server...');
|
||||
this.server = await this.device.gatt.connect();
|
||||
|
||||
console.log('Getting Service...');
|
||||
this.service = await this.server.getPrimaryService(SERVICE_UUID);
|
||||
|
||||
console.log('Getting Characteristics...');
|
||||
this.characteristics.config = await this.service.getCharacteristic(CHAR_CONFIG_UUID);
|
||||
this.characteristics.mode = await this.service.getCharacteristic(CHAR_MODE_UUID);
|
||||
this.characteristics.pwm = await this.service.getCharacteristic(CHAR_PWM_UUID);
|
||||
this.characteristics.ota = await this.service.getCharacteristic(CHAR_OTA_UUID);
|
||||
|
||||
this.connected = true;
|
||||
this.updateConnectionStatus(true);
|
||||
|
||||
// Load current configuration
|
||||
await this.loadConfig();
|
||||
await this.loadCurrentMode();
|
||||
|
||||
// Show control sections
|
||||
document.getElementById('config-section').classList.remove('hidden');
|
||||
document.getElementById('control-section').classList.remove('hidden');
|
||||
document.getElementById('ota-section').classList.remove('hidden');
|
||||
|
||||
console.log('Connected successfully!');
|
||||
|
||||
// Handle disconnection
|
||||
this.device.addEventListener('gattserverdisconnected', () => {
|
||||
this.onDisconnected();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Connection failed:', error);
|
||||
alert('Failed to connect: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async loadConfig() {
|
||||
try {
|
||||
const value = await this.characteristics.config.readValue();
|
||||
const config = this.parseConfig(value);
|
||||
|
||||
document.getElementById('led-pin-a').value = config.ledPinA;
|
||||
document.getElementById('led-pin-b').value = config.ledPinB;
|
||||
document.getElementById('pwm-pin').value = config.pwmPin;
|
||||
document.getElementById('ble-timeout').value = config.bleTimeout;
|
||||
|
||||
console.log('Configuration loaded:', config);
|
||||
} catch (error) {
|
||||
console.error('Failed to load config:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadCurrentMode() {
|
||||
try {
|
||||
const value = await this.characteristics.mode.readValue();
|
||||
const mode = value.getUint8(0);
|
||||
this.updateCurrentMode(mode);
|
||||
} catch (error) {
|
||||
console.error('Failed to load current mode:', error);
|
||||
}
|
||||
}
|
||||
|
||||
parseConfig(dataView) {
|
||||
return {
|
||||
ledPinA: dataView.getInt8(0),
|
||||
ledPinB: dataView.getInt8(1),
|
||||
pwmPin: dataView.getInt8(2),
|
||||
bleTimeout: dataView.getUint32(3, true),
|
||||
magic: dataView.getUint32(7, true)
|
||||
};
|
||||
}
|
||||
|
||||
createConfigBuffer(config) {
|
||||
const buffer = new ArrayBuffer(11);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
view.setInt8(0, config.ledPinA);
|
||||
view.setInt8(1, config.ledPinB);
|
||||
view.setInt8(2, config.pwmPin);
|
||||
view.setUint32(3, config.bleTimeout, true);
|
||||
view.setUint32(7, 0xDEADBEEF, true); // Magic number
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async saveConfig() {
|
||||
try {
|
||||
const config = {
|
||||
ledPinA: parseInt(document.getElementById('led-pin-a').value),
|
||||
ledPinB: parseInt(document.getElementById('led-pin-b').value),
|
||||
pwmPin: parseInt(document.getElementById('pwm-pin').value),
|
||||
bleTimeout: parseInt(document.getElementById('ble-timeout').value)
|
||||
};
|
||||
|
||||
const buffer = this.createConfigBuffer(config);
|
||||
await this.characteristics.config.writeValue(buffer);
|
||||
|
||||
alert('Configuration saved! Device will restart if pins changed.');
|
||||
console.log('Configuration saved:', config);
|
||||
} catch (error) {
|
||||
console.error('Failed to save config:', error);
|
||||
alert('Failed to save configuration: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async setMode(mode) {
|
||||
try {
|
||||
const buffer = new Uint8Array([mode]);
|
||||
await this.characteristics.mode.writeValue(buffer);
|
||||
this.updateCurrentMode(mode);
|
||||
console.log('Mode set to:', MODE_NAMES[mode]);
|
||||
} catch (error) {
|
||||
console.error('Failed to set mode:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async emulatePWM() {
|
||||
try {
|
||||
const buffer = new Uint8Array([1]);
|
||||
await this.characteristics.pwm.writeValue(buffer);
|
||||
console.log('PWM pulse emulated');
|
||||
|
||||
// Read back the new mode
|
||||
setTimeout(() => this.loadCurrentMode(), 100);
|
||||
} catch (error) {
|
||||
console.error('Failed to emulate PWM:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFirmware() {
|
||||
const fileInput = document.getElementById('firmware-file');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
alert('Please select a firmware file');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('This will update the firmware and reset all settings. Continue?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const data = new Uint8Array(arrayBuffer);
|
||||
|
||||
const progressBar = document.getElementById('ota-progress-bar');
|
||||
const progressText = document.getElementById('ota-progress-text');
|
||||
const progressContainer = document.getElementById('ota-progress');
|
||||
|
||||
progressContainer.classList.remove('hidden');
|
||||
|
||||
const chunkSize = 512;
|
||||
const totalChunks = Math.ceil(data.length / chunkSize);
|
||||
|
||||
console.log(`Starting OTA upload: ${data.length} bytes, ${totalChunks} chunks`);
|
||||
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
const start = i * chunkSize;
|
||||
const end = Math.min(start + chunkSize, data.length);
|
||||
const chunk = data.slice(start, end);
|
||||
|
||||
await this.characteristics.ota.writeValue(chunk);
|
||||
|
||||
const progress = Math.round((i + 1) / totalChunks * 100);
|
||||
progressBar.style.width = progress + '%';
|
||||
progressText.textContent = progress + '%';
|
||||
|
||||
console.log(`Upload progress: ${progress}%`);
|
||||
|
||||
// Small delay to prevent overwhelming the device
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
}
|
||||
|
||||
alert('Firmware uploaded successfully! Device will restart.');
|
||||
console.log('OTA upload complete');
|
||||
|
||||
} catch (error) {
|
||||
console.error('OTA upload failed:', error);
|
||||
alert('Firmware upload failed: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
updateCurrentMode(mode) {
|
||||
const modeName = MODE_NAMES[mode] || 'Unknown';
|
||||
document.getElementById('current-mode').textContent = modeName;
|
||||
}
|
||||
|
||||
updateConnectionStatus(connected) {
|
||||
const indicator = document.getElementById('status-indicator');
|
||||
const statusText = document.getElementById('connection-status');
|
||||
const connectBtn = document.getElementById('connect-btn');
|
||||
|
||||
if (connected) {
|
||||
indicator.classList.remove('disconnected');
|
||||
indicator.classList.add('connected');
|
||||
statusText.textContent = 'Connected';
|
||||
connectBtn.textContent = 'Disconnect';
|
||||
connectBtn.onclick = () => this.disconnect();
|
||||
} else {
|
||||
indicator.classList.remove('connected');
|
||||
indicator.classList.add('disconnected');
|
||||
statusText.textContent = 'Disconnected';
|
||||
connectBtn.textContent = 'Connect via BLE';
|
||||
connectBtn.onclick = () => this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.device && this.device.gatt.connected) {
|
||||
this.device.gatt.disconnect();
|
||||
}
|
||||
this.onDisconnected();
|
||||
}
|
||||
|
||||
onDisconnected() {
|
||||
console.log('Disconnected from device');
|
||||
this.connected = false;
|
||||
this.device = null;
|
||||
this.server = null;
|
||||
this.service = null;
|
||||
this.characteristics = {
|
||||
config: null,
|
||||
mode: null,
|
||||
pwm: null,
|
||||
ota: null
|
||||
};
|
||||
|
||||
this.updateConnectionStatus(false);
|
||||
|
||||
// Hide control sections
|
||||
document.getElementById('config-section').classList.add('hidden');
|
||||
document.getElementById('control-section').classList.add('hidden');
|
||||
document.getElementById('ota-section').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize app when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Check if Web Bluetooth is supported
|
||||
if (!navigator.bluetooth) {
|
||||
alert('Web Bluetooth is not supported in this browser. Please use Chrome, Edge, or Opera.');
|
||||
document.getElementById('connect-btn').disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('LED Controller Web-BLE Interface loaded');
|
||||
new LEDController();
|
||||
});
|
||||
313
webapp/css/style.css
Normal file
313
webapp/css/style.css
Normal file
@ -0,0 +1,313 @@
|
||||
/* LED Controller Web-BLE Interface Styles */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-color: #2196F3;
|
||||
--success-color: #4CAF50;
|
||||
--warning-color: #FF9800;
|
||||
--danger-color: #F44336;
|
||||
--dark-bg: #1a1a2e;
|
||||
--card-bg: #16213e;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #b0b0b0;
|
||||
--border-color: #0f4c75;
|
||||
--hover-bg: #1e3a5f;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, var(--dark-bg) 0%, #0f3460 100%);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
padding: 25px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-bottom: 20px;
|
||||
color: var(--primary-color);
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Connection Status */
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.disconnected {
|
||||
background: var(--danger-color);
|
||||
}
|
||||
|
||||
.status-indicator.connected {
|
||||
background: var(--success-color);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Form Elements */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.form-group input[type="number"],
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: var(--dark-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.form-group input[type="number"]:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.form-group small {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.form-group input[type="file"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: var(--dark-bg);
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--success-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: var(--warning-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--border-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-mode {
|
||||
background: var(--hover-bg);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.btn-mode:hover {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Button Grid */
|
||||
.button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Current Mode Display */
|
||||
.current-mode {
|
||||
background: var(--dark-bg);
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.current-mode span {
|
||||
color: var(--primary-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: var(--dark-bg);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
margin: 20px 0;
|
||||
position: relative;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--success-color));
|
||||
transition: width 0.3s ease;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-weight: bold;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Warning Box */
|
||||
.warning {
|
||||
background: rgba(255, 152, 0, 0.1);
|
||||
border: 1px solid var(--warning-color);
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
color: var(--warning-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
footer small {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 600px) {
|
||||
header h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-size: 0.9em;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
0
webapp/data/favicon.ico
Normal file
0
webapp/data/favicon.ico
Normal file
115
webapp/index.html
Normal file
115
webapp/index.html
Normal file
@ -0,0 +1,115 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LED Controller Config</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="icon" href="data/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>✈️ LED Controller</h1>
|
||||
<p class="subtitle">Model Aircraft LED Configuration via Web-BLE</p>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
<h2>🔗 Connection</h2>
|
||||
<div class="connection-status">
|
||||
<span id="status-indicator" class="status-indicator disconnected"></span>
|
||||
<span id="connection-status">Disconnected</span>
|
||||
</div>
|
||||
<button id="connect-btn" class="btn btn-primary">Connect via BLE</button>
|
||||
</div>
|
||||
|
||||
<div id="config-section" class="card hidden">
|
||||
<h2>⚙️ Configuration</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="led-pin-a">LED Strip A GPIO Pin</label>
|
||||
<input type="number" id="led-pin-a" min="-1" max="48" value="-1">
|
||||
<small>-1 = Disabled</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="led-pin-b">LED Strip B GPIO Pin</label>
|
||||
<input type="number" id="led-pin-b" min="-1" max="48" value="-1">
|
||||
<small>-1 = Disabled</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pwm-pin">PWM Input GPIO Pin</label>
|
||||
<input type="number" id="pwm-pin" min="-1" max="48" value="-1">
|
||||
<small>-1 = Disabled</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ble-timeout">BLE Auto-Off Timeout</label>
|
||||
<select id="ble-timeout">
|
||||
<option value="0">Never</option>
|
||||
<option value="60">1 Minute</option>
|
||||
<option value="300">5 Minutes</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button id="save-config-btn" class="btn btn-success">💾 Save Configuration</button>
|
||||
</div>
|
||||
|
||||
<div id="control-section" class="card hidden">
|
||||
<h2>🎮 Control</h2>
|
||||
|
||||
<div class="current-mode">
|
||||
<strong>Current Mode:</strong> <span id="current-mode">Black</span>
|
||||
</div>
|
||||
|
||||
<div class="button-grid">
|
||||
<button class="btn btn-mode" data-mode="0">⚫ Black</button>
|
||||
<button class="btn btn-mode" data-mode="1">🔴 Red</button>
|
||||
<button class="btn btn-mode" data-mode="2">🔵 Blue</button>
|
||||
<button class="btn btn-mode" data-mode="3">🟢 Green</button>
|
||||
<button class="btn btn-mode" data-mode="4">⚪ White</button>
|
||||
<button class="btn btn-mode" data-mode="5">🌈 Rainbow</button>
|
||||
<button class="btn btn-mode" data-mode="6">✨ Rainbow Glitter</button>
|
||||
<button class="btn btn-mode" data-mode="7">🎊 Confetti</button>
|
||||
<button class="btn btn-mode" data-mode="8">🌀 Sinelon</button>
|
||||
<button class="btn btn-mode" data-mode="9">💓 BPM Pulse</button>
|
||||
<button class="btn btn-mode" data-mode="10">🛩️ Navigation</button>
|
||||
<button class="btn btn-mode" data-mode="11">🏃 Chase (Red)</button>
|
||||
<button class="btn btn-mode" data-mode="12">🏃 Chase (RGB)</button>
|
||||
<button class="btn btn-mode" data-mode="13">🎲 Random</button>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<button id="pwm-emulate-btn" class="btn btn-secondary">⏭️ Next Mode (PWM Emulation)</button>
|
||||
</div>
|
||||
|
||||
<div id="ota-section" class="card hidden">
|
||||
<h2>🔄 Firmware Update</h2>
|
||||
<div class="warning">
|
||||
⚠️ Warning: Firmware update will reset all settings!
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="firmware-file">Select Firmware File (.bin)</label>
|
||||
<input type="file" id="firmware-file" accept=".bin">
|
||||
</div>
|
||||
|
||||
<div id="ota-progress" class="progress-container hidden">
|
||||
<div class="progress-bar" id="ota-progress-bar"></div>
|
||||
<div class="progress-text" id="ota-progress-text">0%</div>
|
||||
</div>
|
||||
|
||||
<button id="ota-upload-btn" class="btn btn-warning" disabled>📤 Upload Firmware</button>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>ESP32 LED Controller © 2026</p>
|
||||
<p><small>Supports ESP32 DevKitC & ESP32-C3 MINI</small></p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="app/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user