Files
yaris-obd/static/js/yaris.js
T

323 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 _geocodePending = false;
const _recenterBtn = document.getElementById('btn-recenter');
_recenterBtn.addEventListener('click', function () {
if (_lastLat != null && _lastLon != null) {
map.setView([_lastLat, _lastLon], 15);
}
});
function reverseGeocode(lat, lon) {
if (_geocodePending) return;
_geocodePending = true;
const el = document.getElementById('gps-location-name');
const fallback = `${lat.toFixed(5)}, ${lon.toFixed(5)}`;
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 6000);
fetch(`/api/geocode?lat=${lat}&lon=${lon}`, { signal: ctrl.signal })
.then(r => { clearTimeout(t); return r.ok ? r.json() : null; })
.then(data => { _geocodePending = false; el.textContent = (data && data.label) ? data.label : fallback; })
.catch(() => { clearTimeout(t); _geocodePending = false; el.textContent = fallback; });
}
function formatGpsAge(seconds) {
if (seconds == null) return 'No fix yet.';
if (seconds < 10) return 'Last fix just now.';
if (seconds < 60) return `Last fix ${Math.round(seconds)}s ago.`;
const mins = Math.floor(seconds / 60);
const secs = Math.round(seconds % 60);
return secs ? `Last fix ${mins}m ${secs}s ago.` : `Last fix ${mins}m ago.`;
}
function renderGpsStatus(status) {
const liveBadge = document.getElementById('gps-live-badge');
const fixDetail = document.getElementById('gps-fix-detail');
const locX = document.getElementById('gpsLocationX');
const locY = document.getElementById('gpsLocationY');
let label = 'Unavailable';
let badgeClass = 'text-bg-secondary';
let detail = 'GPS thread has not started.';
if (status.thread_alive) {
if (status.has_fix) {
const stale = status.fix_age_seconds != null && status.fix_age_seconds > 30;
label = stale ? 'Stale fix' : 'Live fix';
badgeClass = stale ? 'text-bg-warning' : 'text-bg-success';
detail = formatGpsAge(status.fix_age_seconds);
// Show lat/lon from API response (not just socket events)
if (locX) locX.textContent = status.latitude != null ? Number(status.latitude).toFixed(6) : '—';
if (locY) locY.textContent = status.longitude != null ? Number(status.longitude).toFixed(6) : '—';
} else {
label = 'No fix';
badgeClass = 'text-bg-warning';
detail = 'GNSS thread is running, but no fix is available yet.';
}
} else {
label = 'Offline';
badgeClass = 'text-bg-danger';
}
liveBadge.className = `badge ${badgeClass}`;
liveBadge.textContent = label;
fixDetail.textContent = detail;
}
async function refreshGpsStatus() {
const response = await fetch('/api/gps/status');
const status = await response.json();
if (!response.ok) {
throw new Error(status.error || 'Could not load GPS status.');
}
renderGpsStatus(status);
}
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 = parseFloat(data[0]);
const lon = parseFloat(data[1]);
if (lat != null && lon != null) {
renderGpsStatus({thread_alive: true, has_fix: true, fix_age_seconds: 0});
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 on first fix or after moving >50m (~0.0005 deg)
const moved = _lastGeoLat === null
|| Math.abs(lat - _lastGeoLat) > 0.0005
|| Math.abs(lon - _lastGeoLon) > 0.0005;
if (moved) {
_lastGeoLat = lat;
_lastGeoLon = lon;
reverseGeocode(lat, lon);
}
}
});
refreshGpsStatus().catch(function (error) {
renderGpsStatus({thread_alive: false, has_fix: false, fix_age_seconds: null});
const fixDetail = document.getElementById('gps-fix-detail');
if (fixDetail) fixDetail.textContent = error.message;
});
window.setInterval(function () {
refreshGpsStatus().catch(function (error) {
const fixDetail = document.getElementById('gps-fix-detail');
if (fixDetail) fixDetail.textContent = error.message;
});
}, 15000);
async function refreshObdStatus() {
try {
const resp = await fetch('/api/obd/status');
const status = await resp.json();
const el = document.getElementById('obd-status');
if (el) el.textContent = status.adapter_present
? (status.car_connected ? 'Connected' : 'Adapter only')
: 'Not detected';
} catch (e) {
// Socket events will eventually update it
}
}
refreshObdStatus();
window.setInterval(refreshObdStatus, 15000);