/** * 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(); });