// ==============================================================================
// 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)
);
}