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();
|
||||
});
|
||||
Reference in New Issue
Block a user