VEX V5 PYTHON | 06.4 MULTI-WHEEL ARRANGEMENTS
Guide objective
Compare two perpendicular trackers with inertial heading and three-wheel geometry, then verify signed local movement before field-pose integration.
01 | TWO WHEELS CAN MEAN DIFFERENT THINGS
A two-wheel arrangement is not defined simply by the number of sensors. You must know which directions they measure and where heading comes from.
| Arrangement | Direct observations | Missing information |
|---|---|---|
| Forward + sideways + inertial | Two translation components and angle change | Still depends on contact and heading accuracy |
| Two parallel forward trackers | Forward travel and differential angle change | No direct sideways measurement |
| Two parallel + sideways tracker | Two translation components and wheel-derived angle change | No independent absolute field position |
The perpendicular two-wheel layout adds the lateral measurement missing in 06.3. The third wheel in the standard three-wheel layout makes heading observable from the separated parallel tracking lines.
02 | COMPARE THE PHYSICAL LAYOUTS
Use independent, unpowered tracking wheels with reliable contact, calibrated scales and low-friction mounts. A sideways wheel needs its rollers or mounting to accommodate forward movement without excessive scrub.
The diagnostic uses port 19 for the sideways tracker in both layouts. This differs from the port-18 sideways example in 06.1. Follow the declaration for the project you actually load.
For THREE, port 18 becomes the right parallel tracker. Check the wiring map before downloading.
03 | SHARE ONE COORDINATE CONVENTION
As in previous guides, body +X is right, +Y is forward and positive angle change is clockwise. Distances are millimetres; angles used in geometry are radians.
| Parameter | Meaning | Example |
|---|---|---|
| xL / xF | Left/right offset of left or forward tracking line | -120 mm |
| xR | Left/right offset of right tracking line | +120 mm |
| yS | Forward/back offset of sideways tracking line | -100 mm |
| B = xR - xL | Separation of the parallel lines | 240 mm |
Parallel-wheel separation is not automatically drive track width. Fore/aft placement of a parallel wheel does not alter its ideal forward rotational term; perpendicular offset does.
04 | TWO PERPENDICULAR TRACKERS PLUS INERTIAL
Read forward wheel travel F, sideways wheel travel S and inertial angle change θ for the same short interval. Remove the offset components:
forward_at_reference = F + xF * theta sideways_at_reference = S - yS * theta
At zero angle change, both corrections vanish. The sideways tracker can now observe a lateral displacement that the one-wheel model would miss, provided it remains in rolling contact.
Inertial heading remains essential to this layout. Two perpendicular wheel readings alone do not determine all three unknowns: forward translation, sideways translation and rotation.
05 | THREE WHEELS DERIVE THE ANGLE CHANGE
For the two parallel trackers, forward travel at the reference is the same underlying component:
L = forward - xL × θ; R = forward - xR × θ.
Subtracting cancels forward translation:
theta = (L - R) / (xR - xL) forward = L + xL * theta sideways = S - yS * theta
With clockwise-positive rotation, the left tracker reports more positive travel than the right during a clockwise turn. Reversing either sensor sign corrupts both the inferred angle and translation.
Heading change is derived from differential wheel travel, not directly measured by either wheel alone.
06 | SYMMETRIC AND ASYMMETRIC OFFSETS
If xL = -120 mm and xR = +120 mm, the reference lies midway between the parallel lines. Forward travel becomes (L + R) / 2.
That simple average is not generally correct when the reference is elsewhere. With unequal offsets use:
forward = (xR * L - xL * R) / (xR - xL)
Example: xL = -100 mm, xR = +140 mm, forward travel = 300 mm and clockwise θ = 0.2 rad. The wheels report L = 320 mm and R = 272 mm.
The angle is (320 - 272)/240 = 0.2 rad. Corrected forward travel is 320 + (-100 × 0.2) = 300 mm. Their plain average is 296 mm, which describes the midpoint of the wheel lines rather than the selected reference.
07 | CHECK A PURE TURN
| Clockwise 90-degree turn about O | Expected value |
|---|---|
| xL / xR / yS | -120 / +120 / -100 mm |
| Left raw travel | +188.5 mm |
| Right raw travel | -188.5 mm |
| Sideways raw travel | -157.1 mm |
| Wheel-derived angle | Approximately +1.571 rad |
| Corrected forward and sideways | Approximately 0 mm each |
Turn in the opposite direction and every rotational travel sign reverses. Repeat both directions to expose sign, scale and mounting errors.
If your physical turn also translates O, corrected translation can legitimately remain non-zero. Mark and observe the selected point instead of assuming every commanded pivot holds it stationary.
08 | REUSABLE GEOMETRY FUNCTIONS
def two_wheel_delta(forward_mm, side_mm, theta_rad, forward_x, side_y):
values = (forward_mm, side_mm, theta_rad, forward_x, side_y)
if not all(math.isfinite(v) for v in values):
raise ValueError("Invalid two-wheel data")
forward = forward_mm + forward_x * theta_rad
sideways = side_mm - side_y * theta_rad
return forward, sideways, theta_rad
def three_wheel_delta(left_mm, right_mm, side_mm, left_x, right_x, side_y):
values = (left_mm, right_mm, side_mm, left_x, right_x, side_y)
if not all(math.isfinite(v) for v in values):
raise ValueError("Invalid three-wheel data")
separation = right_x - left_x
if separation <= 0:
raise ValueError("Right tracking line must be right of left tracking line")
theta = (left_mm - right_mm) / separation
# Weighted geometry works even when the reference is not midway.
forward = (right_x * left_mm - left_x * right_mm) / separation
sideways = side_mm - side_y * theta
return forward, sideways, theta
The functions consume calibrated wheel travel, not raw sensor degrees. Convert signed sensor deltas once with the combined scale from 06.2.
The returned values represent local signed rolling components after rotational correction and an angle increment. They are inputs to the arc and field-pose integration in 06.5, not complete field X/Y coordinates.
09 | SENSOR-ONLY TWO / THREE DIAGNOSTIC
Choose LAYOUT, enter independently calibrated scales and signed offsets, then verify sensor directions before setting CONFIGURED true. This project commands no motors.
Paste below # Begin project code with generated VEX imports and Brain setup above it. Avoid duplicate device declarations.
# =============================================================================
# TEAM [MY TEAM NAME / NUMBER] | MULTI-WHEEL GEOMETRY DIAGNOSTIC
# =============================================================================
# Keep generated VEX imports and brain above '# Begin project code'.
# No motors. Avoid duplicate device declarations in Devices.
# TWO: left/forward tracker port 17, sideways tracker port 19, inertial 20.
# THREE: left tracker 17, right tracker 18, sideways tracker 19.
# THREE can optionally compare its heading change against inertial 20.
# Positive tracker readings: forward for parallel wheels, right for side wheel.
import math
LAYOUT = "TWO" # "TWO" or "THREE"
CONFIGURED = False
COMPARE_INERTIAL_IN_THREE = False
LEFT_MM_PER_SENSOR_TURN = 219.44 # Calibrate each combined scale independently
RIGHT_MM_PER_SENSOR_TURN = 219.44
SIDE_MM_PER_SENSOR_TURN = 219.44
LEFT_X_MM = -120.0
RIGHT_X_MM = 120.0
SIDE_Y_MM = -100.0
POLL_MS = 20
PRINT_MS = 200
MAX_GAP_MS = 100
left_tracker = Rotation(Ports.PORT17, False)
side_tracker = Rotation(Ports.PORT19, False)
if LAYOUT == "THREE":
right_tracker = Rotation(Ports.PORT18, False)
use_inertial = LAYOUT == "TWO" or COMPARE_INERTIAL_IN_THREE
if use_inertial:
inertial = Inertial(Ports.PORT20)
def two_wheel_delta(forward_mm, side_mm, theta_rad, forward_x, side_y):
values = (forward_mm, side_mm, theta_rad, forward_x, side_y)
if not all(math.isfinite(v) for v in values):
raise ValueError("Invalid two-wheel data")
forward = forward_mm + forward_x * theta_rad
sideways = side_mm - side_y * theta_rad
return forward, sideways, theta_rad
def three_wheel_delta(left_mm, right_mm, side_mm, left_x, right_x, side_y):
values = (left_mm, right_mm, side_mm, left_x, right_x, side_y)
if not all(math.isfinite(v) for v in values):
raise ValueError("Invalid three-wheel data")
separation = right_x - left_x
if separation <= 0:
raise ValueError("Right tracking line must be right of left tracking line")
theta = (left_mm - right_mm) / separation
# Weighted geometry works even when the reference is not midway.
forward = (right_x * left_mm - left_x * right_mm) / separation
sideways = side_mm - side_y * theta
return forward, sideways, theta
# =============================================================================
# SENSOR SNAPSHOT AND DELTA DIAGNOSTIC
# =============================================================================
def snapshot():
left = left_tracker.position(TURNS)
side = side_tracker.position(TURNS)
right = right_tracker.position(TURNS) if LAYOUT == "THREE" else 0.0
if use_inertial:
if not inertial.installed() or inertial.is_calibrating():
raise ValueError("Inertial unavailable")
rotation = inertial.rotation(DEGREES)
else:
rotation = 0.0
if not all(math.isfinite(v) for v in (left, right, side, rotation)):
raise ValueError("Invalid sensor reading")
return left, right, side, rotation
if LAYOUT not in ("TWO", "THREE"):
raise ValueError("Choose TWO or THREE")
if not CONFIGURED:
print("Measure scales, offsets and signs before enabling diagnostic.")
else:
scales = (LEFT_MM_PER_SENSOR_TURN, RIGHT_MM_PER_SENSOR_TURN,
SIDE_MM_PER_SENSOR_TURN)
if not all(math.isfinite(v) and v > 0 for v in scales):
raise ValueError("Scales must be positive and finite")
if LAYOUT == "THREE" and RIGHT_X_MM <= LEFT_X_MM:
raise ValueError("Invalid tracking-line separation")
if use_inertial:
# Keep stationary; this setup call may block.
inertial.calibrate()
previous = snapshot()
previous_ms = last_print = brain.timer.time(MSEC)
total_forward = total_side = total_theta = 0.0
total_inertial = 0.0
print("Local signed totals only: NOT field X/Y.")
while True:
now = brain.timer.time(MSEC)
if now - previous_ms > MAX_GAP_MS:
print("Timing fault: stop and reinitialize.")
break
previous_ms = now
try:
current = snapshot()
dl = (current[0] - previous[0]) * LEFT_MM_PER_SENSOR_TURN
dr = (current[1] - previous[1]) * RIGHT_MM_PER_SENSOR_TURN
ds = (current[2] - previous[2]) * SIDE_MM_PER_SENSOR_TURN
inertial_theta = math.radians(current[3] - previous[3])
if LAYOUT == "TWO":
forward, sideways, theta = two_wheel_delta(
dl, ds, inertial_theta, LEFT_X_MM, SIDE_Y_MM)
else:
forward, sideways, theta = three_wheel_delta(
dl, dr, ds, LEFT_X_MM, RIGHT_X_MM, SIDE_Y_MM)
total_forward += forward
total_side += sideways
total_theta += theta
total_inertial += inertial_theta
previous = current
if now - last_print >= PRINT_MS:
print("Local F %.1f mm, S %.1f mm, turn %.1f deg" %
(total_forward, total_side, math.degrees(total_theta)))
if LAYOUT == "THREE" and use_inertial:
print("Wheel minus inertial turn deg:",
math.degrees(total_theta - total_inertial))
last_print = now
except Exception:
print("Feedback/geometry fault: diagnostic invalid; stop.")
break
wait(POLL_MS, MSEC)
In TWO mode the inertial sensor supplies angle change. In THREE mode wheel separation supplies it; optional inertial comparison reports disagreement without blending the measurements.
The printed totals sum body-frame signed rolling components over the run. Because the body axes rotate, these totals are not a vector in the field frame or a final field position.
The snapshot uses separate API reads. They are not atomic. Move slowly during diagnostic tests and review sampling/timing before production odometry. A feedback or timing fault invalidates the run.
10 | WHAT AN INERTIAL COMPARISON CAN TELL YOU
With three wheels, an inertial sensor can provide an independent turn estimate. Compare accumulated wheel-derived and inertial angle changes over controlled turns.
A systematic disagreement suggests checking wheel scales, line separation and sensor signs. Variable disagreement suggests contact loss, scrub, slip or inertial/mounting issues.
Do not simply average the angles and assume the problem is solved. A fusion policy needs a common frame, justified trust assumptions and deliberate handling of conflicting measurements.
11 | CHOOSE A LAYOUT FOR YOUR ROBOT
| Two perpendicular + inertial | Three trackers |
|---|---|
| Fewer wheel mounts and sensors | Extra parallel wheel and contact point |
| Heading relies on inertial measurement | Heading relies on calibrated differential wheel travel |
| Observes sideways travel | Observes sideways travel |
| Needs measured offsets | Needs measured offsets plus parallel separation |
| Useful when packaging is tight | Useful when independent wheel-derived turning is wanted |
Choose based on packaging, contact reliability, motion and the faults you need to detect. Two excellent mounts may outperform three poorly mounted wheels.
Both layouts drift through accumulated measurement error. Neither directly supplies an absolute field reference or repairs contact loss.
12 | VALIDATE BEFORE INTEGRATING POSE
- Check every sensor sign and scale independently.
- Test straight forward/reverse movement with small corrected sideways travel.
- Check the sideways wheel’s measured direction with a suitable fixture.
- Perform controlled turns in both directions and compare predicted wheel travel.
- For THREE, test wheel-derived rotation against an independent physical angle or inertial reading.
- Repeat with normal load and mechanisms in different positions.
Do not force a tank-drive robot sideways against its tyres. Keep simulated tests separate from physical measurements.
Mount misalignment mixes forward and lateral readings. A diameter adjustment cannot fix an incorrectly oriented tracking wheel.
CHALLENGES | COMPARE, DERIVE AND VERIFY
- Compare: explain why two parallel trackers differ from two perpendicular trackers.
- Draw: label ports, measurement directions, O and signed offsets.
- Derive: show how subtracting L and R removes forward translation.
- Calculate: solve the asymmetric example without using a plain average.
- Test: simulate clockwise and anticlockwise pure turns.
- Measure: compare wheel-derived and inertial angle changes across three trials.
- Choose: justify an arrangement using your robot’s packaging and contact behaviour.
- Distinguish: explain why local totals are not field X/Y.
Success evidence
A labelled arrangement, calibrated scales, signed calculations and a diagnostic log. Record residuals and uncertainty rather than treating a plausible result as proof of accuracy.
OFFICIAL AND TECHNICAL REFERENCES
Coordinate conventions vary between sources. The diagram, diagnostic and worked examples are independently written learning material. VEX and VEXcode are trademarks of their respective owners.