⚙️ EZ Template Advanced

Build Your
Competition Robot

Six-motor drivetrain, position-controlled arm, intake toggle, scuff controls, macros, and pneumatics — all in one guide.

// Chapter 01
Six-Motor Drivetrain Setup 🚗
Configure a 3+3 layout in EZ Template — three motors per side, geared for speed and torque.

Port Assignment — Plan Before You Code

Write down your ports before touching the code. A 3+3 tank drive looks like this:

PortMotorReversed?Notes
1Left FrontYes (-1)Left motors face opposite direction
2Left MiddleYes (-2)
3Left BackYes (-3)
4Right FrontNo (4)Right motors face forward
5Right MiddleNo (5)
6Right BackNo (6)
10IMU (Inertial)N/AUsed for turning accuracy
⚠️
Physical test first! Before coding, plug each motor into port 1 and spin it by hand. If the V5 Brain shows the position going negative when pushing it forward, that motor needs to be reversed (negative port number).

Chassis Declaration in robot-config.cpp

📄 src/robot-config.cpp
// ─── 6-MOTOR TANK DRIVE (3+3) ─────────────────────── ez::Drive chassis( // Left motors — negative reverses direction {-1, -2, -3}, // Right motors — positive = forward {4, 5, 6}, // IMU port 10, // Wheel diameter (inches), external gear ratio // Common sizes: 2.75, 3.25, 4.0 3.25, 1.0 );

Gear Cartridge Guide

🔴
Red — 100 RPM
Torque. Great for arms and lifts, not for drive.
🟢
Green — 200 RPM
Balanced. Common for 3.25" wheel drivetrains.
🔵
Blue — 600 RPM
Speed. Used with 3.25" wheels for fast robots. Most common in current season.
💡
If your motors use blue (600 RPM) cartridges with 3.25" wheels and no external gearing, your chassis call is: 3.25, 1.0. If you have a 36:48 external gear ratio (common speed up), use 3.25, (48.0/36.0).

Set Motor Brake Type

📄 src/robot-config.cpp — inside initialize()
// In your initialize() function: chassis.set_drive_brake(MOTOR_BRAKE_COAST); // for driver control // chassis.set_drive_brake(MOTOR_BRAKE_HOLD); // for auton (holds position)
💡
EZ Template automatically switches brake mode between auton (hold) and driver control (coast) if you call set_drive_brake() in the right places. Check the EZ Template docs for ez::as::exit_condition for advanced brake control.
// Chapter 02
Motorized Arm — Position Control 💪
Hold the arm at a target angle, move to presets, and prevent it from slamming into hard stops.

Declare the Arm Motor

📄 src/robot-config.cpp
// Arm motor on port 8, red cartridge (100 RPM for torque) pros::Motor arm(8, pros::E_MOTOR_GEAR_RED); // Declare in main.h so all files can use it
📄 include/main.h — add this line
extern pros::Motor arm; // makes arm available in all .cpp files

Simple Hold-Position Control

The most reliable arm control method for beginners uses the motor's built-in position PID. You set a target, it holds there:

📄 src/main.cpp
// ─── ARM POSITION PRESETS (in motor encoder units) ─── const int ARM_DOWN = 0; // resting position const int ARM_MID = 1200; // mid height — tune this! const int ARM_HIGH = 2600; // scoring height — tune this! const int ARM_SPEED = 100; // max arm velocity (0–100) int armTarget = ARM_DOWN; // current target position void arm_control() { // D-pad UP → move to high position if (master.get_digital(DIGITAL_UP)) { armTarget = ARM_HIGH; } // D-pad DOWN → move to mid position else if (master.get_digital(DIGITAL_DOWN)) { armTarget = ARM_MID; } // D-pad LEFT → return to resting position else if (master.get_digital(DIGITAL_LEFT)) { armTarget = ARM_DOWN; } // Move arm toward its target using move_absolute arm.move_absolute(armTarget, ARM_SPEED); }
⚠️
Tune your preset values! The numbers 1200 and 2600 are placeholders. Run your arm, check arm.get_position() with printf, and replace them with your robot's real values.

Manual Override — Joystick Control

Sometimes you want direct joystick control instead of presets. Add a manual mode using a button hold:

void arm_control() { // Hold L1/L2 for manual joystick control of arm if (master.get_digital(DIGITAL_L1)) { arm.move_velocity(100); // move up manually armTarget = arm.get_position(); // update target so it holds here } else if (master.get_digital(DIGITAL_L2)) { arm.move_velocity(-100); // move down manually armTarget = arm.get_position(); } else { arm.move_absolute(armTarget, ARM_SPEED); // hold last target } }

Using the Arm in Autonomous

void my_auton() { // Drive forward while arm moves simultaneously arm.move_absolute(ARM_HIGH, ARM_SPEED); // start arm moving (non-blocking) chassis.pid_drive_set(24_in, 110); chassis.pid_wait(); // Wait until arm reaches position (within 50 units) while (abs(arm.get_position() - ARM_HIGH) > 50) { pros::delay(10); } // Now score... }
// Chapter 03
Intake Control + Toggle 🔄
Spin on/off, reverse, and add a toggle so one button press keeps it running hands-free.

Declare the Intake Motor

📄 src/robot-config.cpp
pros::Motor intake(9, pros::E_MOTOR_GEAR_GREEN); // In main.h: extern pros::Motor intake;

Basic Hold-Button Control

The simplest intake — hold R1 to run forward, hold R2 to reverse:

void intake_control() { if (master.get_digital(DIGITAL_R1)) { intake.move_voltage(12000); // full speed forward } else if (master.get_digital(DIGITAL_R2)) { intake.move_voltage(-12000); // full speed reverse } else { intake.brake(); } }

Toggle Control — Press Once to Latch On

A toggle means: first press turns it on, second press turns it off. You need to track both the current state and the previous button state to detect the moment of the press:

// ─── TOGGLE STATE VARIABLES ─────────────────────────── bool intakeOn = false; // is intake currently running? bool lastR1State = false; // was R1 pressed last loop? void intake_control() { bool r1Now = master.get_digital(DIGITAL_R1); // Detect rising edge: button just became pressed this frame if (r1Now && !lastR1State) { intakeOn = !intakeOn; // flip the toggle } lastR1State = r1Now; // remember for next loop // R2 always reverses (overrides toggle) if (master.get_digital(DIGITAL_R2)) { intake.move_voltage(-12000); } else if (intakeOn) { intake.move_voltage(12000); } else { intake.brake(); } }
💡
Rising edge detection is the key pattern. r1Now && !lastR1State means "button is pressed NOW but was NOT pressed last loop" — that's the exact moment of a press. Without this, the toggle would flip every 20ms while you hold the button!

Reusable Toggle Helper Function

If you have multiple toggles, make a helper so you're not duplicating the pattern everywhere:

// Put this in main.h — a reusable toggle detector bool toggleOnPress(bool buttonNow, bool& lastState, bool& toggleState) { if (buttonNow && !lastState) toggleState = !toggleState; lastState = buttonNow; return toggleState; } // Usage — now any toggle is one line: bool intakeOn = toggleOnPress(master.get_digital(DIGITAL_R1), lastR1, intakeRunning);
// Chapter 04
Scuff Controls + Macro Buttons 🎮
Remap buttons to a claw-grip style layout and add one-button sequences that run automatically.

What Are Scuff Controls?

Scuff controls (named after Scuf Gaming controllers) remap actions to buttons accessible with your index fingers while keeping thumbs on the joysticks. In VRC this usually means putting high-use actions on the bumpers (L1, L2, R1, R2) so your thumbs never have to leave the sticks.

👍
Standard Layout
Thumbs on sticks to drive, then move thumb to face buttons (A/B/X/Y) for mechanisms. Slower reaction.
Scuff Layout
Thumbs stay on sticks always. Index fingers control bumpers for intake/arm. Much faster during driver control.

Example Scuff Remapping

Here's a proven competition layout for a robot with a 6-motor drive, arm, and intake:

ButtonFingerActionWhy
Left StickLeft ThumbLeft drive sideTank drive
Right StickRight ThumbRight drive sideTank drive
R1Right IndexIntake forward (toggle)Most-used action
R2Right IndexIntake reverseUnjam quickly
L1Left IndexArm upScoring motion
L2Left IndexArm downReset arm
D-pad UPLeft Thumb (off stick)Arm HIGH presetQuick score height
D-pad DOWNLeft Thumb (off stick)Arm DOWN presetQuick retract
ARight Thumb (off stick)Macro: Score sequenceAutomated scoring
BRight Thumb (off stick)Pneumatic toggleClaw/release
💡
The code doesn't change for scuff — you just choose which DIGITAL_XX constant maps to which action. Scuff is a design decision about which physical button you assign to each piece of code.

Macro Buttons — One Press Runs a Sequence

A macro is an automated sequence triggered by a single button during driver control — like pressing A to raise the arm, wait, score, and retract automatically:

⚠️
Macros must run in a separate task, not in your main opcontrol loop. If you use pros::delay() inside opcontrol, the entire robot freezes — no driving while the macro runs. Tasks let both run simultaneously.
// ─── SCORE MACRO — runs in its own task ─────────────── bool macroRunning = false; void score_macro_task(void* param) { macroRunning = true; // Step 1: Raise arm to scoring height arm.move_absolute(ARM_HIGH, 100); while (abs(arm.get_position() - ARM_HIGH) > 80) pros::delay(10); // Step 2: Outtake for 400ms intake.move_voltage(-12000); pros::delay(400); intake.brake(); // Step 3: Return arm to rest arm.move_absolute(ARM_DOWN, 100); macroRunning = false; pros::Task::current().remove(); } // In opcontrol() — trigger the macro with button A: if (master.get_digital_new_press(DIGITAL_A) && !macroRunning) { pros::Task scoreTask(score_macro_task); }
get_digital_new_press() is a built-in PROS shortcut that detects rising edge automatically — it only triggers once per press, which is perfect for macros and toggles. Use it instead of tracking lastButtonState manually when you don't need to track state across the loop.

Emergency Macro Cancel

// Let driver cancel the macro with B button if (master.get_digital_new_press(DIGITAL_B) && macroRunning) { macroRunning = false; arm.move_absolute(armTarget, 100); // stop at current position intake.brake(); }
// Chapter 05
Pneumatics 💨
Single-acting and double-acting pistons — wiring, code, and toggle control.

Pneumatics Basics

💨
Single-Acting
One solenoid. Air pushes the piston out; a spring pulls it back. Simpler, uses less air. One state per solenoid.
💨💨
Double-Acting
Two solenoids. Air pushes out AND pulls back under power. More reliable, more consistent force in both directions.
🔌
ADI Ports
Solenoids plug into the 3-wire (ADI) ports on the V5 Brain — ports A through H — not the smart ports.
⚠️
Air management matters! You have a limited tank of air per match. Count your piston activations during testing and make sure you won't run dry by the end of driver control.

Single-Acting Piston

One solenoid on ADI port 'A'. Setting it true extends the piston; false lets the spring retract it.

📄 src/robot-config.cpp
// Single-acting solenoid on ADI port A pros::ADIDigitalOut piston_single('A'); // In main.h: extern pros::ADIDigitalOut piston_single;
📄 src/main.cpp
// ─── SINGLE-ACTING TOGGLE ──────────────────────────── bool pistonExtended = false; bool lastBtnB = false; void piston_control() { bool btnB = master.get_digital(DIGITAL_B); if (btnB && !lastBtnB) { pistonExtended = !pistonExtended; // flip state piston_single.set_value(pistonExtended); // send to solenoid } lastBtnB = btnB; } // In autonomous — just set the value directly: piston_single.set_value(true); // extend pros::delay(300); piston_single.set_value(false); // retract (spring)

Double-Acting Piston

Two solenoids — one to extend, one to retract. Both on ADI ports. You can't have both true at the same time — that fights against itself.

📄 src/robot-config.cpp
// Double-acting — solenoid A extends, solenoid B retracts pros::ADIDigitalOut piston_extend('A'); pros::ADIDigitalOut piston_retract('B'); // In main.h: extern pros::ADIDigitalOut piston_extend; extern pros::ADIDigitalOut piston_retract;
📄 src/main.cpp
// ─── DOUBLE-ACTING CONTROL ─────────────────────────── bool isExtended = false; void set_piston(bool extend) { isExtended = extend; if (extend) { piston_extend.set_value(true); pros::delay(50); // brief pulse is enough piston_extend.set_value(false); // cut power, piston holds } else { piston_retract.set_value(true); pros::delay(50); piston_retract.set_value(false); } } void piston_control() { if (master.get_digital_new_press(DIGITAL_B)) { set_piston(!isExtended); // toggle on each press } }
💡
Some teams wire double-acting pistons to the 3-wire expander if the Brain runs out of ADI ports. The code is identical — just use pros::ADIDigitalOut({{smartPort, 'A'}}) syntax.

Pneumatics in Autonomous

void my_auton() { // Drive to goal, then fire piston to release game element chassis.pid_drive_set(30_in, 110); chassis.pid_wait(); set_piston(true); // fire! pros::delay(250); set_piston(false); // retract chassis.pid_drive_set(-12_in, 100); // back up chassis.pid_wait(); }
🔗
Want the full deep-dive? The Pneumatics Best Practices guide covers air budgeting with an interactive estimator, build practices that prevent leaks, solenoid wiring and ADI expander setup, 5 programming patterns for efficiency, single vs double-acting decision framework, and a pre-match checklist.
// Chapter 06
Putting It All Together 🤖
The complete opcontrol() and initialize() combining every system — copy and adapt for your robot.
📋
This is a complete, working starting point. Replace port numbers, preset values, and button mappings to match your actual robot.

robot-config.cpp — All Declarations

📄 src/robot-config.cpp
#include "main.h" // ─── DRIVETRAIN ─────────────────────────────────────── ez::Drive chassis( {-1, -2, -3}, // left motors (reversed) {4, 5, 6}, // right motors 10, // IMU port 3.25, 1.0 // wheel diameter, gear ratio ); // ─── MECHANISMS ────────────────────────────────────── pros::Motor arm (8, pros::E_MOTOR_GEAR_RED); pros::Motor intake(9, pros::E_MOTOR_GEAR_GREEN); // ─── PNEUMATICS ────────────────────────────────────── pros::ADIDigitalOut piston('A'); // single-acting // ─── CONTROLLER ────────────────────────────────────── pros::Controller master(pros::E_CONTROLLER_MASTER);

main.cpp — Complete opcontrol()

📄 src/main.cpp
#include "main.h" // ─── ARM PRESETS ────────────────────────────────────── const int ARM_DOWN = 0, ARM_MID = 1200, ARM_HIGH = 2600; int armTarget = ARM_DOWN; // ─── TOGGLE/STATE VARIABLES ─────────────────────────── bool intakeOn = false, lastR1 = false; bool pistonOut = false, lastBtnB = false; bool macroRunning = false; // ─── MECHANISM FUNCTIONS ────────────────────────────── void arm_control() { if (master.get_digital(DIGITAL_L1)) arm.move_velocity(100), armTarget = arm.get_position(); else if (master.get_digital(DIGITAL_L2)) arm.move_velocity(-100), armTarget = arm.get_position(); else if (master.get_digital(DIGITAL_UP)) armTarget = ARM_HIGH; else if (master.get_digital(DIGITAL_DOWN)) armTarget = ARM_DOWN; else arm.move_absolute(armTarget, 100); } void intake_control() { bool r1 = master.get_digital(DIGITAL_R1); if (r1 && !lastR1) intakeOn = !intakeOn; lastR1 = r1; if (master.get_digital(DIGITAL_R2)) intake.move_voltage(-12000); else if (intakeOn) intake.move_voltage(12000); else intake.brake(); } void piston_control() { bool b = master.get_digital(DIGITAL_B); if (b && !lastBtnB) { pistonOut = !pistonOut; piston.set_value(pistonOut); } lastBtnB = b; } // ─── SCORE MACRO TASK ───────────────────────────────── void score_macro_task(void*) { macroRunning = true; arm.move_absolute(ARM_HIGH, 100); while (abs(arm.get_position() - ARM_HIGH) > 80) pros::delay(10); intake.move_voltage(-12000); pros::delay(400); intake.brake(); arm.move_absolute(ARM_DOWN, 100); macroRunning = false; pros::Task::current().remove(); } // ─── MAIN DRIVER CONTROL ───────────────────────────── void opcontrol() { while (true) { chassis.opcontrol_tank(); // 6-motor tank drive arm_control(); // arm presets + manual intake_control(); // toggle intake piston_control(); // single-acting toggle // Score macro — button A, non-blocking via task if (master.get_digital_new_press(DIGITAL_A) && !macroRunning) pros::Task(score_macro_task); pros::delay(ez::E_TASK_DELAY); // always last — 10ms } }

Autonomous Template

📄 src/autons.cpp — starter auton using all systems
void full_auton() { // 1. Raise arm while driving forward arm.move_absolute(ARM_HIGH, 100); chassis.pid_drive_set(24_in, 110); chassis.pid_wait(); // 2. Score — outtake game element intake.move_voltage(-12000); pros::delay(400); intake.brake(); // 3. Fire pneumatic piston.set_value(true); pros::delay(200); piston.set_value(false); // 4. Retract arm and back up arm.move_absolute(ARM_DOWN, 100); chassis.pid_drive_set(-12_in, 100); chassis.pid_wait(); // 5. Turn and drive to next position chassis.pid_turn_set(90_deg, 90); chassis.pid_wait(); chassis.pid_drive_set(18_in, 110); chassis.pid_wait(); }
🏆
You now have a full competition robot codebase! From here, focus on tuning PID constants, refining your autonomous paths, and improving driver consistency through practice. The code is just the foundation — the reps make the difference.
📚
Next steps: Explore odometry in EZ Template for position tracking, look into motion profiling for smoother paths, and consider adding a second controller (partner) for complex mechanisms.
⚙ STEM Highlight Engineering: Abstraction Layers & System Architecture
A full competition robot is a layered system — exactly like software stacks or circuit hierarchies. The hardware layer (motors, pneumatics) is controlled by the firmware layer (V5 Brain), which is managed by your application layer (EZ Template + your code). Abstraction means each layer hides complexity from the layer above: chassis.pid_drive_set(36, 110) abstracts away encoder math, PID loops, and motor voltage — you only think in inches. This is the same principle behind every operating system ever written.
🎤 Interview line: “Our robot uses three abstraction layers. EZ Template abstracts the hardware — we command in inches and degrees, not motor ticks. Our subsystem files abstract each mechanism. Our auton functions combine these into high-level strategies. This mirrors how professional software systems are architected.”
🔬 Check for Understanding
Your arm uses move_absolute(900, 100). What layer of abstraction does this represent?
Raw hardware — you are directly setting motor voltage
Application layer — the PROS motor API abstracts encoder position control so you work in ticks, not voltage
There is no abstraction — the motor just runs
This is firmware code inside the V5 Brain
Related Guides
💾 Version Control → 📄 Organizing Code → 🔬 PID Diagnostics →
← ALL GUIDES