crazyflie_lib/subsystems/high_level_commander.rs
1//! # High-level commander subsystem
2//!
3//! This subsystem is responsible for managing high-level commands and setpoints for the Crazyflie.
4//! It builds on top of the (low-level) [`crate::subsystems::commander::Commander`] subsystem and provides a more user-friendly interface
5//! for controlling the drone's behavior.
6
7use crazyflie_link::Packet;
8use flume::Sender;
9use tokio::time::{sleep, Duration};
10
11use crate::{Error, Result};
12
13use crate::crazyflie::HL_COMMANDER_PORT;
14
15
16// Command type identifiers
17const COMMAND_SET_GROUP_MASK: u8 = 0;
18const COMMAND_STOP: u8 = 3;
19const COMMAND_DEFINE_TRAJECTORY: u8 = 6;
20const COMMAND_TAKEOFF_2: u8 = 7;
21const COMMAND_LAND_2: u8 = 8;
22const COMMAND_SPIRAL: u8 = 11;
23const COMMAND_GO_TO_2: u8 = 12;
24const COMMAND_START_TRAJECTORY_2: u8 = 13;
25
26/// This mask is used to specify that all Crazyflies should respond to the command.
27pub const ALL_GROUPS: u8 = 0;
28
29const TRAJECTORY_LOCATION_MEM: u8 = 1;
30
31/// 4D polynomial trajectory
32pub const TRAJECTORY_TYPE_POLY4D: u8 = 0;
33/// Compressed 4D polynomial trajectory
34pub const TRAJECTORY_TYPE_POLY4D_COMPRESSED: u8 = 1;
35
36
37/// High-level commander interface for a Crazyflie.
38///
39/// The high-level commander is a firmware module that generates smooth
40/// position setpoints from high-level actions such as *take-off*, *go-to*,
41/// *spiral*, and *land*. Internally it plans trajectories (polynomial-based)
42/// that are executed by the Crazyflie.
43///
44/// This Rust type provides an asynchronous, remote client for that module:
45/// it builds and sends the required packets over the high-level commander port,
46/// exposing a small set of ergonomic methods. When using trajectory functions,
47/// ensure the trajectory data has been uploaded to the Crazyflie's memory first.
48///
49/// # Command execution model
50/// Movement commands ([`take_off`](Self::take_off), [`land`](Self::land),
51/// [`go_to`](Self::go_to), [`spiral`](Self::spiral)) immediately trigger execution
52/// on the Crazyflie and then block for their `duration` to simplify command sequencing.
53/// The Crazyflie executes these commands autonomously—if the Rust future is cancelled
54/// or the connection drops, the drone will continue executing the command until completion.
55///
56/// # Notes
57/// The high-level commander can be preempted at any time by setpoints from the commander.
58/// To return control to the high-level commander, see [`crate::subsystems::commander::Commander::notify_setpoint_stop`].
59///
60/// A `HighLevelCommander` is typically obtained from a [`crate::Crazyflie`] instance.
61///
62/// # Safe usage pattern
63/// ```no_run
64/// # use crazyflie_link::LinkContext;
65/// # use crazyflie_lib::Crazyflie;
66/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
67/// # let context = LinkContext::new();
68/// # let cf = Crazyflie::connect_from_uri(
69/// # &context,
70/// # "radio://0/80/2M/E7E7E7E7E7",
71/// # crazyflie_lib::NoTocCache
72/// # ).await?;
73/// // Continue flight sequence even if commands fail
74/// if let Err(e) = cf.high_level_commander.take_off(0.5, None, 2.0, None).await {
75/// eprintln!("Take-off failed: {e}");
76/// }
77///
78/// if let Err(e) = cf.high_level_commander.land(0.0, None, 2.0, None).await {
79/// eprintln!("Landing failed: {e}");
80/// }
81/// # Ok(())
82/// # }
83/// ```
84#[derive(Debug)]
85pub struct HighLevelCommander {
86 uplink: Sender<Packet>,
87}
88
89/// Constructor methods.
90impl HighLevelCommander {
91 /// Create a new HighLevelCommander
92 pub fn new(uplink: Sender<Packet>) -> Self {
93 Self { uplink }
94 }
95}
96
97/// Group mask related commands.
98impl HighLevelCommander {
99 /// Set the group mask for the high-level commander.
100 ///
101 /// # Arguments
102 /// * `group_mask` - The group mask to set. Use `ALL_GROUPS` to set the mask for all Crazyflies.
103 pub async fn set_group_mask(&self, group_mask: u8) -> Result<()> {
104 let mut payload = Vec::with_capacity(2);
105 payload.push(COMMAND_SET_GROUP_MASK);
106 payload.push(group_mask);
107
108 let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
109
110 self.uplink
111 .send_async(pk)
112 .await
113 .map_err(|_| Error::Disconnected)?;
114 Ok(())
115 }
116}
117
118/// High-level movement commands.
119///
120/// # Warning
121/// Avoid overlapping movement commands. When a command is sent to a Crazyflie
122/// while another is currently executing, the generated polynomial can take
123/// unexpected routes and have high peaks.
124impl HighLevelCommander {
125 /// Take off vertically from the current x-y position to the given target height.
126 ///
127 /// # Arguments
128 /// * `height` - Target height (meters) above the world origin.
129 /// * `yaw` - Target yaw (radians). Use `None` to maintain the current yaw.
130 /// * `duration` - Time (seconds) to reach the target height. This method blocks for this duration.
131 /// * `group_mask` - Bitmask selecting which Crazyflies to command. Use `None` for all Crazyflies.
132 pub async fn take_off(&self, height: f32, yaw: Option<f32>, duration: f32, group_mask: Option<u8>) -> Result<()> {
133 let use_current_yaw = yaw.is_none();
134 let target_yaw = yaw.unwrap_or(0.0);
135
136 let group_mask_value = group_mask.unwrap_or(ALL_GROUPS);
137
138 let mut payload = Vec::with_capacity(3 + 3 * 4);
139 payload.push(COMMAND_TAKEOFF_2);
140 payload.push(group_mask_value);
141 payload.extend_from_slice(&height.to_le_bytes());
142 payload.extend_from_slice(&target_yaw.to_le_bytes());
143 payload.push(use_current_yaw as u8);
144 payload.extend_from_slice(&duration.to_le_bytes());
145
146 let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
147
148 self.uplink
149 .send_async(pk)
150 .await
151 .map_err(|_| Error::Disconnected)?;
152
153 sleep(Duration::from_secs_f32(duration)).await;
154 Ok(())
155 }
156
157 /// Land vertically from the current x-y position to the given target height.
158 ///
159 /// # Arguments
160 /// * `height` - Target height (meters) above the world origin.
161 /// * `yaw` - Target yaw (radians). Use `None` to maintain the current yaw.
162 /// * `duration` - Time (seconds) to reach the target height. This method blocks for this duration.
163 /// * `group_mask` - Bitmask selecting which Crazyflies to command. Use `None` for all Crazyflies.
164 pub async fn land(&self, height: f32, yaw: Option<f32>, duration: f32, group_mask: Option<u8>) -> Result<()> {
165 let use_current_yaw = yaw.is_none();
166 let target_yaw = yaw.unwrap_or(0.0);
167
168 let group_mask_value = group_mask.unwrap_or(ALL_GROUPS);
169
170 let mut payload = Vec::with_capacity(3 + 3 * 4);
171 payload.push(COMMAND_LAND_2);
172 payload.push(group_mask_value);
173 payload.extend_from_slice(&height.to_le_bytes());
174 payload.extend_from_slice(&target_yaw.to_le_bytes());
175 payload.push(use_current_yaw as u8);
176 payload.extend_from_slice(&duration.to_le_bytes());
177
178 let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
179
180 self.uplink
181 .send_async(pk)
182 .await
183 .map_err(|_| Error::Disconnected)?;
184
185 sleep(Duration::from_secs_f32(duration)).await;
186 Ok(())
187 }
188
189 /// Stop the current high-level command and disable motors.
190 ///
191 /// This immediately halts any active high-level command (takeoff, land, go_to, spiral,
192 /// or trajectory execution) and stops motor output.
193 ///
194 /// # Arguments
195 /// * `group_mask` - Bitmask selecting which Crazyflies to command. Use `None` for all Crazyflies.
196 pub async fn stop(&self, group_mask: Option<u8>) -> Result<()> {
197 let group_mask_value = group_mask.unwrap_or(ALL_GROUPS);
198
199 let mut payload = Vec::with_capacity(2);
200 payload.push(COMMAND_STOP);
201 payload.push(group_mask_value);
202
203 let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
204
205 self.uplink
206 .send_async(pk)
207 .await
208 .map_err(|_| Error::Disconnected)?;
209 Ok(())
210 }
211
212 /// Move to an absolute or relative position with smooth path planning.
213 ///
214 /// The path is designed to transition smoothly from the current state to the target
215 /// position, gradually decelerating at the goal with minimal overshoot. When the
216 /// system is at hover, the path will be a straight line, but if there is any initial
217 /// velocity, the path will be a smooth curve.
218 ///
219 /// The trajectory is derived by solving for a unique 7th-degree polynomial that
220 /// satisfies the initial conditions of position, velocity, and acceleration, and
221 /// ends at the goal with zero velocity and acceleration. Additionally, the jerk
222 /// (derivative of acceleration) is constrained to be zero at both the starting
223 /// and ending points.
224 ///
225 /// # Arguments
226 /// * `x` - Target x-position in meters
227 /// * `y` - Target y-position in meters
228 /// * `z` - Target z-position in meters
229 /// * `yaw` - Target yaw angle in radians
230 /// * `duration` - Time in seconds to reach the target position. This method blocks for this duration.
231 /// * `relative` - If `true`, positions and yaw are relative to current position; if `false`, absolute
232 /// * `linear` - If `true`, use linear interpolation; if `false`, use polynomial trajectory
233 /// * `group_mask` - Bitmask selecting which Crazyflies to command. Use `None` for all Crazyflies.
234 pub async fn go_to(&self, x: f32, y: f32, z: f32, yaw: f32, duration: f32, relative: bool, linear: bool, group_mask: Option<u8>) -> Result<()> {
235 let group_mask_value = group_mask.unwrap_or(ALL_GROUPS);
236
237 let mut payload = Vec::with_capacity(4 + 5 * 4);
238 payload.push(COMMAND_GO_TO_2);
239 payload.push(group_mask_value);
240 payload.push(relative as u8);
241 payload.push(linear as u8);
242 payload.extend_from_slice(&x.to_le_bytes());
243 payload.extend_from_slice(&y.to_le_bytes());
244 payload.extend_from_slice(&z.to_le_bytes());
245 payload.extend_from_slice(&yaw.to_le_bytes());
246 payload.extend_from_slice(&duration.to_le_bytes());
247
248 let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
249
250 self.uplink
251 .send_async(pk)
252 .await
253 .map_err(|_| Error::Disconnected)?;
254
255 sleep(Duration::from_secs_f32(duration)).await;
256 Ok(())
257 }
258
259 /// Fly a spiral segment.
260 ///
261 /// The Crazyflie moves along an arc around a computed center point, sweeping
262 /// through an angle of up to ±2π (one full turn). While sweeping, the radius
263 /// changes linearly from `initial_radius` to `final_radius`. If the radii are
264 /// equal, the path is a circular arc; if they differ, the path spirals inward
265 /// or outward accordingly. Altitude changes linearly by `altitude_gain` over
266 /// the duration.
267 ///
268 /// # Center placement
269 /// The spiral center is placed differently depending on `sideways` and `clockwise`:
270 /// * `sideways = false`
271 /// * `clockwise = true` → center lies to the **right** of the current heading.
272 /// * `clockwise = false` → center lies to the **left** of the current heading.
273 /// * `sideways = true`
274 /// * `clockwise = true` → center lies **ahead** of the current heading.
275 /// * `clockwise = false` → center lies **behind** the current heading.
276 ///
277 /// # Orientation
278 /// * `sideways = false`: the Crazyflie’s heading follows the tangent of the
279 /// spiral (flies forward along the path).
280 /// * `sideways = true`: the Crazyflie’s heading points toward the spiral center
281 /// while circling around it (flies sideways along the path).
282 ///
283 /// # Direction conventions
284 /// * `clockwise` chooses on which side the center is placed.
285 /// * The **sign of `angle`** sets the travel direction along the arc:
286 /// `angle > 0` sweeps one way; `angle < 0` traverses the arc in the opposite
287 /// direction (i.e., “backwards”). This can make some combinations appear
288 /// counterintuitive—for example, `sideways = false`, `clockwise = true`,
289 /// `angle < 0` will *look* counter-clockwise from above.
290 ///
291 /// # Arguments
292 /// * `angle` - Total spiral angle in radians (limited to ±2π).
293 /// * `initial_radius` - Starting radius in meters (≥ 0).
294 /// * `final_radius` - Ending radius in meters (≥ 0).
295 /// * `altitude_gain` - Vertical displacement in meters (positive = climb,
296 /// negative = descent).
297 /// * `duration` - Time in seconds to complete the spiral. This method blocks for this duration.
298 /// * `sideways` - If `true`, heading points toward the spiral center;
299 /// if `false`, heading follows the spiral tangent.
300 /// * `clockwise` - If `true`, fly clockwise; otherwise counter-clockwise.
301 /// * `group_mask` - Bitmask selecting which Crazyflies this applies to.
302 ///
303 /// # Errors
304 /// Returns [`Error::InvalidArgument`] if any parameters are out of range,
305 /// or [`Error::Disconnected`] if the command cannot be sent.
306 pub async fn spiral(&self, angle: f32, initial_radius: f32, final_radius: f32, altitude_gain: f32, duration: f32, sideways: bool, clockwise: bool, group_mask: Option<u8>) -> Result<()> {
307 // Check if all arguments are within range
308 if angle.abs() > 2.0 * std::f32::consts::PI {
309 return Err(Error::InvalidArgument("angle out of range".to_string()));
310 }
311 if initial_radius < 0.0 {
312 return Err(Error::InvalidArgument("initial_radius must be >= 0".to_string()));
313 }
314 if final_radius < 0.0 {
315 return Err(Error::InvalidArgument("final_radius must be >= 0".to_string()));
316 }
317
318 let group_mask_value = group_mask.unwrap_or(ALL_GROUPS);
319
320 let mut payload = Vec::with_capacity(4 + 5 * 4);
321 payload.push(COMMAND_SPIRAL);
322 payload.push(group_mask_value);
323 payload.push(sideways as u8);
324 payload.push(clockwise as u8);
325 payload.extend_from_slice(&angle.to_le_bytes());
326 payload.extend_from_slice(&initial_radius.to_le_bytes());
327 payload.extend_from_slice(&final_radius.to_le_bytes());
328 payload.extend_from_slice(&altitude_gain.to_le_bytes());
329 payload.extend_from_slice(&duration.to_le_bytes());
330
331 let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
332
333 self.uplink
334 .send_async(pk)
335 .await
336 .map_err(|_| Error::Disconnected)?;
337
338 sleep(Duration::from_secs_f32(duration)).await;
339 Ok(())
340 }
341}
342
343
344/// Trajectory implementations
345impl HighLevelCommander {
346 /// Define a trajectory previously uploaded to memory.
347 ///
348 /// # Arguments
349 /// * `trajectory_id` - Identifier used to reference this trajectory later.
350 /// * `memory_offset` - Byte offset into trajectory memory where the data begins.
351 /// * `piece_count` - Number of segments (pieces) in the trajectory.
352 /// * `trajectory_type` - Type of the trajectory data (e.g. Poly4D).
353 ///
354 /// # Errors
355 /// Returns [`Error::Disconnected`] if the command cannot be sent.
356 pub async fn define_trajectory(&self, trajectory_id: u8, memory_offset: u32, num_pieces: u8, trajectory_type: Option<u8>) -> Result<()> {
357 let trajectory_type_value = trajectory_type.unwrap_or(TRAJECTORY_TYPE_POLY4D);
358
359 let mut payload = Vec::with_capacity(5 + 1 * 4);
360 payload.push(COMMAND_DEFINE_TRAJECTORY);
361 payload.push(trajectory_id);
362 payload.push(TRAJECTORY_LOCATION_MEM);
363 payload.push(trajectory_type_value);
364 payload.extend_from_slice(&memory_offset.to_le_bytes());
365 payload.push(num_pieces);
366
367 let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
368
369 self.uplink
370 .send_async(pk)
371 .await
372 .map_err(|_| Error::Disconnected)?;
373 Ok(())
374 }
375
376 /// Start executing a previously defined trajectory.
377 ///
378 /// The trajectory is identified by `trajectory_id` and can be modified
379 /// at execution time by scaling its speed, shifting its position, aligning
380 /// its yaw, or running it in reverse.
381 ///
382 /// # Arguments
383 /// * `trajectory_id` - Identifier of the trajectory (as defined with [`HighLevelCommander::define_trajectory`]).
384 /// * `time_scale` - Time scaling factor; `1.0` = original speed,
385 /// values >1.0 slow down, values <1.0 speed up.
386 /// * `relative_position` - If `true`, shift trajectory to the current setpoint position.
387 /// * `relative_yaw` - If `true`, align trajectory yaw to the current yaw.
388 /// * `reversed` - If `true`, execute the trajectory in reverse.
389 /// * `group_mask` - Mask selecting which Crazyflies this applies to.
390 /// If `None`, defaults to all Crazyflies.
391 ///
392 /// # Errors
393 /// Returns [`Error::Disconnected`] if the command cannot be sent.
394 pub async fn start_trajectory(&self, trajectory_id: u8, time_scale: f32, relative_position: bool, relative_yaw: bool, reversed: bool, group_mask: Option<u8>) -> Result<()> {
395 let group_mask_value = group_mask.unwrap_or(ALL_GROUPS);
396
397 let mut payload = Vec::with_capacity(5 + 1 * 4);
398 payload.push(COMMAND_START_TRAJECTORY_2);
399 payload.push(group_mask_value);
400 payload.push(relative_position as u8);
401 payload.push(relative_yaw as u8);
402 payload.push(reversed as u8);
403 payload.push(trajectory_id);
404 payload.extend_from_slice(&time_scale.to_le_bytes());
405
406 let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
407
408 self.uplink
409 .send_async(pk)
410 .await
411 .map_err(|_| Error::Disconnected)?;
412 Ok(())
413 }
414}