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]
Field +X right, +Y up; clockwise heading from +Y
Original poseMirrored across X = 0
(300, 500, 45°)(−300, 500, 315°)
(400, 700, 90°)(−400, 700, 270°)
(0, 250, 0°)(0, 250, 0°)
Mirror a route across field X equals zeroA route on the right from the origin through positive X points is reflected to negative X points on the left. Y coordinates stay unchanged. A clockwise 45 degree heading becomes 315 degrees.Mirror line X = 0Original: X positiveMirrored: X negative45° → 315°Shared start (0, 0)
Reflection changes handedness. Mirror target positions and headings, not motor-port assignments.

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.

Choose the transform that matches the actual field arrangement
TransformPositionHeading
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.

A front sensor is an observation source, not a complete obstacle map
ObservationPolicy in this starter
Detected, usable range beyond thresholdOPEN; continue only on a verified route
Detected at or inside thresholdBLOCKED; stop on the first sample
No detection, ALLOW_NO_RETURN FalseUNKNOWN; stop
No detection, ALLOW_NO_RETURN TrueNO_RETURN; explicit limited-observation policy
Exception or unusable numerical rangeFAULT; 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

Success must follow successful movement, not merely selecting a route
StatusInterpretation
PRIMARY_COMPLETEPrimary targets completed their final-pose checks
FALLBACK_COMPLETEOne eligible alternate completed within the original deadline
PRIMARY_STOPPED: reasonIneligible failure; no alternate started
STOP_NOT_SETTLED_OR_CANCELLEDStopped-state gate failed
NO_MATCHING_FALLBACK_CHECKPOINTCurrent estimated pose is outside the approved entry region
FALLBACK_NOT_RELEASEDNo explicit alternate release
FALLBACK_FRONT_GUARD_NOT_RELEASEDCurrent forward observation still blocks departure
FALLBACK_STOPPED: reasonAlternate failed; no second alternate attempt
ROUTES_NOT_VERIFIED / INVALID_ROUTE_CONFIGURATIONSetup prevents route execution
ROUTE_POSE_OR_COMMAND_FAULTRoute 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

  1. Print original and mirrored targets and headings.
  2. Reflect the mirrored route again and check that it returns to the original.
  3. Test start and checkpoint gates in simulation.
  4. Mock OPEN, BLOCKED, UNKNOWN and FAULT observations.
  5. Verify a continuing blockage cannot be bypassed by the release flag.
  6. Test one successful alternate and a failing alternate with no further retry.
  7. Check that all phases share the original deadline.
  8. 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

  1. Transform: mirror (200, 700, 60°) across X = 0 and then X = 500.
  2. Compare: rotate the same pose 180° about the origin and explain the difference.
  3. Check: demonstrate that applying the same reflection twice restores the original pose.
  4. Calculate: a sensor 80 mm behind the front edge reads 420 mm. Find the front-edge gap.
  5. Mock: prove that invalid range data and no detection with the default policy stop movement.
  6. Gate: test positions just inside and outside the checkpoint tolerance.
  7. Budget: demonstrate that the fallback receives only the remaining primary-route time.
  8. 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

The geometry, supervisor and route policies are independently written teaching examples. They require physical validation and are not a general collision-avoidance system.