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 selectedNone 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
| Call | Input units | Default setting | Meaning |
|---|---|---|---|
| drive_forward(200) | Millimetres | AUTON_DRIVE_PERCENT | Forward travel |
| drive_reverse(100, speed=10) | Millimetres; percent | Override: 10 | Reverse travel |
| turn_right(45) | Degrees | AUTON_TURN_PERCENT | Relative right turn |
| turn_left(90) | Degrees | AUTON_TURN_PERCENT | Relative left turn |
| face_heading(0) | Degrees | AUTON_TURN_PERCENT | Target 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
- Verify ports and directions with supported wheels.
- Test one short forward move on a clear, level surface.
- Test reverse and each turning helper separately.
- Run default, override, default calls and check that speed returns to the configured value.
- Record requested and measured distances and headings over three trials.
- 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
- Explain: identify a parameter, an argument and a return value in choose_speed.
- Predict: state the selected speed for default, override and default calls.
- Configure: change the turn default without changing driver controls or drive speed.
- Test: measure a short move at two speeds while keeping distance fixed.
- Compose: create a route using forward, reverse and a turn with one slow override.
- Reuse: define a drive_out_and_back(distance_mm, speed=None) function using existing helpers.
- 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.