1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! # Link Context
//!
//! The Link context keeps track of the radio dongles opened and used by connections.
//! It also keeps track of the async executor
use crate::connection::Connection;
use crate::connection::ConnectionTrait;
use crate::connection::PlatformAck;
use crate::crazyflie_usb_connection::CrazyflieUSBConnection;
use crate::crazyradio::{SharedCrazyradio, WeakSharedCrazyradio};
use crate::crazyradio_connection::CrazyradioConnection;
use crate::error::{Error, Result};
use futures_util::lock::Mutex;
use std::collections::BTreeMap;
/// Context for the link connections
pub struct LinkContext {
radios: Mutex<BTreeMap<usize, WeakSharedCrazyradio>>,
}
impl LinkContext {
/// Create a new link context
pub fn new() -> Self {
Self {
radios: Mutex::new(BTreeMap::new()),
}
}
/// Get SharedCrazyradio for the given radio index
///
/// If the radio is already opened, a new instance of SharedCrazyradio for this radio is returned.
/// Otherwise, the radio is opened and a SharedCrazyradio instance using it is returned.
///
/// Returns an error if the radio cannot be opened.
pub async fn get_radio(&self, radio_nth: usize) -> Result<SharedCrazyradio> {
let mut radios = self.radios.lock().await;
radios.entry(radio_nth).or_insert_with(WeakSharedCrazyradio::default);
let radio = match radios[&radio_nth].upgrade() {
Some(radio) => radio,
None => {
let new_radio = crate::crazyradio::Crazyradio::open_nth_async(radio_nth).await?;
let new_radio = SharedCrazyradio::new(new_radio);
radios.insert(radio_nth, new_radio.downgrade());
new_radio
}
};
Ok(radio)
}
/// Scan for Crazyflies at some given address
///
/// This function will send a packet to every channels and look for an acknowledgement in return.
///
/// The address argument will set the radio packets address to scan for.
///
/// It returns a list of URIs that can be passed to the [LinkContext::open_link()] function.
pub async fn scan(&self, address: [u8; 5]) -> Result<Vec<String>> {
let mut found = Vec::new();
found.extend(CrazyradioConnection::scan(self, address).await?);
found.extend(CrazyflieUSBConnection::scan().await?);
Ok(found)
}
/// Scan for a given list of URIs
///
/// Send a packet to each URI and detect if an acknowledgement is sent back.
///
/// Returns the list of URIs that acknowledged
pub async fn scan_selected(&self, uris: Vec<&str>) -> Result<Vec<String>> {
let mut found = Vec::new();
found.extend(CrazyradioConnection::scan_selected(self, uris.clone()).await?);
found.extend(CrazyflieUSBConnection::scan_selected(uris).await?);
Ok(found)
}
/// Open a link connection to a given URI
///
/// If successful, the link [Connection] is returned.
pub async fn open_link(&self, uri: &str) -> Result<Connection> {
let connection: Option<Box<dyn ConnectionTrait + Send + Sync>> =
if let Some(connection) = CrazyradioConnection::open(self, uri).await? {
Some(Box::new(connection))
} else if let Some(connection) = CrazyflieUSBConnection::open(self, uri).await? {
Some(Box::new(connection))
} else {
None
};
let internal_connection = connection.ok_or(Error::InvalidUri)?;
Ok(Connection::new(internal_connection))
}
/// Send a platform command via the radio link
///
/// This sends a one-shot command directly to the radio/platform chip (nRF)
/// on the Crazyflie without establishing a CRTP connection. The command is
/// sent as raw bytes and the acknowledgement response is returned.
///
/// # Safety around active connections
///
/// This function is safe to call while a CRTP connection is active,
/// because the API enforces that only null-port platform packets can be
/// sent. These packets are handled by the nRF radio chip itself and are
/// never forwarded to the STM32 application processor, so they cannot
/// interfere with the CRTP data stream.
///
/// The following guardrails are enforced:
///
/// - **Minimum length**: `data` must be at least 2 bytes. A single-byte
/// null packet (`0xFF`) is a keepalive that would be forwarded to the
/// STM32, so the second byte (command type) is required.
/// - **Null CRTP header**: `data[0] & 0xF3` must equal `0xF3`. The mask
/// `0xF3` ignores bits 2–3 which carry safelink flow-control counters,
/// so headers like `0xFF`, `0xF7`, `0xFB` are all accepted as valid
/// null-port headers. Any packet with a non-null port or channel would
/// be rejected.
///
/// # Use cases
///
/// Platform commands are useful for operations such as power management,
/// battery voltage queries, radio configuration, or querying platform
/// information.
///
/// Only `radio://` URIs are supported. Other URI schemes will return
/// [`Error::NotSupported`]. Invalid packet data returns [`Error::InvalidData`].
pub async fn platform_command(&self, uri: &str, data: Vec<u8>) -> Result<PlatformAck> {
// Validate that the packet has a null CRTP header and a command type byte.
// A single-byte null packet (0xF3) is a keepalive that would be forwarded
// to the STM32, so we require at least 2 bytes.
if data.len() < 2 || data[0] & 0xF3 != 0xF3 {
return Err(Error::InvalidData);
}
let (radio_nth, channel, address, _, _) =
CrazyradioConnection::parse_uri(uri).map_err(|e| match e {
Error::InvalidUriScheme => Error::NotSupported,
other => other,
})?;
let mut radio = self.get_radio(radio_nth).await?;
let (ack, payload) = radio.send_packet_async(channel, address, data).await?;
Ok(PlatformAck {
received: ack.received,
data: payload,
rssi_dbm: ack.rssi_dbm,
power_detector: ack.power_detector,
retry: ack.retry,
})
}
}