diff --git a/DASHBOARD/dashboard.html b/DASHBOARD/dashboard.html
index 3d2b98a..8dfa1b9 100644
--- a/DASHBOARD/dashboard.html
+++ b/DASHBOARD/dashboard.html
@@ -247,389 +247,52 @@
Event Log
fill.classList.toggle('hot', hot);
}
-function drawMap(){
- const w = canvas.width, h = canvas.height;
- ctx.clearRect(0,0,w,h);
- ctx.fillStyle = '#0f1512';
- ctx.fillRect(0,0,w,h);
- if (trail.length === 0){
- ctx.fillStyle = '#8ea295';
- ctx.font = '12px IBM Plex Mono';
- ctx.textAlign = 'center';
- ctx.fillText('waiting for telemetry…', w/2, h/2);
- return;
- }
-
- // fit bounds with padding
- let minX=Infinity,maxX=-Infinity,minY=Infinity,maxY=-Infinity;
- trail.forEach(p=>{minX=Math.min(minX,p.x);maxX=Math.max(maxX,p.x);
- minY=Math.min(minY,p.y);maxY=Math.max(maxY,p.y);});
- const padCm = 40;
- minX-=padCm; maxX+=padCm; minY-=padCm; maxY+=padCm;
- const spanX = Math.max(maxX-minX, 80), spanY = Math.max(maxY-minY, 80);
- const scale = Math.min(w/spanX, h/spanY);
- const ox = w/2 - ((minX+maxX)/2)*scale;
- const oy = h/2 + ((minY+maxY)/2)*scale; // y flipped for screen coords
-
- const toScreen = (x,y) => [ox + x*scale, oy - y*scale];
-
- // grid, every 50cm
- ctx.strokeStyle = '#1a231e';
- ctx.lineWidth = 1;
- const gridStep = 50;
- for(let gx = Math.floor(minX/gridStep)*gridStep; gx < maxX; gx += gridStep){
- const [sx] = toScreen(gx, 0);
- ctx.beginPath(); ctx.moveTo(sx,0); ctx.lineTo(sx,h); ctx.stroke();
- }
- for(let gy = Math.floor(minY/gridStep)*gridStep; gy < maxY; gy += gridStep){
- const [,sy] = toScreen(0, gy);
- ctx.beginPath(); ctx.moveTo(0,sy); ctx.lineTo(w,sy); ctx.stroke();
- }
-
- // trail line — color-coded by rover state
- ctx.lineWidth = 2.5;
- for (let i = 1; i < trail.length; i++){
- const p0 = trail[i-1], p1 = trail[i];
- const [x0,y0] = toScreen(p0.x, p0.y);
- const [x1,y1] = toScreen(p1.x, p1.y);
- ctx.strokeStyle = STATE_COLORS[p1.state] || '#e8a33d';
- ctx.beginPath();
- ctx.moveTo(x0,y0);
- ctx.lineTo(x1,y1);
- ctx.stroke();
- }
-
- // direction arrows along the path every 25 points
- for (let i = 25; i < trail.length; i += 25){
- const p = trail[i];
- const [ax, ay] = toScreen(p.x, p.y);
- const rad = (p.heading||0) * Math.PI/180;
- ctx.save();
- ctx.translate(ax, ay);
- ctx.rotate(-rad);
- ctx.fillStyle = 'rgba(142,162,149,0.6)';
- ctx.beginPath();
- ctx.moveTo(5,0); ctx.lineTo(-3,3); ctx.lineTo(-3,-3); ctx.closePath();
- ctx.fill();
- ctx.restore();
- }
-
- // start marker
- const [sx0,sy0] = toScreen(trail[0].x, trail[0].y);
- ctx.fillStyle = '#4cc27a';
- ctx.beginPath(); ctx.arc(sx0,sy0,5,0,7); ctx.fill();
- ctx.font = '10px IBM Plex Mono';
- ctx.fillStyle = '#4cc27a';
- ctx.textAlign = 'left';
- ctx.fillText('START', sx0+8, sy0+3);
-
- // hazard flags
- hazards.forEach(hz=>{
- const [hx,hy] = toScreen(hz.x, hz.y);
- ctx.fillStyle = '#e0554a';
- ctx.beginPath(); ctx.arc(hx,hy,5,0,7); ctx.fill();
- ctx.font = '10px IBM Plex Mono';
- ctx.fillStyle = '#e0554a';
- ctx.textAlign = 'left';
- ctx.fillText(hz.cause, hx+8, hy+3);
- });
-
- // rover marker + heading arrow
- const last = trail[trail.length-1];
- const [rx,ry] = toScreen(last.x, last.y);
- const rad = (last.heading||0) * Math.PI/180;
- ctx.save();
- ctx.translate(rx,ry);
- ctx.rotate(-rad);
- ctx.fillStyle = last.danger ? '#e0554a' : '#e8a33d';
- ctx.beginPath();
- ctx.moveTo(12,0); ctx.lineTo(-7,7); ctx.lineTo(-4,0); ctx.lineTo(-7,-7); ctx.closePath();
- ctx.fill();
- ctx.restore();
-
- // legend
- ctx.font = '10px IBM Plex Mono';
- ctx.textAlign = 'left';
- const legend = [['NORMAL','#4cc27a'],['SLOW','#e8a33d'],['AVOIDING','#d4912e'],['DANGER','#e0554a']];
- legend.forEach(([label,color],i) => {
- const lx = 14, ly = h - 14 - (legend.length - 1 - i) * 16;
- ctx.fillStyle = color;
- ctx.fillRect(lx, ly - 6, 10, 10);
- ctx.fillStyle = '#8ea295';
- ctx.fillText(label, lx + 14, ly + 3);
- });
-}
-
-function handleTelemetry(raw){
- // Normalize short & long keys
- const d = {
- tempC: raw.t ?? raw.tempC,
- hum: raw.h ?? raw.hum,
- gas: raw.gas,
- frontCm: raw.dist ?? raw.frontCm,
- water: raw.water,
- tiltDeg: raw.tilt ?? raw.tiltDeg,
- x: raw.x ?? 0,
- y: raw.y ?? 0,
- heading: raw.hd ?? raw.heading ?? 0,
- state: raw.st ?? raw.state ?? 'NORMAL',
- dangerCause: raw.danger ?? raw.dangerCause ?? 'NONE',
- encL: raw.encL,
- encR: raw.encR
- };
- latest = d;
-
- const danger = d.state === 'DANGER';
- document.getElementById('ledGreen').classList.toggle('on-green', !danger);
- document.getElementById('ledRed').classList.toggle('on-red', danger);
- const buzz = document.getElementById('buzzBox');
- buzz.textContent = danger ? `BUZZER — ${d.dangerCause}` : 'Buzzer silent';
- buzz.classList.toggle('active', danger);
-
- const badge = document.getElementById('stateBadge');
- badge.textContent = d.state;
- badge.className = 'state-badge ' + d.state;
-
- if (typeof d.tempC === 'number' && d.tempC > -900) setGauge('temp', d.tempC, '°C', 1);
- if (typeof d.hum === 'number' && d.hum > -900) setGauge('hum', d.hum, '%', 1);
- if (typeof d.gas === 'number') setGauge('gas', d.gas, '', 0);
- if (typeof d.water === 'number') setGauge('water', d.water, '', 0);
- if (typeof d.tiltDeg === 'number') setGauge('tilt', d.tiltDeg, '°', 1);
-
- document.getElementById('s-front').textContent = (d.frontCm ?? '—') + ' cm';
- document.getElementById('s-pos').textContent = `${(d.x??0).toFixed(1)}, ${(d.y??0).toFixed(1)} cm`;
- document.getElementById('s-heading').textContent = (d.heading ?? 0).toFixed(1) + '°';
- document.getElementById('s-enc').textContent = `${d.encL ?? '—'} / ${d.encR ?? '—'}`;
-
- if (typeof d.x === 'number' && typeof d.y === 'number'){
- const prev = trail.length > 0 ? trail[trail.length - 1] : null;
- const moved = !prev || Math.abs(d.x - prev.x) > 0.05 || Math.abs(d.y - prev.y) > 0.05
- || Math.abs((d.heading||0) - (prev.heading||0)) > 0.5;
- if (moved) {
- if (prev) {
- const dx = d.x - prev.x, dy = d.y - prev.y;
- totalDistCm += Math.sqrt(dx*dx + dy*dy);
- }
- trail.push({x:d.x, y:d.y, heading:d.heading||0, danger, state: d.state||'NORMAL'});
- if (trail.length > 4000) trail.shift();
+const scanPoints = [];
+const MAX_POINTS = 200;
+const SCALE = 0.25;
+
+function handleScan(data) {
+ if (data.valid) {
+ const theta = ((data.angle_deg - 90) * Math.PI) / 180;
+ const lateral = data.distance_mm * Math.cos(theta);
+ const forward = data.distance_mm * Math.sin(theta);
+ const w = canvas.width;
+ const h = canvas.height;
+ const canvasX = (w / 2) - (lateral * SCALE);
+ const canvasY = (h - 20) - (forward * SCALE);
+ scanPoints.push({ x: canvasX, y: canvasY, ts: Date.now() });
+ if (scanPoints.length > MAX_POINTS) scanPoints.shift();
}
- document.getElementById('s-dist').textContent =
- totalDistCm < 100 ? totalDistCm.toFixed(0) + ' cm'
- : (totalDistCm / 100).toFixed(2) + ' m';
- }
-
- if (danger && !wasDanger){
- log(`DANGER — ${d.dangerCause}`, '');
- if (typeof d.x === 'number') hazards.push({x:d.x, y:d.y, cause:d.dangerCause});
- }
- if (!danger && wasDanger){
- log('Cleared, resuming', 'info');
- }
- wasDanger = danger;
-
- drawMap();
-}
-
-// ---------- Hot-Pluggable MQTT over WebSocket ----------
-let mqttConn = null;
-const MQTT_TOPIC = 'deeptrack/rover/telemetry';
-const MQTT_WS_URL = 'wss://broker.hivemq.com:8884/mqtt';
-
-function connectMQTT(){
- if (mqttConn && mqttConn.connected){
- disconnectMQTT();
- return;
- }
- log('Connecting to HiveMQ broker…', 'info');
- try {
- mqttConn = mqtt.connect(MQTT_WS_URL, {
- clientId: 'deeptrack-dash-' + Math.random().toString(16).slice(2,8),
- clean: true,
- connectTimeout: 8000,
- reconnectPeriod: 5000
- });
-
- mqttConn.on('connect', () => {
- document.getElementById('connDot').className = 'dot live';
- document.getElementById('connLabel').textContent = 'MQTT Live';
- document.getElementById('mqttBtn').textContent = 'Disconnect MQTT';
- document.getElementById('connectBtn').disabled = true;
- document.getElementById('demoBtn').disabled = true;
- log('MQTT connected — subscribed to ' + MQTT_TOPIC, 'info');
- mqttConn.subscribe(MQTT_TOPIC);
- });
-
- mqttConn.on('message', (topic, payload) => {
- try {
- const d = JSON.parse(payload.toString());
- handleTelemetry(d);
- } catch(e) { /* ignore partial */ }
- });
-
- mqttConn.on('error', (err) => {
- log('MQTT error: ' + err.message, '');
- });
-
- mqttConn.on('close', () => {
- document.getElementById('connDot').className = 'dot off';
- document.getElementById('connLabel').textContent = 'Offline';
- document.getElementById('mqttBtn').textContent = 'Connect MQTT';
- document.getElementById('connectBtn').disabled = false;
- document.getElementById('demoBtn').disabled = false;
- });
-
- } catch(err) {
- log('MQTT connection failed: ' + err.message, '');
- }
-}
-
-function disconnectMQTT(){
- if (mqttConn) {
- mqttConn.end(true);
- mqttConn = null;
- }
- document.getElementById('connDot').className = 'dot off';
- document.getElementById('connLabel').textContent = 'Offline';
- document.getElementById('mqttBtn').textContent = 'Connect MQTT';
- document.getElementById('connectBtn').disabled = false;
- document.getElementById('demoBtn').disabled = false;
- log('MQTT disconnected.', 'info');
-}
-
-document.getElementById('mqttBtn').addEventListener('click', connectMQTT);
-
-// ---------- Web Serial — connect to Gateway USB ----------
-async function connect(){
- if (!('serial' in navigator)){
- log('Web Serial not supported — use Chrome or Edge on desktop.', '');
- alert('Web Serial API is not available in this browser. Please use Chrome or Edge on desktop, connected over HTTPS or localhost.');
- return;
- }
- try{
- port = await navigator.serial.requestPort();
- await port.open({ baudRate: 115200 });
- keepReading = true;
- document.getElementById('connDot').className = 'dot live';
- document.getElementById('connLabel').textContent = 'Serial Live';
- document.getElementById('connectBtn').textContent = 'Disconnect';
- document.getElementById('mqttBtn').disabled = true;
- document.getElementById('demoBtn').disabled = true;
- log('Gateway serial link established.', 'info');
- readLoop();
- }catch(err){
- log('Connection failed: ' + err.message);
- }
-}
-
-async function disconnect(){
- keepReading = false;
- try{ if(reader) await reader.cancel(); }catch(e){}
- try{ if(port) await port.close(); }catch(e){}
- document.getElementById('connDot').className = 'dot off';
- document.getElementById('connLabel').textContent = 'Offline';
- document.getElementById('connectBtn').textContent = 'Connect Serial';
- document.getElementById('mqttBtn').disabled = false;
- document.getElementById('demoBtn').disabled = false;
- log('Disconnected.', 'info');
}
-async function readLoop(){
- const textDecoder = new TextDecoderStream();
- const readableClosed = port.readable.pipeTo(textDecoder.writable);
- reader = textDecoder.readable.getReader();
- let buffer = '';
-
- try{
- while(keepReading){
- const { value, done } = await reader.read();
- if (done) break;
- buffer += value;
- let idx;
- while((idx = buffer.indexOf('\n')) >= 0){
- const line = buffer.slice(0, idx).trim();
- buffer = buffer.slice(idx+1);
- if (!line.startsWith('{')) continue;
- try{
- const d = JSON.parse(line);
- handleTelemetry(d);
- }catch(e){ /* partial/garbled line, skip */ }
- }
+function drawMap() {
+ const w = canvas.width, h = canvas.height;
+ ctx.clearRect(0,0,w,h);
+ ctx.fillStyle = '#0f1512';
+ ctx.fillRect(0,0,w,h);
+ ctx.strokeStyle = '#25332c';
+ ctx.beginPath();
+ ctx.arc(w/2, h-20, 100, 0, Math.PI, true);
+ ctx.arc(w/2, h-20, 200, 0, Math.PI, true);
+ ctx.stroke();
+ ctx.fillStyle = '#4cc27a';
+ ctx.fillRect(w/2 - 5, h-25, 10, 10);
+ const now = Date.now();
+ for (let i = scanPoints.length - 1; i >= 0; i--) {
+ const pt = scanPoints[i];
+ const age = now - pt.ts;
+ if (age > 5000) { scanPoints.splice(i, 1); continue; }
+ const alpha = 1.0 - (age / 5000);
+ ctx.fillStyle = `rgba(0, 255, 255, ${alpha})`;
+ ctx.beginPath();
+ ctx.arc(pt.x, pt.y, 3, 0, Math.PI * 2);
+ ctx.fill();
}
- }catch(e){
- log('Read error: ' + e.message);
- }finally{
- reader.releaseLock();
- }
-}
-
-document.getElementById('connectBtn').addEventListener('click', () => {
- if (keepReading) disconnect(); else connect();
-});
-document.getElementById('resetBtn').addEventListener('click', () => {
- trail = []; hazards = []; totalDistCm = 0;
- document.getElementById('s-dist').textContent = '0 cm';
- drawMap();
- log('Path cleared.', 'info');
-});
-
-// ---------- Demo mode: synthetic telemetry ----------
-let demoRunning = false, demoTimer = null;
-let demoX=0, demoY=0, demoHeading=0, demoT=0;
-
-function demoTick(){
- demoT += 1;
- const avoiding = (demoT % 30 >= 25 && demoT % 30 <= 29);
- const danger = (demoT % 55 === 0);
-
- if (avoiding && demoT % 30 === 25) {
- demoHeading += (Math.random() > 0.5 ? 1 : -1) * (40 + Math.random() * 30);
- } else if (!avoiding && !danger) {
- demoHeading += (Math.random()-0.5) * 8;
- }
-
- const step = danger ? -2 : (avoiding ? 1 : 4);
- demoX += step * Math.cos(demoHeading*Math.PI/180);
- demoY += step * Math.sin(demoHeading*Math.PI/180);
-
- const causes = ['GAS','TEMP','HUMIDITY','WATER','TRAPPED'];
- const demoState = danger ? 'DANGER' : (avoiding ? 'AVOIDING' : (demoT % 20 < 3 ? 'SLOW' : 'NORMAL'));
-
- handleTelemetry({
- tempC: 28 + Math.sin(demoT/10)*4 + (danger?20:0),
- hum: 60 + Math.sin(demoT/14)*10 + (danger?15:0),
- gas: 400 + Math.abs(Math.sin(demoT/8))*300 + (danger?1500:0),
- water: 300 + Math.abs(Math.cos(demoT/9))*200 + (danger?2400:0),
- tiltDeg: 5 + Math.abs(Math.sin(demoT/6))*10,
- frontCm: (20 + Math.abs(Math.sin(demoT/5))*60).toFixed(1),
- encL: demoT*3, encR: demoT*3,
- x: demoX, y: demoY, heading: demoHeading,
- state: demoState,
- dangerCause: danger ? causes[Math.floor(Math.random()*causes.length)] : 'NONE'
- });
+ requestAnimationFrame(drawMap);
}
-document.getElementById('demoBtn').addEventListener('click', () => {
- demoRunning = !demoRunning;
- const btn = document.getElementById('demoBtn');
- document.getElementById('connectBtn').disabled = demoRunning;
- document.getElementById('mqttBtn').disabled = demoRunning;
- if (demoRunning){
- btn.textContent = 'Stop Demo';
- document.getElementById('connDot').className = 'dot live';
- document.getElementById('connLabel').textContent = 'Demo';
- log('Demo mode started — synthetic telemetry.', 'info');
- demoTimer = setInterval(demoTick, 400);
- } else {
- btn.textContent = 'Demo Mode';
- clearInterval(demoTimer);
- document.getElementById('connDot').className = 'dot off';
- document.getElementById('connLabel').textContent = 'Offline';
- log('Demo mode stopped.', 'info');
- }
-});
-
drawMap();