🤖 Getting Started · Beginner · Spartan Design

Flex Training Platform

The 2026–27 Override Hero Bot is the team's second training robot — what the Clawbot was for fundamentals, Flex is for game-relevant practice. Build it from the kit, program it with EZ-Template, and add sensors one at a time on the path to competition.

// Section 01
Why the Flex is Your Second Training Robot 🤖
The Clawbot taught you drive, arm position, and the edit-upload-test cycle. The Flex picks up where the Clawbot stops — real game pieces, real scoring mechanisms, real autonomous decisions.
Short answer: yes, EZ-Template works on the Flex. Stock Flex doesn't ship with an inertial sensor or with PROS-friendly defaults, so the team has added an IMU on port 13 and replaced the stock mecanum wheels with 4" omni wheels for a cleaner tank-drive setup. With those two changes, Flex becomes a full EZ-Template robot.
📝
Mentor's handout available. A print-ready 15-page companion to this curriculum is at flex-mentor-handout.pdf — port map, controller mapping, section walkthroughs with key code, common student traps, and a tear-off quick reference card. Tape it inside the toolbox lid.

From Clawbot Graduation to Flex Onboarding

The Clawbot's value is its simplicity — one drive, one arm, one claw, no game-specific mess. Flex's value is the opposite: it's the actual game robot, with the same scoring mechanisms a competition robot will have. Once a student has finished the Clawbot curriculum, the Flex is the next step before they touch the team's competition robot.

How Flex Trains the Whole Team

Same four-track structure as the Clawbot — one robot, four parallel training tracks. Each track produces a notebook artifact. Tap a track to see what's involved.

Track 1
🔧Build Training
Do: Follow the official VEX Flex build instructions from start to finish. Note where the team's build deviates from stock (omni wheels, added IMU).
Learn: Chain runs, 4-bar linkages, scissor mechanisms — the structural patterns competition robots actually use.
Produce: A working Flex + one build-log notebook entry with photos of each major subsystem.
Track 2
🏎Driver Training
Do: Practice scoring blocks in the actual Override field. Pair drives (one driver, one coach). Run timed scoring drills.
Learn: Block-handling timing, lift-then-drive vs. drive-then-lift trade-offs, how the chassis handles when fully loaded.
Produce: Timed scoring data over 5 runs + 3 observations about what makes a scoring cycle fast vs. slow.
Track 3
💻Programming Training
Do: Set up EZ-Template (Section 3 below). Write driver control for lift + claw. Then a first auton that scores one block.
Learn: Multi-subsystem coordination, current-limit-based stall protection, the difference between encoder-based and sensor-based control.
Produce: One committed code change with before/after auton timing.
Track 4
📐Design + Notebook
Do: Document the wheel choice (omni vs. stock mecanum). Compare in a decision matrix. Note the trade-off in the engineering notebook.
Learn: How a single mechanical choice cascades into different code paths. CAD comparison of mecanum vs. omni rollers.
Produce: A decision-matrix slide + a 3-sentence notebook entry on what your team chose and why.

What's Different Between Clawbot and Flex

AspectClawbotFlexWhy It Matters
Game-relevanceNone (synthetic)Override game robotPractice transfers to real matches
Drive motors2 (red cartridge)2 (green cartridge)Flex is faster; PID retune required
Wheels4" standard4" omni (modified)Omni allows pivot turns; no scrub
MechanismsArm + clawLift + clawSame code patterns, real geometry
Sensors day-oneNone (add IMU)IMU only (added)Both need pots/limits/optical later
Notebook valueBuild log + drillsFull EDP comparison opportunityFlex is a real "iteration target"
📝
RECF EN4 reminder. Building Flex from the kit is reference work — that's fine for the notebook (it's the OEM build). The CODE you write on it must be student-written. Use this guide as reference: rewrite all snippets in your own words, your own commit messages, your own decisions.
// Section 02
The Sensors Already On Your Flex 🧭
Two sources of sensor data come "free" with the robot before you add anything: the IMU your team installed on port 13, and the encoders inside every smart motor. Both are useful, both have limits, and knowing what each gives you saves you from adding sensors you don't actually need.

The IMU on Port 13

The V5 Inertial Sensor (IMU) is what makes chassis.pid_turn_set(90, 110) actually turn 90 degrees instead of guessing from wheel rotations. On Flex with omni wheels, this matters even more than on Clawbot — omnis can slip during pivot turns, and wheel-encoder-derived heading would be wrong. The IMU reads angular velocity from a vibrating-MEMS gyroscope and integrates it over time to produce heading.

⚠️
The IMU must be calibrated on every boot. EZ-Template handles this for you when you call chassis.initialize() at the start of initialize() — the robot must sit perfectly still for 2-3 seconds while calibration runs. If the robot moves during this window, the heading will be off by exactly that amount for the whole match. Plug the brain in BEFORE positioning the robot on the field.

Motor Encoders — Built-In, Already Working

Every V5 smart motor has an internal encoder. After the chassis spins, you can read total ticks via chassis.drive_sensor_left() and chassis.drive_sensor_right(). EZ-Template uses these internally for distance PID. You can also read the lift and claw motor positions:

snippet — reading motor positions
// Lift position in degrees from boot (resets to 0 each power-on) double lift_deg = lift_motor.get_position(); // Claw position the same way double claw_deg = claw_motor.get_position(); // Reset position to 0 (useful as a calibration "tare") lift_motor.tare_position();

Verify the IMU Before Writing Any Code

Before the first time you use the IMU in code, confirm it's actually working with a 30-second test:

  1. Put the robot on a flat surface. Plug in. Wait for the brain to fully boot.
  2. Open the PROS terminal: pros terminal from your project folder.
  3. Run this minimal test program (or use the EZ-Template default code, which prints heading to the screen):
main.cpp — minimal IMU sanity check
#include "main.h" #include "pros/imu.hpp" pros::IMU imu(13); void initialize() { imu.reset(); // blocks ~2 seconds while calibrating } void opcontrol() { while (true) { printf("Heading: %.1f deg\n", imu.get_heading()); pros::delay(100); } }

Pick the robot up, rotate it 90° clockwise by hand. The terminal should show heading change from ~0 to ~90. Rotate another 90° — should read ~180. If the number moves at all but the magnitude is off, the IMU mounting orientation is wrong (it should be flat with the chip parallel to the ground). If the number doesn't change, check the port.

💡
Heading wraps at 360. The IMU returns 0-360, then 0 again. Code that assumes "if heading > 100" will silently break when the robot passes through 0. EZ-Template handles this internally for turn PID — you don't have to think about it. But if you write your own heading-based logic, use chassis.imu_get_rotation() instead, which returns a continuous rotation count (-720, -90, 45, 270, 540...).

What You're NOT Getting (Yet)

From a stock Flex with just the IMU and motor encoders, you can do all of this:

You cannot do any of this until you add the sensors in Section 6:

That gap — what the bare robot can do vs. what a sensored robot can do — is what makes Flex such a good training platform. Every sensor adds capability you can see and measure.

// Section 03
EZ-Template Flex Configuration 🖥️
Start from the stock EZ-Template starter project. Configure the chassis for two-motor tank with 4" omni wheels and the green cartridge. Wire up the lift and claw motors. Verify with one of the built-in auton routines before you write any of your own code.

The Port Map

Your team has settled on this port assignment for Flex. Use the same in code, on the wiring diagram, and on the engineering notebook page. Mismatches between any of those three are how an entire afternoon gets eaten by a bug that turns out to be a swapped cable.

PortDevicePurposePolarity
6Smart MotorLeft drivetrainReversed
4Smart MotorRight drivetrainForward
13Inertial SensorHeading + tilt
15Smart MotorClawVerify on bench
17Smart MotorLiftVerify on bench
💡
"Verify on bench" means: push a positive voltage to the motor with the robot up on a block. If the lift moves DOWN when you say up, flip the polarity (use a negative port number in the constructor). Same for the claw — if pressing "close" opens it, flip the polarity. This is faster than guessing.

Step 1: Start From the EZ-Template Starter Project

Download EZ-Template 3.2.2 from github.com/EZ-Robotics/EZ-Template if your team doesn't have a clean copy. Unzip the Example Project. Open the folder in VS Code (or wherever your team writes PROS code).

The starter project has working defaults — you can build and upload before changing anything. Verify the upload works first (with the robot off the ground): pros mu in the terminal. The brain screen should show the EZ-Template logo and the team number it was last flashed with.

Step 2: Configure the Drive in subsystems.hpp

Open include/subsystems.hpp. This is where you wire ports to objects. Replace whatever's there with the Flex version:

include/subsystems.hpp
#pragma once #include "EZ-Template/api.hpp" #include "api.h" // ─── DRIVE ────────────────────────────────────────────────────────── // Two 11W motors with green (200 RPM) cartridges // 4" omni wheels, direct drive (1:1) // IMU on port 13 inline ez::Drive chassis( {-6}, // Left motor: port 6, reversed (negative) {4}, // Right motor: port 4, forward 13, // IMU port 4.0, // Wheel diameter: 4 inches 200, // Cartridge RPM: 200 = green 1.0 // External gear ratio: 1:1 (direct drive) ); // ─── MECHANISM MOTORS ─────────────────────────────────────────────── inline pros::Motor lift_motor(17); inline pros::Motor claw_motor(15);
💭
Why is the left motor port negative? The two drive motors are mounted facing inward, on opposite sides of the chassis. When the LEFT motor turns its shaft "forward" (from its own perspective), the wheel actually moves the robot backward. The negative port tells EZ-Template to flip the sign for that motor. If your build has both motors facing the same way (rare), neither needs reversing. Verify with the bench test from the tip above.

Step 3: Set the Default Auton in main.cpp

EZ-Template's initialize() function sets PID constants and prepares the chassis. Match yours to Flex's drivetrain values:

src/main.cpp — initialize() partial
void initialize() { pros::delay(500); // stop the user from doing anything until inits run chassis.opcontrol_curve_buttons_toggle(true); chassis.opcontrol_drive_activebrake_set(0.0); chassis.opcontrol_curve_default_set(0, 0); chassis.pid_drive_constants_set(20, 0, 100); chassis.pid_turn_constants_set(3, 0.05, 20, 15); chassis.pid_swing_constants_set(6, 0, 65); chassis.pid_turn_exit_condition_set(90_ms, 3_deg, 250_ms, 7_deg, 500_ms, 500_ms); chassis.pid_swing_exit_condition_set(90_ms, 3_deg, 250_ms, 7_deg, 500_ms, 500_ms); chassis.pid_drive_exit_condition_set(90_ms, 1_in, 250_ms, 3_in, 500_ms, 500_ms); chassis.drive_imu_calibrate(); // 2-second IMU calibration chassis.initialize(); // brings up brain screen, sets brake modes ez::as::auton_selector.autons_add({ Auton("Drive Forward 12in", drive_test), Auton("Turn 90 Right", turn_test), }); }
⚠️
Those PID constants are starting points. They're calibrated for a typical green-cartridge 4" omni tank drive but every chassis is slightly different (weight distribution, friction, motor wear). Run the drive_test auton and measure overshoot. If it overshoots, lower kP. If it undershoots, raise kP. See PID Diagnostics for the full tuning procedure.

Step 4: Run a Built-In Test

EZ-Template ships with example auton routines. Match the team's first run to the simplest one — drive forward 12 inches. Save the file, then in the terminal:

terminal
$ pros mu # make and upload $ pros terminal # watch debug output

On the brain, navigate to the auton selector (LCD buttons). Select Drive Forward 12in. Disable the competition switch. The robot should drive forward roughly one foot and stop.

Step 5: Write the Driver Control Stub

The opcontrol function is what runs during the driver portion of the match. For now, just get the chassis driving with the controller — you'll add lift and claw logic in the next section.

src/main.cpp — opcontrol() chassis only
void opcontrol() { pros::delay(500); chassis.drive_brake_set(MOTOR_BRAKE_COAST); while (true) { chassis.opcontrol_tank(); // left stick = left motor, right stick = right motor // TODO: lift + claw controls go here (Section 4) pros::delay(ez::util::DELAY_TIME); // 10ms loop } }

Upload, hit B on the controller (or whichever activates opcontrol on your team's setup). Drive the robot around. Confirm tank steering feels correct — left stick controls left wheel, right stick controls right wheel.

⚙ STEM Highlight Mechanical Engineering: Why Omni Over Mecanum
Mecanum wheels have rollers at 45° to the wheel's rotation axis — this lets a 4-wheel mecanum drive translate sideways without turning, because the diagonal force vectors of each wheel can cancel rotation while summing into lateral motion. The cost: mecanum wheels lose ~30% of forward traction (the lateral component of every roller is "wasted" when driving straight), they're heavier, and they cost more. Omni wheels have rollers at 90° — perpendicular to rotation — which means full forward traction but no lateral capability. The team's swap from mecanum to omni is a trade: we give up strafing for simpler code, better straight-line speed, and lighter mass. The choice is documented in the engineering notebook with a decision matrix.
🎤 Interview line: "We swapped the stock mecanum wheels for omni wheels on Flex. We lost lateral strafing but gained 30% forward traction efficiency, simpler tank-drive code, and lower mass. For our game strategy — where we drive into the goal directly rather than alongside it — the trade favored omni."
// Section 04
Lift + Claw Control 🤰
Two mechanisms, two control patterns. The lift is "hold-to-move" with current-limit stall protection. The claw is "toggle open/close" with similar protection. Once limit switches and the potentiometer are added (Section 6), the lift upgrades to position-based control — until then, time-based is fine.

Pattern 1: Hold-to-Move (the Lift)

Press a button, the motor runs. Release the button, the motor brakes. Simple, predictable, no internal state to debug. This is the right starting pattern for the lift before sensors are added.

src/main.cpp — opcontrol() lift control
// Inside the while(true) loop in opcontrol(): if (master.get_digital(DIGITAL_R1)) { lift_motor.move(110); // up at ~85% power } else if (master.get_digital(DIGITAL_R2)) { lift_motor.move(-110); // down at ~85% power } else { lift_motor.brake(); // hold position when released }

The brake() call when no button is pressed is what stops the lift from sagging under its own weight (or gravity pulling a held game piece down). It's the V5 motor's hardware brake mode, more reliable than just setting voltage to 0.

💡
Set the brake mode in initialize(). Add lift_motor.set_brake_mode(MOTOR_BRAKE_HOLD); in initialize(). This makes the motor actively resist motion when stopped — better than COAST (free-spinning) or BRAKE (passive resistance) for a lifting mechanism.

Pattern 2: Toggle (the Claw)

One button. Each press flips between "open" and "closed". This is what the Clawbot guide used for its claw — same pattern works on Flex.

src/main.cpp — near opcontrol(), file scope
// File-scope state variable (lives outside opcontrol) bool claw_closed = false;
src/main.cpp — opcontrol() claw control
// Inside the while(true) loop in opcontrol(): if (master.get_digital_new_press(DIGITAL_L1)) { claw_closed = !claw_closed; // flip state on each fresh press } // Drive the motor based on state if (claw_closed) { claw_motor.move(90); // gentle closing pressure } else { claw_motor.move(-90); // open }
⚠️
Why get_digital_new_press, not get_digital? get_digital returns true the whole time the button is held — if you used it for toggle, holding the button for 0.5s would toggle the claw 50 times in the 10ms loop. get_digital_new_press returns true once per press, false until the button is released and pressed again. Always use it for toggles.

Current-Limit Stall Protection (Both Motors)

If the claw is closed on a block and you keep pushing voltage at 90, the motor stalls (the rotor stops rotating but current keeps flowing). Stalled motors heat up fast — a 30-second stall can permanently damage the motor.

The right fix: set a current limit. The motor will be allowed up to that current and no more — if it hits the limit while stalled, the motor backs off automatically.

src/main.cpp — initialize() additions
lift_motor.set_brake_mode(MOTOR_BRAKE_HOLD); lift_motor.set_current_limit(2500); // 2.5A — protects from stall heat claw_motor.set_brake_mode(MOTOR_BRAKE_HOLD); claw_motor.set_current_limit(1800); // 1.8A — gentle grip pressure

The default current limit on a V5 smart motor is 2500mA (2.5A). Setting it lower limits the force the motor can apply. For the claw at 1800mA, the motor will physically be unable to crush a block — it stalls out at a safe pressure level.

Putting It Together

The complete opcontrol with chassis + lift + claw:

src/main.cpp — full opcontrol()
void opcontrol() { pros::delay(500); while (true) { // Chassis (tank) chassis.opcontrol_tank(); // Lift (hold-to-move) if (master.get_digital(DIGITAL_R1)) lift_motor.move(110); else if (master.get_digital(DIGITAL_R2)) lift_motor.move(-110); else lift_motor.brake(); // Claw (toggle) if (master.get_digital_new_press(DIGITAL_L1)) { claw_closed = !claw_closed; } claw_motor.move(claw_closed ? 90 : -90); pros::delay(ez::util::DELAY_TIME); } }

Time-Based Auton Lift — Until Sensors Land

Without a potentiometer, you can't tell the lift to go to "scoring height" by angle. You CAN tell it to run for 1.2 seconds. That's not as good, but it works:

src/autons.cpp — time-based lift
void lift_to_scoring_height_timed() { lift_motor.move(110); pros::delay(1200); // tuned by running and measuring lift_motor.brake(); } void lift_to_ground_timed() { lift_motor.move(-110); pros::delay(1200); lift_motor.brake(); }
⚠️
Time-based control is fragile. A fresh battery and a worn battery will produce different lift speeds — the 1.2s value that lifts to scoring height on a charged battery might undershoot on a half-charged one. This is exactly why Section 6's potentiometer upgrade matters. Time-based is the "still works without sensors" floor — not the goal.
🔬 Check for Understanding
Your team's lift refuses to hold position when released — it sags about 4 inches after each press. The lift code uses lift_motor.brake() when no button is pressed. What's the most likely fix?
Increase the lift motor's voltage when holding
The motor's brake mode is set to COAST (or nothing) — set it to MOTOR_BRAKE_HOLD in initialize() so brake() actively resists motion
Replace the lift motor — the gearbox is worn
Increase the loop delay so brake() has time to apply
// Section 05
Progressive Training Exercises 📋
Six exercises, ordered from "wheels spin" to "auton scores 2 blocks." Each one builds on the last. Don't skip ahead — the failures in exercise 3 are what teach you what exercise 4 needs.
🚫
Run them in order. If exercise 1 doesn't work, exercises 2-6 won't work either. The exercises are deliberately staircased so each failure has one possible cause — the thing you just changed.
Exercise 1 · Beginner
Drive Forward 12 Inches

Use the built-in drive_test auton from EZ-Template. Place the robot with a tape mark at the front bumper. Run it. Measure where the front bumper lands.

Goal: Confirm the chassis configuration in Section 3 is correct. Acceptable error on a first run: ±2 inches. If it goes wildly wrong (wrong direction, crashes), check polarities and IMU calibration before continuing.

Exercise 2 · Beginner
Turn 90° Right and Back

Write an auton: chassis.pid_turn_set(90, 110); chassis.pid_wait(); then chassis.pid_turn_set(0, 110); chassis.pid_wait();. The robot should turn right then return to its starting heading.

Goal: Confirm the IMU is reading correctly and turn PID is tuned acceptably. Mark the front of the robot with tape. After turn 1, the tape should point right of where it started. After turn 2, it should point back toward the start.

Exercise 3 · Intermediate
24-Inch Square

Drive a 24" × 24" square. Four sides, four 90° turns. Use tape on the floor to mark the corners. Measure how far off the robot is when it returns to corner 1.

Goal: Expose accumulated PID error. A well-tuned chassis returns within ~2". A poorly-tuned one spirals outward by 6"+ over the four sides. Record the closing-error distance in the engineering notebook. See PID Diagnostics for what the symptom means.

Exercise 4 · Intermediate
Lift to Time-Based Scoring Height

Write an auton step: lift up for 1.0 seconds, then back down for 1.0 seconds. Watch the lift. Is 1.0s enough to reach the scoring height? Adjust the time. Document what value worked.

Goal: Get comfortable with timed motion control before sensor-based control. Identify the failure modes — battery state matters, gravity matters, friction matters. This is the motivation for adding the potentiometer in Section 6.

Exercise 5 · Intermediate
Drive-Lift-Score

Multi-step auton: drive forward 18", lift to scoring height (timed), drive forward 6" more (now near the goal), open the claw, drive backward 12". Time it. Run it 3 times. Record the spread (best vs. worst time, best vs. worst final position).

Goal: Practice coordinating chassis + mechanism in an auton. Discover what's consistent (chassis PID) vs. what isn't (timed lift, claw release timing). The variation between runs IS the data — it tells you what to sensorize.

Exercise 6 · Advanced
Two-Block Scoring Routine

Auton: grab block 1 (drive to A, close claw, drive back). Score block 1 (drive to goal, lift, open claw). Grab block 2 from a different location. Score block 2. Try to fit it all in 15 seconds (typical V5RC auton period).

Goal: Real auton complexity. You'll discover the lift timing varies between runs (battery). You'll discover the claw doesn't always grab cleanly. These are the problems Section 6's sensors solve.

What to Do With Failure

Failed exercises aren't setbacks — they're the most useful data you'll generate this season. After every failure, do this:

  1. What happened? One sentence. ("Lift overshot the scoring height.")
  2. Why? One hypothesis. ("Battery was at 95% in practice but 60% at the event — lifted faster.")
  3. What sensor would fix it? One name. ("Potentiometer on the lift axle.")
  4. What's the smallest experiment to confirm the hypothesis? One step. ("Run timed lift on a fresh battery, then on a half-battery, measure both heights.")

That four-step pattern is what the engineering notebook scores you on. Judges aren't impressed by "we built a robot." They're impressed by "we ran an experiment, here's what we learned, here's what we did next."

📝
Document the timing data, not the code. The engineering notebook entries from these exercises should focus on what you measured and what you changed in response — not on the C++ code itself (that violates EN4 if AI-assisted). Time data, position data, hypothesis, conclusion. Those are yours.
// Section 06
Sensors to Add to Your Flex 🔮
Five sensors, five capabilities, five afternoons of work. Each one promotes a time-based or guess-based behavior into measurement-based. The team has already picked which sensors to add and where they go — this section is how to integrate them.
The order matters. Limit switches first (cheap, instantly useful for safety + calibration), then potentiometer (precise lift control), then optical (auto-detect block in claw), then distance (parking). Adding them all at once means debugging five things in parallel. Adding them one at a time means each new capability stands on the previous.

For each sensor below, follow the universal 5-step workflow — pick a port, declare it, verify, calibrate, use. The notes below are Flex-specific add-ons to that universal pattern.

Sensor 1: Limit Switch at the Bottom of the Lift

What it gives you

The ability to zero the lift on boot. When the robot powers on, run the lift down at low power until the bottom switch triggers, then call lift_motor.tare_position(). Now position 0 = "bottom" in code, every match, every robot.

Port

Any ADI (3-wire) port — A, B, C, ... H. Pick one that's physically close to the brain. Conventionally A for "first switch added."

Code change

subsystems.hpp — add this
inline pros::ADIDigitalIn lift_bottom('A'); // limit switch, ADI port A
initialize() — calibration routine
// Run the lift down until it hits the bottom switch, then tare position printf("Calibrating lift...\n"); while (!lift_bottom.get_value()) { lift_motor.move(-40); // gentle downward pros::delay(20); } lift_motor.brake(); lift_motor.tare_position(); // THIS is the calibration printf("Lift bottom = 0\n");
⚠️
The gotcha: ADIDigitalIn's get_value() typically returns 0 when pressed (active-low), 1 when released. Test on the bench — if the loop runs forever with the switch pressed, you need !lift_bottom.get_value() instead of lift_bottom.get_value().

Sensor 2: Limit Switch at the Top of the Lift

What it gives you

A hard upper safety stop. Without it, an enthusiastic lift command can crash the mechanism into its mechanical stop — eventually that breaks chains, sprockets, or the motor's gearbox.

Code change

subsystems.hpp
inline pros::ADIDigitalIn lift_top('B'); // limit switch, ADI port B
opcontrol() — bounded lift
// Press up only if not at top; press down only if not at bottom if (master.get_digital(DIGITAL_R1) && !lift_top.get_value()) { lift_motor.move(110); } else if (master.get_digital(DIGITAL_R2) && !lift_bottom.get_value()) { lift_motor.move(-110); } else { lift_motor.brake(); }

Sensor 3: Potentiometer on the Lift Pivot

What it gives you

Absolute lift position, in degrees. Now "go to scoring height" can mean "go to pot reading 1850" — same value every match, same accuracy regardless of battery state. This is the upgrade that makes timed lift control obsolete.

Port

Any ADI port — C is conventional after A and B are used for limit switches.

Code change

subsystems.hpp
inline pros::ADIAnalogIn lift_pot('C'); // potentiometer, ADI port C // Named lift positions — measured on the bench, not guessed constexpr int LIFT_GROUND = 200; // fully down, just above ground constexpr int LIFT_CARRY = 900; // safe driving height with a block constexpr int LIFT_SCORE = 1850; // at scoring goal height constexpr int LIFT_TOLERANCE = 30; // "close enough" band (degrees of pot rotation)

Find the actual pot values by hand-moving the lift to each named position and reading lift_pot.get_value() from the terminal. Write the numbers in the code AND in the engineering notebook.

autons.cpp — position-based lift
void lift_to_position(int target) { int err = target - lift_pot.get_value(); while (std::abs(err) > LIFT_TOLERANCE) { // Simple proportional control: voltage scales with how far we are int voltage = std::clamp(err / 8, -110, 110); lift_motor.move(voltage); pros::delay(20); err = target - lift_pot.get_value(); } lift_motor.brake(); } // Now your auton can say: lift_to_position(LIFT_SCORE); // reliable across battery states
💡
Potentiometer wiring tip. The pot must be mounted on the same shaft as the lift pivot — not on the motor shaft. If the lift has a gear ratio between motor and pivot (it usually does), reading motor rotation gives you wrong numbers. Mount the pot directly on the rotating element you want to measure.

Sensor 4: Optical Sensor on the Claw

What it gives you

Auto-detect when a block is in the claw. Now the driver can press "intake" and the claw will close automatically when a block enters — no need to time it perfectly by eye.

Port

Any Smart Port. Pick one near the claw mechanism. Conventionally 3 or 5.

Code change

subsystems.hpp
inline pros::Optical claw_optical(3); // smart port 3 // What "block detected" looks like — calibrate by reading values // when block is in/out of the claw at various distances constexpr double BLOCK_PROXIMITY_THRESHOLD = 120; // 0-255, higher = closer
opcontrol() — auto-close claw
// Auto-close mode: hold L1 to "arm" the auto-close if (master.get_digital(DIGITAL_L1)) { if (claw_optical.get_proximity() > BLOCK_PROXIMITY_THRESHOLD) { claw_closed = true; // block detected — grab it } else { claw_closed = false; // nothing there — stay open, ready } } else if (master.get_digital_new_press(DIGITAL_L2)) { claw_closed = false; // L2 = manual release } claw_motor.move(claw_closed ? 90 : -90);
⚠️
Optical sensors detect color too. If the Override blocks have a distinctive color, you can use claw_optical.get_hue() to confirm "yes, this is a block, not just my own hand by mistake." Returns 0-360. Read the hue values for actual game blocks during calibration.

Sensor 5: Distance Sensor at the Back of the Robot

What it gives you

End-game parking. The Override game rewards parking near walls or goals in the final seconds. A distance sensor at the back lets the robot back up until it's a specific distance from a wall — no driving over it, no falling short.

Port

Any Smart Port. 7 or 8 work well, away from the drive motors.

Code change

subsystems.hpp
inline pros::Distance rear_distance(7); // smart port 7 constexpr int PARK_DISTANCE_MM = 100; // 10cm from wall
autons.cpp — end-game park
void park_against_back_wall() { while (rear_distance.get() > PARK_DISTANCE_MM) { chassis.drive_set(-50, -50); // back up slowly pros::delay(20); } chassis.drive_set(0, 0); }
💭
Distance sensor returns 9999 when nothing is in range. If the robot is too far from the wall (more than ~2 meters), rear_distance.get() returns 9999 instead of a real distance. The loop above happens to work either way (9999 > 100 keeps the robot backing up), but if you write code that subtracts distances, guard against the 9999 case explicitly.

The Full Sensored Port Map

After all five sensors are integrated, the Flex looks like:

PortDevicePurposeStatus
4Smart MotorRight drivetrainDay-one
6Smart MotorLeft drivetrainDay-one
13Inertial SensorHeadingDay-one
15Smart MotorClawDay-one
17Smart MotorLiftDay-one
3OpticalClaw block-detectSensor 4
7DistanceRear parkingSensor 5
ALimit SwitchLift bottom (zero)Sensor 1
BLimit SwitchLift top (safety)Sensor 2
CPotentiometerLift positionSensor 3

What You Now Have

A bot that can:

This is what makes Flex a complete training platform. By the time the team's competition robot is built, every student has touched all five sensor patterns on a robot that doesn't break things during practice.

⚙ STEM Highlight Systems Engineering: Open Loop vs. Closed Loop
Without sensors, your lift code is open-loop: you tell the motor "run for 1.2 seconds" and hope it ends up at scoring height. The system has no way to detect or correct error. With the potentiometer, the lift becomes closed-loop: the code reads the actual position, compares it to the target, and adjusts. Closed-loop control is more reliable, but adds complexity (sensor failure modes, calibration drift, code path branches). The progression from open-loop to closed-loop is the central narrative of every engineering project. Section 6 walks the Flex through that progression mechanism by mechanism.
🎤 Interview line: "We started with time-based lift control — open-loop — to get our auton running. Once we added the potentiometer, we converted to closed-loop position control. The same auton became 3x more consistent across battery states. We documented both versions and the timing data that justified the upgrade."

Where to Go After Flex

By the time a student has completed all six sections of this guide, they're ready for:

Related Guides
🦾 Clawbot Training → 🔧 Add a Sensor → 🤖 Clawbot Sensor Fusion → 🔬 My Robot Setup →
// Section 07
Driver-Assist Macros 🎮
Buttons that combine sensors with mechanism actions. The bridge between manual control (Sec 4) and full autonomous (Sec 5 Exercise 6). Driver presses one button; sensors decide the right thing to happen. On Flex this matters more than on Clawbot — the lift+claw coordination needed to score blocks under time pressure is exactly what macros solve.
📖 First time meeting these words?
  • Macro: a pre-programmed sequence that runs when you press one button. Like a keyboard shortcut for your robot — instead of pressing "lift up, then claw open, then drive back," you press one key and all three happen.
  • opcontrol(): the function in your code that runs the entire driver-control period (1:45 in matches). It's a loop that reads the controller and tells motors what to do, ~50 times per second.
  • Background task: code that runs in parallel with opcontrol, so it doesn't pause your driving. Used for the dashboard, rumble feedback, and continuous sensor monitoring.
  • Failsafe: a safety pattern. If a macro misbehaves, you should always be able to cancel it. Standard failsafe: move the joystick more than 30%, the running macro stops.
  • Proximity-gating: trusting a sensor reading only when a related sensor confirms the situation is right. Example: trust the optical sensor's color reading only when its proximity reading says something is actually close enough to look at.
  • D-Pad / face buttons: the small directional buttons on the left thumb (D-Pad: up/down/left/right) and the four lettered buttons on the right thumb (A/B/X/Y). On a stock V5 controller, you can't use these and the joysticks at the same time.

Controller Map — Fully-Instrumented Flex

📍 Left Hand (Claw + Modes)
L1Smart grab (auto-close if optical confirms block)
L2 (held)Claw open — manual override
D-Pad ↑Slow-mode toggle (50% drivetrain)
D-Pad ←Precise 90° left turn (IMU)
D-Pad →Precise 90° right turn (IMU)
D-Pad ↓Park-against-wall (distance sensor)
L StickLeft drivetrain (analog)
📍 Right Hand (Lift + Presets)
R1 (held)Lift UP (soft-limit aware)
R2 (held)Lift DOWN (soft-limit aware)
APickup preset (lift GROUND + claw OPEN)
YScore preset (lift SCORE height)
XTravel preset (lift CARRY + claw CLOSED)
BScore sequence combo (drive + lift + release)
R StickRight drivetrain (analog)
Failsafe: Big stick deflection (>30%) during a running macro cancels it. You're never trapped in a misbehaving routine.

This map assumes all five sensors from Section 6 are installed. With fewer sensors, some macros downgrade gracefully — smart grab becomes blind grab, soft limits become hard-limit-only, the park macro stops working. Layer the macros as you layer the sensors.

The Seven Flex Macros

Macro 1 · Foundation
Combined Position Presets (A / Y / X buttons)

Sensors used: Pot on lift; no others.

Why it matters: One button = a coordinated lift + claw state. Driver thinks "score mode," not "press lift-up for 1.2s then release claw." Cuts driver cognitive load by ~40% in field practice. Especially valuable on Flex because the lift travel range is large — eyeballing scoring height by stopwatch is unreliable.

Driver feel: Press A for pickup, Y for score, X for travel. Each takes ~0.5–1.5s to execute depending on lift travel distance. Faster and more reliable than manual sequencing.

Macro 2 · Foundation
Smart Grab (L1 button)

Sensors used: Optical (proximity-gated).

Why it matters: Driver presses L1; claw closes only if the optical sensor confirms a block is actually within range. Saves motor wear on empty-grabs, prevents accidental closes during defensive matches, and prints the detected color to the dashboard for confirmation.

Driver feel: "Grab" becomes a one-button action that just works. L2 stays as manual override (held = open) so you're never trapped if optical fails.

Macro 3 · Foundation
Soft Lift Limits (R1/R2 enhanced)

Sensors used: Pot (continuous lift angle); limit switches stay as backup.

Why it matters: Section 6 already added reactive limit switches that stop the lift when it hits the limit. This adds proactive deceleration as the lift approaches its safe range. Difference: the chain mechanism survives 8 matches instead of breaking at lunch. With the lift's chain drive, slamming into the top stop transmits shock directly into sprocket teeth — that's where chain skips and broken sprockets come from.

Driver feel: R1/R2 work normally in the safe zone. Near the limits, the lift slows to a graceful stop instead of slamming. Driver doesn't notice it most of the time — that's the point.

Macro 4 · Critical
Single-Button Precise Turn (D-Pad ← / →)

Sensors used: IMU (via EZ-Template's pid_turn_set).

Why it matters: The biggest pure driver-experience win in any V5RC robot. Humans are terrible at 90° turns under match pressure. The IMU + EZ-Template makes them perfect. Drivers go from "guessing" to "exactly 90° every time." Even more important on omni-wheel Flex because pivot-turning on omnis amplifies any inconsistency in stick input.

Driver feel: D-pad left or right, robot rotates exactly 90°, brief rumble confirms completion. Used constantly during matches for intersection navigation and goal alignment.

Macro 5 · Critical
Slow-Mode Toggle (D-Pad ↑)

Sensors used: none.

Why it matters: 50% drivetrain speed for precision scoring. Toggle off for travel between scoring zones. Flex with omni wheels actually accelerates faster than the team's competition robot will — slow-mode is the only way to do fine positioning at goal posts without overshoot. Tap on, score, tap off, drive away.

Driver feel: Tap D-pad up to enter slow-mode (rumble pattern confirms). Tap again to exit. The drivetrain feels "heavy" in slow-mode — intentional.

Macro 6 · Critical
Controller Rumble Feedback (background)

Sensors used: Optical (block detected), IMU (turn complete), Pot (lift at preset).

Why it matters: Driver knows what's happening without looking at the brain screen. Subtle but huge. During a tense match, eye contact with the field is everything — rumble lets sensors talk to the driver through their hands.

Driver feel: Brief buzz on grab, double-tap on macro completion, long buzz on collision. Drivers tune their map based on what feels useful vs noisy.

Macro 7 · Critical
Sequential Score Combo (B button)

Sensors used: All of them — Pot, Optical, Distance, IMU.

Why it matters: The bridge from macros to auton. Press B with a block in the claw → robot drives forward toward the goal, lifts to scoring height, opens claw, backs up. A 4-second sequence in one button. The same logic that powers your auton, but driver-triggered. Override's match flow rewards repeated scoring cycles; this macro turns a 6-second manual cycle into a 4-second one-button cycle.

Driver feel: Get the robot roughly in front of a goal with a block in the claw, press B, robot does the rest. You can override with the stick at any time (failsafe). Aggressive teams use this constantly; it's the difference between scoring 3 blocks and 5 blocks in a match.

Worked PROS Solutions for the Flex Macros 📖

📍
Where this all goes in EZ Template: the helper functions go at file scope in src/main.cpp (alongside lift_to_position from Sec 6). The button-press checks go inside opcontrol()'s while loop. The background tasks (haptic, soft limits) get started once at the top of opcontrol() via pros::Task task_name(task_fn);. Order matters: drivetrain first, one-shot macros next, manual mechanisms last.

Helpers — Position Presets (Macro 1)

📄 src/main.cpp — add at file scope
// Coordinated mechanism states. Each preset is a "mode" not just a position. void pickup_preset() { lift_to_position(LIFT_GROUND); // from Sec 6, pot-based claw_motor.move(-90); // open pros::delay(400); claw_motor.brake(); claw_closed = false; } void score_preset() { lift_to_position(LIFT_SCORE); // claw state preserved } void travel_preset() { lift_to_position(LIFT_CARRY); claw_motor.move(90); // close for compact travel pros::delay(400); claw_motor.brake(); claw_closed = true; }

Smart Grab (Macro 2)

📄 src/main.cpp — add at file scope
void smart_grab() { // Only close claw if optical confirms a block is there if (claw_optical.get_proximity() > BLOCK_PROXIMITY_THRESHOLD) { claw_motor.move(90); // close pros::delay(500); claw_motor.brake(); claw_closed = true; int hue = claw_optical.get_hue(); pros::lcd::print(6, "Grabbed: hue=%d", hue); master.rumble("."); // haptic confirm } else { pros::lcd::print(6, "Nothing to grab"); } }

Soft Lift Limits (Macro 3)

📄 src/main.cpp — add at file scope
// Scale lift motor power based on proximity to safe-range limits. // Use this anywhere driver presses R1/R2 for manual lift control. double get_safe_lift_power(double driver_input) { int pos = lift_pot.get_value(); // Approaching upper safe limit (200 below LIFT_SCORE) if (pos > (LIFT_SCORE - 200) && driver_input > 0) return driver_input * 0.3; // 30% power // Approaching lower safe limit (200 above LIFT_GROUND) if (pos < (LIFT_GROUND + 200) && driver_input < 0) return driver_input * 0.3; return driver_input; // full power in safe zone }

Precise Turn (Macro 4)

📄 src/main.cpp — add at file scope
// Wraps EZ-Template's pid_turn_set with a haptic confirm. // Pass -90 for left, +90 for right. void precise_turn(int degrees) { chassis.pid_turn_relative_set(degrees, 110); chassis.pid_wait(); master.rumble("."); }

Rumble Feedback Background Task (Macro 6)

📄 src/main.cpp — add at file scope
// Background task: rumble briefly when optical sees a new block. // Runs in parallel with opcontrol; never blocks driving. void haptic_task_fn() { bool was_present = false; while (true) { bool present = claw_optical.get_proximity() > BLOCK_PROXIMITY_THRESHOLD; if (present && !was_present) master.rumble("."); was_present = present; pros::delay(50); } }

Sequential Score Combo (Macro 7)

📄 src/main.cpp — add at file scope
// Press B with block in claw -> drive forward, lift, release, back up. // Stick deflection at any point cancels (failsafe pattern). void score_combo() { // Phase 1: drive forward until close to goal (or 2s timeout) chassis.drive_set(60, 60); int elapsed = 0; while (rear_distance.get() > 300 && elapsed < 2000) { if (std::abs(master.get_analog(ANALOG_LEFT_Y)) > 30) { chassis.drive_set(0, 0); return; // FAILSAFE } pros::delay(20); elapsed += 20; } chassis.drive_set(0, 0); // Phase 2: lift to scoring height, release lift_to_position(LIFT_SCORE); claw_motor.move(-90); pros::delay(400); claw_motor.brake(); claw_closed = false; // Phase 3: back up to clear the goal chassis.drive_set(-60, -60); pros::delay(600); chassis.drive_set(0, 0); master.rumble("-."); // macro complete }

Putting It All Together — opcontrol() Structure

📄 src/main.cpp — opcontrol() with all macros wired up
bool slow_mode = false; void opcontrol() { pros::Task haptic_task(haptic_task_fn); // start background rumble pros::delay(500); while (true) { // 1. DRIVETRAIN (always-running). Slow-mode halves stick values for fine control. if (slow_mode) { int left_v = master.get_analog(ANALOG_LEFT_Y) * 0.5; int right_v = master.get_analog(ANALOG_RIGHT_Y) * 0.5; chassis.drive_set(left_v, right_v); } else { chassis.opcontrol_tank(); } // 2. ONE-SHOT MACROS (presets + sequence combos) if (master.get_digital_new_press(DIGITAL_A)) pickup_preset(); if (master.get_digital_new_press(DIGITAL_Y)) score_preset(); if (master.get_digital_new_press(DIGITAL_X)) travel_preset(); if (master.get_digital_new_press(DIGITAL_B)) score_combo(); if (master.get_digital_new_press(DIGITAL_L1)) smart_grab(); // 3. D-PAD MACROS if (master.get_digital_new_press(DIGITAL_LEFT)) precise_turn(-90); if (master.get_digital_new_press(DIGITAL_RIGHT)) precise_turn(90); if (master.get_digital_new_press(DIGITAL_UP)) slow_mode = !slow_mode; if (master.get_digital_new_press(DIGITAL_DOWN)) park_against_back_wall(); // 4. MANUAL OVERRIDES (lift hold-to-move with soft limits; claw manual open) double lift_in = 0; if (master.get_digital(DIGITAL_R1)) lift_in = 110; else if (master.get_digital(DIGITAL_R2)) lift_in = -110; double lift_v = get_safe_lift_power(lift_in); if (lift_v != 0) lift_motor.move(lift_v); else lift_motor.brake(); if (master.get_digital(DIGITAL_L2)) { claw_motor.move(-90); // hold L2 = open claw_closed = false; } pros::delay(ez::util::DELAY_TIME); } }
⚙ STEM Highlight Human-Computer Interaction: Reducing Cognitive Load
A macro is a cognitive abstraction: the driver thinks in intentions ("score this block") rather than mechanism states ("lift up while claw closes"). The same principle that lets you press Ctrl+C instead of remembering the underlying clipboard syscalls applies to robot control. Research on pilot workload (NASA TLX studies) shows that reducing decision steps during high-stress tasks improves both speed and error rates. The 40% cognitive-load reduction from macros is the difference between scoring 3 blocks and scoring 5 in the same 1:45 match period. The cost is up-front complexity: writing good macros and proving they're failsafe. The pay-off is multipliable across every driver who uses them.
🎤 Interview line: "We layered driver-assist macros on top of manual control. Each macro reduces a multi-step intention to a single button press. The cost was about 150 lines of helper code with failsafes; the benefit was measurable — driver scoring cycle time dropped from 6 seconds to 4 seconds after macros went in. That's two extra cycles per match."
// Section 08
From Flex to Custom Competition Robot 🤖
Flex is the team's second training robot, not the destination. Once your team has Flex driving, scoring, and running macros reliably, the next step is the team's own competition robot — designed around the lessons Flex taught you. This section is about what transfers, what doesn't, and how to make the transition without rewriting everything.

The full team training arc:

🎯
Recommended training progression:
  1. Clawbot first — learn drive, arm, claw, sensors. Get every team member comfortable with PROS and EZ-Template. (see clawbot-training)
  2. Flex second — build it, drive it on the actual Override field. Discover what the game requires. Add sensors based on what you learn.
  3. Custom competition robot last — design your own based on what Flex taught you. Apply sensor lessons, code patterns, and macro architecture learned on simpler platforms.

What Transfers From Flex to a Custom Robot

The team will rebuild many things for the custom robot. Some things shouldn't be rebuilt — they already work and they generalize. Specifically:

TransfersWhat it means in practice
EZ-Template chassis structureThe ez::Drive chassis(...) constructor pattern. Only the port numbers, wheel diameter, and gear ratio change.
opcontrol() architectureThe Section 7 ordering: drivetrain → one-shot macros → D-pad macros → manual overrides. Works for any robot.
Sensor wiring patternsHow limit switches zero a mechanism, how a potentiometer enables position presets, how an optical sensor proximity-gates a grab. The patterns transfer; only the mounting locations change.
Macro architectureSmart grab, position presets, soft limits, precise turn, slow mode, rumble feedback, sequential combo. Drop them in, change the named constants, done.
Auton phase pattern"drive to A, do X, drive to B, do Y" structured as a sequence of pid_drive_set / pid_wait / mechanism call. Identical on every robot.
Engineering notebook templatesThe decision-matrix structure, calibration tables, before/after data formats. Transfer 1:1.

What Doesn't Transfer (And Why That's Fine)

Doesn't transferWhyWhat to do
Tuned PID constantsDifferent mass, motor count, wheel sizeRerun the tuning procedure from PID Diagnostics
Named position constants (LIFT_SCORE etc.)Different geometry, different pot mountingRe-measure on the bench, update the constants
Mechanism-specific codeIf the custom robot has e.g. a launcher instead of a claw, the actuation logic is differentWrite new code following the same patterns
Port mapCustom motor count, custom sensor placementUpdate subsystems.hpp
Mechanical design choicesHero Bot is one possible design, not the right one for every teamDesign intentionally; document decisions in the notebook

Sensor Onboarding Order on the Custom Robot

When the team's custom robot is built, layer sensors in the same order Flex used (matches the priority stack):

  1. IMU for heading. EZ-Template needs it. Pick a vibration-isolated mounting spot away from drive motors.
  2. Limit switches on every motorized mechanism with travel limits. Cheap insurance.
  3. Potentiometer on whichever mechanism rotates within a fixed range (likely the scoring arm or lift).
  4. Optical Sensor if Override uses alliance-colored game elements. Mount in the intake throat.
  5. Distance Sensor for parking, wall-alignment, and end-game positioning.
  6. AI Vision & GPS only if you have Tiers 1–5 working reliably and the game rewards their use.

Each sensor adds value if and only if the prior tier is reliable. Don't skip ahead — this is the same advice as Flex, scaled up. The order applies to every robot the team will ever build.

Engineering Notebook: The Hero Bot → Custom Comparison

The strongest notebook entry from this whole curriculum is the before/after comparison between Flex and the team's custom robot. Specifically:

That comparison is exactly what Design Award judges look for — it shows the team intentionally iterated, learned, and applied lessons. See engineering-notebook for the full Design Award rubric.

📝
EN4 reminder for the custom-robot phase: the patterns documented in this guide are reference material. As your team designs the custom robot's code, write it from scratch in your own words — don't paste from this guide into your team's production codebase. The patterns transfer; the implementations must be yours.
🔬 Check for Understanding
Your team has finished Flex training and is starting the custom competition robot. Roughly how much of the curriculum's code patterns transfer directly?
~25% — most needs to be rewritten for the new robot
~50% — about half is transferable
~85% — drivetrain, sensor patterns, macros, opcontrol structure, auton phases all transfer; mainly the port map and tuned constants change
~100% — the code is identical between any two V5 robots
← ALL GUIDES