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> |