Missions

Concept: What a Mission Is

A mission is a self-contained task: “drive to the cone and pick it up”, “follow the line to the basket”, “go home”. In RaccoonOS, a match is a sequence of missions — the runner executes them in declaration order, one at a time. When a mission’s sequence() returns, the next mission starts immediately.

The overall shape of every match looks like this:

flowchart TD
    A([Robot starts]) --> B[M000 SetupMission\nHomes hardware, calibrates, waits for light]
    B --> C{Start signal received}
    C --> D[M010 First mission]
    D --> E[M020 Second mission]
    E --> F[...]
    F --> G[M999 ShutdownMission\nParks motors, retracts arms]
    G --> H([Match over])

    style B fill:#4CAF50,color:#fff
    style G fill:#F44336,color:#fff
    style D fill:#42A5F5,color:#fff
    style E fill:#42A5F5,color:#fff
    style F fill:#42A5F5,color:#fff

Every robot has exactly one setup mission (M000), zero or more game missions (M010–M990), and exactly one shutdown mission (M999). This triple role — setup / game / shutdown — is why the framework recognizes three-digit mission prefixes with slots for insertion (M010, M020, …) without renumbering.

Execution order comes from missions.yml, not from class-name prefixes. The number in the name is a convention for readability and sorting on disk. The runner reads the list in config/missions.yml and respects exactly that order.

You can comment out a mission in missions.yml to disable it without deleting code. This is a standard competition workflow — use YAML # comments when you want to quickly enable or disable a task mid-competition.

Writing a Mission

Every mission extends the Mission base class and implements a sequence() method that returns a step tree:

from raccoon import *
from src.hardware.defs import Defs


class M010DriveToConeMission(Mission):
    def sequence(self) -> Sequential:
        return seq([
            drive_forward(25),
            turn_right(90),
            drive_forward(15),
            Defs.claw.open(),
        ])

That’s it. The framework handles execution, timing, error handling, and hardware cleanup.

Real-World Example: ConeBot

Here’s the actual mission from the Ecer2026 ConeBot that drives to a cone on the game table:

from raccoon import *
from src.hardware.defs import Defs


class M010DriveToConeMission(Mission):
    def sequence(self) -> Sequential:
        return seq([
            # Start position: against the wall
            Defs.cone_arm_servo.up(),
            drive_backward(3),
            turn_right(55),

            # Align against the back wall
            wall_align_backward(accel_threshold=0.3),
            drive_forward(2),

            # Sweep to check for cone
            Defs.cone_arm_servo.down(),
            turn_left(25),
            turn_right(25),
            Defs.cone_arm_servo.up(),

            # Drive to the line and follow it
            Defs.front.drive_until_black(),
            forward_single_lineup(
                Defs.front.right,
                entry_threshold=0.9,
                exit_threshold=0.7,
                correction_side=CorrectionSide.RIGHT,
                forward_speed=1.0,
            ),

            # Drive along the line to the cone
            drive_forward(27),
            turn_right(90),
            Defs.front.drive_until_black(),
        ])

Notice how the mission reads almost like natural language: “raise arm, back up, turn, align against wall, find the line, follow it, drive to the cone.”

Composing Steps

Sequential Execution

seq([...]) runs steps one after another. Each step must finish before the next one starts:

seq([
    drive_forward(25),     # Step 1: drive 25 cm
    turn_right(90),        # Step 2: turn 90 degrees (after step 1 finishes)
    drive_forward(25),     # Step 3: drive 25 cm (after step 2 finishes)
])

Parallel Execution

parallel(...) runs multiple things at the same time. Each argument is an independent track:

parallel(
    drive_forward(50),                          # Track 1: drive forward
    seq([                                        # Track 2: arm movement
        wait_until_distance(20),                 #   wait until 20cm traveled
        Defs.arm.down(),                         #   then lower the arm
    ]),
)

Parallel finishes when all tracks complete. You can pass individual steps, lists of steps (implicitly sequential), or explicit seq([...]) blocks.

Resource safety: Parallel validates that no two tracks use the same hardware resource. You can’t drive forward and turn at the same time, because both need the drive system. The framework will raise an error before execution if it detects a conflict.

Real Parallel Example

From the PackingBot — driving forward while operating a grabber:

parallel(
    # Track 1: Follow the line edge
    Defs.front.follow_right_edge(999).until(
        after_cm(125) & on_black(Defs.front.right)
    ),

    # Track 2: Grab POMs during the drive
    seq([
        wait_until_distance(35),      # Wait until we've traveled 35cm
        Defs.pom_grab.closed(),       # Close grabber
        Defs.pom_arm.up(),            # Lift arm
    ]),

    # Track 3: Put arm back down after passing edge
    seq([
        wait_until_distance(45),
        Defs.pom_arm.down(),
    ]),

    # Track 4: Pre-adjust grabber width
    Defs.pom_grab.slightly_open(),
)

Four things happening simultaneously: line following, grabbing objects at the right moment, repositioning the arm, and adjusting the grabber width.

Stop Conditions

Many steps accept a .until(condition) clause that controls when the step finishes:

drive_forward(speed=0.8).until(on_black(Defs.front.right))
drive_forward(speed=1.0).until(on_black(Defs.front.right) | after_cm(50))
drive_forward(speed=1.0).until(after_cm(10) + on_black(Defs.front.right))

Conditions can be combined with | (OR), & (AND), + (THEN), and grouped with parentheses for complex logic. See Stop Conditions for the full reference.

Control Flow

Looping

# Repeat a step 5 times
loop_for(drive_forward(10), iterations=5)

# Repeat forever (use inside do_while_active to stop it)
loop_forever(seq([
    drive_forward(10),
    turn_right(90),
]))

Do While Active

Run a task that stops when a reference step finishes:

do_while_active(
    reference_step=drive_forward(100),  # This controls the lifetime
    task=loop_forever(                   # This runs alongside and gets cancelled
        seq([
            Defs.arm.up(),
            wait_for_seconds(0.5),
            Defs.arm.down(),
            wait_for_seconds(0.5),
        ])
    ),
)

Conditional Branching with if_then()

if_then() evaluates a predicate at runtime and executes one of two branches. The predicate receives the robot instance and must return a boolean quickly — it runs synchronously and should not do long-running work.

from raccoon import *

# Sensor-gated branch: take different paths depending on what the robot detects
if_then(
    lambda robot: robot.defs.front_right_ir.read() > 500,
    then_step=turn_left(90),    # sensor above threshold: go left
    else_step=turn_right(90),   # sensor below threshold: go right
)

The else_step is optional — omitting it means “do nothing on the false branch”:

# Only open the claw if we're close enough
if_then(
    lambda robot: robot.defs.distance_sensor.read() < 300,
    then_step=Defs.claw.open(),
)

Resource validation spans both branches, because either may execute. If both branches claim the same hardware, the framework raises at validation time.

Background Steps

background() starts a step asynchronously and returns immediately, letting the next step in the sequence proceed while the background step runs concurrently. Use wait_for_background() to synchronize later:

from raccoon import *

seq([
    # Launch the servo move, then immediately start driving — no waiting
    background(Defs.arm.down(), name="arm"),
    drive_forward(30),
    # Wait for the arm to finish before continuing
    wait_for_background("arm"),
    Defs.claw.open(),
])

Background steps differ from parallel() in two ways:

  • They do not block the sequence — execution continues to the next step immediately.
  • They are preemptable: if a later foreground step claims a resource that the background step holds, the background step is cancelled with a warning. The foreground step takes priority.

wait_for_background() without a name waits for all currently running background steps:

seq([
    background(scan_step()),
    background(servo_step()),
    do_something_else(),
    wait_for_background(),   # wait for both scan_step and servo_step
])

If a background step finishes before wait_for_background() is reached, the wait returns immediately.

Cross-Mission Background Synchronization

Named background tasks are run-scoped, not mission-scoped. A task started with background(step, name="x") at the end of one mission can be awaited with wait_for_background("x") at the start of the next mission. This is the standard idiom for overlapping arm motion with a mission transition — the arm keeps moving while the runner starts loading the next mission.

The following is adapted from the Ecer2026 ClawBot — M030 kicks off a tray-return background move, and M040 awaits it at its own beginning while already doing other work:

# --- M030CollectDrumsMission ---
class M030CollectDrumsMission(Mission):
    def sequence(self) -> Sequential:
        return seq([
            # ... collect drums ...
            return_tray_to_tray_holder_phase1(),
            # Kick off phase 2 in the background — it will continue into M040
            background(
                return_tray_to_tray_holder_phase2(),
                name="return_tray",
            ),
        ])


# --- M040CollectBotguyMission ---
class M040CollectBotguyMission(Mission):
    def sequence(self) -> Sequential:
        return seq([
            drive_backward().until(on_white(Defs.front.left)),
            backward_line_follow().until(
                after_cm(30) + over_line(Defs.front.right) + after_cm(2)
            ),
            line_follow().until(on_black(Defs.front.right)),
            # Now wait for the tray return that was started in M030
            wait_for_background("return_tray"),
            # ... continue ...
        ])

The key insight: background() and wait_for_background() work across mission boundaries because background handles are keyed by name in the robot’s run context, which persists for the entire match.

Timeout Steps

Wrap any step with a hard time limit using timeout(). If the step completes within the budget it finishes normally; if it exceeds the limit, the step is cancelled and TimeoutError propagates up the sequence, stopping the mission:

from raccoon import *

# Give the arm motor 5 seconds to reach position — cancel if it stalls
timeout(
    motor_move_to(Defs.arm_motor, position=300, velocity=800),
    seconds=5.0,
)

# Ensure the operator presses the button within 30 seconds
timeout(wait_for_button(), seconds=30.0)

Use timeout_or() when you want a recovery action instead of an error:

from raccoon import *

# Try driving 30 cm; if stuck after 3 s, back up 5 cm instead
timeout_or(
    drive_forward(30),
    seconds=3.0,
    fallback=drive_backward(5),
)

timeout_or() never raises — if the primary step times out, the fallback runs and the sequence continues normally.

Real example: The Ecer2026 ClawBot wraps a sensor-guided forward step with a tight timeout to guard against missing the line — if the line isn’t found within 0.5 seconds, the sequence moves on rather than driving indefinitely:

parallel(
    timeout(
        drive_forward().until(on_black(Defs.front.left)),
        seconds=0.5,
    ),
    arm.move_angles(-90, 40, -30),
)
FunctionTimeout behavior
timeout(step, seconds)Raises TimeoutError — mission stops
timeout_or(step, seconds, fallback)Runs fallback — mission continues

Watchdog Timers

Watchdogs are keepalive timers that cancel the mission if they are not “fed” within a deadline. Use them to detect stuck hardware: arm a watchdog before a critical phase, feed it after each step that must complete, and disarm it when the phase is done. If any step hangs and the feed never arrives, the watchdog fires, the mission is cancelled, and the shutdown mission runs.

from raccoon import *

seq([
    start_watchdog("scoring", timeout=5.0),  # Arm: cancel if not fed within 5 s
    drive_forward(30),
    feed_watchdog("scoring"),                 # Feed: reset the deadline
    drive_forward(30),
    feed_watchdog("scoring"),                 # Feed again for the next step
    stop_watchdog("scoring"),                 # Disarm: phase complete
])
StepParametersWhat it does
start_watchdog(name, timeout)name: str = "default", timeout: floatArms a watchdog; fires after timeout seconds if not fed
feed_watchdog(name)name: str = "default"Resets the deadline to now + timeout
stop_watchdog(name)name: str = "default"Disarms the watchdog; no expiry can fire after this

Multiple watchdogs can run simultaneously by using distinct names. Feeding or stopping a non-existent watchdog logs a warning but does not raise.

The WatchdogManager behind these steps is also used internally by the robot runner for Mission.time_budget deadlines (see the Mission Budget section below). On expiry, the watchdog cancels the main-mission task via the same code path as the global shutdown_in timer — the shutdown mission still runs, motors are stopped, and the process exits cleanly.

Environment-Gated Steps

run_if_env() and its named shortcuts let you conditionally skip steps based on the flags raccoon run was launched with. The gate is evaluated at execution time (not when the step tree is built), so it always reflects the actual run mode:

Guard stepRuns when…CLI flag it mirrors
run_unless_no_calibrate(step)--no-calibrate was not passedraccoon run --no-calibrate
run_unless_no_checkpoints(step)--no-checkpoints was not passedraccoon run --no-checkpoints
run_if_debug(step)--debug was passedraccoon run --debug
run_if_dev(step)--dev was passedraccoon run --dev
run_if_env(step, var, equals, negate)Custom env-var gate(any variable)
from raccoon import *

class M000SetupMission(SetupMission):
    setup_time = 120

    def sequence(self) -> Sequential:
        return seq([
            # Skip calibration on fast development runs
            run_unless_no_calibrate(calibrate(distance_cm=50)),

            # Only show an interactive pause in debug mode
            run_if_debug(wait_for_button("Check arm position")),

            # A dev-only sanity hop
            run_if_dev(drive_forward(5)),
        ])

The typical workflow: develop and iterate with raccoon run --no-calibrate --no-checkpoints to skip the slow setup steps. Run without flags on the competition robot.

The generic run_if_env() lets you gate on any environment variable:

from raccoon.step.logic import run_if_env

# Only run when MY_FLAG=1
run_if_env(some_step(), "MY_FLAG")

# Only run when DEMO_MODE is unset
run_if_env(some_step(), "DEMO_MODE", equals=None)

# Run unless MY_FLAG=1 (inverted)
run_if_env(some_step(), "MY_FLAG", negate=True)

Inline Code with run()

Execute arbitrary Python code as a step:

seq([
    drive_forward(25),
    run(lambda robot: print("Reached the cone!")),
    Defs.claw.open(),
])

run() accepts sync or async callables:

async def check_sensor(robot):
    value = robot.defs.front_right_ir.read()
    if value > 2000:
        print("On black line")

seq([
    run(check_sensor),
    drive_forward(10),
])

Deferred Steps

Build a step at runtime based on robot state:

def choose_direction(robot):
    if robot.defs.front_right_ir.read() > 2000:
        return turn_left(90)
    else:
        return turn_right(90)

seq([
    drive_forward(25),
    defer(choose_direction),   # Decides at runtime
    drive_forward(25),
])

Why defer() instead of if_then()? The key difference is when the step tree is built vs when it runs. The entire seq([...]) list is constructed before the mission starts — any Python that reads sensor state at module level runs too early. defer() receives a factory function that is called at the moment the step executes, so it always sees live robot state.

Real example from the PackingBot — a sensor-gated strafe that only fires if the robot detects it landed in the wrong position:

def drive_if_sensor_triggered(sensor):
    def _build(robot):
        if sensor.read() > 500:     # live sensor read at execution time
            return seq([drive_forward(35)])
        else:
            return seq([])           # no-op branch
    return defer(_build)

The comment in the source is explicit: “defer = evaluate _build function at runtime and not compile-time.” When in doubt, use if_then() for simple boolean branches and defer() when you need to inspect any live state to pick among multiple step trees.

Setup Mission

Extending SetupMission

The setup mission runs before the match starts. It must extend SetupMission, not plain Mission. The robot runner enforces this with a TypeError at startup if it receives a plain Mission instance:

from raccoon import *
from src.hardware.defs import Defs


class M000SetupMission(SetupMission):
    setup_time = 120  # 2-minute countdown shown on the UI

    def sequence(self) -> Sequential:
        return seq([
            # Home all servos to known positions
            motor_off(Defs.cone_container_motor),
            Defs.claw.closed(),
            Defs.arm.up(),

            # Run distance calibration (skip with raccoon run --no-calibrate)
            run_unless_no_calibrate(calibrate(distance_cm=50)),
        ])

calibrate() defaults to distance_cm=30.0. The example above passes 50 explicitly for a more accurate run — pass whatever distance your table allows.

Full competition setup pattern (adapted from the Ecer2026 ConeBot): disable servos so the operator can reposition them, gate on a button press, release any actuated motors, home all servos, calibrate for both table surfaces, then activate the ground-level calibration set:

class M000SetupMission(SetupMission):
    setup_time = 120

    def sequence(self) -> Sequential:
        return seq([
            # 1. Go limp — operator repositions mechanisms physically
            fully_disable_servos(),
            wait_for_button("Move Servos"),

            # 2. Release motor so container can be set manually
            motor_off(Defs.cone_container_motor),

            # 3. Home all servos to known positions
            Defs.claw_servo.closed(),
            Defs.cone_arm_servo.container_pos(),

            # 4. Calibrate IR sensors for both floor and ramp surfaces in one pass
            calibrate(distance_cm=50, calibration_sets=["default", "upper"]),
            switch_calibration_set("default"),  # start on ground level

            # 5. Park arm for competition start
            Defs.cone_arm_servo.handl_hight(),
        ])

The fully_disable_servos()wait_for_button() → servo homing sequence is the standard idiom for robots where the arm needs to be repositioned by the operator before the match.

setup_time: Countdown Timer

Set setup_time (seconds, integer) as a class attribute to display a countdown in the UI during the entire setup phase. The clock ticks independently on the robot — no LCM messages required.

class M000SetupMission(SetupMission):
    setup_time = 120   # 120 seconds shown on every UI screen during setup

Set setup_time = 0 (the default) to disable the timer.

Controlling the Timer with Steps

By default the countdown starts the moment the setup mission begins. Use the timer control steps to defer the start until you are physically ready:

StepWhat it does
pause_setup_timer()Freezes the clock at its current remaining value
start_setup_timer()Resets to setup_time and starts counting (full duration from now)
resume_setup_timer()Unpauses the clock without resetting — continues from where it stopped
from raccoon import *

class M000SetupMission(SetupMission):
    setup_time = 120

    def sequence(self) -> Sequential:
        return seq([
            # Freeze the clock — time is not running yet
            pause_setup_timer(),

            # Prepare hardware without burning setup time
            Defs.claw.closed(),
            Defs.arm.up(),

            # Operator presses button: clock starts NOW, full 120 s remaining
            wait_for_button("Ready? Press to start timer"),
            start_setup_timer(),

            # Run calibration while the countdown is live
            calibrate(distance_cm=50),
        ])

All three steps are no-ops outside a SetupMission — you can leave them in shared step functions without worrying about calling context.

pre_start_gate(): Customizing the Wait-for-Light

After the setup sequence completes, the robot normally waits for the start light signal before running main missions. Override pre_start_gate() to customize or bypass this:

class M000SetupMission(SetupMission):
    setup_time = 120

    def sequence(self) -> Sequential:
        return seq([calibrate(distance_cm=50)])

    async def pre_start_gate(self, robot) -> None:
        # Wait for a button press instead of the light sensor
        from raccoon.step import wait_for_button
        await wait_for_button("Press to start").run_step(robot)

class QuickTestSetup(SetupMission):
    def sequence(self) -> Sequential:
        return seq([motor_off(Defs.arm_motor)])

    async def pre_start_gate(self, robot) -> None:
        pass  # Start immediately — no waiting

The default pre_start_gate() delegates to the robot’s built-in wait-for-light / wait-for-button logic defined in robot.physical.

Extending the gate without replacing it: The most useful override pattern is adding behavior before the standard wait-for-light, then explicitly calling back into the base class to preserve the gate. This is how the Ecer2026 DrumBot injects a custom calibration step before the light:

class M000SetupMission(SetupMission):
    setup_time = 120

    def sequence(self) -> Sequential:
        return seq([...])

    async def pre_start_gate(self, robot) -> None:
        # Run our project-specific calibration gate first
        await calibration_gate().run_step(robot)
        # Then hand off to the standard wait-for-light — DON'T skip this
        await robot._pre_start_gate()

Call robot._pre_start_gate() (single underscore — it is a protected method on GenericRobot) to invoke the built-in gate. If you omit this call, the robot will not wait for the start light and the match will begin immediately after your setup sequence.

Mission Time Budget

Each mission can declare a time_budget (seconds) as a class-level attribute. If the mission runs longer than its budget, the WatchdogManager cancels it and routes through the shutdown path — the shutdown mission still runs, subsequent missions do not.

class M010ScoringMission(Mission):
    time_budget = 30.0   # Cancel this mission if it exceeds 30 s

    def sequence(self) -> Sequential:
        return seq([
            drive_forward(50),
            Defs.claw.open(),
            drive_backward(25),
        ])

time_budget = None (the default) means no deadline. The budget watchdog is armed automatically by the robot runner — you do not need to call start_watchdog() manually for this use case.

Mission Registration

Missions are registered in the Robot class. This is also automatically generated for you. Don’t edit this! To change it, edit the raccoon.project.yml file:

class Robot(GenericRobot):
    # ...
    setup_mission = M000SetupMission()        # Runs before start signal
    missions = [                               # Run in order after start
        M010DriveToConeMission(),
        M020CollectConeMission(),
        M030CollectBotguyMission(),
    ]
    shutdown_mission = M99ShutdownMission()    # Always runs at the end

Missions execute in list order. If mission 1 finishes, mission 2 starts immediately.

The shutdown mission always runs — both when all main missions complete normally and when the shutdown_in timer fires mid-mission. Use it for guaranteed cleanup (park motors, retract arms, turn off LEDs) regardless of how the match ended. Do not rely on it as an error-only path; it executes unconditionally.

If the shutdown_in timer fires during a mission, that mission is cancelled first, and then the shutdown mission runs.