ka-obd-lab / static /app.js
leobora's picture
Final v1: demo mode, connection status, NaN handling
9bd3251 verified
Raw
History Blame Contribute Delete
8.48 kB
let ws = null;
let plotlyInitialized = false;
let plotlyDiv = null;
const MAX_POINTS = 200;
let connectionState = "disconnected"; // disconnected, connecting, connected, error
let lastDataTime = 0;
let dataTimeout = null;
function getConfig() {
const mode = document.getElementById("mode-select").value;
const comPort = document.getElementById("com-port").value;
const wifiAddr = document.getElementById("wifi-addr").value;
const parts = wifiAddr.split(":");
return { mode, comPort, wifiHost: parts[0], wifiPort: parts[1] || "35000" };
}
function setConnectionState(state, message) {
connectionState = state;
const dot = document.getElementById("status-dot");
const text = document.getElementById("status-text");
const badge = document.getElementById("connection-badge");
dot.className = "status-dot " + state;
text.textContent = message || state.charAt(0).toUpperCase() + state.slice(1);
badge.className = "connection-badge " + state;
badge.textContent = state === "connected" ? "ONLINE" :
state === "connecting" ? "CONNECTING" :
state === "error" ? "ERROR" : "OFFLINE";
}
function checkDataTimeout() {
if (connectionState === "connected" && Date.now() - lastDataTime > 3000) {
setConnectionState("error", "Car disconnected");
document.querySelectorAll(".card").forEach(card => {
card.classList.add("warning");
});
}
}
function connectWebSocket() {
const config = getConfig();
const host = window.location.host;
const wsUrl = "ws://" + host + "/ws";
if (ws) {
try {
ws.close();
} catch (e) {}
ws = null;
}
setConnectionState("connecting", "Connecting to " + config.mode + "...");
// Reset card warnings
document.querySelectorAll(".card").forEach(card => {
card.classList.remove("warning", "error");
});
ws = new WebSocket(wsUrl);
ws.onopen = function () {
const payload = { mode: config.mode };
if (config.mode === "bluetooth") {
payload.port = config.comPort;
} else if (config.mode === "wifi") {
payload.host = config.wifiHost;
payload.port = parseInt(config.wifiPort, 10) || 35000;
}
ws.send(JSON.stringify(payload));
setConnectionState("connected", "Connected via " + config.mode);
lastDataTime = Date.now();
// Start timeout checker
if (dataTimeout) clearInterval(dataTimeout);
dataTimeout = setInterval(checkDataTimeout, 1000);
};
ws.onmessage = function (event) {
try {
const data = JSON.parse(event.data);
if (data.error) {
setConnectionState("error", data.error);
return;
}
lastDataTime = Date.now();
updateDashboard(data);
} catch (e) {
console.error("Failed to parse message:", e);
}
};
ws.onclose = function () {
setConnectionState("disconnected", "Disconnected");
if (dataTimeout) clearInterval(dataTimeout);
};
ws.onerror = function (err) {
console.error("WebSocket error:", err);
setConnectionState("error", "Connection failed");
};
}
function updateDashboard(data) {
// Gauges - handle NaN
updateGauge("gauge-rpm-text", data.rpm != null ? Math.round(data.rpm) : "--");
updateGauge("gauge-speed-text", data.speed != null ? Math.round(data.speed) : "--");
updateGauge("gauge-coolant-text", data.coolant_temp != null ? Math.round(data.coolant_temp) : "--");
updateGaugeArc("gauge-rpm-arc", data.rpm || 0, 8000);
updateGaugeArc("gauge-speed-arc", data.speed || 0, 200);
updateGaugeArc("gauge-coolant-arc", data.coolant_temp || 0, 120);
// Cards - handle NaN with visual feedback
updateCardWithStatus("val-throttle", "card-throttle", data.throttle_position, "%");
updateCardWithStatus("val-engine-load", "card-engine-load", data.engine_load, "%");
updateCardWithStatus("val-intake-air", "card-intake-air", data.intake_air_temp, "°C");
updateCardWithStatus("val-maf", "card-maf", data.maf, " g/s");
updateCardWithStatus("val-fuel", "card-fuel", data.fuel_level, "%");
updateCardWithStatus("val-battery", "card-battery", data.battery_voltage, " V");
addPlotlyTrace(
data.timestamp || Date.now(),
data.rpm,
data.speed,
data.coolant_temp,
data.throttle_position,
data.engine_load
);
}
addPlotlyTrace(
data.timestamp || Date.now(),
data.rpm,
data.speed,
data.coolant_temp,
data.throttle_position,
data.engine_load
);
}
function updateGauge(id, value) {
const el = document.getElementById(id);
if (!el) return;
el.textContent = value;
}
function updateGaugeArc(id, value, max) {
const el = document.getElementById(id);
if (!el) return;
const pct = Math.min(1, Math.max(0, value / max));
const total = 502.65;
el.setAttribute("stroke-dashoffset", total * (1 - pct));
}
function updateCard(id, value) {
const el = document.getElementById(id);
if (!el) return;
if (typeof value === 'string' && value.includes('Error')) {
el.textContent = "--";
} else {
el.textContent = value;
}
}
function updateCardWithStatus(valueId, cardId, value, unit) {
const el = document.getElementById(valueId);
const card = document.getElementById(cardId);
if (!el || !card) return;
card.classList.remove("warning", "error");
if (value == null || isNaN(value)) {
el.textContent = "--";
el.classList.add("na");
card.classList.add("warning");
} else {
el.textContent = typeof value === 'number' ?
(Number.isInteger(value) ? value : value.toFixed(1)) + unit :
value + unit;
el.classList.remove("na");
}
}
function initPlotly() {
const traces = [
{ x: [], y: [], name: "rpm", type: "scatter", mode: "lines" },
{ x: [], y: [], name: "speed", type: "scatter", mode: "lines" },
{ x: [], y: [], name: "coolant_temp", type: "scatter", mode: "lines" },
{ x: [], y: [], name: "throttle_position", type: "scatter", mode: "lines" },
{ x: [], y: [], name: "engine_load", type: "scatter", mode: "lines" },
];
const layout = {
paper_bgcolor: "#1a1a2e",
plot_bgcolor: "#1a1a2e",
font: { color: "#e0e0e0" },
xaxis: {
title: "Time",
color: "#e0e0e0",
gridcolor: "#333",
},
yaxis: {
title: "Value",
color: "#e0e0e0",
gridcolor: "#333",
},
legend: {
font: { color: "#e0e0e0" },
},
margin: { l: 50, r: 20, t: 40, b: 40 },
};
const config = {
responsive: true,
displayModeBar: false,
};
plotlyDiv = document.getElementById("chart");
if (!plotlyDiv) return;
Plotly.newPlot(plotlyDiv, traces, layout, config);
plotlyInitialized = true;
}
function addPlotlyTrace(timestamp, rpm, speed, coolant, throttle, load) {
if (!plotlyInitialized || !plotlyDiv) return;
const timeStr = new Date(timestamp).toLocaleTimeString();
Plotly.extendTraces(plotlyDiv, {
x: [[timeStr], [timeStr], [timeStr], [timeStr], [timeStr]],
y: [[rpm], [speed], [coolant], [throttle], [load]],
}, [0, 1, 2, 3, 4]);
const traceCount = plotlyDiv.data.length;
const currentLen = plotlyDiv.data[0].x.length;
if (currentLen > MAX_POINTS) {
Plotly.relayout(plotlyDiv, {
xaxis: {
range: [currentLen - MAX_POINTS, currentLen],
},
});
}
}
function retryConnection() {
if (ws) {
try {
ws.close();
} catch (e) {}
ws = null;
}
setTimeout(function () {
connectWebSocket();
}, 1000);
}
function init() {
initPlotly();
setConnectionState("disconnected");
const connectBtn = document.getElementById("connect-btn");
if (connectBtn) {
connectBtn.addEventListener("click", connectWebSocket);
}
const retryBtn = document.getElementById("retry-btn");
if (retryBtn) {
retryBtn.addEventListener("click", retryConnection);
}
}
window.addEventListener("load", init);