Skip to content

Commit

Permalink
Merge pull request #443 from bitcraze/rik/persistentparameterfilestorage
Browse files Browse the repository at this point in the history
Implement persistent parameter file management
  • Loading branch information
tobbeanton committed Mar 25, 2024
2 parents 3e84cee + d124052 commit a77b402
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 1 deletion.
3 changes: 3 additions & 0 deletions cflib/crazyflie/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
4 changes: 3 additions & 1 deletion cflib/localization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
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',
'LighthouseBsVector',
'LighthouseSweepAngleAverageReader',
'LighthouseSweepAngleReader',
'LighthouseConfigFileManager',
'LighthouseConfigWriter']
'LighthouseConfigWriter',
'ParamFileManager']
86 changes: 86 additions & 0 deletions cflib/localization/param_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# -*- 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 <http://www.gnu.org/licenses/>.
import yaml

from cflib.crazyflie.param import PersistentParamState


class ParamFileManager():
"""Reads and writes parameter configurations from file"""
TYPE_ID = 'type'
TYPE = 'persistent_param_state'
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):
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 {}
15 changes: 15 additions & 0 deletions test/localization/fixtures/parameters.yaml
Original file line number Diff line number Diff line change
@@ -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: persistent_param_state
version: '1'
140 changes: 140 additions & 0 deletions test/localization/test_param_io.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
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': 'persistent_param_state',
'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')

0 comments on commit a77b402

Please sign in to comment.