diff --git a/.clang-format b/.clang-format
index ac7e5d71..2c48af9f 100644
--- a/.clang-format
+++ b/.clang-format
@@ -106,6 +106,7 @@ ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
+PackConstructorInitializers: NextLine
# Taken from git's rules
PenaltyBreakAssignment: 30
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f05b58f8..4337bc32 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -105,6 +105,7 @@ target_sources(app
openrtx/src/protocols/M17/M17DSP.cpp
openrtx/src/protocols/M17/M17Golay.cpp
openrtx/src/protocols/M17/M17Callsign.cpp
+ openrtx/src/protocols/M17/Callsign.cpp
openrtx/src/protocols/M17/M17Modulator.cpp
openrtx/src/protocols/M17/M17Demodulator.cpp
openrtx/src/protocols/M17/M17FrameEncoder.cpp
diff --git a/meson.build b/meson.build
index 2e36fa30..2867d53f 100644
--- a/meson.build
+++ b/meson.build
@@ -62,6 +62,7 @@ openrtx_src = ['openrtx/src/core/state.c',
'openrtx/src/protocols/M17/M17DSP.cpp',
'openrtx/src/protocols/M17/M17Golay.cpp',
'openrtx/src/protocols/M17/M17Callsign.cpp',
+ 'openrtx/src/protocols/M17/Callsign.cpp',
'openrtx/src/protocols/M17/M17Modulator.cpp',
'openrtx/src/protocols/M17/M17Demodulator.cpp',
'openrtx/src/protocols/M17/M17FrameEncoder.cpp',
diff --git a/openrtx/include/protocols/M17/Callsign.hpp b/openrtx/include/protocols/M17/Callsign.hpp
new file mode 100644
index 00000000..6f875c22
--- /dev/null
+++ b/openrtx/include/protocols/M17/Callsign.hpp
@@ -0,0 +1,125 @@
+/***************************************************************************
+ * 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 CALLSIGN_H
+#define CALLSIGN_H
+
+#ifndef __cplusplus
+#error This header is C++ only!
+#endif
+
+#include
+#include "M17Datatypes.hpp"
+
+namespace M17
+{
+
+/**
+ * Class representing an M17 callsign object.
+ */
+class Callsign
+{
+public:
+ /**
+ * Default constructor.
+ * By default, an uninitialized callsign is set to invalid.
+ */
+ Callsign();
+
+ /**
+ * Construct a callsign object from an std::string
+ * The callsign can have up to 9 characters
+ *
+ * @param callsign: callsign string
+ */
+ Callsign(const std::string callsign);
+
+ /**
+ * Construct a callsign object from a NULL-terminated string
+ * The callsign can have up to 9 characters
+ *
+ * @param callsign: callsign string
+ */
+ Callsign(const char *callsign);
+
+ /**
+ * Construct a callsign object from a base-40 encoded callsign
+ *
+ * @param encodedCall: encoded callsign value
+ */
+ Callsign(const call_t &encodedCall);
+
+ /**
+ * Test if callsign is empty.
+ * A callsign is considered empty when its first character is NULL.
+ *
+ * @return true if the callsign is empty
+ */
+ inline bool isEmpty() const
+ {
+ return call[0] == '\0';
+ }
+
+ /**
+ * Test if callsign is a special one.
+ *
+ * @return true if the callsign is either ALL, INFO or ECHO
+ */
+ bool isSpecial() const;
+
+ /**
+ * Type-conversion operator to retrieve the callsign in encoded format
+ *
+ * @return the base-40 encoded version of the callsign
+ */
+ operator call_t() const;
+
+ /**
+ * Type-conversion operator to retrieve the callsign as a std::string
+ *
+ * @return a std::string containing the callsign
+ */
+ operator std::string() const;
+
+ /**
+ * Type-conversion operator to retrieve the callsign as a NULL-terminated
+ * string
+ *
+ * @return the callsign as a NULL-terminated string
+ */
+ operator const char *() const;
+
+ /**
+ * Comparison operator.
+ *
+ * @param other the incoming callsign to compare against
+ * @return true if callsigns are equivalent
+ */
+ bool operator==(const Callsign &other) const;
+
+private:
+ static constexpr size_t MAX_CALLSIGN_CHARS = 9;
+ static constexpr size_t MAX_CALLSIGN_LEN = MAX_CALLSIGN_CHARS + 1;
+ char call[MAX_CALLSIGN_LEN];
+};
+
+} // namespace M17
+
+#endif // CALLSIGN_H
diff --git a/openrtx/src/protocols/M17/Callsign.cpp b/openrtx/src/protocols/M17/Callsign.cpp
new file mode 100644
index 00000000..ed662e44
--- /dev/null
+++ b/openrtx/src/protocols/M17/Callsign.cpp
@@ -0,0 +1,153 @@
+/***************************************************************************
+ * 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 *
+ ***************************************************************************/
+
+#include
+#include "protocols/M17/Callsign.hpp"
+
+using namespace M17;
+
+static const char BROADCAST_CALL[] = "ALL";
+static const char INVALID_CALL[] = "INVALID";
+static const char charMap[] = " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-/.";
+
+Callsign::Callsign() : Callsign(INVALID_CALL)
+{
+}
+
+Callsign::Callsign(const std::string callsign) : Callsign(callsign.c_str())
+{
+}
+
+Callsign::Callsign(const char *callsign)
+{
+ std::memset(call, 0, sizeof(call));
+ std::strncpy(call, callsign, MAX_CALLSIGN_CHARS);
+}
+
+Callsign::Callsign(const call_t &encodedCall)
+{
+ bool isBroadcast = true;
+ bool isInvalid = true;
+
+ for (auto &elem : encodedCall) {
+ if (elem != 0xFF)
+ isBroadcast = false;
+
+ if (elem != 0x00)
+ isInvalid = false;
+ }
+
+ std::memset(call, 0, sizeof(call));
+
+ if (isBroadcast) {
+ std::strncpy(call, BROADCAST_CALL, sizeof(call));
+ return;
+ }
+
+ if (isInvalid) {
+ std::strncpy(call, INVALID_CALL, sizeof(call));
+ return;
+ }
+
+ // Convert to little endian format
+ uint64_t encoded = 0;
+ auto p = reinterpret_cast(&encoded);
+ std::copy(encodedCall.rbegin(), encodedCall.rend(), p);
+
+ size_t pos = 0;
+ while (encoded != 0 && pos < MAX_CALLSIGN_CHARS) {
+ call[pos++] = charMap[encoded % 40];
+ encoded /= 40;
+ }
+}
+
+bool Callsign::isSpecial() const
+{
+ if ((std::strcmp(call, "INFO") == 0) || (std::strcmp(call, "ECHO") == 0)
+ || (std::strcmp(call, "ALL") == 0))
+ return true;
+
+ return false;
+}
+
+Callsign::operator std::string() const
+{
+ return std::string(call);
+}
+
+Callsign::operator const char *() const
+{
+ return call;
+}
+
+Callsign::operator call_t() const
+{
+ call_t encoded;
+ uint64_t tmp = 0;
+
+ if (strcmp(call, BROADCAST_CALL) == 0) {
+ encoded.fill(0xFF);
+ return encoded;
+ }
+
+ if (strcmp(call, INVALID_CALL) == 0) {
+ encoded.fill(0x00);
+ return encoded;
+ }
+
+ for (int i = strlen(call) - 1; i >= 0; i--) {
+ tmp *= 40;
+
+ if (call[i] >= 'A' && call[i] <= 'Z') {
+ tmp += (call[i] - 'A') + 1;
+ } else if (call[i] >= '0' && call[i] <= '9') {
+ tmp += (call[i] - '0') + 27;
+ } else if (call[i] == '-') {
+ tmp += 37;
+ } else if (call[i] == '/') {
+ tmp += 38;
+ } else if (call[i] == '.') {
+ tmp += 39;
+ }
+ }
+
+ // Return encoded callsign in big endian format
+ auto *ptr = reinterpret_cast(&tmp);
+ std::copy(ptr, ptr + 6, encoded.rbegin());
+
+ return encoded;
+}
+
+bool Callsign::operator==(const Callsign &other) const
+{
+ // find slash and possibly truncate if slash is within first 3 chars
+ const char *truncatedLocal = call;
+ const char *truncatedIncoming = other.call;
+
+ const char *slash = std::strchr(call, '/');
+ if (slash && (slash - call) <= 2)
+ truncatedLocal = slash + 1;
+
+ slash = std::strchr(other.call, '/');
+ if (slash && (slash - other.call) <= 2)
+ truncatedIncoming = slash + 1;
+
+ return std::strcmp(truncatedLocal, truncatedIncoming) == 0;
+}
diff --git a/scripts/clang_format.sh b/scripts/clang_format.sh
index 8e5d1e1c..55f4e80d 100755
--- a/scripts/clang_format.sh
+++ b/scripts/clang_format.sh
@@ -69,9 +69,11 @@ openrtx/include/interfaces/radio.h
openrtx/include/peripherals/gps.h
openrtx/include/peripherals/rng.h
openrtx/include/peripherals/rtc.h
+openrtx/include/protocols/M17/Callsign.hpp
openrtx/include/protocols/M17/M17FrameDecoder.hpp
openrtx/src/core/dsp.cpp
openrtx/src/core/memory_profiling.cpp
+openrtx/src/protocols/M17/Callsign.cpp
openrtx/src/protocols/M17/M17FrameDecoder.cpp
platform/drivers/ADC/ADC0_GDx.h
platform/drivers/audio/MAX9814.h