From fc162375ba262ddea9d404229aa75aabb97d2c4d Mon Sep 17 00:00:00 2001 From: Rik Bouwmeester Date: Wed, 13 Mar 2024 17:03:14 +0100 Subject: [PATCH 1/6] Support for reading/writing persistent parameters from/to file --- cflib/localization/__init__.py | 4 +- cflib/localization/param_io.py | 85 +++++++++++++ test/localization/fixtures/parameters.yaml | 15 +++ test/localization/test_param_io.py | 140 +++++++++++++++++++++ 4 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 cflib/localization/param_io.py create mode 100644 test/localization/fixtures/parameters.yaml create mode 100644 test/localization/test_param_io.py diff --git a/cflib/localization/__init__.py b/cflib/localization/__init__.py index 7a9cc98d2..6f4252c3e 100644 --- a/cflib/localization/__init__.py +++ b/cflib/localization/__init__.py @@ -25,6 +25,7 @@ from .lighthouse_config_manager import LighthouseConfigWriter from .lighthouse_sweep_angle_reader import LighthouseSweepAngleAverageReader from .lighthouse_sweep_angle_reader import LighthouseSweepAngleReader +from .param_io import ParamFileManager __all__ = [ 'LighthouseBsGeoEstimator', @@ -32,4 +33,5 @@ 'LighthouseSweepAngleAverageReader', 'LighthouseSweepAngleReader', 'LighthouseConfigFileManager', - 'LighthouseConfigWriter'] + 'LighthouseConfigWriter', + 'ParamFileManager'] diff --git a/cflib/localization/param_io.py b/cflib/localization/param_io.py new file mode 100644 index 000000000..1b7676e6c --- /dev/null +++ b/cflib/localization/param_io.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# +# ,---------, ____ _ __ +# | ,-^-, | / __ )(_) /_______________ _____ ___ +# | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ +# | / ,--' | / /_/ / / /_/ /__/ / / /_/ / / /_/ __/ +# +------` /_____/_/\__/\___/_/ \__,_/ /___/\___/ +# +# Copyright (C) 2024 Bitcraze AB +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, in version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import yaml + +from cflib.crazyflie.param import Param, PersistentParamState + +class ParamFileManager(): + """Reads and writes parameter configurations from file""" + TYPE_ID = 'type' + TYPE = 'param_system_configuration' + VERSION_ID = 'version' + VERSION = '1' + PARAMS_ID = 'params' + + @staticmethod + def write(file_name, params={}): + file = open(file_name, 'w') + with file: + file_params = {} + for id, param in params.items(): + assert isinstance(param, PersistentParamState) + if isinstance(param, PersistentParamState): + if param.is_stored: + file_params[id] = {'is_stored': param.is_stored, 'default_value': param.default_value, 'stored_value': param.stored_value} + + data = { + ParamFileManager.TYPE_ID: ParamFileManager.TYPE, + ParamFileManager.VERSION_ID: ParamFileManager.VERSION, + ParamFileManager.PARAMS_ID: file_params + } + + yaml.dump(data, file) + + @staticmethod + def read(file_name): + file = open(file_name, 'r') + with file: + data = None + try: + data = yaml.safe_load(file) + except yaml.YAMLError as exc: + print(exc) + + if ParamFileManager.TYPE_ID not in data: + raise Exception('Type field missing') + + if data[ParamFileManager.TYPE_ID] != ParamFileManager.TYPE: + raise Exception('Unsupported file type') + + if ParamFileManager.VERSION_ID not in data: + raise Exception('Version field missing') + + if data[ParamFileManager.VERSION_ID] != ParamFileManager.VERSION: + raise Exception('Unsupported file version') + + def get_data(input_data): + persistent_params = {} + for id, param in input_data.items(): + persistent_params[id] = PersistentParamState(param['is_stored'], param['default_value'], param['stored_value']) + return persistent_params + + if ParamFileManager.PARAMS_ID in data: + return get_data(data[ParamFileManager.PARAMS_ID]) + else: + return {} diff --git a/test/localization/fixtures/parameters.yaml b/test/localization/fixtures/parameters.yaml new file mode 100644 index 000000000..e6bfc8a5f --- /dev/null +++ b/test/localization/fixtures/parameters.yaml @@ -0,0 +1,15 @@ +params: + activeMarker.back: + default_value: 3 + is_stored: true + stored_value: 10 + cppm.angPitch: + default_value: 50.0 + is_stored: true + stored_value: 55.0 + ctrlMel.i_range_z: + default_value: 0.4000000059604645 + is_stored: true + stored_value: 0.44999998807907104 +type: param_system_configuration +version: '1' diff --git a/test/localization/test_param_io.py b/test/localization/test_param_io.py new file mode 100644 index 000000000..f6865b496 --- /dev/null +++ b/test/localization/test_param_io.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# +# ,---------, ____ _ __ +# | ,-^-, | / __ )(_) /_______________ _____ ___ +# | ( O ) | / __ / / __/ ___/ ___/ __ `/_ / / _ \ +# | / ,--' | / /_/ / / /_/ /__/ / / /_/ / / /_/ __/ +# +------` /_____/_/\__/\___/_/ \__,_/ /___/\___/ +# +# Copyright (C) 2024 Bitcraze AB +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, in version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest +from unittest.mock import ANY +from unittest.mock import mock_open +from unittest.mock import patch + +import yaml + +from cflib.localization import ParamFileManager + +class TestParamFileManager(unittest.TestCase): + def setUp(self): + self.data = { + 'type': 'param_system_configuration', + 'version': '1', + } + + @patch('yaml.safe_load') + def test_that_read_open_correct_file(self, mock_yaml_load): + # Fixture + mock_yaml_load.return_value = self.data + file_name = 'some/name.yaml' + + # Test + with patch('builtins.open', new_callable=mock_open()) as mock_file: + ParamFileManager.read(file_name) + + # Assert + mock_file.assert_called_with(file_name, 'r') + + @patch('yaml.safe_load') + def test_that_missing_file_type_raises(self, mock_yaml_load): + # Fixture + self.data.pop('type') + mock_yaml_load.return_value = self.data + + # Test + # Assert + with self.assertRaises(Exception): + with patch('builtins.open', new_callable=mock_open()): + ParamFileManager.read('some/name.yaml') + + @patch('yaml.safe_load') + def test_that_wrong_file_type_raises(self, mock_yaml_load): + # Fixture + self.data['type'] = 'wrong_type' + mock_yaml_load.return_value = self.data + + # Test + # Assert + with self.assertRaises(Exception): + with patch('builtins.open', new_callable=mock_open()): + ParamFileManager.read('some/name.yaml') + + @patch('yaml.safe_load') + def test_that_missing_version_raises(self, mock_yaml_load): + + # Fixture + self.data.pop('version') + mock_yaml_load.return_value = self.data + + # Test + # Assert + with self.assertRaises(Exception): + with patch('builtins.open', new_callable=mock_open()): + ParamFileManager.read('some/name.yaml') + + @patch('yaml.safe_load') + def test_that_wrong_version_raises(self, mock_yaml_load): + # Fixture + self.data['version'] = 'wrong_version' + mock_yaml_load.return_value = self.data + + # Test + # Assert + with self.assertRaises(Exception): + with patch('builtins.open', new_callable=mock_open()): + ParamFileManager.read('some/name.yaml') + + @patch('yaml.safe_load') + def test_that_no_data_returns_empty_default_data(self, mock_yaml_load): + # Fixture + mock_yaml_load.return_value = self.data + + # Test + with patch('builtins.open', new_callable=mock_open()): + actual_params = ParamFileManager.read('some/name.yaml') + + # Assert + self.assertEqual(0, len(actual_params)) + + @patch('yaml.dump') + def test_file_end_to_end_write_read(self, mock_yaml_dump): + # Fixture + fixture_file = 'test/localization/fixtures/parameters.yaml' + + file = open(fixture_file, 'r') + expected = yaml.safe_load(file) + file.close() + + # Test + params = ParamFileManager.read(fixture_file) + with patch('builtins.open', new_callable=mock_open()): + ParamFileManager.write('some/name.yaml', params=params) + + # Assert + mock_yaml_dump.assert_called_with(expected, ANY) + + @patch('yaml.dump') + def test_file_write_to_correct_file(self, mock_yaml_dump): + # Fixture + file_name = 'some/name.yaml' + + # Test + with patch('builtins.open', new_callable=mock_open()) as mock_file: + ParamFileManager.write(file_name) + + # Assert + mock_file.assert_called_with(file_name, 'w') From ed7fe9ae7581cbaeec06d5b8b4c0667a507ae687 Mon Sep 17 00:00:00 2001 From: Rik Bouwmeester Date: Thu, 14 Mar 2024 11:53:46 +0100 Subject: [PATCH 2/6] Improve stored param type identifier --- cflib/localization/param_io.py | 2 +- test/localization/fixtures/parameters.yaml | 2 +- test/localization/test_param_io.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cflib/localization/param_io.py b/cflib/localization/param_io.py index 1b7676e6c..964a791ee 100644 --- a/cflib/localization/param_io.py +++ b/cflib/localization/param_io.py @@ -27,7 +27,7 @@ class ParamFileManager(): """Reads and writes parameter configurations from file""" TYPE_ID = 'type' - TYPE = 'param_system_configuration' + TYPE = 'persistent_param_state' VERSION_ID = 'version' VERSION = '1' PARAMS_ID = 'params' diff --git a/test/localization/fixtures/parameters.yaml b/test/localization/fixtures/parameters.yaml index e6bfc8a5f..b61abc437 100644 --- a/test/localization/fixtures/parameters.yaml +++ b/test/localization/fixtures/parameters.yaml @@ -11,5 +11,5 @@ params: default_value: 0.4000000059604645 is_stored: true stored_value: 0.44999998807907104 -type: param_system_configuration +type: persistent_param_state version: '1' diff --git a/test/localization/test_param_io.py b/test/localization/test_param_io.py index f6865b496..4a8f3826f 100644 --- a/test/localization/test_param_io.py +++ b/test/localization/test_param_io.py @@ -32,7 +32,7 @@ class TestParamFileManager(unittest.TestCase): def setUp(self): self.data = { - 'type': 'param_system_configuration', + 'type': 'persistent_param_state', 'version': '1', } From 35092d30348c647b23b5c2adda59b2254acad2c8 Mon Sep 17 00:00:00 2001 From: Rik Bouwmeester Date: Thu, 14 Mar 2024 11:54:19 +0100 Subject: [PATCH 3/6] Generalize ParamFileManager writing functionality Filtering for what to write to file should be done before calling the write method --- cflib/localization/param_io.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cflib/localization/param_io.py b/cflib/localization/param_io.py index 964a791ee..08682d916 100644 --- a/cflib/localization/param_io.py +++ b/cflib/localization/param_io.py @@ -40,8 +40,7 @@ def write(file_name, params={}): for id, param in params.items(): assert isinstance(param, PersistentParamState) if isinstance(param, PersistentParamState): - if param.is_stored: - file_params[id] = {'is_stored': param.is_stored, 'default_value': param.default_value, 'stored_value': param.stored_value} + file_params[id] = {'is_stored': param.is_stored, 'default_value': param.default_value, 'stored_value': param.stored_value} data = { ParamFileManager.TYPE_ID: ParamFileManager.TYPE, From 0e963da76ad3a7c593ec559f1cf1587363005bd8 Mon Sep 17 00:00:00 2001 From: Rik Bouwmeester Date: Thu, 14 Mar 2024 15:24:55 +0100 Subject: [PATCH 4/6] Remove unused Parameter import --- cflib/localization/param_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cflib/localization/param_io.py b/cflib/localization/param_io.py index 08682d916..7c8616755 100644 --- a/cflib/localization/param_io.py +++ b/cflib/localization/param_io.py @@ -22,7 +22,7 @@ import yaml -from cflib.crazyflie.param import Param, PersistentParamState +from cflib.crazyflie.param import PersistentParamState class ParamFileManager(): """Reads and writes parameter configurations from file""" From 23165d27e7f57b1c2c83459de3b84006438284fc Mon Sep 17 00:00:00 2001 From: Rik Bouwmeester Date: Thu, 14 Mar 2024 15:33:22 +0100 Subject: [PATCH 5/6] Warn when trying to set non-existing params --- cflib/crazyflie/param.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cflib/crazyflie/param.py b/cflib/crazyflie/param.py index 77d474a39..1dc199e8f 100644 --- a/cflib/crazyflie/param.py +++ b/cflib/crazyflie/param.py @@ -445,6 +445,9 @@ def persistent_store(self, complete_name, callback=None): @param callback Optional callback should take `complete_name` and boolean status as arguments """ element = self.toc.get_element_by_complete_name(complete_name) + if not element: + callback(complete_name, False) + return if not element.is_persistent(): raise AttributeError(f"Param '{complete_name}' is not persistent") From d12405270533e0bd49669f31c8488bf9b5a8af33 Mon Sep 17 00:00:00 2001 From: Rik Bouwmeester Date: Thu, 14 Mar 2024 15:35:12 +0100 Subject: [PATCH 6/6] Formatting --- cflib/localization/param_io.py | 14 ++++++++------ test/localization/test_param_io.py | 12 ++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/cflib/localization/param_io.py b/cflib/localization/param_io.py index 7c8616755..7d337f46f 100644 --- a/cflib/localization/param_io.py +++ b/cflib/localization/param_io.py @@ -19,11 +19,11 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - import yaml from cflib.crazyflie.param import PersistentParamState + class ParamFileManager(): """Reads and writes parameter configurations from file""" TYPE_ID = 'type' @@ -40,7 +40,8 @@ def write(file_name, params={}): for id, param in params.items(): assert isinstance(param, PersistentParamState) if isinstance(param, PersistentParamState): - file_params[id] = {'is_stored': param.is_stored, 'default_value': param.default_value, 'stored_value': param.stored_value} + file_params[id] = {'is_stored': param.is_stored, + 'default_value': param.default_value, 'stored_value': param.stored_value} data = { ParamFileManager.TYPE_ID: ParamFileManager.TYPE, @@ -49,7 +50,7 @@ def write(file_name, params={}): } yaml.dump(data, file) - + @staticmethod def read(file_name): file = open(file_name, 'r') @@ -71,13 +72,14 @@ def read(file_name): if data[ParamFileManager.VERSION_ID] != ParamFileManager.VERSION: raise Exception('Unsupported file version') - + def get_data(input_data): persistent_params = {} for id, param in input_data.items(): - persistent_params[id] = PersistentParamState(param['is_stored'], param['default_value'], param['stored_value']) + persistent_params[id] = PersistentParamState( + param['is_stored'], param['default_value'], param['stored_value']) return persistent_params - + if ParamFileManager.PARAMS_ID in data: return get_data(data[ParamFileManager.PARAMS_ID]) else: diff --git a/test/localization/test_param_io.py b/test/localization/test_param_io.py index 4a8f3826f..1154e8a85 100644 --- a/test/localization/test_param_io.py +++ b/test/localization/test_param_io.py @@ -19,7 +19,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - import unittest from unittest.mock import ANY from unittest.mock import mock_open @@ -29,6 +28,7 @@ from cflib.localization import ParamFileManager + class TestParamFileManager(unittest.TestCase): def setUp(self): self.data = { @@ -48,7 +48,7 @@ def test_that_read_open_correct_file(self, mock_yaml_load): # Assert mock_file.assert_called_with(file_name, 'r') - + @patch('yaml.safe_load') def test_that_missing_file_type_raises(self, mock_yaml_load): # Fixture @@ -85,7 +85,7 @@ def test_that_missing_version_raises(self, mock_yaml_load): with self.assertRaises(Exception): with patch('builtins.open', new_callable=mock_open()): ParamFileManager.read('some/name.yaml') - + @patch('yaml.safe_load') def test_that_wrong_version_raises(self, mock_yaml_load): # Fixture @@ -106,10 +106,10 @@ def test_that_no_data_returns_empty_default_data(self, mock_yaml_load): # Test with patch('builtins.open', new_callable=mock_open()): actual_params = ParamFileManager.read('some/name.yaml') - + # Assert self.assertEqual(0, len(actual_params)) - + @patch('yaml.dump') def test_file_end_to_end_write_read(self, mock_yaml_dump): # Fixture @@ -126,7 +126,7 @@ def test_file_end_to_end_write_read(self, mock_yaml_dump): # Assert mock_yaml_dump.assert_called_with(expected, ANY) - + @patch('yaml.dump') def test_file_write_to_correct_file(self, mock_yaml_dump): # Fixture