Skip to content

Distance Measurement Methods

How to measure distance traveled using different sensor approaches, step by step.

Overview

There are three main approaches to measure distance with our robot:

Method Sensor Accuracy Drift Best For
Ultrasonic HC-SR04 High None Absolute distance to wall
Bump Counting Accelerometer Good Low Relative distance traveled
Double Integration Accelerometer Low High Short movements only

Method 1: Ultrasonic Distance (Simplest)

How It Works

The ultrasonic sensor measures distance to the nearest obstacle:

Robot                    Wall
  █  ──── sound ────►    │
  █  ◄─── echo ─────     │
     └── distance ──────►

Formula: $\(distance = \frac{time \times speed\_of\_sound}{2}\)$

Where speed of sound ≈ 343 m/s at 20°C.

Step-by-Step

Step 1: Read distance at start

start_distance = ultrasonic.measure_cm()
# Example: 45.2 cm

Step 2: Drive robot forward

motors.set_speed(50, 50)
time.sleep(2)
motors.stop()

Step 3: Read distance at end

end_distance = ultrasonic.measure_cm()
# Example: 25.8 cm

Step 4: Calculate travel distance

traveled = start_distance - end_distance
# 45.2 - 25.8 = 19.4 cm traveled

Advantages
  • No calibration needed
  • Absolute measurement
  • No drift over time
Limitations
  • Needs a wall/obstacle in front
  • Only measures forward/backward
  • Max range ~400 cm

Code Example

def measure_distance_ultrasonic():
    """Measure distance traveled using ultrasonic sensor."""
    # Read starting position
    start = ultrasonic.measure_cm()
    print(f"Start distance: {start:.1f} cm")

    # Wait for user to drive robot
    input("Drive the robot, then press Enter...")

    # Read ending position
    end = ultrasonic.measure_cm()
    print(f"End distance: {end:.1f} cm")

    # Calculate travel
    traveled = abs(start - end)
    print(f"Distance traveled: {traveled:.1f} cm")

    return traveled

Method 2: Bump Counting (Best for Wheels)

How It Works

Wheel imperfections create vibrations detected by the accelerometer:

Wheel cross-section:
      ___
     /   \    ← bump hits ground
    |  ●  |
     \___/

As wheel rotates: bump → accelerometer spike → count it

Bumps per rotation: ~10-20 (depends on wheel quality)

Formula: $\(distance = \frac{bump\_count}{bumps\_per\_cm}\)$

Where bumps_per_cm is determined by calibration.

Step-by-Step

Step 1: Record calibration run

Drive robot a known distance (e.g., 50 cm measured with ruler):

# Drive toward wall (using ultrasonic as reference)
start_us = ultrasonic.measure_cm()
motors.set_speed(50, 50)
time.sleep(2)
motors.stop()
end_us = ultrasonic.measure_cm()

known_distance = start_us - end_us

Step 2: Count bumps during run

Apply signal processing:

# 1. Calculate acceleration magnitude
mag = sqrt(ax**2 + ay**2 + az**2) - 1.0

# 2. High-pass filter (remove DC offset)
filtered = alpha * (prev_filtered + mag - prev_mag)

# 3. Count threshold crossings
if filtered > threshold and not was_above:
    bump_count += 1

Step 3: Calculate calibration ratio

bumps_per_cm = bump_count / known_distance
# Example: 523 bumps / 100 cm = 5.23 B/cm

Step 4: Use calibration for future measurements

def estimate_distance(new_bump_count):
    return new_bump_count / bumps_per_cm
Why This Works
  • Bumps are consistent (same wheel, same surface)
  • High-pass filter isolates bumps from other motion
  • Similar to how a car odometer works
Calibration Required

Different surfaces and speeds may need different calibration values.

Signal Processing Pipeline

Raw Accelerometer → Magnitude → High-Pass Filter → Threshold → Count
       │                │              │               │         │
   ax,ay,az         |a|-1.0      isolate bumps    detect edge   N bumps
                    (remove g)   (remove drift)    (rising)

Visualize with:

python analyzers/bump_detector.py recording.csv --plot

Complete Calibration Code

class BumpOdometer:
    """Distance estimation from bump counting."""

    def __init__(self, bumps_per_cm=5.0):
        self.bumps_per_cm = bumps_per_cm
        self.bump_count = 0
        self.threshold = 0.015
        self.hp_alpha = 0.95

        # Filter state
        self.prev_mag = 0
        self.prev_filtered = 0
        self.was_above = False

    def process_sample(self, ax, ay, az):
        """Process one accelerometer sample, return bump if detected."""
        # Step 1: Magnitude minus gravity
        mag = math.sqrt(ax**2 + ay**2 + az**2) - 1.0

        # Step 2: High-pass filter
        filtered = self.hp_alpha * (self.prev_filtered + mag - self.prev_mag)
        self.prev_mag = mag
        self.prev_filtered = filtered

        # Step 3: Threshold crossing (rising edge)
        is_above = filtered > self.threshold
        bump_detected = is_above and not self.was_above
        self.was_above = is_above

        if bump_detected:
            self.bump_count += 1

        return bump_detected

    def get_distance(self):
        """Get estimated distance in cm."""
        return self.bump_count / self.bumps_per_cm

    def calibrate(self, known_distance_cm, bump_count):
        """Set calibration from known distance run."""
        self.bumps_per_cm = bump_count / known_distance_cm
        print(f"Calibrated: {self.bumps_per_cm:.3f} B/cm")

Method 3: Double Integration (Physics-Based)

How It Works

From physics: - Acceleration → integrate → velocity - Velocity → integrate → position

\[v(t) = v_0 + \int a \, dt$$ $$x(t) = x_0 + \int v \, dt\]

In discrete time:

velocity += acceleration * dt
position += velocity * dt

Why It's Problematic

Drift Problem

Any small constant error in acceleration accumulates rapidly:

  • 0.01g error over 1 second = 0.1 m/s velocity error
  • Over 10 seconds = 1 m/s = 100 cm/s drift!
Ideal:     Accel ──►  Velocity ──► Position
             ↑           ↑           ↑
Noise:    +0.01g  →   +10cm/s  →  meters of error!

When It Works

Double integration CAN work for: - Very short movements (< 1 second) - High-quality IMU with low noise - Movements that return to zero velocity (can detect and reset)

Implementation (With Caveats)

class DoubleIntegrator:
    """
    Distance from acceleration integration.

    WARNING: Drifts significantly. Only use for:
    - Short movements (< 1s)
    - Detecting motion vs stopped
    - Rough distance estimates
    """

    def __init__(self, sample_rate=500):
        self.dt = 1.0 / sample_rate
        self.velocity = 0.0
        self.position = 0.0

        # Bias correction
        self.accel_bias = 0.0
        self.calibrated = False

    def calibrate_bias(self, samples):
        """
        Calibrate accelerometer bias when robot is stationary.

        Args:
            samples: List of (ax, ay, az) when robot is still
        """
        total = 0
        for ax, ay, az in samples:
            # Forward axis acceleration (assuming X is forward)
            total += ax
        self.accel_bias = total / len(samples)
        self.calibrated = True
        print(f"Accel bias: {self.accel_bias:.4f} g")

    def process_sample(self, ax):
        """
        Process one sample, integrate acceleration.

        Args:
            ax: Forward acceleration in g
        """
        # Remove bias
        accel_corrected = ax - self.accel_bias

        # Convert g to m/s²
        accel_ms2 = accel_corrected * 9.81

        # Integrate: acceleration → velocity
        self.velocity += accel_ms2 * self.dt

        # Integrate: velocity → position
        self.position += self.velocity * self.dt

    def get_distance_cm(self):
        """Get position in cm."""
        return self.position * 100  # m to cm

    def reset(self):
        """Reset when robot stops."""
        self.velocity = 0
        self.position = 0
Practical Reality

In practice, double integration gives: - Errors of 10-50% over a few seconds - Completely unusable after 10+ seconds - Useful only for detecting "did it move?" not "how far?"

Method 4: Combined Approach (Best)

Sensor Fusion

Use multiple methods together:

┌──────────────────────────────────────────────────────┐
│                   Sensor Fusion                       │
│                                                       │
│  Ultrasonic ───► Absolute reference (when available) │
│       │                                               │
│       ▼                                               │
│  Bump Count ───► Relative distance (always works)    │
│       │                                               │
│       ▼                                               │
│  Integration ──► Short-term smoothing                 │
│                                                       │
│  Output: Best estimate of distance traveled          │
└──────────────────────────────────────────────────────┘

Implementation

class FusedOdometer:
    """Combines multiple distance estimation methods."""

    def __init__(self):
        self.bump_odo = BumpOdometer(bumps_per_cm=5.0)
        self.last_us = None
        self.us_travel = 0

    def process_sample(self, ax, ay, az, us_cm):
        """Process sensors, update distance estimate."""
        # Method 1: Bump counting (always)
        self.bump_odo.process_sample(ax, ay, az)

        # Method 2: Ultrasonic (when valid)
        if us_cm > 0 and us_cm < 400:
            if self.last_us is not None:
                delta = self.last_us - us_cm
                if abs(delta) < 10:  # Sanity check
                    self.us_travel += delta
            self.last_us = us_cm

    def get_distance(self):
        """
        Get best distance estimate.

        Uses ultrasonic when available, falls back to bumps.
        """
        bump_dist = self.bump_odo.get_distance()

        # If ultrasonic agrees with bumps, use ultrasonic (more accurate)
        if abs(self.us_travel - bump_dist) < bump_dist * 0.2:
            return self.us_travel

        # Otherwise, bumps are more reliable
        return bump_dist

    def calibrate_from_ultrasonic(self):
        """Use ultrasonic measurement to calibrate bump ratio."""
        if self.us_travel > 10:  # Need enough distance
            self.bump_odo.calibrate(
                self.us_travel,
                self.bump_odo.bump_count
            )

Practical Comparison

Experiment Setup

  1. Mark a 1-meter track on floor
  2. Drive robot start to end
  3. Compare methods

Typical Results

Method Measured Error Notes
Ruler (truth) 100.0 cm - Ground truth
Ultrasonic 99.2 cm 0.8% Needs wall
Bump count 103.5 cm 3.5% After calibration
Integration 145 cm 45% Drift accumulated

When to Use Each

Ultrasonic: - Measuring distance to obstacle - Calibrating other methods - Detecting approaching wall

Bump Counting: - Measuring distance traveled - Works without obstacles - Good for speed estimation too

Double Integration: - Detecting motion start/stop - Very short movements - As input to more complex filters (Kalman)

Exercise: Calibrate Your Robot

Step 1: Setup

# Deploy robot code
ampy --port /dev/ttyACM0 put src/robot/pico_collector.py main.py

# Start host collector
cd src/host && python host_collector.py

Step 2: Collect Calibration Data

  1. Position robot 50+ cm from wall
  2. Press R to start recording
  3. Drive forward until ~10 cm from wall
  4. Press R to stop recording
  5. Press E to export

Step 3: Analyze

python analyzers/bump_detector.py data/recording_*.csv --plot

Look at the output:

Ultrasonic Distance:
  Start:       48.5 cm
  End:         12.3 cm
  Travel:      36.2 cm

Bump Count:
  Detected:    189 (from accelerometer)

==================================================
  BUMPS PER CM = 5.221
==================================================

Step 4: Verify

Do another run and check if the B/cm ratio predicts correctly:

predicted_distance = new_bumps / 5.221
actual_distance = us_start - us_end

error = abs(predicted_distance - actual_distance) / actual_distance * 100
print(f"Prediction error: {error:.1f}%")

Summary

Scenario Best Method
Distance to wall Ultrasonic
How far did I drive? Bump counting
Am I moving? Any (including integration)
Speed estimation Bump rate or vibration
High precision Fused + calibration

Next Steps

  • [[Reference/extras/signal-processing/basics|Signal Processing Basics]] - Math behind the algorithms
  • [[Reference/extras/signal-processing/robot-data-pipeline|Robot Data Pipeline]] - How data flows through the system
  • [[Reference/07-data-analysis/analyzers-reference|Analyzers Reference]] - CLI tools for analysis