diff --git a/openrtx/include/protocols/M17/DevEstimator.hpp b/openrtx/include/protocols/M17/DevEstimator.hpp
new file mode 100644
index 00000000..2aefa652
--- /dev/null
+++ b/openrtx/include/protocols/M17/DevEstimator.hpp
@@ -0,0 +1,139 @@
+/***************************************************************************
+ * Copyright (C) 2025 by Federico Amedeo Izzo IU2NUO, *
+ * Niccolò Izzo IU2KIN *
+ * Frederik Saraci IU2NRO *
+ * Silvano Seva IU2KWO *
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program; if not, see *
+ ***************************************************************************/
+
+#ifndef DEV_ESTIMATOR_H
+#define DEV_ESTIMATOR_H
+
+#include
+
+/**
+ * Symbol deviation estimator.
+ * This module allows to estimate the outer symbol deviation of a baseband
+ * stream. The baseband samples used for the estimation should be takend at the
+ * ideal sampling point. To work properly, the estimator needs to be initialized
+ * with a reference outer symbol deviation.
+ */
+class DevEstimator
+{
+public:
+ /**
+ * Constructor
+ */
+ DevEstimator() : outerDev({0, 0}), offset(0), posAccum(0), negAccum(0),
+ posCnt(0), negCnt(0)
+ {
+ }
+
+ /**
+ * Destructor
+ */
+ ~DevEstimator()
+ {
+ }
+
+ /**
+ * Initialize the estimator state.
+ * Calling this function clears the internal state.
+ *
+ * @param outerDev: initial value for outer symbol deviation
+ */
+ void init(const std::pair &outerDev)
+ {
+ this->outerDev = outerDev;
+ offset = 0;
+ posAccum = 0;
+ negAccum = 0;
+ posCnt = 0;
+ negCnt = 0;
+ }
+
+ /**
+ * Process a new sample.
+ *
+ * @param sample: baseband sample.
+ */
+ void sample(int16_t value)
+ {
+ int32_t posThresh = (2 * outerDev.first) / 3;
+ int32_t negThresh = (2 * outerDev.second) / 3;
+
+ if (value > posThresh) {
+ posAccum += value;
+ posCnt += 1;
+ }
+
+ if (value < negThresh) {
+ posAccum += value;
+ posCnt += 1;
+ }
+ }
+
+ /**
+ * Update the estimation of outer symbol deviation and zero-offset and
+ * start a new acquisition cycle.
+ */
+ void update()
+ {
+ if ((posCnt == 0) || (negCnt == 0))
+ return;
+
+ int32_t max = posAccum / posCnt;
+ int32_t min = negAccum / negCnt;
+ offset = (max + min) / 2;
+ outerDev.first = max - offset;
+ outerDev.second = min - offset;
+ posAccum = 0;
+ negAccum = 0;
+ posCnt = 0;
+ negCnt = 0;
+ }
+
+ /**
+ * Get the estimated outer symbol deviation from the last update.
+ * The function returns a std::pair where the first element is the positive
+ * deviation and the second the negative one.
+ *
+ * @return outer deviation.
+ */
+ std::pair outerDeviation()
+ {
+ return outerDev;
+ }
+
+ /**
+ * Get the estimated zero-offset from the last update.
+ *
+ * @return zero-offset.
+ */
+ int32_t zeroOffset()
+ {
+ return offset;
+ }
+
+private:
+ std::pair outerDev;
+ int32_t offset;
+ int32_t posAccum;
+ int32_t negAccum;
+ uint32_t posCnt;
+ uint32_t negCnt;
+};
+
+#endif