diff --git a/CMakeLists.txt b/CMakeLists.txt index f50a6b1ba33c7a5edfa0ba33fee2218f89bb7b22..347a3b9eda22db68353512b362f2a28d9768b541 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,6 +135,7 @@ add_executable(catch-tests-boardcore src/tests/catch/test-airbrakesInterp.cpp src/tests/catch/test-pitot.cpp src/tests/catch/test-units.cpp + src/tests/catch/test-registry-frontend.cpp ) target_compile_definitions(catch-tests-boardcore PRIVATE USE_MOCK_PERIPHERALS) sbs_target(catch-tests-boardcore stm32f429zi_stm32f4discovery) diff --git a/cmake/boardcore-host.cmake b/cmake/boardcore-host.cmake index 977102ebbfb21ae52befaac414cea42d649da0d5..b3896678ec6530af04a9c1f113fef95699279077 100644 --- a/cmake/boardcore-host.cmake +++ b/cmake/boardcore-host.cmake @@ -57,6 +57,8 @@ add_library(boardcore-host STATIC EXCLUDE_FROM_ALL ${SBS_BASE}/src/shared/utils/SkyQuaternion/SkyQuaternion.cpp ${SBS_BASE}/src/shared/utils/Stats/Stats.cpp ${SBS_BASE}/src/shared/utils/TestUtils/TestHelper.cpp + ${SBS_BASE}/src/shared/utils/Registry/RegistryFrontend.cpp + ${SBS_BASE}/src/shared/utils/Registry/RegistrySerializer.cpp ) add_library(SkywardBoardcore::Boardcore::host ALIAS boardcore-host) target_include_directories(boardcore-host PUBLIC ${SBS_BASE}/src/shared) diff --git a/cmake/boardcore.cmake b/cmake/boardcore.cmake index 49c749382bc5f0c28f9c8cadaaa283afae039ec5..c437586f8c34dc95259867961074f2e454082a59 100644 --- a/cmake/boardcore.cmake +++ b/cmake/boardcore.cmake @@ -130,7 +130,9 @@ foreach(OPT_BOARD ${BOARDS}) ${SBS_BASE}/src/shared/utils/PinObserver/PinObserver.cpp ${SBS_BASE}/src/shared/utils/SkyQuaternion/SkyQuaternion.cpp ${SBS_BASE}/src/shared/utils/Stats/Stats.cpp - ${SBS_BASE}/src/shared/utils/TestUtils/TestHelper.cpp + ${SBS_BASE}/src/shared/utils/TestUtils/TestHelper.cpp + ${SBS_BASE}/src/shared/utils/Registry/RegistryFrontend.cpp + ${SBS_BASE}/src/shared/utils/Registry/RegistrySerializer.cpp ) add_library(SkywardBoardcore::Boardcore::${OPT_BOARD} ALIAS ${BOARDCORE_LIBRARY}) target_include_directories(${BOARDCORE_LIBRARY} PUBLIC ${SBS_BASE}/src/shared) diff --git a/src/shared/utils/Registry/README.md b/src/shared/utils/Registry/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1d9750a49d921d4f0cffc3d75274286cdd9013d2 --- /dev/null +++ b/src/shared/utils/Registry/README.md @@ -0,0 +1,253 @@ +# Skyward Registry + +### Goal and purpose + +Skyward Registry is a Skyward project to develop a configuration saving +mechanism into persistent (flash memory) and non persistent memories (RAM). + +Its purpose is to save the configurations for the rocket and retrieve them after +a reboot or momentary CPU issues and transient events. + +The need of such sw development has its roots in some events in which +the reboot of the rocket, and therefore its configuration, lent to a misconfiguration, taking defaults values. This caused the propulsion chamber's valves +to open with wrong timing and therefore causing a misfire. +Therefore, it is of the utmost importance to save the configuration and avoid default and hardcoded values. + +## The front-end + +### Invocation examples +In this case we take as example a fictitious configuration entry with uint32_t configuration `id` and value `value` + +Type-unsafe interface methods (which takes any data type given as value, without having any map id->data type if the entry was not already set): + +#### setConfigurationUnsafe +```cpp +float value = 1.3; +// The id could also be a specific enum (will be casted to uint32_t) +uint32_t id = 10; + +if(frontEnd.setUnsafe(id, value) == RegistryError::OK) +{ + // correctly set +} + +``` +#### getConfigurationUnsafe +```cpp +float value; +// The id could also be a specific enum (will be casted to uint32_t) +uint32_t id; + + +if(frontEnd.getConfigurationUnsafe(id, value) == RegistryError::OK) +{ + /* Getted the value */ +} +``` + +#### getConfigurationOrDefaultUnsafe +```cpp +uint32_t ignitionTime, ignitionDefault = 200; +//The id could also be a specific enum (will be casted to uint32_t) +uint32_t id = 0; +/* Default value that will be get (and possibly set) if cannot get an already initialized value*/ +uint32_t default = 400; + +ignitionTime = frontEnd.getConfigurationOrDefaultUnsafe(id, ignitionDefault); +``` + +#### arm +```cpp +frontEnd.arm() +``` + +#### disarm +```cpp +frontEnd.disarm() +``` + +#### isEmpty +```cpp +if(frontEnd.isEmpty()) +{ + /* The front end configuration is empty */ +} +``` + +#### save +```cpp +if(frontEnd.save() == RegistryError::OK) +{ + // Saved correctly the configuration +} +``` + +#### load +```cpp +if(frontEnd.load() == RegistryError::OK){ + // Loaded the configuration correctly +} +``` + +#### clear +```cpp +frontEnd.clear(); +``` + +### How to add new structs +The correct flow to add new types/configuration entries is: + +If the data type needed not exists already you need to follow these steps: + +- `TypeStructure.h`: + - Add the type to the TypeUnion; + - Update the type enum; + - Create the correct overloads in EntryStructsUnion; + + - `RegistrySerializer.h`, `RegistrySerializer.cpp`: + - Update the write + - Update the deserialize + + +### Goals + +| Goal | Description | +|:-----|:--------:| +| G1 | The configuration could be possibly saved in memory | +| G2 | The configuration could be possibly loaded from memory | +|G3 | It will be possible to set the value of a particular configuration entry| +|G4 | It will be possible to get the value of a particular configuration entry | +|G5 | It will be possible to inspect the configured entries | +|G6 | More data types can be used as value for the different configurations entries | +|G7 | There will be a protection against changes to the configuration during flight | +|G8 | No dynamic allocations will be executed during the flight | +|G9 | The registry will offer a persistent configuration saving | +|G10 | It will be possible to verify the integrity of the configuration| +|G11 | It will be possible to explore the current configuration | +|G12 | Thread safeness is guaranteed | + +**Note:** the Registry Frontend purpose is not to assure with 100% certainty that a configuration is saved in memory and will be loaded. Instead it is an +additional safety net which, in case of a restart of the rocket, could possibly lead to have the saved configuration re-loaded after reboot/reset. + +This note about the registry frontend purpose also considers the fact that a configuration may not be saved in memory, that the memory could have issues or even that a configuration is saved but corrupted. + +### Assumptions +The front-end, FE, considers some important assumptions about the usage of such sw component. +This assumptions are about the method call, the order of operations and other assumptions on the usage. +Such assumptions considers the actual necessities for Skyward Registry and might not be true in future. + +| Assumption | Description | +|:-----|:--------:| +|A1 | The FE is constructed and started once, a single time| +|A2 | The FE does saves and loads a single configuration from the registry and no multiple versions| +|A3 | The FE is called before the flight phase for instantiation, load and set | +|A4 | The FE is correctly armed before the flight | +|A5 | The FE disarming is not used during flight phase | +|A6 | The caller considers the return of the get and set methods| +|A7 | Configuration entries does not have pointers as datatype | +|A8 | The backend does not modify the vector to be saved.| +|A9 | The save method is correctly trigger externally when a save is needed | + +### Requirements + +| Requirements | Description | +|:-----|:--------:| +|R1 | The interface must allow setting a value for the specific configuration entries | +|R2 | The interface must allow getting a specified value for a configuration entries | +|R3 | The interface must perform some type check for the specifics entries to get | +|R4 | The interface must not allocate memory during the flight phase | +|R5 | The interface must not change the configuration entries during flight phase | +|R6 | The interface does save the configuration entries using the back-end components | +|R7 | The FE must manage concurrent get/set by multiple threads (Thread safety)| +|R8 | The FE must manage concurrent arm/disarm by multiple threads (Thread safety)| +|R9 | The FE must allow exploration of the actual configuration| +|R10 | The FE should be able to control the configuration state (empty or has elements)| +| R11 | The FE should not be blocked during the actual backend saving procedure | + +### Interface methods + +The unsafe (type-unsafe) methods uses a parameter value for a specific registry entry uint32_t index identifier (could be a casted enumerator from OBSW structures) and a value from a specific data type. The Unsafeness is given by the fact that the data type is given from the value data type given to the set/get methods. + +- `start`: Starts the backend and other objects that requires to be started, as an ActiveObject. + +- `setUnsafe`: A setter method is in need to ensure R1. This method should allow + insert a value for a specific configuration entry while guarantee the different data types for the values (R3). + +- `getUnsafe`: A getter method is in need to ensure the visibility of the configuration. + Such method should get a value for a specified configuration entry and changes the value to passed by reference value parameter. It does check that the type is consistent with the saved one. + +- `getOrSetDefaultUnsafe`: A particular get method which returns the get value and if it is not existing in the configuration set it (if possible!) and returns the default value. + + - `arm`: An "arm" method which guarantees no memory allocations and no changes to the configuration are in place + until a "disarm" method. It is an important functionality to ensure a "safe" memory configuration during flight. + + - `disarm`: A "disarm" method, to disable the stable and "safe" configuration mode of the registry to allow again + allocations and settings on the configuration. + + - `isEmpty`: A method to know if there is an existing configuration or if it is empty + +- `configuredEntries`: A method which returns the already existing entries of the configuration as a set. + +- `load`: Loads from the backend the configuration. If no backend, it loads it from its own vector. + +- `save`: Saves the configuration to the backend. + +- `clear`: Clears the configuration both in frontend and backend components, starting with an empty configuration. + +- `forEach`: Given a callback, it does apply it for each element of the actual configuration with the id and EntryStructUnion parameters. + +### Data structures +The data structures are managed in 2 main header files. +#### RegistryTypes.h +Type structures have: + +- `TypeUnion`: The union type for saving the values of the different configuration entries + +- `EntryStructsUnion`: The structure actually saved into the configuration, with the value and type attributes. Also, it does expose all the useful operations for get/set the value from/to union and create a new instance of EntryStructsUnion through `make(value)` method. + +#### RegistrySerializer.h + +- `RegistryHeader`: The header structure for the serialization and de-serialization. Contains the attributes and information useful for the serialization and de-serialization procedures. +- `RegistryFooter`: Contains the custom checksum of the serialized configuration. + +## The serializer +The serializer simply taken a configuration, it serialize or deserialize it on the vector given in the constructor, following the format specified above. It isolates the serialize and de-serialize procedure from the frontend by making it simpler and more coherent. + +### Exposed methods: +- `RegistrySerializer(vector)` The constructor needs the vector to/from which it writes the serialized data or reads to deserialize it. + +- `serializeConfiguration(configuration)` Serializes the configuration + header and footer to the vector given in the constructor. + + - `deserializeConfiguration(configuration)` From the vector, loads the configuration inserting the entries if the vector checks (Checksum, length, startBytes) are verified. + +### Data saving serialization +serializationVector is a vector that after getSerializedVector will contain: +||| +|:-----|--------:| +|Header| serializardConfigurations| + +As for now, the header (visible in RegistryTypes.h) is composed with 8B of bytes equal to decimal 1 for endianess checking, 4 bytes of vector length (whole vector including the header), 4B of nr. of configuration entries in the serialized vector, 4B of checksum - checksum computed with xor byte per byte of the serialized configurations following the header. + +||||||||| +|:-----|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|----:| +|8B = 1| 4B Length vector | 4B Nr. entries | 4B Checksum | ID_0 | TypeID_0 | Value_0| ... | + +Header closeup (0bit as rightmost one / big endian): + +||||||||||||||||||||| +|:-----|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|----:| +|0|0|0|0|0|0|0|1|v_len 31-24|v_len 23-16|v_len 15-8|v_len 7-0|nr_en 31-24|nr_en 23-16|nr_en 15-8|nr_en 7-0|chsum 31-24|chsum 23-16|chsum 15-8|chsum 7-0| + +The vector will contain only the set configurations. After the header, there will be all the configured entries with configuration ID, Type ID, Value(s). + +In case of multiple values, they are one after the other + +e.g. for a Coordinate: +||||| +|:-----|:--------:|:--------:|--------:| +| ID: 2 | TypeID: 1 | Val_1: 45.50109 | Val_2: 9.15633 | + +Where TypeID in this example is a coordinates type and therefore 45.50109 is the latitude and 9.15633 the longitude + +### Saving +The save of the configuration is done manually by using the RegistryFrontend `save()` method. To reset the memory `clear()` + `save()` should be used. \ No newline at end of file diff --git a/src/shared/utils/Registry/RegistryBackend.h b/src/shared/utils/Registry/RegistryBackend.h new file mode 100644 index 0000000000000000000000000000000000000000..9a1f8cffa571d0068ff685444dbd094082605475 --- /dev/null +++ b/src/shared/utils/Registry/RegistryBackend.h @@ -0,0 +1,81 @@ +/* Copyright (c) 2024 Skyward Experimental Rocketry + * Author: Nicolò Caruso, Davide Mor + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include <vector> + +#include "RegistryTypes.h" + +namespace Boardcore +{ + +/** + * @brief Registry Backend class used to save and load data to the designated + * storage/memory. + */ +class RegistryBackend +{ +public: + /** + * @brief Starts the backend, eventually used in backends that + * need to start classes and other things e.g. an ActiveObject + * + * @return true if successful, false otherwise + */ + virtual bool start() = 0; + + /** + * @brief Loads into the buffer the saved configuration from the + * storage/memory. + * + * @param buf The buffer where the data will be loaded from the + * storage/memory if any is saved. + * + * @return true if successful, false otherwise. + */ + virtual bool load(std::vector<uint8_t>& buf) = 0; + + /** + * @brief Saves the data in the buf to the storage/memory. + * + * @param buf The buf vector with the data to be saved. + * + * @return true if successful, false otherwise. + */ + virtual bool save(std::vector<uint8_t>& buf) = 0; +}; + +/** + * @brief Dummy no-op backend + */ +class DummyBackend final : public RegistryBackend +{ +public: + bool start() override { return true; } + + bool load(std::vector<uint8_t>& buf) override { return true; } + + bool save(std::vector<uint8_t>& buf) override { return true; } +}; + +} // namespace Boardcore \ No newline at end of file diff --git a/src/shared/utils/Registry/RegistryFrontend.cpp b/src/shared/utils/Registry/RegistryFrontend.cpp new file mode 100644 index 0000000000000000000000000000000000000000..87bed26e89e9006a5b20dd0610a0fa50122bbbfa --- /dev/null +++ b/src/shared/utils/Registry/RegistryFrontend.cpp @@ -0,0 +1,116 @@ +/* Copyright (c) 2023 Skyward Experimental Rocketry + * Author: Nicolò Caruso + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "RegistryFrontend.h" + +namespace Boardcore +{ +RegistryFrontend::RegistryFrontend(std::unique_ptr<RegistryBackend> backend) + : backend{std::move(backend)} +{ + serializationVector.reserve(1024); + configuration.reserve( + 1024 / sizeof(std::pair<ConfigurationId, EntryStructsUnion>)); +} + +RegistryError RegistryFrontend::start() +{ + const std::lock_guard<std::recursive_mutex> lock(mutexForRegistry); + if (!backend->start()) + return RegistryError::BACKEND_START_FAIL; + + return RegistryError::OK; +} + +void RegistryFrontend::arm() +{ + const std::lock_guard<std::recursive_mutex> lock(mutexForRegistry); + isArmed = true; +} + +void RegistryFrontend::disarm() +{ + const std::lock_guard<std::recursive_mutex> lock(mutexForRegistry); + isArmed = false; +} + +void RegistryFrontend::forEach(const EntryFunc& predicate) +{ + const std::lock_guard<std::recursive_mutex> lock(mutexForRegistry); + for (auto& it : configuration) + { + predicate(it.first, it.second); + } +} + +RegistryError RegistryFrontend::load() +{ + const std::lock_guard<std::recursive_mutex> lock(mutexForRegistry); + if (isArmed) + return RegistryError::ARMED; + + if (!backend->load(serializationVector)) + return RegistryError::BACKEND_LOAD_FAIL; + + RegistrySerializer serializer(serializationVector); + return serializer.deserializeConfiguration(configuration); +} + +bool RegistryFrontend::isEntryConfigured( + const ConfigurationId configurationIndex) +{ + const std::lock_guard<std::recursive_mutex> lock(mutexForRegistry); + auto iterator = configuration.find(configurationIndex); + return !(iterator == configuration.end()); +} + +bool RegistryFrontend::isEmpty() +{ + const std::lock_guard<std::recursive_mutex> lock(mutexForRegistry); + return configuration.empty(); +} + +RegistryError RegistryFrontend::save() +{ + const std::lock_guard<std::recursive_mutex> lock(mutexForRegistry); + // In case the registry is armed inhibit the saving + if (isArmed) + return RegistryError::ARMED; + + RegistrySerializer serializer(serializationVector); + RegistryError error = serializer.serializeConfiguration(configuration); + if (error != RegistryError::OK) + return error; + + if (!backend->save(serializationVector)) + return RegistryError::BACKEND_SAVE_FAIL; + + return RegistryError::OK; +} + +void RegistryFrontend::clear() +{ + const std::lock_guard<std::recursive_mutex> lock(mutexForRegistry); + configuration.clear(); +}; + +} // namespace Boardcore \ No newline at end of file diff --git a/src/shared/utils/Registry/RegistryFrontend.h b/src/shared/utils/Registry/RegistryFrontend.h new file mode 100644 index 0000000000000000000000000000000000000000..b1d79c67bb5bb004b610a4b53465788d7e5c633b --- /dev/null +++ b/src/shared/utils/Registry/RegistryFrontend.h @@ -0,0 +1,255 @@ +/* Copyright (c) 2023 Skyward Experimental Rocketry + * Author: Nicolò Caruso + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include <diagnostic/PrintLogger.h> +#include <utils/Debug.h> + +#include <cstdint> +#include <mutex> +#include <unordered_map> +#include <vector> + +#include "RegistryBackend.h" +#include "RegistrySerializer.h" +#include "RegistryTypes.h" + +namespace Boardcore +{ + +/** + * @brief This is the front-end for the registry to store and load the + * configuration. Its methods are type unsafe since the type is determined by + * the entry setted. It does check the data types but its job is mainly the one + * of get and set for the given ConfigurationId, the value of the entry. It also + * exposes methods for go into a "safe" state during armed state / flight. + * Finally there are methods to visit the entire configuration (forEach). + */ +class RegistryFrontend +{ +public: + using EntryFunc = std::function<void(ConfigurationId, EntryStructsUnion&)>; + + /** + * @brief Registry front end constructor. Initializes the configuration of + * the underlying objects and reserves 1KB for the vectors and map data + * structures. + */ + RegistryFrontend(std::unique_ptr<RegistryBackend> backend = + std::make_unique<DummyBackend>()); + + /** + * @brief Start function to start frontend and other objects, such as + * ActiveObjects, needed to write to backend, and the backend itself + */ + [[nodiscard]] RegistryError start(); + + /** + * @brief Disables the memory registry set and allocations. + * To be use when the rocket itself is armed and during flight. + */ + void arm(); + + /** + * @brief Enable set methods and memory allocations. + * + * @note To be used when the rocket is NOT in an "armed" state and while on + * ground. + */ + void disarm(); + + /** + * @brief Executes immediately the predicate for each to the configuration + * applying the callback with the id and EntryStructsUnion union as + * parameter for each configured entry in the configuration. + * + * @param predicate The predicate function to execute for each configuration + * entry + */ + void forEach(const EntryFunc& predicate); + + /** + * @brief Verify if there is an existing entry given its enum entry. + * + * @param configurationIndex The configuration entry ID for which we + * verify the entry is configured. + * @return True if such configuration entry exists in the configuration + * otherwise False. + */ + bool isEntryConfigured(const ConfigurationId configurationIndex); + + /** + * @brief Verify that the configuration is empty or exists some setted + * entries + * + * @return True if the configuration has no entries. False otherwise + */ + bool isEmpty(); + + // TYPE UNSAFE INTERFACE METHODS + + /** + * @brief Gets the value for a given configuration entry. + * + * @note It does change the given variable with the correct value if + * existing + * @tparam T The value data type for such configuration entry + * @param configurationIndex Identifies the configuration entry with its + * enumeration value + * @param outValue the specified configuration entry value + * @return OK in case of successful insertion. + * @return ENTRY_NOT_FOUND In case the entry is not found in the + * configuration + * @return INCORRECT_TYPE If the setted data type not corresponds with the + * given value data type + */ + template <typename T> + RegistryError getUnsafe(const ConfigurationId configurationIndex, + T& outValue) + { + std::lock_guard<std::recursive_mutex> lock(mutexForRegistry); + auto iterator = configuration.find(configurationIndex); + /** Checks that the value type corresponds to the set type and finds + * the entry*/ + if (iterator == configuration.end()) + return RegistryError::ENTRY_NOT_FOUND; + if (!iterator->second.get(outValue)) + return RegistryError::INCORRECT_TYPE; + return RegistryError::OK; + } + + /** + * @brief Gets the value for a specified configuration entry. Otherwise + * returns default and try to set the default value + * + * @tparam T The value data type to be returned and eventually set. + * @param configurationIndex Identifies the configuration entry with its + * enumeration value + * @param defaultValue The default value to be returned and set + * (eventually) in case of non-existing configuration entry + * @return The value saved for the configuration entry in the + * configuration or the default value if there is no such entry in the + * configuration + */ + template <typename T> + T getOrSetDefaultUnsafe(const ConfigurationId configurationIndex, + T defaultValue) + { + std::lock_guard<std::recursive_mutex> lock(mutexForRegistry); + T returnValue; + if (getUnsafe(configurationIndex, returnValue) == RegistryError::OK) + { + return returnValue; + } + if (setUnsafe(configurationIndex, defaultValue) != RegistryError::OK) + LOG_ERR(logger, + "Registry - Could not insert the default configuration"); + return defaultValue; + } + + /** + * @brief Sets the value for the configuration entry with the specified + * enum + * + * @note The set applies only to frontend, the change does not apply to + * backend, save should be triggered manually to do so + * @tparam T The configuration value datatype + * @param configurationIndex The ID of the configuration entry to set + * @param value The value to be set for the specified configuration + * entry + * @return OK if it was possible to set the configurationEntry. + * @return ARMED If the registry is in an armed state, therefore setting a + * configuration is forbidden + */ + template <typename T> + RegistryError setUnsafe(ConfigurationId configurationIndex, T value) + { + std::lock_guard<std::recursive_mutex> lock(mutexForRegistry); + /* In case that the configuration is in an armed state it cannot be + * modified */ + if (isArmed) + return RegistryError::ARMED; + EntryStructsUnion entry = EntryStructsUnion::make(value); + auto insert = configuration.insert({configurationIndex, entry}); + if (!insert.second) + insert.first->second = entry; + return RegistryError::OK; + } + + // LOAD AND SAVE TO BACKEND + + /** + * @brief Loads from the backend the configuration + * + * @note The operation overrides configurations but maintains the ones that + * are not present in the backend configuration + * @return OK if the configuration exists in memory and is not + * corrupted + * @return MALFORMED_SERIALIZED_VECTOR if the vector has a bad format (see + * serializer) + * @return CHECKSUM_FAIL In case the saved Checksum is incorrect (see + * serializer) + * @return NO_SUCH_TYPE In case there are unspecified type ids (see + * serializer) + * @return WRONG_ENDIANESS In case the endianess of serialization not + * corresponds (see serializer) + */ + RegistryError load(); + + /** + * @brief Saves the configuration to the backend + * + * @attention: The save will be inhibited in case of "armed" state in order + * to avoid unwanted allocations to the serializationVector during flight. + * + * @return OK if could save correctly + * @return MALFORMED_SERIALIZED_DATA if the vector not have the + * appropriate length (see serializer) + * @return CHECKSUM_FAIL In case the saved Checksum not corresponds (see + * serializer) + * @return NO_SUCH_TYPE In case there are unspecified type ids (see + * serializer) + */ + RegistryError save(); + + /** + * @brief Clear the configuration actually saved, resetting to empty + * configuration. + * + * @note Does affect the underlying backend. + * + * @attention It does not directly delete the backend saved copy, save + * should be used to trigger such action + */ + void clear(); + +private: + std::recursive_mutex mutexForRegistry; + std::unordered_map<ConfigurationId, EntryStructsUnion> configuration; + bool isArmed = false; + std::vector<uint8_t> serializationVector; + std::unique_ptr<RegistryBackend> backend; + PrintLogger logger = Logging::getLogger("registry-frontend"); +}; + +} // namespace Boardcore diff --git a/src/shared/utils/Registry/RegistrySerializer.cpp b/src/shared/utils/Registry/RegistrySerializer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4cea30426e02659d6a9f3f74cce70107c7a1c3b4 --- /dev/null +++ b/src/shared/utils/Registry/RegistrySerializer.cpp @@ -0,0 +1,232 @@ +/* Copyright (c) 2023 Skyward Experimental Rocketry + * Author: Nicolò Caruso + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "RegistrySerializer.h" + +#include <functional> +#include <numeric> + +namespace Boardcore +{ +RegistrySerializer::RegistrySerializer(std::vector<uint8_t>& vector) + : serializationVector(vector), vectorWritePosition(0) +{ +} + +RegistryError RegistrySerializer::serializeConfiguration( + RegistryConfiguration& configuration) +{ + RegistryError error = RegistryError::OK; + bool success = true; + + // Resizes the serialization vector + serializationVector.resize(size(configuration)); + + // Write the header + RegistryHeader header; + header.startBytes = 1; + header.totalSize = serializationVector.size(); + header.nrEntries = configuration.size(); + error = serialize(header); + if (error != RegistryError::OK) + return RegistryError::NO_SPACE_FOR_HEADER; + + // Add the configuration entries one after the other + for (auto& entry : configuration) + { + TypesEnum type; + // Appends the entry ID + success &= (serialize(entry.first) == RegistryError::OK); + // Appends the configuration entry + type = entry.second.getType(); + success &= (serialize(type) == RegistryError::OK); + if (!success) + return RegistryError::WRONG_WRITES_SIZE; + + switch (entry.second.getType()) + { + case TypesEnum::COORDINATES: + { + Coordinates coordinate{0, 0}; + entry.second.get(coordinate); + success &= (serialize(coordinate) == RegistryError::OK); + break; + } + + case TypesEnum::UINT32: + { + uint32_t uint32Value = 0; + entry.second.get(uint32Value); + success &= (serialize(uint32Value) == RegistryError::OK); + break; + } + + case TypesEnum::FLOAT: + { + float floatValue = 0; + entry.second.get(floatValue); + success &= (serialize(floatValue) == RegistryError::OK); + break; + } + + default: + return RegistryError::NO_SUCH_TYPE; + break; + } + if (!success) + return RegistryError::WRONG_WRITES_SIZE; + } + + // Compute the Footer and write it + RegistryFooter footer; + footer.checksum = computeChecksum(); + + // Add the footer to the serialized data + return serialize(footer); +} + +RegistryError RegistrySerializer::deserializeConfiguration( + RegistryConfiguration& configuration) +{ + bool success = true; + + // Case the vector is empty/not have even the vector size + if (serializationVector.size() < + sizeof(RegistryHeader) + sizeof(RegistryFooter)) + return RegistryError::MALFORMED_SERIALIZED_DATA; + + RegistryHeader header; + success &= (deserialize(header) == RegistryError::OK); + if (!success) + return RegistryError::MALFORMED_SERIALIZED_DATA; + + if (header.startBytes != 1) + { + return RegistryError::WRONG_ENDIANESS; + } + + // Save the current vector position before jumping to the footer + uint32_t previousPos = vectorWritePosition; + vectorWritePosition = header.totalSize - sizeof(RegistryFooter); + RegistryFooter footer; + success &= (deserialize(footer) == RegistryError::OK); + if (!success) + return RegistryError::MALFORMED_SERIALIZED_DATA; + // Restore vector position + vectorWritePosition = previousPos; + + uint32_t savedChecksum = computeChecksum(); + if (footer.checksum != savedChecksum) + return RegistryError::CHECKSUM_FAIL; + + // Set the configuration from the saved configuration + uint32_t counter = 0; + + while (vectorWritePosition < serializationVector.size() - sizeof(footer) && + counter < header.nrEntries) + { + ConfigurationId id = 0; + TypesEnum typeId; + // Gets the ID of the entry, the ID of the data type, the value + success &= (deserialize(id) == RegistryError::OK); + success &= (deserialize(typeId) == RegistryError::OK); + if (!success) + return RegistryError::MALFORMED_SERIALIZED_DATA; + + switch (typeId) + { + case TypesEnum::COORDINATES: + { + Coordinates coordinate{0, 0}; + success &= (deserialize(coordinate) == RegistryError::OK); + if (!success) + return RegistryError::MALFORMED_SERIALIZED_DATA; + EntryStructsUnion entry = EntryStructsUnion::make(coordinate); + auto insert = configuration.insert({id, entry}); + if (!insert.second) + insert.first->second = entry; + break; + } + case TypesEnum::FLOAT: + { + float floatValue = 0; + success &= (deserialize(floatValue) == RegistryError::OK); + if (!success) + return RegistryError::MALFORMED_SERIALIZED_DATA; + EntryStructsUnion entry = EntryStructsUnion::make(floatValue); + auto insert = configuration.insert({id, entry}); + if (!insert.second) + insert.first->second = entry; + break; + } + case TypesEnum::UINT32: + { + uint32_t uint32Value = 0; + success &= (deserialize(uint32Value) == RegistryError::OK); + if (!success) + return RegistryError::MALFORMED_SERIALIZED_DATA; + EntryStructsUnion entry = EntryStructsUnion::make(uint32Value); + auto insert = configuration.insert({id, entry}); + if (!insert.second) + insert.first->second = entry; + break; + } + default: + { + return RegistryError::NO_SUCH_TYPE; + } + } + + counter++; + } + + return RegistryError::OK; +} + +uint32_t RegistrySerializer::computeChecksum() +{ + uint32_t counter = 0; + return std::accumulate( + serializationVector.begin() + sizeof(RegistryHeader), + serializationVector.end() - sizeof(RegistryFooter), 0, + [&counter](uint32_t acc, uint8_t element) + { + acc ^= static_cast<uint32_t>(element >> (3 - (counter % 4)) * 8); + counter++; + return acc; + }); +} + +size_t RegistrySerializer::size(RegistryConfiguration& configuration) +{ + size_t totalSize = sizeof(RegistryHeader) + sizeof(RegistryFooter); + + // Compute the overall space required for the configurations + for (auto& entry : configuration) + { + totalSize += sizeof(entry.first); + totalSize += entry.second.sizeBytes(); + } + return totalSize; +} + +} // namespace Boardcore \ No newline at end of file diff --git a/src/shared/utils/Registry/RegistrySerializer.h b/src/shared/utils/Registry/RegistrySerializer.h new file mode 100644 index 0000000000000000000000000000000000000000..1f1685d000214d0375b87659608abe7acefc2d3d --- /dev/null +++ b/src/shared/utils/Registry/RegistrySerializer.h @@ -0,0 +1,195 @@ +/* Copyright (c) 2023 Skyward Experimental Rocketry + * Author: Nicolò Caruso + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include <utils/Debug.h> + +#include <cstdint> +#include <cstring> +#include <functional> +#include <unordered_map> +#include <vector> + +#include "RegistryTypes.h" + +namespace Boardcore +{ + +using RegistryConfiguration = + std::unordered_map<ConfigurationId, EntryStructsUnion>; + +/** + * @brief Serialization header, with useful information about the serialized + * data. Header to the actually serialized data. + */ +struct RegistryHeader +{ + uint64_t startBytes; ///< Bytes at start, initialized as 1 + uint32_t totalSize; ///< Total size of serialized data in bytes + uint32_t nrEntries; ///< Nr of configuration entries +}; + +/** + * @brief Registry Footer, with checksum of the configuration data (not whole + * data). Placed at the end of the actually serialized data + */ +struct RegistryFooter +{ + uint32_t checksum; +}; + +/** + * @brief Serialization and de-serialization class for the registry. It does + * serialize and deserialize the configuration to the specified vector. + */ +class RegistrySerializer +{ +public: + /** + * @brief Construct a new Registry Serializer object + * + * @param vector The reference to the vector for + * serialization/deserialization procedures + */ + explicit RegistrySerializer(std::vector<uint8_t>& vector); + + /** + * @brief Serializes the configuration map into the uint8_t vector for + * serialized data + * + * @note In case an error is returned no guarantees are made about the + * contents of the vector + * @note The vector is resized if not of the exact size for serialization + * @param configuration The configuration from which we read the + * current entries to be serialized + * @return OK If the de-serialization was successful and the entries where + * added into the map + * @return MALFORMED_SERIALIZED_DATA if the vector not have the + * appropriate length for the header, footer and configuration + * @return CHECKSUM_FAIL In case the saved Checksum not corresponds with the + * one recomputed from the serialized configuration + * @return NO_SUCH_TYPE In case the type id not corresponds to any defined + * data type for the configuration + */ + RegistryError serializeConfiguration(RegistryConfiguration& configuration); + + /** + * @brief De-serializes the data from a serialized vector into the + * configuration map. In case of malformed serialized vectors, does not + * changes the configuration map and returns an error + * + * @param configuration The map in which we want to insert the entries + * from the serialized vector + * @note The deserialization adds/overwrites configuration entries. The + * already present entries, if in the deserialized data, are overriden. + * @note Pre-existing configuration entries are not removed (but might be + * overwritten) + * @return OK If the de-serialization was successful and the entries where + * added into the map + * @return MALFORMED_SERIALIZED_DATA if the vector not have the + * appropriate length for the header, footer and configuration data + * @return CHECKSUM_FAIL In case the saved Checksum not corresponds with the + * one recomputed from the serialized configuration + * @return NO_SUCH_TYPE In case the type id not corresponds to any defined + * data type for the configuration + * @return WRONG_ENDIANESS In case the endianess of the loaded data not + * corresponds + */ + RegistryError deserializeConfiguration( + RegistryConfiguration& configuration); + +private: + std::vector<uint8_t>& serializationVector; + uint32_t vectorWritePosition; + + /** + * @brief Computes a custom checksum of the serialized configuration data + * + * @return uint32_t The computed custom checksum + */ + uint32_t computeChecksum(); + + /** + * + * @brief Deserializes from the vector the data in sequential order and + * inserts/overwrites entries into the configuration. + * + * @tparam T the element data type + * @param element The element we want to get from the serialized vector + * @return OK If the read was successful + * @return MALFORMED_SERIALIZED_DATA Otherwise, in case the vector is not + * long enough to read the element + */ + template <typename T> + RegistryError deserialize(T& element) + { + size_t elSize = sizeof(T); + + if (serializationVector.size() < vectorWritePosition + elSize) + return RegistryError::MALFORMED_SERIALIZED_DATA; + + std::memcpy(&element, serializationVector.data() + vectorWritePosition, + elSize); + + vectorWritePosition += elSize; + return RegistryError::OK; + } + + /** + * @brief serialize functions writes to the vector the elements to + * serialize. It does such task using memcpy and using the positional + * attribute to know the current position where to write and updates such + * attribute. + * + * @tparam T the element data type + * @param element The element to be written in the serialized vector + * @return OK If it could successfully write + * @return WRONG_WRITES_SIZE Otherwise, in case could not write due to not + * enough space in the vector + */ + template <typename T> + RegistryError serialize(T& element) + { + size_t elSize = sizeof(T); + + if (serializationVector.size() < vectorWritePosition + elSize) + return RegistryError::WRONG_WRITES_SIZE; + + std::memcpy(serializationVector.data() + vectorWritePosition, &element, + elSize); + + vectorWritePosition += elSize; + return RegistryError::OK; + } + + /** + * @brief The size function to get the size of the vector that will be + * serialized. It does take into account the header, actual configuration + * and footer. + * + * @return size_t The size that the serialized vector will have after the + * serialization process + */ + size_t size(RegistryConfiguration& configuration); +}; + +} // namespace Boardcore \ No newline at end of file diff --git a/src/shared/utils/Registry/RegistryTypes.h b/src/shared/utils/Registry/RegistryTypes.h new file mode 100644 index 0000000000000000000000000000000000000000..fa05eb9bdebeed21dba5c530ec3efebf7fe40e94 --- /dev/null +++ b/src/shared/utils/Registry/RegistryTypes.h @@ -0,0 +1,230 @@ +/* Copyright (c) 2023 Skyward Experimental Rocketry + * Author: Nicolò Caruso + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include <cstdint> + +namespace Boardcore +{ +/** + * @brief Coordinates struct with latitude [degree], longitude [degree] + */ +struct Coordinates +{ + float latitude; + float longitude; +}; + +// The Configuration ID for the configuration entries +using ConfigurationId = uint32_t; + +// Types used for the union +enum TypesEnum +{ + UINT32, + FLOAT, + COORDINATES +}; + +/** + * @brief Union type used for the underlying saving mechanism for the + * configuration values + */ +union TypeUnion +{ + float float_type; + uint32_t + uint32_type; ///< used for both uint32_t and uint8_t type with upcast + Coordinates coordinates_type; +}; + +static_assert( + sizeof(TypeUnion) == 0x8, + "TypeUnion size has changed, make sure to update code relying on it."); + +/** + * @brief RegistryError enumeration as return type. + */ +enum RegistryError +{ + OK, ///< Correct condition + MALFORMED_SERIALIZED_DATA, ///< Malformed data while deserializing + CHECKSUM_FAIL, ///< The custom checksum check fails + INCORRECT_TYPE, ///< The typeId and value type not correspond + WRONG_WRITES_SIZE, ///< Cannot write due to wrong data size + NO_SPACE_FOR_HEADER, ///< There is not enough space to write the header + NO_SUCH_TYPE, ///< There is no such type in TypeEnum + ARMED, ///< The registry is armed, the operation is not allowed + ENTRY_NOT_FOUND, ///< Not found such entry + WRONG_ENDIANESS, ///< The endianess not corresponds + BACKEND_START_FAIL, //< Backend failed to start + BACKEND_LOAD_FAIL, //< Backend failed to load data + BACKEND_SAVE_FAIL, //< Backend failed to save data +}; + +/** + * @brief Union data struct to be stored in the map. It does contain the + * enumeration index and the value of such configuration entry + */ +struct EntryStructsUnion +{ + + /** + * @brief Default constructor to construct a new Entry Structs Union object + */ + EntryStructsUnion() = default; + + /** + * @brief Returns the size in byte of the type + value + * + * @return size_t The type + value size in Bytes + */ + size_t sizeBytes() + { + size_t returnVal = sizeof(TypesEnum); + switch (type) + { + case TypesEnum::COORDINATES: + returnVal += sizeof(Coordinates); + break; + case TypesEnum::UINT32: + returnVal += sizeof(uint32_t); + break; + case TypesEnum::FLOAT: + returnVal += sizeof(float); + break; + default: + break; + } + + return returnVal; + } + + /** + * @brief Gets from the TypeUnion the float value and returns it. + * + * @param outValue the float value saved into the union type + * @return True if the data type is correct w.r.t. the saved one + */ + bool get(float& outValue) + { + if (type != TypesEnum::FLOAT) + return false; + outValue = value.float_type; + return true; + } + + /** + * @brief Get from Union object the unsigned integer 32b value. + * + * @param outValue the uint32_t value saved into the union type + * @return True if the data type is correct w.r.t. the saved one + */ + bool get(uint32_t& outValue) + { + if (type != TypesEnum::UINT32) + return false; + outValue = value.uint32_type; + return true; + } + + /** + * @brief Get from Union object the coordinates value. + * + * @param outValue the coordinates value saved into the union type + * @return True if the data type is correct w.r.t. the saved one + */ + bool get(Coordinates& outValue) + { + if (type != TypesEnum::COORDINATES) + return false; + outValue = value.coordinates_type; + return true; + } + + /** + * @brief Set the Union object with its float value + * + * @param value The value to be set into the union type + * @return The created EntryStructUnion instance for such value and type + */ + static EntryStructsUnion make(float value) + { + TypeUnion returnValue; + returnValue.float_type = value; + return EntryStructsUnion(returnValue, TypesEnum::FLOAT); + } + + /** + * @brief Set the Union object with its unsigned 32bit integer value + * + * @param value The value to be set into the union type + * @return The created EntryStructUnion instance for such value and type + */ + static EntryStructsUnion make(uint32_t value) + { + TypeUnion returnValue; + returnValue.uint32_type = value; + return EntryStructsUnion(returnValue, TypesEnum::UINT32); + } + + /** + * @brief Set the Union object with its Coordinates value + * + * @param value The value to be set into the union type + * @return The created EntryStructUnion instance for such value and type + */ + static EntryStructsUnion make(Coordinates value) + { + TypeUnion returnValue; + returnValue.coordinates_type = value; + return EntryStructsUnion(returnValue, TypesEnum::COORDINATES); + } + + /** + * @brief Get the Value object + * + * @return TypeUnion the type of this entry + */ + TypeUnion getValue() { return value; } + + /** + * @brief Get the Type object + * + * @return TypeUnion the type of this entry + */ + TypesEnum getType() { return type; } + +private: + TypeUnion value; + TypesEnum type; + /** + * @brief Constructor for the struct. It does take the right configuration + * struct for such entry configuration + */ + EntryStructsUnion(TypeUnion value, TypesEnum typeId) + : value(value), type(typeId) + { + } +}; + +} // namespace Boardcore \ No newline at end of file diff --git a/src/tests/catch/test-registry-frontend.cpp b/src/tests/catch/test-registry-frontend.cpp new file mode 100644 index 0000000000000000000000000000000000000000..aa989018e04395113c26baa2666d53c3ed0e7fea --- /dev/null +++ b/src/tests/catch/test-registry-frontend.cpp @@ -0,0 +1,396 @@ +/* Copyright (c) 2023 Skyward Experimental Rocketry + * Author: Nicolò Caruso + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifdef STANDALONE_CATCH1_TEST +#include "catch-tests-entry.cpp" +#endif +#include <utils/Registry/RegistryFrontend.h> + +#include <catch2/catch.hpp> +#include <cstdint> + +using namespace Boardcore; + +/* Magic numbers for testing the set/get for each data type, they do not + * reflect the reals configuration ids and values in use! */ +static constexpr uint32_t TEST_VALUE_UINT32 = 30; +static constexpr uint32_t TEST_VALUE_UINT32_2 = 32; +static constexpr float TEST_VALUE_FLOAT = 1.45; +static constexpr float TEST_VALUE_LATITUDE = 45.50109; +static constexpr float TEST_VALUE_LONGITUDE = 9.15633; +static constexpr uint32_t COORDINATE_ID = 8; +static constexpr uint32_t DEPLOYMENT_ALTITUDE_ID = 50; +static constexpr uint32_t TARGET_COORDINATES_ID = 1; +static constexpr uint32_t VENTING_VALVE_ATOMIC_TIMING_ID = 49; +static constexpr uint32_t ALGORITHM_ID = 128; +static constexpr uint32_t IGNITION_TIME_ID = 3; +static constexpr uint32_t FLOAT_VALUE_ID = 13; + +/** + * @brief Test function for the forEach frontend function. Tests that the value + * and id are correctly passed to this function + * + * @param entryId The id of the configuration entry + * @param entry The EntryStructsUnion structure of the configuration entry + */ +void visitFunction(ConfigurationId entryId, EntryStructsUnion &entry) +{ + switch (entryId) + { + case FLOAT_VALUE_ID: + float valf; + entry.get(valf); + REQUIRE(valf == TEST_VALUE_FLOAT); + break; + + case ALGORITHM_ID: + uint32_t valu32; + entry.get(valu32); + REQUIRE(valu32 == TEST_VALUE_UINT32); + break; + + case COORDINATE_ID: + Coordinates coordinate; + entry.get(coordinate); + REQUIRE(coordinate.latitude == TEST_VALUE_LATITUDE); + REQUIRE(coordinate.longitude == TEST_VALUE_LONGITUDE); + break; + + default: + REQUIRE(false); + } +}; + +/** + * @brief Fake backend to test the frontend without a real backend class that + * uses memory/storage + */ +class FakeBackend : public RegistryBackend +{ +public: + int &startCount, &saveCount, &loadCount; + + FakeBackend(int &startCount, int &saveCount, int &loadCount) + : startCount(startCount), saveCount(saveCount), loadCount(loadCount) + { + } + + bool start() override + { + startCount++; + return true; + } + + bool load(std::vector<uint8_t> &) override + { + loadCount++; + return true; + } + + bool save(std::vector<uint8_t> &) override + { + saveCount++; + return true; + } +}; + +TEST_CASE( + "RegistryFrontend test - Set, Get, GetOrSetDefault, forEach, isEmpty, ") +{ + RegistryFrontend registry; + float floatValue; + uint32_t uint32Value; + Coordinates coordinatesValue{TEST_VALUE_LATITUDE, TEST_VALUE_LONGITUDE}, + coordinatesGet; + + // IS EMTPY + + /* Check that the registry is first empty and see that isEntryConfigured + * works */ + REQUIRE(registry.isEmpty()); + REQUIRE_FALSE(registry.isEntryConfigured(ALGORITHM_ID)); + + // GET, SET, IS CONFIGURED + + // Checks that there are effectively non-initialized entry configurations + REQUIRE(registry.getUnsafe(static_cast<uint32_t>(DEPLOYMENT_ALTITUDE_ID), + floatValue) != RegistryError::OK); + REQUIRE(registry.getUnsafe(static_cast<uint32_t>(TARGET_COORDINATES_ID), + coordinatesValue) != RegistryError::OK); + REQUIRE(registry.getUnsafe( + static_cast<uint32_t>(VENTING_VALVE_ATOMIC_TIMING_ID), + uint32Value) != RegistryError::OK); + REQUIRE(registry.getUnsafe(static_cast<uint32_t>(ALGORITHM_ID), + uint32Value) != RegistryError::OK); + + // Check set configuration results in right get + REQUIRE(registry.setUnsafe(static_cast<uint32_t>(ALGORITHM_ID), + TEST_VALUE_UINT32) == RegistryError::OK); + REQUIRE(!registry.isEmpty()); + + uint32Value = 0; + REQUIRE(registry.isEntryConfigured(ALGORITHM_ID)); + REQUIRE(registry.getUnsafe(ALGORITHM_ID, uint32Value) == RegistryError::OK); + REQUIRE(uint32Value == TEST_VALUE_UINT32); + uint32Value = 0; + REQUIRE(registry.setUnsafe(COORDINATE_ID, TEST_VALUE_UINT32_2) == + RegistryError::OK); + REQUIRE(registry.getUnsafe(COORDINATE_ID, uint32Value) == + RegistryError::OK); + REQUIRE(uint32Value == TEST_VALUE_UINT32_2); + uint32Value = 0; + REQUIRE(registry.setUnsafe(COORDINATE_ID, TEST_VALUE_UINT32) == + RegistryError::OK); + REQUIRE(registry.getUnsafe(COORDINATE_ID, uint32Value) == + RegistryError::OK); + REQUIRE(uint32Value == TEST_VALUE_UINT32); + + /* Checks that get configuration is false if the type is incorrect w.r.t. + * the type of the set type */ + REQUIRE_FALSE(registry.getUnsafe(COORDINATE_ID, floatValue) == + RegistryError::OK); + + // GET OR SET DEFAULT TEST + uint32Value = + registry.getOrSetDefaultUnsafe(ALGORITHM_ID, TEST_VALUE_UINT32); + REQUIRE(uint32Value == TEST_VALUE_UINT32); + + registry.clear(); + REQUIRE(registry.getUnsafe(ALGORITHM_ID, uint32Value) == + RegistryError::ENTRY_NOT_FOUND); + uint32Value = + registry.getOrSetDefaultUnsafe(ALGORITHM_ID, TEST_VALUE_UINT32); + REQUIRE(uint32Value == TEST_VALUE_UINT32); + REQUIRE(registry.getUnsafe(ALGORITHM_ID, uint32Value) == RegistryError::OK); + REQUIRE(uint32Value == TEST_VALUE_UINT32); + + floatValue = 0; + REQUIRE(registry.getUnsafe(FLOAT_VALUE_ID, floatValue) == + RegistryError::ENTRY_NOT_FOUND); + floatValue = + registry.getOrSetDefaultUnsafe(FLOAT_VALUE_ID, TEST_VALUE_FLOAT); + REQUIRE(floatValue == TEST_VALUE_FLOAT); + REQUIRE(registry.getUnsafe(FLOAT_VALUE_ID, floatValue) == + RegistryError::OK); + REQUIRE(floatValue == TEST_VALUE_FLOAT); + + coordinatesGet = + registry.getOrSetDefaultUnsafe(COORDINATE_ID, coordinatesValue); + REQUIRE(coordinatesGet.latitude == coordinatesValue.latitude); + REQUIRE(coordinatesGet.longitude == coordinatesValue.longitude); + + // VISIT + registry.forEach(visitFunction); +} + +TEST_CASE("RegistryFrontend test - Arm/Disarm test") +{ + RegistryFrontend registry; + uint32_t uint32Value = 0; + Coordinates coordinatesValue{TEST_VALUE_LATITUDE, TEST_VALUE_LONGITUDE}, + coordinateGet; + float floatValue = 0; + + REQUIRE(registry.setUnsafe(COORDINATE_ID, coordinatesValue) == + RegistryError::OK); + registry.arm(); + + // If the registry is "armed" no set are allowed but gets are + REQUIRE(registry.setUnsafe(ALGORITHM_ID, TEST_VALUE_UINT32) != + RegistryError::OK); + REQUIRE(registry.getUnsafe(ALGORITHM_ID, uint32Value) != RegistryError::OK); + REQUIRE(registry.getUnsafe(COORDINATE_ID, coordinateGet) == + RegistryError::OK); + REQUIRE(coordinateGet.latitude == coordinatesValue.latitude); + REQUIRE(coordinateGet.longitude == coordinatesValue.longitude); + floatValue = + registry.getOrSetDefaultUnsafe(FLOAT_VALUE_ID, TEST_VALUE_FLOAT); + REQUIRE(floatValue == TEST_VALUE_FLOAT); + REQUIRE(registry.getUnsafe(FLOAT_VALUE_ID, floatValue) != + RegistryError::OK); + + // DISARM AND SET NEW ENTRIES + + registry.disarm(); + REQUIRE(registry.setUnsafe(ALGORITHM_ID, TEST_VALUE_UINT32) == + RegistryError::OK); + REQUIRE(registry.getUnsafe(ALGORITHM_ID, uint32Value) == RegistryError::OK); + REQUIRE(uint32Value == TEST_VALUE_UINT32); + floatValue = + registry.getOrSetDefaultUnsafe(FLOAT_VALUE_ID, TEST_VALUE_FLOAT); + REQUIRE(floatValue == TEST_VALUE_FLOAT); + REQUIRE(registry.getUnsafe(FLOAT_VALUE_ID, floatValue) == + RegistryError::OK); +} + +TEST_CASE("RegistryFrontend test - serialization/deserialization test") +{ + RegistryFrontend registry; + Coordinates coordinatesValue{TEST_VALUE_LATITUDE, TEST_VALUE_LONGITUDE}, + coordinateGet{0, 0}; + uint32_t valueInt = 0; + float valueFloat = 0; + + registry.clear(); + // FIRST SET OF THE CONFIGURATION + REQUIRE(registry.setUnsafe(COORDINATE_ID, coordinatesValue) == + RegistryError::OK); + REQUIRE(registry.save() == RegistryError::OK); + + // LOAD AND CHECK CONFIGURATION + REQUIRE(registry.save() == RegistryError::OK); + REQUIRE(registry.getUnsafe(COORDINATE_ID, coordinateGet) == + RegistryError::OK); + REQUIRE(registry.load() == RegistryError::OK); + REQUIRE(registry.load() == RegistryError::OK); + REQUIRE(registry.save() == RegistryError::OK); + REQUIRE(registry.load() == RegistryError::OK); + REQUIRE(registry.getUnsafe(COORDINATE_ID, coordinateGet) == + RegistryError::OK); + REQUIRE(coordinateGet.latitude == coordinatesValue.latitude); + REQUIRE(coordinateGet.longitude == coordinatesValue.longitude); + + // SET OTHER DATA TYPES CONFIGURATIONS ENTRIES + + REQUIRE(registry.setUnsafe((VENTING_VALVE_ATOMIC_TIMING_ID), + TEST_VALUE_UINT32) == RegistryError::OK); + valueInt = 0; + REQUIRE(registry.save() == RegistryError::OK); + REQUIRE(registry.getUnsafe(VENTING_VALVE_ATOMIC_TIMING_ID, valueInt) == + RegistryError::OK); + REQUIRE(valueInt == TEST_VALUE_UINT32); + REQUIRE(registry.setUnsafe(FLOAT_VALUE_ID, TEST_VALUE_FLOAT) == + RegistryError::OK); + valueFloat = 0; + + REQUIRE(registry.getUnsafe(VENTING_VALVE_ATOMIC_TIMING_ID, valueInt) == + RegistryError::OK); + REQUIRE(valueInt == TEST_VALUE_UINT32); + REQUIRE(registry.getUnsafe(FLOAT_VALUE_ID, valueFloat) == + RegistryError::OK); + REQUIRE(valueFloat == TEST_VALUE_FLOAT); + + // SAVE AGAIN WITH ALL TYPES INSIDE CONFIGURATION + + REQUIRE(registry.getUnsafe(COORDINATE_ID, coordinateGet) == + RegistryError::OK); + REQUIRE(registry.getUnsafe(COORDINATE_ID, coordinateGet) == + RegistryError::OK); + REQUIRE(registry.save() == RegistryError::OK); + REQUIRE(registry.getUnsafe(COORDINATE_ID, coordinateGet) == + RegistryError::OK); + REQUIRE(registry.save() == RegistryError::OK); + REQUIRE(registry.getUnsafe(COORDINATE_ID, coordinateGet) == + RegistryError::OK); + REQUIRE(registry.load() == RegistryError::OK); + REQUIRE(registry.getUnsafe(VENTING_VALVE_ATOMIC_TIMING_ID, valueInt) == + RegistryError::OK); + REQUIRE(registry.getUnsafe(FLOAT_VALUE_ID, valueFloat) == + RegistryError::OK); + REQUIRE(registry.getUnsafe(COORDINATE_ID, coordinateGet) == + RegistryError::OK); + + registry.save(); + registry.save(); + registry.save(); + registry.save(); + registry.save(); + registry.save(); + registry.load(); + + REQUIRE(registry.getUnsafe(COORDINATE_ID, coordinateGet) == + RegistryError::OK); + + registry.save(); + REQUIRE(registry.load() == RegistryError::OK); + registry.save(); + REQUIRE(registry.load() == RegistryError::OK); + registry.save(); + REQUIRE(registry.load() == RegistryError::OK); + REQUIRE(registry.load() == RegistryError::OK); + REQUIRE(registry.load() == RegistryError::OK); + REQUIRE(registry.load() == RegistryError::OK); + + REQUIRE(registry.getUnsafe(COORDINATE_ID, coordinateGet) == + RegistryError::OK); + REQUIRE(registry.getUnsafe(VENTING_VALVE_ATOMIC_TIMING_ID, valueInt) == + RegistryError::OK); + REQUIRE(registry.getUnsafe(FLOAT_VALUE_ID, valueFloat) == + RegistryError::OK); + REQUIRE(registry.getUnsafe(COORDINATE_ID, coordinateGet) == + RegistryError::OK); + + // CHECK AFTER RELOAD + + coordinateGet.latitude = 0; + coordinateGet.longitude = 0; + REQUIRE(registry.getUnsafe(COORDINATE_ID, coordinateGet) == + RegistryError::OK); + REQUIRE(coordinateGet.latitude == coordinatesValue.latitude); + REQUIRE(coordinateGet.longitude == coordinatesValue.longitude); + + valueInt = 0; + REQUIRE(registry.getUnsafe(VENTING_VALVE_ATOMIC_TIMING_ID, valueInt) == + RegistryError::OK); + REQUIRE(valueInt == TEST_VALUE_UINT32); + + valueFloat = 0; + REQUIRE(registry.getUnsafe(FLOAT_VALUE_ID, valueFloat) == + RegistryError::OK); + REQUIRE(valueFloat == TEST_VALUE_FLOAT); + + // CANCEL CONFIGURATION + + registry.clear(); + REQUIRE(registry.getUnsafe(COORDINATE_ID, coordinateGet) != + RegistryError::OK); + REQUIRE(registry.getUnsafe(VENTING_VALVE_ATOMIC_TIMING_ID, valueInt) != + RegistryError::OK); + REQUIRE(registry.getUnsafe(FLOAT_VALUE_ID, valueFloat) != + RegistryError::OK); +} + +TEST_CASE("RegistryFrontend test - Backend test") +{ + int startCount = 0, saveCount = 0, loadCount = 0; + + RegistryFrontend registry( + std::make_unique<FakeBackend>(startCount, saveCount, loadCount)); + + REQUIRE(startCount == 0); + REQUIRE(saveCount == 0); + REQUIRE(loadCount == 0); + + REQUIRE(registry.start() == RegistryError::OK); + REQUIRE(startCount == 1); + REQUIRE(saveCount == 0); + REQUIRE(loadCount == 0); + + REQUIRE(registry.save() == RegistryError::OK); + REQUIRE(startCount == 1); + REQUIRE(saveCount == 1); + REQUIRE(loadCount == 0); + + REQUIRE(registry.load() == RegistryError::OK); + REQUIRE(startCount == 1); + REQUIRE(saveCount == 1); + REQUIRE(loadCount == 1); +} \ No newline at end of file