document.addEventListener('DOMContentLoaded', () => { // --- State --- let isConnected = false; let liveMode = false; let currentGroup = 1; let eventSource = null; let sparklineData = {}; let dashRefreshTimer = null; // Cached data for export let lastDTCs = []; let lastMeasurements = {}; let ecuInfoCache = {}; // --- DOM Elements --- const portSelect = document.getElementById('portSelect'); const refreshPortsBtn = document.getElementById('refreshPortsBtn'); const connectBtn = document.getElementById('connectBtn'); const statusDot = document.getElementById('statusDot'); const statusText = document.getElementById('statusText'); const mainContent = document.getElementById('mainContent'); const ecuPartNum = document.getElementById('ecuPartNum'); const ecuExtra = document.getElementById('ecuExtra'); const dtcContainer = document.getElementById('dtcContainer'); const readDTCBtn = document.getElementById('readDTCBtn'); const clearDTCBtn = document.getElementById('clearDTCBtn'); const exportDTCBtn = document.getElementById('exportDTCBtn'); const groupPills = document.getElementById('groupPills'); const customGroupInput = document.getElementById('customGroupInput'); const readGroupBtn = document.getElementById('readGroupBtn'); const groupNameLabel = document.getElementById('groupNameLabel'); const measContainer = document.getElementById('measContainer'); const liveModeToggle = document.getElementById('liveModeToggle'); const exportMeasBtn = document.getElementById('exportMeasBtn'); const toggleLogBtn = document.getElementById('toggleLogBtn'); const logContainer = document.getElementById('logContainer'); const confirmModal = document.getElementById('confirmModal'); const cancelModalBtn = document.getElementById('cancelModalBtn'); const confirmClearBtn = document.getElementById('confirmClearBtn'); const dashLiveToggle = document.getElementById('dashLiveToggle'); const exportAllBtn = document.getElementById('exportAllBtn'); // --- Initialization --- initGroupPills(); fetchPorts(); // --- Event Listeners --- refreshPortsBtn.addEventListener('click', fetchPorts); connectBtn.addEventListener('click', () => { if (isConnected) { disconnect(); } else { connect(); } }); readDTCBtn.addEventListener('click', fetchDTCs); clearDTCBtn.addEventListener('click', () => { confirmModal.classList.remove('hidden'); }); cancelModalBtn.addEventListener('click', () => { confirmModal.classList.add('hidden'); }); confirmClearBtn.addEventListener('click', () => { confirmModal.classList.add('hidden'); clearDTCs(); }); readGroupBtn.addEventListener('click', () => { let group = customGroupInput.value ? parseInt(customGroupInput.value) : currentGroup; if (liveModeToggle.checked) { startLiveMode(group); } else { fetchMeasGroup(group); } }); liveModeToggle.addEventListener('change', (e) => { if (e.target.checked) { startLiveMode(currentGroup); } else { stopLiveMode(); } }); toggleLogBtn.addEventListener('click', () => { logContainer.classList.toggle('collapsed'); toggleLogBtn.textContent = logContainer.classList.contains('collapsed') ? '▼' : '▲'; }); // Dashboard auto-refresh toggle dashLiveToggle.addEventListener('change', (e) => { if (e.target.checked) { startDashboardRefresh(); } else { stopDashboardRefresh(); } }); // Export buttons exportDTCBtn.addEventListener('click', exportDTCsToFile); exportMeasBtn.addEventListener('click', exportMeasToFile); exportAllBtn.addEventListener('click', exportFullReport); // --- Gauge Drawing --- function drawGauge(canvasId, value, min, max, color = '#00d4ff', warnThresh = null, dangerThresh = null) { const canvas = document.getElementById(canvasId); if (!canvas) return; const ctx = canvas.getContext('2d'); const w = canvas.width; const h = canvas.height; const cx = w / 2; const cy = h / 2 + 10; const r = Math.min(w, h) / 2 - 15; ctx.clearRect(0, 0, w, h); // Angle range: from 220° to -40° (sweeping 260°) const startAngle = (220 * Math.PI) / 180; const endAngle = (-40 * Math.PI) / 180; const totalAngle = startAngle - endAngle; // Background arc ctx.beginPath(); ctx.arc(cx, cy, r, endAngle, startAngle, false); ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)'; ctx.lineWidth = 10; ctx.lineCap = 'round'; ctx.stroke(); // Value arc if (typeof value === 'number' && !isNaN(value)) { const clamped = Math.max(min, Math.min(max, value)); const pct = (clamped - min) / (max - min); const valAngle = startAngle - pct * totalAngle; // Determine color based on thresholds let arcColor = color; if (dangerThresh !== null && value >= dangerThresh) { arcColor = '#ff3366'; } else if (warnThresh !== null && value >= warnThresh) { arcColor = '#ffa502'; } ctx.beginPath(); ctx.arc(cx, cy, r, startAngle, valAngle, true); ctx.strokeStyle = arcColor; ctx.lineWidth = 10; ctx.lineCap = 'round'; ctx.stroke(); // Glow effect ctx.beginPath(); ctx.arc(cx, cy, r, startAngle, valAngle, true); ctx.strokeStyle = arcColor; ctx.lineWidth = 10; ctx.lineCap = 'round'; ctx.shadowColor = arcColor; ctx.shadowBlur = 12; ctx.stroke(); ctx.shadowBlur = 0; // Min/Max labels ctx.font = '10px Outfit, sans-serif'; ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.textAlign = 'center'; const minLabelX = cx + (r + 12) * Math.cos(startAngle); const minLabelY = cy + (r + 12) * Math.sin(startAngle); ctx.fillText(min, minLabelX, minLabelY); const maxLabelX = cx + (r + 12) * Math.cos(endAngle); const maxLabelY = cy + (r + 12) * Math.sin(endAngle); ctx.fillText(max, maxLabelX, maxLabelY); } } function updateGauges(group, measurements) { if (!measurements || measurements.length < 1) return; if (group === 1) { // Group 1: Compressor & Blower Status // Field 0: Compressor Shut-off Code if (measurements[0]) { const compCode = measurements[0].value; const compRing = document.querySelector('.compressor-ring'); const compValEl = document.getElementById('gaugeValCompressor'); // Compressor is typically ON if shut-off code is 0 const isOn = (compCode === 0); compRing.className = `compressor-ring ${isOn ? 'on' : 'off'}`; compValEl.textContent = isOn ? 'ON' : 'OFF'; compValEl.style.color = isOn ? 'var(--accent-green)' : 'var(--text-muted)'; } // Field 1: Target Blower Voltage if (measurements[1]) { const blowerVal = measurements[1].value; document.getElementById('gaugeValBlower').textContent = typeof blowerVal === 'number' ? blowerVal.toFixed(1) : blowerVal; drawGauge('gaugeCanvasBlower', blowerVal, 0, 15, '#00d4ff'); } // Field 2: System Voltage if (measurements[2]) { const sysV = measurements[2].value; document.getElementById('gaugeValSysV').textContent = typeof sysV === 'number' ? sysV.toFixed(1) : sysV; drawGauge('gaugeCanvasSysV', sysV, 10, 16, '#00e676', null, null); } } if (group === 2) { // Group 2: Temperature Sensors (Raw) // Field 0: Interior Sensor (G56) if (measurements[0]) { const intTemp = measurements[0].value; document.getElementById('gaugeValInterior').textContent = typeof intTemp === 'number' ? intTemp.toFixed(1) : intTemp; // Using generic 0-255 range for ADC counts since we don't have the exact formula mapping yet drawGauge('gaugeCanvasInterior', intTemp, 0, 255, '#00d4ff', 200, 240); } } if (group === 3) { // Group 3: External Sensors (Raw) // Field 0: Outside Air Sensor (G17) if (measurements[0]) { const outTemp = measurements[0].value; document.getElementById('gaugeValOutside').textContent = typeof outTemp === 'number' ? outTemp.toFixed(1) : outTemp; drawGauge('gaugeCanvasOutside', outTemp, 0, 255, '#00d4ff', 200, 240); } // Field 1: Evaporator Sensor (G263) if (measurements[1]) { const evapTemp = measurements[1].value; document.getElementById('gaugeValEvap').textContent = typeof evapTemp === 'number' ? evapTemp.toFixed(1) : evapTemp; drawGauge('gaugeCanvasEvap', evapTemp, 0, 255, '#33ddff', 200, 240); } } } // --- Car Diagram Updates --- function updateDiagram(group, measurements) { if (!measurements || measurements.length < 1) return; if (group === 2) { // Group 2: Interior Sensor (G56) ADC, Dash Sensor (G86) ADC, Fresh Air Sensor (G89) ADC, Set Temperature updateSensorDot('sensorG56', measurements[0], 'diag_G56', '°C'); updateSensorDot('sensorG86', measurements[1], 'diag_G86', '°C'); updateSensorDot('sensorG89', measurements[2], 'diag_G89', '°C'); } if (group === 3) { // Group 3: Outside Air (G17) ADC, Evaporator (G263) ADC, Coolant Temp ADC, Sunlight (G107) ADC updateSensorDot('sensorG17', measurements[0], 'diag_G17', '°C'); updateSensorDot('sensorG263', measurements[1], 'diag_G263', '°C'); updateSensorDot('sensorCoolant', measurements[2], 'diag_Coolant', '°C'); if (measurements[3]) { updateSensorDot('sensorG107', measurements[3], 'diag_G107', ''); } } if (group === 8) { // Group 8: Sensor Voltages - Field 0 is G65 High Pressure if (measurements[0]) { updateSensorDot('sensorG65', measurements[0], 'diag_G65', ' V'); } } } function updateSensorDot(groupId, meas, textId, suffix) { if (!meas) return; const textEl = document.getElementById(textId); const groupEl = document.getElementById(groupId); if (!textEl || !groupEl) return; const val = meas.value; const displayVal = typeof val === 'number' ? val.toFixed(1) : val; textEl.textContent = `${displayVal}${suffix}`; // Determine sensor health from value const circles = groupEl.querySelectorAll('circle'); const outerCircle = circles[0]; const innerCircle = circles[1]; const rect = groupEl.querySelector('rect'); if (typeof val === 'number') { if (val <= -40 || val >= 220) { // Fault / Open circuit setSensorColor(outerCircle, innerCircle, rect, '#ff3366', 'glow-fault'); outerCircle.style.animation = 'sensor-pulse-fault 1.5s ease-in-out infinite'; } else if (val >= 100 || val <= -20) { // Warning range setSensorColor(outerCircle, innerCircle, rect, '#ffa502', 'glow-ok'); outerCircle.style.animation = ''; } else { // Normal setSensorColor(outerCircle, innerCircle, rect, '#00d4ff', 'glow-ok'); outerCircle.style.animation = ''; } } } function setSensorColor(outerCircle, innerCircle, rect, color, filterName) { const colorMap = { '#ff3366': { fill: 'rgba(255,51,102,0.15)', stroke: 'rgba(255,51,102,0.3)' }, '#ffa502': { fill: 'rgba(255,165,2,0.15)', stroke: 'rgba(255,165,2,0.3)' }, '#00d4ff': { fill: 'rgba(0,212,255,0.15)', stroke: 'rgba(0,212,255,0.3)' }, '#00e676': { fill: 'rgba(0,230,118,0.15)', stroke: 'rgba(0,230,118,0.3)' } }; const mapped = colorMap[color] || { fill: 'rgba(0,212,255,0.15)', stroke: 'rgba(0,212,255,0.3)' }; outerCircle.setAttribute('stroke', color); outerCircle.setAttribute('fill', mapped.fill); innerCircle.setAttribute('fill', color); outerCircle.setAttribute('filter', `url(#${filterName})`); if (rect) { rect.setAttribute('stroke', mapped.stroke); } } // Dashboard auto-refresh: cycles through groups 1, 2, 3, 8 to populate gauges + diagram let dashGroupCycle = [1, 2, 3, 8]; let dashCycleIdx = 0; function startDashboardRefresh() { if (dashRefreshTimer) clearInterval(dashRefreshTimer); dashCycleIdx = 0; addLog('Dashboard auto-refresh started.', 'system'); // Immediately fetch first group fetchDashGroup(); dashRefreshTimer = setInterval(() => { if (!isConnected) { stopDashboardRefresh(); return; } fetchDashGroup(); }, 2000); } function stopDashboardRefresh() { if (dashRefreshTimer) { clearInterval(dashRefreshTimer); dashRefreshTimer = null; } addLog('Dashboard auto-refresh stopped.', 'system'); } async function fetchDashGroup() { const group = dashGroupCycle[dashCycleIdx % dashGroupCycle.length]; dashCycleIdx++; try { const res = await fetch(`/api/meas/${group}`); const data = await res.json(); if (data.success) { updateGauges(group, data.measurements); updateDiagram(group, data.measurements); // Cache for export lastMeasurements[group] = { name: data.name, group: group, measurements: data.measurements, timestamp: new Date().toISOString() }; } } catch (e) { // Silently fail on dash refresh } } // --- Export Functions --- function exportDTCsToFile() { if (lastDTCs.length === 0) { alert('No fault codes to export. Read faults first.'); return; } const report = { export_type: 'Fault Codes', vehicle: 'Audi A4 B5 (8D2)', module: '08 - Auto HVAC', ecu_info: ecuInfoCache, timestamp: new Date().toISOString(), fault_codes: lastDTCs }; downloadJSON(report, `audi_hvac_dtcs_${dateStamp()}.json`); addLog(`Exported ${lastDTCs.length} fault codes.`, 'data'); } function exportMeasToFile() { if (Object.keys(lastMeasurements).length === 0) { alert('No measurement data to export. Read some groups first.'); return; } const report = { export_type: 'Measurement Blocks', vehicle: 'Audi A4 B5 (8D2)', module: '08 - Auto HVAC', ecu_info: ecuInfoCache, timestamp: new Date().toISOString(), measurement_blocks: lastMeasurements }; downloadJSON(report, `audi_hvac_meas_${dateStamp()}.json`); addLog('Exported measurement blocks.', 'data'); } async function exportFullReport() { addLog('Generating full diagnostic report...', 'system'); // Read fresh DTCs try { const dtcRes = await fetch('/api/dtcs'); const dtcData = await dtcRes.json(); if (dtcData.success) { lastDTCs = dtcData.dtcs; renderDTCs(lastDTCs); } } catch (e) {} // Read all 8 known groups for (let g = 1; g <= 8; g++) { try { const res = await fetch(`/api/meas/${g}`); const data = await res.json(); if (data.success) { lastMeasurements[g] = { name: data.name, group: g, measurements: data.measurements, timestamp: new Date().toISOString() }; updateGauges(g, data.measurements); updateDiagram(g, data.measurements); } } catch (e) {} } const report = { export_type: 'Full Diagnostic Report', vehicle: 'Audi A4 B5 (8D2)', module: '08 - Auto HVAC', ecu_info: ecuInfoCache, generated_at: new Date().toISOString(), fault_codes: lastDTCs, measurement_blocks: lastMeasurements, analysis_prompt: 'Please analyze this Audi A4 B5 HVAC diagnostic data. Identify potential issues based on the fault codes and measurement values. Check for out-of-range sensor readings, abnormal temperatures, compressor issues, and any correlations between fault codes and measurement data.' }; downloadJSON(report, `audi_hvac_full_report_${dateStamp()}.json`); addLog('Full diagnostic report exported.', 'data'); } function downloadJSON(obj, filename) { const blob = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } function dateStamp() { const d = new Date(); return `${d.getFullYear()}${(d.getMonth()+1).toString().padStart(2,'0')}${d.getDate().toString().padStart(2,'0')}_${d.getHours().toString().padStart(2,'0')}${d.getMinutes().toString().padStart(2,'0')}`; } // --- API Functions --- async function fetchPorts() { try { portSelect.innerHTML = ''; const res = await fetch('/api/ports'); const data = await res.json(); portSelect.innerHTML = ''; if (data.ports.length === 0) { portSelect.innerHTML = ''; } else { data.ports.forEach(p => { const opt = document.createElement('option'); opt.value = p.device; opt.textContent = `${p.device} ${p.is_likely ? '(KKL)' : ''}`; portSelect.appendChild(opt); }); if (data.auto) { portSelect.value = data.auto; } } } catch (e) { addLog(`Error fetching ports: ${e}`, 'error'); } } async function connect() { const port = portSelect.value; if (!port) { alert('Please select a port'); return; } connectBtn.textContent = 'Connecting...'; connectBtn.disabled = true; addLog(`Connecting to ${port} (Address 0x08)...`, 'system'); try { const res = await fetch('/api/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ port }) }); const data = await res.json(); if (data.success) { isConnected = true; connectBtn.textContent = 'Disconnect'; connectBtn.disabled = false; connectBtn.classList.replace('btn-primary', 'btn-danger'); statusDot.classList.replace('disconnected', 'connected'); statusText.textContent = 'Connected (0x08)'; ecuInfoCache = data.ecu_info; ecuPartNum.textContent = data.ecu_info.part_number || 'Unknown'; ecuExtra.textContent = data.ecu_info.extra.join(', ') || 'N/A'; mainContent.classList.remove('hidden'); addLog(`Connected successfully. ECU: ${data.ecu_info.part_number}`, 'data'); // Auto-read faults and group 1 fetchDTCs(); fetchMeasGroup(1); } else { throw new Error(data.error); } } catch (e) { connectBtn.textContent = 'Connect'; connectBtn.disabled = false; addLog(`Connection failed: ${e.message}`, 'error'); alert(`Connection failed: ${e.message}`); } } async function disconnect() { if (liveModeToggle.checked) { liveModeToggle.click(); } stopDashboardRefresh(); dashLiveToggle.checked = false; try { await fetch('/api/disconnect', { method: 'POST' }); } catch(e) {} isConnected = false; connectBtn.textContent = 'Connect'; connectBtn.classList.replace('btn-danger', 'btn-primary'); statusDot.classList.replace('connected', 'disconnected'); statusText.textContent = 'Disconnected'; mainContent.classList.add('hidden'); addLog('Disconnected.', 'system'); } async function fetchDTCs() { if (!isConnected) return; readDTCBtn.disabled = true; readDTCBtn.textContent = 'Reading...'; addLog('Requesting Fault Codes...', 'system'); try { const res = await fetch('/api/dtcs'); const data = await res.json(); if (data.success) { lastDTCs = data.dtcs; renderDTCs(data.dtcs); addLog(`Read ${data.dtcs.length} faults.`, 'data'); } else { throw new Error(data.error); } } catch (e) { addLog(`Error reading DTCs: ${e.message}`, 'error'); } finally { readDTCBtn.disabled = false; readDTCBtn.textContent = 'Read Faults'; } } async function clearDTCs() { if (!isConnected) return; clearDTCBtn.disabled = true; addLog('Sending clear faults command...', 'system'); try { const res = await fetch('/api/dtcs/clear', { method: 'POST' }); const data = await res.json(); if (data.success) { addLog('Faults cleared successfully.', 'data'); setTimeout(fetchDTCs, 1000); } else { throw new Error(data.error); } } catch (e) { addLog(`Error clearing DTCs: ${e.message}`, 'error'); } finally { clearDTCBtn.disabled = false; } } async function fetchMeasGroup(group) { if (!isConnected) return; addLog(`Requesting Measurement Group ${group}...`, 'system'); try { const res = await fetch(`/api/meas/${group}`); const data = await res.json(); if (data.success) { renderMeasurements(data); updateGauges(group, data.measurements); updateDiagram(group, data.measurements); // Cache for export lastMeasurements[group] = { name: data.name, group: group, measurements: data.measurements, timestamp: new Date().toISOString() }; addLog(`Group ${group} read success.`, 'data'); } else { throw new Error(data.error); } } catch (e) { addLog(`Error reading group ${group}: ${e.message}`, 'error'); } } // --- Live Mode (SSE) --- function startLiveMode(group) { if (eventSource) { eventSource.close(); } addLog(`Starting Live Monitor for Group ${group}...`, 'system'); eventSource = new EventSource(`/api/meas/live/${group}`); eventSource.onmessage = (e) => { const data = JSON.parse(e.data); renderMeasurements(data, true); updateGauges(group, data.measurements); updateDiagram(group, data.measurements); }; eventSource.onerror = (e) => { addLog('Live monitor connection error.', 'error'); stopLiveMode(); liveModeToggle.checked = false; }; } async function stopLiveMode() { if (eventSource) { eventSource.close(); eventSource = null; } try { await fetch('/api/meas/live/stop', { method: 'POST' }); } catch(e) {} addLog('Stopped live monitor.', 'system'); } // --- UI Rendering --- function renderDTCs(dtcs) { dtcContainer.innerHTML = ''; if (dtcs.length === 0) { dtcContainer.innerHTML = `
No fault codes stored in module.