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
| Corrected reading relationship | Normal correction for this mounting |
|---|---|
| Left farther than right | Turn right |
| Right farther than left | Turn left |
| Difference within tolerance | Stop 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
| Result | Meaning |
|---|---|
| READINGS_ALIGNED | Corrected difference remained inside the example tolerance |
| APPROACH_THRESHOLD_REACHED | Nearer reading is at or below the requested threshold |
| ALIGNMENT_LOST | Difference exceeded the approach limit |
| INVALID_READING | At least one reading failed detection or range checks |
| CLEARANCE_STOP | At least one reading reached the minimum-clearance threshold |
| TIMEOUT / CANCELLED / MODE_CHANGED | Action 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 | Setup | Record |
|---|---|---|
| Stationary readings | Several known gaps, square to wall | Both readings, offset and variation |
| Alignment sign | Small left and right starting angles | Turn direction and final physical angle |
| Approach | Square start, low command limit | Stop trigger and final chassis gap |
| Target loss | Stationary robot with target moved out of view | Invalid-reading result; no movement |
| Repeated trials | Three trials with unchanged settings | Final 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
- Configure: document sensor ports, spacing and sensor-face offset from the bumper.
- Measure: compare reported distances with several known physical gaps.
- Predict: explain the correction when the left reading is greater than the right.
- Calculate: find the sensor target for a 200 mm bumper gap when the bumper extends 60 mm ahead of the sensor.
- Test: validate alignment from small angles in both directions.
- Approach: record three final gaps using the same low-speed settings.
- Abort: verify that target loss, cancellation and timeout stop the action.
- 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.