Real-time vehicle dynamics tracking using GPS+IMU sensor fusion. Professional-grade algorithms on $50 hardware.
A complete ESP32-C3 firmware that fuses GPS and IMU data to track your vehicle's position, velocity, and acceleration in real-time. Built for track day logging, vehicle dynamics research, and DIY automotive projects.
What it does:
- Tracks position (±1m accuracy between GPS updates)
- Measures velocity in 2D (earth frame)
- Calculates true acceleration (gravity compensated)
- Detects driving modes (idle, accelerating, braking, cornering)
- Streams telemetry at 20-30Hz over WiFi
- Built-in mobile dashboard - view live data on your phone
How it works:
- 7-state Extended Kalman Filter fuses GPS (5Hz) and IMU (50Hz)
- Gyro integrates yaw between GPS updates to maintain heading
- Body-frame accelerations transformed to earth frame using current orientation
- Zero-velocity updates eliminate IMU drift when stationary
- Constant Turn Rate and Acceleration (CTRA) model for cornering
Why it's useful:
- Track day data logging without $500+ commercial systems
- Vehicle dynamics research and education
- DIY EV/kit car development
- Test suspension, brakes, and aerodynamics
- Learn sensor fusion practically
| Component | Cost | Purpose | Where to Buy |
|---|---|---|---|
| ESP32-C3 DevKit | $5 | Main controller | AliExpress, Amazon |
| WT901 IMU | $25 | 9-axis motion sensor | WitMotion store |
| NEO-6M GPS | $15 | Position/velocity | AliExpress, Amazon |
| Wires + USB cable | $5 | Connections | - |
- WS2812 RGB LED ($2) - Visual status indicator
- SD card module ($5) - Offline data logging (not yet implemented)
- CAN transceiver ($10) - Read vehicle data (future feature)
ESP32-C3 DevKitC-02 WT901 IMU NEO-6M GPS
┌─────────────────┐ ┌──────────┐ ┌──────────┐
│ │ │ │ │ │
│ 3V3 ────────────┼──────────┤ VCC │ │ │
│ GND ────────────┼──────────┤ GND │ │ │
│ GPIO19 (RX) ────┼──────────┤ TX │ │ │
│ GPIO18 (TX) ────┼──────────┤ RX │ │ │
│ │ └──────────┘ │ │
│ 3V3 ────────────┼──────────────────────────────────┤ VCC │
│ GND ────────────┼──────────────────────────────────┤ GND │
│ GPIO4 (RX) ────┼──────────────────────────────────┤ TX │
│ GPIO5 (TX) ────┼──────────────────────────────────┤ RX │
│ │ └──────────┘
│ GPIO8 ──────────┼── (Optional: WS2812 LED data pin)
│ │
└─────────────────┘
Important: GPS uses hardware UART0 (the same as USB console). During GPS operation, you won't see serial output unless using a separate USB-JTAG debugger. This is by design to free up the UART for GPS.
Flash directly from your browser - no toolchain required:
- Go to https://jctoledo.github.io/blackbox/
- Connect your ESP32-C3 via USB
- Select "Blackbox" from the dropdown
- Click "Connect Device" and select your serial port
- Click "Flash Firmware"
Requirements: Chrome, Edge, or Opera (Web Serial API)
Note: You may need to hold the BOOT button when connecting to enter flash mode.
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
# Install ESP32 tools
cargo install espup cargo-espflash
espup install
# Load ESP environment (required in EVERY new terminal)
source $HOME/export-esp.sh
# Add rust-src component (required for ESP32)
rustup component add rust-src --toolchain espgit clone https://github.com/jctoledo/blackbox.git
cd blackbox/sensors/blackbox
# Edit WiFi credentials
nano src/main.rs
# Change these lines:
# const WIFI_SSID: &str = "YourNetworkName";
# const WIFI_PASSWORD: &str = "YourPassword";
# const MQTT_BROKER: &str = "mqtt://192.168.1.100:1883"; // Your broker IP
# const TCP_SERVER: &str = "192.168.1.100:9000"; // Your server IPThe project includes a .cargo/config.toml that configures everything automatically.
# Navigate to the sensor project
cd sensors/blackbox
# Load ESP environment first (in every new terminal!)
source $HOME/export-esp.sh
# Quick check (fast, no linking)
cargo check
# Build firmware
cargo build --release
# Build and flash in one command
cargo run --releaseFirst build takes 10-20 minutes as it downloads ESP-IDF. Subsequent builds: 30s-2min.
UDP Telemetry (recommended)
# In another terminal, from repo root
cd tools/python
python3 udp_telemetry_server.pyReceives 20Hz binary telemetry and displays:
[20Hz] Spd: 45.3km/h Pos:(123.4,456.7)m Vel:(+12.58,+0.32)m/s Acc:(+1.23,-0.15,+9.81)m/s² Yaw:+45° wz:+12°/s CORNER
MQTT (for status messages and debugging)
# Requires mosquitto or another MQTT broker
mosquitto_sub -h 192.168.1.100 -t 'car/#' -vUbuntu/Debian Linux
# System dependencies
sudo apt-get update
sudo apt-get install -y git wget flex bison gperf python3 python3-pip \
python3-venv cmake ninja-build ccache libffi-dev libssl-dev \
dfu-util libusb-1.0-0 build-essential
# Rust and ESP toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
cargo install espup cargo-espflash
espup install
source $HOME/export-esp.sh
rustup component add rust-src --toolchain espmacOS
# Install Homebrew if needed
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# System dependencies
brew install cmake ninja dfu-util
# Rust and ESP toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
cargo install espup cargo-espflash
espup install
source $HOME/export-esp.sh
rustup component add rust-src --toolchain espWindows
- Install Visual Studio Build Tools (C++ development)
- Install Python 3
- Install Git
- Install Rust via rustup-init.exe
- Open PowerShell and run:
cargo install espup cargo-espflash
espup install
# Restart terminal to load environment
rustup component add rust-src --toolchain espThe .cargo/config.toml file in the sensor project configures the target and build settings automatically:
# Navigate to the sensor project first
cd sensors/blackbox
# ALWAYS load ESP environment first!
source $HOME/export-esp.sh
# Quick check (no linking, fast)
cargo check
# Full build
cargo build --release
# Build and flash with monitor
cargo run --release
# Format code
cargo fmt
# Lint
cargo clippy -- -D warnings
# Clean build (if things go wrong)
cargo cleanNo need to specify --target or -Zbuild-std - it's all configured!
Click to see troubleshooting
Error: "can't find crate for core" or "can't find crate for std"
This means the ESP toolchain isn't loaded or rust-src is missing:
# Solution 1: Make sure you're in the sensor directory
cd sensors/blackbox
# Solution 2: Load ESP environment
source $HOME/export-esp.sh
# Solution 3: Add rust-src component
rustup component add rust-src --toolchain esp
# Solution 4: Verify .cargo/config.toml exists
cat .cargo/config.toml
# Should contain:
# [unstable]
# build-std = ["std", "panic_abort"]Error: "linking with riscv32-esp-elf-gcc failed"
source $HOME/export-esp.sh # You forgot this!Error: "CMake not found"
# Ubuntu/Debian
sudo apt-get install cmake ninja-build
# macOS
brew install cmake ninjaError: "Permission denied" on serial port (Linux)
sudo usermod -a -G dialout $USER
# Log out and back in for group change to take effectBuild hangs or crashes
cargo clean
rm -rf ~/.espressif
espup install
source $HOME/export-esp.sh
rustup component add rust-src --toolchain esp
cargo build --releaseError: "espflash: command not found"
cargo install cargo-espflashGPS (5Hz) IMU (50Hz)
│ │
│ NMEA │ Binary packets
│ sentences │
▼ ▼
┌─────────────────────────────────┐
│ Sensor Parsers │
│ • Parse lat/lon/speed │
│ • Parse ax,ay,az,wx,wy,wz │
│ • Parse roll,pitch,yaw │
└────────────┬────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Coordinate Transforms │
│ • Remove gravity from accel │
│ • Body → Earth frame │
└────────────┬────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Extended Kalman Filter │
│ State: [x, y, ψ, vx, vy, │
│ bias_ax, bias_ay] │
│ │
│ • Predict using IMU (50Hz) │
│ • Update using GPS (5Hz) │
│ • ZUPT when stationary │
└────────────┬────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Mode Classifier │
│ • Detect: IDLE, ACCEL, │
│ BRAKE, CORNER │
└────────────┬────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Binary Telemetry (20Hz) │
│ • 67 bytes with checksum │
│ • UDP stream │
└─────────────────────────────────┘
blackbox/
├── sensors/
│ └── blackbox/
│ ├── .cargo/
│ │ └── config.toml # ESP32-C3 build configuration
│ ├── src/
│ │ ├── main.rs # Main loop and setup
│ │ ├── imu.rs # WT901 UART parser
│ │ ├── gps.rs # NMEA parser with warmup
│ │ ├── ekf.rs # 7-state Extended Kalman Filter
│ │ ├── transforms.rs # Body↔Earth coordinate math
│ │ ├── mode.rs # Driving mode classifier
│ │ ├── binary_telemetry.rs # 67-byte packet format
│ │ ├── websocket_server.rs # Mobile dashboard & WebSocket server
│ │ ├── udp_stream.rs # High-speed UDP client
│ │ ├── mqtt.rs # MQTT client for status
│ │ ├── wifi.rs # WiFi connection manager
│ │ └── rgb_led.rs # WS2812 status LED
│ ├── Cargo.toml # Rust dependencies
│ ├── sdkconfig.defaults # ESP-IDF configuration
│ └── build.rs # Build script
├── tools/
│ └── python/
│ ├── udp_telemetry_server.py # Python receiver (UDP)
│ └── mqtt_binary_decoder.py # Python receiver (MQTT)
└── README.md # This file
Extended Kalman Filter (ekf.rs)
- 7-state vector:
[x, y, ψ, vx, vy, bias_ax, bias_ay] - Prediction: IMU accelerations integrated to update velocity and position
- Update: GPS position/velocity fused with Kalman gain
- CTRA motion model when speed > 2 m/s and turning
Zero-Velocity Update (main.rs)
- Detects stationary:
|accel| < 0.18g && |yaw_rate| < 12°/s && GPS_speed < 3.5m/s - Forces velocity to zero
- Estimates IMU biases from residual accelerations
- Eliminates drift over time
Mode Detection (mode.rs)
- IDLE: Low acceleration or below minimum speed
- ACCEL: Longitudinal accel exceeds threshold (configurable via presets)
- BRAKE: Longitudinal decel exceeds threshold (configurable via presets)
- CORNER: Lateral accel AND yaw rate exceed thresholds, same sign
- ACCEL+CORNER: Trail throttle / corner exit
- BRAKE+CORNER: Trail braking / corner entry
Thresholds are adjustable via the dashboard's Driving Preset selector with 4 built-in profiles plus custom tuning.
struct TelemetryPacket {
header: u16, // 0xAA55 magic
timestamp_ms: u32,
ax, ay, az: f32, // Acceleration (m/s²)
wz: f32, // Yaw rate (rad/s)
roll, pitch: f32, // Orientation (rad)
yaw: f32, // EKF yaw (rad)
x, y: f32, // Position (m, ENU frame)
vx, vy: f32, // Velocity (m/s)
speed_kmh: f32, // Display speed
mode: u8, // 0=IDLE, 1=ACCEL, 2=BRAKE, 4=CORNER, 5=ACCEL+CORNER, 6=BRAKE+CORNER
lat, lon: f32, // GPS (degrees)
gps_valid: u8, // 0=no fix, 1=valid
checksum: u16, // Sum of all bytes (excl. checksum)
}car/telemetry_bin- Binary packets (if using MQTT instead of TCP)car/status- JSON status messagescar/config- Configuration updatescar/gps_raw- NMEA sentences (debug only)
Edit sensors/blackbox/src/main.rs:
const WIFI_SSID: &str = "YourNetwork";
const WIFI_PASSWORD: &str = "YourPassword";
const MQTT_BROKER: &str = "mqtt://192.168.1.100:1883";
const TCP_SERVER: &str = "192.168.1.100:9000";Edit sensors/blackbox/src/ekf.rs:
const Q_ACC: f32 = 0.40; // Process noise: acceleration (m/s²)²
const Q_GYRO: f32 = 0.005; // Process noise: gyro (rad/s)²
const Q_BIAS: f32 = 1e-3; // Process noise: bias drift (m/s²)²
const R_POS: f32 = 20.0; // Measurement noise: GPS position (m)²
const R_VEL: f32 = 0.2; // Measurement noise: GPS velocity (m/s)²
const R_YAW: f32 = 0.10; // Measurement noise: magnetometer (rad)²Mode detection thresholds can be configured live from the mobile dashboard using the Driving Preset selector. Choose from 4 built-in presets or create custom settings:
| Preset | Accel | Brake | Lateral | Yaw | Min Speed | Best For |
|---|---|---|---|---|---|---|
| Track | 0.35g | 0.55g | 0.50g | 0.15 rad/s | 4.0 m/s | Racing, track days |
| Canyon | 0.22g | 0.35g | 0.28g | 0.10 rad/s | 3.0 m/s | Spirited mountain roads |
| City | 0.10g | 0.18g | 0.12g | 0.05 rad/s | 2.0 m/s | Daily driving (default) |
| Highway | 0.12g | 0.22g | 0.14g | 0.04 rad/s | 5.0 m/s | Highway cruising |
| Custom | User-defined via sliders | Fine-tuning |
Each threshold has an entry and exit value (hysteresis) to prevent oscillation. Exit thresholds are typically 50% of entry values.
Threshold tuning tips:
- Too many false detections? Increase thresholds
- Missing real events? Decrease thresholds
- Road bumps triggering modes? Increase thresholds or check calibration
Preset Selection:
- Open dashboard at
http://192.168.71.1 - Scroll to "Driving Preset" section
- Tap a preset button - settings apply immediately
- Tap "Custom" to access individual sliders for fine-tuning
Note: Mode detection requires speed > min_speed. This prevents false detection from IMU noise when stationary. Higher min_speed on Highway preset filters out parking lot maneuvers.
| Metric | Value |
|---|---|
| Update rate | 50 Hz (IMU predict) / 5 Hz (GPS update) |
| Telemetry rate | 20 Hz (UDP stream) |
| Position accuracy | ±1-2m (GPS dependent) |
| Velocity accuracy | ±0.2 m/s |
| Latency | <20ms sensor-to-transmission |
| Memory usage | ~50KB RAM, ~800KB flash |
| Power | ~0.5W @ 5V |
| Pattern | Meaning |
|---|---|
| 3 magenta blinks | Boot sequence started (AP mode) |
| 5 green blinks | WiFi AP started |
| 3 cyan blinks | HTTP server ready |
| Yellow pulses | IMU calibration in progress |
| Continuous red blink | Critical error (WiFi AP failed) |
| Pattern | Meaning |
|---|---|
| 3 blue blinks | Boot sequence started (Station mode) |
| 5 green blinks | WiFi connected to network |
| 3 magenta blinks | MQTT connected |
| 5 fast red blinks | MQTT connection failed (continuing without MQTT) |
| 3 cyan blinks | UDP socket ready |
| Yellow pulses | IMU calibration in progress |
| Continuous red blink | Critical error (WiFi connection failed) |
| Pattern | Meaning |
|---|---|
| Cyan pulse (2s cycle) | GPS locked, system operational |
| Yellow fast blink | Waiting for GPS fix |
| 3 green-white flashes | Settings changed via dashboard |
| 3 orange blinks | WiFi disconnected (repeats every 5s) |
| 2 red blinks | MQTT disconnected (Station mode only, repeats every 5s) |
Note: MQTT status LED only applies to Station mode. In Access Point mode, MQTT is not used and no red blinks will occur for MQTT status.
Proper calibration is critical for accurate mode detection. The IMU calibration measures accelerometer biases that are subtracted from all future readings.
-
Mount the device in its final position in the car
- Must be rigidly attached (no wobble or vibration)
- Orientation matters: device forward = car forward
- Does NOT need to be perfectly level (IMU handles tilt)
-
Park on reasonably level ground
- Doesn't need to be perfectly flat
- Avoid steep hills during calibration
-
Turn OFF the engine
- Engine vibration corrupts calibration
- For EVs, ensure the car is in Park with no systems active
-
Power on the ESP32
- Wait for boot sequence LED pattern
-
Don't touch ANYTHING during yellow LED flashing (~10 seconds)
- The device collects 150 accelerometer samples
- ANY movement corrupts the bias calculation
- Keep doors closed, don't sit in the car
-
Wait for operational state
- Yellow fast blink = waiting for GPS lock
- Cyan pulse = GPS locked, ready to drive
You can trigger recalibration from the dashboard:
- Open dashboard at
http://192.168.71.1 - Stop the vehicle on level ground, engine off
- Tap the CLR button to reset and recalibrate
- Mode detection triggers incorrectly (constant ACCEL or BRAKE)
- Non-zero acceleration shown when stationary
- Speed reads incorrectly
- G-meter not centered when parked
Fix: Power cycle and recalibrate following the procedure above.
| Do | Don't |
|---|---|
| Mount rigidly with zip ties or bracket | Use velcro or loose mounting |
| Calibrate with engine off | Calibrate while idling |
| Wait for full yellow LED sequence | Touch car during calibration |
| Orient device correctly (forward = forward) | Mount sideways or upside down |
| Recalibrate if detection seems wrong | Assume it's a software bug |
The firmware includes a built-in web dashboard that runs directly on the ESP32. No external server needed - just connect your phone and view live telemetry.
- Power on the device - it creates a WiFi network called "Blackbox"
- Connect your phone to the "Blackbox" WiFi (password:
blackbox123) - Open a browser and go to
http://192.168.71.1 - View live telemetry - data streams at ~30Hz via WebSocket
| Feature | Description |
|---|---|
| G-meter | Real-time lateral/longitudinal G display with trail |
| Max G values | Tracks peak L/R/Accel/Brake G-forces |
| Speed | Current speed and session max speed |
| Driving mode | IDLE, ACCEL, BRAKE, CORNER, or combined states (ACCEL+CORNER, BRAKE+CORNER) |
| Driving presets | One-tap presets for Track, Canyon, City, Highway, or Custom tuning |
| Session timer | Time since dashboard loaded |
| GPS status | Current coordinates and fix status |
| Recording | Capture data locally for CSV export |
| Action | Effect |
|---|---|
| CLR button | Clear max G values |
| Tap max speed | Reset max speed only |
| Double-tap G-meter | Reset max G values only |
| Preset buttons | Switch driving profile (Track/Canyon/City/Highway/Custom) |
| Reset button | Reset sliders to current preset defaults (Custom mode only) |
| Apply button | Send custom slider values to device (Custom mode only) |
| REC button | Start/stop recording data |
| Export button | Download recorded data as CSV |
The indicator in the top-right shows connection quality:
- Blue dot + "WS" - WebSocket connected (~30Hz, best)
- Green dot + "HTTP" - HTTP polling fallback (~15Hz)
- Gray dot + "Offline" - No connection
The firmware supports two WiFi modes, each with different connectivity options:
Best for: Mobile use, track days, standalone operation
┌──────────────┐ WiFi: "Blackbox" ┌──────────────┐
│ ESP32 │◄─────────────────────────►│ Phone │
│ (creates │ 192.168.71.1:80 │ (connects) │
│ network) │ │ │
└──────────────┘ └──────────────┘
- ESP32 creates its own WiFi network:
Blackbox/blackbox123 - Connect your phone directly to view the dashboard
- No router or internet required
- Data delivered via WebSocket (fast) or HTTP polling (fallback)
- No Python tools - dashboard only
To use (default, no changes needed):
cargo build --releaseBest for: Home testing, data logging to laptop, multiple receivers
┌──────────────┐ ┌──────────────┐
│ ESP32 │ Your WiFi Network │ Laptop │
│ (connects │◄─────────────────────────►│ (Python │
│ to router) │ │ tools) │
└──────────────┘ └──────────────┘
│ │
│ ┌──────────┐ │
└──────────────►│ Router │◄────────────┘
└──────────┘
- ESP32 connects to your existing WiFi network
- Telemetry sent via UDP to your laptop (high-speed, 20Hz)
- Status messages via MQTT broker
- Python tools can receive and log data
- Dashboard NOT available (no web server in this mode)
To use:
# Set environment variables before building
export WIFI_MODE="station"
export WIFI_SSID="YourHomeNetwork"
export WIFI_PASSWORD="YourPassword"
export MQTT_BROKER="mqtt://192.168.1.100:1883"
export UDP_SERVER="192.168.1.100:9000"
cargo build --releaseThe Python tools in tools/python/ only work in Station mode when ESP32 is on the same network as your laptop.
Setup:
# Install dependencies
cd tools/python
pip install -r requirements.txt
# Start MQTT broker (if not already running)
# On Ubuntu: sudo apt install mosquitto && sudo systemctl start mosquitto
# On macOS: brew install mosquitto && brew services start mosquittoReceive UDP Telemetry (recommended):
python3 udp_telemetry_server.pyOutput:
[20Hz] Spd: 45.3km/h Pos:(123.4,456.7)m Acc:(+1.23,-0.15)m/s² CORNER
Receive MQTT Status:
python3 mqtt_binary_decoder.pySubscribe to raw MQTT topics:
mosquitto_sub -h localhost -t 'car/#' -v| Feature | Access Point | Station |
|---|---|---|
| Mobile dashboard | ✅ Yes | ❌ No |
| Python tools | ❌ No | ✅ Yes |
| Needs router | ❌ No | ✅ Yes |
| Range | ~30m | Network dependent |
| Multiple receivers | ❌ No | ✅ Yes |
| Data logging to laptop | ❌ No | ✅ Yes |
- SD card logging for offline operation
-
Web dashboard for live visualization✓ Built-in mobile dashboard - CAN bus integration for vehicle data
- Support for external wheel speed sensors
- Bluetooth for phone connectivity
- Over-the-air firmware updates
Contributions welcome! Whether it's:
- Bug fixes
- New sensor drivers
- Documentation improvements
- Performance optimizations
Open an issue or pull request on GitHub.
MIT License - see LICENSE for details.
Built with:
- esp-idf-hal - Rust ESP32 HAL
- esp-idf-svc - ESP-IDF services
- WT901 IMU from WitMotion
- U-blox NEO-6M GPS module
Inspired by open-source motorsport and robotics projects that prove pro-grade systems don't need pro prices.
Questions? Open an issue.
Ready to track? Start with Quick Start ↑