Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PicoDrone - BLE Drone Controller</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .joystick { | |
| width: 120px; | |
| height: 120px; | |
| background: rgba(255, 255, 255, 0.2); | |
| border-radius: 50%; | |
| position: relative; | |
| touch-action: none; | |
| } | |
| .joystick-knob { | |
| width: 50px; | |
| height: 50px; | |
| background: rgba(255, 255, 255, 0.8); | |
| border-radius: 50%; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); | |
| } | |
| .battery-level { | |
| height: 20px; | |
| border-radius: 10px; | |
| transition: width 0.3s ease; | |
| } | |
| .connection-pulse { | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| 100% { opacity: 1; } | |
| } | |
| .gauge { | |
| width: 100%; | |
| height: 10px; | |
| border-radius: 5px; | |
| overflow: hidden; | |
| } | |
| .gauge-fill { | |
| height: 100%; | |
| transition: width 0.3s ease; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-white min-h-screen"> | |
| <div class="container mx-auto px-4 py-8 max-w-md"> | |
| <!-- Header --> | |
| <header class="flex justify-between items-center mb-8"> | |
| <div> | |
| <h1 class="text-2xl font-bold">PicoDrone</h1> | |
| <p class="text-gray-400 text-sm">BLE Drone Controller</p> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <div id="connectionStatus" class="w-3 h-3 rounded-full bg-red-500"></div> | |
| <span id="connectionText" class="text-sm">Disconnected</span> | |
| </div> | |
| </header> | |
| <!-- Connection Panel --> | |
| <div class="bg-gray-800 rounded-xl p-6 mb-6 shadow-lg"> | |
| <h2 class="text-lg font-semibold mb-4 flex items-center"> | |
| <i class="fas fa-bluetooth-b mr-2"></i> BLE Connection | |
| </h2> | |
| <div class="mb-4"> | |
| <label class="block text-sm font-medium mb-2">Select Device</label> | |
| <div class="flex"> | |
| <select id="deviceList" class="bg-gray-700 text-white rounded-l-lg p-2 flex-grow focus:outline-none"> | |
| <option value="">No devices found</option> | |
| </select> | |
| <button id="refreshBtn" class="bg-blue-600 hover:bg-blue-700 px-4 rounded-r-lg"> | |
| <i class="fas fa-sync-alt"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <button id="connectBtn" class="w-full py-3 bg-green-600 hover:bg-green-700 rounded-lg font-medium flex items-center justify-center"> | |
| <i class="fas fa-plug mr-2"></i> Connect to Drone | |
| </button> | |
| </div> | |
| <!-- Status Panel --> | |
| <div class="bg-gray-800 rounded-xl p-6 mb-6 shadow-lg"> | |
| <h2 class="text-lg font-semibold mb-4 flex items-center"> | |
| <i class="fas fa-info-circle mr-2"></i> Drone Status | |
| </h2> | |
| <div class="grid grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <p class="text-sm text-gray-400">Battery</p> | |
| <div class="flex items-center"> | |
| <div class="gauge bg-gray-700 mr-2"> | |
| <div id="batteryLevel" class="gauge-fill bg-green-500" style="width: 75%"></div> | |
| </div> | |
| <span id="batteryPercent">75%</span> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="text-sm text-gray-400">Signal Strength</p> | |
| <div class="flex items-center"> | |
| <div class="gauge bg-gray-700 mr-2"> | |
| <div id="signalLevel" class="gauge-fill bg-blue-500" style="width: 90%"></div> | |
| </div> | |
| <span id="signalPercent">-</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-3 gap-2 text-center"> | |
| <div class="bg-gray-700 p-2 rounded-lg"> | |
| <p class="text-sm text-gray-400">Altitude</p> | |
| <p id="altitudeValue" class="font-medium">0.0 m</p> | |
| </div> | |
| <div class="bg-gray-700 p-2 rounded-lg"> | |
| <p class="text-sm text-gray-400">Speed</p> | |
| <p id="speedValue" class="font-medium">0.0 m/s</p> | |
| </div> | |
| <div class="bg-gray-700 p-2 rounded-lg"> | |
| <p class="text-sm text-gray-400">Flight Time</p> | |
| <p id="flightTime" class="font-medium">00:00</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Flight Controls --> | |
| <div class="bg-gray-800 rounded-xl p-6 shadow-lg"> | |
| <h2 class="text-lg font-semibold mb-6 flex items-center"> | |
| <i class="fas fa-gamepad mr-2"></i> Flight Controls | |
| </h2> | |
| <div class="flex justify-between items-center mb-8"> | |
| <!-- Left Joystick (Throttle/Yaw) --> | |
| <div class="text-center"> | |
| <p class="text-sm mb-2">Throttle/Yaw</p> | |
| <div id="leftJoystick" class="joystick mx-auto"> | |
| <div class="joystick-knob"></div> | |
| </div> | |
| </div> | |
| <!-- Right Joystick (Pitch/Roll) --> | |
| <div class="text-center"> | |
| <p class="text-sm mb-2">Pitch/Roll</p> | |
| <div id="rightJoystick" class="joystick mx-auto"> | |
| <div class="joystick-knob"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Action Buttons --> | |
| <div class="grid grid-cols-3 gap-3 mb-4"> | |
| <button id="takeoffBtn" class="bg-green-600 hover:bg-green-700 py-3 rounded-lg font-medium disabled:opacity-50" disabled> | |
| <i class="fas fa-rocket"></i> Takeoff | |
| </button> | |
| <button id="landBtn" class="bg-yellow-600 hover:bg-yellow-700 py-3 rounded-lg font-medium disabled:opacity-50" disabled> | |
| <i class="fas fa-landmark"></i> Land | |
| </button> | |
| <button id="emergencyBtn" class="bg-red-600 hover:bg-red-700 py-3 rounded-lg font-medium disabled:opacity-50" disabled> | |
| <i class="fas fa-exclamation-triangle"></i> Stop | |
| </button> | |
| </div> | |
| <!-- Trim Controls --> | |
| <div class="bg-gray-700 rounded-lg p-3 mb-4"> | |
| <p class="text-sm mb-2 text-center">Trim Adjustments</p> | |
| <div class="grid grid-cols-4 gap-2"> | |
| <button class="trim-btn bg-gray-600 hover:bg-gray-500 p-2 rounded" data-axis="pitch" data-direction="up"> | |
| <i class="fas fa-arrow-up"></i> Pitch | |
| </button> | |
| <button class="trim-btn bg-gray-600 hover:bg-gray-500 p-2 rounded" data-axis="roll" data-direction="left"> | |
| <i class="fas fa-arrow-left"></i> Roll | |
| </button> | |
| <button class="trim-btn bg-gray-600 hover:bg-gray-500 p-2 rounded" data-axis="roll" data-direction="right"> | |
| <i class="fas fa-arrow-right"></i> Roll | |
| </button> | |
| <button class="trim-btn bg-gray-600 hover:bg-gray-500 p-2 rounded" data-axis="pitch" data-direction="down"> | |
| <i class="fas fa-arrow-down"></i> Pitch | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Flight Mode Selector --> | |
| <div class="mb-4"> | |
| <label class="block text-sm font-medium mb-2">Flight Mode</label> | |
| <select id="flightMode" class="w-full bg-gray-700 text-white rounded-lg p-2 focus:outline-none"> | |
| <option value="beginner">Beginner (Limited)</option> | |
| <option value="normal" selected>Normal</option> | |
| <option value="acrobatic">Acrobatic</option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Console Log --> | |
| <div class="bg-gray-800 rounded-xl p-4 mt-6 shadow-lg"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <h3 class="text-sm font-semibold"> | |
| <i class="fas fa-terminal mr-1"></i> Console | |
| </h3> | |
| <button id="clearConsole" class="text-xs text-gray-400 hover:text-white"> | |
| <i class="fas fa-trash-alt mr-1"></i> Clear | |
| </button> | |
| </div> | |
| <div id="consoleOutput" class="bg-black text-green-400 font-mono text-xs p-2 rounded h-24 overflow-y-auto"> | |
| > Welcome to PicoDrone controller<br> | |
| > Ready to connect... | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Simulated BLE functionality for demonstration | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // UI Elements | |
| const connectBtn = document.getElementById('connectBtn'); | |
| const refreshBtn = document.getElementById('refreshBtn'); | |
| const deviceList = document.getElementById('deviceList'); | |
| const connectionStatus = document.getElementById('connectionStatus'); | |
| const connectionText = document.getElementById('connectionText'); | |
| const takeoffBtn = document.getElementById('takeoffBtn'); | |
| const landBtn = document.getElementById('landBtn'); | |
| const emergencyBtn = document.getElementById('emergencyBtn'); | |
| const consoleOutput = document.getElementById('consoleOutput'); | |
| const clearConsole = document.getElementById('clearConsole'); | |
| // Status elements | |
| const batteryLevel = document.getElementById('batteryLevel'); | |
| const batteryPercent = document.getElementById('batteryPercent'); | |
| const signalLevel = document.getElementById('signalLevel'); | |
| const signalPercent = document.getElementById('signalPercent'); | |
| const altitudeValue = document.getElementById('altitudeValue'); | |
| const speedValue = document.getElementById('speedValue'); | |
| const flightTime = document.getElementById('flightTime'); | |
| // Joystick elements | |
| const leftJoystick = document.getElementById('leftJoystick'); | |
| const rightJoystick = document.getElementById('rightJoystick'); | |
| const leftKnob = leftJoystick.querySelector('.joystick-knob'); | |
| const rightKnob = rightJoystick.querySelector('.joystick-knob'); | |
| // Flight mode selector | |
| const flightMode = document.getElementById('flightMode'); | |
| // Simulated devices | |
| const devices = [ | |
| { name: 'PicoDrone-1', id: 'pico-1234' }, | |
| { name: 'PicoDrone-2', id: 'pico-5678' }, | |
| { name: 'PicoDrone-Test', id: 'pico-9ABC' } | |
| ]; | |
| // State variables | |
| let isConnected = false; | |
| let isFlying = false; | |
| let flightStartTime = null; | |
| let flightTimer = null; | |
| let batterySimulator = null; | |
| // Initialize the app | |
| function init() { | |
| populateDeviceList(); | |
| setupEventListeners(); | |
| setupJoysticks(); | |
| } | |
| // Populate device list with simulated devices | |
| function populateDeviceList() { | |
| deviceList.innerHTML = ''; | |
| if (devices.length === 0) { | |
| deviceList.innerHTML = '<option value="">No devices found</option>'; | |
| return; | |
| } | |
| devices.forEach(device => { | |
| const option = document.createElement('option'); | |
| option.value = device.id; | |
| option.textContent = device.name; | |
| deviceList.appendChild(option); | |
| }); | |
| } | |
| // Set up event listeners | |
| function setupEventListeners() { | |
| // Connect button | |
| connectBtn.addEventListener('click', function() { | |
| if (isConnected) { | |
| disconnectFromDrone(); | |
| } else { | |
| const selectedDeviceId = deviceList.value; | |
| if (selectedDeviceId) { | |
| connectToDrone(selectedDeviceId); | |
| } else { | |
| logToConsole('Please select a device first'); | |
| } | |
| } | |
| }); | |
| // Refresh button | |
| refreshBtn.addEventListener('click', function() { | |
| logToConsole('Scanning for BLE devices...'); | |
| // Simulate scanning delay | |
| setTimeout(() => { | |
| populateDeviceList(); | |
| logToConsole(`Found ${devices.length} device(s)`); | |
| }, 1000); | |
| }); | |
| // Action buttons | |
| takeoffBtn.addEventListener('click', function() { | |
| if (isConnected) { | |
| takeoff(); | |
| } | |
| }); | |
| landBtn.addEventListener('click', function() { | |
| if (isConnected && isFlying) { | |
| land(); | |
| } | |
| }); | |
| emergencyBtn.addEventListener('click', function() { | |
| if (isConnected) { | |
| emergencyStop(); | |
| } | |
| }); | |
| // Trim buttons | |
| document.querySelectorAll('.trim-btn').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| const axis = this.dataset.axis; | |
| const direction = this.dataset.direction; | |
| adjustTrim(axis, direction); | |
| }); | |
| }); | |
| // Flight mode selector | |
| flightMode.addEventListener('change', function() { | |
| if (isConnected) { | |
| const mode = this.value; | |
| setFlightMode(mode); | |
| logToConsole(`Flight mode changed to: ${mode}`); | |
| } | |
| }); | |
| // Clear console | |
| clearConsole.addEventListener('click', function() { | |
| consoleOutput.innerHTML = '> Console cleared<br>'; | |
| }); | |
| } | |
| // Set up joystick controls | |
| function setupJoysticks() { | |
| setupJoystick(leftJoystick, leftKnob, (x, y) => { | |
| // Throttle (Y) and Yaw (X) controls | |
| if (isConnected) { | |
| const throttle = ((y * -1 + 1) / 2 * 100).toFixed(0); | |
| const yaw = (x * 100).toFixed(0); | |
| // In a real app, send these values to the drone via BLE | |
| logToConsole(`Throttle: ${throttle}%, Yaw: ${yaw}%`, true); | |
| } | |
| }); | |
| setupJoystick(rightJoystick, rightKnob, (x, y) => { | |
| // Pitch (Y) and Roll (X) controls | |
| if (isConnected) { | |
| const pitch = (y * 100).toFixed(0); | |
| const roll = (x * 100).toFixed(0); | |
| // In a real app, send these values to the drone via BLE | |
| logToConsole(`Pitch: ${pitch}%, Roll: ${roll}%`, true); | |
| } | |
| }); | |
| } | |
| // Helper function to set up a joystick | |
| function setupJoystick(joystick, knob, callback) { | |
| let isDragging = false; | |
| // Touch events | |
| joystick.addEventListener('touchstart', handleStart); | |
| joystick.addEventListener('touchmove', handleMove); | |
| joystick.addEventListener('touchend', handleEnd); | |
| // Mouse events | |
| joystick.addEventListener('mousedown', handleStart); | |
| document.addEventListener('mousemove', handleMove); | |
| document.addEventListener('mouseup', handleEnd); | |
| function handleStart(e) { | |
| e.preventDefault(); | |
| isDragging = true; | |
| updatePosition(e); | |
| } | |
| function handleMove(e) { | |
| if (!isDragging) return; | |
| e.preventDefault(); | |
| updatePosition(e); | |
| } | |
| function handleEnd(e) { | |
| isDragging = false; | |
| // Return knob to center | |
| knob.style.transform = 'translate(-50%, -50%)'; | |
| callback(0, 0); | |
| } | |
| function updatePosition(e) { | |
| const rect = joystick.getBoundingClientRect(); | |
| const centerX = rect.left + rect.width / 2; | |
| const centerY = rect.top + rect.height / 2; | |
| let clientX, clientY; | |
| if (e.type.includes('touch')) { | |
| clientX = e.touches[0].clientX; | |
| clientY = e.touches[0].clientY; | |
| } else { | |
| clientX = e.clientX; | |
| clientY = e.clientY; | |
| } | |
| const x = clientX - centerX; | |
| const y = clientY - centerY; | |
| // Calculate distance from center | |
| const distance = Math.sqrt(x * x + y * y); | |
| const maxDistance = rect.width / 2 - 25; // 25 is half knob width | |
| // Normalized values (-1 to 1) | |
| let normX = x / maxDistance; | |
| let normY = y / maxDistance; | |
| // Limit to joystick bounds | |
| if (distance > maxDistance) { | |
| normX = (x / distance) * 1; | |
| normY = (y / distance) * 1; | |
| } | |
| // Update knob position | |
| knob.style.transform = `translate(${normX * 35}px, ${normY * 35}px) translate(-50%, -50%)`; | |
| // Call callback with normalized values | |
| callback(normX, normY); | |
| } | |
| } | |
| // Connect to drone (simulated) | |
| function connectToDrone(deviceId) { | |
| logToConsole(`Connecting to device ${deviceId}...`); | |
| // Simulate connection delay | |
| setTimeout(() => { | |
| isConnected = true; | |
| updateConnectionStatus(true); | |
| enableControls(); | |
| // Start battery simulation | |
| startBatterySimulation(); | |
| logToConsole('Connected successfully!'); | |
| logToConsole('PicoDrone ready for commands'); | |
| // Update device name in UI | |
| const device = devices.find(d => d.id === deviceId); | |
| if (device) { | |
| connectionText.textContent = device.name; | |
| } | |
| }, 1500); | |
| } | |
| // Disconnect from drone (simulated) | |
| function disconnectFromDrone() { | |
| logToConsole('Disconnecting from drone...'); | |
| // Stop any ongoing processes | |
| if (isFlying) { | |
| emergencyStop(); | |
| } | |
| if (batterySimulator) { | |
| clearInterval(batterySimulator); | |
| batterySimulator = null; | |
| } | |
| if (flightTimer) { | |
| clearInterval(flightTimer); | |
| flightTimer = null; | |
| } | |
| // Simulate disconnection delay | |
| setTimeout(() => { | |
| isConnected = false; | |
| isFlying = false; | |
| updateConnectionStatus(false); | |
| disableControls(); | |
| // Reset status displays | |
| batteryLevel.style.width = '0%'; | |
| batteryPercent.textContent = '0%'; | |
| signalLevel.style.width = '0%'; | |
| signalPercent.textContent = '-'; | |
| altitudeValue.textContent = '0.0 m'; | |
| speedValue.textContent = '0.0 m/s'; | |
| flightTime.textContent = '00:00'; | |
| logToConsole('Disconnected successfully'); | |
| connectionText.textContent = 'Disconnected'; | |
| }, 1000); | |
| } | |
| // Update connection status UI | |
| function updateConnectionStatus(connected) { | |
| if (connected) { | |
| connectionStatus.classList.remove('bg-red-500'); | |
| connectionStatus.classList.add('bg-green-500', 'connection-pulse'); | |
| connectionText.textContent = 'Connected'; | |
| connectBtn.innerHTML = '<i class="fas fa-unplug mr-2"></i> Disconnect'; | |
| connectBtn.classList.remove('bg-green-600', 'hover:bg-green-700'); | |
| connectBtn.classList.add('bg-red-600', 'hover:bg-red-700'); | |
| } else { | |
| connectionStatus.classList.remove('bg-green-500', 'connection-pulse'); | |
| connectionStatus.classList.add('bg-red-500'); | |
| connectionText.textContent = 'Disconnected'; | |
| connectBtn.innerHTML = '<i class="fas fa-plug mr-2"></i> Connect to Drone'; | |
| connectBtn.classList.remove('bg-red-600', 'hover:bg-red-700'); | |
| connectBtn.classList.add('bg-green-600', 'hover:bg-green-700'); | |
| } | |
| } | |
| // Enable flight controls | |
| function enableControls() { | |
| takeoffBtn.disabled = false; | |
| landBtn.disabled = !isFlying; | |
| emergencyBtn.disabled = false; | |
| } | |
| // Disable flight controls | |
| function disableControls() { | |
| takeoffBtn.disabled = true; | |
| landBtn.disabled = true; | |
| emergencyBtn.disabled = true; | |
| } | |
| // Takeoff command | |
| function takeoff() { | |
| if (!isFlying) { | |
| logToConsole('Initiating takeoff sequence...'); | |
| // Simulate takeoff delay | |
| setTimeout(() => { | |
| isFlying = true; | |
| flightStartTime = new Date(); | |
| startFlightTimer(); | |
| landBtn.disabled = false; | |
| logToConsole('Drone is airborne!'); | |
| // Simulate altitude increase | |
| let altitude = 0; | |
| const altitudeInterval = setInterval(() => { | |
| if (!isFlying) { | |
| clearInterval(altitudeInterval); | |
| return; | |
| } | |
| altitude += 0.1; | |
| if (altitude > 10) altitude = 10; // Max altitude for demo | |
| altitudeValue.textContent = altitude.toFixed(1) + ' m'; | |
| }, 200); | |
| }, 2000); | |
| } | |
| } | |
| // Land command | |
| function land() { | |
| if (isFlying) { | |
| logToConsole('Initiating landing sequence...'); | |
| // Simulate landing delay | |
| setTimeout(() => { | |
| isFlying = false; | |
| if (flightTimer) { | |
| clearInterval(flightTimer); | |
| flightTimer = null; | |
| } | |
| landBtn.disabled = true; | |
| logToConsole('Drone has landed safely'); | |
| // Simulate altitude decrease | |
| let altitude = parseFloat(altitudeValue.textContent); | |
| const altitudeInterval = setInterval(() => { | |
| altitude -= 0.2; | |
| if (altitude < 0) { | |
| altitude = 0; | |
| clearInterval(altitudeInterval); | |
| } | |
| altitudeValue.textContent = altitude.toFixed(1) + ' m'; | |
| }, 200); | |
| }, 2000); | |
| } | |
| } | |
| // Emergency stop command | |
| function emergencyStop() { | |
| logToConsole('EMERGENCY STOP ACTIVATED!', false, 'text-red-500'); | |
| // Immediate stop | |
| isFlying = false; | |
| if (flightTimer) { | |
| clearInterval(flightTimer); | |
| flightTimer = null; | |
| } | |
| landBtn.disabled = true; | |
| // Simulate rapid descent | |
| let altitude = parseFloat(altitudeValue.textContent); | |
| const crashInterval = setInterval(() => { | |
| altitude -= 0.5; | |
| if (altitude <= 0) { | |
| altitude = 0; | |
| clearInterval(crashInterval); | |
| logToConsole('Drone has crashed!', false, 'text-red-500'); | |
| } | |
| altitudeValue.textContent = altitude.toFixed(1) + ' m'; | |
| }, 100); | |
| } | |
| // Adjust trim | |
| function adjustTrim(axis, direction) { | |
| if (isConnected) { | |
| logToConsole(`Adjusting ${axis} trim: ${direction}`); | |
| // In a real app, send trim commands to the drone | |
| } | |
| } | |
| // Set flight mode | |
| function setFlightMode(mode) { | |
| // In a real app, send flight mode command to the drone | |
| } | |
| // Start flight timer | |
| function startFlightTimer() { | |
| flightTimer = setInterval(() => { | |
| const now = new Date(); | |
| const diff = now - flightStartTime; | |
| const minutes = Math.floor(diff / 60000); | |
| const seconds = Math.floor((diff % 60000) / 1000); | |
| flightTime.textContent = | |
| `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
| // Simulate speed changes | |
| const speed = (0.5 + Math.random() * 2).toFixed(1); | |
| speedValue.textContent = `${speed} m/s`; | |
| // Simulate signal strength fluctuations | |
| const signal = 70 + Math.floor(Math.random() * 30); | |
| signalLevel.style.width = `${signal}%`; | |
| signalPercent.textContent = `${signal}%`; | |
| }, 1000); | |
| } | |
| // Simulate battery drain | |
| function startBatterySimulation() { | |
| let battery = 75; // Start at 75% | |
| batteryLevel.style.width = `${battery}%`; | |
| batteryPercent.textContent = `${battery}%`; | |
| batterySimulator = setInterval(() => { | |
| if (!isConnected) return; | |
| // Drain faster when flying | |
| const drainRate = isFlying ? 0.5 : 0.1; | |
| battery -= drainRate; | |
| if (battery < 0) battery = 0; | |
| batteryLevel.style.width = `${battery}%`; | |
| batteryPercent.textContent = `${Math.floor(battery)}%`; | |
| // Change color based on level | |
| if (battery < 20) { | |
| batteryLevel.classList.remove('bg-green-500', 'bg-yellow-500'); | |
| batteryLevel.classList.add('bg-red-500'); | |
| } else if (battery < 50) { | |
| batteryLevel.classList.remove('bg-green-500', 'bg-red-500'); | |
| batteryLevel.classList.add('bg-yellow-500'); | |
| } else { | |
| batteryLevel.classList.remove('bg-yellow-500', 'bg-red-500'); | |
| batteryLevel.classList.add('bg-green-500'); | |
| } | |
| // Critical battery warning | |
| if (battery < 10 && isFlying) { | |
| logToConsole('WARNING: Critical battery level! Land immediately!', false, 'text-red-500'); | |
| } else if (battery < 20) { | |
| logToConsole('Warning: Low battery', false, 'text-yellow-500'); | |
| } | |
| // Auto-land at 5% if flying | |
| if (battery <= 5 && isFlying) { | |
| logToConsole('AUTO-LANDING: Battery critically low', false, 'text-red-500'); | |
| land(); | |
| } | |
| }, 1000); | |
| } | |
| // Log messages to console | |
| function logToConsole(message, isCommand = false, colorClass = '') { | |
| const now = new Date(); | |
| const timestamp = now.toLocaleTimeString(); | |
| const messageElement = document.createElement('div'); | |
| if (colorClass) { | |
| messageElement.className = colorClass; | |
| } | |
| messageElement.innerHTML = `> [${timestamp}] ${message}`; | |
| consoleOutput.appendChild(messageElement); | |
| consoleOutput.scrollTop = consoleOutput.scrollHeight; | |
| } | |
| // Initialize the app | |
| init(); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=privateuserh/privdrone" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |