diff --git a/CMakeLists.txt b/CMakeLists.txt
index 53388554c62a16673075b89389685826820750d8..4aa347cdf592935741311b6b54d4d52cb7edd373 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -112,7 +112,9 @@ add_executable(lyra-gs-entry
 target_include_directories(lyra-gs-entry PRIVATE ${OBSW_INCLUDE_DIRS})
 sbs_target(lyra-gs-entry stm32f767zi_lyra_gs)
 
-# Parafoil targets
+#-----------------------------------------------------------------------------#
+#                            Parafoil entrypoints                             #
+#-----------------------------------------------------------------------------#
 
 add_executable(parafoil-progressive-rotation src/Parafoil/parafoil-entry.cpp ${PARAFOIL_COMPUTER})
 target_include_directories(parafoil-progressive-rotation PRIVATE ${OBSW_INCLUDE_DIRS})
@@ -155,3 +157,7 @@ target_compile_definitions(parafoil-t-approach-dynamic PRIVATE
     DYNAMIC_TARGET_LONGITUDE_OFFSET=50
 )
 sbs_target(parafoil-t-approach-dynamic stm32f429zi_death_stack_v2)
+
+add_executable(mockup-main src/MockupMain/mockup-entry.cpp ${MOCKUP_MAIN_COMPUTER})
+target_include_directories(mockup-main PRIVATE ${OBSW_INCLUDE_DIRS})
+sbs_target(mockup-main stm32f429zi_death_stack_v2)
\ No newline at end of file
diff --git a/cmake/dependencies.cmake b/cmake/dependencies.cmake
index 71a0052121fd66e140d2988c9fba3841d73f6e0d..3e21ba5ca01b6a6ec7ad1a552d88a645a452f780 100644
--- a/cmake/dependencies.cmake
+++ b/cmake/dependencies.cmake
@@ -143,4 +143,14 @@ set(PARAFOIL_COMPUTER
     src/Parafoil/Wing/WingAlgorithm.cpp
     src/Parafoil/Wing/Guidance/ClosedLoopGuidanceAlgorithm.cpp
     src/Parafoil/Wing/Guidance/EarlyManeuverGuidanceAlgorithm.cpp
+)
+
+set(MOCKUP_MAIN_COMPUTER 
+    src/MockupMain/PinHandler/PinHandler.cpp
+    src/MockupMain/Radio/Radio.cpp
+    src/MockupMain/Radio/MessageHandler.cpp
+    src/MockupMain/FlightStatsRecorder/FlightStatsRecorder.cpp
+    src/MockupMain/Sensors/Sensors.cpp
+    src/MockupMain/StateMachines/FlightModeManager/FlightModeManager.cpp
+    src/MockupMain/StateMachines/NASController/NASController.cpp
 )
\ No newline at end of file
diff --git a/src/MockupMain/BoardScheduler.h b/src/MockupMain/BoardScheduler.h
new file mode 100644
index 0000000000000000000000000000000000000000..1769434b2c5254281ec7aa3ff57fb2a18d5817fc
--- /dev/null
+++ b/src/MockupMain/BoardScheduler.h
@@ -0,0 +1,120 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 <scheduler/TaskScheduler.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+namespace MockupMain
+{
+
+/**
+ * @brief The class that manages the task schedulers of the board.
+ * It takes care of handing out task schedulers to modules.
+ */
+class BoardScheduler : public Boardcore::Injectable
+{
+public:
+    /**
+     * @brief Enclosing struct to avoid polluting the BoardScheduler namespace.
+     */
+    struct Priority
+    {
+        /**
+         * @brief Priority levels for the board schedulers.
+         */
+        enum PriorityLevel
+        {
+            LOW      = miosix::PRIORITY_MAX - 4,
+            MEDIUM   = miosix::PRIORITY_MAX - 3,
+            HIGH     = miosix::PRIORITY_MAX - 2,
+            CRITICAL = miosix::PRIORITY_MAX - 1,
+        };
+    };
+
+    Boardcore::TaskScheduler& nasController() { return critical; }
+    Boardcore::TaskScheduler& sensors() { return high; }
+    Boardcore::TaskScheduler& pinHandler() { return high; }
+    Boardcore::TaskScheduler& radio() { return medium; }
+
+    static Priority::PriorityLevel flightModeManagerPriority()
+    {
+        return Priority::MEDIUM;
+    }
+
+    static Priority::PriorityLevel nasControllerPriority()
+    {
+        return Priority::MEDIUM;
+    }
+
+    /**
+     * @brief Starts all the schedulers
+     */
+    [[nodiscard]] bool start()
+    {
+        if (!critical.start())
+        {
+            LOG_ERR(logger, "Critical priority scheduler failed to start");
+            return false;
+        }
+
+        if (!high.start())
+        {
+            LOG_ERR(logger, "High priority scheduler failed to start");
+            return false;
+        }
+
+        if (!medium.start())
+        {
+            LOG_ERR(logger, "Medium priority scheduler failed to start");
+            return false;
+        }
+
+        if (!low.start())
+        {
+            LOG_ERR(logger, "Low priority scheduler failed to start");
+            return false;
+        }
+
+        started = true;
+        return true;
+    }
+
+    /**
+     * @brief Returns if all the schedulers are up and running
+     */
+    bool isStarted() { return started; }
+
+private:
+    Boardcore::TaskScheduler critical{Priority::CRITICAL};
+    Boardcore::TaskScheduler high{Priority::HIGH};
+    Boardcore::TaskScheduler medium{Priority::MEDIUM};
+    Boardcore::TaskScheduler low{Priority::LOW};
+
+    std::atomic<bool> started{false};
+
+    Boardcore::PrintLogger logger =
+        Boardcore::Logging::getLogger("BoardScheduler");
+};
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/Buses.h b/src/MockupMain/Buses.h
new file mode 100644
index 0000000000000000000000000000000000000000..26de2b81ec5e67bb61851920e28eae4382822f3a
--- /dev/null
+++ b/src/MockupMain/Buses.h
@@ -0,0 +1,53 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 <drivers/spi/SPIBus.h>
+#include <drivers/usart/USART.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+namespace MockupMain
+{
+
+/**
+ * @brief Manages all the buses of the mockup board.
+ * It provides access to the buses used by the sensors and other peripherals.
+ */
+class Buses : public Boardcore::Injectable
+{
+public:
+    Boardcore::USART usart1;
+    Boardcore::USART usart2;
+    Boardcore::USART usart3;
+    Boardcore::USART uart4;
+
+    Boardcore::SPIBus spi1;
+    Boardcore::SPIBus spi2;
+
+    Buses()
+        : usart1(USART1, 115200), usart2(USART2, 115200),
+          usart3(USART3, 115200), uart4(UART4, 115200), spi1(SPI1), spi2(SPI2)
+    {
+    }
+};
+}  // namespace MockupMain
diff --git a/src/MockupMain/Configs/FlightModeManagerConfig.h b/src/MockupMain/Configs/FlightModeManagerConfig.h
new file mode 100644
index 0000000000000000000000000000000000000000..b41e8c9f2e4479eb760f08fb2f26b93397b1ea0d
--- /dev/null
+++ b/src/MockupMain/Configs/FlightModeManagerConfig.h
@@ -0,0 +1,43 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 <units/Time.h>
+
+#include <chrono>
+
+namespace MockupMain
+{
+namespace Config
+{
+namespace FlightModeManager
+{
+
+/* linter-off */ using namespace Boardcore::Units::Time;
+
+constexpr auto LOGGING_DELAY = 5_s;
+constexpr auto CONTROL_DELAY = 5_s;
+
+}  // namespace FlightModeManager
+}  // namespace Config
+}  // namespace MockupMain
diff --git a/src/MockupMain/Configs/NASConfig.h b/src/MockupMain/Configs/NASConfig.h
new file mode 100644
index 0000000000000000000000000000000000000000..09d9239c792ce4dbc0a4939df0583175b3937b98
--- /dev/null
+++ b/src/MockupMain/Configs/NASConfig.h
@@ -0,0 +1,82 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Authors: Davide Mor, Niccolò Betto, Davide Basso
+ *
+ * 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 <algorithms/NAS/NASConfig.h>
+#include <common/ReferenceConfig.h>
+#include <units/Acceleration.h>
+#include <units/Frequency.h>
+#include <units/Time.h>
+
+namespace MockupMain
+{
+namespace Config
+{
+namespace NAS
+{
+
+/* linter off */ using namespace Boardcore::Units::Time;
+/* linter off */ using namespace Boardcore::Units::Frequency;
+/* linter off */ using namespace Boardcore::Units::Acceleration;
+
+constexpr auto UPDATE_RATE         = 50_hz;
+constexpr auto UPDATE_RATE_SECONDS = 0.02_s;
+
+constexpr int CALIBRATION_SAMPLES_COUNT = 20;
+constexpr auto CALIBRATION_SLEEP_TIME   = 100_ms;
+
+static const Boardcore::NASConfig CONFIG = {
+    .T              = UPDATE_RATE_SECONDS.value(),
+    .SIGMA_BETA     = 0.0001,
+    .SIGMA_W        = 0.0019,
+    .SIGMA_ACC      = 0.202,
+    .SIGMA_MAG      = 0.0047,
+    .SIGMA_GPS      = {0.0447f, 0.0447f, 1.0f / 30.0f, 1.0f / 30.0f},
+    .SIGMA_BAR      = 4.5097f,
+    .SIGMA_POS      = 2.0,
+    .SIGMA_VEL      = 1.0,
+    .SIGMA_PITOT    = 1e-3,
+    .P_POS          = 0.0,
+    .P_POS_VERTICAL = 0.0,
+    .P_VEL          = 0.0,
+    .P_VEL_VERTICAL = 0.0,
+    .P_ATT          = 0.1,
+    .P_BIAS         = 0.01,
+    .SATS_NUM       = 6.0,
+    .NED_MAG        = Common::ReferenceConfig::nedMag};
+
+// Only use one out of every 50 samples (1 Hz)
+constexpr int MAGNETOMETER_DECIMATE = 50;
+
+// Maximum allowed acceleration to correct with GPS
+constexpr auto DISABLE_GPS_ACCELERATION_THRESHOLD = 34.0_mps2;
+
+// How much confidence to apply to the accelerometer to check if it is 1g
+constexpr auto ACCELERATION_1G_CONFIDENCE = 0.5_mps2;
+// How many samples will determine that we are in fact measuring gravity
+// acceleration
+constexpr int ACCELERATION_1G_SAMPLES = 20;
+
+}  // namespace NAS
+}  // namespace Config
+}  // namespace MockupMain
diff --git a/src/MockupMain/Configs/PinHandlerConfig.h b/src/MockupMain/Configs/PinHandlerConfig.h
new file mode 100644
index 0000000000000000000000000000000000000000..3af0c89c204ccfa355827cb8fe39120538e68c0b
--- /dev/null
+++ b/src/MockupMain/Configs/PinHandlerConfig.h
@@ -0,0 +1,51 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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/PinObserver/PinObserver.h>
+
+#include <chrono>
+
+namespace MockupMain
+{
+namespace Config
+{
+namespace PinHandler
+{
+
+/* linter-off */ using namespace std::chrono_literals;
+
+namespace PinObserver
+{
+constexpr auto PERIOD = 20ms;
+}
+
+namespace Expulsion
+{
+constexpr auto DETECTION_THRESHOLD   = 20;
+constexpr auto TRIGGERING_TRANSITION = Boardcore::PinTransition::FALLING_EDGE;
+}  // namespace Expulsion
+
+}  // namespace PinHandler
+}  // namespace Config
+}  // namespace MockupMain
diff --git a/src/MockupMain/Configs/RadioConfig.h b/src/MockupMain/Configs/RadioConfig.h
new file mode 100644
index 0000000000000000000000000000000000000000..28249df624a17a3288817b77517e3fdbae49f1c2
--- /dev/null
+++ b/src/MockupMain/Configs/RadioConfig.h
@@ -0,0 +1,65 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 <common/MavlinkOrion.h>
+#include <units/Frequency.h>
+#include <units/Time.h>
+
+namespace MockupMain
+{
+namespace Config
+{
+namespace Radio
+{
+
+/* linter off */ using namespace Boardcore::Units::Frequency;
+/* linter off */ using namespace Boardcore::Units::Time;
+
+constexpr auto LOW_RATE_TELEMETRY  = 0.5_hz;
+constexpr auto HIGH_RATE_TELEMETRY = 4_hz;
+constexpr auto MESSAGE_QUEUE_SIZE  = 10;
+
+namespace Mavlink
+{
+constexpr uint8_t SYSTEM_ID    = SysIDs::MAV_SYSID_PAYLOAD;
+constexpr uint8_t COMPONENT_ID = 0;
+}  // namespace Mavlink
+
+namespace MavlinkDriver
+{
+constexpr auto PKT_LENGTH       = 255;
+constexpr auto PKT_QUEUE_SIZE   = 20;
+constexpr auto MSG_LENGTH       = MAVLINK_MAX_DIALECT_PAYLOAD_SIZE;
+constexpr auto SLEEP_AFTER_SEND = 0_ms;
+constexpr auto MAX_PKT_AGE      = 200_ms;
+}  // namespace MavlinkDriver
+
+namespace Xbee
+{
+constexpr auto ENABLE_80KPS_DATA_RATE = true;
+constexpr auto TIMEOUT                = 5000_ms;
+}  // namespace Xbee
+
+}  // namespace Radio
+}  // namespace Config
+}  // namespace MockupMain
diff --git a/src/MockupMain/Configs/SensorsConfig.h b/src/MockupMain/Configs/SensorsConfig.h
new file mode 100644
index 0000000000000000000000000000000000000000..cbabea3652ca82cf0eab6235718de6b2f33cd3ca
--- /dev/null
+++ b/src/MockupMain/Configs/SensorsConfig.h
@@ -0,0 +1,145 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 <drivers/adc/InternalADC.h>
+#include <sensors/ADS131M08/ADS131M08Defs.h>
+#include <sensors/BMX160/BMX160Config.h>
+#include <sensors/H3LIS331DL/H3LIS331DL.h>
+#include <sensors/LIS3MDL/LIS3MDL.h>
+#include <sensors/LPS22DF/LPS22DF.h>
+#include <sensors/calibration/AxisOrientation.h>
+#include <units/Frequency.h>
+
+#include <chrono>
+
+namespace MockupMain
+{
+namespace Config
+{
+namespace Sensors
+{
+
+/* linter off */ using namespace Boardcore::Units::Frequency;
+/* linter off */ using namespace std::chrono_literals;
+
+namespace BMX160
+{
+constexpr auto ENABLED       = true;
+constexpr auto SAMPLING_RATE = 50_hz;
+
+constexpr auto CALIBRATION_DURATION = 2000ms;
+constexpr auto ACC_FSR      = Boardcore::BMX160Config::AccelerometerRange::G_16;
+constexpr auto GYRO_FSR     = Boardcore::BMX160Config::GyroscopeRange::DEG_1000;
+constexpr auto ACC_GYRO_ODR = Boardcore::BMX160Config::OutputDataRate::HZ_100;
+constexpr auto MAG_ODR      = Boardcore::BMX160Config::OutputDataRate::HZ_100;
+
+static const Boardcore::AxisOrthoOrientation AXIS_ORIENTATION = {
+    Boardcore::Direction::POSITIVE_Z, Boardcore::Direction::POSITIVE_Y};
+
+constexpr auto TEMP_DIVIDER   = 0;
+constexpr auto FIFO_WATERMARK = 40;
+
+constexpr auto FIFO_HEADER_SIZE = 1;
+constexpr auto ACC_DATA_SIZE    = 6;
+constexpr auto GYRO_DATA_SIZE   = 6;
+constexpr auto MAG_DATA_SIZE    = 8;
+}  // namespace BMX160
+
+namespace H3LIS331DL
+{
+constexpr auto ENABLED       = true;
+constexpr auto SAMPLING_RATE = 800_hz;
+constexpr auto ODR = Boardcore::H3LIS331DLDefs::OutputDataRate::ODR_1000;
+constexpr auto BDU =
+    Boardcore::H3LIS331DLDefs::BlockDataUpdate::BDU_CONTINUOS_UPDATE;
+constexpr auto FSR = Boardcore::H3LIS331DLDefs::FullScaleRange::FS_100;
+}  // namespace H3LIS331DL
+
+namespace LPS22DF
+{
+constexpr auto ENABLED       = true;
+constexpr auto SAMPLING_RATE = 20_hz;
+constexpr auto AVG           = Boardcore::LPS22DF::AVG_4;
+constexpr auto ODR           = Boardcore::LPS22DF::ODR_50;
+}  // namespace LPS22DF
+
+namespace UBXGPS
+{
+constexpr auto ENABLED       = true;
+constexpr auto SAMPLING_RATE = 10_khz;
+}  // namespace UBXGPS
+
+namespace ADS131M08
+{
+constexpr auto ENABLED       = true;
+constexpr auto SAMPLING_RATE = 1600_hz;
+constexpr auto OVERSAMPLING_RATIO =
+    Boardcore::ADS131M08Defs::OversamplingRatio::OSR_512;
+constexpr bool GLOBAL_CHOP_MODE = true;
+}  // namespace ADS131M08
+
+namespace LIS3MDL
+{
+constexpr auto ENABLED       = true;
+constexpr auto SAMPLING_RATE = 100_hz;
+constexpr auto ODR           = Boardcore::LIS3MDL::ODR_155_HZ;
+constexpr auto FSR           = Boardcore::LIS3MDL::FS_4_GAUSS;
+}  // namespace LIS3MDL
+
+namespace InternalADC
+{
+constexpr auto ENABLED       = true;
+constexpr auto SAMPLING_RATE = 100_hz;
+constexpr auto VBAT_CH       = Boardcore::InternalADC::Channel::CH5;
+constexpr auto VBAT_COEFF    = (150 + 40.2) / 40.2;
+}  // namespace InternalADC
+
+namespace LoadCell
+{
+constexpr auto ENABLED       = true;
+constexpr auto SAMPLING_RATE = InternalADC::SAMPLING_RATE;
+constexpr auto ADC_CHANNEL   = Boardcore::ADS131M08Defs::Channel::CHANNEL_0;
+
+namespace Calibration
+{
+constexpr float P0_VOLTAGE = -488.47e-6;
+constexpr float P0_MASS    = 3.272;
+constexpr float P1_VOLTAGE = -2.24e-6;
+constexpr float P1_MASS    = 30.233;
+}  // namespace Calibration
+
+}  // namespace LoadCell
+
+namespace MagCalibration
+{
+constexpr auto FILE_ENABLED     = true;
+constexpr auto SAMPLING_RATE    = 10_hz;
+constexpr auto CALIBRATION_PATH = "/sd/bmx160_magnetometer_calibration.csv";
+}  // namespace MagCalibration
+
+}  // namespace Sensors
+
+}  // namespace Config
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/FlightStatsRecorder/FlightStatsRecorder.cpp b/src/MockupMain/FlightStatsRecorder/FlightStatsRecorder.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d1e8d33c103e250ff97693cd8566d53ad75a21ac
--- /dev/null
+++ b/src/MockupMain/FlightStatsRecorder/FlightStatsRecorder.cpp
@@ -0,0 +1,120 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Authors: Davide Mor, Niccolò Betto, Davide Basso
+ *
+ * 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 "FlightStatsRecorder.h"
+
+#include <MockupMain/Configs/NASConfig.h>
+#include <MockupMain/StateMachines/FlightModeManager/FlightModeManager.h>
+#include <common/ReferenceConfig.h>
+#include <utils/AeroUtils/AeroUtils.h>
+
+using namespace Boardcore;
+using namespace Boardcore::Units::Speed;
+using namespace Boardcore::Units::Length;
+using namespace Boardcore::Units::Pressure;
+using namespace Boardcore::Units::Acceleration;
+using namespace miosix;
+using namespace Eigen;
+using namespace Common;
+
+namespace MockupMain
+{
+
+void FlightStatsRecorder::reset()
+{
+    Lock<FastMutex> lock{statsMutex};
+    stats = Stats{};
+}
+
+FlightStatsRecorder::Stats FlightStatsRecorder::getStats()
+{
+    Lock<FastMutex> lock{statsMutex};
+    return stats;
+}
+
+void FlightStatsRecorder::dropDetected(uint64_t ts)
+{
+    Lock<FastMutex> lock{statsMutex};
+    stats.dropTs = ts;
+}
+
+void FlightStatsRecorder::deploymentDetected(uint64_t ts, Meter alt)
+{
+    Lock<FastMutex> lock{statsMutex};
+    stats.dplTs  = ts;
+    stats.dplAlt = alt;
+}
+
+void FlightStatsRecorder::updateAcc(const AccelerometerData& data)
+{
+    auto fmmState = getModule<FlightModeManager>()->getState();
+
+    // Do nothing if it was not dropped yet
+    if (fmmState != FlightModeManagerState::FLYING_DROGUE_DESCENT)
+        return;
+
+    auto acc = MeterPerSecondSquared{static_cast<Vector3f>(data).norm()};
+    Lock<FastMutex> lock{statsMutex};
+
+    if (fmmState != FlightModeManagerState::FLYING_DROGUE_DESCENT)
+    {
+        // Record this event only after drop, before deployment
+        if (acc > stats.dropMaxAcc)
+        {
+            stats.dropMaxAcc   = acc;
+            stats.dropMaxAccTs = data.accelerationTimestamp;
+        }
+    }
+    else
+    {
+        // Record this event only after deployment
+        if (acc > stats.dplMaxAcc)
+        {
+            stats.dplMaxAcc   = acc;
+            stats.dplMaxAccTs = data.accelerationTimestamp;
+        }
+    }
+}
+
+void FlightStatsRecorder::updateNas(const NASState& data, float refTemperature)
+{
+    auto fmmState = getModule<FlightModeManager>()->getState();
+
+    // Do nothing if it was not dropped yet
+    if (fmmState != FlightModeManagerState::FLYING_DROGUE_DESCENT)
+        return;
+
+    Lock<FastMutex> lock{statsMutex};
+
+    // Record this event only during flight
+    auto speed = MeterPerSecond{Vector3f{data.vn, data.vd, data.ve}.norm()};
+    auto alt   = Meter{-data.d};
+
+    if (speed > stats.maxSpeed)
+    {
+        stats.maxSpeed    = speed;
+        stats.maxSpeedAlt = alt;
+        stats.maxSpeedTs  = data.timestamp;
+    }
+}
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/FlightStatsRecorder/FlightStatsRecorder.h b/src/MockupMain/FlightStatsRecorder/FlightStatsRecorder.h
new file mode 100644
index 0000000000000000000000000000000000000000..b6276ba14c31561b3bd3317cc11eb3ab9e621e1d
--- /dev/null
+++ b/src/MockupMain/FlightStatsRecorder/FlightStatsRecorder.h
@@ -0,0 +1,82 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Authors: Davide Mor, Niccolò Betto, Davide Basso
+ *
+ * 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 <algorithms/NAS/NASState.h>
+#include <miosix.h>
+#include <sensors/SensorData.h>
+#include <units/Acceleration.h>
+#include <units/Length.h>
+#include <units/Pressure.h>
+#include <units/Speed.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+namespace MockupMain
+{
+
+class FlightModeManager;
+
+class FlightStatsRecorder
+    : public Boardcore::InjectableWithDeps<FlightModeManager>
+{
+public:
+    struct Stats
+    {
+        // Drop
+        uint64_t dropTs = 0;
+
+        // Maximum acceleration after drop, before deployment
+        uint64_t dropMaxAccTs = 0;
+        Boardcore::Units::Acceleration::MeterPerSecondSquared dropMaxAcc{0};
+
+        // Maximum vertical speed
+        uint64_t maxSpeedTs = 0;
+        Boardcore::Units::Speed::MeterPerSecond maxSpeed{0};
+        Boardcore::Units::Length::Meter maxSpeedAlt{0};
+
+        // Deployment
+        uint64_t dplTs = 0;
+        Boardcore::Units::Length::Meter dplAlt{0};
+
+        // Maximum acceleration after deployment
+        uint64_t dplMaxAccTs = 0;
+        Boardcore::Units::Acceleration::MeterPerSecondSquared dplMaxAcc{0};
+    };
+
+    void reset();
+
+    Stats getStats();
+
+    void dropDetected(uint64_t ts);
+    void deploymentDetected(uint64_t ts, Boardcore::Units::Length::Meter alt);
+
+    void updateAcc(const Boardcore::AccelerometerData& data);
+    void updateNas(const Boardcore::NASState& data, float refTemperature);
+    void updatePressure(const Boardcore::PressureData& data);
+
+private:
+    miosix::FastMutex statsMutex;
+    Stats stats;
+};
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/PinHandler/PinData.h b/src/MockupMain/PinHandler/PinData.h
new file mode 100644
index 0000000000000000000000000000000000000000..6266bcb574d592a8bf18c497f3016373675f109d
--- /dev/null
+++ b/src/MockupMain/PinHandler/PinData.h
@@ -0,0 +1,50 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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>
+#include <iostream>
+
+namespace MockupMain
+{
+
+struct PinChangeData
+{
+    uint64_t timestamp    = 0;
+    uint8_t pinId         = 0;
+    bool lastState        = false;
+    uint32_t changesCount = 0;
+
+    static std::string header()
+    {
+        return "timestamp,pinId,lastState,changesCount\n";
+    }
+
+    void print(std::ostream& os) const
+    {
+        os << timestamp << "," << static_cast<int>(pinId) << ","
+           << static_cast<int>(lastState) << "," << changesCount << "\n";
+    }
+};
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/PinHandler/PinHandler.cpp b/src/MockupMain/PinHandler/PinHandler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7235935c742ffc1fbcf5c6851230e4c5ccf50a1f
--- /dev/null
+++ b/src/MockupMain/PinHandler/PinHandler.cpp
@@ -0,0 +1,107 @@
+
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 "PinHandler.h"
+
+#include <MockupMain/BoardScheduler.h>
+#include <MockupMain/Configs/PinHandlerConfig.h>
+#include <common/Events.h>
+#include <drivers/timer/TimestampTimer.h>
+#include <events/EventBroker.h>
+#include <interfaces-impl/hwmapping.h>
+
+#include "PinData.h"
+
+using namespace Boardcore;
+using namespace Common;
+namespace config = MockupMain::Config::PinHandler;
+namespace hwmap  = miosix::inputs;
+
+namespace MockupMain
+{
+
+const decltype(PinHandler::PIN_LIST) PinHandler::PIN_LIST = {
+    PinList::NOSECONE_PIN,
+};
+
+bool PinHandler::start()
+{
+    using namespace std::chrono;
+
+    auto& scheduler = getModule<BoardScheduler>()->pinHandler();
+
+    pinObserver = std::make_unique<PinObserver>(
+        scheduler, milliseconds{config::PinObserver::PERIOD}.count());
+
+    bool expulsionPinDetachResult = pinObserver->registerPinCallback(
+        hwmap::expulsion::getPin(),
+        [this](auto t) { onExpulsionPinTransition(t); },
+        config::Expulsion::DETECTION_THRESHOLD);
+
+    if (!expulsionPinDetachResult)
+    {
+        LOG_ERR(logger,
+                "Failed to register pin callback for the detach ramp pin");
+        return false;
+    }
+
+    started = true;
+    return true;
+}
+
+bool PinHandler::isStarted() { return started; }
+
+PinData PinHandler::getPinData(PinList pin)
+{
+    switch (pin)
+    {
+        case PinList::NOSECONE_PIN:
+            return pinObserver->getPinData(hwmap::expulsion::getPin());
+        default:
+            return PinData{};
+    }
+}
+
+void PinHandler::logPin(PinList pin)
+{
+    auto pinData       = getPinData(pin);
+    auto pinChangeData = PinChangeData{
+        .timestamp    = TimestampTimer::getTimestamp(),
+        .pinId        = static_cast<uint8_t>(pin),
+        .lastState    = pinData.lastState,
+        .changesCount = pinData.changesCount,
+    };
+    Logger::getInstance().log(pinChangeData);
+}
+
+void PinHandler::onExpulsionPinTransition(PinTransition transition)
+{
+    logPin(PinList::NOSECONE_PIN);
+    LOG_INFO(logger, "Expulsion Pin transition detected: {}",
+             static_cast<int>(transition));
+
+    if (transition == config::Expulsion::TRIGGERING_TRANSITION)
+        EventBroker::getInstance().post(FLIGHT_NC_DETACHED, TOPIC_FLIGHT);
+}
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/PinHandler/PinHandler.h b/src/MockupMain/PinHandler/PinHandler.h
new file mode 100644
index 0000000000000000000000000000000000000000..5373ac4891aac52bfaef9c5cdc003f979b811e32
--- /dev/null
+++ b/src/MockupMain/PinHandler/PinHandler.h
@@ -0,0 +1,71 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 <common/MavlinkOrion.h>
+#include <diagnostic/PrintLogger.h>
+#include <utils/DependencyManager/DependencyManager.h>
+#include <utils/PinObserver/PinObserver.h>
+
+namespace MockupMain
+{
+class BoardScheduler;
+
+enum class PinList : uint8_t
+{
+    NOSECONE_PIN = 0,
+};
+
+/**
+ * @brief This class contains the handlers for the detach pins on the payload.
+ *
+ * It uses Boardcore's PinObserver to bind these functions to the GPIO pins.
+ * The handlers post an event on the EventBroker.
+ */
+class PinHandler : public Boardcore::InjectableWithDeps<BoardScheduler>
+{
+public:
+    static const std::array<PinList, 1> PIN_LIST;
+
+    [[nodiscard]] bool start();
+
+    bool isStarted();
+
+    /**
+     * @brief Returns information about the specified pin.
+     */
+    Boardcore::PinData getPinData(PinList pin);
+
+private:
+    void logPin(PinList pin);
+
+    void onExpulsionPinTransition(Boardcore::PinTransition transition);
+
+    std::unique_ptr<Boardcore::PinObserver> pinObserver;
+
+    std::atomic<bool> started{false};
+
+    Boardcore::PrintLogger logger = Boardcore::Logging::getLogger("PinHandler");
+};
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/Radio/MessageHandler.cpp b/src/MockupMain/Radio/MessageHandler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4253cdcf46329d938c20757a972f29ce31279bb5
--- /dev/null
+++ b/src/MockupMain/Radio/MessageHandler.cpp
@@ -0,0 +1,839 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 <MockupMain/BoardScheduler.h>
+#include <MockupMain/Configs/RadioConfig.h>
+#include <MockupMain/FlightStatsRecorder/FlightStatsRecorder.h>
+#include <MockupMain/PinHandler/PinHandler.h>
+#include <MockupMain/Sensors/Sensors.h>
+#include <MockupMain/StateMachines/FlightModeManager/FlightModeManager.h>
+#include <MockupMain/StateMachines/NASController/NASController.h>
+#include <common/Events.h>
+#include <diagnostic/CpuMeter/CpuMeter.h>
+#include <events/EventBroker.h>
+
+#include "Radio.h"
+
+using namespace Boardcore;
+using namespace Boardcore::Units::Speed;
+using namespace Boardcore::Units::Length;
+using namespace Boardcore::Units::Acceleration;
+using namespace Common;
+namespace config = MockupMain::Config::Radio;
+
+namespace MockupMain
+{
+
+void Radio::MavlinkBackend::handleMessage(const mavlink_message_t& msg)
+{
+    switch (msg.msgid)
+    {
+        case MAVLINK_MSG_ID_PING_TC:
+        {
+            return enqueueAck(msg);
+        }
+
+        case MAVLINK_MSG_ID_COMMAND_TC:
+        {
+            return handleCommand(msg);
+        }
+
+        case MAVLINK_MSG_ID_SYSTEM_TM_REQUEST_TC:
+        {
+            auto tmId = static_cast<SystemTMList>(
+                mavlink_msg_system_tm_request_tc_get_tm_id(&msg));
+
+            if (!enqueueSystemTm(tmId))
+                return enqueueNack(msg);
+
+            return enqueueAck(msg);
+        }
+
+        case MAVLINK_MSG_ID_SENSOR_TM_REQUEST_TC:
+        {
+            auto sensorId = static_cast<SensorsTMList>(
+                mavlink_msg_sensor_tm_request_tc_get_sensor_name(&msg));
+
+            if (!enqueueSensorsTm(sensorId))
+                return enqueueNack(msg);
+
+            return enqueueAck(msg);
+        }
+
+        case MAVLINK_MSG_ID_SET_REFERENCE_ALTITUDE_TC:
+        {
+            bool allowed = parent.getModule<FlightModeManager>()->isTestMode();
+            if (!allowed)
+                return enqueueNack(msg);
+
+            float altitude =
+                mavlink_msg_set_reference_altitude_tc_get_ref_altitude(&msg);
+
+            parent.getModule<NASController>()->setReferenceAltitude(
+                Meter{altitude});
+            break;
+        }
+
+        case MAVLINK_MSG_ID_SET_REFERENCE_TEMPERATURE_TC:
+        {
+            auto allowed = parent.getModule<FlightModeManager>()->isTestMode();
+            if (!allowed)
+                return enqueueNack(msg);
+
+            float temperature =
+                mavlink_msg_set_reference_temperature_tc_get_ref_temp(&msg);
+
+            parent.getModule<NASController>()->setReferenceTemperature(
+                temperature);
+            break;
+        }
+
+        case MAVLINK_MSG_ID_SET_COORDINATES_TC:
+        {
+            auto allowed = parent.getModule<FlightModeManager>()->isTestMode();
+            if (!allowed)
+                return enqueueNack(msg);
+
+            float latitude = mavlink_msg_set_coordinates_tc_get_latitude(&msg);
+            float longitude =
+                mavlink_msg_set_coordinates_tc_get_longitude(&msg);
+
+            parent.getModule<NASController>()->setReferenceCoordinates(
+                latitude, longitude);
+            break;
+        }
+
+        case MAVLINK_MSG_ID_SET_ORIENTATION_QUAT_TC:
+        {
+            if (parent.getModule<NASController>()->getState() !=
+                NASControllerState::READY)
+            {
+                return enqueueNack(msg);
+            }
+
+            // Scalar first quaternion, W is the first element
+            auto quat = Eigen::Quaternionf{
+                mavlink_msg_set_orientation_quat_tc_get_quat_w(&msg),
+                mavlink_msg_set_orientation_quat_tc_get_quat_x(&msg),
+                mavlink_msg_set_orientation_quat_tc_get_quat_y(&msg),
+                mavlink_msg_set_orientation_quat_tc_get_quat_z(&msg),
+            };
+
+            parent.getModule<NASController>()->setOrientation(
+                quat.normalized());
+
+            float qNorm = quat.norm();
+            if (std::abs(qNorm - 1) > 0.001)
+                return enqueueWack(msg);
+            else
+                return enqueueAck(msg);
+        }
+
+        case MAVLINK_MSG_ID_RAW_EVENT_TC:
+        {
+            uint8_t topicId = mavlink_msg_raw_event_tc_get_topic_id(&msg);
+            uint8_t eventId = mavlink_msg_raw_event_tc_get_event_id(&msg);
+
+            bool testMode = parent.getModule<FlightModeManager>()->isTestMode();
+            // Raw events are allowed in test mode only
+            if (!testMode)
+                return enqueueNack(msg);
+
+            EventBroker::getInstance().post(topicId, eventId);
+            return enqueueAck(msg);
+        }
+
+        default:
+        {
+            return enqueueNack(msg);
+        }
+    }
+}
+
+void Radio::MavlinkBackend::handleCommand(const mavlink_message_t& msg)
+{
+    auto command = static_cast<MavCommandList>(
+        mavlink_msg_command_tc_get_command_id(&msg));
+
+    switch (command)
+    {
+        case MAV_CMD_START_LOGGING:
+        {
+            bool started = Logger::getInstance().start();
+            if (!started)
+                return enqueueNack(msg);
+
+            Logger::getInstance().resetStats();
+            return enqueueAck(msg);
+        }
+
+        case MAV_CMD_STOP_LOGGING:
+        {
+            Logger::getInstance().stop();
+            return enqueueAck(msg);
+        }
+
+        case MAV_CMD_SAVE_CALIBRATION:
+        {
+            bool testMode = parent.getModule<FlightModeManager>()->isTestMode();
+            // Save calibration data in test mode only
+            if (!testMode)
+                return enqueueNack(msg);
+
+            bool magResult = parent.getModule<Sensors>()->saveMagCalibration();
+            if (magResult)
+                return enqueueAck(msg);
+            else
+                return enqueueNack(msg);
+        }
+
+        default:
+        {
+            // Map the command to an event and post it, if it exists
+            auto event = mavCmdToEvent(command);
+            if (event == LAST_EVENT)
+                return enqueueNack(msg);
+
+            EventBroker::getInstance().post(event, TOPIC_TMTC);
+            return enqueueAck(msg);
+        }
+    }
+}
+
+void Radio::MavlinkBackend::enqueueAck(const mavlink_message_t& msg)
+{
+    mavlink_message_t ackMsg;
+    mavlink_msg_ack_tm_pack(config::Mavlink::SYSTEM_ID,
+                            config::Mavlink::COMPONENT_ID, &ackMsg, msg.msgid,
+                            msg.seq);
+    enqueueMessage(ackMsg);
+}
+
+void Radio::MavlinkBackend::enqueueNack(const mavlink_message_t& msg)
+{
+    mavlink_message_t nackMsg;
+    mavlink_msg_nack_tm_pack(config::Mavlink::SYSTEM_ID,
+                             config::Mavlink::COMPONENT_ID, &nackMsg, msg.msgid,
+                             msg.seq, 0);
+    enqueueMessage(nackMsg);
+}
+
+void Radio::MavlinkBackend::enqueueWack(const mavlink_message_t& msg)
+{
+    mavlink_message_t wackMsg;
+    mavlink_msg_wack_tm_pack(config::Mavlink::SYSTEM_ID,
+                             config::Mavlink::COMPONENT_ID, &wackMsg, msg.msgid,
+                             msg.seq, 0);
+    enqueueMessage(wackMsg);
+}
+
+bool Radio::MavlinkBackend::enqueueSystemTm(SystemTMList tmId)
+{
+    switch (tmId)
+    {
+        case MAV_SYS_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_sys_tm_t tm;
+
+            tm.timestamp    = TimestampTimer::getTimestamp();
+            tm.logger       = Logger::getInstance().isStarted();
+            tm.event_broker = EventBroker::getInstance().isRunning();
+            tm.radio        = parent.isStarted();
+            tm.sensors      = parent.getModule<Sensors>()->isStarted();
+            tm.actuators    = false;  // Not present on main mockup
+            tm.pin_handler  = parent.getModule<PinHandler>()->isStarted();
+            tm.scheduler    = parent.getModule<BoardScheduler>()->isStarted();
+            tm.can_handler  = false;  // Not present on main mockup
+
+            mavlink_msg_sys_tm_encode(config::Mavlink::SYSTEM_ID,
+                                      config::Mavlink::COMPONENT_ID, &msg, &tm);
+            enqueueMessage(msg);
+            return true;
+        }
+
+        case MAV_LOGGER_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_logger_tm_t tm;
+
+            // Get the logger stats
+            auto stats = Logger::getInstance().getStats();
+
+            tm.timestamp          = stats.timestamp;
+            tm.log_number         = stats.logNumber;
+            tm.too_large_samples  = stats.tooLargeSamples;
+            tm.dropped_samples    = stats.droppedSamples;
+            tm.queued_samples     = stats.queuedSamples;
+            tm.buffers_filled     = stats.buffersFilled;
+            tm.buffers_written    = stats.buffersWritten;
+            tm.writes_failed      = stats.writesFailed;
+            tm.last_write_error   = stats.lastWriteError;
+            tm.average_write_time = stats.averageWriteTime;
+            tm.max_write_time     = stats.maxWriteTime;
+
+            mavlink_msg_logger_tm_encode(config::Mavlink::SYSTEM_ID,
+                                         config::Mavlink::COMPONENT_ID, &msg,
+                                         &tm);
+            enqueueMessage(msg);
+            return true;
+        }
+
+        case MAV_MAVLINK_STATS_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_mavlink_stats_tm_t tm;
+
+            // Get the mavlink stats
+            auto stats = driver->getStatus();
+
+            tm.timestamp               = stats.timestamp;
+            tm.n_send_queue            = stats.nSendQueue;
+            tm.max_send_queue          = stats.maxSendQueue;
+            tm.n_send_errors           = stats.nSendErrors;
+            tm.msg_received            = stats.mavStats.msg_received;
+            tm.buffer_overrun          = stats.mavStats.buffer_overrun;
+            tm.parse_error             = stats.mavStats.parse_error;
+            tm.parse_state             = stats.mavStats.parse_state;
+            tm.packet_idx              = stats.mavStats.packet_idx;
+            tm.current_rx_seq          = stats.mavStats.current_rx_seq;
+            tm.current_tx_seq          = stats.mavStats.current_tx_seq;
+            tm.packet_rx_success_count = stats.mavStats.packet_rx_success_count;
+            tm.packet_rx_drop_count    = stats.mavStats.packet_rx_drop_count;
+
+            mavlink_msg_mavlink_stats_tm_encode(config::Mavlink::SYSTEM_ID,
+                                                config::Mavlink::COMPONENT_ID,
+                                                &msg, &tm);
+            enqueueMessage(msg);
+            return true;
+        }
+
+        case MAV_NAS_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_nas_tm_t tm;
+
+            auto state    = parent.getModule<NASController>()->getState();
+            auto nasState = parent.getModule<NASController>()->getNasState();
+            auto ref = parent.getModule<NASController>()->getReferenceValues();
+
+            tm.timestamp       = nasState.timestamp;
+            tm.nas_n           = nasState.n;
+            tm.nas_e           = nasState.e;
+            tm.nas_d           = nasState.d;
+            tm.nas_vn          = nasState.vn;
+            tm.nas_ve          = nasState.ve;
+            tm.nas_vd          = nasState.vd;
+            tm.nas_qx          = nasState.qx;
+            tm.nas_qy          = nasState.qy;
+            tm.nas_qz          = nasState.qz;
+            tm.nas_qw          = nasState.qw;
+            tm.nas_bias_x      = nasState.bx;
+            tm.nas_bias_y      = nasState.by;
+            tm.nas_bias_z      = nasState.bz;
+            tm.ref_pressure    = ref.refPressure;
+            tm.ref_temperature = ref.refTemperature;
+            tm.ref_latitude    = ref.refLatitude;
+            tm.ref_longitude   = ref.refLongitude;
+            tm.state           = static_cast<uint8_t>(state);
+
+            mavlink_msg_nas_tm_encode(config::Mavlink::SYSTEM_ID,
+                                      config::Mavlink::COMPONENT_ID, &msg, &tm);
+            enqueueMessage(msg);
+            return true;
+        }
+
+        case MAV_FLIGHT_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_payload_flight_tm_t tm;
+
+            auto* sensors = parent.getModule<Sensors>();
+            auto* nas     = parent.getModule<NASController>();
+            auto* fmm     = parent.getModule<FlightModeManager>();
+
+            auto imu      = sensors->getBMX160WithCorrectionLastSample();
+            auto gps      = sensors->getUBXGPSLastSample();
+            auto lps22df  = sensors->getLPS22DFLastSample();
+            auto nasState = nas->getNasState();
+            // auto ref      = nas->getReferenceValues();
+
+            tm.timestamp = TimestampTimer::getTimestamp();
+            // No digital pressure sensor, use static
+            tm.pressure_digi   = lps22df.pressure;
+            tm.pressure_static = lps22df.pressure;
+            // No dynamic pressure sensor
+            tm.pressure_dynamic = -1.f;
+            // Not present on parafoil board
+            tm.airspeed_pitot = 0;
+            tm.altitude_agl   = -nasState.d;
+
+            // Sensors
+            tm.acc_x   = imu.accelerationX;
+            tm.acc_y   = imu.accelerationY;
+            tm.acc_z   = imu.accelerationZ;
+            tm.gyro_x  = imu.angularSpeedX;
+            tm.gyro_y  = imu.angularSpeedY;
+            tm.gyro_z  = imu.angularSpeedZ;
+            tm.mag_x   = imu.magneticFieldX;
+            tm.mag_y   = imu.magneticFieldY;
+            tm.mag_z   = imu.magneticFieldZ;
+            tm.gps_lat = gps.latitude;
+            tm.gps_lon = gps.longitude;
+            tm.gps_alt = gps.height;
+            tm.gps_fix = gps.fix;
+
+            // Servos
+            tm.left_servo_angle  = -1.f;
+            tm.right_servo_angle = -1.f;
+
+            // Algorithms
+            tm.nas_n      = nasState.n;
+            tm.nas_e      = nasState.e;
+            tm.nas_d      = nasState.d;
+            tm.nas_vn     = nasState.vn;
+            tm.nas_ve     = nasState.ve;
+            tm.nas_vd     = nasState.vd;
+            tm.nas_qx     = nasState.qx;
+            tm.nas_qy     = nasState.qy;
+            tm.nas_qz     = nasState.qz;
+            tm.nas_qw     = nasState.qw;
+            tm.nas_bias_x = nasState.bx;
+            tm.nas_bias_y = nasState.by;
+            tm.nas_bias_z = nasState.bz;
+
+            // Wind estimation, used for Load Cell and ADC data
+
+            tm.wes_n =
+                parent.getModule<Sensors>()->getLoadCellLastSample().load;
+            tm.wes_e =
+                parent.getModule<Sensors>()
+                    ->getADS131LastSample()
+                    .getVoltage(
+                        MockupMain::Config::Sensors::LoadCell::ADC_CHANNEL)
+                    .voltage;
+
+            tm.battery_voltage = sensors->getBatteryVoltage().batVoltage;
+            // No integrated camera
+            tm.cam_battery_voltage = -1.f;
+            tm.temperature         = lps22df.temperature;
+
+            // State machines
+            tm.fmm_state = static_cast<uint8_t>(fmm->getState());
+
+            mavlink_msg_payload_flight_tm_encode(config::Mavlink::SYSTEM_ID,
+                                                 config::Mavlink::COMPONENT_ID,
+                                                 &msg, &tm);
+            enqueueMessage(msg);
+            return true;
+        }
+
+        case MAV_STATS_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_payload_stats_tm_t tm;
+
+            auto* nas        = parent.getModule<NASController>();
+            auto* pinHandler = parent.getModule<PinHandler>();
+            auto* recorder   = parent.getModule<FlightStatsRecorder>();
+            auto& logger     = Logger::getInstance();
+
+            auto stats       = recorder->getStats();
+            auto ref         = nas->getReferenceValues();
+            auto cpuStats    = CpuMeter::getCpuStats();
+            auto loggerStats = logger.getStats();
+
+            // Log CPU stats and reset them
+            CpuMeter::resetCpuStats();
+            logger.log(cpuStats);
+
+            tm.timestamp = TimestampTimer::getTimestamp();
+
+            // No liftoff stats for parafoil, used for drop time
+            tm.liftoff_ts         = stats.dropTs;
+            tm.liftoff_max_acc_ts = stats.dropMaxAccTs;
+            tm.liftoff_max_acc =
+                MeterPerSecondSquared{stats.dropMaxAcc}.value();
+
+            // Max speed stats
+            tm.max_speed_ts       = stats.maxSpeedTs;
+            tm.max_speed          = MeterPerSecond{stats.maxSpeed}.value();
+            tm.max_speed_altitude = Meter{stats.maxSpeedAlt}.value();
+
+            // Useless for parafoil
+            tm.max_mach_ts = 0;
+            tm.max_mach    = -1.f;
+
+            // No apogee stats for parafoil
+            tm.apogee_ts         = -1;
+            tm.apogee_max_acc_ts = 0;
+            tm.apogee_lat        = -1.f;
+            tm.apogee_lon        = -1.f;
+            tm.apogee_alt        = -1.f;
+            tm.apogee_max_acc    = -1.f;
+
+            // Wing stats
+            tm.wing_target_lat      = -1.f;
+            tm.wing_target_lon      = -1.f;
+            tm.wing_active_target_n = -1.f;
+            tm.wing_active_target_e = -1.f;
+            tm.wing_algorithm       = 0;
+
+            // Deployment stats
+            tm.dpl_ts         = stats.dplTs;
+            tm.dpl_max_acc_ts = stats.dplMaxAccTs;
+            tm.dpl_alt        = Meter{stats.dplAlt}.value();
+            tm.dpl_max_acc    = MeterPerSecondSquared{stats.dplMaxAcc}.value();
+
+            // NAS reference values
+            tm.ref_lat = ref.refLatitude;
+            tm.ref_lon = ref.refLongitude;
+            tm.ref_alt = ref.refAltitude;
+
+            tm.min_pressure = -1.f;
+
+            // CPU stats
+            tm.cpu_load  = cpuStats.mean;
+            tm.free_heap = cpuStats.freeHeap;
+
+            // Logger stats
+            tm.log_good   = (loggerStats.lastWriteError == 0);
+            tm.log_number = loggerStats.logNumber;
+
+            tm.nas_state = static_cast<uint8_t>(nas->getState());
+            tm.wnc_state = 0;
+
+            // Pins, some not present on parafoil board
+            tm.pin_nosecone =
+                pinHandler->getPinData(PinList::NOSECONE_PIN).lastState;
+            tm.pin_launch      = 0;
+            tm.cutter_presence = 0;
+            // Can handler, not present on parafoil board
+            tm.main_board_state  = 0;
+            tm.motor_board_state = 0;
+
+            tm.main_can_status  = 0;
+            tm.motor_can_status = 0;
+            tm.rig_can_status   = 0;
+
+            tm.hil_state = 0;
+
+            mavlink_msg_payload_stats_tm_encode(config::Mavlink::SYSTEM_ID,
+                                                config::Mavlink::COMPONENT_ID,
+                                                &msg, &tm);
+            enqueueMessage(msg);
+            return true;
+        }
+
+        case MAV_PIN_OBS_ID:
+        {
+            auto pinHandler = parent.getModule<PinHandler>();
+
+            for (auto pin : PinHandler::PIN_LIST)
+            {
+                mavlink_message_t msg;
+                mavlink_pin_tm_t tm;
+
+                auto pinData = pinHandler->getPinData(pin);
+
+                tm.timestamp             = TimestampTimer::getTimestamp();
+                tm.pin_id                = static_cast<uint8_t>(pin);
+                tm.last_change_timestamp = pinData.lastStateTimestamp;
+                tm.changes_counter       = pinData.changesCount;
+                tm.current_state         = pinData.lastState;
+
+                mavlink_msg_pin_tm_encode(config::Mavlink::SYSTEM_ID,
+                                          config::Mavlink::COMPONENT_ID, &msg,
+                                          &tm);
+                enqueueMessage(msg);
+            }
+
+            return true;
+        }
+
+        case MAV_SENSORS_STATE_ID:
+        {
+            auto sensors = parent.getModule<Sensors>()->getSensorInfo();
+            for (auto sensor : sensors)
+            {
+                mavlink_message_t msg;
+                mavlink_sensor_state_tm_t tm;
+
+                constexpr size_t maxNameLen =
+                    sizeof(tm.sensor_name) / sizeof(tm.sensor_name[0]) - 1;
+
+                std::strncpy(tm.sensor_name, sensor.id.c_str(), maxNameLen);
+                tm.sensor_name[maxNameLen] = '\0';  // Ensure null-termination
+                tm.initialized             = sensor.isInitialized;
+                tm.enabled                 = sensor.isEnabled;
+
+                mavlink_msg_sensor_state_tm_encode(
+                    config::Mavlink::SYSTEM_ID, config::Mavlink::COMPONENT_ID,
+                    &msg, &tm);
+                enqueueMessage(msg);
+            }
+
+            return true;
+        }
+
+        case MAV_REFERENCE_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_reference_tm_t tm;
+
+            auto ref = parent.getModule<NASController>()->getReferenceValues();
+
+            tm.timestamp       = TimestampTimer::getTimestamp();
+            tm.ref_altitude    = ref.refAltitude;
+            tm.ref_pressure    = ref.refPressure;
+            tm.ref_temperature = ref.refTemperature;
+            tm.ref_latitude    = ref.refLatitude;
+            tm.ref_longitude   = ref.refLongitude;
+            tm.msl_pressure    = ref.mslPressure;
+            tm.msl_temperature = ref.mslTemperature;
+
+            mavlink_msg_reference_tm_encode(config::Mavlink::SYSTEM_ID,
+                                            config::Mavlink::COMPONENT_ID, &msg,
+                                            &tm);
+            enqueueMessage(msg);
+            return true;
+        }
+
+        case MAV_CALIBRATION_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_calibration_tm_t tm;
+
+            auto cal = parent.getModule<Sensors>()->getCalibrationData();
+
+            tm.timestamp            = TimestampTimer::getTimestamp();
+            tm.gyro_bias_x          = 0.0f;
+            tm.gyro_bias_y          = 0.0f;
+            tm.gyro_bias_z          = 0.0f;
+            tm.mag_bias_x           = cal.magBiasX;
+            tm.mag_bias_y           = cal.magBiasY;
+            tm.mag_bias_z           = cal.magBiasZ;
+            tm.mag_scale_x          = cal.magScaleX;
+            tm.mag_scale_y          = cal.magScaleY;
+            tm.mag_scale_z          = cal.magScaleZ;
+            tm.static_press_1_bias  = 0.0f;
+            tm.static_press_1_scale = 0.0f;
+            tm.static_press_2_bias  = 0.0f;
+            tm.static_press_2_scale = 0.0f;
+            tm.dpl_bay_press_bias   = 0.0f;
+            tm.dpl_bay_press_scale  = 0.0f;
+            tm.dynamic_press_bias   = 0.0f;
+            tm.dynamic_press_scale  = 0.0f;
+
+            mavlink_msg_calibration_tm_encode(config::Mavlink::SYSTEM_ID,
+                                              config::Mavlink::COMPONENT_ID,
+                                              &msg, &tm);
+            enqueueMessage(msg);
+            return true;
+        }
+
+        default:
+            return false;
+    }
+}
+
+bool Radio::MavlinkBackend::enqueueSensorsTm(SensorsTMList tmId)
+{
+    switch (tmId)
+    {
+        case MAV_GPS_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_gps_tm_t tm;
+
+            auto sample = parent.getModule<Sensors>()->getUBXGPSLastSample();
+
+            tm.fix          = sample.fix;
+            tm.height       = sample.height;
+            tm.latitude     = sample.latitude;
+            tm.longitude    = sample.longitude;
+            tm.n_satellites = sample.satellites;
+            tm.speed        = sample.speed;
+            tm.timestamp    = sample.gpsTimestamp;
+            tm.track        = sample.track;
+            tm.vel_down     = sample.velocityDown;
+            tm.vel_east     = sample.velocityEast;
+            tm.vel_north    = sample.velocityNorth;
+            strcpy(tm.sensor_name, "UBXGPS");
+
+            mavlink_msg_gps_tm_encode(config::Mavlink::SYSTEM_ID,
+                                      config::Mavlink::COMPONENT_ID, &msg, &tm);
+            enqueueMessage(msg);
+            return true;
+        }
+
+        case SensorsTMList::MAV_BMX160_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_imu_tm_t tm;
+
+            auto imuData = parent.getModule<Sensors>()
+                               ->getBMX160WithCorrectionLastSample();
+
+            strcpy(tm.sensor_name, "BMX160");
+            tm.acc_x     = imuData.accelerationX;
+            tm.acc_y     = imuData.accelerationY;
+            tm.acc_z     = imuData.accelerationZ;
+            tm.gyro_x    = imuData.angularSpeedX;
+            tm.gyro_y    = imuData.angularSpeedY;
+            tm.gyro_z    = imuData.angularSpeedZ;
+            tm.mag_x     = imuData.magneticFieldX;
+            tm.mag_y     = imuData.magneticFieldY;
+            tm.mag_z     = imuData.magneticFieldZ;
+            tm.timestamp = imuData.accelerationTimestamp;
+
+            mavlink_msg_imu_tm_encode(config::Mavlink::SYSTEM_ID,
+                                      config::Mavlink::COMPONENT_ID, &msg, &tm);
+
+            return true;
+        }
+
+        case SensorsTMList::MAV_LPS22DF_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_pressure_tm_t tm;
+
+            auto pressureData =
+                parent.getModule<Sensors>()->getLPS22DFLastSample();
+
+            tm.timestamp = pressureData.pressureTimestamp;
+            strcpy(tm.sensor_name, "LPS22");
+            tm.pressure = pressureData.pressure;
+
+            mavlink_msg_pressure_tm_encode(config::Mavlink::SYSTEM_ID,
+                                           config::Mavlink::COMPONENT_ID, &msg,
+                                           &tm);
+
+            return true;
+        }
+
+        case MAV_BATTERY_VOLTAGE_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_voltage_tm_t tm;
+
+            auto data = parent.getModule<Sensors>()->getBatteryVoltage();
+
+            tm.voltage   = data.voltage;
+            tm.timestamp = data.voltageTimestamp;
+            strcpy(tm.sensor_name, "BatteryVoltage");
+
+            mavlink_msg_voltage_tm_encode(config::Mavlink::SYSTEM_ID,
+                                          config::Mavlink::COMPONENT_ID, &msg,
+                                          &tm);
+            enqueueMessage(msg);
+            return true;
+        }
+
+        case MAV_ADS131M08_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_adc_tm_t tm;
+
+            auto sample = parent.getModule<Sensors>()->getADS131LastSample();
+
+            tm.channel_0 =
+                sample.getVoltage(ADS131M08Defs::Channel::CHANNEL_0).voltage;
+            tm.channel_1 =
+                sample.getVoltage(ADS131M08Defs::Channel::CHANNEL_1).voltage;
+            tm.channel_2 =
+                sample.getVoltage(ADS131M08Defs::Channel::CHANNEL_2).voltage;
+            tm.channel_3 =
+                sample.getVoltage(ADS131M08Defs::Channel::CHANNEL_3).voltage;
+            tm.channel_4 =
+                sample.getVoltage(ADS131M08Defs::Channel::CHANNEL_4).voltage;
+            tm.channel_5 =
+                sample.getVoltage(ADS131M08Defs::Channel::CHANNEL_5).voltage;
+            tm.channel_6 =
+                sample.getVoltage(ADS131M08Defs::Channel::CHANNEL_6).voltage;
+            tm.channel_7 =
+                sample.getVoltage(ADS131M08Defs::Channel::CHANNEL_7).voltage;
+            tm.timestamp = sample.timestamp;
+            strcpy(tm.sensor_name, "ADS131M08");
+
+            mavlink_msg_adc_tm_encode(config::Mavlink::SYSTEM_ID,
+                                      config::Mavlink::COMPONENT_ID, &msg, &tm);
+            enqueueMessage(msg);
+            return true;
+        }
+
+        case MAV_LIS3MDL_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_imu_tm_t tm;
+
+            auto sample = parent.getModule<Sensors>()->getLIS3MDLLastSample();
+
+            tm.acc_x     = 0;
+            tm.acc_y     = 0;
+            tm.acc_z     = 0;
+            tm.gyro_x    = 0;
+            tm.gyro_y    = 0;
+            tm.gyro_z    = 0;
+            tm.mag_x     = sample.magneticFieldX;
+            tm.mag_y     = sample.magneticFieldY;
+            tm.mag_z     = sample.magneticFieldZ;
+            tm.timestamp = sample.magneticFieldTimestamp;
+            strcpy(tm.sensor_name, "LIS3MDL");
+
+            mavlink_msg_imu_tm_encode(config::Mavlink::SYSTEM_ID,
+                                      config::Mavlink::COMPONENT_ID, &msg, &tm);
+            enqueueMessage(msg);
+            return true;
+        }
+
+        case MAV_H3LIS331DL_ID:
+        {
+            mavlink_message_t msg;
+            mavlink_imu_tm_t tm;
+
+            auto sample = parent.getModule<Sensors>()->getH3LISLastSample();
+
+            tm.mag_x     = 0;
+            tm.mag_y     = 0;
+            tm.mag_z     = 0;
+            tm.gyro_x    = 0;
+            tm.gyro_y    = 0;
+            tm.gyro_z    = 0;
+            tm.acc_x     = sample.accelerationX;
+            tm.acc_y     = sample.accelerationY;
+            tm.acc_z     = sample.accelerationZ;
+            tm.timestamp = sample.accelerationTimestamp;
+            strcpy(tm.sensor_name, "H3LIS331DL");
+
+            mavlink_msg_imu_tm_encode(config::Mavlink::SYSTEM_ID,
+                                      config::Mavlink::COMPONENT_ID, &msg, &tm);
+            enqueueMessage(msg);
+            return true;
+        }
+
+        default:
+            return false;
+    }
+}
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/Radio/Radio.cpp b/src/MockupMain/Radio/Radio.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..82c68fb9486f17970acbaa75938902f3bd579b30
--- /dev/null
+++ b/src/MockupMain/Radio/Radio.cpp
@@ -0,0 +1,228 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Authors: Niccolò Betto, Davide Basso
+ *
+ * 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 "Radio.h"
+
+#include <MockupMain/BoardScheduler.h>
+#include <MockupMain/Buses.h>
+#include <common/Radio.h>
+#include <radio/Xbee/APIFramesLog.h>
+#include <radio/Xbee/ATCommands.h>
+
+using namespace Boardcore;
+using namespace Boardcore::Units::Time;
+using namespace Common;
+namespace config = MockupMain::Config::Radio;
+
+namespace
+{
+
+// Static radio instance that will handle the radio interrupts
+std::atomic<Xbee::Xbee*> staticTransceiver{nullptr};
+inline void handleDioIRQ()
+{
+    auto transceiver = staticTransceiver.load();
+    if (transceiver)
+        transceiver->handleATTNInterrupt();
+}
+
+}  // namespace
+
+void __attribute__((used)) EXTI1_IRQHandlerImpl() { handleDioIRQ(); }
+
+namespace MockupMain
+{
+
+Radio::~Radio()
+{
+    auto transceiverPtr = transceiver.get();
+    staticTransceiver.compare_exchange_strong(transceiverPtr, nullptr);
+}
+
+bool Radio::start()
+{
+    auto& scheduler = getModule<BoardScheduler>()->radio();
+
+    SPIBusConfig config{};
+    config.clockDivider = SPI::ClockDivider::DIV_16;
+
+    transceiver = std::make_unique<Xbee::Xbee>(
+        getModule<Buses>()->spi2, config, miosix::xbee::cs::getPin(),
+        miosix::xbee::attn::getPin(), miosix::xbee::reset::getPin());
+    transceiver->setOnFrameReceivedListener([this](Xbee::APIFrame& frame)
+                                            { handleXbeeFrame(frame); });
+
+    Xbee::setDataRate(*transceiver, Config::Radio::Xbee::ENABLE_80KPS_DATA_RATE,
+                      Millisecond{Config::Radio::Xbee::TIMEOUT}.value());
+
+    // Set the static instance for handling radio interrupts
+    staticTransceiver = transceiver.get();
+
+    // Initialize the Mavlink driver
+    radioMavlink.driver = std::make_unique<MavDriver>(
+        transceiver.get(), [this](MavDriver*, const mavlink_message_t& msg)
+        { handleRadioMessage(msg); },
+        Millisecond{config::MavlinkDriver::SLEEP_AFTER_SEND}.value(),
+        Millisecond{config::MavlinkDriver::MAX_PKT_AGE}.value());
+
+    if (!radioMavlink.driver->start())
+    {
+        LOG_ERR(logger, "Failed to initialize the Mavlink driver");
+        return false;
+    }
+
+    // Add the high rate telemetry task
+    auto highRateTask =
+        scheduler.addTask([this]() { enqueueHighRateTelemetry(); },
+                          Config::Radio::HIGH_RATE_TELEMETRY);
+
+    if (highRateTask == 0)
+    {
+        LOG_ERR(logger, "Failed to add the high rate telemetry task");
+        return false;
+    }
+
+    auto lowRateTask =
+        scheduler.addTask([this]() { enqueueLowRateTelemetry(); },
+                          Config::Radio::LOW_RATE_TELEMETRY);
+
+    if (lowRateTask == 0)
+    {
+        LOG_ERR(logger, "Failed to add the low rate telemetry task");
+        return false;
+    }
+
+    started = true;
+    return true;
+}
+
+bool Radio::isStarted() { return started; }
+
+void Radio::handleXbeeFrame(Boardcore::Xbee::APIFrame& frame)
+{
+    using namespace Xbee;
+    bool logged = false;
+    switch (frame.frameType)
+    {
+        case FTYPE_AT_COMMAND:
+        {
+            ATCommandFrameLog dest;
+            logged = ATCommandFrameLog::toFrameType(frame, &dest);
+            if (logged)
+                Logger::getInstance().log(dest);
+            break;
+        }
+        case FTYPE_AT_COMMAND_RESPONSE:
+        {
+            ATCommandResponseFrameLog dest;
+            logged = ATCommandResponseFrameLog::toFrameType(frame, &dest);
+            if (logged)
+                Logger::getInstance().log(dest);
+            break;
+        }
+        case FTYPE_MODEM_STATUS:
+        {
+            ModemStatusFrameLog dest;
+            logged = ModemStatusFrameLog::toFrameType(frame, &dest);
+            if (logged)
+                Logger::getInstance().log(dest);
+            break;
+        }
+        case FTYPE_TX_REQUEST:
+        {
+            TXRequestFrameLog dest;
+            logged = TXRequestFrameLog::toFrameType(frame, &dest);
+            if (logged)
+                Logger::getInstance().log(dest);
+            break;
+        }
+        case FTYPE_TX_STATUS:
+        {
+            TXStatusFrameLog dest;
+            logged = TXStatusFrameLog::toFrameType(frame, &dest);
+            if (logged)
+                Logger::getInstance().log(dest);
+            break;
+        }
+        case FTYPE_RX_PACKET_FRAME:
+        {
+            RXPacketFrameLog dest;
+            logged = RXPacketFrameLog::toFrameType(frame, &dest);
+            if (logged)
+                Logger::getInstance().log(dest);
+            break;
+        }
+    }
+
+    if (!logged)
+    {
+        APIFrameLog api;
+        APIFrameLog::fromAPIFrame(frame, &api);
+        Logger::getInstance().log(api);
+    }
+}
+
+void Radio::handleRadioMessage(const mavlink_message_t& msg)
+{
+    radioMavlink.handleMessage(msg);
+}
+
+void Radio::enqueueHighRateTelemetry()
+{
+    radioMavlink.enqueueSystemTm(MAV_FLIGHT_ID);
+    radioMavlink.flushQueue();
+}
+
+void Radio::enqueueLowRateTelemetry()
+{
+    radioMavlink.enqueueSystemTm(MAV_STATS_ID);
+}
+
+void Radio::MavlinkBackend::enqueueMessage(const mavlink_message_t& msg)
+{
+    miosix::Lock<miosix::FastMutex> lock(mutex);
+
+    // Insert the message inside the queue only if there is enough space
+    if (index < queue.size())
+    {
+        queue[index] = msg;
+        index++;
+    }
+}
+
+void Radio::MavlinkBackend::flushQueue()
+{
+    Lock<FastMutex> lock(mutex);
+
+    for (uint32_t i = 0; i < index; i++)
+        driver->enqueueMsg(queue[i]);
+
+    // Reset the index
+    index = 0;
+}
+
+void Radio::MavlinkBackend::logStatus()
+{
+    Logger::getInstance().log(driver->getStatus());
+}
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/Radio/Radio.h b/src/MockupMain/Radio/Radio.h
new file mode 100644
index 0000000000000000000000000000000000000000..cf7034436387a8d1cef0cd1c2bbb48352d4273c3
--- /dev/null
+++ b/src/MockupMain/Radio/Radio.h
@@ -0,0 +1,119 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Authors: Niccolò Betto, Davide Basso
+ *
+ * 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 <MockupMain/Configs/RadioConfig.h>
+#include <common/MavlinkOrion.h>
+#include <radio/MavlinkDriver/MavlinkDriver.h>
+#include <radio/SerialTransceiver/SerialTransceiver.h>
+#include <radio/Xbee/Xbee.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+namespace MockupMain
+{
+using MavDriver =
+    Boardcore::MavlinkDriver<Config::Radio::MavlinkDriver::PKT_LENGTH,
+                             Config::Radio::MavlinkDriver::PKT_QUEUE_SIZE,
+                             Config::Radio::MavlinkDriver::MSG_LENGTH>;
+
+class BoardScheduler;
+class Sensors;
+class Buses;
+class FlightModeManager;
+class NASController;
+class PinHandler;
+class FlightStatsRecorder;
+
+class Radio
+    : public Boardcore::InjectableWithDeps<BoardScheduler, Sensors, Buses,
+                                           FlightModeManager, NASController,
+                                           PinHandler, FlightStatsRecorder>
+{
+public:
+    /**
+     * @brief Unsets the static instance for handling radio interrupts, if the
+     * current one was set by this radio instance.
+     */
+    ~Radio();
+
+    /**
+     * @brief Initializes the radio and Mavlink driver, and sets the static
+     * instance for handling radio interrupts.
+     */
+    [[nodiscard]] bool start();
+
+    bool isStarted();
+
+private:
+    struct MavlinkBackend
+    {
+        Radio& parent;  // Reference to the parent Radio instance
+
+        std::unique_ptr<MavDriver> driver;
+
+        std::array<mavlink_message_t, Config::Radio::MESSAGE_QUEUE_SIZE> queue;
+        size_t index = 0;
+        miosix::FastMutex mutex;
+
+        void handleMessage(const mavlink_message_t& msg);
+        void handleCommand(const mavlink_message_t& msg);
+
+        void enqueueAck(const mavlink_message_t& msg);
+        void enqueueNack(const mavlink_message_t& msg);
+        void enqueueWack(const mavlink_message_t& msg);
+
+        bool enqueueSystemTm(SystemTMList tmId);
+        bool enqueueSensorsTm(SensorsTMList sensorId);
+
+        /**
+         * @brief Enqueues a message in the message queue.
+         */
+        void enqueueMessage(const mavlink_message_t& msg);
+
+        /**
+         * @brief Flushes the message queue to the driver.
+         */
+        void flushQueue();
+
+        /**
+         * @brief Logs the status of MavlinkDriver and the transceiver
+         */
+        void logStatus();
+    };
+
+    void handleXbeeFrame(Boardcore::Xbee::APIFrame& frame);
+
+    void handleRadioMessage(const mavlink_message_t& msg);
+
+    void enqueueHighRateTelemetry();
+    void enqueueLowRateTelemetry();
+
+    std::unique_ptr<Boardcore::Xbee::Xbee> transceiver;
+    MavlinkBackend radioMavlink{.parent = *this};
+
+    std::atomic<bool> started{false};
+
+    Boardcore::PrintLogger logger = Boardcore::Logging::getLogger("Radio");
+};
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/Sensors/AnalogLoadCellSensor.h b/src/MockupMain/Sensors/AnalogLoadCellSensor.h
new file mode 100644
index 0000000000000000000000000000000000000000..3aa25b97f03a7bb7d759518397c70e9e56a4d43c
--- /dev/null
+++ b/src/MockupMain/Sensors/AnalogLoadCellSensor.h
@@ -0,0 +1,74 @@
+/* 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 <sensors/Sensor.h>
+#include <sensors/SensorData.h>
+
+#include <functional>
+
+// The class was copied from rig-dev, commit 5c9464e5
+
+namespace Boardcore
+{
+
+class AnalogLoadCellSensor : public Boardcore::Sensor<Boardcore::LoadCellData>
+{
+public:
+    AnalogLoadCellSensor(std::function<Boardcore::ADCData()> getVoltage,
+                         float p0Voltage, float p0Mass, float p1Voltage,
+                         float p1Mass)
+        : getVoltage{getVoltage}, p0Voltage{p0Voltage}, p0Mass{p0Mass},
+          p1Voltage{p1Voltage}, p1Mass{p1Mass}
+    {
+    }
+
+    bool init() override { return true; }
+
+    bool selfTest() override { return true; }
+
+    float voltageToMass(float voltage)
+    {
+        // Two point calibration
+        // m = dmass/dvoltage
+        float scale  = (p1Mass - p0Mass) / (p1Voltage - p0Voltage);
+        float offset = p0Mass - scale * p0Voltage;  // Calculate offset
+        return scale * voltage + offset;
+    }
+
+private:
+    Boardcore::LoadCellData sampleImpl() override
+    {
+        auto voltage = getVoltage();
+        return {voltage.voltageTimestamp, -voltageToMass(voltage.voltage)};
+    }
+
+    std::function<Boardcore::ADCData()> getVoltage;
+
+    float p0Voltage;
+    float p0Mass;
+    float p1Voltage;
+    float p1Mass;
+};
+
+}  // namespace Boardcore
diff --git a/src/MockupMain/Sensors/SensorData.h b/src/MockupMain/Sensors/SensorData.h
new file mode 100644
index 0000000000000000000000000000000000000000..650609a8cef58dc42368198614d03926048c7aee
--- /dev/null
+++ b/src/MockupMain/Sensors/SensorData.h
@@ -0,0 +1,53 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 <sensors/SensorData.h>
+
+namespace MockupMain
+{
+
+struct SensorCalibrationData
+{
+    uint64_t timestamp = 0;
+    float magBiasX     = 0.0f;
+    float magBiasY     = 0.0f;
+    float magBiasZ     = 0.0f;
+    float magScaleX    = 0.0f;
+    float magScaleY    = 0.0f;
+    float magScaleZ    = 0.0f;
+
+    static std::string header()
+    {
+        return "timestamp,magBiasX,magBiasY,magBiasZ,magScaleX,magScaleY,"
+               "magScaleZ\n";
+    }
+
+    void print(std::ostream& os) const
+    {
+        os << timestamp << "," << magBiasX << "," << magBiasY << "," << magBiasZ
+           << "," << magScaleX << "," << magScaleY << "," << magScaleZ << "\n";
+    }
+};
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/Sensors/Sensors.cpp b/src/MockupMain/Sensors/Sensors.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f74e2d34caf96dab180a409dbe2187dddb3f07d6
--- /dev/null
+++ b/src/MockupMain/Sensors/Sensors.cpp
@@ -0,0 +1,530 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 "Sensors.h"
+
+#include <MockupMain/BoardScheduler.h>
+#include <MockupMain/Buses.h>
+#include <MockupMain/FlightStatsRecorder/FlightStatsRecorder.h>
+
+#include <chrono>
+
+using namespace Boardcore;
+using namespace Boardcore::Units::Frequency;
+namespace config = MockupMain::Config::Sensors;
+namespace hwmap  = miosix::sensors;
+
+namespace MockupMain
+{
+
+bool Sensors::start()
+{
+    if (config::BMX160::ENABLED)
+    {
+        bmx160Init();
+        bmx160WithCorrectionInit();
+    }
+
+    if (config::LIS3MDL::ENABLED)
+        lis3mdlInit();
+
+    if (config::H3LIS331DL::ENABLED)
+        h3lisInit();
+
+    if (config::LPS22DF::ENABLED)
+        lps22Init();
+
+    if (config::UBXGPS::ENABLED)
+        ubxGpsInit();
+
+    if (config::ADS131M08::ENABLED)
+        ads131Init();
+
+    if (config::InternalADC::ENABLED)
+        internalADCInit();
+
+    if (!postSensorCreationHook())
+    {
+        LOG_ERR(logger, "Sensors post-creation hook failed");
+        return false;
+    }
+
+    if (!sensorManagerInit())
+    {
+        LOG_ERR(logger, "Failed to initialize sensor manager");
+        return false;
+    }
+
+    auto& scheduler = getModule<BoardScheduler>()->sensors();
+
+    magCalibrationTaskId = scheduler.addTask(
+        [this]()
+        {
+            auto mag = getLIS3MDLLastSample();
+
+            miosix::Lock<miosix::FastMutex> lock{magCalibrationMutex};
+            magCalibrator.feed(mag);
+        },
+        config::MagCalibration::SAMPLING_RATE);
+
+    if (magCalibrationTaskId == 0)
+    {
+        LOG_ERR(logger, "Failed to add magnetometer calibration task");
+        return false;
+    }
+
+    // Immediately disable the task, so that is enabled only when needed
+    // from FlightModeManager. It is calibrated during the pre-flight
+    // initialization phase.
+    scheduler.disableTask(magCalibrationTaskId);
+
+    started = true;
+    return true;
+}
+
+bool Sensors::isStarted() { return started; }
+
+void Sensors::calibrate()
+{
+    bmx160WithCorrection->startCalibration();
+
+    miosix::Thread::sleep(
+        std::chrono::milliseconds{config::BMX160::CALIBRATION_DURATION}
+            .count());
+
+    bmx160WithCorrection->stopCalibration();
+}
+
+SensorCalibrationData Sensors::getCalibrationData()
+{
+    miosix::Lock<miosix::FastMutex> lock{magCalibrationMutex};
+
+    return SensorCalibrationData{
+        .timestamp = TimestampTimer::getTimestamp(),
+        .magBiasX  = magCalibration.getb().x(),
+        .magBiasY  = magCalibration.getb().y(),
+        .magBiasZ  = magCalibration.getb().z(),
+        .magScaleX = magCalibration.getA().x(),
+        .magScaleY = magCalibration.getA().y(),
+        .magScaleZ = magCalibration.getA().z(),
+    };
+}
+
+void Sensors::resetMagCalibrator()
+{
+    miosix::Lock<miosix::FastMutex> lock{magCalibrationMutex};
+    magCalibrator = SoftAndHardIronCalibration{};
+}
+
+void Sensors::enableMagCalibrator()
+{
+    getModule<BoardScheduler>()->sensors().enableTask(magCalibrationTaskId);
+}
+
+void Sensors::disableMagCalibrator()
+{
+    getModule<BoardScheduler>()->sensors().disableTask(magCalibrationTaskId);
+}
+
+bool Sensors::saveMagCalibration()
+{
+    miosix::Lock<miosix::FastMutex> lock{magCalibrationMutex};
+
+    SixParametersCorrector calibration = magCalibrator.computeResult();
+
+    // Check if the calibration is valid
+    if (!std::isnan(calibration.getb()[0]) &&
+        !std::isnan(calibration.getb()[1]) &&
+        !std::isnan(calibration.getb()[2]) &&
+        !std::isnan(calibration.getA()[0]) &&
+        !std::isnan(calibration.getA()[1]) &&
+        !std::isnan(calibration.getA()[2]))
+    {
+        // Its valid, save it and apply it
+        magCalibration = calibration;
+        return magCalibration.toFile(config::MagCalibration::CALIBRATION_PATH);
+    }
+    else
+    {
+        return false;
+    }
+}
+
+BMX160Data Sensors::getBMX160LastSample()
+{
+    return bmx160 ? bmx160->getLastSample() : BMX160Data{};
+}
+
+BMX160WithCorrectionData Sensors::getBMX160WithCorrectionLastSample()
+{
+    return bmx160WithCorrection ? bmx160WithCorrection->getLastSample()
+                                : BMX160WithCorrectionData{};
+}
+
+LIS3MDLData Sensors::getLIS3MDLLastSample()
+{
+    return lis3mdl ? lis3mdl->getLastSample() : LIS3MDLData{};
+}
+
+H3LIS331DLData Sensors::getH3LISLastSample()
+{
+    return h3lis331dl ? h3lis331dl->getLastSample() : H3LIS331DLData{};
+}
+
+LPS22DFData Sensors::getLPS22DFLastSample()
+{
+    return lps22df ? lps22df->getLastSample() : LPS22DFData{};
+}
+
+UBXGPSData Sensors::getUBXGPSLastSample()
+{
+    return ubxgps ? ubxgps->getLastSample() : UBXGPSData{};
+}
+
+ADS131M08Data Sensors::getADS131LastSample()
+{
+    return ads131m08 ? ads131m08->getLastSample() : ADS131M08Data{};
+}
+
+LoadCellData Sensors::getLoadCellLastSample()
+{
+    return loadCell ? loadCell->getLastSample() : LoadCellData{};
+}
+
+InternalADCData Sensors::getInternalADCLastSample()
+{
+    return internalAdc ? internalAdc->getLastSample() : InternalADCData{};
+}
+
+BatteryVoltageSensorData Sensors::getBatteryVoltage()
+{
+    auto sample = getInternalADCLastSample();
+
+    BatteryVoltageSensorData data;
+    data.voltageTimestamp = sample.timestamp;
+    data.channelId        = static_cast<uint8_t>(config::InternalADC::VBAT_CH);
+    data.voltage          = sample.voltage[config::InternalADC::VBAT_CH];
+    data.batVoltage       = sample.voltage[config::InternalADC::VBAT_CH] *
+                      config::InternalADC::VBAT_COEFF;
+
+    return data;
+}
+
+std::vector<SensorInfo> Sensors::getSensorInfo()
+{
+    if (manager)
+    {
+        std::vector<SensorInfo> infos{};
+
+#define PUSH_SENSOR_INFO(instance, name)                         \
+    if (instance)                                                \
+        infos.push_back(manager->getSensorInfo(instance.get())); \
+    else                                                         \
+        infos.push_back(                                         \
+            SensorInfo { #name, config::name::SAMPLING_RATE, nullptr, false })
+
+        PUSH_SENSOR_INFO(bmx160, BMX160);
+        PUSH_SENSOR_INFO(bmx160WithCorrection, BMX160);
+        PUSH_SENSOR_INFO(h3lis331dl, H3LIS331DL);
+        PUSH_SENSOR_INFO(lis3mdl, LIS3MDL);
+        PUSH_SENSOR_INFO(lps22df, LPS22DF);
+        PUSH_SENSOR_INFO(ubxgps, UBXGPS);
+        PUSH_SENSOR_INFO(ads131m08, ADS131M08);
+        PUSH_SENSOR_INFO(internalAdc, InternalADC);
+
+#undef PUSH_SENSOR_INFO
+
+        return infos;
+    }
+    else
+    {
+        return {};
+    }
+}
+
+void Sensors::bmx160Init()
+{
+    SPIBusConfig spiConfig;
+    spiConfig.clockDivider = SPI::ClockDivider::DIV_4;
+
+    BMX160Config config;
+    config.fifoMode      = BMX160Config::FifoMode::DISABLED;
+    config.fifoWatermark = config::BMX160::FIFO_WATERMARK;
+    config.fifoInterrupt = BMX160Config::FifoInterruptPin::PIN_INT1;
+
+    config.temperatureDivider = config::BMX160::TEMP_DIVIDER;
+
+    config.accelerometerRange = config::BMX160::ACC_FSR;
+    config.gyroscopeRange     = config::BMX160::GYRO_FSR;
+
+    config.accelerometerDataRate = config::BMX160::ACC_GYRO_ODR;
+    config.gyroscopeDataRate     = config::BMX160::ACC_GYRO_ODR;
+    config.magnetometerRate      = config::BMX160::MAG_ODR;
+
+    config.gyroscopeUnit = BMX160Config::GyroscopeMeasureUnit::RAD;
+
+    bmx160 = std::make_unique<BMX160>(getModule<Buses>()->spi1,
+                                      hwmap::bmx160::cs::getPin(), config,
+                                      spiConfig);
+
+    LOG_INFO(logger, "BMX160 initialized!");
+}
+
+void Sensors::bmx160WithCorrectionInit()
+{
+    bmx160WithCorrection = std::make_unique<BMX160WithCorrection>(
+        bmx160.get(), config::BMX160::AXIS_ORIENTATION);
+
+    LOG_INFO(logger, "BMX160WithCorrection initialized!");
+}
+
+void Sensors::h3lisInit()
+{
+    SPIBusConfig spiConfig;
+    spiConfig.clockDivider = SPI::ClockDivider::DIV_16;
+
+    h3lis331dl = std::make_unique<H3LIS331DL>(
+        getModule<Buses>()->spi1, hwmap::h3lis331dl::cs::getPin(), spiConfig,
+        config::H3LIS331DL::ODR, config::H3LIS331DL::BDU,
+        config::H3LIS331DL::FSR);
+
+    LOG_INFO(logger, "H3LIS331DL initialized!");
+}
+
+void Sensors::lis3mdlInit()
+{
+    SPIBusConfig spiConfig;
+    spiConfig.clockDivider = SPI::ClockDivider::DIV_32;
+
+    LIS3MDL::Config config;
+    config.odr                = config::LIS3MDL::ODR;
+    config.scale              = config::LIS3MDL::FSR;
+    config.temperatureDivider = 1;
+
+    lis3mdl = std::make_unique<LIS3MDL>(getModule<Buses>()->spi1,
+                                        hwmap::lis3mdl::cs::getPin(), spiConfig,
+                                        config);
+
+    LOG_INFO(logger, "LIS3MDL initialized!");
+}
+
+void Sensors::lps22Init()
+{
+    auto spiConfig         = LPS22DF::getDefaultSPIConfig();
+    spiConfig.clockDivider = SPI::ClockDivider::DIV_16;
+
+    LPS22DF::Config config;
+    config.avg = config::LPS22DF::AVG;
+    config.odr = config::LPS22DF::ODR;
+
+    lps22df = std::make_unique<LPS22DF>(getModule<Buses>()->spi1,
+                                        hwmap::lps22df::cs::getPin(), spiConfig,
+                                        config);
+
+    LOG_INFO(logger, "LPS22DF initialized!");
+}
+
+void Sensors::ubxGpsInit()
+{
+    auto spiConfig         = UBXGPSSpi::getDefaultSPIConfig();
+    spiConfig.clockDivider = SPI::ClockDivider::DIV_64;
+
+    ubxgps = std::make_unique<UBXGPSSpi>(
+        getModule<Buses>()->spi1, hwmap::ubxgps::cs::getPin(), spiConfig,
+        Kilohertz{config::UBXGPS::SAMPLING_RATE}.value());
+
+    LOG_INFO(logger, "UBXGPS initialized!");
+}
+
+void Sensors::ads131Init()
+{
+    SPIBusConfig spiConfig;
+    spiConfig.clockDivider = SPI::ClockDivider::DIV_32;
+
+    ADS131M08::Config config;
+    config.oversamplingRatio     = config::ADS131M08::OVERSAMPLING_RATIO;
+    config.globalChopModeEnabled = config::ADS131M08::GLOBAL_CHOP_MODE;
+
+    ads131m08 = std::make_unique<ADS131M08>(getModule<Buses>()->spi1,
+                                            hwmap::ads131m08::cs::getPin(),
+                                            spiConfig, config);
+
+    LOG_INFO(logger, "ADS131M08 initialized!");
+}
+
+void Sensors::loadCellInit()
+{
+    loadCell = std::make_unique<AnalogLoadCellSensor>(
+        [this]()
+        {
+            auto sample = getADS131LastSample();
+            return sample.getVoltage(config::LoadCell::ADC_CHANNEL);
+        },
+        config::LoadCell::Calibration::P0_VOLTAGE,
+        config::LoadCell::Calibration::P0_MASS,
+        config::LoadCell::Calibration::P1_VOLTAGE,
+        config::LoadCell::Calibration::P1_MASS);
+
+    LOG_INFO(logger, "LoadCell initialized!");
+}
+
+void Sensors::internalADCInit()
+{
+    internalAdc = std::make_unique<InternalADC>(ADC3);
+    internalAdc->enableChannel(config::InternalADC::VBAT_CH);
+    internalAdc->enableTemperature();
+    internalAdc->enableVbat();
+
+    LOG_INFO(logger, "InternalADC initialized!");
+}
+
+void Sensors::bmx160Callback()
+{
+    auto fifoSize = bmx160->getLastFifoSize();
+    auto& fifo    = bmx160->getLastFifo();
+
+    Logger::getInstance().log(bmx160->getTemperature());
+
+    for (auto i = 0; i < fifoSize; i++)
+        Logger::getInstance().log(fifo.at(i));
+
+    Logger::getInstance().log(bmx160->getFifoStats());
+}
+
+void Sensors::bmx160WithCorrectionCallback()
+{
+    BMX160WithCorrectionData lastSample = bmx160WithCorrection->getLastSample();
+
+    // Update acceleration stats
+    getModule<FlightStatsRecorder>()->updateAcc(lastSample);
+
+    Logger::getInstance().log(lastSample);
+}
+
+void Sensors::h3lisCallback()
+{
+    H3LIS331DLData lastSample = h3lis331dl->getLastSample();
+    Logger::getInstance().log(lastSample);
+}
+
+void Sensors::lis3mdlCallback()
+{
+    LIS3MDLData lastSample = lis3mdl->getLastSample();
+    Logger::getInstance().log(lastSample);
+}
+
+void Sensors::lps22Callback()
+{
+    LPS22DFData lastSample = lps22df->getLastSample();
+    Logger::getInstance().log(lastSample);
+}
+
+void Sensors::ubxGpsCallback()
+{
+    UBXGPSData lastSample = ubxgps->getLastSample();
+    Logger::getInstance().log(lastSample);
+}
+
+void Sensors::ads131Callback()
+{
+    ADS131M08Data lastSample = ads131m08->getLastSample();
+    Logger::getInstance().log(lastSample);
+}
+
+void Sensors::loadCellCallback()
+{
+    LoadCellData lastSample = loadCell->getLastSample();
+    Logger::getInstance().log(lastSample);
+}
+
+void Sensors::internalADCCallback()
+{
+    InternalADCData lastSample = internalAdc->getLastSample();
+    Logger::getInstance().log(lastSample);
+}
+
+bool Sensors::sensorManagerInit()
+{
+    SensorManager::SensorMap_t map;
+
+    if (bmx160)
+    {
+        SensorInfo info{"BMX160", config::BMX160::SAMPLING_RATE,
+                        [this]() { bmx160Callback(); }};
+        map.emplace(bmx160.get(), info);
+    }
+
+    if (bmx160WithCorrection)
+    {
+        SensorInfo info{"BMX160WithCorrection", config::BMX160::SAMPLING_RATE,
+                        [this]() { bmx160WithCorrectionCallback(); }};
+        map.emplace(bmx160WithCorrection.get(), info);
+    }
+
+    if (h3lis331dl)
+    {
+        SensorInfo info{"H3LIS331DL", config::H3LIS331DL::SAMPLING_RATE,
+                        [this]() { h3lisCallback(); }};
+        map.emplace(h3lis331dl.get(), info);
+    }
+
+    if (lis3mdl)
+    {
+        SensorInfo info{"LIS3MDL", config::LIS3MDL::SAMPLING_RATE,
+                        [this]() { lis3mdlCallback(); }};
+        map.emplace(lis3mdl.get(), info);
+    }
+
+    if (lps22df)
+    {
+        SensorInfo info{"LPS22DF", config::LPS22DF::SAMPLING_RATE,
+                        [this]() { lps22Callback(); }};
+        map.emplace(lps22df.get(), info);
+    }
+
+    if (ubxgps)
+    {
+        SensorInfo info{"UBXGPS", config::UBXGPS::SAMPLING_RATE,
+                        [this]() { ubxGpsCallback(); }};
+        map.emplace(ubxgps.get(), info);
+    }
+
+    if (ads131m08)
+    {
+        SensorInfo info{"ADS131M08", config::ADS131M08::SAMPLING_RATE,
+                        [this]() { ads131Callback(); }};
+        map.emplace(ads131m08.get(), info);
+    }
+
+    if (internalAdc)
+    {
+        SensorInfo info{"InternalADC", config::InternalADC::SAMPLING_RATE,
+                        [this]() { internalADCCallback(); }};
+        map.emplace(internalAdc.get(), info);
+    }
+
+    auto& scheduler = getModule<BoardScheduler>()->sensors();
+    manager         = std::make_unique<SensorManager>(map, &scheduler);
+    return manager->start();
+}
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/Sensors/Sensors.h b/src/MockupMain/Sensors/Sensors.h
new file mode 100644
index 0000000000000000000000000000000000000000..3c18acd0217358eaba202f943e6735a7d30763bf
--- /dev/null
+++ b/src/MockupMain/Sensors/Sensors.h
@@ -0,0 +1,188 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 <MockupMain/Configs/SensorsConfig.h>
+#include <drivers/adc/InternalADC.h>
+#include <sensors/ADS131M08/ADS131M08.h>
+#include <sensors/BMX160/BMX160.h>
+#include <sensors/BMX160/BMX160WithCorrection.h>
+#include <sensors/H3LIS331DL/H3LIS331DL.h>
+#include <sensors/LIS3MDL/LIS3MDL.h>
+#include <sensors/LPS22DF/LPS22DF.h>
+#include <sensors/SensorManager.h>
+#include <sensors/UBXGPS/UBXGPSSpi.h>
+#include <sensors/analog/BatteryVoltageSensor.h>
+#include <sensors/calibration/SoftAndHardIronCalibration/SoftAndHardIronCalibration.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+#include "AnalogLoadCellSensor.h"
+#include "SensorData.h"
+
+namespace MockupMain
+{
+class BoardScheduler;
+class Buses;
+class FlightStatsRecorder;
+
+/**
+ * @brief Manages all the sensors of the parafoil board.
+ */
+class Sensors : public Boardcore::InjectableWithDeps<BoardScheduler, Buses,
+                                                     FlightStatsRecorder>
+{
+public:
+    [[nodiscard]] bool start();
+
+    /**
+     * @brief Returns whether all enabled sensors are started and running.
+     */
+    bool isStarted();
+
+    /**
+     * @brief Calibrates the sensors that need calibration.
+     *
+     * @note This function is blocking.
+     */
+    void calibrate();
+
+    /**
+     * @brief Returns the current sensors calibration parameters.
+     *
+     * @internal Ensure mutexes are unlocked before calling this function.
+     */
+    SensorCalibrationData getCalibrationData();
+
+    /**
+     * @brief Resets the magnetometer calibration.
+     */
+    void resetMagCalibrator();
+
+    /**
+     * @brief Enables the magnetometer calibration task.
+     */
+    void enableMagCalibrator();
+
+    /**
+     * @brief Disables the magnetometer calibration task.
+     */
+    void disableMagCalibrator();
+
+    /**
+     * @brief Saves the current magnetometer calibration to file, overwriting
+     * the previous one, and sets it as the active calibration.
+     *
+     * @return Whether the calibration was saved successfully.
+     */
+    bool saveMagCalibration();
+
+    Boardcore::BMX160Data getBMX160LastSample();
+    Boardcore::BMX160WithCorrectionData getBMX160WithCorrectionLastSample();
+    Boardcore::H3LIS331DLData getH3LISLastSample();
+    Boardcore::LIS3MDLData getLIS3MDLLastSample();
+    Boardcore::LPS22DFData getLPS22DFLastSample();
+    Boardcore::UBXGPSData getUBXGPSLastSample();
+    Boardcore::ADS131M08Data getADS131LastSample();
+    Boardcore::InternalADCData getInternalADCLastSample();
+    Boardcore::LoadCellData getLoadCellLastSample();
+
+    Boardcore::BatteryVoltageSensorData getBatteryVoltage();
+
+    /**
+     * @brief Returns information about all sensors managed by this class
+     */
+    std::vector<Boardcore::SensorInfo> getSensorInfo();
+
+protected:
+    /**
+     * @brief A function that is called after all sensors have been created but
+     * before they are inserted into the sensor manager.
+     *
+     * It can be overridden by subclasses to perform additional setup on the
+     * sensors.
+     *
+     * @return Whether the additional setup was successful. If false is
+     * returned, initialization will stop immediately after returning from this
+     * function and the sensors will not be started.
+     */
+    virtual bool postSensorCreationHook() { return true; }
+
+    std::unique_ptr<Boardcore::BMX160> bmx160;
+    std::unique_ptr<Boardcore::H3LIS331DL> h3lis331dl;
+    std::unique_ptr<Boardcore::LIS3MDL> lis3mdl;
+    std::unique_ptr<Boardcore::LPS22DF> lps22df;
+    std::unique_ptr<Boardcore::UBXGPSSpi> ubxgps;
+    std::unique_ptr<Boardcore::ADS131M08> ads131m08;
+    std::unique_ptr<Boardcore::InternalADC> internalAdc;
+
+    std::unique_ptr<Boardcore::BMX160WithCorrection> bmx160WithCorrection;
+    std::unique_ptr<Boardcore::AnalogLoadCellSensor> loadCell;
+
+    std::unique_ptr<Boardcore::SensorManager> manager;
+
+private:
+    /**
+     * Sensors initialization and callback functions.
+     */
+
+    void bmx160Init();
+    void bmx160Callback();
+
+    void bmx160WithCorrectionInit();
+    void bmx160WithCorrectionCallback();
+
+    void h3lisInit();
+    void h3lisCallback();
+
+    void lis3mdlInit();
+    void lis3mdlCallback();
+
+    void lps22Init();
+    void lps22Callback();
+
+    void ubxGpsInit();
+    void ubxGpsCallback();
+
+    void ads131Init();
+    void ads131Callback();
+
+    void loadCellInit();
+    void loadCellCallback();
+
+    void internalADCInit();
+    void internalADCCallback();
+
+    bool sensorManagerInit();
+
+    // Live calibration of the magnetomer
+    miosix::FastMutex magCalibrationMutex;
+    Boardcore::SoftAndHardIronCalibration magCalibrator;
+    Boardcore::SixParametersCorrector magCalibration;
+    size_t magCalibrationTaskId = 0;
+
+    std::atomic<bool> started{false};
+
+    Boardcore::PrintLogger logger = Boardcore::Logging::getLogger("Sensors");
+};
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/StateMachines/FlightModeManager/FlightModeManager.cpp b/src/MockupMain/StateMachines/FlightModeManager/FlightModeManager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..08fe16fa0f93425d53b9941fe3ea109f0a402c51
--- /dev/null
+++ b/src/MockupMain/StateMachines/FlightModeManager/FlightModeManager.cpp
@@ -0,0 +1,550 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 "FlightModeManager.h"
+
+#include <MockupMain/BoardScheduler.h>
+#include <MockupMain/Configs/FlightModeManagerConfig.h>
+#include <MockupMain/FlightStatsRecorder/FlightStatsRecorder.h>
+#include <MockupMain/Sensors/Sensors.h>
+#include <common/Events.h>
+#include <drivers/timer/TimestampTimer.h>
+#include <events/EventBroker.h>
+
+using namespace Boardcore;
+using namespace Common;
+using namespace Boardcore::Units::Time;
+namespace config = MockupMain::Config::FlightModeManager;
+
+namespace MockupMain
+{
+
+FlightModeManager::FlightModeManager()
+    : HSM(&FlightModeManager::PreFlight, miosix::STACK_DEFAULT_FOR_PTHREAD,
+          BoardScheduler::flightModeManagerPriority())
+{
+    EventBroker::getInstance().subscribe(this, TOPIC_FLIGHT);
+    EventBroker::getInstance().subscribe(this, TOPIC_FMM);
+    EventBroker::getInstance().subscribe(this, TOPIC_TMTC);
+    EventBroker::getInstance().subscribe(this, TOPIC_NAS);
+}
+
+FlightModeManager::~FlightModeManager()
+{
+    EventBroker::getInstance().unsubscribe(this);
+}
+
+FlightModeManagerState FlightModeManager::getState() { return state; }
+
+bool FlightModeManager::isTestMode() const
+{
+    return state == FlightModeManagerState::READY_TEST_MODE;
+}
+
+State FlightModeManager::PreFlight(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            return HANDLED;
+        }
+
+        case EV_EXIT:
+        {
+            return HANDLED;
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&FlightModeManager::state_top);
+        }
+
+        case EV_INIT:
+        {
+            return transition(&FlightModeManager::PreFlightInit);
+        }
+
+        case TMTC_START_LOGGING:
+        {
+            Logger::getInstance().start();
+            return HANDLED;
+        }
+
+        case TMTC_STOP_LOGGING:
+        {
+            Logger::getInstance().stop();
+            return HANDLED;
+        }
+
+        case TMTC_RESET_BOARD:
+        {
+            Logger::getInstance().stop();
+            miosix::reboot();
+            __builtin_unreachable();
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State FlightModeManager::PreFlightInit(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(FlightModeManagerState::PRE_FLIGHT_INIT);
+            return HANDLED;
+        }
+
+        case EV_EXIT:
+        {
+            return HANDLED;
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&FlightModeManager::PreFlight);
+        }
+
+        case EV_INIT:
+        {
+            return HANDLED;
+        }
+
+        case FMM_INIT_OK:
+        {
+            return transition(&FlightModeManager::PreFlightInitDone);
+        }
+
+        case FMM_INIT_ERROR:
+        {
+            return transition(&FlightModeManager::PreFlightInitError);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State FlightModeManager::PreFlightInitError(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(FlightModeManagerState::PRE_FLIGHT_INIT_ERROR);
+            return HANDLED;
+        }
+
+        case EV_EXIT:
+        {
+            return HANDLED;
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&FlightModeManager::PreFlight);
+        }
+
+        case EV_INIT:
+        {
+            return HANDLED;
+        }
+
+        case TMTC_FORCE_INIT:
+        {
+            return transition(&FlightModeManager::PreFlightInitDone);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State FlightModeManager::PreFlightInitDone(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(FlightModeManagerState::PRE_FLIGHT_INIT_DONE);
+            EventBroker::getInstance().post(FMM_CALIBRATE, TOPIC_FMM);
+            return HANDLED;
+        }
+
+        case EV_EXIT:
+        {
+            return HANDLED;
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&FlightModeManager::PreFlight);
+        }
+
+        case EV_INIT:
+        {
+            return HANDLED;
+        }
+
+        case FMM_CALIBRATE:
+        {
+            return transition(&FlightModeManager::PreFlightSensorCalibration);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State FlightModeManager::PreFlightSensorCalibration(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(
+                FlightModeManagerState::PRE_FLIGHT_ALGORITHM_CALIBRATION);
+
+            // Wait for sensors to stabilize before calibration
+            // The first few LPS28DFW samples contain garbage data
+            miosix::Thread::sleep(100);
+            getModule<Sensors>()->calibrate();
+
+            EventBroker::getInstance().post(FMM_ALGOS_CALIBRATE, TOPIC_FMM);
+            return HANDLED;
+        }
+
+        case EV_EXIT:
+        {
+            return HANDLED;
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&FlightModeManager::PreFlight);
+        }
+
+        case EV_INIT:
+        {
+            return HANDLED;
+        }
+
+        case FMM_ALGOS_CALIBRATE:
+        {
+            return transition(
+                &FlightModeManager::PreFlightAlgorithmCalibration);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State FlightModeManager::PreFlightAlgorithmCalibration(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(
+                FlightModeManagerState::PRE_FLIGHT_ALGORITHM_CALIBRATION);
+            // Calibrate after a delay to allow calibrated sensors to stabilize
+            EventBroker::getInstance().postDelayed(NAS_CALIBRATE, TOPIC_NAS,
+                                                   100);
+            return HANDLED;
+        }
+
+        case EV_EXIT:
+        {
+            return HANDLED;
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&FlightModeManager::PreFlight);
+        }
+
+        case EV_INIT:
+        {
+            return HANDLED;
+        }
+
+        case NAS_READY:
+        {
+            EventBroker::getInstance().post(FMM_READY, TOPIC_FMM);
+            return transition(&FlightModeManager::Ready);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State FlightModeManager::Ready(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(FlightModeManagerState::READY);
+            return HANDLED;
+        }
+
+        case EV_EXIT:
+        {
+            return HANDLED;
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&FlightModeManager::state_top);
+        }
+
+        case EV_INIT:
+        {
+            return HANDLED;
+        }
+
+        case TMTC_ENTER_TEST_MODE:
+        {
+            return transition(&FlightModeManager::ReadyTestMode);
+        }
+
+        case TMTC_CALIBRATE:
+        {
+            return transition(&FlightModeManager::PreFlightSensorCalibration);
+        }
+
+        case FLIGHT_NC_DETACHED:
+        case TMTC_FORCE_EXPULSION:
+        {
+            return transition(&FlightModeManager::FlyingDrogueDescent);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State FlightModeManager::ReadyTestMode(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(FlightModeManagerState::READY_TEST_MODE);
+            return HANDLED;
+        }
+
+        case EV_EXIT:
+        {
+            return HANDLED;
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&FlightModeManager::Ready);
+        }
+
+        case EV_INIT:
+        {
+            return HANDLED;
+        }
+
+        case TMTC_FORCE_LANDING:
+        {
+            return transition(&FlightModeManager::Landed);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State FlightModeManager::Flying(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            return HANDLED;
+        }
+
+        case EV_EXIT:
+        {
+            return HANDLED;
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&FlightModeManager::state_top);
+        }
+
+        case EV_INIT:
+        {
+            return transition(&FlightModeManager::FlyingDrogueDescent);
+        }
+
+        case TMTC_FORCE_LANDING:
+        {
+            return transition(&FlightModeManager::Landed);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State FlightModeManager::FlyingDrogueDescent(const Event& event)
+{
+    static uint16_t controlDelayId;
+
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(FlightModeManagerState::FLYING_DROGUE_DESCENT);
+            // Send the event to the WingController
+            controlDelayId = EventBroker::getInstance().postDelayed(
+                FLIGHT_DROGUE_DESCENT, TOPIC_FLIGHT,
+                Millisecond{config::CONTROL_DELAY}.value());
+
+            getModule<FlightStatsRecorder>()->dropDetected(
+                TimestampTimer::getTimestamp());
+
+            return HANDLED;
+        }
+
+        case EV_EXIT:
+        {
+            EventBroker::getInstance().removeDelayed(controlDelayId);
+            return HANDLED;
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&FlightModeManager::Flying);
+        }
+
+        case EV_INIT:
+        {
+            return HANDLED;
+        }
+
+        case TMTC_FORCE_LANDING:
+        {
+            return transition(&FlightModeManager::Landed);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State FlightModeManager::Landed(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(FlightModeManagerState::LANDED);
+
+            EventBroker::getInstance().post(FLIGHT_LANDING_DETECTED,
+                                            TOPIC_FLIGHT);
+            EventBroker::getInstance().postDelayed(
+                FMM_STOP_LOGGING, TOPIC_FMM,
+                Millisecond{config::LOGGING_DELAY}.value());
+
+            return HANDLED;
+        }
+
+        case EV_EXIT:
+        {
+            return HANDLED;
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&FlightModeManager::state_top);
+        }
+
+        case EV_INIT:
+        {
+            return HANDLED;
+        }
+
+        case FMM_STOP_LOGGING:
+        {
+            Logger::getInstance().stop();
+            return HANDLED;
+        }
+
+        case TMTC_RESET_BOARD:
+        {
+            Logger::getInstance().stop();
+            miosix::reboot();
+            __builtin_unreachable();
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+void FlightModeManager::updateState(FlightModeManagerState newState)
+{
+    state = newState;
+
+    auto status = FlightModeManagerStatus{
+        .timestamp = TimestampTimer::getTimestamp(),
+        .state     = newState,
+    };
+    Logger::getInstance().log(status);
+}
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/StateMachines/FlightModeManager/FlightModeManager.h b/src/MockupMain/StateMachines/FlightModeManager/FlightModeManager.h
new file mode 100644
index 0000000000000000000000000000000000000000..896b7b2615d6789fd305eb7b6e43705bbdec33d4
--- /dev/null
+++ b/src/MockupMain/StateMachines/FlightModeManager/FlightModeManager.h
@@ -0,0 +1,154 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 <events/HSM.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+#include "FlightModeManagerData.h"
+
+namespace MockupMain
+{
+class Sensors;
+class Actuators;
+class FlightStatsRecorder;
+
+/**
+ * State machine that manages the flight modes of the Parafoil.
+ *
+ * HSM Schema:
+ *
+ * PreFlight
+ * ├── PreFlightInit
+ * ├── PreFlightInitError
+ * ├── PreFlightInitDone
+ * ├── PreFlightSensorCalibration
+ * └── PreFlightAlgorithmCalibration
+ *
+ * Ready
+ * └── ReadyTestMode
+ *
+ * Flying
+ * └── FlyingDrogueDescent
+ *
+ * Landed
+ */
+class FlightModeManager
+    : public Boardcore::HSM<FlightModeManager>,
+      public Boardcore::InjectableWithDeps<Sensors, Actuators,
+                                           FlightStatsRecorder>
+{
+public:
+    FlightModeManager();
+    ~FlightModeManager();
+
+    /**
+     * @brief Returns the current state of the FlightModeManager.
+     */
+    FlightModeManagerState getState();
+
+    bool isTestMode() const;
+
+    /**
+     * @brief Super state for when the parafoil has not yet ready for launch.
+     */
+    Boardcore::State PreFlight(const Boardcore::Event& event);
+
+    /**
+     * @brief State in which the parafoil is initializing.
+     *
+     * Super state: PreFlight
+     */
+    Boardcore::State PreFlightInit(const Boardcore::Event& event);
+
+    /**
+     * @brief State in which the init has failed
+     *
+     * Super state: PreFlight
+     */
+    Boardcore::State PreFlightInitError(const Boardcore::Event& event);
+
+    /**
+     * @brief State in which the init is done and a calibration event is
+     * thrown
+     *
+     * Super state: PreFlight
+     */
+    Boardcore::State PreFlightInitDone(const Boardcore::Event& event);
+
+    /**
+     * @brief Calibration of all sensors.
+     *
+     * Super state: PreFlight
+     */
+    Boardcore::State PreFlightSensorCalibration(const Boardcore::Event& event);
+
+    /**
+     * @brief Calibration of all algorithms.
+     *
+     * Super state: PreFlight
+     */
+    Boardcore::State PreFlightAlgorithmCalibration(
+        const Boardcore::Event& event);
+
+    /**
+     * @brief Super state in which the parafoil is waiting to be dropped.
+     */
+    Boardcore::State Ready(const Boardcore::Event& event);
+
+    /**
+     * @brief The parafoil will accept specific telecommands otherwise
+     * considered risky.
+     *
+     * Super state: Ready
+     */
+    Boardcore::State ReadyTestMode(const Boardcore::Event& event);
+
+    /**
+     * @brief Super state for when the Parafoil is flying.
+     */
+    Boardcore::State Flying(const Boardcore::Event& event);
+
+    /**
+     * @brief State in which the parafoil wing is opened and starts guiding
+     * itself
+     *
+     * Super state: Flying
+     */
+    Boardcore::State FlyingDrogueDescent(const Boardcore::Event& event);
+
+    /**
+     * @brief The parafoil ended the flight and closes the log.
+     */
+    Boardcore::State Landed(const Boardcore::Event& event);
+
+private:
+    void updateState(FlightModeManagerState newState);
+
+    std::atomic<FlightModeManagerState> state{
+        FlightModeManagerState::PRE_FLIGHT};
+
+    Boardcore::PrintLogger logger = Boardcore::Logging::getLogger("FMM");
+};
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/StateMachines/FlightModeManager/FlightModeManagerData.h b/src/MockupMain/StateMachines/FlightModeManager/FlightModeManagerData.h
new file mode 100644
index 0000000000000000000000000000000000000000..89ce930c0511d9610fbd1dba4f4851676ab213a2
--- /dev/null
+++ b/src/MockupMain/StateMachines/FlightModeManager/FlightModeManagerData.h
@@ -0,0 +1,60 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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>
+#include <ostream>
+#include <string>
+
+namespace MockupMain
+{
+
+enum class FlightModeManagerState : uint8_t
+{
+    PRE_FLIGHT = 0,
+    PRE_FLIGHT_INIT,
+    PRE_FLIGHT_INIT_ERROR,
+    PRE_FLIGHT_INIT_DONE,
+    PRE_FLIGHT_SENSOR_CALIBRATION,
+    PRE_FLIGHT_ALGORITHM_CALIBRATION,
+    PRE_FLIGHT_DISARMED,
+    READY                 = 8,   // ARMED
+    READY_TEST_MODE       = 7,   // TEST_MODE
+    FLYING_DROGUE_DESCENT = 10,  // DROGUE_DESCENT
+    LANDED                = 12,  // LANDED
+};
+
+struct FlightModeManagerStatus
+{
+    uint64_t timestamp           = 0;
+    FlightModeManagerState state = FlightModeManagerState::PRE_FLIGHT;
+
+    static std::string header() { return "timestamp,state\n"; }
+
+    void print(std::ostream& os) const
+    {
+        os << timestamp << "," << (int)state << "\n";
+    }
+};
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/StateMachines/NASController/NASController.cpp b/src/MockupMain/StateMachines/NASController/NASController.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d9247a757ddfe0b780bf5fa1102adac18d25c925
--- /dev/null
+++ b/src/MockupMain/StateMachines/NASController/NASController.cpp
@@ -0,0 +1,370 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Authors: Niccolò Betto, Davide Mor, Davide Basso
+ *
+ * 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 <MockupMain/BoardScheduler.h>
+#include <MockupMain/Configs/NASConfig.h>
+#include <MockupMain/FlightStatsRecorder/FlightStatsRecorder.h>
+#include <MockupMain/Sensors/Sensors.h>
+#include <MockupMain/StateMachines/NASController/NASController.h>
+#include <algorithms/NAS/StateInitializer.h>
+#include <common/Events.h>
+#include <common/ReferenceConfig.h>
+#include <events/EventBroker.h>
+#include <utils/SkyQuaternion/SkyQuaternion.h>
+
+using namespace Boardcore;
+using namespace Boardcore::Units::Time;
+using namespace Boardcore::Units::Length;
+using namespace Boardcore::Units::Acceleration;
+using namespace Eigen;
+using namespace Common;
+namespace config = MockupMain::Config::NAS;
+
+namespace MockupMain
+{
+
+NASController::NASController()
+    : FSM(&NASController::Init, miosix::STACK_DEFAULT_FOR_PTHREAD,
+          BoardScheduler::nasControllerPriority()),
+      nas(config::CONFIG)
+{
+    EventBroker::getInstance().subscribe(this, TOPIC_NAS);
+    EventBroker::getInstance().subscribe(this, TOPIC_FLIGHT);
+}
+
+bool NASController::start()
+{
+    // Setup the NAS
+    Matrix<float, 13, 1> x = Matrix<float, 13, 1>::Zero();
+    // Create the initial quaternion
+    Vector4f q = SkyQuaternion::eul2quat({0, 0, 0});
+
+    // Set the initial quaternion inside the matrix
+    x(NAS::IDX_QUAT + 0) = q(0);
+    x(NAS::IDX_QUAT + 1) = q(1);
+    x(NAS::IDX_QUAT + 2) = q(2);
+    x(NAS::IDX_QUAT + 3) = q(3);
+
+    // Set the NAS x matrix
+    nas.setX(x);
+    // Set the initial reference values from the default ones
+    nas.setReferenceValues(ReferenceConfig::defaultReferenceValues);
+
+    auto& scheduler = getModule<BoardScheduler>()->nasController();
+    // Add the task to the scheduler
+    auto task = scheduler.addTask([this] { update(); }, config::UPDATE_RATE,
+                                  TaskScheduler::Policy::RECOVER);
+
+    if (task == 0)
+    {
+        LOG_ERR(logger, "Failed to add NAS update task");
+        return false;
+    }
+
+    if (!FSM::start())
+    {
+        LOG_ERR(logger, "Failed to start NASController FSM active object");
+        return false;
+    }
+
+    started = true;
+    return true;
+}
+
+NASState NASController::getNasState()
+{
+    miosix::Lock<miosix::FastMutex> l(nasMutex);
+    return nas.getState();
+}
+
+ReferenceValues NASController::getReferenceValues()
+{
+    miosix::Lock<miosix::FastMutex> l(nasMutex);
+    return nas.getReferenceValues();
+}
+
+NASControllerState NASController::getState() { return state; }
+
+void NASController::setOrientation(const Eigen::Quaternionf& quat)
+{
+    Lock<FastMutex> lock{nasMutex};
+
+    auto x               = nas.getX();
+    x(NAS::IDX_QUAT + 0) = quat.x();
+    x(NAS::IDX_QUAT + 1) = quat.y();
+    x(NAS::IDX_QUAT + 2) = quat.z();
+    x(NAS::IDX_QUAT + 3) = quat.w();
+    nas.setX(x);
+}
+
+void NASController::Init(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(NASControllerState::INIT);
+            break;
+        }
+
+        case NAS_CALIBRATE:
+        {
+            transition(&NASController::Calibrating);
+            break;
+        }
+    }
+}
+
+void NASController::Calibrating(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(NASControllerState::CALIBRATING);
+            calibrate();
+            break;
+        }
+
+        case NAS_READY:
+        {
+            transition(&NASController::Ready);
+            break;
+        }
+    }
+}
+
+// State skipped on entry because we don't have Armed and Disarmed
+// states in the FMM
+void NASController::Ready(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(NASControllerState::READY);
+            transition(&NASController::Active);
+            break;
+        }
+    }
+}
+
+void NASController::Active(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(NASControllerState::ACTIVE);
+            break;
+        }
+
+        case NAS_CALIBRATE:
+        {
+            transition(&NASController::Calibrating);
+            break;
+        }
+
+        case FLIGHT_LANDING_DETECTED:
+        {
+            transition(&NASController::End);
+            break;
+        }
+    }
+}
+
+void NASController::End(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            updateState(NASControllerState::END);
+            break;
+        }
+    }
+}
+
+void NASController::calibrate()
+{
+    Sensors* sensors = getModule<Sensors>();
+
+    Vector3f accSum = Vector3f::Zero();
+    Vector3f magSum = Vector3f::Zero();
+    float baroSum   = 0.0f;
+
+    for (int i = 0; i < config::CALIBRATION_SAMPLES_COUNT; i++)
+    {
+        BMX160Data imu    = sensors->getBMX160WithCorrectionLastSample();
+        PressureData baro = sensors->getLPS22DFLastSample();
+
+        accSum +=
+            Vector3f{imu.accelerationX, imu.accelerationY, imu.accelerationZ};
+        magSum += Vector3f{imu.magneticFieldX, imu.magneticFieldY,
+                           imu.magneticFieldZ};
+
+        baroSum += baro.pressure;
+
+        Thread::sleep(Millisecond{config::CALIBRATION_SLEEP_TIME}.value());
+    }
+
+    Vector3f meanAcc = accSum / config::CALIBRATION_SAMPLES_COUNT;
+    meanAcc.normalize();
+    Vector3f meanMag = magSum / config::CALIBRATION_SAMPLES_COUNT;
+    meanMag.normalize();
+    float meanBaro = baroSum / config::CALIBRATION_SAMPLES_COUNT;
+
+    // Use the triad to compute initial state
+    StateInitializer init;
+    init.triad(meanAcc, meanMag, ReferenceConfig::nedMag);
+
+    miosix::Lock<miosix::FastMutex> l(nasMutex);
+
+    // Compute reference values
+    ReferenceValues reference = nas.getReferenceValues();
+    reference.refPressure     = meanBaro;
+    reference.refAltitude     = Aeroutils::relAltitude(
+        reference.refPressure, reference.mslPressure, reference.mslTemperature);
+
+    // Also update the reference with the GPS if we have fix
+    UBXGPSData gps = sensors->getUBXGPSLastSample();
+    if (gps.fix == 3)
+    {
+        // Don't use the GPS altitude because it is not reliable
+        reference.refLatitude  = gps.latitude;
+        reference.refLongitude = gps.longitude;
+    }
+
+    // Update the algorithm reference values
+    nas.setX(init.getInitX());
+    nas.setReferenceValues(reference);
+
+    EventBroker::getInstance().post(NAS_READY, TOPIC_NAS);
+}
+
+void NASController::update()
+{
+    // Update the NAS state only if the FSM is active
+    if (state != NASControllerState::ACTIVE)
+        return;
+
+    auto* sensors = getModule<Sensors>();
+
+    auto imu  = sensors->getBMX160WithCorrectionLastSample();
+    auto gps  = sensors->getUBXGPSLastSample();
+    auto baro = sensors->getLPS22DFLastSample();
+
+    // Calculate acceleration
+    Vector3f acc   = static_cast<AccelerometerData>(imu);
+    auto accLength = MeterPerSecondSquared{acc.norm()};
+
+    miosix::Lock<miosix::FastMutex> l(nasMutex);
+
+    // Perform initial NAS prediction
+    nas.predictGyro(imu);
+    nas.predictAcc(imu);
+
+    // NOTE: Magnetometer correction has been disabled
+
+    if (lastGpsTimestamp < gps.gpsTimestamp && gps.fix == 3 &&
+        accLength < Config::NAS::DISABLE_GPS_ACCELERATION_THRESHOLD)
+    {
+        nas.correctGPS(gps);
+    }
+
+    if (lastBaroTimestamp < baro.pressureTimestamp)
+        nas.correctBaro(baro.pressure);
+
+    // Correct with accelerometer if the acceleration is in specs
+    if (lastAccTimestamp < imu.accelerationTimestamp && acc1g)
+        nas.correctAcc(imu);
+
+    // Check if the accelerometer is measuring 1g
+    if (accLength < (MeterPerSecondSquared{G{1}} +
+                     Config::NAS::ACCELERATION_1G_CONFIDENCE / 2) &&
+        accLength > (MeterPerSecondSquared{G{1}} -
+                     Config::NAS::ACCELERATION_1G_CONFIDENCE / 2))
+    {
+        if (acc1gSamplesCount < Config::NAS::ACCELERATION_1G_SAMPLES)
+            acc1gSamplesCount++;
+        else
+            acc1g = true;
+    }
+    else
+    {
+        acc1gSamplesCount = 0;
+        acc1g             = false;
+    }
+
+    lastGyroTimestamp = imu.angularSpeedTimestamp;
+    lastAccTimestamp  = imu.accelerationTimestamp;
+    lastMagTimestamp  = imu.magneticFieldTimestamp;
+    lastGpsTimestamp  = gps.gpsTimestamp;
+    lastBaroTimestamp = baro.pressureTimestamp;
+
+    auto state = nas.getState();
+    auto ref   = nas.getReferenceValues();
+
+    getModule<FlightStatsRecorder>()->updateNas(state, ref.refTemperature);
+    Logger::getInstance().log(state);
+}
+
+void NASController::updateState(NASControllerState newState)
+{
+    state = newState;
+
+    auto status = NASControllerStatus{
+        .timestamp = TimestampTimer::getTimestamp(),
+        .state     = newState,
+    };
+    Logger::getInstance().log(status);
+}
+
+void NASController::setReferenceAltitude(Meter altitude)
+{
+    miosix::Lock<miosix::FastMutex> l(nasMutex);
+
+    auto ref        = nas.getReferenceValues();
+    ref.refAltitude = altitude.value();
+    nas.setReferenceValues(ref);
+}
+
+void NASController::setReferenceTemperature(float temperature)
+{
+    miosix::Lock<miosix::FastMutex> l(nasMutex);
+
+    auto ref           = nas.getReferenceValues();
+    ref.refTemperature = temperature;
+    nas.setReferenceValues(ref);
+}
+
+void NASController::setReferenceCoordinates(float latitude, float longitude)
+{
+    miosix::Lock<miosix::FastMutex> l(nasMutex);
+
+    auto ref         = nas.getReferenceValues();
+    ref.refLatitude  = latitude;
+    ref.refLongitude = longitude;
+    nas.setReferenceValues(ref);
+}
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/StateMachines/NASController/NASController.h b/src/MockupMain/StateMachines/NASController/NASController.h
new file mode 100644
index 0000000000000000000000000000000000000000..688cb1479ce126924ab0f905fec801cc14b68c73
--- /dev/null
+++ b/src/MockupMain/StateMachines/NASController/NASController.h
@@ -0,0 +1,106 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Authors: Niccolò Betto, Davide Basso
+ *
+ * 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 <algorithms/NAS/NAS.h>
+#include <diagnostic/PrintLogger.h>
+#include <events/FSM.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+#include "NASControllerData.h"
+
+namespace MockupMain
+{
+class BoardScheduler;
+class Sensors;
+class FlightStatsRecorder;
+
+class NASController
+    : public Boardcore::FSM<NASController>,
+      public Boardcore::InjectableWithDeps<BoardScheduler, Sensors,
+                                           FlightStatsRecorder>
+{
+public:
+    /**
+     * @brief Initializes the NAS controller.
+     *
+     * Sets the initial FSM state to idle, subscribes to NAS and flight events
+     * and initializes the NAS algorithm.
+     */
+    NASController();
+
+    /**
+     * @brief Adds the NAS update function into the scheduler and starts the FSM
+     * thread
+     */
+    bool start() override;
+
+    Boardcore::NASState getNasState();
+    Boardcore::ReferenceValues getReferenceValues();
+
+    NASControllerState getState();
+
+    void setOrientation(const Eigen::Quaternionf& orientation);
+
+    void setReferenceAltitude(Boardcore::Units::Length::Meter altitude);
+    void setReferenceTemperature(float temperature);
+    void setReferenceCoordinates(float latitude, float longitude);
+
+private:
+    void calibrate();
+
+    /**
+     * @brief Update the NAS estimation
+     */
+    void update();
+
+    // FSM states
+    void Init(const Boardcore::Event& event);
+    void Calibrating(const Boardcore::Event& event);
+    void Ready(const Boardcore::Event& event);
+    void Active(const Boardcore::Event& event);
+    void End(const Boardcore::Event& event);
+
+    void updateState(NASControllerState newState);
+
+    std::atomic<NASControllerState> state{NASControllerState::INIT};
+
+    Boardcore::NAS nas;  ///< The NAS algorithm instance
+    miosix::FastMutex nasMutex;
+
+    int magDecimateCount  = 0;
+    int acc1gSamplesCount = 0;
+    bool acc1g            = false;
+
+    uint64_t lastGyroTimestamp = 0;
+    uint64_t lastAccTimestamp  = 0;
+    uint64_t lastMagTimestamp  = 0;
+    uint64_t lastGpsTimestamp  = 0;
+    uint64_t lastBaroTimestamp = 0;
+
+    std::atomic<bool> started{false};
+
+    Boardcore::PrintLogger logger = Boardcore::Logging::getLogger("NAS");
+};
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/StateMachines/NASController/NASControllerData.h b/src/MockupMain/StateMachines/NASController/NASControllerData.h
new file mode 100644
index 0000000000000000000000000000000000000000..c60bfce9668d49766f207c92e46f2962b3095258
--- /dev/null
+++ b/src/MockupMain/StateMachines/NASController/NASControllerData.h
@@ -0,0 +1,54 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Authors: Niccolò Betto, Davide Basso
+ *
+ * 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>
+#include <ostream>
+#include <string>
+
+namespace MockupMain
+{
+
+enum class NASControllerState : uint8_t
+{
+    INIT = 0,
+    CALIBRATING,
+    READY,
+    ACTIVE,
+    END
+};
+
+struct NASControllerStatus
+{
+    uint64_t timestamp       = 0;
+    NASControllerState state = NASControllerState::INIT;
+
+    static std::string header() { return "timestamp,state\n"; }
+
+    void print(std::ostream& os) const
+    {
+        os << timestamp << "," << (int)state << "\n";
+    }
+};
+
+}  // namespace MockupMain
diff --git a/src/MockupMain/mockup-entry.cpp b/src/MockupMain/mockup-entry.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0ea06da34b5cc5ddfb6ff4a99a39999a7b441190
--- /dev/null
+++ b/src/MockupMain/mockup-entry.cpp
@@ -0,0 +1,174 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Davide Basso
+ *
+ * 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 <MockupMain/BoardScheduler.h>
+#include <MockupMain/Buses.h>
+#include <MockupMain/FlightStatsRecorder/FlightStatsRecorder.h>
+#include <MockupMain/PinHandler/PinHandler.h>
+#include <MockupMain/Radio/Radio.h>
+#include <MockupMain/Sensors/Sensors.h>
+#include <MockupMain/StateMachines/FlightModeManager/FlightModeManager.h>
+#include <MockupMain/StateMachines/NASController/NASController.h>
+#include <common/Events.h>
+#include <common/Topics.h>
+#include <diagnostic/PrintLogger.h>
+#include <diagnostic/StackLogger.h>
+#include <events/EventBroker.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+#include <iomanip>
+#include <iostream>
+
+/**
+ * @brief Starts a module and checks if it started correctly.
+ * Must be followed by a semicolon or a block of code.
+ * The block of code will be executed only if the module started correctly.
+ *
+ * @example START_MODULE(sensors) { miosix::ledOn(); }
+ */
+#define START_MODULE(module)                                             \
+    std::cout << "Starting " #module << std::endl;                       \
+    if (!module->start())                                                \
+    {                                                                    \
+        initResult = false;                                              \
+        std::cerr << "*** Failed to start " #module " ***" << std::endl; \
+    }                                                                    \
+    else
+
+/**
+ * @brief Starts a singleton and checks if it started correctly.
+ * Must be followed by a semicolon or a block of code.
+ * The block of code will be executed only if the singleton started correctly.
+ *
+ * @example `START_SINGLETON(Logger) { miosix::ledOn(); }`
+ */
+#define START_SINGLETON(singleton)                                          \
+    std::cout << "Starting " #singleton << std::endl;                       \
+    if (!singleton::getInstance().start())                                  \
+    {                                                                       \
+        initResult = false;                                                 \
+        std::cerr << "*** Failed to start " #singleton " ***" << std::endl; \
+    }
+
+// Build type string for printing during startup
+#if defined(DEBUG)
+#define BUILD_TYPE "Debug"
+#else
+#define BUILD_TYPE "Release"
+#endif
+
+using namespace Boardcore;
+using namespace MockupMain;
+using namespace Common;
+
+int main()
+{
+    miosix::ledOff();
+    std::cout << "MockupMain Entrypoint " << "(" << BUILD_TYPE << ")"
+              << " by Skyward Experimental Rocketry" << std::endl;
+
+    // cppcheck-suppress unreadVariable
+    auto logger = Logging::getLogger("Parafoil");
+    DependencyManager depman{};
+
+    std::cout << "Instantiating modules" << std::endl;
+    bool initResult = true;
+
+    // Core components
+    auto buses = new Buses();
+    initResult &= depman.insert(buses);
+    auto scheduler = new BoardScheduler();
+    initResult &= depman.insert(scheduler);
+
+    // Global state machine
+    auto flightModeManager = new FlightModeManager();
+    initResult &= depman.insert(flightModeManager);
+
+    // Attitude estimation
+    auto nasController = new NASController();
+    initResult &= depman.insert(nasController);
+
+    // Sensors
+    auto sensors = new Sensors();
+    initResult &= depman.insert(sensors);
+    auto pinHandler = new PinHandler();
+    initResult &= depman.insert(pinHandler);
+
+    // Radio
+    auto radio = new Radio();
+    initResult &= depman.insert(radio);
+
+    // Stats recorder
+    auto flightStatsRecorder = new FlightStatsRecorder();
+    initResult &= depman.insert(flightStatsRecorder);
+
+    std::cout << "Injecting module dependencies" << std::endl;
+    initResult &= depman.inject();
+
+    START_SINGLETON(Logger)
+    {
+        std::cout << "Logger Ok!\n"
+                  << "\tLog number: "
+                  << Logger::getInstance().getCurrentLogNumber() << std::endl;
+    }
+    START_SINGLETON(EventBroker);
+
+    START_MODULE(pinHandler);
+    START_MODULE(radio);
+    START_MODULE(nasController);
+    START_MODULE(flightModeManager);
+
+    START_MODULE(scheduler);
+
+    START_MODULE(sensors);
+
+    if (initResult)
+    {
+        EventBroker::getInstance().post(FMM_INIT_OK, TOPIC_FMM);
+        // Turn on the initialization led on the CU
+        miosix::ledOn();
+        std::cout << "Parafoil initialization Ok!" << std::endl;
+    }
+    else
+    {
+        EventBroker::getInstance().post(FMM_INIT_ERROR, TOPIC_FMM);
+        std::cerr << "*** Parafoil initialization error ***" << std::endl;
+    }
+
+    std::cout << "Sensors status:" << std::endl;
+    auto sensorInfo = sensors->getSensorInfo();
+    for (const auto& info : sensorInfo)
+    {
+        std::cout << "\t" << std::setw(16) << std::left << info.id << " "
+                  << (info.isInitialized ? "Ok" : "Error") << "\n";
+    }
+    std::cout.flush();
+
+    // Collect stack usage statistics
+    while (true)
+    {
+        StackLogger::getInstance().log();
+        Thread::sleep(1000);
+    }
+
+    return 0;
+}