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.

This guide focuses on perpendicular TWO and parallel-plus-sideways THREE
ArrangementDirect observationsMissing information
Forward + sideways + inertialTwo translation components and angle changeStill depends on contact and heading accuracy
Two parallel forward trackersForward travel and differential angle changeNo direct sideways measurement
Two parallel + sideways trackerTwo translation components and wheel-derived angle changeNo 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

Two and three tracking-wheel layoutsTwo-wheel layout has one forward tracker on the left and one sideways tracker behind the reference, plus inertial heading. Three-wheel layout adds a parallel forward tracker on the right to derive turning from wheel differences.TWO + INERTIALTHREE TRACKERSOOF / 17L / 17R / 18SIDE / 19SIDE / 19FORWARD ↑FORWARD ↑Heading from inertial on port 20Heading from separated L/R lines
Illustrative top views, not to scale. Pink wheels show their rolling directions; O is the selected reference point.

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.

Offsets are signed distances from the same reference O
ParameterMeaningExample
xL / xFLeft/right offset of left or forward tracking line-120 mm
xRLeft/right offset of right tracking line+120 mm
ySForward/back offset of sideways tracking line-100 mm
B = xR - xLSeparation of the parallel lines240 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

Offsets make wheel travel non-zero while O stays still
Clockwise 90-degree turn about OExpected 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 angleApproximately +1.571 rad
Corrected forward and sidewaysApproximately 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

More hardware does not automatically guarantee better accuracy
Two perpendicular + inertialThree trackers
Fewer wheel mounts and sensorsExtra parallel wheel and contact point
Heading relies on inertial measurementHeading relies on calibrated differential wheel travel
Observes sideways travelObserves sideways travel
Needs measured offsetsNeeds measured offsets plus parallel separation
Useful when packaging is tightUseful 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

  1. Check every sensor sign and scale independently.
  2. Test straight forward/reverse movement with small corrected sideways travel.
  3. Check the sideways wheel’s measured direction with a suitable fixture.
  4. Perform controlled turns in both directions and compare predicted wheel travel.
  5. For THREE, test wheel-derived rotation against an independent physical angle or inertial reading.
  6. 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

  1. Compare: explain why two parallel trackers differ from two perpendicular trackers.
  2. Draw: label ports, measurement directions, O and signed offsets.
  3. Derive: show how subtracting L and R removes forward translation.
  4. Calculate: solve the asymmetric example without using a plain average.
  5. Test: simulate clockwise and anticlockwise pure turns.
  6. Measure: compare wheel-derived and inertial angle changes across three trials.
  7. Choose: justify an arrangement using your robot’s packaging and contact behaviour.
  8. 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.