Debugging Strategies for FTC Development

Introduction to Systematic Debugging

Effective debugging is not just about finding bugs—it's about developing a systematic approach to problem-solving. In FTC development, you need strategies that work both in the lab and under competition pressure. This lesson teaches you how to approach debugging methodically and efficiently.

Why Systematic Debugging Matters

  • FTC competitions have limited time for debugging and testing
  • Hardware-software interactions create complex failure modes
  • Systematic approaches prevent you from making the same mistakes
  • Good debugging skills transfer to other programming domains
  • Competition environments require quick problem resolution

The Scientific Method of Debugging

Apply the scientific method to debugging: observe the problem, form a hypothesis, test it, and refine your understanding. This systematic approach helps you avoid random trial-and-error debugging.

Debugging Framework - Core Structure

public class DebuggingFramework {
    private static final String TAG = "DebugFramework";
    private boolean debugMode = true;
    private List<String> debugLog = new ArrayList<>();
    
    /**
     * Step 1: Observe and Document the Problem
     */
    public void observeProblem(String problemDescription, Object... data) {
        String observation = String.format("OBSERVATION: %s | Data: %s", 
                                          problemDescription, 
                                          Arrays.toString(data));
        logDebug(observation);
        debugLog.add(observation);
    }
    
    /**
     * Step 2: Form a Hypothesis
     */
    public void formHypothesis(String hypothesis, String expectedOutcome) {
        String hypothesisEntry = String.format("HYPOTHESIS: %s | Expected: %s", 
                                              hypothesis, 
                                              expectedOutcome);
        logDebug(hypothesisEntry);
        debugLog.add(hypothesisEntry);
    }

Understanding the Scientific Method in Debugging

The scientific method provides a systematic approach to debugging. First, you observe and document the problem with specific data. Then you form a hypothesis about what might be causing the issue. This structured approach prevents random trial-and-error debugging.

Debugging Framework - Testing and Conclusion Methods

    /**
     * Step 3: Test the Hypothesis
     */
    public void testHypothesis(String testDescription, boolean result, Object actualOutcome) {
        String testResult = String.format("TEST: %s | Result: %s | Actual: %s", 
                                         testDescription, 
                                         result ? "PASS" : "FAIL", 
                                         actualOutcome);
        logDebug(testResult);
        debugLog.add(testResult);
    }
    
    /**
     * Step 4: Document the Conclusion
     */
    public void documentConclusion(String conclusion, String nextSteps) {
        String conclusionEntry = String.format("CONCLUSION: %s | Next: %s", 
                                              conclusion, 
                                              nextSteps);
        logDebug(conclusionEntry);
        debugLog.add(conclusionEntry);
    }
    
    private void logDebug(String message) {
        if (debugMode) {
            Log.d(TAG, message);
        }
    }
    
    public List<String> getDebugLog() {
        return new ArrayList<>(debugLog);
    }
    
    public void clearDebugLog() {
        debugLog.clear();
    }
}

Completing the Debugging Cycle

The testing and conclusion methods complete the debugging cycle. Testing verifies whether your hypothesis was correct, and conclusions document what you learned and what steps to take next. This creates a record that helps with future debugging and team communication.

Common FTC Debugging Scenarios

FTC development has specific challenges that require targeted debugging strategies. Understanding these common scenarios helps you develop effective debugging approaches.

Hardware Debugger - Basic Structure

public class HardwareDebugger {
    private HardwareMap hardwareMap;
    private Telemetry telemetry;
    private DebuggingFramework debugger;
    
    public HardwareDebugger(HardwareMap hardwareMap, Telemetry telemetry) {
        this.hardwareMap = hardwareMap;
        this.telemetry = telemetry;
        this.debugger = new DebuggingFramework();
    }
    
    public boolean debugMotorConnection(String motorName) {
        debugger.observeProblem("Motor " + motorName + " not responding");
        
        // Step 1: Check if motor exists in hardware map
        debugger.formHypothesis("Motor not found in hardware map", "HardwareMap.get() throws exception");
        
        try {
            DcMotor motor = hardwareMap.get(DcMotor.class, motorName);
            debugger.testHypothesis("Motor exists in hardware map", true, motor.getDeviceName());
            
            // Step 2: Check if motor responds to basic commands
            debugger.formHypothesis("Motor hardware connection issue", "setPower() should work without exception");
            
            try {
                motor.setPower(0.1);
                Thread.sleep(100); // Brief test
                motor.setPower(0.0);
                debugger.testHypothesis("Motor responds to commands", true, "Power set successfully");
                
                debugger.documentConclusion("Motor connection is working", "Check software logic");
                return true;
                
            } catch (Exception e) {
                debugger.testHypothesis("Motor responds to commands", false, e.getMessage());
                debugger.documentConclusion("Motor hardware connection issue", "Check wiring and power");
                return false;
            }
            
        } catch (Exception e) {
            debugger.testHypothesis("Motor exists in hardware map", false, e.getMessage());
            debugger.documentConclusion("Motor not configured in hardware map", "Check configuration file");
            return false;
        }
    }

Understanding Hardware Debugging

The hardware debugger systematically tests each component of the motor connection. First, it checks if the motor exists in the hardware map. Then it tests if the motor responds to basic commands. This helps identify whether the issue is configuration, wiring, or software-related.

Hardware Debugger - Sensor Connection Methods

    public boolean debugSensorConnection(String sensorName, Class<?> sensorType) {
        debugger.observeProblem("Sensor " + sensorName + " not providing data");
        
        try {
            Object sensor = hardwareMap.get(sensorType, sensorName);
            debugger.testHypothesis("Sensor exists in hardware map", true, sensor.getClass().getSimpleName());
            
            // Test sensor functionality based on type
            if (sensor instanceof ColorSensor) {
                return debugColorSensor((ColorSensor) sensor, sensorName);
            } else if (sensor instanceof DistanceSensor) {
                return debugDistanceSensor((DistanceSensor) sensor, sensorName);
            }
            
            return true;
            
        } catch (Exception e) {
            debugger.testHypothesis("Sensor exists in hardware map", false, e.getMessage());
            debugger.documentConclusion("Sensor not configured", "Check configuration file");
            return false;
        }
    }

Understanding Sensor Debugging

The sensor debugging method first checks if the sensor exists in the hardware map, then calls specific debugging methods based on the sensor type. This allows for customized testing for each type of sensor.

Hardware Debugger - Specific Sensor Debug Methods

    private boolean debugColorSensor(ColorSensor sensor, String sensorName) {
        debugger.formHypothesis("Color sensor not reading values", "getRed(), getGreen(), getBlue() should return values");
        
        try {
            int red = sensor.red();
            int green = sensor.green();
            int blue = sensor.blue();
            
            String reading = String.format("R:%d G:%d B:%d", red, green, blue);
            debugger.testHypothesis("Color sensor reading values", true, reading);
            
            debugger.documentConclusion("Color sensor working", "Check sensor positioning and lighting");
            return true;
            
        } catch (Exception e) {
            debugger.testHypothesis("Color sensor reading values", false, e.getMessage());
            debugger.documentConclusion("Color sensor hardware issue", "Check wiring and power");
            return false;
        }
    }
    
    private boolean debugDistanceSensor(DistanceSensor sensor, String sensorName) {
        debugger.formHypothesis("Distance sensor not reading", "getDistance() should return distance value");
        
        try {
            double distance = sensor.getDistance(DistanceUnit.INCH);
            debugger.testHypothesis("Distance sensor reading", true, "Distance: " + distance + " inches");
            
            debugger.documentConclusion("Distance sensor working", "Check sensor positioning and calibration");
            return true;
            
        } catch (Exception e) {
            debugger.testHypothesis("Distance sensor reading", false, e.getMessage());
            debugger.documentConclusion("Distance sensor hardware issue", "Check wiring and power");
            return false;
        }
    }
    
    public void printDebugLog() {
        List<String> log = debugger.getDebugLog();
        telemetry.addLine("=== HARDWARE DEBUG LOG ===");
        for (String entry : log) {
            telemetry.addLine(entry);
        }
        telemetry.update();
    }
}

Understanding Sensor-Specific Debugging

Each sensor type has different testing requirements. Color sensors need to read RGB values, and distance sensors need to return distance measurements. These specific methods ensure each sensor type is tested appropriately.

State Machine Debugging

State machines are common in FTC autonomous programs but can be difficult to debug. Use systematic approaches to track state transitions and identify issues.

Debuggable State Machine - Core Structure

public class DebuggableStateMachine<T extends Enum<T>> {
    private T currentState;
    private T previousState;
    private double stateStartTime;
    private double currentTime;
    private List<StateTransition> transitionHistory = new ArrayList<>();
    private DebuggingFramework debugger;
    
    public DebuggableStateMachine(T initialState, DebuggingFramework debugger) {
        this.currentState = initialState;
        this.previousState = initialState;
        this.debugger = debugger;
        this.stateStartTime = 0.0;
        this.currentTime = 0.0;
    }
    
    public void update(double time) {
        this.currentTime = time;
        
        // Log state duration if it's been too long
        double stateDuration = time - stateStartTime;
        if (stateDuration > 10.0) { // 10 second timeout
            debugger.observeProblem("State " + currentState + " has been active for " + stateDuration + " seconds");
            debugger.formHypothesis("State transition condition not met", "Check transition logic");
        }
    }

Understanding State Machine Debugging

The debuggable state machine tracks the current state, previous state, and how long each state has been active. It automatically detects when a state has been active too long, which often indicates a bug in the transition logic.

Debuggable State Machine - Transition and History Methods

    public void transitionTo(T newState, String reason) {
        if (newState != currentState) {
            previousState = currentState;
            double stateDuration = currentTime - stateStartTime;
            
            // Record the transition
            StateTransition transition = new StateTransition(
                previousState, newState, reason, stateDuration, currentTime
            );
            transitionHistory.add(transition);
            
            // Log the transition
            debugger.observeProblem("State transition: " + previousState + " -> " + newState + " (" + reason + ")");
            
            currentState = newState;
            stateStartTime = currentTime;
        }
    }
    
    public T getCurrentState() {
        return currentState;
    }
    
    public T getPreviousState() {
        return previousState;
    }
    
    public double getStateDuration() {
        return currentTime - stateStartTime;
    }
    
    public List<StateTransition> getTransitionHistory() {
        return new ArrayList<>(transitionHistory);
    }

Understanding State Transitions

The transitionTo method records every state change with the reason for the transition and how long the previous state was active. This creates a complete history that helps identify patterns in state machine behavior.

Debuggable State Machine - Debug Display and Helper Class

    public void debugStateMachine(Telemetry telemetry) {
        telemetry.addLine("=== STATE MACHINE DEBUG ===");
        telemetry.addData("Current State", currentState);
        telemetry.addData("Previous State", previousState);
        telemetry.addData("State Duration", String.format("%.2fs", getStateDuration()));
        telemetry.addData("Total Time", String.format("%.2fs", currentTime));
        
        telemetry.addLine("\nRecent Transitions:");
        int startIndex = Math.max(0, transitionHistory.size() - 5);
        for (int i = startIndex; i < transitionHistory.size(); i++) {
            StateTransition t = transitionHistory.get(i);
            telemetry.addLine(String.format("%.1fs: %s -> %s (%s)", 
                t.timestamp, t.fromState, t.toState, t.reason));
        }
        
        telemetry.update();
    }
    
    private static class StateTransition {
        T fromState, toState;
        String reason;
        double duration, timestamp;
        
        StateTransition(T from, T to, String reason, double duration, double timestamp) {
            this.fromState = from;
            this.toState = to;
            this.reason = reason;
            this.duration = duration;
            this.timestamp = timestamp;
        }
    }
}

Understanding State Machine Display

The debugStateMachine method displays comprehensive information about the state machine's current status and recent history. The StateTransition helper class stores all the details about each state change for analysis.

Using the Debuggable State Machine

Now let's see how to use the debuggable state machine in an actual autonomous OpMode. This example shows how to integrate the debugging framework with real robot behavior.

Debuggable Autonomous OpMode - Setup and Initialization

public class DebuggableAutonomousOpMode extends OpMode {
    private enum AutonomousState {
        INIT, DRIVE_FORWARD, TURN_LEFT, DETECT_COLOR, COMPLETE
    }
    
    private DebuggableStateMachine<AutonomousState> stateMachine;
    private DebuggingFramework debugger;
    private HardwareDebugger hardwareDebugger;
    
    private DcMotor leftMotor, rightMotor;
    private ColorSensor colorSensor;
    private double startTime;
    
    @Override
    public void init() {
        debugger = new DebuggingFramework();
        stateMachine = new DebuggableStateMachine<>(AutonomousState.INIT, debugger);
        hardwareDebugger = new HardwareDebugger(hardwareMap, telemetry);
        
        // Debug hardware connections
        debugger.observeProblem("Initializing autonomous sequence");
        
        boolean motorsOK = hardwareDebugger.debugMotorConnection("left_motor") &&
                           hardwareDebugger.debugMotorConnection("right_motor");
        
        boolean sensorOK = hardwareDebugger.debugSensorConnection("color_sensor", ColorSensor.class);
        
        if (!motorsOK || !sensorOK) {
            debugger.documentConclusion("Hardware issues detected", "Fix hardware before proceeding");
            return;
        }
        
        // Initialize hardware
        leftMotor = hardwareMap.get(DcMotor.class, "left_motor");
        rightMotor = hardwareMap.get(DcMotor.class, "right_motor");
        colorSensor = hardwareMap.get(ColorSensor.class, "color_sensor");
        
        debugger.documentConclusion("Hardware initialized successfully", "Ready to start autonomous");
    }

Understanding Autonomous Setup

The initialization method sets up all the debugging components and tests hardware connections before starting the autonomous sequence. This ensures that any hardware issues are identified early.

Debuggable Autonomous OpMode - Main Loop and State Handling

    @Override
    public void start() {
        startTime = getRuntime();
        stateMachine.transitionTo(AutonomousState.DRIVE_FORWARD, "Starting autonomous sequence");
        debugger.observeProblem("Autonomous sequence started");
    }
    
    @Override
    public void loop() {
        double currentTime = getRuntime();
        stateMachine.update(currentTime);
        
        AutonomousState currentState = stateMachine.getCurrentState();
        
        switch (currentState) {
            case INIT:
                // Should not reach here
                debugger.observeProblem("Unexpected INIT state in loop");
                break;
                
            case DRIVE_FORWARD:
                // Drive forward for 2 seconds
                if (currentTime - startTime < 2.0) {
                    leftMotor.setPower(0.5);
                    rightMotor.setPower(0.5);
                } else {
                    leftMotor.setPower(0.0);
                    rightMotor.setPower(0.0);
                    stateMachine.transitionTo(AutonomousState.TURN_LEFT, "Drive forward complete");
                }
                break;
                
            case TURN_LEFT:
                // Turn left for 1 second
                if (stateMachine.getStateDuration() < 1.0) {
                    leftMotor.setPower(-0.3);
                    rightMotor.setPower(0.3);
                } else {
                    leftMotor.setPower(0.0);
                    rightMotor.setPower(0.0);
                    stateMachine.transitionTo(AutonomousState.DETECT_COLOR, "Turn complete");
                }
                break;
                
            case DETECT_COLOR:
                // Check for red color
                int red = colorSensor.red();
                int green = colorSensor.green();
                int blue = colorSensor.blue();
                
                if (red > green && red > blue && red > 100) {
                    debugger.observeProblem("Red color detected: R=" + red + " G=" + green + " B=" + blue);
                    stateMachine.transitionTo(AutonomousState.COMPLETE, "Red color detected");
                } else if (stateMachine.getStateDuration() > 5.0) {
                    debugger.observeProblem("Color detection timeout after 5 seconds");
                    stateMachine.transitionTo(AutonomousState.COMPLETE, "Color detection timeout");
                }
                break;
                
            case COMPLETE:
                // Stop all motors
                leftMotor.setPower(0.0);
                rightMotor.setPower(0.0);
                
                if (stateMachine.getStateDuration() > 1.0) {
                    requestOpModeStop();
                }
                break;
        }
        
        // Display debug information
        stateMachine.debugStateMachine(telemetry);
        telemetry.addLine("");
        hardwareDebugger.printDebugLog();
    }
}

Understanding State Machine Logic

Each state in the autonomous sequence has specific logic and transition conditions. The debugging framework tracks all state changes and provides detailed information about what's happening at each step.

Complete Debugging Framework Example

import com.qualcomm.robotcore.eventloop.opmode.OpMode;
import com.qualcomm.robotcore.eventloop.opmode.Autonomous;
import com.qualcomm.robotcore.hardware.DcMotor;
import com.qualcomm.robotcore.hardware.ColorSensor;
import com.qualcomm.robotcore.hardware.DistanceSensor;
import com.qualcomm.robotcore.util.DistanceUnit;
import android.util.Log;
import java.util.*;

@Autonomous(name = "Debugging Framework Example")
public class DebuggingFrameworkExample extends OpMode {
    private enum AutonomousState {
        INIT, DRIVE_FORWARD, TURN_LEFT, DETECT_COLOR, COMPLETE
    }
    
    private DebuggableStateMachine<AutonomousState> stateMachine;
    private DebuggingFramework debugger;
    private HardwareDebugger hardwareDebugger;
    
    private DcMotor leftMotor, rightMotor;
    private ColorSensor colorSensor;
    private double startTime;
    
    @Override
    public void init() {
        debugger = new DebuggingFramework();
        stateMachine = new DebuggableStateMachine<>(AutonomousState.INIT, debugger);
        hardwareDebugger = new HardwareDebugger(hardwareMap, telemetry);
        
        // Debug hardware connections
        debugger.observeProblem("Initializing autonomous sequence");
        
        boolean motorsOK = hardwareDebugger.debugMotorConnection("left_motor") &&
                           hardwareDebugger.debugMotorConnection("right_motor");
        
        boolean sensorOK = hardwareDebugger.debugSensorConnection("color_sensor", ColorSensor.class);
        
        if (!motorsOK || !sensorOK) {
            debugger.documentConclusion("Hardware issues detected", "Fix hardware before proceeding");
            return;
        }
        
        // Initialize hardware
        leftMotor = hardwareMap.get(DcMotor.class, "left_motor");
        rightMotor = hardwareMap.get(DcMotor.class, "right_motor");
        colorSensor = hardwareMap.get(ColorSensor.class, "color_sensor");
        
        debugger.documentConclusion("Hardware initialized successfully", "Ready to start autonomous");
    }
    
    @Override
    public void start() {
        startTime = getRuntime();
        stateMachine.transitionTo(AutonomousState.DRIVE_FORWARD, "Starting autonomous sequence");
        debugger.observeProblem("Autonomous sequence started");
    }
    
    @Override
    public void loop() {
        double currentTime = getRuntime();
        stateMachine.update(currentTime);
        
        AutonomousState currentState = stateMachine.getCurrentState();
        
        switch (currentState) {
            case INIT:
                debugger.observeProblem("Unexpected INIT state in loop");
                break;
                
            case DRIVE_FORWARD:
                if (currentTime - startTime < 2.0) {
                    leftMotor.setPower(0.5);
                    rightMotor.setPower(0.5);
                } else {
                    leftMotor.setPower(0.0);
                    rightMotor.setPower(0.0);
                    stateMachine.transitionTo(AutonomousState.TURN_LEFT, "Drive forward complete");
                }
                break;
                
            case TURN_LEFT:
                if (stateMachine.getStateDuration() < 1.0) {
                    leftMotor.setPower(-0.3);
                    rightMotor.setPower(0.3);
                } else {
                    leftMotor.setPower(0.0);
                    rightMotor.setPower(0.0);
                    stateMachine.transitionTo(AutonomousState.DETECT_COLOR, "Turn complete");
                }
                break;
                
            case DETECT_COLOR:
                int red = colorSensor.red();
                int green = colorSensor.green();
                int blue = colorSensor.blue();
                
                if (red > green && red > blue && red > 100) {
                    debugger.observeProblem("Red color detected: R=" + red + " G=" + green + " B=" + blue);
                    stateMachine.transitionTo(AutonomousState.COMPLETE, "Red color detected");
                } else if (stateMachine.getStateDuration() > 5.0) {
                    debugger.observeProblem("Color detection timeout after 5 seconds");
                    stateMachine.transitionTo(AutonomousState.COMPLETE, "Color detection timeout");
                }
                break;
                
            case COMPLETE:
                leftMotor.setPower(0.0);
                rightMotor.setPower(0.0);
                
                if (stateMachine.getStateDuration() > 1.0) {
                    requestOpModeStop();
                }
                break;
        }
        
        // Display debug information
        stateMachine.debugStateMachine(telemetry);
        telemetry.addLine("");
        hardwareDebugger.printDebugLog();
    }
    
    // Debugging Framework Classes
    public static class DebuggingFramework {
        private static final String TAG = "DebugFramework";
        private boolean debugMode = true;
        private List<String> debugLog = new ArrayList<>();
        
        public void observeProblem(String problemDescription, Object... data) {
            String observation = String.format("OBSERVATION: %s | Data: %s", 
                                              problemDescription, 
                                              Arrays.toString(data));
            logDebug(observation);
            debugLog.add(observation);
        }
        
        public void formHypothesis(String hypothesis, String expectedOutcome) {
            String hypothesisEntry = String.format("HYPOTHESIS: %s | Expected: %s", 
                                                  hypothesis, 
                                                  expectedOutcome);
            logDebug(hypothesisEntry);
            debugLog.add(hypothesisEntry);
        }
        
        public void testHypothesis(String testDescription, boolean result, Object actualOutcome) {
            String testResult = String.format("TEST: %s | Result: %s | Actual: %s", 
                                             testDescription, 
                                             result ? "PASS" : "FAIL", 
                                             actualOutcome);
            logDebug(testResult);
            debugLog.add(testResult);
        }
        
        public void documentConclusion(String conclusion, String nextSteps) {
            String conclusionEntry = String.format("CONCLUSION: %s | Next: %s", 
                                                  conclusion, 
                                                  nextSteps);
            logDebug(conclusionEntry);
            debugLog.add(conclusionEntry);
        }
        
        private void logDebug(String message) {
            if (debugMode) {
                Log.d(TAG, message);
            }
        }
        
        public List<String> getDebugLog() {
            return new ArrayList<>(debugLog);
        }
        
        public void clearDebugLog() {
            debugLog.clear();
        }
    }
    
    public static class HardwareDebugger {
        private HardwareMap hardwareMap;
        private Telemetry telemetry;
        private DebuggingFramework debugger;
        
        public HardwareDebugger(HardwareMap hardwareMap, Telemetry telemetry) {
            this.hardwareMap = hardwareMap;
            this.telemetry = telemetry;
            this.debugger = new DebuggingFramework();
        }
        
        public boolean debugMotorConnection(String motorName) {
            debugger.observeProblem("Motor " + motorName + " not responding");
            
            try {
                DcMotor motor = hardwareMap.get(DcMotor.class, motorName);
                debugger.testHypothesis("Motor exists in hardware map", true, motor.getDeviceName());
                
                try {
                    motor.setPower(0.1);
                    Thread.sleep(100);
                    motor.setPower(0.0);
                    debugger.testHypothesis("Motor responds to commands", true, "Power set successfully");
                    debugger.documentConclusion("Motor connection is working", "Check software logic");
                    return true;
                    
                } catch (Exception e) {
                    debugger.testHypothesis("Motor responds to commands", false, e.getMessage());
                    debugger.documentConclusion("Motor hardware connection issue", "Check wiring and power");
                    return false;
                }
                
            } catch (Exception e) {
                debugger.testHypothesis("Motor exists in hardware map", false, e.getMessage());
                debugger.documentConclusion("Motor not configured in hardware map", "Check configuration file");
                return false;
            }
        }
        
        public boolean debugSensorConnection(String sensorName, Class<?> sensorType) {
            debugger.observeProblem("Sensor " + sensorName + " not providing data");
            
            try {
                Object sensor = hardwareMap.get(sensorType, sensorName);
                debugger.testHypothesis("Sensor exists in hardware map", true, sensor.getClass().getSimpleName());
                
                if (sensor instanceof ColorSensor) {
                    return debugColorSensor((ColorSensor) sensor, sensorName);
                }
                
                return true;
                
            } catch (Exception e) {
                debugger.testHypothesis("Sensor exists in hardware map", false, e.getMessage());
                debugger.documentConclusion("Sensor not configured", "Check configuration file");
                return false;
            }
        }
        
        private boolean debugColorSensor(ColorSensor sensor, String sensorName) {
            debugger.formHypothesis("Color sensor not reading values", "getRed(), getGreen(), getBlue() should return values");
            
            try {
                int red = sensor.red();
                int green = sensor.green();
                int blue = sensor.blue();
                
                String reading = String.format("R:%d G:%d B:%d", red, green, blue);
                debugger.testHypothesis("Color sensor reading values", true, reading);
                
                debugger.documentConclusion("Color sensor working", "Check sensor positioning and lighting");
                return true;
                
            } catch (Exception e) {
                debugger.testHypothesis("Color sensor reading values", false, e.getMessage());
                debugger.documentConclusion("Color sensor hardware issue", "Check wiring and power");
                return false;
            }
        }
        
        public void printDebugLog() {
            List<String> log = debugger.getDebugLog();
            telemetry.addLine("=== HARDWARE DEBUG LOG ===");
            for (String entry : log) {
                telemetry.addLine(entry);
            }
            telemetry.update();
        }
    }
    
    public static class DebuggableStateMachine<T extends Enum<T>> {
        private T currentState;
        private T previousState;
        private double stateStartTime;
        private double currentTime;
        private List<StateTransition> transitionHistory = new ArrayList<>();
        private DebuggingFramework debugger;
        
        public DebuggableStateMachine(T initialState, DebuggingFramework debugger) {
            this.currentState = initialState;
            this.previousState = initialState;
            this.debugger = debugger;
            this.stateStartTime = 0.0;
            this.currentTime = 0.0;
        }
        
        public void update(double time) {
            this.currentTime = time;
            
            double stateDuration = time - stateStartTime;
            if (stateDuration > 10.0) {
                debugger.observeProblem("State " + currentState + " has been active for " + stateDuration + " seconds");
                debugger.formHypothesis("State transition condition not met", "Check transition logic");
            }
        }
        
        public void transitionTo(T newState, String reason) {
            if (newState != currentState) {
                previousState = currentState;
                double stateDuration = currentTime - stateStartTime;
                
                StateTransition transition = new StateTransition(
                    previousState, newState, reason, stateDuration, currentTime
                );
                transitionHistory.add(transition);
                
                debugger.observeProblem("State transition: " + previousState + " -> " + newState + " (" + reason + ")");
                
                currentState = newState;
                stateStartTime = currentTime;
            }
        }
        
        public T getCurrentState() {
            return currentState;
        }
        
        public double getStateDuration() {
            return currentTime - stateStartTime;
        }
        
        public void debugStateMachine(Telemetry telemetry) {
            telemetry.addLine("=== STATE MACHINE DEBUG ===");
            telemetry.addData("Current State", currentState);
            telemetry.addData("Previous State", previousState);
            telemetry.addData("State Duration", String.format("%.2fs", getStateDuration()));
            telemetry.addData("Total Time", String.format("%.2fs", currentTime));
            
            telemetry.addLine("\nRecent Transitions:");
            int startIndex = Math.max(0, transitionHistory.size() - 5);
            for (int i = startIndex; i < transitionHistory.size(); i++) {
                StateTransition t = transitionHistory.get(i);
                telemetry.addLine(String.format("%.1fs: %s -> %s (%s)", 
                    t.timestamp, t.fromState, t.toState, t.reason));
            }
            
            telemetry.update();
        }
        
        private static class StateTransition {
            T fromState, toState;
            String reason;
            double duration, timestamp;
            
            StateTransition(T from, T to, String reason, double duration, double timestamp) {
                this.fromState = from;
                this.toState = to;
                this.reason = reason;
                this.duration = duration;
                this.timestamp = timestamp;
            }
        }
    }
}

Performance Debugging

Performance issues in FTC can cause missed commands, laggy telemetry, or unreliable autonomous behavior. Learn to identify and fix performance bottlenecks.

Performance Monitor - Core Structure

public class PerformanceMonitor {
    private Map<String, Long> operationStartTimes = new HashMap<>();
    private Map<String, List<Long>> operationDurations = new HashMap<>();
    private long loopStartTime;
    private int loopCount = 0;
    private double averageLoopTime = 0.0;
    private double maxLoopTime = 0.0;
    private DebuggingFramework debugger;
    
    public PerformanceMonitor(DebuggingFramework debugger) {
        this.debugger = debugger;
    }
    
    public void startLoop() {
        loopStartTime = System.nanoTime();
        loopCount++;
    }
    
    public void endLoop() {
        long loopDuration = System.nanoTime() - loopStartTime;
        double loopTimeMs = loopDuration / 1_000_000.0;
        
        // Update average loop time
        averageLoopTime = (averageLoopTime * (loopCount - 1) + loopTimeMs) / loopCount;
        maxLoopTime = Math.max(maxLoopTime, loopTimeMs);
        
        // Check for performance issues
        if (loopTimeMs > 50.0) { // More than 50ms per loop
            debugger.observeProblem("Slow loop detected: " + String.format("%.2f", loopTimeMs) + "ms");
            debugger.formHypothesis("Loop taking too long", "Check for expensive operations in loop");
        }
    }

Understanding Performance Monitoring

The performance monitor tracks how long each loop takes to execute. It calculates the average and maximum loop times, and alerts you when loops take too long. This helps identify performance bottlenecks in your code.

Performance Monitor - Operation Tracking Methods

    public void startOperation(String operationName) {
        operationStartTimes.put(operationName, System.nanoTime());
    }
    
    public void endOperation(String operationName) {
        Long startTime = operationStartTimes.remove(operationName);
        if (startTime != null) {
            long duration = System.nanoTime() - startTime;
            double durationMs = duration / 1_000_000.0;
            
            // Store duration for analysis
            operationDurations.computeIfAbsent(operationName, k -> new ArrayList<>()).add(duration);
            
            // Check for slow operations
            if (durationMs > 10.0) { // More than 10ms
                debugger.observeProblem("Slow operation: " + operationName + " took " + String.format("%.2f", durationMs) + "ms");
            }
        }
    }

Understanding Operation Tracking

The operation tracking methods allow you to measure how long specific operations take. You can wrap any code section with startOperation() and endOperation() calls to identify which parts of your code are causing performance issues.

Performance Monitor - Analysis and Reporting

    public void analyzePerformance(Telemetry telemetry) {
        telemetry.addLine("=== PERFORMANCE ANALYSIS ===");
        telemetry.addData("Loop Count", loopCount);
        telemetry.addData("Average Loop Time", String.format("%.2fms", averageLoopTime));
        telemetry.addData("Max Loop Time", String.format("%.2fms", maxLoopTime));
        
        if (averageLoopTime > 20.0) {
            telemetry.addLine("WARNING: Average loop time is high!");
        }
        
        telemetry.addLine("\nOperation Analysis:");
        for (Map.Entry<String, List<Long>> entry : operationDurations.entrySet()) {
            String operation = entry.getKey();
            List<Long> durations = entry.getValue();
            
            if (durations.size() > 0) {
                double avgDuration = durations.stream().mapToLong(Long::longValue).average().orElse(0) / 1_000_000.0;
                double maxDuration = durations.stream().mapToLong(Long::longValue).max().orElse(0) / 1_000_000.0;
                
                telemetry.addData(operation, String.format("Avg: %.2fms, Max: %.2fms, Count: %d", 
                    avgDuration, maxDuration, durations.size()));
            }
        }
        
        telemetry.update();
    }
    
    public void reset() {
        operationStartTimes.clear();
        operationDurations.clear();
        loopCount = 0;
        averageLoopTime = 0.0;
        maxLoopTime = 0.0;
    }
}

Understanding Performance Analysis

The analyzePerformance method provides detailed statistics about your code's performance. It shows average and maximum loop times, and breaks down the performance of individual operations. This helps you identify which parts of your code need optimization.

Competition Debugging Strategies

Competition environments require different debugging strategies. You have limited time and need to work quickly and efficiently.

Competition Debugging Checklist

  • Have a pre-competition debugging checklist ready
  • Use quick diagnostic tools that provide immediate feedback
  • Keep debugging logs concise but informative
  • Have backup strategies for common failure modes
  • Practice debugging under time pressure before competition
  • Document successful debugging approaches for future reference

Competition Debugger - Basic Structure

public class CompetitionDebugger {
    private Telemetry telemetry;
    private boolean quickMode = true; // Competition mode
    private Map<String, Object> lastValues = new HashMap<>();
    
    public CompetitionDebugger(Telemetry telemetry) {
        this.telemetry = telemetry;
    }
    
    /**
     * Quick hardware check for competition
     */
    public boolean quickHardwareCheck(HardwareMap hardwareMap) {
        telemetry.addLine("=== QUICK HARDWARE CHECK ===");
        
        boolean allOK = true;
        
        // Check motors
        try {
            DcMotor leftMotor = hardwareMap.get(DcMotor.class, "left_motor");
            DcMotor rightMotor = hardwareMap.get(DcMotor.class, "right_motor");
            leftMotor.setPower(0.0);
            rightMotor.setPower(0.0);
            telemetry.addData("Motors", "OK");
        } catch (Exception e) {
            telemetry.addData("Motors", "FAIL: " + e.getMessage());
            allOK = false;
        }
        
        // Check sensors
        try {
            ColorSensor colorSensor = hardwareMap.get(ColorSensor.class, "color_sensor");
            int red = colorSensor.red();
            telemetry.addData("Color Sensor", "OK (R=" + red + ")");
        } catch (Exception e) {
            telemetry.addData("Color Sensor", "FAIL: " + e.getMessage());
            allOK = false;
        }
        
        telemetry.addData("Overall Status", allOK ? "READY" : "ISSUES DETECTED");
        telemetry.update();
        
        return allOK;
    }

Understanding Competition Debugging

The competition debugger provides quick, essential diagnostics that can be run rapidly during competition. It focuses on the most critical hardware components and provides immediate feedback about their status.

Competition Debugger - Monitoring and Display Methods

    /**
     * Monitor critical values for changes
     */
    public void monitorValue(String name, Object value) {
        Object lastValue = lastValues.get(name);
        if (!Objects.equals(lastValue, value)) {
            telemetry.addData(name + " (CHANGED)", value);
            lastValues.put(name, value);
        } else {
            telemetry.addData(name, value);
        }
    }
    
    /**
     * Quick state display for competition
     */
    public void displayState(String state, double time, String... additionalInfo) {
        telemetry.clear();
        telemetry.addLine("=== COMPETITION STATUS ===");
        telemetry.addData("State", state);
        telemetry.addData("Time", String.format("%.1fs", time));
        
        for (String info : additionalInfo) {
            telemetry.addLine(info);
        }
        
        telemetry.update();
    }
    
    /**
     * Emergency stop with debugging info
     */
    public void emergencyStop(String reason, DcMotor... motors) {
        telemetry.addLine("!!! EMERGENCY STOP !!!");
        telemetry.addData("Reason", reason);
        
        for (DcMotor motor : motors) {
            motor.setPower(0.0);
        }
        
        telemetry.update();
    }
}

Understanding Competition Monitoring

The monitoring methods help track critical values during competition. The monitorValue method highlights when values change, making it easy to spot unexpected behavior. The displayState method provides a clean competition status display, and emergencyStop provides a safe way to stop the robot with debugging information.

Complete Performance and Competition Debugging Example

import com.qualcomm.robotcore.eventloop.opmode.OpMode;
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import com.qualcomm.robotcore.hardware.DcMotor;
import com.qualcomm.robotcore.hardware.ColorSensor;
import android.util.Log;
import java.util.*;

@TeleOp(name = "Performance and Competition Debugging Example")
public class PerformanceAndCompetitionDebuggingExample extends OpMode {
    private PerformanceMonitor performanceMonitor;
    private CompetitionDebugger competitionDebugger;
    private DebuggingFramework debugger;
    
    private DcMotor leftMotor, rightMotor;
    private ColorSensor colorSensor;
    private int loopCount = 0;
    
    @Override
    public void init() {
        debugger = new DebuggingFramework();
        performanceMonitor = new PerformanceMonitor(debugger);
        competitionDebugger = new CompetitionDebugger(telemetry);
        
        // Quick hardware check for competition
        boolean hardwareOK = competitionDebugger.quickHardwareCheck(hardwareMap);
        
        if (!hardwareOK) {
            debugger.observeProblem("Hardware check failed", "Hardware issues detected");
            return;
        }
        
        // Initialize hardware
        leftMotor = hardwareMap.get(DcMotor.class, "left_motor");
        rightMotor = hardwareMap.get(DcMotor.class, "right_motor");
        colorSensor = hardwareMap.get(ColorSensor.class, "color_sensor");
        
        debugger.documentConclusion("Hardware initialized successfully", "Ready for operation");
    }
    
    @Override
    public void loop() {
        performanceMonitor.startLoop();
        
        // Monitor gamepad input
        performanceMonitor.startOperation("gamepad_processing");
        double leftStickY = gamepad1.left_stick_y;
        double rightStickY = gamepad1.right_stick_y;
        performanceMonitor.endOperation("gamepad_processing");
        
        // Monitor motor control
        performanceMonitor.startOperation("motor_control");
        leftMotor.setPower(leftStickY);
        rightMotor.setPower(rightStickY);
        performanceMonitor.endOperation("motor_control");
        
        // Monitor sensor reading
        performanceMonitor.startOperation("sensor_reading");
        int red = colorSensor.red();
        int green = colorSensor.green();
        int blue = colorSensor.blue();
        performanceMonitor.endOperation("sensor_reading");
        
        // Competition monitoring
        competitionDebugger.monitorValue("Left Stick Y", leftStickY);
        competitionDebugger.monitorValue("Right Stick Y", rightStickY);
        competitionDebugger.monitorValue("Color Red", red);
        
        // Emergency stop check
        if (gamepad1.back) {
            competitionDebugger.emergencyStop("Back button pressed", leftMotor, rightMotor);
            return;
        }
        
        // Display performance analysis every 100 loops
        if (loopCount % 100 == 0) {
            performanceMonitor.analyzePerformance(telemetry);
        }
        
        // Display competition status
        competitionDebugger.displayState("TeleOp Running", getRuntime(), 
            "Loop: " + loopCount,
            "Motors: " + leftMotor.getPower() + ", " + rightMotor.getPower()
        );
        
        performanceMonitor.endLoop();
        loopCount++;
    }
    
    // Performance Monitor Class
    public static class PerformanceMonitor {
        private Map<String, Long> operationStartTimes = new HashMap<>();
        private Map<String, List<Long>> operationDurations = new HashMap<>();
        private long loopStartTime;
        private int loopCount = 0;
        private double averageLoopTime = 0.0;
        private double maxLoopTime = 0.0;
        private DebuggingFramework debugger;
        
        public PerformanceMonitor(DebuggingFramework debugger) {
            this.debugger = debugger;
        }
        
        public void startLoop() {
            loopStartTime = System.nanoTime();
            loopCount++;
        }
        
        public void endLoop() {
            long loopDuration = System.nanoTime() - loopStartTime;
            double loopTimeMs = loopDuration / 1_000_000.0;
            
            averageLoopTime = (averageLoopTime * (loopCount - 1) + loopTimeMs) / loopCount;
            maxLoopTime = Math.max(maxLoopTime, loopTimeMs);
            
            if (loopTimeMs > 50.0) {
                debugger.observeProblem("Slow loop detected: " + String.format("%.2f", loopTimeMs) + "ms");
                debugger.formHypothesis("Loop taking too long", "Check for expensive operations in loop");
            }
        }
        
        public void startOperation(String operationName) {
            operationStartTimes.put(operationName, System.nanoTime());
        }
        
        public void endOperation(String operationName) {
            Long startTime = operationStartTimes.remove(operationName);
            if (startTime != null) {
                long duration = System.nanoTime() - startTime;
                double durationMs = duration / 1_000_000.0;
                
                operationDurations.computeIfAbsent(operationName, k -> new ArrayList<>()).add(duration);
                
                if (durationMs > 10.0) {
                    debugger.observeProblem("Slow operation: " + operationName + " took " + String.format("%.2f", durationMs) + "ms");
                }
            }
        }
        
        public void analyzePerformance(Telemetry telemetry) {
            telemetry.addLine("=== PERFORMANCE ANALYSIS ===");
            telemetry.addData("Loop Count", loopCount);
            telemetry.addData("Average Loop Time", String.format("%.2fms", averageLoopTime));
            telemetry.addData("Max Loop Time", String.format("%.2fms", maxLoopTime));
            
            if (averageLoopTime > 20.0) {
                telemetry.addLine("WARNING: Average loop time is high!");
            }
            
            telemetry.addLine("\nOperation Analysis:");
            for (Map.Entry<String, List<Long>> entry : operationDurations.entrySet()) {
                String operation = entry.getKey();
                List<Long> durations = entry.getValue();
                
                if (durations.size() > 0) {
                    double avgDuration = durations.stream().mapToLong(Long::longValue).average().orElse(0) / 1_000_000.0;
                    double maxDuration = durations.stream().mapToLong(Long::longValue).max().orElse(0) / 1_000_000.0;
                    
                    telemetry.addData(operation, String.format("Avg: %.2fms, Max: %.2fms, Count: %d", 
                        avgDuration, maxDuration, durations.size()));
                }
            }
            
            telemetry.update();
        }
        
        public void reset() {
            operationStartTimes.clear();
            operationDurations.clear();
            loopCount = 0;
            averageLoopTime = 0.0;
            maxLoopTime = 0.0;
        }
    }
    
    // Competition Debugger Class
    public static class CompetitionDebugger {
        private Telemetry telemetry;
        private boolean quickMode = true;
        private Map<String, Object> lastValues = new HashMap<>();
        
        public CompetitionDebugger(Telemetry telemetry) {
            this.telemetry = telemetry;
        }
        
        public boolean quickHardwareCheck(HardwareMap hardwareMap) {
            telemetry.addLine("=== QUICK HARDWARE CHECK ===");
            
            boolean allOK = true;
            
            try {
                DcMotor leftMotor = hardwareMap.get(DcMotor.class, "left_motor");
                DcMotor rightMotor = hardwareMap.get(DcMotor.class, "right_motor");
                leftMotor.setPower(0.0);
                rightMotor.setPower(0.0);
                telemetry.addData("Motors", "OK");
            } catch (Exception e) {
                telemetry.addData("Motors", "FAIL: " + e.getMessage());
                allOK = false;
            }
            
            try {
                ColorSensor colorSensor = hardwareMap.get(ColorSensor.class, "color_sensor");
                int red = colorSensor.red();
                telemetry.addData("Color Sensor", "OK (R=" + red + ")");
            } catch (Exception e) {
                telemetry.addData("Color Sensor", "FAIL: " + e.getMessage());
                allOK = false;
            }
            
            telemetry.addData("Overall Status", allOK ? "READY" : "ISSUES DETECTED");
            telemetry.update();
            
            return allOK;
        }
        
        public void monitorValue(String name, Object value) {
            Object lastValue = lastValues.get(name);
            if (!Objects.equals(lastValue, value)) {
                telemetry.addData(name + " (CHANGED)", value);
                lastValues.put(name, value);
            } else {
                telemetry.addData(name, value);
            }
        }
        
        public void displayState(String state, double time, String... additionalInfo) {
            telemetry.clear();
            telemetry.addLine("=== COMPETITION STATUS ===");
            telemetry.addData("State", state);
            telemetry.addData("Time", String.format("%.1fs", time));
            
            for (String info : additionalInfo) {
                telemetry.addLine(info);
            }
            
            telemetry.update();
        }
        
        public void emergencyStop(String reason, DcMotor... motors) {
            telemetry.addLine("!!! EMERGENCY STOP !!!");
            telemetry.addData("Reason", reason);
            
            for (DcMotor motor : motors) {
                motor.setPower(0.0);
            }
            
            telemetry.update();
        }
    }
    
    // Debugging Framework Class (from previous example)
    public static class DebuggingFramework {
        private static final String TAG = "DebugFramework";
        private boolean debugMode = true;
        private List<String> debugLog = new ArrayList<>();
        
        public void observeProblem(String problemDescription, Object... data) {
            String observation = String.format("OBSERVATION: %s | Data: %s", 
                                              problemDescription, 
                                              Arrays.toString(data));
            logDebug(observation);
            debugLog.add(observation);
        }
        
        public void formHypothesis(String hypothesis, String expectedOutcome) {
            String hypothesisEntry = String.format("HYPOTHESIS: %s | Expected: %s", 
                                                  hypothesis, 
                                                  expectedOutcome);
            logDebug(hypothesisEntry);
            debugLog.add(hypothesisEntry);
        }
        
        public void testHypothesis(String testDescription, boolean result, Object actualOutcome) {
            String testResult = String.format("TEST: %s | Result: %s | Actual: %s", 
                                             testDescription, 
                                             result ? "PASS" : "FAIL", 
                                             actualOutcome);
            logDebug(testResult);
            debugLog.add(testResult);
        }
        
        public void documentConclusion(String conclusion, String nextSteps) {
            String conclusionEntry = String.format("CONCLUSION: %s | Next: %s", 
                                                  conclusion, 
                                                  nextSteps);
            logDebug(conclusionEntry);
            debugLog.add(conclusionEntry);
        }
        
        private void logDebug(String message) {
            if (debugMode) {
                Log.d(TAG, message);
            }
        }
        
        public List<String> getDebugLog() {
            return new ArrayList<>(debugLog);
        }
        
        public void clearDebugLog() {
            debugLog.clear();
        }
    }
}

Debugging Strategy Practice Exercise

Create a comprehensive debugging system for a complex robot behavior and practice systematic debugging approaches.

  • Create a robot behavior that combines multiple sensors and actuators
  • Implement the scientific method debugging framework
  • Add performance monitoring to identify bottlenecks
  • Create competition-ready debugging tools
  • Write a debugging checklist for common failure modes
  • Practice debugging the system with intentional bugs
  • Document your debugging process and findings
// Example: Robot searches for an object using a color sensor, approaches it, grabs it with a servo, and returns
import com.qualcomm.robotcore.eventloop.opmode.OpMode;
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import com.qualcomm.robotcore.hardware.DcMotor;
import com.qualcomm.robotcore.hardware.Servo;
import com.qualcomm.robotcore.hardware.ColorSensor;

@TeleOp(name = "DebuggingExercise")
public class DebuggingExerciseOpMode extends OpMode {
    private DcMotor leftMotor, rightMotor;
    private Servo grabberServo;
    private ColorSensor colorSensor;
    private enum RobotState { INIT, SEARCH, APPROACH, GRAB, RETURN, COMPLETE }
    private RobotState currentState = RobotState.INIT;
    private double startTime = 0.0;
    
    @Override
    public void init() {
        leftMotor = hardwareMap.get(DcMotor.class, "left_motor");
        rightMotor = hardwareMap.get(DcMotor.class, "right_motor");
        grabberServo = hardwareMap.get(Servo.class, "grabber_servo");
        colorSensor = hardwareMap.get(ColorSensor.class, "color_sensor");
        currentState = RobotState.SEARCH;
    }
    
    @Override
    public void loop() {
        switch (currentState) {
            case SEARCH:
                // Search for red object
                if (colorSensor.red() > 100) {
                    currentState = RobotState.APPROACH;
                    startTime = getRuntime();
                } else {
                    leftMotor.setPower(0.2);
                    rightMotor.setPower(0.2);
                }
                break;
            case APPROACH:
                // Approach for 2 seconds
                if (getRuntime() - startTime > 2.0) {
                    currentState = RobotState.GRAB;
                } else {
                    leftMotor.setPower(0.3);
                    rightMotor.setPower(0.3);
                }
                break;
            case GRAB:
                grabberServo.setPosition(1.0); // Close grabber
                currentState = RobotState.RETURN;
                startTime = getRuntime();
                break;
            case RETURN:
                // Return for 2 seconds
                if (getRuntime() - startTime > 2.0) {
                    currentState = RobotState.COMPLETE;
                } else {
                    leftMotor.setPower(-0.3);
                    rightMotor.setPower(-0.3);
                }
                break;
            case COMPLETE:
                leftMotor.setPower(0);
                rightMotor.setPower(0);
                break;
        }
    }
}

// This code combines multiple actuators (motors, servo) and a sensor (color sensor) in a state machine.

Additional Resources

Open full interactive app