Advanced Debugging Techniques

Introduction to Advanced Debugging

Advanced debugging in Android Studio is essential for FTC development. When your robot doesn't behave as expected, you need powerful tools to identify and fix issues quickly. This lesson covers professional debugging techniques that will help you troubleshoot complex robot behaviors efficiently.

Why Advanced Debugging Matters

  • FTC robots operate in real-time environments where issues must be resolved quickly
  • Complex autonomous programs require systematic debugging approaches
  • Hardware-software interactions can create difficult-to-reproduce bugs
  • Competition environments demand reliable debugging skills

Android Studio Debugger Setup for FTC

Setting up the Android Studio debugger for FTC projects requires a specific configuration to work with the Control Hub and Expansion Hub. The debugger allows you to pause execution, inspect variables, and step through code line by line.

Basic Debug Logging Setup

// Add this at the top of your OpMode class
private static final String TAG = "MyDebugOpMode";

// This TAG will be used in all log statements to identify your OpMode's logs

Understanding the TAG Constant

The TAG constant is a string identifier that appears in all log messages from this OpMode. It helps you filter and organize logs when debugging. Choose a descriptive name that clearly identifies your OpMode.

Adding Debug Logging to Init Method

@Override
public void init() {
    // Log the start of initialization
    Log.d(TAG, "Initializing OpMode");
    
    // Your initialization code goes here
    
    // Log successful completion
    Log.d(TAG, "Initialization complete");
}

Understanding Log Levels

Android provides different log levels for different types of messages:

  • Log.d() - Debug messages for development
  • Log.i() - Information messages for general status
  • Log.w() - Warning messages for potential issues
  • Log.e() - Error messages for actual problems

Error Handling with Logging

@Override
public void init() {
    Log.d(TAG, "Initializing OpMode");
    
    try {
        // Your initialization code here
        initializeHardware();
        Log.d(TAG, "Hardware initialized successfully");
    } catch (Exception e) {
        // Log the error with full details
        Log.e(TAG, "Hardware initialization failed", e);
        
        // Also show error on telemetry for driver station
        telemetry.addData("Error", "Hardware init failed: " + e.getMessage());
    }
}

Understanding Try-Catch for Debugging

The try-catch block allows you to catch exceptions and handle them gracefully. The Log.e() call includes the exception object, which provides the full stack trace for debugging.

Complete Debug OpMode 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.Servo;
import android.util.Log;

@TeleOp(name = "Debug Example")
public class MyDebugOpMode extends OpMode {
    private static final String TAG = "MyDebugOpMode";
    
    private DcMotor leftMotor, rightMotor;
    private Servo armServo;
    
    @Override
    public void init() {
        // Enable debug logging
        Log.d(TAG, "Initializing OpMode");
        
        // Initialize hardware with debug info
        try {
            initializeHardware();
            Log.d(TAG, "Hardware initialized successfully");
        } catch (Exception e) {
            Log.e(TAG, "Hardware initialization failed", e);
            telemetry.addData("Error", "Hardware init failed: " + e.getMessage());
        }
    }
    
    private void initializeHardware() {
        // Initialize motors
        leftMotor = hardwareMap.get(DcMotor.class, "left_motor");
        rightMotor = hardwareMap.get(DcMotor.class, "right_motor");
        
        // Initialize servo
        armServo = hardwareMap.get(Servo.class, "arm_servo");
        
        // Log hardware details
        Log.d(TAG, "Motors initialized: " + leftMotor.getDeviceName() + ", " + rightMotor.getDeviceName());
        Log.d(TAG, "Servo initialized: " + armServo.getDeviceName());
    }
    
    @Override
    public void loop() {
        // Your robot control code here
        // Add breakpoints in this method to debug robot behavior
    }
}

Setting Up Debug Logging

Debug logging is the foundation of advanced debugging. By adding strategic log statements throughout your code, you can track program execution and identify where issues occur. The TAG constant helps organize log messages, and different log levels (d for debug, e for error) provide appropriate detail.

Setting Breakpoints

Breakpoints are the foundation of debugging. They allow you to pause execution at specific points in your code to inspect the program state. In Android Studio, you can set breakpoints by clicking in the gutter next to line numbers.

Breakpoint Best Practices

  • Set breakpoints at the beginning of methods you suspect have issues
  • Use breakpoints to inspect variable values before and after critical operations
  • Place breakpoints before conditional statements to verify logic flow
  • Remove or disable breakpoints when not actively debugging to improve performance

Conditional Breakpoints and Watch Expressions

Conditional breakpoints are powerful tools for debugging complex robot behaviors. They only pause execution when specific conditions are met, making it easier to debug rare or specific scenarios.

State Machine Setup

public class StateMachineOpMode extends OpMode {
    // Define possible robot states
    enum RobotState {
        IDLE,      // Robot is waiting
        MOVING,    // Robot is in motion
        COMPLETE   // Robot has finished its task
    }
    
    // Track current state and loop count
    private RobotState currentState = RobotState.IDLE;
    private int loopCount = 0;
}

Understanding State Machines

A state machine is a programming pattern where your robot can be in one of several defined states at any time. Each state represents a different behavior or phase of operation. The robot transitions between states based on conditions or events.

Adding the Main Loop with State Logic

@Override
public void loop() {
    // This is where you'll set conditional breakpoints
    switch (currentState) {
        case IDLE:
            handleIdleState();
            break;
            
        case MOVING:
            handleMovingState();
            break;
            
        case COMPLETE:
            handleCompleteState();
            break;
    }
    
    // Update loop counter and telemetry
    loopCount++;
    telemetry.addData("State", currentState);
    telemetry.addData("Loop Count", loopCount);
}

Understanding the Switch Statement

The switch statement checks the current state and calls the appropriate handler method. This makes the code more organized and easier to debug. Each case represents a different state of the robot.

Implementing State Handler Methods

private void handleIdleState() {
    // Set conditional breakpoint here with condition: loopCount > 100
    if (loopCount > 100) {
        currentState = RobotState.MOVING;
        Log.d("StateMachine", "Transitioning to MOVING state");
    }
}

private void handleMovingState() {
    // Set conditional breakpoint here with condition: getRuntime() > 5.0
    if (getRuntime() > 5.0) {
        currentState = RobotState.COMPLETE;
        Log.d("StateMachine", "Movement complete");
    }
}

private void handleCompleteState() {
    // Set breakpoint with condition: true (always break)
    Log.d("StateMachine", "OpMode complete");
    requestOpModeStop();
}

Understanding Conditional Breakpoints

Conditional breakpoints only pause execution when a specific condition is true. This is useful for debugging rare events or specific scenarios without stopping on every loop iteration.

  • loopCount > 100 - Only breaks when the robot has been idle for 100 loops
  • getRuntime() > 5.0 - Only breaks when the robot has been moving for 5 seconds
  • true - Always breaks (useful for debugging the complete state)

Complete State Machine Example

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

@TeleOp(name = "State Machine Debug")
public class StateMachineOpMode extends OpMode {
    // Define possible robot states
    enum RobotState {
        IDLE,      // Robot is waiting
        MOVING,    // Robot is in motion
        COMPLETE   // Robot has finished its task
    }
    
    // Track current state and loop count
    private RobotState currentState = RobotState.IDLE;
    private int loopCount = 0;
    private DcMotor driveMotor;
    
    @Override
    public void init() {
        driveMotor = hardwareMap.get(DcMotor.class, "drive_motor");
        Log.d("StateMachine", "Initialized in IDLE state");
    }
    
    @Override
    public void loop() {
        // Set conditional breakpoints in each case
        switch (currentState) {
            case IDLE:
                handleIdleState();
                break;
                
            case MOVING:
                handleMovingState();
                break;
                
            case COMPLETE:
                handleCompleteState();
                break;
        }
        
        // Update loop counter and telemetry
        loopCount++;
        telemetry.addData("State", currentState);
        telemetry.addData("Loop Count", loopCount);
        telemetry.addData("Runtime", "%.2f", getRuntime());
    }
    
    private void handleIdleState() {
        // Set conditional breakpoint here with condition: loopCount > 100
        if (loopCount > 100) {
            currentState = RobotState.MOVING;
            Log.d("StateMachine", "Transitioning to MOVING state");
            driveMotor.setPower(0.5); // Start moving
        }
    }
    
    private void handleMovingState() {
        // Set conditional breakpoint here with condition: getRuntime() > 5.0
        if (getRuntime() > 5.0) {
            currentState = RobotState.COMPLETE;
            Log.d("StateMachine", "Movement complete");
            driveMotor.setPower(0); // Stop moving
        }
    }
    
    private void handleCompleteState() {
        // Set breakpoint with condition: true (always break)
        Log.d("StateMachine", "OpMode complete");
        requestOpModeStop();
    }
}

Watch Expressions

Watch expressions allow you to monitor specific variables or expressions without setting breakpoints. They're displayed in the debug window and update in real-time as your program runs.

Setting Up Variables for Watching

public class WatchExpressionsExample extends OpMode {
    // Hardware components
    private DcMotor leftMotor, rightMotor;
    
    // Variables to watch during debugging
    private double targetPower = 0.5;    // Maximum power limit
    private double currentPower = 0.0;   // Current motor power
    private boolean isMoving = false;    // Movement state
    
    // These variables will be monitored in the debugger

Understanding Watch Expressions

Watch expressions allow you to monitor specific variables or expressions in real-time during debugging. They appear in the debug window and update as your program runs, making it easy to track how values change over time.

Initializing Hardware Components

@Override
public void init() {
    // Initialize motors
    leftMotor = hardwareMap.get(DcMotor.class, "left_motor");
    rightMotor = hardwareMap.get(DcMotor.class, "right_motor");
    
    // Set up watch expressions for these variables:
    // - targetPower (double)
    // - currentPower (double)
    // - isMoving (boolean)
    // - leftMotor.getPower() (method call)
    // - rightMotor.getPower() (method call)
    // - getRuntime() (method call)
}

Understanding Method Calls in Watch Expressions

You can watch method calls like leftMotor.getPower() to see the actual power being sent to the motor. This is useful for verifying that your commands are being executed correctly.

Implementing Gamepad Control Logic

@Override
public void loop() {
    // Check if the left stick is being used
    if (gamepad1.left_stick_y > 0.1) {
        // Calculate power based on stick position
        currentPower = gamepad1.left_stick_y * targetPower;
        isMoving = true;
    } else {
        // Stop the robot
        currentPower = 0.0;
        isMoving = false;
    }
    
    // Apply power to motors
    leftMotor.setPower(currentPower);
    rightMotor.setPower(currentPower);
}

Understanding the Control Logic

The control logic checks if the left stick Y-axis is pressed beyond a small threshold (0.1) to avoid accidental movement. When pressed, it calculates the power as a percentage of the target power. When released, it stops the robot.

Adding Telemetry for Debugging

// Add this to the end of the loop() method
// Add telemetry for debugging
telemetry.addData("Target Power", targetPower);
telemetry.addData("Current Power", currentPower);
telemetry.addData("Is Moving", isMoving);
telemetry.addData("Left Motor Power", leftMotor.getPower());
telemetry.addData("Right Motor Power", rightMotor.getPower());
telemetry.addData("Runtime", "%.2f", getRuntime());

Understanding Telemetry vs Watch Expressions

Telemetry displays information on the driver station screen, while watch expressions are only visible in the debugger. Use telemetry for information the driver needs, and watch expressions for detailed debugging information.

Complete Watch Expressions Example

import com.qualcomm.robotcore.eventloop.opmode.OpMode;
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import com.qualcomm.robotcore.hardware.DcMotor;

@TeleOp(name = "Watch Expressions Example")
public class WatchExpressionsExample extends OpMode {
    // Hardware components
    private DcMotor leftMotor, rightMotor;
    
    // Variables to watch during debugging
    private double targetPower = 0.5;    // Maximum power limit
    private double currentPower = 0.0;   // Current motor power
    private boolean isMoving = false;    // Movement state
    
    @Override
    public void init() {
        // Initialize motors
        leftMotor = hardwareMap.get(DcMotor.class, "left_motor");
        rightMotor = hardwareMap.get(DcMotor.class, "right_motor");
        
        // Set up watch expressions for these variables:
        // - targetPower
        // - currentPower
        // - isMoving
        // - leftMotor.getPower()
        // - rightMotor.getPower()
        // - getRuntime()
    }
    
    @Override
    public void loop() {
        // Watch how these values change during execution
        if (gamepad1.left_stick_y > 0.1) {
            currentPower = gamepad1.left_stick_y * targetPower;
            isMoving = true;
        } else {
            currentPower = 0.0;
            isMoving = false;
        }
        
        // Apply power to motors
        leftMotor.setPower(currentPower);
        rightMotor.setPower(currentPower);
        
        // Add telemetry for debugging
        telemetry.addData("Target Power", targetPower);
        telemetry.addData("Current Power", currentPower);
        telemetry.addData("Is Moving", isMoving);
        telemetry.addData("Left Motor Power", leftMotor.getPower());
        telemetry.addData("Right Motor Power", rightMotor.getPower());
        telemetry.addData("Runtime", "%.2f", getRuntime());
    }
}

Logging Strategies for Robot State

Effective logging is crucial for debugging robot behavior. You need to log enough information to understand what's happening without overwhelming the system or making logs unreadable.

Setting Up Logging Configuration

public class StructuredLoggingOpMode extends OpMode {
    private static final String TAG = "RobotDebug";
    private static final boolean DEBUG_MODE = true;      // Enable debug logging
    private static final boolean VERBOSE_LOGGING = false; // Enable verbose logging
    
    // Hardware components
    private DcMotor driveMotor;
    private Servo armServo;
    private ColorSensor colorSensor;
}

Understanding Logging Configuration

The boolean constants control which logging levels are active. You can change these values to enable or disable different levels of logging without modifying the code. This is useful for performance tuning and debugging different scenarios.

Creating Custom Logging Methods

// Logging methods with different levels
private void logInfo(String message) {
    Log.i(TAG, message);
    telemetry.addData("INFO", message);
}

private void logDebug(String message) {
    if (DEBUG_MODE) {
        Log.d(TAG, message);
        telemetry.addData("DEBUG", message);
    }
}

private void logVerbose(String message) {
    if (VERBOSE_LOGGING) {
        Log.v(TAG, message);
    }
}

private void logError(String message, Exception e) {
    Log.e(TAG, message, e);
    telemetry.addData("ERROR", message + ": " + e.getMessage());
}

Understanding Logging Levels

Each logging method serves a different purpose:

  • logInfo() - Always logs important information to both logcat and telemetry
  • logDebug() - Only logs when DEBUG_MODE is true, useful for development
  • logVerbose() - Only logs when VERBOSE_LOGGING is true, for detailed debugging
  • logError() - Always logs errors with full exception details

Initializing Hardware with Logging

@Override
public void init() {
    // Initialize hardware
    driveMotor = hardwareMap.get(DcMotor.class, "drive_motor");
    armServo = hardwareMap.get(Servo.class, "arm_servo");
    colorSensor = hardwareMap.get(ColorSensor.class, "color_sensor");
    
    // Log successful initialization
    logInfo("Hardware initialized successfully");
    
    // Log detailed hardware information (debug level)
    logDebug("Drive motor: " + driveMotor.getDeviceName());
    logDebug("Arm servo: " + armServo.getDeviceName());
    logDebug("Color sensor: " + colorSensor.getDeviceName());
}

Understanding Hardware Logging

The initialization logs help you verify that all hardware components are properly connected and accessible. The debug-level logs provide detailed information about each device, which is useful for troubleshooting hardware issues.

Adding Event-Based Logging

@Override
public void loop() {
    // Log state changes (important events)
    if (gamepad1.a) {
        logInfo("Button A pressed - starting autonomous sequence");
        startAutonomousSequence();
    }
    
    // Log sensor readings periodically (debug information)
    if (getRuntime() % 2.0 < 0.1) { // Every 2 seconds
        logDebug("Color sensor reading: R=" + colorSensor.red() + ", G=" + colorSensor.green() + ", B=" + colorSensor.blue());
    }
    
    // Log motor behavior (verbose information)
    double motorPower = gamepad1.left_stick_y;
    if (Math.abs(motorPower) > 0.1) {
        logVerbose("Setting motor power to: " + motorPower);
        driveMotor.setPower(motorPower);
    }
}

Understanding Event-Based Logging

Different types of events are logged at different levels:

  • Info level - Important user actions (button presses, mode changes)
  • Debug level - Periodic sensor readings and system status
  • Verbose level - Frequent motor commands and detailed behavior

Creating Sequence Logging

private void startAutonomousSequence() {
    logInfo("Starting autonomous sequence");
    
    // Log each step of the sequence
    logDebug("Step 1: Moving arm to position");
    armServo.setPosition(0.5);
    
    logDebug("Step 2: Driving forward");
    driveMotor.setPower(0.5);
    
    logInfo("Autonomous sequence started");
}

Understanding Sequence Logging

Sequence logging helps you track the progress of complex operations. Each step is logged at the debug level, while the start and completion are logged at the info level. This makes it easy to see where a sequence might be failing or taking too long.

Complete Structured Logging 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.Servo;
import com.qualcomm.robotcore.hardware.ColorSensor;
import android.util.Log;

@TeleOp(name = "Structured Logging Example")
public class StructuredLoggingOpMode extends OpMode {
    private static final String TAG = "RobotDebug";
    private static final boolean DEBUG_MODE = true;      // Enable debug logging
    private static final boolean VERBOSE_LOGGING = false; // Enable verbose logging
    
    // Hardware components
    private DcMotor driveMotor;
    private Servo armServo;
    private ColorSensor colorSensor;
    
    @Override
    public void init() {
        // Initialize hardware
        driveMotor = hardwareMap.get(DcMotor.class, "drive_motor");
        armServo = hardwareMap.get(Servo.class, "arm_servo");
        colorSensor = hardwareMap.get(ColorSensor.class, "color_sensor");
        
        // Log successful initialization
        logInfo("Hardware initialized successfully");
        
        // Log detailed hardware information (debug level)
        logDebug("Drive motor: " + driveMotor.getDeviceName());
        logDebug("Arm servo: " + armServo.getDeviceName());
        logDebug("Color sensor: " + colorSensor.getDeviceName());
    }
    
    @Override
    public void loop() {
        // Log state changes (important events)
        if (gamepad1.a) {
            logInfo("Button A pressed - starting autonomous sequence");
            startAutonomousSequence();
        }
        
        // Log sensor readings periodically (debug information)
        if (getRuntime() % 2.0 < 0.1) { // Every 2 seconds
            logDebug("Color sensor reading: R=" + colorSensor.red() + ", G=" + colorSensor.green() + ", B=" + colorSensor.blue());
        }
        
        // Log motor behavior (verbose information)
        double motorPower = gamepad1.left_stick_y;
        if (Math.abs(motorPower) > 0.1) {
            logVerbose("Setting motor power to: " + motorPower);
            driveMotor.setPower(motorPower);
        }
    }
    
    private void startAutonomousSequence() {
        logInfo("Starting autonomous sequence");
        
        // Log each step of the sequence
        logDebug("Step 1: Moving arm to position");
        armServo.setPosition(0.5);
        
        logDebug("Step 2: Driving forward");
        driveMotor.setPower(0.5);
        
        logInfo("Autonomous sequence started");
    }
    
    // Logging methods with different levels
    private void logInfo(String message) {
        Log.i(TAG, message);
        telemetry.addData("INFO", message);
    }
    
    private void logDebug(String message) {
        if (DEBUG_MODE) {
            Log.d(TAG, message);
            telemetry.addData("DEBUG", message);
        }
    }
    
    private void logVerbose(String message) {
        if (VERBOSE_LOGGING) {
            Log.v(TAG, message);
        }
    }
    
    private void logError(String message, Exception e) {
        Log.e(TAG, message, e);
        telemetry.addData("ERROR", message + ": " + e.getMessage());
    }
}

Remote Debugging on Control Hub

Remote debugging allows you to debug code running on the actual Control Hub hardware. This is essential for debugging issues that only occur on the real robot.

ADB Connection Setup

// First, ensure your Control Hub is connected to the same network
// Then use ADB to connect to the Control Hub

// In terminal/command prompt:
// adb connect 192.168.43.1:5555
// (Replace with your Control Hub's IP address)

// Verify connection:
// adb devices

// In your OpMode, add remote debugging support:
public class RemoteDebugOpMode extends OpMode {
    private static final String TAG = "RemoteDebug";
    private boolean remoteDebugEnabled = false;
    
    @Override
    public void init() {
        // Check if remote debugging is available
        try {
            // This will only work if ADB is connected
            Log.d(TAG, "Remote debugging available");
            remoteDebugEnabled = true;
        } catch (Exception e) {
            Log.w(TAG, "Remote debugging not available: " + e.getMessage());
            remoteDebugEnabled = false;
        }
        
        // Initialize hardware with remote debugging support
        initializeHardwareWithDebugging();
    }
    
    private void initializeHardwareWithDebugging() {
        try {
            // Initialize motors
            DcMotor leftMotor = hardwareMap.get(DcMotor.class, "left_motor");
            DcMotor rightMotor = hardwareMap.get(DcMotor.class, "right_motor");
            
            if (remoteDebugEnabled) {
                Log.d(TAG, "Motors initialized: " + leftMotor.getDeviceName() + ", " + rightMotor.getDeviceName());
            }
            
            // Initialize sensors
            ColorSensor colorSensor = hardwareMap.get(ColorSensor.class, "color_sensor");
            
            if (remoteDebugEnabled) {
                Log.d(TAG, "Color sensor initialized: " + colorSensor.getDeviceName());
            }
            
        } catch (Exception e) {
            Log.e(TAG, "Hardware initialization failed", e);
            if (remoteDebugEnabled) {
                // Additional debugging information for remote sessions
                Log.e(TAG, "Hardware map contents: " + hardwareMap.getAll(DcMotor.class).size() + " motors, " + 
                      hardwareMap.getAll(ColorSensor.class).size() + " color sensors");
            }
        }
    }
    
    @Override
    public void loop() {
        if (remoteDebugEnabled) {
            // Enhanced logging for remote debugging
            Log.d(TAG, "Loop iteration: " + getRuntime() + "s");
            
            // Log gamepad state
            if (gamepad1.a) {
                Log.d(TAG, "Button A pressed");
            }
            if (Math.abs(gamepad1.left_stick_y) > 0.1) {
                Log.d(TAG, "Left stick Y: " + gamepad1.left_stick_y);
            }
        }
        
        // Your normal OpMode logic here
    }
}

Practice: Advanced Debugging

Try these debugging exercises to reinforce your understanding:

  • Create an OpMode with a state machine that controls a robot drivetrain
  • Intentionally introduce a bug (e.g., wrong state transition condition)
  • Set up conditional breakpoints to catch the bug
  • Use watch expressions to monitor state variables
  • Implement structured logging to track the bug
  • Use remote debugging to verify the fix works on hardware

Additional Resources

Open full interactive app