diff --git a/CMakeLists.txt b/CMakeLists.txt
index fd08e56e359e567de9880b2f34f3493ed2d14e29..1cb8b7df5a53d3ae8288e4c4c926e2d89e134a19 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -280,22 +280,19 @@ sbs_target(test-fsm stm32f429zi_stm32f4discovery)
 add_executable(test-sx1278fsk-bidir src/tests/radio/sx1278/fsk/test-sx1278-bidir.cpp)
 sbs_target(test-sx1278fsk-bidir stm32f429zi_skyward_groundstation_v2)
 
-add_executable(test-sx1278fsk-tx src/tests/radio/sx1278/fsk/test-sx1278-bench-serial.cpp)
+add_executable(test-sx1278fsk-tx src/tests/radio/sx1278/test-sx1278-bench-serial.cpp)
 target_compile_definitions(test-sx1278fsk-tx PRIVATE DISABLE_RX)
 sbs_target(test-sx1278fsk-tx stm32f429zi_skyward_groundstation_v2)
 
-add_executable(test-sx1278fsk-rx src/tests/radio/sx1278/fsk/test-sx1278-bench-serial.cpp)
+add_executable(test-sx1278fsk-rx src/tests/radio/sx1278/test-sx1278-bench-serial.cpp)
 target_compile_definitions(test-sx1278fsk-rx PRIVATE DISABLE_TX)
 sbs_target(test-sx1278fsk-rx stm32f429zi_skyward_groundstation_v2)
 
-add_executable(test-sx1278fsk-gui src/tests/radio/sx1278/fsk/test-sx1278-bench-gui.cpp)
-sbs_target(test-sx1278fsk-gui stm32f429zi_skyward_groundstation_v2)
-
-add_executable(test-sx1278fsk-gui-rx src/tests/radio/sx1278/fsk/test-sx1278-bench-gui.cpp)
+add_executable(test-sx1278fsk-gui-rx src/tests/radio/sx1278/test-sx1278-bench-gui.cpp)
 target_compile_definitions(test-sx1278fsk-gui-rx PRIVATE DISABLE_TX)
 sbs_target(test-sx1278fsk-gui-rx stm32f429zi_skyward_groundstation_v2)
 
-add_executable(test-sx1278fsk-gui-tx src/tests/radio/sx1278/fsk/test-sx1278-bench-gui.cpp)
+add_executable(test-sx1278fsk-gui-tx src/tests/radio/sx1278/test-sx1278-bench-gui.cpp)
 target_compile_definitions(test-sx1278fsk-gui-tx PRIVATE DISABLE_RX)
 sbs_target(test-sx1278fsk-gui-tx stm32f429zi_skyward_groundstation_v2)
 
@@ -311,14 +308,30 @@ sbs_target(test-sx1278lora-bidir stm32f429zi_skyward_groundstation_v2)
 add_executable(test-sx1278lora-mavlink src/tests/radio/sx1278/lora/test-sx1278-mavlink.cpp)
 sbs_target(test-sx1278lora-mavlink stm32f429zi_skyward_groundstation_v2)
 
-add_executable(test-sx1278lora-rx src/tests/radio/sx1278/lora/test-sx1278-simple.cpp)
-target_compile_definitions(test-sx1278lora-rx PRIVATE ENABLE_RX)
-sbs_target(test-sx1278lora-rx stm32f429zi_skyward_groundstation_v2)
+add_executable(test-sx1278lora-simple-rx src/tests/radio/sx1278/lora/test-sx1278-simple.cpp)
+target_compile_definitions(test-sx1278lora-simple-rx PRIVATE ENABLE_RX)
+sbs_target(test-sx1278lora-simple-rx stm32f429zi_skyward_groundstation_v2)
 
-add_executable(test-sx1278lora-tx src/tests/radio/sx1278/lora/test-sx1278-simple.cpp)
-target_compile_definitions(test-sx1278lora-tx PRIVATE ENABLE_TX)
+add_executable(test-sx1278lora-simple-tx src/tests/radio/sx1278/lora/test-sx1278-simple.cpp)
+target_compile_definitions(test-sx1278lora-simple-tx PRIVATE ENABLE_TX)
+sbs_target(test-sx1278lora-simple-tx stm32f429zi_skyward_groundstation_v2)
+
+add_executable(test-sx1278lora-tx src/tests/radio/sx1278/test-sx1278-bench-serial.cpp)
+target_compile_definitions(test-sx1278lora-tx PRIVATE DISABLE_RX SX1278_IS_LORA)
 sbs_target(test-sx1278lora-tx stm32f429zi_skyward_groundstation_v2)
 
+add_executable(test-sx1278lora-rx src/tests/radio/sx1278/test-sx1278-bench-serial.cpp)
+target_compile_definitions(test-sx1278lora-rx PRIVATE DISABLE_TX SX1278_IS_LORA)
+sbs_target(test-sx1278lora-rx stm32f429zi_skyward_groundstation_v2)
+
+add_executable(test-sx1278lora-gui-rx src/tests/radio/sx1278/test-sx1278-bench-gui.cpp)
+target_compile_definitions(test-sx1278lora-gui-rx PRIVATE DISABLE_TX SX1278_IS_LORA)
+sbs_target(test-sx1278lora-gui-rx stm32f429zi_skyward_groundstation_v2)
+
+add_executable(test-sx1278lora-gui-tx src/tests/radio/sx1278/test-sx1278-bench-gui.cpp)
+target_compile_definitions(test-sx1278lora-gui-tx PRIVATE DISABLE_RX SX1278_IS_LORA)
+sbs_target(test-sx1278lora-gui-tx stm32f429zi_skyward_groundstation_v2)
+
 #-----------------------------------------------------------------------------#
 #                               Tests - Sensors                               #
 #-----------------------------------------------------------------------------#
diff --git a/src/entrypoints/sx1278-serial.cpp b/src/entrypoints/sx1278-serial.cpp
index 39be5c51fd0e5e691bba7e48a6f36e63943cac9d..fdd565e83e0ddf5ead9adddf02fa118d90c7671c 100644
--- a/src/entrypoints/sx1278-serial.cpp
+++ b/src/entrypoints/sx1278-serial.cpp
@@ -159,14 +159,6 @@ int main()
 {
     initBoard();
 
-    // Generic SPI configuration
-    SPIBusConfig spi_config;
-    spi_config.clockDivider = SPI::ClockDivider::DIV_64;
-    spi_config.mode         = SPI::Mode::MODE_0;
-    spi_config.bitOrder     = SPI::Order::MSB_FIRST;
-    spi_config.byteOrder    = SPI::Order::MSB_FIRST;
-    spi_config.writeBit     = SPI::WriteBit::INVERTED;
-
     SPIBus bus(SX1278_SPI);
     GpioPin cs = cs::getPin();
 
diff --git a/src/shared/radio/SX1278/SX1278Common.h b/src/shared/radio/SX1278/SX1278Common.h
index 6dd68834f5ed960c5e64d002c6f7bea9787091dc..605c97c34c9511884a8877c5487352067547b33e 100644
--- a/src/shared/radio/SX1278/SX1278Common.h
+++ b/src/shared/radio/SX1278/SX1278Common.h
@@ -27,6 +27,7 @@
 #include <radio/Transceiver.h>
 
 #include <memory>
+#include <cmath>
 
 #include "SX1278Defs.h"
 
@@ -43,6 +44,22 @@ using DioMapping = RegDioMapping::Mapping;
  */
 class ISX1278 : public Transceiver
 {
+public:
+    /**
+     * @brief Get the RSSI in dBm, during last packet receive.
+     */
+    virtual float getLastRxRssi() = 0;
+
+    /**
+     * @brief Get the frequency error index in Hz, during last packet receive (NaN if not available).
+     */
+    virtual float getLastRxFei() { return std::nanf(""); }
+
+    /**
+     * @brief Get the signal to noise ratio, during last packet receive (NaN if not available).
+     */
+    virtual float getLastRxSnr() { return std::nanf(""); }
+
 protected:
     /*
      * Stuff used internally by SX1278Common
diff --git a/src/shared/radio/SX1278/SX1278Fsk.h b/src/shared/radio/SX1278/SX1278Fsk.h
index be110c8c55f9767e059639f2dd0cadb023cf3817..153fe72b71c445767cc288612b7d25461f6e1c9a 100644
--- a/src/shared/radio/SX1278/SX1278Fsk.h
+++ b/src/shared/radio/SX1278/SX1278Fsk.h
@@ -184,12 +184,12 @@ public:
     /**
      * @brief Get the RSSI in dBm, during last packet receive.
      */
-    float getLastRxRssi();
+    float getLastRxRssi() override;
 
     /**
      * @brief Get the frequency error index in Hz, during last packet receive.
      */
-    float getLastRxFei();
+    float getLastRxFei() override;
 
 private:
     void rateLimitTx();
diff --git a/src/shared/radio/SX1278/SX1278Lora.h b/src/shared/radio/SX1278/SX1278Lora.h
index 94159b59089e15daf3e874e4644aad4f7c9a1f8c..e7e04b7a8cb0d44ddd6b749968323fd1dfbccdcb 100644
--- a/src/shared/radio/SX1278/SX1278Lora.h
+++ b/src/shared/radio/SX1278/SX1278Lora.h
@@ -180,12 +180,12 @@ public:
     /**
      * @brief Get the RSSI in dBm, during last packet receive.
      */
-    float getLastRxRssi();
+    float getLastRxRssi() override;
 
     /**
      * @brief Get the RSSI in dBm, during last packet receive.
      */
-    float getLastRxSnr();
+    float getLastRxSnr() override;
 
 private:
     void readFifo(uint8_t addr, uint8_t *dst, uint8_t size);
diff --git a/src/tests/radio/sx1278/fsk/test-sx1278-bench-gui.cpp b/src/tests/radio/sx1278/fsk/test-sx1278-bench-gui.cpp
deleted file mode 100644
index a4b85cc607f1b6a25cfa0af0bf9396004c8ad963..0000000000000000000000000000000000000000
--- a/src/tests/radio/sx1278/fsk/test-sx1278-bench-gui.cpp
+++ /dev/null
@@ -1,159 +0,0 @@
-/* Copyright (c) 2022 Skyward Experimental Rocketry
- * Author: Davide Mor
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-#include <drivers/interrupt/external_interrupts.h>
-#include <radio/SX1278/SX1278Frontends.h>
-#include <radio/SX1278/SX1278Fsk.h>
-
-#include <thread>
-
-#include "common.h"
-#include "gui/GUI.h"
-
-// Include body of the test
-#include "test-sx1278-bench.cpp"
-
-using namespace Boardcore;
-using namespace miosix;
-using namespace mxgui;
-
-#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 EXTI2_IRQHandlerImpl
-#define SX1278_IRQ_DIO3 EXTI11_IRQHandlerImpl
-
-#else
-#error "Target not supported"
-#endif
-
-void __attribute__((used)) SX1278_IRQ_DIO0()
-{
-    if (sx1278)
-        sx1278->handleDioIRQ();
-}
-
-void __attribute__((used)) SX1278_IRQ_DIO1()
-{
-    if (sx1278)
-        sx1278->handleDioIRQ();
-}
-
-void __attribute__((used)) SX1278_IRQ_DIO3()
-{
-    if (sx1278)
-        sx1278->handleDioIRQ();
-}
-
-void initBoard()
-{
-    GpioPin dio0_pin = dio0::getPin();
-    GpioPin dio1_pin = dio1::getPin();
-    GpioPin dio3_pin = dio3::getPin();
-
-    enableExternalInterrupt(dio0_pin.getPort(), dio0_pin.getNumber(),
-                            InterruptTrigger::RISING_EDGE);
-    enableExternalInterrupt(dio1_pin.getPort(), dio1_pin.getNumber(),
-                            InterruptTrigger::RISING_EDGE);
-    enableExternalInterrupt(dio3_pin.getPort(), dio3_pin.getNumber(),
-                            InterruptTrigger::RISING_EDGE);
-}
-
-GUI *gui = nullptr;
-
-void initGUI()
-{
-    // TODO: This should be in bsp
-    using GpioUserBtn = Gpio<GPIOA_BASE, 0>;
-    GpioUserBtn::mode(Mode::INPUT_PULL_DOWN);
-
-    gui = new GUI();
-
-    ButtonHandler::getInstance().registerButtonCallback(
-        GpioUserBtn::getPin(),
-        [](auto event) { gui->screen_manager.onButtonEvent(event); });
-}
-
-int main()
-{
-    initBoard();
-    initGUI();
-
-    SX1278Fsk::Config config = {
-        .freq_rf  = 422075000,
-        .freq_dev = 25000,
-        .bitrate  = 19200,
-        .rx_bw    = SX1278Fsk::Config::RxBw::HZ_83300,
-        .afc_bw   = SX1278Fsk::Config::RxBw::HZ_125000,
-        .ocp      = 120,
-        .power    = 17,
-        .shaping  = SX1278Fsk::Config::Shaping::GAUSSIAN_BT_1_0,
-        .dc_free  = SX1278Fsk::Config::DcFree::WHITENING};
-    SX1278Fsk::Error err;
-
-    SPIBus bus(SX1278_SPI);
-    GpioPin cs = cs::getPin();
-
-    std::unique_ptr<SX1278::ISX1278Frontend> frontend(new RA01Frontend());
-
-    sx1278 =
-        new SX1278Fsk(bus, cs, SPI::ClockDivider::DIV_64, std::move(frontend));
-
-    printf("\n[sx1278] Configuring sx1278...\n");
-    if ((err = sx1278->init(config)) != SX1278Fsk::Error::NONE)
-    {
-        gui->stats_screen.updateError(err);
-        printf("[sx1278] sx1278->init error: %s\n", stringFromErr(err));
-
-        while (1)
-            Thread::wait();
-    }
-
-    printConfig(config);
-    gui->stats_screen.updateReady();
-
-    // Initialize backgrounds threads
-    spawnThreads();
-
-    while (1)
-    {
-        StatsScreen::Data data = {
-            stats.txBitrate(), stats.rxBitrate(), stats.corrupted_count,
-            stats.sent_count,  stats.recv_count,  0.0f /* TODO: Packet loss */,
-            stats.rssi};
-
-        gui->stats_screen.updateStats(data);
-        Thread::sleep(100);
-    }
-}
diff --git a/src/tests/radio/sx1278/fsk/test-sx1278-bench-serial.cpp b/src/tests/radio/sx1278/fsk/test-sx1278-bench-serial.cpp
deleted file mode 100644
index 6bbfaa3c3eec55c245eddacfe2a31e5266f06c2d..0000000000000000000000000000000000000000
--- a/src/tests/radio/sx1278/fsk/test-sx1278-bench-serial.cpp
+++ /dev/null
@@ -1,149 +0,0 @@
-/* Copyright (c) 2021 Skyward Experimental Rocketry
- * Author: Davide Mor
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-#include <drivers/interrupt/external_interrupts.h>
-#include <radio/SX1278/SX1278Frontends.h>
-#include <radio/SX1278/SX1278Fsk.h>
-
-#include <thread>
-
-#include "common.h"
-
-// Include body of the test
-#include "test-sx1278-bench.cpp"
-
-using namespace Boardcore;
-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 EXTI2_IRQHandlerImpl
-#define SX1278_IRQ_DIO3 EXTI11_IRQHandlerImpl
-
-#else
-#error "Target not supported"
-#endif
-
-void __attribute__((used)) SX1278_IRQ_DIO0()
-{
-    if (sx1278)
-        sx1278->handleDioIRQ();
-}
-
-void __attribute__((used)) SX1278_IRQ_DIO1()
-{
-    if (sx1278)
-        sx1278->handleDioIRQ();
-}
-
-void __attribute__((used)) SX1278_IRQ_DIO3()
-{
-    if (sx1278)
-        sx1278->handleDioIRQ();
-}
-
-void initBoard()
-{
-    GpioPin dio0_pin = dio0::getPin();
-    GpioPin dio1_pin = dio1::getPin();
-    GpioPin dio3_pin = dio3::getPin();
-
-    enableExternalInterrupt(dio0_pin.getPort(), dio0_pin.getNumber(),
-                            InterruptTrigger::RISING_EDGE);
-    enableExternalInterrupt(dio1_pin.getPort(), dio1_pin.getNumber(),
-                            InterruptTrigger::RISING_EDGE);
-    enableExternalInterrupt(dio3_pin.getPort(), dio3_pin.getNumber(),
-                            InterruptTrigger::RISING_EDGE);
-}
-
-int main()
-{
-    initBoard();
-
-    SX1278Fsk::Config config = {
-        .freq_rf  = 422075000,
-        .freq_dev = 25000,
-        .bitrate  = 19200,
-        .rx_bw    = SX1278Fsk::Config::RxBw::HZ_83300,
-        .afc_bw   = SX1278Fsk::Config::RxBw::HZ_125000,
-        .ocp      = 120,
-        .power    = 17,
-        .shaping  = SX1278Fsk::Config::Shaping::GAUSSIAN_BT_1_0,
-        .dc_free  = SX1278Fsk::Config::DcFree::WHITENING};
-    SX1278Fsk::Error err;
-
-    SPIBus bus(SX1278_SPI);
-    GpioPin cs = cs::getPin();
-
-    std::unique_ptr<SX1278::ISX1278Frontend> frontend(new RA01Frontend());
-
-    sx1278 =
-        new SX1278Fsk(bus, cs, SPI::ClockDivider::DIV_64, std::move(frontend));
-
-    printf("\n[sx1278] Configuring sx1278...\n");
-    if ((err = sx1278->init(config)) != SX1278Fsk::Error::NONE)
-    {
-        printf("[sx1278] sx1278->init error: %s\n", stringFromErr(err));
-        return -1;
-    }
-
-    printConfig(config);
-    printf("\n[sx1278] Initialization complete!\n");
-
-    // Initialize backgrounds threads
-    spawnThreads();
-
-    while (1)
-    {
-        printf(
-            "\n[sx1278] Stats:\n"
-            "Tx bitrate:        %.2f kb/s\n"
-            "Packet sent:       %d\n"
-            "Rx bitrate:        %.2f kb/s\n"
-            "Packet received:   %d\n"
-            "Corrupted packets: %d\n"
-            "Packet loss:       %.2f %%\n"
-            "RSSI:              %.2f dBm\n"
-            "FEI:               %.2f dBm\n",
-            static_cast<float>(stats.txBitrate()) / 1000.0f, stats.sent_count,
-            static_cast<float>(stats.rxBitrate()) / 1000.0f, stats.recv_count,
-            stats.corrupted_count, 0.0f /* TODO: Packet loss */, stats.rssi,
-            stats.fei);
-
-        miosix::Thread::sleep(2000);
-    }
-
-    return 0;
-}
diff --git a/src/tests/radio/sx1278/fsk/gui/GUI.h b/src/tests/radio/sx1278/gui/GUI.h
similarity index 84%
rename from src/tests/radio/sx1278/fsk/gui/GUI.h
rename to src/tests/radio/sx1278/gui/GUI.h
index bd4544a4e595f265e2553cfe75127064f51416d2..d42aa0125b80a9b25da95fbcc428a2be535dc6a1 100644
--- a/src/tests/radio/sx1278/fsk/gui/GUI.h
+++ b/src/tests/radio/sx1278/gui/GUI.h
@@ -49,8 +49,9 @@ public:
         int corrupted_count;
         int sent_count;
         int recv_count;
-        float packet_loss;
         float rssi;
+        float fei;
+        float snr;
     };
 
     StatsScreen()
@@ -72,16 +73,19 @@ public:
         rx_data.setCell(&lbl_rx_bitrate, 0, 0);
         rx_data.setCell(&lbl_recv_count, 1, 0);
         rx_data.setCell(&lbl_corrupted_count, 2, 0);
-        rx_data.setCell(&lbl_packet_loss, 3, 0);
 
         rx_data.setCell(&rx_bitrate, 0, 1);
         rx_data.setCell(&recv_count, 1, 1);
         rx_data.setCell(&corrupted_count, 2, 1);
-        rx_data.setCell(&packet_loss, 3, 1);
 
         lbl_misc_data.setTextColor(mxgui::blue);
         misc_data.setCell(&lbl_rssi, 0, 0);
+        misc_data.setCell(&lbl_fei, 1, 0);
+        misc_data.setCell(&lbl_snr, 2, 0);
+
         misc_data.setCell(&rssi, 0, 1);
+        misc_data.setCell(&fei, 1, 1);
+        misc_data.setCell(&snr, 2, 1);
 
         root.addView(&status, 0.1);
         root.addView(&lbl_tx_data, 0.1);
@@ -92,24 +96,6 @@ public:
         root.addView(&misc_data, 0.2);
     }
 
-    void updateError(Boardcore::SX1278Fsk::Error value)
-    {
-        switch (value)
-        {
-            case Boardcore::SX1278Fsk::Error::BAD_VALUE:
-                status.setBackgroundColor(mxgui::red);
-                status.setText("BAD VALUE");
-                break;
-            case Boardcore::SX1278Fsk::Error::BAD_VERSION:
-                status.setBackgroundColor(mxgui::red);
-                status.setText("BAD VERSION");
-                break;
-
-            default:
-                break;
-        }
-    }
-
     void updateReady()
     {
         status.setBackgroundColor(mxgui::green);
@@ -124,18 +110,18 @@ public:
         rx_bitrate.setText(format_link_speed(stats.rx_bitrate));
         recv_count.setText(fmt::format("{}", stats.recv_count));
         corrupted_count.setText(fmt::format("{}", stats.corrupted_count));
-        packet_loss.setText(
-            fmt::format("{:.2f} %", stats.packet_loss * 100.0f));
 
         rssi.setText(fmt::format("{} dBm", stats.rssi));
+        fei.setText(fmt::format("{} Hz", stats.fei));
+        snr.setText(fmt::format("{}", stats.snr));
     }
 
     Boardcore::VerticalLayout root{10};
     Boardcore::TextView status{"LOADING"};
 
     Boardcore::GridLayout tx_data{2, 2};
-    Boardcore::GridLayout rx_data{4, 2};
-    Boardcore::GridLayout misc_data{1, 2};
+    Boardcore::GridLayout rx_data{3, 2};
+    Boardcore::GridLayout misc_data{3, 2};
 
     Boardcore::TextView lbl_tx_data{"Tx data"};
     Boardcore::TextView lbl_rx_data{"Rx data"};
@@ -146,16 +132,18 @@ public:
     Boardcore::TextView lbl_rx_bitrate{"Rx bitrate:"};
     Boardcore::TextView lbl_recv_count{"Packets received:"};
     Boardcore::TextView lbl_corrupted_count{"Corrupted packets:"};
-    Boardcore::TextView lbl_packet_loss{"Packet loss:"};
     Boardcore::TextView lbl_rssi{"RSSI:"};
+    Boardcore::TextView lbl_fei{"FEI:"};
+    Boardcore::TextView lbl_snr{"SNR:"};
 
     Boardcore::TextView tx_bitrate{"0.00 b/s"};
     Boardcore::TextView rx_bitrate{"0.00 b/s"};
     Boardcore::TextView corrupted_count{"0"};
     Boardcore::TextView recv_count{"0"};
     Boardcore::TextView sent_count{"0"};
-    Boardcore::TextView packet_loss{"0 %"};
     Boardcore::TextView rssi{"0 dBm"};
+    Boardcore::TextView fei{"0 Hz"};
+    Boardcore::TextView snr{"0"};
 };
 
 class GUI
diff --git a/src/tests/radio/sx1278/sx1278-init.h b/src/tests/radio/sx1278/sx1278-init.h
new file mode 100644
index 0000000000000000000000000000000000000000..0cdcfd2e6e75e255352220b32e5562611fe0f707
--- /dev/null
+++ b/src/tests/radio/sx1278/sx1278-init.h
@@ -0,0 +1,177 @@
+/* Copyright (c) 2023 Skyward Experimental Rocketry
+ * Author: Davide Mor
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#pragma once
+
+#include <drivers/interrupt/external_interrupts.h>
+
+// SX1278 includes
+#include <radio/SX1278/SX1278Frontends.h>
+#include <radio/SX1278/SX1278Fsk.h>
+#include <radio/SX1278/SX1278Lora.h>
+
+// Uncomment the following line to enable Lora mode
+// Or use SBS to define it for you
+// #define SX1278_IS_LORA
+
+#if defined _BOARD_STM32F429ZI_SKYWARD_GS_V2
+#include "interfaces-impl/hwmapping.h"
+
+// Uncomment the following line to enable Ebyte module
+// #define SX1278_IS_EBYTE
+
+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;
+
+#ifdef SX1278_IS_EBYTE
+using txen = miosix::Gpio<GPIOE_BASE, 4>;
+using rxen = miosix::Gpio<GPIOD_BASE, 4>;
+#endif
+
+#define SX1278_SPI SPI4
+
+#define SX1278_IRQ_DIO0 EXTI6_IRQHandlerImpl
+#define SX1278_IRQ_DIO1 EXTI2_IRQHandlerImpl
+#define SX1278_IRQ_DIO3 EXTI11_IRQHandlerImpl
+
+#else
+#error "Target not supported"
+#endif
+
+#ifdef SX1278_IS_LORA
+static constexpr size_t SX1278_MTU = Boardcore::SX1278Lora::MTU;
+Boardcore::SX1278Lora *sx1278      = nullptr;
+#else
+static constexpr size_t SX1278_MTU = Boardcore::SX1278Fsk::MTU;
+Boardcore::SX1278Fsk *sx1278       = nullptr;
+#endif
+
+#ifdef SX1278_IRQ_DIO0
+void __attribute__((used)) SX1278_IRQ_DIO0()
+{
+    if (sx1278)
+        sx1278->handleDioIRQ();
+}
+#endif
+
+#ifdef SX1278_IRQ_DIO1
+void __attribute__((used)) SX1278_IRQ_DIO1()
+{
+    if (sx1278)
+        sx1278->handleDioIRQ();
+}
+#endif
+
+#ifdef SX1278_IRQ_DIO3
+void __attribute__((used)) SX1278_IRQ_DIO3()
+{
+    if (sx1278)
+        sx1278->handleDioIRQ();
+}
+#endif
+
+void initBoard()
+{
+#ifdef SX1278_IS_EBYTE
+    rxen::mode(miosix::Mode::OUTPUT);
+    txen::mode(miosix::Mode::OUTPUT);
+    rxen::low();
+    txen::low();
+#endif
+
+#ifdef SX1278_IRQ_DIO0
+    miosix::GpioPin dio0_pin = dio0::getPin();
+    enableExternalInterrupt(dio0_pin.getPort(), dio0_pin.getNumber(),
+                            InterruptTrigger::RISING_EDGE);
+#endif
+
+#ifdef SX1278_IRQ_DIO1
+    miosix::GpioPin dio1_pin = dio1::getPin();
+    enableExternalInterrupt(dio1_pin.getPort(), dio1_pin.getNumber(),
+                            InterruptTrigger::RISING_EDGE);
+#endif
+
+#ifdef SX1278_IRQ_DIO3
+    miosix::GpioPin dio3_pin = dio3::getPin();
+    enableExternalInterrupt(dio3_pin.getPort(), dio3_pin.getNumber(),
+                            InterruptTrigger::RISING_EDGE);
+#endif
+}
+
+bool initRadio()
+{
+    // Initialize frontend (if any)
+#ifdef IS_EBYTE
+    std::unique_ptr<Boardcore::SX1278::ISX1278Frontend> frontend(
+        new Boardcore::EbyteFrontend(txen::getPin(), rxen::getPin()));
+#else
+    std::unique_ptr<Boardcore::SX1278::ISX1278Frontend> frontend(
+        new Boardcore::RA01Frontend());
+#endif
+
+    Boardcore::SPIBus bus(SX1278_SPI);
+    miosix::GpioPin cs = cs::getPin();
+
+    // Initialize actual radio driver
+#ifdef SX1278_IS_LORA
+    // Run default configuration
+    Boardcore::SX1278Lora::Config config;
+    Boardcore::SX1278Lora::Error err;
+
+    sx1278 = new Boardcore::SX1278Lora(
+        bus, cs, Boardcore::SPI::ClockDivider::DIV_64, std::move(frontend));
+
+    printf("\n[sx1278] Configuring sx1278 lora...\n");
+    if ((err = sx1278->init(config)) != Boardcore::SX1278Lora::Error::NONE)
+    {
+        printf("[sx1278] sx1278->init error\n");
+        return false;
+    }
+
+    printf("\n[sx1278] Initialization complete!\n");
+#else
+    // Run default configuration
+    Boardcore::SX1278Fsk::Config config;
+    Boardcore::SX1278Fsk::Error err;
+
+    sx1278 = new Boardcore::SX1278Fsk(
+        bus, cs, Boardcore::SPI::ClockDivider::DIV_64, std::move(frontend));
+
+    printf("\n[sx1278] Configuring sx1278 fsk...\n");
+    if ((err = sx1278->init(config)) != Boardcore::SX1278Fsk::Error::NONE)
+    {
+        // FIXME: Why does clang-format put this line up here?
+        printf("[sx1278] sx1278->init error\n");
+        return false;
+    }
+
+    printf("\n[sx1278] Initialization complete!\n");
+#endif
+
+    return true;
+}
\ No newline at end of file
diff --git a/src/tests/radio/sx1278/test-sx1278-bench-gui.cpp b/src/tests/radio/sx1278/test-sx1278-bench-gui.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0182e06f2ae8d6c91d35194d1a272b84b724b2f5
--- /dev/null
+++ b/src/tests/radio/sx1278/test-sx1278-bench-gui.cpp
@@ -0,0 +1,71 @@
+/* Copyright (c) 2022 Skyward Experimental Rocketry
+ * Author: Davide Mor
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "gui/GUI.h"
+
+// Include body of the test
+#include "test-sx1278-bench.cpp"
+
+using namespace mxgui;
+
+GUI *gui = nullptr;
+
+void initGUI()
+{
+    // TODO: This should be in bsp
+    using GpioUserBtn = Gpio<GPIOA_BASE, 0>;
+    GpioUserBtn::mode(Mode::INPUT_PULL_DOWN);
+
+    gui = new GUI();
+
+    ButtonHandler::getInstance().registerButtonCallback(
+        GpioUserBtn::getPin(),
+        [](auto event) { gui->screen_manager.onButtonEvent(event); });
+}
+
+int main()
+{
+    initBoard();
+    initGUI();
+    if (!initRadio())
+    {
+        while (1)
+            ;
+    }
+
+    // Set display to ready
+    gui->stats_screen.updateReady();
+
+    // Initialize backgrounds threads
+    spawnThreads();
+
+    while (1)
+    {
+        StatsScreen::Data data = {
+            stats.txBitrate(), stats.rxBitrate(), stats.corrupted_count,
+            stats.sent_count,  stats.recv_count,  stats.rssi,
+            stats.fei,         stats.snr};
+
+        gui->stats_screen.updateStats(data);
+        Thread::sleep(100);
+    }
+}
diff --git a/src/tests/radio/sx1278/test-sx1278-bench-serial.cpp b/src/tests/radio/sx1278/test-sx1278-bench-serial.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e4cc2db9645decea157f8cd85e6eb40ae6db9b58
--- /dev/null
+++ b/src/tests/radio/sx1278/test-sx1278-bench-serial.cpp
@@ -0,0 +1,57 @@
+/* Copyright (c) 2021 Skyward Experimental Rocketry
+ * Author: Davide Mor
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+// Include the body of the test
+#include "test-sx1278-bench.cpp"
+
+int main()
+{
+    initBoard();
+    if(!initRadio()) {
+        while(1);
+    }
+
+    // Initialize backgrounds threads
+    spawnThreads();
+
+    while (1)
+    {
+        printf(
+            "\n[sx1278] Stats:\n"
+            "Tx bitrate:        %.2f kb/s\n"
+            "Packet sent:       %d\n"
+            "Rx bitrate:        %.2f kb/s\n"
+            "Packet received:   %d\n"
+            "Corrupted packets: %d\n"
+            "RSSI:              %.2f dBm\n"
+            "FEI:               %.2f Hz\n"
+            "SNR:               %.2f\n",
+            static_cast<float>(stats.txBitrate()) / 1000.0f, stats.sent_count,
+            static_cast<float>(stats.rxBitrate()) / 1000.0f, stats.recv_count,
+            stats.corrupted_count, stats.rssi,
+            stats.fei, stats.snr);
+
+        miosix::Thread::sleep(2000);
+    }
+
+    return 0;
+}
diff --git a/src/tests/radio/sx1278/fsk/test-sx1278-bench.cpp b/src/tests/radio/sx1278/test-sx1278-bench.cpp
similarity index 97%
rename from src/tests/radio/sx1278/fsk/test-sx1278-bench.cpp
rename to src/tests/radio/sx1278/test-sx1278-bench.cpp
index 66e153a862d7c9c74f44fdd0ab51767c6795ff46..ca8a8454ed1bda520671872953aaedf1c26adee9 100644
--- a/src/tests/radio/sx1278/fsk/test-sx1278-bench.cpp
+++ b/src/tests/radio/sx1278/test-sx1278-bench.cpp
@@ -20,9 +20,10 @@
  * THE SOFTWARE.
  */
 
+#include "sx1278-init.h"
+
 #include <drivers/timer/TimestampTimer.h>
 #include <miosix.h>
-#include <radio/SX1278/SX1278Fsk.h>
 #include <utils/MovingAverage.h>
 
 #include <thread>
@@ -30,8 +31,6 @@
 using namespace Boardcore;
 using namespace miosix;
 
-SX1278Fsk *sx1278 = nullptr;
-
 // Simple xorshift RNG
 uint32_t xorshift32()
 {
@@ -86,6 +85,7 @@ struct LinkStats
 
     float rssi = 0.0f;
     float fei  = 0.0f;
+    float snr  = 0.0f;
 
     uint64_t txBitrate()
     {
@@ -120,6 +120,7 @@ void recvLoop()
 
             stats.rssi = sx1278->getLastRxRssi();
             stats.fei  = sx1278->getLastRxFei();
+            stats.snr  = sx1278->getLastRxSnr();
         }
         else
         {