diff --git a/Event/DigiEvent/include/Event/UTDigit.h b/Event/DigiEvent/include/Event/UTDigit.h
index 1bdbcf05e4e82a53ecba60a1241ea126092329ed..0e4da0afbb24f425ad37994d047fed5385698714 100644
--- a/Event/DigiEvent/include/Event/UTDigit.h
+++ b/Event/DigiEvent/include/Event/UTDigit.h
@@ -34,7 +34,8 @@ namespace LHCb {
    * Upstream tracker digitization class
    *
    * @author Xuhao Yuan (based on code by Andy Beiter and Matthew Needham)
-   *
+   * @author Wojciech Krupa
+
    */
 
   class UTDigit final : public KeyedObject<LHCb::Detector::UT::ChannelID> {
@@ -75,6 +76,26 @@ namespace LHCb {
       digit.m_daqID           = 0;
     }
 
+    /// constructor for NZS events with mcm parameters
+    UTDigit( LHCb::Detector::UT::ChannelID utchannelID, float charge, unsigned int utdaqID, int asic, signed int mcmVal,
+             unsigned int mcmStrip )
+        : KeyedObject<LHCb::Detector::UT::ChannelID>{utchannelID}
+        , m_depositedCharge( charge )
+        , m_daqID( utdaqID )
+        , m_asicNum( asic )
+        , m_mcmVal( mcmVal )
+        , m_mcmStrip( mcmStrip ) {
+      assert( utchannelID.isUT() && "Non-UT channelID" );
+    }
+
+    /// constructor for  simplified NZS events with mcm parameters but without daqid and asic_num
+    UTDigit( LHCb::Detector::UT::ChannelID utchannelID, float charge, signed int mcmVal, unsigned int mcmStrip )
+        : KeyedObject<LHCb::Detector::UT::ChannelID>{utchannelID}
+        , m_depositedCharge( charge )
+        , m_mcmVal( mcmVal )
+        , m_mcmStrip( mcmStrip ) {
+      assert( utchannelID.isUT() && "Non-UT channelID" );
+    }
     // Move assignment operator
     UTDigit& operator=( UTDigit&& other ) noexcept = default;
 
@@ -109,6 +130,27 @@ namespace LHCb {
     /// short cut for strip
     [[nodiscard]] unsigned int strip() const { return channelID().strip(); };
 
+    /// short cut for stave
+    [[nodiscard]] unsigned int stave() const { return channelID().stave(); };
+
+    /// short cut for module
+    [[nodiscard]] unsigned int module() const { return channelID().module(); };
+
+    /// short cut for face
+    [[nodiscard]] unsigned int face() const { return channelID().face(); };
+
+    /// short cut for side
+    [[nodiscard]] unsigned int side() const { return channelID().side(); };
+
+    /// short cut for asic_num
+    [[nodiscard]] unsigned int asic() const { return m_asicNum; };
+
+    /// short cut for mcm
+    [[nodiscard]] signed int mcmVal() const { return m_mcmVal; };
+
+    /// short cut for mcmStrip
+    [[nodiscard]] unsigned int mcmStrip() const { return m_mcmStrip; };
+
     /// Print the unique sector name
     [[nodiscard]] std::string sectorName() const;
 
@@ -135,6 +177,9 @@ namespace LHCb {
   private:
     float        m_depositedCharge{0.0}; ///< charge deposited on strip
     unsigned int m_daqID{0};
+    int          m_asicNum{-1};
+    signed int   m_mcmVal{0};
+    unsigned int m_mcmStrip{0};
 
   }; // class UTDigit
 
diff --git a/UT/UTDAQ/CMakeLists.txt b/UT/UTDAQ/CMakeLists.txt
index c4da2965f3698c401a98e568e84458e6a612e844..27a53f60f41c98ac493938afe5b7b7e845cd0bc2 100644
--- a/UT/UTDAQ/CMakeLists.txt
+++ b/UT/UTDAQ/CMakeLists.txt
@@ -38,9 +38,10 @@ gaudi_add_module(UTDAQ
         src/component/UTRawBankMonitor.cpp
         src/component/UTReadoutTool.cpp
         src/component/UTRawBankToUTDigitsAlg.cpp
+        src/component/UTRawBankToUTNZSDigitsAlg.cpp    
     LINK
         LHCb::DAQEventLib
-        LHCb::DAQUtilsLib
+        LHCb::DAQUtilsLib 
         LHCb::DetDescLib
         LHCb::DigiEvent
         LHCb::LHCbKernel
@@ -53,3 +54,12 @@ gaudi_add_module(UTDAQ
 )
 
 gaudi_add_tests(QMTest)
+
+if(NOT USE_DD4HEP)
+    set_property(
+        TEST
+            UTDAQ.ut_decoding_nzs
+        PROPERTY
+            DISABLED TRUE
+    )
+endif()
diff --git a/UT/UTDAQ/src/component/UTRawBankToUTNZSDigitsAlg.cpp b/UT/UTDAQ/src/component/UTRawBankToUTNZSDigitsAlg.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..218c6226abb68f285827b1d74e03f2a91dd6fbd2
--- /dev/null
+++ b/UT/UTDAQ/src/component/UTRawBankToUTNZSDigitsAlg.cpp
@@ -0,0 +1,210 @@
+/*****************************************************************************\
+* (c) Copyright 2000-2018 CERN for the benefit of the LHCb Collaboration      *
+*                                                                             *
+* This software is distributed under the terms of the GNU General Public      *
+* Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   *
+*                                                                             *
+* In applying this licence, CERN does not waive the privileges and immunities *
+* granted to it by virtue of its status as an Intergovernmental Organization  *
+* or submit itself to any jurisdiction.                                       *
+\*****************************************************************************/
+
+/** @class UTRawBankToUTNZSDigitsAlg
+ *
+ *  Algorithm to decode UTNZSRawBank to UTDigits (TELL40 NZS decoder)
+ *  Implementation file for class : UTNZSRawBankToUTDigitsAlg
+ *
+ *  - \b BankLocation: Location of UTRawBank
+ *  - \b OutputDigitData:  Location of output UTDigit
+ *
+ *  @author Wojciech Krupa (based on code by Xuhao Yuan, wokrupa@cern.ch)
+ *  @date   2021-04-19  / 2023-05-25 / 2024-03-13
+ */
+
+#include "Event/RawBank.h"
+#include "Event/RawEvent.h"
+#include "Event/UTDigit.h"
+#include "Kernel/IUTReadoutTool.h"
+#include "Kernel/UTDAQBoard.h"
+#include "Kernel/UTDAQDefinitions.h"
+#include "Kernel/UTDAQID.h"
+#include "Kernel/UTNZSDecoder.h"
+#include "LHCbAlgs/Transformer.h"
+#include "UTDAQ/UTDAQHelper.h"
+#include <algorithm>
+#include <bitset>
+#include <cstdint>
+#include <iomanip>
+#include <string>
+#include <vector>
+
+typedef std::vector<std::pair<unsigned int, int>> BanksTypes;
+
+class UTRawBankToUTNZSDigitsAlg : public LHCb::Algorithm::Transformer<LHCb::UTDigits( const LHCb::RawBank::View& )> {
+
+public:
+  /// Standard constructor
+  UTRawBankToUTNZSDigitsAlg( const std::string& name, ISvcLocator* pSvcLocator )
+      : Transformer{name, pSvcLocator, {{"UTBank", {}}}, {{"OutputDigitData", LHCb::UTDigitLocation::UTDigits}}} {}
+
+  LHCb::UTDigits operator()( const LHCb::RawBank::View& ) const override; ///< Algorithm execution
+
+  // Counters for decoded banks
+  mutable Gaudi::Accumulators::Counter<> m_validHeaders{this, "#Nb Valid UT banks"};
+  mutable Gaudi::Accumulators::Counter<> m_validDigits{this, "#Nb Valid UT digits"};
+  mutable Gaudi::Accumulators::Counter<> m_invalidBanks{this, "#Nb Invalid banks"};
+  mutable Gaudi::Accumulators::Counter<> m_errors_insert{this, "#Error - failure during inserting hit in TES"};
+
+private:
+  // The readout tool for handling DAQID and ChannelID
+  ToolHandle<IUTReadoutTool> readoutTool{this, "ReadoutTool", "UTReadoutTool"};
+};
+
+// Declaration of the Algorithm Factory
+DECLARE_COMPONENT( UTRawBankToUTNZSDigitsAlg )
+
+using namespace LHCb;
+
+//=============================================================================
+// Main execution
+//=============================================================================
+
+LHCb::UTDigits UTRawBankToUTNZSDigitsAlg::operator()( const LHCb::RawBank::View& banks ) const {
+
+  LHCb::UTDigits digits;
+
+  for ( const LHCb::RawBank* bank : banks ) {
+
+    if ( msgLevel( MSG::DEBUG ) ) {
+      debug() << "------------------------- ------- UT RAW BANK  ------------------------------- -----------" << endmsg;
+      debug() << "Bank: 0x" << std::uppercase << std::hex << bank->sourceID() << endmsg;
+      debug() << "   Size: " << std::dec << bank->size() << "B" << endmsg;
+      debug() << "   Type: " << bank->type() << endmsg;
+      debug() << "   Version: " << bank->version() << endmsg;
+      debug() << "|     |      HEADER                 | LANE5        | LANE4        | LANE3        | LANE2        "
+                 "| LANE1        | LANE0       |"
+              << endmsg;
+      // Dump raw UT bank
+      u_int8_t     iw2     = 0;
+      unsigned int counter = 0;
+      for ( const u_int8_t* w = bank->begin<u_int8_t>(); w != bank->end<u_int8_t>(); ++w ) {
+        ++iw2;
+        if ( ( iw2 - 1 ) % 32 == 0 ) {
+          counter++;
+          if ( counter % 34 == 0 ) {
+            debug() << endmsg;
+            debug() << "---------------------------------------------------------------------------------------------"
+                       "-------------------------------"
+                    << endmsg;
+          }
+          debug() << endmsg;
+          debug() << "0x" << std::setfill( '0' ) << std::setw( 2 ) << std::hex << unsigned( iw2 ) << " ";
+        }
+        if ( ( iw2 - 1 ) % 4 == 0 ) debug() << " | ";
+
+        debug() << std::uppercase << std::hex << std::setw( 2 ) << unsigned( *w ) << " ";
+      }
+      debug() << endmsg;
+    }
+
+    auto decode = [&]( UTNZSDecoder<::UTDAQ::version::v5> decoder ) {
+      if ( msgLevel( MSG::DEBUG ) ) {
+        std::string bxid = std::bitset<12>( decoder.getBXID() ).to_string();
+        std::string flag = std::bitset<4>( decoder.getflags() ).to_string();
+        debug() << "Bxid: " << bxid << " " << std::dec << decoder.getBXID() << " Flag bits: " << flag << endmsg;
+        debug() << "Clusters in lanes from 0 to 5: ";
+        for ( unsigned int lane = 0; lane < 6; lane++ ) { debug() << decoder.nClusters( lane ) << " "; }
+        debug() << endmsg;
+      }
+
+      // Decode NZS event
+      for ( const UTNZSWord& aWord : decoder.posRange() ) {
+        try {
+
+          if ( msgLevel( MSG::DEBUG ) ) {
+            std::string binary = std::bitset<16>( aWord.value() ).to_string();
+            debug() << "----------- DECODED DIGIT --------------------" << endmsg;
+            debug() << "Binary: " << binary << " Hex: " << std::hex << std::setfill( '0' ) << std::uppercase
+                    << std::setw( 4 ) << aWord.value() << endmsg;
+            debug() << "Position in the bank: " << std::dec << aWord.pos() << endmsg;
+            debug() << "Lane: " << std::dec << aWord.lane() << "  ASIC: " << aWord.asic_num() << endmsg;
+          }
+          // Decode left digit of the NZS Word
+          if ( aWord.not_nzs_header() ) {
+            ++m_validDigits;
+            UTDAQID daqidL =
+                UTDAQID( ( UTDAQID::BoardID )( LHCb::UTDAQ::boardIDfromSourceID( bank->sourceID() ) ),
+                         ( UTDAQID::LaneID )( aWord.lane() ), ( UTDAQID::ChannelID )( aWord.channelIDL() & 0x1ff ) );
+
+            Detector::UT::ChannelID channelIDL = readoutTool->daqIDToChannelID( daqidL );
+
+            if ( msgLevel( MSG::DEBUG ) ) {
+              debug() << "Channel from data bank: " << std::dec << aWord.channelIDL()
+                      << " ADC from data bank: " << static_cast<int16_t>( (int8_t)aWord.adcL() ) << endmsg;
+              debug() << daqidL << endmsg;
+              debug() << channelIDL << endmsg;
+              debug() << "Left Channel: " << aWord.channelIDL()
+                      << " ADC: " << static_cast<int16_t>( (int8_t)aWord.adcL() ) << endmsg;
+            }
+            try {
+              digits.insert( new UTDigit( channelIDL, (int8_t)aWord.adcL(), daqidL.id(), aWord.asic_num(),
+                                          aWord.mcm_val(), aWord.mcm_ch() ),
+                             channelIDL );
+            } catch ( ... ) {
+              if ( msgLevel( MSG::DEBUG ) ) debug() << "Problem with inserting" << endmsg;
+              ++m_errors_insert;
+              continue;
+            }
+          } else {
+            if ( msgLevel( MSG::DEBUG ) ) debug() << "Skipping this digit" << endmsg;
+          }
+          // Decode right digit of the NZS Word
+          if ( aWord.not_nzs_header() ) {
+            ++m_validDigits;
+            UTDAQID daqidR =
+                UTDAQID( ( UTDAQID::BoardID )( LHCb::UTDAQ::boardIDfromSourceID( bank->sourceID() ) ),
+                         ( UTDAQID::LaneID )( aWord.lane() ), ( UTDAQID::ChannelID )( aWord.channelIDR() & 0x1ff ) );
+
+            Detector::UT::ChannelID channelIDR = readoutTool->daqIDToChannelID( daqidR );
+
+            if ( msgLevel( MSG::DEBUG ) ) {
+              std::string binary = std::bitset<16>( aWord.value() ).to_string();
+              debug() << "Channel from data bank: " << std::dec << aWord.channelIDR()
+                      << " ADC from data bank: " << static_cast<int16_t>( (int8_t)aWord.adcR() ) << endmsg;
+              debug() << daqidR << endmsg;
+              debug() << channelIDR << endmsg;
+              debug() << "Right Channel: " << aWord.channelIDR()
+                      << " ADC: " << static_cast<int16_t>( (int8_t)aWord.adcR() ) << endmsg;
+            }
+            try {
+              digits.insert( new UTDigit( channelIDR, (int8_t)aWord.adcR(), daqidR.id(), aWord.asic_num(),
+                                          aWord.mcm_val(), aWord.mcm_ch() ),
+                             channelIDR );
+            } catch ( ... ) {
+              if ( msgLevel( MSG::DEBUG ) ) debug() << "Problem with inserting" << endmsg;
+              ++m_errors_insert;
+              continue;
+            }
+          } else {
+            if ( msgLevel( MSG::DEBUG ) ) debug() << "Skipping this digit" << endmsg;
+          }
+        } catch ( const std::exception& ex ) {
+          if ( msgLevel( MSG::DEBUG ) ) debug() << ex.what() << endmsg;
+        } catch ( ... ) {
+          if ( msgLevel( MSG::DEBUG ) ) debug() << "Uknown type issue for: " << bank->sourceID() << endmsg;
+        }
+      }
+    };
+
+    if ( ::UTDAQ::version{bank->version()} == ::UTDAQ::version::v5 ) {
+      decode( UTNZSDecoder<::UTDAQ::version::v5>{*bank} );
+      ++m_validHeaders;
+    } else {
+      debug() << "Wrong version of the RawBank" << endmsg;
+      ++m_invalidBanks;
+    }
+  }
+  return digits;
+}
+
+//=============================================================================
diff --git a/UT/UTDAQ/tests/options/ut_test_nzs.py b/UT/UTDAQ/tests/options/ut_test_nzs.py
new file mode 100644
index 0000000000000000000000000000000000000000..bcbdb3a809b22567ac7aec09a348f35205610c0b
--- /dev/null
+++ b/UT/UTDAQ/tests/options/ut_test_nzs.py
@@ -0,0 +1,50 @@
+###############################################################################
+# (c) Copyright 2019 CERN for the benefit of the LHCb Collaboration           #
+#                                                                             #
+# This software is distributed under the terms of the GNU General Public      #
+# Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   #
+#                                                                             #
+# In applying this licence, CERN does not waive the privileges and immunities #
+# granted to it by virtue of its status as an Intergovernmental Organization  #
+# or submit itself to any jurisdiction.                                       #
+###############################################################################
+import logging
+from Gaudi.Configuration import VERBOSE, DEBUG, INFO
+from PRConfig.TestFileDB import test_file_db
+from DDDB.CheckDD4Hep import UseDD4Hep
+from PyConf.Algorithms import UTRawBankToUTNZSDigitsAlg
+
+from PyConf.application import (configure_input, configure, CompositeNode,
+                                make_odin, default_raw_banks)
+
+# DD4HEP configuration
+if UseDD4Hep:
+    from Configurables import LHCb__Det__LbDD4hep__DD4hepSvc as DD4hepSvc
+    dd4hepsvc = DD4hepSvc()
+    dd4hepsvc.GeometryVersion = "run3/trunk"
+    dd4hepsvc.GeometryMain = "LHCb.xml"
+    dd4hepsvc.DetectorList = ["/world", "UT"]
+
+# I/O configuration
+options = test_file_db["ut-nzs-2024"].make_lbexec_options(
+    simulation=False,
+    python_logging_level=logging.INFO,
+    evt_max=100,
+    data_type='Upgrade',
+    geometry_version="run3/trunk",
+    conditions_version='master')
+
+# Setting up algorithms pipeline
+configure_input(options)
+odin = make_odin()
+decoder_nzs = UTRawBankToUTNZSDigitsAlg(
+    name="UTRawToNZSDigits",
+    UTBank=default_raw_banks("UTNZS"),
+    OutputLevel=INFO)
+decoder_nzs_err = UTRawBankToUTNZSDigitsAlg(
+    name="UTErrRawToNZSDigits",
+    UTBank=default_raw_banks("UTError"),
+    OutputLevel=INFO)
+
+top_node = CompositeNode("UT_NZSDecoding", [decoder_nzs, decoder_nzs_err])
+configure(options, top_node)
diff --git a/UT/UTDAQ/tests/qmtest/ut_decoding_nzs.qmt b/UT/UTDAQ/tests/qmtest/ut_decoding_nzs.qmt
new file mode 100644
index 0000000000000000000000000000000000000000..eb06a9bf802ddc62f47e913dd30db924c4b236af
--- /dev/null
+++ b/UT/UTDAQ/tests/qmtest/ut_decoding_nzs.qmt
@@ -0,0 +1,25 @@
+<?xml version="1.0" ?><!DOCTYPE extension  PUBLIC '-//QM/2.3/Extension//EN'  'http://www.codesourcery.com/qm/dtds/2.3/-//qm/2.3/extension//en.dtd'>
+<!--
+    (c) Copyright 2000-2018 CERN for the benefit of the LHCb Collaboration
+
+    This software is distributed under the terms of the GNU General Public
+    Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".
+
+    In applying this licence, CERN does not waive the privileges and immunities
+    granted to it by virtue of its status as an Intergovernmental Organization
+    or submit itself to any jurisdiction.
+-->
+<extension class="GaudiTest.GaudiExeTest" kind="test">
+  <argument name="program"><text>gaudirun.py</text></argument>
+  <argument name="unsupported_platforms"><set>
+    <text>detdesc</text>
+  </set></argument>
+  <argument name="args"><set>
+    <text>$UTDAQROOT/tests/options/ut_test_nzs.py</text>
+  </set></argument>
+  <argument name="validator"><text>
+countErrorLines({"FATAL": 0, "ERROR": 0, "WARNING": 0})
+  </text></argument>
+</extension>
+
+
diff --git a/UT/UTKernel/include/Kernel/UTDAQID.h b/UT/UTKernel/include/Kernel/UTDAQID.h
index 84faa67d8f98cae7f734be9094d87fc42a10b777..21f75631bbd422286df54c2bf87104b6fd86c960 100644
--- a/UT/UTKernel/include/Kernel/UTDAQID.h
+++ b/UT/UTKernel/include/Kernel/UTDAQID.h
@@ -33,16 +33,14 @@ class UTDAQID final {
 
   template <masks m>
   [[nodiscard]] static constexpr unsigned int extract( unsigned int i ) {
-    constexpr auto b =
-        __builtin_ctz( static_cast<unsigned int>( m ) ); // FIXME: C++20 replace __builtin_ctz with std::countr_zero
+    constexpr auto b = std::countr_zero( static_cast<unsigned int>( m ) );
     return ( i & static_cast<unsigned int>( m ) ) >> b;
   }
 
   template <masks m>
   [[nodiscard]] static constexpr unsigned int shift( unsigned int i ) {
-    constexpr auto b =
-        __builtin_ctz( static_cast<unsigned int>( m ) ); // FIXME: C++20 replace __builtin_ctz with std::countr_zero
-    auto v = ( i << static_cast<unsigned int>( b ) );
+    constexpr auto b = std::countr_zero( static_cast<unsigned int>( m ) );
+    auto           v = ( i << static_cast<unsigned int>( b ) );
     assert( extract<m>( v ) == i );
     return v;
   }
diff --git a/UT/UTKernel/include/Kernel/UTNZSDecoder.h b/UT/UTKernel/include/Kernel/UTNZSDecoder.h
new file mode 100644
index 0000000000000000000000000000000000000000..e23a021b125fcc71ef8045b67267e5a24d8e248c
--- /dev/null
+++ b/UT/UTKernel/include/Kernel/UTNZSDecoder.h
@@ -0,0 +1,175 @@
+/*****************************************************************************\
+* (c) Copyright 2000-2018 CERN for the benefit of the LHCb Collaboration      *
+*                                                                             *
+* This software is distributed under the terms of the GNU General Public      *
+* Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   *
+*                                                                             *
+* In applying this licence, CERN does not waive the privileges and immunities *
+* granted to it by virtue of its status as an Intergovernmental Organization  *
+* or submit itself to any jurisdiction.                                       *
+\*****************************************************************************/
+
+/** @class
+ *  Implementation of UTRawBank decoder
+ *
+ *  @author Wojciech Krupa (based on code by Xuhao Yuan, wokrupa@cern.ch)
+ *  @date   2020-06-17 / 2023-06-08
+ */
+
+#pragma once
+#include "Event/RawBank.h"
+#include "Kernel/STLExtensions.h"
+#include "SiDAQ/SiRawBankDecoder.h"
+#include "SiDAQ/SiRawBufferWord.h"
+#include "UTClusterWord.h"
+#include "UTDAQ/UTHeaderWord.h"
+#include "UTDAQ/UTNZSWord.h"
+#include "UTDAQDefinitions.h"
+#include <cassert>
+
+template <UTDAQ::version>
+class UTNZSDecoder;
+
+template <>
+class UTNZSDecoder<UTDAQ::version::v4> : public SiRawBankDecoder<UTClusterWord> {
+public:
+  explicit UTNZSDecoder( const LHCb::RawBank& bank ) : SiRawBankDecoder<UTClusterWord>{bank.data()} {
+    assert( UTDAQ::version{bank.version()} == UTDAQ::version::v4 );
+  }
+};
+
+template <>
+class UTNZSDecoder<UTDAQ::version::v5> final {
+  enum class banksize { left_bit = 10, center_bit = 16, right_bit = 15 };
+
+  // Method which allow moving within lane by jump betweens 256b TELL40 lines. It returns 1, 16 (jump to next line), 17,
+  // 32, ... for N (numb of hits) = 1, 2, 3, ...
+  constexpr static unsigned int nskip( unsigned int digLane ) {
+    return static_cast<unsigned int>( banksize::center_bit ) * ( ( digLane + 1 ) / 2 - 1 ) + ( 1 - ( digLane % 2 ) );
+  }
+
+  constexpr static unsigned int get_digits( unsigned int input ) {
+    if ( input == 0x08 ) return 264;
+    if ( input == 0xC6 ) return 198;
+    if ( input == 0x84 ) return 132;
+    if ( input == 0x42 )
+      return 66;
+    else
+      return 0;
+  }
+
+  // Method for reading number of hits in each line for 64b (2x32b) event header.
+  constexpr static UTDAQ::digiVec make_digiVec( LHCb::span<const uint32_t, 2> header ) {
+    UTHeaderWord headerL{header[0]}, headerR{header[1]};
+    return {get_digits( headerR.nClustersLane0() ), get_digits( headerR.nClustersLane1() ),
+            get_digits( headerR.nClustersLane2() ), get_digits( headerR.nClustersLane3() ),
+            get_digits( headerL.nClustersLane4() ), get_digits( headerL.nClustersLane5() )};
+  }
+
+  constexpr static UTDAQ::headerVec make_headerVec( LHCb::span<const uint32_t, 2> header ) {
+    UTHeaderWord headerL{header[0]}, headerR{header[1]};
+    return {headerL.getFlags(), headerL.getBXID()};
+  }
+
+public:
+  explicit UTNZSDecoder( const LHCb::RawBank& bank )
+      : UTNZSDecoder{bank.range<uint16_t>().subspan( 4 ), bank.range<uint32_t>().first<2>()} {
+    assert( UTDAQ::version{bank.version()} == UTDAQ::version::v5 );
+    if ( bank.type() == LHCb::RawBank::UT &&
+         ( bank.size() / sizeof( unsigned int ) != ( ( nClusters() + 1 ) / 2 ) * 8 &&
+           nClusters() != 0 ) ) { // TODO: add a dedicated UT DAQ StatusCode category, and
+                                  // throw a GaudiException with a value inside that
+                                  // category
+      std::string str_msg = "Error: unexpected UT RawBank size, expected: ";
+      str_msg.append( std::to_string( ( ( nClusters() + 1 ) / 2 ) * 8 ) );
+      str_msg.append( " received: " + std::to_string( bank.size() / sizeof( unsigned int ) ) );
+      const char* cc_msge = str_msg.c_str();
+      throw std::runtime_error{cc_msge};
+    }
+  }
+
+  UTNZSDecoder( LHCb::span<const uint16_t> body, LHCb::span<const uint32_t, 2> header )
+      : m_bank( body ), m_nDigits{make_digiVec( header )}, m_header{make_headerVec( header )} {}
+
+  class pos_range final { // NON clustering iterator.
+
+  private:
+    const LHCb::span<const uint16_t> m_bank;  // Data Bank 16b in lane (max 2 hits)
+    UTDAQ::digiVec                   m_Digit; // Number of hits in each lane
+    struct Sentinel                  final {};
+
+    class Iterator final {
+      const LHCb::span<const uint16_t> m_bank;
+      UTDAQ::digiVec                   m_iterDigit;
+      unsigned int                     m_lane   = UTDAQ::max_nlane;
+      unsigned int                     m_pos    = 0; // Current position in lane
+      unsigned int                     m_maxpos = 0; // Max position in lane (determined by number of hits in lane)
+      unsigned int                     m_asic_num;   // ASIC_num + 8b of NZS header
+      unsigned int                     m_nzs_header; // Remaining 16b of NZS header
+      unsigned int                     m_inlane_counter = 0; //
+
+    public:
+      Iterator( LHCb::span<const uint16_t> bank, const UTDAQ::digiVec m_Digit ) : m_bank{bank}, m_iterDigit{m_Digit} {
+        auto PosLane = std::find_if( m_iterDigit.begin(), m_iterDigit.end(),
+                                     []( unsigned int& element ) { return element != 0; } );
+        if ( PosLane != m_iterDigit.end() ) {
+          m_lane   = std::distance( m_iterDigit.begin(), PosLane );                // first non-zero lane
+          m_pos    = static_cast<unsigned int>( banksize::left_bit ) - 2 * m_lane; // initial pos (10, 8, 6, 4, 2, 0)
+          m_maxpos = m_pos + nskip( m_iterDigit[m_lane] ); // max posiotion (initial + distance distance depends on
+        }                                                  // number of hits )
+      }
+
+      // dereferencing
+      [[nodiscard]] constexpr UTNZSWord operator*() const {
+        return UTNZSWord{m_bank[m_pos], m_lane, m_asic_num, m_nzs_header, m_inlane_counter};
+      }
+
+      constexpr Iterator& operator++() {
+
+        if ( m_inlane_counter % 66 == 0 ) m_nzs_header = m_bank[m_pos];
+        if ( m_inlane_counter % 66 == 1 ) m_asic_num = m_bank[m_pos];
+        m_inlane_counter++;
+        m_pos += ( ( m_pos % 2 ) ? static_cast<unsigned int>( banksize::right_bit )
+                                 : 1u ); // if we read odd word we moving to the second in the line (1), otherwise jump
+
+        if ( m_pos > m_maxpos ) {
+          m_inlane_counter = 0; // to next line
+          ++m_lane;             // if we reach last hit, we jump to the next line
+          while ( m_lane < UTDAQ::max_nlane && m_iterDigit[m_lane] == 0 ) {
+            ++m_lane;
+          }                                  // it there is no hits in lane, we jump to next one
+          if ( m_lane < UTDAQ::max_nlane ) { // if we didn't reach the end of lanes
+            m_pos = static_cast<unsigned int>( banksize::left_bit ) - 2 * m_lane; // changing initial and max position
+            m_maxpos = m_pos + nskip( m_iterDigit[m_lane] );
+          }
+        }
+
+        return *this;
+      }
+
+      constexpr bool operator!=( Sentinel ) const { return m_lane < UTDAQ::max_nlane; }
+    };
+
+  public:
+    constexpr pos_range( LHCb::span<const uint16_t> bank, const UTDAQ::digiVec& ClusterVec )
+        : m_bank{std::move( bank )}, m_Digit{ClusterVec} {}
+    [[nodiscard]] auto           begin() const { return Iterator{m_bank, m_Digit}; }
+    [[nodiscard]] constexpr auto end() const { return Sentinel{}; }
+  };
+
+  [[nodiscard]] constexpr unsigned int nClusters() const {
+    static_assert( UTDAQ::max_nlane == 6 );
+    return std::max( {m_nDigits[0], m_nDigits[1], m_nDigits[2], m_nDigits[3], m_nDigits[4], m_nDigits[5]} );
+  }
+  [[nodiscard]] constexpr unsigned int nClusters( unsigned int laneID ) const { return m_nDigits[laneID]; }
+  [[nodiscard]] constexpr unsigned int getflags() const { return m_header[0]; }
+  [[nodiscard]] constexpr unsigned int getBXID() const { return m_header[1]; }
+  [[nodiscard]] auto                   posRange() const {
+    return pos_range{m_bank.first( ( ( nClusters() + 1 ) / 2 ) * 16 - 4 ), m_nDigits};
+  }
+
+private:
+  LHCb::span<const uint16_t> m_bank;
+  UTDAQ::digiVec             m_nDigits;
+  UTDAQ::headerVec           m_header;
+};
diff --git a/UT/UTKernel/include/UTDAQ/UTNZSWord.h b/UT/UTKernel/include/UTDAQ/UTNZSWord.h
new file mode 100644
index 0000000000000000000000000000000000000000..f48aa8ed19289a6ba9ca630288df59481894c43d
--- /dev/null
+++ b/UT/UTKernel/include/UTDAQ/UTNZSWord.h
@@ -0,0 +1,116 @@
+/*****************************************************************************\
+* (c) Copyright 2020 CERN for the benefit of the LHCb Collaboration      *
+*                                                                             *
+* This software is distributed under the terms of the GNU General Public      *
+* Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   *
+*                                                                             *
+* In applying this licence, CERN does not waive the privileges and immunities *
+* granted to it by virtue of its status as an Intergovernmental Organization  *
+* or submit itself to any jurisdiction.                                       *
+\*****************************************************************************/
+#pragma once
+#include <iostream>
+#include <string>
+
+/** @class UTNZSWord UTNZSWord.h "UTDAQ/UTNZSWord.h"
+ *
+ *  Class for encapsulating header word in RAW data format
+ *  for the UT Si detectors.
+ *
+ *  @author Wojciech Krupa (based on code by Xuhao Yuan, wokrupa@cern.ch)
+ *  @date   2020-06-17  / 2023-06-12
+ */
+
+class UTNZSWord final {
+  enum class mask {
+    strip   = 0xFFE0,
+    adc     = 0x1F,
+    adcL    = 0xFF,
+    adcR    = 0xFF00,
+    mcm_val = 0xFC0000,
+    mcm_ch  = 0x03FC00,
+    mem_sep = 0x0003FE,
+    ev_par  = 0x000001
+  };
+
+  static constexpr unsigned int maxstrip = 512;
+
+  template <mask m>
+  [[nodiscard]] static constexpr unsigned int extract( unsigned int i ) {
+    constexpr auto b = __builtin_ctz( static_cast<unsigned int>( m ) );
+    return ( i & static_cast<unsigned int>( m ) ) >> b;
+  }
+
+  template <mask m>
+  [[nodiscard]] static constexpr unsigned int shift( unsigned int i ) {
+    constexpr auto b = __builtin_ctz( static_cast<unsigned int>( m ) );
+    auto           v = ( i << static_cast<unsigned int>( b ) );
+    assert( extract<m>( v ) == i );
+    return v;
+  }
+
+public:
+  /** constructer with int
+    @param value
+    */
+  explicit constexpr UTNZSWord( unsigned int value ) : m_value( value ) {}
+
+  constexpr UTNZSWord( unsigned int value, unsigned int nlane ) : m_value( value ), m_lane( nlane ){};
+
+  constexpr UTNZSWord( unsigned int value, unsigned int nlane, unsigned int asic_num, unsigned int nzs_header,
+                       unsigned int pos )
+      : m_value( value ), m_lane( nlane ), m_asic_num( asic_num ), m_nzs_header( nzs_header ), m_pos( pos ){};
+
+  /** The actual value
+    @return valueW
+    */
+  [[nodiscard]] constexpr unsigned int value() const { return m_value; };
+
+  [[nodiscard]] constexpr unsigned int channelIDL() const {
+    return maxstrip * m_lane + asic_num() % 4 * 128 + ( 127 - ( 2 * m_pos % 132 - 4 ) );
+  }
+  [[nodiscard]] constexpr unsigned int channelIDR() const {
+    return maxstrip * m_lane + asic_num() % 4 * 128 + ( 127 - ( 2 * m_pos % 132 - 3 ) );
+  }
+  [[nodiscard]] constexpr unsigned int adcL() const { return extract<mask::adcL>( m_value ); }
+
+  [[nodiscard]] constexpr unsigned int adcR() const { return extract<mask::adcR>( m_value ); }
+
+  [[nodiscard]] constexpr unsigned int pos() const { return m_pos; }
+
+  [[nodiscard]] constexpr bool not_nzs_header() const { return m_pos % 66 > 1 ? 1 : 0; }
+
+  [[nodiscard]] constexpr unsigned int asic_num() const { return extract<mask::adcR>( m_asic_num ); }
+
+  [[nodiscard]] constexpr unsigned int nzs_header() const {
+    return ( extract<mask::adcL>( m_asic_num ) << 16 ) | m_nzs_header;
+  }
+
+  [[nodiscard]] constexpr int mcm_val() const { // 2 complement for 6b since mcm_val may be negative
+    int extracted = extract<mask::mcm_val>( nzs_header() );
+    return ( extracted & 0x20 ) ? ( extracted | 0xFFFFFFC0 ) : extracted;
+  }
+  [[nodiscard]] constexpr unsigned int mcm_ch() const { return extract<mask::mcm_ch>( nzs_header() ); }
+
+  [[nodiscard]] constexpr unsigned int mem_sep() const { return extract<mask::mem_sep>( nzs_header() ); }
+
+  [[nodiscard]] constexpr unsigned int ev_par() const { return extract<mask::ev_par>( nzs_header() ); }
+
+  [[nodiscard]] constexpr unsigned int lane() const { return m_lane; }
+
+  // Operator overloading for stringoutput
+  friend std::ostream& operator<<( std::ostream& s, const UTNZSWord& obj ) { return obj.fillStream( s ); }
+
+  // Fill the ASCII output stream
+  std::ostream& fillStream( std::ostream& s ) const {
+    return s << "{ "
+             << " value:\t" << value() << " }\n";
+  }
+
+private:
+  unsigned int m_value      = 0;
+  unsigned int m_lane       = 0;
+  unsigned int m_asic_num   = 0;
+  unsigned int m_nzs_header = 0;
+  unsigned int m_pos        = 0;
+};