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}