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
โ„น๏ธ
What is an expo curve? A linear joystick maps 50% stick โ†’ 50% speed. An exponential curve keeps low stick values slow (fine control) while high stick values reach full power. This makes the robot easier to drive precisely at low speed without sacrificing top-speed capability. LemLib's 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:

ButtonFirst-stageSecond-stageEffect
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
none00Stop
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
}