diff --git a/CMakeLists.txt b/CMakeLists.txt index 68b3532122a7fe2659711f896b7c888a45e6bff6..5cd9dd4d171df80edbf0fb5ccd951a6041775814 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,6 +107,7 @@ add_executable(catch-tests-boardcore src/tests/catch/test-packetqueue.cpp src/tests/catch/test-sensormanager-catch.cpp src/tests/catch/xbee/test-xbee-parser.cpp + src/tests/catch/test-modulemanager.cpp ) target_compile_definitions(catch-tests-boardcore PRIVATE USE_MOCK_PERIPHERALS) sbs_target(catch-tests-boardcore stm32f429zi_stm32f4discovery) diff --git a/src/shared/utils/ModuleManager/ModuleManager.hpp b/src/shared/utils/ModuleManager/ModuleManager.hpp new file mode 100644 index 0000000000000000000000000000000000000000..40dc1d859070c99b6d945d0d9d6a56942b093838 --- /dev/null +++ b/src/shared/utils/ModuleManager/ModuleManager.hpp @@ -0,0 +1,222 @@ +/* Copyright (c) 2022 Skyward Experimental Rocketry + * Authors: Matteo Pignataro, 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 <Singleton.h> +#include <assert.h> +#include <stdint.h> +#include <utils/Debug.h> + +#include <array> +#include <atomic> + +namespace Boardcore +{ +class Module +{ +public: + virtual ~Module() = default; +}; + +/** + * @brief The module manager is a singleton object, so it can be instantiated + * only once. It contains all the active software modules which can be + * accessed in a centralized way. + * + * @note Because modules are identified by their type, only one module per type + * can be inserted into the module manager. This means that every module + * conceptually behaves like a singleton. + * + * Example: + * @code{.cpp} + * class SensorsModule : public Module {...}; + * class Sensors : public SensorsModule {...}; + * + * ModuleManager::getInstance().insert<SensorsModule>(new Sensors(args..)); + * + * // The user + * ModuleManager::getInstance().get<SensorsModule>(); + * + * // This way substituting the instance below, the user doesn't actually know + * // the difference as far as the upper interface is respected. + * @endcode + */ +class ModuleManager : public Singleton<ModuleManager> +{ + friend class Singleton<ModuleManager>; + +public: + ModuleManager() {} + + ~ModuleManager() + { + // Delete all the modules to avoid memory leaks + for (size_t i = 0; i < MODULES_NUMBER; i++) + { + // It is okay to have nullptr in delete + delete modules[i]; + } + } + + /** + * @brief Inserts the module inside the array. + * + * @param element Module to be added. T must be subclass of Module. + * + * @returns false in case an object of the same class has already been + * inserted or the maximum number of modules has been reached. + * + * @note Further insertions of modules after the first 'get()' call are not + * allowed. Please notice also that the module manager from this point + * handles completely the objects. Therefore at the destruction of the + * module manager, all the modules will be deleted. + */ + template <typename T> + [[nodiscard]] bool insert(T *element) + { + // Verify that T is a subclass of module + static_assert(std::is_base_of<Module, T>(), + "Class must be subclass of Module"); + static_assert((std::is_same<Module, T>() == false), + "Class must be subclass of Module and not Module itself"); + + if (!insertionAcceptance) + { + assert(false && + "Cannot insert any other module after first get() call"); + return false; + } + + // Take the module type id + size_t id = getId<T>(); + + // This is the case in which the last slot is being occupied, so a + // failure is returned + if (id == MODULES_NUMBER) + { + return false; + } + + // The module is added if only a module of the same subclass hasn't + // already been added + if (modules[id] == nullptr) + { + modules[id] = element; + return true; + } + return false; + } + + /** + * @brief Get the Module object if present. + * @returns T Software module. + * @returns nullptr in case of a non existing software module. + * + * @note After the get call, no further insertion is allowed. + */ + template <class T> + T *get() + { + // Verify that T is a subclass of module but not actually a strict + // Module + static_assert(std::is_base_of<Module, T>(), + "Class must be subclass of Module"); + static_assert((std::is_same<Module, T>() == false), + "Class must be subclass of Module and not Module itself"); + + // Inhibit further insertions + insertionAcceptance = false; + + // Retrieve the module type id + size_t id = getId<T>(); + + // If the module is actually present, returns it by downcasting the + // object. It can be done because at every type, a unique id is assigned + if (modules[id] != nullptr) + { + return static_cast<T *>(modules[id]); + } + + // Fail if the module hasn't been added before + assert(false && "Get of a non previously inserted module"); + return nullptr; + } + +private: + static constexpr size_t MODULES_NUMBER = 256; + + /** @brief Array that contains all the possible modules */ + std::array<Module *, MODULES_NUMBER> modules = {nullptr}; + + /** + * @brief This boolean flag just enables the user to insert software modules + * at the beginning but not after the first get. + * + * @note It enforces the fact that after the first get call no further + * insertions are allowed. + */ + std::atomic<bool> insertionAcceptance{true}; + + /** + * @brief Get the next id with respect to the current one. + * @returns size_t incremented currentID + * + * @note This is not a thread safe function. + */ + size_t getNextId() + { + // Static variable, initialized only the first time + static size_t currentId = 0; + + if (currentId == MODULES_NUMBER) + { + return MODULES_NUMBER; + } + currentId++; + return currentId; + } + + /** + * @brief This function "assigns" to every type a unique sequential id + * based on the already assigned ones. + * + * @returns size_t A unique ID for the type T + * + * @note This is a thread safe function. It leverages on the cxa_guard of + * miosix around a static variable initialization that yields + * every thread that tries to initialize the variable concurrently. So + * getNextId is executed atomically. + * + * Reference of cxa_guard function: + * miosix/stdlib_integration/libstdcpp_integration.cpp + */ + template <typename T> + size_t getId() + { + // This thing works because a new static variable newId is created for + // every type T and the initial assignment is "called" only when the + // static variable is created + static size_t newId = getNextId(); + return newId; + } +}; +} // namespace Boardcore diff --git a/src/tests/catch/test-modulemanager.cpp b/src/tests/catch/test-modulemanager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9b0b96f5cbf9f64d090a4607ee7731bf2e57b94b --- /dev/null +++ b/src/tests/catch/test-modulemanager.cpp @@ -0,0 +1,113 @@ +/* Copyright (c) 2022 Skyward Experimental Rocketry + * Author: Matteo Pignataro + * + * 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 <catch2/catch.hpp> +#include <utils/ModuleManager/ModuleManager.hpp> + +using namespace Boardcore; + +namespace Boardcore +{ +class SensorsModule : public Module +{ +protected: + volatile int dummy = 0; + +public: + virtual int getDummy() { return dummy; }; + virtual void toggleDummy(){}; +}; + +class HILSensors : public SensorsModule +{ +public: + void toggleDummy() override { dummy = dummy == 0 ? 2000 : 0; } +}; + +class Sensors : public SensorsModule +{ +public: + void toggleDummy() override { dummy = dummy == 0 ? 1000 : 0; } +}; + +class Radio : public Module +{ + volatile int dummy = 0; + +public: + void setDummy(int p) { dummy = p; } + int getDummy() { return dummy; } +}; +} // namespace Boardcore + +TEST_CASE("ModuleManager - Insert module") +{ + ModuleManager& modules = ModuleManager::getInstance(); + Sensors* sensors = new Sensors(); + + REQUIRE(modules.insert<SensorsModule>(sensors)); + REQUIRE_FALSE(modules.get<SensorsModule>() == nullptr); + + // Perform a toggle of the module and read according to the expected number + modules.get<SensorsModule>()->toggleDummy(); + REQUIRE(modules.get<SensorsModule>()->getDummy() == 1000); +} + +TEST_CASE("ModuleManager - Insert already existent module") +{ + ModuleManager& modules = ModuleManager::getInstance(); + Sensors* sensors = new Sensors(); + HILSensors* hil = new HILSensors(); + + REQUIRE(modules.insert<SensorsModule>(sensors)); + REQUIRE_FALSE(modules.insert<SensorsModule>(hil)); + + // Verify that the initially inserted module didn't change + modules.get<SensorsModule>()->toggleDummy(); + REQUIRE(modules.get<SensorsModule>()->getDummy() == 1000); +} + +TEST_CASE("ModuleManager - Get without insertion") +{ + ModuleManager& modules = ModuleManager::getInstance(); + + REQUIRE(modules.get<SensorsModule>() == nullptr); +} + +TEST_CASE("ModuleManager - Insertion after get call") +{ + ModuleManager& modules = ModuleManager::getInstance(); + Sensors* sensors = new Sensors(); + Radio* radio = new Radio(); + + REQUIRE(modules.insert<Radio>(radio)); + + // Trigger the inhibition of further insertions + REQUIRE_FALSE(modules.get<Radio>() == nullptr); + REQUIRE_FALSE(modules.insert<Sensors>(sensors)); + + REQUIRE_FALSE(modules.get<Radio>() == nullptr); + + // Verify that the initially inserted module didn't change + modules.get<Radio>()->setDummy(345); + REQUIRE(modules.get<Radio>()->getDummy() == 345); +} \ No newline at end of file