CARLA ? C-Shenron based Simualtor for Sensor data generation.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

386 lines
16 KiB

document.addEventListener('DOMContentLoaded', () => {
const scenarioList = document.getElementById('scenario-list');
const weatherSelect = document.getElementById('weather');
const framesInput = document.getElementById('frames');
const launchForm = document.getElementById('launch-form');
const launchBtn = document.getElementById('launch-btn');
const launchText = document.getElementById('launch-text');
const launchSpinner = document.getElementById('launch-spinner');
const terminalOutput = document.getElementById('terminal-output');
const clearTerminalBtn = document.getElementById('clear-terminal');
// New Elements
const stopBtn = document.getElementById('stop-btn');
const simStatusDot = document.getElementById('sim-status-dot');
const simStatusText = document.getElementById('sim-status-text');
const launchSimBtn = document.getElementById('launch-sim-btn');
// Idle Toggle Elements
const idleToggleContainer = document.getElementById('idle-toggle-container');
const gpuIdleToggle = document.getElementById('gpu-idle-toggle');
let currentEventSource = null;
let idleTimer = null;
let isSimulatorRunning = false;
let isSimulationActive = false; // user currently running a scenario
// --- Collapsible Config Card Logic ---
const configSection = document.getElementById('config-section');
const configToggleHeader = document.getElementById('config-toggle-header');
// Initial State Persistence
if (localStorage.getItem('config_collapsed') === 'true') {
configSection.classList.add('collapsed');
}
configToggleHeader.addEventListener('click', () => {
configSection.classList.toggle('collapsed');
localStorage.setItem('config_collapsed', configSection.classList.contains('collapsed'));
});
// Auto Idle Tracker
function resetIdleTimer() {
if (idleTimer) clearTimeout(idleTimer);
// If simulation is not active and simulator is ready but NOT paused
if (!isSimulationActive && isSimulatorRunning && gpuIdleToggle && !gpuIdleToggle.checked) {
idleTimer = setTimeout(() => {
console.log("Auto-idling simulator due to 30s inactivity...");
gpuIdleToggle.checked = true;
gpuIdleToggle.dispatchEvent(new Event('change'));
}, 30000);
}
}
if (gpuIdleToggle) {
gpuIdleToggle.addEventListener('change', (e) => {
const isIdle = e.target.checked;
const endpoint = isIdle ? '/api/simulator/idle' : '/api/simulator/wake';
fetch(endpoint, { method: 'POST' });
if (!isIdle) {
resetIdleTimer();
} else {
if (idleTimer) clearTimeout(idleTimer);
}
});
}
// Simulator Status Polling
function updateSimulatorStatus() {
fetch('/api/simulator/status')
.then(res => res.json())
.then(data => {
const status = data.status; // offline, loading, ready
simStatusDot.className = 'dot';
if (status === 'ready') {
if (!isSimulatorRunning) {
isSimulatorRunning = true;
resetIdleTimer(); // start tracker as soon as it becomes ready
}
if (idleToggleContainer) idleToggleContainer.style.display = 'flex';
if (gpuIdleToggle && gpuIdleToggle.checked !== data.is_paused) {
gpuIdleToggle.checked = data.is_paused;
}
if (data.is_paused) {
simStatusDot.classList.add('yellow');
simStatusText.textContent = 'Ready (Idle)';
} else {
simStatusDot.classList.add('green');
simStatusText.textContent = 'Ready';
}
launchSimBtn.style.display = 'none';
} else if (status === 'loading') {
isSimulatorRunning = false;
if (idleToggleContainer) idleToggleContainer.style.display = 'none';
simStatusDot.classList.add('yellow');
simStatusText.textContent = 'Loading...';
launchSimBtn.style.display = 'none';
} else {
isSimulatorRunning = false;
if (idleToggleContainer) idleToggleContainer.style.display = 'none';
if (idleTimer) clearTimeout(idleTimer);
simStatusDot.classList.add('red');
simStatusText.textContent = 'Offline';
launchSimBtn.style.display = 'block';
}
})
.catch(() => {
simStatusDot.className = 'dot red';
simStatusText.textContent = 'Error';
});
}
updateSimulatorStatus();
setInterval(updateSimulatorStatus, 3000);
// Launch CarlaUE4 action
launchSimBtn.addEventListener('click', () => {
simStatusText.textContent = 'Starting...';
simStatusDot.className = 'dot yellow';
fetch('/api/simulator/launch', { method: 'POST' });
});
// Fetch Initial Config
fetch('/api/config')
.then(response => response.json())
.then(config => {
// Populate Scenarios
config.scenarios.forEach((scenario, index) => {
const isChecked = scenario === config.default_scenario ? 'checked' : '';
const cardHtml = `
<label class="scenario-card">
<input type="radio" name="scenario" value="${scenario}" ${isChecked}>
<div class="card-content">
<span class="card-title">${scenario}</span>
</div>
</label>
`;
scenarioList.insertAdjacentHTML('beforeend', cardHtml);
});
// Listen for scenario changes to load dynamic parameters
const scenarioRadios = document.querySelectorAll('input[name="scenario"]');
scenarioRadios.forEach(radio => {
radio.addEventListener('change', (e) => loadScenarioParams(e.target.value));
});
// Load params for the initially selected scenario
if (config.default_scenario) {
loadScenarioParams(config.default_scenario);
}
// Populate Weather
config.weather_options.forEach(weather => {
const isSelected = weather === config.default_weather ? 'selected' : '';
const optionHtml = `<option value="${weather}" ${isSelected}>${weather}</option>`;
weatherSelect.insertAdjacentHTML('beforeend', optionHtml);
});
// Default Frames
framesInput.value = config.default_frames;
appendLog(`> Config loaded successfully. Found ${config.scenarios.length} scenarios.`, 'system');
})
.catch(error => {
appendLog(`> Error loading config: ${error.message}`, 'error');
});
// Handle Launch
launchForm.addEventListener('submit', (e) => {
e.preventDefault();
isSimulationActive = true;
if (idleTimer) clearTimeout(idleTimer);
// Ensure simulator is awake before scenario runs just in case
if (gpuIdleToggle && gpuIdleToggle.checked) {
gpuIdleToggle.checked = false;
fetch('/api/simulator/wake', { method: 'POST' });
}
// UI Loading State
setLoadingState(true);
appendLog(`\n> --- NEW SIMULATION RUN ---`, 'info');
// Close existing stream if any
if (currentEventSource) {
currentEventSource.close();
}
const formData = new FormData(launchForm);
// Scenario radions are in the sidebar, outside the form
const scenarioRadio = document.querySelector('input[name="scenario"]:checked');
const scenarioValue = scenarioRadio ? scenarioRadio.value : 'braking';
// Serialize dynamic parameters
const paramInputs = document.querySelectorAll('#dynamic-params-container input');
let paramsString = [];
paramInputs.forEach(input => {
if (input.value !== '') {
paramsString.push(`${input.name}=${input.value}`);
}
});
const payload = {
scenario: scenarioValue,
frames: formData.get('frames'),
weather: formData.get('weather'),
no_record: formData.get('no_record') === 'on',
params: paramsString.join(',')
};
// We use fetch API to trigger the run (which could take long), but Server Sent Events (SSE)
// doesn't support POST natively without fetch streams. So we do a POST fetch that returns an SSE stream.
startStream('/api/run', payload);
});
// Clear Terminal
clearTerminalBtn.addEventListener('click', () => {
terminalOutput.innerHTML = '';
appendLog('> Terminal cleared.', 'system');
});
// Stop Simulation
if (stopBtn) {
stopBtn.addEventListener('click', () => {
stopBtn.disabled = true;
stopBtn.querySelector('.btn-text').textContent = 'Stopping...';
appendLog('\n> Sending stop signal to simulation. Waiting for graceful exit...', 'warn');
fetch('/api/stop', { method: 'POST' })
.then(res => res.json())
.then(data => {
console.log('Stop requested:', data.message);
})
.catch(err => {
appendLog('> Failed to send stop signal: ' + err.message, 'error');
stopBtn.disabled = false;
});
});
}
// Utility: Append log to terminal
function appendLog(text, type = '') {
const line = document.createElement('div');
line.className = `log-line ${type}`;
// Advanced heuristic for colored logs
if (text.includes('[INFO]')) line.classList.add('info');
if (text.includes('[WARN]')) line.classList.add('warn');
if (text.includes('[ERROR]')) line.classList.add('error');
if (text.includes('[SUCCESS]')) line.classList.add('success');
if (text.includes('[FRAME ')) line.classList.add('frame-log');
if (text.includes('Traceback') || text.includes('Exception')) line.classList.add('error');
line.textContent = text;
// Smart Scroll Lock
// Check if the user is currently at the bottom of the scroll view
const isAtBottom = terminalOutput.scrollHeight - terminalOutput.scrollTop - terminalOutput.clientHeight < 50;
terminalOutput.appendChild(line);
if (isAtBottom) {
terminalOutput.scrollTop = terminalOutput.scrollHeight;
}
}
// Utility: Post JSON and stream response
async function startStream(url, payload) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let boundary = buffer.indexOf('\n\n');
while (boundary !== -1) {
const chunk = buffer.substring(0, boundary);
buffer = buffer.substring(boundary + 2);
if (chunk.startsWith('data: ')) {
const data = chunk.substring(6);
// Check for completion marker
if (data.startsWith('[PROCESS_COMPLETED]')) {
appendLog(`> Simulation Finished. ${data}`, 'info');
setLoadingState(false);
document.getElementById('sim-progress-container').style.display = 'none';
isSimulationActive = false;
resetIdleTimer();
} else {
// Intercept progress bar logs
if (data.includes('SIMULATING:')) {
const match = data.match(/(\d+)%\|.*\|\s*(\d+)\/(\d+)/);
if (match) {
document.getElementById('sim-progress-container').style.display = 'block';
document.getElementById('sim-progress-fill').style.width = match[1] + '%';
document.getElementById('sim-progress-text').textContent = `${match[1]}% (${match[2]} / ${match[3]} frames)`;
continue; // Skip appending this line to the terminal box
}
}
// Prevent carriage return visual artifacts
if (data.includes('\r')) {
const parts = data.split('\r');
const lastPart = parts[parts.length - 1];
const lastLine = terminalOutput.lastElementChild;
if (lastLine && !lastLine.textContent.includes('\n')) {
lastLine.textContent = lastPart;
continue;
}
}
appendLog(data);
}
}
boundary = buffer.indexOf('\n\n');
}
}
} catch (error) {
appendLog(`> Connection error: ${error.message}`, 'error');
setLoadingState(false);
}
}
function setLoadingState(isLoading) {
launchBtn.disabled = isLoading;
launchBtn.style.display = isLoading ? 'none' : 'block';
if (stopBtn) {
stopBtn.style.display = isLoading ? 'block' : 'none';
}
if (!isLoading) {
launchText.textContent = "Launch Another Simulation";
if (stopBtn) {
stopBtn.disabled = false;
stopBtn.querySelector('.btn-text').textContent = 'Stop Simulation';
}
}
}
// Utility: Fetch and render dynamic scenario parameters
function loadScenarioParams(scenarioName) {
const container = document.getElementById('dynamic-params-container');
container.innerHTML = '<div class="input-group full-width"><span class="helper">Loading parameters...</span></div>';
fetch(`/api/scenario_params/${scenarioName}`)
.then(res => res.json())
.then(data => {
container.innerHTML = '';
if (data.success && Object.keys(data.params).length > 0) {
Object.entries(data.params).forEach(([key, val]) => {
const inputHtml = `
<div class="input-group">
<label for="param_${key}">${key}</label>
<div class="input-wrapper">
<input type="text" id="param_${key}" name="${key}" value="${val}">
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', inputHtml);
});
} else {
container.innerHTML = '<div class="input-group full-width"><span class="helper">No tunable constants found for this scenario.</span></div>';
}
})
.catch(err => {
container.innerHTML = '<div class="input-group full-width"><span class="helper" style="color:var(--danger)">Failed to load parameters.</span></div>';
});
}
});