Driver Control
Read a joystick, drive the chassis, toggle pneumatics, and debounce buttonsโall inside the opcontrol() loop.
The opcontrol() Loop
Driver control in PROS is a polling loop: you call
pros::Controller::get_analog() or get_digital() every iteration
and act on the current state. There is no built-in event systemโyou implement one yourself.
The loop should run at ~10 ms (100 Hz) for responsive feeling. Never use pros::delay(0)โalways yield with at least pros::delay(10) or let LemLib handle it.
Tank Drive
Tank drive uses the left joystick Y-axis for the left wheels
and the right joystick Y-axis for the right wheels.
LemLib's chassis.tank() handles the motor calls and applies an expo curve.
src/main.cppint leftY = master.get_analog(pros::E_CONTROLLER_ANALOG_LEFT_Y); int rightY = master.get_analog(pros::E_CONTROLLER_ANALOG_RIGHT_Y); // Deadband: ignore tiny stick movements caused by joystick drift abs(leftY) < 10 ? leftY = 0 : leftY = leftY; abs(rightY) < 10 ? rightY = 0 : rightY = rightY; chassis.tank(leftY, rightY, true); // true = use expo curve
ExpoDriveCurve takes a deadband, a minimum
output, and a gain to shape the curve.
Button Debouncing (Toggle)
Many mechanisms need a toggle: press once to deploy, press again to retract.
The problem is that get_digital() returns true for every loop
iteration while the button is heldโpotentially hundreds of toggles per second.
Two patterns handle this:
Pattern 1: Manual "was pressed" flag
// In opcontrol(), before the loop: bool slideDeployed = false; bool digitalXWasPressed = false; // Inside the loop: if (master.get_digital(pros::E_CONTROLLER_DIGITAL_X)) { if (!digitalXWasPressed) { // Only act on the leading edge digitalXWasPressed = true; slideDeployed = !slideDeployed; slideMechanism(slideDeployed); } } else { digitalXWasPressed = false; // Reset when button released }
Pattern 2: PROS get_digital_new_press()
PROS provides a helper that fires only once per button pressโinternally it maintains the same flag:
if (master.get_digital_new_press(pros::E_CONTROLLER_DIGITAL_Y)) { descoreDeployed = !descoreDeployed; descoreMechanism.set_value(descoreDeployed); }
Use Pattern 2 when you can. Use Pattern 1 when you also need to react to button-held state (as in the slide mechanism that can be triggered by either X or RIGHT).
Intake Control
The two-stage intake has four different operating modes depending on which bumper you press:
| Button | First-stage | Second-stage | Effect |
|---|---|---|---|
L1 | โ127 (in) | โ200 velocity (in) | Score into mid goal (fast) |
L2 | โ127 (in) | โ127 (in) | Score into long goal (full speed) |
R1 | โ127 (in) | +127 (out) | Store block in basket |
R2 | +127 (out) | โ127 (in) | Score into low goal |
| none | 0 | 0 | Stop |
src/main.cppif (master.get_digital(pros::E_CONTROLLER_DIGITAL_L1)) { firstStageIntake.move(-127); secondStageIntake.move_velocity(-200); } else if (master.get_digital(pros::E_CONTROLLER_DIGITAL_L2)) { firstStageIntake.move(-127); secondStageIntake.move(-127); } else if (master.get_digital(pros::E_CONTROLLER_DIGITAL_R1)) { firstStageIntake.move(-127); secondStageIntake.move(127); } else if (master.get_digital(pros::E_CONTROLLER_DIGITAL_R2)) { firstStageIntake.move(127); secondStageIntake.move(-127); } else { firstStageIntake.move(0); secondStageIntake.move(0); }
move() vs move_velocity().
move(value) sets voltage (โ127 to +127) โ simple but speed varies with battery level.
move_velocity(rpm) uses the motor's internal PID to maintain a target RPM regardless
of load. Use move_velocity when consistent speed matters (e.g., color sorting).
Full opcontrol()
src/main.cppvoid opcontrol() { bool slideDeployed = false; bool descoreDeployed = false; bool alignerDeployed = false; bool digitalXWasPressed = false; aligner.set_value(false); while (true) { // โโ Drive โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ int leftY = master.get_analog(pros::E_CONTROLLER_ANALOG_LEFT_Y); int rightY = master.get_analog(pros::E_CONTROLLER_ANALOG_RIGHT_Y); abs(leftY) < 10 ? leftY = 0 : leftY = leftY; abs(rightY) < 10 ? rightY = 0 : rightY = rightY; chassis.tank(leftY, rightY, true); // โโ Slide (X or Right d-pad) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ if (master.get_digital(pros::E_CONTROLLER_DIGITAL_X) || master.get_digital(pros::E_CONTROLLER_DIGITAL_RIGHT)) { if (!digitalXWasPressed) { digitalXWasPressed = true; slideDeployed = !slideDeployed; slideMechanism(slideDeployed); } } else { digitalXWasPressed = false; } // โโ Descore (Y) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ if (master.get_digital_new_press(pros::E_CONTROLLER_DIGITAL_Y)) { descoreDeployed = !descoreDeployed; descoreMechanism.set_value(descoreDeployed); } // โโ Aligner (A) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ if (master.get_digital_new_press(pros::E_CONTROLLER_DIGITAL_A)) { alignerDeployed = !alignerDeployed; aligner.set_value(alignerDeployed); } // โโ Intake (L/R bumpers) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ if (master.get_digital(pros::E_CONTROLLER_DIGITAL_L1)) { firstStageIntake.move(-127); secondStageIntake.move_velocity(-200); } else if (master.get_digital(pros::E_CONTROLLER_DIGITAL_L2)) { firstStageIntake.move(-127); secondStageIntake.move(-127); } else if (master.get_digital(pros::E_CONTROLLER_DIGITAL_R1)) { firstStageIntake.move(-127); secondStageIntake.move(127); } else if (master.get_digital(pros::E_CONTROLLER_DIGITAL_R2)) { firstStageIntake.move(127); secondStageIntake.move(-127); } else { firstStageIntake.move(0); secondStageIntake.move(0); } } // No delay neededโchassis.tank() yields internally }