VEX V5 PYTHON | 03.3 ARC TURNS
Guide objective
Calculate a curved route from its centre radius and path length, then convert the inside and outside wheel travel into coordinated motor targets.
01 | AN ARC IS A MOVING TURN
A point turn rotates the robot approximately on the spot. An arc turn moves its centre along a curved path while its heading changes.
On a differential-drive robot, both sides can move forward, but the outside side travels farther and faster. Equal forward travel produces a straight line rather than a curve.
This guide defines a forward arc by its centre-path radius, centre-path length and LEFT or RIGHT direction.
02 | DEFINE YOUR MEASUREMENTS
| Symbol | Meaning | Example |
|---|---|---|
| R | Radius from the turn centre to the robot’s centre path | 400 mm |
| s | Distance along the curved centre path | 1,000 mm |
| T | Spacing between left and right wheel contact centres | 300 mm |
| θ | Angle swept around the turn centre | s ÷ R radians |
R is not the radius of the inside wheel, and s is not the straight-line distance between start and finish. The robot’s centre follows the middle path between the two sides.
T is track width, not front-to-back wheelbase. With a four-wheel skid-steer chassis, turning includes tyre scrub. Measured physical track is the starting model; effective turning behaviour may need calibration.
03 | PATH LENGTH DETERMINES THE ANGLE
angle_radians = centre_path_length / centre_radius angle_degrees = angle_radians × 180 / π
For a 1,000 mm centre path around a 400 mm radius, θ = 1,000 ÷ 400 = 2.5 radians, about 143.24°.
That is a large turn, not a short gentle bend. Predict the final orientation before running it. For the first hardware test, the sample uses a shorter 300 mm path: 0.75 radians, about 42.97°.
If you know the angle instead, calculate centre path length with s = R × θ, using radians. A 90° arc at R = 400 mm has s ≈ 628.32 mm.
04 | INSIDE AND OUTSIDE PATHS
inside_radius = R − T / 2 outside_radius = R + T / 2 inside_distance = θ × inside_radius outside_distance = θ × outside_radius
Both sides sweep the same angle, but at different radii. With R = 400 mm and T = 300 mm, the radii are 250 mm inside and 550 mm outside.
| Path | Radius | Distance for θ = 2.5 radians |
|---|---|---|
| Inside | 250 mm | 625 mm |
| Robot centre | 400 mm | 1,000 mm |
| Outside | 550 mm | 1,375 mm |
Check your calculation: (625 + 1,375) ÷ 2 = 1,000 mm. Also, outside minus inside distance is θ × T: 750 mm here.
05 | MATCH THE SPEED RATIO
To finish together in an ideal constant-speed model, the speed ratio should match the distance ratio.
inside_speed = outside_speed × inside_distance / outside_distance
For the worked example, inside/outside = 625 ÷ 1,375 ≈ 0.4545. With an outside command of 20%, the inside command is about 9.09%.
We use the outside speed as the reference so neither side needs to exceed it. This assumes identical wheels, gearing and motor cartridges. Different hardware requires conversion to comparable wheel speeds.
Requested percentages are not measured ground speeds. Independent motor acceleration and target stopping can distort the arc, especially near the start and finish.
06 | CHOOSE LEFT AND RIGHT CORRECTLY
| Forward curve | Left side | Right side |
|---|---|---|
| LEFT | Inside: shorter/slower | Outside: longer/faster |
| RIGHT | Outside: longer/faster | Inside: shorter/slower |
For a right arc, the left side travels farther. For a left arc, swap the distances and speed commands.
The helper intentionally supports forward travel only. Do not add REVERSE without defining whether LEFT/RIGHT means curve location or heading-change direction. Reversing both side commands reverses the sign of the heading change for the same left/right speed difference.
07 | CONVERT TRAVEL INTO MOTOR DEGREES
wheel_travel = π × wheel_diameter motor_degrees = side_distance / wheel_travel × 360
The sample assumes direct drive: one motor output-shaft revolution produces one wheel revolution. With a 101.6 mm wheel, nominal wheel travel is about 319.19 mm.
For 625 mm inside travel, the motor target is about 704.92°. For 1,375 mm outside travel, it is about 1,550.82°. These are motor rotation targets, not the robot’s 143.24° heading change.
With external gearing, calculate motor output turns from your measured wheel-per-motor ratio. Do not multiply by the internal cartridge ratio again: the API’s position refers to motor output-shaft rotation.
Calibrate straight-line travel before evaluating an arc. Wrong wheel travel affects both the path length and your geometric interpretation.
08 | START BOTH SIDES WITHOUT BLOCKING
left_drive.spin_for(FORWARD, left_degrees, DEGREES,
left_speed, PERCENT, wait=False)
right_drive.spin_for(FORWARD, right_degrees, DEGREES,
right_speed, PERCENT, wait=False)If the first spin_for blocks, the left side could finish before the right starts. That produces sequential movements rather than one coordinated curve.
Starting both non-blocking commands allows near-concurrent motion, although the calls are not perfectly simultaneous. Monitor both sides, keep a deadline, and stop both if the routine is cancelled.
Even matched targets and speed ratios do not provide continuous path synchronisation. A more advanced controller compares both sides’ progress during movement.
09 | COMPLETE FORWARD-ARC PROJECT
Keep generated imports and Brain setup above this block, including import math. Paste below # Begin project code, outside the generated region, replacing the previous starter.
Check all ports, cartridges and reversal flags. Positive forward commands on both groups must mean robot-forward. Avoid duplicate manual and Devices-panel definitions.
The example assumes direct drive, equal wheel diameters and matching cartridges. It uses a short forward right arc of R = 400 mm and s = 300 mm, not the longer worked example.
# =============================================================================
# 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
# - Autonomous: short forward right arc.
# - Hold B to cancel the autonomous arc.
# - Driver control retains left axis3 / right axis1.
# - 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)
# =============================================================================
# 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
TRACK_MM = 300 # Left/right wheel contact centre spacing
WHEEL_DIAMETER_MM = 101.6
WHEEL_TRAVEL_MM = math.pi * WHEEL_DIAMETER_MM
ARC_OUTSIDE_PERCENT = 20
ARC_LIMIT_MS = 5000
ENCODER_TOLERANCE_DEGREES = 10
# Direct drive, equal wheels and matching cartridges on both sides assumed.
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 arc_geometry(radius_mm, path_mm, track_mm):
for value in (radius_mm, path_mm, track_mm):
if not isinstance(value, (int, float)) or not 0 < value < float("inf"):
raise ValueError("Geometry values must be positive and finite")
if radius_mm <= track_mm / 2:
raise ValueError("This forward-arc starter needs radius above half track")
angle_radians = path_mm / radius_mm
inside_mm = angle_radians * (radius_mm - track_mm / 2)
outside_mm = angle_radians * (radius_mm + track_mm / 2)
return angle_radians, inside_mm, outside_mm
def arc_stop_reason():
if controller.buttonB.pressing():
return "CANCELLED"
if not (competition.is_enabled() and competition.is_autonomous()):
return "MODE_CHANGED"
return None
def drive_arc(direction, radius_mm, path_mm, outside_speed=None):
# Forward travel only. LEFT/RIGHT describe the forward curve direction.
if direction not in (LEFT, RIGHT):
raise ValueError("Use LEFT or RIGHT")
angle, inside_mm, outside_mm = arc_geometry(radius_mm, path_mm, TRACK_MM)
speed = ARC_OUTSIDE_PERCENT if outside_speed is None else outside_speed
if not isinstance(speed, (int, float)) or not 0 < speed <= 100:
raise ValueError("Outside speed must be above 0 and at most 100")
if arc_stop_reason() is not None:
stop_drive()
return arc_stop_reason()
inside_speed = speed * inside_mm / outside_mm
inside_degrees = inside_mm / WHEEL_TRAVEL_MM * 360
outside_degrees = outside_mm / WHEEL_TRAVEL_MM * 360
if direction == LEFT:
left_degrees, right_degrees = inside_degrees, outside_degrees
left_speed, right_speed = inside_speed, speed
else:
left_degrees, right_degrees = outside_degrees, inside_degrees
left_speed, right_speed = speed, inside_speed
motors = (left_motor_1, left_motor_2, right_motor_1, right_motor_2)
starts = [motor.position(DEGREES) for motor in motors]
targets = (left_degrees, left_degrees, right_degrees, right_degrees)
start_ms = brain.timer.time(MSEC)
try:
left_drive.set_stopping(BRAKE)
right_drive.set_stopping(BRAKE)
left_drive.set_timeout(ARC_LIMIT_MS + 500, MSEC)
right_drive.set_timeout(ARC_LIMIT_MS + 500, MSEC)
# Both sides start without waiting for the first to finish.
left_drive.spin_for(FORWARD, left_degrees, DEGREES,
left_speed, PERCENT, wait=False)
right_drive.spin_for(FORWARD, right_degrees, DEGREES,
right_speed, PERCENT, wait=False)
wait(LOOP_DELAY_MS, MSEC)
while True:
reason = arc_stop_reason()
if reason is not None:
return reason
if brain.timer.time(MSEC) - start_ms >= ARC_LIMIT_MS:
return "TIMEOUT"
if left_drive.is_done() and right_drive.is_done():
stop_drive()
wait(100, MSEC)
reason = arc_stop_reason()
if reason is not None:
return reason
if brain.timer.time(MSEC) - start_ms >= ARC_LIMIT_MS:
return "TIMEOUT"
errors = [abs((motor.position(DEGREES) - start) - target)
for motor, start, target in zip(motors, starts, targets)]
if max(errors) <= ENCODER_TOLERANCE_DEGREES:
return "ENCODER_TARGETS_CONFIRMED"
return "ENDED_OFF_TARGET"
wait(LOOP_DELAY_MS, MSEC)
finally:
stop_drive()
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)
# =============================================================================
# PRE-AUTON: CALLED ONCE AT PROJECT STARTUP
# =============================================================================
def pre_autonomous():
stop_drive()
show_status("Setup ready")
# Later: initialise sensors and starting values here.
# Finish setup before the robot is enabled for a match.
# =============================================================================
# AUTONOMOUS: A FINITE ROUTINE
# =============================================================================
def autonomous():
show_status("Forward right arc")
result = drive_arc(RIGHT, radius_mm=400, path_mm=300)
show_status(result)
# No next move: inspect physical path and final heading first.
# =============================================================================
# DRIVER CONTROL: REPEATING UPDATES WHILE ENABLED
# =============================================================================
def driver_control():
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)
The helper checks each motor’s encoder change against its target after both groups report completion. ENCODER_TARGETS_CONFIRMED verifies motor rotation within the chosen tolerance; it does not prove the physical arc or final heading.
Holding B cancels this invocation and stops both groups. Cancellation, mode change or timeout prevents continued autonomous commands. This project ends after one arc so you can inspect the path.
Driver control remains the established basic split-stick loop. The arc helper does not include the slew limiter from 03.1: independently ramping the sides can change their ratio and requires a coordinated design.
10 | KNOW THE RADIUS LIMIT
This helper requires R > T/2. Both inside and outside distances are then positive, so both sides move forward.
At R = T/2, the inside path radius is zero and that side would stay approximately still: a pivot turn. At smaller radii, the ideal inside wheel travel becomes negative, requiring opposite-side directions.
Those are useful movements, but they need a different helper and clear sign conventions. Do not force them through a forward-only function. Very tight valid arcs can still demand an inside speed too low for reliable motion under load.
11 | TEST THE SHAPE, NOT JUST THE ENCODERS
- Verify side directions with supported wheels and measure track and wheel travel.
- Mark a clear start position and forward heading on a level surface.
- Draw or mark the intended centre path and predicted end orientation. Allow clearance for the robot’s full swept footprint.
- Run the short arc at low speed and record the path from above or from a useful fixed viewpoint.
- Compare final centre position, heading change and encoder outcome.
- Repeat three times before changing one parameter.
Check the whole swept path, not only the final point. A robot can end near the right spot after travelling through an unsafe or unintended part of the field.
12 | TROUBLESHOOT WITH A TEST LOG
| Observation | Check first |
|---|---|
| Curve bends the wrong way | Left/right mapping and motor reversal |
| Travel is consistently too short or long | Wheel travel calibration and gearing |
| Arc is too tight or too wide | Track model, tyre scrub and speed ratio |
| One side ends much earlier | Cartridges, load, speed response and side progress |
| Encoder targets pass but path is wrong | Slip, scrub and starting orientation |
| Timeout occurs | Requested path, low inside speed and drivetrain load |
Do not change physical track arbitrarily to hide every error. First verify straight-line calibration, mechanics and comparable wheel speeds. Record any effective track calibration with the conditions under which it works.
CHALLENGES | DESIGN AND VERIFY AN ARC
- Calculate: find the heading change for R = 500 mm and centre path s = 500 mm.
- Find both paths: use T = 300 mm to calculate inside and outside travel.
- Set speeds: calculate inside command for a 20% outside command.
- Swap direction: explain how the assignments change for a left arc.
- Predict: calculate centre path length for a 90° turn at R = 500 mm.
- Test: run the short supplied arc three times and document path and final heading.
- Evaluate: explain why encoder success does not prove radius accuracy.
- Extend carefully: propose how a future controller could keep inside and outside progress synchronised.
Check the calculations
θ = 1 radian ≈ 57.30°. Inside travel is 350 mm and outside travel is 650 mm. Inside command is 20 × 350 ÷ 650 ≈ 10.77%. A 90° centre path at R = 500 mm is about 785.40 mm.
OFFICIAL REFERENCES
The arc geometry and helper are team-written logic. This is an independent learning resource; VEX and VEXcode are trademarks of their respective owners.