Advanced Debugging Techniques
Introduction to Advanced Debugging
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
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 logsUnderstanding 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 developmentLog.i()- Information messages for general statusLog.w()- Warning messages for potential issuesLog.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
Setting Breakpoints
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
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 loopsgetRuntime() > 5.0- Only breaks when the robot has been moving for 5 secondstrue- 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
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 debuggerUnderstanding 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
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 telemetrylogDebug()- Only logs when DEBUG_MODE is true, useful for developmentlogVerbose()- Only logs when VERBOSE_LOGGING is true, for detailed debugginglogError()- 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
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