CrocoDocs
CrocoDocs

Introduction

What is CrocoDocs?Season Breakdown

Getting Started

Programming in FTCJavaBlocksAndroid Studio

Control Systems

IntroductionJoystick MappingPID ControlMotion ProfilingKalman FilterLow-Pass Filter

Autonomous

IntroductionTime vs Encoder-Based MovementOdometryMotion PlanningPure PursuitSensor Fusion

Codebase Etiquette and Good Practices

IntroductionNaming ConventionsCode OrganizationComments and DocumentationTeam Collaboration

Libraries

LibrariesNextFTCPedro PathingFTC DashboardMercurialPanelsSloth

Sensors and Vision

Vision OverviewVision Basics

Mercurial

Command-based robotics framework by Dairy Foundation

What is Mercurial?

Mercurial is a command-based framework for FTC developed by Dairy Foundation. It provides a clean, modern way to organize robot code using commands, subsystems, and scheduled operations - similar to WPILib's command framework but designed specifically for FTC with reduced boilerplate.

Mercurial is part of the larger Dairy Foundation ecosystem, which includes:

  • Core: Runtime and lifecycle management
  • Util: Type-safe units and utilities
  • Pasteurized: Advanced gamepad handling
  • Mercurial: Command-based framework (this library)

Why Choose Mercurial?

Traditional FTC Code Problems:

  • All logic mixed in OpMode classes
  • Hard to reuse code between OpModes
  • Difficult to test individual components
  • Complex state management

Mercurial Solutions:

  • Separation of concerns (commands vs subsystems)
  • Reusable command compositions
  • Unit-tested scheduler
  • Lambda-based command creation
  • Reduced boilerplate vs other frameworks
  • Modern Kotlin-friendly design

Installation

Add to your build.gradle (Module: TeamCode):

dependencies {
    // Mercurial (includes bundled dependencies)
    implementation "dev.frozenmilk.mercurial:Mercurial:1.0.0"
    
    // Required dependencies (if not auto-included)
    implementation "dev.frozenmilk.dairy:Core:1.0.0"
    implementation "dev.frozenmilk.dairy:Util:1.0.1"
}
dependencies {
    // Mercurial (includes bundled dependencies)
    implementation("dev.frozenmilk.mercurial:Mercurial:1.0.0")
    
    // Required dependencies (if not auto-included)
    implementation("dev.frozenmilk.dairy:Core:1.0.0")
    implementation("dev.frozenmilk.dairy:Util:1.0.1")
}

Core Concepts

1. Commands

Commands represent actions your robot performs:

  • Move arm to position
  • Drive forward for distance
  • Intake game element
  • Wait for time

Commands can be:

  • Sequential: Run one after another
  • Parallel: Run at the same time
  • Conditional: Run based on conditions
  • Lambda-based: Quick inline definitions

2. Subsystems

Subsystems represent robot mechanisms:

  • Drivetrain
  • Arm
  • Intake
  • Shooter

Each subsystem:

  • Owns specific hardware
  • Prevents conflicting commands
  • Provides methods for commands to use

3. Scheduler

The Scheduler manages command execution:

  • Runs commands at the right time
  • Handles interruptions
  • Manages subsystem requirements
  • Provides lifecycle management

Basic Usage

Creating a Subsystem

import dev.frozenmilk.mercurial.subsystems.Subsystem;

public class ArmSubsystem implements Subsystem {
    private DcMotor armMotor;
    private double targetPosition;
    
    public ArmSubsystem(HardwareMap hardwareMap) {
        armMotor = hardwareMap.get(DcMotor.class, "arm");
        armMotor.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER);
        armMotor.setMode(DcMotor.RunMode.RUN_TO_POSITION);
    }
    
    @Override
    public void periodic() {
        // Called every loop - update telemetry, read sensors, etc.
        armMotor.setTargetPosition((int) targetPosition);
        armMotor.setPower(0.8);
    }
    
    // Command-callable methods
    public void setPosition(double position) {
        this.targetPosition = position;
    }
    
    public boolean atTarget() {
        return Math.abs(armMotor.getCurrentPosition() - targetPosition) < 10;
    }
    
    public void stop() {
        armMotor.setPower(0);
    }
}
import dev.frozenmilk.mercurial.subsystems.Subsystem

class ArmSubsystem(hardwareMap: HardwareMap) : Subsystem {
    private val armMotor: DcMotor = hardwareMap.get(DcMotor::class.java, "arm")
    private var targetPosition: Double = 0.0
    
    init {
        armMotor.mode = DcMotor.RunMode.STOP_AND_RESET_ENCODER
        armMotor.mode = DcMotor.RunMode.RUN_TO_POSITION
    }
    
    override fun periodic() {
        // Called every loop - update telemetry, read sensors, etc.
        armMotor.targetPosition = targetPosition.toInt()
        armMotor.power = 0.8
    }
    
    // Command-callable methods
    fun setPosition(position: Double) {
        targetPosition = position
    }
    
    fun atTarget(): Boolean {
        return Math.abs(armMotor.currentPosition - targetPosition) < 10
    }
    
    fun stop() {
        armMotor.power = 0.0
    }
}

Creating Commands

import dev.frozenmilk.mercurial.commands.Command;
import dev.frozenmilk.mercurial.commands.util.Wait;

public class ArmCommands {
    private ArmSubsystem arm;
    
    public ArmCommands(ArmSubsystem arm) {
        this.arm = arm;
    }
    
    // Lambda-based instant command
    public Command moveToHigh() {
        return Command.once(() -> arm.setPosition(1000))
                     .requires(arm);
    }
    
    // Command that waits until arm reaches target
    public Command moveToHighAndWait() {
        return Command.once(() -> arm.setPosition(1000))
                     .andThen(Command.waitUntil(() -> arm.atTarget()))
                     .requires(arm);
    }
    
    // Sequential command chain
    public Command scoreSample() {
        return moveToHighAndWait()
                .andThen(Command.wait(0.5))  // Wait to settle
                .andThen(openClaw())
                .andThen(Command.wait(0.3))
                .andThen(moveToLow());
    }
    
    // Parallel commands
    public Command intakeAndDrive() {
        return Command.parallel(
            startIntake(),
            driveForward(24)
        );
    }
}
import dev.frozenmilk.mercurial.commands.Command
import dev.frozenmilk.mercurial.commands.util.Wait

class ArmCommands(private val arm: ArmSubsystem) {
    
    // Lambda-based instant command
    fun moveToHigh(): Command {
        return Command.once { arm.setPosition(1000.0) }
            .requires(arm)
    }
    
    // Command that waits until arm reaches target
    fun moveToHighAndWait(): Command {
        return Command.once { arm.setPosition(1000.0) }
            .andThen(Command.waitUntil { arm.atTarget() })
            .requires(arm)
    }
    
    // Sequential command chain
    fun scoreSample(): Command {
        return moveToHighAndWait()
            .andThen(Command.wait(0.5))  // Wait to settle
            .andThen(openClaw())
            .andThen(Command.wait(0.3))
            .andThen(moveToLow())
    }
    
    // Parallel commands
    fun intakeAndDrive(): Command {
        return Command.parallel(
            startIntake(),
            driveForward(24.0)
        )
    }
}

Using Commands in OpMode

import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import com.qualcomm.robotcore.eventloop.opmode.OpMode;
import dev.frozenmilk.mercurial.Mercurial;
import dev.frozenmilk.mercurial.commands.Command;

@TeleOp(name = "Mercurial TeleOp")
public class MercurialTeleOp extends OpMode {
    private ArmSubsystem arm;
    private ArmCommands armCommands;
    private DriveSubsystem drive;
    
    @Override
    public void init() {
        // Initialize subsystems
        arm = new ArmSubsystem(hardwareMap);
        drive = new DriveSubsystem(hardwareMap);
        
        // Initialize commands
        armCommands = new ArmCommands(arm);
        
        // Register subsystems with Mercurial
        Mercurial.register(arm);
        Mercurial.register(drive);
    }
    
    @Override
    public void loop() {
        // Manual drive control
        double drive = -gamepad1.left_stick_y;
        double turn = gamepad1.right_stick_x;
        this.drive.drive(drive, turn);
        
        // Bind commands to buttons
        if (gamepad1.a) {
            // Schedule command - runs until complete
            Mercurial.schedule(armCommands.moveToHigh());
        }
        
        if (gamepad1.b) {
            Mercurial.schedule(armCommands.moveToLow());
        }
        
        if (gamepad1.x) {
            // Schedule complex command sequence
            Mercurial.schedule(armCommands.scoreSample());
        }
        
        // Mercurial automatically calls subsystem.periodic() and runs commands
    }
}
import com.qualcomm.robotcore.eventloop.opmode.TeleOp
import com.qualcomm.robotcore.eventloop.opmode.OpMode
import dev.frozenmilk.mercurial.Mercurial
import dev.frozenmilk.mercurial.commands.Command

@TeleOp(name = "Mercurial TeleOp")
class MercurialTeleOp : OpMode() {
    private lateinit var arm: ArmSubsystem
    private lateinit var armCommands: ArmCommands
    private lateinit var drive: DriveSubsystem
    
    override fun init() {
        // Initialize subsystems
        arm = ArmSubsystem(hardwareMap)
        drive = DriveSubsystem(hardwareMap)
        
        // Initialize commands
        armCommands = ArmCommands(arm)
        
        // Register subsystems with Mercurial
        Mercurial.register(arm)
        Mercurial.register(drive)
    }
    
    override fun loop() {
        // Manual drive control
        val driveValue = -gamepad1.left_stick_y.toDouble()
        val turn = gamepad1.right_stick_x.toDouble()
        drive.drive(driveValue, turn)
        
        // Bind commands to buttons
        if (gamepad1.a) {
            // Schedule command - runs until complete
            Mercurial.schedule(armCommands.moveToHigh())
        }
        
        if (gamepad1.b) {
            Mercurial.schedule(armCommands.moveToLow())
        }
        
        if (gamepad1.x) {
            // Schedule complex command sequence
            Mercurial.schedule(armCommands.scoreSample())
        }
        
        // Mercurial automatically calls subsystem.periodic() and runs commands
    }
}

Advanced Features

Conditional Commands

// Run different commands based on condition
public Command smartScore() {
    return Command.select(
        () -> arm.getCurrentPosition() > 500,  // Condition
        moveToHigh(),      // If true
        moveToMid()        // If false
    );
}

// Run command only if condition is true
public Command safeScore() {
    return Command.waitUntil(() -> sensor.isReady())
                 .andThen(score());
}
// Run different commands based on condition
fun smartScore(): Command {
    return Command.select(
        { arm.currentPosition > 500 },  // Condition
        moveToHigh(),      // If true
        moveToMid()        // If false
    )
}

// Run command only if condition is true
fun safeScore(): Command {
    return Command.waitUntil { sensor.isReady() }
        .andThen(score())
}

Parallel Racing and Deadlines

// Run commands in parallel, end when first finishes
public Command raceIntake() {
    return Command.race(
        spinIntake(),           // Spin forever
        Command.wait(2.0)       // Timeout after 2 seconds
    );
}

// Run commands in parallel, end when specific one finishes
public Command driveWithTimeout() {
    return Command.deadline(
        driveForward(48),       // Main command (determines when to stop)
        updateTelemetry(),      // Side effect (runs while main runs)
        monitorSensors()        // Another side effect
    );
}
// Run commands in parallel, end when first finishes
fun raceIntake(): Command {
    return Command.race(
        spinIntake(),           // Spin forever
        Command.wait(2.0)       // Timeout after 2 seconds
    )
}

// Run commands in parallel, end when specific one finishes
fun driveWithTimeout(): Command {
    return Command.deadline(
        driveForward(48.0),     // Main command (determines when to stop)
        updateTelemetry(),      // Side effect (runs while main runs)
        monitorSensors()        // Another side effect
    )
}

Command Decorators

// Run command repeatedly
Command repeatIntake = spinIntake().repeatedly();

// Run with timeout
Command timedDrive = driveForward(24).withTimeout(3.0);

// Run until condition
Command driveToLine = driveForward(100)
    .until(() -> colorSensor.red() > 200);

// Add action before/after
Command scoringSequence = moveToHigh()
    .beforeStarting(() -> telemetry.speak("Scoring"))
    .andThen(() -> telemetry.speak("Complete"));
// Run command repeatedly
val repeatIntake = spinIntake().repeatedly()

// Run with timeout
val timedDrive = driveForward(24.0).withTimeout(3.0)

// Run until condition
val driveToLine = driveForward(100.0)
    .until { colorSensor.red() > 200 }

// Add action before/after
val scoringSequence = moveToHigh()
    .beforeStarting { telemetry.speak("Scoring") }
    .andThen { telemetry.speak("Complete") }

Autonomous Example

@Autonomous(name = "Mercurial Auto")
public class MercurialAuto extends OpMode {
    private DriveSubsystem drive;
    private ArmSubsystem arm;
    private IntakeSubsystem intake;
    
    @Override
    public void init() {
        drive = new DriveSubsystem(hardwareMap);
        arm = new ArmSubsystem(hardwareMap);
        intake = new IntakeSubsystem(hardwareMap);
        
        Mercurial.register(drive);
        Mercurial.register(arm);
        Mercurial.register(intake);
        
        // Build autonomous sequence
        Command auto = Command.sequential(
            // Score preload
            arm.moveToHigh(),
            Command.wait(1.0),
            intake.outtake(),
            Command.wait(0.5),
            arm.moveToGround(),
            
            // Get sample 1
            drive.driveForward(24),
            Command.parallel(
                intake.intake(),
                arm.moveToIntake()
            ),
            Command.wait(1.0),
            
            // Score sample 1
            drive.driveBackward(24),
            arm.moveToHigh(),
            Command.wait(1.0),
            intake.outtake(),
            
            // Park
            arm.moveToTravel(),
            drive.strafeRight(36),
            drive.driveForward(12)
        );
        
        // Schedule it to run when OpMode starts
        Mercurial.schedule(auto);
    }
    
    @Override
    public void loop() {
        // Mercurial handles everything automatically
        telemetry.addData("Status", "Running autonomous");
        telemetry.update();
    }
}
@Autonomous(name = "Mercurial Auto")
class MercurialAuto : OpMode() {
    private lateinit var drive: DriveSubsystem
    private lateinit var arm: ArmSubsystem
    private lateinit var intake: IntakeSubsystem
    
    override fun init() {
        drive = DriveSubsystem(hardwareMap)
        arm = ArmSubsystem(hardwareMap)
        intake = IntakeSubsystem(hardwareMap)
        
        Mercurial.register(drive)
        Mercurial.register(arm)
        Mercurial.register(intake)
        
        // Build autonomous sequence
        val auto = Command.sequential(
            // Score preload
            arm.moveToHigh(),
            Command.wait(1.0),
            intake.outtake(),
            Command.wait(0.5),
            arm.moveToGround(),
            
            // Get sample 1
            drive.driveForward(24.0),
            Command.parallel(
                intake.intake(),
                arm.moveToIntake()
            ),
            Command.wait(1.0),
            
            // Score sample 1
            drive.driveBackward(24.0),
            arm.moveToHigh(),
            Command.wait(1.0),
            intake.outtake(),
            
            // Park
            arm.moveToTravel(),
            drive.strafeRight(36.0),
            drive.driveForward(12.0)
        )
        
        // Schedule it to run when OpMode starts
        Mercurial.schedule(auto)
    }
    
    override fun loop() {
        // Mercurial handles everything automatically
        telemetry.addData("Status", "Running autonomous")
        telemetry.update()
    }
}

Mercurial vs Other Frameworks

FeatureMercurialNextFTCFTCLib Commands
Learning CurveModerateLowModerate
BoilerplateMinimalMediumHigher
Lambda Commands✅ Yes✅ YesLimited
Kotlin Support✅ Excellent✅ Good⚠️ Java-focused
Command Composition✅ Rich✅ Good✅ Rich
EcosystemDairy FoundationStandaloneFTCLib Suite
DocumentationGrowingExtensiveExtensive
CommunitySmallGrowingLarge

When to Choose Mercurial

Choose Mercurial if:

  • You want modern, low-boilerplate code
  • You prefer Kotlin or Kotlin-friendly Java
  • You like lambda-based command definitions
  • You value clean, composable command structures
  • You're interested in the Dairy Foundation ecosystem

Choose something else if:

  • You want the largest community (use FTCLib)
  • You want the most documentation (use NextFTC)
  • You prefer imperative programming (use plain OpModes)
  • Your team is unfamiliar with functional concepts

Tips for Success

  1. Start Simple: Begin with one subsystem and a few commands
  2. Think in Actions: Break robot behavior into discrete commands
  3. Compose Commands: Build complex sequences from simple ones
  4. Use Lambda Commands: Quick inline definitions reduce code
  5. Test Subsystems: Test each subsystem independently first
  6. Read Dairy Docs: Check docs.dairy.foundation for updates

Common Patterns

Button Binding with Edge Detection

public class TeleOp extends OpMode {
    private boolean lastA = false;
    
    @Override
    public void loop() {
        // Edge detection - only trigger on button press, not hold
        boolean currentA = gamepad1.a;
        if (currentA && !lastA) {
            Mercurial.schedule(armCommands.moveToHigh());
        }
        lastA = currentA;
    }
}
class TeleOp : OpMode() {
    private var lastA = false
    
    override fun loop() {
        // Edge detection - only trigger on button press, not hold
        val currentA = gamepad1.a
        if (currentA && !lastA) {
            Mercurial.schedule(armCommands.moveToHigh())
        }
        lastA = currentA
    }
}

Default Commands for Subsystems

@Override
public void init() {
    arm = new ArmSubsystem(hardwareMap);
    
    // Set default command - runs when no other command uses this subsystem
    arm.setDefaultCommand(
        Command.repeating(() -> {
            double power = -gamepad2.left_stick_y;
            arm.manualControl(power);
        }).requires(arm)
    );
}
override fun init() {
    arm = ArmSubsystem(hardwareMap)
    
    // Set default command - runs when no other command uses this subsystem
    arm.setDefaultCommand(
        Command.repeating {
            val power = -gamepad2.left_stick_y.toDouble()
            arm.manualControl(power)
        }.requires(arm)
    )
}

Resources

  • Official Docs: docs.dairy.foundation
  • GitHub: Dairy Foundation GitHub
  • Maven Repository: Maven Central

Next Steps

  1. Add Mercurial to your build.gradle
  2. Create a simple subsystem (arm or intake)
  3. Write basic commands for it
  4. Test in a TeleOp OpMode
  5. Build command sequences for autonomous
  6. Explore Pasteurized for advanced gamepad handling
  7. Check Dairy Foundation docs for updates

FTC Dashboard

Real-time telemetry, configuration, and debugging tool

Panels

All-in-one plugin-based dashboard for FTC robots

On this page

What is Mercurial?Why Choose Mercurial?InstallationCore Concepts1. Commands2. Subsystems3. SchedulerBasic UsageCreating a SubsystemCreating CommandsUsing Commands in OpModeAdvanced FeaturesConditional CommandsParallel Racing and DeadlinesCommand DecoratorsAutonomous ExampleMercurial vs Other FrameworksWhen to Choose MercurialTips for SuccessCommon PatternsButton Binding with Edge DetectionDefault Commands for SubsystemsResourcesNext Steps