app.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866
  1. document.addEventListener('DOMContentLoaded', () => {
  2. // --- State ---
  3. let isConnected = false;
  4. let liveMode = false;
  5. let currentGroup = 1;
  6. let eventSource = null;
  7. let sparklineData = {};
  8. let dashRefreshTimer = null;
  9. // Cached data for export
  10. let lastDTCs = [];
  11. let lastMeasurements = {};
  12. let ecuInfoCache = {};
  13. // --- DOM Elements ---
  14. const portSelect = document.getElementById('portSelect');
  15. const refreshPortsBtn = document.getElementById('refreshPortsBtn');
  16. const connectBtn = document.getElementById('connectBtn');
  17. const statusDot = document.getElementById('statusDot');
  18. const statusText = document.getElementById('statusText');
  19. const mainContent = document.getElementById('mainContent');
  20. const ecuPartNum = document.getElementById('ecuPartNum');
  21. const ecuExtra = document.getElementById('ecuExtra');
  22. const dtcContainer = document.getElementById('dtcContainer');
  23. const readDTCBtn = document.getElementById('readDTCBtn');
  24. const clearDTCBtn = document.getElementById('clearDTCBtn');
  25. const exportDTCBtn = document.getElementById('exportDTCBtn');
  26. const groupPills = document.getElementById('groupPills');
  27. const customGroupInput = document.getElementById('customGroupInput');
  28. const readGroupBtn = document.getElementById('readGroupBtn');
  29. const groupNameLabel = document.getElementById('groupNameLabel');
  30. const measContainer = document.getElementById('measContainer');
  31. const liveModeToggle = document.getElementById('liveModeToggle');
  32. const exportMeasBtn = document.getElementById('exportMeasBtn');
  33. const toggleLogBtn = document.getElementById('toggleLogBtn');
  34. const logContainer = document.getElementById('logContainer');
  35. const confirmModal = document.getElementById('confirmModal');
  36. const cancelModalBtn = document.getElementById('cancelModalBtn');
  37. const confirmClearBtn = document.getElementById('confirmClearBtn');
  38. const dashLiveToggle = document.getElementById('dashLiveToggle');
  39. const exportAllBtn = document.getElementById('exportAllBtn');
  40. // --- Initialization ---
  41. initGroupPills();
  42. fetchPorts();
  43. // --- Event Listeners ---
  44. refreshPortsBtn.addEventListener('click', fetchPorts);
  45. connectBtn.addEventListener('click', () => {
  46. if (isConnected) {
  47. disconnect();
  48. } else {
  49. connect();
  50. }
  51. });
  52. readDTCBtn.addEventListener('click', fetchDTCs);
  53. clearDTCBtn.addEventListener('click', () => {
  54. confirmModal.classList.remove('hidden');
  55. });
  56. cancelModalBtn.addEventListener('click', () => {
  57. confirmModal.classList.add('hidden');
  58. });
  59. confirmClearBtn.addEventListener('click', () => {
  60. confirmModal.classList.add('hidden');
  61. clearDTCs();
  62. });
  63. readGroupBtn.addEventListener('click', () => {
  64. let group = customGroupInput.value ? parseInt(customGroupInput.value) : currentGroup;
  65. if (liveModeToggle.checked) {
  66. startLiveMode(group);
  67. } else {
  68. fetchMeasGroup(group);
  69. }
  70. });
  71. liveModeToggle.addEventListener('change', (e) => {
  72. if (e.target.checked) {
  73. startLiveMode(currentGroup);
  74. } else {
  75. stopLiveMode();
  76. }
  77. });
  78. toggleLogBtn.addEventListener('click', () => {
  79. logContainer.classList.toggle('collapsed');
  80. toggleLogBtn.textContent = logContainer.classList.contains('collapsed') ? '▼' : '▲';
  81. });
  82. // Dashboard auto-refresh toggle
  83. dashLiveToggle.addEventListener('change', (e) => {
  84. if (e.target.checked) {
  85. startDashboardRefresh();
  86. } else {
  87. stopDashboardRefresh();
  88. }
  89. });
  90. // Export buttons
  91. exportDTCBtn.addEventListener('click', exportDTCsToFile);
  92. exportMeasBtn.addEventListener('click', exportMeasToFile);
  93. exportAllBtn.addEventListener('click', exportFullReport);
  94. // --- Gauge Drawing ---
  95. function drawGauge(canvasId, value, min, max, color = '#00d4ff', warnThresh = null, dangerThresh = null) {
  96. const canvas = document.getElementById(canvasId);
  97. if (!canvas) return;
  98. const ctx = canvas.getContext('2d');
  99. const w = canvas.width;
  100. const h = canvas.height;
  101. const cx = w / 2;
  102. const cy = h / 2 + 10;
  103. const r = Math.min(w, h) / 2 - 15;
  104. ctx.clearRect(0, 0, w, h);
  105. // Angle range: from 220° to -40° (sweeping 260°)
  106. const startAngle = (220 * Math.PI) / 180;
  107. const endAngle = (-40 * Math.PI) / 180;
  108. const totalAngle = startAngle - endAngle;
  109. // Background arc
  110. ctx.beginPath();
  111. ctx.arc(cx, cy, r, endAngle, startAngle, false);
  112. ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)';
  113. ctx.lineWidth = 10;
  114. ctx.lineCap = 'round';
  115. ctx.stroke();
  116. // Value arc
  117. if (typeof value === 'number' && !isNaN(value)) {
  118. const clamped = Math.max(min, Math.min(max, value));
  119. const pct = (clamped - min) / (max - min);
  120. const valAngle = startAngle - pct * totalAngle;
  121. // Determine color based on thresholds
  122. let arcColor = color;
  123. if (dangerThresh !== null && value >= dangerThresh) {
  124. arcColor = '#ff3366';
  125. } else if (warnThresh !== null && value >= warnThresh) {
  126. arcColor = '#ffa502';
  127. }
  128. ctx.beginPath();
  129. ctx.arc(cx, cy, r, startAngle, valAngle, true);
  130. ctx.strokeStyle = arcColor;
  131. ctx.lineWidth = 10;
  132. ctx.lineCap = 'round';
  133. ctx.stroke();
  134. // Glow effect
  135. ctx.beginPath();
  136. ctx.arc(cx, cy, r, startAngle, valAngle, true);
  137. ctx.strokeStyle = arcColor;
  138. ctx.lineWidth = 10;
  139. ctx.lineCap = 'round';
  140. ctx.shadowColor = arcColor;
  141. ctx.shadowBlur = 12;
  142. ctx.stroke();
  143. ctx.shadowBlur = 0;
  144. // Min/Max labels
  145. ctx.font = '10px Outfit, sans-serif';
  146. ctx.fillStyle = 'rgba(255,255,255,0.3)';
  147. ctx.textAlign = 'center';
  148. const minLabelX = cx + (r + 12) * Math.cos(startAngle);
  149. const minLabelY = cy + (r + 12) * Math.sin(startAngle);
  150. ctx.fillText(min, minLabelX, minLabelY);
  151. const maxLabelX = cx + (r + 12) * Math.cos(endAngle);
  152. const maxLabelY = cy + (r + 12) * Math.sin(endAngle);
  153. ctx.fillText(max, maxLabelX, maxLabelY);
  154. }
  155. }
  156. function updateGauges(group, measurements) {
  157. if (!measurements || measurements.length < 1) return;
  158. if (group === 1) {
  159. // Group 1: Compressor & Blower Status
  160. // Field 0: Compressor Shut-off Code
  161. if (measurements[0]) {
  162. const compCode = measurements[0].value;
  163. const compRing = document.querySelector('.compressor-ring');
  164. const compValEl = document.getElementById('gaugeValCompressor');
  165. // Compressor is typically ON if shut-off code is 0
  166. const isOn = (compCode === 0);
  167. compRing.className = `compressor-ring ${isOn ? 'on' : 'off'}`;
  168. compValEl.textContent = isOn ? 'ON' : 'OFF';
  169. compValEl.style.color = isOn ? 'var(--accent-green)' : 'var(--text-muted)';
  170. }
  171. // Field 1: Target Blower Voltage
  172. if (measurements[1]) {
  173. const blowerVal = measurements[1].value;
  174. document.getElementById('gaugeValBlower').textContent = typeof blowerVal === 'number' ? blowerVal.toFixed(1) : blowerVal;
  175. drawGauge('gaugeCanvasBlower', blowerVal, 0, 15, '#00d4ff');
  176. }
  177. // Field 2: System Voltage
  178. if (measurements[2]) {
  179. const sysV = measurements[2].value;
  180. document.getElementById('gaugeValSysV').textContent = typeof sysV === 'number' ? sysV.toFixed(1) : sysV;
  181. drawGauge('gaugeCanvasSysV', sysV, 10, 16, '#00e676', null, null);
  182. }
  183. }
  184. if (group === 2) {
  185. // Group 2: Temperature Sensors (Raw)
  186. // Field 0: Interior Sensor (G56)
  187. if (measurements[0]) {
  188. const intTemp = measurements[0].value;
  189. document.getElementById('gaugeValInterior').textContent = typeof intTemp === 'number' ? intTemp.toFixed(1) : intTemp;
  190. // Using generic 0-255 range for ADC counts since we don't have the exact formula mapping yet
  191. drawGauge('gaugeCanvasInterior', intTemp, 0, 255, '#00d4ff', 200, 240);
  192. }
  193. }
  194. if (group === 3) {
  195. // Group 3: External Sensors (Raw)
  196. // Field 0: Outside Air Sensor (G17)
  197. if (measurements[0]) {
  198. const outTemp = measurements[0].value;
  199. document.getElementById('gaugeValOutside').textContent = typeof outTemp === 'number' ? outTemp.toFixed(1) : outTemp;
  200. drawGauge('gaugeCanvasOutside', outTemp, 0, 255, '#00d4ff', 200, 240);
  201. }
  202. // Field 1: Evaporator Sensor (G263)
  203. if (measurements[1]) {
  204. const evapTemp = measurements[1].value;
  205. document.getElementById('gaugeValEvap').textContent = typeof evapTemp === 'number' ? evapTemp.toFixed(1) : evapTemp;
  206. drawGauge('gaugeCanvasEvap', evapTemp, 0, 255, '#33ddff', 200, 240);
  207. }
  208. }
  209. }
  210. // --- Car Diagram Updates ---
  211. function updateDiagram(group, measurements) {
  212. if (!measurements || measurements.length < 1) return;
  213. if (group === 2) {
  214. // Group 2: Interior Sensor (G56) ADC, Dash Sensor (G86) ADC, Fresh Air Sensor (G89) ADC, Set Temperature
  215. updateSensorDot('sensorG56', measurements[0], 'diag_G56', '°C');
  216. updateSensorDot('sensorG86', measurements[1], 'diag_G86', '°C');
  217. updateSensorDot('sensorG89', measurements[2], 'diag_G89', '°C');
  218. }
  219. if (group === 3) {
  220. // Group 3: Outside Air (G17) ADC, Evaporator (G263) ADC, Coolant Temp ADC, Sunlight (G107) ADC
  221. updateSensorDot('sensorG17', measurements[0], 'diag_G17', '°C');
  222. updateSensorDot('sensorG263', measurements[1], 'diag_G263', '°C');
  223. updateSensorDot('sensorCoolant', measurements[2], 'diag_Coolant', '°C');
  224. if (measurements[3]) {
  225. updateSensorDot('sensorG107', measurements[3], 'diag_G107', '');
  226. }
  227. }
  228. if (group === 8) {
  229. // Group 8: Sensor Voltages - Field 0 is G65 High Pressure
  230. if (measurements[0]) {
  231. updateSensorDot('sensorG65', measurements[0], 'diag_G65', ' V');
  232. }
  233. }
  234. }
  235. function updateSensorDot(groupId, meas, textId, suffix) {
  236. if (!meas) return;
  237. const textEl = document.getElementById(textId);
  238. const groupEl = document.getElementById(groupId);
  239. if (!textEl || !groupEl) return;
  240. const val = meas.value;
  241. const displayVal = typeof val === 'number' ? val.toFixed(1) : val;
  242. textEl.textContent = `${displayVal}${suffix}`;
  243. // Determine sensor health from value
  244. const circles = groupEl.querySelectorAll('circle');
  245. const outerCircle = circles[0];
  246. const innerCircle = circles[1];
  247. const rect = groupEl.querySelector('rect');
  248. if (typeof val === 'number') {
  249. if (val <= -40 || val >= 220) {
  250. // Fault / Open circuit
  251. setSensorColor(outerCircle, innerCircle, rect, '#ff3366', 'glow-fault');
  252. outerCircle.style.animation = 'sensor-pulse-fault 1.5s ease-in-out infinite';
  253. } else if (val >= 100 || val <= -20) {
  254. // Warning range
  255. setSensorColor(outerCircle, innerCircle, rect, '#ffa502', 'glow-ok');
  256. outerCircle.style.animation = '';
  257. } else {
  258. // Normal
  259. setSensorColor(outerCircle, innerCircle, rect, '#00d4ff', 'glow-ok');
  260. outerCircle.style.animation = '';
  261. }
  262. }
  263. }
  264. function setSensorColor(outerCircle, innerCircle, rect, color, filterName) {
  265. const colorMap = {
  266. '#ff3366': { fill: 'rgba(255,51,102,0.15)', stroke: 'rgba(255,51,102,0.3)' },
  267. '#ffa502': { fill: 'rgba(255,165,2,0.15)', stroke: 'rgba(255,165,2,0.3)' },
  268. '#00d4ff': { fill: 'rgba(0,212,255,0.15)', stroke: 'rgba(0,212,255,0.3)' },
  269. '#00e676': { fill: 'rgba(0,230,118,0.15)', stroke: 'rgba(0,230,118,0.3)' }
  270. };
  271. const mapped = colorMap[color] || { fill: 'rgba(0,212,255,0.15)', stroke: 'rgba(0,212,255,0.3)' };
  272. outerCircle.setAttribute('stroke', color);
  273. outerCircle.setAttribute('fill', mapped.fill);
  274. innerCircle.setAttribute('fill', color);
  275. outerCircle.setAttribute('filter', `url(#${filterName})`);
  276. if (rect) {
  277. rect.setAttribute('stroke', mapped.stroke);
  278. }
  279. }
  280. // Dashboard auto-refresh: cycles through groups 1, 2, 3, 8 to populate gauges + diagram
  281. let dashGroupCycle = [1, 2, 3, 8];
  282. let dashCycleIdx = 0;
  283. function startDashboardRefresh() {
  284. if (dashRefreshTimer) clearInterval(dashRefreshTimer);
  285. dashCycleIdx = 0;
  286. addLog('Dashboard auto-refresh started.', 'system');
  287. // Immediately fetch first group
  288. fetchDashGroup();
  289. dashRefreshTimer = setInterval(() => {
  290. if (!isConnected) {
  291. stopDashboardRefresh();
  292. return;
  293. }
  294. fetchDashGroup();
  295. }, 2000);
  296. }
  297. function stopDashboardRefresh() {
  298. if (dashRefreshTimer) {
  299. clearInterval(dashRefreshTimer);
  300. dashRefreshTimer = null;
  301. }
  302. addLog('Dashboard auto-refresh stopped.', 'system');
  303. }
  304. async function fetchDashGroup() {
  305. const group = dashGroupCycle[dashCycleIdx % dashGroupCycle.length];
  306. dashCycleIdx++;
  307. try {
  308. const res = await fetch(`/api/meas/${group}`);
  309. const data = await res.json();
  310. if (data.success) {
  311. updateGauges(group, data.measurements);
  312. updateDiagram(group, data.measurements);
  313. // Cache for export
  314. lastMeasurements[group] = {
  315. name: data.name,
  316. group: group,
  317. measurements: data.measurements,
  318. timestamp: new Date().toISOString()
  319. };
  320. }
  321. } catch (e) {
  322. // Silently fail on dash refresh
  323. }
  324. }
  325. // --- Export Functions ---
  326. function exportDTCsToFile() {
  327. if (lastDTCs.length === 0) {
  328. alert('No fault codes to export. Read faults first.');
  329. return;
  330. }
  331. const report = {
  332. export_type: 'Fault Codes',
  333. vehicle: 'Audi A4 B5 (8D2)',
  334. module: '08 - Auto HVAC',
  335. ecu_info: ecuInfoCache,
  336. timestamp: new Date().toISOString(),
  337. fault_codes: lastDTCs
  338. };
  339. downloadJSON(report, `audi_hvac_dtcs_${dateStamp()}.json`);
  340. addLog(`Exported ${lastDTCs.length} fault codes.`, 'data');
  341. }
  342. function exportMeasToFile() {
  343. if (Object.keys(lastMeasurements).length === 0) {
  344. alert('No measurement data to export. Read some groups first.');
  345. return;
  346. }
  347. const report = {
  348. export_type: 'Measurement Blocks',
  349. vehicle: 'Audi A4 B5 (8D2)',
  350. module: '08 - Auto HVAC',
  351. ecu_info: ecuInfoCache,
  352. timestamp: new Date().toISOString(),
  353. measurement_blocks: lastMeasurements
  354. };
  355. downloadJSON(report, `audi_hvac_meas_${dateStamp()}.json`);
  356. addLog('Exported measurement blocks.', 'data');
  357. }
  358. async function exportFullReport() {
  359. addLog('Generating full diagnostic report...', 'system');
  360. // Read fresh DTCs
  361. try {
  362. const dtcRes = await fetch('/api/dtcs');
  363. const dtcData = await dtcRes.json();
  364. if (dtcData.success) {
  365. lastDTCs = dtcData.dtcs;
  366. renderDTCs(lastDTCs);
  367. }
  368. } catch (e) {}
  369. // Read all 8 known groups
  370. for (let g = 1; g <= 8; g++) {
  371. try {
  372. const res = await fetch(`/api/meas/${g}`);
  373. const data = await res.json();
  374. if (data.success) {
  375. lastMeasurements[g] = {
  376. name: data.name,
  377. group: g,
  378. measurements: data.measurements,
  379. timestamp: new Date().toISOString()
  380. };
  381. updateGauges(g, data.measurements);
  382. updateDiagram(g, data.measurements);
  383. }
  384. } catch (e) {}
  385. }
  386. const report = {
  387. export_type: 'Full Diagnostic Report',
  388. vehicle: 'Audi A4 B5 (8D2)',
  389. module: '08 - Auto HVAC',
  390. ecu_info: ecuInfoCache,
  391. generated_at: new Date().toISOString(),
  392. fault_codes: lastDTCs,
  393. measurement_blocks: lastMeasurements,
  394. 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.'
  395. };
  396. downloadJSON(report, `audi_hvac_full_report_${dateStamp()}.json`);
  397. addLog('Full diagnostic report exported.', 'data');
  398. }
  399. function downloadJSON(obj, filename) {
  400. const blob = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' });
  401. const url = URL.createObjectURL(blob);
  402. const a = document.createElement('a');
  403. a.href = url;
  404. a.download = filename;
  405. a.click();
  406. URL.revokeObjectURL(url);
  407. }
  408. function dateStamp() {
  409. const d = new Date();
  410. 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')}`;
  411. }
  412. // --- API Functions ---
  413. async function fetchPorts() {
  414. try {
  415. portSelect.innerHTML = '<option value="">Scanning...</option>';
  416. const res = await fetch('/api/ports');
  417. const data = await res.json();
  418. portSelect.innerHTML = '';
  419. if (data.ports.length === 0) {
  420. portSelect.innerHTML = '<option value="">No ports found</option>';
  421. } else {
  422. data.ports.forEach(p => {
  423. const opt = document.createElement('option');
  424. opt.value = p.device;
  425. opt.textContent = `${p.device} ${p.is_likely ? '(KKL)' : ''}`;
  426. portSelect.appendChild(opt);
  427. });
  428. if (data.auto) {
  429. portSelect.value = data.auto;
  430. }
  431. }
  432. } catch (e) {
  433. addLog(`Error fetching ports: ${e}`, 'error');
  434. }
  435. }
  436. async function connect() {
  437. const port = portSelect.value;
  438. if (!port) {
  439. alert('Please select a port');
  440. return;
  441. }
  442. connectBtn.textContent = 'Connecting...';
  443. connectBtn.disabled = true;
  444. addLog(`Connecting to ${port} (Address 0x08)...`, 'system');
  445. try {
  446. const res = await fetch('/api/connect', {
  447. method: 'POST',
  448. headers: { 'Content-Type': 'application/json' },
  449. body: JSON.stringify({ port })
  450. });
  451. const data = await res.json();
  452. if (data.success) {
  453. isConnected = true;
  454. connectBtn.textContent = 'Disconnect';
  455. connectBtn.disabled = false;
  456. connectBtn.classList.replace('btn-primary', 'btn-danger');
  457. statusDot.classList.replace('disconnected', 'connected');
  458. statusText.textContent = 'Connected (0x08)';
  459. ecuInfoCache = data.ecu_info;
  460. ecuPartNum.textContent = data.ecu_info.part_number || 'Unknown';
  461. ecuExtra.textContent = data.ecu_info.extra.join(', ') || 'N/A';
  462. mainContent.classList.remove('hidden');
  463. addLog(`Connected successfully. ECU: ${data.ecu_info.part_number}`, 'data');
  464. // Auto-read faults and group 1
  465. fetchDTCs();
  466. fetchMeasGroup(1);
  467. } else {
  468. throw new Error(data.error);
  469. }
  470. } catch (e) {
  471. connectBtn.textContent = 'Connect';
  472. connectBtn.disabled = false;
  473. addLog(`Connection failed: ${e.message}`, 'error');
  474. alert(`Connection failed: ${e.message}`);
  475. }
  476. }
  477. async function disconnect() {
  478. if (liveModeToggle.checked) {
  479. liveModeToggle.click();
  480. }
  481. stopDashboardRefresh();
  482. dashLiveToggle.checked = false;
  483. try {
  484. await fetch('/api/disconnect', { method: 'POST' });
  485. } catch(e) {}
  486. isConnected = false;
  487. connectBtn.textContent = 'Connect';
  488. connectBtn.classList.replace('btn-danger', 'btn-primary');
  489. statusDot.classList.replace('connected', 'disconnected');
  490. statusText.textContent = 'Disconnected';
  491. mainContent.classList.add('hidden');
  492. addLog('Disconnected.', 'system');
  493. }
  494. async function fetchDTCs() {
  495. if (!isConnected) return;
  496. readDTCBtn.disabled = true;
  497. readDTCBtn.textContent = 'Reading...';
  498. addLog('Requesting Fault Codes...', 'system');
  499. try {
  500. const res = await fetch('/api/dtcs');
  501. const data = await res.json();
  502. if (data.success) {
  503. lastDTCs = data.dtcs;
  504. renderDTCs(data.dtcs);
  505. addLog(`Read ${data.dtcs.length} faults.`, 'data');
  506. } else {
  507. throw new Error(data.error);
  508. }
  509. } catch (e) {
  510. addLog(`Error reading DTCs: ${e.message}`, 'error');
  511. } finally {
  512. readDTCBtn.disabled = false;
  513. readDTCBtn.textContent = 'Read Faults';
  514. }
  515. }
  516. async function clearDTCs() {
  517. if (!isConnected) return;
  518. clearDTCBtn.disabled = true;
  519. addLog('Sending clear faults command...', 'system');
  520. try {
  521. const res = await fetch('/api/dtcs/clear', { method: 'POST' });
  522. const data = await res.json();
  523. if (data.success) {
  524. addLog('Faults cleared successfully.', 'data');
  525. setTimeout(fetchDTCs, 1000);
  526. } else {
  527. throw new Error(data.error);
  528. }
  529. } catch (e) {
  530. addLog(`Error clearing DTCs: ${e.message}`, 'error');
  531. } finally {
  532. clearDTCBtn.disabled = false;
  533. }
  534. }
  535. async function fetchMeasGroup(group) {
  536. if (!isConnected) return;
  537. addLog(`Requesting Measurement Group ${group}...`, 'system');
  538. try {
  539. const res = await fetch(`/api/meas/${group}`);
  540. const data = await res.json();
  541. if (data.success) {
  542. renderMeasurements(data);
  543. updateGauges(group, data.measurements);
  544. updateDiagram(group, data.measurements);
  545. // Cache for export
  546. lastMeasurements[group] = {
  547. name: data.name,
  548. group: group,
  549. measurements: data.measurements,
  550. timestamp: new Date().toISOString()
  551. };
  552. addLog(`Group ${group} read success.`, 'data');
  553. } else {
  554. throw new Error(data.error);
  555. }
  556. } catch (e) {
  557. addLog(`Error reading group ${group}: ${e.message}`, 'error');
  558. }
  559. }
  560. // --- Live Mode (SSE) ---
  561. function startLiveMode(group) {
  562. if (eventSource) {
  563. eventSource.close();
  564. }
  565. addLog(`Starting Live Monitor for Group ${group}...`, 'system');
  566. eventSource = new EventSource(`/api/meas/live/${group}`);
  567. eventSource.onmessage = (e) => {
  568. const data = JSON.parse(e.data);
  569. renderMeasurements(data, true);
  570. updateGauges(group, data.measurements);
  571. updateDiagram(group, data.measurements);
  572. };
  573. eventSource.onerror = (e) => {
  574. addLog('Live monitor connection error.', 'error');
  575. stopLiveMode();
  576. liveModeToggle.checked = false;
  577. };
  578. }
  579. async function stopLiveMode() {
  580. if (eventSource) {
  581. eventSource.close();
  582. eventSource = null;
  583. }
  584. try {
  585. await fetch('/api/meas/live/stop', { method: 'POST' });
  586. } catch(e) {}
  587. addLog('Stopped live monitor.', 'system');
  588. }
  589. // --- UI Rendering ---
  590. function renderDTCs(dtcs) {
  591. dtcContainer.innerHTML = '';
  592. if (dtcs.length === 0) {
  593. dtcContainer.innerHTML = `
  594. <div class="empty-state success">
  595. <div class="icon">✓</div>
  596. <p>No fault codes stored in module.</p>
  597. </div>
  598. `;
  599. return;
  600. }
  601. dtcs.forEach(dtc => {
  602. const card = document.createElement('div');
  603. card.className = 'dtc-card';
  604. const badgeClass = dtc.status.toLowerCase();
  605. card.innerHTML = `
  606. <div class="dtc-header">
  607. <span class="dtc-code">${dtc.code}</span>
  608. <span class="dtc-badge ${badgeClass}">${dtc.status}</span>
  609. </div>
  610. <div class="dtc-desc">${dtc.description}</div>
  611. <div class="dtc-details">Elaboration: ${dtc.details} (${dtc.elaboration_hex})</div>
  612. `;
  613. dtcContainer.appendChild(card);
  614. });
  615. }
  616. function initGroupPills() {
  617. for(let i=1; i<=8; i++) {
  618. const pill = document.createElement('div');
  619. pill.className = `pill ${i === 1 ? 'active' : ''}`;
  620. pill.textContent = i;
  621. pill.dataset.group = i;
  622. pill.addEventListener('click', () => {
  623. document.querySelectorAll('.pill').forEach(p => p.classList.remove('active'));
  624. pill.classList.add('active');
  625. currentGroup = i;
  626. customGroupInput.value = '';
  627. if (liveModeToggle.checked) {
  628. startLiveMode(i);
  629. } else {
  630. fetchMeasGroup(i);
  631. }
  632. });
  633. groupPills.appendChild(pill);
  634. }
  635. customGroupInput.addEventListener('change', (e) => {
  636. const val = parseInt(e.target.value);
  637. if (val > 0 && val <= 255) {
  638. document.querySelectorAll('.pill').forEach(p => p.classList.remove('active'));
  639. currentGroup = val;
  640. }
  641. });
  642. }
  643. function renderMeasurements(data, isLive = false) {
  644. if (!data.measurements) return;
  645. groupNameLabel.textContent = data.name || `Group ${data.group}`;
  646. if (!isLive || measContainer.children.length !== 4) {
  647. measContainer.innerHTML = '';
  648. sparklineData = {};
  649. for(let i=0; i<4; i++) {
  650. const card = document.createElement('div');
  651. card.className = 'meas-card';
  652. card.id = `measCard_${i}`;
  653. const canvas = document.createElement('canvas');
  654. canvas.className = 'meas-sparkline';
  655. canvas.id = `sparkline_${i}`;
  656. card.innerHTML = `
  657. <div class="meas-label" id="measLabel_${i}">---</div>
  658. <div class="meas-value-area">
  659. <span class="meas-val" id="measVal_${i}">--</span>
  660. <span class="meas-unit" id="measUnit_${i}"></span>
  661. </div>
  662. `;
  663. card.appendChild(canvas);
  664. measContainer.appendChild(card);
  665. sparklineData[i] = [];
  666. }
  667. }
  668. data.measurements.forEach((meas, i) => {
  669. if (i >= 4) return;
  670. const card = document.getElementById(`measCard_${i}`);
  671. const valEl = document.getElementById(`measVal_${i}`);
  672. document.getElementById(`measLabel_${i}`).textContent = meas.label;
  673. document.getElementById(`measUnit_${i}`).textContent = meas.unit;
  674. if (valEl.textContent != meas.value) {
  675. valEl.textContent = meas.value;
  676. card.classList.add('updated');
  677. setTimeout(() => card.classList.remove('updated'), 200);
  678. }
  679. if (isLive && typeof meas.value === 'number') {
  680. updateSparkline(i, meas.value);
  681. }
  682. });
  683. }
  684. function updateSparkline(idx, val) {
  685. const arr = sparklineData[idx];
  686. arr.push(val);
  687. if (arr.length > 30) arr.shift();
  688. const canvas = document.getElementById(`sparkline_${idx}`);
  689. if (!canvas) return;
  690. canvas.width = canvas.parentElement.clientWidth;
  691. canvas.height = 40;
  692. const ctx = canvas.getContext('2d');
  693. const w = canvas.width;
  694. const h = canvas.height;
  695. ctx.clearRect(0, 0, w, h);
  696. if (arr.length < 2) return;
  697. const max = Math.max(...arr);
  698. const min = Math.min(...arr);
  699. const range = (max - min) || 1;
  700. const step = w / (30 - 1);
  701. ctx.beginPath();
  702. ctx.moveTo(0, h - ((arr[0] - min) / range) * (h - 5) - 2);
  703. for (let i = 1; i < arr.length; i++) {
  704. const x = i * step;
  705. const y = h - ((arr[i] - min) / range) * (h - 5) - 2;
  706. ctx.lineTo(x, y);
  707. }
  708. ctx.strokeStyle = '#00d4ff';
  709. ctx.lineWidth = 2;
  710. ctx.stroke();
  711. ctx.lineTo(w, h);
  712. ctx.lineTo(0, h);
  713. ctx.closePath();
  714. const gradient = ctx.createLinearGradient(0, 0, 0, h);
  715. gradient.addColorStop(0, 'rgba(0, 212, 255, 0.2)');
  716. gradient.addColorStop(1, 'rgba(0, 212, 255, 0)');
  717. ctx.fillStyle = gradient;
  718. ctx.fill();
  719. }
  720. function addLog(msg, type = 'system') {
  721. const line = document.createElement('div');
  722. line.className = `log-line ${type}`;
  723. const now = new Date();
  724. const time = `${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}:${now.getSeconds().toString().padStart(2,'0')}`;
  725. line.textContent = `[${time}] ${msg}`;
  726. logContainer.appendChild(line);
  727. if (logContainer.children.length > 100) {
  728. logContainer.removeChild(logContainer.children[0]);
  729. }
  730. logContainer.scrollTop = logContainer.scrollHeight;
  731. }
  732. // Draw initial empty gauges
  733. drawGauge('gaugeCanvasInterior', null, -10, 60, '#00d4ff');
  734. drawGauge('gaugeCanvasOutside', null, -30, 60, '#00d4ff');
  735. drawGauge('gaugeCanvasEvap', null, -10, 50, '#33ddff');
  736. drawGauge('gaugeCanvasBlower', null, 0, 14, '#00d4ff');
  737. drawGauge('gaugeCanvasSysV', null, 10, 16, '#00e676');
  738. });