crazyflie_lib/subsystems/
localization.rs

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