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}