Files
yaris-obd/static/js/yaris.js
T
Mathieu MoranandCopilot 11f07dec38 fix(geocode): accurate street name + always shows on dashboard
- Cache key uses 4 decimal places (~11m) instead of 3 (~111m) for correct street
- Dashboard geocodes on first GPS fix, then again every >50m moved (throttled 15s)
- Removed stale console.log

Co-authored-by: Copilot <[email protected]>
2026-08-10 03:21:37 -04:00

337 lines
12 KiB
JavaScript

const socket = io();
// ── Map ────────────────────────────────────────────────────────────────────
var map = L.map('map', { minZoom: 8, maxZoom: 15 }).setView([48.505, -0.09], 12);
const mapTiles = L.tileLayer('/static/vendor/tiles/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.maptiler.com/copyright/" target="_blank">MapTiler</a> &copy; <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a> contributors',
minZoom: 8,
maxZoom: 15,
});
var carMarker = L.marker(map.getCenter()).addTo(map).bindPopup('Your Yaris');
// ── Speed history chart ────────────────────────────────────────────────────
const MAX_SPEED_POINTS = 60; // ~5 min at typical OBD poll rate
const ctx = document.getElementById('speedGraph').getContext('2d');
// Gradient fill (created after canvas is sized)
function makeGradient() {
const grad = ctx.createLinearGradient(0, 0, 0, ctx.canvas.clientHeight || 200);
grad.addColorStop(0, 'rgba(78,115,223,0.35)');
grad.addColorStop(1, 'rgba(78,115,223,0.02)');
return grad;
}
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Speed (km/h)',
data: [],
borderColor: '#4e73df',
backgroundColor: makeGradient(),
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: '#4e73df',
tension: 0.4,
fill: true,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 200 },
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function (ctx) {
return ctx.parsed.y != null ? ctx.parsed.y + ' km/h' : '';
}
}
}
},
scales: {
x: {
grid: { display: false },
ticks: {
maxTicksLimit: 6,
color: '#adb5bd',
font: { size: 10 },
maxRotation: 0,
}
},
y: {
beginAtZero: true,
suggestedMax: 60,
grid: { color: 'rgba(0,0,0,.05)' },
ticks: {
color: '#adb5bd',
font: { size: 10 },
callback: function (v) { return v + ' km/h'; },
maxTicksLimit: 5,
}
}
}
}
});
// Session stats
let sessionMaxSpeed = 0;
let sessionSpeedSum = 0;
let sessionSpeedCount = 0;
function updateSpeedStats(speed) {
if (speed == null || typeof speed !== 'number') return;
// Update session max
if (speed > sessionMaxSpeed) {
sessionMaxSpeed = speed;
const maxEl = document.getElementById('speed-max-badge');
if (maxEl) maxEl.textContent = speed.toFixed(0) + ' km/h';
}
// Update session avg
sessionSpeedSum += speed;
sessionSpeedCount++;
const avgEl = document.getElementById('speed-avg-badge');
if (avgEl) avgEl.textContent = (sessionSpeedSum / sessionSpeedCount).toFixed(1) + ' km/h';
// Points count
const ptEl = document.getElementById('speed-points-badge');
if (ptEl) ptEl.textContent = sessionSpeedCount;
}
// ── WebSocket events ──────────────────────────────────────────────────────
socket.on('connect', function () {
console.log('socket.io connected');
});
socket.on('obd_status', function (data) {
const el = document.getElementById('obd-status');
if (el) el.innerHTML = data;
});
socket.on('obd_speed', function (data) {
const statusEl = document.getElementById('obd-speed');
const nowBadge = document.getElementById('speed-now-badge');
const lastTime = document.getElementById('speed-last-time');
if (!Array.isArray(data)) {
if (statusEl) statusEl.textContent = data;
if (nowBadge) nowBadge.textContent = '—';
return;
}
const time = data[0]; // "HH:MM:SS"
const speed = data[1]; // number, km/h
// Update stat cards and badges
if (statusEl) statusEl.textContent = speed != null ? speed + ' km/h' : '—';
if (nowBadge) nowBadge.textContent = speed != null ? speed + ' km/h' : '—';
if (lastTime) lastTime.textContent = 'Last: ' + time;
updateSpeedStats(speed);
// Push into chart (oldest at left, newest at right)
const labels = chart.data.labels;
const points = chart.data.datasets[0].data;
labels.push(time);
points.push(speed);
if (labels.length > MAX_SPEED_POINTS) {
labels.shift();
points.shift();
}
// Refresh gradient in case canvas resized
chart.data.datasets[0].backgroundColor = makeGradient();
// Dynamic Y max: 20% headroom above session max, min 60
const displayMax = Math.max(60, Math.ceil((sessionMaxSpeed * 1.2) / 10) * 10);
chart.options.scales.y.suggestedMax = displayMax;
chart.update('none'); // skip animation for live updates
});
socket.on('obd_rpm', function (data) {
const el = document.getElementById('obd-rpm');
if (el) el.textContent = (data != null && data !== '-') ? data + ' rpm' : '—';
});
socket.on('obd_coolant_temp', function (data) {
const el = document.getElementById('obd-coolant');
if (el) el.textContent = (data != null && data !== '-') ? data + ' °C' : '—';
});
socket.on('obd_fuel_level', function (data) {
const el = document.getElementById('obd-fuel');
if (el) el.textContent = (data != null && data !== '-') ? data + ' %' : '—';
});
socket.on('obd_throttle', function (data) {
const el = document.getElementById('obd-throttle');
if (el) el.textContent = (data != null && data !== '-') ? data + ' %' : '—';
});
socket.on('obd_error', function (data) {
const el = document.getElementById('obd-error');
if (el) el.innerHTML = data;
});
let mapCentered = false;
let _lastLat = null, _lastLon = null;
let _lastGeoLat = null, _lastGeoLon = null;
let _geocodeTimer = null;
const _recenterBtn = document.getElementById('btn-recenter');
_recenterBtn.addEventListener('click', function () {
if (_lastLat != null && _lastLon != null) {
map.setView([_lastLat, _lastLon], 15);
}
});
function reverseGeocode(lat, lon) {
const el = document.getElementById('gps-location-name');
const fallback = `${lat.toFixed(5)}, ${lon.toFixed(5)}`;
fetch(`/api/geocode?lat=${lat}&lon=${lon}`, { signal: AbortSignal.timeout(6000) })
.then(r => r.ok ? r.json() : null)
.then(data => { el.textContent = (data && data.label) ? data.label : fallback; })
.catch(() => { el.textContent = fallback; });
}
function _movedEnough(lat, lon) {
if (_lastGeoLat === null) return true;
// ~0.0005 deg ≈ 50m
return Math.abs(lat - _lastGeoLat) > 0.0005 || Math.abs(lon - _lastGeoLon) > 0.0005;
}
socket.on('gps_location', function(data) {
const LocationX = document.getElementById('gpsLocationX');
const LocationY = document.getElementById('gpsLocationY');
LocationX.innerHTML = data[0];
LocationY.innerHTML = data[1];
const lat = data[0];
const lon = data[1];
if (lat != null && lon != null) {
if (!map.hasLayer(mapTiles)) {
mapTiles.addTo(map);
}
carMarker.setLatLng([lat, lon]);
if (!mapCentered) {
map.setView([lat, lon], 15);
mapCentered = true;
}
_lastLat = lat;
_lastLon = lon;
_recenterBtn.hidden = false;
// Geocode when moved >50m, throttled to at most once every 15s
if (!_geocodeTimer && _movedEnough(lat, lon)) {
_lastGeoLat = lat;
_lastGeoLon = lon;
reverseGeocode(lat, lon);
_geocodeTimer = setTimeout(() => { _geocodeTimer = null; }, 15000);
}
}
});
function formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return days ? `${days}d ${hours}h` : `${hours}h ${minutes}m`;
}
function setHealthValue(id, value) {
document.getElementById(id).textContent = value;
}
function healthWarningLabel(warning) {
return {
under_voltage: "Power supply undervoltage",
frequency_capped: "CPU frequency capped",
throttled: "CPU currently throttled",
soft_temperature_limit: "CPU temperature limit active",
high_temperature: "CPU temperature is high",
disk_nearly_full: "Disk is nearly full",
memory_pressure: "Memory usage is high",
}[warning] || warning;
}
function historicalHealthWarningLabel(warning) {
return {
under_voltage: "Power supply undervoltage",
frequency_capped: "CPU frequency capping",
throttled: "CPU throttling",
soft_temperature_limit: "CPU temperature limiting",
}[warning] || warning;
}
async function refreshHealth() {
const response = await fetch("/api/health");
const health = await response.json();
if (!response.ok) {
throw new Error(health.error || "Could not load Pi health.");
}
setHealthValue(
"pi-temperature",
health.temperature_c == null ? "Unavailable" : `${health.temperature_c.toFixed(1)} °C`
);
setHealthValue(
"pi-memory",
health.memory == null ? "Unavailable" : `${health.memory.used_percent.toFixed(1)}%`
);
setHealthValue(
"pi-disk",
health.disk == null
? "Unavailable"
: `${health.disk.used_percent.toFixed(1)}% · ${YarisUI.formatBytes(health.disk.free_bytes)} free`
);
setHealthValue(
"pi-uptime",
health.uptime_seconds == null ? "Unavailable" : formatUptime(health.uptime_seconds)
);
const warning = document.getElementById("pi-health-warning");
const currentWarnings = health.warnings.map(healthWarningLabel);
const historicalWarnings = health.historical_warnings.map(
historicalHealthWarningLabel
);
const warnings = currentWarnings.concat(
historicalWarnings.length
? [`Previously detected: ${historicalWarnings.join(", ")}`]
: []
);
warning.hidden = warnings.length === 0;
warning.textContent = warnings.join(" · ");
const error = document.getElementById("pi-health-error");
const unavailable = Object.keys(health.errors);
error.hidden = unavailable.length === 0;
error.textContent = unavailable.length
? `Unavailable health sources: ${unavailable.join(", ")}`
: "";
document.getElementById("pi-health-updated").textContent =
`Updated ${new Date().toLocaleTimeString()}`;
}
refreshHealth().catch(function (error) {
const message = document.getElementById("pi-health-error");
message.textContent = error.message;
message.hidden = false;
});
window.setInterval(function () {
refreshHealth().catch(function (error) {
const message = document.getElementById("pi-health-error");
message.textContent = error.message;
message.hidden = false;
});
}, 30000);