Introduction
This was my teams second competition robot for the V5RC Push Back challenge. We started this season with a hopper-style configuration, but after a few competitions, we quickly realized that our initial design was far too inefficient to keep up with the fast-paced strategies teams started exhibiting. Our first basket-style robot. We utilized a three stage intake system with


Hardware
As such, we started a new robot design, with the central concept of being able to fit underneath the long goals on each side of the field in order to have enhanced maneuverability. The core of this functionality, actuated two bar design that squats down to the height of the center goal, giving us enough clearance to scoot under the bar.
Once we decided on this lift design, we were able to design the intake around it. We used a decision matrix to evaluate between many different intake and lift designs. The obvious solution would be to use some elastics to hold the lift upwards. However the bands had no clear mounting point, which meant it was impractical to build. After much brainstorming, we came up with an innovative design, a polycarbonate compliant “spring” that pushed our intake up, with two 75mm pneumatic pistons pulling the lift downwards. This fit in the material constraints of the game, giving us a minimal footprint way to keep our air usage down when maneuvering around the field.
We utilized CAD software to fully design this robot to iterate rapidly, run stress simulations and design custom polycarbonate to strengthen our robot. Next, I used CAM software to laser cut the pieces out, which we then installed to our robot. We primarily built the robot out of 0.500” x 1.000” x 0.500” C-Channel Aluminum extrusion, with .250” x .375” L-Channel Aluminum extrusion on the lift to minimize weight.

Chassis:


Our chassis was designed to maximize strength, agility, and speed. We achieved this by initially designing our robot around field obstacles. The rear of our chassis was bidirectionally funneled through a 1/16” polycarbonate drive cap, which was custom laser cut to ±.001”, which minimized drive friction. We used six 11 watt motors with a 600rpm planetary gearbox, which was further geared down on a 4:3 reduction to achieve 450 rpm on our six drive wheels. CAD allowed us to fully map out our drivetrain layout, cutting down on production time and ensuring compatibility with future subsystems.
Combined Lift and Intake


This subsystem is a bit of a unique beast. To keep the weight of the 2 bar linkage down and to ensure proper geometry of the lift, the entirety of the intake structure is made of custom polycarbonate. In CAD, we used 2D geometry sketches to sketch an optimal path of the balls, which we then used to calculate the optimal angle for the intake ramp and sprockets. We then used motion study tools in Inventor to find the optimal placement of the pneumatic pistons. Each of the plastic pieces were made in Inventor with the use of the Sheet Metal tools, enabling complex bent geometries in CAD. It further maintained the internal stresses of the plastic, which came in handy when utilizing FEA stress modelling to ensure the strength of each part.


Software
The software for this robot was written in C++, and utilizes PROS, an RTOS that enables fine-grain control over each device. The overall philosophy for our software was to have modular bits of code that could evolve easily alongside the evolution of our robot.
PID Controller
A PID controller is a constant feedback loop that models the robots velocity as a function of its current error (distance to target), change in error, and accumulated error. This allowed us to accurately control our robot around the field, minimizing under/overshooting and establishing a strong foundation for us to base the rest of our code around.
//full code on github repository
// calculate integral
integral += error;
if (sgn(error) != sgn((prevError)) && signFlipReset) integral = 0;
if (fabs(error) > windupRange && windupRange != 0) integral = 0;
// calculate derivative
const float derivative = error - prevError;
prevError = error;
// calculate output
return error * kP + integral * kI + derivative * kD;
Odometry
Odometry is a localization method that tracks the robots position on the field using two tracking wheels, mounted perpendicular to each other, alongside the robots current heading, read from our inertial sensor. Every loop, we read how far each tracking wheel has spun since the last update, then use basic trigonometry to convert that local movement into a change in our global X and Y coordinate.
and represent how far the robot moved locally, left/right and forward/back, since the last update . Since these values are measured relative to the robot and not the field, we rotate them by our current heading to find how much our global and actually changed, then add that onto our current position estimate.
//full code on github repository
//...
// calculate local x and y
float localX = 0;
float localY = 0;
if (deltaHeading == 0) { // prevent divide by 0
localX = deltaX;
localY = deltaY;
} else {
localX = 2 * sin(deltaHeading / 2) * (deltaX / deltaHeading + horizontalOffset);
localY = 2 * sin(deltaHeading / 2) * (deltaY / deltaHeading + verticalOffset);
}
// save previous pose
lemlib::Pose prevPose = odomPose;
// calculate global x and y
odomPose.x += localY * sin(avgHeading);
odomPose.y += localY * cos(avgHeading);
odomPose.x += localX * -cos(avgHeading);
odomPose.y += localX * sin(avgHeading);
odomPose.theta = heading;
//...This allowed us to track our position on the field in real time while relying solely on two perpendicular optical trackers and an IMU. This approach to programming helped us rank #33 in programming skills scores out of the entire world.
State Machine
A state machine is a programming pattern that restricts a subsystems behavior to one of several predefined states at any given time, with clearly defined conditions for switching between them. Our intake, for example, cycles between states such as Brake, Intaking, Center, High, Outtaking, and Matchloading, with each state defining exactly which motors run and at what speed. This gave our driver control and autonomous code a single, shared interface into the intake, meaning a change to how any one state behaved was instantly reflected everywhere that state was used.
flowchart TB
subgraph main["Main Execution Thread"]
s(( )) -->|Intake Constructor| init["Init"]
end
subgraph driver["Driver Control (Main Loop)"]
buttons["Buttons"] -->|Set globalProfile enum| profile["ProfileSelect"]
end
subgraph task["Intake Background Task (20ms Loop)"]
thread["Start of Thread"]
thread -->|Anti-Jam Check| aj["AntiJam Logic"]
thread -->|Color Sensing| cs["Color Sort"]
thread -->|Piston Safety| es["Enforce Sizing"]
aj --> merge{ }
cs --> merge
es --> merge
merge -->|Move Motors| hw
subgraph hw["Write Hardware States"]
volt["Update Voltages"]
pist["Update Pistons"]
end
hw --> delay["Delay"]
delay --> thread
end
init --> thread
profile -->|Update Shared State| thread