diff --git a/CMakeLists.txt b/CMakeLists.txt
index bdc4f924ba4dd5fda3b65643d70be80e52fcb30b..3695cb6d85a68baced219cc0d63dc3ad4d888c7f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -134,6 +134,7 @@ add_executable(catch-tests-boardcore
src/tests/catch/test-sensormanager-catch.cpp
src/tests/catch/xbee/test-xbee-parser.cpp
src/tests/catch/test-modulemanager.cpp
+ src/tests/catch/test-dependencymanager.cpp
src/tests/catch/test-MEA.cpp
src/tests/catch/test-airbrakesInterp.cpp
src/tests/catch/test-pitot.cpp
diff --git a/cmake/boardcore-host.cmake b/cmake/boardcore-host.cmake
index a0a685e46cd1549783fdb6626be877a60b50389b..6f22f2b1fde182ffdb17fb4b6470bc51de995bb8 100644
--- a/cmake/boardcore-host.cmake
+++ b/cmake/boardcore-host.cmake
@@ -57,6 +57,7 @@ set(BOARDCORE_HOST_SRC
${SBS_BASE}/src/shared/utils/TestUtils/TestHelper.cpp
${SBS_BASE}/src/shared/utils/Registry/RegistryFrontend.cpp
${SBS_BASE}/src/shared/utils/Registry/RegistrySerializer.cpp
+ ${SBS_BASE}/src/shared/utils/DependencyManager/DependencyManager.cpp
)
# Create a library specific for host builds
diff --git a/cmake/boardcore.cmake b/cmake/boardcore.cmake
index a95d56bd1dfa7ab560f5d28dcd60cdad9a249053..309394fcfbd8a65727d0878fc11c102e9f6906cc 100644
--- a/cmake/boardcore.cmake
+++ b/cmake/boardcore.cmake
@@ -135,6 +135,7 @@ set(BOARDCORE_SRC
${BOARDCORE_PATH}/src/shared/utils/Registry/RegistryFrontend.cpp
${BOARDCORE_PATH}/src/shared/utils/Registry/RegistrySerializer.cpp
${BOARDCORE_PATH}/src/shared/utils/Registry/Backend/FileBackend.cpp
+ ${BOARDCORE_PATH}/src/shared/utils/DependencyManager/DependencyManager.cpp
)
# Creates the Skyward::Boardcore::${BOARD_NAME} library
diff --git a/src/shared/utils/DependencyManager/DependencyManager.cpp b/src/shared/utils/DependencyManager/DependencyManager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fe4d3357452d98ded20b57d0de23be2c188db6a8
--- /dev/null
+++ b/src/shared/utils/DependencyManager/DependencyManager.cpp
@@ -0,0 +1,115 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Authors: 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.
+ */
+
+#include "DependencyManager.h"
+
+#include <cxxabi.h>
+#include <fmt/format.h>
+
+using namespace Boardcore;
+
+// Simple utility function to demangle type infos
+std::string type_name_demangled(const std::type_info& info)
+{
+ char* demangled =
+ abi::__cxa_demangle(info.name(), nullptr, nullptr, nullptr);
+ std::string demangled2{demangled};
+ std::free(demangled);
+
+ return demangled2;
+}
+
+void DependencyManager::graphviz(std::ostream& os)
+{
+ os << "digraph {" << std::endl;
+
+ for (auto& module : modules)
+ {
+ for (auto& dep : module.second.deps)
+ {
+ os << fmt::format(" \"{}({})\" -> \"{}({})\"", module.second.name,
+ module.second.impl, modules[dep].name,
+ modules[dep].impl)
+ << std::endl;
+ }
+ }
+
+ os << "}" << std::endl;
+}
+
+bool DependencyManager::inject()
+{
+ load_success = true;
+
+ for (auto& module : modules)
+ {
+ LOG_INFO(logger, "Configuring [{}]...", module.second.name);
+ DependencyInjector injector{*this, module.second};
+ module.second.ptr->inject(injector);
+ }
+
+ if (load_success)
+ {
+ LOG_INFO(logger, "Configuring successful!");
+ }
+ else
+ {
+ LOG_ERR(logger, "Failed to inject modules!");
+ }
+
+ return load_success;
+}
+
+bool DependencyManager::insertImpl(Injectable* ptr,
+ const std::type_info& module_info,
+ const std::type_info& impl_info)
+{
+ // Early check to see if ptr is nullptr, fail if that's the case
+ if (ptr == nullptr)
+ return false;
+
+ auto idx = std::type_index{module_info};
+ auto module_name = type_name_demangled(module_info);
+ auto impl_name = type_name_demangled(impl_info);
+
+ return modules.insert({idx, ModuleInfo{ptr, module_name, impl_name, {}}})
+ .second;
+}
+
+Injectable* DependencyInjector::getImpl(const std::type_info& module_info)
+{
+ auto idx = std::type_index{module_info};
+ auto iter = manager.modules.find(idx);
+ if (iter == manager.modules.end())
+ {
+ manager.load_success = false;
+
+ std::string module_name = type_name_demangled(module_info);
+ LOG_ERR(logger, "[{}] requires [{}], but the latter is not present",
+ info.name, module_name);
+
+ return nullptr;
+ }
+
+ info.deps.push_back(idx);
+ return iter->second.ptr;
+}
diff --git a/src/shared/utils/DependencyManager/DependencyManager.h b/src/shared/utils/DependencyManager/DependencyManager.h
new file mode 100644
index 0000000000000000000000000000000000000000..4a1826b2574c4c26c57747f12b5768bc904a6349
--- /dev/null
+++ b/src/shared/utils/DependencyManager/DependencyManager.h
@@ -0,0 +1,276 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Authors: 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 <diagnostic/PrintLogger.h>
+
+#include <ostream>
+#include <string>
+#include <typeindex>
+#include <typeinfo>
+#include <unordered_map>
+#include <vector>
+
+namespace Boardcore
+{
+
+class DependencyInjector;
+class DependencyManager;
+
+/**
+ * @brief Interface for an injectable dependency.
+ */
+class Injectable
+{
+public:
+ virtual ~Injectable() = default;
+
+ /**
+ * @brief Invoked by the DependencyManager to inject dependencies.
+ * Override this method to retrieve dependencies from the injector via
+ * `DependencyInjector::get()`.
+ *
+ * @param injector Proxy class used to obtain dependencies.
+ */
+ virtual void inject(DependencyInjector &injector) {}
+};
+
+/**
+ * @brief Main DependencyManager class.
+ *
+ * This utility is meant to be used as a dependency injector for Skyward OBSW.
+ *
+ * Dependencies and dependents should inherit from `Injectable`. Normally though
+ * you should extend from `InjectableWithDeps` in case of dependencies.
+ *
+ * Here's a quick example (for more examples look at
+ * src/tests/catch/test-dependencymanager.cpp):
+ * @code{.cpp}
+ *
+ * // A simple direct dependency
+ * class MyDependency1 : public Injectable {};
+ *
+ * // Abstracting direct dependencies with a common interface
+ * class MyDependency2Iface {};
+ * class MyDependency2 : public Injectable, public MyDependency2Iface {};
+ *
+ * // A simple dependant (which can become a dependency itself)
+ * class MyDependant : public InjectableWithDeps<MyDependency1,
+ * MyDependency2Iface> {};
+ *
+ * DependencyManager dependency_mgr;
+ *
+ * // Initialize the dependencies
+ * MyDependency1 *dep1 = ;
+ * MyDependency2Iface *dep2 = new MyDependency2();
+ *
+ * dependency_mgr.insert<MyDependency1>(new MyDependency1());
+ * dependency_mgr.insert<MyDependency2Iface>(new MyDependency2());
+ * dependency_mgr.insert<MyDependant>(new MyDependant());
+ *
+ * // Inject and resolve all dependencies!
+ * dependency_mgr.inject();
+ *
+ * // Optionally, print the resulting graph
+ * dependency_mgr.graphviz(std::cout);
+ * @endcode
+ */
+class DependencyManager
+{
+ friend class DependencyInjector;
+
+private:
+ struct ModuleInfo
+ {
+ Injectable *ptr;
+ // Name of the module interface
+ std::string name;
+ // Name of the actual concrete implementation of this module interface
+ std::string impl;
+ std::vector<std::type_index> deps;
+ };
+
+public:
+ DependencyManager() {}
+
+ /**
+ * @brief Insert a new dependency.
+ *
+ * @param dependency Injectable to insert in the DependencyManager.
+ * @returns True if successful, false otherwise.
+ */
+ template <typename T>
+ [[nodiscard]] bool insert(T *dependency)
+ {
+ return insertImpl(dynamic_cast<Injectable *>(dependency), typeid(T),
+ typeid(*dependency));
+ }
+
+ /**
+ * @brief Generate a gaphviz compatible output showing dependencies.
+ * Needs to be called after inject.
+ *
+ * @param os Output stream to write to.
+ */
+ void graphviz(std::ostream &os);
+
+ /**
+ * @brief Inject all dependencies into all inserted .
+ *
+ * @returns True if successful, false otherwise.
+ */
+ [[nodiscard]] bool inject();
+
+private:
+ [[nodiscard]] bool insertImpl(Injectable *ptr,
+ const std::type_info &module_info,
+ const std::type_info &impl_info);
+
+ Boardcore::PrintLogger logger =
+ Boardcore::Logging::getLogger("DependencyManager");
+
+ bool load_success = true;
+ std::unordered_map<std::type_index, ModuleInfo> modules;
+};
+
+/**
+ * @brief Proxy class used to obtain dependencies.
+ */
+class DependencyInjector
+{
+ friend class DependencyManager;
+
+private:
+ DependencyInjector(DependencyManager &manager,
+ DependencyManager::ModuleInfo &info)
+ : manager(manager), info(info)
+ {
+ }
+
+public:
+ /**
+ * @brief Retrieve a specific dependencies, recording it and tracking
+ * unsatisfied dependencies.
+ *
+ * @returns The requested dependency or nullptr if not found.
+ */
+ template <typename T>
+ T *get()
+ {
+ return dynamic_cast<T *>(getImpl(typeid(T)));
+ }
+
+private:
+ Injectable *getImpl(const std::type_info &module_info);
+
+ Boardcore::PrintLogger logger =
+ Boardcore::Logging::getLogger("DependencyManager");
+
+ DependencyManager &manager;
+ DependencyManager::ModuleInfo &info;
+};
+
+namespace DependencyManagerDetails
+{
+
+// Storage implementation
+template <typename... Types>
+struct Storage
+{
+ // No-op, base case
+ void inject(DependencyInjector &injector) {}
+
+ // No-op, dummy get (this should never be reached)
+ template <typename T>
+ T *get()
+ {
+ return nullptr;
+ }
+};
+
+template <typename Type, typename... Types>
+struct Storage<Type, Types...> : public Storage<Types...>
+{
+ using Super = Storage<Types...>;
+
+ // Recursive implementation
+ Type *item = nullptr;
+
+ void inject(DependencyInjector &injector)
+ {
+ item = injector.get<Type>();
+ // Call parent function
+ Super::inject(injector);
+ }
+
+ template <typename T>
+ typename std::enable_if_t<std::is_same<T, Type>::value, T *> get()
+ {
+ return item;
+ }
+
+ template <typename T>
+ typename std::enable_if_t<!std::is_same<T, Type>::value, T *> get()
+ {
+ return Super::template get<T>();
+ }
+};
+
+// Find type in list implementation
+template <typename T, typename... Types>
+struct Contains : std::false_type
+{
+};
+
+template <typename T, typename Type, typename... Types>
+struct Contains<T, Type, Types...>
+ : std::integral_constant<bool, std::is_same<T, Type>::value ||
+ Contains<T, Types...>::value>
+{
+};
+
+} // namespace DependencyManagerDetails
+
+template <typename... Types>
+class InjectableWithDeps : public Injectable
+{
+public:
+ virtual void inject(DependencyInjector &injector) override
+ {
+ storage.inject(injector);
+ }
+
+ template <typename T>
+ T *getModule()
+ {
+ static_assert(DependencyManagerDetails::Contains<T, Types...>::value,
+ "Dependency T is not present in the dependencies");
+
+ return storage.template get<T>();
+ }
+
+private:
+ DependencyManagerDetails::Storage<Types...> storage;
+};
+
+} // namespace Boardcore
diff --git a/src/tests/catch/test-dependencymanager.cpp b/src/tests/catch/test-dependencymanager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..863e87f5123f1529c438a5d2e783b12a60ba7866
--- /dev/null
+++ b/src/tests/catch/test-dependencymanager.cpp
@@ -0,0 +1,165 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: 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.
+ */
+
+#include <utils/DependencyManager/DependencyManager.h>
+
+#include <catch2/catch.hpp>
+
+using namespace Boardcore;
+
+namespace Boardcore
+{
+class B;
+
+class A : public Injectable
+{
+public:
+ A() {}
+
+ void bing_a(bool value) { this->value = value; }
+
+ bool bong_a() { return value; }
+
+ void inject(DependencyInjector &getter) { b = getter.get<B>(); }
+
+private:
+ B *b = nullptr;
+ bool value = false;
+};
+
+class B : public Injectable
+{
+public:
+ B() {}
+
+ void bing_b(bool value) { this->value = value; }
+
+ bool bong_b() { return value; }
+
+ void inject(DependencyInjector &getter) { a = getter.get<A>(); }
+
+private:
+ A *a = nullptr;
+ bool value = false;
+};
+
+class CIface
+{
+public:
+ virtual void bing_c() = 0;
+ virtual bool bong_c() = 0;
+};
+
+class C : public CIface, public InjectableWithDeps<A, B>
+{
+public:
+ void bing_c() override
+ {
+ value = getModule<A>()->bong_a() && getModule<B>()->bong_b();
+ }
+
+ bool bong_c() override { return value; }
+
+private:
+ bool value = false;
+};
+
+class D : public Injectable
+{
+public:
+ void bing_d() { value = c->bong_c(); }
+
+ bool bong_d() { return value; }
+
+ void inject(DependencyInjector &getter) { c = getter.get<CIface>(); }
+
+private:
+ CIface *c = nullptr;
+ bool value = false;
+};
+} // namespace Boardcore
+
+TEST_CASE("DependencyManager - Circular dependencies")
+{
+ DependencyManager manager;
+
+ Boardcore::A *a = new Boardcore::A();
+ Boardcore::B *b = new Boardcore::B();
+
+ REQUIRE(manager.insert<Boardcore::A>(a));
+ REQUIRE(manager.insert<Boardcore::B>(b));
+ REQUIRE(manager.inject());
+
+ a->bing_a(true);
+ REQUIRE(a->bong_a());
+
+ a->bing_a(false);
+ REQUIRE(!a->bong_a());
+
+ b->bing_b(true);
+ REQUIRE(b->bong_b());
+
+ b->bing_b(false);
+ REQUIRE(!b->bong_b());
+}
+
+TEST_CASE("DependencyManager - Virtual Dependencies")
+{
+ DependencyManager manager;
+
+ Boardcore::A *a = new Boardcore::A();
+ Boardcore::B *b = new Boardcore::B();
+ Boardcore::C *c = new Boardcore::C();
+ Boardcore::D *d = new Boardcore::D();
+
+ REQUIRE(manager.insert<Boardcore::A>(a));
+ REQUIRE(manager.insert<Boardcore::B>(b));
+ REQUIRE(manager.insert<Boardcore::CIface>(c));
+ REQUIRE(manager.insert<Boardcore::D>(d));
+ REQUIRE(manager.inject());
+
+ a->bing_a(false);
+ b->bing_b(false);
+
+ c->bing_c();
+ REQUIRE(!c->bong_c());
+ d->bing_d();
+ REQUIRE(!d->bong_d());
+
+ a->bing_a(true);
+ b->bing_b(true);
+
+ c->bing_c();
+ REQUIRE(c->bong_c());
+ d->bing_d();
+ REQUIRE(d->bong_d());
+}
+
+TEST_CASE("DependencyManager - Inject fail")
+{
+ DependencyManager manager;
+
+ Boardcore::A *a = new Boardcore::A();
+
+ REQUIRE(manager.insert<Boardcore::A>(a));
+ REQUIRE_FALSE(manager.inject());
+}