Skip to content

Commit a77b402

Browse files
authored
Merge pull request #443 from bitcraze/rik/persistentparameterfilestorage
Implement persistent parameter file management
2 parents 3e84cee + d124052 commit a77b402

File tree

5 files changed

+247
-1
lines changed

5 files changed

+247
-1
lines changed

cflib/crazyflie/param.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,9 @@ def persistent_store(self, complete_name, callback=None):
445445
@param callback Optional callback should take `complete_name` and boolean status as arguments
446446
"""
447447
element = self.toc.get_element_by_complete_name(complete_name)
448+
if not element:
449+
callback(complete_name, False)
450+
return
448451
if not element.is_persistent():
449452
raise AttributeError(f"Param '{complete_name}' is not persistent")
450453

cflib/localization/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
from .lighthouse_config_manager import LighthouseConfigWriter
2626
from .lighthouse_sweep_angle_reader import LighthouseSweepAngleAverageReader
2727
from .lighthouse_sweep_angle_reader import LighthouseSweepAngleReader
28+
from .param_io import ParamFileManager
2829

2930
__all__ = [
3031
'LighthouseBsGeoEstimator',
3132
'LighthouseBsVector',
3233
'LighthouseSweepAngleAverageReader',
3334
'LighthouseSweepAngleReader',
3435
'LighthouseConfigFileManager',
35-
'LighthouseConfigWriter']
36+
'LighthouseConfigWriter',
37+
'ParamFileManager']

cflib/localization/param_io.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# ,---------, ____ _ __
4+
# | ,-^-, | / __ )(_) /_______________ _____ ___
5+
# | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \
6+
# | / ,--' | / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
7+
# +------` /_____/_/\__/\___/_/ \__,_/ /___/\___/
8+
#
9+
# Copyright (C) 2024 Bitcraze AB
10+
#
11+
# This program is free software: you can redistribute it and/or modify
12+
# it under the terms of the GNU General Public License as published by
13+
# the Free Software Foundation, in version 3.
14+
#
15+
# This program is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU General Public License
21+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
import yaml
23+
24+
from cflib.crazyflie.param import PersistentParamState
25+
26+
27+
class ParamFileManager():
28+
"""Reads and writes parameter configurations from file"""
29+
TYPE_ID = 'type'
30+
TYPE = 'persistent_param_state'
31+
VERSION_ID = 'version'
32+
VERSION = '1'
33+
PARAMS_ID = 'params'
34+
35+
@staticmethod
36+
def write(file_name, params={}):
37+
file = open(file_name, 'w')
38+
with file:
39+
file_params = {}
40+
for id, param in params.items():
41+
assert isinstance(param, PersistentParamState)
42+
if isinstance(param, PersistentParamState):
43+
file_params[id] = {'is_stored': param.is_stored,
44+
'default_value': param.default_value, 'stored_value': param.stored_value}
45+
46+
data = {
47+
ParamFileManager.TYPE_ID: ParamFileManager.TYPE,
48+
ParamFileManager.VERSION_ID: ParamFileManager.VERSION,
49+
ParamFileManager.PARAMS_ID: file_params
50+
}
51+
52+
yaml.dump(data, file)
53+
54+
@staticmethod
55+
def read(file_name):
56+
file = open(file_name, 'r')
57+
with file:
58+
data = None
59+
try:
60+
data = yaml.safe_load(file)
61+
except yaml.YAMLError as exc:
62+
print(exc)
63+
64+
if ParamFileManager.TYPE_ID not in data:
65+
raise Exception('Type field missing')
66+
67+
if data[ParamFileManager.TYPE_ID] != ParamFileManager.TYPE:
68+
raise Exception('Unsupported file type')
69+
70+
if ParamFileManager.VERSION_ID not in data:
71+
raise Exception('Version field missing')
72+
73+
if data[ParamFileManager.VERSION_ID] != ParamFileManager.VERSION:
74+
raise Exception('Unsupported file version')
75+
76+
def get_data(input_data):
77+
persistent_params = {}
78+
for id, param in input_data.items():
79+
persistent_params[id] = PersistentParamState(
80+
param['is_stored'], param['default_value'], param['stored_value'])
81+
return persistent_params
82+
83+
if ParamFileManager.PARAMS_ID in data:
84+
return get_data(data[ParamFileManager.PARAMS_ID])
85+
else:
86+
return {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
params:
2+
activeMarker.back:
3+
default_value: 3
4+
is_stored: true
5+
stored_value: 10
6+
cppm.angPitch:
7+
default_value: 50.0
8+
is_stored: true
9+
stored_value: 55.0
10+
ctrlMel.i_range_z:
11+
default_value: 0.4000000059604645
12+
is_stored: true
13+
stored_value: 0.44999998807907104
14+
type: persistent_param_state
15+
version: '1'

test/localization/test_param_io.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# ,---------, ____ _ __
4+
# | ,-^-, | / __ )(_) /_______________ _____ ___
5+
# | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \
6+
# | / ,--' | / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
7+
# +------` /_____/_/\__/\___/_/ \__,_/ /___/\___/
8+
#
9+
# Copyright (C) 2024 Bitcraze AB
10+
#
11+
# This program is free software: you can redistribute it and/or modify
12+
# it under the terms of the GNU General Public License as published by
13+
# the Free Software Foundation, in version 3.
14+
#
15+
# This program is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU General Public License
21+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
import unittest
23+
from unittest.mock import ANY
24+
from unittest.mock import mock_open
25+
from unittest.mock import patch
26+
27+
import yaml
28+
29+
from cflib.localization import ParamFileManager
30+
31+
32+
class TestParamFileManager(unittest.TestCase):
33+
def setUp(self):
34+
self.data = {
35+
'type': 'persistent_param_state',
36+
'version': '1',
37+
}
38+
39+
@patch('yaml.safe_load')
40+
def test_that_read_open_correct_file(self, mock_yaml_load):
41+
# Fixture
42+
mock_yaml_load.return_value = self.data
43+
file_name = 'some/name.yaml'
44+
45+
# Test
46+
with patch('builtins.open', new_callable=mock_open()) as mock_file:
47+
ParamFileManager.read(file_name)
48+
49+
# Assert
50+
mock_file.assert_called_with(file_name, 'r')
51+
52+
@patch('yaml.safe_load')
53+
def test_that_missing_file_type_raises(self, mock_yaml_load):
54+
# Fixture
55+
self.data.pop('type')
56+
mock_yaml_load.return_value = self.data
57+
58+
# Test
59+
# Assert
60+
with self.assertRaises(Exception):
61+
with patch('builtins.open', new_callable=mock_open()):
62+
ParamFileManager.read('some/name.yaml')
63+
64+
@patch('yaml.safe_load')
65+
def test_that_wrong_file_type_raises(self, mock_yaml_load):
66+
# Fixture
67+
self.data['type'] = 'wrong_type'
68+
mock_yaml_load.return_value = self.data
69+
70+
# Test
71+
# Assert
72+
with self.assertRaises(Exception):
73+
with patch('builtins.open', new_callable=mock_open()):
74+
ParamFileManager.read('some/name.yaml')
75+
76+
@patch('yaml.safe_load')
77+
def test_that_missing_version_raises(self, mock_yaml_load):
78+
79+
# Fixture
80+
self.data.pop('version')
81+
mock_yaml_load.return_value = self.data
82+
83+
# Test
84+
# Assert
85+
with self.assertRaises(Exception):
86+
with patch('builtins.open', new_callable=mock_open()):
87+
ParamFileManager.read('some/name.yaml')
88+
89+
@patch('yaml.safe_load')
90+
def test_that_wrong_version_raises(self, mock_yaml_load):
91+
# Fixture
92+
self.data['version'] = 'wrong_version'
93+
mock_yaml_load.return_value = self.data
94+
95+
# Test
96+
# Assert
97+
with self.assertRaises(Exception):
98+
with patch('builtins.open', new_callable=mock_open()):
99+
ParamFileManager.read('some/name.yaml')
100+
101+
@patch('yaml.safe_load')
102+
def test_that_no_data_returns_empty_default_data(self, mock_yaml_load):
103+
# Fixture
104+
mock_yaml_load.return_value = self.data
105+
106+
# Test
107+
with patch('builtins.open', new_callable=mock_open()):
108+
actual_params = ParamFileManager.read('some/name.yaml')
109+
110+
# Assert
111+
self.assertEqual(0, len(actual_params))
112+
113+
@patch('yaml.dump')
114+
def test_file_end_to_end_write_read(self, mock_yaml_dump):
115+
# Fixture
116+
fixture_file = 'test/localization/fixtures/parameters.yaml'
117+
118+
file = open(fixture_file, 'r')
119+
expected = yaml.safe_load(file)
120+
file.close()
121+
122+
# Test
123+
params = ParamFileManager.read(fixture_file)
124+
with patch('builtins.open', new_callable=mock_open()):
125+
ParamFileManager.write('some/name.yaml', params=params)
126+
127+
# Assert
128+
mock_yaml_dump.assert_called_with(expected, ANY)
129+
130+
@patch('yaml.dump')
131+
def test_file_write_to_correct_file(self, mock_yaml_dump):
132+
# Fixture
133+
file_name = 'some/name.yaml'
134+
135+
# Test
136+
with patch('builtins.open', new_callable=mock_open()) as mock_file:
137+
ParamFileManager.write(file_name)
138+
139+
# Assert
140+
mock_file.assert_called_with(file_name, 'w')

0 commit comments

Comments
 (0)