All projects

High Stakes robot, VRC 2024-25

Won 13 awards in the 2024-25 season, including 3 Excellence Awards, 2 Tournament Champion titles, and the Design Award that qualified the team for the World Championship.

The High Stakes robot, a red and black frame with a tall lift, on a practice field with checkered walls.
The High Stakes robot on a practice field in April 2025.
Team
VEX Team 2147Z
Role
Design and programming lead
Period
Sep 2024 to May 2025
Drive
6 × 11 W motors, 450 rpm, 6.3 ft/s
Free speed
6.3 ft/s at 450 rpm
Autonomous
21 tuned routines

What the game rewards

In High Stakes, rings are scored onto mobile goals and wall stakes, and a goal moved into a corner has its value doubled or cancelled. Our scoring analysis found that doubled goals and negative corner goals were each worth just over a quarter of the 118 available points, more than any other way of scoring. So the robot is built to possess goals and hold corners instead of climbing at the end of the match. Every subsystem came out of that one decision. The first robot paired a 450 rpm chassis with a hook intake and a two-bar pivot lift, and a pneumatic clamp held the goal steady while we filled it.

Every mechanism started from the same three limits. The robot had to start inside an 18 × 18 × 18 in volume and draw at most 88 W of combined motor power, and every custom plastic part had to fit on one 12 × 24 in sheet no thicker than 0.07 in.

Choosing designs with a decision matrix

We chose every mechanism with a decision matrix, which is a scored table of options and criteria. Each design was scored against criteria such as speed, weight, and ease of build. The winner was often not the fastest design on paper. It was the one we could build and repair between matches.

  1. Drivetrain4 configurations29.3 / 30
  2. Hook intakevs flex wheel, conveyor, claw25 / 40
  3. Two-bar liftvs four bar, chain bar, double reverse four bar36 / 40
  4. Deposit liftDecember redesign, 3 candidates26 / 30
  5. Passive hangvs winch, piston36 / 40

Score in points, out of the maximum for that matrix

Winning decision-matrix score for each mechanism. The scores of the other candidates are not shown.

Chassis

The chassis is a tradeoff between speed and pushing power. A 450 rpm drive on 3.25 in wheels won the matrix at 29.3 out of 30. Its free speed is 6.3 ft/s, which is quick enough to reach a corner first without giving up the torque to hold it. It runs six 11 W motors through a 36:48 reduction, with 4 omni wheels for turning and 2 traction wheels for pushing resistance.

After the first tournament the chassis was the one subsystem we rebuilt. Its center of gravity sat behind the center of the frame, which made the robot tip and sink into the foam tiles, and the bottom of the intake hit the ground. We lengthened the frame by 7 holes, swapping the 23 in C-channel rails for 30 in rails, and added 2 more powered omni wheels. Spreading the same weight over 8 points of contact stopped the sinking and let us hold our ground against defense, without touching the gearing we already trusted.

Intake

The intake is a hook system: a chain run of plastic hooks that catches rings at floor level and carries each one up and over a mobile goal. It scored 25 out of 40 on its matrix and won on speed and accuracy against a flex wheel, a conveyor, and a claw.

Our first full robot test failed 2 of 3 trials, because rings hit the goal stake and dropped to the floor instead of seating. I made a parameter chart of how hook height, shaft termination distance, and hook speed each moved where a ring landed. It showed that raising the hooks alone did not fix it. Extending and raising the termination shaft with a 5x L-channel threw the ring past the stake.

First full-robot test
Failed 2 of 3 trials

Lift

The lift is a two-bar, a single pivoting linkage that carries the whole intake up to wall stake height. It won its matrix at 36 out of 40 against a four bar, a chain bar, and a double reverse four bar, mostly on weight and ease of build. A 1x33 beam on it snapped from the constant tension of the intake hanging out past the pivot, and we replaced it with a half-cut 3x33 C-channel that puts more material along the loaded axis.

By December our reflection showed that our wall stake cycle was slower than most competitive designs, and that faster teams were reaching the center goal before us. We scored three replacements: an actuated hook stage, a six-bar ring redirect, and a two-bar deposit mechanism. The redirect could hold 2 rings at once, which looked good on paper, but every team we watched running one had to clear jams in the middle of a match. We chose the two-bar deposit lift, which scored 26 out of 30. It ends on the opposite side of the robot from the top intake shaft and drops a ring straight onto the stake.

Sweeper and hang

Four rings sit stacked in each corner, and clearing them was costing us corners we had already won. The sweeper is an arm that extends off the side of the chassis and wedges between the ring stack and the field perimeter, and then the robot pivots and drags the whole stack out. A fixed wedge needs no air and no moving parts, but it clears only one ring per pass, which is far too slow against a stack of 4. So the sweeper is an actuated 2x22 C-channel arm on pillow block bearings, driven by a single-acting piston, with a polycarbonate face that keeps the stack in front of the arm.

The hang lifts the robot onto the center ladder and is worth 3 to 12 points in the last 30 seconds. We had no motor ports and no air left to spend, so we limited the design to passive parts. It scored 36 out of 40 against a winch and a piston, winning almost entirely on the resources it did not use. Two half-cut 3x16 C-channels mount to the two-bar frame under tension from latex tubing, and 1.5 in standoffs grip the rung.

Built by hand

Every custom part was cut, drilled, and bent by hand. I modeled each part in Fusion 360 with the Sheet Metal tools, which unfold a bent part into a flat pattern. I printed the pattern at scale, taped it to the polycarbonate, and cut and drilled through the paper. Bends were formed with a heat gun, including a 70 degree bend on the plate that sets the clamp tilt angle. Working this way kept the parts on one sheet and let us replace a broken part the same night.

Control software

The code is C++ on PROS, built on LemLib, with a fork of it that I maintain as LemLib-2147 (v1.1.0 through v1.2.1) so we could add features the original did not have. Each subsystem is its own class with its own background task on a 20 ms loop, so the intake and the lift update independently of what the driver code is doing. Driver control and autonomous share one interface into every mechanism.

The lateral PID is tuned to 10, 0.05, and 60, and the angular PID to 7.3, 0.0075, and 78.6. The lift runs the same controller off a rotation sensor instead of a motor encoder. Dividing the raw centidegree reading by 36000 converts it to rotations, and the threshold check in the code is a workaround for the sensor wrapping from 1.0 back to 0 at the bottom of the travel: a reading above 0.875 is treated as 0, so a wrapped value does not command a full rotation backwards.

uk=Kp ek+Ki∑j=0kej+Kd(ek−ek−1)u_k = K_p\, e_k + K_i \sum_{j=0}^{k} e_j + K_d \left(e_k - e_{k-1}\right)
The discrete PID controller. The output u is built from the error e, its running sum, and its change since the last loop, and the loop period is folded into the gains. The code also zeroes the sum on a sign change or outside the windup range, which this form leaves out.
void Lift::moveToPID(float position) {
  double rotationData = static_cast<double>(liftRotation.get_angle()) / 36000.0;
  if (rotationData > .875) { rotationData = 0; }

  float error = position - rotationData;
  liftMotor.move(liftPID.update(error));
}
Lift position control from the rotation sensor, with the wrap workaround.

Odometry and the distance reset

Odometry reports where the robot is in field coordinates. It uses a 2.75 in horizontal tracking wheel mounted 3.5 in off center, a 2 in vertical tracking wheel offset 0.75 in, and an inertial sensor for heading. Because the position is in field coordinates, retuning a routine means changing one coordinate instead of rewriting every move after it.

The weakness is drift. The rounding in each update is tiny, but it adds up: the reported position drifted about 1.5 inches over a 30 second autonomous routine, and it keeps growing over a longer skills run. A GPS sensor would fix that directly, but its strips are not guaranteed at every competition, and we could not build a season around a sensor that might not be there. Instead I added a distanceReset class to our LemLib fork. It uses 2 distance sensors aimed at perpendicular walls, averages 10 filtered readings off each wall, converts them to an absolute field position, and writes that straight back into odometry. A skills routine can now correct itself halfway through.

Δlocal=2sin⁡(Δθ2)(dΔθ+r)\Delta_{\text{local}} = 2 \sin\left(\tfrac{\Delta\theta}{2}\right)\left(\tfrac{d}{\Delta\theta} + r\right)
The local movement of the robot over one loop. Here d is the distance a tracking wheel rolled, r is the wheel's offset from the robot's center, and Δθ is the change in heading. This is exact for movement along an arc.
Xt=Xt−1+Δysin⁡θˉ−Δxcos⁡θˉX_t = X_{t-1} + \Delta y \sin\bar\theta - \Delta x \cos\bar\theta
The global position update. Δx and Δy are the local movement from the last step, and the bar over θ marks the average heading over the update (the starting heading plus half the change). Angles are in radians, measured clockwise from the field's +Y axis.
Yt=Yt−1+Δycos⁡θˉ+Δxsin⁡θˉY_t = Y_{t-1} + \Delta y \cos\bar\theta + \Delta x \sin\bar\theta
The same update for the Y coordinate.
lemlib::distanceReset reset(xRobotDistance, yRobotDistance, 6, 5);

// mid-routine, facing into quadrant II
reset.reset(lemlib::x, lemlib::y, lemlib::II, 10);
Setting up the reset with both sensors and their offsets, and calling it mid-routine.

AutoBuilder

Writing an autonomous routine usually means guessing a coordinate, running it, and editing the number after watching the robot miss. AutoBuilder removes the guessing. I push the robot around the field by hand with odometry running, and at each position I want, a controller button prints the matching LemLib call to the brain terminal with the live position already filled in. A drives to a point, X turns to a heading, B turns to a point, and the other buttons print the lift, intake, and clamp calls. Writing a routine became a matter of driving it once and pasting the output. We ended the season with 21 tuned routines, and a bad field or a broken part stopped costing us a night of retuning.

if (controller.get_digital_new_press(pros::E_CONTROLLER_DIGITAL_A)) {
  printf("chassis.moveToPoint(%f, %f, 1500, {}, false); \n",
         chassis.getPose().x, chassis.getPose().y);
} else if (controller.get_digital_new_press(pros::E_CONTROLLER_DIGITAL_X)) {
  printf("chassis.turnToHeading(%f, 500); \n", chassis.getPose().theta);
}
How two of the buttons print their calls.

Subsystem states

Each mechanism runs as a state machine. The intake has states such as regular, which drives the motor at a set voltage, ejecting, which reverses a wrong-colored ring back out, and relative, which moves the hooks a fixed distance to seat a ring. Its task picks one state every 20 ms before it touches a motor, and two checks run ahead of that on every pass. Color sort compares the hue from the optical sensor with our alliance range and flags an opposing ring for ejection. Anti-jam watches for a motor drawing more than 1800 mA at zero efficiency and reverses it briefly to clear.

The lift resolves its state into a target height and feeds that to its own PID loop. The clamp has a task that watches a distance sensor and closes by itself once a goal is inside, so the driver only holds a heading toward the goal. Because each mechanism is built this way, a change to one state reached all 21 autonomous routines without editing any of them.

void Intake::intakeTask() {
  autoEject();
  antiJam();

  switch (currentState) {
  case regular:
    intakeMotor.move(intakeVoltage);
    break;

  case ejecting:
    while (intakeOptical.get_proximity() > distanceThreshold) {
      intakeMotor.move(127);
    }
    delay(5);
    intakeMotor.move(-127);
    delay(80);
    setState(regular);
    break;
  }

  delay(20);
}
The intake task. The checks run first, then the current state decides what the motor does.

Autonomous routines

Three of the autonomous routines, recorded on the practice field.

Autonomous routine 1, practice field, May 2025.
Autonomous routine 2, practice field, May 2025.
Autonomous routine 3, practice field, May 2025.

Result

The team won 13 awards in the season. Twelve came at 10 VRC events: 3 Excellence Awards, 2 Tournament Champion titles, 2 Innovate Awards, 2 Judges Awards, an Amaze Award, a Robot Skills Champion award, and the Design Award at the California Region 4 championship, which qualified us for the World Championship. The thirteenth was a special recognition award at the VEX AI World Championship in April 2025.

More projects

All projects