VEX V5 PYTHON | 05.5 MECHANISM POSITION
Guide objective
Establish a physical reference, move a mechanism to repeatable targets and apply the same travel guards to driver and autonomous requests.
01 | CONTROL THE MECHANISM, NOT JUST THE MOTOR
A timed lift command can finish at different heights when load, friction or battery conditions change. A position controller compares a target with measured mechanism movement and adjusts output.
Mount an external Rotation Sensor on a shaft whose motion has a known relationship to the lift or arm. A slipping coupling, loose chain or changing transmission can break that relationship.
Position control needs a trustworthy zero and a measured working range. Software limits based on an unknown reference cannot tell you where the mechanism’s physical stops are.
02 | ESTABLISH ZERO BEFORE NORMAL MOVEMENT
Use the 04.5 homing method: approach a physical lower reference slowly, stop, confirm the switch and assign sensor position zero.
If the lower switch starts pressed, the example first backs off and confirms release. It then seeks the reference again. Homing has a timeout and a bound on relative sensor travel.
The switch trigger defines zero. It need not be the mechanism’s hard-bottom position. Measure the rest of the travel from the same reference.
Resetting the sensor value without confirming a physical reference changes a number, not the mechanism’s location. The code blocks ordinary targets until homing succeeds.
03 | MEASURE TRAVEL AND LEAVE MARGINS
| Setting | Example | Purpose |
|---|---|---|
| Measured sensor span | 6.0 turns | Travel measured from switch zero |
| Lower soft limit | 0.15 turns | Keep ordinary targets above the lower reference |
| Upper soft limit | 5.85 turns | Leave margin below the upper boundary |
| Target tolerance | ±0.05 turns | Position acceptance window |
| Output cap | 15% | Conservative test request limit |
Mount physical switches so they trigger before damaging hard stops. Leave enough clearance for braking, sensor error, load and flex.
Software margins are not a substitute for mechanical design. A switch can disconnect and look unpressed; a sensor can report plausible but incorrect values. The starter cannot detect every failure.
04 | TARGETS IN TURNS, DEGREES OR PERCENT
The example controls multi-turn sensor position. For the six-turn span, 30% means 1.8 sensor turns and 70% means 4.2 turns.
| Target | Calculation | Result |
|---|---|---|
| 30% of sensor span | 0.30 × 6.0 | 1.8 turns |
| 70% of sensor span | 0.70 × 6.0 | 4.2 turns |
| 0% | 0 × 6.0 | Rejected: below ordinary soft limit |
| 100% | 1 × 6.0 | Rejected: above ordinary soft limit |
A percentage of shaft rotation is not automatically a percentage of lift height. A variable spool radius, linkage or cascade can create a different relationship. Calibrate physical heights or use a lookup table where needed.
For an arm, define a verified sensor-turns-per-arm-turn ratio. Arm angle change = sensor turn change × 360 / ratio. An example ratio of 5 means 0.5 sensor turns corresponds to 36 degrees of arm movement.
Use position() for this multi-turn mapping. A single-turn angle that wraps at 360 degrees cannot distinguish several full revolutions.
05 | USE A SIGNED POSITION ERROR
Error = target sensor position - measured sensor position. Positive output must increase the measured travel; negative output must decrease it.
| Target | Position | Error | Requested output at Kp = 20 percent/turn |
|---|---|---|---|
| 1.8 turns | 1.0 turns | +0.8 turns | +16%, capped to +15% |
| 1.8 turns | 1.7 turns | +0.1 turns | +2% |
| 1.8 turns | 2.0 turns | -0.2 turns | -4% |
Verify direction at low output with a supported mechanism before enabling normal movement. If positive commands reduce the sensor reading, error grows instead of shrinking.
The example uses motor velocity percentage, not a direct torque command. Small requests may not overcome friction or a gravity load.
06 | EVERY NORMAL REQUEST USES THE SAME GUARD
Manual L1/L2 movement and automatic presets both call set_mechanism_output. It reads position and switch state, applies the guard and sends the permitted command.
| Condition | Into-boundary request | Away-from-boundary request |
|---|---|---|
| Lower switch pressed or lower soft limit reached | Down blocked | Up allowed if reference and other checks are valid |
| Upper switch pressed or upper soft limit reached | Up blocked | Down allowed if reference and other checks are valid |
| Both switches pressed | Blocked | Blocked; fault |
| Unhomed or faulted | Blocked | Blocked until deliberate recovery |
Homing is a separate bounded procedure because it intentionally travels outside the normal lower working range. Do not bypass the normal guard for ordinary presets.
Failed preset movement invalidates the reference conservatively in this teaching project. Investigate and deliberately re-home; do not keep retrying a jam.
07 | REACHING AND HOLDING ARE DIFFERENT
The example moves toward a target, stops inside tolerance and checks that position rate remains low for 200 ms. This is a move-and-stop policy.
BRAKE resists movement but does not actively maintain an externally measured lift height. HOLD uses the motor’s internal encoder to resist motion at its stopped motor position. Neither provides a guaranteed mechanical lock or dependable support when disabled.
For a gravity-loaded lift or arm, use suitable gearing, counterbalance and support. A stopped mechanism may back-drive under load. Choose and test stopping behaviour before using elevated targets.
An active external-sensor hold loop needs continued feedback, limits and fault handling. An arm may also need angle-dependent gravity feedforward. Do not add an arbitrary upward bias to this starter and assume all limits still work.
08 | SINGLE-MECHANISM STARTER PROJECT
This complete example uses one motor on Smart Port 9, a Rotation Sensor on 19 and lower/upper switches on Brain 3-Wire A/B. It contains no drivetrain commands.
Paste below # Begin project code, with generated VEX imports and Brain setup above it. Avoid duplicate device declarations. MOTION_CONFIGURED and RUN_AUTO_TARGET begin false.
Driver X requests homing; A and Y request 30% and 70% presets; L1/L2 request manual up/down; B cancels. All normal requests share the output guard.
# =============================================================================
# TEAM [MY TEAM NAME / NUMBER] | LIFT / ARM POSITION 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
# - Driver A: 30% target; Y: 70% target (rising edges)
# - 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.
import math
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
RATE_TOLERANCE_TURNS_PER_SEC = 0.10
SETTLE_MS = 200
MAX_LOOP_GAP_S = 0.10
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)
except Exception:
homed = False
fault = "HOMING_READ_OR_COMMAND_FAULT"
return fault
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 fail_move(reason):
global homed, fault
fault = reason
homed = False
stop_mechanism()
return reason
def move_to_turns(target, mode="AUTO"):
if (not isinstance(target, (int, float)) or not math.isfinite(target)
or not SOFT_MIN_TURNS <= target <= SOFT_MAX_TURNS):
stop_mechanism()
return "TARGET_OUTSIDE_WORKING_RANGE"
if not homed or fault is not None or not MOTION_CONFIGURED:
stop_mechanism()
return "NOT_HOMED_OR_FAULTED"
started = brain.timer.time(MSEC)
previous_time = started
previous_position = None
settled_since = None
try:
while True:
reason = abort_reason(mode, started, MOVE_TIMEOUT_MS)
if reason is not None:
return fail_move(reason)
if not homed or fault is not None:
return "NOT_HOMED_OR_FAULTED"
now = brain.timer.time(MSEC)
dt = (now - previous_time) / 1000.0
if dt <= 0:
wait(POLL_MS, MSEC)
continue
if dt > MAX_LOOP_GAP_S:
return fail_move("LOOP_TIMING_FAULT")
previous_time = now
position = mechanism_rotation.position(TURNS)
if not math.isfinite(position):
return fail_move("INVALID_POSITION")
if not -0.1 <= position <= FULL_TRAVEL_TURNS + 0.1:
return fail_move("POSITION_FAULT")
error = target - position
slow = (previous_position is not None
and abs((position - previous_position) / dt)
<= RATE_TOLERANCE_TURNS_PER_SEC)
previous_position = position
if abs(error) <= TARGET_TOLERANCE_TURNS:
# Move-and-stop policy: brake, then observe settling.
set_mechanism_output(0)
if fault is not None:
return fail_move(fault)
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
requested = error * POSITION_KP_PERCENT_PER_TURN
if set_mechanism_output(requested) == 0:
return fail_move("LIMIT_OR_OUTPUT_BLOCKED")
wait(POLL_MS, MSEC)
except Exception:
return fail_move("READ_OR_COMMAND_FAULT")
finally:
stop_mechanism()
def move_to_percent(target_percent, mode="AUTO"):
if (not isinstance(target_percent, (int, float))
or not math.isfinite(target_percent)
or not 0 <= target_percent <= 100):
stop_mechanism()
return "INVALID_PERCENTAGE"
# Percentage of measured sensor span, not automatically physical height.
target = target_percent / 100.0 * FULL_TRAVEL_TURNS
return move_to_turns(target, mode)
# =============================================================================
# AUTONOMOUS AND DRIVER: ONE OUTPUT OWNER AT A TIME
# =============================================================================
def autonomous():
global homed
try:
result = home_mechanism("AUTO")
status(result)
if result != "HOMED" or not RUN_AUTO_TARGET:
return
for target_percent in (30, 70, 30):
result = move_to_percent(target_percent, "AUTO")
status(result)
if result != "SETTLED":
break
wait(300, MSEC)
finally:
stop_mechanism()
homed = False # Re-home after mode change/manual repositioning.
def driver_control():
global homed, fault
previous_x = controller.buttonX.pressing()
previous_a = controller.buttonA.pressing()
previous_y = controller.buttonY.pressing()
try:
while mode_active("DRIVER"):
x = controller.buttonX.pressing()
a = controller.buttonA.pressing()
y = controller.buttonY.pressing()
if x and not previous_x:
status(home_mechanism("DRIVER"))
elif a and not previous_a:
status(move_to_percent(30, "DRIVER"))
elif y and not previous_y:
status(move_to_percent(70, "DRIVER"))
# Refresh after any blocking preset/home action.
previous_x = controller.buttonX.pressing()
previous_a = controller.buttonA.pressing()
previous_y = controller.buttonY.pressing()
up = controller.buttonL1.pressing()
down = controller.buttonL2.pressing()
requested = MAX_OUTPUT_PERCENT * (int(up) - int(down))
set_mechanism_output(requested)
wait(POLL_MS, MSEC)
except Exception:
fault = "DRIVER_READ_OR_COMMAND_FAULT"
finally:
stop_mechanism()
homed = False
stop_mechanism()
status("Unhomed: configure and test before motion")
competition = Competition(driver_control, autonomous)
while True:
wait(100, MSEC)
Autonomous homes first. With RUN_AUTO_TARGET enabled after testing, it requests 30%, 70% and 30%, stopping the sequence after failure.
Driver presets block the driver mechanism loop until completion or cancellation. B, enable state and mode are still polled during the move. For a full competition robot, a blocking mechanism preset must not freeze your drivetrain controls; use a single-owner state-machine design or carefully separated subsystem tasks.
This project invalidates homing on leaving a mode. If the mechanism moves while disabled, request a fresh home before driver targets. Do not silently preserve a reference after manual repositioning.
09 | TEST BEFORE RAISING A LOAD
- Support the mechanism and verify sensor direction and switch readings without normal motion.
- Test controlled homing, including a switch that starts pressed.
- Measure the actual sensor span and choose conservative soft margins.
- Test small unloaded targets within the working range.
- Check cancellation and fault branches in simulation.
- Add a representative supported load and check settling and stopping behaviour.
- Repeat each preset three times and measure the actual physical position.
Do not deliberately drive into a hard stop to test the software. Use simulated boundary values for fault tests and controlled low-output physical tests with suitable support.
10 | TUNE AND HANDLE FAILURE
| Behaviour | Inspect first |
|---|---|
| Moves away from target | Motor/sensor sign relationship |
| Stops short or times out | Friction, gravity load, small P output or blocked guard |
| Oscillates near target | Gain, backlash and stopping behaviour |
| Sensor settled but height differs | Transmission slip or nonlinear height mapping |
| Drifts after completion | Back-driving and holding/support design |
| Cannot move after a failed preset | Fault/reference invalidation; inspect before re-homing |
| Repeated limit stop | Target mapping, margins and switch placement |
Ordinary returns and exceptions stop through finally. Forced task termination can bypass ordinary cleanup; test competition transitions and keep one motor-output owner.
The starter has no dedicated motor-current stall detector. Timeouts and sensor travel bounds reduce some risks but do not replace mechanical protection.
CHALLENGES | POSITION A MECHANISM WITHIN ITS RANGE
- Map: label motor, sensor, switches, physical stops and soft limits.
- Measure: document zero and the sensor-to-height or sensor-to-angle relationship.
- Calculate: find 30% and 70% targets for your measured span.
- Guard: simulate into-limit and away-from-limit requests.
- Test: compare three preset trials at two positions.
- Cancel: verify an aborted move does not continue the autonomous sequence.
- Evaluate: distinguish position arrival, braking, motor HOLD and mechanical support.
- Document: write the recovery policy for a lost reference or fault.
Success evidence
A labelled setup, measured calibration, saved project and repeated-trial log. Explain how manual and automatic output share the guard, and how the mechanism remains supported after stopping or disabling.
OFFICIAL REFERENCES
The controller, limit policies and challenges are independently written teaching examples. VEX and VEXcode are trademarks of their respective owners.