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

> Learn how to set up the planner schedule for quadrotor control in Peng by configuring the quad.yaml file. Automatically activate trajectory planners during runtime for efficient control.

- Repository: [Yang Zhou/peng](https://github.com/makeecat/peng)
- Tags: how-to-guide
- Published: 2026-03-06

---

**Peng uses a configuration-driven planner schedule defined in [`config/quad.yaml`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/src/config.rs), the `Config` struct contains the `planner_schedule` field, which is a vector of `PlannerStep` structs ([source](https://github.com/makeecat/peng/blob/main/src/config.rs#L24-L48)). 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.

```rust
// 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`](https://github.com/makeecat/peng/blob/main/config/quad.yaml) under the `planner_schedule` key ([source](https://github.com/makeecat/peng/blob/main/config/quad.yaml#L60-L98)). 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.

```yaml
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`](https://github.com/makeecat/peng/blob/main/src/main.rs), the application deserializes [`quad.yaml`](https://github.com/makeecat/peng/blob/main/quad.yaml) into the `Config` struct, then maps the `PlannerStep` entries into `PlannerStepConfig` objects used by the simulation loop ([source](https://github.com/makeecat/peng/blob/main/src/main.rs#L71-L78)):

```rust
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`](https://github.com/makeecat/peng/blob/main/src/lib.rs) checks every simulation step against the schedule to determine if a planner switch is required ([source](https://github.com/makeecat/peng/blob/main/src/lib.rs#L72-L88)):

```rust
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`](https://github.com/makeecat/peng/blob/main/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:

```yaml
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`](https://github.com/makeecat/peng/blob/main/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:

```yaml
  - 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

```rust
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:

```rust
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`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/src/config.rs) deserializes the YAML into `Vec<PlannerStep>`; [`src/main.rs`](https://github.com/makeecat/peng/blob/main/src/main.rs) converts these into `PlannerStepConfig` objects.
- `update_planner` in [`src/lib.rs`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/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.