Skip to main content

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;
9
10use crate::{Error, Result};
11
12use crate::crazyflie::HL_COMMANDER_PORT;
13
14
15// Command type identifiers
16const COMMAND_SET_GROUP_MASK: u8 = 0;
17const COMMAND_STOP: u8 = 3;
18const COMMAND_DEFINE_TRAJECTORY: u8 = 6;
19const COMMAND_TAKEOFF_2: u8 = 7;
20const COMMAND_LAND_2: u8 = 8;
21const COMMAND_SPIRAL: u8 = 11;
22const COMMAND_GO_TO_2: u8 = 12;
23const COMMAND_START_TRAJECTORY_2: u8 = 13;
24
25/// This mask is used to specify that all Crazyflies should respond to the command.
26pub const ALL_GROUPS: u8 = 0;
27
28const TRAJECTORY_LOCATION_MEM: u8 = 1;
29
30/// 4D polynomial trajectory
31pub const TRAJECTORY_TYPE_POLY4D: u8 = 0;
32/// Compressed 4D polynomial trajectory
33pub const TRAJECTORY_TYPE_POLY4D_COMPRESSED: u8 = 1;
34
35
36/// High-level commander interface for a Crazyflie.
37///
38/// The high-level commander is a firmware module that generates smooth
39/// position setpoints from high-level actions such as *take-off*, *go-to*,
40/// *spiral*, and *land*. Internally it plans trajectories (polynomial-based)
41/// that are executed by the Crazyflie.
42///
43/// This Rust type provides an asynchronous, remote client for that module:
44/// it builds and sends the required packets over the high-level commander port,
45/// exposing a small set of ergonomic methods. When using trajectory functions,
46/// ensure the trajectory data has been uploaded to the Crazyflie's memory first.
47///
48/// # Command execution model
49/// Movement commands ([`take_off`](Self::take_off), [`land`](Self::land),
50/// [`go_to`](Self::go_to), [`spiral`](Self::spiral)) return immediately after sending
51/// the command to the Crazyflie. The caller is responsible for waiting the appropriate
52/// duration before issuing the next command. The Crazyflie executes these commands
53/// autonomously—if the connection drops, the drone will continue executing the command
54/// 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.
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        Ok(())
154    }
155
156    /// Land vertically from the current x-y position to the given target height.
157    ///
158    /// # Arguments
159    /// * `height` - Target height (meters) above the world origin.
160    /// * `yaw` - Target yaw (radians). Use `None` to maintain the current yaw.
161    /// * `duration` - Time (seconds) to reach the target height.
162    /// * `group_mask` - Bitmask selecting which Crazyflies to command. Use `None` for all Crazyflies.
163    pub async fn land(&self, height: f32, yaw: Option<f32>, duration: f32, group_mask: Option<u8>) -> Result<()> {
164        let use_current_yaw = yaw.is_none();
165        let target_yaw = yaw.unwrap_or(0.0);
166
167        let group_mask_value = group_mask.unwrap_or(ALL_GROUPS);
168
169        let mut payload = Vec::with_capacity(3 + 3 * 4);
170        payload.push(COMMAND_LAND_2);
171        payload.push(group_mask_value);
172        payload.extend_from_slice(&height.to_le_bytes());
173        payload.extend_from_slice(&target_yaw.to_le_bytes());
174        payload.push(use_current_yaw as u8);
175        payload.extend_from_slice(&duration.to_le_bytes());
176
177        let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
178
179        self.uplink
180            .send_async(pk)
181            .await
182            .map_err(|_| Error::Disconnected)?;
183
184        Ok(())
185    }
186
187    /// Stop the current high-level command and disable motors.
188    ///
189    /// This immediately halts any active high-level command (takeoff, land, go_to, spiral, 
190    /// or trajectory execution) and stops motor output.
191    ///
192    /// # Arguments
193    /// * `group_mask` - Bitmask selecting which Crazyflies to command. Use `None` for all Crazyflies.
194    pub async fn stop(&self, group_mask: Option<u8>) -> Result<()> {
195        let group_mask_value = group_mask.unwrap_or(ALL_GROUPS);
196
197        let mut payload = Vec::with_capacity(2);
198        payload.push(COMMAND_STOP);
199        payload.push(group_mask_value);
200
201        let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
202
203        self.uplink
204            .send_async(pk)
205            .await
206            .map_err(|_| Error::Disconnected)?;
207        Ok(())
208    }
209
210    /// Move to an absolute or relative position with smooth path planning.
211    ///
212    /// The path is designed to transition smoothly from the current state to the target
213    /// position, gradually decelerating at the goal with minimal overshoot. When the 
214    /// system is at hover, the path will be a straight line, but if there is any initial
215    /// velocity, the path will be a smooth curve.
216    ///
217    /// The trajectory is derived by solving for a unique 7th-degree polynomial that
218    /// satisfies the initial conditions of position, velocity, and acceleration, and
219    /// ends at the goal with zero velocity and acceleration. Additionally, the jerk
220    /// (derivative of acceleration) is constrained to be zero at both the starting
221    /// and ending points.
222    ///
223    /// # Arguments
224    /// * `x` - Target x-position in meters
225    /// * `y` - Target y-position in meters
226    /// * `z` - Target z-position in meters
227    /// * `yaw` - Target yaw angle in radians
228    /// * `duration` - Time in seconds to reach the target position.
229    /// * `relative` - If `true`, positions and yaw are relative to current position; if `false`, absolute
230    /// * `linear` - If `true`, use linear interpolation; if `false`, use polynomial trajectory
231    /// * `group_mask` - Bitmask selecting which Crazyflies to command. Use `None` for all Crazyflies.
232    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<()> {
233        let group_mask_value = group_mask.unwrap_or(ALL_GROUPS);
234
235        let mut payload = Vec::with_capacity(4 + 5 * 4);
236        payload.push(COMMAND_GO_TO_2);
237        payload.push(group_mask_value);
238        payload.push(relative as u8);
239        payload.push(linear as u8);
240        payload.extend_from_slice(&x.to_le_bytes());
241        payload.extend_from_slice(&y.to_le_bytes());
242        payload.extend_from_slice(&z.to_le_bytes());
243        payload.extend_from_slice(&yaw.to_le_bytes());
244        payload.extend_from_slice(&duration.to_le_bytes());
245
246        let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
247
248        self.uplink
249            .send_async(pk)
250            .await
251            .map_err(|_| Error::Disconnected)?;
252
253        Ok(())
254    }
255
256    /// Fly a spiral segment.
257    ///
258    /// The Crazyflie moves along an arc around a computed center point, sweeping
259    /// through an angle of up to ±2π (one full turn). While sweeping, the radius
260    /// changes linearly from `initial_radius` to `final_radius`. If the radii are
261    /// equal, the path is a circular arc; if they differ, the path spirals inward
262    /// or outward accordingly. Altitude changes linearly by `altitude_gain` over
263    /// the duration.
264    ///
265    /// # Center placement
266    /// The spiral center is placed differently depending on `sideways` and `clockwise`:
267    /// * `sideways = false`
268    ///   * `clockwise = true`  → center lies to the **right** of the current heading.
269    ///   * `clockwise = false` → center lies to the **left** of the current heading.
270    /// * `sideways = true`
271    ///   * `clockwise = true`  → center lies **ahead** of the current heading.
272    ///   * `clockwise = false` → center lies **behind** the current heading.
273    ///
274    /// # Orientation
275    /// * `sideways = false`: the Crazyflie’s heading follows the tangent of the
276    ///   spiral (flies forward along the path).
277    /// * `sideways = true`: the Crazyflie’s heading points toward the spiral center
278    ///   while circling around it (flies sideways along the path).
279    ///
280    /// # Direction conventions
281    /// * `clockwise` chooses on which side the center is placed.
282    /// * The **sign of `angle`** sets the travel direction along the arc:
283    ///   `angle > 0` sweeps one way; `angle < 0` traverses the arc in the opposite
284    ///   direction (i.e., “backwards”). This can make some combinations appear
285    ///   counterintuitive—for example, `sideways = false`, `clockwise = true`,
286    ///   `angle < 0` will *look* counter-clockwise from above.
287    ///
288    /// # Arguments
289    /// * `angle` - Total spiral angle in radians (limited to ±2π).
290    /// * `initial_radius` - Starting radius in meters (≥ 0).
291    /// * `final_radius` - Ending radius in meters (≥ 0).
292    /// * `altitude_gain` - Vertical displacement in meters (positive = climb,
293    ///   negative = descent).
294    /// * `duration` - Time in seconds to complete the spiral.
295    /// * `sideways` - If `true`, heading points toward the spiral center;
296    ///   if `false`, heading follows the spiral tangent.
297    /// * `clockwise` - If `true`, fly clockwise; otherwise counter-clockwise.
298    /// * `group_mask` - Bitmask selecting which Crazyflies this applies to.
299    ///
300    /// # Errors
301    /// Returns [`Error::InvalidArgument`] if any parameters are out of range,
302    /// or [`Error::Disconnected`] if the command cannot be sent.
303    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<()> {
304        // Check if all arguments are within range
305        if angle.abs() > 2.0 * std::f32::consts::PI {
306            return Err(Error::InvalidArgument("angle out of range".to_string()));
307        }
308        if initial_radius < 0.0 {
309            return Err(Error::InvalidArgument("initial_radius must be >= 0".to_string()));
310        }
311        if final_radius < 0.0 {
312            return Err(Error::InvalidArgument("final_radius must be >= 0".to_string()));
313        }
314
315        let group_mask_value = group_mask.unwrap_or(ALL_GROUPS);
316
317        let mut payload = Vec::with_capacity(4 + 5 * 4);
318        payload.push(COMMAND_SPIRAL);
319        payload.push(group_mask_value);
320        payload.push(sideways as u8);
321        payload.push(clockwise as u8);
322        payload.extend_from_slice(&angle.to_le_bytes());
323        payload.extend_from_slice(&initial_radius.to_le_bytes());
324        payload.extend_from_slice(&final_radius.to_le_bytes());
325        payload.extend_from_slice(&altitude_gain.to_le_bytes());
326        payload.extend_from_slice(&duration.to_le_bytes());
327
328        let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
329
330        self.uplink
331            .send_async(pk)
332            .await
333            .map_err(|_| Error::Disconnected)?;
334
335        Ok(())
336    }
337}
338
339
340/// Trajectory implementations
341impl HighLevelCommander {
342    /// Define a trajectory previously uploaded to memory.
343    ///
344    /// # Arguments
345    /// * `trajectory_id` - Identifier used to reference this trajectory later.
346    /// * `memory_offset` - Byte offset into trajectory memory where the data begins.
347    /// * `piece_count` - Number of segments (pieces) in the trajectory.
348    /// * `trajectory_type` - Type of the trajectory data (e.g. Poly4D).
349    ///
350    /// # Errors
351    /// Returns [`Error::Disconnected`] if the command cannot be sent.
352    pub async fn define_trajectory(&self, trajectory_id: u8, memory_offset: u32, num_pieces: u8, trajectory_type: Option<u8>) -> Result<()> {
353        let trajectory_type_value = trajectory_type.unwrap_or(TRAJECTORY_TYPE_POLY4D);
354
355        let mut payload = Vec::with_capacity(5 + 1 * 4);
356        payload.push(COMMAND_DEFINE_TRAJECTORY);
357        payload.push(trajectory_id);
358        payload.push(TRAJECTORY_LOCATION_MEM);
359        payload.push(trajectory_type_value);
360        payload.extend_from_slice(&memory_offset.to_le_bytes());
361        payload.push(num_pieces);
362
363        let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
364
365        self.uplink
366            .send_async(pk)
367            .await
368            .map_err(|_| Error::Disconnected)?;
369        Ok(())
370    }
371
372    /// Start executing a previously defined trajectory.
373    ///
374    /// The trajectory is identified by `trajectory_id` and can be modified
375    /// at execution time by scaling its speed, shifting its position, aligning
376    /// its yaw, or running it in reverse.
377    ///
378    /// # Arguments
379    /// * `trajectory_id` - Identifier of the trajectory (as defined with [`HighLevelCommander::define_trajectory`]).
380    /// * `time_scale` - Time scaling factor; `1.0` = original speed,
381    ///   values >1.0 slow down, values <1.0 speed up.
382    /// * `relative_position` - If `true`, shift trajectory to the current setpoint position.
383    /// * `relative_yaw` - If `true`, align trajectory yaw to the current yaw.
384    /// * `reversed` - If `true`, execute the trajectory in reverse.
385    /// * `group_mask` - Mask selecting which Crazyflies this applies to.
386    ///   If `None`, defaults to all Crazyflies.
387    ///
388    /// # Errors
389    /// Returns [`Error::Disconnected`] if the command cannot be sent.
390    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<()> {
391        let group_mask_value = group_mask.unwrap_or(ALL_GROUPS);
392
393        let mut payload = Vec::with_capacity(5 + 1 * 4);
394        payload.push(COMMAND_START_TRAJECTORY_2);
395        payload.push(group_mask_value);
396        payload.push(relative_position as u8);
397        payload.push(relative_yaw as u8);
398        payload.push(reversed as u8);
399        payload.push(trajectory_id);
400        payload.extend_from_slice(&time_scale.to_le_bytes());
401
402        let pk = Packet::new(HL_COMMANDER_PORT, 0, payload);
403
404        self.uplink
405            .send_async(pk)
406            .await
407            .map_err(|_| Error::Disconnected)?;
408        Ok(())
409    }
410}