VEX V5 PYTHON | 04.2 DISTANCE SENSORS

Guide objective

Use front-facing distance sensors to compare wall clearance, align the chassis and approach a chosen stopping threshold with bounded movement.

01 | USE A WALL AS A REFERENCE

A known wall can provide a useful local reference. A distance sensor can help your robot approach a chosen clearance; two sensors can also indicate whether the front is approximately square to that wall.

These are different tasks. Approach changes distance. Alignment changes angle. Begin by testing each separately, then align before approaching.

A sensor detects an object in its view. It does not identify that object as the intended wall, so the environment and mounting must support your interpretation.

02 | KNOW WHAT THE SENSOR REPORTS

The V5 Distance Sensor reports distance to a detected object. The documented operating range is about 20 to 2,000 mm. The API describes approximate accuracy of ±15 mm below 200 mm and about 5% above 200 mm.

front_distance = Distance(Ports.PORT11)

if front_distance.is_object_detected():
    distance_mm = front_distance.object_distance(MM)

Check detection before reading distance, and validate the result for your application. The range accepted by the example is narrower than the device’s documented range.

Do not choose a tiny tolerance that implies precision the sensor cannot provide. Test the wall material, angle, surface and mounting distance you will actually use.

03 | MOUNT TWO SENSORS CONSISTENTLY

Mount one sensor towards the front-left and one towards the front-right. Their faces should be on the same front plane, at the same height, facing parallel forwards, with a known sideways separation.

Both must see the same broad, flat wall. Avoid an arrangement where one sees a goal, wheel, robot part or wall edge while the other sees the intended surface.

front_left_distance = Distance(Ports.PORT11)
front_right_distance = Distance(Ports.PORT12)

Record actual ports and sensor-face locations. Use either manual declarations or Devices-panel definitions, not both. For angle estimates, sensor spacing is different from drivetrain track width.

04 | DISTANCE STARTS AT THE SENSOR FACE

A reading of 250 mm means approximately 250 mm from the sensor face to the detected surface along its measurement direction. It is not automatically the gap from the frontmost robot part.

If the bumper extends 60 mm beyond a forward-facing sensor, a 250 mm reading corresponds to roughly 190 mm bumper clearance when square to the wall.

To aim for a 200 mm bumper gap in that arrangement, use a sensor-face target of about 260 mm before allowing for measurement uncertainty and run-on.

Measure the whole robot footprint. Turning can swing corners towards the wall even when the sensor faces still have clearance.

05 | ONE SENSOR CAN CONTROL AN APPROACH

Read a valid distance, calculate the remaining gap to the stopping threshold and reduce the forward command as that gap closes.

distance_error = measured_distance − target_distance
forward_command = min(speed_limit, gain × distance_error)

When the threshold is reached, issue a stop. Do not keep creeping forward to make the number look exact. If the robot starts closer than the threshold, this forward-only approach stops without backing away.

Very low commands may not overcome drivetrain friction. A deadline should report that limitation rather than allowing an endless approach. Avoid adding an arbitrary minimum speed without testing its stopping behaviour.

06 | COMPARE TWO READINGS FOR ALIGNMENT

For matched front-facing sensors seeing the same flat wall, a difference suggests an angle to the wall. Measure any fixed difference while physically square and store it as ALIGN_OFFSET_MM.

alignment_error = (left_distance − right_distance) − alignment_offset
Verify the sign on your build at very low speed
Corrected reading relationshipNormal correction for this mounting
Left farther than rightTurn right
Right farther than leftTurn left
Difference within toleranceStop turning and check for stable agreement

Do not treat unequal readings as angle automatically. Different surfaces, mount offsets, sensor bias or two different objects can produce the same difference.

Equal readings are an alignment condition under the model, not proof of exact physical squareness.

07 | RELATE THE DIFFERENCE TO AN ANGLE

With sensors separated sideways by B, parallel beams, a shared front plane and a flat wall, an ideal geometric approximation is:

right-positive yaw ≈ atan((right_distance − left_distance) / B)

If B = 200 mm, left = 320 mm and right = 300 mm, the estimate is atan(−20/200) ≈ −5.71°: the robot points slightly left, so it needs a right correction.

This depends on the mounting and target assumptions. The control code uses the corrected distance difference directly rather than claiming a precise angle estimate.

A 25 mm difference tolerance with 200 mm spacing corresponds to about 7.1° in the ideal model. It is a starting test tolerance, not precision alignment. Validate uncertainty before tightening it.

08 | STOP ON INVALID READINGS

def clamp(value, limit):
    return max(-limit, min(limit, value))

def read_distance_mm(sensor):
    if not sensor.is_object_detected():
        return None
    value = sensor.object_distance(MM)
    # Chained comparison also rejects NaN and infinities.
    if not VALID_MIN_MM <= value <= VALID_MAX_MM:
        return None
    return value

def read_pair():
    left = read_distance_mm(front_left_distance)
    right = read_distance_mm(front_right_distance)
    if left is None or right is None:
        return None
    return left, right

The helper returns None if no object is detected or the result is outside the application window. It never turns a missing reading into zero distance or reuses the last good reading to keep moving.

The example immediately aborts on an invalid pair. A more advanced retry policy should stop the motors during the retry and have a short, explicit deadline.

Sequential sensor calls are not an atomic snapshot. A stale but plausible reading cannot be identified by this simple range check alone. Test response time and keep movement slow.

09 | COMPLETE ALIGN-THEN-APPROACH PROJECT

Keep generated imports and Brain setup above this block. Paste below # Begin project code, outside the generated region, replacing earlier starters. Check motor ports, cartridges, reversal flags and distance-sensor ports.

The explicit drivetrain is left ports 3 and 4, right ports 1 and 2. The two example distance sensors use ports 11 and 12. Positive motor-group commands must mean robot-forward on both sides.

Start with TEST_MODE = "ALIGN". Test APPROACH only from a square starting orientation. Use COMBINED after both independent tests behave as expected.

# =============================================================================
# TEAM [MY TEAM NAME / NUMBER]
# =============================================================================
# ROBOT CONFIGURATION
# - FOUR-motor drivetrain:
#       Left  = Ports 3 and 4
#       Right = Ports 1 and 2
# - Example: blue cartridges, left normal, right reversed.
#   VERIFY your actual ports, cartridges and direction settings.
# DRIVER CONTROLS
# - Left joystick vertical (axis3): forward/back
# - Right joystick horizontal (axis1): turn
# - Two front distance sensors: Ports 11 and 12.
# - Autonomous MOVES to align or approach a wall.
# - Hold B to cancel autonomous; driver control retains the earlier scheme.
# - Add BELOW # Begin project code, outside generated configuration.
# - Do not also define these drive motors in the Devices panel.
# - Keep generated from vex import * and brain = Brain() above.

# =============================================================================
# FOUR-MOTOR DRIVETRAIN
# =============================================================================
left_motor_1 = Motor(Ports.PORT3, GearSetting.RATIO_6_1, False)
left_motor_2 = Motor(Ports.PORT4, GearSetting.RATIO_6_1, False)
right_motor_1 = Motor(Ports.PORT1, GearSetting.RATIO_6_1, True)
right_motor_2 = Motor(Ports.PORT2, GearSetting.RATIO_6_1, True)

left_drive = MotorGroup(left_motor_1, left_motor_2)
right_drive = MotorGroup(right_motor_1, right_motor_2)

# =============================================================================
# CONTROLLER, SETTINGS AND HELPERS
# =============================================================================
controller = Controller(PRIMARY)  # Do not duplicate a generated Controller
DEADBAND = 5
DRIVE_LIMIT_PERCENT = 30          # Start low; increase only after testing
TURN_GAIN = 0.7
LOOP_DELAY_MS = 20
front_left_distance = Distance(Ports.PORT11)
front_right_distance = Distance(Ports.PORT12)
# Mount both on the same front plane, facing parallel forwards.
VALID_MIN_MM = 40             # Application acceptance window, not device spec
VALID_MAX_MM = 1200
MIN_CLEARANCE_MM = 150        # Sensor-face distance; validate chassis clearance
TARGET_SENSOR_MM = 250
ALIGN_OFFSET_MM = 0          # Measured difference when physically square
ALIGN_TOLERANCE_MM = 25      # Starting example; evaluate sensor uncertainty
APPROACH_ALIGNMENT_LIMIT_MM = 50
ALIGN_DWELL_MS = 200
ALIGN_KP_PERCENT_PER_MM = 0.05
MAX_ALIGN_PERCENT = 8
APPROACH_KP_PERCENT_PER_MM = 0.08
MAX_APPROACH_PERCENT = 12
ACTION_LIMIT_MS = 5000
TEST_MODE = "ALIGN"          # ALIGN, APPROACH or COMBINED

def apply_deadband(value):
    return 0 if abs(value) <= DEADBAND else value

def drive_mix(forward, turn):
    left = forward + turn
    right = forward - turn
    largest = max(100, abs(left), abs(right))
    left = left * 100 / largest
    right = right * 100 / largest
    return (left * DRIVE_LIMIT_PERCENT / 100,
            right * DRIVE_LIMIT_PERCENT / 100)

def set_side_output(side, output):
    if abs(output) < 0.01:
        side.stop(BRAKE)
    elif output > 0:
        side.spin(FORWARD, output, PERCENT)
    else:
        side.spin(REVERSE, -output, PERCENT)

def clamp(value, limit):
    return max(-limit, min(limit, value))

def read_distance_mm(sensor):
    if not sensor.is_object_detected():
        return None
    value = sensor.object_distance(MM)
    # Chained comparison also rejects NaN and infinities.
    if not VALID_MIN_MM <= value <= VALID_MAX_MM:
        return None
    return value

def read_pair():
    left = read_distance_mm(front_left_distance)
    right = read_distance_mm(front_right_distance)
    if left is None or right is None:
        return None
    return left, right

def action_stop_reason(start_ms):
    if not (competition.is_enabled() and competition.is_autonomous()):
        return "MODE_CHANGED"
    if controller.buttonB.pressing():
        return "CANCELLED"
    if brain.timer.time(MSEC) - start_ms >= ACTION_LIMIT_MS:
        return "TIMEOUT"
    return None

def align_to_wall():
    start_ms = brain.timer.time(MSEC)
    aligned_start_ms = None
    try:
        while True:
            reason = action_stop_reason(start_ms)
            if reason is not None:
                return reason
            pair = read_pair()
            if pair is None:
                return "INVALID_READING"
            left_mm, right_mm = pair
            if min(pair) <= MIN_CLEARANCE_MM:
                return "CLEARANCE_STOP"
            error_mm = (left_mm - right_mm) - ALIGN_OFFSET_MM
            if abs(error_mm) <= ALIGN_TOLERANCE_MM:
                stop_drive()
                if aligned_start_ms is None:
                    aligned_start_ms = brain.timer.time(MSEC)
                if brain.timer.time(MSEC) - aligned_start_ms >= ALIGN_DWELL_MS:
                    return "READINGS_ALIGNED"
            else:
                aligned_start_ms = None
                turn = clamp(ALIGN_KP_PERCENT_PER_MM * error_mm, MAX_ALIGN_PERCENT)
                # For matched, parallel front sensors seeing the same flat wall:
                # left reading larger -> right turn (left forward, right reverse).
                set_side_output(left_drive, turn)
                set_side_output(right_drive, -turn)
            wait(LOOP_DELAY_MS, MSEC)
    finally:
        stop_drive()

def approach_wall(target_sensor_mm):
    if not MIN_CLEARANCE_MM < target_sensor_mm < VALID_MAX_MM:
        raise ValueError("Target must exceed minimum clearance and be within range")
    start_ms = brain.timer.time(MSEC)
    try:
        while True:
            reason = action_stop_reason(start_ms)
            if reason is not None:
                return reason
            pair = read_pair()
            if pair is None:
                return "INVALID_READING"
            left_mm, right_mm = pair
            nearest_mm = min(pair)
            if nearest_mm <= MIN_CLEARANCE_MM:
                return "CLEARANCE_STOP"
            alignment_error = (left_mm - right_mm) - ALIGN_OFFSET_MM
            if abs(alignment_error) > APPROACH_ALIGNMENT_LIMIT_MM:
                return "ALIGNMENT_LOST"
            if nearest_mm <= target_sensor_mm:
                return "APPROACH_THRESHOLD_REACHED"
            # Forward only: decreasing distance error reduces the command.
            forward = min(MAX_APPROACH_PERCENT,
                          APPROACH_KP_PERCENT_PER_MM * (nearest_mm - target_sensor_mm))
            set_side_output(left_drive, forward)
            set_side_output(right_drive, forward)
            wait(LOOP_DELAY_MS, MSEC)
    finally:
        stop_drive()

def show_status(message):
    brain.screen.clear_screen()
    brain.screen.set_cursor(1, 1)
    brain.screen.print(message)

def stop_drive():
    left_drive.stop(BRAKE)
    right_drive.stop(BRAKE)

# =============================================================================
# PRE-AUTON: CALLED ONCE AT PROJECT STARTUP
# =============================================================================
def pre_autonomous():
    stop_drive()
    show_status("Setup ready")
    # Later: initialise sensors and starting values here.
    # Finish setup before the robot is enabled for a match.

# =============================================================================
# AUTONOMOUS: A FINITE ROUTINE
# =============================================================================
def autonomous():
    try:
        if TEST_MODE == "ALIGN":
            result = align_to_wall()
        elif TEST_MODE == "APPROACH":
            # Place the robot square to the wall before this individual test.
            result = approach_wall(TARGET_SENSOR_MM)
        elif TEST_MODE == "COMBINED":
            result = align_to_wall()
            show_status(result)
            if result != "READINGS_ALIGNED":
                return
            result = approach_wall(TARGET_SENSOR_MM)
        else:
            raise ValueError("Unknown TEST_MODE")
        show_status(result)
        print("WALL_TEST", TEST_MODE, result)
    finally:
        stop_drive()

# =============================================================================
# DRIVER CONTROL: REPEATING UPDATES WHILE ENABLED
# =============================================================================
def driver_control():
    show_status("Driver control")
    while competition.is_enabled() and competition.is_driver_control():
        forward = apply_deadband(controller.axis3.position())
        turn = apply_deadband(controller.axis1.position()) * TURN_GAIN
        left_output, right_output = drive_mix(forward, turn)
        set_side_output(left_drive, left_output)
        set_side_output(right_drive, right_output)
        wait(LOOP_DELAY_MS, MSEC)
    stop_drive()

# =============================================================================
# PROGRAM START: SETUP, THEN CALLBACK REGISTRATION
# =============================================================================
pre_autonomous()
competition = Competition(driver_control, autonomous)

# Keep the main task alive without running a driving loop here.
while True:
    wait(100, MSEC)

The alignment action turns slowly until the difference stays within tolerance for the chosen dwell. It checks clearance while turning. The approach action uses the nearer reading, sends equal forward commands and aborts if alignment is lost.

This baseline does not steer continuously during approach; that keeps approach and turn corrections separate. A combined proportional controller needs careful testing to avoid oscillation and corner contact.

Holding B, a mode change, timeout, invalid reading or clearance violation stops the action. The basic driver callback retains the earlier controls and does not implement automatic obstacle avoidance.

10 | UNDERSTAND THE RESULT LABELS

Sensor conditions are not physical-position guarantees
ResultMeaning
READINGS_ALIGNEDCorrected difference remained inside the example tolerance
APPROACH_THRESHOLD_REACHEDNearer reading is at or below the requested threshold
ALIGNMENT_LOSTDifference exceeded the approach limit
INVALID_READINGAt least one reading failed detection or range checks
CLEARANCE_STOPAt least one reading reached the minimum-clearance threshold
TIMEOUT / CANCELLED / MODE_CHANGEDAction aborted; no next movement is allowed

The final stop may include physical run-on. Use the measured stopping distance from 03.2 to select a conservative trigger and approach speed.

The sample does not wait for independently verified physical rest before reporting a threshold. Observe the actual final gap and include it in your test log.

11 | FILTER CAREFULLY

A few noisy readings can make a controller twitch. A short median or moving-average filter may help after the raw sensor behaviour is understood.

Filtering also adds delay. A long average can make an approaching robot react too late. Never fill a filter with invalid readings or let an old average hide that the object is no longer detected.

The sample uses unfiltered readings and a stopped alignment dwell. A dwell checks repeated agreement, but does not prove that every API call supplied a new independent measurement.

12 | BUILD A WALL TEST LOG

Test detection loss without driving towards an unseen obstacle
TestSetupRecord
Stationary readingsSeveral known gaps, square to wallBoth readings, offset and variation
Alignment signSmall left and right starting anglesTurn direction and final physical angle
ApproachSquare start, low command limitStop trigger and final chassis gap
Target lossStationary robot with target moved out of viewInvalid-reading result; no movement
Repeated trialsThree trials with unchanged settingsFinal angle, gap and variation

Use a broad flat target and a clear level area. Verify directions with supported wheels before floor tests. Keep hands away from moving robots.

Investigate a sign error, inconsistent target detection or mounting problem before increasing gains. Tune one parameter at a time and record the surface and robot load.

CHALLENGES | ALIGN WITH EVIDENCE

  1. Configure: document sensor ports, spacing and sensor-face offset from the bumper.
  2. Measure: compare reported distances with several known physical gaps.
  3. Predict: explain the correction when the left reading is greater than the right.
  4. Calculate: find the sensor target for a 200 mm bumper gap when the bumper extends 60 mm ahead of the sensor.
  5. Test: validate alignment from small angles in both directions.
  6. Approach: record three final gaps using the same low-speed settings.
  7. Abort: verify that target loss, cancellation and timeout stop the action.
  8. Evaluate: explain why equal readings do not guarantee an exact wall angle.
Check the clearance calculation

The nominal sensor target is 200 + 60 = 260 mm when square. Add suitable allowance for sensor uncertainty and measured stopping behaviour; do not treat the nominal arithmetic as a guarantee of final clearance.

OFFICIAL REFERENCES

The alignment and approach logic are team-written examples. This is an independent learning resource; VEX and VEXcode are trademarks of their respective owners.