We’re moving to a new home! Our website is currently in test mode while we update and transfer our content. Some pages and resources may be temporarily unavailable. We’ll be back with all resources shortly. Thank you for your patience.

VEX V5 PYTHON | 02.5 REUSABLE MOVEMENT FUNCTIONS

Guide objective

Create clear movement functions, keep settings together and write autonomous routes with configurable drive and turn speeds.

01 | FROM COMMANDS TO A READABLE ROUTE

When a route contains several moves, repeatedly writing speed, units, timeout and stopping settings becomes difficult to maintain. A function packages those steps under one name.

drive_forward(200)
turn_right(45)
drive_reverse(100, speed=10)
face_heading(0)

You can read this as a route: forward 200 mm, right 45 degrees, reverse 100 mm slowly, then face heading 0. These are functions we create, not built-in VEX commands.

02 | PARAMETERS MAKE A FUNCTION REUSABLE

def drive_forward(distance_mm):
    drivetrain.drive_for(FORWARD, distance_mm, MM)

# These calls supply different arguments.
drive_forward(200)
drive_forward(350)

distance_mm is a parameter: the named input in the definition. 200 is an argument: the actual value supplied in a call. The same body runs with a different distance each time.

This small example assumes a configured drivetrain and existing speed settings. The complete version below adds settings and checks.

Indent the function body. Defining a function stores the instructions; calling it runs them. Names such as distance_mm make the expected unit visible.

03 | KEEP CONFIGURATION IN ONE PLACE

AUTON_DRIVE_PERCENT = 20
AUTON_TURN_PERCENT = 15
AUTON_STOP_MODE = BRAKE
MOVE_TIMEOUT_SECONDS = 3

Use separate drive and turn settings because a suitable straight-line speed may be too fast for a turn. These are cautious starting examples, not tuned values for every robot.

Capital letters are a Python naming convention for settings intended to remain fixed during a run. They do not prevent reassignment. Edit and retest one setting at a time.

Keep driver-control limits separate. Changing autonomous speed should not silently change joystick response.

04 | DEFAULT SPEED OR ONE-MOVE OVERRIDE

def choose_speed(speed, default_speed):
    selected = default_speed if speed is None else speed
    if not isinstance(selected, (int, float)) or not 0 < selected <= 100:
        raise ValueError("Speed must be greater than 0 and at most 100")
    return selected

None means no override was supplied. The helper selects the appropriate configured default. The valid range for this lesson is greater than 0 and at most 100 percent; use a stop command rather than a zero-speed movement.

drive_forward(200)            # Configured drive speed: 20%
drive_forward(100, speed=10)  # Override for this movement
drive_forward(200)            # Back to configured speed: 20%

Each call explicitly sets speed, so the slow override does not leak into the next move. VEX velocity settings persist until changed, which makes this reset important.

Using speed=None also lets the helper read the current configuration when called. A numeric Python default is evaluated when the function is defined.

05 | ONE SHARED DRIVE HELPER

def drive_distance(direction, distance_mm, speed=None):
    check_positive(distance_mm, "Distance")
    selected_speed = choose_speed(speed, AUTON_DRIVE_PERCENT)
    if not prepare_movement():
        return
    drivetrain.set_drive_velocity(selected_speed, PERCENT)
    drivetrain.drive_for(direction, distance_mm, MM)
    drivetrain.stop(AUTON_STOP_MODE)

def drive_forward(distance_mm, speed=None):
    drive_distance(FORWARD, distance_mm, speed)

def drive_reverse(distance_mm, speed=None):
    drive_distance(REVERSE, distance_mm, speed)

drive_forward and drive_reverse are small wrappers around drive_distance. They choose direction while the shared helper handles validation, preparation, speed, units and stopping.

Supply positive distances to these wrappers. The direction name expresses forward or reverse; avoid combining a negative distance with REVERSE.

check_positive and prepare_movement are defined in the complete project. They reject invalid distances and check setup and competition state before starting.

06 | REUSE THE PATTERN FOR TURNS

def turn_relative(direction, angle_degrees, speed=None):
    check_positive(angle_degrees, "Angle")
    selected_speed = choose_speed(speed, AUTON_TURN_PERCENT)
    if not prepare_movement():
        return
    drivetrain.set_turn_velocity(selected_speed, PERCENT)
    drivetrain.turn_for(direction, angle_degrees, DEGREES)
    drivetrain.stop(AUTON_STOP_MODE)

def turn_left(angle_degrees, speed=None):
    turn_relative(LEFT, angle_degrees, speed)

def turn_right(angle_degrees, speed=None):
    turn_relative(RIGHT, angle_degrees, speed)

def face_heading(target_degrees, speed=None):
    if (not isinstance(target_degrees, (int, float))
            or not 0 <= target_degrees < 360):
        raise ValueError("Heading must be from 0 to below 360")
    selected_speed = choose_speed(speed, AUTON_TURN_PERCENT)
    if not prepare_movement():
        return
    drivetrain.set_turn_velocity(selected_speed, PERCENT)
    drivetrain.turn_to_heading(target_degrees, DEGREES)
    drivetrain.stop(AUTON_STOP_MODE)

turn_left and turn_right describe changes from the current orientation. face_heading targets a direction in the calibrated heading reference. Its input range is deliberately restricted to 0 up to below 360 in this helper.

A heading target requires a heading-capable drivetrain. The complete project uses SmartDrive with an explicitly declared inertial sensor.

Every turn selects the turn default, not the drive default. Lowering one does not change the other.

07 | THE MOVEMENT CONTRACT

Know what each helper accepts
CallInput unitsDefault settingMeaning
drive_forward(200)MillimetresAUTON_DRIVE_PERCENTForward travel
drive_reverse(100, speed=10)Millimetres; percentOverride: 10Reverse travel
turn_right(45)DegreesAUTON_TURN_PERCENTRelative right turn
turn_left(90)DegreesAUTON_TURN_PERCENTRelative left turn
face_heading(0)DegreesAUTON_TURN_PERCENTTarget heading

The helper checks positive finite distances and relative angles, a heading within its chosen range and a valid speed. Invalid inputs raise ValueError before issuing a new movement. They are programming errors to fix, not obstacle detection.

These blocking commands run sequentially. Do not add wait=False without redesigning the route: the next command could begin before the previous target is reached.

08 | COMPLETE PROJECT WITH VISIBLE CONFIGURATION

Keep the VEXcode generated imports and Brain setup above this block, including import math. Place this team code below # Begin project code, outside the generated region. Replace the previous starter rather than adding duplicate objects.

Match the example motor ports, gear cartridges and reversal flags to your robot. The right-side reversal is an example, not a universal rule. Use manual declarations or Devices-panel configuration for each device, not both.

The example geometry assumes direct drive. Measure wheel travel, track width and wheelbase. Confirm the installed API’s external gearing convention before changing the ratio.

# =============================================================================
# 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
# - This program MOVES in autonomous and driver-control modes.
# - Inertial sensor: Port 20 (example; check your robot)
# - Autonomous: short route using reusable movement helpers.
# - Example geometry assumes DIRECT drive (external ratio 1).
# - 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)

# =============================================================================
# MEASURED GEOMETRY: EXAMPLES, REPLACE WITH YOUR MEASUREMENTS
# =============================================================================
WHEEL_DIAMETER_MM = 101.6
WHEEL_TRAVEL_MM = math.pi * WHEEL_DIAMETER_MM
TRACK_WIDTH_MM = 300
WHEEL_BASE_MM = 250
EXTERNAL_GEAR_RATIO = 1.0  # This example is direct drive only

inertial_sensor = Inertial(Ports.PORT20)

drivetrain = SmartDrive(
    left_drive, right_drive, inertial_sensor,
    WHEEL_TRAVEL_MM, TRACK_WIDTH_MM, WHEEL_BASE_MM,
    MM, EXTERNAL_GEAR_RATIO
)

# =============================================================================
# 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
AUTON_SPEED_PERCENT = 20
AUTON_DRIVE_PERCENT = 20
AUTON_TURN_PERCENT = 15
AUTON_STOP_MODE = BRAKE
CALIBRATION_LIMIT_MS = 5000
setup_ready = False
MOVE_TIMEOUT_SECONDS = 3
PAUSE_MS = 500

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 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)

def autonomous_ready():
    return (setup_ready and competition.is_enabled()
            and competition.is_autonomous())

def choose_speed(speed, default_speed):
    selected = default_speed if speed is None else speed
    if not isinstance(selected, (int, float)) or not 0 < selected <= 100:
        raise ValueError("Speed must be greater than 0 and at most 100")
    return selected

def check_positive(value, name):
    if not isinstance(value, (int, float)) or not 0 < value < float("inf"):
        raise ValueError(name + " must be a positive finite number")

def prepare_movement():
    if not autonomous_ready():
        stop_drive()
        return False
    drivetrain.set_stopping(AUTON_STOP_MODE)
    drivetrain.set_timeout(MOVE_TIMEOUT_SECONDS, SECONDS)
    return True

def drive_distance(direction, distance_mm, speed=None):
    check_positive(distance_mm, "Distance")
    selected_speed = choose_speed(speed, AUTON_DRIVE_PERCENT)
    if not prepare_movement():
        return
    drivetrain.set_drive_velocity(selected_speed, PERCENT)
    drivetrain.drive_for(direction, distance_mm, MM)
    drivetrain.stop(AUTON_STOP_MODE)

def drive_forward(distance_mm, speed=None):
    drive_distance(FORWARD, distance_mm, speed)

def drive_reverse(distance_mm, speed=None):
    drive_distance(REVERSE, distance_mm, speed)

def turn_relative(direction, angle_degrees, speed=None):
    check_positive(angle_degrees, "Angle")
    selected_speed = choose_speed(speed, AUTON_TURN_PERCENT)
    if not prepare_movement():
        return
    drivetrain.set_turn_velocity(selected_speed, PERCENT)
    drivetrain.turn_for(direction, angle_degrees, DEGREES)
    drivetrain.stop(AUTON_STOP_MODE)

def turn_left(angle_degrees, speed=None):
    turn_relative(LEFT, angle_degrees, speed)

def turn_right(angle_degrees, speed=None):
    turn_relative(RIGHT, angle_degrees, speed)

def face_heading(target_degrees, speed=None):
    if (not isinstance(target_degrees, (int, float))
            or not 0 <= target_degrees < 360):
        raise ValueError("Heading must be from 0 to below 360")
    selected_speed = choose_speed(speed, AUTON_TURN_PERCENT)
    if not prepare_movement():
        return
    drivetrain.set_turn_velocity(selected_speed, PERCENT)
    drivetrain.turn_to_heading(target_degrees, DEGREES)
    drivetrain.stop(AUTON_STOP_MODE)

# =============================================================================
# PRE-AUTON: CALLED ONCE AT PROJECT STARTUP
# =============================================================================
def pre_autonomous():
    global setup_ready
    setup_ready = False
    stop_drive()
    show_status("Keep robot still")
    inertial_sensor.calibrate()
    elapsed_ms = 0
    while inertial_sensor.is_calibrating() and elapsed_ms < CALIBRATION_LIMIT_MS:
        wait(10, MSEC)
        elapsed_ms += 10
    if inertial_sensor.is_calibrating():
        show_status("Calibration timeout")
        return
    # Place the robot facing your chosen route-zero direction first.
    drivetrain.set_heading(0, DEGREES)
    drivetrain.set_stopping(BRAKE)
    setup_ready = True
    show_status("Setup ready")

# =============================================================================
# AUTONOMOUS: A FINITE ROUTINE
# =============================================================================
def autonomous():
    if not autonomous_ready():
        stop_drive()
        return
    show_status("Running example route")
    drive_forward(200)
    turn_right(45)
    drive_reverse(100, speed=10)
    face_heading(0)
    stop_drive()
    if autonomous_ready():
        show_status("Commands ended; inspect results")

# =============================================================================
# DRIVER CONTROL: REPEATING UPDATES WHILE ENABLED
# =============================================================================
def driver_control():
    if not setup_ready:
        stop_drive()
        return
    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)

Start while disabled, facing your chosen route-zero direction, and keep the robot still during inertial calibration. Verify sensor connection and readings before trusting a completed calibration wait.

The driver callback retains left-stick forward/back and right-stick turning. Competition(driver_control, autonomous) registers function names without calling them immediately.

09 | A RETURNED COMMAND IS NOT PROOF OF ARRIVAL

The timeout bounds how long a movement may try to finish. If it expires, a blocking command can return before reaching the target, and this simple route may continue to its next instruction.

The helpers do not return an arrival-success flag. Check actual travel and heading. A timeout is not an emergency stop, collision sensor or proof of accuracy.

prepare_movement checks competition state before each move. It does not continuously poll while a blocking move runs; disabling through competition control must stop robot operation. Test that behaviour with your setup.

The next guide develops timeout handling and cancellation. For now, use short individual moves in a clear test area and inspect results before combining them.

10 | TEST THE HELPER BEFORE THE WHOLE ROUTE

  1. Verify ports and directions with supported wheels.
  2. Test one short forward move on a clear, level surface.
  3. Test reverse and each turning helper separately.
  4. Run default, override, default calls and check that speed returns to the configured value.
  5. Record requested and measured distances and headings over three trials.
  6. Only then run the combined example route with room for its full path.

Changing speed can change slip and stopping distance. A reusable function makes tuning easier; it does not remove the need to measure.

CHALLENGES | BUILD A SMALL MOVEMENT LIBRARY

  1. Explain: identify a parameter, an argument and a return value in choose_speed.
  2. Predict: state the selected speed for default, override and default calls.
  3. Configure: change the turn default without changing driver controls or drive speed.
  4. Test: measure a short move at two speeds while keeping distance fixed.
  5. Compose: create a route using forward, reverse and a turn with one slow override.
  6. Reuse: define a drive_out_and_back(distance_mm, speed=None) function using existing helpers.
  7. Review: explain why command completion and physical arrival are different.
Success evidence

A saved project with explicit configuration, reusable helpers, a readable route and a short test log. Default speed resumes after an override, units are clear and measured outcomes support your settings.

OFFICIAL REFERENCES

This is an independent learning resource; VEX and VEXcode are trademarks of their respective owners.