Skip to content

Commit abbffe7

Browse files
committed
Add persistent parameter bindings and extract shared value_to_python helper
Expose crazyflie-lib's persistent parameter API to Python: is_persistent, get_default_value, persistent_get_state, persistent_store, persistent_clear. Extract Value-to-Python conversion into a shared value.rs module, replacing duplicate match blocks in param.rs and log.rs.
1 parent 3436e46 commit abbffe7

6 files changed

Lines changed: 258 additions & 35 deletions

File tree

cflib2/_rust.pyi

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,85 @@ class Param:
11121112
- The value is out of range for the parameter type
11131113
- The value cannot be represented accurately (e.g., fractional value for integer param)
11141114
"""
1115+
async def is_persistent(self, name: builtins.str) -> bool:
1116+
r"""
1117+
Check if a parameter supports persistent storage
1118+
1119+
Returns False for parameters that do not support persistence.
1120+
Raises an error if the parameter does not exist.
1121+
1122+
# Arguments
1123+
* `name` - Parameter name in format "group.name"
1124+
"""
1125+
async def get_default_value(self, name: builtins.str) -> int | float:
1126+
r"""
1127+
Get the firmware's default value for a parameter
1128+
1129+
Raises an error if the parameter is read-only or does not exist.
1130+
1131+
# Arguments
1132+
* `name` - Parameter name in format "group.name"
1133+
1134+
# Returns
1135+
The default value (int or float depending on parameter type)
1136+
"""
1137+
async def persistent_get_state(self, name: builtins.str) -> PersistentParamState:
1138+
r"""
1139+
Get the persistent storage state of a parameter
1140+
1141+
Returns a PersistentParamState with:
1142+
- `is_stored`: True if a value is currently in persistent storage
1143+
- `default_value`: The firmware's default value
1144+
- `stored_value`: The stored value, or None if not stored
1145+
1146+
Raises an error if the parameter does not exist or is not persistent.
1147+
1148+
# Arguments
1149+
* `name` - Parameter name in format "group.name"
1150+
"""
1151+
async def persistent_store(self, name: builtins.str) -> None:
1152+
r"""
1153+
Store the current parameter value to persistent storage
1154+
1155+
The parameter's current value (set with `set()`) will be saved so that
1156+
it persists across reboots. Raises an error if the parameter does not
1157+
exist or is not persistent.
1158+
1159+
# Arguments
1160+
* `name` - Parameter name in format "group.name"
1161+
"""
1162+
async def persistent_clear(self, name: builtins.str) -> None:
1163+
r"""
1164+
Clear the stored value from persistent storage
1165+
1166+
After clearing, the parameter will revert to the firmware default on
1167+
the next reboot. Raises an error if the parameter does not exist or
1168+
is not persistent.
1169+
1170+
# Arguments
1171+
* `name` - Parameter name in format "group.name"
1172+
"""
1173+
1174+
@typing.final
1175+
class PersistentParamState:
1176+
r"""
1177+
State of a persistent parameter returned by `Param.persistent_get_state()`
1178+
"""
1179+
@property
1180+
def is_stored(self) -> bool:
1181+
r"""
1182+
True if a value is currently stored in persistent storage
1183+
"""
1184+
@property
1185+
def default_value(self) -> int | float:
1186+
r"""
1187+
The firmware's default value for this parameter
1188+
"""
1189+
@property
1190+
def stored_value(self) -> int | float | None:
1191+
r"""
1192+
The value stored in persistent storage, or None if not stored
1193+
"""
11151194

11161195
@typing.final
11171196
class Platform:

rust/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ mod error;
3030
mod link_context;
3131
pub mod subsystems;
3232
mod toc_cache;
33+
pub mod value;
3334

3435
use crazyflie::Crazyflie;
3536
use link_context::LinkContext;
3637
use subsystems::{
37-
Commander, Console, Log, LogBlock, LogData, LogStream, Param, Platform, AppChannel,
38+
Commander, Console, Log, LogBlock, LogData, LogStream, Param, PersistentParamState, Platform, AppChannel,
3839
Localization, EmergencyControl, ExternalPose, Lighthouse, LocoPositioning,
3940
LighthouseAngleData, LighthouseAngles,
4041
Memory, Poly, Poly4D, CompressedStart, CompressedSegment,
@@ -53,6 +54,7 @@ fn _rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
5354
m.add_class::<LogData>()?;
5455
m.add_class::<LogStream>()?;
5556
m.add_class::<Param>()?;
57+
m.add_class::<PersistentParamState>()?;
5658
m.add_class::<Platform>()?;
5759
m.add_class::<AppChannel>()?;
5860
m.add_class::<Localization>()?;

rust/src/subsystems/log.rs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use std::sync::Arc;
2626
use tokio::sync::Mutex;
2727

2828
use crate::error::to_pyerr;
29+
use crate::value::value_to_python;
2930

3031
/// Log data returned by LogStream.next()
3132
#[gen_stub_pyclass]
@@ -226,20 +227,7 @@ impl LogStream {
226227
Python::attach(|py| {
227228
let data = PyDict::new(py);
228229
for (name, value) in log_data.data {
229-
let py_value = match value {
230-
crazyflie_lib::Value::U8(v) => v.into_pyobject(py)?.into_any().unbind(),
231-
crazyflie_lib::Value::U16(v) => v.into_pyobject(py)?.into_any().unbind(),
232-
crazyflie_lib::Value::U32(v) => v.into_pyobject(py)?.into_any().unbind(),
233-
crazyflie_lib::Value::U64(v) => v.into_pyobject(py)?.into_any().unbind(),
234-
crazyflie_lib::Value::I8(v) => v.into_pyobject(py)?.into_any().unbind(),
235-
crazyflie_lib::Value::I16(v) => v.into_pyobject(py)?.into_any().unbind(),
236-
crazyflie_lib::Value::I32(v) => v.into_pyobject(py)?.into_any().unbind(),
237-
crazyflie_lib::Value::I64(v) => v.into_pyobject(py)?.into_any().unbind(),
238-
crazyflie_lib::Value::F16(v) => v.to_f32().into_pyobject(py)?.into_any().unbind(),
239-
crazyflie_lib::Value::F32(v) => v.into_pyobject(py)?.into_any().unbind(),
240-
crazyflie_lib::Value::F64(v) => v.into_pyobject(py)?.into_any().unbind(),
241-
};
242-
data.set_item(name, py_value)?;
230+
data.set_item(name, value_to_python(py, value)?)?;
243231
}
244232

245233
Ok(LogData {

rust/src/subsystems/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,5 @@ pub use high_level_commander::HighLevelCommander;
3636
pub use localization::{Localization, EmergencyControl, ExternalPose, Lighthouse, LocoPositioning, LighthouseAngleData, LighthouseAngles};
3737
pub use log::{Log, LogBlock, LogData, LogStream};
3838
pub use memory::{Memory, Poly, Poly4D, CompressedStart, CompressedSegment};
39-
pub use param::Param;
39+
pub use param::{Param, PersistentParamState};
4040
pub use platform::{Platform, AppChannel};

rust/src/subsystems/param.rs

Lines changed: 132 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,41 @@ use pyo3_stub_gen_derive::*;
2626
use std::sync::Arc;
2727

2828
use crate::error::to_pyerr;
29+
use crate::value::value_to_python;
30+
31+
/// State of a persistent parameter returned by `Param.persistent_get_state()`
32+
#[gen_stub_pyclass]
33+
#[pyclass]
34+
pub struct PersistentParamState {
35+
is_stored: bool,
36+
default_value: Py<PyAny>,
37+
stored_value: Py<PyAny>,
38+
}
39+
40+
#[gen_stub_pymethods]
41+
#[pymethods]
42+
impl PersistentParamState {
43+
/// True if a value is currently stored in persistent storage
44+
#[getter]
45+
#[gen_stub(override_return_type(type_repr = "bool"))]
46+
fn is_stored(&self) -> bool {
47+
self.is_stored
48+
}
49+
50+
/// The firmware's default value for this parameter
51+
#[getter]
52+
#[gen_stub(override_return_type(type_repr = "int | float"))]
53+
fn default_value(&self, py: Python<'_>) -> Py<PyAny> {
54+
self.default_value.clone_ref(py)
55+
}
56+
57+
/// The value stored in persistent storage, or None if not stored
58+
#[getter]
59+
#[gen_stub(override_return_type(type_repr = "int | float | None"))]
60+
fn stored_value(&self, py: Python<'_>) -> Py<PyAny> {
61+
self.stored_value.clone_ref(py)
62+
}
63+
}
2964

3065
/// Access to the Crazyflie Param Subsystem
3166
///
@@ -88,25 +123,8 @@ impl Param {
88123
fn get<'py>(&self, py: Python<'py>, name: String) -> PyResult<Bound<'py, PyAny>> {
89124
let cf = self.cf.clone();
90125
pyo3_async_runtimes::tokio::future_into_py(py, async move {
91-
let value: crazyflie_lib::Value = cf.param.get(&name).await.map_err(to_pyerr)?;
92-
93-
// Convert Rust Value to Python object
94-
// We need to acquire the GIL to create Python objects
95-
Python::attach(|py| {
96-
Ok(match value {
97-
crazyflie_lib::Value::U8(v) => v.into_pyobject(py)?.into_any().unbind(),
98-
crazyflie_lib::Value::U16(v) => v.into_pyobject(py)?.into_any().unbind(),
99-
crazyflie_lib::Value::U32(v) => v.into_pyobject(py)?.into_any().unbind(),
100-
crazyflie_lib::Value::U64(v) => v.into_pyobject(py)?.into_any().unbind(),
101-
crazyflie_lib::Value::I8(v) => v.into_pyobject(py)?.into_any().unbind(),
102-
crazyflie_lib::Value::I16(v) => v.into_pyobject(py)?.into_any().unbind(),
103-
crazyflie_lib::Value::I32(v) => v.into_pyobject(py)?.into_any().unbind(),
104-
crazyflie_lib::Value::I64(v) => v.into_pyobject(py)?.into_any().unbind(),
105-
crazyflie_lib::Value::F16(v) => v.to_f32().into_pyobject(py)?.into_any().unbind(),
106-
crazyflie_lib::Value::F32(v) => v.into_pyobject(py)?.into_any().unbind(),
107-
crazyflie_lib::Value::F64(v) => v.into_pyobject(py)?.into_any().unbind(),
108-
})
109-
})
126+
let value = cf.param.get(&name).await.map_err(to_pyerr)?;
127+
Python::attach(|py| value_to_python(py, value))
110128
})
111129
}
112130

@@ -187,4 +205,99 @@ impl Param {
187205
Ok(())
188206
})
189207
}
208+
209+
/// Check if a parameter supports persistent storage
210+
///
211+
/// Returns False for parameters that do not support persistence.
212+
/// Raises an error if the parameter does not exist.
213+
///
214+
/// # Arguments
215+
/// * `name` - Parameter name in format "group.name"
216+
#[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, bool]"))]
217+
fn is_persistent<'py>(&self, py: Python<'py>, name: String) -> PyResult<Bound<'py, PyAny>> {
218+
let cf = self.cf.clone();
219+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
220+
cf.param.is_persistent(&name).await.map_err(to_pyerr)
221+
})
222+
}
223+
224+
/// Get the firmware's default value for a parameter
225+
///
226+
/// Raises an error if the parameter is read-only or does not exist.
227+
///
228+
/// # Arguments
229+
/// * `name` - Parameter name in format "group.name"
230+
///
231+
/// # Returns
232+
/// The default value (int or float depending on parameter type)
233+
#[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, int | float]"))]
234+
fn get_default_value<'py>(&self, py: Python<'py>, name: String) -> PyResult<Bound<'py, PyAny>> {
235+
let cf = self.cf.clone();
236+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
237+
let value = cf.param.get_default_value(&name).await.map_err(to_pyerr)?;
238+
Python::attach(|py| value_to_python(py, value))
239+
})
240+
}
241+
242+
/// Get the persistent storage state of a parameter
243+
///
244+
/// Returns a PersistentParamState with:
245+
/// - `is_stored`: True if a value is currently in persistent storage
246+
/// - `default_value`: The firmware's default value
247+
/// - `stored_value`: The stored value, or None if not stored
248+
///
249+
/// Raises an error if the parameter does not exist or is not persistent.
250+
///
251+
/// # Arguments
252+
/// * `name` - Parameter name in format "group.name"
253+
#[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, PersistentParamState]"))]
254+
fn persistent_get_state<'py>(&self, py: Python<'py>, name: String) -> PyResult<Bound<'py, PyAny>> {
255+
let cf = self.cf.clone();
256+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
257+
let state = cf.param.persistent_get_state(&name).await.map_err(to_pyerr)?;
258+
Python::attach(|py| {
259+
let stored = match state.stored_value {
260+
Some(v) => value_to_python(py, v)?,
261+
None => py.None(),
262+
};
263+
Ok(PersistentParamState {
264+
is_stored: state.is_stored,
265+
default_value: value_to_python(py, state.default_value)?,
266+
stored_value: stored,
267+
})
268+
})
269+
})
270+
}
271+
272+
/// Store the current parameter value to persistent storage
273+
///
274+
/// The parameter's current value (set with `set()`) will be saved so that
275+
/// it persists across reboots. Raises an error if the parameter does not
276+
/// exist or is not persistent.
277+
///
278+
/// # Arguments
279+
/// * `name` - Parameter name in format "group.name"
280+
#[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, None]"))]
281+
fn persistent_store<'py>(&self, py: Python<'py>, name: String) -> PyResult<Bound<'py, PyAny>> {
282+
let cf = self.cf.clone();
283+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
284+
cf.param.persistent_store(&name).await.map_err(to_pyerr)
285+
})
286+
}
287+
288+
/// Clear the stored value from persistent storage
289+
///
290+
/// After clearing, the parameter will revert to the firmware default on
291+
/// the next reboot. Raises an error if the parameter does not exist or
292+
/// is not persistent.
293+
///
294+
/// # Arguments
295+
/// * `name` - Parameter name in format "group.name"
296+
#[gen_stub(override_return_type(type_repr = "collections.abc.Coroutine[typing.Any, typing.Any, None]"))]
297+
fn persistent_clear<'py>(&self, py: Python<'py>, name: String) -> PyResult<Bound<'py, PyAny>> {
298+
let cf = self.cf.clone();
299+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
300+
cf.param.persistent_clear(&name).await.map_err(to_pyerr)
301+
})
302+
}
190303
}

rust/src/value.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// ,---------, ____ _ __
2+
// | ,-^-, | / __ )(_) /_______________ _____ ___
3+
// | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \
4+
// | / ,--' | / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
5+
// +------` /_____/_/\__/\___/_/ \__,_/ /___/\___/
6+
//
7+
// Copyright (C) 2026 Bitcraze AB
8+
//
9+
// This program is free software: you can redistribute it and/or modify
10+
// it under the terms of the GNU General Public License as published by
11+
// the Free Software Foundation, either version 3 of the License, or
12+
// (at your option) any later version.
13+
//
14+
// This program is distributed in the hope that it will be useful,
15+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
// GNU General Public License for more details.
18+
//
19+
// You should have received a copy of the GNU General Public License
20+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
22+
//! Crazyflie Value to Python conversion utilities
23+
24+
use pyo3::prelude::*;
25+
26+
/// Convert a crazyflie_lib::Value to a Python int or float.
27+
pub fn value_to_python(py: Python<'_>, value: crazyflie_lib::Value) -> PyResult<Py<PyAny>> {
28+
Ok(match value {
29+
crazyflie_lib::Value::U8(v) => v.into_pyobject(py)?.into_any().unbind(),
30+
crazyflie_lib::Value::U16(v) => v.into_pyobject(py)?.into_any().unbind(),
31+
crazyflie_lib::Value::U32(v) => v.into_pyobject(py)?.into_any().unbind(),
32+
crazyflie_lib::Value::U64(v) => v.into_pyobject(py)?.into_any().unbind(),
33+
crazyflie_lib::Value::I8(v) => v.into_pyobject(py)?.into_any().unbind(),
34+
crazyflie_lib::Value::I16(v) => v.into_pyobject(py)?.into_any().unbind(),
35+
crazyflie_lib::Value::I32(v) => v.into_pyobject(py)?.into_any().unbind(),
36+
crazyflie_lib::Value::I64(v) => v.into_pyobject(py)?.into_any().unbind(),
37+
crazyflie_lib::Value::F16(v) => v.to_f32().into_pyobject(py)?.into_any().unbind(),
38+
crazyflie_lib::Value::F32(v) => v.into_pyobject(py)?.into_any().unbind(),
39+
crazyflie_lib::Value::F64(v) => v.into_pyobject(py)?.into_any().unbind(),
40+
})
41+
}

0 commit comments

Comments
 (0)