Skip to main content

crazyflie_lib/subsystems/
localization.rs

1//! # Localization subsystem
2//!
3//! This subsystem provides access to the Crazyflie's localization services including
4//! external position/pose streaming, lighthouse positioning
5//! system data, and Loco Positioning System (UWB) communication.
6//!
7//! ## External Position and Pose
8//!
9//! Send position data from external tracking systems (motion capture, etc.) to the
10//! Crazyflie's onboard state estimator:
11//! ```no_run
12//! # async fn external_pos(crazyflie: &crazyflie_lib::Crazyflie) -> crazyflie_lib::Result<()> {
13//! // Send position update
14//! crazyflie.localization.external_pose
15//!     .send_external_position([1.0, 2.0, 0.5]).await?;
16//!
17//! // Or send full pose with orientation
18//! crazyflie.localization.external_pose
19//!     .send_external_pose([1.0, 2.0, 0.5], [0.0, 0.0, 0.0, 1.0]).await?;
20//! # Ok(())
21//! # }
22//! ```
23//!
24//! ## Lighthouse Positioning
25//!
26//! Access lighthouse sweep angle data for position estimation and base station calibration:
27//! ```no_run
28//! use futures::StreamExt;
29//!
30//! # async fn lighthouse(crazyflie: &crazyflie_lib::Crazyflie) -> crazyflie_lib::Result<()> {
31//! // Enable angle streaming
32//! crazyflie.param.set("locSrv.enLhAngleStream", 1u8).await?;
33//!
34//! let mut angle_stream = crazyflie.localization.lighthouse.angle_stream().await;
35//! while let Some(data) = angle_stream.next().await {
36//!     println!("Base station {}: x={:?}, y={:?}",
37//!         data.base_station, data.angles.x, data.angles.y);
38//! }
39//! # Ok(())
40//! # }
41//! ```
42//!
43//! ## Loco Positioning System
44//!
45//! Send Loco Positioning Protocol (LPP) packets to ultra-wide-band positioning nodes:
46//! ```no_run
47//! # async fn loco_pos(crazyflie: &crazyflie_lib::Crazyflie) -> crazyflie_lib::Result<()> {
48//! // Send LPP packet to node 5
49//! let lpp_data = vec![0x01, 0x02, 0x03];
50//! crazyflie.localization.loco_positioning
51//!     .send_short_lpp_packet(5, &lpp_data).await?;
52//! # Ok(())
53//! # }
54//! ```
55
56use crazyflie_link::Packet;
57use flume::{Receiver, Sender};
58use async_broadcast::{broadcast, Receiver as BroadcastReceiver};
59use futures::Stream;
60use half::f16;
61
62use crate::{Error, Result};
63
64use crate::crazyflie::{LOCALIZATION_PORT, SUPERVISOR_PORT};
65use crate::subsystems::supervisor::{
66    CMD_EMERGENCY_STOP, CMD_EMERGENCY_STOP_WATCHDOG, SUPERVISOR_CH_COMMAND,
67};
68
69// Channels
70const POSITION_CHANNEL: u8 = 0;
71const GENERIC_CHANNEL: u8 = 1;
72
73// Generic channel message types
74const _RANGE_STREAM_REPORT: u8 = 0;
75const _RANGE_STREAM_REPORT_FP16: u8 = 1;
76const LPS_SHORT_LPP_PACKET: u8 = 2;
77const _EMERGENCY_STOP: u8 = 3;
78const _EMERGENCY_STOP_WATCHDOG: u8 = 4;
79const _COMM_GNSS_NMEA: u8 = 6;
80const _COMM_GNSS_PROPRIETARY: u8 = 7;
81const EXT_POSE: u8 = 8;
82const _EXT_POSE_PACKED: u8 = 9;
83const LH_ANGLE_STREAM: u8 = 10;
84const LH_PERSIST_DATA: u8 = 11;
85
86/// Lighthouse angle sweep data
87#[derive(Debug, Clone)]
88pub struct LighthouseAngleData {
89    /// Base station ID
90    pub base_station: u8,
91    /// Angle measurements
92    pub angles: LighthouseAngles,
93}
94
95/// Lighthouse sweep angles for all 4 sensors
96#[derive(Debug, Clone)]
97pub struct LighthouseAngles {
98    /// Horizontal angles for 4 sensors (rad)
99    pub x: [f32; 4],
100    /// Vertical angles for 4 sensors (rad)
101    pub y: [f32; 4],
102}
103
104/// Localization subsystem
105///
106/// Provides access to localization services including emergency stop,
107/// external position/pose streaming, and lighthouse positioning system.
108pub struct Localization{
109    /// Emergency stop controls
110    pub emergency: EmergencyControl,
111    /// External position and pose streaming
112    pub external_pose: ExternalPose,
113    /// Lighthouse positioning system
114    pub lighthouse: Lighthouse,
115    /// Loco Positioning System (UWB)
116    pub loco_positioning: LocoPositioning,
117}
118
119impl Localization {
120    pub(crate) fn new(uplink: Sender<Packet>, downlink: Receiver<Packet>) -> Self {
121        let emergency = EmergencyControl { uplink: uplink.clone() };
122        let external_pose = ExternalPose { uplink: uplink.clone() };
123
124        let (mut angle_broadcast, angle_receiver) = broadcast(100);
125        let (mut persist_broadcast, persist_receiver) = broadcast(10);
126
127        // Enable overflow mode so old messages are dropped instead of blocking
128        angle_broadcast.set_overflow(true);
129        persist_broadcast.set_overflow(true);
130
131        // Spawn background task to process incoming localization packets
132        tokio::spawn(async move {
133            while let Ok(pk) = downlink.recv_async().await {
134                if pk.get_channel() != GENERIC_CHANNEL || pk.get_data().is_empty() {
135                    continue;
136                }
137
138                let packet_type = pk.get_data()[0];
139                let data = &pk.get_data()[1..];
140
141                match packet_type {
142                    LH_ANGLE_STREAM => {
143                        if let Ok(angle_data) = decode_lh_angle(data) {
144                            let _ = angle_broadcast.broadcast(angle_data).await;
145                        }
146                    }
147                    LH_PERSIST_DATA if !data.is_empty() => {
148                        let success = data[0] != 0;
149                        let _ = persist_broadcast.broadcast(success).await;
150                    }
151                    _ => {} // Ignore unknown packet types
152                }
153            }
154        });
155
156        let lighthouse = Lighthouse {
157            uplink: uplink.clone(),
158            angle_stream_receiver: angle_receiver,
159            persist_receiver,
160        };
161
162        let loco_positioning = LocoPositioning { uplink: uplink.clone() };
163
164        Self { emergency, external_pose, lighthouse, loco_positioning }
165    }
166}
167
168/// Decode lighthouse angle stream packet
169///
170/// Packet format (from Python): '<Bfhhhfhhh'
171/// - B: base station ID
172/// - f: x[0] angle (float32)
173/// - h: x[1] diff (int16 fp16)
174/// - h: x[2] diff (int16 fp16)
175/// - h: x[3] diff (int16 fp16)
176/// - f: y[0] angle (float32)
177/// - h: y[1] diff (int16 fp16)
178/// - h: y[2] diff (int16 fp16)
179/// - h: y[3] diff (int16 fp16)
180fn decode_lh_angle(data: &[u8]) -> Result<LighthouseAngleData> {
181    if data.len() < 21 {
182        return Err(Error::ProtocolError("LH_ANGLE_STREAM packet too short".to_owned()));
183    }
184
185    let base_station = data[0];
186
187    // Read x[0] as f32
188    let x0 = f32::from_le_bytes([data[1], data[2], data[3], data[4]]);
189
190    // Read x diffs as i16 and convert from fp16
191    let x1_diff_i16 = i16::from_le_bytes([data[5], data[6]]);
192    let x2_diff_i16 = i16::from_le_bytes([data[7], data[8]]);
193    let x3_diff_i16 = i16::from_le_bytes([data[9], data[10]]);
194
195    let x1 = x0 - f16::from_bits(x1_diff_i16 as u16).to_f32();
196    let x2 = x0 - f16::from_bits(x2_diff_i16 as u16).to_f32();
197    let x3 = x0 - f16::from_bits(x3_diff_i16 as u16).to_f32();
198
199    // Read y[0] as f32
200    let y0 = f32::from_le_bytes([data[11], data[12], data[13], data[14]]);
201
202    // Read y diffs as i16 and convert from fp16
203    let y1_diff_i16 = i16::from_le_bytes([data[15], data[16]]);
204    let y2_diff_i16 = i16::from_le_bytes([data[17], data[18]]);
205    let y3_diff_i16 = i16::from_le_bytes([data[19], data[20]]);
206
207    let y1 = y0 - f16::from_bits(y1_diff_i16 as u16).to_f32();
208    let y2 = y0 - f16::from_bits(y2_diff_i16 as u16).to_f32();
209    let y3 = y0 - f16::from_bits(y3_diff_i16 as u16).to_f32();
210
211    Ok(LighthouseAngleData {
212        base_station,
213        angles: LighthouseAngles {
214            x: [x0, x1, x2, x3],
215            y: [y0, y1, y2, y3],
216        },
217    })
218}
219
220/// Emergency control interface
221///
222/// Provides emergency stop functionality that immediately stops all motors.
223pub struct EmergencyControl {
224    uplink: Sender<Packet>,
225}
226
227impl EmergencyControl {
228    /// Send emergency stop command
229    ///
230    /// Immediately stops all motors and puts the Crazyflie into a locked state.
231    /// The drone will require a reboot before it can fly again.
232    #[deprecated(since = "0.8.1", note = "Use [`Supervisor::send_emergency_stop`](crate::subsystems::supervisor::Supervisor::send_emergency_stop) instead")]
233    pub async fn send_emergency_stop(&self) -> Result<()> {
234        // Route to supervisor port for compatibility
235        let pk = Packet::new(SUPERVISOR_PORT, SUPERVISOR_CH_COMMAND, vec![CMD_EMERGENCY_STOP]);
236        self.uplink.send_async(pk).await.map_err(|_| Error::Disconnected)?;
237        Ok(())
238    }
239
240    /// Send emergency stop watchdog
241    ///
242    /// Activates/resets a watchdog failsafe that will automatically emergency stop
243    /// the drone if this message isn't sent every 1000ms. Once activated by the first
244    /// call, you must continue sending this periodically forever or the drone will
245    /// automatically emergency stop. Use only if you need automatic failsafe behavior.
246    #[deprecated(since = "0.8.1", note = "Use [`Supervisor::send_emergency_stop_watchdog`](crate::subsystems::supervisor::Supervisor::send_emergency_stop_watchdog) instead")]
247    pub async fn send_emergency_stop_watchdog(&self) -> Result<()> {
248        // Route to supervisor port for compatibility
249        let pk = Packet::new(
250            SUPERVISOR_PORT,
251            SUPERVISOR_CH_COMMAND,
252            vec![CMD_EMERGENCY_STOP_WATCHDOG],
253        );
254        self.uplink.send_async(pk).await.map_err(|_| Error::Disconnected)?;
255        Ok(())
256    }
257}
258
259/// External pose interface
260///
261/// Provides functionality to send external position and pose data from motion 
262/// capture systems or other external tracking sources to the Crazyflie's 
263/// onboard state estimator.
264pub struct ExternalPose {
265    uplink: Sender<Packet>,
266}
267
268impl ExternalPose {
269    /// Send external position (x, y, z) to the Crazyflie
270    ///
271    /// Updates the Crazyflie's position estimate with 3D position data.
272    ///
273    /// # Arguments
274    /// * `pos` - Position array [x, y, z] in meters
275    pub async fn send_external_position(&self, pos: [f32; 3]) -> Result<()> {
276        let mut payload = Vec::with_capacity(3 * 4);
277        payload.extend_from_slice(&pos[0].to_le_bytes());
278        payload.extend_from_slice(&pos[1].to_le_bytes());
279        payload.extend_from_slice(&pos[2].to_le_bytes());
280
281        let pk = Packet::new(LOCALIZATION_PORT, POSITION_CHANNEL, payload);
282        self.uplink.send_async(pk).await.map_err(|_| Error::Disconnected)?;
283        Ok(())
284    }
285
286    /// Send external pose (position + quaternion) to the Crazyflie
287    ///
288    /// Updates the Crazyflie's position estimate with full 6DOF pose data.
289    /// Includes both position and orientation.
290    ///
291    /// # Arguments
292    /// * `pos` - Position array [x, y, z] in meters
293    /// * `quat` - Quaternion array [qx, qy, qz, qw]
294    pub async fn send_external_pose(&self, pos: [f32; 3], quat: [f32; 4]) -> Result<()> {
295        let mut payload = Vec::with_capacity(1 + 7 * 4);
296        payload.push(EXT_POSE);
297        payload.extend_from_slice(&pos[0].to_le_bytes());
298        payload.extend_from_slice(&pos[1].to_le_bytes());
299        payload.extend_from_slice(&pos[2].to_le_bytes());
300        payload.extend_from_slice(&quat[0].to_le_bytes());
301        payload.extend_from_slice(&quat[1].to_le_bytes());
302        payload.extend_from_slice(&quat[2].to_le_bytes());
303        payload.extend_from_slice(&quat[3].to_le_bytes());
304
305        let pk = Packet::new(LOCALIZATION_PORT, GENERIC_CHANNEL, payload);
306        self.uplink.send_async(pk).await.map_err(|_| Error::Disconnected)?;
307        Ok(())
308    }
309}
310
311/// Loco Positioning System (UWB) interface
312///
313/// Provides functionality to send Loco Positioning Protocol (LPP) packets
314/// to ultra-wide-band positioning nodes.
315pub struct LocoPositioning {
316    uplink: Sender<Packet>,
317}
318
319impl LocoPositioning {
320    /// Send Loco Positioning Protocol (LPP) packet to a specific destination
321    ///
322    /// # Arguments
323    /// * `dest_id` - Destination node ID
324    /// * `data` - LPP packet payload
325    pub async fn send_short_lpp_packet(&self, dest_id: u8, data: &[u8]) -> Result<()> {
326        let mut payload = Vec::with_capacity(2 + data.len());
327        payload.push(LPS_SHORT_LPP_PACKET);
328        payload.push(dest_id);
329        payload.extend_from_slice(data);
330
331        let pk = Packet::new(LOCALIZATION_PORT, GENERIC_CHANNEL, payload);
332        self.uplink.send_async(pk).await.map_err(|_| Error::Disconnected)?;
333        Ok(())
334    }
335}
336
337/// Lighthouse positioning system interface
338///
339/// Provides functionality to receive lighthouse sweep angle data and manage
340/// lighthouse base station configuration persistence.
341pub struct Lighthouse {
342    uplink: Sender<Packet>,
343    angle_stream_receiver: BroadcastReceiver<LighthouseAngleData>,
344    persist_receiver: BroadcastReceiver<bool>,
345}
346
347impl Lighthouse {
348    /// Get a stream of lighthouse angle measurements
349    ///
350    /// Returns a Stream that yields [LighthouseAngleData] whenever lighthouse
351    /// sweep angle data is received from the Crazyflie. This is typically used
352    /// for lighthouse base station calibration and geometry estimation.
353    ///
354    /// To enable the angle stream, set the parameter `locSrv.enLhAngleStream` to 1
355    /// on the Crazyflie.
356    ///
357    /// # Example
358    /// ```no_run
359    /// # use crazyflie_lib::Crazyflie;
360    /// # use futures::StreamExt;
361    /// # async fn example(crazyflie: &Crazyflie) -> Result<(), Box<dyn std::error::Error>> {
362    /// // Enable angle streaming
363    /// crazyflie.param.set("locSrv.enLhAngleStream", 1u8).await?;
364    ///
365    /// let mut angle_stream = crazyflie.localization.lighthouse.angle_stream().await;
366    /// while let Some(data) = angle_stream.next().await {
367    ///     println!("Base station {}: x={:?}, y={:?}",
368    ///         data.base_station, data.angles.x, data.angles.y);
369    /// }
370    /// # Ok(())
371    /// # }
372    /// ```
373    pub async fn angle_stream(&self) -> impl Stream<Item = LighthouseAngleData> + use<> {
374        self.angle_stream_receiver.clone()
375    }
376
377    /// Persist lighthouse geometry and calibration data to permanent storage
378    ///
379    /// Sends a command to persist lighthouse geometry and/or calibration data
380    /// to permanent storage in the Crazyflie, then waits for confirmation.
381    /// The geometry and calibration data must have been previously written to
382    /// RAM via the memory subsystem.
383    ///
384    /// # Arguments
385    /// * `geo_list` - List of base station IDs (0-15) for which to persist geometry data
386    /// * `calib_list` - List of base station IDs (0-15) for which to persist calibration data
387    ///
388    /// # Returns
389    /// * `Ok(true)` if data was successfully persisted
390    /// * `Ok(false)` if persistence failed
391    /// * `Err` if there was a communication error or timeout (5 seconds)
392    ///
393    /// # Example
394    /// ```no_run
395    /// # use crazyflie_lib::Crazyflie;
396    /// # async fn example(crazyflie: &Crazyflie) -> crazyflie_lib::Result<()> {
397    /// // Persist geometry for base stations 0 and 1, calibration for base station 0
398    /// let success = crazyflie.localization.lighthouse
399    ///     .persist_lighthouse_data(&[0, 1], &[0]).await?;
400    ///
401    /// if success {
402    ///     println!("Data persisted successfully");
403    /// }
404    /// # Ok(())
405    /// # }
406    /// ```
407    pub async fn persist_lighthouse_data(&self, geo_list: &[u8], calib_list: &[u8]) -> Result<bool> {
408        self.send_lh_persist_data_packet(geo_list, calib_list).await?;
409        self.wait_persist_confirmation().await
410    }
411
412    /// Wait for lighthouse persistence confirmation
413    ///
414    /// After sending geometry or calibration data to be persisted (via
415    /// send_lh_persist_data_packet), this function waits for and returns
416    /// the confirmation from the Crazyflie.
417    ///
418    /// Returns `Ok(true)` if data was successfully persisted, `Ok(false)` if
419    /// persistence failed, or an error if no confirmation is received within
420    /// the timeout.
421    async fn wait_persist_confirmation(&self) -> Result<bool> {
422        let mut receiver = self.persist_receiver.clone();
423        match tokio::time::timeout(
424            std::time::Duration::from_secs(5),
425            receiver.recv()
426        ).await {
427            Ok(Ok(success)) => Ok(success),
428            Ok(Err(_)) => Err(Error::Disconnected),
429            Err(_) => Err(Error::Timeout),
430        }
431    }
432
433    /// Send lighthouse persist data packet
434    ///
435    /// Sends a command to persist lighthouse geometry and/or calibration data
436    /// to permanent storage in the Crazyflie. The geometry and calibration data
437    /// must have been previously written to RAM via the memory subsystem.
438    ///
439    /// # Arguments
440    /// * `geo_list` - List of base station IDs (0-15) for which to persist geometry data
441    /// * `calib_list` - List of base station IDs (0-15) for which to persist calibration data
442    ///
443    /// Use [wait_persist_confirmation] to wait for the result.
444   async fn send_lh_persist_data_packet(&self, geo_list: &[u8], calib_list: &[u8]) -> Result<()> {
445        // Validate base station IDs
446        const MAX_BS_NR: u8 = 15;
447        for &bs in geo_list {
448            if bs > MAX_BS_NR {
449                return Err(Error::ProtocolError(format!(
450                    "Invalid geometry base station ID: {} (max: {})", bs, MAX_BS_NR
451                )));
452            }
453        }
454        for &bs in calib_list {
455            if bs > MAX_BS_NR {
456                return Err(Error::ProtocolError(format!(
457                    "Invalid calibration base station ID: {} (max: {})", bs, MAX_BS_NR
458                )));
459            }
460        }
461
462        // Build bitmasks
463        let mut mask_geo: u16 = 0;
464        let mut mask_calib: u16 = 0;
465
466        for &bs in geo_list {
467            mask_geo |= 1 << bs;
468        }
469        for &bs in calib_list {
470            mask_calib |= 1 << bs;
471        }
472
473        // Build packet
474        let mut payload = Vec::with_capacity(5);
475        payload.push(LH_PERSIST_DATA);
476        payload.extend_from_slice(&mask_geo.to_le_bytes());
477        payload.extend_from_slice(&mask_calib.to_le_bytes());
478
479        let pk = Packet::new(LOCALIZATION_PORT, GENERIC_CHANNEL, payload);
480        self.uplink.send_async(pk).await.map_err(|_| Error::Disconnected)?;
481        Ok(())
482    }
483}