Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')