From a7b01b3c26e9660f82caa6c137d8bb679a61574f Mon Sep 17 00:00:00 2001
From: Ettore Pane <ettore.pane@skywarder.eu>
Date: Fri, 14 Mar 2025 13:54:15 +0100
Subject: [PATCH] [CONRIGv2] Initial ConRIGv2 on-board software

---
 .vscode/c_cpp_properties.json        |  22 +++
 CMakeLists.txt                       |   4 +
 cmake/dependencies.cmake             |   6 +
 src/ConRIG/BoardScheduler.h          |   4 +-
 src/ConRIGv2/BoardScheduler.h        |  70 ++++++++
 src/ConRIGv2/Buses.h                 |  51 ++++++
 src/ConRIGv2/Buttons/Buttons.cpp     | 245 +++++++++++++++++++++++++++
 src/ConRIGv2/Buttons/Buttons.h       |  61 +++++++
 src/ConRIGv2/Configs/ButtonsConfig.h |  44 +++++
 src/ConRIGv2/Configs/RadioConfig.h   |  55 ++++++
 src/ConRIGv2/Hub/Hub.cpp             | 171 +++++++++++++++++++
 src/ConRIGv2/Hub/Hub.h               |  81 +++++++++
 src/ConRIGv2/Radio/Radio.cpp         | 245 +++++++++++++++++++++++++++
 src/ConRIGv2/Radio/Radio.h           |  95 +++++++++++
 src/ConRIGv2/conrig-v2-entry.cpp     | 109 ++++++++++++
 15 files changed, 1261 insertions(+), 2 deletions(-)
 create mode 100644 src/ConRIGv2/BoardScheduler.h
 create mode 100644 src/ConRIGv2/Buses.h
 create mode 100644 src/ConRIGv2/Buttons/Buttons.cpp
 create mode 100644 src/ConRIGv2/Buttons/Buttons.h
 create mode 100644 src/ConRIGv2/Configs/ButtonsConfig.h
 create mode 100644 src/ConRIGv2/Configs/RadioConfig.h
 create mode 100644 src/ConRIGv2/Hub/Hub.cpp
 create mode 100644 src/ConRIGv2/Hub/Hub.h
 create mode 100644 src/ConRIGv2/Radio/Radio.cpp
 create mode 100644 src/ConRIGv2/Radio/Radio.h
 create mode 100644 src/ConRIGv2/conrig-v2-entry.cpp

diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json
index 1ada27469..38b527054 100755
--- a/.vscode/c_cpp_properties.json
+++ b/.vscode/c_cpp_properties.json
@@ -218,6 +218,28 @@
                 "${workspaceFolder}/skyward-boardcore/src/bsps/stm32f429zi_con_rig"
             ]
         },
+        {
+            "name": "stm32f767zi_conrig_v2",
+            "cStandard": "c11",
+            "cppStandard": "c++14",
+            "compilerPath": "/opt/arm-miosix-eabi/bin/arm-miosix-eabi-g++",
+            "defines": [
+                "${defaultDefines}",
+                "_MIOSIX_BOARDNAME=stm32f767zi_conrig_v2",
+                "_BOARD_STM32F767ZI_CONRIG_V2",
+                "_ARCH_CORTEXM7_STM32F7",
+                "HSE_VALUE=25000000",
+                "SYSCLK_FREQ_216MHz=216000000",
+                "__ENABLE_XRAM",
+                "V_DDA_VOLTAGE=3.3f"
+            ],
+            "includePath": [
+                "${defaultIncludePaths}",
+                "${workspaceFolder}/skyward-boardcore/libs/miosix-kernel/miosix/arch/cortexM7_stm32f7/common",
+                "${workspaceFolder}/skyward-boardcore/src/bsps/stm32f767zi_conrig_v2/config",
+                "${workspaceFolder}/skyward-boardcore/src/bsps/stm32f767zi_conrig_v2"
+            ]
+        },
         {
             "name": "stm32f429zi_stm32f4discovery",
             "cStandard": "c11",
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2e28274d6..d44bbc9de 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -76,6 +76,10 @@ add_executable(con_rig-entry src/ConRIG/con_rig-entry.cpp ${CON_RIG_COMPUTER})
 target_include_directories(con_rig-entry PRIVATE ${OBSW_INCLUDE_DIRS})
 sbs_target(con_rig-entry stm32f429zi_con_rig)
 
+add_executable(conrig-v2-entry src/ConRIGv2/conrig-v2-entry.cpp ${CONRIG_V2_COMPUTER})
+target_include_directories(conrig-v2-entry PRIVATE ${OBSW_INCLUDE_DIRS})
+sbs_target(conrig-v2-entry stm32f767zi_conrig_v2)
+
 add_executable(rovie-groundstation-entry 
     src/Groundstation/Rovie/rovie-groundstation-entry.cpp 
     ${GROUNDSTATION_COMMON} ${GROUNDSTATION_ROVIE}
diff --git a/cmake/dependencies.cmake b/cmake/dependencies.cmake
index 27364e888..31286e428 100644
--- a/cmake/dependencies.cmake
+++ b/cmake/dependencies.cmake
@@ -73,6 +73,12 @@ set(CON_RIG_COMPUTER
     src/ConRIG/Serial/Serial.cpp
 )
 
+set(CONRIG_V2_COMPUTER
+    src/ConRIGv2/Buttons/Buttons.cpp
+    src/ConRIGv2/Radio/Radio.cpp
+    src/ConRIGv2/Hub/Hub.cpp
+)
+
 set(PAYLOAD_COMPUTER
     src/Payload/Actuators/Actuators.cpp
     src/Payload/CanHandler/CanHandler.cpp
diff --git a/src/ConRIG/BoardScheduler.h b/src/ConRIG/BoardScheduler.h
index 3736c89c1..741441282 100644
--- a/src/ConRIG/BoardScheduler.h
+++ b/src/ConRIG/BoardScheduler.h
@@ -1,5 +1,5 @@
-/* Copyright (c) 2022 Skyward Experimental Rocketry
- * Author: Alberto Nidasio
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Ettore Pane
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
diff --git a/src/ConRIGv2/BoardScheduler.h b/src/ConRIGv2/BoardScheduler.h
new file mode 100644
index 000000000..9ce95f48a
--- /dev/null
+++ b/src/ConRIGv2/BoardScheduler.h
@@ -0,0 +1,70 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Ettore Pane
+ *
+ * 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 <scheduler/TaskScheduler.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+namespace ConRIGv2
+{
+
+/**
+ * @brief Class that wraps the main task schedulers of the entire OBSW.
+ */
+class BoardScheduler : public Boardcore::Injectable
+{
+public:
+    BoardScheduler()
+        : radio{miosix::PRIORITY_MAX - 1}, buttons{miosix::PRIORITY_MAX - 2}
+    {
+    }
+
+    [[nodiscard]] bool start()
+    {
+        if (!radio.start())
+        {
+            LOG_ERR(logger, "Failed to start radio scheduler");
+            return false;
+        }
+
+        if (!buttons.start())
+        {
+            LOG_ERR(logger, "Failed to start buttons scheduler");
+            return false;
+        }
+
+        return true;
+    }
+
+    Boardcore::TaskScheduler& getRadioScheduler() { return radio; }
+
+    Boardcore::TaskScheduler& getButtonsScheduler() { return buttons; }
+
+private:
+    Boardcore::PrintLogger logger =
+        Boardcore::Logging::getLogger("boardscheduler");
+
+    Boardcore::TaskScheduler radio;
+    Boardcore::TaskScheduler buttons;
+};
+}  // namespace ConRIGv2
diff --git a/src/ConRIGv2/Buses.h b/src/ConRIGv2/Buses.h
new file mode 100644
index 000000000..e5502a21d
--- /dev/null
+++ b/src/ConRIGv2/Buses.h
@@ -0,0 +1,51 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Ettore Pane
+ *
+ * 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 <miosix.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+namespace ConRIGv2
+{
+
+struct Buses : public Boardcore::Injectable
+{
+private:
+    Boardcore::SPIBus spi1{SPI1};
+    Boardcore::SPIBus spi6{SPI6};
+
+    Boardcore::USART usart2{USART2, 115200};
+    Boardcore::USART uart4{UART4, 115200};
+
+public:
+    Boardcore::SPIBus& getRadio() { return spi6; }
+
+    Boardcore::USART& getUsart2() { return usart2; }
+    Boardcore::USART& getUsart4() { return uart4; }
+
+    Boardcore::SPIBus& getEthernet() { return spi1; }
+};
+
+}  // namespace ConRIGv2
diff --git a/src/ConRIGv2/Buttons/Buttons.cpp b/src/ConRIGv2/Buttons/Buttons.cpp
new file mode 100644
index 000000000..e7056ab79
--- /dev/null
+++ b/src/ConRIGv2/Buttons/Buttons.cpp
@@ -0,0 +1,245 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Ettore Pane
+ *
+ * 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 "Buttons.h"
+
+#include <ConRIGv2/BoardScheduler.h>
+#include <ConRIGv2/Configs/ButtonsConfig.h>
+#include <ConRIGv2/Radio/Radio.h>
+#include <interfaces-impl/hwmapping.h>
+
+using namespace std;
+using namespace miosix;
+using namespace Boardcore;
+using namespace ConRIGv2;
+
+Buttons::Buttons()
+{
+    resetState();
+    state.arm_switch = false;
+}
+
+bool Buttons::start()
+{
+    TaskScheduler& scheduler = getModule<BoardScheduler>()->getRadioScheduler();
+
+    return scheduler.addTask([this]() { periodicStatusCheck(); },
+                             Config::Buttons::BUTTON_SAMPLE_PERIOD) != 0;
+}
+
+mavlink_conrig_state_tc_t Buttons::getState() { return state; }
+
+void Buttons::resetState()
+{
+    // Preserve the arm switch state
+    auto armSwitch   = state.arm_switch;
+    state            = {};
+    state.arm_switch = armSwitch;
+}
+
+void Buttons::periodicStatusCheck()
+{
+    state.arm_switch = btns::arm::value();
+
+    if (!btns::ignition::value() && state.arm_switch)
+    {
+        if (guard > Config::Buttons::GUARD_THRESHOLD)
+        {
+            guard              = 0;
+            state.ignition_btn = true;
+            LOG_DEBUG(logger, "Ignition button pressed");
+        }
+        else
+        {
+            guard++;
+        }
+    }
+    else if (btns::ox_filling::value())
+    {
+        if (guard > Config::Buttons::GUARD_THRESHOLD)
+        {
+            guard                = 0;
+            state.ox_filling_btn = true;
+            LOG_DEBUG(logger, "Ox filling button pressed");
+        }
+        else
+        {
+            guard++;
+        }
+    }
+    else if (btns::ox_release::value())
+    {
+        if (guard > Config::Buttons::GUARD_THRESHOLD)
+        {
+            guard                = 0;
+            state.ox_release_btn = true;
+            LOG_DEBUG(logger, "Ox release button pressed");
+        }
+        else
+        {
+            guard++;
+        }
+    }
+    else if (btns::ox_detach::value())
+    {
+        if (guard > Config::Buttons::GUARD_THRESHOLD)
+        {
+            guard               = 0;
+            state.ox_detach_btn = true;
+            LOG_DEBUG(logger, "Ox detach button pressed");
+        }
+        else
+        {
+            guard++;
+        }
+    }
+    else if (btns::n2_3way::value())
+    {
+        if (guard > Config::Buttons::GUARD_THRESHOLD)
+        {
+            guard               = 0;
+            state.ox_detach_btn = true;
+            LOG_DEBUG(logger, "n2 3way button pressed");
+        }
+        else
+        {
+            guard++;
+        }
+    }
+    else if (btns::n2_filling::value())
+    {
+        if (guard > Config::Buttons::GUARD_THRESHOLD)
+        {
+            guard                = 0;
+            state.n2_filling_btn = true;
+            LOG_DEBUG(logger, "N2 filling button pressed");
+        }
+        else
+        {
+            guard++;
+        }
+    }
+    else if (btns::n2_release::value())
+    {
+        if (guard > Config::Buttons::GUARD_THRESHOLD)
+        {
+            guard                = 0;
+            state.n2_release_btn = true;
+            LOG_DEBUG(logger, "N2 release button pressed");
+        }
+        else
+        {
+            guard++;
+        }
+    }
+    else if (btns::n2_detach::value())
+    {
+        if (guard > Config::Buttons::GUARD_THRESHOLD)
+        {
+            guard               = 0;
+            state.n2_detach_btn = true;
+            LOG_DEBUG(logger, "N2 detach button pressed");
+        }
+        else
+        {
+            guard++;
+        }
+    }
+    else if (btns::nitrogen::value())
+    {
+        if (guard > Config::Buttons::GUARD_THRESHOLD)
+        {
+            guard              = 0;
+            state.nitrogen_btn = true;
+            LOG_DEBUG(logger, "Nitrogen button pressed");
+        }
+        else
+        {
+            guard++;
+        }
+    }
+    else if (btns::ox_venting::value())
+    {
+        if (guard > Config::Buttons::GUARD_THRESHOLD)
+        {
+            guard                = 0;
+            state.ox_venting_btn = true;
+            LOG_DEBUG(logger, "Ox venting button pressed");
+        }
+        else
+        {
+            guard++;
+        }
+    }
+    else if (btns::n2_quenching::value())
+    {
+        if (guard > Config::Buttons::GUARD_THRESHOLD)
+        {
+            guard                  = 0;
+            state.n2_quenching_btn = true;
+            LOG_DEBUG(logger, "N2 quenching button pressed");
+        }
+        else
+        {
+            guard++;
+        }
+    }
+    else if (btns::tars3::value())
+    {
+        if (guard > Config::Buttons::GUARD_THRESHOLD)
+        {
+            guard           = 0;
+            state.tars3_btn = true;
+            LOG_DEBUG(logger, "Tars3 button pressed");
+        }
+        else
+        {
+            guard++;
+        }
+    }
+    else if (btns::tars3m::value())
+    {
+        if (guard > Config::Buttons::GUARD_THRESHOLD)
+        {
+            guard            = 0;
+            state.tars3m_btn = true;
+            LOG_DEBUG(logger, "Tars3m button pressed");
+        }
+        else
+        {
+            guard++;
+        }
+    }
+    else
+    {
+        // Reset all the states and guard
+        guard = 0;
+        resetState();
+    }
+
+    // Set the internal button state in Radio module
+    getModule<Radio>()->setButtonsState(state);
+}
+
+void Buttons::enableIgnition() { ui::armedLed::high(); }
+
+void Buttons::disableIgnition() { ui::armedLed::low(); }
diff --git a/src/ConRIGv2/Buttons/Buttons.h b/src/ConRIGv2/Buttons/Buttons.h
new file mode 100644
index 000000000..5d2c26d0e
--- /dev/null
+++ b/src/ConRIGv2/Buttons/Buttons.h
@@ -0,0 +1,61 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Ettore Pane
+ *
+ * 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 <ConRIGv2/BoardScheduler.h>
+#include <common/MavlinkOrion.h>
+#include <diagnostic/PrintLogger.h>
+#include <scheduler/TaskScheduler.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+namespace ConRIGv2
+{
+
+class Radio;
+
+class Buttons : public Boardcore::InjectableWithDeps<BoardScheduler, Radio>
+{
+public:
+    Buttons();
+
+    [[nodiscard]] bool start();
+
+    mavlink_conrig_state_tc_t getState();
+
+    void enableIgnition();
+    void disableIgnition();
+
+private:
+    void resetState();
+
+    void periodicStatusCheck();
+
+    mavlink_conrig_state_tc_t state;
+
+    // Counter guard to avoid spurious triggers
+    uint8_t guard = 0;
+
+    Boardcore::PrintLogger logger = Boardcore::Logging::getLogger("buttons");
+};
+
+}  // namespace ConRIGv2
diff --git a/src/ConRIGv2/Configs/ButtonsConfig.h b/src/ConRIGv2/Configs/ButtonsConfig.h
new file mode 100644
index 000000000..8ede7d400
--- /dev/null
+++ b/src/ConRIGv2/Configs/ButtonsConfig.h
@@ -0,0 +1,44 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Ettore Pane
+ *
+ * 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/Frequency.h>
+
+namespace ConRIGv2
+{
+namespace Config
+{
+namespace Buttons
+{
+
+/* linter off */ using namespace Boardcore::Units::Frequency;
+
+constexpr Hertz BUTTON_SAMPLE_PERIOD = 50_hz;
+
+constexpr uint8_t GUARD_THRESHOLD =
+    5;  // 5 samples to trigger the guard and activate a single button
+
+}  // namespace Buttons
+}  // namespace Config
+
+}  // namespace ConRIGv2
diff --git a/src/ConRIGv2/Configs/RadioConfig.h b/src/ConRIGv2/Configs/RadioConfig.h
new file mode 100644
index 000000000..8f2695e67
--- /dev/null
+++ b/src/ConRIGv2/Configs/RadioConfig.h
@@ -0,0 +1,55 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Ettore Pane
+ *
+ * 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>
+
+namespace ConRIGv2
+{
+
+namespace Config
+{
+namespace Radio
+{
+
+/* linter off */ using namespace Boardcore::Units::Frequency;
+
+constexpr unsigned int MAV_OUT_QUEUE_SIZE = 20;
+constexpr unsigned int MAV_MAX_LENGTH     = MAVLINK_MAX_DIALECT_PAYLOAD_SIZE;
+
+constexpr unsigned int CIRCULAR_BUFFER_SIZE = 10;
+
+constexpr uint16_t MAV_SLEEP_AFTER_SEND = 0;
+constexpr size_t MAV_OUT_BUFFER_MAX_AGE = 10;
+
+// Mavlink ids
+constexpr uint8_t MAV_SYSTEM_ID    = 171;
+constexpr uint8_t MAV_COMPONENT_ID = 96;
+
+// Periodic telemetries frequency
+constexpr Hertz PING_GSE_PERIOD = 2_hz;
+
+}  // namespace Radio
+}  // namespace Config
+}  // namespace ConRIGv2
diff --git a/src/ConRIGv2/Hub/Hub.cpp b/src/ConRIGv2/Hub/Hub.cpp
new file mode 100644
index 000000000..1b12bec43
--- /dev/null
+++ b/src/ConRIGv2/Hub/Hub.cpp
@@ -0,0 +1,171 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Ettore Pane
+ *
+ * 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 "Hub.h"
+
+#include <ConRIGv2/Buses.h>
+#include <ConRIGv2/Radio/Radio.h>
+#include <Groundstation/Common/Config/EthernetConfig.h>
+#include <interfaces-impl/hwmapping.h>
+
+using namespace Boardcore;
+using namespace miosix;
+
+static std::weak_ptr<Wiz5500> gEthernet;
+
+void __attribute__((used)) MIOSIX_ETHERNET_IRQ()
+{
+    if (auto ethernet = gEthernet.lock())
+        ethernet->handleINTn();
+}
+
+namespace ConRIGv2
+{
+WizIp generateRandomIp()
+{
+    WizIp ip = Groundstation::IP_BASE;
+    ip.d     = (rand() % 253) + 1;  // Generate in range 1-254
+
+    return ip;
+}
+
+WizMac generateRandomMac()
+{
+    WizMac mac = Groundstation::MAC_BASE;
+    mac.e      = (rand() % 253) + 1;  // Generate in range 1-254
+    mac.f      = (rand() % 253) + 1;  // Generate in range 1-254
+
+    return mac;
+}
+
+Boardcore::Wiz5500::PhyState Hub::getEthernetState()
+{
+    return wiz5500->getPhyState();
+}
+
+bool Hub::start()
+{
+    auto* buses = getModule<Buses>();
+
+    serial2 = std::make_unique<SerialTransceiver>(buses->getUsart2());
+    serial4 = std::make_unique<SerialTransceiver>(buses->getUsart4());
+
+    mavDriver2 = std::make_unique<SerialMavDriver>(
+        serial2.get(), [this](auto channel, const mavlink_message_t& msg)
+        { dispatchToRIG(msg); }, 0, 10);
+
+    mavDriver4 = std::make_unique<SerialMavDriver>(
+        serial4.get(), [this](auto channel, const mavlink_message_t& msg)
+        { dispatchToRIG(msg); }, 0, 10);
+
+    if (!mavDriver2->start())
+    {
+        LOG_ERR(logger, "Error starting the MAVLink driver for USART2");
+        return false;
+    }
+
+    if (!mavDriver4->start())
+    {
+        LOG_ERR(logger, "Error starting the MAVLink driver for USART4");
+        return false;
+    }
+
+    if (!initEthernet())
+    {
+        LOG_ERR(logger, "Error starting the Ethernet driver");
+        return false;
+    }
+
+    mavDriverEth = std::make_unique<EthernetMavDriver>(
+        ethernet.get(),
+        [this](EthernetMavDriver* channel, const mavlink_message_t& msg)
+        { dispatchToRIG(msg); }, 0, 10);
+
+    if (!mavDriverEth->start())
+        return false;
+
+    return true;
+}
+
+bool Hub::initEthernet()
+{
+    auto* buses = getModule<Buses>();
+
+    // Initialize the Wiz5500
+    wiz5500 = std::make_unique<Wiz5500>(
+        buses->getEthernet(), ethernet::cs::getPin(), ethernet::intr::getPin(),
+        SPI::ClockDivider::DIV_64);
+
+    // Store the global ethernet instance for the interrupt handler
+    gEthernet = wiz5500;
+
+    // Check SPI communication
+    if (!wiz5500->checkVersion())
+    {
+        LOG_ERR(logger, "Error checking the Wiz5500 version");
+        return false;
+    }
+
+    // Reset the device
+    wiz5500->reset();
+
+    // Setup ip and other stuff
+    wiz5500->setSubnetMask(Groundstation::SUBNET);
+    wiz5500->setGatewayIp(Groundstation::GATEWAY);
+    wiz5500->setSourceIp(generateRandomIp());
+    wiz5500->setSourceMac(generateRandomMac());
+
+    wiz5500->setOnIpConflict([this]()
+                             { wiz5500->setSourceIp(generateRandomIp()); });
+
+    // Ok now open the UDP socket
+    if (!wiz5500->openUdp(0, Groundstation::RECV_PORT, {255, 255, 255, 255},
+                          Groundstation::SEND_PORT, 500))
+    {
+        LOG_ERR(logger, "Error opening the UDP socket");
+        return false;
+    }
+
+    // Initialize Ethernet Transceiver
+    ethernet = std::make_unique<UdpTransceiver>(this->wiz5500, 0);
+
+    return true;
+}
+
+void Hub::dispatchToPorts(const mavlink_message_t& msg)
+{
+    if (mavDriver2 && mavDriver2->isStarted())
+        mavDriver2->enqueueMsg(msg);
+
+    if (mavDriver4 && mavDriver4->isStarted())
+        mavDriver4->enqueueMsg(msg);
+
+    if (mavDriverEth && mavDriverEth->isStarted())
+        mavDriverEth->enqueueMsg(msg);
+}
+
+void Hub::dispatchToRIG(const mavlink_message_t& msg)
+{
+    if (msg.sysid == MAV_SYSID_RIG)
+        getModule<Radio>()->enqueueMessage(msg);
+}
+}  // namespace ConRIGv2
diff --git a/src/ConRIGv2/Hub/Hub.h b/src/ConRIGv2/Hub/Hub.h
new file mode 100644
index 000000000..ada16a0a7
--- /dev/null
+++ b/src/ConRIGv2/Hub/Hub.h
@@ -0,0 +1,81 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Ettore Pane
+ *
+ * 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 <drivers/WIZ5500/WIZ5500.h>
+#include <radio/MavlinkDriver/MavlinkDriver.h>
+#include <radio/SerialTransceiver/SerialTransceiver.h>
+#include <radio/UdpTransceiver/UdpTransceiver.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+#include <memory>
+
+namespace ConRIGv2
+{
+class Buses;
+class Radio;
+
+using SerialMavDriver =
+    Boardcore::MavlinkDriver<1024, 10, MAVLINK_MAX_DIALECT_PAYLOAD_SIZE>;
+using EthernetMavDriver =
+    Boardcore::MavlinkDriver<1024, 10, MAVLINK_MAX_DIALECT_PAYLOAD_SIZE>;
+
+/**
+ * @brief Central hub connecting all outgoing and incoming modules.
+ */
+class Hub : public Boardcore::InjectableWithDeps<Buses, Radio>
+{
+public:
+    bool start();
+
+    /**
+     * @brief Dispatch messages to external ports (serial, ethernet).
+     */
+    void dispatchToPorts(const mavlink_message_t& msg);
+
+    /**
+     * @brief Dispatch messages to the RIG.
+     */
+    void dispatchToRIG(const mavlink_message_t& msg);
+
+    Boardcore::Wiz5500::PhyState getEthernetState();
+
+private:
+    bool initEthernet();
+
+    std::unique_ptr<Boardcore::SerialTransceiver> serial2;
+    std::unique_ptr<SerialMavDriver> mavDriver2;
+
+    std::unique_ptr<Boardcore::SerialTransceiver> serial4;
+    std::unique_ptr<SerialMavDriver> mavDriver4;
+
+    std::shared_ptr<Boardcore::Wiz5500> wiz5500;
+    std::unique_ptr<Boardcore::UdpTransceiver> ethernet;
+    std::unique_ptr<EthernetMavDriver> mavDriverEth;
+
+    bool started = false;
+
+    Boardcore::PrintLogger logger = Boardcore::Logging::getLogger("Hub");
+};
+}  // namespace ConRIGv2
diff --git a/src/ConRIGv2/Radio/Radio.cpp b/src/ConRIGv2/Radio/Radio.cpp
new file mode 100644
index 000000000..8e7ebb843
--- /dev/null
+++ b/src/ConRIGv2/Radio/Radio.cpp
@@ -0,0 +1,245 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Ettore Pane
+ *
+ * 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 <common/MavlinkOrion.h>
+#include <common/Radio.h>
+#include <diagnostic/SkywardStack.h>
+#include <drivers/interrupt/external_interrupts.h>
+#include <events/EventBroker.h>
+#include <interfaces-impl/hwmapping.h>
+#include <radio/SX1278/SX1278Frontends.h>
+
+#include <thread>
+
+using namespace miosix;
+using namespace Boardcore;
+using namespace ConRIGv2;
+using namespace Common;
+
+SX1278Lora* gRadio{nullptr};
+
+void handleDioIRQ()
+{
+    SX1278Lora* instance = gRadio;
+    if (instance)
+        instance->handleDioIRQ();
+}
+
+void setIRQRadio(SX1278Lora* radio)
+{
+    FastInterruptDisableLock dl;
+    gRadio = radio;
+}
+
+void __attribute__((used)) MIOSIX_RADIO_DIO0_IRQ() { handleDioIRQ(); }
+void __attribute__((used)) MIOSIX_RADIO_DIO1_IRQ() { handleDioIRQ(); }
+void __attribute__((used)) MIOSIX_RADIO_DIO3_IRQ() { handleDioIRQ(); }
+
+void Radio::handleMessage(const mavlink_message_t& msg)
+{
+    switch (msg.msgid)
+    {
+        case MAVLINK_MSG_ID_GSE_TM:
+        {
+            int armingState = mavlink_msg_gse_tm_get_arming_state(&msg);
+            messagesReceived += 1;
+
+            isArmed = armingState == 1;
+            if (armingState == 1)
+                getModule<Buttons>()->enableIgnition();
+            else
+                getModule<Buttons>()->disableIgnition();
+
+            break;
+        }
+
+        case MAVLINK_MSG_ID_ACK_TM:
+        {
+            int id = mavlink_msg_ack_tm_get_recv_msgid(&msg);
+            // we assume this ack is about the last sent message
+            if (id == MAVLINK_MSG_ID_CONRIG_STATE_TC)
+            {
+                Lock<FastMutex> lock{buttonsMutex};
+                // Reset the internal button state
+                buttonState = {};
+            }
+
+            break;
+        }
+    }
+
+    getModule<Hub>()->dispatchToPorts(msg);
+}
+
+bool Radio::enqueueMessage(const mavlink_message_t& msg)
+{
+    Lock<FastMutex> lock{queueMutex};
+    if (messageQueue.isFull())
+    {
+        return false;
+    }
+    else
+    {
+        messageQueue.put(msg);
+        return true;
+    }
+}
+
+void Radio::sendPeriodicPing()
+{
+    mavlink_message_t msg;
+
+    {
+        Lock<FastMutex> lock{buttonsMutex};
+        mavlink_msg_conrig_state_tc_encode(Config::Radio::MAV_SYSTEM_ID,
+                                           Config::Radio::MAV_COMPONENT_ID,
+                                           &msg, &buttonState);
+    }
+
+    // Flush the queue
+    {
+        Lock<FastMutex> lock{queueMutex};
+        // TODO(davide.mor): Maybe implement a maximum per ping?
+        for (size_t i = 0; i < messageQueue.count(); i++)
+        {
+            try
+            {
+                mavDriver->enqueueMsg(messageQueue.pop());
+            }
+            catch (...)
+            {
+                // This shouldn't happen, but still try to prevent it
+            }
+        }
+    }
+
+    // Finally make sure we always send the periodic ping
+    mavDriver->enqueueMsg(msg);
+}
+
+void Radio::buzzerOn()
+{
+    buzzer.enableChannel(TimerUtils::Channel::MIOSIX_BUZZER_CHANNEL);
+}
+
+void Radio::buzzerOff()
+{
+    buzzer.disableChannel(TimerUtils::Channel::MIOSIX_BUZZER_CHANNEL);
+}
+
+void Radio::buzzerTask()
+{
+    constexpr int beepPeriod = 5;  // seconds
+
+    if ((!isArmed && messagesReceived > beepPeriod * 2) ||
+        (isArmed && messagesReceived > 0))
+    {
+        messagesReceived = 0;
+        buzzerOn();
+    }
+    else
+    {
+        buzzerOff();
+    }
+}
+
+void Radio::setButtonsState(const mavlink_conrig_state_tc_t& state)
+{
+    Lock<FastMutex> lock{buttonsMutex};
+    // The OR operator is introduced to make sure that the receiver
+    // understood the command
+    buttonState.ox_filling_btn |= state.ox_filling_btn;
+    buttonState.ox_release_btn |= state.ox_release_btn;
+    buttonState.n2_filling_btn |= state.n2_filling_btn;
+    buttonState.n2_release_btn |= state.n2_release_btn;
+    buttonState.n2_detach_btn |= state.n2_detach_btn;
+    buttonState.ox_venting_btn |= state.ox_venting_btn;
+    buttonState.nitrogen_btn |= state.nitrogen_btn;
+    buttonState.ox_detach_btn |= state.ox_detach_btn;
+    buttonState.n2_quenching_btn |= state.n2_quenching_btn;
+    buttonState.n2_3way_btn |= state.n2_3way_btn;
+    buttonState.tars3_btn |= state.tars3_btn;
+    buttonState.tars3m_btn |= state.tars3m_btn;
+    buttonState.ignition_btn |= state.ignition_btn;
+    buttonState.arm_switch |= state.arm_switch;
+}
+
+bool Radio::start()
+{
+    // Setup the frontend
+    std::unique_ptr<SX1278::ISX1278Frontend> frontend =
+        std::make_unique<EbyteFrontend>(radio::txEn::getPin(),
+                                        radio::rxEn::getPin());
+
+    // Setup transceiver
+    radio = std::make_unique<SX1278Lora>(
+        getModule<Buses>()->getRadio(), radio::cs::getPin(),
+        radio::dio0::getPin(), radio::dio1::getPin(), radio::dio3::getPin(),
+        SPI::ClockDivider::DIV_64, std::move(frontend));
+
+    // Store the global radio instance
+    setIRQRadio(radio.get());
+
+    // Initialize radio
+    auto result = radio->init(RIG_RADIO_CONFIG);
+    if (result != SX1278Lora::Error::NONE)
+    {
+        LOG_ERR(logger, "Failed to initialize RIG radio");
+        return false;
+    }
+
+    // Initialize mavdriver
+    mavDriver = std::make_unique<MavDriver>(
+        radio.get(), [this](MavDriver*, const mavlink_message_t& msg)
+        { handleMessage(msg); }, Config::Radio::MAV_SLEEP_AFTER_SEND,
+        Config::Radio::MAV_OUT_BUFFER_MAX_AGE);
+
+    if (!mavDriver->start())
+    {
+        LOG_ERR(logger, "Failed to initialize ConRIGv2 mav driver");
+        return false;
+    }
+
+    TaskScheduler& scheduler = getModule<BoardScheduler>()->getRadioScheduler();
+
+    if (scheduler.addTask([this]() { sendPeriodicPing(); },
+                          Config::Radio::PING_GSE_PERIOD,
+                          TaskScheduler::Policy::RECOVER) == 0)
+    {
+        LOG_ERR(logger, "Failed to add ping task");
+        return false;
+    }
+
+    scheduler.addTask([this]() { buzzerTask(); }, 50,
+                      TaskScheduler::Policy::RECOVER);
+
+    return true;
+}
+
+MavlinkStatus Radio::getMavlinkStatus() { return mavDriver->getStatus(); }
+
+Radio::Radio() : buzzer(MIOSIX_BUZZER_TIM, 523), buttonState()
+{
+    buzzer.setDutyCycle(TimerUtils::Channel::MIOSIX_BUZZER_CHANNEL, 0.5);
+}
diff --git a/src/ConRIGv2/Radio/Radio.h b/src/ConRIGv2/Radio/Radio.h
new file mode 100644
index 000000000..399eb42fb
--- /dev/null
+++ b/src/ConRIGv2/Radio/Radio.h
@@ -0,0 +1,95 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Ettore Pane
+ *
+ * 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 <ConRIGv2/BoardScheduler.h>
+#include <ConRIGv2/Buses.h>
+#include <ConRIGv2/Buttons/Buttons.h>
+#include <ConRIGv2/Configs/RadioConfig.h>
+#include <ConRIGv2/Hub/Hub.h>
+#include <common/MavlinkOrion.h>
+#include <diagnostic/PrintLogger.h>
+#include <drivers/timer/PWM.h>
+#include <radio/MavlinkDriver/MavlinkDriver.h>
+#include <radio/SX1278/SX1278Lora.h>
+#include <scheduler/TaskScheduler.h>
+#include <utils/DependencyManager/DependencyManager.h>
+#include <utils/collections/CircularBuffer.h>
+
+#include <cstdint>
+#include <thread>
+
+namespace ConRIGv2
+{
+
+using MavDriver = Boardcore::MavlinkDriver<Boardcore::SX1278Lora::MTU,
+                                           Config::Radio::MAV_OUT_QUEUE_SIZE,
+                                           Config::Radio::MAV_MAX_LENGTH>;
+
+class Radio
+    : public Boardcore::InjectableWithDeps<Buses, BoardScheduler, Buttons, Hub>
+{
+public:
+    Radio();
+
+    [[nodiscard]] bool start();
+
+    Boardcore::MavlinkStatus getMavlinkStatus();
+
+    void setButtonsState(const mavlink_conrig_state_tc_t& state);
+
+    bool enqueueMessage(const mavlink_message_t& msg);
+
+private:
+    void sendPeriodicPing();
+    void buzzerTask();
+    void handleMessage(const mavlink_message_t& msg);
+
+    std::unique_ptr<Boardcore::SX1278Lora> radio;
+    std::unique_ptr<MavDriver> mavDriver;
+
+    void buzzerOn();
+    void buzzerOff();
+
+    Boardcore::PWM buzzer;
+
+    std::atomic<uint32_t> buzzerCounter{0};
+    std::atomic<uint32_t> buzzerOverflow{100};
+
+    Boardcore::CircularBuffer<mavlink_message_t,
+                              Config::Radio::CIRCULAR_BUFFER_SIZE>
+        messageQueue;
+
+    miosix::FastMutex queueMutex;
+    miosix::FastMutex buttonsMutex;
+
+    // Button internal state
+    mavlink_conrig_state_tc_t buttonState;
+
+    std::atomic<uint8_t> messagesReceived{0};
+    std::atomic<bool> isArmed{false};
+
+    Boardcore::PrintLogger logger = Boardcore::Logging::getLogger("radio");
+};
+
+}  // namespace ConRIGv2
diff --git a/src/ConRIGv2/conrig-v2-entry.cpp b/src/ConRIGv2/conrig-v2-entry.cpp
new file mode 100644
index 000000000..de3105b2e
--- /dev/null
+++ b/src/ConRIGv2/conrig-v2-entry.cpp
@@ -0,0 +1,109 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Ettore Pane
+ *
+ * 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 <ConRIGv2/BoardScheduler.h>
+#include <ConRIGv2/Buses.h>
+#include <ConRIGv2/Buttons/Buttons.h>
+#include <ConRIGv2/Configs/ButtonsConfig.h>
+#include <ConRIGv2/Hub/Hub.h>
+#include <ConRIGv2/Radio/Radio.h>
+#include <diagnostic/CpuMeter/CpuMeter.h>
+#include <diagnostic/PrintLogger.h>
+#include <events/EventBroker.h>
+#include <interfaces-impl/hwmapping.h>
+#include <miosix.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+#include <iostream>
+#include <thread>
+
+using namespace miosix;
+using namespace Boardcore;
+using namespace ConRIGv2;
+
+int main()
+{
+    PrintLogger logger = Logging::getLogger("main");
+    DependencyManager manager;
+
+    Buses* buses              = new Buses();
+    BoardScheduler* scheduler = new BoardScheduler();
+    Radio* radio              = new Radio();
+    Buttons* buttons          = new Buttons();
+    Hub* hub                  = new Hub();
+
+    bool initResult = manager.insert<BoardScheduler>(scheduler) &&
+                      manager.insert<Buses>(buses) &&
+                      manager.insert<Radio>(radio) &&
+                      manager.insert<Hub>(hub) &&
+                      manager.insert<Buttons>(buttons) && manager.inject();
+
+    manager.graphviz(std::cout);
+
+    if (!initResult)
+    {
+        LOG_ERR(logger, "Failed to inject dependencies");
+        return 0;
+    }
+
+    if (!radio->start())
+    {
+        initResult = false;
+        LOG_ERR(logger, "Error starting the radio");
+    }
+
+    if (!hub->start())
+    {
+        initResult = false;
+        LOG_ERR(logger, "Error starting the mavlink dispatcher hub");
+    }
+
+    if (!buttons->start())
+    {
+        initResult = false;
+        LOG_ERR(logger, "Error starting the buttons");
+    }
+
+    if (!scheduler->start())
+    {
+        initResult = false;
+        LOG_ERR(logger, "Error starting the General Purpose Scheduler");
+    }
+
+    if (!initResult)
+    {
+        LOG_ERR(logger, "Init failure!");
+        led2On();  // Led RED
+    }
+    else
+    {
+        LOG_INFO(logger, "All good!");
+        led4On();  // Led GREEN
+    }
+
+    // Periodical statistics
+    while (true)
+    {
+        Thread::sleep(1000);
+        CpuMeter::resetCpuStats();
+    }
+}
-- 
GitLab