How to Set Up the Planner Schedule for Quadrotor Control in Peng

Peng uses a configuration-driven planner schedule defined in config/quad.yaml that maps simulation steps to specific trajectory planners, activating them automatically during runtime based on the planner_schedule vector deserialized into the Config struct.

Peng is a Rust-based quadrotor simulator that relies on a timed sequence of trajectory planners to control flight paths. The planner schedule for quadrotor control is declared declaratively in YAML and executed by the simulation loop, allowing you to chain maneuvers like takeoff, figure-eights, and landing without recompiling code.

Understanding the PlannerSchedule Configuration

The schedule is defined by two core components: the Rust data structures that deserialize the configuration and the YAML file that supplies the timing and parameters.

The Config Struct and PlannerStep Definition

In src/config.rs, the Config struct contains the planner_schedule field, which is a vector of PlannerStep structs (source). Each PlannerStep specifies exactly when and which planner should activate:

  • step – The simulation time in milliseconds when the planner should start.
  • planner_type – A string identifier matching variants in the PlannerType enum (e.g., MinimumJerkLine, Lissajous, Circle).
  • params – A serde_yaml::Value containing planner-specific parameters passed directly to the constructor.
// src/config.rs (simplified)
pub struct Config {
    pub planner_schedule: Vec<PlannerStep>,
    // ... other fields
}

pub struct PlannerStep {
    pub step: usize,              // milliseconds
    pub planner_type: String,
    pub params: serde_yaml::Value,
}

YAML Structure and Timing

The actual schedule is written in config/quad.yaml under the planner_schedule key (source). Entries must be listed in chronological order by step. The simulation runs at a configurable frequency (typically 1000 Hz), so a step value of 1000 corresponds to 1 second of simulation time.

planner_schedule:
  - step: 1000
    planner_type: MinimumJerkLine
    params:
      end_position: [0.0, 0.0, 1.0]
      end_yaw: 0.0
      duration: 2.5
  - step: 5000
    planner_type: Lissajous
    params:
      center: [0.5, 0.5, 1.0]
      amplitude: [0.5, 0.5, 0.2]
      frequency: [1.0, 2.0, 3.0]
      phase: [0.0, 1.5707963267948966, 0.0]
      duration: 20.0
      end_yaw: 6.283185307179586
      ramp_time: 5.0

Each params block is planner-specific. Because Peng uses serde_yaml::Value, you can supply nested vectors, maps, or scalars without modifying the configuration parser.

Runtime Activation Logic

During startup and simulation, the raw configuration is transformed into executable planner instances through a two-stage process.

Converting Configuration to Runtime Objects

In src/main.rs, the application deserializes quad.yaml into the Config struct, then maps the PlannerStep entries into PlannerStepConfig objects used by the simulation loop (source):

let planner_config: Vec<PlannerStepConfig> = config
    .planner_schedule
    .iter()
    .map(|step| PlannerStepConfig {
        step: step.step,
        planner_type: step.planner_type.clone(),
        params: step.params.clone(),
    })
    .collect();

This conversion decouples the file format from the runtime representation, allowing the simulation to work with validated configuration objects.

Step Matching and Planner Switching

The update_planner function in src/lib.rs checks every simulation step against the schedule to determine if a planner switch is required (source):

pub fn update_planner(
    planner_manager: &mut PlannerManager,
    step: usize,
    time: f32,
    simulation_frequency: usize,
    quad: &Quadrotor,
    obstacles: &[Obstacle],
    planner_config: &[PlannerStepConfig],
) -> Result<(), SimulationError> {
    if let Some(planner_step) = planner_config
        .iter()
        .find(|s| s.step * simulation_frequency == step * 1000)
    {
        log::info!("Time: {:.2} s,\tSwitch {}", time, planner_step.planner_type);
        planner_manager.set_planner(create_planner(planner_step, quad, time, obstacles)?);
    }
    Ok(())
}

Key implementation detail: The comparison s.step * simulation_frequency == step * 1000 converts the millisecond step from the YAML into simulation steps. For a 1000 Hz simulation, a YAML step of 1000 (1 second) matches when the simulation counter reaches step 1000. Planners are switched exactly once per schedule entry and run until the next entry replaces them.

Configuring Custom Trajectories

To add a new trajectory segment to your quadrotor mission, append a new block to the planner_schedule list in config/quad.yaml.

Example: Adding a Circular Trajectory

The following entry activates a circular path at 10 seconds (step 10000) with a 2.5 meter radius:

planner_schedule:
  - step: 10000
    planner_type: Circle
    params:
      center: [2.0, 2.0, 3.0]
      radius: 2.5
      angular_velocity: 0.5
      duration: 30.0
      end_yaw: 0.0

Requirements for valid entries:

  1. Use a unique step value that aligns with your simulation.simulation_frequency (default 1000 Hz).
  2. Ensure planner_type matches a variant defined in the PlannerType enum in src/lib.rs.
  3. Provide all required keys under params for the specific planner implementation.

Example: Emergency Landing Sequence

To insert a landing maneuver at 2 minutes (120 seconds), add:

  - step: 120000
    planner_type: Landing
    params:
      descend_rate: 0.5
      target_altitude: 0.0
      duration: 10.0

Programmatic Schedule Manipulation

You can inspect or modify the planner schedule at runtime before starting the simulation.

Loading and Debugging the Schedule

use peng_quad::Config;

fn inspect_schedule() -> Result<(), Box<dyn std::error::Error>> {
    let cfg = Config::from_yaml("config/quad.yaml")?;
    
    for step in &cfg.planner_schedule {
        println!(
            "At {} ms: activate {} with {:?}",
            step.step,
            step.planner_type,
            step.params
        );
    }
    Ok(())
}

Appending Planners Dynamically

For automated testing or parameter sweeps, push entries directly to the vector:

let mut cfg = Config::from_yaml("config/quad.yaml")?;
cfg.planner_schedule.push(peng_quad::PlannerStep {
    step: 180000,  // 3 minutes
    planner_type: "ReturnToHome".into(),
    params: serde_yaml::from_str(r#"
        home_position: [0.0, 0.0, 1.0]
        speed: 2.0
    "#)?,
});

Summary

  • The planner schedule is defined in config/quad.yaml as a chronological list of PlannerStep entries.
  • Each entry specifies a step (milliseconds), planner_type, and params (arbitrary YAML).
  • src/config.rs deserializes the YAML into Vec<PlannerStep>; src/main.rs converts these into PlannerStepConfig objects.
  • update_planner in src/lib.rs activates planners when the simulation step matches step * simulation_frequency == current_step * 1000.
  • Planners run until explicitly replaced by a subsequent schedule entry.

Frequently Asked Questions

How do I convert real time to the step value in the YAML file?

Multiply the desired time in seconds by 1000 to get the millisecond value. For example, to start a planner at 5.5 seconds, set step: 5500. The simulation loop automatically scales this by the simulation_frequency (default 1000 Hz) to align with internal step counters.

Can I run multiple planners simultaneously using the schedule?

No. The planner_schedule is designed for sequential activation. The update_planner function replaces the active planner when a step matches, so only one planner controls the quadrotor at any given time. To combine behaviors, implement a composite planner as a single planner_type with parameters describing the sub-behaviors.

What happens if two entries have the same step value?

The find iterator in update_planner returns the first matching entry it encounters. If duplicate steps exist, only the first one in the vector activates, while subsequent entries with identical steps are ignored for that simulation frame. Ensure your YAML entries use unique, increasing step values.

Where are the available planner types defined?

The supported strings for planner_type correspond to the PlannerType enum and the factory logic in src/lib.rs. Common values include MinimumJerkLine for straight-line trajectories, Lissajous for figure-eight patterns, Circle for circular orbits, and Landing for controlled descent. Check the create_planner function implementation for the complete list of supported identifiers.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →