323 lines
11 KiB
JavaScript
323 lines
11 KiB
JavaScript
/**
|
|
* 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();
|
|
});
|