VEX V5 PYTHON | 07.6 ROUTES AND FALLBACKS
Guide objective
Mirror field targets correctly, monitor a forward obstacle sensor and use a bounded alternate route from a verified checkpoint.
01 | REUSE ROUTES WITHOUT COPYING ERRORS
You have a reliable route on one side of a field. Now you need its counterpart on the other side, and an alternative if the preferred corridor is unavailable.
These are three different tasks: mirroring transforms geometry; sensing reports observations; a fallback chooses a tested alternative. None automatically solves the others.
This guide uses stop-and-settle waypoint routes from 07.5. The same geometric reflection also applies to the X/Y points used by the 07.4 path follower.
02 | REFLECT X AND HEADING TOGETHER
For reflection across the field line X = 0, negate X, keep Y and negate heading modulo 360. More generally, across X = c, the new X is 2c − X.
def mirror_pose(pose, mirror_x=0.0):
x, y, heading = pose
if not all(math.isfinite(v) for v in (x, y, heading, mirror_x)):
raise ValueError("Use finite pose values")
return (2*mirror_x-x, y, (-heading) % 360)
def mirror_route(route, mirror_x=0.0):
return [mirror_pose(p, mirror_x) for p in route]
| Original pose | Mirrored across X = 0 |
|---|---|
| (300, 500, 45°) | (−300, 500, 315°) |
| (400, 700, 90°) | (−400, 700, 270°) |
| (0, 250, 0°) | (0, 250, 0°) |
Check a heading vector: a 45° forward direction points up and right. Reflection makes it point up and left, which is 315° under this convention.
03 | REFLECTION IS NOT A HALF TURN
Rotating a route 180° about the origin gives (−X, −Y, heading + 180°). Reflecting it across X = 0 gives (−X, Y, −heading). These are different transformations.
| Transform | Position | Heading |
|---|---|---|
| Mirror across X = c | (2c − X, Y) | −heading mod 360 |
| Rotate 180° about origin | (−X, −Y) | heading + 180° mod 360 |
Mirror the start pose, final headings, checkpoint gates and field-based mechanism targets too. Keep motor ports and physical device reversals unchanged. A reverse-travel flag stays a reverse-travel flag.
If you use explicitly signed arc commands, reflection reverses their clockwise/anticlockwise sign. A geometric follower should instead recalculate curvature from the reflected points.
A mathematically mirrored route still needs testing. Field features, mechanisms, sensor mounting and available space may not be symmetric.
04 | PLAN CLEARANCE FOR THE WHOLE ROBOT
A line from one waypoint to another describes a reference point, not the entire chassis. Include wheels, wings, arms and carried objects when checking a corridor.
For a robot 350 mm wide, a centreline 175 mm from a wall leaves zero nominal side clearance. Pose error, mechanical flex and turning scrub require additional space.
Turns in place have a swept footprint too. Route validation must cover the approach and all final-heading rotations. A single forward beam cannot check that footprint.
05 | KNOW WHAT THE DISTANCE SENSOR OBSERVES
The V5 Distance Sensor reports whether it detects an object ahead and the range to the detected object. Its specified measurement range is 20 to 2000 mm.
No detected object is not proof of free space. A thin post, object outside the beam, unsuitable surface or obstruction below the sensor can be missed.
The example distinguishes OPEN, BLOCKED, NO_RETURN, UNKNOWN and FAULT. OPEN means only that the current detected range exceeds the configured forward threshold.
| Observation | Policy in this starter |
|---|---|
| Detected, usable range beyond threshold | OPEN; continue only on a verified route |
| Detected at or inside threshold | BLOCKED; stop on the first sample |
| No detection, ALLOW_NO_RETURN False | UNKNOWN; stop |
| No detection, ALLOW_NO_RETURN True | NO_RETURN; explicit limited-observation policy |
| Exception or unusable numerical range | FAULT; stop |
ALLOW_NO_RETURN defaults False. Turning it on does not improve sensing; it permits travel without a return on an independently checked corridor. Faults still stop the routine.
06 | MEASURE FROM THE FRONT EDGE
A range reading starts at the sensor. If the sensor sits 100 mm behind the robot’s frontmost edge and reads 450 mm, the nominal front-edge gap is 350 mm.
bumper_gap = measured_range_mm - SENSOR_TO_FRONT_MM
Use a threshold based on measured stopping behaviour at your chosen speed, sensing and command delay, uncertainty and a margin. Do not assume a brake command makes the chassis stop without travel.
For illustration, 300 mm/s with ideal deceleration 600 mm/s² needs 75 mm. A 20 ms delay adds 6 mm; adding a 100 mm margin gives 181 mm. Those numbers are a calculation example, not validated braking data.
The starter uses a 300 mm front-edge threshold. Validate it for the robot, load, speed and surface. The guard also stops turns when it sees a close object, a deliberately conservative choice.
07 | A FALLBACK NEEDS AN ENTRY CHECKPOINT
A fallback path is a pretested route with a known entry region. It is not permission to improvise a detour from any position.
The example checkpoint is (0, 250, 0°). After a blockage, the robot brakes, keeps updating the existing pose estimate and confirms it has settled. The checkpoint gate then requires position within 30 mm and heading within 3°.
If the robot stopped halfway along another segment, the gate can fail. That is the correct outcome: the alternate route has not been validated from that location.
Never reset odometry to the checkpoint to make the check pass. A label in the program does not move the physical robot.
08 | RELEASE ONCE, THEN KEEP MONITORING
The supervisor allows one alternate attempt only after a detected blockage. Cancellation, invalid pose, sensor faults, other failures and expired budgets do not trigger retries.
FALLBACK_RELEASED must be deliberately configured after checking the alternate corridor and its entry. The front guard must also become permissive. A release flag alone cannot override a continuing BLOCKED observation.
This starter therefore cannot turn away from a persistent obstacle while its front guard remains blocked. It stops. More advanced avoidance needs suitable sensing and an independently validated rotation or escape policy.
There is no indefinite wait for an obstacle to disappear. The release observation is checked once after settling. If it does not pass, the routine ends.
09 | COMPLETE MIRRORED-ROUTE AND FALLBACK STARTER
Paste below # Begin project code, keeping generated VEX imports and Brain setup above it. Do not duplicate device declarations.
Drive motors: left 3/4, right 1/2. Forward tracker: 17. Sideways tracker: 19. Inertial: 20. Forward Distance Sensor: 16. Verify every sign, offset, scale and physical start.
MOTION_CONFIGURED, ROUTES_VERIFIED and FALLBACK_RELEASED begin False. ALLOW_NO_RETURN also begins False. The example cannot report successful navigation by ignoring missing observations.
# =============================================================================
# TEAM [MY TEAM NAME / NUMBER] | ROUTE MIRRORING AND CHECKPOINT FALLBACK STARTER
# =============================================================================
# Keep generated VEX imports and brain above '# Begin project code'.
# Manual device configuration; do not duplicate declarations in Devices.
# Drive left 3/4, right 1/2, blue cartridges; verify reversals on your robot.
# Two perpendicular trackers: forward 17, sideways 19; inertial 20.
# One callback owns pose updates AND drivetrain output; no odometry thread.
import math
left_1 = Motor(Ports.PORT3, GearSetting.RATIO_6_1, False)
left_2 = Motor(Ports.PORT4, GearSetting.RATIO_6_1, False)
right_1 = Motor(Ports.PORT1, GearSetting.RATIO_6_1, True)
right_2 = Motor(Ports.PORT2, GearSetting.RATIO_6_1, True)
left_drive = MotorGroup(left_1, left_2)
right_drive = MotorGroup(right_1, right_2)
forward_tracker = Rotation(Ports.PORT17, False)
side_tracker = Rotation(Ports.PORT19, False)
inertial = Inertial(Ports.PORT20)
controller = Controller(PRIMARY)
MOTION_CONFIGURED = False
FORWARD_MM_PER_SENSOR_TURN = 219.44 # Calibrated examples only
SIDE_MM_PER_SENSOR_TURN = 219.44
FORWARD_X_MM = -120.0
SIDE_Y_MM = -100.0
START_POSE = (0.0, 0.0, 0.0) # Physically confirmed autonomous start
DISTANCE_KP = 0.05 # percent/mm
TURN_KP = 0.7 # percent/degree
MAX_DRIVE = 20.0
MAX_TURN = 12.0
MAX_WHEEL_OUTPUT = 25.0
DRIVE_ALIGN_DEG = 12.0 # Rotate only outside this window
POSITION_TOL_MM = 20.0
HEADING_TOL_DEG = 2.0
POSITION_RATE_TOL = 30.0 # estimated field speed mm/s
TURN_RATE_TOL = 8.0 # degrees/s
SETTLE_MS = 200
POLL_MS = 20
MAX_GAP_MS = 100
ready = False
pose_valid = False
pose = None
previous_sensors = None
last_update_ms = 0
class FieldPose:
def __init__(self, x_mm, y_mm, heading_deg):
if not all(math.isfinite(v) for v in (x_mm, y_mm, heading_deg)):
raise ValueError("Invalid starting pose")
self.x = x_mm
self.y = y_mm
self.theta = math.radians(heading_deg) # Keep unwrapped internally.
def integrate(self, forward_arc, sideways_arc, dtheta):
if not all(math.isfinite(v) for v in (forward_arc, sideways_arc, dtheta)):
raise ValueError("Invalid local increment")
half = dtheta / 2.0
# Stable sin(half)/half limit near a straight move.
factor = (1.0 - half * half / 6.0 if abs(half) < 1e-6
else math.sin(half) / half)
forward_chord = forward_arc * factor
side_chord = sideways_arc * factor
midpoint = self.theta + half
dx = side_chord * math.cos(midpoint) + forward_chord * math.sin(midpoint)
dy = forward_chord * math.cos(midpoint) - side_chord * math.sin(midpoint)
new_x, new_y = self.x + dx, self.y + dy
new_theta = self.theta + dtheta
if not all(math.isfinite(v) for v in (new_x, new_y, new_theta)):
raise ValueError("Invalid resulting pose")
self.x, self.y, self.theta = new_x, new_y, new_theta
return dx, dy
def pose(self):
return self.x, self.y, math.degrees(self.theta) % 360.0
def clamp(value, low, high):
return max(low, min(high, value))
def angle_error(target, current):
return (target - current + 180) % 360 - 180
def target_geometry(x, y, target_x, target_y):
dx, dy = target_x - x, target_y - y
distance = math.sqrt(dx * dx + dy * dy)
bearing = None if distance < 1e-6 else math.degrees(math.atan2(dx, dy)) % 360
return distance, bearing
# =============================================================================
# FINAL APPROACH SETTINGS AND REQUEST ENVELOPE
# =============================================================================
SLOW_ZONE_MM = 600.0
NEAR_GOAL_CAP = 4.0 # Upper bound at the position-window edge, not a floor
GOAL_TIMEOUT_MS = 14000 # Shared by FACE, DRIVE, HEADING and final verification
GOAL = (500.0, 500.0, 90.0)
def approach_request(distance_mm):
if not math.isfinite(distance_mm) or distance_mm < 0:
raise ValueError("Invalid distance")
if distance_mm <= POSITION_TOL_MM:
return 0.0
fraction = clamp((distance_mm-POSITION_TOL_MM) /
(SLOW_ZONE_MM-POSITION_TOL_MM), 0.0, 1.0)
envelope = NEAR_GOAL_CAP + (MAX_DRIVE-NEAR_GOAL_CAP)*fraction
return min(DISTANCE_KP*distance_mm, envelope)
def navigation_output(mode, pose, target_x, target_y, final_heading=None):
x, y, heading = pose
distance, bearing = target_geometry(x, y, target_x, target_y)
if mode == "HEADING":
desired = final_heading % 360
error = angle_error(desired, heading)
near = abs(error) <= HEADING_TOL_DEG
elif mode == "FACE":
if distance <= POSITION_TOL_MM:
return 0.0, 0.0, True # At the point: no meaningful bearing needed.
error = angle_error(bearing, heading)
near = abs(error) <= HEADING_TOL_DEG
else: # DRIVE, forward-only in this lesson.
if distance <= POSITION_TOL_MM:
return 0.0, 0.0, True
error = angle_error(bearing, heading)
near = False
if near:
return 0.0, 0.0, True
turn = clamp(TURN_KP * error, -MAX_TURN, MAX_TURN)
base = 0.0
if mode == "DRIVE" and abs(error) <= DRIVE_ALIGN_DEG:
base = approach_request(distance)
left, right = base + turn, base - turn
peak = max(abs(left), abs(right))
if peak > MAX_WHEEL_OUTPUT:
scale = MAX_WHEEL_OUTPUT / peak
left *= scale
right *= scale
return left, right, False
# =============================================================================
# FRONT SENSOR AND ROUTE APPROVAL SETTINGS
# =============================================================================
front_distance = Distance(Ports.PORT16)
SENSOR_TO_FRONT_MM = 100.0 # Sensor sits this far behind the frontmost edge
STOP_CLEARANCE_MM = 300.0 # Example physical bumper gap; verify stopping trials
ALLOW_NO_RETURN = False # False stops on no detection, not just exceptions
ROUTES_VERIFIED = False # Whole corridors AND turning footprints tested
FALLBACK_RELEASED = False # Explicit release after checking alternate corridor
MIRROR_ROUTE = False
MIRROR_X_MM = 0.0
ROUTE_BUDGET_MS = 14000
CHECKPOINT = (0.0, 250.0, 0.0)
CHECKPOINT_POSITION_TOL_MM = 30.0
CHECKPOINT_HEADING_TOL_DEG = 3.0
PRIMARY_ROUTE = [(0.0, 250.0, 0.0), (0.0, 500.0, 0.0)]
FALLBACK_ROUTE = [(250.0, 250.0, 90.0), (250.0, 500.0, 0.0),
(0.0, 500.0, 270.0)]
def mirror_pose(pose, mirror_x=0.0):
x, y, heading = pose
if not all(math.isfinite(v) for v in (x, y, heading, mirror_x)):
raise ValueError("Use finite pose values")
return (2*mirror_x-x, y, (-heading) % 360)
def mirror_route(route, mirror_x=0.0):
return [mirror_pose(p, mirror_x) for p in route]
def front_status():
try:
if not front_distance.is_object_detected():
return "NO_RETURN" if ALLOW_NO_RETURN else "UNKNOWN"
distance = front_distance.object_distance(MM)
if not math.isfinite(distance) or not 20 <= distance <= 2000:
return "FAULT"
bumper_gap = distance-SENSOR_TO_FRONT_MM
return "BLOCKED" if bumper_gap <= STOP_CLEARANCE_MM else "OPEN"
except Exception:
return "FAULT"
def checkpoint_matches(current, checkpoint):
distance = math.hypot(current[0]-checkpoint[0], current[1]-checkpoint[1])
return (distance <= CHECKPOINT_POSITION_TOL_MM and
abs(angle_error(checkpoint[2], current[2])) <= CHECKPOINT_HEADING_TOL_DEG)
# =============================================================================
# SENSOR / MOTOR ACCESS
# =============================================================================
def stop_drive():
left_drive.stop(BRAKE)
right_drive.stop(BRAKE)
def side_output(group, output):
if output == 0:
group.stop(BRAKE)
else:
group.spin(FORWARD if output > 0 else REVERSE, abs(output), PERCENT)
def sensor_snapshot():
if not inertial.installed() or inertial.is_calibrating():
raise ValueError("Inertial unavailable")
values = (forward_tracker.position(TURNS), side_tracker.position(TURNS),
inertial.rotation(DEGREES))
if not all(math.isfinite(v) for v in values):
raise ValueError("Invalid sensors")
return values
def initialize_pose():
global pose, previous_sensors, last_update_ms, pose_valid
pose_valid = False
# Only at the physically verified stationary START_POSE.
values = sensor_snapshot()
pose = FieldPose(*START_POSE)
previous_sensors = values
last_update_ms = brain.timer.time(MSEC)
pose_valid = True
def read_pose():
global previous_sensors, last_update_ms, pose_valid
if not pose_valid:
raise ValueError("Pose invalid")
now = brain.timer.time(MSEC)
try:
if now - last_update_ms > MAX_GAP_MS:
raise ValueError("Pose update gap")
current = sensor_snapshot()
f = (current[0] - previous_sensors[0]) * FORWARD_MM_PER_SENSOR_TURN
s = (current[1] - previous_sensors[1]) * SIDE_MM_PER_SENSOR_TURN
theta = math.radians(current[2] - previous_sensors[2])
pose.integrate(f + FORWARD_X_MM * theta, s - SIDE_Y_MM * theta, theta)
previous_sensors = current
last_update_ms = now
return pose.pose()
except Exception:
pose_valid = False
raise
# =============================================================================
# BOUNDED MOVEMENT LOOP
# =============================================================================
def run_navigation(mode, target_x, target_y, final_heading=None, timeout_ms=5000,
deadline_ms=None):
stop_drive()
if (mode not in ("FACE", "DRIVE", "HEADING") or timeout_ms <= 0
or not all(math.isfinite(v) for v in (target_x, target_y))
or (mode == "HEADING" and
(final_heading is None or not math.isfinite(final_heading)))):
return "INVALID_TARGET"
started = previous_ms = brain.timer.time(MSEC)
previous_pose = None
settled_since = None
try:
while True:
if (not ready or not MOTION_CONFIGURED or not competition.is_enabled()
or not competition.is_autonomous() or controller.buttonB.pressing()):
return "CANCELLED_OR_NOT_READY"
now = brain.timer.time(MSEC)
if deadline_ms is not None and now >= deadline_ms:
return "DEADLINE"
if now - started >= timeout_ms:
return "TIMEOUT"
current_pose = read_pose() # Faults latch pose invalid.
obstacle = front_status() # Conservative: applies during turns too.
if obstacle == "BLOCKED":
return "OBSTACLE_BLOCKED"
if obstacle not in ("OPEN", "NO_RETURN"):
return "OBSTACLE_SENSOR_" + obstacle
left, right, near = navigation_output(
mode, current_pose, target_x, target_y, final_heading)
dt = (now - previous_ms) / 1000.0
slow = False
if previous_pose is not None and dt > 0:
dx = current_pose[0] - previous_pose[0]
dy = current_pose[1] - previous_pose[1]
speed = math.sqrt(dx * dx + dy * dy) / dt
rate = abs(angle_error(current_pose[2], previous_pose[2])) / dt
slow = speed <= POSITION_RATE_TOL and rate <= TURN_RATE_TOL
previous_pose, previous_ms = current_pose, now
if near:
stop_drive()
if slow:
if settled_since is None:
settled_since = now
if now - settled_since >= SETTLE_MS:
return "SETTLED"
else:
settled_since = None
else:
settled_since = None
side_output(left_drive, left)
side_output(right_drive, right)
wait(POLL_MS, MSEC)
except Exception:
return "POSE_OR_COMMAND_FAULT"
finally:
stop_drive()
# =============================================================================
# SHARED-DEADLINE FINAL POSE SEQUENCE
# =============================================================================
def finish_at_pose(target_x, target_y, final_heading, timeout_ms=GOAL_TIMEOUT_MS):
stop_drive()
try:
values = (target_x, target_y, final_heading, timeout_ms,
SLOW_ZONE_MM, NEAR_GOAL_CAP, MAX_DRIVE, DISTANCE_KP)
if (not all(isinstance(v, (int, float)) and math.isfinite(v) for v in values)
or timeout_ms <= 0 or SLOW_ZONE_MM <= POSITION_TOL_MM
or not 0 < NEAR_GOAL_CAP <= MAX_DRIVE <= MAX_WHEEL_OUTPUT
or DISTANCE_KP <= 0):
return "INVALID_GOAL_OR_CONFIGURATION"
final_heading %= 360
deadline = brain.timer.time(MSEC) + timeout_ms
for phase in ("FACE", "DRIVE", "HEADING"):
remaining = deadline-brain.timer.time(MSEC)
if remaining <= 0:
return "DEADLINE"
result = run_navigation(phase, target_x, target_y, final_heading,
timeout_ms=remaining, deadline_ms=deadline)
print(phase, result)
if result != "SETTLED":
return phase + ": " + result
# The turn has settled. Check position again with fresh feedback.
if (not ready or not MOTION_CONFIGURED or not competition.is_enabled()
or not competition.is_autonomous() or controller.buttonB.pressing()):
return "CANCELLED_OR_NOT_READY"
if brain.timer.time(MSEC) >= deadline:
return "DEADLINE"
current = read_pose()
distance, bearing = target_geometry(current[0], current[1], target_x, target_y)
if distance > POSITION_TOL_MM:
return "POSITION_CHANGED_DURING_TURN"
if abs(angle_error(final_heading, current[2])) > HEADING_TOL_DEG:
return "HEADING_CHANGED_AFTER_SETTLE"
return "POSE_SETTLED"
except Exception:
return "POSE_OR_COMMAND_FAULT"
finally:
stop_drive()
# =============================================================================
# CHECKED ROUTE EXECUTION; ONE DEADLINE, NO POSE RESET
# =============================================================================
def checked_route(route):
if not isinstance(route, (list, tuple)) or not route:
raise ValueError("Need a non-empty route")
result = []
for p in route:
if len(p) != 3 or not all(math.isfinite(v) for v in p):
raise ValueError("Each target needs finite X, Y, heading")
result.append((p[0], p[1], p[2] % 360))
return result
def run_checked_route(route, deadline):
for index, target in enumerate(route):
remaining = deadline-brain.timer.time(MSEC)
if remaining <= 0:
return "DEADLINE"
result = finish_at_pose(*target, timeout_ms=remaining)
print("target", index, result)
if result != "POSE_SETTLED":
return result
return "ROUTE_COMPLETE"
def settle_stopped(deadline):
# Update the SAME estimator while braking; never reset to the checkpoint.
stop_drive()
previous, previous_ms, slow_since = None, brain.timer.time(MSEC), None
while brain.timer.time(MSEC) < deadline:
if (not competition.is_enabled() or not competition.is_autonomous()
or controller.buttonB.pressing() or not ready or not MOTION_CONFIGURED):
return False
now = brain.timer.time(MSEC)
current = read_pose()
dt = (now-previous_ms)/1000.0
slow = False
if previous is not None and dt > 0:
speed = math.hypot(current[0]-previous[0], current[1]-previous[1])/dt
rate = abs(angle_error(current[2], previous[2]))/dt
slow = speed <= POSITION_RATE_TOL and rate <= TURN_RATE_TOL
if slow:
if slow_since is None:
slow_since = now
if now-slow_since >= SETTLE_MS:
return True
else:
slow_since = None
previous, previous_ms = current, now
wait(POLL_MS, MSEC)
return False
def run_primary_with_fallback():
stop_drive()
try:
if not ROUTES_VERIFIED:
return "ROUTES_NOT_VERIFIED"
if (not all(math.isfinite(v) for v in (ROUTE_BUDGET_MS, SENSOR_TO_FRONT_MM,
STOP_CLEARANCE_MM, CHECKPOINT_POSITION_TOL_MM,
CHECKPOINT_HEADING_TOL_DEG)) or ROUTE_BUDGET_MS <= 0
or SENSOR_TO_FRONT_MM < 0 or STOP_CLEARANCE_MM <= 0
or CHECKPOINT_POSITION_TOL_MM <= 0
or not 0 < CHECKPOINT_HEADING_TOL_DEG <= 180):
return "INVALID_ROUTE_CONFIGURATION"
primary = checked_route(PRIMARY_ROUTE)
fallback = checked_route(FALLBACK_ROUTE)
checkpoint = checked_route([CHECKPOINT])[0]
if MIRROR_ROUTE:
primary = mirror_route(primary, MIRROR_X_MM)
fallback = mirror_route(fallback, MIRROR_X_MM)
checkpoint = mirror_pose(checkpoint, MIRROR_X_MM)
deadline = brain.timer.time(MSEC)+ROUTE_BUDGET_MS
result = run_checked_route(primary, deadline)
if result == "ROUTE_COMPLETE":
return "PRIMARY_COMPLETE"
# Only a detected blockage is eligible. Faults/cancellation never retry.
if "OBSTACLE_BLOCKED" not in result:
return "PRIMARY_STOPPED: " + result
if not settle_stopped(deadline):
return "STOP_NOT_SETTLED_OR_CANCELLED"
current = read_pose()
if not checkpoint_matches(current, checkpoint):
return "NO_MATCHING_FALLBACK_CHECKPOINT"
if not FALLBACK_RELEASED:
return "FALLBACK_NOT_RELEASED"
# This example does not turn away while its front guard is blocked.
# A clearance change must actually be observed, not merely assumed.
if front_status() not in ("OPEN", "NO_RETURN"):
return "FALLBACK_FRONT_GUARD_NOT_RELEASED"
if brain.timer.time(MSEC) >= deadline:
return "DEADLINE"
result = run_checked_route(fallback, deadline) # One alternate attempt
return "FALLBACK_COMPLETE" if result == "ROUTE_COMPLETE" else "FALLBACK_STOPPED: " + result
except Exception:
return "ROUTE_POSE_OR_COMMAND_FAULT"
finally:
stop_drive()
# =============================================================================
# COMPETITION CALLBACKS
# =============================================================================
def autonomous():
global pose_valid
stop_drive()
if not ready or not MOTION_CONFIGURED:
return
try:
# Mirrored origin pose is also mirrored; this example's start is invariant.
initialize_pose() # Physically verify START_POSE before each run.
print("route supervisor", run_primary_with_fallback())
except Exception:
print("STARTUP_POSE_FAULT")
finally:
stop_drive()
pose_valid = False
def driver_control():
global pose_valid
stop_drive()
pose_valid = False
competition = Competition(driver_control, autonomous)
stop_drive()
if MOTION_CONFIGURED and not competition.is_enabled() and inertial.installed():
inertial.calibrate() # Stationary disabled setup; calibration may block.
ready = not inertial.is_calibrating() and not competition.is_enabled()
while True:
wait(100, MSEC)
The common 14-second route deadline covers all primary movements, stopped settling and the fallback. Time already spent on the primary is not refunded. A longer alternate can fail on budget, so choose realistic routes and test timings.
The example start (0, 0, 0°) is unchanged by mirroring across X = 0. For another start or mirror line, transform START_POSE consistently and physically place the robot there before initializing the estimator.
10 | FOLLOW THE RETURN STATUS
| Status | Interpretation |
|---|---|
| PRIMARY_COMPLETE | Primary targets completed their final-pose checks |
| FALLBACK_COMPLETE | One eligible alternate completed within the original deadline |
| PRIMARY_STOPPED: reason | Ineligible failure; no alternate started |
| STOP_NOT_SETTLED_OR_CANCELLED | Stopped-state gate failed |
| NO_MATCHING_FALLBACK_CHECKPOINT | Current estimated pose is outside the approved entry region |
| FALLBACK_NOT_RELEASED | No explicit alternate release |
| FALLBACK_FRONT_GUARD_NOT_RELEASED | Current forward observation still blocks departure |
| FALLBACK_STOPPED: reason | Alternate failed; no second alternate attempt |
| ROUTES_NOT_VERIFIED / INVALID_ROUTE_CONFIGURATION | Setup prevents route execution |
| ROUTE_POSE_OR_COMMAND_FAULT | Route data, feedback or command failed |
Brake cleanup runs on normal returns and caught exceptions. Maintain one drivetrain-output owner and test mode transitions. Forced task termination is not equivalent to an ordinary function return.
11 | TEST TRANSFORMS BEFORE MOVEMENT
- Print original and mirrored targets and headings.
- Reflect the mirrored route again and check that it returns to the original.
- Test start and checkpoint gates in simulation.
- Mock OPEN, BLOCKED, UNKNOWN and FAULT observations.
- Verify a continuing blockage cannot be bypassed by the release flag.
- Test one successful alternate and a failing alternate with no further retry.
- Check that all phases share the original deadline.
- Run short physical tests in a clear area with conservative speed.
Record physical clearance, estimated and measured endpoint, guard observation, selected route, returned status and remaining budget. Mirrored success on paper is not evidence of mirrored physical reliability.
12 | COMMON MISTAKES
- Negating X but keeping heading: mirror orientation too.
- Swapping motor ports: reflection is a route transformation.
- Calling no return clear: label the limited observation honestly.
- Waiting forever: use a shared deadline and bounded decision.
- Restarting pose at a fallback: preserve the actual estimate.
- Retrying every failure: faults and cancellation need different responses from a blocked corridor.
- Checking just the endpoint: verify the connector, whole corridor and turning footprint.
CHALLENGES | MIRROR, OBSERVE AND RECOVER
- Transform: mirror (200, 700, 60°) across X = 0 and then X = 500.
- Compare: rotate the same pose 180° about the origin and explain the difference.
- Check: demonstrate that applying the same reflection twice restores the original pose.
- Calculate: a sensor 80 mm behind the front edge reads 420 mm. Find the front-edge gap.
- Mock: prove that invalid range data and no detection with the default policy stop movement.
- Gate: test positions just inside and outside the checkpoint tolerance.
- Budget: demonstrate that the fallback receives only the remaining primary-route time.
- Evaluate: explain why a single front sensor cannot validate an automatic sideways detour.
Calculation checks
Across X = 0: (−200, 700, 300°). Across X = 500: (800, 700, 300°). A 180° rotation gives (−200, −700, 240°). The front-edge gap is 420 − 80 = 340 mm.
OFFICIAL REFERENCES
- VEX V5 Distance Sensor: detection and range methods
- VEX motor output and stopping modes
- VEX competition callbacks and mode checks
The geometry, supervisor and route policies are independently written teaching examples. They require physical validation and are not a general collision-avoidance system.