diff --git a/cflib/utils/param_file_helper.py b/cflib/utils/param_file_helper.py new file mode 100644 index 000000000..08ae21c35 --- /dev/null +++ b/cflib/utils/param_file_helper.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# || ____ _ __ +# +------+ / __ )(_) /_______________ _____ ___ +# | 0xBC | / __ / / __/ ___/ ___/ __ `/_ / / _ \ +# +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/ +# || || /_____/_/\__/\___/_/ \__,_/ /___/\___/ +# +# 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; either version 2 +# of the License, or (at your option) any later version. +# +# 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 . +from threading import Event + +from cflib.crazyflie import Crazyflie +from cflib.localization.param_io import ParamFileManager + + +class ParamFileHelper: + '''ParamFileHelper is a helper to synchonously write multiple paramteters + from a file and store them in persistent memory''' + + def __init__(self, crazyflie): + if isinstance(crazyflie, Crazyflie): + self._cf = crazyflie + self.persistent_sema = None + self.success = False + else: + raise TypeError('ParamFileHelper only takes a Crazyflie Object') + + def _persistent_stored_callback(self, complete_name, success): + self.success = success + if not success: + print(f'Persistent params: failed to store {complete_name}!') + else: + print(f'Persistent params: stored {complete_name}!') + self.persistent_sema.set() + + def store_params_from_file(self, filename): + params = ParamFileManager().read(filename) + for param, state in params.items(): + self.persistent_sema = Event() + self._cf.param.set_value(param, state.stored_value) + self._cf.param.persistent_store(param, self._persistent_stored_callback) + self.persistent_sema.wait() + if not self.success: + break + return self.success diff --git a/examples/parameters/persistent_params_from_file.py b/examples/parameters/persistent_params_from_file.py new file mode 100644 index 000000000..dfb5008d6 --- /dev/null +++ b/examples/parameters/persistent_params_from_file.py @@ -0,0 +1,60 @@ +# -*- 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 . +""" +Example to show how to write several persistent parameters from a yaml file. +The params in the file should be formatted like this; + +params: + activeMarker.back: + default_value: 3 + is_stored: true + stored_value: 30 +type: persistent_param_state +version: '1' +""" +import argparse +import logging + +import cflib.crtp +from cflib.crazyflie import Crazyflie +from cflib.crazyflie.syncCrazyflie import SyncCrazyflie +from cflib.utils import uri_helper +from cflib.utils.param_file_helper import ParamFileHelper + + +uri = uri_helper.uri_from_env(default='radio://0/80/2M/E7E7E7E7E7') + +# Only output errors from the logging framework +logging.basicConfig(level=logging.ERROR) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-f', '--file', type=str, help='The yaml file containing the arguments. ') + args = parser.parse_args() + + cflib.crtp.init_drivers() + + cf = Crazyflie(rw_cache='./cache') + with SyncCrazyflie(uri, cf=cf) as scf: + writer = ParamFileHelper(scf.cf) + writer.store_params_from_file(args.file) diff --git a/test/utils/fixtures/five_params.yaml b/test/utils/fixtures/five_params.yaml new file mode 100644 index 000000000..4f75c8e40 --- /dev/null +++ b/test/utils/fixtures/five_params.yaml @@ -0,0 +1,23 @@ +params: + activeMarker.back: + default_value: 3 + is_stored: true + stored_value: 10 + activeMarker.front: + default_value: 3 + is_stored: true + stored_value: 10 + activeMarker.left: + 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' diff --git a/test/utils/fixtures/single_param.yaml b/test/utils/fixtures/single_param.yaml new file mode 100644 index 000000000..4c16c331b --- /dev/null +++ b/test/utils/fixtures/single_param.yaml @@ -0,0 +1,7 @@ +params: + activeMarker.back: + default_value: 3 + is_stored: true + stored_value: 10 +type: persistent_param_state +version: '1' diff --git a/test/utils/test_param_file_helper.py b/test/utils/test_param_file_helper.py new file mode 100644 index 000000000..a06a8c953 --- /dev/null +++ b/test/utils/test_param_file_helper.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# +# || ____ _ __ +# +------+ / __ )(_) /_______________ _____ ___ +# | 0xBC | / __ / / __/ ___/ ___/ __ `/_ / / _ \ +# +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/ +# || || /_____/_/\__/\___/_/ \__,_/ /___/\___/ +# +# Copyright (C) 2018 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; either version 2 +# of the License, or (at your option) any later version. +# +# 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 threading import Event +from unittest.mock import MagicMock +from unittest.mock import patch + +from cflib.crazyflie import Crazyflie +from cflib.crazyflie.syncCrazyflie import SyncCrazyflie +from cflib.utils.param_file_helper import ParamFileHelper + + +class ParamFileHelperTests(unittest.TestCase): + + def setUp(self): + self.cf_mock = MagicMock(spec=Crazyflie) + self.helper = ParamFileHelper(self.cf_mock) + + def test_ParamFileHelper_SyncCrazyflieAsParam_ThrowsException(self): + cf_mock = MagicMock(spec=SyncCrazyflie) + helper = None + try: + helper = ParamFileHelper(cf_mock) + except Exception: + self.assertIsNone(helper) + else: + self.fail('Expect exception') + + def test_ParamFileHelper_Crazyflie_Object(self): + helper = ParamFileHelper(self.cf_mock) + self.assertIsNotNone(helper) + + @patch('cflib.crazyflie.Param') + def test_ParamFileHelper_writesAndStoresParamFromFileToCrazyflie(self, mock_Param): + # Setup + cf_mock = MagicMock(spec=Crazyflie) + cf_mock.param = mock_Param + helper = ParamFileHelper(cf_mock) + # Mock blocking wait and call callback instead. This lets the flow work as it would in the asynch world + + def mock_wait(self, timeout=None): + helper._persistent_stored_callback('activeMarker.back', True) + return + + with patch.object(Event, 'wait', new=mock_wait): + self.assertTrue(helper.store_params_from_file('test/utils/fixtures/single_param.yaml')) + mock_Param.set_value.assert_called_once_with('activeMarker.back', 10) + mock_Param.persistent_store.assert_called_once_with('activeMarker.back', helper._persistent_stored_callback) + + @patch('cflib.crazyflie.Param') + def test_ParamFileHelper_writesParamAndFailsToSetPersistantShouldReturnFalse(self, mock_Param): + # Setup + cf_mock = MagicMock(spec=Crazyflie) + cf_mock.param = mock_Param + helper = ParamFileHelper(cf_mock) + # Mock blocking wait and call callback instead. This lets the flow work as it would in the asynch world + + def mock_wait(self, timeout=None): + helper._persistent_stored_callback('activeMarker.back', False) + return + + with patch.object(Event, 'wait', new=mock_wait): + self.assertFalse(helper.store_params_from_file('test/utils/fixtures/single_param.yaml')) + mock_Param.set_value.assert_called_once_with('activeMarker.back', 10) + mock_Param.persistent_store.assert_called_once_with('activeMarker.back', helper._persistent_stored_callback) + + @patch('cflib.crazyflie.Param') + def test_ParamFileHelper_TryWriteSeveralParamsPersistantShouldBreakAndReturnFalse(self, mock_Param): + # Setup + cf_mock = MagicMock(spec=Crazyflie) + cf_mock.param = mock_Param + helper = ParamFileHelper(cf_mock) + # Mock blocking wait and call callback instead. This lets the flow work as it would in the asynch world + + def mock_wait(self, timeout=None): + helper._persistent_stored_callback('activeMarker.back', False) + return + + with patch.object(Event, 'wait', new=mock_wait): + # Test and assert + self.assertFalse(helper.store_params_from_file('test/utils/fixtures/five_params.yaml')) + # Assert it breaks directly by checking number of calls + mock_Param.set_value.assert_called_once_with('activeMarker.back', 10) + mock_Param.persistent_store.assert_called_once_with('activeMarker.back', helper._persistent_stored_callback) + + @patch('cflib.crazyflie.Param') + def test_ParamFileHelper_writesAndStoresAllParamsFromFileToCrazyflie(self, mock_Param): + # Setup + cf_mock = MagicMock(spec=Crazyflie) + cf_mock.param = mock_Param + helper = ParamFileHelper(cf_mock) + # Mock blocking wait and call callback instead. This lets the flow work as it would in the asynch world + + def mock_wait(self, timeout=None): + helper._persistent_stored_callback('something', True) + return + with patch.object(Event, 'wait', new=mock_wait): + # Test and Assert + self.assertTrue(helper.store_params_from_file('test/utils/fixtures/five_params.yaml')) + self.assertEquals(5, len(mock_Param.set_value.mock_calls)) + self.assertEquals(5, len(mock_Param.persistent_store.mock_calls))