VEX V5 PYTHON | 04.5 HOMING AND TRAVEL LIMITS

Guide objective

Establish a verified mechanism zero with a limit switch, then apply shared physical-switch and software-position guards to every movement command.

01 | KNOW WHERE THE MECHANISM STARTS

An encoder can report movement, but a useful travel limit needs a physical reference. Homing moves a mechanism deliberately to a known reference, confirms it and assigns a position value.

A limit switch provides a contact signal. Combined with rotation feedback, it can establish a lower reference and help prevent later commands from pushing beyond the working range.

Homing is an intentional motor action. It must be slow, bounded and tested for the actual mechanism.

02 | READ A PHYSICAL SWITCH

lower_limit = Limit(brain.three_wire_port.a)

if lower_limit.pressing():
    print("Lower switch pressed")

Limit switches use the Brain’s 3-Wire ports, not numbered Smart Ports. The example uses A for lower and B for upper contact. pressing() returns the current Boolean state.

Test each switch manually with motor movement disabled. Confirm that pressing and releasing produce the expected readings before connecting those readings to motion.

Mount the actuator so the switch triggers before the mechanism reaches a damaging hard stop. Do not use the switch body as a structural stop.

03 | THREE LAYERS HAVE DIFFERENT ROLES

These layers complement one another
LayerRoleLimitation
Physical switchDetect contact at a mounted reference or boundaryCan fail, disconnect or be mounted incorrectly
Software travel limitBlock commands beyond a calibrated position rangeDepends on valid zero and sensor feedback
Mechanical designProvide appropriate support, clearance and robust travel boundariesMust not be repeatedly driven against damaging stops

A Boolean switch is not a complete safety system. A disconnected switch can look unpressed, and a stuck switch can look like a valid contact. Timeouts and plausibility checks reduce some risks but cannot detect every failure.

04 | HOME, THEN ENABLE NORMAL TRAVEL

  1. Mark the mechanism unhomed before starting.
  2. If the lower switch is already pressed, move away slowly until a confirmed release.
  3. Seek the lower switch slowly in the homing direction.
  4. Stop immediately when it presses.
  5. Confirm the press while stopped.
  6. Set rotation position to zero and mark homing successful.

The initial release check helps detect a switch that remains pressed. If the switch is initially unpressed, the starter seeks it directly. More demanding homing systems can add a coarse seek followed by release and a second slow approach.

Switch zero is the trigger position, not necessarily the mechanism’s absolute hard-bottom position. Define your travel measurements from the same reference.

05 | DEBOUNCE WITHOUT DRIVING INTO THE SWITCH

A mechanical switch can briefly alternate between pressed and released around contact. A confirmation interval helps reject a transient signal.

First stop the motor, then confirm the state. Do not continue driving into a pressed switch while waiting for it to remain pressed for 60 ms.

The example retries the seek if the signal becomes unstable, under the same overall homing deadline. Choose the interval from tests, not from an assumption that every switch behaves identically.

06 | BOUND HOMING EVEN BEFORE ZERO IS KNOWN

Before homing, ordinary absolute software limits cannot be trusted. The homing action instead uses a short deadline, a maximum relative sensor-travel bound, low command power and physical switch conditions.

If the switch never appears, report failure, stop and keep the mechanism unhomed. Do not reset the encoder merely because the timer ended.

The relative bound assumes the sensor responds correctly. A frozen or disconnected reading can defeat it; the timeout remains a separate bound. Check motor/sensor sign and switch operation before enabling homing.

07 | PUT NORMAL LIMITS INSIDE THE WORKING RANGE

For an illustrative six-turn span, normal software travel might be limited to 0.15–5.85 sensor turns. These margins must be measured for the actual build and stopping behaviour.

A limit should stop motion into the boundary, not blindly block retreat
ConditionBlocked commandNormally allowed recovery
At lower switch or lower software limitDown / negativeUp / positive
At upper switch or upper software limitUp / positiveDown / negative
Both switches pressedBoth directionsInspect and deliberately recover
Not homed or implausible positionBoth directionsVerified homing or fault investigation

Do not re-zero whenever a switch activates during normal operation. Unexpected contact may indicate a fault. The starter only establishes zero during an explicit successful homing action.

08 | SHARE THE GUARD BETWEEN DRIVER AND AUTONOMOUS

def guard_output(position, requested, lower_pressed, upper_pressed, ready, failed):
    if not ready or failed or (lower_pressed and upper_pressed):
        return 0
    if not -0.1 <= position <= FULL_TRAVEL_TURNS + 0.1:
        return 0
    if requested < 0 and (lower_pressed or position <= SOFT_MIN_TURNS):
        return 0
    if requested > 0 and (upper_pressed or position >= SOFT_MAX_TURNS):
        return 0
    return max(-MAX_OUTPUT_PERCENT, min(MAX_OUTPUT_PERCENT, requested))

def set_mechanism_output(requested):
    global fault, homed
    position = mechanism_rotation.position(TURNS)
    lower = lower_limit.pressing()
    upper = upper_limit.pressing()
    if lower and upper:
        fault = "SWITCH_CONFLICT"
        homed = False
    if homed and not -0.1 <= position <= FULL_TRAVEL_TURNS + 0.1:
        fault = "POSITION_FAULT"
        homed = False
    output = guard_output(position, requested, lower, upper, homed, fault is not None)
    if not MOTION_CONFIGURED or not competition.is_enabled() or controller.buttonB.pressing():
        output = 0
    if abs(output) < 0.01:
        stop_mechanism()
    else:
        direction = FORWARD if output > 0 else REVERSE
        mechanism_motor.spin(direction, abs(output), PERCENT)
    return output

Driver button commands and autonomous target corrections both pass through set_mechanism_output. The helper checks the switches, homed state, fault state and position before sending a motor command.

Faults remain latched until a deliberate new homing attempt after inspection. Changing modes does not silently clear a switch conflict or bad reference.

Homing has a separate, tightly bounded low-output path because it needs to establish the reference. It does not provide a general driver override of normal limits.

09 | COMPLETE MECHANISM-ONLY COMPETITION STARTER

Keep generated imports and Brain setup above this block. Paste below # Begin project code, outside the generated region, replacing previous starters. This project has one mechanism and no drivetrain.

The example uses motor port 9, Rotation Sensor port 19, lower 3-Wire A and upper 3-Wire B. Match directions, cartridges, ports and measured travel to the build. Avoid duplicate device declarations.

MOTION_CONFIGURED starts False, so movement is blocked until the configuration and physical switch tests are complete. Autonomous homes only unless RUN_AUTO_TARGET is deliberately enabled.

# =============================================================================
# TEAM [MY TEAM NAME / NUMBER] | SINGLE-MECHANISM HOMING STARTER
# =============================================================================
# CONFIGURATION
# - Mechanism motor: Smart Port 9, green cartridge (example)
# - Rotation Sensor: Smart Port 19
# - Lower limit switch: Brain 3-Wire A
# - Upper limit switch: Brain 3-Wire B
# - Positive motor/sensor direction must mean increasing mechanism travel.
# CONTROLS
# - Driver X rising edge: request homing
# - L1/L2: up/down after successful homing
# - Controller B: cancel current action / stop while held
# - No drivetrain in this mechanism-only example.
# Keep generated VEX imports and Brain setup above this code.

mechanism_motor = Motor(Ports.PORT9, GearSetting.RATIO_18_1, False)
mechanism_rotation = Rotation(Ports.PORT19, False)
lower_limit = Limit(brain.three_wire_port.a)
upper_limit = Limit(brain.three_wire_port.b)
controller = Controller(PRIMARY)

MOTION_CONFIGURED = False        # Enable only after directions/switches are tested
RUN_AUTO_TARGET = False          # False: autonomous homes only
FULL_TRAVEL_TURNS = 6.0           # Measured sensor span; teaching example
SOFT_MIN_TURNS = 0.15
SOFT_MAX_TURNS = 5.85
HOME_PERCENT = 8
MAX_OUTPUT_PERCENT = 15
HOMING_TIMEOUT_MS = 5000
HOMING_MAX_TRAVEL_TURNS = 7.0      # Bound relative sensor travel during homing
MOVE_TIMEOUT_MS = 4000
SWITCH_CONFIRM_MS = 60
POLL_MS = 20
TARGET_TOLERANCE_TURNS = 0.05
POSITION_KP_PERCENT_PER_TURN = 20
STOP_MODE = BRAKE                 # Choose for the supported actual mechanism
homed = False
fault = None

def stop_mechanism():
    mechanism_motor.stop(STOP_MODE)

def status(text):
    brain.screen.clear_screen()
    brain.screen.set_cursor(1, 1)
    brain.screen.print(text)
    print("MECHANISM", text)

def mode_active(mode):
    if not competition.is_enabled():
        return False
    return competition.is_autonomous() if mode == "AUTO" else competition.is_driver_control()

def abort_reason(mode, start_ms, timeout_ms):
    if not mode_active(mode):
        return "MODE_CHANGED"
    if controller.buttonB.pressing():
        return "CANCELLED"
    if brain.timer.time(MSEC) - start_ms >= timeout_ms:
        return "TIMEOUT"
    if lower_limit.pressing() and upper_limit.pressing():
        return "SWITCH_CONFLICT"
    return None

def wait_switch_state(switch, pressed, mode, start_ms):
    # Motors are already stopped before this confirmation pause.
    confirmation_start = brain.timer.time(MSEC)
    while brain.timer.time(MSEC) - confirmation_start < SWITCH_CONFIRM_MS:
        reason = abort_reason(mode, start_ms, HOMING_TIMEOUT_MS)
        if reason is not None:
            return reason
        if switch.pressing() != pressed:
            return "UNSTABLE"
        wait(POLL_MS, MSEC)
    return "CONFIRMED"

def home_mechanism(mode):
    global homed, fault
    homed = False
    if not MOTION_CONFIGURED:
        stop_mechanism()
        return "CONFIGURATION_REQUIRED"
    fault = None  # A deliberate new home attempt, after inspection.
    start_ms = brain.timer.time(MSEC)
    start_turns = mechanism_rotation.position(TURNS)
    phase = "BACK_OFF" if lower_limit.pressing() else "SEEK"
    status("Homing: " + phase)
    try:
        while True:
            reason = abort_reason(mode, start_ms, HOMING_TIMEOUT_MS)
            if reason is not None:
                fault = reason
                return reason
            travel = abs(mechanism_rotation.position(TURNS) - start_turns)
            if not travel <= HOMING_MAX_TRAVEL_TURNS:
                fault = "HOMING_TRAVEL_FAULT"
                return fault
            if phase == "BACK_OFF":
                if upper_limit.pressing():
                    fault = "UPPER_LIMIT_DURING_BACKOFF"
                    return fault
                if lower_limit.pressing():
                    mechanism_motor.spin(FORWARD, HOME_PERCENT, PERCENT)
                else:
                    stop_mechanism()
                    result = wait_switch_state(lower_limit, False, mode, start_ms)
                    if result == "CONFIRMED":
                        phase = "SEEK"
                    elif result != "UNSTABLE":
                        fault = result
                        return result
            else:
                if lower_limit.pressing():
                    # Stop immediately; do not drive into the switch during debounce.
                    stop_mechanism()
                    result = wait_switch_state(lower_limit, True, mode, start_ms)
                    if result == "CONFIRMED":
                        mechanism_rotation.set_position(0, TURNS)
                        homed = True
                        return "HOMED"
                    if result != "UNSTABLE":
                        fault = result
                        return result
                else:
                    mechanism_motor.spin(REVERSE, HOME_PERCENT, PERCENT)
            wait(POLL_MS, MSEC)
    finally:
        stop_mechanism()

def guard_output(position, requested, lower_pressed, upper_pressed, ready, failed):
    if not ready or failed or (lower_pressed and upper_pressed):
        return 0
    if not -0.1 <= position <= FULL_TRAVEL_TURNS + 0.1:
        return 0
    if requested < 0 and (lower_pressed or position <= SOFT_MIN_TURNS):
        return 0
    if requested > 0 and (upper_pressed or position >= SOFT_MAX_TURNS):
        return 0
    return max(-MAX_OUTPUT_PERCENT, min(MAX_OUTPUT_PERCENT, requested))

def set_mechanism_output(requested):
    global fault, homed
    position = mechanism_rotation.position(TURNS)
    lower = lower_limit.pressing()
    upper = upper_limit.pressing()
    if lower and upper:
        fault = "SWITCH_CONFLICT"
        homed = False
    if homed and not -0.1 <= position <= FULL_TRAVEL_TURNS + 0.1:
        fault = "POSITION_FAULT"
        homed = False
    output = guard_output(position, requested, lower, upper, homed, fault is not None)
    if not MOTION_CONFIGURED or not competition.is_enabled() or controller.buttonB.pressing():
        output = 0
    if abs(output) < 0.01:
        stop_mechanism()
    else:
        direction = FORWARD if output > 0 else REVERSE
        mechanism_motor.spin(direction, abs(output), PERCENT)
    return output

def move_to_percent(target_percent):
    if not isinstance(target_percent, (int, float)) or not 0 <= target_percent <= 100:
        raise ValueError("Use a percentage from 0 to 100")
    target = target_percent / 100 * FULL_TRAVEL_TURNS
    if not SOFT_MIN_TURNS <= target <= SOFT_MAX_TURNS:
        raise ValueError("Target must be inside software travel limits")
    if not homed or fault is not None:
        stop_mechanism()
        return "NOT_HOMED_OR_FAULTED"
    start_ms = brain.timer.time(MSEC)
    try:
        while True:
            reason = abort_reason("AUTO", start_ms, MOVE_TIMEOUT_MS)
            if reason is not None:
                return reason
            if not homed or fault is not None:
                return "NOT_HOMED_OR_FAULTED"
            position = mechanism_rotation.position(TURNS)
            if not -0.1 <= position <= FULL_TRAVEL_TURNS + 0.1:
                set_mechanism_output(0)
                return "POSITION_FAULT"
            error = target - position
            if abs(error) <= TARGET_TOLERANCE_TURNS:
                return "POSITION_THRESHOLD_REACHED"
            if set_mechanism_output(error * POSITION_KP_PERCENT_PER_TURN) == 0:
                return "LIMIT_STOP"
            wait(POLL_MS, MSEC)
    finally:
        stop_mechanism()

def autonomous():
    result = home_mechanism("AUTO")
    status(result)
    if result == "HOMED" and RUN_AUTO_TARGET:
        status(move_to_percent(30))

def driver_control():
    previous_x = controller.buttonX.pressing()
    try:
        while mode_active("DRIVER"):
            current_x = controller.buttonX.pressing()
            if current_x and not previous_x:
                stop_mechanism()
                status(home_mechanism("DRIVER"))
                current_x = controller.buttonX.pressing()
            previous_x = current_x
            up = controller.buttonL1.pressing()
            down = controller.buttonL2.pressing()
            requested = MAX_OUTPUT_PERCENT * (int(up) - int(down))
            set_mechanism_output(requested)
            wait(POLL_MS, MSEC)
    finally:
        stop_mechanism()

stop_mechanism()
status("Unhomed: configure and test before motion")
competition = Competition(driver_control, autonomous)
while True:
    wait(100, MSEC)

In enabled driver mode, a new X press requests homing. L1/L2 request up/down after success; pressing both requests zero. B cancels the current homing or autonomous action and stops normal driver output while held.

The example does not home while disabled. Driver-triggered homing runs in the same motor-owning callback, so the normal button loop does not compete with it.

The 30% autonomous target is 1.8 sensor turns for the illustrative six-turn span. Targets at 0% or 100% are outside this example’s software-safe range and are rejected.

10 | CHOOSE STOPPING FOR THE ACTUAL MECHANISM

BRAKE is the starter’s placeholder for a supported test mechanism. A loaded lift may require suitable holding behaviour or a mechanical support arrangement.

Powered HOLD cannot support a load after power loss or during conditions that prevent motor actuation. Do not put hands under a raised mechanism while relying on code to hold it.

Measure run-on and choose margins accordingly. The target helper reports a position threshold, not a verified stationary dwell. Add a settling check if the application requires it.

11 | RECOVER DELIBERATELY

After failed homing, remain unhomed. Investigate wiring, switch mounting, sensor coupling and direction before requesting another attempt.

If the mechanism moves while disabled, slips or is repositioned manually, the old reference may no longer be valid. Re-home rather than assuming that an earlier success remains trustworthy.

The starter retains a successful reference between normal mode changes when no fault occurs. Decide whether your actual mechanism requires homing every enable cycle, and document that policy.

A deliberate re-home request clears the software fault to attempt recovery, but it cannot repair the physical cause. Do not repeatedly retry a jammed mechanism.

12 | TEST WITHOUT PUSHING AGAINST HARD STOPS

Use supported low-output tests and simulated faults
TestExpected behaviour
Manual switch test, movement blockedPressed/released readings agree with contact
Lower switch pressed at home requestControlled back-off and release before seek
Homing switch absent in a simulated testTimeout; no zero assigned
Both switches pressedConflict fault; no normal output
Lower/upper normal limitInto-boundary command blocked; retreat allowed
Cancel or disable during homingStop; no successful reference assigned
Restart from an unknown positionUnhomed until deliberate successful homing

Start with conservative travel margins and a supported mechanism. Do not unplug sensors while commanding movement or deliberately drive into a hard stop to create a failure.

Record switch repeatability, homing time, sensor zero and final physical position across three trials. A change in trigger location affects every later target.

CHALLENGES | ESTABLISH AND PROTECT A REFERENCE

  1. Configure: label Smart Ports and 3-Wire ports on a mechanism diagram.
  2. Explain: distinguish a physical switch, a software limit and a mechanical stop.
  3. Trace: describe homing when the lower switch starts pressed and when it starts released.
  4. Verify: show why the motor stops before the confirmation pause.
  5. Guard: test into-limit and away-from-limit requests with simulated positions.
  6. Abort: demonstrate cancellation and timeout without assigning a new zero.
  7. Repeat: record three homing trials and compare the physical reference.
  8. Document: write a recovery policy for a fault or manual mechanism movement.
Success evidence

A labelled setup, verified switch/direction checks, a saved project and a test log. Show that normal motion stays blocked before homing and that both driver and autonomous commands use the shared guard.

OFFICIAL REFERENCES

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