Refactoring Lebot: A Case Study
This document chronicles the refactoring of lebot into lebot2, applying the principles from our Coding Guidelines. It serves both as a learning resource and a reference for future refactoring projects.
Table of Contents
- The Robot: Lebot
- Problems with the Original Code
- The Refactoring Plan
- Implementation Journal
- Before and After Comparisons
- Lessons Learned
The Robot: Lebot
Lebot is a ball-launching robot designed for a game where robots collect balls and launch them at targets. Understanding the physical design is essential before discussing code architecture.
Physical Components
┌─────────────────────────────────────────────────────────────┐
│ ROBOT TOP VIEW │
│ │
│ [Left Motor] [Right Motor] │
│ ○ ○ │
│ [Flywheel] ←── Launches balls at calculated speed │
│ ↑ │
│ [Paddle] ←── Lifts ball into flywheel (also gate) │
│ ↑ │
│ [Back Sensor] ←── Detects ball ready to launch │
│ ↑ │
│ ┌─────────┐ │
│ │ LOADER │ ←── Side belt moves balls toward rear │
│ │ CHAMBER │ (holds up to 3 balls) │
│ └─────────┘ │
│ ↑ │
│ [Front Sensor] ←── Detects incoming balls │
│ ↑ │
│ [Intake] ←── Overhead belts pull balls in │
│ ↑ │
│ ══════════════ (funnel opening) │
│ │
│ [Omni Casters] │
│ ○ ○ │
└─────────────────────────────────────────────────────────────┘
Ball Flow Path
- Intake: Overhead belts at the front pull balls into the robot
- Funnel: Guides balls from wide intake into single-file chamber
- Loader Chamber: Side belt moves balls toward the rear; holds max 3 balls
- Paddle Gate: When down, stops balls at rear; when up, allows launch
- Flywheel: Spinning wheel that launches balls when paddle lifts them into contact
Hardware Inventory
| Component | Type | Purpose | |
|---|---|---|---|
| leftFront, rightFront | DcMotorEx | Differential drive | |
| intake | DcMotorEx | Overhead belt motor | |
| conveyor | DcMotorEx | Side belt motor (loader) | |
| launcher | DcMotorEx | Flywheel motor | likely to add a 2nd motor |
| paddle | Servo | Lifter/gate mechanism | |
| frontDist | Rev2mDistanceSensor | Detects balls entering | |
| backDist | Rev2mDistanceSensor | Detects balls ready to launch | |
| limelight | Limelight3A | Vision targeting | |
| imu | BNO055IMU | Heading for turns |
Operating Modes
The robot operates in distinct modes during a match:
- INTAKE - Actively gathering balls
- Overhead belts ON, side belt ON
- Paddle DOWN (gate closed)
- TRANSIT - Driving to launch position
- All ball handling OFF
- Driver controls active
- TARGETING - Aligning with target
- IMU-based rough turn
- Limelight fine adjustment
- LAUNCHING - Firing balls
- Flywheel spins to calculated speed
- Paddle lifts ball into flywheel
- Side belt feeds next ball
- Repeat until empty
Key Constraints
- Max 3 balls: Game rule AND physical limit
- Flywheel must be at speed: Ball contact before speed = jam
- Speed depends on distance: Calculated from Limelight measurements
- Dual-role components:
- Side belt: intake helper AND launch feeder
- Paddle: launch trigger AND loader gate
Problems with the Original Code
The original lebot/Robot.java (746 lines) exhibits several anti-patterns identified in our Coding Guidelines.
1. God Class Anti-Pattern
The Robot class handles everything:
- Drivetrain control
- Intake motors
- Flywheel and paddle
- Limelight vision
- Distance sensors
- Ball counting
- Multiple state machines
Guideline Violated: “Break the robot into subsystems - independent units that each handle one responsibility.”
2. Integer-Based State Machines
// Original code
public int index = 0;
public void shootSequence() {
switch (index) {
case 0: // what does 0 mean?
index++;
break;
case 1: // and 1?
// ...
}
}
Problems:
- Magic numbers (what is case 4?)
- Hard to read and debug
- Easy to make off-by-one errors
Guideline Violated: “Use enums for state machine states.”
3. Duplicate Hardware Initialization
IMU initialized in both Robot.java AND tankDrive.java:
// In Robot.java
imu = hardwareMap.get(BNO055IMU.class, "imu");
// In tankDrive.java
imu = hardwareMap.get(BNO055IMU.class, "imu");
Guideline Violated: “Each piece of hardware should be owned by exactly one subsystem.”
4. Mixed Sensor Reads and Logic
// Original - reads scattered throughout
public void update(Canvas fieldOverlay) {
LLResult llResult = limelight.getLatestResult(); // read
if (llResult != null && llResult.isValid()) { // logic
botpose = llResult.getBotpose_MT2(); // more logic
}
updateDistance(); // another read
if (shootingAll) {
shootALLSequence(); // logic using reads
}
// ...
}
Guideline Violated: “Follow the Read→Process→Write pattern.”
5. Direct Dashboard Calls in Telemetry
// Original - side effect in getTelemetry
public Map<String, Object> getTelemetry(boolean debug) {
// ... build map ...
dashboard.sendTelemetryPacket(p); // WRONG! Side effect!
return telemetry2;
}
Guideline Violated: “Telemetry should be collected, not transmitted, by subsystems.”
The Refactoring Plan
New Architecture
lebot2/
├── Robot.java # Coordinator, complex behaviors
├── DriverControls.java # Gamepad handling
├── Lebot2_6832.java # Unified OpMode (Auton+TeleOp)
├── subsystem/
│ ├── Subsystem.java # Interface
│ ├── drivetrain/
│ │ ├── DriveTrainBase.java # Interface for swappable drivetrains
│ │ └── TankDrive.java # Differential drive implementation
│ ├── Intake.java # Overhead belts only
│ ├── Loader.java # Side belt + sensors + ball counting
│ ├── Launcher.java # Flywheel + paddle + launch sequence
│ └── Vision.java # Limelight
└── util/
└── (shared utilities)
Subsystem Ownership
| Subsystem | Hardware Owned | Primary Responsibility |
|---|---|---|
| TankDrive | leftFront, rightFront, IMU, Pinpoint | Locomotion, heading control |
| Intake | intake motor | Pull balls into robot |
| Loader | conveyor motor, frontDist, backDist | Ball counting, feeding |
| Launcher | shooter motor, paddle servo | Speed calc, launch sequence |
| Vision | limelight | Target detection |
Design Decisions
- Swappable Drivetrain: Interface pattern allows TankDrive, MecanumDrive, SwerveDrive
- Loader owns belt: Side belt’s primary purpose is ball management
- Launcher owns paddle: Paddle’s critical function is launch triggering
- Cross-subsystem coordination: Robot class orchestrates multi-subsystem behaviors
Implementation Journal
Step 1: Directory Structure and Interfaces
Completed: January 2025
Created the lebot2 directory structure and copied the Subsystem interface pattern from deepthought.
Files created:
lebot2/subsystem/Subsystem.java- Base interface extending TelemetryProviderlebot2/util/TelemetryProvider.java- Telemetry collection interface
The Subsystem interface:
public interface Subsystem extends TelemetryProvider {
void update(Canvas fieldOverlay);
void stop();
void resetStates();
}
Step 2: DriveTrainBase Interface and TankDrive
Completed: January 2025
Created swappable drivetrain architecture.
Key decisions:
- Interface allows future chassis types (MecanumDrive, SwerveDrive)
- TankDrive ignores strafe parameter (differential drive limitation)
- Heading control via PID for autonomous turns
Files created:
lebot2/subsystem/drivetrain/DriveTrainBase.javalebot2/subsystem/drivetrain/TankDrive.java
DriveTrainBase interface:
public interface DriveTrainBase extends Subsystem {
void drive(double throttle, double strafe, double turn);
void setHeadingTarget(double degrees);
void setLimelightTarget(double tx);
boolean isTurning();
double getHeading();
Pose2d getPose();
void setPose(Pose2d pose);
}
Step 3: Simple Subsystems (Intake)
Completed: January 2025
The Intake subsystem is the simplest - just overhead belts with on/off control.
Files created:
lebot2/subsystem/Intake.java
Key pattern: Simple subsystems don’t need state machines. Just provide control methods and let the coordinator decide when to use them.
Step 4: Loader Subsystem (Belt + Sensors + Ball Counting)
Completed: January 2025
The Loader manages ball flow through the robot with two distance sensors.
Files created:
lebot2/subsystem/Loader.java
Ball counting logic:
public enum LoaderState {
EMPTY,
HAS_BALLS,
FULL
}
// Front sensor: ball entering
// Back sensor: ball ready to launch
// Rising edge detection for accurate counting
Sensor ownership decision: Loader owns both distance sensors because their primary purpose is ball management, even though the back sensor is read during launch.
Step 5: Launcher Subsystem (Flywheel + Paddle)
Completed: January 2025
The most complex subsystem with a proper enum-based state machine.
Files created:
lebot2/subsystem/Launcher.java
Launch state machine:
public enum LaunchState {
IDLE, // Waiting for command
SPINNING_UP, // Flywheel ramping to target speed
READY, // At speed, waiting for fire command
FIRING, // Paddle up, ball launching
COOLDOWN // Reset before next ball
}
Distance-based speed calculation: The flywheel speed is calculated from target distance (via Limelight) to ensure consistent arc trajectories.
Step 6: Vision Subsystem
Completed: January 2025
Wraps Limelight3A for target detection.
Files created:
lebot2/subsystem/Vision.java
Features:
- Pipeline switching for alliance colors
- Target detection with tx/ty values
- Distance calculation from ty angle
Step 7: Robot Coordinator
Completed: January 2025
The Robot class orchestrates multi-subsystem behaviors through articulations.
Files created:
lebot2/Robot.java
Articulation enum:
public enum Articulation {
MANUAL, // Direct driver control
INTAKE, // Gathering balls
TRANSIT, // Driving to position
TARGETING, // Aligning with target
LAUNCHING, // Single ball launch
LAUNCH_ALL, // Multi-ball sequence
RELEASE // Emergency release
}
Cross-subsystem coordination example:
case LAUNCHING:
if (launcher.getState() == Launcher.LaunchState.READY) {
launcher.fire();
}
if (launcher.getState() == Launcher.LaunchState.COOLDOWN) {
loader.feedForward();
articulation = Articulation.MANUAL;
}
break;
Step 8: DriverControls and Unified OpMode
Completed: January 2025
Files created:
lebot2/DriverControls.java- Gamepad input handlinglebot2/Lebot2_6832.java- Unified OpMode (Auton+TeleOp)
Unified OpMode pattern (from deepthought):
public enum GameState {
AUTONOMOUS,
TELE_OP,
TEST,
DEMO
}
// Same initialization for all modes
// GameState selected during init_loop
// No controller input during competition autonomous
Step 9: RoadRunner Integration
Completed: January 2025
Integrated RoadRunner v1.0 trajectory following with dual localization options.
Files created:
lebot2/rr_localize/TankDrive.java- Drive wheel encoder localizationlebot2/rr_localize/PinpointLocalizer.java- Pinpoint odometry wrapperlebot2/rr_localize/TankDrivePinpoint.java- Pinpoint-based localizationlebot2/rr_localize/Localizer.java(interface from rrQuickStart)lebot2/rr_localize/tuning/TuningOpModes.java- Tuning utilities
Dual localization approach:
| Class | Localization | Use Case |
|——-|————–|———-|
| TankDrive | Drive wheel encoders | Testing, comparison baseline |
| TankDrivePinpoint | Pinpoint odometry pods | Competition (more accurate) |
Why two options? Drive wheel encoders can slip, especially on mecanum or when pushing. Dedicated odometry pods provide more reliable position tracking.
Motor configuration (for lebot2 chassis):
// Differential drive with rear-mounted motors
leftMotors = Arrays.asList(hardwareMap.get(DcMotorEx.class, "leftRear"));
rightMotors = Arrays.asList(hardwareMap.get(DcMotorEx.class, "rightRear"));
// Left motor reversed so positive power = forward
leftMotors.get(0).setDirection(DcMotorSimple.Direction.REVERSE);
Step 10: SDK Compatibility Fixes
Completed: January 2025
Updated imports for GoBildaPinpointDriver after SDK update.
The issue: The 2024-2025 SDK moved GoBildaPinpointDriver from org.firstinspires.ftc.teamcode to com.qualcomm.hardware.gobilda.
Files fixed (6 total across robots):
lebot2/rr_localize/tuning/TuningOpModes.javadeepthought/rr_localize/tuning/TuningOpModes.javaswervolicious/rr_localize/tuning/TuningOpModes.javalebot/PinpointLocalizer.javaswervolicious/rr_localize/PinpointLocalizer.javadeepthought/rr_localize/PinpointLocalizer.java
Old import:
import org.firstinspires.ftc.teamcode.GoBildaPinpointDriver;
New import:
import com.qualcomm.hardware.gobilda.GoBildaPinpointDriver;
Lesson: When SDK versions change, check for moved/renamed classes in the release notes.
Before and After Comparisons
State Machine: Integer vs Enum
Before (lebot/Robot.java):
public int index = 0;
public void shootSequence() {
switch (index) {
case 0:
index++;
break;
case 1:
shooter.setVelocity(minShooterSpeed, AngleUnit.DEGREES);
index++;
break;
// ... cases 2, 3, 4, 5 ...
}
}
After (lebot2/subsystem/Launcher.java):
public enum LaunchState {
IDLE,
SPINNING_UP,
READY,
FIRING,
COOLDOWN
}
private LaunchState state = LaunchState.IDLE;
@Override
public void update(Canvas fieldOverlay) {
switch (state) {
case IDLE:
// Clear state, nothing to do
break;
case SPINNING_UP:
if (isFlywheelAtSpeed()) {
state = LaunchState.READY;
}
break;
// ... etc
}
}
Why it’s better:
- Self-documenting (SPINNING_UP vs case 1)
- IDE autocomplete support
- Compiler catches typos
- Easy to add/remove states
Lessons Learned
1. Hardware Ownership Matters
Deciding which subsystem owns dual-role components requires understanding the highest-priority use case.
Example: The paddle servo could belong to either Loader or Launcher:
- Loader perspective: It’s a gate that stops balls
- Launcher perspective: It’s the trigger that fires balls
Decision: Launcher owns it because the timing-critical function (coordinating with flywheel speed) is more important than the passive gate function.
2. State Machines Need Clear Entry/Exit Conditions
Each state transition should have documented:
- What condition triggers entry
- What actions happen on entry
- What condition triggers exit
Anti-pattern: States that transition “after a delay” without explaining why.
Good pattern:
case SPINNING_UP:
// Entry: prepareToLaunch() called with distance
// Exit: Flywheel reaches target velocity (±5%)
if (isFlywheelAtSpeed()) {
state = LaunchState.READY;
}
break;
3. Cross-Subsystem Coordination Belongs in Robot
A subsystem should never directly call methods on another subsystem. The Robot coordinator handles all multi-subsystem behaviors.
Why? If Launcher directly called loader.feedForward(), then:
- Launcher needs a reference to Loader
- Testing Launcher requires mocking Loader
- The dependency graph becomes tangled
Better: Robot watches both subsystems and triggers the feed when appropriate.
4. Swappable Interfaces Enable Experimentation
The DriveTrainBase interface lets us test different chassis configurations:
- TankDrive for the ball launcher
- MecanumDrive for holonomic movement
- SwerveDrive for ultimate flexibility
Same game code, different locomotion.
5. Keep Original Code As Reference
We kept lebot/ intact while building lebot2/. This provided:
- A working baseline if refactoring broke things
- Side-by-side comparison for learning
- Easy reference for hardware names and values
Don’t delete the old code until the new code is competition-tested.
6. Dual Localization Options Are Worth The Effort
Creating both TankDrive (encoder-based) and TankDrivePinpoint variants:
- Allows comparison testing on the actual robot
- Provides fallback if Pinpoint has issues
- Teaches team members how localization works
The small extra effort pays off in debugging time saved.
7. SDK Updates Break Things
The GoBildaPinpointDriver package move (org.firstinspires.ftc.teamcode → com.qualcomm.hardware.gobilda) broke 6 files across 3 robots.
Lesson: When updating SDK versions:
- Read the release notes fully
- Search codebase for any imported classes that moved
- Test build immediately after SDK update
- Commit SDK update separately from other changes
8. Unified OpMode Reduces Duplication
Having one OpMode that handles both Autonomous and TeleOp:
- Single initialization path (less bugs)
- Easy mode switching for testing
- Shared telemetry dashboard
- Natural transition from auton to teleop
The GameState enum makes this clean and explicit.
Next Steps
Remaining work to complete the refactoring:
- Configure Pinpoint PARAMS - Measure pod offsets for lebot2 chassis
- Tune feedforward gains - Run RoadRunner tuning routines
- Implement autonomous routines - Use RoadRunner trajectory builder
- Field test - Compare TankDrive vs TankDrivePinpoint accuracy
- Remove lebot/ - Once lebot2 is proven in competition
Last updated: January 2025
