diff --git a/algo/CMakeLists.txt b/algo/CMakeLists.txt
index ac9e3f380412904997dfeceaef45b3df00b2257d..7a980fd045abcbce5e13a466da2fb2de62a130d0 100644
--- a/algo/CMakeLists.txt
+++ b/algo/CMakeLists.txt
@@ -82,12 +82,14 @@ set(SRCS
   trigger/TimeClusterTrigger.cxx
   evselector/DigiEventSelector.cxx
   evselector/DigiEventSelectorConfig.cxx
+  unpack/CommonUnpacker.cxx
   unpack/Unpack.cxx
   unpack/UnpackChain.cxx
   detectors/sts/ReadoutConfig.cxx
   detectors/sts/HitfinderChain.cxx
   detectors/sts/ReadoutConfigLegacy.cxx
   detectors/sts/Unpack.cxx
+  detectors/sts/UnpackMS.cxx
   detectors/much/ReadoutConfig.cxx
   detectors/much/Unpack.cxx
   detectors/tof/HitFinder.cxx
diff --git a/algo/base/Options.cxx b/algo/base/Options.cxx
index 536eb6fefd754479e439931ea14d647d47fe3507..9c97c10df487a9bb3c8761cb1aa0f2120ab8e7c1 100644
--- a/algo/base/Options.cxx
+++ b/algo/base/Options.cxx
@@ -3,6 +3,8 @@
    Authors: Felix Weiglhofer [committer] */
 #include "Options.h"
 
+#include "util/StlUtils.h"
+
 #include <boost/program_options.hpp>
 
 #include <iostream>
@@ -125,3 +127,9 @@ Options::Options(int argc, char** argv)
     std::exit(EXIT_FAILURE);
   }
 }
+
+bool Options::HasOutput(RecoData recoData) const { return Contains(fOutputTypes, recoData); }
+
+bool Options::Has(fles::Subsystem detector) const { return Contains(fDetectors, detector); }
+
+bool Options::Has(Step step) const { return Contains(fRecoSteps, step); }
diff --git a/algo/base/Options.h b/algo/base/Options.h
index c9959ffe28315032a024063e0f6b8405993ab1eb..f57c4293b87af8be254060445a94879ad98e6501 100644
--- a/algo/base/Options.h
+++ b/algo/base/Options.h
@@ -43,21 +43,17 @@ namespace cbm::algo
     bool DumpArchive() const { return fDumpArchive; }
 
     const std::vector<Step>& Steps() const { return fRecoSteps; }
-    bool HasStep(Step step) const { return std::find(fRecoSteps.begin(), fRecoSteps.end(), step) != fRecoSteps.end(); }
+
     const std::vector<RecoData>& OutputTypes() const { return fOutputTypes; }
-    bool HasOutput(RecoData recoData) const
-    {
-      return std::find(fOutputTypes.begin(), fOutputTypes.end(), recoData) != fOutputTypes.end();
-    }
+    bool HasOutput(RecoData recoData) const;
 
     bool CompressArchive() const { return fCompressArchive; }
 
     const std::vector<fles::Subsystem>& Detectors() const { return fDetectors; }
-    bool HasDetector(fles::Subsystem detector) const
-    {
-      return std::find(fDetectors.begin(), fDetectors.end(), detector) != fDetectors.end();
-    }
 
+    bool Has(fles::Subsystem detector) const;
+
+    bool Has(Step step) const;
 
    private:                  // members
     std::string fParamsDir;  // TODO: can we make this a std::path?
diff --git a/algo/base/RecoParams.h b/algo/base/RecoParams.h
index 5c88e0d23145eca2915ad99202b7c3e85be98e18..d51fdbef8f393dc569f8b013cc577ca8560d1d3a 100644
--- a/algo/base/RecoParams.h
+++ b/algo/base/RecoParams.h
@@ -22,11 +22,6 @@ namespace cbm::algo
       BlockSort        = 0,
       CUBSegmentedSort = 1,
     };
-    enum class UnpackMode : u8
-    {
-      CPU,
-      XPU,
-    };
     enum class AllocationMode : u8
     {
       Auto,     //< Static on GPU, dynamic on CPU
@@ -38,7 +33,6 @@ namespace cbm::algo
       SortMode digiSortMode;
       SortMode clusterSortMode;
 
-      UnpackMode unpackMode;
       u8 findClustersMultiKernels;
 
       f32 timeCutDigiAbs;
@@ -76,7 +70,6 @@ namespace cbm::algo
                          "Digi sort mode (0 = block sort, 1 = cub segmented sort))"),
         config::Property(&STS::clusterSortMode, "clusterSortMode", "Cluster sort mode"),
 
-        config::Property(&STS::unpackMode, "unpackMode", "Unpack mode (0 = legacy, 1 = XPU)"),
         config::Property(&STS::findClustersMultiKernels, "findClustersMultiKernels",
                          "Split cluster finding into multiple kernels"),
 
@@ -100,18 +93,13 @@ namespace cbm::algo
     static constexpr auto Properties = std::make_tuple(config::Property(&RecoParams::sts, "sts", "STS reco settings"));
   };
 
-};  // namespace cbm::algo
+}  // namespace cbm::algo
 
 CBM_ENUM_DICT(cbm::algo::RecoParams::SortMode,
   {"BlockSort", RecoParams::SortMode::BlockSort},
   {"CUBSegmentedSort", RecoParams::SortMode::CUBSegmentedSort}
 );
 
-CBM_ENUM_DICT(cbm::algo::RecoParams::UnpackMode,
-  {"CPU", RecoParams::UnpackMode::CPU},
-  {"XPU", RecoParams::UnpackMode::XPU}
-);
-
 CBM_ENUM_DICT(cbm::algo::RecoParams::AllocationMode,
   {"Auto", RecoParams::AllocationMode::Auto},
   {"Static", RecoParams::AllocationMode::Static},
diff --git a/algo/base/util/StlUtils.h b/algo/base/util/StlUtils.h
new file mode 100644
index 0000000000000000000000000000000000000000..e84898ccdbf38dd293b1c21c717327f811952897
--- /dev/null
+++ b/algo/base/util/StlUtils.h
@@ -0,0 +1,24 @@
+/* Copyright (C) 2024 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer] */
+
+#pragma once
+
+/**
+ * @file StlUtils.h
+ *
+ * @brief This file contains utility functions for STL containers.
+ */
+
+#include <algorithm>
+
+namespace cbm
+{
+
+  template<class C, class T>
+  bool Contains(const C& container, const T& value)
+  {
+    return std::find(container.begin(), container.end(), value) != container.end();
+  }
+
+}  // namespace cbm
diff --git a/algo/detectors/sts/Unpack.cxx b/algo/detectors/sts/Unpack.cxx
index 7602d592a58879881441cb1da7d2884ffb1a7ee0..d0f6ee3d31d2d69efdec406d1ead8c2dbad1d033 100644
--- a/algo/detectors/sts/Unpack.cxx
+++ b/algo/detectors/sts/Unpack.cxx
@@ -1,183 +1,48 @@
-/* Copyright (C) 2021 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+/* Copyright (C) 2024 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
    SPDX-License-Identifier: GPL-3.0-only
-   Authors: Pierre-Alain Loizeau, Volker Friese [committer] */
+   Authors: Felix Weiglhofer [committer], Dominik Smith */
 
 #include "Unpack.h"
 
-#include "StsXyterMessage.h"
+#include "log.hpp"
 
-#include <cassert>
-#include <cmath>
-#include <utility>
-#include <vector>
+using namespace cbm::algo::sts;
+using fles::Subsystem;
 
-using std::unique_ptr;
-using std::vector;
-
-namespace cbm::algo::sts
+Unpack::Unpack(const ReadoutConfigLegacy& readout) : fReadout(readout)
 {
-
-  // ----   Algorithm execution   ---------------------------------------------
-  Unpack::resultType Unpack::operator()(const uint8_t* msContent, const fles::MicrosliceDescriptor& msDescr,
-                                        const uint64_t tTimeslice) const
-  {
-    // --- Output data
-    resultType result = {};
-
-    // --- Current Timeslice start time in epoch units. Note that it is always a multiple of epochs
-    // --- and the epoch is a multiple of ns.
-    TimeSpec time;
-    const uint64_t epochLengthInNs = fkEpochLength * fkClockCycleNom / fkClockCycleDen;
-    time.currentTsTime             = tTimeslice / epochLengthInNs;
-
-    // --- Current TS_MSB epoch cycle
-    auto const msTime = msDescr.idx;  // Unix time of MS in ns
-    time.currentCycle = std::ldiv(msTime, fkCycleLength).quot;
-
-    // --- Number of messages in microslice
-    auto msSize = msDescr.size;
-    if (msSize % sizeof(stsxyter::Message) != 0) {
-      result.second.fNumErrInvalidMsSize++;
-      return result;
-    }
-    const uint32_t numMessages = msSize / sizeof(stsxyter::Message);
-    if (numMessages < 2) {
-      result.second.fNumErrInvalidMsSize++;
-      return result;
-    }
-
-    const u32 maxDigis = numMessages - 2;  // -2 for the TS_MSB and EPOCH messages
-    result.first.reserve(maxDigis);
-
-    // --- Interpret MS content as sequence of SMX messages
-    auto message = reinterpret_cast<const stsxyter::Message*>(msContent);
-
-    // --- The first message in the MS is expected to be of type EPOCH and can be ignored.
-    if (message[0].GetMessType() != stsxyter::MessType::Epoch) {
-      result.second.fNumErrInvalidFirstMessage++;
-      return result;
-    }
-
-    // --- The second message must be of type ts_msb.
-    if (message[1].GetMessType() != stsxyter::MessType::TsMsb) {
-      result.second.fNumErrInvalidFirstMessage++;
-      return result;
-    }
-    ProcessTsmsbMessage(message[1], time);
-
-    // --- Message loop
-    for (uint32_t messageNr = 2; messageNr < numMessages; messageNr++) {
-
-      // --- Action depending on message type
-      switch (message[messageNr].GetMessType()) {
-
-        case stsxyter::MessType::Hit: {
-          ProcessHitMessage(message[messageNr], time, result.first, result.second);
-          break;
-        }
-        case stsxyter::MessType::TsMsb: {
-          ProcessTsmsbMessage(message[messageNr], time);
-          break;
-        }
-        default: {
-          result.second.fNumNonHitOrTsbMessage++;
-          break;
-        }
-
-      }  //? Message type
-
-    }  //# Messages
-
-    return result;
-  }
-  // --------------------------------------------------------------------------
-
-
-  // -----   Process hit message   --------------------------------------------
-  inline void Unpack::ProcessHitMessage(const stsxyter::Message& message, const TimeSpec& time,
-                                        vector<CbmStsDigi>& digiVec, UnpackMonitorData& monitor) const
-  {
-
-    // --- Check eLink and get parameters
-    uint16_t elink = message.GetLinkIndexHitBinning();
-    if (elink >= fParams.fElinkParams.size()) {
-      monitor.fNumErrElinkOutOfRange++;
-      return;
-    }
-    const UnpackElinkPar& elinkPar = fParams.fElinkParams.at(elink);
-    uint32_t asicNr                = elinkPar.fAsicNr;
-
-    // --- Check minimum adc cut
-    if (message.GetHitAdc() <= elinkPar.fAdcMinCut) {
-      return;
-    }
-
-    // --- Check for masked channel
-    if (!elinkPar.fChanMask.empty() && elinkPar.fChanMask[message.GetHitChannel()] == true) {
-      return;
-    }
-
-    // --- Hardware-to-software address
-    uint32_t channel = 0;
-    if (asicNr < fParams.fNumAsicsPerModule / 2) {  // front side (n side); channels counted upward
-      channel = message.GetHitChannel() + fParams.fNumChansPerAsic * asicNr;
-    }
-    else {  // back side (p side); channels counted downward
-      channel = fParams.fNumChansPerAsic * (asicNr + 1) - message.GetHitChannel() - 1;
-    }
-
-    // --- Expand time stamp to time within timeslice (in clock cycle)
-    uint64_t messageTime = message.GetHitTimeBinning() + time.currentEpochTime;
-
-    // --- Convert time stamp from clock cycles to ns. Round to nearest full ns.
-    messageTime = (messageTime * fkClockCycleNom + fkClockCycleDen / 2) / fkClockCycleDen;
-
-    // --- Correct ASIC-wise offsets
-    messageTime -= elinkPar.fTimeOffset;
-
-    // --- Apply walk correction if applicable
-    if (message.GetHitAdc() <= elinkPar.fWalk.size()) {
-      double walk = elinkPar.fWalk[message.GetHitAdc() - 1];
-      messageTime += walk;
-    }
-
-    // --- Charge
-    double charge = elinkPar.fAdcOffset + (message.GetHitAdc() - 1) * elinkPar.fAdcGain;
-
-    if (messageTime > CbmStsDigi::kMaxTimestamp) {
-      monitor.fNumErrTimestampOverflow++;
-      return;
-    }
-
-    // --- Create output digi
-    digiVec.emplace_back(elinkPar.fAddress, channel, messageTime, charge);
-  }
-  // --------------------------------------------------------------------------
-
-
-  // -----   Process an epoch (TS_MSB) message   ------------------------------
-  inline void Unpack::ProcessTsmsbMessage(const stsxyter::Message& message, TimeSpec& time) const
-  {
-    // The compression of time is based on the hierarchy epoch cycle - epoch - message time.
-    // Cycles are counted from the start of Unix time and are multiples of an epoch (ts_msb).
-    // The epoch number is counted within each cycle. The time in the hit message is expressed
-    // in units of the readout clock cycle relative to the current epoch.
-    // The ts_msb message indicates the start of a new epoch. Its basic information is the epoch
-    // number within the current cycle. A cycle wrap resets the epoch number to zero, so it is
-    // indicated by the epoch number being smaller than the previous one (epoch messages are
-    // seemingly not consecutively in the data stream, but only if there are hit messages in between).
-    auto epoch = message.GetTsMsbValBinning();
-
-    // --- Cycle wrap
-    if (epoch < time.currentEpoch) time.currentCycle++;
-
-    // --- Update current epoch counter
-    time.currentEpoch = epoch;
-
-    // --- Calculate epoch time in clocks cycles relative to timeslice start time
-    time.currentEpochTime = (time.currentCycle * fkEpochsPerCycle + epoch - time.currentTsTime) * fkEpochLength;
-  }
-  // --------------------------------------------------------------------------
-
-
-}  // namespace cbm::algo::sts
+  uint32_t numChansPerAsicSts   = 128;  // R/O channels per ASIC for STS
+  uint32_t numAsicsPerModuleSts = 16;   // Number of ASICs per module for STS
+
+  constexpr i64 SystemTimeOffset = -970;  // ns?
+
+  auto equipIdsSts = fReadout.GetEquipmentIds();
+  for (auto& equip : equipIdsSts) {
+    std::unique_ptr<sts::UnpackPar> par(new sts::UnpackPar());
+    par->fNumChansPerAsic   = numChansPerAsicSts;
+    par->fNumAsicsPerModule = numAsicsPerModuleSts;
+    const size_t numElinks  = fReadout.GetNumElinks(equip);
+    for (size_t elink = 0; elink < numElinks; elink++) {
+      sts::UnpackElinkPar elinkPar;
+      auto mapEntry        = fReadout.Map(equip, elink);
+      elinkPar.fAddress    = mapEntry.first;   // Module address for this elink
+      elinkPar.fAsicNr     = mapEntry.second;  // ASIC number within module
+      elinkPar.fTimeOffset = SystemTimeOffset;
+      elinkPar.fAdcMinCut  = fReadout.AdcCutMap(equip, elink);
+      elinkPar.fAdcOffset  = 1.;
+      elinkPar.fAdcGain    = 1.;
+      elinkPar.fWalk       = fReadout.WalkMap(elinkPar.fAddress, elinkPar.fAsicNr);
+      elinkPar.fChanMask   = fReadout.MaskMap(equip, elink);
+      // TODO: Add parameters for time and ADC calibration
+      par->fElinkParams.push_back(elinkPar);
+    }
+    fAlgos[equip].SetParams(std::move(par));
+    L_(debug) << "--- Configured equipment " << equip << " with " << numElinks << " elinks";
+  }  //# equipments
+}
+
+Unpack::Result_t Unpack::operator()(const fles::Timeslice& ts) const
+{
+  constexpr int SystemVersion = 0x20;
+  return DoUnpack(Subsystem::STS, ts, SystemVersion);
+}
diff --git a/algo/detectors/sts/Unpack.h b/algo/detectors/sts/Unpack.h
index ec476c55618e91f44daba8333fe3593b38abcbb0..8530069e87d37d61c18f8fc85aacb12e04be6534 100644
--- a/algo/detectors/sts/Unpack.h
+++ b/algo/detectors/sts/Unpack.h
@@ -1,159 +1,32 @@
-/* Copyright (C) 2021 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+/* Copyright (C) 2024 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
    SPDX-License-Identifier: GPL-3.0-only
-   Authors: Pierre-Alain Loizeau, Volker Friese [committer] */
+   Authors: Felix Weiglhofer [committer], Dominik Smith */
 
-#ifndef CBM_ALGO_UNPACKSTS_H
-#define CBM_ALGO_UNPACKSTS_H 1
+#pragma once
 
-#include "CbmStsDigi.h"
-#include "Definitions.h"
-#include "MicrosliceDescriptor.hpp"
-#include "StsXyterMessage.h"
-#include "Timeslice.hpp"
-
-#include <cassert>
-#include <cstddef>
-#include <cstdint>
-#include <memory>
-#include <vector>
+#include "CommonUnpacker.h"
+#include "ReadoutConfigLegacy.h"
+#include "UnpackMS.h"
 
 namespace cbm::algo::sts
 {
 
+  namespace detail
+  {
+    using UnpackBase = CommonUnpacker<CbmStsDigi, UnpackMS, UnpackMonitorData>;
+  }
 
-  /** @struct UnpackStsElinkPar
-   ** @author Volker Friese <v.friese@gsi.de>
-   ** @since 25 November 2021
-   ** @brief STS Unpacking parameters for one eLink / ASIC
-   **/
-  struct UnpackElinkPar {
-    int32_t fAddress     = 0;     ///< CbmStsAddress for the connected module
-    uint32_t fAsicNr     = 0;     ///< Number of connected ASIC within the module
-    uint64_t fTimeOffset = 0.;    ///< Time calibration parameter
-    double fAdcOffset    = 0.;    ///< Charge calibration parameter
-    double fAdcGain      = 0.;    ///< Charge calibration parameter
-    uint32_t fAdcMinCut  = 0.;    ///< Minimum Acd cut
-    std::vector<double> fWalk;    ///< Walk correction coefficients
-    std::vector<bool> fChanMask;  ///< Channel masking flags
-  };
-
-
-  /** @struct UnpackStsPar
-   ** @author Volker Friese <v.friese@gsi.de>
-   ** @since 25 November 2021
-   ** @brief Parameters required for the STS unpacking (specific to one component)
-   **/
-  struct UnpackPar {
-    uint32_t fNumChansPerAsic                = 0;   ///< Number of channels per ASIC
-    uint32_t fNumAsicsPerModule              = 0;   ///< Number of ASICS per module
-    std::vector<UnpackElinkPar> fElinkParams = {};  ///< Parameters for each eLink
-  };
-
-
-  /** @struct UnpackStsMoni
-   ** @author Volker Friese <v.friese@gsi.de>
-   ** @since 2 December 2021
-   ** @brief Monitoring data for STS unpacking
-   **/
-  struct UnpackMonitorData {
-    uint32_t fNumNonHitOrTsbMessage     = 0;
-    uint32_t fNumErrElinkOutOfRange     = 0;  ///< Elink not contained in parameters
-    uint32_t fNumErrInvalidFirstMessage = 0;  ///< First message is not TS_MSB or second is not EPOCH
-    uint32_t fNumErrInvalidMsSize       = 0;  ///< Microslice size is not multiple of message size
-    uint32_t fNumErrTimestampOverflow   = 0;  ///< Overflow in 64 bit time stamp
-    bool HasErrors()
-    {
-      uint32_t numErrors = fNumNonHitOrTsbMessage + fNumErrElinkOutOfRange + fNumErrInvalidFirstMessage
-                           + fNumErrInvalidMsSize + fNumErrTimestampOverflow;
-      return (numErrors > 0 ? true : false);
-    }
-    std::string print()
-    {
-      std::stringstream ss;
-      ss << "errors " << fNumNonHitOrTsbMessage << " | " << fNumErrElinkOutOfRange << " | "
-         << fNumErrInvalidFirstMessage << " | " << fNumErrInvalidMsSize << " | " << fNumErrTimestampOverflow << " | ";
-      return ss.str();
-    }
-  };
-
-  /** @class UnpackSts
-   ** @author Pierre-Alain Loizeau <p.-a.loizeau@gsi.de>
-   ** @author Volker Friese <v.friese@gsi.de>
-   ** @since 25 November 2021
-   ** @brief Unpack algorithm for STS
-   **/
-  class Unpack {
+  class Unpack : public detail::UnpackBase {
 
    public:
-    typedef std::pair<std::vector<CbmStsDigi>, UnpackMonitorData> resultType;
-
-
-    /** @brief Default constructor **/
-    Unpack(){};
-
+    using Result_t = detail::UnpackBase::Result_t;
 
-    /** @brief Destructor **/
-    ~Unpack(){};
+    Unpack(const ReadoutConfigLegacy& readout);
 
+    Result_t operator()(const fles::Timeslice&) const;
 
-    /** @brief Algorithm execution
-     ** @param  msContent  Microslice payload
-     ** @param  msDescr    Microslice descriptor
-     ** @param  tTimeslice Unix start time of timeslice [ns]
-     ** @return STS digi data
-     **/
-    resultType operator()(const uint8_t* msContent, const fles::MicrosliceDescriptor& msDescr,
-                          const uint64_t tTimeslice) const;
-
-    /** @brief Set the parameter container
-     ** @param params Pointer to parameter container
-     **/
-    void SetParams(std::unique_ptr<UnpackPar> params) { fParams = *(std::move(params)); }
-
-   private:  // types
-    /**
-     * @brief Structure to hold the current time information for the current microslice
-     */
-    struct TimeSpec {
-      u64 currentTsTime    = 0;  ///< Unix time of timeslice in units of epoch length
-      u64 currentCycle     = 0;  ///< Current epoch cycle
-      u32 currentEpoch     = 0;  ///< Current epoch number within epoch cycle
-      u64 currentEpochTime = 0;  ///< Current epoch time relative to timeslice in clock cycles
-    };
-
-   private:  // methods
-    /** @brief Process a hit message
-     ** @param message SMX message (32-bit word)
-     ** @param digiVec Vector to append the created digi to
-     ** @param monitor Reference to monitor object
-     **/
-    void ProcessHitMessage(const stsxyter::Message& message, const TimeSpec& time, std::vector<CbmStsDigi>& digiVec,
-                           UnpackMonitorData& monitor) const;
-
-    /** @brief Process an epoch message (TS_MSB)
-     ** @param message SMX message (32-bit word)
-     **/
-    void ProcessTsmsbMessage(const stsxyter::Message& message, TimeSpec& time) const;
-
-
-   private:                  // members
-    UnpackPar fParams = {};  ///< Parameter container
-
-    /** Number of TS_MSB epochs per cycle **/
-    static constexpr uint64_t fkEpochsPerCycle = stsxyter::kuTsMsbNbTsBinsBinning;
-
-    /** Length of TS_MSB epoch in clock cycles **/
-    static constexpr uint64_t fkEpochLength = stsxyter::kuHitNbTsBinsBinning;
-
-    /** Clock cycle nominator [ns] and denominator. The clock cycle in ns is nominator / denominator. **/
-    static constexpr uint32_t fkClockCycleNom = stsxyter::kulClockCycleNom;
-    static constexpr uint32_t fkClockCycleDen = stsxyter::kulClockCycleDen;
-
-    /** Epoch cycle length in ns **/
-    static constexpr uint64_t fkCycleLength = (fkEpochsPerCycle * fkEpochLength * fkClockCycleNom) / fkClockCycleDen;
+   private:
+    ReadoutConfigLegacy fReadout;
   };
 
-
-} /* namespace cbm::algo::sts */
-
-#endif /* CBM_ALGO_UNPACKSTS_H */
+}  // namespace cbm::algo::sts
diff --git a/algo/detectors/sts/UnpackMS.cxx b/algo/detectors/sts/UnpackMS.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..4ffb8dc9221433044f607c6ebe4060ca8edec9da
--- /dev/null
+++ b/algo/detectors/sts/UnpackMS.cxx
@@ -0,0 +1,183 @@
+/* Copyright (C) 2021 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Pierre-Alain Loizeau, Volker Friese [committer] */
+
+#include "UnpackMS.h"
+
+#include "StsXyterMessage.h"
+
+#include <cassert>
+#include <cmath>
+#include <utility>
+#include <vector>
+
+using std::unique_ptr;
+using std::vector;
+
+namespace cbm::algo::sts
+{
+
+  // ----   Algorithm execution   ---------------------------------------------
+  UnpackMS::Result_t UnpackMS::operator()(const uint8_t* msContent, const fles::MicrosliceDescriptor& msDescr,
+                                          const uint64_t tTimeslice) const
+  {
+    // --- Output data
+    Result_t result = {};
+
+    // --- Current Timeslice start time in epoch units. Note that it is always a multiple of epochs
+    // --- and the epoch is a multiple of ns.
+    TimeSpec time;
+    const uint64_t epochLengthInNs = fkEpochLength * fkClockCycleNom / fkClockCycleDen;
+    time.currentTsTime             = tTimeslice / epochLengthInNs;
+
+    // --- Current TS_MSB epoch cycle
+    auto const msTime = msDescr.idx;  // Unix time of MS in ns
+    time.currentCycle = std::ldiv(msTime, fkCycleLength).quot;
+
+    // --- Number of messages in microslice
+    auto msSize = msDescr.size;
+    if (msSize % sizeof(stsxyter::Message) != 0) {
+      result.second.fNumErrInvalidMsSize++;
+      return result;
+    }
+    const uint32_t numMessages = msSize / sizeof(stsxyter::Message);
+    if (numMessages < 2) {
+      result.second.fNumErrInvalidMsSize++;
+      return result;
+    }
+
+    const u32 maxDigis = numMessages - 2;  // -2 for the TS_MSB and EPOCH messages
+    result.first.reserve(maxDigis);
+
+    // --- Interpret MS content as sequence of SMX messages
+    auto message = reinterpret_cast<const stsxyter::Message*>(msContent);
+
+    // --- The first message in the MS is expected to be of type EPOCH and can be ignored.
+    if (message[0].GetMessType() != stsxyter::MessType::Epoch) {
+      result.second.fNumErrInvalidFirstMessage++;
+      return result;
+    }
+
+    // --- The second message must be of type ts_msb.
+    if (message[1].GetMessType() != stsxyter::MessType::TsMsb) {
+      result.second.fNumErrInvalidFirstMessage++;
+      return result;
+    }
+    ProcessTsmsbMessage(message[1], time);
+
+    // --- Message loop
+    for (uint32_t messageNr = 2; messageNr < numMessages; messageNr++) {
+
+      // --- Action depending on message type
+      switch (message[messageNr].GetMessType()) {
+
+        case stsxyter::MessType::Hit: {
+          ProcessHitMessage(message[messageNr], time, result.first, result.second);
+          break;
+        }
+        case stsxyter::MessType::TsMsb: {
+          ProcessTsmsbMessage(message[messageNr], time);
+          break;
+        }
+        default: {
+          result.second.fNumNonHitOrTsbMessage++;
+          break;
+        }
+
+      }  //? Message type
+
+    }  //# Messages
+
+    return result;
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   Process hit message   --------------------------------------------
+  inline void UnpackMS::ProcessHitMessage(const stsxyter::Message& message, const TimeSpec& time,
+                                          vector<CbmStsDigi>& digiVec, UnpackMonitorData& monitor) const
+  {
+
+    // --- Check eLink and get parameters
+    uint16_t elink = message.GetLinkIndexHitBinning();
+    if (elink >= fParams.fElinkParams.size()) {
+      monitor.fNumErrElinkOutOfRange++;
+      return;
+    }
+    const UnpackElinkPar& elinkPar = fParams.fElinkParams.at(elink);
+    uint32_t asicNr                = elinkPar.fAsicNr;
+
+    // --- Check minimum adc cut
+    if (message.GetHitAdc() <= elinkPar.fAdcMinCut) {
+      return;
+    }
+
+    // --- Check for masked channel
+    if (!elinkPar.fChanMask.empty() && elinkPar.fChanMask[message.GetHitChannel()] == true) {
+      return;
+    }
+
+    // --- Hardware-to-software address
+    uint32_t channel = 0;
+    if (asicNr < fParams.fNumAsicsPerModule / 2) {  // front side (n side); channels counted upward
+      channel = message.GetHitChannel() + fParams.fNumChansPerAsic * asicNr;
+    }
+    else {  // back side (p side); channels counted downward
+      channel = fParams.fNumChansPerAsic * (asicNr + 1) - message.GetHitChannel() - 1;
+    }
+
+    // --- Expand time stamp to time within timeslice (in clock cycle)
+    uint64_t messageTime = message.GetHitTimeBinning() + time.currentEpochTime;
+
+    // --- Convert time stamp from clock cycles to ns. Round to nearest full ns.
+    messageTime = (messageTime * fkClockCycleNom + fkClockCycleDen / 2) / fkClockCycleDen;
+
+    // --- Correct ASIC-wise offsets
+    messageTime -= elinkPar.fTimeOffset;
+
+    // --- Apply walk correction if applicable
+    if (message.GetHitAdc() <= elinkPar.fWalk.size()) {
+      double walk = elinkPar.fWalk[message.GetHitAdc() - 1];
+      messageTime += walk;
+    }
+
+    // --- Charge
+    double charge = elinkPar.fAdcOffset + (message.GetHitAdc() - 1) * elinkPar.fAdcGain;
+
+    if (messageTime > CbmStsDigi::kMaxTimestamp) {
+      monitor.fNumErrTimestampOverflow++;
+      return;
+    }
+
+    // --- Create output digi
+    digiVec.emplace_back(elinkPar.fAddress, channel, messageTime, charge);
+  }
+  // --------------------------------------------------------------------------
+
+
+  // -----   Process an epoch (TS_MSB) message   ------------------------------
+  inline void UnpackMS::ProcessTsmsbMessage(const stsxyter::Message& message, TimeSpec& time) const
+  {
+    // The compression of time is based on the hierarchy epoch cycle - epoch - message time.
+    // Cycles are counted from the start of Unix time and are multiples of an epoch (ts_msb).
+    // The epoch number is counted within each cycle. The time in the hit message is expressed
+    // in units of the readout clock cycle relative to the current epoch.
+    // The ts_msb message indicates the start of a new epoch. Its basic information is the epoch
+    // number within the current cycle. A cycle wrap resets the epoch number to zero, so it is
+    // indicated by the epoch number being smaller than the previous one (epoch messages are
+    // seemingly not consecutively in the data stream, but only if there are hit messages in between).
+    auto epoch = message.GetTsMsbValBinning();
+
+    // --- Cycle wrap
+    if (epoch < time.currentEpoch) time.currentCycle++;
+
+    // --- Update current epoch counter
+    time.currentEpoch = epoch;
+
+    // --- Calculate epoch time in clocks cycles relative to timeslice start time
+    time.currentEpochTime = (time.currentCycle * fkEpochsPerCycle + epoch - time.currentTsTime) * fkEpochLength;
+  }
+  // --------------------------------------------------------------------------
+
+
+}  // namespace cbm::algo::sts
diff --git a/algo/detectors/sts/UnpackMS.h b/algo/detectors/sts/UnpackMS.h
new file mode 100644
index 0000000000000000000000000000000000000000..6ae726b8795539608b1f999ae9eb0e67ddb07984
--- /dev/null
+++ b/algo/detectors/sts/UnpackMS.h
@@ -0,0 +1,152 @@
+/* Copyright (C) 2021 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Pierre-Alain Loizeau, Volker Friese [committer] */
+#pragma once
+
+#include "CbmStsDigi.h"
+#include "Definitions.h"
+#include "MicrosliceDescriptor.hpp"
+#include "StsXyterMessage.h"
+
+#include <cassert>
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+namespace cbm::algo::sts
+{
+
+  /** @struct UnpackStsElinkPar
+   ** @author Volker Friese <v.friese@gsi.de>
+   ** @since 25 November 2021
+   ** @brief STS Unpacking parameters for one eLink / ASIC
+   **/
+  struct UnpackElinkPar {
+    int32_t fAddress     = 0;     ///< CbmStsAddress for the connected module
+    uint32_t fAsicNr     = 0;     ///< Number of connected ASIC within the module
+    uint64_t fTimeOffset = 0.;    ///< Time calibration parameter
+    double fAdcOffset    = 0.;    ///< Charge calibration parameter
+    double fAdcGain      = 0.;    ///< Charge calibration parameter
+    uint32_t fAdcMinCut  = 0.;    ///< Minimum Acd cut
+    std::vector<double> fWalk;    ///< Walk correction coefficients
+    std::vector<bool> fChanMask;  ///< Channel masking flags
+  };
+
+
+  /** @struct UnpackStsPar
+   ** @author Volker Friese <v.friese@gsi.de>
+   ** @since 25 November 2021
+   ** @brief Parameters required for the STS unpacking (specific to one component)
+   **/
+  struct UnpackPar {
+    uint32_t fNumChansPerAsic                = 0;   ///< Number of channels per ASIC
+    uint32_t fNumAsicsPerModule              = 0;   ///< Number of ASICS per module
+    std::vector<UnpackElinkPar> fElinkParams = {};  ///< Parameters for each eLink
+  };
+
+
+  /** @struct UnpackStsMoni
+   ** @author Volker Friese <v.friese@gsi.de>
+   ** @since 2 December 2021
+   ** @brief Monitoring data for STS unpacking
+   **/
+  struct UnpackMonitorData {
+    uint32_t fNumNonHitOrTsbMessage     = 0;
+    uint32_t fNumErrElinkOutOfRange     = 0;  ///< Elink not contained in parameters
+    uint32_t fNumErrInvalidFirstMessage = 0;  ///< First message is not TS_MSB or second is not EPOCH
+    uint32_t fNumErrInvalidMsSize       = 0;  ///< Microslice size is not multiple of message size
+    uint32_t fNumErrTimestampOverflow   = 0;  ///< Overflow in 64 bit time stamp
+    bool HasErrors()
+    {
+      uint32_t numErrors = fNumNonHitOrTsbMessage + fNumErrElinkOutOfRange + fNumErrInvalidFirstMessage
+                           + fNumErrInvalidMsSize + fNumErrTimestampOverflow;
+      return (numErrors > 0 ? true : false);
+    }
+    std::string print()
+    {
+      std::stringstream ss;
+      ss << "errors " << fNumNonHitOrTsbMessage << " | " << fNumErrElinkOutOfRange << " | "
+         << fNumErrInvalidFirstMessage << " | " << fNumErrInvalidMsSize << " | " << fNumErrTimestampOverflow << " | ";
+      return ss.str();
+    }
+  };
+
+  /** @class UnpackMS
+   ** @author Pierre-Alain Loizeau <p.-a.loizeau@gsi.de>
+   ** @author Volker Friese <v.friese@gsi.de>
+   ** @since 25 November 2021
+   ** @brief Unpack algorithm for STS
+   **/
+  class UnpackMS {
+
+   public:
+    using Result_t = std::pair<std::vector<CbmStsDigi>, UnpackMonitorData>;
+
+
+    /** @brief Default constructor **/
+    UnpackMS() = default;
+
+
+    /** @brief Destructor **/
+    ~UnpackMS() = default;
+
+
+    /** @brief Algorithm execution
+     ** @param  msContent  Microslice payload
+     ** @param  msDescr    Microslice descriptor
+     ** @param  tTimeslice Unix start time of timeslice [ns]
+     ** @return STS digi data
+     **/
+    Result_t operator()(const uint8_t* msContent, const fles::MicrosliceDescriptor& msDescr,
+                        const uint64_t tTimeslice) const;
+
+    /** @brief Set the parameter container
+     ** @param params Pointer to parameter container
+     **/
+    void SetParams(std::unique_ptr<UnpackPar> params) { fParams = *(std::move(params)); }
+
+   private:  // types
+    /**
+     * @brief Structure to hold the current time information for the current microslice
+     */
+    struct TimeSpec {
+      u64 currentTsTime    = 0;  ///< Unix time of timeslice in units of epoch length
+      u64 currentCycle     = 0;  ///< Current epoch cycle
+      u32 currentEpoch     = 0;  ///< Current epoch number within epoch cycle
+      u64 currentEpochTime = 0;  ///< Current epoch time relative to timeslice in clock cycles
+    };
+
+   private:  // methods
+    /** @brief Process a hit message
+     ** @param message SMX message (32-bit word)
+     ** @param digiVec Vector to append the created digi to
+     ** @param monitor Reference to monitor object
+     **/
+    void ProcessHitMessage(const stsxyter::Message& message, const TimeSpec& time, std::vector<CbmStsDigi>& digiVec,
+                           UnpackMonitorData& monitor) const;
+
+    /** @brief Process an epoch message (TS_MSB)
+     ** @param message SMX message (32-bit word)
+     **/
+    void ProcessTsmsbMessage(const stsxyter::Message& message, TimeSpec& time) const;
+
+
+   private:                  // members
+    UnpackPar fParams = {};  ///< Parameter container
+
+    /** Number of TS_MSB epochs per cycle **/
+    static constexpr uint64_t fkEpochsPerCycle = stsxyter::kuTsMsbNbTsBinsBinning;
+
+    /** Length of TS_MSB epoch in clock cycles **/
+    static constexpr uint64_t fkEpochLength = stsxyter::kuHitNbTsBinsBinning;
+
+    /** Clock cycle nominator [ns] and denominator. The clock cycle in ns is nominator / denominator. **/
+    static constexpr uint32_t fkClockCycleNom = stsxyter::kulClockCycleNom;
+    static constexpr uint32_t fkClockCycleDen = stsxyter::kulClockCycleDen;
+
+    /** Epoch cycle length in ns **/
+    static constexpr uint64_t fkCycleLength = (fkEpochsPerCycle * fkEpochLength * fkClockCycleNom) / fkClockCycleDen;
+  };
+
+} /* namespace cbm::algo::sts */
diff --git a/algo/global/Reco.cxx b/algo/global/Reco.cxx
index 14f61de9fd55a774fdb2d94aaa0439bf32e94cad..44d90d27bac8711620a9e016e29eb6eb3db4df6e 100644
--- a/algo/global/Reco.cxx
+++ b/algo/global/Reco.cxx
@@ -41,6 +41,14 @@ void Reco::Validate(const Options& opts)
   if (!BuildInfo::WITH_ZSTD && opts.CompressArchive()) {
     throw FatalError("Archive compression enabled but compiled without Zstd: Remove --archive-compression flag");
   }
+
+  if (opts.Has(Step::LocalReco) && !opts.Has(Step::Unpack)) {
+    throw FatalError("Local reco can't run without unpacking: Add 'Unpack' to the reco steps");
+  }
+
+  if (opts.Has(Step::Tracking) && !opts.Has(Step::LocalReco)) {
+    throw FatalError("Tracking can't run without local reco: Add 'LocalReco' to the reco steps");
+  }
 }
 
 void Reco::Init(const Options& opts)
@@ -78,6 +86,11 @@ void Reco::Init(const Options& opts)
   // Unpackers
   fUnpack.Init();
 
+  if (Opts().Has(fles::Subsystem::STS) && Opts().Has(Step::Unpack)) {
+    sts::ReadoutConfigLegacy cfg{};
+    fStsUnpack = std::make_unique<sts::Unpack>(cfg);
+  }
+
   // --- Event building
   fs::path configFile = opts.ParamsDir() / "EventbuildConfig.yaml";
   evbuild::Config config(YAML::LoadFile(configFile.string()));
@@ -112,42 +125,58 @@ void Reco::Init(const Options& opts)
 
 RecoResults Reco::Run(const fles::Timeslice& ts)
 {
-  if (!fInitialized) throw std::runtime_error("Chain not initialized");
+  if (!fInitialized) {
+    throw std::runtime_error("Chain not initialized");
+  }
 
   xpu::t_add_bytes(ts_utils::SizeBytes(ts));
 
+  ProcessingMonitor procMon;
+
   RecoResults results;
-  xpu::timings tsTimes;
   {
-    xpu::scoped_timer t_(fmt::format("TS {}", ts.index()), &tsTimes);
+    xpu::scoped_timer t_(fmt::format("TS {}", ts.index()), &procMon.time);
     xpu::t_add_bytes(ts_utils::SizeBytes(ts));
 
     L_(info) << ">>> Processing TS " << ts.index();
     xpu::set<cbm::algo::Params>(Params());
 
-    Unpack::resultType unpackResult;
-    UnpackMonitorData unpackMonitor;
-
-    if (Opts().HasStep(Step::Unpack)) {
-      switch (Params().sts.unpackMode) {
-        case RecoParams::UnpackMode::XPU:
-          // digis = fUnpackXpu.Exec(ts);
-          throw std::runtime_error("XPU unpacker currently not implemented");
-          break;
-        default:
-        case RecoParams::UnpackMode::CPU:
-          unpackResult  = fUnpack.Run(ts);
-          unpackMonitor = unpackResult.second;
-          QueueUnpackerMetrics(ts, unpackMonitor, unpackResult.first);
-          break;
+    DigiData digis;
+
+    if (Opts().Has(Step::Unpack)) {
+      xpu::scoped_timer timerU("Unpack", &procMon.timeUnpack);
+      xpu::t_add_bytes(ts_utils::SizeBytes(ts));
+
+      if (fStsUnpack) {
+        auto [stsDigis, stsMonitor] = (*fStsUnpack)(ts);
+
+        digis.fSts = std::move(stsDigis);
+        QueueUnpackerMetricsDet(stsMonitor);
       }
+
+      auto [digisU, unpackMonitor] = fUnpack.Run(ts);
+
+      digis.fMuch  = std::move(digisU.fMuch);
+      digis.fTof   = std::move(digisU.fTof);
+      digis.fBmon  = std::move(digisU.fBmon);
+      digis.fTrd   = std::move(digisU.fTrd);
+      digis.fTrd2d = std::move(digisU.fTrd2d);
+      digis.fRich  = std::move(digisU.fRich);
+      digis.fPsd   = std::move(digisU.fPsd);
+      digis.fFsd   = std::move(digisU.fFsd);
+
+      L_(info) << "TS contains Digis: STS=" << digis.fSts.size() << " MUCH=" << digis.fMuch.size()
+               << " TOF=" << digis.fTof.size() << " BMON=" << digis.fBmon.size() << " TRD=" << digis.fTrd.size()
+               << " TRD2D=" << digis.fTrd2d.size() << " RICH=" << digis.fRich.size() << " PSD=" << digis.fPsd.size()
+               << " FSD=" << digis.fFsd.size();
+      QueueUnpackerMetrics(ts, unpackMonitor, digis);
     }
 
     // --- Digi event building
     std::vector<DigiEvent> events;
     evbuild::EventbuildChainMonitorData evbuildMonitor;
-    if (Opts().HasStep(Step::DigiTrigger)) {
-      auto [ev, mon] = fEventBuild->Run(unpackResult.first);
+    if (Opts().Has(Step::DigiTrigger)) {
+      auto [ev, mon] = fEventBuild->Run(digis);
       events         = std::move(ev);
       evbuildMonitor = mon;
       QueueEvbuildMetrics(evbuildMonitor);
@@ -156,9 +185,9 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
     sts::HitfinderMonitor stsHitfinderMonitor;
     PartitionedSpan<sts::Hit> stsHits;
     PartitionedVector<sts::Cluster> stsClusters;
-    if (Opts().HasStep(Step::LocalReco) && Opts().HasDetector(fles::Subsystem::STS)) {
+    if (Opts().Has(Step::LocalReco) && Opts().Has(fles::Subsystem::STS)) {
       bool storeClusters  = Opts().HasOutput(RecoData::Cluster);
-      auto stsResults     = fStsHitFinder(unpackResult.first.fSts, storeClusters);
+      auto stsResults     = fStsHitFinder(digis.fSts, storeClusters);
       stsHits             = stsResults.hits;
       stsClusters         = std::move(stsResults.clusters);
       stsHitfinderMonitor = stsResults.monitor;
@@ -166,8 +195,8 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
     }
 
     PartitionedVector<tof::Hit> tofHits;
-    if (Opts().HasStep(Step::LocalReco) && Opts().HasDetector(fles::Subsystem::TOF)) {
-      auto [caldigis, calmonitor]          = fTofCalibrator.Run(unpackResult.first.fTof);
+    if (Opts().Has(Step::LocalReco) && Opts().Has(fles::Subsystem::TOF)) {
+      auto [caldigis, calmonitor]          = fTofCalibrator.Run(digis.fTof);
       auto [hits, hitmonitor, digiindices] = fTofHitFinder.Run(caldigis);
       tofHits                              = std::move(hits);
       QueueTofCalibMetrics(calmonitor);
@@ -175,7 +204,7 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
     }
 
     // --- Tracking
-    if (Opts().HasStep(Step::Tracking)) {
+    if (Opts().Has(Step::Tracking)) {
       TrackingChain::Input_t input{
         .stsHits = stsHits,
         .tofHits = tofHits,
@@ -189,7 +218,7 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
       QueueTrackingMetrics(trackingOutput.monitorData);
     }
 
-    if (Opts().HasOutput(RecoData::RawDigi)) results.stsDigis = std::move(unpackResult.first.fSts);
+    if (Opts().HasOutput(RecoData::RawDigi)) results.stsDigis = std::move(digis.fSts);
     if (Opts().HasOutput(RecoData::DigiEvent)) results.events = std::move(events);
     if (Opts().HasOutput(RecoData::Cluster)) results.stsClusters = std::move(stsClusters);
     if (Opts().HasOutput(RecoData::Hit)) {
@@ -197,11 +226,8 @@ RecoResults Reco::Run(const fles::Timeslice& ts)
       results.tofHits = std::move(tofHits);
     }
   }
-  PrintTimings(tsTimes);
-
-  ProcessingMonitor processingMonitor;
-  processingMonitor.fTime = tsTimes;
-  QueueProcessingMetrics(processingMonitor);
+  PrintTimings(procMon.time);
+  QueueProcessingMetrics(procMon);
 
   return results;
 }
@@ -262,21 +288,18 @@ void Reco::QueueUnpackerMetrics(const fles::Timeslice& ts, const UnpackMonitorDa
                              {"tsIdDelta", tsDelta},
                              {"unpackTimeTotal", monitor.fTime.wall()},
                              {"unpackThroughput", monitor.fTime.throughput()},
-                             {"unpackBytesInSts", monitor.fNumBytesInSts},
                              {"unpackBytesInMuch", monitor.fNumBytesInMuch},
                              {"unpackBytesInTof", monitor.fNumBytesInTof},
                              {"unpackBytesInBmon", monitor.fNumBytesInBmon},
                              {"unpackBytesInTrd", monitor.fNumBytesInTrd},
                              {"unpackBytesInTrd2d", monitor.fNumBytesInTrd2d},
                              {"unpackBytesInRich", monitor.fNumBytesInRich},
-                             {"unpackBytesOutSts", sizeBytes(stsDigis)},
                              {"unpackBytesOutMuch", sizeBytes(muchDigis)},
                              {"unpackBytesOutTof", sizeBytes(tofDigis)},
                              {"unpackBytesOutBmon", sizeBytes(bmonDigis)},
                              {"unpackBytesOutTrd", sizeBytes(trdDigis)},
                              {"unpackBytesOutTrd2d", sizeBytes(trd2dDigis)},
                              {"unpackBytesOutRich", sizeBytes(richDigis)},
-                             {"unpackExpansionFactorSts", expansionFactor(monitor.fNumBytesInSts, stsDigis)},
                              {"unpackExpansionFactorMuch", expansionFactor(monitor.fNumBytesInMuch, muchDigis)},
                              {"unpackExpansionFactorTof", expansionFactor(monitor.fNumBytesInTof, tofDigis)},
                              {"unpackExpansionFactorBmon", expansionFactor(monitor.fNumBytesInBmon, bmonDigis)},
@@ -290,6 +313,28 @@ void Reco::QueueUnpackerMetrics(const fles::Timeslice& ts, const UnpackMonitorDa
                            });
 }
 
+template<class MSMonitor>
+void Reco::QueueUnpackerMetricsDet(const UnpackMonitor<MSMonitor>& monitor)
+{
+  if (!HasMonitor()) {
+    return;
+  }
+
+  std::string_view det = ToString(monitor.system);
+
+  auto MkKey = [&](std::string_view key) { return fmt::format("{}{}", det, key); };
+
+  GetMonitor().QueueMetric("cbmreco", {{"hostname", fles::system::current_hostname()}, {"child", Opts().ChildId()}},
+                           {
+                             {MkKey("unpackBytesIn"), monitor.sizeBytesIn},
+                             {MkKey("unpackBytesOut"), monitor.sizeBytesOut},
+                             {MkKey("unpackExpansionFactor"), monitor.ExpansionFactor()},
+                             {MkKey("unpackNumMs"), monitor.numMs},
+                             {MkKey("unpackNumErrInvalidSysVer"), monitor.errInvalidSysVer},
+                             {MkKey("unpackNumErrInvalidEqId"), monitor.errInvalidEqId},
+                           });
+}
+
 void Reco::QueueStsRecoMetrics(const sts::HitfinderMonitor& monitor)
 {
   if (!HasMonitor()) return;
@@ -398,7 +443,7 @@ void Reco::QueueProcessingMetrics(const ProcessingMonitor& mon)
 
   GetMonitor().QueueMetric("cbmreco", {{"hostname", fles::system::current_hostname()}, {"child", Opts().ChildId()}},
                            {
-                             {"processingTimeTotal", mon.fTime.wall()},
-                             {"processingThroughput", mon.fTime.throughput()},
+                             {"processingTimeTotal", mon.time.wall()},
+                             {"processingThroughput", mon.time.throughput()},
                            });
 }
diff --git a/algo/global/Reco.h b/algo/global/Reco.h
index 905dfa33a899959cc63cb4e053407445cc297d59..8649f997df76961fce165e40f62f118cce1c47bb 100644
--- a/algo/global/Reco.h
+++ b/algo/global/Reco.h
@@ -11,6 +11,7 @@
 #include "ca/TrackingChain.h"
 #include "global/RecoResults.h"
 #include "sts/HitfinderChain.h"
+#include "sts/Unpack.h"
 #include "tof/CalibratorChain.h"
 #include "tof/HitfinderChain.h"
 
@@ -26,7 +27,8 @@ namespace cbm::algo
   class Options;
 
   struct ProcessingMonitor {
-    xpu::timings fTime;  //< total processing time
+    xpu::timings time;        //< total processing time
+    xpu::timings timeUnpack;  //< time spent in unpacking
   };
 
   class Reco : SubChain {
@@ -54,6 +56,7 @@ namespace cbm::algo
 
     // STS
     UnpackChain fUnpack;
+    std::unique_ptr<sts::Unpack> fStsUnpack;
     sts::HitfinderChain fStsHitFinder;
 
     // TOF
@@ -69,6 +72,8 @@ namespace cbm::algo
     void Validate(const Options& opts);
 
     void QueueUnpackerMetrics(const fles::Timeslice&, const UnpackMonitorData&, const DigiData&);
+    template<class MSMonitor>
+    void QueueUnpackerMetricsDet(const UnpackMonitor<MSMonitor>&);
     void QueueStsRecoMetrics(const sts::HitfinderMonitor&);
     void QueueTofRecoMetrics(const tof::HitfindMonitorData&);
     void QueueTofCalibMetrics(const tof::CalibrateMonitorData&);
diff --git a/algo/test/realdata_test.sh b/algo/test/realdata_test.sh
index 290c4f3b5cc5484348139721295143504a8ad502..9b462bb291b06a808dbecd660c3e209232e09368 100755
--- a/algo/test/realdata_test.sh
+++ b/algo/test/realdata_test.sh
@@ -52,7 +52,8 @@ function ensure_gt_zero {
     if [ -n "$value" ] && [ "$value" -gt 0 ]; then
       continue
     else
-      echo "$pattern: FAIL ($placeholder is not greater than zero.)"
+      echo "$pattern: FAIL ($placeholder is not greater than zero: $value)"
+      echo "Tried to match: $sed_pattern"
       echo "Output:"
       echo "$output"
       exit 1
@@ -70,21 +71,15 @@ check_arg "$tsa_file" "tsa_file"
 echo "Running '$cbmreco_bin $reco_args'"
 log="$($cbmreco_bin $reco_args 2>&1)"
 reco_ok=$?
-echo "$log"
-echo "=============================="
 if [ $reco_ok -ne 0 ]; then
+  echo "$log"
+  echo "=============================="
   echo "Error: Reconstruction failed. Exit early."
   exit 1
 fi
 
 # Check Digis
-ensure_gt_zero "$log" "Timeslice contains %1 STS Digis"
-ensure_gt_zero "$log" "Timeslice contains %1 TOF Digis"
-ensure_gt_zero "$log" "Timeslice contains %1 BMON Digis"
-ensure_gt_zero "$log" "Timeslice contains %1 MUCH Digis"
-ensure_gt_zero "$log" "Timeslice contains %1 TRD Digis"
-ensure_gt_zero "$log" "Timeslice contains %1 TRD2D Digis"
-ensure_gt_zero "$log" "Timeslice contains %1 RICH Digis"
+ensure_gt_zero "$log" "TS contains Digis: STS=%1 MUCH=%2 TOF=%3 BMON=%4 TRD=%5 TRD2D=%6 RICH=%7 PSD=0 FSD=0"
 
 # Check Events
 ensure_gt_zero "$log" "Triggers: %1, events %2"
diff --git a/algo/unpack/CommonUnpacker.cxx b/algo/unpack/CommonUnpacker.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..ab4d188b29bd5563e39fec686c318d1c7940f583
--- /dev/null
+++ b/algo/unpack/CommonUnpacker.cxx
@@ -0,0 +1,40 @@
+/* Copyright (C) 2024 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer], Dominik Smith */
+#include "CommonUnpacker.h"
+
+using namespace cbm::algo;
+
+detail::MSData::MSData(const fles::Timeslice& ts, fles::Subsystem subsystem, gsl::span<u16> legalEqIds, u8 sys_ver)
+{
+  monitor.system = subsystem;
+
+  for (uint64_t comp = 0; comp < ts.num_components(); comp++) {
+    auto this_subsystem = static_cast<fles::Subsystem>(ts.descriptor(comp, 0).sys_id);
+
+    if (this_subsystem != subsystem) {
+      continue;
+    }
+
+    const u64 numMsInComp = ts.num_microslices(comp);
+    const u16 componentId = ts.descriptor(comp, 0).eq_id;
+
+    if (ts.descriptor(comp, 0).sys_ver != sys_ver) {
+      monitor.errInvalidSysVer++;
+      continue;
+    }
+
+    if (std::find(legalEqIds.begin(), legalEqIds.end(), componentId) == legalEqIds.end()) {
+      monitor.errInvalidEqId++;
+      continue;
+    }
+
+    monitor.sizeBytesIn += ts.size_component(comp);
+    monitor.numMs += numMsInComp;
+    for (u64 mslice = 0; mslice < numMsInComp; mslice++) {
+      msEqIds.push_back(componentId);
+      msDesc.push_back(ts.descriptor(comp, mslice));
+      msContent.push_back(ts.content(comp, mslice));
+    }
+  }
+}
diff --git a/algo/unpack/CommonUnpacker.h b/algo/unpack/CommonUnpacker.h
new file mode 100644
index 0000000000000000000000000000000000000000..2db2791b2d7d100b45dd0f87d80ff70281e72c45
--- /dev/null
+++ b/algo/unpack/CommonUnpacker.h
@@ -0,0 +1,138 @@
+/* Copyright (C) 2024 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Felix Weiglhofer [committer], Dominik Smith */
+#pragma once
+
+#include "Definitions.h"
+#include "PODVector.h"
+#include "Timeslice.hpp"
+#include "compat/Algorithm.h"
+#include "compat/OpenMP.h"
+
+#include <cstdint>
+#include <gsl/span>
+#include <map>
+#include <vector>
+
+#include <xpu/host.h>
+
+namespace cbm::algo
+{
+
+  namespace detail
+  {
+
+    struct UnpackMonitorBase {
+      fles::Subsystem system;       // subsystem
+      size_t numMs            = 0;  // number of microslices
+      size_t sizeBytesIn      = 0;  // total size of microslice contents
+      size_t sizeBytesOut     = 0;  // total size of unpacked digis
+      size_t errInvalidSysVer = 0;
+      size_t errInvalidEqId   = 0;
+
+      double ExpansionFactor() const { return sizeBytesIn > 0 ? static_cast<double>(sizeBytesOut) / sizeBytesIn : 0.0; }
+    };
+
+    /**
+     * @brief Collection of MS data to unpack
+     *
+     * @note Helper struct for CommonUnpacker. Moved out of class body to avoid code duplication from different template specializations.
+     */
+    struct MSData {
+      std::vector<u16> msEqIds;                        // equipment ids of microslices
+      std::vector<fles::MicrosliceDescriptor> msDesc;  // microslice descriptors
+      std::vector<const u8*> msContent;                // pointer to microslice contents
+
+      UnpackMonitorBase monitor;  // monitoring data
+
+      MSData(const fles::Timeslice& ts, fles::Subsystem system, gsl::span<u16> legalEqIds, u8 sys_ver);
+      ~MSData() = default;
+    };
+
+  }  // namespace detail
+
+  template<class MSMonitor>
+  struct UnpackMonitor : detail::UnpackMonitorBase {
+    std::vector<MSMonitor> msMonitor;  // monitoring data per microslice
+  };
+
+  template<class Digi, class UnpackAlgo, class MSMonitor>
+  class CommonUnpacker {
+
+   protected:
+    using Monitor_t = UnpackMonitor<MSMonitor>;
+    using Result_t  = std::pair<PODVector<Digi>, Monitor_t>;
+
+    std::map<u16, UnpackAlgo> fAlgos;  //< Equipment id to unpacker map. Filled by child class!
+
+    Result_t DoUnpack(const fles::Subsystem subsystem, const fles::Timeslice& ts, u8 sys_ver) const
+    {
+      xpu::scoped_timer t_(fles::to_string(subsystem));
+      auto legalEqIds = GetEqIds();
+
+      detail::MSData msData{ts, subsystem, gsl::make_span(legalEqIds), sys_ver};
+      const size_t numMs = msData.monitor.numMs;
+
+      Result_t out;
+      auto& digisOut   = out.first;
+      auto& monitorOut = out.second;
+
+      static_cast<detail::UnpackMonitorBase&>(monitorOut) = msData.monitor;
+
+      std::vector<std::vector<Digi>> msDigis(numMs);  // unpacked digis per microslice
+      monitorOut.msMonitor.resize(numMs);             // monitoring data per microslice
+
+      xpu::t_add_bytes(msData.monitor.sizeBytesIn);
+
+      xpu::push_timer("Unpack");
+      xpu::t_add_bytes(msData.monitor.sizeBytesIn);
+      CBM_PARALLEL_FOR(schedule(dynamic))
+      for (size_t i = 0; i < numMs; i++) {
+        auto result             = fAlgos.at(msData.msEqIds[i])(msData.msContent[i], msData.msDesc[i], ts.start_time());
+        msDigis[i]              = std::move(result.first);
+        monitorOut.msMonitor[i] = std::move(result.second);
+      }
+      xpu::pop_timer();
+
+      xpu::push_timer("Resize");
+      size_t nDigisTotal = 0;
+      for (const auto& digis : msDigis) {
+        nDigisTotal += digis.size();
+      }
+      digisOut.resize(nDigisTotal);
+      monitorOut.sizeBytesOut = nDigisTotal * sizeof(Digi);
+      xpu::pop_timer();
+
+      xpu::push_timer("Merge");
+      xpu::t_add_bytes(monitorOut.sizeBytesOut);
+      CBM_PARALLEL_FOR(schedule(dynamic))
+      for (size_t i = 0; i < numMs; i++) {
+        size_t offset = 0;
+        for (size_t x = 0; x < i; x++)
+          offset += msDigis[x].size();
+        std::copy(msDigis[i].begin(), msDigis[i].end(), digisOut.begin() + offset);
+      }
+      xpu::pop_timer();
+
+      DoSort(digisOut);
+
+      return out;
+    }
+
+   private:
+    void DoSort(gsl::span<Digi> digis) const
+    {
+      Sort(digis.begin(), digis.end(), [](const Digi& a, const Digi& b) { return a.GetTime() < b.GetTime(); });
+    }
+
+    std::vector<uint16_t> GetEqIds() const
+    {
+      std::vector<uint16_t> eqIds;
+      eqIds.reserve(fAlgos.size());
+      for (const auto& [eqId, algo] : fAlgos) {
+        eqIds.push_back(eqId);
+      }
+      return eqIds;
+    }
+  };
+}  // namespace cbm::algo
diff --git a/algo/unpack/Unpack.cxx b/algo/unpack/Unpack.cxx
index 660fde3d82ea8f6d568c763e6781518d40f48d64..76912437224e489d1a28e422827494c3500afdb4 100644
--- a/algo/unpack/Unpack.cxx
+++ b/algo/unpack/Unpack.cxx
@@ -30,11 +30,6 @@ namespace cbm::algo
     DigiData& digiTs           = result.first;
     UnpackMonitorData& monitor = result.second;
 
-    if (DetectorEnabled(Subsystem::STS)) {
-      monitor.fNumBytesInSts +=
-        ParallelMsLoop(Subsystem::STS, monitor, digiTs.fSts, monitor.fSts, *timeslice, fAlgoSts, 0x20);
-    }
-
     if (DetectorEnabled(Subsystem::TOF)) {
       monitor.fNumBytesInTof +=
         ParallelMsLoop(Subsystem::TOF, monitor, digiTs.fTof, monitor.fTof, *timeslice, fAlgoTof, 0x00);
@@ -301,33 +296,9 @@ namespace cbm::algo
     fSubsystems = subIds;
 
     // --- Common parameters for all components for STS
-    uint32_t numChansPerAsicSts   = 128;  // R/O channels per ASIC for STS
-    uint32_t numAsicsPerModuleSts = 16;   // Number of ASICs per module for STS
 
     // Create one algorithm per component for STS and configure it with parameters
-    auto equipIdsSts = fStsConfig.GetEquipmentIds();
-    for (auto& equip : equipIdsSts) {
-      std::unique_ptr<sts::UnpackPar> par(new sts::UnpackPar());
-      par->fNumChansPerAsic   = numChansPerAsicSts;
-      par->fNumAsicsPerModule = numAsicsPerModuleSts;
-      const size_t numElinks  = fStsConfig.GetNumElinks(equip);
-      for (size_t elink = 0; elink < numElinks; elink++) {
-        sts::UnpackElinkPar elinkPar;
-        auto mapEntry        = fStsConfig.Map(equip, elink);
-        elinkPar.fAddress    = mapEntry.first;   // Module address for this elink
-        elinkPar.fAsicNr     = mapEntry.second;  // ASIC number within module
-        elinkPar.fTimeOffset = fSystemTimeOffset[Subsystem::STS];
-        elinkPar.fAdcMinCut  = fStsConfig.AdcCutMap(equip, elink);
-        elinkPar.fAdcOffset  = 1.;
-        elinkPar.fAdcGain    = 1.;
-        if (fApplyWalkCorrection) elinkPar.fWalk = fStsConfig.WalkMap(elinkPar.fAddress, elinkPar.fAsicNr);
-        elinkPar.fChanMask = fStsConfig.MaskMap(equip, elink);
-        // TODO: Add parameters for time and ADC calibration
-        par->fElinkParams.push_back(elinkPar);
-      }
-      fAlgoSts[equip].SetParams(std::move(par));
-      L_(debug) << "--- Configured equipment " << equip << " with " << numElinks << " elinks";
-    }  //# equipments
+
 
     // Create one algorithm per component for MUCH and configure it with parameters
     auto equipIdsMuch = fMuchConfig.GetEquipmentIds();
@@ -443,8 +414,6 @@ namespace cbm::algo
       L_(debug) << "--- Configured equipment " << equip << " with " << numAsics << " asics";
     }
 
-    L_(info) << "--- Configured " << fAlgoSts.size()
-             << " unpacker algorithms for STS. (Walk correction = " << fApplyWalkCorrection << ")";
     //  L_(debug) << "Readout map:" << fStsConfig.PrintReadoutMap();
     L_(info) << "--- Configured " << fAlgoMuch.size() << " unpacker algorithms for MUCH.";
     L_(info) << "--- Configured " << fAlgoRich.size() << " unpacker algorithms for RICH.";
diff --git a/algo/unpack/Unpack.h b/algo/unpack/Unpack.h
index eb9f06cf1918953b50d81c15bdde9099a168e975..99e10884348234bcfb0f74839e82a0cd1513ccae 100644
--- a/algo/unpack/Unpack.h
+++ b/algo/unpack/Unpack.h
@@ -16,7 +16,6 @@
 #include "rich/Unpack.h"
 #include "sts/Digi.h"
 #include "sts/ReadoutConfigLegacy.h"
-#include "sts/Unpack.h"
 #include "tof/ReadoutConfig.h"
 #include "tof/Unpack.h"
 #include "trd/ReadoutConfig.h"
@@ -40,7 +39,6 @@ namespace cbm::algo
    ** @brief Monitoring data for unpacking
    **/
   struct UnpackMonitorData {
-    std::vector<sts::UnpackMonitorData> fSts;      ///< Monitoring data for STS
     std::vector<much::UnpackMonitorData> fMuch;    ///< Monitoring data for MUCH
     std::vector<tof::UnpackMonitorData> fTof;      ///< Monitoring data for TOF
     std::vector<bmon::UnpackMonitorData> fBmon;    ///< Monitoring data for Bmon
@@ -50,7 +48,6 @@ namespace cbm::algo
     xpu::timings fTime;
     size_t fNumMs               = 0;
     size_t fNumBytes            = 0;
-    size_t fNumBytesInSts       = 0;
     size_t fNumBytesInMuch      = 0;
     size_t fNumBytesInTof       = 0;
     size_t fNumBytesInBmon      = 0;
@@ -116,12 +113,6 @@ namespace cbm::algo
     /** @brief Parameters for RICH unpackers **/
     rich::ReadoutConfig fRichConfig{};
 
-
-    /**
-     * @brief Set whether to apply walk correction. Must be set before Init(). (default: true)
-    **/
-    void SetApplyWalkCorrection(bool applyWalkCorrection) { fApplyWalkCorrection = applyWalkCorrection; }
-
     /** @brief Initialize unpackers and fill parameters from config objects
      * @param subIds: vector of subsystem identifiers to unpack, default: all
      * @see Init()
@@ -166,12 +157,8 @@ namespace cbm::algo
 
 
    private:                                     // members
-    bool fApplyWalkCorrection          = true;  ///< Apply walk correction
     std::vector<Subsystem> fSubsystems = {};    ///< Detector identifiers to unpack
 
-    /** @brief STS unpackers **/
-    std::map<uint16_t, sts::Unpack> fAlgoSts = {};
-
     /** @brief MUCH unpackers **/
     std::map<uint16_t, much::Unpack> fAlgoMuch = {};
 
@@ -197,7 +184,7 @@ namespace cbm::algo
 
    private:  // methods
     template<typename UnpackAlgo>
-    std::vector<uint16_t> GetEqIds(const std::map<uint16_t, UnpackAlgo>& algoMap)
+    std::vector<uint16_t> GetEqIds(const std::map<uint16_t, UnpackAlgo>& algoMap) const
     {
       std::vector<uint16_t> eqIds;
       eqIds.reserve(algoMap.size());
diff --git a/algo/unpack/UnpackChain.cxx b/algo/unpack/UnpackChain.cxx
index 68ff5f9bd15fb0a4ba4166d1ac1148b0e9ce61af..8dd57874cbb3b95fe49440fcfb34c8fab4c63f3c 100644
--- a/algo/unpack/UnpackChain.cxx
+++ b/algo/unpack/UnpackChain.cxx
@@ -10,12 +10,11 @@ using fles::Subsystem;
 
 void UnpackChain::Init()
 {
-  fUnpack.SetApplyWalkCorrection(true);
-  if (Opts().HasDetector(Subsystem::TRD)) {
+  if (Opts().Has(Subsystem::TRD)) {
     auto yaml          = YAML::LoadFile((Opts().ParamsDir() / "TrdReadoutSetup.yaml").string());
     fUnpack.fTrdConfig = config::Read<trd::ReadoutConfig>(yaml);
   }
-  if (Opts().HasDetector(Subsystem::TRD2D)) {
+  if (Opts().Has(Subsystem::TRD2D)) {
     auto yaml            = YAML::LoadFile((Opts().ParamsDir() / "Trd2dReadoutSetup.yaml").string());
     fUnpack.fTrd2dConfig = config::Read<trd2d::ReadoutConfig>(yaml);
   }
@@ -26,15 +25,5 @@ Unpack::resultType UnpackChain::Run(const fles::Timeslice& timeslice)
 {
   auto result = fUnpack(&timeslice);
 
-  auto& digis = result.first;
-
-  if (Opts().HasDetector(Subsystem::STS)) L_(info) << "Timeslice contains " << digis.fSts.size() << " STS Digis";
-  if (Opts().HasDetector(Subsystem::TOF)) L_(info) << "Timeslice contains " << digis.fTof.size() << " TOF Digis";
-  if (Opts().HasDetector(Subsystem::BMON)) L_(info) << "Timeslice contains " << digis.fBmon.size() << " BMON Digis";
-  if (Opts().HasDetector(Subsystem::MUCH)) L_(info) << "Timeslice contains " << digis.fMuch.size() << " MUCH Digis";
-  if (Opts().HasDetector(Subsystem::TRD)) L_(info) << "Timeslice contains " << digis.fTrd.size() << " TRD Digis";
-  if (Opts().HasDetector(Subsystem::TRD2D)) L_(info) << "Timeslice contains " << digis.fTrd2d.size() << " TRD2D Digis";
-  if (Opts().HasDetector(Subsystem::RICH)) L_(info) << "Timeslice contains " << digis.fRich.size() << " RICH Digis";
-
   return result;
 }