FTCDashboard Comprehensive Guide

What is FTCDashboard?

FTCDashboard is a powerful web-based dashboard that provides real-time robot monitoring and control capabilities for FTC teams. Unlike basic telemetry that only shows text output, FTCDashboard offers a rich graphical interface with live data plotting, field visualization, configuration management, remote control features, and camera streaming.

Key Benefits of FTCDashboard

  • Real-time data visualization with live graphs and charts
  • Field view for robot position tracking and path visualization
  • Runtime configuration without code recompilation
  • Remote OpMode control and custom buttons
  • Camera streaming integration
  • Multi-device support for team collaboration

Installation and Setup

FTCDashboard is installed as a Gradle dependency in your Android Studio project. The library automatically sets up a web server on your robot that can be accessed from any device on the same network.

Step 1: Add Gradle Dependency

dependencies {
    // Other dependencies...
    implementation 'com.acmerobotics.dashboard:dashboard:0.4.17'
}

// Make sure you have the correct repositories
repositories {
    google()
    mavenCentral()
    maven { url = "https://maven.brott.dev/" }
}

Step 2: Required Imports

import com.acmerobotics.dashboard.FtcDashboard;
import com.acmerobotics.dashboard.config.Config;
import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
import com.acmerobotics.dashboard.telemetry.TelemetryPacket;
import com.acmerobotics.roadrunner.geometry.Pose2d;
import org.firstinspires.ftc.robotcore.external.hardware.camera.Webcam;

Step 3: Basic OpMode Setup

@Config
public class ComprehensiveDashboardOpMode extends OpMode {
    private FtcDashboard dashboard;
    private MultipleTelemetry telemetry;
    private TelemetryPacket packet;
    
    // Hardware objects
    private DcMotor leftMotor, rightMotor;
    private Servo armServo;
    private Webcam webcam;
    
    // Robot position tracking
    private double x = 0, y = 0, heading = 0;
    private List<Pose2d> actualPath = new ArrayList<>();
    
    // Configuration variables
    public static double DRIVE_SPEED = 0.5;
    public static double TURN_SPEED = 0.3;
    public static int TARGET_POSITION = 1000;
    public static boolean USE_ENCODERS = true;
    public static double PID_KP = 0.1;
    public static double PID_KI = 0.0;
    public static double PID_KD = 0.0;
    
    // Camera configuration
    public static int CAMERA_WIDTH = 640;
    public static int CAMERA_HEIGHT = 480;
    public static int CAMERA_FPS = 30;
    
    // Status tracking
    private boolean cameraActive = false;
    private int frameCount = 0;
    private boolean isExecutingAction = false;
    private String currentAction = "None";
    
    @Override
    public void init() {
        initializeDashboard();
        initializeHardware();
        initializeCamera();
        
        telemetry.addData("Status", "Dashboard initialized");
        telemetry.update();
    }
    
    private void initializeDashboard() {
        dashboard = FtcDashboard.getInstance();
        telemetry = new MultipleTelemetry(this.telemetry, dashboard.getTelemetry());
        packet = new TelemetryPacket();
    }
    
    private void initializeHardware() {
        leftMotor = hardwareMap.get(DcMotor.class, "left_motor");
        rightMotor = hardwareMap.get(DcMotor.class, "right_motor");
        armServo = hardwareMap.get(Servo.class, "arm_servo");
        
        if (USE_ENCODERS) {
            leftMotor.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER);
            rightMotor.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER);
        }
    }
    
    private void initializeCamera() {
        try {
            webcam = hardwareMap.get(Webcam.class, "webcam");
            webcam.setViewportRenderer(Webcam.ViewportRenderer.BUFFERED);
            webcam.setResolution(CAMERA_WIDTH, CAMERA_HEIGHT);
            webcam.setFps(CAMERA_FPS);
            cameraActive = webcam.isOpen();
        } catch (Exception e) {
            telemetry.addData("Camera Error", "Failed to initialize: " + e.getMessage());
            cameraActive = false;
        }
    }

Telemetry and Plotting

FTCDashboard transforms basic telemetry into a powerful visualization tool. While traditional telemetry only displays text data, FTCDashboard provides real-time plotting capabilities that make it much easier to understand robot behavior, tune PID controllers, and debug autonomous programs.

Real-time Data Plotting

@Override
public void loop() {
    updateRobotPosition();
    updateCameraStatus();
    
    // Basic telemetry (shows in both Driver Station and Dashboard)
    telemetry.addData("Loop Time", "%.2f seconds", getRuntime());
    telemetry.addData("Battery Voltage", "%.2f V", getBatteryVoltage());
    
    // Dashboard-specific telemetry with plotting
    packet.put("Loop Time", getRuntime());
    packet.put("Battery Voltage", getBatteryVoltage());
    packet.put("Left Motor Power", leftMotor.getPower());
    packet.put("Right Motor Power", rightMotor.getPower());
    packet.put("Left Encoder Position", leftMotor.getCurrentPosition());
    packet.put("Right Encoder Position", rightMotor.getCurrentPosition());
    packet.put("Robot X Position", x);
    packet.put("Robot Y Position", y);
    packet.put("Robot Heading", heading);
    
    // Create scatter plots for analysis
    packet.put("Time vs Left Power", getRuntime(), leftMotor.getPower());
    packet.put("Time vs Right Power", getRuntime(), rightMotor.getPower());
    packet.put("Power vs Position", leftMotor.getPower(), leftMotor.getCurrentPosition());
    
    // Color code the plots
    packet.put("Left Motor Power:color", "red");
    packet.put("Right Motor Power:color", "blue");
    packet.put("Robot X Position:color", "green");
    packet.put("Robot Y Position:color", "orange");
    
    // Send telemetry to both destinations
    telemetry.update();
    dashboard.sendTelemetryPacket(packet);
    
    sleep(50); // 20 Hz update rate
}

private void updateRobotPosition() {
    // Read current encoder positions
    double leftPos = leftMotor.getCurrentPosition();
    double rightPos = rightMotor.getCurrentPosition();
    
    // Calculate average distance traveled (simplified odometry)
    double distance = (leftPos + rightPos) / 2.0 * 0.001; // 0.001 meters per tick
    
    // Calculate x, y position based on current heading
    x = distance * Math.cos(heading);
    y = distance * Math.sin(heading);
    
    // Update heading based on difference between left and right encoders
    double headingChange = (rightPos - leftPos) * 0.0001;
    heading += headingChange;
    
    // Record position for path tracking
    if (actualPath.size() > 100) {
        actualPath.remove(0);
    }
    actualPath.add(new Pose2d(x, y, heading));
}

private void updateCameraStatus() {
    frameCount++;
    if (webcam != null) {
        cameraActive = webcam.isOpen();
    }
    
    packet.put("Camera Active", cameraActive);
    packet.put("Frame Count", frameCount);
}

private double getBatteryVoltage() {
    return hardwareMap.voltageSensor.get("Expansion Hub 2").getVoltage();
}

Configuration Variables

FTCDashboard's configuration system allows you to change robot parameters without recompiling your code. This is incredibly useful for tuning PID values, adjusting autonomous parameters, and testing different configurations during development and competition.

Configuration Groups

// Drive configuration group
public static class DriveConfig {
    @Value(min = 0.0, max = 1.0)
    public static double MAX_SPEED = 0.8;
    
    @Value(min = 0.0, max = 1.0)
    public static double TURN_SPEED = 0.6;
    
    public static boolean REVERSE_LEFT = false;
    public static boolean REVERSE_RIGHT = false;
}

// PID configuration group
public static class PIDConfig {
    @Value(min = 0.0, max = 2.0)
    public static double KP = 0.1;
    
    @Value(min = 0.0, max = 1.0)
    public static double KI = 0.0;
    
    @Value(min = 0.0, max = 1.0)
    public static double KD = 0.0;
    
    @Value(min = 0, max = 2000)
    public static int TOLERANCE = 50;
}

// Camera configuration group
public static class CameraConfig {
    @Value(min = 320, max = 1280)
    public static int WIDTH = 640;
    
    @Value(min = 240, max = 720)
    public static int HEIGHT = 480;
    
    @Value(min = 10, max = 60)
    public static int FPS = 30;
    
    public static boolean ENABLE_STREAMING = true;
}

Field View Visualization

FTCDashboard's field view provides a powerful way to visualize your robot's position and movement on the game field. This feature is essential for autonomous programming, path planning, and debugging robot navigation.

Field View Setup

// Field dimensions (adjust for your specific game)
private static final double FIELD_WIDTH = 12.0;   // 12 feet in meters
private static final double FIELD_LENGTH = 12.0;  // 12 feet in meters

private void setupFieldView() {
    // Set field dimensions for proper coordinate system
    packet.fieldOverlay().setFieldDimensions(FIELD_WIDTH, FIELD_LENGTH);
    
    // Draw field boundaries
    packet.fieldOverlay()
        .setStroke("black")
        .setStrokeWidth(0.05)
        .strokeRect(0, 0, FIELD_WIDTH, FIELD_LENGTH);
    
    // Draw coordinate system reference
    packet.fieldOverlay()
        .setStroke("red")
        .setStrokeWidth(0.02)
        .strokeLine(0, 0, 1, 0); // X-axis
    
    packet.fieldOverlay()
        .setStroke("green")
        .setStrokeWidth(0.02)
        .strokeLine(0, 0, 0, 1); // Y-axis
}

private void drawRobotOnField() {
    // Draw robot as a blue circle
    packet.fieldOverlay()
        .setFill("blue")
        .fillCircle(x, y, 0.1);
    
    // Draw robot heading as a yellow line
    packet.fieldOverlay()
        .setStroke("yellow")
        .setStrokeWidth(0.03)
        .strokeLine(x, y, 
                   x + 0.2 * Math.cos(heading), 
                   y + 0.2 * Math.sin(heading));
}

private void drawActualPath() {
    // Draw actual path in red
    packet.fieldOverlay()
        .setStroke("red")
        .setStrokeWidth(0.02);
    
    for (int i = 0; i < actualPath.size() - 1; i++) {
        Pose2d current = actualPath.get(i);
        Pose2d next = actualPath.get(i + 1);
        
        packet.fieldOverlay().strokeLine(current.getX(), current.getY(), 
                                       next.getX(), next.getY());
    }
}

private void drawPlannedPath() {
    // Define planned path waypoints
    double[][] waypoints = {
        {0, 0, 0},      // Start at origin
        {1, 0, 0},      // Move 1 meter right
        {1, 1, Math.PI/2}, // Move 1 meter up, turn 90 degrees
        {0, 1, Math.PI},   // Move 1 meter left, turn 180 degrees
        {0, 0, -Math.PI/2} // Return to start, turn -90 degrees
    };
    
    // Draw planned path in blue
    packet.fieldOverlay()
        .setStroke("blue")
        .setStrokeWidth(0.03);
    
    for (int i = 0; i < waypoints.length - 1; i++) {
        double[] current = waypoints[i];
        double[] next = waypoints[i + 1];
        
        packet.fieldOverlay().strokeLine(current[0], current[1], 
                                       next[0], next[1]);
    }
    
    // Draw waypoint markers
    for (double[] waypoint : waypoints) {
        packet.fieldOverlay()
            .setFill("blue")
            .fillCircle(waypoint[0], waypoint[1], 0.05);
    }
}

private void drawGameElements() {
    // Draw cones as orange circles
    packet.fieldOverlay()
        .setFill("orange")
        .fillCircle(2, 2, 0.1);
    packet.fieldOverlay()
        .setFill("orange")
        .fillCircle(8, 3, 0.1);
    
    // Draw cubes as purple rectangles
    packet.fieldOverlay()
        .setFill("purple")
        .fillRect(1, 1, 0.2, 0.2);
    packet.fieldOverlay()
        .setFill("purple")
        .fillRect(9, 6, 0.2, 0.2);
    
    // Draw scoring zones as semi-transparent green rectangles
    packet.fieldOverlay()
        .setFill("green")
        .setFillAlpha(0.2)
        .fillRect(10, 10, 2, 2);
}

OpMode Controls

FTCDashboard provides powerful remote control capabilities that allow you to start, stop, and control OpModes from the web interface. This is particularly useful for autonomous testing, debugging, and competition scenarios.

Custom Button Actions

// Custom action methods
private void checkCustomActions() {
    // These actions would be triggered by custom buttons in the dashboard
    // For now, we'll use gamepad buttons as examples
    
    if (gamepad1.a && !isExecutingAction) {
        executeForwardAction();
    } else if (gamepad1.b && !isExecutingAction) {
        executeTurnAction();
    } else if (gamepad1.x && !isExecutingAction) {
        executeArmUpAction();
    } else if (gamepad1.y && !isExecutingAction) {
        executeArmDownAction();
    } else if (gamepad1.back) {
        stopAllActions();
    }
}

private void executeForwardAction() {
    isExecutingAction = true;
    currentAction = "Moving Forward";
    
    int targetPosition = (int) TARGET_POSITION;
    leftMotor.setTargetPosition(targetPosition);
    rightMotor.setTargetPosition(targetPosition);
    
    leftMotor.setMode(DcMotor.RunMode.RUN_TO_POSITION);
    rightMotor.setMode(DcMotor.RunMode.RUN_TO_POSITION);
    
    leftMotor.setPower(DRIVE_SPEED);
    rightMotor.setPower(DRIVE_SPEED);
}

private void executeTurnAction() {
    isExecutingAction = true;
    currentAction = "Turning";
    
    // Calculate encoder ticks for turn
    double wheelCircumference = 2 * Math.PI * 2; // 2 inch radius
    double robotCircumference = 2 * Math.PI * 12; // 12 inch robot width
    int targetTicks = (int) ((90.0 / 360.0) * robotCircumference / wheelCircumference * 1440);
    
    leftMotor.setTargetPosition(targetTicks);
    rightMotor.setTargetPosition(-targetTicks);
    
    leftMotor.setMode(DcMotor.RunMode.RUN_TO_POSITION);
    rightMotor.setMode(DcMotor.RunMode.RUN_TO_POSITION);
    
    leftMotor.setPower(TURN_SPEED);
    rightMotor.setPower(TURN_SPEED);
}

private void executeArmUpAction() {
    isExecutingAction = true;
    currentAction = "Arm Up";
    armServo.setPosition(1.0);
    
    // Simulate action completion after a delay
    new Thread(() -> {
        try {
            Thread.sleep(1000);
            stopAllActions();
        } catch (InterruptedException e) {
            // Handle interruption
        }
    }).start();
}

private void executeArmDownAction() {
    isExecutingAction = true;
    currentAction = "Arm Down";
    armServo.setPosition(0.0);
    
    // Simulate action completion after a delay
    new Thread(() -> {
        try {
            Thread.sleep(1000);
            stopAllActions();
        } catch (InterruptedException e) {
            // Handle interruption
        }
    }).start();
}

private void stopAllActions() {
    isExecutingAction = false;
    currentAction = "None";
    
    leftMotor.setPower(0);
    rightMotor.setPower(0);
    
    leftMotor.setMode(DcMotor.RunMode.RUN_USING_ENCODER);
    rightMotor.setMode(DcMotor.RunMode.RUN_USING_ENCODER);
}

Camera Streaming

FTCDashboard provides powerful camera streaming capabilities that allow you to view live video feeds from your robot's cameras directly in the web interface. This is essential for vision-based autonomous programming and real-time robot monitoring.

Camera Integration

private void updateCameraConfiguration() {
    // Update camera settings if they've changed
    if (webcam != null && webcam.isOpen()) {
        try {
            webcam.setResolution(CameraConfig.WIDTH, CameraConfig.HEIGHT);
            webcam.setFps(CameraConfig.FPS);
        } catch (Exception e) {
            telemetry.addData("Camera Config Error", e.getMessage());
        }
    }
}

private void sendCameraData() {
    packet.put("Camera Active", cameraActive);
    packet.put("Frame Count", frameCount);
    packet.put("Camera Width", CameraConfig.WIDTH);
    packet.put("Camera Height", CameraConfig.HEIGHT);
    packet.put("Camera FPS", CameraConfig.FPS);
    packet.put("Enable Streaming", CameraConfig.ENABLE_STREAMING);
    
    // Simulate color detection (in practice, you'd analyze actual camera frames)
    if (CameraConfig.ENABLE_STREAMING) {
        double redIntensity = Math.random() * 255;
        double greenIntensity = Math.random() * 255;
        double blueIntensity = Math.random() * 255;
        
        packet.put("Red Intensity", redIntensity);
        packet.put("Green Intensity", greenIntensity);
        packet.put("Blue Intensity", blueIntensity);
        packet.put("Red Detected", redIntensity > 150);
        packet.put("Green Detected", greenIntensity > 150);
        packet.put("Blue Detected", blueIntensity > 150);
    }
}

@Override
public void stop() {
    // Close camera properly
    if (webcam != null && webcam.isOpen()) {
        webcam.close();
        cameraActive = false;
    }
    
    // Stop all motors for safety
    leftMotor.setPower(0);
    rightMotor.setPower(0);
    
    // Send final status to dashboard
    packet.put("Status", "Stopped");
    packet.put("Final Frame Count", frameCount);
    dashboard.sendTelemetryPacket(packet);
    
    telemetry.addData("Status", "OpMode stopped");
    telemetry.update();
}

Complete Integration Example

Here's how to integrate all dashboard features into a single comprehensive OpMode:

Complete Dashboard Integration

@Override
public void loop() {
    // 1. Update robot position and camera status
    updateRobotPosition();
    updateCameraStatus();
    updateCameraConfiguration();
    
    // 2. Check for custom actions
    checkCustomActions();
    
    // 3. Monitor action completion
    if (isExecutingAction) {
        if (leftMotor.getMode() == DcMotor.RunMode.RUN_TO_POSITION) {
            if (!leftMotor.isBusy() && !rightMotor.isBusy()) {
                stopAllActions();
            }
        }
    }
    
    // 4. Normal robot control when not executing custom action
    if (!isExecutingAction) {
        normalControl();
    }
    
    // 5. Update field view
    setupFieldView();
    drawRobotOnField();
    drawActualPath();
    drawPlannedPath();
    drawGameElements();
    
    // 6. Send all data to dashboard
    sendAllTelemetry();
    
    // 7. Update telemetry
    updateTelemetry();
    
    sleep(50); // 20 Hz update rate
}

private void normalControl() {
    double leftPower = gamepad1.left_stick_y * DriveConfig.MAX_SPEED;
    double rightPower = gamepad1.right_stick_y * DriveConfig.MAX_SPEED;
    
    // Handle turning
    if (Math.abs(gamepad1.right_stick_x) > 0.1) {
        leftPower = DriveConfig.TURN_SPEED * gamepad1.right_stick_x;
        rightPower = -DriveConfig.TURN_SPEED * gamepad1.right_stick_x;
    }
    
    leftMotor.setPower(leftPower);
    rightMotor.setPower(rightPower);
}

private void sendAllTelemetry() {
    // Basic telemetry
    packet.put("Loop Time", getRuntime());
    packet.put("Battery Voltage", getBatteryVoltage());
    packet.put("Status", isExecutingAction ? "Executing Action" : "Ready");
    packet.put("Current Action", currentAction);
    
    // Motor data
    packet.put("Left Motor Power", leftMotor.getPower());
    packet.put("Right Motor Power", rightMotor.getPower());
    packet.put("Left Encoder", leftMotor.getCurrentPosition());
    packet.put("Right Encoder", rightMotor.getCurrentPosition());
    
    // Robot position
    packet.put("Robot X", x);
    packet.put("Robot Y", y);
    packet.put("Robot Heading", heading);
    
    // Configuration values
    packet.put("Drive Speed", DriveConfig.MAX_SPEED);
    packet.put("Turn Speed", DriveConfig.TURN_SPEED);
    packet.put("PID Kp", PIDConfig.KP);
    packet.put("PID Ki", PIDConfig.KI);
    packet.put("PID Kd", PIDConfig.KD);
    
    // Camera data
    sendCameraData();
    
    // Send to dashboard
    dashboard.sendTelemetryPacket(packet);
}

private void updateTelemetry() {
    telemetry.addData("Status", isExecutingAction ? "Executing Action" : "Ready");
    telemetry.addData("Current Action", currentAction);
    telemetry.addData("Left Power", "%.2f", leftMotor.getPower());
    telemetry.addData("Right Power", "%.2f", rightMotor.getPower());
    telemetry.addData("Position", "(%.2f, %.2f)", x, y);
    telemetry.addData("Heading", "%.2f rad", heading);
    telemetry.addData("Camera Active", cameraActive);
    telemetry.addData("Frame Count", frameCount);
    telemetry.update();
}

Practice Exercise: Comprehensive Dashboard Implementation

Create a complete OpMode that demonstrates all FTCDashboard features working together.

  • Set up FTCDashboard with all required imports and initialization
  • Implement real-time data plotting with multiple data series
  • Create configuration variables for different robot parameters
  • Add field view visualization with robot tracking and path planning
  • Implement custom button actions for robot control
  • Add camera streaming with basic image processing
  • Test all features and verify they work together properly
// Your comprehensive OpMode should include:
// 1. Dashboard initialization and hardware setup
// 2. Real-time plotting of motor data, position, and sensor values
// 3. Configuration variables for PID, drive, and camera settings
// 4. Field view with robot position, planned path, and actual path
// 5. Custom buttons for autonomous actions and testing
// 6. Camera streaming with basic color detection
// 7. Integration of all features in a single loop() method

Dashboard Best Practices

  • Use appropriate update frequencies (10-20 Hz) to avoid overwhelming the interface
  • Organize configuration variables into logical groups
  • Use different colors and line styles for clear data visualization
  • Implement proper error handling for all dashboard features
  • Test dashboard functionality thoroughly before competition
  • Document custom button functions and configuration parameters
  • Monitor performance and adjust settings as needed

Troubleshooting Dashboard Issues

  • Dashboard not accessible: Check network connection and firewall settings
  • No data appearing: Ensure MultipleTelemetry is used and telemetry.update() is called
  • Configuration not updating: Verify variables are static and public with @Config annotation
  • Field view not showing: Check fieldOverlay() methods and coordinate system
  • Camera not streaming: Verify camera hardware and configuration settings
  • Performance issues: Reduce update frequency or simplify data being sent
  • Custom buttons not working: Check button implementation and event handling

Further Reading Resources

Open full interactive app