diff --git a/scripts/logdecoder/General/logdecoder.cpp b/scripts/logdecoder/General/logdecoder.cpp
index a237a6efc86fc7c05e94f49c7dc478fe92f2bd63..2de381cf780757a66c1dd754ac20d448f3981aa0 100644
--- a/scripts/logdecoder/General/logdecoder.cpp
+++ b/scripts/logdecoder/General/logdecoder.cpp
@@ -43,6 +43,7 @@
 #include <RIGv2/Sensors/SensorsData.h>
 #include <RIGv2/StateMachines/GroundModeManager/GroundModeManagerData.h>
 #include <RIGv2/StateMachines/TARS1/TARS1Data.h>
+#include <RIGv2/StateMachines/TARS3/TARS3Data.h>
 #include <algorithms/MEA/MEAData.h>
 #include <logger/Deserializer.h>
 #include <logger/LogTypes.h>
@@ -124,8 +125,10 @@ void registerTypes(Deserializer& ds)
     ds.registerType<RIGv2::N2TankPressureData>();
     ds.registerType<RIGv2::ActuatorsData>();
     ds.registerType<RIGv2::GroundModeManagerData>();
-    ds.registerType<RIGv2::TarsActionData>();
-    ds.registerType<RIGv2::TarsSampleData>();
+    ds.registerType<RIGv2::Tars1ActionData>();
+    ds.registerType<RIGv2::Tars1SampleData>();
+    ds.registerType<RIGv2::Tars3ActionData>();
+    ds.registerType<RIGv2::Tars3SampleData>();
 
     // Groundstation (ARP)
     ds.registerType<Antennas::StepperXData>();
diff --git a/scripts/logdecoder/RIGv2/logdecoder.cpp b/scripts/logdecoder/RIGv2/logdecoder.cpp
index 6062e9ff7a2a7f5c962c439b4cd167fc406ea0e3..78ad363572439805c264dbfdab9a3972f71ee74e 100644
--- a/scripts/logdecoder/RIGv2/logdecoder.cpp
+++ b/scripts/logdecoder/RIGv2/logdecoder.cpp
@@ -24,6 +24,7 @@
 #include <RIGv2/Sensors/SensorsData.h>
 #include <RIGv2/StateMachines/GroundModeManager/GroundModeManagerData.h>
 #include <RIGv2/StateMachines/TARS1/TARS1Data.h>
+#include <RIGv2/StateMachines/TARS3/TARS3Data.h>
 #include <logger/Deserializer.h>
 #include <logger/LogTypes.h>
 #include <tscpp/stream.h>
@@ -68,8 +69,10 @@ void registerTypes(Deserializer& ds)
     ds.registerType<N2TankPressureData>();
     ds.registerType<ActuatorsData>();
     ds.registerType<GroundModeManagerData>();
-    ds.registerType<TarsActionData>();
-    ds.registerType<TarsSampleData>();
+    ds.registerType<RIGv2::Tars1ActionData>();
+    ds.registerType<RIGv2::Tars1SampleData>();
+    ds.registerType<RIGv2::Tars3ActionData>();
+    ds.registerType<RIGv2::Tars3SampleData>();
     ds.registerType<VoltageData>();
 }
 
diff --git a/src/RIGv2/BoardScheduler.h b/src/RIGv2/BoardScheduler.h
index 11556aa7f0bc041d1079f7e4883f70f2addcee78..3d29aaa94d303cf047d33b72473f6fb09c40f9fc 100644
--- a/src/RIGv2/BoardScheduler.h
+++ b/src/RIGv2/BoardScheduler.h
@@ -33,16 +33,16 @@ class BoardScheduler : public Boardcore::Injectable
 {
 public:
     BoardScheduler()
-        : tars1(Config::Scheduler::TARS1_PRIORITY),
+        : tars(Config::Scheduler::TARS_PRIORITY),
           sensors(Config::Scheduler::SENSORS_PRIORITY)
     {
     }
 
     [[nodiscard]] bool start()
     {
-        if (!tars1.start())
+        if (!tars.start())
         {
-            LOG_ERR(logger, "Failed to start TARS1 scheduler");
+            LOG_ERR(logger, "Failed to start TARS scheduler");
             return false;
         }
 
@@ -58,7 +58,9 @@ public:
 
     bool isStarted() { return started; }
 
-    Boardcore::TaskScheduler& getTars1Scheduler() { return tars1; }
+    Boardcore::TaskScheduler& getTars1Scheduler() { return tars; }
+
+    Boardcore::TaskScheduler& getTars3Scheduler() { return tars; }
 
     Boardcore::TaskScheduler& getSensorsScheduler() { return sensors; }
 
@@ -72,7 +74,7 @@ private:
 
     std::atomic<bool> started{false};
 
-    Boardcore::TaskScheduler tars1;
+    Boardcore::TaskScheduler tars;
     Boardcore::TaskScheduler sensors;
 };
 
diff --git a/src/RIGv2/Configs/SchedulerConfig.h b/src/RIGv2/Configs/SchedulerConfig.h
index be37f3cf651b7c1e4e89c628ba9e6e5122bf6b7d..129a7f13b5593750e61e3265f712d62754611bfc 100644
--- a/src/RIGv2/Configs/SchedulerConfig.h
+++ b/src/RIGv2/Configs/SchedulerConfig.h
@@ -33,8 +33,8 @@ namespace Config
 namespace Scheduler
 {
 
-// Used for TARS1 task scheduler/FSM
-static const miosix::Priority TARS1_PRIORITY = miosix::PRIORITY_MAX - 1;
+// Used for TARS1/TARS3 task scheduler/FSM
+static const miosix::Priority TARS_PRIORITY = miosix::PRIORITY_MAX - 1;
 // Used for Sensors TaskScheduler
 static const miosix::Priority SENSORS_PRIORITY = miosix::PRIORITY_MAX - 2;
 
diff --git a/src/RIGv2/Configs/TARS3Config.h b/src/RIGv2/Configs/TARS3Config.h
new file mode 100644
index 0000000000000000000000000000000000000000..3c9011441df83bce8b43c2024c567c9f552e678c
--- /dev/null
+++ b/src/RIGv2/Configs/TARS3Config.h
@@ -0,0 +1,57 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Niccolò Betto
+ *
+ * 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>
+
+#include <chrono>
+#include <cstddef>
+#include <cstdint>
+
+namespace RIGv2
+{
+namespace Config
+{
+namespace TARS3
+{
+/* linter off */ using namespace std::chrono;
+/* linter off */ using namespace Boardcore::Units::Frequency;
+
+constexpr Hertz SAMPLE_PERIOD         = 100_hz;
+constexpr size_t MEDIAN_SAMPLE_NUMBER = 10;
+
+constexpr auto WAIT_BETWEEN_CYCLES = 1000ms;
+
+constexpr float PRESSURE_CHANGE_TOLERANCE   = 0.035;  // [bar]
+constexpr auto PRESSURE_STABILIZE_WAIT_TIME = 1000ms;
+
+// Cold refueling parameters
+constexpr float PRESSURE_LOWER_RANGE = 17;  // [bar]
+constexpr float PRESSURE_UPPER_RANGE = 20;  // [bar]
+
+constexpr auto FILLING_TIME = 3000ms;
+constexpr auto VENTING_TIME = 3000ms;
+
+}  // namespace TARS3
+}  // namespace Config
+}  // namespace RIGv2
diff --git a/src/RIGv2/StateMachines/TARS1/TARS1.cpp b/src/RIGv2/StateMachines/TARS1/TARS1.cpp
index 2d1e27c004ddcf5845b3cba132cb95c99ae3f576..d4c66dca7b78e4aff6f3b1799d31d18fc62fd5ae 100644
--- a/src/RIGv2/StateMachines/TARS1/TARS1.cpp
+++ b/src/RIGv2/StateMachines/TARS1/TARS1.cpp
@@ -34,7 +34,7 @@ using namespace miosix;
 
 TARS1::TARS1()
     : FSM(&TARS1::state_ready, miosix::STACK_DEFAULT_FOR_PTHREAD,
-          Config::Scheduler::TARS1_PRIORITY)
+          Config::Scheduler::TARS_PRIORITY)
 {
     EventBroker::getInstance().subscribe(this, TOPIC_TARS);
     EventBroker::getInstance().subscribe(this, TOPIC_MOTOR);
@@ -70,7 +70,7 @@ void TARS1::state_ready(const Event& event)
     {
         case EV_ENTRY:
         {
-            logAction(TarsActionType::READY);
+            logAction(Tars1ActionType::READY);
             break;
         }
 
@@ -101,7 +101,7 @@ void TARS1::state_refueling(const Event& event)
             actuators->closeAllServos();
 
             LOG_INFO(logger, "TARS start washing");
-            logAction(TarsActionType::WASHING);
+            logAction(Tars1ActionType::WASHING);
 
             // Start washing
             actuators->openServoWithTime(ServosList::OX_VENTING_VALVE,
@@ -124,7 +124,7 @@ void TARS1::state_refueling(const Event& event)
         case TARS_WASHING_DONE:
         {
             LOG_INFO(logger, "TARS washing done");
-            logAction(TarsActionType::OPEN_FILLING);
+            logAction(Tars1ActionType::OPEN_FILLING);
 
             // Open the filling for a long time
             actuators->openServoWithTime(ServosList::OX_FILLING_VALVE,
@@ -140,7 +140,7 @@ void TARS1::state_refueling(const Event& event)
         case TARS_PRESSURE_STABILIZED:
         {
             LOG_INFO(logger, "TARS check mass");
-            logAction(TarsActionType::CHECK_MASS);
+            logAction(Tars1ActionType::CHECK_MASS);
 
             // Lock in a new mass value
             {
@@ -173,7 +173,7 @@ void TARS1::state_refueling(const Event& event)
             }
 
             LOG_INFO(logger, "TARS open venting");
-            logAction(TarsActionType::OPEN_VENTING);
+            logAction(Tars1ActionType::OPEN_VENTING);
 
             // Open the venting and check for pressure stabilization
             actuators->openServo(ServosList::OX_VENTING_VALVE);
@@ -191,7 +191,7 @@ void TARS1::state_refueling(const Event& event)
         case TARS_CHECK_PRESSURE_STABILIZE:
         {
             LOG_INFO(logger, "TARS check pressure");
-            logAction(TarsActionType::CHECK_PRESSURE);
+            logAction(Tars1ActionType::CHECK_PRESSURE);
 
             {
                 Lock<FastMutex> lock(sampleMutex);
@@ -221,7 +221,7 @@ void TARS1::state_refueling(const Event& event)
         case TARS_FILLING_DONE:
         {
             LOG_INFO(logger, "TARS filling done");
-            logAction(TarsActionType::AUTOMATIC_STOP);
+            logAction(Tars1ActionType::AUTOMATIC_STOP);
 
             actuators->closeAllServos();
             transition(&TARS1::state_ready);
@@ -231,7 +231,7 @@ void TARS1::state_refueling(const Event& event)
         case MOTOR_MANUAL_ACTION:
         {
             LOG_INFO(logger, "TARS manual stop");
-            logAction(TarsActionType::MANUAL_STOP);
+            logAction(Tars1ActionType::MANUAL_STOP);
 
             // Disable next event
             EventBroker::getInstance().removeDelayed(nextDelayedEventId);
@@ -242,7 +242,7 @@ void TARS1::state_refueling(const Event& event)
         case MOTOR_START_TARS:
         {
             LOG_INFO(logger, "TARS manual stop");
-            logAction(TarsActionType::MANUAL_STOP);
+            logAction(Tars1ActionType::MANUAL_STOP);
 
             // The user requested that we stop
             getModule<Actuators>()->closeAllServos();
@@ -277,14 +277,14 @@ void TARS1::sample()
     }
 }
 
-void TARS1::logAction(TarsActionType action)
+void TARS1::logAction(Tars1ActionType action)
 {
-    TarsActionData data = {TimestampTimer::getTimestamp(), action};
+    Tars1ActionData data = {TimestampTimer::getTimestamp(), action};
     sdLogger.log(data);
 }
 
 void TARS1::logSample(float pressure, float mass)
 {
-    TarsSampleData data = {TimestampTimer::getTimestamp(), pressure, mass};
+    Tars1SampleData data = {TimestampTimer::getTimestamp(), pressure, mass};
     sdLogger.log(data);
 }
diff --git a/src/RIGv2/StateMachines/TARS1/TARS1.h b/src/RIGv2/StateMachines/TARS1/TARS1.h
index eba7653f2d985ada2ef80897f3b798946e4254a9..cac66d3696db63a917c02fe5643a972d7ba80700 100644
--- a/src/RIGv2/StateMachines/TARS1/TARS1.h
+++ b/src/RIGv2/StateMachines/TARS1/TARS1.h
@@ -53,7 +53,7 @@ private:
     void state_ready(const Boardcore::Event& event);
     void state_refueling(const Boardcore::Event& event);
 
-    void logAction(TarsActionType action);
+    void logAction(Tars1ActionType action);
     void logSample(float pressure, float mass);
 
     Boardcore::Logger& sdLogger   = Boardcore::Logger::getInstance();
diff --git a/src/RIGv2/StateMachines/TARS1/TARS1Data.h b/src/RIGv2/StateMachines/TARS1/TARS1Data.h
index bf1fc75e8c92389dfef10f7a79c76de8a81c59cb..8cf7d67f0183195173b220976364462a093c569c 100644
--- a/src/RIGv2/StateMachines/TARS1/TARS1Data.h
+++ b/src/RIGv2/StateMachines/TARS1/TARS1Data.h
@@ -29,7 +29,7 @@
 namespace RIGv2
 {
 
-enum class TarsActionType : uint8_t
+enum class Tars1ActionType : uint8_t
 {
     READY = 0,
     WASHING,
@@ -41,14 +41,14 @@ enum class TarsActionType : uint8_t
     MANUAL_STOP,
 };
 
-struct TarsActionData
+struct Tars1ActionData
 {
     uint64_t timestamp;
-    TarsActionType action;
+    Tars1ActionType action;
 
-    TarsActionData() : timestamp{0}, action{TarsActionType::READY} {}
+    Tars1ActionData() : timestamp{0}, action{Tars1ActionType::READY} {}
 
-    TarsActionData(uint64_t timestamp, TarsActionType action)
+    Tars1ActionData(uint64_t timestamp, Tars1ActionType action)
         : timestamp{timestamp}, action{action}
     {
     }
@@ -61,15 +61,15 @@ struct TarsActionData
     }
 };
 
-struct TarsSampleData
+struct Tars1SampleData
 {
     uint64_t timestamp;
     float pressure;
     float mass;
 
-    TarsSampleData() : timestamp{0}, pressure{0}, mass{0} {}
+    Tars1SampleData() : timestamp{0}, pressure{0}, mass{0} {}
 
-    TarsSampleData(uint64_t timestamp, float pressure, float mass)
+    Tars1SampleData(uint64_t timestamp, float pressure, float mass)
         : timestamp{timestamp}, pressure{pressure}, mass{mass}
     {
     }
diff --git a/src/RIGv2/StateMachines/TARS3/MedianFilter.h b/src/RIGv2/StateMachines/TARS3/MedianFilter.h
new file mode 100644
index 0000000000000000000000000000000000000000..0d14f3f1b12f4cd83a2ecb0a3d01fbc5623fe371
--- /dev/null
+++ b/src/RIGv2/StateMachines/TARS3/MedianFilter.h
@@ -0,0 +1,57 @@
+/* Copyright (c) 2024 Skyward Experimental Rocketry
+ * Authors: Davide Mor
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <array>
+#include <memory>
+
+namespace RIGv2
+{
+
+template <typename T, size_t Max>
+class MedianFilter
+{
+public:
+    MedianFilter() {}
+
+    void reset() { idx = 0; }
+
+    void add(T value)
+    {
+        values[idx] = value;
+        idx         = (idx + 1) % Max;
+    }
+
+    T calcMedian()
+    {
+        std::sort(values.begin(), values.end());
+        return values[idx / 2];
+    }
+
+private:
+    size_t idx                = 0;
+    std::array<T, Max> values = {0};
+};
+
+}  // namespace RIGv2
diff --git a/src/RIGv2/StateMachines/TARS3/TARS3.cpp b/src/RIGv2/StateMachines/TARS3/TARS3.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5fe64473103bb6e53a3d7ef7fca74f88f9166ef8
--- /dev/null
+++ b/src/RIGv2/StateMachines/TARS3/TARS3.cpp
@@ -0,0 +1,410 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Niccolò Betto
+ *
+ * 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 "TARS3.h"
+
+#include <RIGv2/Actuators/Actuators.h>
+#include <RIGv2/BoardScheduler.h>
+#include <RIGv2/Configs/TARS3Config.h>
+#include <RIGv2/Sensors/Sensors.h>
+#include <common/Events.h>
+#include <common/Topics.h>
+#include <drivers/timer/TimestampTimer.h>
+#include <events/EventBroker.h>
+
+using namespace std::chrono;
+using namespace Boardcore;
+using namespace Common;
+
+namespace RIGv2
+{
+
+TARS3::TARS3()
+    : HSM(&TARS3::Ready, miosix::STACK_DEFAULT_FOR_PTHREAD,
+          Config::Scheduler::TARS_PRIORITY)
+{
+    EventBroker::getInstance().subscribe(this, TOPIC_TARS);
+    EventBroker::getInstance().subscribe(this, TOPIC_MOTOR);
+}
+
+bool TARS3::start()
+{
+    TaskScheduler& scheduler = getModule<BoardScheduler>()->getTars1Scheduler();
+
+    uint8_t result =
+        scheduler.addTask([this]() { sample(); }, Config::TARS3::SAMPLE_PERIOD);
+
+    if (result == 0)
+    {
+        LOG_ERR(logger, "Failed to add TARS3 sample task");
+        return false;
+    }
+
+    if (!HSM::start())
+    {
+        LOG_ERR(logger, "Failed to activate TARS3 thread");
+        return false;
+    }
+
+    return true;
+}
+
+void TARS3::sample()
+{
+    Sensors* sensors = getModule<Sensors>();
+
+    pressureFilter.add(sensors->getOxTankBottomPressure().pressure);
+    massFilter.add(sensors->getOxTankWeight().load);
+    medianSamples++;
+
+    if (medianSamples == Config::TARS3::MEDIAN_SAMPLE_NUMBER)
+    {
+        float pressure = pressureFilter.calcMedian();
+        float mass     = massFilter.calcMedian();
+        medianSamples  = 0;
+
+        logSample(pressure, mass);
+
+        {
+            Lock<FastMutex> lock(sampleMutex);
+            pressureSample = pressure;
+            massSample     = mass;
+        }
+    }
+}
+
+void TARS3::logAction(Tars3Action action, float data)
+{
+    auto data = Tars3ActionData{.timestamp = TimestampTimer::getTimestamp(),
+                                .action    = action,
+                                .data      = data};
+    sdLogger.log(data);
+}
+
+void TARS3::logSample(float pressure, float mass)
+{
+    Tars3SampleData data = {TimestampTimer::getTimestamp(), pressure, mass};
+    sdLogger.log(data);
+}
+
+State TARS3::Ready(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            logAction(Tars3Action::READY);
+            return HANDLED;
+        }
+
+        case MOTOR_START_TARS:
+        {
+            return transition(&TARS3::Refueling);
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&TARS3::state_top);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State TARS3::Refueling(const Event& event)
+{
+    Actuators* actuators = getModule<Actuators>();
+
+    switch (event)
+    {
+        case EV_INIT:
+        {
+            previousPressure = 0.0f;
+            currentPressure  = 0.0f;
+            fillingTime      = Config::TARS3::FILLING_TIME;
+            ventingTime      = Config::TARS3::VENTING_TIME;
+
+            // Initialize the valves to a known closed state
+            actuators->closeAllServos();
+
+            LOG_INFO(logger, "TARS3 cold refueling start");
+            logAction(Tars3Action::START);
+
+            return transition(&TARS3::RefuelingWaitAfterCycle);
+        }
+
+        case EV_ENTRY:
+        {
+            return HANDLED;
+        }
+
+        case MOTOR_MANUAL_ACTION:
+        {
+            LOG_INFO(logger, "TARS3 stopped because of manual valve action");
+            logAction(Tars3Action::MANUAL_ACTION_STOP);
+
+            return transition(&TARS3::Ready);
+        }
+
+        case MOTOR_STOP_TARS:
+        {
+            LOG_INFO(logger, "TARS3 stopped because of manual stop");
+            logAction(Tars3Action::MANUAL_STOP);
+
+            return transition(&TARS3::Ready);
+        }
+
+        case EV_EXIT:
+        {
+            actuators->closeAllServos();
+            EventBroker::getInstance().removeDelayed(delayedEventId);
+
+            return HANDLED;
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&TARS3::state_top);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State TARS3::RefuelingWaitAfterCycle(const Event& event)
+{
+    {
+        Lock<FastMutex> lock(sampleMutex);
+        previousPressure = currentPressure;
+        currentPressure  = pressureSample;
+    }
+
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            LOG_INFO(logger, "Waiting for system to stabilize after cycle");
+            logAction(Tars3Action::WAITING_CYCLE);
+
+            EventBroker::getInstance().postDelayed(
+                TARS_CHECK_PRESSURE_STABILIZE, TOPIC_TARS,
+                milliseconds{Config::TARS3::WAIT_BETWEEN_CYCLES}.count());
+
+            return HANDLED;
+        }
+
+        case TARS_CHECK_PRESSURE_STABILIZE:
+        {
+            float pressureChange = std::abs(currentPressure - previousPressure);
+
+            if (pressureChange < Config::TARS3::PRESSURE_CHANGE_TOLERANCE)
+            {
+                // The pressure is stable
+                LOG_INFO(logger,
+                         "Pressure is stable, proceeding with the cycle");
+                logAction(Tars3Action::PRESSURE_STABLE_CYCLE, pressureChange);
+
+                EventBroker::getInstance().post(TARS_PRESSURE_STABILIZED,
+                                                TOPIC_TARS);
+            }
+            else
+            {
+                // Pressure is not stable yet, schedule a new check
+                LOG_INFO(logger,
+                         "Waiting for pressure to stabilize before cycle");
+                logAction(Tars3Action::WAITING_CYCLE, pressureChange);
+
+                delayedEventId = EventBroker::getInstance().postDelayed(
+                    TARS_CHECK_PRESSURE_STABILIZE, TOPIC_TARS,
+                    milliseconds{Config::TARS3::PRESSURE_STABILIZE_WAIT_TIME}
+                        .count());
+            }
+
+            return HANDLED;
+        }
+
+        case TARS_PRESSURE_STABILIZED:
+        {
+            if (currentPressure < Config::TARS3::PRESSURE_LOWER_RANGE)
+            {
+                // If OX is cold enough, start a new filling-venting cycle
+                return transition(&TARS3::RefuelingFilling);
+            }
+            else
+            {
+                // OX is too hot, vent to lower the temperature
+                return transition(&TARS3::RefuelingVenting);
+            }
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&TARS3::Refueling);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State TARS3::RefuelingFilling(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            LOG_INFO(logger, "Filling");
+            logAction(Tars3Action::FILLING, fillingTime.count());
+
+            getModule<Actuators>()->openServoWithTime(
+                ServosList::OX_FILLING_VALVE, fillingTime.count());
+
+            return HANDLED;
+        }
+
+        case MOTOR_OX_FIL_CLOSE:
+        {
+            LOG_INFO(logger, "Filling done");
+
+            return transition(&TARS3::RefuelingWaitAfterFilling);
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&TARS3::Refueling);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State TARS3::RefuelingWaitAfterFilling(const Event& event)
+{
+    {
+        Lock<FastMutex> lock(sampleMutex);
+        previousPressure = currentPressure;
+        currentPressure  = pressureSample;
+    }
+
+    switch (event)
+    {
+        case EV_ENTRY:
+        case TARS_CHECK_PRESSURE_STABILIZE:
+        {
+            float pressureChange = std::abs(currentPressure - previousPressure);
+
+            if (pressureChange < Config::TARS3::PRESSURE_CHANGE_TOLERANCE)
+            {
+                // The pressure is stable
+                LOG_INFO(logger,
+                         "Pressure is stable after filling, proceeding");
+                logAction(Tars3Action::PRESSURE_STABLE_FILLING, pressureChange);
+
+                EventBroker::getInstance().post(TARS_PRESSURE_STABILIZED,
+                                                TOPIC_TARS);
+            }
+            else
+            {
+                // Pressure is not stable yet, schedule a new check
+                LOG_INFO(logger,
+                         "Waiting for pressure to stabilize after filling");
+                logAction(Tars3Action::WAITING_FILLING, pressureChange);
+
+                delayedEventId = EventBroker::getInstance().postDelayed(
+                    TARS_CHECK_PRESSURE_STABILIZE, TOPIC_TARS,
+                    milliseconds{Config::TARS3::PRESSURE_STABILIZE_WAIT_TIME}
+                        .count());
+            }
+
+            return HANDLED;
+        }
+
+        case TARS_PRESSURE_STABILIZED:
+        {
+            if (currentPressure > Config::TARS3::PRESSURE_UPPER_RANGE)
+            {
+                // OX is hot, vent to lower the temperature and end the cycle
+                return transition(&TARS3::RefuelingVenting);
+            }
+            else
+            {
+                // OX is still cold enough, keep filling
+                return transition(&TARS3::RefuelingFilling);
+            }
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&TARS3::Refueling);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
+
+State TARS3::RefuelingVenting(const Event& event)
+{
+    switch (event)
+    {
+        case EV_ENTRY:
+        {
+            LOG_INFO(logger, "Venting");
+            logAction(Tars3Action::VENTING, ventingTime.count());
+
+            getModule<Actuators>()->openServoWithTime(
+                ServosList::OX_VENTING_VALVE, ventingTime.count());
+
+            return HANDLED;
+        }
+
+        case MOTOR_OX_VEN_CLOSE:
+        {
+            LOG_INFO(logger, "Venting done");
+
+            return transition(&TARS3::RefuelingWaitAfterCycle);
+        }
+
+        case EV_EMPTY:
+        {
+            return tranSuper(&TARS3::Refueling);
+        }
+
+        default:
+        {
+            return UNHANDLED;
+        }
+    }
+}
diff --git a/src/RIGv2/StateMachines/TARS3/TARS3.h b/src/RIGv2/StateMachines/TARS3/TARS3.h
new file mode 100644
index 0000000000000000000000000000000000000000..6d81274b8b685983bcf04c307d99d0014afcdff8
--- /dev/null
+++ b/src/RIGv2/StateMachines/TARS3/TARS3.h
@@ -0,0 +1,109 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Niccolò Betto
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#pragma once
+
+#include <events/HSM.h>
+#include <utils/DependencyManager/DependencyManager.h>
+
+#include "MedianFilter.h"
+#include "TARS3Data.h"
+
+namespace RIGv2
+{
+
+class Sensors;
+class Actuators;
+class BoardScheduler;
+
+class TARS3
+    : public Boardcore::InjectableWithDeps<BoardScheduler, Sensors, Actuators>,
+      public Boardcore::HSM<TARS3>
+{
+public:
+    TARS3();
+
+    [[nodiscard]] bool start();
+
+private:
+    void sample();
+
+    // HSM states
+
+    /**
+     * @brief TARS3 is ready and waiting for the user to start cold refueling.
+     */
+    Boardcore::State Ready(const Boardcore::Event& event);
+
+    /**
+     * @brief Super state for when TARS3 is refueling.
+     */
+    Boardcore::State Refueling(const Boardcore::Event& event);
+
+    /**
+     * @brief TARS3 is waiting for the pressure to stabilize after completing a
+     * filling-venting cycle.
+     * Super state: Refueling
+     */
+    Boardcore::State RefuelingWaitAfterCycle(const Boardcore::Event& event);
+
+    /**
+     * @brief TARS3 is filling the OX tank.
+     * Super state: Refueling
+     */
+    Boardcore::State RefuelingFilling(const Boardcore::Event& event);
+
+    /**
+     * @brief TARS3 is waiting for the pressure to stabilize after filling.
+     * Super state: Refueling
+     */
+    Boardcore::State RefuelingWaitAfterFilling(const Boardcore::Event& event);
+
+    /**
+     * @brief TARS3 is venting the OX tank.
+     * Super state: Refueling
+     */
+    Boardcore::State RefuelingVenting(const Boardcore::Event& event);
+
+    void logAction(Tars3Action action, float data = 0);
+    void logSample(float pressure, float mass);
+
+    std::chrono::milliseconds fillingTime = 0ms;
+    std::chrono::milliseconds ventingTime = 0ms;
+
+    float previousPressure = 0;
+    float currentPressure  = 0;
+
+    int medianSamples = 0;
+    MedianFilter<float, Config::TARS3::MEDIAN_SAMPLE_NUMBER> massFilter;
+    MedianFilter<float, Config::TARS3::MEDIAN_SAMPLE_NUMBER> pressureFilter;
+
+    miosix::FastMutex sampleMutex;
+    float massSample     = 0;
+    float pressureSample = 0;
+
+    uint16_t delayedEventId = 0;
+
+    Boardcore::Logger& sdLogger   = Boardcore::Logger::getInstance();
+    Boardcore::PrintLogger logger = Boardcore::Logging::getLogger("tars3");
+};
+}  // namespace RIGv2
diff --git a/src/RIGv2/StateMachines/TARS3/TARS3Data.h b/src/RIGv2/StateMachines/TARS3/TARS3Data.h
new file mode 100644
index 0000000000000000000000000000000000000000..36a1e3b025c3a5000aee22ed49ec2905637918d8
--- /dev/null
+++ b/src/RIGv2/StateMachines/TARS3/TARS3Data.h
@@ -0,0 +1,118 @@
+/* Copyright (c) 2025 Skyward Experimental Rocketry
+ * Author: Niccolò Betto
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <iostream>
+#include <string>
+
+namespace RIGv2
+{
+
+enum class Tars3Action : uint32_t
+{
+    READY = 0,
+    START,
+    WAITING_CYCLE,
+    WAITING_FILLING,
+    PRESSURE_STABLE_CYCLE,
+    PRESSURE_STABLE_FILLING,
+    FILLING,
+    VENTING,
+    MANUAL_STOP,
+    MANUAL_ACTION_STOP,
+};
+
+static std::array<const char*, 10> TARS3_ACTION_STRINGS = {
+    "READY",
+    "START",
+    "WAITING_CYCLE",
+    "WAITING_FILLING",
+    "PRESSURE_STABLE_CYCLE",
+    "PRESSURE_STABLE_FILLING",
+    "FILLING",
+    "VENTING",
+    "MANUAL_STOP",
+    "MANUAL_ACTION_STOP",
+};
+
+static std::array<const char*, 10> TARS3_ACTION_DATA_TYPE = {
+    "",                // READY
+    "",                // START
+    "PRESSURE_DELTA",  // WAITING_CYCLE
+    "PRESSURE_DELTA",  // WAITING_FILLING
+    "PRESSURE_DELTA",  // PRESSURE_STABLE_CYCLE
+    "PRESSURE_DELTA",  // PRESSURE_STABLE_FILLING
+    "OPEN_TIME",       // FILLING
+    "OPEN_TIME",       // VENTING
+    "",                // MANUAL_STOP
+    "",                // MANUAL_ACTION_STOP
+};
+
+inline std::ostream& operator<<(std::ostream& os, Tars3Action action)
+{
+    os << TARS3_ACTION_STRINGS[static_cast<uint32_t>(action)];
+    return os;
+}
+
+struct Tars3ActionData
+{
+    uint64_t timestamp = 0;
+    Tars3Action action = Tars3Action::READY;
+    float data         = 0;  // Additional data attached to the action
+
+    static std::string header()
+    {
+        return "timestamp,action,actionName,data,dataType\n";
+    }
+
+    void print(std::ostream& os) const
+    {
+        os << timestamp << "," << (int)action << "," << action << "," << data
+           << "," << TARS3_ACTION_DATA_TYPE[static_cast<uint32_t>(action)]
+           << "\n";
+    }
+};
+
+struct Tars3SampleData
+{
+    uint64_t timestamp;
+    float pressure;
+    float mass;
+
+    Tars3SampleData() : timestamp{0}, pressure{0}, mass{0} {}
+
+    Tars3SampleData(uint64_t timestamp, float pressure, float mass)
+        : timestamp{timestamp}, pressure{pressure}, mass{mass}
+    {
+    }
+
+    static std::string header() { return "timestamp,pressure,mass\n"; }
+
+    void print(std::ostream& os) const
+    {
+        os << timestamp << "," << pressure << "," << mass << "\n";
+    }
+};
+
+}  // namespace RIGv2
diff --git a/src/RIGv2/rig-v2-entry.cpp b/src/RIGv2/rig-v2-entry.cpp
index 6548c191f8c6863add690115be2c3471a51d389e..5a61a17d7daeb4e9498dbd50ff650192b9c99681 100644
--- a/src/RIGv2/rig-v2-entry.cpp
+++ b/src/RIGv2/rig-v2-entry.cpp
@@ -29,6 +29,7 @@
 #include <RIGv2/Sensors/Sensors.h>
 #include <RIGv2/StateMachines/GroundModeManager/GroundModeManager.h>
 #include <RIGv2/StateMachines/TARS1/TARS1.h>
+#include <RIGv2/StateMachines/TARS3/TARS3.h>
 #include <common/Events.h>
 #include <diagnostic/CpuMeter/CpuMeter.h>
 #include <diagnostic/StackLogger.h>
@@ -60,6 +61,7 @@ int main()
     CanHandler* canHandler = new CanHandler();
     GroundModeManager* gmm = new GroundModeManager();
     TARS1* tars1           = new TARS1();
+    TARS3* tars3           = new TARS3();
     Radio* radio           = new Radio();
 
     Logger& sdLogger    = Logger::getInstance();
@@ -83,7 +85,8 @@ int main()
                       manager.insert<CanHandler>(canHandler) &&
                       manager.insert<Registry>(registry) &&
                       manager.insert<GroundModeManager>(gmm) &&
-                      manager.insert<TARS1>(tars1) && manager.inject();
+                      manager.insert<TARS1>(tars1) &&
+                      manager.insert<TARS3>(tars3) && manager.inject();
 
     if (!initResult)
     {
@@ -172,6 +175,13 @@ int main()
         std::cout << "*** Failed to start TARS1 ***" << std::endl;
     }
 
+    std::cout << "Starting TARS3" << std::endl;
+    if (!tars3->start())
+    {
+        initResult = false;
+        std::cout << "*** Failed to start TARS3 ***" << std::endl;
+    }
+
     std::cout << "Starting BoardScheduler" << std::endl;
     if (!scheduler->start())
     {