Refactoring Lebot

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

  1. The Robot: Lebot
  2. Problems with the Original Code
  3. The Refactoring Plan
  4. Implementation Journal
  5. Before and After Comparisons
  6. 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

  1. Intake: Overhead belts at the front pull balls into the robot
  2. Funnel: Guides balls from wide intake into single-file chamber
  3. Loader Chamber: Side belt moves balls toward the rear; holds max 3 balls
  4. Paddle Gate: When down, stops balls at rear; when up, allows launch
  5. 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:

  1. INTAKE - Actively gathering balls
    • Overhead belts ON, side belt ON
    • Paddle DOWN (gate closed)
  2. TRANSIT - Driving to launch position
    • All ball handling OFF
    • Driver controls active
  3. TARGETING - Aligning with target
    • IMU-based rough turn
    • Limelight fine adjustment
  4. 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

  1. Swappable Drivetrain: Interface pattern allows TankDrive, MecanumDrive, SwerveDrive
  2. Loader owns belt: Side belt’s primary purpose is ball management
  3. Launcher owns paddle: Paddle’s critical function is launch triggering
  4. 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 TelemetryProvider
  • lebot2/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.java
  • lebot2/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 handling
  • lebot2/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 localization
  • lebot2/rr_localize/PinpointLocalizer.java - Pinpoint odometry wrapper
  • lebot2/rr_localize/TankDrivePinpoint.java - Pinpoint-based localization
  • lebot2/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.java
  • deepthought/rr_localize/tuning/TuningOpModes.java
  • swervolicious/rr_localize/tuning/TuningOpModes.java
  • lebot/PinpointLocalizer.java
  • swervolicious/rr_localize/PinpointLocalizer.java
  • deepthought/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.teamcodecom.qualcomm.hardware.gobilda) broke 6 files across 3 robots.

Lesson: When updating SDK versions:

  1. Read the release notes fully
  2. Search codebase for any imported classes that moved
  3. Test build immediately after SDK update
  4. 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:

  1. Configure Pinpoint PARAMS - Measure pod offsets for lebot2 chassis
  2. Tune feedforward gains - Run RoadRunner tuning routines
  3. Implement autonomous routines - Use RoadRunner trajectory builder
  4. Field test - Compare TankDrive vs TankDrivePinpoint accuracy
  5. Remove lebot/ - Once lebot2 is proven in competition

Last updated: January 2025