// ============================================================================== // DriveSafe HUD Dashboard JavaScript // Establishes real-time EventSource connection, draws Chart.js EKG, and manages HUD state. // ============================================================================== let earChart; const maxChartPoints = 40; let chartData = Array(maxChartPoints).fill(0.3); let chartLabels = Array(maxChartPoints).fill(""); let renderedChatLogsCount = 0; // --- Initialize Chart.js on DOM Load --- document.addEventListener("DOMContentLoaded", () => { const ctx = document.getElementById('earChart').getContext('2d'); // Create futuristic neon line chart earChart = new Chart(ctx, { type: 'line', data: { labels: chartLabels, datasets: [{ label: 'Eye Aspect Ratio (EAR)', data: chartData, borderColor: '#00bfff', borderWidth: 2, pointRadius: 0, fill: true, backgroundColor: 'rgba(0, 191, 255, 0.05)', tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { min: 0.0, max: 0.45, grid: { color: 'rgba(255, 255, 255, 0.03)' }, ticks: { color: '#64748b', font: { family: 'Share Tech Mono', size: 10 } } } }, animation: { duration: 0 } // Disable default anims for absolute high-speed rendering } }); // Initialize Telemetry EventSource listener initTelemetry(); }); // --- Server-Sent Events (SSE) Telemetry Listener --- function initTelemetry() { const sse = new EventSource('/telemetry'); sse.onmessage = (event) => { const data = JSON.parse(event.data); // 1. Update Core Readouts updateMetrics(data); // 2. Shift EAR Graph Data updateChart(data.ear); // 3. Update Conversation Chat Logs updateChat(data.chat_history); // 4. Update HUD panel glowing states updateStateOverlays(data.state, data.alert_message); }; sse.onerror = (err) => { console.error("SSE connection dropped:", err); }; } // --- Update Circular Gauge & Numeric Metrics --- function updateMetrics(data) { // Current numeric readout document.getElementById("ear-val").innerText = data.ear.toFixed(2); // Circular radial progress math const gauge = document.getElementById("ear-gauge"); const radius = gauge.r.baseVal.value; const circumference = 2 * Math.PI * radius; // EAR bounds are normally between 0.10 (fully closed) and 0.35 (wide open) // Normalize EAR to 0% - 100% range let percentage = (data.ear - 0.10) / (0.35 - 0.10); percentage = Math.max(0, Math.min(1, percentage)); // Clamp between 0 and 1 const offset = circumference - (percentage * circumference); gauge.style.strokeDashoffset = offset; // Change gauge color relative to EAR thresholds const warnThreshold = 0.23; const closedThreshold = 0.20; if (data.ear <= closedThreshold) { gauge.style.stroke = "var(--danger-color)"; } else if (data.ear <= warnThreshold) { gauge.style.stroke = "var(--warn-color)"; } else { gauge.style.stroke = "var(--safe-color)"; } // Update Drowsiness Count const countElement = document.getElementById("drowsiness-count"); countElement.innerText = data.drowsiness_count; if (data.drowsiness_count > 0) { countElement.style.color = "var(--danger-color)"; countElement.style.textShadow = "0 0 10px rgba(255, 40, 80, 0.4)"; } else { countElement.style.color = "var(--safe-color)"; countElement.style.textShadow = "none"; } // Update Engine FPS document.getElementById("engine-fps").innerText = `${data.fps} FPS`; // Update Tracking active button toggle const trackingBtn = document.getElementById("toggle-tracking-btn"); const label = trackingBtn.querySelector("span"); if (data.detection_active) { trackingBtn.classList.add("active"); label.innerText = "ACTIVE TRACKING"; } else { trackingBtn.classList.remove("active"); label.innerText = "TRACKING PAUSED"; } } // --- EKG EAR Waveform Chart Shifter --- function updateChart(newEar) { chartData.push(newEar); chartData.shift(); // Swap graph colors based on EAR values if (newEar < 0.20) { earChart.data.datasets[0].borderColor = "#ff2850"; earChart.data.datasets[0].backgroundColor = "rgba(255, 40, 80, 0.05)"; } else if (newEar < 0.23) { earChart.data.datasets[0].borderColor = "#ffaa00"; earChart.data.datasets[0].backgroundColor = "rgba(255, 170, 0, 0.05)"; } else { earChart.data.datasets[0].borderColor = "#00ff80"; earChart.data.datasets[0].backgroundColor = "rgba(0, 255, 128, 0.05)"; } earChart.update(); } // --- Dynamic Conversational Chat Logs renderer --- function updateChat(history) { if (history.length === renderedChatLogsCount) return; const chatBox = document.getElementById("chat-box"); // Render only new logs added since last pass for (let i = renderedChatLogsCount; i < history.length; i++) { const log = history[i]; // System logs represent state announcements if (log.speaker === "System") { const systemDiv = document.createElement("div"); systemDiv.className = "chat-bubble system-bubble"; systemDiv.innerText = log.message; chatBox.appendChild(systemDiv); } else { // User query if (log.query) { const userDiv = document.createElement("div"); userDiv.className = "chat-bubble driver-bubble"; userDiv.innerHTML = `Driver${escapeHTML(log.query)}`; chatBox.appendChild(userDiv); } // AI response if (log.message) { const aiDiv = document.createElement("div"); aiDiv.className = "chat-bubble slm-bubble"; aiDiv.innerHTML = `DriveSafe SLM${escapeHTML(log.message)}`; chatBox.appendChild(aiDiv); } } } renderedChatLogsCount = history.length; // Auto-scroll chat box down with a short delay for fluid rendering setTimeout(() => { chatBox.scrollTop = chatBox.scrollHeight; }, 50); } // --- Toggle HUD Glowing Cockpit States --- function updateStateOverlays(state, alertMsg) { const mainPanel = document.getElementById("main-panel"); const badge = document.getElementById("hud-state-badge"); const slmIndicator = document.getElementById("slm-indicator"); // Clean current state classes mainPanel.className = "glass-panel video-panel"; if (state === "NORMAL") { mainPanel.classList.add("state-normal"); badge.innerText = alertMsg ? alertMsg : "NORMAL"; badge.style.borderColor = "var(--safe-color)"; badge.style.color = "var(--safe-color)"; badge.style.boxShadow = "0 0 10px rgba(0, 255, 128, 0.2)"; slmIndicator.innerText = "SLM STANDBY"; slmIndicator.style.color = "var(--safe-color)"; } else if (state === "CLOSED_3S") { mainPanel.classList.add("state-warn"); badge.innerText = alertMsg ? alertMsg : "EYES CLOSED (3-5s)"; badge.style.borderColor = "var(--warn-color)"; badge.style.color = "var(--warn-color)"; badge.style.boxShadow = "0 0 15px rgba(255, 170, 0, 0.3)"; } else if (state === "CLOSED_5S" || state === "WAITING_REST_RESPONSE" || state === "WAITING_SONG_RESPONSE") { mainPanel.classList.add("state-danger"); badge.innerText = alertMsg ? alertMsg : "CRITICAL WARNING"; badge.style.borderColor = "var(--danger-color)"; badge.style.color = "var(--danger-color)"; badge.style.boxShadow = "0 0 25px rgba(255, 40, 80, 0.5)"; if (state.startsWith("WAITING")) { slmIndicator.innerText = "SLM ACTIVE DIALOGUE"; slmIndicator.style.color = "var(--accent-color)"; } } else if (state === "PLAYING_MUSIC") { mainPanel.classList.add("state-normal"); badge.innerText = "BEATS PLAYING"; badge.style.borderColor = "var(--accent-color)"; badge.style.color = "var(--accent-color)"; badge.style.boxShadow = "0 0 10px rgba(0, 191, 255, 0.2)"; } } // --- Control Desk AJAX REST Bridge --- function toggleTracking() { fetch('/api/toggle_detection', { method: 'POST' }) .then(response => response.json()) .then(data => { console.log("Tracking toggled, active state:", data.detection_active); }) .catch(err => console.error("Error toggling tracking:", err)); } function triggerReset() { fetch('/api/reset', { method: 'POST' }) .then(response => response.json()) .then(data => { console.log("System reset:", data.message); }) .catch(err => console.error("Error triggering reset:", err)); } function triggerMusic() { fetch('/api/trigger_music', { method: 'POST' }) .then(response => response.json()) .then(data => { console.log("Music play request sent:", data.message); }) .catch(err => console.error("Error playing energetic beats:", err)); } // --- Helper Functions --- function escapeHTML(str) { return str.replace(/[&<>'"]/g, tag => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[tag] || tag) ); }