PathHole is an end‑to‑end system that turns a small ESP32‑powered vehicle into a pothole‑sensing, path‑following robot with a rich web dashboard.
The system is made of three main parts:
- ESP32 firmware: runs on the car, reads the IMU (MPU6050), estimates pose (position + heading), detects potholes, controls motors, and streams telemetry over WebSockets.
- Node.js backend: exposes a WebSocket server for the ESP32 and the React dashboard, validates all messages, persists telemetry and pothole data to MongoDB, and provides REST APIs for saved routes and statistics.
- React dashboard (Vite): visualizes live telemetry, renders a local map, shows pothole events, allows recording and saving routes, and sends autonomous driving commands back to the ESP32.
This README explains all of these in detail: architecture, data models, message formats, hardware/firmware, and how to run and extend the project.
At the top level, the project is split into the client, server, and ESP32 firmware:
path-hole/
ESP-32/
ESP32/ # Blynk-based prototype firmware (pothole detection + manual control)
ESP32.ino
ESP32_WS/ # WebSocket-based firmware used by this dashboard/backend
ESP32_WS.ino
client/ # React + Vite dashboard
public/
src/
App.jsx
main.jsx
hooks/
useWebSocket.js
components/
MapPanel.jsx
ControlPanel.jsx
PotholeAlert.jsx
ConnectionStatus.jsx
Chassis3D.jsx
ui/... # shadcn-like UI primitives (Card, Button, Slider, ...)
package.json
vite.config.*
server/ # Node.js backend (Express + ws + MongoDB)
models/
Route.js
Telemetry.js
Pothole.js
server.js
package.json
.gitignore
README.md # This file
-
ESP32 vehicle
- Hardware: ESP32, L298N motor driver, MPU6050 IMU, vibration sensor, active buzzer, wheels.
- Firmware:
ESP32_WS.ino– WebSocket‑based, integrates with this server/dashboard.ESP32.ino– Blynk‑based, used as an earlier prototype.
-
Backend server (
server/)- Node.js + Express + ws.
- MongoDB via Mongoose for persistence.
- Validates all messages using Ajv (JSON Schema).
- Maintains groups of WebSocket clients:
esp32anddashboard. - REST APIs for route CRUD and statistics.
-
React dashboard (
client/)- React + Vite SPA.
- Uses custom
useWebSockethook to connect to the backend and subscribe to live telemetry, pothole events, route completions, and status. - Components:
ControlPanel– manual driving & maybe other controls.MapPanel– local 2D map, route recording, route builder, auto‑drive, stats.Chassis3D– real‑time 3D visualization of vehicle orientation.PotholeAlert– surface pothole events.ConnectionStatus– backend/ESP32 connectivity state.
Telemetry & pothole detection path:
- ESP32 (firmware
ESP32_WS.ino) reads IMU data every loop and estimates:posX,posY– local 2D position estimate.headingDeg– heading in degrees.distanceMeters– integrated distance.- Filtered gyro and accel vectors.
- ESP32 sends WebSocket messages:
{ type: "telemetry", source: "esp32", ts, data: { ... } }to the backend. - Backend validates the message via Ajv (
telemetrySchema), then:- Broadcasts telemetry to all dashboard clients.
- Every ~2 seconds, stores a
Telemetrydocument in MongoDB (ifcurrentRouteIdis set).
- ESP32 computes jerk on Z axis (difference in successive Z acceleration). If jerk exceeds thresholds, it emits a
{ type: "pothole", ... }event with severity and value. - Backend validates the pothole, broadcasts it to dashboards, and stores it as a
Potholedocument (tagged withcurrentRouteId). - Dashboard consumes telemetry and pothole events via
useWebSocketand updates UI:MapPanelupdates live trail and pothole markers.Chassis3Dupdates vehicle orientation using a complementary filter inApp.jsx.
Manual control path:
- User interacts with the dashboard
ControlPanel(e.g., forward/reverse/left/right/stop), which constructs amotorControlmessage:{ type: "motorControl", source: "ui", data: { direction, speedLeft, speedRight } }.
- Dashboard sends this message over the WebSocket using the
sendcallback fromuseWebSocket. - Backend validates (
motorControlSchema) and forwards the message to the connected ESP32 client. - ESP32 receives the message and calls
setMotor()to drive the L298N driver pins. - A failsafe in the firmware stops motors if commands get stale (
lastCmdMsgap).
Route recording & persistence:
- When the car is driven manually, each telemetry update triggers
MapPanelto append a point{x, y, heading}totrailstate. - The user can type a route name and click Save route.
MapPanelsendsPOST /api/routesto the backend with a payload like:{ "name": "Test route", "description": "", "path": [ { "x": 0, "y": 0, "heading": 0 }, { "x": 1.2, "y": 0.5, "heading": 15 }, ... ] }- Backend saves a
Routedocument in MongoDB and returns it. MapPanelupdates itsrouteslist and selects the newly created route.
Autonomous drive path:
- The user selects a saved route or uses the Programmed route builder to generate a path (segments of meters + turns).
- The user sets auto speed and clicks Start autonomous drive (or Drive programmed route).
MapPanelsends anautoDrivemessage over WebSocket:{ "type": "autoDrive", "source": "ui", "ts": 123456789, "data": { "routeId": "<mongo-id>", "speed": 120, "path": [ {"x": ..., "y": ..., "heading": ...}, ... ] } }- Backend validates (
autoDriveSchema) and forwards it to ESP32, settingcurrentRouteId(for associating telemetry/potholes to the route). - ESP32 loads the path into a
route[]array and setsautoMode = true. - On each loop iteration, ESP32 calls
autoNavigate():- Computes vector to current waypoint.
- If distance < threshold ~0.15m, advance to next waypoint; if at end, stop and emit
routeCompleteevent. - Otherwise, compute steering error, map to left/right PWM speeds, and command motors.
- When route is completed, ESP32 sends
{ type: "routeComplete" }. - Backend broadcasts
routeComplete(includingrouteId), andMapPanelrecords the run inrecentRuns.
+----------------------+ +-------------------------+ +-------------------------+
| ESP32 Car | WS JSON | Node.js Backend | WS JSON | React Dashboard |
| (ESP32_WS.ino) +---------->+ (server/server.js) +--------->+ (client/src) |
| | | | | |
| - MPU6050 IMU | | - ws WebSocket server | | - useWebSocket hook |
| - Motor driver |<----------+ - message validation |<---------+ - ControlPanel |
| - Pothole detection | WS JSON | - broadcast telemetry | WS JSON | - MapPanel (map/routes)|
| - autoNavigate() | | - MongoDB (routes, | | - Chassis3D (3D view) |
+----------------------+ | telemetry, potholes) | +-------------------------+
+-------------------------+
- Node.js backend.
- Express – REST API endpoints.
- ws – WebSocket server sharing the same HTTP server.
- Ajv – JSON schema validation for all typed WebSocket messages.
- Mongoose – MongoDB ODM.
- dotenv – load
.envfor configuration. - cors – allow cross‑origin requests from the React client.
Environment variables (via .env in server/ or process env):
PORT(optional): port for HTTP + WebSocket server.- Default:
8080.
- Default:
MONGO_URL: MongoDB connection string.- Default:
mongodb://127.0.0.1:27017/pathhole.
- Default:
Example .env:
PORT=8080
MONGO_URL=mongodb://127.0.0.1:27017/pathholename: { type: String, required: true },
description: { type: String },
path: [
{
x: { type: Number, required: true },
y: { type: Number, required: true },
heading: { type: Number }
}
],
createdAt: { type: Date, default: Date.now }Each Route is a sequence of waypoints in local coordinates (meters), with optional heading at each point.
routeId: { type: mongoose.Schema.Types.ObjectId, ref: "Route" },
posX: Number,
posY: Number,
heading: Number,
speedLeft: Number,
speedRight: Number,
ts: { type: Number, required: true }A downsampled snapshot of the car’s state, stored every ~2 seconds during a run when currentRouteId is set on the server.
routeId: { type: mongoose.Schema.Types.ObjectId, ref: "Route" },
posX: Number,
posY: Number,
severity: String, // "low" | "medium" | "high" | (other -> grouped as "unknown")
value: Number, // jerk magnitude
ts: { type: Number, required: true }A Pothole document represents a single pothole event as detected on the ESP32.
Base URL: http://<server-host>:<PORT>/api (default http://localhost:8080/api).
- Description: List recent routes.
- Response (200): array of route summaries, sorted by
createdAtdesc.- Fields:
_id,name,description,createdAt.
- Fields:
- Description: Create a new route (from live trail or programmed builder).
- Body (JSON):
{ "name": "My test route", // required "description": "optional", // optional "path": [ // required, non-empty { "x": 0, "y": 0, "heading": 0 }, { "x": 1.2, "y": 0.5, "heading": 10 } ] } - Errors:
400 { error: "invalid_route" }if name missing or path invalid.500 { error: "failed_to_create_route" }on server error.
- Description: Return a full route including its full
patharray. - Responses:
200– full route document.404 { error: "route_not_found" }.500 { error: "failed_to_get_route" }.
- Description: Return calculated statistics for a route.
- Logic:
- Distance is computed by summing Euclidean distance between consecutive
pathpoints. - Pothole counts are aggregated from
Potholedocuments for thatrouteId.
- Distance is computed by summing Euclidean distance between consecutive
- Response (200):
{ "distanceMeters": 12.34, "potholesTotal": 5, "potholesBySeverity": { "low": 3, "medium": 1, "high": 1, "unknown": 0 } }
- Description: Return all potholes recorded for a route.
- Response (200): array of objects with
posX,posY,severity,value,ts. - Errors:
404 route_not_found,500 failed_to_get_route_potholes.
- Description: Update route
nameand/ordescription. - Body:
{ "name": "New name", // optional "description": "New desc" // optional } - Behavior:
- Only non-empty
nameand stringdescriptionare considered. - If no valid fields, returns
400 { error: "nothing_to_update" }.
- Only non-empty
- Responses:
200– updated route.404 route_not_found.500 failed_to_update_route.
- Description: Delete a route by id.
- Responses:
204on success.404 route_not_found.500 failed_to_delete_route.
All WebSocket messages use a common envelope schema:
{
"type": "telemetry" | "pothole" | "motorControl" | "autoDrive" | "pathCommand" | "hello" | "status" | "ping" | "pong" | "error" | "routeComplete",
"source": "esp32" | "ui" | "server",
"ts": 1234567890,
"data": { ... }
}The server compiles multiple JSON schemas via Ajv:
envelopeSchema– common outer shape.helloSchema– for client role registration.telemetrySchema– ESP32 telemetry.motorControlSchema– UI motor control commands.potholeSchema– ESP32 pothole events.pathCommandSchema– generic path commands (not heavily used by current UI).autoDriveSchema– autonomous drive path + speed.
If an incoming message fails validation, the server replies with an error message:
{
"type": "error",
"source": "server",
"ts": 1234567890,
"data": { "reason": "invalid_..." }
}- On connection, each WebSocket client sends
type: "hello":- ESP32:
{ data: { role: "esp32", deviceId: "esp32-01" } }. - Dashboard:
{ data: { role: "dashboard" } }.
- ESP32:
- Server tracks clients in
groups:{ esp32: Set(), dashboard: Set() }. statusUpdate()broadcastsstatusevents to all dashboards, e.g.:{ "type": "status", "source": "server", "ts": 1234567890, "data": { "esp32Connected": true, "reactClients": 1 } }
type: "telemetry",source: "esp32".datacontains:{ "speedLeft": 0-255, "speedRight": 0-255, "distance": 12.345, "heading": 90.0, "posX": 1.23, "posY": 0.45, "gyro": { "x": ..., "y": ..., "z": ... }, "accel": { "x": ..., "y": ..., "z": ... } }
Server actions:
- Broadcasts telemetry to dashboards.
- Every 2 seconds, persists a
Telemetrydocument withrouteId = currentRouteId.
type: "pothole",source: "esp32".datacontains:{ "severity": "low" | "medium" | "high", "value": 1.23, "posX": ..., "posY": ... }
Server actions:
- Broadcasts pothole to dashboards.
- Persists a
Potholedocument associated withcurrentRouteId.
type: "motorControl",source: "ui".datacontains:{ "direction": "forward" | "reverse" | "left" | "right" | "stop", "speedLeft": 0-255, "speedRight": 0-255 }
Server actions:
- Validates message.
- Finds first ESP32 client (if none, returns
esp32_disconnectederror). - Resets
currentRouteId = null(manual drive not linked to a route). - Forwards message to ESP32.
type: "autoDrive",source: "ui".datacontains:{ "routeId": "<mongo id>", // optional, for linking data "speed": 0-255, "path": [ {"x": ..., "y": ..., "heading": ...}, ... ] }
Server actions:
- Validates message.
- Forwards to ESP32.
- Sets
currentRouteIdtodata.routeId(or null if not provided).
type: "routeComplete",source: "esp32",datais empty from the ESP32 side.
Server actions:
- Broadcasts
routeCompletewithdata.routeId = currentRouteId. - Resets
currentRouteId = null.
There are two firmware sketches under ESP-32/.
This is the firmware that integrates directly with the Node backend and React dashboard.
Key features:
- Connects to Wi‑Fi (
WIFI_SSID,WIFI_PASS). - Maintains a WebSocket connection to the backend (
WS_HOST,WS_PORT,WS_PATH,USE_SSL). - Uses
Adafruit_MPU6050andAdafruit_Sensorfor IMU readings. - Estimates:
- Vehicle speed (
estSpeedMps). - Integrated distance (
distanceMeters). - Heading (
headingDeg). - 2D position (
posX,posY).
- Vehicle speed (
- Sends telemetry at ~20 Hz (every ~50 ms).
- Detects potholes via vertical acceleration jerk.
- Supports two modes:
- Manual control via
motorControlmessages. - Autonomous navigation via
autoDrive/pathCommandmessages.
- Manual control via
Important sections:
-
Wi‑Fi & WebSocket configuration:
const char* WIFI_SSID = "iot"; const char* WIFI_PASS = "110110110"; const char* WS_HOST = "10.15.82.112"; // set to your server IP const int WS_PORT = 8080; const char* WS_PATH = "/ws"; const bool USE_SSL = false;
You must change
WS_HOSTto point to your backend machine. -
WebSocket event handler
wsEvent:- On connect, sends
hellomessage to register asesp32. - On
motorControl, callssetMotor(...)and recordslastCmdMsfor failsafe. - On
autoDrive/pathCommand, loads waypoints intoroute[]and enablesautoMode. - On
ping, replies withpong.
- On connect, sends
-
IMU calibration & filtering:
calibrateIMU(n)samples IMUntimes to compute biases for gyro & accel.- Uses low‑pass filters and thresholds to reduce drift and noise.
-
sendTelemetry():- Reads IMU.
- Adjusts accel/gyro with biases.
- Computes estimated speed by blending PWM‑based and accel‑based estimates.
- Integrates heading and position.
- Sends
telemetryJSON via WebSocket. - Computes jerk on Z and triggers
potholeevents above thresholds.
-
autoNavigate():- Computes distance and angle to current waypoint.
- If close enough, moves to next waypoint.
- At final point, stops motors, sends
routeCompleteevent.
Failsafe:
- In
loop(), if command is stale (now - lastCmdMs > 300 ms), motors are stopped.
This sketch uses Blynk Cloud instead of your custom Node WebSocket server.
- Driven by Blynk virtual pins
V0(forward) andV4(reverse). - Uses MPU6050 Z acceleration and a vibration sensor to detect potholes.
- Sends sensor values to Blynk widgets using
Blynk.virtualWrite(V1, V2, V3). - Not directly integrated with the React dashboard; included as a previous iteration / alternative.
Security note:
ESP32.inocurrently contains hard‑codedBLYNK_AUTH_TOKENand Wi‑Fi credentials. In any public repository, you should remove or replace these with placeholders and use a privatesecrets.hor similar.
- React with Vite.
- Custom WebSocket hook (
useWebSocket.js). - Component library inspired by shadcn/ui for Cards, Buttons, Sliders.
- Styling via Tailwind‑like utility classes.
The app entry point is client/src/main.jsx, which mounts App into #root.
Signature:
const { connected, serverStatus, telemetry, pothole, routeEvent, send } = useWebSocket(url)Behavior:
- Connects to WebSocket URL (e.g.,
ws://localhost:8080/ws). - On open:
- Marks as
connected. - Sends
hellomessage:{ type: 'hello', source: 'ui', data: { role: 'dashboard' } }.
- Marks as
- On message:
type === 'telemetry'→setTelemetry({ ...msg.data, ts: msg.ts }).type === 'status'→setServerStatus(msg.data).type === 'pothole'→setPothole({ ...msg.data, ts: msg.ts }).type === 'routeComplete'→setRouteEvent({ ...msg.data, ts: msg.ts }).
- On close:
- Marks
connected = false. - Reconnects with exponential backoff up to 10 seconds.
- Marks
The send(obj) helper checks readyState and serializes the object as JSON.
Important pieces:
- Reads
wsUrlfrom env:VITE_WS_URL(defaults tows://localhost:8080/ws). - Calls
useWebSocket(wsUrl). - Maintains a complementary filter for orientation using telemetry’s gyro & accel:
- Integrates gyro to get yaw/roll/pitch.
- Uses accel vectors to compute stable roll/pitch.
- Combines them with
alpha = 0.96.
- Provides UI controls to:
- Zero orientation.
- Adjust 3D
smoothingforChassis3D.
- Displays:
- Connection status.
- 3D chassis with orientation.
- Distance traveled.
- Pothole alerts.
- The
MapPanelwith routing features.
MapPanel orchestrates most of the route logic and visualization.
Key state variables:
trail– live path from telemetry (list of{x, y, heading}).potholes– live pothole markers from events.routes– routes fetched from/api/routes.selectedRouteId– active route selection.selectedRoutePath– full path array from the server for the selected route.routeStats– stats from/api/routes/:id/stats.routePotholes– stored potholes from/api/routes/:id/potholes.useStoredPotholes– toggles between showing live vs stored potholes.driveStatus–'idle' | 'driving' | 'completed'.autoSpeed– PWM value for auto drive.builderSegments– list of{ length, turn }segments for the programmed route.builderPath– generated path points from segments.recentRuns– last few route completion events.
Key behaviors:
-
Live trail accumulation:
- On telemetry updates with numeric
posX/posY, append totrail; keep last 2000 points.
- On telemetry updates with numeric
-
Live pothole accumulation:
- On
potholeevents, append{x, y}topotholes.
- On
-
Loading routes:
- On mount, fetch
/api/routesand populateroutes. - On
selectedRouteIdchange:- Fetch
/api/routes/:idfor path. - Fetch
/api/routes/:id/statsfor summary stats. - Fetch
/api/routes/:id/potholesfor stored potholes.
- Fetch
- On mount, fetch
-
Canvas rendering:
- Collects all points from
trail,selectedRoutePath,builderPath, and the active set of potholes. - Computes bounding box and scales them to fit the canvas with padding.
- Draws:
- Live trail (dark line).
- Selected saved route (green line).
- Programmed route (blue line).
- Potholes (red dots).
- Collects all points from
-
Route saving from live trail:
handleSaveRoute()poststrailaspathto/api/routes.
-
Export route JSON:
handleExportRoute()builds a JSON object containing route metadata, path, and stored potholes and triggers a browser download.
-
Route rename/delete:
handleRenameRoute()PUTs to/api/routes/:id.handleDeleteRoute()DELETEs/api/routes/:idand updatesroutes.
-
Autonomous drive:
handleStartAuto()sends anautoDrivemessage based on the selected saved route.handleStartBuilderAuto()sends anautoDrivemessage based on the builderPath (programmed route) and optionalselectedRouteId.
-
Programmed route builder:
- User adds segments: each segment is defined by length (m) and turn (degrees).
- A deterministic path generator walks from
(0,0)withheading=0, adding each segment sequentially to buildbuilderPath. - User can save a programmed route just like a normal route, using
/api/routes.
-
Recent runs:
- On
routeEvent(from WebSocket), updatesrecentRunswith route name and timestamp.
- On
ControlPanel– (not fully documented here, but conceptually): sendsmotorControlmessages, may show speed/heading or other controls.PotholeAlert– surfaces latest pothole event to user.ConnectionStatus– indicates whether the dashboard is connected to the server and whether the ESP32 is online.Chassis3D– 3D rendering using the orientation provided byApp.jsx. It’s fedpitch,roll,yaw, andsmoothinglevel.
- Node.js (LTS) and npm.
- MongoDB (local or remote instance).
- Arduino IDE or PlatformIO for ESP32 firmware.
- ESP32 development board + supporting hardware (MPU6050, L298N, motors, etc.).
Make sure MongoDB is running and accessible at MONGO_URL (e.g. mongodb://127.0.0.1:27017/pathhole).
cd server
npm install
# create .env (or rely on defaults)
# PORT=8080
# MONGO_URL=mongodb://127.0.0.1:27017/pathhole
npm startThis runs server.js with nodemon on the configured port (default 8080) and prints:
MongoDB connected
WS listening on :8080/ws
cd client
npm install
# Optionally configure WebSocket URL (defaults to ws://localhost:8080/ws)
# Create .env.local or .env:
# VITE_WS_URL=ws://<backend-host>:8080/ws
npm run devOpen the displayed Vite dev server URL (e.g. http://localhost:5173) in your browser.
- Open
ESP-32/ESP32_WS/ESP32_WS.inoin Arduino IDE. - Install required libraries (via Library Manager):
WiFi(bundled with ESP32 core).WebSocketsClient.ArduinoJson.Adafruit_MPU6050.Adafruit_Sensor.
- Configure Wi‑Fi and server:
- Set
WIFI_SSIDandWIFI_PASS. - Set
WS_HOSTto the IP or hostname of the machine running the Node server. - Leave
WS_PORTandWS_PATHmatching the server (8080and/wsunless changed).
- Set
- Choose your ESP32 board and serial port.
- Upload the sketch.
- Open Serial Monitor at 115200 baud to see logs.
Once ESP32 connects, you should see it:
- Connect to Wi‑Fi.
- Connect to WebSocket.
- Send
hellomessage. - Start streaming telemetry.
- Start server and MongoDB.
- Start React client.
- Flash ESP32_WS firmware and power the car.
- Confirm in the dashboard:
- Connection status shows server and ESP32 connected.
- Live telemetry (orientation, distance) updates.
MapPanelshows a live trail when you drive.
- Trigger a pothole (e.g., by a controlled bump) and verify:
- Pothole alert is shown.
- Red dot appears on the map.
- Save the trail as a route and then start an autonomous drive.
Some ideas and natural extension points:
-
Global coordinates / GPS integration:
- Currently
posX/posYare in a local frame. You can extend telemetry and routes to include GPS data and plot on real maps (Leaflet/Mapbox).
- Currently
-
More advanced SLAM / sensor fusion:
- Replace the simple velocity integration with a proper EKF or Madgwick filter for better position/orientation estimates.
-
WebSocket authentication:
- Right now, any client can connect and send messages if they know the endpoint. You can add JWT‑based authentication or per‑device secrets.
-
Better route planning:
- Use algorithms (e.g., A*) in the dashboard or server to generate optimal paths around detected potholes.
-
Historical analytics:
- Add pages to visualize heatmaps of pothole density, route comparison, historical statistics, etc., by querying
TelemetryandPotholecollections.
- Add pages to visualize heatmaps of pothole density, route comparison, historical statistics, etc., by querying
-
Multi‑vehicle support:
- Track multiple ESP32 cars with different
deviceIdvalues and manage them independently in the dashboard.
- Track multiple ESP32 cars with different
PathHole combines embedded sensing, real‑time WebSockets, and a web dashboard to:
- Detect potholes using IMU jerk.
- Live‑stream telemetry from an ESP32 car.
- Visualize paths and potholes on a local map.
- Persist and replay routes from MongoDB.
- Command the car to drive autonomously along saved or programmed routes.
This README walked through the repository layout, backend design and APIs, ESP32 firmware logic, React dashboard architecture, and practical steps to run and extend the system. You can now treat this as the main documentation entry point for the project.

