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
PlannerTypeenum (e.g.,MinimumJerkLine,Lissajous,Circle). - params – A
serde_yaml::Valuecontaining 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:
- Use a unique
stepvalue that aligns with yoursimulation.simulation_frequency(default 1000 Hz). - Ensure
planner_typematches a variant defined in thePlannerTypeenum insrc/lib.rs. - Provide all required keys under
paramsfor 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.yamlas a chronological list ofPlannerStepentries. - Each entry specifies a
step(milliseconds),planner_type, andparams(arbitrary YAML). src/config.rsdeserializes the YAML intoVec<PlannerStep>;src/main.rsconverts these intoPlannerStepConfigobjects.update_plannerinsrc/lib.rsactivates planners when the simulation step matchesstep * 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →