Skip to content

Commit bd8dcff

Browse files
authored
Merge pull request #3 from bitcraze/rik/persistent_parameters
Persistent parameter bindings
2 parents 4ea6e12 + ae96685 commit bd8dcff

7 files changed

Lines changed: 421 additions & 35 deletions

File tree

cflib2/_rust.pyi

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

11161205
@typing.final
11171206
class Platform:

examples/persistent_param.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# ,---------, ____ _ __
2+
# | ,-^-, | / __ )(_) /_______________ _____ ___
3+
# | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \
4+
# | / ,--' | / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
5+
# +------` /_____/_/\__/\___/_/ \__,_/ /___/\___/
6+
#
7+
# Copyright (C) 2025 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+
Demonstrate persistent parameter storage on the Crazyflie.
23+
24+
Persistent parameters retain their values across reboots by storing them
25+
in EEPROM. This example shows how to:
26+
- List all persistent parameters
27+
- Get default values
28+
- Query persistent storage state
29+
- Store a parameter value to EEPROM
30+
- Clear a stored value from EEPROM
31+
32+
Example usage:
33+
python persistent_param.py # Use default URI
34+
python persistent_param.py --uri radio://0/80/2M/E7E7E7E701 # Custom URI
35+
"""
36+
37+
import asyncio
38+
from dataclasses import dataclass
39+
40+
import tyro
41+
42+
from cflib2 import Crazyflie, LinkContext
43+
44+
45+
@dataclass
46+
class Args:
47+
uri: str = "radio://0/80/2M/E7E7E7E7E7"
48+
"""Crazyflie URI"""
49+
50+
51+
async def main() -> None:
52+
args = tyro.cli(Args)
53+
54+
print(f"Connecting to {args.uri}...")
55+
context = LinkContext()
56+
cf = await Crazyflie.connect_from_uri(context, args.uri)
57+
print("Connected!\n")
58+
59+
param = cf.param()
60+
61+
try:
62+
# Step 1: List persistent parameters
63+
print("=== Persistent Parameters ===")
64+
persistent_params = []
65+
66+
for name in param.names():
67+
if await param.is_persistent(name):
68+
persistent_params.append(name)
69+
70+
print(f"Found {len(persistent_params)} persistent parameters\n")
71+
72+
# Step 2: Get default values
73+
print("=== Default Values ===\n")
74+
75+
test_params = ["ring.effect", "activeMarker.back", "pm.lowVoltage"]
76+
77+
for name in test_params:
78+
value = await param.get_default_value(name)
79+
print(f"{name}: {value}")
80+
81+
# Step 3: Get persistent state
82+
print("\n=== Persistent Parameter States ===\n")
83+
84+
for name in test_params:
85+
state = await param.persistent_get_state(name)
86+
print(f"{name}:")
87+
print(f" Default value: {state.default_value}")
88+
if state.is_stored:
89+
print(f" Stored value: {state.stored_value}")
90+
else:
91+
print(" Stored: No (using default)")
92+
print()
93+
94+
# Step 4: Store a value to EEPROM
95+
print("=== Storing a Parameter ===\n")
96+
97+
test_param = "ring.effect"
98+
99+
current_value = await param.get(test_param)
100+
print(f"Current value of {test_param}: {current_value}")
101+
102+
new_value = 10
103+
print(f"Setting {test_param} to {new_value}")
104+
await param.set(test_param, new_value)
105+
106+
print("Storing to EEPROM...")
107+
await param.persistent_store(test_param)
108+
print("Stored successfully!\n")
109+
110+
# Verify it's now marked as stored
111+
state = await param.persistent_get_state(test_param)
112+
print("Verification:")
113+
print(f" Default value: {state.default_value}")
114+
if state.is_stored:
115+
print(f" Stored value: {state.stored_value}")
116+
else:
117+
print(" Stored: No (using default)")
118+
119+
# Step 5: Clear a stored value from EEPROM
120+
print("\n=== Clearing a Stored Parameter ===\n")
121+
122+
print("Clearing stored value from EEPROM...")
123+
await param.persistent_clear(test_param)
124+
print("Cleared successfully!\n")
125+
126+
# Verify it's now using the default again
127+
state = await param.persistent_get_state(test_param)
128+
print("Verification:")
129+
print(f" Default value: {state.default_value}")
130+
if state.is_stored:
131+
print(f" Stored value: {state.stored_value}")
132+
else:
133+
print(" Stored: No (using default)")
134+
135+
finally:
136+
print("\nDisconnecting...")
137+
await cf.disconnect()
138+
print("Done!")
139+
140+
141+
if __name__ == "__main__":
142+
asyncio.run(main())

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};

0 commit comments

Comments
 (0)