diff --git a/justfile b/justfile
index ce734005edcfe218cf1b0e3081e1ff35e982a948..07fe54015a21d49ccce824cb90f74265b03d1ec1 100644
--- a/justfile
+++ b/justfile
@@ -1,7 +1,8 @@
 export RUST_LOG := "debug"
 
-alias db := device-build
 alias hb := host-build
+alias dbs := device-build-sender
+alias dbr := device-build-receiver
 
 default:
     ./arpist gemini.csv
@@ -12,7 +13,12 @@ host-build:
     cargo build --release
     cp target/release/arpist ..
 
-device-build:
+device-build-sender:
     #!/bin/bash
     cd on-device
-    ./sbs -df test-mavlink-parse
+    ./sbs -df arpist-device-sender
+
+device-build-receiver:
+    #!/bin/bash
+    cd on-device
+    ./sbs -df arpist-device-receiver
diff --git a/on-device/CMakeLists.txt b/on-device/CMakeLists.txt
index 390a8d6e75ef5209504a311345ae84db7db239a3..e11574bbe17f9a694b11f72f1ebb87f495ff8c1b 100644
--- a/on-device/CMakeLists.txt
+++ b/on-device/CMakeLists.txt
@@ -30,60 +30,17 @@ include(libs/boardcore/cmake/sbs.cmake)
 project(Arpist)
 
 #-----------------------------------------------------------------------------#
-#                              Flight entrypoints                             #
+#                              Arpist entrypoints                             #
 #-----------------------------------------------------------------------------#
 
 add_executable(test-mavlink-parse src/entrypoints/test_mavlink_parse.cpp ${RADIO})
 target_include_directories(test-mavlink-parse PRIVATE ${ARPIST_INCLUDE_DIRS})
 sbs_target(test-mavlink-parse stm32f429zi_skyward_groundstation_v2)
 
-# add_executable(test-hil src/entrypoints/HIL/test-hil.cpp ${HIL})
-# target_include_directories(test-hil PRIVATE ${OBSW_INCLUDE_DIRS})
-# target_compile_definitions(test-hil PRIVATE HILTest)
-# sbs_target(test-hil stm32f767zi_compute_unit)
+add_executable(arpist-device-sender src/entrypoints/arpist_device_sender.cpp ${RADIO})
+target_include_directories(arpist-device-sender PRIVATE ${ARPIST_INCLUDE_DIRS})
+sbs_target(arpist-device-sender stm32f429zi_skyward_groundstation_v2)
 
-# add_executable(base-groundstation-entry
-#     src/entrypoints/Groundstation/base-groundstation-entry.cpp
-#     ${GROUNDSTATION_COMMON} ${GROUNDSTATION_BASE}
-# )
-# target_include_directories(base-groundstation-entry PRIVATE ${OBSW_INCLUDE_DIRS})
-# sbs_target(base-groundstation-entry stm32f767zi_gemini_gs)
-
-# add_executable(nokia-groundstation-entry
-#     src/entrypoints/Groundstation/nokia-groundstation-entry.cpp
-#     ${GROUNDSTATION_COMMON} ${GROUNDSTATION_NOKIA}
-# )
-# target_include_directories(nokia-groundstation-entry PRIVATE ${OBSW_INCLUDE_DIRS})
-# sbs_target(nokia-groundstation-entry stm32f429zi_skyward_groundstation_v2)
-
-# add_executable(automated-antennas-entry
-#     src/entrypoints/Groundstation/Automated/automated-antennas-entry.cpp
-#     ${ANTENNAS} ${GROUNDSTATION_COMMON} ${GROUNDSTATION_AUTOMATED}
-# )
-# target_include_directories(automated-antennas-entry PRIVATE ${OBSW_INCLUDE_DIRS})
-# sbs_target(automated-antennas-entry stm32f767zi_automated_antennas)
-
-# add_executable(test-automated-radio
-#     src/entrypoints/Groundstation/Automated/test-automated-radio.cpp
-#     ${ANTENNAS} ${GROUNDSTATION_COMMON} ${GROUNDSTATION_AUTOMATED}
-# )
-# target_include_directories(test-automated-radio PRIVATE ${OBSW_INCLUDE_DIRS})
-# # target_compile_definitions(test-automated-radio PRIVATE NO_SD_LOGGING)
-# sbs_target(test-automated-radio stm32f767zi_automated_antennas)
-
-# add_executable(test-steps src/entrypoints/Groundstation/Automated/test-steps.cpp ${ANTENNAS})
-# target_include_directories(test-steps PRIVATE ${OBSW_INCLUDE_DIRS})
-# # target_compile_definitions(test-steps PRIVATE NO_SD_LOGGING)
-# sbs_target(test-steps stm32f767zi_automated_antennas)
-
-# add_executable(test-actuators src/entrypoints/Groundstation/Automated/test-actuators.cpp
-#     ${ANTENNAS} ${GROUNDSTATION_COMMON} ${GROUNDSTATION_AUTOMATED}
-# )
-# target_include_directories(test-actuators PRIVATE ${OBSW_INCLUDE_DIRS})
-# # target_compile_definitions(test-actuators PRIVATE NO_SD_LOGGING)
-# sbs_target(test-actuators stm32f767zi_automated_antennas)
-
-# add_executable(test-smcontroller src/entrypoints/Groundstation/Automated/test-smcontroller.cpp ${GROUNDSTATION_COMMON})
-# target_include_directories(test-smcontroller PRIVATE ${OBSW_INCLUDE_DIRS})
-# # target_compile_definitions(test-smcontroller PRIVATE NO_SD_LOGGING)
-# sbs_target(test-smcontroller stm32f767zi_nucleo)
+add_executable(arpist-device-receiver src/entrypoints/arpist_device_receiver.cpp ${RADIO})
+target_include_directories(arpist-device-receiver PRIVATE ${ARPIST_INCLUDE_DIRS})
+sbs_target(arpist-device-receiver stm32f429zi_skyward_groundstation_v2)
diff --git a/on-device/cmake/dependencies.cmake b/on-device/cmake/dependencies.cmake
index 0c2fafe3732ef912f2ab26c709c25986c2738e47..9a187f6768162930431860866ae336d8d7008975 100644
--- a/on-device/cmake/dependencies.cmake
+++ b/on-device/cmake/dependencies.cmake
@@ -25,5 +25,5 @@ set(ARPIST_INCLUDE_DIRS
 )
 
 set(RADIO
-    src/shared/Radio.cpp
+    src/shared/radio/Radio.cpp
 )
diff --git a/on-device/src/config/Mavlink.h b/on-device/src/config/Mavlink.h
new file mode 100644
index 0000000000000000000000000000000000000000..4313049ed8f4b0f7d2822d175931118a08506f70
--- /dev/null
+++ b/on-device/src/config/Mavlink.h
@@ -0,0 +1,29 @@
+/* Copyright (c) 2023 Skyward Experimental Rocketry
+ * Author: Matteo Pignataro
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+// Ignore warnings as these are auto-generated headers made by a third party
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wcast-align"
+#pragma GCC diagnostic ignored "-Waddress-of-packed-member"
+#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
+#include <mavlink_lib/lyra/mavlink.h>
+#pragma GCC diagnostic pop
diff --git a/on-device/src/config/RadioConfig.h b/on-device/src/config/RadioConfig.h
index 004914211489e5a0242cdb7ff8b9a213fa7a3289..7fba28f5b96374588e7a547b5988b8a2bb73bdad 100644
--- a/on-device/src/config/RadioConfig.h
+++ b/on-device/src/config/RadioConfig.h
@@ -24,15 +24,24 @@
 
 #include <radio/SX1278/SX1278Fsk.h>
 
+namespace Arpist
+{
+
+constexpr size_t MAV_OUT_QUEUE_SIZE     = 10;
+constexpr uint16_t MAV_SLEEP_AFTER_SEND = 0;
+constexpr size_t MAV_OUT_BUFFER_MAX_AGE = 10;
+
 // Radio configuration
 constexpr Boardcore::SX1278Fsk::Config RADIO_CONFIG = {
-    .freq_rf = 421000000,
-    .freq_dev = 50000,
-    .bitrate = 48000,
-    .rx_bw = Boardcore::SX1278Fsk::Config::RxBw::HZ_125000,
-    .afc_bw = Boardcore::SX1278Fsk::Config::RxBw::HZ_125000,
-    .ocp = 120,
-    .power = 13,
-    .shaping = Boardcore::SX1278Fsk::Config::Shaping::GAUSSIAN_BT_1_0,
-    .dc_free = Boardcore::SX1278Fsk::Config::DcFree::WHITENING,
-    .enable_crc = true};
+    .freq_rf    = 434000000,
+    .freq_dev   = 50000,
+    .bitrate    = 48000,
+    .rx_bw      = Boardcore::SX1278Fsk::Config::RxBw::HZ_125000,
+    .afc_bw     = Boardcore::SX1278Fsk::Config::RxBw::HZ_125000,
+    .ocp        = 120,
+    .power      = 13,
+    .shaping    = Boardcore::SX1278Fsk::Config::Shaping::GAUSSIAN_BT_1_0,
+    .dc_free    = Boardcore::SX1278Fsk::Config::DcFree::WHITENING,
+    .enable_crc = false};
+
+}  // namespace Arpist
diff --git a/on-device/src/entrypoints/arpist-device-sender.cpp b/on-device/src/entrypoints/arpist-device-sender.cpp
deleted file mode 100644
index fbd0ef5b6c28792b1cff761a0710d60fff318fdd..0000000000000000000000000000000000000000
--- a/on-device/src/entrypoints/arpist-device-sender.cpp
+++ /dev/null
@@ -1,301 +0,0 @@
-/* Copyright (c) 2024 Skyward Experimental Rocketry
- * Author: Federico Lolli
- *
- * 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 <drivers/interrupt/external_interrupts.h>
-#include <filesystem/console/console_device.h>
-#include <mavlink_lib/gemini/mavlink.h>
-// SX1278 includes
-#include <radio/SX1278/SX1278Frontends.h>
-#include <radio/SX1278/SX1278Fsk.h>
-#include <radio/SX1278/SX1278Lora.h>
-#include <config/radio.h>
-
-#include <iostream>
-#include <thread>
-
-using namespace miosix;
-
-#if defined _BOARD_STM32F429ZI_SKYWARD_GS_V2
-#include "interfaces-impl/hwmapping.h"
-
-using cs = peripherals::ra01::pc13::cs;
-using dio0 = peripherals::ra01::pc13::dio0;
-using dio1 = peripherals::ra01::pc13::dio1;
-using dio3 = peripherals::ra01::pc13::dio3;
-
-using sck = interfaces::spi4::sck;
-using miso = interfaces::spi4::miso;
-using mosi = interfaces::spi4::mosi;
-
-#define SX1278_SPI SPI4
-
-#define SX1278_IRQ_DIO0 EXTI6_IRQHandlerImpl
-#define SX1278_IRQ_DIO1 EXTI4_IRQHandlerImpl
-#define SX1278_IRQ_DIO3 EXTI11_IRQHandlerImpl
-#else
-#error "Target not supported"
-#endif
-
-// === CONSTANTS ===
-static constexpr size_t SX1278_MTU = Boardcore::SX1278Fsk::MTU;
-constexpr size_t PACKET_SIZE = MAVLINK_MSG_ID_PAYLOAD_FLIGHT_TM_LEN;
-
-/** @brief Number of packets to send */
-constexpr size_t MSG_NUM = 580;
-
-/** @brief End of transmission character */
-constexpr uint8_t ACK = 0x06;
-
-// === GLOBALS ===
-Boardcore::SX1278Fsk *sx1278 = nullptr;
-Boardcore::SPIBus sx1278_bus(SX1278_SPI);
-
-volatile int dio0_cnt = 0;
-volatile int dio1_cnt = 0;
-volatile int dio3_cnt = 0;
-
-// === INTERRUPTS ===
-#ifdef SX1278_IRQ_DIO0
-void __attribute__((used)) SX1278_IRQ_DIO0()
-{
-    dio0_cnt++;
-    if (sx1278)
-        sx1278->handleDioIRQ();
-}
-#endif
-
-#ifdef SX1278_IRQ_DIO1
-void __attribute__((used)) SX1278_IRQ_DIO1()
-{
-    dio1_cnt++;
-    if (sx1278)
-        sx1278->handleDioIRQ();
-}
-#endif
-
-#ifdef SX1278_IRQ_DIO3
-void __attribute__((used)) SX1278_IRQ_DIO3()
-{
-    dio3_cnt++;
-    if (sx1278)
-        sx1278->handleDioIRQ();
-}
-#endif
-
-// === DEFINITIONS ===
-void recvLoop();
-void sendLoop();
-mavlink_message_t readPacketFromSerial();
-void initBoard();
-
-// === MAIN ===
-int main()
-{
-    initBoard();
-
-#if defined SX1278_IS_SENDER
-    sendLoop();
-#elif defined SX1278_IS_RECEIVER
-    recvLoop();
-#else
-    // this may cause problems with stdin and stdout
-    // interfering with each other
-
-    // Actually spawn threads
-    std::thread send([]()
-                     { sendLoop(); });
-    recvLoop();
-#endif
-
-    return 0;
-}
-
-// === IMPLEMENTATIONS ===
-void recvLoop()
-{
-    uint8_t msg[SX1278_MTU];
-    while (1)
-    {
-        int len = sx1278->receive(msg, sizeof(msg));
-        if (len == PACKET_SIZE)
-        {
-            mavlink_payload_flight_tm_t tm;
-            memcpy(&tm, msg, PACKET_SIZE);
-
-            // auto serial = miosix::DefaultConsole::instance().get();
-            // serial->writeBlock(msg, len, 0);
-            std::cout << "[sx1278] Received packet - time: " << tm.timestamp
-                      << std::endl;
-            // std::cout << "[sx1278] tm.timestamp: " << tm.timestamp <<
-            // std::endl; std::cout << "[sx1278] tm.pressure_digi: " <<
-            // tm.pressure_digi
-            //           << std::endl;
-            // std::cout << "[sx1278] tm.pressure_static: " <<
-            // tm.pressure_static
-            //           << std::endl;
-            // std::cout << "[sx1278] tm.airspeed_pitot: " << tm.airspeed_pitot
-            //           << std::endl;
-            // std::cout << "[sx1278] tm.altitude_agl: " << tm.altitude_agl
-            //           << std::endl;
-            // std::cout << "[sx1278] tm.acc_x: " << tm.acc_x << std::endl;
-            // std::cout << "[sx1278] tm.acc_y: " << tm.acc_y << std::endl;
-            // std::cout << "[sx1278] tm.acc_z: " << tm.acc_z << std::endl;
-            // std::cout << "[sx1278] tm.gyro_x: " << tm.gyro_x << std::endl;
-            // std::cout << "[sx1278] tm.gyro_y: " << tm.gyro_y << std::endl;
-            // std::cout << "[sx1278] tm.gyro_z: " << tm.gyro_z << std::endl;
-            // std::cout << "[sx1278] tm.mag_x: " << tm.mag_x << std::endl;
-            // std::cout << "[sx1278] tm.mag_y: " << tm.mag_y << std::endl;
-            // std::cout << "[sx1278] tm.mag_z: " << tm.mag_z << std::endl;
-            // std::cout << "[sx1278] tm.gps_lat: " << tm.gps_lat << std::endl;
-            // std::cout << "[sx1278] tm.gps_lon: " << tm.gps_lon << std::endl;
-            // std::cout << "[sx1278] tm.gps_alt: " << tm.gps_alt << std::endl;
-            // std::cout << "[sx1278] tm.left_servo_angle: " <<
-            // tm.left_servo_angle
-            //           << std::endl;
-            // std::cout << "[sx1278] tm.right_servo_angle: "
-            //           << tm.right_servo_angle << std::endl;
-            // std::cout << "[sx1278] tm.nas_n: " << tm.nas_n << std::endl;
-            // std::cout << "[sx1278] tm.nas_e: " << tm.nas_e << std::endl;
-            // std::cout << "[sx1278] tm.nas_d: " << tm.nas_d << std::endl;
-            // std::cout << "[sx1278] tm.nas_vn: " << tm.nas_vn << std::endl;
-            // std::cout << "[sx1278] tm.nas_ve: " << tm.nas_ve << std::endl;
-            // std::cout << "[sx1278] tm.nas_vd: " << tm.nas_vd << std::endl;
-            // std::cout << "[sx1278] tm.nas_qx: " << tm.nas_qx << std::endl;
-            // std::cout << "[sx1278] tm.nas_qy: " << tm.nas_qy << std::endl;
-            // std::cout << "[sx1278] tm.nas_qz: " << tm.nas_qz << std::endl;
-            // std::cout << "[sx1278] tm.nas_qw: " << tm.nas_qw << std::endl;
-            // std::cout << "[sx1278] tm.nas_bias_x: " << tm.nas_bias_x
-            //           << std::endl;
-            // std::cout << "[sx1278] tm.nas_bias_y: " << tm.nas_bias_y
-            //           << std::endl;
-            // std::cout << "[sx1278] tm.nas_bias_z: " << tm.nas_bias_z
-            //           << std::endl;
-            // std::cout << "[sx1278] tm.wes_n: " << tm.wes_n << std::endl;
-            // std::cout << "[sx1278] tm.wes_e: " << tm.wes_e << std::endl;
-            // std::cout << "[sx1278] tm.vbat: " << tm.vbat << std::endl;
-            // std::cout << "[sx1278] tm.vsupply_5v: " << tm.vsupply_5v
-            //           << std::endl;
-            // std::cout << "[sx1278] tm.temperature: " << tm.temperature
-            //           << std::endl;
-            // std::cout << "[sx1278] tm.fmm_state: " << tm.fmm_state <<
-            // std::endl; std::cout << "[sx1278] tm.nas_state: " << tm.nas_state
-            // << std::endl; std::cout << "[sx1278] tm.wes_state: " <<
-            // tm.wes_state << std::endl; std::cout << "[sx1278] tm.gps_fix: "
-            // << tm.gps_fix << std::endl; std::cout << "[sx1278]
-            // tm.pin_nosecone: " << tm.pin_nosecone
-            //           << std::endl;
-            // std::cout << "[sx1278] tm.logger_error: " << tm.logger_error
-            //           << std::endl;
-        }
-    }
-}
-
-void sendLoop()
-{
-    uint8_t msg[SX1278_MTU];
-    while (1)
-    {
-        mavlink_message_t tm = readPacketFromSerial();
-        // std::cout << "[sx1278] Sending packet" << std::endl;
-        sx1278->send(&tm, PACKET_SIZE);
-    }
-}
-
-/**
- * @brief Read a packet from the serial port
- * @warning This function will parse raw bytes coming from
- * serial into the struct
- * @return mavlink_payload_flight_tm_t
- */
-mavlink_message_t readPacketFromSerial()
-{
-    mavlink_message_t msg;
-    bool stop_flag = false;
-    ssize_t rcv_size;
-    uint8_t serial_buffer[171];
-    uint8_t parse_result = 0;
-    mavlink_status_t status;
-
-    auto serial = DefaultConsole::instance().get();
-
-    while (!stop_flag)
-    {
-        // Check for a new message on the device
-        rcv_size = serial->readBlock(serial_buffer, 171, 0);
-        // printf("Received %d bytes\n", rcv_size);
-
-        // If there's a new message ...
-        if (rcv_size > 0)
-        {
-            for (int i = 0; i < rcv_size; i++)
-            {
-                parse_result = 0;
-                // ... parse received bytes
-                parse_result =
-                    mavlink_parse_char(MAVLINK_COMM_0,
-                                       serial_buffer[i], // byte to parse
-                                       &msg,             // where to parse it
-                                       &status);         // stats to update
-
-                // When a valid message is found ...
-                if (parse_result == 1)
-                    break;
-            }
-        }
-
-        break;
-    }
-
-    // printf("Received message with ID %d\n", msg.msgid);
-
-    serial->writeBlock(&ACK, 1, 0);
-
-    // this may be shrunk to the above statement (needs further testing)
-    // memcpy(ptr_to_tm, serial_buffer, PACKET_SIZE);
-
-    return msg;
-}
-
-void initBoard()
-{
-    printf("[sx1278] Configuring RA01 frontend...\n");
-    std::unique_ptr<Boardcore::SX1278::ISX1278Frontend> frontend(
-        new Boardcore::RA01Frontend());
-
-    // Run default configuration
-    Boardcore::SX1278Fsk::Error err;
-
-    sx1278 = new Boardcore::SX1278Fsk(sx1278_bus, cs::getPin(), dio0::getPin(),
-                                      dio1::getPin(), dio3::getPin(),
-                                      Boardcore::SPI::ClockDivider::DIV_256,
-                                      std::move(frontend));
-
-    printf("\n[sx1278] Configuring sx1278 fsk...\n");
-    if ((err = sx1278->init(RADIO_CONFIG)) != Boardcore::SX1278Fsk::Error::NONE)
-    {
-        // FIXME: Why does clang-format put this line up here?
-        printf("[sx1278] sx1278->init error\n");
-        return;
-    }
-
-    printf("\n[sx1278] Initialization complete!\n");
-}
diff --git a/on-device/src/entrypoints/arpist_device_receiver.cpp b/on-device/src/entrypoints/arpist_device_receiver.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..569c98c6a6f135155dcb34354c6ab4cb5d7170c1
--- /dev/null
+++ b/on-device/src/entrypoints/arpist_device_receiver.cpp
@@ -0,0 +1,86 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Federico Lolli
+ *
+ * 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 <filesystem/console/console_device.h>
+#include <mavlink_lib/gemini/mavlink.h>
+#include <radio/MavlinkDriver/MavlinkDriver.h>
+#include <shared/mavlink/Utils.h>
+#include <shared/radio/Radio.h>
+
+using namespace miosix;
+using namespace Boardcore;
+using namespace Arpist;
+
+// === CONSTANTS ===
+/** @brief End of transmission character */
+constexpr uint8_t ACK = 0x06;
+
+using MavDriver = MavlinkDriver<20, 10>;
+
+// === DEFINITIONS ===
+void onReceive(MavDriver *channel, mavlink_message_t msg);
+
+// === MAIN ===
+int main()
+{
+    mavlink_message_t msg;
+    mavlink_payload_flight_tm_t payload;
+    MavDriver *mavlink;
+
+    // init radio
+    if (Radio::init())
+    {
+        printf("Radio init success\n");
+    }
+    else
+    {
+        printf("Radio init failed\n");
+        return -1;
+    }
+
+    // init mavlink
+    mavlink = new MavDriver(sx1278, onReceive, 256);
+    if (mavlink->start())
+    {
+        printf("Mavlink init success\n");
+    }
+    else
+    {
+        printf("Mavlink init failed\n");
+        return -1;
+    }
+
+    while (1)
+    {
+    }
+
+    return 0;
+}
+
+void onReceive(MavDriver *channel, mavlink_message_t msg)
+{
+    mavlink_payload_flight_tm_t payload;
+
+    printf("Received message with ID %d\n", msg.msgid);
+    mavlink_msg_payload_flight_tm_decode(&msg, &payload);
+    printf("Received payload with timestamp %lld\n", payload.timestamp);
+}
diff --git a/on-device/src/shared/Radio.h b/on-device/src/entrypoints/arpist_device_sender.cpp
similarity index 58%
rename from on-device/src/shared/Radio.h
rename to on-device/src/entrypoints/arpist_device_sender.cpp
index 9eedcba02ec3ed9b24ebcc074d935c04e7155033..4bed47b038427e9c895873b2fac4d8d7422add33 100644
--- a/on-device/src/shared/Radio.h
+++ b/on-device/src/entrypoints/arpist_device_sender.cpp
@@ -20,40 +20,42 @@
  * THE SOFTWARE.
  */
 
-#pragma once
-
-#include <radio/SX1278/SX1278Frontends.h>
-#include <radio/SX1278/SX1278Fsk.h>
-#include <radio/SX1278/SX1278Lora.h>
-
-#if defined _BOARD_STM32F429ZI_SKYWARD_GS_V2
-#include "interfaces-impl/hwmapping.h"
-
-#define SX1278_SPI SPI4
-#define SX1278_IRQ_DIO0 EXTI6_IRQHandlerImpl
-#define SX1278_IRQ_DIO1 EXTI4_IRQHandlerImpl
-#define SX1278_IRQ_DIO3 EXTI11_IRQHandlerImpl
-#else
-#error "Target not supported"
-#endif
-
-namespace Arpist
+#include <filesystem/console/console_device.h>
+#include <mavlink_lib/gemini/mavlink.h>
+#include <radio/MavlinkDriver/MavlinkDriver.h>
+#include <shared/mavlink/Utils.h>
+#include <shared/radio/Radio.h>
+
+using namespace miosix;
+using namespace Boardcore;
+using namespace Arpist;
+
+// === CONSTANTS ===
+/** @brief End of transmission character */
+constexpr uint8_t ACK = 0x06;
+
+// === MAIN ===
+int main()
 {
+    mavlink_message_t msg;
 
-    static volatile int dio0_cnt = 0;
-    static volatile int dio1_cnt = 0;
-    static volatile int dio3_cnt = 0;
-
-    static Boardcore::SX1278Fsk *sx1278 = nullptr;
-    static Boardcore::SPIBus sx1278_bus(SX1278_SPI);
+    // start radio
+    if (Radio::getInstance().start([](const mavlink_message_t&) {}))
+    {
+        printf("Radio start success\n");
+    }
+    else
+    {
+        printf("Radio start failed\n");
+        return -1;
+    }
 
-    /**
-     * @brief Radio class
-     */
-    class Radio
+    while (true)
     {
-    public:
-        static bool init();
-    };
+        msg = readPacketFromSerial();
+        DefaultConsole::instance().get()->writeBlock(&ACK, 1, 0);
+        Radio::getInstance().sendMsg(msg);
+    }
 
-} // namespace Arpist
+    return 0;
+}
diff --git a/on-device/src/entrypoints/test_mavlink_parse.cpp b/on-device/src/entrypoints/test_mavlink_parse.cpp
index f24bc95bd9170cfbd9c85fa56b48e60238dec092..2eae9dd95a3bbc4ee08c18793ae2c13d34f9a5b4 100644
--- a/on-device/src/entrypoints/test_mavlink_parse.cpp
+++ b/on-device/src/entrypoints/test_mavlink_parse.cpp
@@ -23,9 +23,9 @@
 // #include <filesystem/console/console_device.h>
 #include <mavlink_lib/gemini/mavlink.h>
 // SX1278 includes
-#include <radio/SerialTransceiver/SerialTransceiver.h>
 #include <radio/MavlinkDriver/MavlinkDriver.h>
-#include <shared/Radio.h>
+#include <radio/SerialTransceiver/SerialTransceiver.h>
+#include <shared/radio/Radio.h>
 
 #include <iostream>
 #include <thread>
@@ -62,6 +62,7 @@ int main()
 
     return 0;
 }
+
 /**
  * @brief Read a packet from the serial port
  * @warning This function will parse raw bytes coming from
@@ -94,9 +95,9 @@ mavlink_message_t readPacketFromSerial()
                 // ... parse received bytes
                 parse_result =
                     mavlink_parse_char(MAVLINK_COMM_0,
-                                       serial_buffer[i], // byte to parse
-                                       &msg,             // where to parse it
-                                       &status);         // stats to update
+                                       serial_buffer[i],  // byte to parse
+                                       &msg,              // where to parse it
+                                       &status);          // stats to update
 
                 // When a valid message is found ...
                 if (parse_result == 1)
diff --git a/on-device/src/shared/Radio.cpp b/on-device/src/shared/Radio.cpp
deleted file mode 100644
index f6f86aad6a3c2da661c7ceebc1db5e85c485e139..0000000000000000000000000000000000000000
--- a/on-device/src/shared/Radio.cpp
+++ /dev/null
@@ -1,81 +0,0 @@
-/* Copyright (c) 2024 Skyward Experimental Rocketry
- * Author: Federico Lolli
- *
- * 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 <drivers/interrupt/external_interrupts.h>
-#include <radio/SX1278/SX1278Frontends.h>
-#include <radio/SX1278/SX1278Fsk.h>
-#include <radio/SX1278/SX1278Lora.h>
-#include <config/RadioConfig.h>
-
-#include "Radio.h"
-
-// === INTERRUPTS ===
-#ifdef SX1278_IRQ_DIO0
-void __attribute__((used)) SX1278_IRQ_DIO0()
-{
-    Arpist::dio0_cnt++;
-    if (Arpist::sx1278)
-        Arpist::sx1278->handleDioIRQ();
-}
-#endif
-
-#ifdef SX1278_IRQ_DIO1
-void __attribute__((used)) SX1278_IRQ_DIO1()
-{
-    Arpist::dio1_cnt++;
-    if (Arpist::sx1278)
-        Arpist::sx1278->handleDioIRQ();
-}
-#endif
-
-#ifdef SX1278_IRQ_DIO3
-void __attribute__((used)) SX1278_IRQ_DIO3()
-{
-    Arpist::dio3_cnt++;
-    if (Arpist::sx1278)
-        Arpist::sx1278->handleDioIRQ();
-}
-#endif
-
-namespace Arpist
-{
-
-    bool Radio::init()
-    {
-        using cs = miosix::peripherals::ra01::pc13::cs;
-        using dio0 = miosix::peripherals::ra01::pc13::dio0;
-        using dio1 = miosix::peripherals::ra01::pc13::dio1;
-        using dio3 = miosix::peripherals::ra01::pc13::dio3;
-        using sck = miosix::interfaces::spi4::sck;
-        using miso = miosix::interfaces::spi4::miso;
-        using mosi = miosix::interfaces::spi4::mosi;
-
-        std::unique_ptr<Boardcore::SX1278::ISX1278Frontend> frontend(
-            new Boardcore::RA01Frontend());
-        sx1278 = new Boardcore::SX1278Fsk(
-            sx1278_bus, cs::getPin(), dio0::getPin(), dio1::getPin(),
-            dio3::getPin(), Boardcore::SPI::ClockDivider::DIV_256,
-            std::move(frontend));
-        return sx1278->init(RADIO_CONFIG) == Boardcore::SX1278Fsk::Error::NONE;
-    }
-
-} // namespace Arpist
diff --git a/on-device/src/shared/mavlink/Utils.h b/on-device/src/shared/mavlink/Utils.h
new file mode 100644
index 0000000000000000000000000000000000000000..9db699c87e32f17cb925f5a1eaf47f03f96477bd
--- /dev/null
+++ b/on-device/src/shared/mavlink/Utils.h
@@ -0,0 +1,73 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Federico Lolli
+ *
+ * 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 <filesystem/console/console_device.h>
+#include <mavlink_lib/gemini/mavlink.h>
+
+/**
+ * @brief Read a packet from the serial port
+ * @warning This function will parse raw bytes coming from
+ * serial into the struct
+ * @return mavlink_payload_flight_tm_t
+ */
+mavlink_message_t readPacketFromSerial()
+{
+    bool stop_flag = false;
+    ssize_t rcv_size;
+    uint8_t serial_buffer[128];
+    uint8_t parse_result = 0;
+    mavlink_message_t msg;
+    mavlink_status_t status;
+
+    auto serial = miosix::DefaultConsole::instance().get();
+
+    while (!stop_flag)
+    {
+        // Check for a new message on the device
+        rcv_size = serial->readBlock(serial_buffer, 128, 0);
+        // LOG_DEBUG("Received %d bytes\n", rcv_size);
+
+        // If there's a new message ...
+        if (rcv_size > 0)
+        {
+            for (int i = 0; i < rcv_size; i++)
+            {
+                parse_result = 0;
+                // ... parse received bytes
+                parse_result =
+                    mavlink_parse_char(MAVLINK_COMM_0,
+                                       serial_buffer[i],  // byte to parse
+                                       &msg,              // where to parse it
+                                       &status);          // stats to update
+
+                // When a valid message is found ...
+                if (parse_result == 1)
+                {
+                    stop_flag = true;
+                    break;
+                }
+            }
+        }
+    }
+
+    return msg;
+}
diff --git a/on-device/src/shared/radio/Radio.cpp b/on-device/src/shared/radio/Radio.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5dc2f0ab4ab9f628f57d1c8db29dd4ffdf83e248
--- /dev/null
+++ b/on-device/src/shared/radio/Radio.cpp
@@ -0,0 +1,120 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Federico Lolli
+ *
+ * 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 <config/RadioConfig.h>
+#include <radio/SX1278/SX1278Frontends.h>
+
+#include <memory>
+
+// === INTERRUPTS ===
+#ifdef SX1278_IRQ_DIO0
+void __attribute__((used)) SX1278_IRQ_DIO0()
+{
+    Arpist::Radio::getInstance().handleDioIRQ();
+}
+#endif
+
+#ifdef SX1278_IRQ_DIO1
+void __attribute__((used)) SX1278_IRQ_DIO1()
+{
+    Arpist::Radio::getInstance().handleDioIRQ();
+}
+#endif
+
+#ifdef SX1278_IRQ_DIO3
+void __attribute__((used)) SX1278_IRQ_DIO3()
+{
+    Arpist::Radio::getInstance().handleDioIRQ();
+}
+#endif
+
+namespace Arpist
+{
+
+void Radio::handleDioIRQ()
+{
+    if (started)
+    {
+        sx1278->handleDioIRQ();
+    }
+}
+
+bool Radio::sendMsg(const mavlink_message_t &msg)
+{
+    if (!started)
+    {
+        return false;
+    }
+
+    return mav_driver->enqueueMsg(msg);
+}
+
+bool Radio::send(uint8_t *pkt, size_t len) { return sx1278->send(pkt, len); }
+
+ssize_t Radio::receive(uint8_t *pkt, size_t max_len)
+{
+    return sx1278->receive(pkt, max_len);
+}
+
+bool Radio::start(std::function<void(const mavlink_message_t &)> onReceive)
+{
+    // initialize frontend
+    std::unique_ptr<Boardcore::SX1278::ISX1278Frontend> frontend =
+        std::make_unique<Boardcore::Skyward433Frontend>();
+
+    std::unique_ptr<Boardcore::SX1278Fsk> sx1278 =
+        std::make_unique<Boardcore::SX1278Fsk>(
+            sx1278_bus, radio_cs::getPin(), radio_dio0::getPin(),
+            radio_dio1::getPin(), radio_dio3::getPin(),
+            Boardcore::SPI::ClockDivider::DIV_64, std::move(frontend));
+
+    // First check if the device is even connected
+    if (!sx1278->checkVersion())
+        return false;
+
+    // Configure the radio
+    if (sx1278->configure(RADIO_CONFIG) != Boardcore::SX1278Fsk::Error::NONE)
+        return false;
+
+    // Initialize
+    this->sx1278 = std::move(sx1278);
+
+    auto mav_handler =
+        [onReceive](RadioMavDriver *channel, const mavlink_message_t &msg)
+    { onReceive(msg); };
+
+    mav_driver = std::make_unique<RadioMavDriver>(
+        this, mav_handler, Arpist::MAV_SLEEP_AFTER_SEND,
+        Arpist::MAV_OUT_BUFFER_MAX_AGE);
+
+    if (!mav_driver->start())
+    {
+        return false;
+    }
+
+    started = true;
+    return true;
+}
+
+}  // namespace Arpist
diff --git a/on-device/src/shared/radio/Radio.h b/on-device/src/shared/radio/Radio.h
new file mode 100644
index 0000000000000000000000000000000000000000..edd4b372014885c2f8fb5d37204015a9e57c8ae1
--- /dev/null
+++ b/on-device/src/shared/radio/Radio.h
@@ -0,0 +1,92 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Author: Federico Lolli
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#pragma once
+
+#include <Singleton.h>
+#include <config/Mavlink.h>
+#include <config/RadioConfig.h>
+#include <radio/MavlinkDriver/MavlinkDriver.h>
+#include <radio/SX1278/SX1278Fsk.h>
+#include <radio/SX1278/SX1278Lora.h>
+
+#if defined _BOARD_STM32F429ZI_SKYWARD_GS_V2
+#include "interfaces-impl/hwmapping.h"
+
+#define SX1278_SPI SPI4
+#define SX1278_IRQ_DIO0 EXTI6_IRQHandlerImpl
+#define SX1278_IRQ_DIO1 EXTI4_IRQHandlerImpl
+#define SX1278_IRQ_DIO3 EXTI11_IRQHandlerImpl
+
+#else
+#error "Target not supported"
+#endif
+
+namespace Arpist
+{
+
+#if defined _BOARD_STM32F429ZI_SKYWARD_GS_V2
+// pin mapping
+using radio_cs   = miosix::peripherals::ra01::pc13::cs;
+using radio_dio0 = miosix::peripherals::ra01::pc13::dio0;
+using radio_dio1 = miosix::peripherals::ra01::pc13::dio1;
+using radio_dio3 = miosix::peripherals::ra01::pc13::dio3;
+using radio_sck  = miosix::interfaces::spi4::sck;
+using radio_miso = miosix::interfaces::spi4::miso;
+using radio_mosi = miosix::interfaces::spi4::mosi;
+#endif
+
+using RadioMavDriver =
+    Boardcore::MavlinkDriver<Boardcore::SX1278Fsk::MTU,
+                             Arpist::MAV_OUT_QUEUE_SIZE,
+                             MAVLINK_MAX_DIALECT_PAYLOAD_SIZE>;
+
+// static volatile int dio0_cnt = 0;
+// static volatile int dio1_cnt = 0;
+// static volatile int dio3_cnt = 0;
+
+/**
+ * @brief Radio class
+ */
+class Radio : public Boardcore::Singleton<Radio>, public Boardcore::Transceiver
+{
+    friend class Boardcore::Singleton<Radio>;
+
+public:
+    void handleDioIRQ();
+
+    bool start(std::function<void(const mavlink_message_t&)> onReceive);
+
+    bool sendMsg(const mavlink_message_t& msg);
+
+private:
+    ssize_t receive(uint8_t* pkt, size_t max_len) override;
+
+    bool send(uint8_t* pkt, size_t len) override;
+
+    bool started = false;
+    Boardcore::SPIBus sx1278_bus{SX1278_SPI};
+    std::unique_ptr<Boardcore::SX1278Fsk> sx1278;
+    std::unique_ptr<RadioMavDriver> mav_driver;
+};
+
+}  // namespace Arpist