diff --git a/on-device/src/entrypoints/arpist-device-sender.cpp b/on-device/src/entrypoints/arpist-device-sender.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fbd0ef5b6c28792b1cff761a0710d60fff318fdd
--- /dev/null
+++ b/on-device/src/entrypoints/arpist-device-sender.cpp
@@ -0,0 +1,301 @@
+/* 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");
+}