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(); - + \ No newline at end of file diff --git a/GATEWAY/include/telemetry_packet.h b/GATEWAY/include/telemetry_packet.h index df75373..bc0d1d8 100644 --- a/GATEWAY/include/telemetry_packet.h +++ b/GATEWAY/include/telemetry_packet.h @@ -1,30 +1,32 @@ #pragma once + #include -/* - Shared telemetry packet for ESP-NOW communication between - the Rover ESP32 and the Gateway ESP32. - 48 bytes — well under ESP-NOW's 250-byte limit. +// Standard telemetry (sent slowly) +typedef struct __attribute__((packed)) { + float temperature; + float humidity; + float ax, ay, az; + float gx, gy, gz; + uint16_t gasRaw; + uint16_t waterRaw; + uint32_t dangerState; // 0=safe, 1=danger +} TelemetryPacket; - Also used by the gateway to parse Serial2 bridge JSON - and to relay JSON to the laptop dashboard. -*/ +// Fast scan telemetry (sent rapidly) +typedef struct __attribute__((packed)) { + uint8_t type; // 1 = scan + uint8_t seq; // Sequence number + int16_t angle_deg; // Servo angle in degrees + uint16_t distance_mm; // VL53L0X distance + uint8_t valid; // 1 if valid, 0 if out of range + uint32_t timestamp_ms; // timestamp +} ScanPacket; +// Gateway to Rover control packet (Heartbeat) typedef struct __attribute__((packed)) { - float tempC; // DHT22 temperature (°C) - float humidity; // DHT22 relative humidity (%) - int16_t gasRaw; // MQ-4 Methane sensor ADC (0-4095) - float frontCm; // HC-SR04 front distance (cm) - int16_t tofRaw; // VL53L0X sim pot ADC (0-4095) - float tiltDeg; // MPU6050 tilt angle (degrees) - int16_t waterRaw; // Water sensor ADC (0-4095) - uint32_t encL, encR; // Wheel encoder pulse counts - float x, y; // Dead-reckoned position (cm) - float heading; // Heading (degrees, 0=+X, CCW positive) - uint8_t state; // 0=NORMAL 1=SLOW 2=AVOIDING 3=DANGER - uint8_t dangerCause; // 0=NONE 1=GAS 2=TILT 3=WATER 4=TEMP 5=HUMIDITY 6=TRAPPED -} TelemetryPacket; + uint8_t type; // 0 = command + int16_t motor_l; // -255 to 255 + int16_t motor_r; // -255 to 255 +} ControlPacket; -// String lookups for JSON serialization / LCD display -static const char* const STATE_NAMES[] = {"NORMAL","SLOW","AVOIDING","DANGER"}; -static const char* const DANGER_NAMES[] = {"NONE","GAS","TILT","WATER","TEMP","HUMIDITY","TRAPPED"}; diff --git a/GATEWAY/src/main.cpp b/GATEWAY/src/main.cpp index 5fb40e9..511e20f 100644 --- a/GATEWAY/src/main.cpp +++ b/GATEWAY/src/main.cpp @@ -1,419 +1,284 @@ -/* - DEEPTRACK GATEWAY — ESP-NOW Receiver + LCD Console + Hot-Pluggable MQTT & Serial Relay - - This ESP32 sits on a desk, acting as the base station console. - It receives telemetry from the Rover ESP32 over ESP-NOW (real hardware) - or over a Serial bridge (Wokwi simulation via socat + rfc2217). - If neither source provides data within 5 seconds, a built-in demo - data generator activates so the LCD, LEDs, and Dashboard can be tested. - - Telemetry Relaying (Hot-Pluggable Dual-Output): - 1. USB Serial → 115200 baud JSON stream to laptop (Web Serial API) - 2. MQTT Stream → Publishes JSON to broker.hivemq.com (non-blocking) - - Components: - 16×2 I2C LCD (0x27) → SDA 21 / SCL 22 (Auto-cycles every 3s between ENV/NAV/STATUS) - Red LED → GPIO 26 (DANGER indicator) - Green LED → GPIO 27 (NORMAL / LINK OK) - Yellow LED → GPIO 25 (Heartbeat blink on packet reception) -*/ - +#include #include #include -#include -#include #include #include -#include "telemetry_packet.h" -// ---------- WiFi & MQTT Config (Hot-pluggable / non-blocking) ---------- -const char* WIFI_SSID = "Wokwi-GUEST"; -const char* WIFI_PASS = ""; -const char* MQTT_SERVER = "broker.hivemq.com"; -const int MQTT_PORT = 1883; -const char* MQTT_TOPIC = "deeptrack/rover/telemetry"; +#define USE_ESP_NOW 0 -WiFiClient espClient; -PubSubClient mqttClient(espClient); - -unsigned long lastMqttAttempt = 0; -bool mqttWasConnected = false; - -// ---------- Pin map ---------- -#define LED_RED_PIN 26 -#define LED_GREEN_PIN 27 -#define LED_YELLOW_PIN 25 +#include +#include "telemetry_packet.h" // ---------- LCD ---------- LiquidCrystal_I2C lcd(0x27, 16, 2); +uint32_t lastDataTime = 0; +int lcdPage = 0; +#define NUM_PAGES 3 +uint32_t lastPageChange = 0; -// Custom characters (HD44780 can store 8 custom chars, 5×8 pixels each) -byte thermIcon[8] = { - 0b00100, 0b01010, 0b01010, 0b01110, - 0b01110, 0b11111, 0b11111, 0b01110 -}; -byte signalIcon[8] = { - 0b00001, 0b00001, 0b00101, 0b00101, - 0b10101, 0b10101, 0b10101, 0b00000 -}; -byte warnIcon[8] = { - 0b00000, 0b00100, 0b00100, 0b01010, - 0b01110, 0b11111, 0b11111, 0b00000 -}; +// Custom Icons +byte thermIcon[8] = { B00100, B01010, B01010, B01110, B01110, B11111, B11111, B01110 }; +byte signalIcon[8] = { B00000, B10000, B10100, B10100, B10101, B10101, B10101, B10101 }; +byte warnIcon[8] = { B00000, B00100, B01010, B11011, B11011, B11011, B11111, B00000 }; -// ---------- State ---------- -TelemetryPacket telem; // latest telemetry data -volatile bool espNowReady = false; // set by ISR-like callback -bool linkActive = false; -bool demoActive = false; +TelemetryPacket latestTel = {0}; -unsigned long lastDataTime = 0; // millis() of last received packet -unsigned long lastPageSwitch = 0; -unsigned long lastHeartbeatOn = 0; -unsigned long lastDemoTick = 0; -uint32_t packetCount = 0; -int lcdPage = 0; -const int NUM_PAGES = 3; // ENV, NAV, STATUS +#define WIFI_SSID "Wokwi-GUEST" +#define WIFI_PASS "" +#define MQTT_SERVER "broker.hivemq.com" +#define MQTT_PORT 1883 +#define MQTT_TOPIC_TELEMETRY "rover/telemetry" +#define MQTT_TOPIC_SCAN "rover/scan" -// Demo mode synthetic state -float demoX = 0, demoY = 0, demoH = 0; -uint32_t demoTick = 0; +WiFiClient espClient; +PubSubClient mqttClient(espClient); + +uint8_t roverAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; -// ---------- ESP-NOW receive callback ---------- -void onDataReceived(const uint8_t *mac, const uint8_t *data, int len) { - if (len == sizeof(TelemetryPacket)) { - memcpy(&telem, data, sizeof(TelemetryPacket)); - espNowReady = true; - } -} -// ---------- LCD display ---------- void updateLCD() { lcd.clear(); - - // DANGER page overrides everything - if (telem.state == 3) { - lcd.setCursor(0, 0); - lcd.write(2); // warning icon - lcd.print(" DANGER!! "); - lcd.write(2); - lcd.setCursor(0, 1); - const char* cause = (telem.dangerCause < 7) ? DANGER_NAMES[telem.dangerCause] : "???"; - char line2[17]; - snprintf(line2, sizeof(line2), ">>> %-7s <<<", cause); - lcd.print(line2); - return; - } - - switch (lcdPage) { - case 0: { // ENV — temperature, humidity, gas, water + + if (millis() - lastDataTime > 3000) { lcd.setCursor(0, 0); - lcd.write(0); // thermometer icon - char e1[17]; - snprintf(e1, sizeof(e1), "%.1fC H:%.0f%%", telem.tempC, telem.humidity); - lcd.print(e1); + lcd.write(2); // warning + lcd.print(" LINK LOST! "); + lcd.write(2); lcd.setCursor(0, 1); - char e2[17]; - snprintf(e2, sizeof(e2), "CH4:%04d Wt:%04d", (int)telem.gasRaw, (int)telem.waterRaw); - lcd.print(e2); - break; - } - case 1: { // NAV — position, heading, front distance + lcd.print("No data >3s"); + return; + } + + if (latestTel.dangerState == 1) { lcd.setCursor(0, 0); - char n1[17]; - snprintf(n1, sizeof(n1), "X:%-5.0f Y:%-5.0f", telem.x, telem.y); - lcd.print(n1); + lcd.write(2); // warning icon + lcd.print(" DANGER!! "); + lcd.write(2); lcd.setCursor(0, 1); - char n2[17]; - snprintf(n2, sizeof(n2), "Hd:%-3.0f Ft:%-4.0f", telem.heading, telem.frontCm); - lcd.print(n2); + lcd.print("EMERGENCY STOP"); + return; + } + + switch (lcdPage) { + case 0: { // ENV + char e1[17], e2[17]; + snprintf(e1, sizeof(e1), " T:%.1fC H:%.1f%%", latestTel.temperature, latestTel.humidity); + snprintf(e2, sizeof(e2), " Gas:%d W:%d", latestTel.gasRaw, latestTel.waterRaw); + lcd.setCursor(0, 0); lcd.write(0); lcd.print(e1); + lcd.setCursor(0, 1); lcd.print(e2); break; } - case 2: { // STATUS — state, link, MQTT status, packet count - lcd.setCursor(0, 0); - const char* st = (telem.state < 4) ? STATE_NAMES[telem.state] : "???"; - char s1[17]; - snprintf(s1, sizeof(s1), "ST:%-6s M:%s", st, mqttClient.connected() ? "ON" : "--"); - lcd.print(s1); - lcd.setCursor(0, 1); - lcd.write(1); // signal icon - char s2[17]; - snprintf(s2, sizeof(s2), "%s Pk:%lu", - linkActive ? "OK " : "-- ", - (unsigned long)packetCount); - lcd.print(s2); + + case 1: { // NAV + char n1[17], n2[17]; + snprintf(n1, sizeof(n1), " P:%.1f R:%.1f", + atan2(-latestTel.ax, sqrt(latestTel.ay * latestTel.ay + latestTel.az * latestTel.az)) * 180.0 / 3.14159265, + atan2(latestTel.ay, latestTel.az) * 180.0 / 3.14159265); + snprintf(n2, sizeof(n2), " GYR:%d", (int)latestTel.gz); + lcd.setCursor(0, 0); lcd.print(n1); + lcd.setCursor(0, 1); lcd.print(n2); break; } - } -} - -void showLinkLost() { - lcd.clear(); - lcd.setCursor(0, 0); - lcd.write(2); // warning - lcd.print(" LINK LOST! "); - lcd.write(2); - lcd.setCursor(0, 1); - lcd.print("No data >3s"); -} - -void showBootScreen() { - lcd.clear(); - lcd.setCursor(0, 0); - lcd.print("DEEPTRACK GTWRY"); - lcd.setCursor(0, 1); - lcd.print("Waiting link..."); -} - -// ---------- LEDs ---------- -void updateLEDs() { - bool danger = (telem.state == 3); - digitalWrite(LED_RED_PIN, danger ? HIGH : LOW); - digitalWrite(LED_GREEN_PIN, danger ? LOW : HIGH); -} - -void heartbeatBlink() { - digitalWrite(LED_YELLOW_PIN, HIGH); - lastHeartbeatOn = millis(); -} - -// ---------- Dual Relay: Serial + Hot-Pluggable MQTT ---------- -void relayTelemetry() { - const char* stName = (telem.state < 4) ? STATE_NAMES[telem.state] : "UNKNOWN"; - const char* dcName = (telem.dangerCause < 7) ? DANGER_NAMES[telem.dangerCause] : "NONE"; - - char json[256]; - snprintf(json, sizeof(json), - "{\"t\":%.1f,\"h\":%.1f,\"gas\":%d,\"dist\":%.1f,\"water\":%d,\"tilt\":%.1f,\"x\":%.1f,\"y\":%.1f,\"hd\":%.0f,\"st\":\"%s\",\"danger\":\"%s\"}", - telem.tempC, telem.humidity, - (int)telem.gasRaw, telem.frontCm, - (int)telem.waterRaw, telem.tiltDeg, - telem.x, telem.y, telem.heading, - stName, dcName - ); - // 1. Always output to USB Serial (for laptop Web Serial Dashboard) - Serial.println(json); - - // 2. Publish to MQTT if connected (hot-pluggable) - if (mqttClient.connected()) { - mqttClient.publish(MQTT_TOPIC, json); - } -} - -// ---------- Parse JSON from Serial bridge ---------- -bool parseJsonTelemetry(const String& line) { - JsonDocument doc; - DeserializationError err = deserializeJson(doc, line); - if (err) return false; - - telem.tempC = doc["t"] | doc["tempC"] | 0.0f; - telem.humidity = doc["h"] | doc["hum"] | 0.0f; - telem.gasRaw = doc["gas"] | (int16_t)0; - telem.frontCm = doc["dist"] | doc["frontCm"] | 999.0f; - telem.tofRaw = doc["tof"] | doc["tofRaw"] | (int16_t)4095; - telem.tiltDeg = doc["tilt"] | doc["tiltDeg"] | 0.0f; - telem.waterRaw = doc["water"] | (int16_t)0; - telem.encL = doc["encL"] | (uint32_t)0; - telem.encR = doc["encR"] | (uint32_t)0; - telem.x = doc["x"] | 0.0f; - telem.y = doc["y"] | 0.0f; - telem.heading = doc["hd"] | doc["heading"] | 0.0f; - - // Parse state string → uint8_t - const char* stStr = doc["st"] | doc["state"] | "NORMAL"; - telem.state = 0; - for (int i = 0; i < 4; i++) { - if (strcmp(stStr, STATE_NAMES[i]) == 0) { telem.state = i; break; } - } - - const char* dcStr = doc["danger"] | doc["dangerCause"] | "NONE"; - telem.dangerCause = 0; - for (int i = 0; i < 7; i++) { - if (strcmp(dcStr, DANGER_NAMES[i]) == 0) { telem.dangerCause = i; break; } - } - - return true; -} - -// ---------- Demo data generator ---------- -void generateDemoData() { - demoTick++; - bool danger = (demoTick % 55 == 0); - bool avoiding = (demoTick % 30 >= 25 && demoTick % 30 <= 29); - - telem.tempC = 28.0f + sinf(demoTick / 10.0f) * 4.0f + (danger ? 22.0f : 0); - telem.humidity = 60.0f + sinf(demoTick / 14.0f) * 10.0f; - telem.gasRaw = (int16_t)(400 + fabsf(sinf(demoTick / 8.0f)) * 300 + (danger ? 1500 : 0)); - telem.frontCm = 20.0f + fabsf(sinf(demoTick / 5.0f)) * 60.0f; - telem.tofRaw = 4095; - telem.tiltDeg = 5.0f + fabsf(sinf(demoTick / 6.0f)) * 10.0f; - telem.waterRaw = 300; - telem.encL = demoTick * 3; - telem.encR = demoTick * 3; - - if (avoiding && demoTick % 30 == 25) { - demoH += ((demoTick % 2) ? 1.0f : -1.0f) * (40.0f + (float)(demoTick % 30)); - } else if (!danger) { - demoH += (sinf(demoTick / 3.0f) - 0.5f) * 4.0f; + case 2: { // SYS + char s1[17], s2[17]; + snprintf(s1, sizeof(s1), " MQTT:%s", mqttClient.connected() ? "ON" : "OFF"); + snprintf(s2, sizeof(s2), " WiFi:%s", WiFi.status() == WL_CONNECTED ? "ON" : "OFF"); + lcd.setCursor(0, 0); lcd.print(s1); + lcd.setCursor(0, 1); lcd.write(1); lcd.print(s2); + break; + } } - float step = danger ? -2.0f : (avoiding ? 1.0f : 4.0f); - demoX += step * cosf(demoH * PI / 180.0f); - demoY += step * sinf(demoH * PI / 180.0f); - - telem.x = demoX; - telem.y = demoY; - telem.heading = fmodf(demoH + 3600.0f, 360.0f); - telem.state = danger ? 3 : (avoiding ? 2 : (demoTick % 20 < 3 ? 1 : 0)); - telem.dangerCause = danger ? 1 : 0; // GAS or NONE } -// ---------- Process new data (from any source) ---------- -void processNewData() { - relayTelemetry(); - updateLEDs(); - updateLCD(); - heartbeatBlink(); - linkActive = true; +void onDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len) { + if (len == sizeof(TelemetryPacket)) { + TelemetryPacket *pkt = (TelemetryPacket*)incomingData; + memcpy(&latestTel, pkt, sizeof(TelemetryPacket)); + lastDataTime = millis(); + char json[256]; + snprintf(json, sizeof(json), + "{\"t\":%.1f,\"h\":%.1f,\"ax\":%.2f,\"ay\":%.2f,\"az\":%.2f,\"gx\":%.2f,\"gy\":%.2f,\"gz\":%.2f,\"gas\":%d,\"water\":%d,\"danger\":%d}", + pkt->temperature, pkt->humidity, + pkt->ax, pkt->ay, pkt->az, + pkt->gx, pkt->gy, pkt->gz, + pkt->gasRaw, pkt->waterRaw, + pkt->dangerState + ); + Serial.print("TELEMETRY:"); + Serial.println(json); + if (mqttClient.connected()) { + mqttClient.publish(MQTT_TOPIC_TELEMETRY, json); + } + } else if (len == sizeof(ScanPacket)) { + ScanPacket *pkt = (ScanPacket*)incomingData; + lastDataTime = millis(); + char json[128]; + snprintf(json, sizeof(json), + "{\"type\":\"scan\",\"seq\":%d,\"angle_deg\":%d,\"distance_mm\":%d,\"valid\":%s,\"timestamp_ms\":%d}", + pkt->seq, pkt->angle_deg, pkt->distance_mm, pkt->valid ? "true" : "false", pkt->timestamp_ms + ); + Serial.print("SCAN:"); + Serial.println(json); + if (mqttClient.connected()) { + mqttClient.publish(MQTT_TOPIC_SCAN, json); + } + } } -// ---------- Non-blocking WiFi & MQTT Handler ---------- -void handleNetwork() { - unsigned long now = millis(); - // If WiFi is disconnected, attempt connection in background without blocking - if (WiFi.status() != WL_CONNECTED) { - if (now - lastMqttAttempt > 10000) { - lastMqttAttempt = now; - WiFi.begin(WIFI_SSID, WIFI_PASS); +void onMqttMessage(char* topic, byte* payload, unsigned int length) { + String msg; + for (unsigned int i = 0; i < length; i++) msg += (char)payload[i]; + if (String(topic) == "rover/control") { + if (msg.startsWith("L:")) { + int spaceIdx = msg.indexOf(' '); + if (spaceIdx > -1) { + ControlPacket pkt; + pkt.type = 0; + pkt.motor_l = msg.substring(2, spaceIdx).toInt(); + pkt.motor_r = msg.substring(msg.indexOf("R:") + 2).toInt(); +#if USE_ESP_NOW + esp_now_send(roverAddress, (uint8_t*)&pkt, sizeof(ControlPacket)); +#endif + Serial.println(msg); // Bridge to Wokwi Simulator + } + } } - return; - } +} - // WiFi is connected -> handle MQTT - if (!mqttClient.connected()) { - if (now - lastMqttAttempt > 5000) { - lastMqttAttempt = now; - String clientId = "DEEPTRACK-GW-" + String(random(0xffff), HEX); - if (mqttClient.connect(clientId.c_str())) { - Serial.println("MQTT connected to " + String(MQTT_SERVER)); - mqttWasConnected = true; - } +void reconnectMqtt() { + if (!mqttClient.connected()) { + Serial.println("Connecting to MQTT..."); + String clientId = "Gateway-"; + clientId += String(random(0xffff), HEX); + if (mqttClient.connect(clientId.c_str())) { + Serial.println("MQTT Connected"); + mqttClient.subscribe("rover/control"); + } } - } else { - mqttClient.loop(); - } } -// ---------- Setup ---------- void setup() { - Serial.begin(115200); - delay(200); - - // Pins - pinMode(LED_RED_PIN, OUTPUT); - pinMode(LED_GREEN_PIN, OUTPUT); - pinMode(LED_YELLOW_PIN, OUTPUT); - - digitalWrite(LED_GREEN_PIN, HIGH); - digitalWrite(LED_RED_PIN, LOW); - digitalWrite(LED_YELLOW_PIN, LOW); - - // LCD init - lcd.init(); - lcd.backlight(); - lcd.createChar(0, thermIcon); - lcd.createChar(1, signalIcon); - lcd.createChar(2, warnIcon); - showBootScreen(); - - // WiFi in STA mode for both ESP-NOW & optional MQTT - WiFi.mode(WIFI_STA); - WiFi.begin(WIFI_SSID, WIFI_PASS); // Non-blocking connect attempt - - // ESP-NOW init - if (esp_now_init() != ESP_OK) { - Serial.println("ESP-NOW init failed"); - } else { - esp_now_register_recv_cb(onDataReceived); - Serial.println("ESP-NOW receiver ready"); - } - - // MQTT setup - mqttClient.setServer(MQTT_SERVER, MQTT_PORT); - mqttClient.setBufferSize(512); - - // Zero-init telemetry - memset(&telem, 0, sizeof(telem)); - telem.frontCm = 999.0f; - telem.tofRaw = 4095; - - lastDataTime = millis(); - Serial.println("Gateway boot OK"); + Serial.begin(115200); + + pinMode(26, OUTPUT); // RED + pinMode(27, OUTPUT); // GREEN + pinMode(25, OUTPUT); // YELLOW + + // LCD init + lcd.init(); + lcd.backlight(); + lcd.createChar(0, thermIcon); + lcd.createChar(1, signalIcon); + lcd.createChar(2, warnIcon); + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("DEEPTRACK GTWRY"); + lcd.setCursor(0, 1); + lcd.print("Waiting link..."); + + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSID, WIFI_PASS); + + // In simulation, wait briefly for WiFi, but don't block ESP-NOW + for(int i=0; i<10 && WiFi.status() != WL_CONNECTED; i++) { + delay(500); + } + + mqttClient.setServer(MQTT_SERVER, MQTT_PORT); + mqttClient.setCallback(onMqttMessage); +#if USE_ESP_NOW + + if (esp_now_init() != ESP_OK) { + Serial.println("ESP-NOW Init Failed"); + return; + } + esp_now_register_recv_cb(onDataRecv); + + esp_now_peer_info_t peerInfo; + memcpy(peerInfo.peer_addr, roverAddress, 6); + peerInfo.channel = 0; + peerInfo.encrypt = false; + esp_now_add_peer(&peerInfo); +#endif } -// ---------- Main loop ---------- void loop() { - unsigned long now = millis(); - - // Maintain WiFi & MQTT connectivity non-blockingly - handleNetwork(); - - // --- Source 1: ESP-NOW (real hardware) --- - if (espNowReady) { - espNowReady = false; - lastDataTime = now; - packetCount++; - demoActive = false; - processNewData(); - } - - // --- Source 2: Serial JSON input (Wokwi bridge or direct) --- - if (Serial.available()) { - String line = Serial.readStringUntil('\n'); - int start = line.indexOf('{'); - int end = line.lastIndexOf('}'); - if (start >= 0 && end > start) { - String jsonSub = line.substring(start, end + 1); - if (parseJsonTelemetry(jsonSub)) { - lastDataTime = now; - packetCount++; - demoActive = false; - processNewData(); - } + if (WiFi.status() == WL_CONNECTED) { + if (!mqttClient.connected()) reconnectMqtt(); + mqttClient.loop(); } - } + - // --- Source 3: Demo fallback (no data for 5s after boot) --- - if (!demoActive && now > 5000 && (now - lastDataTime) > 5000) { - demoActive = true; - Serial.println("{\"info\":\"No data source detected - entering demo mode\"}"); - } - if (demoActive && (now - lastDemoTick) >= 500) { - lastDemoTick = now; - generateDemoData(); - processNewData(); - } - // --- Auto-cycle LCD pages every 3s (unless DANGER) --- - if (telem.state != 3 && (now - lastPageSwitch) >= 3000) { - lcdPage = (lcdPage + 1) % NUM_PAGES; - lastPageSwitch = now; - updateLCD(); - } + // --- Auto-cycle LCD pages every 3s (unless DANGER) --- + if (millis() - lastPageChange >= 3000) { + lastPageChange = millis(); + if (latestTel.dangerState != 1) { + lcdPage = (lcdPage + 1) % NUM_PAGES; + updateLCD(); + } + } - // --- Heartbeat LED off after 80ms blink --- - if (digitalRead(LED_YELLOW_PIN) == HIGH && (now - lastHeartbeatOn) > 80) { - digitalWrite(LED_YELLOW_PIN, LOW); - } + // Check serial for commands (direct typing) or telemetry (from Wokwi Bridge) + + if (Serial.available()) { + digitalWrite(25, !digitalRead(25)); // Toggle YELLOW LED on serial RX + String line = Serial.readStringUntil('\n'); + + // Check for commands (direct typing) + if (line.startsWith("L:")) { + int spaceIdx = line.indexOf(' '); + if (spaceIdx > -1) { + ControlPacket pkt; + pkt.type = 0; + pkt.motor_l = line.substring(2, spaceIdx).toInt(); + pkt.motor_r = line.substring(line.indexOf("R:") + 2).toInt(); +#if USE_ESP_NOW + esp_now_send(roverAddress, (uint8_t*)&pkt, sizeof(ControlPacket)); +#endif + } + } else { + + // Extract JSON telemetry from Wokwi Serial Bridge + int start = line.indexOf('{'); + int end = line.lastIndexOf('}'); + if (start >= 0 && end > start) { + String jsonSub = line.substring(start, end + 1); + + // Keep the LCD alive + lastDataTime = millis(); + Serial.println("RX: " + jsonSub); + + if (jsonSub.indexOf("\"type\":\"scan\"") > 0) { + if (mqttClient.connected()) mqttClient.publish(MQTT_TOPIC_SCAN, jsonSub.c_str()); + } else { + if (mqttClient.connected()) mqttClient.publish(MQTT_TOPIC_TELEMETRY, jsonSub.c_str()); + + // Parse to update LCD + JsonDocument doc; + DeserializationError error = deserializeJson(doc, jsonSub); + if (!error) { + digitalWrite(27, HIGH); // GREEN ON + digitalWrite(26, LOW); // RED OFF + latestTel.temperature = doc["t"] | 0.0f; + latestTel.temperature = doc["t"] | 0.0f; + latestTel.humidity = doc["h"] | 0.0f; + latestTel.ax = doc["ax"] | 0.0f; + latestTel.ay = doc["ay"] | 0.0f; + latestTel.az = doc["az"] | 1.0f; + latestTel.gx = doc["gx"] | 0.0f; + latestTel.gy = doc["gy"] | 0.0f; + latestTel.gz = doc["gz"] | 0.0f; + latestTel.gasRaw = doc["gas"] | 0; + latestTel.waterRaw = doc["water"] | 0; + latestTel.dangerState = doc["danger"] | 0; + } + } + } + } + } - // --- Link lost check (no data for 3s, only when NOT in demo) --- - if (!demoActive && linkActive && (now - lastDataTime) > 3000) { - linkActive = false; - showLinkLost(); - digitalWrite(LED_GREEN_PIN, LOW); - // Blink red LED - digitalWrite(LED_RED_PIN, (now / 500) % 2 == 0 ? HIGH : LOW); - } } diff --git a/diagram.json b/diagram.json index 3d3fb6b..064ea7b 100644 --- a/diagram.json +++ b/diagram.json @@ -86,10 +86,10 @@ { "type": "wokwi-text", "id": "front_label", - "top": -401, - "left": 380, + "top": -390, + "left": 350, "attrs": { - "text": "HC-SR04 MOUNTED ON SG90 SCAN SERVO - ROTATES TO SCAN LEFT/RIGHT" + "text": "HC-SR04 FIXED FORWARD SENSOR (TRIG=19, ECHO=18)" } }, { @@ -160,24 +160,6 @@ "value": "10000" } }, - { - "type": "wokwi-potentiometer", - "id": "tof_sim", - "top": -344, - "left": 226, - "attrs": { - "value": "512" - } - }, - { - "type": "wokwi-text", - "id": "tof_label", - "top": -290, - "left": 113, - "attrs": { - "text": "VL53L0X SIM: POT GPIO39 | REAL SENSOR: I2C GPIO21 / GPIO22" - } - }, { "type": "wokwi-servo", "id": "scan_servo", @@ -190,10 +172,10 @@ { "type": "wokwi-text", "id": "servo_label", - "top": -401, - "left": 620, + "top": -390, + "left": 600, "attrs": { - "text": "SG90 SCANNING SERVO - GPIO13 - CARRIES THE HC-SR04" + "text": "SG90 SCANNING SERVO (GPIO13)" } }, { @@ -290,213 +272,715 @@ }, { "type": "wokwi-text", - "id": "driver_label", - "top": 61, - "left": 189, - "attrs": { - "text": "4WD MOTOR OUTPUTS - LEDS STAND IN FOR TB6612 + DC MOTORS" - } - }, - { - "type": "wokwi-led", - "id": "motor_front_left", - "top": 115, - "left": 270, + "id": "powerbank_label", + "top": 413, + "left": 134, "attrs": { - "color": "green", - "label": "FRONT LEFT" + "text": "REAL PROTOTYPE POWER: 10000mAh USB POWER BANK - 5V / 3A PREFERRED" } }, { - "type": "wokwi-led", - "id": "motor_front_right", - "top": 115, - "left": 411, + "type": "wokwi-text", + "id": "receiver_label", + "top": -454, + "left": 125, "attrs": { - "color": "blue", - "label": "FRONT RIGHT" + "text": "REAL LINK: ESP-NOW -> RECEIVER ESP32 -> USB -> NODE.JS DASHBOARD" } }, { - "type": "wokwi-led", - "id": "motor_rear_left", - "top": 239, - "left": 270, + "type": "wokwi-text", + "id": "simulation_note", + "top": 486, + "left": 143, "attrs": { - "color": "green", - "label": "REAR LEFT" + "text": "WOKWI: ONE MCU PER PROJECT; RECEIVER + ESP-NOW REQUIRE REAL HARDWARE" } }, { - "type": "wokwi-led", - "id": "motor_rear_right", - "top": 239, - "left": 411, + "type": "wokwi-text", + "id": "safety_note", + "top": 532, + "left": 143, "attrs": { - "color": "blue", - "label": "REAR RIGHT" + "text": "SAFETY: HC-SR04 ECHO + MQ ANALOG OUTPUT USE 10k / 15k DIVIDERS" } }, { - "type": "wokwi-resistor", - "id": "motor_fl_resistor", - "top": 154, - "left": 189, + "type": "chip-vl53l0x", + "id": "tof", + "top": -344, + "left": 226, "attrs": { - "value": "220" + "distance": "500" } }, { - "type": "wokwi-resistor", - "id": "motor_fr_resistor", - "top": 154, - "left": 456, + "type": "wokwi-text", + "id": "tof_label", + "top": -390, + "left": 100, "attrs": { - "value": "220" + "text": "VL53L0X ToF SENSOR (ON SERVO) - I2C (21, 22)" } }, { - "type": "wokwi-resistor", - "id": "motor_rl_resistor", - "top": 279, - "left": 189, - "attrs": { - "value": "220" - } + "type": "chip-tb6612fng", + "id": "driver", + "top": 180, + "left": 350, + "attrs": {} }, { - "type": "wokwi-resistor", - "id": "motor_rr_resistor", - "top": 279, - "left": 456, + "type": "wokwi-text", + "id": "driver_label", + "top": 130, + "left": 240, "attrs": { - "value": "220" + "text": "TB6612FNG MOTOR DRIVER - ChA: LEFT (25,16,17) | ChB: RIGHT (14,33,2) | STBY: 5" } }, { - "type": "wokwi-text", - "id": "motor_pin_label", - "top": 347, - "left": 174, + "type": "wokwi-led", + "id": "tt_motor_fl", + "top": 140, + "left": 180, "attrs": { - "text": "LEFT PWM GPIO25 | RIGHT PWM GPIO14 | REAL MOTORS REQUIRE A DRIVER" + "color": "yellow", + "label": "TT MOTOR FL (LEFT)" } }, { - "type": "wokwi-text", - "id": "powerbank_label", - "top": 413, - "left": 134, + "type": "wokwi-led", + "id": "tt_motor_rl", + "top": 240, + "left": 180, "attrs": { - "text": "REAL PROTOTYPE POWER: 10000mAh USB POWER BANK - 5V / 3A PREFERRED" + "color": "yellow", + "label": "TT MOTOR RL (LEFT)" } }, { - "type": "wokwi-text", - "id": "receiver_label", - "top": -454, - "left": 125, + "type": "wokwi-led", + "id": "tt_motor_fr", + "top": 140, + "left": 550, "attrs": { - "text": "REAL LINK: ESP-NOW -> RECEIVER ESP32 -> USB -> NODE.JS DASHBOARD" + "color": "cyan", + "label": "TT MOTOR FR (RIGHT)" } }, { - "type": "wokwi-text", - "id": "simulation_note", - "top": 486, - "left": 143, + "type": "wokwi-led", + "id": "tt_motor_rr", + "top": 240, + "left": 550, "attrs": { - "text": "WOKWI: ONE MCU PER PROJECT; RECEIVER + ESP-NOW REQUIRE REAL HARDWARE" + "color": "cyan", + "label": "TT MOTOR RR (RIGHT)" } }, { "type": "wokwi-text", - "id": "safety_note", - "top": 532, - "left": 143, + "id": "tt_motors_label", + "top": 320, + "left": 200, "attrs": { - "text": "SAFETY: HC-SR04 ECHO + MQ ANALOG OUTPUT USE 10k / 15k DIVIDERS" + "text": "4x TT DC MOTORS - LEFT PAIR WIRED IN PARALLEL TO AO1/AO2 | RIGHT PAIR TO BO1/BO2" } } ], "connections": [ - ["esp:TX", "$serialMonitor:RX", "", []], - ["esp:RX", "$serialMonitor:TX", "", []], - - ["dht:VCC", "esp:3V3", "red", ["h36", "v75"]], - ["dht:GND", "esp:GND.1", "black", ["h42", "v120"]], - ["dht:SDA", "esp:23", "green", ["h55"]], - ["dht_pullup:1", "esp:3V3", "red", ["v-18"]], - ["dht_pullup:2", "dht:SDA", "green", ["h-22"]], - - ["gas:VCC", "esp:3V3", "red", ["h25", "v190"]], - ["gas:GND", "esp:GND.1", "black", ["h30", "v95"]], - ["gas:SIG", "esp:34", "orange", ["h35", "v120"]], - - ["front:VCC", "esp:5V", "red", ["h700", "v-260"]], - ["front:GND", "esp:GND.1", "black", ["h705", "v-230"]], - ["front:TRIG", "esp:19", "cyan", ["h-680"]], - ["front:ECHO", "echo_r_upper:1", "yellow", ["h-680", "v420"]], - ["echo_r_upper:2", "esp:18", "yellow", ["h16"]], - ["echo_r_upper:2", "echo_r_lower:1", "yellow", ["h9", "v22"]], - ["echo_r_lower:2", "esp:GND.2", "black", ["v20"]], - - ["front:VCC", "scan_servo:V+", "red", ["v10", "h150"]], - - ["imu:VCC", "esp:3V3", "red", ["h29", "v-100"]], - ["imu:GND", "esp:GND.2", "black", ["h34", "v-55"]], - ["imu:SDA", "esp:21", "blue", ["h56"]], - ["imu:SCL", "esp:22", "purple", ["h72"]], - - ["encoder_left:VCC", "esp:3V3", "red", ["h22", "v-108"]], - ["encoder_left:GND", "esp:GND.2", "black", ["h33", "v-90"]], - ["encoder_left:CLK", "esp:32", "green", ["h49"]], - - ["encoder_right:VCC", "esp:3V3", "red", ["h21", "v-100"]], - ["encoder_right:GND", "esp:GND.2", "black", ["h29", "v-73"]], - ["encoder_right:CLK", "esp:35", "green", ["h42"]], - ["encoder_right_pullup:1", "esp:3V3", "red", ["v-26"]], - ["encoder_right_pullup:2", "encoder_right:CLK", "green", ["h-26"]], - - ["tof_sim:VCC", "esp:3V3", "red", ["v24", "h-72"]], - ["tof_sim:GND", "esp:GND.1", "black", ["v30", "h-60"]], - ["tof_sim:SIG", "esp:VN", "orange", ["v39", "h-42"]], - - ["water_sim:VCC", "esp:3V3", "red", ["v36", "h-118"]], - ["water_sim:GND", "esp:GND.2", "black", ["v47", "h-129"]], - ["water_sim:SIG", "esp:VP", "cyan", ["v59", "h-140"]], - - ["scan_servo:V+", "esp:5V", "red", ["v44", "h-61"]], - ["scan_servo:GND", "esp:GND.1", "black", ["v57", "h-71"]], - ["scan_servo:PWM", "esp:13", "purple", ["v70", "h-81"]], - - ["buzzer:1", "esp:GND.2", "black", ["v33", "h-68"]], - ["buzzer:2", "buzzer_resistor:1", "orange", ["v21"]], - ["buzzer_resistor:2", "esp:4", "orange", ["h-32"]], - - ["status_red:A", "status_red_resistor:1", "red", ["v-15"]], - ["status_red_resistor:2", "esp:26", "red", ["h-40", "v-20"]], - ["status_red:C", "esp:GND.2", "black", ["v40", "h-90"]], - - ["status_green:A", "status_green_resistor:1", "green", ["v-15"]], - ["status_green_resistor:2", "esp:27", "green", ["h-40", "v-20"]], - ["status_green:C", "esp:GND.2", "black", ["v40", "h-190"]], - - ["esp:25", "motor_fl_resistor:1", "green", ["h55", "v45"]], - ["motor_fl_resistor:2", "motor_front_left:A", "green", ["h16"]], - ["motor_front_left:C", "esp:GND.2", "black", ["v31", "h-91"]], - - ["esp:25", "motor_rl_resistor:1", "green", ["h63", "v145"]], - ["motor_rl_resistor:2", "motor_rear_left:A", "green", ["h16"]], - ["motor_rear_left:C", "esp:GND.2", "black", ["v28", "h-99"]], - - ["esp:14", "motor_fr_resistor:1", "blue", ["h89", "v64"]], - ["motor_fr_resistor:2", "motor_front_right:A", "blue", ["h-17"]], - ["motor_front_right:C", "esp:GND.2", "black", ["v42", "h-135"]], - - ["esp:14", "motor_rr_resistor:1", "blue", ["h97", "v164"]], - ["motor_rr_resistor:2", "motor_rear_right:A", "blue", ["h-17"]], - ["motor_rear_right:C", "esp:GND.2", "black", ["v34", "h-144"]] + [ + "esp:TX", + "$serialMonitor:RX", + "", + [] + ], + [ + "esp:RX", + "$serialMonitor:TX", + "", + [] + ], + [ + "dht:VCC", + "esp:3V3", + "red", + [ + "h36", + "v75" + ] + ], + [ + "dht:GND", + "esp:GND.1", + "black", + [ + "h42", + "v120" + ] + ], + [ + "dht:SDA", + "esp:23", + "green", + [ + "h55" + ] + ], + [ + "dht_pullup:1", + "esp:3V3", + "red", + [ + "v-18" + ] + ], + [ + "dht_pullup:2", + "dht:SDA", + "green", + [ + "h-22" + ] + ], + [ + "gas:VCC", + "esp:3V3", + "red", + [ + "h25", + "v190" + ] + ], + [ + "gas:GND", + "esp:GND.1", + "black", + [ + "h30", + "v95" + ] + ], + [ + "gas:SIG", + "esp:34", + "orange", + [ + "h35", + "v120" + ] + ], + [ + "front:VCC", + "esp:5V", + "red", + [ + "h700", + "v-260" + ] + ], + [ + "front:GND", + "esp:GND.1", + "black", + [ + "h705", + "v-230" + ] + ], + [ + "front:TRIG", + "esp:19", + "cyan", + [ + "h-680" + ] + ], + [ + "front:ECHO", + "echo_r_upper:1", + "yellow", + [ + "h-680", + "v420" + ] + ], + [ + "echo_r_upper:2", + "esp:18", + "yellow", + [ + "h16" + ] + ], + [ + "echo_r_upper:2", + "echo_r_lower:1", + "yellow", + [ + "h9", + "v22" + ] + ], + [ + "echo_r_lower:2", + "esp:GND.2", + "black", + [ + "v20" + ] + ], + [ + "front:VCC", + "scan_servo:V+", + "red", + [ + "v10", + "h150" + ] + ], + [ + "imu:VCC", + "esp:3V3", + "red", + [ + "h29", + "v-100" + ] + ], + [ + "imu:GND", + "esp:GND.2", + "black", + [ + "h34", + "v-55" + ] + ], + [ + "imu:SDA", + "esp:21", + "blue", + [ + "h56" + ] + ], + [ + "imu:SCL", + "esp:22", + "purple", + [ + "h72" + ] + ], + [ + "encoder_left:VCC", + "esp:3V3", + "red", + [ + "h22", + "v-108" + ] + ], + [ + "encoder_left:GND", + "esp:GND.2", + "black", + [ + "h33", + "v-90" + ] + ], + [ + "encoder_left:CLK", + "esp:32", + "green", + [ + "h49" + ] + ], + [ + "encoder_right:VCC", + "esp:3V3", + "red", + [ + "h21", + "v-100" + ] + ], + [ + "encoder_right:GND", + "esp:GND.2", + "black", + [ + "h29", + "v-73" + ] + ], + [ + "encoder_right:CLK", + "esp:35", + "green", + [ + "h42" + ] + ], + [ + "encoder_right_pullup:1", + "esp:3V3", + "red", + [ + "v-26" + ] + ], + [ + "encoder_right_pullup:2", + "encoder_right:CLK", + "green", + [ + "h-26" + ] + ], + [ + "water_sim:VCC", + "esp:3V3", + "red", + [ + "v36", + "h-118" + ] + ], + [ + "water_sim:GND", + "esp:GND.2", + "black", + [ + "v47", + "h-129" + ] + ], + [ + "water_sim:SIG", + "esp:VP", + "cyan", + [ + "v59", + "h-140" + ] + ], + [ + "scan_servo:V+", + "esp:5V", + "red", + [ + "v44", + "h-61" + ] + ], + [ + "scan_servo:GND", + "esp:GND.1", + "black", + [ + "v57", + "h-71" + ] + ], + [ + "scan_servo:PWM", + "esp:13", + "purple", + [ + "v70", + "h-81" + ] + ], + [ + "buzzer:1", + "esp:GND.2", + "black", + [ + "v33", + "h-68" + ] + ], + [ + "buzzer:2", + "buzzer_resistor:1", + "orange", + [ + "v21" + ] + ], + [ + "buzzer_resistor:2", + "esp:4", + "orange", + [ + "h-32" + ] + ], + [ + "status_red:A", + "status_red_resistor:1", + "red", + [ + "v-15" + ] + ], + [ + "status_red_resistor:2", + "esp:26", + "red", + [ + "h-40", + "v-20" + ] + ], + [ + "status_red:C", + "esp:GND.2", + "black", + [ + "v40", + "h-90" + ] + ], + [ + "status_green:A", + "status_green_resistor:1", + "green", + [ + "v-15" + ] + ], + [ + "status_green_resistor:2", + "esp:27", + "green", + [ + "h-40", + "v-20" + ] + ], + [ + "status_green:C", + "esp:GND.2", + "black", + [ + "v40", + "h-190" + ] + ], + [ + "tof:VCC", + "esp:3V3", + "red", + [ + "v24", + "h-72" + ] + ], + [ + "tof:GND", + "esp:GND.1", + "black", + [ + "v30", + "h-60" + ] + ], + [ + "tof:SDA", + "esp:21", + "blue", + [ + "v39", + "h-42" + ] + ], + [ + "tof:SCL", + "esp:22", + "purple", + [ + "v48", + "h-54" + ] + ], + [ + "driver:VM", + "esp:5V", + "red", + [ + "v-30", + "h-80" + ] + ], + [ + "driver:VCC", + "esp:3V3", + "red", + [ + "v-20", + "h-80" + ] + ], + [ + "driver:GND", + "esp:GND.2", + "black", + [ + "v10", + "h-80" + ] + ], + [ + "driver:GND2", + "esp:GND.2", + "black", + [ + "v20", + "h-80" + ] + ], + [ + "driver:STBY", + "esp:5", + "purple", + [ + "v-10", + "h80" + ] + ], + [ + "driver:PWMA", + "esp:25", + "green", + [ + "v-40", + "h80" + ] + ], + [ + "driver:AIN1", + "esp:16", + "blue", + [ + "v-50", + "h80" + ] + ], + [ + "driver:AIN2", + "esp:17", + "orange", + [ + "v-60", + "h80" + ] + ], + [ + "driver:PWMB", + "esp:14", + "green", + [ + "v40", + "h80" + ] + ], + [ + "driver:BIN1", + "esp:33", + "blue", + [ + "v50", + "h80" + ] + ], + [ + "driver:BIN2", + "esp:2", + "orange", + [ + "v60", + "h80" + ] + ], + [ + "tt_motor_fl:A", + "driver:AO1", + "yellow", + [ + "h-20", + "v40" + ] + ], + [ + "tt_motor_fl:C", + "driver:AO2", + "black", + [ + "h-30", + "v60" + ] + ], + [ + "tt_motor_rl:A", + "driver:AO1", + "yellow", + [ + "h-20", + "v-40" + ] + ], + [ + "tt_motor_rl:C", + "driver:AO2", + "black", + [ + "h-30", + "v-20" + ] + ], + [ + "tt_motor_fr:A", + "driver:BO1", + "cyan", + [ + "h20", + "v40" + ] + ], + [ + "tt_motor_fr:C", + "driver:BO2", + "black", + [ + "h30", + "v60" + ] + ], + [ + "tt_motor_rr:A", + "driver:BO1", + "cyan", + [ + "h20", + "v-40" + ] + ], + [ + "tt_motor_rr:C", + "driver:BO2", + "black", + [ + "h30", + "v-20" + ] + ] ], "dependencies": {} } \ No newline at end of file diff --git a/dist/tb6612fng_v2.json b/dist/tb6612fng_v2.json new file mode 100644 index 0000000..7024e82 --- /dev/null +++ b/dist/tb6612fng_v2.json @@ -0,0 +1,5 @@ +{ + "name": "tb6612fng", + "author": "", + "pins": ["VM","VCC","GND","AO1","AO2","BO2","BO1","GND2","PWMB","BIN2","BIN1","STBY","AIN1","AIN2","PWMA"] +} diff --git a/dist/tb6612fng_v2.wasm b/dist/tb6612fng_v2.wasm new file mode 100755 index 0000000..626c97f Binary files /dev/null and b/dist/tb6612fng_v2.wasm differ diff --git a/dist/vl53l0x_v2.json b/dist/vl53l0x_v2.json new file mode 100644 index 0000000..3e4b889 --- /dev/null +++ b/dist/vl53l0x_v2.json @@ -0,0 +1,15 @@ +{ + "name": "vl53l0x", + "author": "", + "pins": ["VCC", "GND", "SCL", "SDA", "XSHUT", "GPIO1"], + "controls": [ + { + "id": "distance", + "label": "Distance (mm)", + "type": "range", + "min": 30, + "max": 2000, + "step": 1 + } + ] +} diff --git a/dist/vl53l0x_v2.wasm b/dist/vl53l0x_v2.wasm new file mode 100755 index 0000000..fa5a7c4 Binary files /dev/null and b/dist/vl53l0x_v2.wasm differ diff --git a/docs/HARDWARE_BOM.md b/docs/HARDWARE_BOM.md new file mode 100644 index 0000000..2d1069f --- /dev/null +++ b/docs/HARDWARE_BOM.md @@ -0,0 +1,177 @@ +# DEEPTRACK — Complete Hardware Shopping List + +Everything you need to buy for both the Rover and the Gateway. +All audit fixes are baked in (separate 5V bus regulated by a buck converter, MQ-4 voltage divider, decoupling caps). Both ESP32 boards use **USB Type-C**, not Micro-USB. + +> Search on **Robu.in**, **Amazon.in**, **Quartz Components**, or your local electronics shop. +> Prices are approximate mid-2026 INR. + +--- + +## ROVER (Goes Inside the Mine) + +### Brain + Chassis + Power + +| # | What to Search | Spec | Qty | ₹ Est. | Why | +|---|---|---|---|---|---| +| 1 | ESP32 DevKit V1 (30-pin) | ESP32-WROOM-32, **USB Type-C** | 1 | 350–500 | Rover MCU. 30-pin fits breadboard with 1 row free on each side | +| 2 | 4WD Robot Car Chassis Kit | Acrylic 2-layer, includes 4× TT motors + 4× 65mm wheels + screws + standoffs | 1 kit | 400–650 | If your kit does NOT include motors/wheels, buy 4× "BO Motor 200RPM" (₹40–60 each) and 4× "65mm Robot Wheel" (₹20–30 each) separately | +| 3 | 10000mAh Power Bank | 5V output, 2A or higher. USB-A output port | 1 | 500–900 | Must sustain 2A+ continuous draw. Avoid ultra-slim (auto-shutoff on low current). Mi/Ambrane/Syska work well | +| 4 | USB-A to Type-C Cable (short) | 30cm data cable | 1 | 60–100 | Power bank → ESP32 (rover's ESP32 has a Type-C port, not Micro-USB). Keep it short so it doesn't snag on the chassis | +| 5 | USB-A Breakout Board | Female USB-A socket on a small PCB with screw terminals (5V, GND) | 1 | 30–50 | Cut a spare USB cable OR use this breakout to tap 5V directly from the power bank for the separate 5V bus. Feeds into the buck converter below. See Wiring Guide for details | +| 5a | **Buck Converter Module** (LM2596 or MP1584EN) | Adjustable DC-DC step-down, screw terminals or header pins, input up to 24–40V, output adjustable via trimpot | 1 | 60–120 | Regulates the tap from the power bank/battery down to a clean, stable **5V** for the separate 5V bus (motors, servo, MQ-4, HC-SR04). Trim the output to 5.0V with a multimeter **before** connecting any sensor. Sits between the USB-A Breakout Board and the 5V bus | + +### Motor Driver + +| # | What to Search | Spec | Qty | ₹ Est. | Why | +|---|---|---|---|---|---| +| 6 | TB6612FNG Dual Motor Driver Module | Breakout board (NOT bare chip). Pins: VM, VCC, GND, STBY, AIN1, AIN2, PWMA, AO1, AO2, BIN1, BIN2, PWMB, BO1, BO2 | 2 | 400–700 | One H-bridge channel per motor. Two modules provide 4 independent output channels, avoiding two TT motors sharing one channel | + +### Sensors + +| # | What to Search | Spec | Qty | ₹ Est. | Why | +|---|---|---|---|---|---| +| 7 | DHT22 Sensor Module (3-pin) | AM2302 on a small PCB, 3 pins: VCC, DATA, GND | 1 | 180–280 | Temperature + humidity. The 3-pin module has an onboard 10kΩ pull-up. If you get the bare 4-pin sensor instead, you must add your own 10kΩ pull-up to 3V3 | +| 8 | MQ-4 Gas Sensor Module | Blue PCB, 4 pins: VCC, GND, AO, DO | 1 | 150–220 | Methane (CH4) detection. **Must be powered from 5V** (heater coil). AO output is 0–5V, needs voltage divider to GPIO (included in resistors below) | +| 9 | HC-SR04 Ultrasonic Sensor | 4-pin: VCC, TRIG, ECHO, GND | 1 | 50–90 | Forward obstacle distance. ECHO is 5V logic — needs voltage divider (included in resistors below) | +| 10 | MPU6050 GY-521 Module | 6-axis accel + gyro, I2C, address 0x68 | 1 | 120–200 | Tilt / orientation sensing. Onboard 3.3V regulator + I2C pull-ups | +| 11 | VL53L0X GY-VL53L0XV2 Module | Time-of-Flight laser distance, I2C, address 0x29 | 1 | 250–400 | Mounts on the servo for sweeping obstacle scan. Onboard regulator + pull-ups | +| 12 | LM393 Speed Sensor Module | Slotted optical sensor + comparator PCB + encoder disc | 2 | 40–70 each | One per side. Disc attaches to motor shaft, sensor counts slots. DO = digital pulse output | +| 13 | Water Level Sensor Module | Flat PCB with exposed traces, 3 pins: VCC, GND, SIG | 1 | 30–60 | Flood detection. Power from 3.3V so output stays within ESP32 ADC range | + +### Actuators + Indicators + +| # | What to Search | Spec | Qty | ₹ Est. | Why | +|---|---|---|---|---|---| +| 14 | SG90 Micro Servo 9g | 180° rotation, 3 wires (brown=GND, red=5V, orange=signal) | 1 | 80–130 | Mounts the VL53L0X for scanning sweep (30°–150°). Powered from 5V bus | +| 15 | Active Piezo Buzzer | 5V, 2-pin | 1 | 15–30 | Danger alarm. Code uses `tone()` so a passive buzzer also works | +| 16 | 5mm Red LED | Standard through-hole | 1 | 5–10 | Danger indicator | +| 17 | 5mm Green LED | Standard through-hole | 1 | 5–10 | Normal status | + +### Resistors + Capacitors + +| # | What to Search | Value | Qty | ₹ Est. | Purpose | +|---|---|---|---|---|---| +| 18 | 10kΩ Resistor ¼W | 10kΩ | 4 | 2–5 each | 1× DHT22 pull-up (skip if 3-pin module), 1× HC-SR04 ECHO divider upper, 1× MQ-4 AO divider upper, 1× right encoder pull-up | +| 19 | 15kΩ Resistor ¼W | 15kΩ | 2 | 2–5 each | 1× HC-SR04 ECHO divider lower, 1× MQ-4 AO divider lower | +| 20 | 220Ω Resistor ¼W | 220Ω | 3 | 2–5 each | 1× buzzer, 1× red LED, 1× green LED | +| 21 | 470µF Electrolytic Capacitor | 470µF 16V (or 25V) | 2 | 5–10 | One near each TB6612 VM/GND input to absorb motor startup/current spikes | +| 22 | 100nF Ceramic Capacitor | 0.1µF (code "104") | 3 | 2–5 each | 1× across ESP32 3V3/GND, 1× across each TB6612 VCC/GND | + +> **Tip:** Buy a resistor assortment kit (₹80–120) and a capacitor assortment kit (₹60–100) instead of individual pieces. You'll have spares for mistakes. + +--- + +## GATEWAY (Sits at Base Desk, Connected to Laptop) + +| # | What to Search | Spec | Qty | ₹ Est. | Why | +|---|---|---|---|---|---| +| 23 | ESP32 DevKit V1 (30-pin) | Same as Rover — **USB Type-C** | 1 | 350–500 | Receives ESP-NOW from Rover, shows on LCD, sends to laptop via USB Serial | +| 24 | 16×2 I2C LCD Module | LCD1602 with PCF8574 I2C backpack soldered on. 4 pins: VCC, GND, SDA, SCL. Address 0x27 | 1 | 150–220 | Displays live telemetry. Blue backlight / white text. Has a small blue contrast potentiometer on the back — adjust with screwdriver until text is visible | +| 25 | 5mm Red LED | Standard | 1 | 5–10 | Danger | +| 26 | 5mm Green LED | Standard | 1 | 5–10 | Normal / Link OK | +| 27 | 5mm Yellow LED | Standard | 1 | 5–10 | Heartbeat — blinks on each received packet | +| 28 | 220Ω Resistor ¼W | 220Ω | 3 | 2–5 each | 1 per LED | +| 29 | **USB-C to USB-C Cable** (1–1.5m) | Data-capable cable, 1–1.5m | 1 | 100–180 | Gateway ESP32 → Laptop. **No USB-A cable is used here** — the Gateway ESP32 has a Type-C port, so this needs a laptop with a USB-C port. If your laptop only has USB-A ports, get a USB-C-to-USB-A cable instead (see note below) | +| 29a | **Buck Converter Module** (LM2596 or MP1584EN) | Adjustable DC-DC step-down, screw terminals or header pins | 1 | 60–120 | Regulates the Gateway's 5V rail (LCD + LEDs + ESP32) to a clean, trimmed 5.0V — same role as the Rover's buck converter, just on the Gateway side. Sits between the incoming 5V source and the Gateway's 5V bus | + +--- + +## WIRING + ASSEMBLY SUPPLIES + +| # | What to Search | Qty | ₹ Est. | Why | +|---|---|---|---|---| +| 30 | Half-size Breadboard (400 pts) | 2 | 80–120 each | 1 for Rover (mounts on chassis), 1 for Gateway | +| 31 | Male-to-Male Jumper Wires 20cm | 1 pack (40 wires) | 80–120 | Breadboard-to-breadboard | +| 32 | Male-to-Female Jumper Wires 20cm | 1 pack (40 wires) | 80–120 | ESP32 headers → sensor modules | +| 33 | Female-to-Female Jumper Wires 20cm | 1 pack (20 wires) | 60–80 | Module-to-module when both have male headers | +| 34 | Double-sided foam tape | 1 roll | 20–30 | Mount breadboard + sensors on chassis | +| 35 | Small zip ties | 1 pack | 20–30 | Cable management on chassis | +| 36 | Small Phillips screwdriver | 1 | 20–40 | Chassis assembly + LCD contrast adjustment | + +--- + +## DASHBOARD CONNECTION (How the Cable Works) + +> **Cable note:** Both ESP32 boards in this list use **USB Type-C**, not Micro-USB. If your laptop has a USB-A port and no USB-A cable is on hand, buy a **USB-C to USB-A** cable instead of Type-C to Type-C for item #29 — either works as the data + power link, just match it to the port your laptop actually has. + +The Gateway ESP32 stays plugged into your laptop via USB cable (#29). This single cable: +1. **Powers the Gateway** — 5V from laptop USB +2. **Carries serial data** — Gateway prints JSON at 115200 baud + +On the laptop: +1. Open `DASHBOARD/dashboard.html` in **Google Chrome** (or Edge / Chromium) +2. Click **"Connect Serial"** +3. Select the Gateway's port (`/dev/ttyUSB0` on Linux, `COMx` on Windows) +4. Dashboard goes live — gauges, path trace, and event log update in real-time + +> **Web Serial requires Chrome/Edge/Chromium.** Firefox and Safari do NOT support it. + +> **Linux permission fix** if you get "Permission denied": +> ``` +> sudo usermod -a -G dialout $USER +> ``` +> Log out and back in. + +--- + +## COST SUMMARY + +| Section | ₹ Estimate | +|---|---| +| Rover (ESP32 + Chassis + Sensors + Driver + Actuators + Passives + Caps + Buck Converter) | 2,470 – 3,990 | +| Gateway (ESP32 + LCD + LEDs + Resistors + Cable + Buck Converter) | 760–1,270 | +| Wiring Supplies (Breadboards + Wires + Tape) | 350–550 | +| **TOTAL** | **3,580 – 5,810** | + +> If you already own a 10000mAh power bank, subtract ₹500–900. + +--- + +## PRINTABLE CHECKLIST + +```text +ROVER: +[ ] ESP32 DevKit V1 ×1 +[ ] 4WD Chassis Kit (4× TT motors, 4× wheels, acrylic frame) ×1 +[ ] TB6612FNG Motor Driver Module ×2 +[ ] DHT22 Sensor Module (3-pin preferred) ×1 +[ ] MQ-4 Gas Sensor Module ×1 +[ ] HC-SR04 Ultrasonic Sensor ×1 +[ ] MPU6050 GY-521 Module ×1 +[ ] VL53L0X GY-VL53L0XV2 Module ×1 +[ ] LM393 Speed Sensor + Encoder Disc ×2 +[ ] Water Level Sensor Module ×1 +[ ] SG90 Micro Servo ×1 +[ ] Active Piezo Buzzer ×1 +[ ] 5mm Red LED ×1 +[ ] 5mm Green LED ×1 +[ ] 10kΩ Resistors ×4 +[ ] 15kΩ Resistors ×2 +[ ] 220Ω Resistors ×3 +[ ] 470µF Electrolytic Capacitor ×2 +[ ] 100nF Ceramic Capacitors ×3 +[ ] 10000mAh Power Bank ×1 +[ ] Short USB-A to Type-C Cable (30cm) ×1 +[ ] USB-A Breakout Board ×1 +[ ] Buck Converter Module (LM2596/MP1584EN) ×1 + +GATEWAY: +[ ] ESP32 DevKit V1 (USB Type-C) ×1 +[ ] 16×2 I2C LCD (PCF8574) ×1 +[ ] 5mm Red LED ×1 +[ ] 5mm Green LED ×1 +[ ] 5mm Yellow LED ×1 +[ ] 220Ω Resistors ×3 +[ ] USB-C to USB-C (or USB-C to USB-A) Cable (1–1.5m) ×1 +[ ] Buck Converter Module (LM2596/MP1584EN) ×1 + +WIRING: +[ ] Breadboards ×2 +[ ] M-M Jumper Wires (40pc) ×1 +[ ] M-F Jumper Wires (40pc) ×1 +[ ] F-F Jumper Wires (20pc) ×1 +[ ] Double-sided foam tape ×1 +[ ] Zip ties ×1 +[ ] Small screwdriver ×1 +``` diff --git a/docs/WIRING_GUIDE.md b/docs/WIRING_GUIDE.md new file mode 100644 index 0000000..57e1752 --- /dev/null +++ b/docs/WIRING_GUIDE.md @@ -0,0 +1,450 @@ +# DEEPTRACK — Circuit Wiring Guide + +Step-by-step pin-by-pin wiring for both the Rover and Gateway. +All connections are **corrected** — diagram simulation shortcuts are replaced with real-hardware-safe wiring. + +--- + +## CRITICAL DIFFERENCE FROM WOKWI DIAGRAM + +The Wokwi diagram routes all 5V loads through the ESP32's tiny 5V pin. On real hardware this **will** brownout and potentially damage the board. This guide uses a **separate 5V power bus**. + +### Power Bus Setup (Do This First) + +``` +POWER BANK (USB-A port) + │ + ├──[USB-A Breakout Board or cut USB cable]──→ +5V BUS (red wire) + │ │ + │ ├→ ESP32 VIN pin + │ ├→ TB6612FNG VM pin + │ ├→ SG90 Servo V+ (red wire) + │ ├→ HC-SR04 VCC + │ ├→ MQ-4 Module VCC + │ │ + │ [470µF cap across +5V and GND] + │ + └──────────────────────────────────────→ GND BUS (black wire) + │ + ├→ ESP32 GND (any GND pin) + ├→ TB6612FNG GND (both GND pins) + ├→ All sensor GND pins + ├→ All LED cathodes (via their circuits) + └→ Buzzer negative pin +``` + +**How to tap 5V from the power bank:** +- **Option A (cleanest):** Use a USB-A female breakout board. Plug the power bank's USB cable into it. The breakout exposes 5V and GND as screw terminals or header pins. +- **Option B (quick):** Cut a spare USB-A cable. The **red** wire is +5V, **black** is GND. Strip, tin, and connect to breadboard rails. + +The ESP32 gets its 5V through the **VIN** pin (not through Micro-USB in this setup). The onboard AMS1117 regulator converts VIN → 3.3V for the ESP32's logic. + +> You can ALSO power the ESP32 via its Micro-USB (plug a second cable from the power bank). In that case, skip connecting VIN and use the ESP32's `5V` pin only for the 3.3V LDO input — do NOT draw motor/servo current from it. + +--- + +## ROVER ESP32 — Complete Pin Map + +### GPIO Assignment Table + +| GPIO | Direction | Connected To | Wire Color | Notes | +|---|---|---|---|---| +| **VIN** | Power In | 5V Bus (+) | Red | Powers the ESP32 via onboard regulator | +| **GND** | Power | GND Bus | Black | Use multiple GND pins to distribute current | +| **3V3** | Power Out | 3.3V rail for logic sensors | Red | Max ~600mA from LDO. Feeds: DHT22, MPU6050, VL53L0X, encoders, water sensor | +| **23** | Output | DHT22 DATA | Green | + 10kΩ pull-up to 3V3 (skip if using 3-pin module) | +| **34** | ADC Input | MQ-4 AO (via voltage divider) | Orange | Input-only pin, no pull-up available | +| **19** | Output | HC-SR04 TRIG | Cyan | 3.3V trigger pulse, HC-SR04 accepts it fine | +| **18** | Input | HC-SR04 ECHO (via voltage divider) | Yellow | 5V → 3.0V through 10k/15k divider | +| **21** | I2C SDA | MPU6050 SDA + VL53L0X SDA | Blue | Shared I2C bus, different addresses | +| **22** | I2C SCL | MPU6050 SCL + VL53L0X SCL | Purple | Shared I2C bus | +| **32** | Input (interrupt) | Left LM393 Speed Sensor DO | Green | Rising edge interrupt for pulse counting | +| **35** | Input (interrupt) | Right LM393 Speed Sensor DO | Green | Input-only pin. Needs external 10kΩ pull-up to 3V3 | +| **36 (VP)** | ADC Input | Water Level Sensor SIG | Cyan | Input-only pin. Powered from 3V3, output 0–3.3V | +| **13** | PWM Output | SG90 Servo signal (orange wire) | Purple | 50Hz PWM for servo position | +| **4** | Output | Buzzer (+) via 220Ω resistor | Orange | `tone()` generates alarm frequency | +| **26** | Output | Red LED anode via 220Ω | Red | Danger indicator | +| **27** | Output | Green LED anode via 220Ω | Green | Normal indicator | +| **25** | PWM Output | TB6612FNG PWMA | Green | Left motor speed (LEDC CH0, 5kHz) | +| **16** | Output | TB6612FNG AIN1 | Blue | Left motor direction bit 1 | +| **17** | Output | TB6612FNG AIN2 | Orange | Left motor direction bit 2 | +| **14** | PWM Output | TB6612FNG PWMB | Green | Right motor speed (LEDC CH1, 5kHz) | +| **33** | Output | TB6612FNG BIN1 | Blue | Right motor direction bit 1 | +| **2** | Output | TB6612FNG BIN2 | Orange | Right motor direction bit 2. **Caution:** GPIO2 is a boot strapping pin — disconnect this wire if flashing fails | +| **5** | Output | TB6612FNG STBY | Purple | Pull HIGH to enable driver, LOW = standby | + +--- + +## ROVER — Step-by-Step Wiring + +### Step 1: Power Rails on Breadboard + +1. Run a **red wire** from the 5V bus to the breadboard's **+** rail (top) +2. Run a **black wire** from the GND bus to the breadboard's **−** rail (top) +3. Bridge the top and bottom power rails with jumper wires (red + to +, black − to −) +4. Plug the **ESP32** into the breadboard, centered, straddling the middle gap +5. Connect ESP32 **VIN** → breadboard **+5V rail** +6. Connect ESP32 **GND** (any GND pin) → breadboard **GND rail** +7. Place the **470µF electrolytic capacitor** across the +5V and GND rails. **Long leg (+) to +5V, short leg (−) to GND** + +> The ESP32's `3V3` output pin now provides 3.3V for logic-level sensors. + +### Step 2: DHT22 Temperature + Humidity Sensor + +``` +DHT22 Module (3-pin): + VCC → ESP32 3V3 + GND → GND rail + DATA → ESP32 GPIO 23 + +If using bare 4-pin DHT22: + Pin 1 (VCC) → ESP32 3V3 + Pin 2 (DATA) → ESP32 GPIO 23 + Pin 3 (not connected) + Pin 4 (GND) → GND rail + + 10kΩ resistor between Pin 1 (VCC/3V3) and Pin 2 (DATA) +``` + +### Step 3: MQ-4 Gas Sensor (with Voltage Divider) + +The MQ-4 module outputs 0–5V on AO. ESP32 GPIO34 max is 3.3V. The voltage divider scales 5V down to 3.0V. + +``` +MQ-4 Module: + VCC → 5V bus (NOT 3V3!) + GND → GND rail + AO → [10kΩ resistor] → junction point → ESP32 GPIO 34 + │ + [15kΩ resistor] + │ + GND rail + +Voltage at junction = 5V × 15k/(10k+15k) = 3.0V max ← safe +``` + +**Wiring on breadboard:** +1. MQ-4 AO pin → one end of 10kΩ resistor (Row A) +2. Other end of 10kΩ → junction row (Row B) +3. From junction row (Row B) → jumper wire to ESP32 GPIO34 +4. From junction row (Row B) → one end of 15kΩ resistor +5. Other end of 15kΩ → GND rail + +### Step 4: HC-SR04 Ultrasonic Sensor (with Voltage Divider) + +``` +HC-SR04: + VCC → 5V bus + GND → GND rail + TRIG → ESP32 GPIO 19 (direct, 3.3V trigger is accepted by HC-SR04) + ECHO → [10kΩ resistor] → junction → ESP32 GPIO 18 + │ + [15kΩ resistor] + │ + GND rail + +Voltage at junction = 5V × 15k/(10k+15k) = 3.0V max ← safe +``` + +Identical divider circuit to the MQ-4. Build it the same way on the breadboard. + +### Step 5: MPU6050 IMU (I2C Bus) + +``` +MPU6050 GY-521: + VCC → ESP32 3V3 + GND → GND rail + SDA → ESP32 GPIO 21 + SCL → ESP32 GPIO 22 + (AD0, INT, XDA, XCL — leave unconnected) +``` + +No external pull-ups needed — the GY-521 module has 4.7kΩ pull-ups onboard. + +### Step 6: VL53L0X ToF Sensor (I2C Bus — Shared with MPU6050) + +``` +VL53L0X GY-VL53L0XV2: + VIN → ESP32 3V3 + GND → GND rail + SDA → ESP32 GPIO 21 (same wire/row as MPU6050 SDA) + SCL → ESP32 GPIO 22 (same wire/row as MPU6050 SCL) + (XSHUT, GPIO1 — leave unconnected) +``` + +Both I2C devices share the same SDA/SCL lines. They have different addresses (MPU=0x68, VL53=0x29) so there's no conflict. Both modules have onboard pull-ups. + +> **Mount the VL53L0X on the SG90 servo horn** using double-sided tape or a small bracket. The servo sweeps 30°–150°, and the VL53L0X scans distances at each angle. + +### Step 7: LM393 Speed Sensors (Wheel Encoders) + +``` +Left Speed Sensor: + VCC → ESP32 3V3 + GND → GND rail + DO → ESP32 GPIO 32 + +Right Speed Sensor: + VCC → ESP32 3V3 + GND → GND rail + DO → ESP32 GPIO 35 + + 10kΩ pull-up resistor between ESP32 3V3 and GPIO 35 + (GPIO35 is input-only, has no internal pull-up) +``` + +**Mounting:** Attach the slotted encoder disc to one of the motor shafts on each side. Position the LM393 sensor so the disc's slots pass through the sensor's optical gap. Use hot glue or zip ties to secure. + +### Step 8: Water Level Sensor + +``` +Water Level Sensor: + VCC → ESP32 3V3 (NOT 5V — keeps output within 0–3.3V) + GND → GND rail + SIG → ESP32 GPIO 36 (VP) +``` + +### Step 9: SG90 Scanning Servo + +``` +SG90 Servo (3 wires): + Brown wire (GND) → GND rail + Red wire (V+) → 5V bus (NOT through ESP32) + Orange wire (Signal) → ESP32 GPIO 13 +``` + +> Power the servo from the 5V bus, not the ESP32's 5V pin. Servo stall current can reach 700mA. + +### Step 10: Buzzer + +``` +Buzzer: + (+) positive pin → [220Ω resistor] → ESP32 GPIO 4 + (−) negative pin → GND rail +``` + +### Step 11: Status LEDs + +``` +Red LED (Danger): + Anode (long leg) → [220Ω resistor] → ESP32 GPIO 26 + Cathode (short leg) → GND rail + +Green LED (Normal): + Anode (long leg) → [220Ω resistor] → ESP32 GPIO 27 + Cathode (short leg) → GND rail +``` + +### Step 12: TB6612FNG Motor Driver + +``` +TB6612FNG Module: + VM → 5V bus (motor power) + VCC → ESP32 3V3 (logic reference voltage) + GND → GND rail (both GND pins) + STBY → ESP32 GPIO 5 (HIGH = enabled) + + PWMA → ESP32 GPIO 25 (left motor speed PWM) + AIN1 → ESP32 GPIO 16 (left direction) + AIN2 → ESP32 GPIO 17 (left direction) + AO1 → Left motors (+) terminal ┐ + AO2 → Left motors (−) terminal ┘ Front-Left and Rear-Left wired in parallel + + PWMB → ESP32 GPIO 14 (right motor speed PWM) + BIN1 → ESP32 GPIO 33 (right direction) + BIN2 → ESP32 GPIO 2 (right direction) ⚠ see note below + BO1 → Right motors (+) terminal ┐ + BO2 → Right motors (−) terminal ┘ Front-Right and Rear-Right wired in parallel +``` + +> Place a **100nF ceramic capacitor** across VCC and GND on the TB6612 module — solder it or tuck it into the breadboard next to the module. + +**Motor wiring — parallel pairs:** +``` + AO1 ───┬──→ Front-Left Motor (+) + └──→ Rear-Left Motor (+) + + AO2 ───┬──→ Front-Left Motor (−) + └──→ Rear-Left Motor (−) + + BO1 ───┬──→ Front-Right Motor (+) + └──→ Rear-Right Motor (+) + + BO2 ───┬──→ Front-Right Motor (−) + └──→ Rear-Right Motor (−) +``` + +If the wheels spin the wrong direction, swap the (+) and (−) wires for that motor pair. + +### Step 13: Decoupling Capacitor on ESP32 + +Place a **100nF ceramic capacitor** between ESP32 **3V3** pin and **GND** pin (any GND). This filters high-frequency noise from the motors reaching the ESP32's logic rail. + +--- + +## ⚠ GPIO2 Boot Note + +GPIO2 (used for TB6612 BIN2) is a strapping pin. If the motor driver holds it HIGH during power-on, the ESP32 may fail to enter flash mode. + +**If you cannot upload firmware:** +1. Disconnect the jumper wire from GPIO2 +2. Flash the firmware via USB +3. Reconnect the jumper wire +4. Press the RST button on the ESP32 + +This only matters during flashing. Once the firmware is running, GPIO2 works fine as an output. + +--- + +## GATEWAY ESP32 — Complete Pin Map + +| GPIO | Direction | Connected To | Wire Color | +|---|---|---|---| +| **5V** | Power Out | LCD VCC | Red | +| **GND** | Power | LCD GND + all LED cathodes | Black | +| **21** | I2C SDA | LCD SDA | Green | +| **22** | I2C SCL | LCD SCL | Blue | +| **26** | Output | Red LED anode via 220Ω | Red | +| **27** | Output | Green LED anode via 220Ω | Green | +| **25** | Output | Yellow LED anode via 220Ω | Orange | + +--- + +## GATEWAY — Step-by-Step Wiring + +The Gateway is powered entirely by the laptop USB cable. No external power needed. + +### Step 1: LCD 16×2 (I2C) + +``` +LCD I2C Module (4-pin header on the backpack): + GND → ESP32 GND + VCC → ESP32 5V + SDA → ESP32 GPIO 21 + SCL → ESP32 GPIO 22 +``` + +> If the LCD shows blocks or is blank, use a small screwdriver to turn the **blue potentiometer** on the back of the I2C backpack. This adjusts contrast. + +### Step 2: Status LEDs + +``` +Red LED (Danger): + Anode (long leg) → [220Ω resistor] → ESP32 GPIO 26 + Cathode (short leg) → GND + +Green LED (Normal): + Anode (long leg) → [220Ω resistor] → ESP32 GPIO 27 + Cathode (short leg) → GND + +Yellow LED (Heartbeat): + Anode (long leg) → [220Ω resistor] → ESP32 GPIO 25 + Cathode (short leg) → GND +``` + +That's it for the Gateway. Plug it into the laptop via USB and you're live. + +--- + +## COMPLETE SYSTEM DIAGRAM + +``` + ┌─────────── MINE ───────────┐ + │ │ + │ ┌─── ROVER ESP32 ───┐ │ + │ │ │ │ + ┌──────────────┐ │ │ DHT22 → GPIO23 │ │ + │ 10000mAh │ │ │ MQ-4 → GPIO34 │ │ + │ Power Bank │───┼───→│ HC-SR04→ GPIO18 │ │ + │ (5V / 2A+) │ │ │ MPU6050→ I2C │ │ + └──────────────┘ │ │ VL53L0X→ I2C │ │ + │ │ Encoders → 32,35 │ │ + │ │ Water → GPIO36 │ │ + │ │ Servo → GPIO13 │ │ + │ │ Motors → TB6612 │ │ + │ │ Buzzer → GPIO4 │ │ + │ │ LEDs → 26,27 │ │ + │ │ │ │ + │ │ ESP-NOW TX │ │ + │ └───────┬────────────┘ │ + │ │ │ + └────────────┼─────────────────┘ + │ + ~200m wireless range + (no WiFi router needed) + │ + ┌────────────┼─────────────────┐ + │ │ BASE DESK │ + │ ┌───────▼────────────┐ │ + │ │ GATEWAY ESP32 │ │ + │ │ │ │ + │ │ ESP-NOW RX │ │ + │ │ 16×2 LCD (I2C) │ │ + │ │ Red/Green/Yellow │ │ + │ │ LEDs │ │ + │ │ │ │ + │ └───────┬────────────┘ │ + │ │ USB Cable │ + │ │ (data + power) │ + │ ┌───────▼────────────┐ │ + │ │ LAPTOP │ │ + │ │ Chrome Browser │ │ + │ │ dashboard.html │ │ + │ │ (Web Serial API) │ │ + │ └───────────────────┘ │ + └─────────────────────────────┘ +``` + +--- + +## VOLTAGE DIVIDER REFERENCE + +Both the HC-SR04 ECHO and MQ-4 AO use identical voltage dividers: + +``` +5V Signal ──── [10kΩ] ──── Junction ──── [15kΩ] ──── GND + │ + ESP32 GPIO + (reads 3.0V max) + + V_out = V_in × R_lower / (R_upper + R_lower) + V_out = 5.0V × 15kΩ / (10kΩ + 15kΩ) + V_out = 5.0V × 0.60 + V_out = 3.0V ← safe for ESP32 (max 3.3V) +``` + +--- + +## MOTOR DIRECTION TRUTH TABLE (TB6612FNG) + +| IN1 | IN2 | PWM | Motor Action | +|---|---|---|---| +| HIGH | LOW | 0–255 | Forward (speed = PWM duty) | +| LOW | HIGH | 0–255 | Reverse (speed = PWM duty) | +| HIGH | HIGH | 255 | Short brake (motor locked) | +| LOW | LOW | any | Coast (free spin) | + +STBY must be HIGH for the driver to operate. STBY LOW = all outputs disabled (standby mode). + +--- + +## FIRST POWER-ON CHECKLIST + +Before applying power, verify: + +``` +[ ] 470µF cap on 5V bus (correct polarity — long leg to +5V) +[ ] 100nF cap on ESP32 3V3-GND +[ ] 100nF cap on TB6612 VCC-GND +[ ] MQ-4 VCC on 5V bus (NOT 3V3) +[ ] MQ-4 AO goes through 10k/15k divider to GPIO34 (NOT direct) +[ ] HC-SR04 ECHO goes through 10k/15k divider to GPIO18 (NOT direct) +[ ] HC-SR04 VCC on 5V bus +[ ] Servo V+ on 5V bus (NOT ESP32 5V pin) +[ ] TB6612 VM on 5V bus +[ ] All GND wires connected to the same GND bus +[ ] No bare wire ends touching each other +[ ] DHT22 on 3V3 (not 5V) +[ ] Water sensor on 3V3 (not 5V) +[ ] LED resistors (220Ω) are present (no direct GPIO → LED) +``` + +> **MQ-4 warm-up:** The MQ-4 sensor needs 24–48 hours of continuous power for the heater to stabilize on first use. Readings in the first few minutes will be unreliable. For the hackathon demo, power it on as early as possible. diff --git a/include/telemetry_packet.h b/include/telemetry_packet.h index df75373..bc0d1d8 100644 --- a/include/telemetry_packet.h +++ b/include/telemetry_packet.h @@ -1,30 +1,32 @@ #pragma once + #include -/* - Shared telemetry packet for ESP-NOW communication between - the Rover ESP32 and the Gateway ESP32. - 48 bytes — well under ESP-NOW's 250-byte limit. +// Standard telemetry (sent slowly) +typedef struct __attribute__((packed)) { + float temperature; + float humidity; + float ax, ay, az; + float gx, gy, gz; + uint16_t gasRaw; + uint16_t waterRaw; + uint32_t dangerState; // 0=safe, 1=danger +} TelemetryPacket; - Also used by the gateway to parse Serial2 bridge JSON - and to relay JSON to the laptop dashboard. -*/ +// Fast scan telemetry (sent rapidly) +typedef struct __attribute__((packed)) { + uint8_t type; // 1 = scan + uint8_t seq; // Sequence number + int16_t angle_deg; // Servo angle in degrees + uint16_t distance_mm; // VL53L0X distance + uint8_t valid; // 1 if valid, 0 if out of range + uint32_t timestamp_ms; // timestamp +} ScanPacket; +// Gateway to Rover control packet (Heartbeat) typedef struct __attribute__((packed)) { - float tempC; // DHT22 temperature (°C) - float humidity; // DHT22 relative humidity (%) - int16_t gasRaw; // MQ-4 Methane sensor ADC (0-4095) - float frontCm; // HC-SR04 front distance (cm) - int16_t tofRaw; // VL53L0X sim pot ADC (0-4095) - float tiltDeg; // MPU6050 tilt angle (degrees) - int16_t waterRaw; // Water sensor ADC (0-4095) - uint32_t encL, encR; // Wheel encoder pulse counts - float x, y; // Dead-reckoned position (cm) - float heading; // Heading (degrees, 0=+X, CCW positive) - uint8_t state; // 0=NORMAL 1=SLOW 2=AVOIDING 3=DANGER - uint8_t dangerCause; // 0=NONE 1=GAS 2=TILT 3=WATER 4=TEMP 5=HUMIDITY 6=TRAPPED -} TelemetryPacket; + uint8_t type; // 0 = command + int16_t motor_l; // -255 to 255 + int16_t motor_r; // -255 to 255 +} ControlPacket; -// String lookups for JSON serialization / LCD display -static const char* const STATE_NAMES[] = {"NORMAL","SLOW","AVOIDING","DANGER"}; -static const char* const DANGER_NAMES[] = {"NONE","GAS","TILT","WATER","TEMP","HUMIDITY","TRAPPED"}; diff --git a/src/main.cpp b/src/main.cpp index 347a19c..bc75ade 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,546 +1,322 @@ -/* - DEEPTRACK — FOUR-WHEEL MINE RESCUE ROVER - Sensor + safety + autonomous obstacle avoidance for ESP32 - - Communication: - ESP-NOW broadcast → Gateway ESP32 (binary TelemetryPacket, ~200m range) - Serial JSON → Wokwi serial monitor / optional socat bridge - Serial (USB) → debug output - - Sensors: - DHT22 → GPIO23 (temperature / humidity) - MQ-4 → GPIO34 (analog) (methane / CH4 gas sensor) - HC-SR04 → TRIG 19 / ECHO 18 (mounted on SG90 scan servo) - MPU6050 → SDA 21 / SCL 22 (orientation / tilt) - Left encoder → GPIO32 (wheel pulse count) - Right encoder→ GPIO35 (wheel pulse count) - VL53L0X sim → GPIO39 / VN (analog)(secondary front distance) - Water sensor → GPIO36 / VP (analog)(flood detection) - - Actuators: - SG90 scan servo → GPIO13 - Buzzer → GPIO4 - Red LED → GPIO26 (DANGER) - Green LED → GPIO27 (NORMAL) - Left motor PWM → GPIO25 - Right motor PWM → GPIO14 - - Obstacle avoidance sequence: - HALT → REVERSE → LOOK_RIGHT → LOOK_LEFT → TURN → resume -*/ - +#include +#include #include #include -#include + +#define USE_ESP_NOW 0 + #include #include #include +#include #include #include "telemetry_packet.h" -// ---------- ESP-NOW ---------- -uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; - -// ---------- Pin map ---------- +// --- Pin Definitions (Original Preserved) --- #define DHT_PIN 23 -#define DHT_TYPE DHT22 - #define GAS_PIN 34 #define TRIG_PIN 19 #define ECHO_PIN 18 - #define ENC_LEFT_PIN 32 #define ENC_RIGHT_PIN 35 - -#define TOF_SIM_PIN 39 #define WATER_PIN 36 - #define SERVO_PIN 13 #define BUZZER_PIN 4 - #define LED_RED_PIN 26 #define LED_GREEN_PIN 27 +#define I2C_SDA 21 +#define I2C_SCL 22 + +// Motor Pins (TB6612FNG) +#define L_PWM 25 +#define L_DIR1 16 +#define L_DIR2 17 +#define R_PWM 14 +#define R_DIR1 33 +#define R_DIR2 2 +#define STBY_PIN 5 + +// --- Constants & Config --- +#define DHT_TYPE DHT22 +#define OBSTACLE_STOP_CM 15 +#define L_PWM_CH 0 +#define R_PWM_CH 1 +#define PWM_FREQ 5000 +#define PWM_RES 8 -#define MOTOR_LEFT_PIN 25 -#define MOTOR_RIGHT_PIN 14 - -// ---------- LEDC motor PWM channels ---------- -#define MOTOR_LEFT_CH 0 -#define MOTOR_RIGHT_CH 1 - -// ---------- Servo scan angles ---------- -const int SERVO_CENTER = 90; -const int SERVO_LEFT = 150; -const int SERVO_RIGHT = 30; - -// ---------- Thresholds ---------- -const int GAS_ALARM_RAW = 1800; -const float OBSTACLE_STOP_CM = 20.0; -const float OBSTACLE_SLOW_CM = 45.0; -const int TOF_SIM_STOP_RAW = 300; -const float TILT_WARN_DEG = 25.0; -const float TILT_STOP_DEG = 40.0; -const int WATER_ALARM_RAW = 2500; -const float TEMP_ALARM_C = 50.0; -const float HUMIDITY_ALARM_PCT = 85.0; - -const unsigned long DHT_INTERVAL_MS = 2000; -const unsigned long SENSOR_INTERVAL_MS = 100; -const unsigned long SWEEP_STEP_MS = 20; -const unsigned long SERVO_SETTLE_MS = 300; -const unsigned long AVOID_HALT_MS = 150; -const unsigned long AVOID_REVERSE_MS = 400; -const unsigned long AVOID_TURN_MS = 700; -const unsigned long TELEMETRY_MS = 500; - -// ---------- Odometry ---------- -const float WHEEL_DIAMETER_CM = 6.5; -const int ENCODER_TICKS_PER_REV = 20; -const float WHEEL_BASE_CM = 15.0; -const float CM_PER_TICK = (PI * WHEEL_DIAMETER_CM) / ENCODER_TICKS_PER_REV; -const float SIM_MAX_SPEED_CM_S = 30.0; - -// ---------- Globals ---------- +// --- Globals --- DHT dht(DHT_PIN, DHT_TYPE); Adafruit_MPU6050 mpu; +Adafruit_VL53L0X lox = Adafruit_VL53L0X(); Servo scanServo; -volatile unsigned long leftPulses = 0; -volatile unsigned long rightPulses = 0; - -float lastTempC = NAN, lastHumidity = NAN; -bool dhtFault = false; - -float frontDistanceCm = 999; -int tofSimRaw = 4095; -int gasRaw = 0; -int waterRaw = 0; -float tiltDeg = 0; - -int servoAngle = SERVO_CENTER; -int servoDir = 1; -unsigned long lastSweepStep = 0; - -// ---------- Rover state ---------- -enum RoverState { NORMAL, SLOW, AVOIDING, DANGER }; -RoverState state = NORMAL; - -enum AvoidPhase { - AVOID_NONE, - AVOID_HALT, - AVOID_REVERSE, - AVOID_LOOK_RIGHT, - AVOID_LOOK_LEFT, - AVOID_TURN -}; -AvoidPhase avoidPhase = AVOID_NONE; -unsigned long avoidPhaseStart = 0; -float rightClearanceCm = 999; -float leftClearanceCm = 999; -int turnDir = 0; - -enum DangerCause { NONE_, GAS, TILT, WATER, TEMP, HUMIDITY, TRAPPED }; -DangerCause dangerCause = NONE_; - -unsigned long lastDhtRead = 0; -unsigned long lastSensorRead = 0; -unsigned long lastTelemetry = 0; - -float robotX = 0, robotY = 0; -float headingDeg = 0; -unsigned long prevLeftPulses = 0, prevRightPulses = 0; -int lastLeftCmdSigned = 0, lastRightCmdSigned = 0; - -// ---------- Forward declarations ---------- -const char* stateName(RoverState s); -const char* dangerCauseName(DangerCause c); - -// ---------- ISRs ---------- -void IRAM_ATTR onLeftPulse() { leftPulses++; } -void IRAM_ATTR onRightPulse() { rightPulses++; } +uint8_t gatewayAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; -// ---------- Helpers ---------- -float readUltrasonicCm() { - digitalWrite(TRIG_PIN, LOW); - delayMicroseconds(2); - digitalWrite(TRIG_PIN, HIGH); - delayMicroseconds(10); - digitalWrite(TRIG_PIN, LOW); - unsigned long duration = pulseIn(ECHO_PIN, HIGH, 30000UL); - if (duration == 0) return 999.0; - return duration * 0.0343 / 2.0; -} +// State +TelemetryPacket currentTelemetry; +uint32_t lastTelemetryTime = 0; +bool emergencyStopped = false; -void setMotors(int leftSpeed, int rightSpeed) { - lastLeftCmdSigned = leftSpeed; - lastRightCmdSigned = rightSpeed; - ledcWrite(MOTOR_LEFT_CH, constrain(abs(leftSpeed), 0, 255)); - ledcWrite(MOTOR_RIGHT_CH, constrain(abs(rightSpeed), 0, 255)); -} +// Encoders +volatile uint32_t leftPulses = 0; +volatile uint32_t rightPulses = 0; -void soundBuzzer(bool on, int freq = 2000) { - if (on) tone(BUZZER_PIN, freq); - else noTone(BUZZER_PIN); -} +void IRAM_ATTR onLeftPulse() { leftPulses++; } +void IRAM_ATTR onRightPulse() { rightPulses++; } -void setStatusLeds(bool danger) { - digitalWrite(LED_RED_PIN, danger ? HIGH : LOW); - digitalWrite(LED_GREEN_PIN, danger ? LOW : HIGH); +// Scan State +int currentAngle = 90; +int scanDirection = 10; +uint32_t lastScanTime = 0; +uint8_t scanSeq = 0; + +// Motor state (target) +int target_l = 0; +int target_r = 0; + +// --- Function Prototypes --- +void setMotors(int left, int right); +void shortBrake(); + +// --- ESP-NOW Callback --- +void onDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len) { + if (len == sizeof(ControlPacket)) { + ControlPacket *pkt = (ControlPacket*)incomingData; + if (pkt->type == 0) { + target_l = pkt->motor_l; + target_r = pkt->motor_r; + emergencyStopped = false; // Reset emergency on new command + } + } } -// ---------- Setup ---------- void setup() { - Serial.begin(115200); - delay(300); - - // WiFi STA mode for ESP-NOW (no AP connection needed) - WiFi.mode(WIFI_STA); - WiFi.disconnect(); - - // ESP-NOW init - if (esp_now_init() != ESP_OK) { - Serial.println("ESP-NOW init failed"); - } else { - // Register broadcast peer + Serial.begin(115200); + Wire.begin(I2C_SDA, I2C_SCL); + + // Motor Pins + pinMode(L_DIR1, OUTPUT); + pinMode(L_DIR2, OUTPUT); + pinMode(R_DIR1, OUTPUT); + pinMode(R_DIR2, OUTPUT); + pinMode(STBY_PIN, OUTPUT); + + ledcSetup(L_PWM_CH, PWM_FREQ, PWM_RES); + ledcSetup(R_PWM_CH, PWM_FREQ, PWM_RES); + ledcAttachPin(L_PWM, L_PWM_CH); + ledcAttachPin(R_PWM, R_PWM_CH); + + digitalWrite(STBY_PIN, HIGH); // Enable driver + + // LED & Buzzer + pinMode(BUZZER_PIN, OUTPUT); + pinMode(LED_RED_PIN, OUTPUT); + pinMode(LED_GREEN_PIN, OUTPUT); + + // Ultrasonic + pinMode(TRIG_PIN, OUTPUT); + pinMode(ECHO_PIN, INPUT); + + // Sensors + pinMode(GAS_PIN, INPUT); + pinMode(WATER_PIN, INPUT); + + // Encoders + pinMode(ENC_LEFT_PIN, INPUT_PULLUP); + pinMode(ENC_RIGHT_PIN, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(ENC_LEFT_PIN), onLeftPulse, RISING); + attachInterrupt(digitalPinToInterrupt(ENC_RIGHT_PIN), onRightPulse, RISING); + + dht.begin(); + scanServo.attach(SERVO_PIN, 500, 2400); + + if (!mpu.begin()) Serial.println("Failed to find MPU6050"); + if (!lox.begin(0x29, true)) { + Serial.println("Failed to boot VL53L0X"); + } + + WiFi.mode(WIFI_STA); +#if USE_ESP_NOW + if (esp_now_init() != ESP_OK) { + Serial.println("Error initializing ESP-NOW"); + return; + } + esp_now_register_recv_cb(onDataRecv); + esp_now_peer_info_t peerInfo = {}; - memcpy(peerInfo.peer_addr, broadcastAddress, 6); - peerInfo.channel = 0; + memcpy(peerInfo.peer_addr, gatewayAddress, 6); + peerInfo.channel = 0; peerInfo.encrypt = false; - if (esp_now_add_peer(&peerInfo) != ESP_OK) { - Serial.println("ESP-NOW add peer failed"); - } else { - Serial.println("ESP-NOW transmitter ready"); - } - } - - dht.begin(); - Wire.begin(); - - if (!mpu.begin()) { - Serial.println("WARN: MPU6050 not detected"); - } else { - mpu.setAccelerometerRange(MPU6050_RANGE_8_G); - mpu.setGyroRange(MPU6050_RANGE_500_DEG); - mpu.setFilterBandwidth(MPU6050_BAND_21_HZ); - } - - pinMode(TRIG_PIN, OUTPUT); - pinMode(ECHO_PIN, INPUT); - pinMode(GAS_PIN, INPUT); - - pinMode(ENC_LEFT_PIN, INPUT_PULLUP); - pinMode(ENC_RIGHT_PIN, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(ENC_LEFT_PIN), onLeftPulse, RISING); - attachInterrupt(digitalPinToInterrupt(ENC_RIGHT_PIN), onRightPulse, RISING); - - pinMode(BUZZER_PIN, OUTPUT); - pinMode(LED_RED_PIN, OUTPUT); - pinMode(LED_GREEN_PIN, OUTPUT); - setStatusLeds(false); - - ledcSetup(MOTOR_LEFT_CH, 5000, 8); - ledcAttachPin(MOTOR_LEFT_PIN, MOTOR_LEFT_CH); - ledcSetup(MOTOR_RIGHT_CH, 5000, 8); - ledcAttachPin(MOTOR_RIGHT_PIN, MOTOR_RIGHT_CH); - - scanServo.setPeriodHertz(50); - scanServo.attach(SERVO_PIN, 500, 2400); - scanServo.write(servoAngle); - - Serial.println("Mine Rescue Rover boot OK"); + esp_now_add_peer(&peerInfo); +#endif } -// ---------- Sensor reads ---------- -void readEnvironment() { - float h = dht.readHumidity(); - float t = dht.readTemperature(); - if (isnan(h) || isnan(t)) { dhtFault = true; } - else { dhtFault = false; lastHumidity = h; lastTempC = t; } +void shortBrake() { + digitalWrite(STBY_PIN, HIGH); + digitalWrite(L_DIR1, HIGH); + digitalWrite(L_DIR2, HIGH); + digitalWrite(R_DIR1, HIGH); + digitalWrite(R_DIR2, HIGH); + ledcWrite(L_PWM_CH, 255); + ledcWrite(R_PWM_CH, 255); } -void readObstacleSensors() { - frontDistanceCm = readUltrasonicCm(); - tofSimRaw = analogRead(TOF_SIM_PIN); +void setMotors(int left, int right) { + if (emergencyStopped) return; + + digitalWrite(STBY_PIN, HIGH); + + if (left > 0) { + digitalWrite(L_DIR1, HIGH); + digitalWrite(L_DIR2, LOW); + } else if (left < 0) { + digitalWrite(L_DIR1, LOW); + digitalWrite(L_DIR2, HIGH); + } else { + digitalWrite(L_DIR1, HIGH); + digitalWrite(L_DIR2, HIGH); + } + ledcWrite(L_PWM_CH, abs(left)); + + if (right > 0) { + digitalWrite(R_DIR1, HIGH); + digitalWrite(R_DIR2, LOW); + } else if (right < 0) { + digitalWrite(R_DIR1, LOW); + digitalWrite(R_DIR2, HIGH); + } else { + digitalWrite(R_DIR1, HIGH); + digitalWrite(R_DIR2, HIGH); + } + ledcWrite(R_PWM_CH, abs(right)); } -void readGasSensor() { gasRaw = analogRead(GAS_PIN); } -void readWaterSensor() { waterRaw = analogRead(WATER_PIN); } - -void readTilt() { - sensors_event_t a, g, temp; - if (!mpu.getEvent(&a, &g, &temp)) return; - float ax = a.acceleration.x, ay = a.acceleration.y, az = a.acceleration.z; - tiltDeg = atan2(sqrt(ax * ax + ay * ay), az) * 180.0 / PI; +float readUltrasonic() { + digitalWrite(TRIG_PIN, LOW); + delayMicroseconds(2); + digitalWrite(TRIG_PIN, HIGH); + delayMicroseconds(10); + digitalWrite(TRIG_PIN, LOW); + unsigned long duration = pulseIn(ECHO_PIN, HIGH, 30000UL); + if (duration == 0) return 999.0; + return (duration * 0.0343) / 2.0; } -bool frontBlocked() { - return (frontDistanceCm <= OBSTACLE_STOP_CM) || (tofSimRaw <= TOF_SIM_STOP_RAW); -} +void loop() { + uint32_t now = millis(); + + if (Serial.available()) { + String cmd = Serial.readStringUntil('\n'); + if (cmd.startsWith("L:")) { + int spaceIdx = cmd.indexOf(' '); + if (spaceIdx > -1) { + target_l = cmd.substring(2, spaceIdx).toInt(); + target_r = cmd.substring(cmd.indexOf("R:") + 2).toInt(); + emergencyStopped = false; + } + } + } -// ---------- Odometry ---------- -void updateOdometry() { - noInterrupts(); - unsigned long lp = leftPulses, rp = rightPulses; - interrupts(); - - long deltaLeft = (long)(lp - prevLeftPulses); - long deltaRight = (long)(rp - prevRightPulses); - prevLeftPulses = lp; - prevRightPulses = rp; - - float distLeft, distRight; - - if (deltaLeft == 0 && deltaRight == 0) { - float dt = SENSOR_INTERVAL_MS / 1000.0f; - distLeft = (lastLeftCmdSigned / 255.0f) * SIM_MAX_SPEED_CM_S * dt; - distRight = (lastRightCmdSigned / 255.0f) * SIM_MAX_SPEED_CM_S * dt; - } else { - distLeft = deltaLeft * CM_PER_TICK; - distRight = deltaRight * CM_PER_TICK; - if (lastLeftCmdSigned < 0) distLeft = -distLeft; - if (lastRightCmdSigned < 0) distRight = -distRight; - if (lastLeftCmdSigned == 0) distLeft = 0; - if (lastRightCmdSigned == 0) distRight = 0; - } - - float distCenter = (distLeft + distRight) / 2.0f; - float deltaHeadingDeg = (distRight - distLeft) / WHEEL_BASE_CM * (180.0f / PI); - float headingRad = headingDeg * PI / 180.0f; - - robotX += distCenter * cos(headingRad); - robotY += distCenter * sin(headingRad); - headingDeg += deltaHeadingDeg; - while (headingDeg >= 360) headingDeg -= 360; - while (headingDeg < 0) headingDeg += 360; -} -// ---------- Cruise sweep ---------- -void updateCruiseSweep() { - if (avoidPhase != AVOID_NONE) return; - if (millis() - lastSweepStep < SWEEP_STEP_MS) return; - lastSweepStep = millis(); + // 1. Ultrasonic Obstacle Check (Fixed Forward) + float frontDist = readUltrasonic(); + if (frontDist < OBSTACLE_STOP_CM && frontDist > 0) { + if (!emergencyStopped && (target_l > 0 || target_r > 0)) { + Serial.println("OBSTACLE - EMERGENCY STOP"); + shortBrake(); + emergencyStopped = true; + } + } - servoAngle += servoDir * 2; - if (servoAngle >= SERVO_LEFT) { servoAngle = SERVO_LEFT; servoDir = -1; } - if (servoAngle <= SERVO_RIGHT) { servoAngle = SERVO_RIGHT; servoDir = 1; } - scanServo.write(servoAngle); -} + // Apply motor state + if (!emergencyStopped) { + setMotors(target_l, target_r); + } -// ---------- Obstacle-avoidance state machine ---------- -void runAvoidance() { - unsigned long elapsed = millis() - avoidPhaseStart; - - switch (avoidPhase) { - case AVOID_NONE: - if (frontBlocked()) { - avoidPhase = AVOID_HALT; - avoidPhaseStart = millis(); - setMotors(0, 0); - } - break; - - case AVOID_HALT: - setMotors(0, 0); - if (elapsed >= AVOID_HALT_MS) { - avoidPhase = AVOID_REVERSE; - avoidPhaseStart = millis(); - } - break; - - case AVOID_REVERSE: - setMotors(-120, -120); - if (elapsed >= AVOID_REVERSE_MS) { - setMotors(0, 0); - scanServo.write(SERVO_RIGHT); - avoidPhase = AVOID_LOOK_RIGHT; - avoidPhaseStart = millis(); - } - break; - - case AVOID_LOOK_RIGHT: - setMotors(0, 0); - if (elapsed >= SERVO_SETTLE_MS) { - rightClearanceCm = readUltrasonicCm(); - scanServo.write(SERVO_LEFT); - avoidPhase = AVOID_LOOK_LEFT; - avoidPhaseStart = millis(); - } - break; - - case AVOID_LOOK_LEFT: - setMotors(0, 0); - if (elapsed >= SERVO_SETTLE_MS) { - leftClearanceCm = readUltrasonicCm(); - bool rightOpen = rightClearanceCm > OBSTACLE_STOP_CM; - bool leftOpen = leftClearanceCm > OBSTACLE_STOP_CM; - - if (!rightOpen && !leftOpen) { - dangerCause = TRAPPED; - avoidPhase = AVOID_NONE; - scanServo.write(SERVO_CENTER); + // 2. Servo Sweep & Scan (VL53L0X) + if (now - lastScanTime > 33) { + lastScanTime = now; + scanServo.write(currentAngle); + + VL53L0X_RangingMeasurementData_t measure; + lox.rangingTest(&measure, false); + + ScanPacket spkt; + spkt.type = 1; + spkt.seq = scanSeq++; + spkt.angle_deg = currentAngle; + spkt.timestamp_ms = now; + + if (measure.RangeStatus != 4) { + spkt.distance_mm = measure.RangeMilliMeter; + spkt.valid = 1; } else { - turnDir = (rightClearanceCm >= leftClearanceCm) ? 1 : -1; - avoidPhase = AVOID_TURN; - avoidPhaseStart = millis(); + spkt.distance_mm = 800; + spkt.valid = 0; } - } - break; - - case AVOID_TURN: - if (turnDir > 0) setMotors(160, -160); - else setMotors(-160, 160); - if (elapsed >= AVOID_TURN_MS) { - setMotors(0, 0); - scanServo.write(SERVO_CENTER); - servoAngle = SERVO_CENTER; - avoidPhase = AVOID_NONE; - } - break; - } -} - -// ---------- Decision / arbitration ---------- -void decideState() { - dangerCause = NONE_; - - if (gasRaw >= GAS_ALARM_RAW) dangerCause = GAS; - else if (waterRaw >= WATER_ALARM_RAW) dangerCause = WATER; - else if (tiltDeg >= TILT_STOP_DEG) dangerCause = TILT; - else if (!dhtFault && lastTempC >= TEMP_ALARM_C) dangerCause = TEMP; - else if (!dhtFault && lastHumidity >= HUMIDITY_ALARM_PCT) dangerCause = HUMIDITY; - - if (dangerCause != NONE_) { - state = DANGER; - avoidPhase = AVOID_NONE; - return; - } - - runAvoidance(); - - if (dangerCause == TRAPPED) { state = DANGER; return; } - if (avoidPhase != AVOID_NONE) { state = AVOIDING; return; } - - bool slow = (frontDistanceCm <= OBSTACLE_SLOW_CM) || (tiltDeg >= TILT_WARN_DEG); - state = slow ? SLOW : NORMAL; -} - -void actOnState() { - bool danger = (state == DANGER); - setStatusLeds(danger); - - int buzzFreq = 2000; - switch (dangerCause) { - case GAS: buzzFreq = 2500; break; - case TILT: buzzFreq = 3000; break; - case WATER: buzzFreq = 1800; break; - case TEMP: buzzFreq = 2700; break; - case HUMIDITY: buzzFreq = 1600; break; - case TRAPPED: buzzFreq = 1200; break; - default: buzzFreq = 2000; break; - } - soundBuzzer(danger, buzzFreq); - - switch (state) { - case NORMAL: setMotors(200, 200); break; - case SLOW: setMotors(110, 110); break; - case AVOIDING: break; // driven by runAvoidance() - case DANGER: - switch (dangerCause) { - case GAS: case TEMP: setMotors(-150, -150); break; - default: setMotors(0, 0); break; - } - break; - } -} - -// ---------- Telemetry ---------- -const char* stateName(RoverState s) { - switch (s) { - case NORMAL: return "NORMAL"; - case SLOW: return "SLOW"; - case AVOIDING: return "AVOIDING"; - case DANGER: return "DANGER"; - } - return "UNKNOWN"; -} +#if USE_ESP_NOW + + + esp_now_send(gatewayAddress, (uint8_t *) &spkt, sizeof(ScanPacket)); +#endif + + char json[128]; + snprintf(json, sizeof(json), + "{\"type\":\"scan\",\"seq\":%d,\"angle_deg\":%d,\"distance_mm\":%d,\"valid\":%s,\"timestamp_ms\":%u}", + spkt.seq, spkt.angle_deg, spkt.distance_mm, spkt.valid ? "true" : "false", spkt.timestamp_ms + ); + Serial.println(json); + + + currentAngle += scanDirection; + if (currentAngle >= 150 || currentAngle <= 30) { + scanDirection = -scanDirection; + } + } -const char* dangerCauseName(DangerCause c) { - switch (c) { - case GAS: return "GAS"; - case TILT: return "TILT"; - case WATER: return "WATER"; - case TEMP: return "TEMP"; - case HUMIDITY: return "HUMIDITY"; - case TRAPPED: return "TRAPPED"; - default: return "NONE"; - } -} + // 3. Environmental Telemetry + if (now - lastTelemetryTime > 1000) { + lastTelemetryTime = now; + + currentTelemetry.temperature = dht.readTemperature(); + currentTelemetry.humidity = dht.readHumidity(); + currentTelemetry.gasRaw = analogRead(GAS_PIN); + currentTelemetry.waterRaw = analogRead(WATER_PIN); + + sensors_event_t a, g, temp; + mpu.getEvent(&a, &g, &temp); + currentTelemetry.ax = a.acceleration.x; + currentTelemetry.ay = a.acceleration.y; + currentTelemetry.az = a.acceleration.z; + currentTelemetry.gx = g.gyro.x; + currentTelemetry.gy = g.gyro.y; + currentTelemetry.gz = g.gyro.z; + + currentTelemetry.dangerState = emergencyStopped ? 1 : 0; + + digitalWrite(LED_RED_PIN, emergencyStopped ? HIGH : LOW); + digitalWrite(LED_GREEN_PIN, emergencyStopped ? LOW : HIGH); + if (emergencyStopped) { + tone(BUZZER_PIN, 1000, 100); + } else { + noTone(BUZZER_PIN); + } +#if USE_ESP_NOW + + + esp_now_send(gatewayAddress, (uint8_t *) ¤tTelemetry, sizeof(TelemetryPacket)); +#endif + + char json[256]; + snprintf(json, sizeof(json), + "{\"t\":%.1f,\"h\":%.1f,\"ax\":%.2f,\"ay\":%.2f,\"az\":%.2f,\"gx\":%.2f,\"gy\":%.2f,\"gz\":%.2f,\"gas\":%d,\"water\":%d,\"danger\":%d}", + currentTelemetry.temperature, currentTelemetry.humidity, + currentTelemetry.ax, currentTelemetry.ay, currentTelemetry.az, + currentTelemetry.gx, currentTelemetry.gy, currentTelemetry.gz, + currentTelemetry.gasRaw, currentTelemetry.waterRaw, + currentTelemetry.dangerState + ); + Serial.println(json); -void printTelemetry() { - noInterrupts(); - unsigned long lp = leftPulses, rp = rightPulses; - interrupts(); - - // Clean single-line JSON for Serial monitor + gateway bridge - char json[256]; - snprintf(json, sizeof(json), - "{\"t\":%.1f,\"h\":%.1f,\"gas\":%d,\"dist\":%.1f,\"water\":%d,\"tilt\":%.1f,\"x\":%.1f,\"y\":%.1f,\"hd\":%.0f,\"st\":\"%s\",\"danger\":\"%s\"}", - dhtFault ? -999.0f : lastTempC, - dhtFault ? -999.0f : lastHumidity, - gasRaw, frontDistanceCm, - waterRaw, tiltDeg, - robotX, robotY, headingDeg, - stateName(state), - dangerCauseName(dangerCause) - ); - - Serial.println(json); - - // ESP-NOW binary packet to gateway - TelemetryPacket pkt; - pkt.tempC = dhtFault ? -999.0f : lastTempC; - pkt.humidity = dhtFault ? -999.0f : lastHumidity; - pkt.gasRaw = (int16_t)gasRaw; - pkt.frontCm = frontDistanceCm; - pkt.tofRaw = (int16_t)tofSimRaw; - pkt.tiltDeg = tiltDeg; - pkt.waterRaw = (int16_t)waterRaw; - pkt.encL = (uint32_t)lp; - pkt.encR = (uint32_t)rp; - pkt.x = robotX; - pkt.y = robotY; - pkt.heading = headingDeg; - pkt.state = (uint8_t)state; - pkt.dangerCause = (uint8_t)dangerCause; - - esp_now_send(broadcastAddress, (uint8_t*)&pkt, sizeof(pkt)); + } } - -// ---------- Main loop ---------- -void loop() { - unsigned long now = millis(); - - if (now - lastDhtRead >= DHT_INTERVAL_MS) { - lastDhtRead = now; - readEnvironment(); - } - - if (now - lastSensorRead >= SENSOR_INTERVAL_MS) { - lastSensorRead = now; - readObstacleSensors(); - readGasSensor(); - readWaterSensor(); - readTilt(); - - decideState(); - updateOdometry(); - actOnState(); - } - - updateCruiseSweep(); - - if (now - lastTelemetry >= TELEMETRY_MS) { - lastTelemetry = now; - printTelemetry(); - } -} \ No newline at end of file diff --git a/tb6612fng.c b/tb6612fng.chip.c similarity index 100% rename from tb6612fng.c rename to tb6612fng.chip.c diff --git a/tb6612fng.chip.json b/tb6612fng.chip.json index 4855e84..7024e82 100644 --- a/tb6612fng.chip.json +++ b/tb6612fng.chip.json @@ -1,21 +1,5 @@ { "name": "tb6612fng", - "author": "Antigravity", - "pins": [ - "VM", - "VCC", - "GND", - "AO1", - "AO2", - "BO2", - "BO1", - "GND2", - "PWMB", - "BIN2", - "BIN1", - "STBY", - "AIN1", - "AIN2", - "PWMA" - ] + "author": "", + "pins": ["VM","VCC","GND","AO1","AO2","BO2","BO1","GND2","PWMB","BIN2","BIN1","STBY","AIN1","AIN2","PWMA"] } diff --git a/tb6612fng.svg b/tb6612fng.svg new file mode 100644 index 0000000..1a98f7b --- /dev/null +++ b/tb6612fng.svg @@ -0,0 +1,6 @@ + + + + TB6612FNG + TOSHIBA + diff --git a/vl53l0x.c b/vl53l0x.chip.c similarity index 65% rename from vl53l0x.c rename to vl53l0x.chip.c index 842b996..1e80f66 100644 --- a/vl53l0x.c +++ b/vl53l0x.chip.c @@ -32,34 +32,33 @@ static void update_measurement_registers(chip_state_t *chip) { uint32_t distance_mm = attr_read(chip->distance_attr); if (distance_mm > 8191) distance_mm = 8191; // 13-bit max range - // Range Millimeter High and Low bytes in result buffer + // Range Millimeter High and Low bytes in result buffer (0x14 + 10 = 0x1E) chip->registers[0x1E] = (uint8_t)((distance_mm >> 8) & 0xFF); chip->registers[0x1F] = (uint8_t)(distance_mm & 0xFF); - // Status: Sample Ready (bit 2 or bit 0 set, 0x04 / 0x07) - chip->registers[REG_RESULT_INTERRUPT_STATUS] = 0x07; - chip->registers[REG_RESULT_RANGE_STATUS] = 0x00; // Device ready / Valid measurement + // Status: Sample Ready (0x04 = NEW_SAMPLE_READY) + chip->registers[REG_RESULT_INTERRUPT_STATUS] = 0x04; + chip->registers[REG_RESULT_RANGE_STATUS] = 0x01; } static bool on_i2c_connect(void *user_data, uint32_t address, bool read) { chip_state_t *chip = (chip_state_t *)user_data; - - if (read) { - // When master starts a read, update sensor register data - update_measurement_registers(chip); - } else { - // Starting a write sequence: the first byte will be the register pointer + if (!read) { chip->is_first_write_byte = true; } - - return true; // ACK address + return true; } static uint8_t on_i2c_read(void *user_data) { chip_state_t *chip = (chip_state_t *)user_data; - - // Return current register byte and auto-increment pointer uint8_t value = chip->registers[chip->reg_ptr]; + + if (chip->reg_ptr == REG_SYSRANGE_START) { + value = 0x00; + } else if (chip->reg_ptr == 0x83) { + value = 0x01; + } + chip->reg_ptr++; return value; } @@ -68,22 +67,36 @@ static bool on_i2c_write(void *user_data, uint8_t data) { chip_state_t *chip = (chip_state_t *)user_data; if (chip->is_first_write_byte) { - // First byte after connect is register address index chip->reg_ptr = data; chip->is_first_write_byte = false; } else { - // Subsequent bytes are data written to the register chip->registers[chip->reg_ptr] = data; - // Handle triggers (e.g. Sysrange Start) - if (chip->reg_ptr == REG_SYSRANGE_START) { + if (chip->reg_ptr == 0x0B) { // SYSTEM_INTERRUPT_CLEAR + chip->registers[REG_RESULT_INTERRUPT_STATUS] = 0x00; + chip->registers[REG_RESULT_RANGE_STATUS] = 0x00; + } else if (chip->reg_ptr == REG_SYSRANGE_START) { update_measurement_registers(chip); + chip->registers[REG_RESULT_INTERRUPT_STATUS] = 0x04; + chip->registers[REG_RESULT_RANGE_STATUS] = 0x01; + } else if (chip->reg_ptr == 0x94) { // NVM Read selector + if (data == 0x6B) { + chip->registers[0x90] = 0x00; + chip->registers[0x91] = 0x00; + chip->registers[0x92] = 0x84; // 4 aperture SPADs + chip->registers[0x93] = 0x00; + } else { + chip->registers[0x90] = 0xFF; // All good SPADs + chip->registers[0x91] = 0xFF; + chip->registers[0x92] = 0xFF; + chip->registers[0x93] = 0xFF; + } } chip->reg_ptr++; } - return true; // ACK byte + return true; } static void on_i2c_disconnect(void *user_data) { @@ -109,9 +122,16 @@ void chip_init(void) { chip->registers[REG_IDENTIFICATION_MODEL_ID] = 0xEE; chip->registers[0xC1] = 0xAA; chip->registers[REG_IDENTIFICATION_REVISION_ID] = 0x10; - chip->registers[0x51] = 0x00; // VCSEL Period - chip->registers[0x61] = 0x00; - chip->registers[REG_RESULT_INTERRUPT_STATUS] = 0x07; + chip->registers[0x50] = 0x07; // Pre-range VCSEL period + chip->registers[0x70] = 0x05; // Final-range VCSEL period + chip->registers[0x83] = 0x00; + chip->registers[0x84] = 0x01; // Osc frequency (MSB) + chip->registers[0x85] = 0x00; // Osc frequency (LSB) + chip->registers[0x88] = 0x00; + chip->registers[0x89] = 0x00; + chip->registers[0x91] = 0x3C; // Stop variable + chip->registers[REG_RESULT_INTERRUPT_STATUS] = 0x04; + chip->registers[REG_RESULT_RANGE_STATUS] = 0x01; // Initial measurement values update_measurement_registers(chip); diff --git a/vl53l0x.chip.json b/vl53l0x.chip.json index 0812bd7..3e4b889 100644 --- a/vl53l0x.chip.json +++ b/vl53l0x.chip.json @@ -1,14 +1,7 @@ { - "name": "VL53L0X Time-of-Flight Sensor", - "author": "Antigravity", - "pins": [ - "VCC", - "GND", - "SCL", - "SDA", - "XSHUT", - "GPIO1" - ], + "name": "vl53l0x", + "author": "", + "pins": ["VCC", "GND", "SCL", "SDA", "XSHUT", "GPIO1"], "controls": [ { "id": "distance", diff --git a/vl53l0x.svg b/vl53l0x.svg new file mode 100644 index 0000000..d7cccd9 --- /dev/null +++ b/vl53l0x.svg @@ -0,0 +1,7 @@ + + + + + + VL53L0X + diff --git a/wokwi-api.h b/wokwi-api.h new file mode 100644 index 0000000..f3fd624 --- /dev/null +++ b/wokwi-api.h @@ -0,0 +1,162 @@ +#include +#include + +#ifndef WOKWI_API_H +#define WOKWI_API_H + +enum pin_value { + LOW = 0, + HIGH = 1 +}; + +enum pin_mode { + INPUT = 0, + OUTPUT = 1, + INPUT_PULLUP = 2, + INPUT_PULLDOWN = 3, + ANALOG = 4, + + OUTPUT_LOW = 16, + OUTPUT_HIGH = 17, +}; + +enum edge { + RISING = 1, + FALLING = 2, + BOTH = 3, +}; + +int __attribute__((export_name("__wokwi_api_version_1"))) __attribute__((weak)) __wokwi_api_version_1(void) { return 1; } + +#ifdef __cplusplus +extern "C" { +#endif + +typedef int32_t pin_t; +#define NO_PIN ((pin_t)-1) + +typedef struct { + void *user_data; + uint32_t edge; + void (*pin_change)(void *user_data, pin_t pin, uint32_t value); +} pin_watch_config_t; + +extern __attribute__((export_name("chipInit"))) void chip_init(void); + +extern __attribute__((import_name("pinInit"))) pin_t pin_init(const char *name, uint32_t mode); + +extern __attribute__((import_name("pinRead"))) uint32_t pin_read(pin_t pin); +extern __attribute__((import_name("pinWrite"))) void pin_write(pin_t pin, uint32_t value); +extern __attribute__((import_name("pinWatch"))) bool pin_watch(pin_t pin, const pin_watch_config_t *config); +extern __attribute__((import_name("pinWatchStop"))) void pin_watch_stop(pin_t pin); +extern __attribute__((import_name("pinMode"))) void pin_mode(pin_t pin, uint32_t value); +extern __attribute__((import_name("pinADCRead"))) float pin_adc_read(pin_t pin); +extern __attribute__((import_name("pinDACWrite"))) float pin_dac_write(pin_t pin, float voltage); + +typedef uint32_t string_t; +#define STRING_NULL 0 + +extern __attribute__((import_name("stringGetLength"))) uint32_t string_get_length(string_t string); +extern __attribute__((import_name("stringRead"))) uint32_t string_read(string_t string, char *buf, uint32_t buffer_size); + +extern __attribute__((import_name("attrInit"))) uint32_t attr_init(const char *name, uint32_t default_value); +extern __attribute__((import_name("attrInitFloat"))) uint32_t attr_init_float(const char *name, float default_value); +extern __attribute__((import_name("attrRead"))) uint32_t attr_read(uint32_t attr_id); +extern __attribute__((import_name("attrReadFloat"))) float attr_read_float(uint32_t attr_id); +extern __attribute__((import_name("attrStringInit"))) string_t attr_string_init(const char *name); + +typedef struct { + void *user_data; + uint32_t address; + pin_t scl; + pin_t sda; + bool (*connect)(void *user_data, uint32_t address, bool read); + uint8_t (*read)(void *user_data); + bool (*write)(void *user_data, uint8_t data); + void (*disconnect)(void *user_data); + uint32_t reserved[8]; +} i2c_config_t; + +typedef uint32_t i2c_dev_t; + +extern __attribute__((import_name("i2cInit"))) i2c_dev_t i2c_init(const i2c_config_t *config); + +typedef struct { + void *user_data; + pin_t rx; + pin_t tx; + uint32_t baud_rate; + void (*rx_data)(void *user_data, uint8_t byte); + void (*write_done)(void *user_data); + uint32_t reserved[8]; +} uart_config_t; + +typedef uint32_t uart_dev_t; + +extern __attribute__((import_name("uartInit"))) uart_dev_t uart_init(const uart_config_t *config); +extern __attribute__((import_name("uartWrite"))) bool uart_write(uart_dev_t uart, uint8_t *buffer, uint32_t count); + +typedef struct { + void *user_data; + pin_t sck; + pin_t mosi; + pin_t miso; + uint32_t mode; + void (*done)(void *user_data, uint8_t *buffer, uint32_t count); + uint32_t reserved[8]; +} spi_config_t; +typedef uint32_t spi_dev_t; + +extern __attribute__((import_name("spiInit"))) spi_dev_t spi_init(const spi_config_t *spi_config); +extern __attribute__((import_name("spiStart"))) void spi_start(const spi_dev_t spi, uint8_t *buffer, uint32_t count); +extern __attribute__((import_name("spiStop"))) void spi_stop(const spi_dev_t spi); + +typedef struct { + void *user_data; + void (*callback)(void *user_data); + uint32_t reserved[8]; +} timer_config_t; + +typedef uint32_t timer_t; + +extern __attribute__((import_name("timerInit"))) timer_t timer_init(const timer_config_t *config); +extern __attribute__((import_name("timerStart"))) void timer_start(const timer_t timer, uint32_t micros, bool repeat); +extern __attribute__((import_name("timerStartNanos"))) void timer_start_ns_d(const timer_t timer, double nanos, bool repeat); +static void timer_start_ns(const timer_t timer, uint64_t nanos, bool repeat) { + timer_start_ns_d(timer, (double)nanos, repeat); +} +extern __attribute__((import_name("timerStop"))) void timer_stop(const timer_t timer); + +extern __attribute__((import_name("getSimNanos"))) double get_sim_nanos_d(void); + +static uint64_t get_sim_nanos(void) { + return (uint64_t)get_sim_nanos_d(); +} + +typedef uint32_t buffer_t; +extern __attribute__((import_name("framebufferInit"))) buffer_t framebuffer_init(uint32_t *pixel_width, uint32_t *pixel_height); +extern __attribute__((import_name("bufferRead"))) void buffer_read(buffer_t buffer, uint32_t offset, void *data, uint32_t data_len); +extern __attribute__((import_name("bufferWrite"))) void buffer_write(buffer_t buffer, uint32_t offset, void *data, uint32_t data_len); + +// Experimental API - subject to change +extern __attribute__((import_name("_symbolResolve"))) void* _symbol_resolve(char *symbol_name); +extern __attribute__((import_name("_mcuReadMemory"))) bool _mcu_read_memory(const void *address, void *target, uint32_t size); +extern __attribute__((import_name("_mcuReadUint32"))) uint32_t _mcu_read_uint32(const void *address); +extern __attribute__((import_name("_mcuReadUint32"))) void* _mcu_read_ptr(const void *address); +extern __attribute__((import_name("_mcuReadPC"))) uint32_t _mcu_read_pc(); +extern __attribute__((import_name("_mcuReadSP"))) uint32_t _mcu_read_sp(); + +typedef struct { + void *user_data; + void (*callback)(void *user_data, uint32_t core, uint32_t sp); + uint32_t sp_min; + uint32_t sp_max; + uint32_t reserved[8]; +} sp_monitor_config_t; +extern __attribute__((import_name("_mcuMonitorSP"))) uint32_t _mcu_monitor_sp(const sp_monitor_config_t *config); + +#ifdef __cplusplus +} +#endif + +#endif /* WOKWI_API_H */ diff --git a/wokwi.toml b/wokwi.toml index e959a67..10bb368 100644 --- a/wokwi.toml +++ b/wokwi.toml @@ -2,4 +2,12 @@ version = 1 firmware = '.pio/build/esp32dev/firmware.bin' elf = '.pio/build/esp32dev/firmware.elf' -rfc2217ServerPort = 4000 \ No newline at end of file +rfc2217ServerPort = 4000 + +[[chip]] +name = "vl53l0x" +binary = "dist/vl53l0x_v2.wasm" + +[[chip]] +name = "tb6612fng" +binary = "dist/tb6612fng_v2.wasm" \ No newline at end of file