diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/CMakeLists.txt b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/CMakeLists.txt
index 106b7ab0412d9e2635c2e56cfe2983a915fd1972..255407b674e81c18cce6734223cb58e08e564439 100644
--- a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/CMakeLists.txt
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/CMakeLists.txt
@@ -10,7 +10,9 @@ find_package( tdaq-common COMPONENTS  eformat_write  )
 atlas_add_component( L1CaloFEXByteStream
                      src/*.cxx
                      src/components/*.cxx
-                     INCLUDE_DIRS ${TDAQ-COMMON_INCLUDE_DIRS}
+                     src/bytestreamDecoder/src/*.cxx
+                     DEFINITIONS OFFLINE_DECODER
+                     INCLUDE_DIRS ${TDAQ-COMMON_INCLUDE_DIRS} src/bytestreamDecoder
                      LINK_LIBRARIES TrigT1ResultByteStreamLib xAODTrigger ${TDAQ-COMMON_LIBRARIES} )
 
 # Install files from the package:
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/python/L1CaloFEXByteStreamConfig.py b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/python/L1CaloFEXByteStreamConfig.py
index 744e24704c8be6a3232afb53114684b97ed7ffa8..8d610ddf80932b26ca744e4c7bbae5d4fe84e828 100644
--- a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/python/L1CaloFEXByteStreamConfig.py
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/python/L1CaloFEXByteStreamConfig.py
@@ -4,6 +4,37 @@
 from AthenaConfiguration.ComponentFactory import CompFactory
 from libpyeformat_helper import SourceIdentifier, SubDetector
 
+def eFexByteStreamToolCfg(name, flags, writeBS=False):
+  tool = CompFactory.eFexByteStreamTool(name)
+  efex_roi_moduleids = [0x1000,0x1100]
+  tool.ROBIDs = [int(SourceIdentifier(SubDetector.TDAQ_CALO_FEAT_EXTRACT_ROI, moduleid)) for moduleid in efex_roi_moduleids]
+  if writeBS:
+    # write BS == read xAOD
+    tool.eEMTOBContainerReadKey  ="L1_eFexEMTOB"
+    tool.eTAUTOBContainerReadKey ="L1_eFexTauRoI"
+    tool.eEMxTOBContainerReadKey="L1_eFexEMxRoI"
+    tool.eTAUxTOBContainerReadKey ="L1_eFexTauxRoI"
+
+
+    tool.eEMTOBContainerWriteKey  =""
+    tool.eTAUTOBContainerWriteKey =""
+    tool.eEMxTOBContainerWriteKey=""
+    tool.eTAUxTOBContainerWriteKey =""
+  else:
+    # read BS == write xAOD
+    tool.eEMTOBContainerReadKey  =""
+    tool.eTAUTOBContainerReadKey =""
+    tool.eEMxTOBContainerReadKey=""
+    tool.eTAUxTOBContainerReadKey =""
+
+
+    tool.eEMTOBContainerWriteKey  ="L1_eFexEMRoI"
+    tool.eTAUTOBContainerWriteKey ="L1_eFexTauRoI"
+    tool.eEMxTOBContainerWriteKey="L1_eFexEMxRoI"
+    tool.eTAUxTOBContainerWriteKey ="L1_eFexTauxRoI"
+
+  return tool
+
 
 def jFexByteStreamToolCfg(name, flags, writeBS=False):
   tool = CompFactory.jFexByteStreamTool(name)
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/README.md b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..c34ab583b8b540c2354196a886e076ce44e87d8b
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/README.md
@@ -0,0 +1 @@
+These files are copied from https://gitlab.cern.ch/atlas-l1calo-online/bytestreamDecoder/-/tree/offline - should not be modified by humans in this repo, any changes should be made in the online software repo instead. 
\ No newline at end of file
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloBsDecoderRun3.h b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloBsDecoderRun3.h
new file mode 100644
index 0000000000000000000000000000000000000000..deac8fc4a63c99d9bd8990321de715d1ddf20924
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloBsDecoderRun3.h
@@ -0,0 +1,111 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+#ifndef L1CALO_BS_DECODER_RUN3_H
+#define L1CALO_BS_DECODER_RUN3_H
+
+#include <list>
+#include <stdint.h>
+
+#include "bytestreamDecoder/L1CaloRdoFexTob.h"
+
+class L1CaloRdoEfexTob;
+class L1CaloRdoEfexTower;
+class L1CaloRdoGfexTob;
+class L1CaloRdoGfexTower;
+class L1CaloRdoJfexTob;
+class L1CaloRdoJfexTower;
+class L1CaloRdoMuonTob;     // **FIXME** Different class for run 3?
+class L1CaloRdoRodInfo;
+
+class L1CaloBsDecoderRun3
+{
+public:
+   L1CaloBsDecoderRun3();
+#ifndef OFFLINE_DECODER
+   void decodeEfexData( const uint32_t* beg, const uint32_t* end,
+                        std::list<L1CaloRdoEfexTower>& dat,
+                        std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+#endif
+   void decodeEfexTobs( const uint32_t* beg, const uint32_t* end,
+                        std::list<L1CaloRdoEfexTob>& tob,
+                        std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+#ifndef OFFLINE_DECODER
+   void decodeJfexData( const uint32_t* beg, const uint32_t* end,
+                        std::list<L1CaloRdoJfexTower>& dat,
+                        std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+
+   void decodeJfexTobs( const uint32_t* beg, const uint32_t* end,
+                        std::list<L1CaloRdoJfexTob>& tob,
+                        std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+
+   void decodeGfexData( const uint32_t* beg, const uint32_t* end,
+                        std::list<L1CaloRdoGfexTower>& dat,
+                        std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+
+   void decodeGfexTobs( const uint32_t* beg, const uint32_t* end,
+                        std::list<L1CaloRdoGfexTob>& dat,
+                        std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+
+   void decodePh1TopoData( const uint32_t* beg, const uint32_t* end,
+                           std::list<L1CaloRdoEfexTob>& etob,
+                           std::list<L1CaloRdoJfexTob>& jtob,
+                           std::list<L1CaloRdoGfexTob>& gtob,
+                           std::list<L1CaloRdoMuonTob>& mtob,
+                           std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+
+   //>>void decodePh1TopoHits( const uint32_t* beg, const uint32_t* end,
+   //>>                     std::list<L1CaloRdoPh1TopoHit>& dat,
+   //>>                     std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+#endif
+   void setVerbosity( bool verbosity );
+
+private:
+#ifndef OFFLINE_DECODER
+   uint32_t decodeEfexDataChan( const uint32_t payload[],
+                                const uint32_t efexNumber,
+                                const uint32_t shelfNumber,
+                                const uint32_t errorMask,
+                                std::list<L1CaloRdoEfexTower>& dat,
+                                std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+#endif
+   bool decodeEfexTobSlice( const uint32_t payload[], size_t& index,
+                            const uint32_t efexNumber, const uint32_t shelfNumber,
+                            const uint32_t numSlices, const uint32_t errorMask,
+                            std::list<L1CaloRdoEfexTob>& tob,
+                            std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+#ifndef OFFLINE_DECODER
+   uint32_t decodeJfexDataChan( const uint32_t payload[],
+                                const uint32_t jfexNumber,
+                                const uint32_t fpgaNumber,
+                                const uint32_t errorMask,
+                                std::list<L1CaloRdoJfexTower>& dat,
+                                std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+
+   bool decodeJfexTobSlice( const uint32_t payload[], size_t& index,
+                            const uint32_t jfexNumber, const uint32_t fpgaNumber,
+                            const uint32_t sliceNumber, const uint32_t numSlices,
+                            const uint32_t errorMask,
+                            std::list<L1CaloRdoJfexTob>& tob,
+                            std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+
+   uint32_t decodeGfexDataChan( const uint32_t payload[],
+                                const uint32_t fpgaNumber,
+                                const uint32_t chanNumber,
+                                const uint32_t errorMask,
+                                std::list<L1CaloRdoGfexTower>& dat,
+                                std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+#endif
+   void decodeOneEfexTob( const uint32_t word[], const uint32_t shelfNumber,
+                          const uint32_t efexNumber, const uint32_t fpgaNumber,
+                          const uint32_t errorMask,
+                          const uint32_t numSlices, const uint32_t sliceNum,
+                          L1CaloRdoFexTob::TobType tobType,
+                          L1CaloRdoFexTob::TobSource tobSource,
+                          std::list<L1CaloRdoEfexTob>& tob,
+                          std::list<L1CaloRdoRodInfo>::const_iterator rodInfo );
+
+   int m_verbosity;
+};
+
+#endif
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloBsDecoderUtil.h b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloBsDecoderUtil.h
new file mode 100644
index 0000000000000000000000000000000000000000..1f2c28c0ef122d16633a11b2b3f88fdf30f10952
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloBsDecoderUtil.h
@@ -0,0 +1,54 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+#ifndef L1CALO_BS_DECODER_UTIL_H
+#define L1CALO_BS_DECODER_UTIL_H
+
+#include <list>
+
+class L1CaloRdoRodInfo;
+
+namespace eformat { 
+   template <class TPointer> class ROBFragment;
+}
+
+class L1CaloBsDecoderUtil
+{
+public:
+
+   static void decodeRodInfo( const eformat::ROBFragment<const uint32_t*>* rod,
+                              std::list<L1CaloRdoRodInfo>& dat );
+
+   template <typename Tar, typename Dat, typename Iter>
+   static Tar& findRdo( const Tar& target, Dat& data, Iter begin, Iter end );
+
+   template <typename Tar, typename Dat>
+   static Tar& findRdo( const Tar& target, Dat& data );
+
+private:
+   L1CaloBsDecoderUtil();
+};
+
+template <typename Tar, typename Dat, typename Iter> Tar&
+L1CaloBsDecoderUtil::findRdo( const Tar& target, Dat& data, Iter begin, Iter end )
+{
+   while ( (begin != end) && ( target < (*begin) ) ) 
+      ++begin;
+
+   if ( begin == end )
+      return *(data.insert( end.base(), target ));  
+ 
+   if ( begin->sameDatum( target ) )
+      return *begin;
+
+   return *(data.insert( begin.base(), target ));
+}
+
+template <typename Tar, typename Dat> Tar&
+L1CaloBsDecoderUtil::findRdo( const Tar& target, Dat& data )
+{
+   return findRdo( target, data, data.rbegin(), data.rend() );
+} 
+
+#endif
+
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloRdo.h b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloRdo.h
new file mode 100644
index 0000000000000000000000000000000000000000..4a0f1ab172d44a65e6e319a3e286fd576fb7b6cd
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloRdo.h
@@ -0,0 +1,92 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+#ifndef L1CALO_RDO_H
+#define L1CALO_RDO_H
+
+#include <vector>
+#include <list>
+#include <string>
+
+#ifndef OFFLINE_DECODER
+#include "channelMappings/L1CaloDetectorRegion.h"
+#endif
+
+class L1CaloRdoRodInfo;
+
+class L1CaloRdo 
+{
+public:
+   virtual ~L1CaloRdo();
+
+   int getCrate( ) const;
+   int getModule( ) const;
+   int getEta( ) const;
+   int getPhi( ) const;
+   int getLayer( ) const;
+   int getValue( size_t slice ) const;
+   int getValue( ) const;
+   int getFlag( size_t slice ) const;
+   int getFlag( ) const;
+   int getL1aPos( ) const;
+#ifndef OFFLINE_DECODER
+   L1CaloDetectorRegion getRegion( ) const;
+#else
+    void setWord0( uint32_t val, size_t slice ){ if(slice < m_word0s.size()) m_word0s[slice] = val; };
+    void setWord1( uint32_t val, size_t slice ){ if(slice < m_word1s.size()) m_word1s[slice] = val; };
+    uint32_t getWord0( size_t slice ) const { return (slice < m_word0s.size()) ? m_word0s[slice] : 0; };
+    uint32_t getWord1( size_t slice ) const { return (slice < m_word1s.size()) ? m_word1s[slice] : 0; };
+#endif
+   const std::list<L1CaloRdoRodInfo>::const_iterator& getRodInfo( ) const;
+   int getModuleStatus( ) const;
+
+   bool getModuleErrorGlinkParity( ) const;
+   bool getModuleErrorGlinkProtocol( ) const;
+   bool getModuleErrorBcnMismatch( ) const;
+   bool getModuleErrorFifoOverflow( ) const;
+   bool getModuleErrorSpecific( ) const;
+   bool getModuleErrorUnused( ) const;
+   bool getModuleErrorGlinkTimeout( ) const;
+   bool getModuleErrorGlinkDown( ) const;
+   
+   size_t numSlices( ) const;
+   bool sameDatum( const L1CaloRdo& rhs ) const;
+   
+   void setValue( int val, size_t slice );
+   void setValue( int val );
+   void setFlag( int flag, size_t slice );
+   void setFlag( int flag );
+   void setRodInfo( std::list<L1CaloRdoRodInfo>::const_iterator& rodInfo );
+   
+   void info( ) const;
+   virtual void infoSpecific( ) const;   
+   virtual std::string getType( ) const = 0;
+
+protected:
+   L1CaloRdo( int crate, int module, int eta, int phi, 
+              int layer, int numSlices );
+#ifndef OFFLINE_DECODER
+   void setRegion( const L1CaloDetectorRegion& region );
+#endif
+private:
+   L1CaloRdo( );
+   
+   int m_crate;
+   int m_module;
+   int m_eta;
+   int m_phi;
+   int m_layer;
+   std::vector<int> m_vals;
+   std::vector<int> m_flags;
+   int m_l1aPos;
+#ifndef OFFLINE_DECODER
+   L1CaloDetectorRegion m_region;
+#else
+   std::vector<uint32_t> m_word0s,m_word1s;
+#endif
+   std::list<L1CaloRdoRodInfo>::const_iterator m_rodInfo;
+};
+
+bool operator<(const L1CaloRdo& lhs, const L1CaloRdo& rhs);
+
+#endif
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloRdoEfexTob.h b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloRdoEfexTob.h
new file mode 100644
index 0000000000000000000000000000000000000000..b06c4ceda94ba3fbc8dfc55c9059f555605b121d
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloRdoEfexTob.h
@@ -0,0 +1,28 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+#ifndef L1CALO_RDO_EFEX_EMTAU_H
+#define L1CALO_RDO_EFEX_EMTAU_H
+
+#include "bytestreamDecoder/L1CaloRdoFexTob.h"
+
+class L1CaloRdoEfexTob : public L1CaloRdoFexTob
+{
+public:
+   L1CaloRdoEfexTob( int crate, int module, int eta, int phi, int numSlices,
+                     TobType tobType, TobSource source, int id = 0, int fibre = 0, int tobSeq = 0 ); 
+   
+   virtual void infoSpecific( ) const override;
+   
+   //??bool getOverflow( size_t slice ) const;
+   //??bool getOverflow( ) const;
+   
+   int getClusterEt( size_t slice ) const;
+   int getClusterEt( ) const;
+   int getIsol( size_t slice ) const;
+   int getIsol( ) const;
+
+private:
+};
+
+#endif
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloRdoFexTob.h b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloRdoFexTob.h
new file mode 100644
index 0000000000000000000000000000000000000000..7ac9304f7ca332dee94d03cef30d3a67cf49cde9
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloRdoFexTob.h
@@ -0,0 +1,56 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+#ifndef L1CALO_RDO_FEX_TOB_H
+#define L1CALO_RDO_FEX_TOB_H
+
+#include <string>
+
+#include "bytestreamDecoder/L1CaloRdo.h"
+
+class L1CaloRdoFexTob : public L1CaloRdo
+{
+public:
+   enum TobType { Invalid, EM, Tau, LargeJet, SmallJet, Energy, Muon };
+   enum TobSource { EfexTob, EfexXtob, JfexTob, JfexXtob, GfexTob, GfexXtob, Ph1Topo };
+   
+   L1CaloRdoFexTob( int crate, int module, int eta, int phi, int numSlices,
+                    TobType tobType, TobSource source, int id = 0, int fibre = 0, int tobSeq = 0 ); 
+
+   bool sameDatum( const L1CaloRdoFexTob& rhs ) const;
+   virtual std::string getType( ) const override;
+   
+   TobType getTobType( ) const;
+   std::string getTobTypeString( ) const;
+   int getTobID( ) const;
+   bool getIsEM( ) const;
+   bool getIsTau( ) const;
+   bool getIsJet( ) const;
+   bool getIsLargeJet( ) const;
+   bool getIsSmallJet( ) const;
+   bool getIsEnergy( ) const;
+   bool getIsMuon( ) const;
+   
+   TobSource getTobSource( ) const;
+   std::string getTobSourceString( ) const;
+   bool getIsTob( ) const;
+   bool getIsXtob( ) const;
+   bool getIsEfex( ) const;
+   bool getIsJfex( ) const;
+   bool getIsGfex( ) const;
+   bool getIsTopo( ) const;
+   
+   int getFibre( ) const;
+   int getTobSeq( ) const;
+
+private:
+   TobType m_tobType;
+   TobSource m_source;
+   int m_id;
+   int m_fibre;
+   int m_tobSeq;
+};
+
+bool operator<(const L1CaloRdoFexTob& lhs, const L1CaloRdoFexTob& rhs);
+
+#endif
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloRdoRodInfo.h b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloRdoRodInfo.h
new file mode 100644
index 0000000000000000000000000000000000000000..c24742df17b088fd7035bf2f2503d83834a56131
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/bytestreamDecoder/L1CaloRdoRodInfo.h
@@ -0,0 +1,72 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+#ifndef L1CALO_RDO_ROD_INFO_H
+#define L1CALO_RDO_ROD_INFO_H
+
+#include <string>
+#include <vector>
+
+class L1CaloRdoRodInfo
+{
+public:
+   L1CaloRdoRodInfo( int system, int rod, int run, int bcnum, int triggerType, int detType, int version ); 
+   std::string getType( ) const;
+
+   void info( ) const;
+   int  getMinorVersion( ) const;
+   bool getIsRun1( ) const;
+   bool getIsRun2( ) const;
+   bool getIsRun3( ) const;
+   int  getSystemId( ) const;
+   int  getRodId( ) const;
+   int  getSourceId( ) const;
+   int  getRunNumber( ) const;
+   int  getBcNumber( ) const;
+   int  getTriggerType( ) const;
+   
+   int  getLevel1Id( ) const;
+   int  getDataSize( ) const;
+   int  getStepNumber( ) const;
+   int  getOrbitCount( ) const;
+
+   bool getBcnumMismatch( ) const;
+   bool getGlinkTimeout( ) const;
+   bool getDataTransportError( ) const;
+   bool getRodOverflow( ) const;
+
+   bool getModuleLinkError( ) const;
+   bool getCmmParityError( ) const;
+   bool getGlinkError( ) const;
+   
+   bool getRoiOverflow( ) const;
+
+   bool getTriggerTypeTimeout( ) const;
+   
+   int  getModuleStatus( const int module ) const;
+   
+   void setLvl1Id( int lvl1 );
+   void setSize( int size );
+   void setStatus1( int status );
+   void setStatus2( int status );
+   void setModuleStatus( int module, int status );
+   
+   // Minimum ROD minor version for Run 2.
+   static const unsigned int s_minRun2Version = 0x1004;
+   
+private:
+   int m_system;
+   int m_rod;
+   int m_run;
+   int m_bcnum;
+   int m_triggerType;
+   int m_detType;
+   int m_lvl1;
+   int m_size;
+   int m_status1;
+   int m_status2;
+   int m_version;
+   std::vector<int> m_status;
+};
+
+#endif
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloBsDecoderRun3.cxx b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloBsDecoderRun3.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..96b6e46908eeaaecc0f211f2165b873703f5d164
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloBsDecoderRun3.cxx
@@ -0,0 +1,1386 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+#include <algorithm>
+#include <bitset>
+#include <iostream>
+#include <iomanip>
+#include <map>
+#include <vector>
+
+#include "eformat/ROBFragment.h"
+
+#include "bytestreamDecoder/L1CaloBsDecoderRun3.h"
+#include "bytestreamDecoder/L1CaloBsDecoderUtil.h"
+#include "bytestreamDecoder/L1CaloRdoEfexTob.h"
+#ifndef OFFLINE_DECODER
+#include "bytestreamDecoder/L1CaloRdoEfexTower.h"
+#include "bytestreamDecoder/L1CaloRdoGfexTob.h"
+#include "bytestreamDecoder/L1CaloRdoGfexTower.h"
+#include "bytestreamDecoder/L1CaloRdoJfexTob.h"
+#include "bytestreamDecoder/L1CaloRdoJfexTower.h"
+#include "bytestreamDecoder/L1CaloRdoMuonTob.h"     // **FIXME** Different class for run 3?
+#include "channelMappings/EfexCellMapping.h"
+#include "channelMappings/GfexCellMapping.h"
+#include "channelMappings/JfexCellMapping.h"
+#include "defsL1Calo/CrateDefs.h"
+#include "defsL1Calo/EfexDefs.h"
+#include "infraL1Calo/EfexLatomeFibrePacker.h"
+#include "infraL1Calo/EfexTrexFibrePacker.h"
+#include "infraL1Calo/GfexLatomeCentralFibrePacker.h"
+#include "infraL1Calo/GfexLatomeForwardFibrePacker.h"
+#include "infraL1Calo/GfexTrexFibrePacker.h"
+#include "infraL1Calo/JfexLatomeFibrePacker.h"
+#include "infraL1Calo/JfexTrexFibrePacker.h"
+#endif
+
+#include "ers/ers.h"
+#define ERS_ERROR(message) { std::stringstream s; s << __FUNCTION__ << " - " << message;ers::error(ers::Message(ERS_HERE, s.str()));}
+
+
+/*!
+ * \class L1CaloBsDecoderRun3
+ * Decoder for Run 3 data formats: eFEX, jFEX, gFEX and phase 1 Topo.
+ * Utility methods shared with Runs 1 & 2 are now in L1CaloBsDecoderUtil.
+ */
+
+L1CaloBsDecoderRun3::L1CaloBsDecoderRun3()
+: m_verbosity(0)
+{
+#ifndef OFFLINE_DECODER
+  // Force initialisation of mapping tables.
+  EfexCellMapping dummyEfexMapping(0,0,0,0,0,0);
+  if (!dummyEfexMapping.getDetectorRegion().getValidity()) {
+    std::cerr << "L1CaloBsDecoderRun3::ctor: unexpected invalid eFEX mapping!?" << std::endl;
+  }
+  JfexCellMapping dummyJfexMapping(0,1,0,0);                    // Processor number 1-4
+  if (!dummyJfexMapping.getDetectorRegion().getValidity()) {
+    std::cerr << "L1CaloBsDecoderRun3::ctor: unexpected invalid jFEX mapping!?" << std::endl;
+  }
+#endif
+}
+
+#ifndef OFFLINE_DECODER
+/*!
+ * Decode eFEX input fibre data.
+ * For the EM layer we have up to 10 supercell values per tower
+ * though there is still just a single hadronic value.
+ * It seemed overkill to have an RDO per supercell, so instead
+ * the main RDO "Value" is set to the sum of 10 EM supercells
+ * with the individual supercell Ets saved in a separate vector.
+ * These are always stored in 1441 order (PS, F0-F3, M0-M3, BK)
+ * even in regions where some supercells are missing.
+ * The flag field is used for status/error bits.
+ * \param beg pointer to start of rod data payload
+ * \param end pointer to end of rod data payload
+ * \param tower list of RDO to be filled
+ * \param rodInfo iterator to ROD information for this block
+ */
+void
+L1CaloBsDecoderRun3::decodeEfexData( const uint32_t* beg, const uint32_t* end,
+                                     std::list<L1CaloRdoEfexTower>& tower,
+                                     std::list<L1CaloRdoRodInfo>::const_iterator rodInfo )
+{
+   const uint32_t* payload( beg );
+   const size_t fragmentSize = end - beg;
+   
+   // The data block is optimised for production by firmware with the length
+   // in a trailer at the end. The SWROD concatenates a number of such blocks
+   // (removing the 2 word header used to fill the ROD fragment header).
+   // So we need to find the end and work backwards.
+   
+   size_t numTowers = tower.size();
+   size_t index = fragmentSize;
+   
+   // Loop looking backwards for eFEX processor input data blocks.
+   while ( index > 0 ) {
+      if ( index < 4 ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeEfexData: remaining block size " << index
+                   << " is too small for the eFEX FPGA trailer" << std::endl;
+         return;
+      }
+      const uint32_t ctrlTrailer2 = payload[--index];
+      const uint32_t ctrlTrailer1 = payload[--index];
+      const uint32_t fpgaTrailer2 = payload[--index];
+      const uint32_t fpgaTrailer1 = payload[--index];
+
+      const uint32_t ctrlErrors  =  ctrlTrailer2 & 0x3f;
+      const uint32_t efexNumber  = (ctrlTrailer1 >> 12) & 0xf;
+      const uint32_t shelfNumber = (ctrlTrailer1 >> 16) & 0x1;  // Just use 1 bit from 4 bit field
+      const size_t   payloadSize = (ctrlTrailer1 & 0xfff) - 2;
+      
+      if ( payloadSize > index ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeEfexData: remaining eFEX block size "
+                   << index << " is too small for the claimed payload size "
+                   << payloadSize << std::endl;
+         return;
+      }
+      
+      if (efexNumber >= CrateDefs::numAtcaFexSlots() ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeEfexData: invalid eFEX number " << efexNumber
+                   << " (out of range 0-" << CrateDefs::numAtcaFexSlots()-1 << ")" << std::endl;
+         return;
+      }
+      
+      // We can now work forwards from the start of this block
+      // decoding each set of 8 words per input fibre channel.
+      // The payloadSize has had two trailer words subtracted.
+      index -= payloadSize;
+      size_t chanIndex = 0;
+      uint32_t chanErrorOR = 0;
+      std::bitset<49> chansWithError;
+      bool anyErrorBit = false;
+      while ( chanIndex < payloadSize ) {
+         if ( (payloadSize - chanIndex) < 8 ) {
+            std::cerr << "L1CaloBsDecoderRun3::decodeEfexData: crate " << shelfNumber << " efex " << efexNumber
+                      << ": remaining eFEX block size " << (payloadSize - chanIndex)
+                      << " is too small for one eFEX input fibre block (8)" << std::endl;
+            return;
+         }
+         const uint32_t chanNumber = payload[index+chanIndex+7] & 0xff;
+         const uint32_t chanErrors = this->decodeEfexDataChan ( &payload[index+chanIndex], efexNumber, shelfNumber,
+                                                                ctrlErrors, tower, rodInfo );
+         if ( chanErrors ) {
+            chanErrorOR |= ( chanErrors & 0x7 );   // Only three lowest bits get ORed
+            chansWithError[chanNumber] = 1;
+            anyErrorBit = true;
+         }
+         chanIndex += 8;
+      }
+      if ( anyErrorBit ) {
+         chanErrorOR |= 0x8;   // Extra bit set if any of the three lower bits are set
+      }
+      const uint64_t fpgaErrorBits = ( (const uint64_t)(fpgaTrailer2 & 0x1ffff) << 32 ) | fpgaTrailer1;
+      const uint64_t chanErrorBits = chansWithError.to_ullong();
+      const uint32_t fpgaErrorOR = ( fpgaTrailer2 >> 28 ) & 0xf;
+      if ( fpgaErrorBits != chanErrorBits || fpgaErrorOR != chanErrorOR ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeEfexData: crate " << shelfNumber << " efex " << efexNumber
+                   << ": mismatch between errors in FPGA trailer: "
+                   << std::hex << fpgaErrorBits << " " << fpgaErrorOR
+                   << " and those derived from channels: " << chanErrorBits << " " << chanErrorOR
+                   << std::dec << std::endl;
+      }
+   }
+   if ( m_verbosity > 0 )
+   {
+      std::cout << "L1CaloBsDecoderRun3::decodeEfexData: n.towers added="
+                << tower.size() - numTowers << std::endl;
+   }
+}
+
+/*!
+ * Decode the data from one eFEX input fibre (only ever one slice).
+ * \param payload payload vector starting at this 8 word block
+ * \param efexNumber number of this eFEX in its shelf
+ * \param shelfNumber shelf number
+ * \param errorMask global error bits set for this ROD fragment
+ * \param tower list of RDO to be filled
+ * \param rodInfo iterator to ROD information for this block
+ * \return whether decoding succeeded (false to abort decoding)
+ */
+uint32_t
+L1CaloBsDecoderRun3::decodeEfexDataChan( const uint32_t payload[],
+                                         const uint32_t efexNumber, const uint32_t shelfNumber,
+                                         const uint32_t errorMask,
+                                         std::list<L1CaloRdoEfexTower>& tower,
+                                         std::list<L1CaloRdoRodInfo>::const_iterator rodInfo )
+{
+   // The EM and hadronic fibres have different encoding and cover
+   // different numbers of towers with more or less granularity.
+   // Channel numbers 0-39 are EM, 40-48 are hadronic.
+   const uint32_t chanNumber = ( payload[7]       ) & 0xff;
+   const uint32_t fpgaNumber = ( payload[7] >>  8 ) &  0x3;
+   const uint32_t errorBits  = ( payload[7] >> 28 ) &  0xf;
+   if ((int)chanNumber >= EfexDefs::numInputFibresPerFpga()) {
+      std::cerr << "L1CaloBsDecoderRun3::decodeEfexDataChan: invalid channel " << chanNumber
+                << " (out of range 0-" << EfexDefs::numInputFibresPerFpga()-1 << ")" << std::endl;
+      return 0xffffffff;
+   }
+   
+   // The EfexCellMapping returns global coordinates but the RDO objects
+   // expect local eta,phi within the module. So prepare these here.
+   // Find local eta,phi from global EfexCellMapping and module offset.
+   // There is no good way of handling the localEta. Below we add one
+   // so the barrel eta has core towers in localEta 1-16 with 0 and 17
+   // being core towers on C and A sides. Overlap towers extend further
+   // and both localEta and localPhi may be negative.
+   int moduleEta = -24 + (efexNumber % 3) * 16;
+   int modulePhi =  2 + (shelfNumber * 32) + (efexNumber / 3) * 8;
+   
+   // The fibre packer decoder methods expect a vector of seven words.
+   std::vector<FibrePackerBase::myDataWord> encodedData;
+   for ( size_t i = 0; i < 7; i++ ) {
+      encodedData.push_back( payload[i] );
+   }
+   const FibrePackerBase::InputDataFrameType frameType( FibrePackerBase::InputDataFrameType::Normal );
+   
+   if (chanNumber < 40) {
+      // Each EM fibre has 10 supercells for each of two 0.1*0.1 trigger towers.
+      // The EfexLatomeFibrePacker returns the complete set of 20 supercells.
+      EfexLatomeFibrePacker packer;
+      std::vector<FibrePackerBase::myDataWord> cells = packer.getUnpackedData( encodedData, frameType );
+      
+      // Create two tower RDOs, each containing half the supercells.
+      for ( size_t k = 0; k < 2; k++ ) {
+         std::vector<uint32_t> towerCells( 10, 0 );
+         uint32_t towerSumEt( 0 );
+         for (size_t i = 0; i < 10; i++) {
+            towerCells[i] = cells[i+k*10];
+            towerSumEt += towerCells[i];
+         }
+         // **FIXME** Fibre packer does not yet return quality bits!
+         //           There should be one per middle layer supercell.
+         //           We can OR those with error bits for this block.
+         //           TEMP: for the moment we only have the block bits.
+         const uint32_t towerFlag = errorMask;
+         
+         if ( towerSumEt || towerFlag ) {
+            const uint32_t wordNumber = k * 10;  // Only need the first of 10 supercells
+            EfexCellMapping mapping( shelfNumber, efexNumber, fpgaNumber, chanNumber, wordNumber );
+            L1CaloDetectorRegion region = mapping.getDetectorRegion();
+            EfexHardwareInfo hwInfo( mapping.getHardwareInfo() );
+            if ( region.getValidity() && hwInfo.getValidity() ) {
+               int localEta = region.getEtaIndex() - moduleEta + 1;
+               int localPhi = region.getPhiIndex() - modulePhi;
+
+               const uint32_t layer = 0; // EM
+               L1CaloRdoEfexTower newOne( shelfNumber, efexNumber, localEta, localPhi, layer, region );
+               newOne.setRodInfo( rodInfo );
+               L1CaloRdoEfexTower& rdo = L1CaloBsDecoderUtil::findRdo( newOne, tower );
+
+               rdo.setHardwareInfo( fpgaNumber, chanNumber, wordNumber,
+                                    hwInfo.getMpodNumber(), hwInfo.getFibreNumber() );
+               rdo.setValue( towerSumEt );
+               rdo.setFlag( towerFlag );
+               rdo.setSupercells( towerCells );
+               if ( m_verbosity > 0 )
+               {
+                  std::cout << "L1CaloBsDecoderRun3::decodeEfexDataChan: EM"
+                            << ", shelf=" << shelfNumber << ", module=" << efexNumber
+                            << std::hex << ", Et=0x" << towerSumEt << ", flag=0x" << towerFlag
+                            << std::dec << std::endl;
+               }
+            }
+         }
+      }
+   }
+      
+   else {
+      // Each hadronic fibre has a 4*4 array of 16 towers.
+      EfexTrexFibrePacker packer;
+      std::vector<FibrePackerBase::myDataWord> towerVals = packer.getUnpackedData( encodedData, frameType );
+      size_t numHadTowers = std::min(towerVals.size(),(size_t)16);
+      
+      // Create an RDO for each tower.
+      for ( size_t wordNumber = 0; wordNumber < numHadTowers; wordNumber++ ) {
+         // At least for the start of Run 3 the tower Et is still limited to 8 bits
+         // as in Runs 1 & 2. After the legacy CP/JEP system is decommissioned this
+         // may change to enable use of the full 10 bit field.
+         const uint32_t towerEt = ( towerVals[wordNumber] == 0x3fe ) ? 0 : towerVals[wordNumber] & 0xff;
+         const uint32_t invalid = ( towerVals[wordNumber] == 0x3fe ) ? 1 : 0;
+         const uint32_t towerFlag = errorMask | ( invalid << 31 );
+         
+         if ( towerEt || towerFlag ) {
+            EfexCellMapping mapping( shelfNumber, efexNumber, fpgaNumber, chanNumber, wordNumber );
+            L1CaloDetectorRegion region = mapping.getDetectorRegion();
+            EfexHardwareInfo hwInfo( mapping.getHardwareInfo() );
+            if ( region.getValidity() && hwInfo.getValidity() ) {
+               int localEta = region.getEtaIndex() - moduleEta + 1;
+               int localPhi = region.getPhiIndex() - modulePhi;
+
+               const uint32_t layer = 1; // Hadronic
+               L1CaloRdoEfexTower newOne( shelfNumber, efexNumber, localEta, localPhi, layer, region );
+               newOne.setRodInfo( rodInfo );
+               L1CaloRdoEfexTower& rdo = L1CaloBsDecoderUtil::findRdo( newOne, tower );
+
+               rdo.setHardwareInfo( fpgaNumber, chanNumber, wordNumber,
+                                    hwInfo.getMpodNumber(), hwInfo.getFibreNumber() );
+               rdo.setValue( towerEt );
+               rdo.setFlag( towerFlag );
+               if ( m_verbosity > 0 )
+               {
+                  std::cout << "L1CaloBsDecoderRun3::decodeEfexDataChan: Had"
+                            << ", shelf=" << shelfNumber << ", module=" << efexNumber
+                            << std::hex << ", Et=0x" << towerEt << ", flag=0x" << towerFlag
+                            << std::dec << std::endl;
+               }
+            }
+         }
+      }
+   }
+   
+   return errorBits;
+}
+#endif // OFFLINE
+/*!
+ * Decode eFEX TOBs and XTOBs.
+ * The RDO value encodes the cluster Et in the value field
+ * and the isolation bits and other information in the flag.
+ * \param beg pointer to start of rod data payload
+ * \param end pointer to end of rod data payload
+ * \param tob list of RDO to be filled
+ * \param rodInfo iterator to ROD information for this block
+ */
+void
+L1CaloBsDecoderRun3::decodeEfexTobs( const uint32_t* beg, const uint32_t* end,
+                                     std::list<L1CaloRdoEfexTob>& tob,
+                                     std::list<L1CaloRdoRodInfo>::const_iterator rodInfo )
+{
+   const uint32_t* payload( beg );
+   const size_t fragmentSize = end - beg;
+   
+   // The data block is optimised for production by firmware as a set
+   // of nested blocks with the lengths at the end. So we need to find
+   // the end and work backwards.
+
+   if ( fragmentSize < 2 ) {
+      std::cerr << "L1CaloBsDecoderRun3::decodeEfexTobs: fragment size " << fragmentSize
+                << " is too small for the ROD trailer" << std::endl;
+      return;
+   }
+   
+   size_t index = fragmentSize;
+   const uint32_t rodTrailer2 = payload[--index];
+   const uint32_t rodTrailer1 = payload[--index];
+   
+   const uint32_t rodErrors = rodTrailer2 & 0x7f;
+   const size_t payloadSize = rodTrailer1 & 0xffff;
+   if ( (payloadSize + 2) != fragmentSize ) {
+      // Actual ROD fragment payload size does not match that claimed in the trailer.
+      std::cerr << "L1CaloBsDecoderRun3::decodeEfexTobs: payload size " << payloadSize
+                << " inconsistent with ROD fragment size " << fragmentSize << std::endl;
+      return;
+   }
+   
+   // Loop looking backwards for eFEX module blocks.
+   while ( index > 0 ) {
+      if ( index < 2 ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeEfexTobs: remaining block size " << index
+                   << " is too small for the eFEX trailer" << std::endl;
+         return;
+      }
+      size_t efexIndex = index;
+      const uint32_t efexTrailer2 = payload[--efexIndex];
+      const uint32_t efexTrailer1 = payload[--efexIndex];
+      const size_t efexBlockSize = efexTrailer1 & 0xfff;
+      const uint32_t efexNumber  = (efexTrailer1 >> 12) & 0xf;
+      const uint32_t shelfNumber = (efexTrailer1 >> 16) & 0x1;   // 4 bit field, but only 1 but used
+      //??const uint32_t procErrMap  = (efexTrailer1 >> 20) & 0xf;  // Currently unused
+      const uint32_t numSlices   = (efexTrailer1 >> 24) & 0xf;
+      //??const uint32_t l1aSlice    = (efexTrailer1 >> 28) & 0xf;  // Currently unused
+      const uint32_t efexErrors  = efexTrailer2 & 0x3f;
+      if ( efexBlockSize > index ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeEfexTobs: eFEX block size " << efexBlockSize
+                   << " exceeds remaining data size " << index
+                   << " (shelf " << shelfNumber << " eFEX " << efexNumber << ")" << std::endl;
+         return;
+      }
+      // Update index to previous eFEX block (if any).
+      index = efexIndex - efexBlockSize;
+      
+      // Combine rod and efex error fields.
+      const uint32_t errorMask = efexErrors | (rodErrors << 6);
+      
+      // Loop looking backwards for eFEX processor blocks.
+      // There should be one block per slice per FPGA
+      // (except for errors giving corrective trailers).
+      while ( efexIndex > index ) {
+         if ( (efexIndex - index) < 2 ) {
+            std::cerr << "L1CaloBsDecoderRun3::decodeEfexTobs: remaining eFEX block size "
+                      << (efexIndex - index)
+                      << " is too small for the eFEX slice trailer" << std::endl;
+            return;
+         }
+         size_t procIndex = efexIndex;
+         bool ret = this->decodeEfexTobSlice ( payload, procIndex, efexNumber, shelfNumber,
+                                               numSlices, errorMask, tob, rodInfo );
+         if ( ! ret ) {
+            return;
+         }
+         efexIndex = procIndex;
+      }
+   }
+}
+
+/*!
+ * Decode one eFEX FPGA block of TOBs and XTOBs for one slice.
+ * \param payload entire ROD fragment payload vector
+ * \param index just past end of block for this slice
+ *        (this method updates it to the start of the block)
+ * \param efexNumber number of this eFEX in its shelf
+ * \param shelfNumber shelf number
+ * \param numSlices number of TOB slices read out in this event
+ * \param errorMask global error bits set for this ROD fragment
+ * \param tob list of RDO to be filled
+ * \return whether decoding succeeded (false to abort decoding
+           in which case the returned index is not valid)
+ */
+bool
+L1CaloBsDecoderRun3::decodeEfexTobSlice( const uint32_t payload[], size_t& index,
+                                         const uint32_t efexNumber, const uint32_t shelfNumber,
+                                         const uint32_t numSlices, const uint32_t errorMask,
+                                         std::list<L1CaloRdoEfexTob>& tob,
+                                         std::list<L1CaloRdoRodInfo>::const_iterator rodInfo )
+{
+   if ( index < 1 ) {
+      return false;
+   }
+   
+   const uint32_t sliceTrailer = payload[index-1];
+   const uint32_t corrective  = (sliceTrailer >> 31) & 0x01;
+   const uint32_t tauInputErr = (sliceTrailer >> 30) & 0x01;
+   const uint32_t emInputErr  = (sliceTrailer >> 29) & 0x01;
+   
+   if ( corrective ) {
+      // Corrective trailer: no data from this processor FPGA.
+      // Not much we can do so just report it and carry on.
+      size_t blockSize = sliceTrailer & 0xfff;
+      const uint32_t failingBCN  = (sliceTrailer >> 12) & 0xfff;
+      const uint32_t fpgaNumber  = (sliceTrailer >> 24) &   0x3;
+      std::cerr << "L1CaloBsDecoderRun3::decodeEfexTobs: corrective trailer from FPGA "
+                << fpgaNumber << " eFEX " << efexNumber << " shelf " << shelfNumber
+                << ", failing BCN " << failingBCN << ", input errors tau=" << tauInputErr
+                << ", em=" << emInputErr << ", trailer size " << blockSize << std::endl;
+       // corrective trailer blockSize doesn't include the trailer itself (length of one)
+       // and also doesn't include padding word (which occurs if the blockSize is even
+       // because the corrective trailer adds one more word and then a padding word is needed
+      blockSize += (blockSize%2 == 1) ? 1 : 2;
+      index -= blockSize;   // Ought to be 2
+   }
+   else {
+      // October 2021: New format implemented! K.Char in lowest 8 bits.
+      const uint32_t tobType     = (sliceTrailer >>  8) &   0x1;
+      const uint32_t numTobs     = (sliceTrailer >>  9) &   0x7;
+      const uint32_t numEmXtobs  = (sliceTrailer >> 12) &  0x3f;
+      const uint32_t numTauXtobs = (sliceTrailer >> 18) &  0x3f;
+      const uint32_t sliceNum    = (sliceTrailer >> 24) &   0x7;
+      const uint32_t safeMode    = (sliceTrailer >> 27) &   0x1;
+      const uint32_t fpgaNumber  = (sliceTrailer >> 28) &   0x3;
+
+      // Work out how long this block should be. Each TOB is one word,
+      // each XTOB is two words. If the total is odd (including one word
+      // for the trailer) there should be an extra zero padding word.
+      // However in safe mode all the TOBs and XTOBs are suppressed
+      // and we only have the trailer and padding word.
+      size_t blockSize = (safeMode == 0)
+                       ? (1 + numTobs + 2 * (numEmXtobs + numTauXtobs))
+                       : 2;
+      blockSize += (blockSize % 2);
+
+      // Move index to start of this block.
+      index -= blockSize;
+
+      if ( safeMode ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeEfexTobs: safe mode from FPGA "
+                   << fpgaNumber << " eFEX " << efexNumber << " shelf " << shelfNumber
+                   << ", missed " << numTobs << " TOBs, " << numEmXtobs << " EM XTOBs, "
+                   << numTauXtobs << " Tau XTOBs" << std::endl;
+      }
+      else {
+         // Luxury, we can now work forwards inside the slice block!
+         // We expect first TOBs (if any) then EM xTOBs, then Tau xTOBs.
+         const size_t totalTobs = numTobs + numEmXtobs + numTauXtobs;
+         L1CaloRdoFexTob::TobType fexTobType = (tobType == 0)
+                                             ? L1CaloRdoFexTob::TobType::EM
+                                             : L1CaloRdoFexTob::TobType::Tau;
+         L1CaloRdoFexTob::TobSource fexTobSource = L1CaloRdoFexTob::TobSource::EfexTob;
+         size_t sliceIndex = index;
+         for (size_t iTOB = 0; iTOB < totalTobs; iTOB++) {
+            if (iTOB >= numTobs) {
+               fexTobSource = L1CaloRdoFexTob::TobSource::EfexXtob;
+               fexTobType = (iTOB < (numTobs + numEmXtobs))
+                          ? L1CaloRdoFexTob::TobType::EM
+                          : L1CaloRdoFexTob::TobType::Tau;
+            }
+            this->decodeOneEfexTob ( &payload[sliceIndex], shelfNumber, efexNumber, fpgaNumber,
+                                     errorMask, numSlices, sliceNum, fexTobType, fexTobSource,
+                                     tob, rodInfo );
+            // One or two words per TOB or xTOB.
+            sliceIndex += (fexTobSource == L1CaloRdoFexTob::TobSource::EfexTob) ? 1 : 2;
+         }
+      }
+   }
+   return true;
+}
+#ifndef OFFLINE_DECODER
+/*!
+ * Decode jFEX input fibre data.
+ * We create one RDO per tower using the value field for its Et.
+ * The flag field is used for status/error bits.
+ * \param beg pointer to start of rod data payload
+ * \param end pointer to end of rod data payload
+ * \param tower list of RDO to be filled
+ * \param rodInfo iterator to ROD information for this block
+ */
+void
+L1CaloBsDecoderRun3::decodeJfexData( const uint32_t* beg, const uint32_t* end,
+                                     std::list<L1CaloRdoJfexTower>& tower,
+                                     std::list<L1CaloRdoRodInfo>::const_iterator rodInfo )
+{
+   const uint32_t* payload( beg );
+   const size_t fragmentSize = end - beg;
+   
+   // The data block is optimised for production by firmware with the length
+   // in a trailer at the end. The SWROD concatenates a number of such blocks
+   // (removing the 2 word header used to fill the ROD fragment header).
+   // So we need to find the end and work backwards.
+   
+   size_t numTowers = tower.size();
+   size_t index = fragmentSize;
+   
+   // Loop looking backwards for jFEX processor input data blocks.
+   while ( index > 0 ) {
+      if ( index < 2 ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeJfexData: remaining block size " << index
+                   << " is too small for the jFEX FPGA trailer" << std::endl;
+         return;
+      }
+      const uint32_t fpgaTrailer2 = payload[--index];
+      const uint32_t fpgaTrailer1 = payload[--index];
+
+      const uint32_t fpgaErrors  =  fpgaTrailer2 & 0x3f;
+      const uint32_t jfexNumber  = (fpgaTrailer1 >> 20) & 0x7;
+      const uint32_t fpgaNumber  = (fpgaTrailer1 >> 18) & 0x3;
+      const size_t   payloadSize = (fpgaTrailer1 & 0xffff);
+      
+      if ( payloadSize > index ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeJfexData: remaining jFEX block size "
+                   << index << " is too small for the claimed payload size "
+                   << payloadSize << std::endl;
+         return;
+      }
+      
+      // We can now work forwards from the start of this block
+      // decoding each set of 8 words per input fibre channel.
+      index -= payloadSize;
+      size_t chanIndex = 0;
+      while ( chanIndex < payloadSize ) {
+         if ( (payloadSize - chanIndex) < 8 ) {
+            std::cerr << "L1CaloBsDecoderRun3::decodeJfexData: remaining jFEX block size "
+                      << (payloadSize - chanIndex)
+                      << " is too small for one jFEX input fibre block (8)" << std::endl;
+            return;
+         }
+         this->decodeJfexDataChan ( &payload[index+chanIndex], jfexNumber, fpgaNumber,
+                                    fpgaErrors, tower, rodInfo );
+         chanIndex += 8;
+      }
+   }
+   if ( m_verbosity > 0 )
+   {
+      std::cout << "L1CaloBsDecoderRun3::decodeJfexData: n.towers added="
+                << tower.size() - numTowers << std::endl;
+   }
+}
+
+/*!
+ * Decode the data from one jFEX input fibre (only ever one slice).
+ * \param payload payload vector starting at this 8 word block
+ * \param jfexNumber number of this jFEX in its shelf
+ * \param fpgaNumber FPGA number
+ * \param errorMask global error bits set for this ROD fragment
+ * \param tower list of RDO to be filled
+ * \param rodInfo iterator to ROD information for this block
+ * \return whether decoding succeeded (false to abort decoding)
+ */
+uint32_t
+L1CaloBsDecoderRun3::decodeJfexDataChan( const uint32_t payload[],
+                                         const uint32_t jfexNumber, const uint32_t fpgaNumber,
+                                         const uint32_t errorMask,
+                                         std::list<L1CaloRdoJfexTower>& tower,
+                                         std::list<L1CaloRdoRodInfo>::const_iterator rodInfo )
+{
+   // **FIXME** Use 2 for jFEX shelf number as currently generated in mappings.
+   // **FIXME** Its logical crate ID in COOL should be 34 though. Rationalise?!
+   const uint32_t shelfNumber = 2;
+   
+   // The LATOME and TREX fibres have slightly different encoding.
+   // Towards the forward regions and especially FCAL they cover
+   // different numbers of towers with more or less granularity.
+   // Channels 0-23 & 36-59 are standard inputs, 24-35 are overlap.
+   // The HEC overlap minipod (AVR) is #3 of 1-5 in each FPGA.
+   const uint32_t chanNumber = ( payload[7]       ) & 0xff;
+   const uint32_t errorBits  = ( payload[7] >> 28 ) &  0xf;
+   
+   // **FIXME** Ignore overlap for the moment.
+   if (chanNumber >= 24 &&  // **FIXME** Remove this!
+       chanNumber <  36) {  // **FIXME** Remove this!
+     return 0;              // **FIXME** Remove this!
+   }                        // **FIXME** Remove this!
+   // **FIXME** Ignore end jFEX modules for now.
+   if (jfexNumber == 0 ||   // **FIXME** Remove this!
+       jfexNumber == 5) {   // **FIXME** Remove this!
+     return 0;              // **FIXME** Remove this!
+   }                        // **FIXME** Remove this!
+   
+   // The JfexCellMapping returns global coordinates but the RDO objects
+   // expect local eta within the module. So prepare this here.
+   // **FIXME** Not sure how to handle internal eta for FCAL (esp C side).
+   int moduleEta = -24 + (jfexNumber % 6) * 8;
+   
+   // The fibre packer decoder methods expect a vector of seven words.
+   std::vector<FibrePackerBase::myDataWord> encodedData;
+   for ( size_t i = 0; i < 7; i++ ) {
+      encodedData.push_back( payload[i] );
+   }
+   const FibrePackerBase::InputDataFrameType frameType( FibrePackerBase::InputDataFrameType::Normal );
+   
+   // **FIXME** Need mapping to decide when to use Latome vs TREX decoders.
+   // **FIXME** But they are identical apart from handling of saturation
+   // **FIXME** so for the moment always use Latome.
+   
+   bool trex = false;  // **FIXME**
+   
+   // Unpack sixteen 0.1*0.1 trigger towers.
+   std::vector<FibrePackerBase::myDataWord> towers;
+   if (trex) {
+      JfexTrexFibrePacker packer;
+      towers = packer.getUnpackedData( encodedData, frameType );
+   }
+   else {
+      // Unpack sixteen 0.1*0.1 trigger towers.
+      JfexLatomeFibrePacker packer;
+      towers = packer.getUnpackedData( encodedData, frameType );
+   }
+
+   // Create RDO per tower.
+   for ( size_t iTower = 0; iTower < towers.size(); iTower++ ) {
+      const uint32_t towerEt = towers[iTower];
+
+      if ( towerEt || errorMask ) {
+         // The JfexCellMapping needs the "unit number", ie U1-U4 not
+         // the FPGA "processor" number as in the readout. Additionally
+         // the readout uses 0-3 whereas JfexCellMapping expects 1-4.
+         // We also want the fibre number. The (FPGA,chan) JfexCellMapping
+         // constructor returns that (numbered with the whole module).
+         // NB per FPGA minipod (AVR) 1, 2, 4 & 5 are full input data.
+         // Minipod 3 is the overlap one, also used for TTC, IPbus, etc.
+         // The channel (MGT) numbering in the readout is 0-59 which are
+         // the direct fibres. But in firmware and mappings 0-59 are PMA
+         // loopback channels. The direct fibres are actually 60-119.
+         // But for the moment the PMA loopback inputs are not read out
+         // so, except for the mapping lookup, we use 0-59.
+         int unitNumber = JfexDefs::processorNumberToUnitNumber(fpgaNumber+1);
+         JfexHardwareInfo hwInfoFpga( JfexCellMapping( unitNumber, chanNumber+60 ).getHardwareInfo() );
+         int mgtFibreNumber = hwInfoFpga.getFibreNumber();
+         JfexCellMapping mapping( jfexNumber, unitNumber, mgtFibreNumber, iTower );
+         L1CaloDetectorRegion region = mapping.getDetectorRegion();
+         JfexHardwareInfo hwInfo( mapping.getHardwareInfo() );
+         
+         if ( region.getValidity() && hwInfo.getValidity() ) {
+            int layer = (region.getLayer() == L1CaloDetectorRegion::Electromagnetic) ? 0 : 1;
+            int localEta = region.getEtaIndex() - moduleEta;
+            int localPhi = region.getPhiIndex();
+
+            L1CaloRdoJfexTower newOne( shelfNumber, jfexNumber, localEta, localPhi, layer, region );
+            newOne.setRodInfo( rodInfo );
+            L1CaloRdoJfexTower& rdo = L1CaloBsDecoderUtil::findRdo( newOne, tower );
+
+            rdo.setHardwareInfo( fpgaNumber, chanNumber, iTower,
+                                 hwInfo.getAvr(), hwInfo.getFibreNumber() );
+            rdo.setValue( towerEt );
+            rdo.setFlag( errorMask );
+            
+            if ( m_verbosity > 0 )
+            {
+               std::cout << "L1CaloBsDecoderRun3::decodeJfexDataChan: "
+                         << "module=" << jfexNumber
+                         << std::hex << ", Et=0x" << towerEt << ", flag=0x" << errorMask
+                         << std::dec << std::endl;
+            }
+            //std::cout << "L1CaloBsDecoderRun3::addJfexTower: fpga=" << fpgaNumber
+            //          << ", chan=" << chanNumber << ", word=" << iTower
+            //          << ", unit=" << unitNumber
+            //          << ", mpod=" << hwInfo.getAvr()
+            //          << ", modFibre=" << hwInfo.getFibreNumber()
+            //          << ", mgtFibre=" << mgtFibreNumber
+            //          << ", regionEta=" << localEta
+            //          << ", regionPhi=" << localPhi
+            //          << ", layer=" << layer
+            //          << ", phiFpga=" << (localPhi/16)
+            //          << ", hwFpga=" << hwInfo.getFpgaNumber()
+            //          << ", validity=" << region.getValidity()
+            //          << ", Et=" << towerEt
+            //          << ", errors=0x" << std::hex << errorMask << std::dec
+            //          << std::endl;
+         }
+      }
+   }
+   
+   return errorBits;
+}
+
+/*!
+ * Decode jFEX TOBs and XTOBs.
+ * The RDO value encodes the cluster Et in the value field
+ * and the isolation bits and other information in the flag.
+ * \param beg pointer to start of rod data payload
+ * \param end pointer to end of rod data payload
+ * \param tob list of RDO to be filled
+ * \param rodInfo iterator to ROD information for this block
+ */
+void
+L1CaloBsDecoderRun3::decodeJfexTobs( const uint32_t* beg, const uint32_t* end,
+                                     std::list<L1CaloRdoJfexTob>& tob,
+                                     std::list<L1CaloRdoRodInfo>::const_iterator rodInfo )
+{
+   const uint32_t* payload( beg );
+   const size_t fragmentSize = end - beg;
+   
+   // The data block is optimised for production by firmware as a set
+   // of nested blocks with the lengths at the end. So we need to find
+   // the end and work backwards.
+
+   if ( fragmentSize < 2 ) {
+      std::cerr << "L1CaloBsDecoderRun3::decodeJfexTobs: fragment size " << fragmentSize
+                << " is too small for the ROD trailer" << std::endl;
+      return;
+   }
+   
+   size_t index = fragmentSize;
+   const uint32_t rodTrailer2 = payload[--index];
+   const uint32_t rodTrailer1 = payload[--index];
+   
+   const uint32_t rodErrors = rodTrailer2 & 0x7f;
+   const size_t payloadSize = rodTrailer1 & 0xffff;
+   if ( (payloadSize + 2) != fragmentSize ) {
+      // Actual ROD fragment payload size does not match that claimed in the trailer.
+      std::cerr << "L1CaloBsDecoderRun3::decodeJfexTobs: payload size " << payloadSize
+                << " inconsistent with ROD fragment size " << fragmentSize << std::endl;
+      return;
+   }
+   //??const uint32_t rodShelfNumber = (rodTrailer1 >> 18) & 0x3;
+   
+   // Loop looking backwards for jFEX FPGA blocks.
+   while ( index > 0 ) {
+      if ( index < 2 ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeJfexTobs: remaining block size " << index
+                   << " is too small for the jFEX trailer" << std::endl;
+         return;
+      }
+      size_t fpgaIndex = index;
+      const uint32_t fpgaTrailer2 = payload[--fpgaIndex];
+      const uint32_t fpgaTrailer1 = payload[--fpgaIndex];
+      const size_t fpgaBlockSize  = fpgaTrailer1 & 0xffff;
+      const uint32_t jfexNumber   = (fpgaTrailer1 >> 20) & 0x7;
+      const uint32_t fpgaNumber   = (fpgaTrailer1 >> 18) & 0x3;
+      const uint32_t numSlices    = (fpgaTrailer1 >> 24) & 0xf;
+      const uint32_t sliceNumber  = (fpgaTrailer1 >> 28) & 0xf;
+      const uint32_t fpgaErrors   = fpgaTrailer2 & 0x3f;
+      if ( fpgaBlockSize > index ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeJfexTobs: jFEX FPGA block size "
+                   << fpgaBlockSize << " exceeds remaining data size " << index
+                   << " (jFEX " << jfexNumber << " FPGA " << fpgaNumber << ")" << std::endl;
+         return;
+      }
+      // Update index to previous jFEX FPGA block (if any).
+      index = fpgaIndex - fpgaBlockSize;
+      
+      // Combine rod and jfex error fields.
+      const uint32_t errorMask = fpgaErrors | (rodErrors << 6);
+      
+      bool ret = this->decodeJfexTobSlice ( payload, fpgaIndex, jfexNumber, fpgaNumber,
+                                            sliceNumber, numSlices, errorMask, tob, rodInfo );
+      if ( ! ret ) {
+         return;
+      }
+   }
+}
+
+/*!
+ * Decode one jFEX FPGA block of TOBs and XTOBs for one slice.
+ * \param payload entire ROD fragment payload vector
+ * \param index just past end of block for this slice
+ *        (this method updates it to the start of the block)
+ * \param jfexNumber number of this jFEX in its shelf
+ * \param fpgaNumber FPGA number
+ * \param sliceNumber number of this readout slice
+ * \param numSlices total number of TOB slices read out in this event
+ * \param errorMask global error bits set for this ROD fragment
+ * \param tob list of RDO to be filled
+ * \return whether decoding succeeded (false to abort decoding
+           in which case the returned index is not valid)
+ */
+bool
+L1CaloBsDecoderRun3::decodeJfexTobSlice( const uint32_t payload[], size_t& index,
+                                         const uint32_t jfexNumber, const uint32_t fpgaNumber,
+                                         const uint32_t sliceNumber, const uint32_t numSlices,
+                                         const uint32_t errorMask,
+                                         std::list<L1CaloRdoJfexTob>& tob,
+                                         std::list<L1CaloRdoRodInfo>::const_iterator rodInfo )
+{
+   if ( index < 1 ) {
+      return false;
+   }
+   
+   const uint32_t countTrailer1 = payload[index-2];
+   const uint32_t countTrailer2 = payload[index-1];
+   
+   // **FIXME** Not sure if jFEX will send corrective trailers?
+   const uint32_t corrective = 0;
+   if ( corrective ) {
+      // **FIXME** To be implemented if required.
+   }
+   else {
+      const uint32_t safeMode     = (countTrailer1      ) &  0x1;
+      const uint32_t numSJetTobs  = (countTrailer1 >>  1) &  0xf;
+      const uint32_t numLJetTobs  = (countTrailer1 >>  5) &  0xf;
+      const uint32_t numTauTobs   = (countTrailer1 >>  9) &  0xf;
+      const uint32_t numElecTobs  = (countTrailer1 >> 13) &  0xf;
+      const uint32_t numSJetXtobs = (countTrailer2 >>  1) & 0x3f;
+      const uint32_t numLJetXtobs = (countTrailer2 >>  7) & 0x3f;
+      const uint32_t numTauXtobs  = (countTrailer2 >> 13) & 0x3f;
+      const uint32_t numElecXtobs = (countTrailer2 >> 19) & 0x3f;
+
+      // Work out how long this block should be. For jFEX both TOBs
+      // and XTOBs are one word. Apart from the counts of small jets,
+      // large jets, taus and (forward) electrons there are two TOBs
+      // for SumEt and MissingEt.
+      // If the total is odd there should be an extra zero padding word.
+      // However in safe mode all the TOBs and XTOBs are suppressed
+      // and we only have the trailer with the counts.
+      
+      // For ease of later decoding, fill a vector with the end index
+      // of each type of TOB/XTOB word.
+      std::vector<size_t> end;
+      end.push_back( 0 ) ;
+      if ( !safeMode ) {
+         end.push_back( end.back() + numSJetTobs );
+         end.push_back( end.back() + numLJetTobs );
+         end.push_back( end.back() + numTauTobs );
+         end.push_back( end.back() + numElecTobs );
+         end.push_back( end.back() + 1 );  // SumEt
+         end.push_back( end.back() + 1 );  // MissEt
+         end.push_back( end.back() + numSJetXtobs );
+         end.push_back( end.back() + numLJetXtobs );
+         end.push_back( end.back() + numTauXtobs );
+         end.push_back( end.back() + numElecXtobs );
+      }
+      
+      size_t blockSize = end.back();    // End of TOBs/XTOBs
+      blockSize += (blockSize % 2);     // Possible padding
+      blockSize += 2;                   // Two count words
+      
+      size_t numTobs   = (safeMode) ? 0 : end[6];      // End index of MissEt TOBs
+      size_t totalTobs = (safeMode) ? 0 : end.back();  // End index of TOBs+XTOBs
+
+      // Move index to start of this block.
+      index -= blockSize;
+
+      // Luxury, we can now work forwards inside the slice block!
+      size_t sliceIndex = index;
+      L1CaloRdoFexTob::TobType fexTobType;
+      L1CaloRdoFexTob::TobSource fexTobSource;
+      for (size_t iTOB = 0; iTOB < totalTobs; iTOB++) {
+         // Small jets, taus and electron TOBs and XTOBs
+         // all have similar structure so extract common
+         // values and override for other TOB types.
+         // TOBs and xTOBs now have identical formats
+         // so first find the TOB type, then process
+         // the values afterwards.
+         const uint32_t tobWord  = payload[sliceIndex++];
+         uint32_t flagInfo = (tobWord >> 21) & 0xfff;
+         uint32_t etValue  = (tobWord >> 10) & 0x7ff;
+         uint32_t tobPhi   = (tobWord >>  1) &   0xf;
+         uint32_t tobEta   = (tobWord >>  5) &  0x1f;
+         fexTobSource = (iTOB < numTobs)
+                      ? L1CaloRdoFexTob::TobSource::JfexTob
+                      : L1CaloRdoFexTob::TobSource::JfexXtob;
+         if        (iTOB < end[1]) {
+            fexTobType = L1CaloRdoFexTob::TobType::SmallJet;
+            flagInfo = 0;  // None defined
+         } else if (iTOB < end[2]) {
+            fexTobType = L1CaloRdoFexTob::TobType::LargeJet;
+         } else if (iTOB < end[3]) {
+            fexTobType = L1CaloRdoFexTob::TobType::Tau;
+         } else if (iTOB < end[4]) {
+            fexTobType = L1CaloRdoFexTob::TobType::EM;
+         } else if (iTOB < end[6]) {
+            fexTobType = L1CaloRdoFexTob::TobType::Energy;
+         } else if (iTOB < end[7]) {
+            fexTobType = L1CaloRdoFexTob::TobType::SmallJet;
+         } else if (iTOB < end[8]) {
+            fexTobType = L1CaloRdoFexTob::TobType::LargeJet;
+            flagInfo = (tobWord      ) &  0x3ff;
+            etValue  = (tobWord >> 10) & 0x1fff;
+         } else if (iTOB < end[9]) {
+            fexTobType = L1CaloRdoFexTob::TobType::Tau;
+         } else if (iTOB < end[10]) {
+            fexTobType = L1CaloRdoFexTob::TobType::EM;
+         } else {
+            // Padding word: skip.
+            continue;
+         }
+         
+         if        (fexTobType == L1CaloRdoFexTob::TobType::SmallJet) {
+            flagInfo = 0;  // None defined
+         } else if (fexTobType == L1CaloRdoFexTob::TobType::LargeJet) {
+            flagInfo = 0;  // None defined
+            etValue  = (tobWord >> 10) & 0x1fff;
+         } else if (fexTobType == L1CaloRdoFexTob::TobType::Energy) {
+            // **FIXME** SumEt and MissEt to be implemented!
+            flagInfo = (tobWord & 0x1) | (tobWord & 0x1000) >> 15;
+            etValue  = (tobWord & 0x7ffffffe) >> 1;
+            tobPhi = 0;
+            tobEta = 0;
+         }
+
+         // **FIXME** REMOVE THIS TEMPORARY HACK ASAP!!
+         // **FIXME** Ignore & fix  wrong slice numbers.
+         uint32_t sliceNumberHacked = sliceNumber;
+         if (numSlices == 1 && sliceNumber == 2) {
+           sliceNumberHacked = 0;
+         }
+         //>>if ( sliceNumber >= numSlices ) {
+         if ( sliceNumberHacked >= numSlices ) {
+            std::cerr << "L1CaloBsDecoderRun3::decodeJfexTobs: TOB slice " << sliceNumber
+                      << " exceeds number of slices " << numSlices << " in processor trailer"
+                      << std::endl;
+         }
+         else if ( etValue || flagInfo ) {
+            // Should already be zero suppressed in the readout
+            // but no harm in making doubly sure.
+            // Add the error bits from the jFEX control FPGA (low 6 bits)
+            // and the error bits from the ROD output packet (low 7 bits)
+            // to the isolation bits (shifted up 16 bits).
+            // This probably needs more sophisticated treatment.
+            // **FIXME** Check use of tobEta and adjusted tobPhi by FPGA.
+            const uint32_t modulePhi = tobPhi + 16 * fpgaNumber;
+            const uint32_t flagMask = (flagInfo << 16) | errorMask;
+            const uint32_t shelfNumber = 0x22;   // Hard code to P1 value (34)
+            L1CaloRdoJfexTob newOne( shelfNumber, jfexNumber, tobEta, modulePhi,
+                                     numSlices, fexTobType, fexTobSource );
+            newOne.setRodInfo( rodInfo );
+            L1CaloRdoJfexTob& rdo = L1CaloBsDecoderUtil::findRdo( newOne, tob );
+            //>>rdo.setValue( etValue, sliceNumber );
+            //>>rdo.setFlag( flagMask, sliceNumber );
+            rdo.setValue( etValue, sliceNumberHacked );
+            rdo.setFlag( flagMask, sliceNumberHacked );
+            if ( m_verbosity > 0 )
+            {
+               std::cout << "L1CaloBsDecoderRun3::decodeJfexTobSlice: tobType=" << fexTobType
+                         << ", tobSource=" << fexTobSource << ", slice=" << sliceNumber
+                         << ", shelf=" << shelfNumber << ", module=" << jfexNumber
+                         << ", eta=" << tobEta << ", phi=" << modulePhi
+                         << std::hex << ", Et=0x" << etValue << ", flag=0x" << flagMask
+                         << std::dec << ", numSlices=" << numSlices << std::endl;
+            }
+         }
+      }
+   }
+   return true;
+}
+
+/*!
+ * Decode gFEX input fibre data.
+ * We create one RDO per tower using the value field for its Et.
+ * The flag field is used for status/error bits.
+ * \param beg pointer to start of rod data payload
+ * \param end pointer to end of rod data payload
+ * \param tower list of RDO to be filled
+ * \param rodInfo iterator to ROD information for this block
+ */
+void
+L1CaloBsDecoderRun3::decodeGfexData( const uint32_t* beg, const uint32_t* end,
+                                     std::list<L1CaloRdoGfexTower>& tower,
+                                     std::list<L1CaloRdoRodInfo>::const_iterator rodInfo )
+{
+   const uint32_t* payload( beg );
+   
+   // The data block consists of up to three sections, one per processor FPGA.
+   // Each block starts with a one word header that identifies the FPGA and has
+   // the block length and a few other fields.
+   
+   size_t numTowers = tower.size();
+   
+   // Loop looking backwards for jFEX processor input data blocks.
+   while ( payload < end ) {
+      const uint32_t word = *payload++;
+      
+      // The first word must be the header.
+      
+      const uint32_t fpgaCode = ( word >> 28 ) & 0xf;
+      if ( fpgaCode < 0xa || fpgaCode > 0xc ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeGfexData: invalid FPGA code 0x"
+                   << std::hex << fpgaCode << std::dec << std::endl;
+         return;
+      }
+      const uint32_t fpgaNumber = fpgaCode - 0xa;   // A=0, B=1, C=2
+      const uint32_t headerVer = ( word >> 24 ) & 0xf;
+      const uint32_t headerLen = ( word >> 22 ) & 0x3;
+      if ( headerVer > 1 || headerLen > 1 ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeGfexData: header version " << headerVer
+                   << " or length " << headerLen << " is not yet supported" << std::endl;
+         return;
+      }
+      const uint32_t truncatedFlag = (word >> 12) & 0x1;
+      if ( truncatedFlag ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeGfexData: WARNING data truncated" << std::endl;
+      }
+      const uint32_t numFpgaWords = word & 0xfff;
+      if ( ( numFpgaWords % 7 ) != 0 ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodeGfexData: input data size " << numFpgaWords
+                   << " is not a multiple of 7" << std::endl;
+         return;
+      }
+      const uint32_t numInputFibres = numFpgaWords / 7;
+      
+      // Loop over the input fibres and decode each block of 7 words.
+      // **FIXME** Not sure if gFEX data will have error flags?
+      // **FIXME** For the moment hard code to zero.
+      
+      const uint32_t fpgaErrors = 0;
+      
+      for ( size_t chanNumber = 0; chanNumber < numInputFibres; chanNumber++ ) {
+         // Ignore the spare fibres for the moment.
+         if ( chanNumber < 48 || chanNumber >= 52 ) {
+            this->decodeGfexDataChan ( payload, fpgaNumber, chanNumber,
+                                       fpgaErrors, tower, rodInfo );
+         }
+         payload += 7;
+      }
+   }
+   if ( m_verbosity > 0 )
+   {
+      std::cout << "L1CaloBsDecoderRun3::decodeGfexData: n.towers added="
+                << tower.size() - numTowers << std::endl;
+   }
+}
+
+/*!
+ * Decode the data from one gFEX input fibre (only ever one slice).
+ * \param payload payload vector starting at this 7 word block
+ * \param fpgaNumber FPGA number (0-2 for A-C)
+ * \param errorMask global error bits set for this ROD fragment
+ * \param tower list of RDO to be filled
+ * \param rodInfo iterator to ROD information for this block
+ * \return whether decoding succeeded (false to abort decoding)
+ */
+uint32_t
+L1CaloBsDecoderRun3::decodeGfexDataChan( const uint32_t payload[],
+                                         const uint32_t fpgaNumber,
+                                         const uint32_t chanNumber,
+                                         const uint32_t errorMask,
+                                         std::list<L1CaloRdoGfexTower>& tower,
+                                         std::list<L1CaloRdoRodInfo>::const_iterator rodInfo )
+{
+   // **FIXME** Use 3 for gFEX shelf number as currently generated in mappings.
+   // **FIXME** Its logical crate ID in COOL should be 35 though. Rationalise?!
+   const uint32_t shelfNumber = 3;
+   const uint32_t gfexNumber = 0;   // **CHECK** What is used in COOL/OKS?
+   
+   // Ignore the spare fibres for the moment.
+   if ( chanNumber >= 48 && chanNumber < 52 ) {
+      return 0;
+   }
+   
+   // Ignore FPGA C for the moment (different format).
+   if ( fpgaNumber >= 2 ) {
+      return 0;
+   }
+   
+   // The fibre packer decoder methods expect a vector of seven words.
+   std::vector<FibrePackerBase::myDataWord> encodedData;
+   for ( size_t i = 0; i < 7; i++ ) {
+      encodedData.push_back( payload[i] );
+   }
+   const FibrePackerBase::InputDataFrameType frameType( FibrePackerBase::InputDataFrameType::Normal );
+   
+   // **FIXME** Need mapping to decide when to use Latome vs TREX decoders.
+   // **FIXME** Temporarily assume the first 50 fibres are EM (central Latome)
+   // **FIXME** and second 50 are hadronic (TREX). Surely wrong!!
+   
+   // Unpack up to sixteen 0.1*0.1 trigger towers.
+   // NB central FPGAs only have eight per fibre
+   // but additionally have eight fine positions.
+   // For the moment we ignore the latter.
+   std::vector<FibrePackerBase::myDataWord> towers;
+   size_t numTowers = 8;
+   L1CaloDetectorRegion::LayerTypeEnum layerEnum = L1CaloDetectorRegion::Electromagnetic;
+   if ( chanNumber >= 50 ) {
+      GfexTrexFibrePacker packer;
+      towers = packer.getUnpackedData( encodedData, frameType );
+      layerEnum = L1CaloDetectorRegion::Hadronic;
+   }
+   else {
+      // Unpack sixteen 0.1*0.1 trigger towers.
+      GfexLatomeCentralFibrePacker packer;
+      towers = packer.getUnpackedData( encodedData, frameType );
+   }
+
+   // Create RDO per tower.
+   for ( size_t iTower = 0; iTower < numTowers && iTower < towers.size(); iTower++ ) {
+      int towerEt = towers[iTower];
+
+      if ( towerEt || errorMask ) {
+         // **FIXME** Create temporary mapping information. Probably WRONG!
+         int moduleFibre = chanNumber + fpgaNumber * 100;
+         int localPhi = GfexDefs::phiBinFromFibreSeqNum( moduleFibre, iTower );
+         int localEta = GfexDefs::etaBinFromFibreSeqNum( moduleFibre, iTower );
+         
+         // **FIXME** Create L1CaloDetectorRegion (normally comes from mappings).
+         const bool valid(true);        // **TEMP**
+         const double binWidth = 0.2;   // **TEMP** OK for central region
+         L1CaloDetectorRegion region( L1CaloDetectorRegion::GFEX, layerEnum, valid,
+                                      localEta, localPhi, binWidth, binWidth,
+                                      (localEta * 0.1), (localPhi * 0.1) );
+         
+         if ( region.getValidity() && localPhi >= 0 && localEta >= 0 ) {
+            int layer = (region.getLayer() == L1CaloDetectorRegion::Electromagnetic) ? 0 : 1;
+
+            L1CaloRdoGfexTower newOne( shelfNumber, gfexNumber, localEta, localPhi, layer, region );
+            newOne.setRodInfo( rodInfo );
+            L1CaloRdoGfexTower& rdo = L1CaloBsDecoderUtil::findRdo( newOne, tower );
+
+            // **FIXME** Invent minipod number and fibre in minipod.
+            // **FIXME** Use chanNumber ignoring spare fibres.
+            int fibreInFpga = (chanNumber > 48) ? (chanNumber - 4) : chanNumber;
+            int moduleMinipod = (fibreInFpga / 12) + (fpgaNumber * 4);
+            int fibreInMinipod = fibreInFpga % 12;
+            rdo.setHardwareInfo( fpgaNumber, chanNumber, iTower,
+                                 moduleMinipod, fibreInMinipod );
+            rdo.setValue( towerEt );
+            rdo.setFlag( errorMask );
+            
+            if ( m_verbosity > 0 )
+            {
+               std::cout << "L1CaloBsDecoderRun3::decodeGfexDataChan: "
+                         << "FPGA=" << fpgaNumber
+                         << std::hex << ", Et=0x" << towerEt << ", flag=0x" << errorMask
+                         << std::dec << std::endl;
+            }
+            //std::cout << "L1CaloBsDecoderRun3::decodeGfexDataChan: fpga=" << fpgaNumber
+            //          << ", chan=" << chanNumber << ", word=" << iTower
+            //          << ", mpod=" << moduleMinipod
+            //          << ", mpodFibre=" << fibreInMinipod
+            //          << ", regionEta=" << localEta
+            //          << ", regionPhi=" << localPhi
+            //          << ", layer=" << layer
+            //          << ", Et=" << towerEt
+            //          << ", errors=0x" << std::hex << errorMask << std::dec
+            //          << std::endl;
+         }
+      }
+   }
+   
+   return 0;   // No error bits yet
+}
+
+/*!
+ * Decode gFEX TOBs.
+ * We create one RDO per TOB using the value field for its Et.
+ * The flag field is used for status/error bits.
+ * **FIXME** We also create one RDO for the energy TOBs.
+ * \param beg pointer to start of rod data payload
+ * \param end pointer to end of rod data payload
+ * \param tob list of RDO to be filled
+ * \param rodInfo iterator to ROD information for this block
+ */
+void
+L1CaloBsDecoderRun3::decodeGfexTobs( const uint32_t* beg, const uint32_t* end,
+                                     std::list<L1CaloRdoGfexTob>& tob,
+                                     std::list<L1CaloRdoRodInfo>::const_iterator rodInfo )
+{
+  // **FIXME** To be implemented!
+}
+
+/*!
+ * Decode Ph1Topo input data: these are TOBs from FEXes and MuCTPI.
+ * We create one RDO per TOB using the value field for its Et.
+ * The flag field is used for status/error bits.
+ * \param beg pointer to start of rod data payload
+ * \param end pointer to end of rod data payload
+ * \param etob list of eFEX TOB RDOs to be filled
+ * \param jtob list of jFEX TOB RDOs to be filled
+ * \param gtob list of gFEX TOB RDOs to be filled
+ * \param mtob list of muon TOB RDOs to be filled
+ * \param rodInfo iterator to ROD information for this block
+ */
+void
+L1CaloBsDecoderRun3::decodePh1TopoData( const uint32_t* beg, const uint32_t* end,
+                                        std::list<L1CaloRdoEfexTob>& etob,
+                                        std::list<L1CaloRdoJfexTob>& jtob,
+                                        std::list<L1CaloRdoGfexTob>& gtob,
+                                        std::list<L1CaloRdoMuonTob>& mtob,
+                                        std::list<L1CaloRdoRodInfo>::const_iterator rodInfo )
+{
+   const uint32_t* payload( beg );
+   const size_t fragmentSize = end - beg;
+   
+   // Fibre types of Ph1Topo inputs. These are defined in the VHDL at:
+   // https://gitlab.cern.ch/atlas-l1calo/l1topo/ph1topo/-/blob/master/src/infr/topo_mapping_pkg.vhd#L27
+   // For our purposes we only need to distinguish different formats
+   // and ignore those that do not appear in the readout.
+   enum TopoInputFibreTypes { Unknown=0x00, IPB=0x01, TTC=0x02, EM1=0x03, EM2=0x04,
+                              G1=0x05, G2=0x06, J1=0x07, J2=0x08, JF=0x09, JFXE=0x0a,
+                              M0A=0x0b, M0C=0x0c, M1A=0x0d, M1C=0x0e, M2A=0x0f, M2C=0x10,
+                              TAU1=0x11, TAU2=0x12, gJ1=0x13, gJ2=0x14 };
+   
+   // The data block is optimised for production by firmware with the length
+   // in a trailer at the end. The SWROD concatenates a number of such blocks
+   // (removing the 2 word header used to fill the ROD fragment header).
+   // So we need to find the end and work backwards.
+   
+   //??size_t numTobs = tower.size();
+   size_t index = fragmentSize;
+   
+   // Loop looking backwards for Ph1Topo processor input data blocks.
+   while ( index > 0 ) {
+      if ( index < 2 ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodePh1TopoData: remaining block size " << index
+                   << " is too small for the Ph1Topo FPGA trailer" << std::endl;
+         return;
+      }
+      const uint32_t fpgaTrailer2 = payload[--index];
+      const uint32_t fpgaTrailer1 = payload[--index];
+
+      const uint32_t fpgaErrors  =  fpgaTrailer2 & 0x3f;
+      //??const uint32_t sliceNum    = (fpgaTrailer1 >> 28) & 0xf;
+      //??const uint32_t numSlices   = (fpgaTrailer1 >> 24) & 0xf;
+      const uint32_t sliceNum    = 0;  // **FIXME** Temporarily hard code until set properly
+      const uint32_t numSlices   = 1;  // **FIXME** Temporarily hard code until set properly
+      //>>const uint32_t topoNumber  = (fpgaTrailer1 >> 22) & 0x3;
+      //>>const uint32_t fpgaNumber  = (fpgaTrailer1 >> 21) & 0x1;
+      //>>const uint32_t fwType      = (fpgaTrailer1 >> 19) & 0x3; // 0: Mult, 1: Algo, 2: Beta
+      const size_t   payloadSize = (fpgaTrailer1 & 0xffff);
+      
+      if ( payloadSize > index ) {
+         std::cerr << "L1CaloBsDecoderRun3::decodePh1TopoData: remaining Ph1Topo block size "
+                   << index << " is too small for the claimed payload size "
+                   << payloadSize << std::endl;
+         return;
+      }
+      
+      // We can now work forwards from the start of this block
+      // decoding each set of 8 words per input fibre channel.
+      // In Ph1Topo the input data channels can be TOBs of many
+      // different types. The type is identifed in the 8th word.
+      // We then need the appropriate mapping to give us which
+      // P1 FEX crate and module number is the source.
+      // NB this may be incorrect for test rig setups.
+      index -= payloadSize;
+      size_t chanIndex = 0;
+      uint32_t fexShelf = 0;
+      uint32_t fexModule = 0;
+      uint32_t fexFpga = 0;
+      while ( chanIndex < payloadSize ) {
+         if ( (payloadSize - chanIndex) < 8 ) {
+            std::cerr << "L1CaloBsDecoderRun3::decodePh1TopoData: remaining Ph1Topo block size "
+                      << (payloadSize - chanIndex)
+                      << " is too small for one Ph1Topo input fibre block (8)" << std::endl;
+            return;
+         }
+         //>>const uint32_t chanNumber = ( payload[7]       ) & 0xff;
+         const uint32_t fibreType  = ( payload[7] >>  8 ) & 0x1f;
+         //>>const uint32_t errorBits  = ( payload[7] >> 30 ) &  0x3;
+         
+         L1CaloRdoFexTob::TobType tobType(L1CaloRdoFexTob::TobType::Invalid);
+         L1CaloRdoFexTob::TobSource tobSource(L1CaloRdoFexTob::TobSource::Ph1Topo);
+         
+         // Most fibres have up to six TOBs, but jFEX fibres have seven.
+         size_t numTobs = 6;
+         if (fibreType == TopoInputFibreTypes::J1 || fibreType == TopoInputFibreTypes::JF ||
+             fibreType == TopoInputFibreTypes::J2 || fibreType == TopoInputFibreTypes::JFXE) {
+           numTobs = 7;
+         }
+         
+         for (size_t iTob = 0; iTob < numTobs; iTob++) {
+            // TOBs from eFEX.
+            if (fibreType == TopoInputFibreTypes::EM1 || fibreType == TopoInputFibreTypes::TAU1 ||
+                fibreType == TopoInputFibreTypes::EM2 || fibreType == TopoInputFibreTypes::TAU2) {
+                tobType = (fibreType <= TopoInputFibreTypes::EM2)  // Assumes enum order!
+                        ? L1CaloRdoFexTob::TobType::EM
+                        : L1CaloRdoFexTob::TobType::Tau;
+               // Need mapping!
+               this->decodeOneEfexTob( &payload[index+chanIndex+iTob], fexShelf, fexModule, fexFpga,
+                                       fpgaErrors, numSlices, sliceNum, tobType, tobSource,
+                                       etob, rodInfo );
+            }
+            // Fibres from jFEX.
+            /*
+            else if () {
+            }
+            */
+         }
+         chanIndex += 8;
+      }
+   }
+   //??if ( m_verbosity > 0 )
+   //??{
+   //??   std::cout << "L1CaloBsDecoderRun3::decodeJfexData: n.towers added="
+   //??             << tower.size() - numTowers << std::endl;
+   //??}
+}
+#endif // OFFLINE
+/*!
+ * Decode word(s) for one eFEX TOB (or xTOB) and create one RDO.
+ * **FIXME** Document other parameters when stable!
+ * \param tob list of RDO to be filled
+ * \param rodInfo iterator to ROD information for this block
+ */
+void
+L1CaloBsDecoderRun3::decodeOneEfexTob( const uint32_t word[], const uint32_t shelfNumber,
+                                       const uint32_t efexNumber, const uint32_t/* fpgaNumber*/,
+                                       const uint32_t errorMask,
+                                       const uint32_t numSlices, const uint32_t sliceNum,
+                                       L1CaloRdoFexTob::TobType tobType,
+                                       L1CaloRdoFexTob::TobSource tobSource,
+                                       std::list<L1CaloRdoEfexTob>& tob,
+                                       std::list<L1CaloRdoRodInfo>::const_iterator rodInfo )
+{
+   const uint32_t tobWord  = word[0];
+   const uint32_t isolInfo = (tobWord      ) & 0xffc000;
+   const uint32_t tobPhi   = (tobWord >> 24) &      0x7;
+   const uint32_t tobEta   = (tobWord >> 27) &      0x7;
+   const uint32_t tobFpga  = (tobWord >> 30) &      0x3;
+   
+   uint32_t etValue(0);
+   if (tobSource == L1CaloRdoFexTob::TobSource::EfexXtob) {
+      // XTOB: Et from second TOB word.
+      etValue = word[1] & 0xffff;
+   }
+   else {  // EfexTob or Ph1Topo
+      // TOB: Et from single TOB word.
+      etValue = tobWord & 0xfff;
+   }
+   
+   if ( sliceNum >= numSlices ) {
+       ERS_ERROR("L1CaloBsDecoderRun3::decodeOneEfexTob: TOB slice " << sliceNum
+                << " exceeds number of slices " << numSlices << " in processor trailer");
+   }
+   else if ( etValue ) {
+      // Should already be zero suppressed in the readout
+      // but no harm in making doubly sure.
+      // Add the error bits from the eFEX control FPGA (low 6 bits)
+      // and the error bits from the ROD output packet (low 7 bits)
+      // to the isolation bits (which have the low 14 bits unused).
+      // This probably needs more sophisticated treatment.
+      // The internal eta within one FPGA is in the range 0-5
+      // where 1-4 are the standard values and 0 & 5 are only
+      // used at the extreme eta, ie |eta| > 2.4.
+      // To get an eta within the module we use the range 0-17
+      // where 0 & 17 are only used at the extremes.
+      const uint32_t moduleEta = 1 + (tobEta - 1) + 4 * tobFpga;
+      const uint32_t flagMask = isolInfo | errorMask;
+      L1CaloRdoEfexTob newOne( shelfNumber, efexNumber, moduleEta, tobPhi,
+                               numSlices, tobType, tobSource );
+      newOne.setRodInfo( rodInfo );
+      L1CaloRdoEfexTob& rdo = L1CaloBsDecoderUtil::findRdo( newOne, tob );
+      rdo.setValue( etValue, sliceNum );
+      rdo.setFlag( flagMask, sliceNum );
+#ifdef OFFLINE_DECODER
+      // store the raw words - used in offline to construct the EDM objects
+       rdo.setWord0( word[0], sliceNum );
+      if(tobSource == L1CaloRdoFexTob::TobSource::EfexXtob) rdo.setWord1(word[1], sliceNum);
+#endif
+      if ( m_verbosity > 0 )
+      {
+         std::cout << "L1CaloBsDecoderRun3::decodeOneEfexTob: tobType=" << tobType
+                   << ", tobSource=" << tobSource << ", slice=" << sliceNum
+                   << ", shelf=" << shelfNumber << ", module=" << efexNumber
+                   << ", eta=" << moduleEta << ", phi=" << tobPhi
+                   << std::hex << ", Et=0x" << etValue << ", flag=0x" << flagMask
+                   << std::dec << ", numSlices=" << numSlices << std::endl;
+      }
+   }
+}
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloBsDecoderUtil.cxx b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloBsDecoderUtil.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..d86bf9747e616bb638fa0607ef1f67492eeb1c0a
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloBsDecoderUtil.cxx
@@ -0,0 +1,80 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+#include <iostream>
+#include <iomanip>
+
+#include "eformat/ROBFragment.h"
+
+#include "bytestreamDecoder/L1CaloBsDecoderUtil.h"
+#include "bytestreamDecoder/L1CaloRdoRodInfo.h"
+
+/*!
+ * \class L1CaloBsDecoderUtil
+ * Utility methods for bytestream decoder classes for runs 1, 2 and 3.
+ */
+
+L1CaloBsDecoderUtil::L1CaloBsDecoderUtil()
+{
+}
+
+void
+L1CaloBsDecoderUtil::decodeRodInfo( const eformat::ROBFragment<const uint32_t*>* rod,
+                                    std::list<L1CaloRdoRodInfo>& dat )
+{
+   // Create the basic object
+   int sourceId  = rod->rod_source_id() & 0xffffff;
+   int runNumber = rod->rod_run_no();
+   int bcNumber  = rod->rod_bc_id();
+   int triggerType  = rod->rod_lvl1_trigger_type();
+   int detEventType = rod->rod_detev_type();
+   int version = rod->rod_version();
+//<<   // **FIXME** Hack for testing Run 3: set rod version
+//<<   version |= 0x1004;   // **FIXME** Remove this!
+   L1CaloRdoRodInfo rdo( sourceId>>16, sourceId&0xffff, runNumber, bcNumber, triggerType, detEventType, version );
+
+   // Add in the Level-1 ID and size
+   rdo.setLvl1Id( rod->rod_lvl1_id() );
+   rdo.setSize( rod->rod_ndata() );
+   
+   // Add in the status words
+   const uint32_t* status;
+   rod->rod_status(status);
+   int stat = *status++;
+   rdo.setStatus1( stat );
+   stat = *status;
+   rdo.setStatus2( stat );
+   
+   // Now add in the module status words in the payload, if not RoI or CTP
+   bool hasModuleStatus = true;
+   if ( (sourceId>>16) > 0x74 )
+      hasModuleStatus = false;
+   if ( (sourceId>>16) == 0x73 )
+      hasModuleStatus = false;
+      
+   if ( hasModuleStatus )
+   {
+      const uint32_t* it_data;
+      rod->rod_data( it_data );
+      const uint32_t ndata = rod->rod_ndata();
+      ++it_data;
+      for ( uint32_t i = 1; i < ndata; ++i, ++it_data ) {
+         if ( ((*it_data)>>28) == 0xd )
+         {
+            int idat = (*it_data)&0xfff;
+            int module = idat>>8;
+            int modStat = rdo.getModuleStatus( module );
+            rdo.setModuleStatus( module, modStat | (idat&0xff) );
+         }
+         if ( ((*it_data)>>28) == 0xf )
+         {
+            int idat = (*it_data)&0x1ff;
+            int module = idat>>8;
+            int modStat = rdo.getModuleStatus( module );
+            rdo.setModuleStatus( module, modStat | (idat&0xff) );
+         }
+      }
+   }
+   
+   dat.push_back( rdo );
+}
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloRdo.cxx b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloRdo.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..c66ee885ed9917fb788c676414bd728a4bb0022b
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloRdo.cxx
@@ -0,0 +1,290 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+#include "bytestreamDecoder/L1CaloRdo.h"
+
+#include "bytestreamDecoder/L1CaloRdoRodInfo.h"
+
+#include <iostream>
+
+/*!
+ * \class L1CaloRdo
+   Abstract base class for L1Calo "Raw Data Objects" (RDOs).
+   Each RDO represents some information associated with an eta,phi,layer
+   for the complete set of readout time slices (bunch crossings).
+   Different coordinates imply different RDOs, but all timeslices for
+   the same coordinate are always kept in the same RDO.
+   For each RDO there is an integer value and flag per timeslice.
+   What this means is up to the subclasses.
+   The value may be an Et of towers or jet elements, threshold
+   multiplicities or hit bits indicating which thresholds were passed.
+   Sometimes the value is bit coded to pack two values into one word.
+   The flags are generally error bits, but in some cases other values
+   are bit packed into the flags.
+   Where either field is bit packed the subclass should have specific
+   methods to decode them.
+ */
+
+L1CaloRdo::L1CaloRdo(  int crate, int module, int eta, int phi, 
+                       int layer, int numSlices )
+: m_crate( crate )
+, m_module( module )
+, m_eta( eta )
+, m_phi( phi )
+, m_layer( layer )
+, m_vals( numSlices, 0 )
+, m_flags( numSlices, 0 )
+, m_l1aPos( numSlices/2 )
+#ifdef OFFLINE_DECODER
+, m_word0s(numSlices,0),m_word1s(numSlices,0)
+#endif
+{
+   // std::cout << "Object Created" << std::endl;
+}
+
+L1CaloRdo::~L1CaloRdo( )
+{
+   // std::cout << "Object Deleted" << std::endl;
+}
+
+bool
+operator<( const L1CaloRdo& lhs, const L1CaloRdo& rhs )
+{
+   if ( lhs.getCrate() < rhs.getCrate() )
+      return true;
+   if ( lhs.getCrate() > rhs.getCrate() )
+      return false;
+   if ( lhs.getModule() < rhs.getModule() )
+      return true;
+   if ( lhs.getModule() > rhs.getModule() )
+      return false;
+   if ( lhs.getEta() < rhs.getEta() )
+      return true;
+   if ( lhs.getEta() > rhs.getEta() )
+      return false;
+   if ( lhs.getPhi() < rhs.getPhi() )
+      return true;
+   if ( lhs.getPhi() > rhs.getPhi() )
+      return false;
+   if ( lhs.getLayer() < rhs.getLayer() )
+      return true;
+   return false;
+}
+
+int 
+L1CaloRdo::getCrate( ) const
+{
+   return m_crate;
+}
+
+int 
+L1CaloRdo::getModule( ) const
+{
+   return m_module;
+}
+
+int 
+L1CaloRdo::getEta( ) const
+{
+   return m_eta;
+}
+
+int 
+L1CaloRdo::getPhi( ) const
+{
+   return m_phi;
+}
+
+int 
+L1CaloRdo::getLayer( ) const
+{
+   return m_layer;
+}
+
+int 
+L1CaloRdo::getValue( size_t slice ) const
+{
+   if ( slice < m_vals.size() )
+      return m_vals[slice];
+   return 0;
+}
+
+int 
+L1CaloRdo::getValue( ) const
+{
+   return getValue( getL1aPos() );
+}
+
+int 
+L1CaloRdo::getFlag( size_t slice ) const
+{
+   if ( slice < m_flags.size() )
+      return m_flags[slice];
+   return 0;
+}
+
+int 
+L1CaloRdo::getFlag( ) const
+{
+   return getFlag( getL1aPos() );
+}
+
+int
+L1CaloRdo::getL1aPos( ) const
+{
+   return m_l1aPos;
+}
+#ifndef OFFLINE_DECODER
+L1CaloDetectorRegion 
+L1CaloRdo::getRegion( ) const
+{
+   return m_region;
+}
+#endif
+
+const std::list<L1CaloRdoRodInfo>::const_iterator&
+L1CaloRdo::getRodInfo( ) const
+{
+   return m_rodInfo;
+}
+
+int
+L1CaloRdo::getModuleStatus( ) const
+{
+   return m_rodInfo->getModuleStatus( getModule() );
+}
+
+bool
+L1CaloRdo::getModuleErrorGlinkParity( ) const
+{
+   return getModuleStatus( ) & 0x01 ;
+}
+
+bool
+L1CaloRdo::getModuleErrorGlinkProtocol( ) const
+{
+   return getModuleStatus( ) & 0x02 ;
+}
+
+bool
+L1CaloRdo::getModuleErrorBcnMismatch( ) const
+{
+   return getModuleStatus( ) & 0x04 ;
+}
+
+bool
+L1CaloRdo::getModuleErrorFifoOverflow( ) const
+{
+   return getModuleStatus( ) & 0x08 ;
+}
+
+bool
+L1CaloRdo::getModuleErrorSpecific( ) const
+{
+   return getModuleStatus( ) & 0x10 ;
+}
+
+bool
+L1CaloRdo::getModuleErrorUnused( ) const
+{
+   return getModuleStatus( ) & 0x20 ;
+}
+
+bool
+L1CaloRdo::getModuleErrorGlinkTimeout( ) const
+{
+   return getModuleStatus( ) & 0x40 ;
+}
+
+bool
+L1CaloRdo::getModuleErrorGlinkDown( ) const
+{
+   return getModuleStatus( ) & 0x80 ;
+}
+
+size_t
+L1CaloRdo::numSlices( ) const
+{
+   return m_vals.size();
+}
+
+bool
+L1CaloRdo::sameDatum( const L1CaloRdo& rhs ) const
+{
+   if ( m_crate != rhs.m_crate )
+      return false;
+   if ( m_module != rhs.m_module )
+      return false;
+   if ( m_eta != rhs.m_eta )
+      return false;
+   if ( m_phi != rhs.m_phi )
+      return false;
+   if ( m_layer != rhs.m_layer )
+      return false;
+   return true;
+}
+
+void
+L1CaloRdo::setValue( int val, size_t slice ) 
+{
+   if ( slice < m_vals.size() )
+      m_vals[slice] = val;
+}
+
+void
+L1CaloRdo::setValue( int val ) 
+{
+   setValue( val, getL1aPos() );
+}
+
+void
+L1CaloRdo::setFlag( int flag, size_t slice ) 
+{
+   if ( slice < m_flags.size() )
+      m_flags[slice] = flag;
+}
+
+void
+L1CaloRdo::setFlag( int flag ) 
+{
+   setFlag( flag, getL1aPos() );
+}
+
+void
+L1CaloRdo::setRodInfo( std::list<L1CaloRdoRodInfo>::const_iterator& rodInfo )
+{
+   m_rodInfo = rodInfo;
+}
+
+void 
+L1CaloRdo::info( ) const
+{
+   std::cout << "RDO Object Type: " << getType()
+             << "  Crate: " << getCrate()
+             << "  Module: " << getModule()
+	     << "  Eta: " << getEta()
+	     << "  Phi: " << getPhi()
+	     << "  Layer: " << getLayer() << std::endl;
+   std::cout << "      Values:  " << std::hex;
+   for ( size_t i = 0 ; i < m_vals.size() ; ++i )
+      std::cout << getValue(i) << " ";
+   std::cout << "      Flags:  ";
+   for ( size_t i = 0 ; i < m_flags.size() ; ++i )
+      std::cout << getFlag(i) << " ";
+   std::cout << std::endl << std::dec;
+   infoSpecific();
+}
+
+void 
+L1CaloRdo::infoSpecific( ) const
+{
+}
+
+#ifndef OFFLINE_DECODER
+void
+L1CaloRdo::setRegion( const L1CaloDetectorRegion& region )
+{
+   m_region = region;
+}
+#endif
+
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloRdoEfexTob.cxx b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloRdoEfexTob.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..7608ff56f56c208364f27839741d4db0c9db969c
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloRdoEfexTob.cxx
@@ -0,0 +1,89 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+#include "bytestreamDecoder/L1CaloRdoEfexTob.h"
+
+#include <iomanip>
+#include <iostream>
+
+//>>#include "channelMappings/CpmTowerMapping.h"   // Use Efex mapping!!
+
+/*!
+  \class L1CaloRdoEfexTob
+  L1CaloRdoFexTob subclass for eFEX EM/Tau TOBs or XTOBs.
+  These may come from eFEX or Ph1Topo inputs.
+  The available flags are different in these two cases.
+  The RDO object has two 32 bit words: value and flag.
+  The TOBs and XTOBs differ mainly in the number of bits
+  used for the Et value (12 vs 16). We store the Et in
+  the value word and everything else in the flag keeping
+  all the fields in their location in the hardware format.
+  
+  The id, fibre and tobSeq values were used in Run 2 for topo
+  and for distinguishing different sources of the same TOB
+  (eg here from FEX readout vs Ph1Topo readout perhaps).
+ */
+
+
+L1CaloRdoEfexTob::L1CaloRdoEfexTob( int crate, int module, int eta, int phi, int numSlices,
+                                        TobType tobType, TobSource source, int id, int fibre, int tobSeq ) 
+: L1CaloRdoFexTob( crate, module, eta, phi, numSlices, tobType, source, id, fibre, tobSeq )
+{
+   //??CpmTowerMapping map( crate, module, eta, phi );
+   //??setRegion( map.getL1CaloDetectorRegion() );
+}
+
+void
+L1CaloRdoEfexTob::infoSpecific( ) const
+{
+   std::cout << "      Isolation:  " << std::hex;
+   for ( size_t s = 0 ; s < numSlices() ; ++s )
+   {
+      std::cout << std::setw(2) << getIsol( s ) << " ";
+   }
+   std::cout << std::dec << std::endl;
+}
+
+/*
+bool 
+L1CaloRdoEfexTob::getOverflow( size_t slice ) const
+{
+   return ( getFlag( slice ) & 0x10 );  // **FIXME**
+}
+
+bool 
+L1CaloRdoEfexTob::getOverflow( ) const
+{
+   return getOverflow( getL1aPos() );
+}
+*/
+
+/*!
+  Return 12 bit (TOB) or 16 bit (XTOB) cluster Et (from the RDO value).
+ */
+int 
+L1CaloRdoEfexTob::getClusterEt( size_t slice ) const
+{
+   return ( getValue( slice ) & 0xffff );
+}
+
+int 
+L1CaloRdoEfexTob::getClusterEt( ) const
+{
+   return getClusterEt( getL1aPos() );
+}
+
+/*!
+  Return three 2 bit isolation hits (packed into bits 18-23 of the RDO flag).
+ */
+int 
+L1CaloRdoEfexTob::getIsol( size_t slice ) const
+{
+   return ( ( getFlag( slice ) >> 18 ) & 0x3f );
+}
+
+int 
+L1CaloRdoEfexTob::getIsol( ) const
+{
+   return getIsol( getL1aPos() );
+}
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloRdoFexTob.cxx b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloRdoFexTob.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..544105f590db1d95b7b0e2180844598a16a798f5
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloRdoFexTob.cxx
@@ -0,0 +1,216 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+#include "bytestreamDecoder/L1CaloRdoFexTob.h"
+
+#include <iostream>
+#include <sstream>
+
+/*!
+  \class L1CaloRdoFexTob
+  L1CaloRdo subclass for FEX Trigger Objects (TOBs and XTOBs).
+  These may come from FEX or phase 1 Topo readout.
+ */
+
+
+L1CaloRdoFexTob::L1CaloRdoFexTob( int crate, int module, int eta, int phi, int numSlices,
+                                  L1CaloRdoFexTob::TobType tobType, L1CaloRdoFexTob::TobSource source,
+                                  int id, int fibre, int tobSeq )
+: L1CaloRdo( crate, module, eta, phi, 0, numSlices )
+, m_tobType( tobType )
+, m_source( source )
+, m_id( id )
+, m_fibre( fibre )
+, m_tobSeq( tobSeq )
+{
+}
+
+std::string
+L1CaloRdoFexTob::getType( ) const
+{
+   return getTobTypeString() + std::string( "Tob:" ) + getTobSourceString();
+}
+
+bool
+L1CaloRdoFexTob::sameDatum( const L1CaloRdoFexTob& rhs ) const
+{
+   if ( m_tobType != rhs.m_tobType )
+      return false;
+   if ( m_source != rhs.m_source )
+      return false;
+   if ( m_id != rhs.m_id )
+      return false;
+   return L1CaloRdo::sameDatum( rhs );
+}
+
+L1CaloRdoFexTob::TobType
+L1CaloRdoFexTob::getTobType( ) const
+{
+   return m_tobType;
+}
+
+std::string
+L1CaloRdoFexTob::getTobTypeString( ) const
+{
+   if      ( getIsEM() )       { return "EM"; }
+   else if ( getIsTau() )      { return "Tau"; }
+   else if ( getIsLargeJet() ) { return "LJet"; }
+   else if ( getIsSmallJet() ) { return "SJet"; }
+   else if ( getIsEnergy() )   { return "Energy"; }
+   else if ( getIsMuon() )     { return "Muon"; }
+   else                        { return ""; }
+}
+
+int
+L1CaloRdoFexTob::getTobID( ) const
+{
+   return m_id;
+}
+
+bool
+L1CaloRdoFexTob::getIsEM( ) const
+{
+   return ( getTobType() == TobType::EM );
+}
+
+bool
+L1CaloRdoFexTob::getIsTau( ) const
+{
+   return ( getTobType() == TobType::Tau );
+}
+
+bool
+L1CaloRdoFexTob::getIsJet( ) const
+{
+   return ( getTobType() == TobType::LargeJet ||
+            getTobType() == TobType::SmallJet );
+}
+
+bool
+L1CaloRdoFexTob::getIsLargeJet( ) const
+{
+   return ( getTobType() == TobType::LargeJet );
+}
+
+bool
+L1CaloRdoFexTob::getIsSmallJet( ) const
+{
+   return ( getTobType() == TobType::SmallJet );
+}
+
+bool
+L1CaloRdoFexTob::getIsEnergy( ) const
+{
+   return ( getTobType() == TobType::Energy );
+}
+
+bool
+L1CaloRdoFexTob::getIsMuon( ) const
+{
+   return ( getTobType() == TobType::Muon );
+}
+
+L1CaloRdoFexTob::TobSource
+L1CaloRdoFexTob::getTobSource( ) const
+{
+   return m_source;
+}
+
+std::string
+L1CaloRdoFexTob::getTobSourceString( ) const
+{
+   if      ( getIsTob() )       { return "Tob"; }
+   else if ( getIsXtob() )      { return "Xtob"; }
+   else if ( getIsTopo() )      { std::ostringstream s; s << "Topo" << m_id; return s.str(); }
+   else                         { return ""; }
+}
+
+bool
+L1CaloRdoFexTob::getIsTob( ) const
+{
+   return ( getTobSource() == TobSource::EfexTob ||
+            getTobSource() == TobSource::JfexTob ||
+            getTobSource() == TobSource::GfexTob ||
+            getTobSource() == TobSource::Ph1Topo );
+}
+
+bool
+L1CaloRdoFexTob::getIsXtob( ) const
+{
+   return ( getTobSource() == TobSource::EfexXtob ||
+            getTobSource() == TobSource::JfexXtob ||
+            getTobSource() == TobSource::GfexXtob );
+}
+
+bool
+L1CaloRdoFexTob::getIsEfex( ) const
+{
+   return ( getTobSource() == TobSource::EfexTob ||
+            getTobSource() == TobSource::EfexXtob );
+}
+
+bool
+L1CaloRdoFexTob::getIsJfex( ) const
+{
+   return ( getTobSource() == TobSource::JfexTob ||
+            getTobSource() == TobSource::JfexXtob );
+}
+
+bool
+L1CaloRdoFexTob::getIsGfex( ) const
+{
+   return ( getTobSource() == TobSource::GfexTob ||
+            getTobSource() == TobSource::GfexXtob );
+}
+
+bool
+L1CaloRdoFexTob::getIsTopo( ) const
+{
+   return ( getTobSource() == TobSource::Ph1Topo );
+}
+
+int
+L1CaloRdoFexTob::getFibre( ) const
+{
+   return m_fibre;
+}
+
+int
+L1CaloRdoFexTob::getTobSeq( ) const
+{
+   return m_tobSeq;
+}
+
+bool
+operator<( const L1CaloRdoFexTob& lhs, const L1CaloRdoFexTob& rhs )
+{
+   if ( lhs.getCrate() < rhs.getCrate() )
+      return true;
+   if ( lhs.getCrate() > rhs.getCrate() )
+      return false;
+   if ( lhs.getModule() < rhs.getModule() )
+      return true;
+   if ( lhs.getModule() > rhs.getModule() )
+      return false;
+   if ( lhs.getEta() < rhs.getEta() )
+      return true;
+   if ( lhs.getEta() > rhs.getEta() )
+      return false;
+   if ( lhs.getPhi() < rhs.getPhi() )
+      return true;
+   if ( lhs.getPhi() > rhs.getPhi() )
+      return false;
+   if ( lhs.getLayer() < rhs.getLayer() )
+      return true;
+   if ( lhs.getTobType() < rhs.getTobType() )
+      return true;
+   if ( lhs.getTobType() > rhs.getTobType() )
+      return false;
+   if ( lhs.getTobSource( ) < rhs.getTobSource( ) )
+      return true;
+   if ( lhs.getTobSource( ) > rhs.getTobSource( ) )
+      return false;
+   if ( lhs.getTobID( ) < rhs.getTobID( ) )
+      return true;
+   return false;
+}
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloRdoRodInfo.cxx b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloRdoRodInfo.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..6c795282ff1fb676da04a702fc49609f808321fd
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/bytestreamDecoder/src/L1CaloRdoRodInfo.cxx
@@ -0,0 +1,231 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+#include "bytestreamDecoder/L1CaloRdoRodInfo.h"
+
+#include <iostream>
+
+// Class mainly intended to hold information from the ROD fragment header.
+// It also contains error codes from the L1Calo ROD fragment status block
+// for the legacy Run 1 and Run 2 system.
+// Many of these are no longer relevant for Run 3 FEX/Topo ROD fragments.
+// But it still seems worth having a common ROD info class and other error
+// bits from these might be added?
+
+L1CaloRdoRodInfo::L1CaloRdoRodInfo( int system, int rod, int run, int bcnum, int triggerType, int detType, int version )
+: m_system( system )
+, m_rod( rod )
+, m_run( run )
+, m_bcnum( bcnum )
+, m_triggerType( triggerType )
+, m_detType( detType )
+, m_lvl1( 0 )
+, m_size( 0 )
+, m_status1( 0 )
+, m_status2 ( 0 )
+, m_version( version )
+, m_status( 16, 0 )
+{
+}
+
+std::string
+L1CaloRdoRodInfo::getType( ) const
+{
+    return std::string( "RodInfo" );
+}
+
+void 
+L1CaloRdoRodInfo::info( ) const
+{
+   std::cout << "RDO Object Type: " << getType();
+   std::cout << std::hex;
+   std::cout << "    System Id: " << getSystemId();
+   std::cout << "   ROD Id: " << getRodId() << std::endl << "     ";
+   std::cout << "   Run Num: " << getRunNumber();
+   std::cout << "   Level-1 Id: " << getLevel1Id();
+   std::cout << "   Bc Number: " << getBcNumber();
+   std::cout << "   Trig Type: " << getTriggerType();
+   std::cout << "   Step Number: " << getStepNumber();
+   std::cout << "   Data Size: " << getDataSize();
+   std::cout << "   Minor Ver: " << getMinorVersion();
+   std::cout << std::endl;
+   std::cout << std::dec;
+}
+
+int
+L1CaloRdoRodInfo::getMinorVersion( ) const
+{
+   return ( m_version & 0xffff );
+}
+
+bool
+L1CaloRdoRodInfo::getIsRun1( ) const
+{
+   return ( (unsigned int)this->getMinorVersion() < s_minRun2Version );
+}
+
+bool
+L1CaloRdoRodInfo::getIsRun2( ) const
+{
+   return ( this->getIsRun3() || (unsigned int)this->getMinorVersion() >= s_minRun2Version );
+}
+
+bool
+L1CaloRdoRodInfo::getIsRun3( ) const
+{
+   // Run3 fragments have some of bits 12-15 set.
+   return ( ( this->getRodId() & 0xf000 ) != 0 );
+}
+
+int 
+L1CaloRdoRodInfo::getSystemId( ) const
+{
+   return m_system;
+}
+
+int 
+L1CaloRdoRodInfo::getRodId( ) const
+{
+   return m_rod;
+}
+
+int
+L1CaloRdoRodInfo::getSourceId( ) const
+{
+   return ( (m_system<<16) | m_rod );
+}
+
+int 
+L1CaloRdoRodInfo::getRunNumber( ) const
+{
+   return m_run;
+}
+
+int 
+L1CaloRdoRodInfo::getBcNumber( ) const
+{
+   return m_bcnum;
+}
+
+int 
+L1CaloRdoRodInfo::getTriggerType( ) const
+{
+   return m_triggerType;
+}
+
+int 
+L1CaloRdoRodInfo::getLevel1Id( ) const
+{
+   return m_lvl1;
+}
+
+int 
+L1CaloRdoRodInfo::getDataSize( ) const
+{
+   return m_size;
+}
+
+int 
+L1CaloRdoRodInfo::getStepNumber( ) const
+{
+   return ( (m_detType>>4) & 0xfff);
+}
+
+int 
+L1CaloRdoRodInfo::getOrbitCount( ) const
+{
+   return ( (m_detType>>16) & 0xffff);
+}
+
+bool 
+L1CaloRdoRodInfo::getBcnumMismatch( ) const
+{
+   return ( m_status1 & 0x1 );
+}
+
+bool 
+L1CaloRdoRodInfo::getGlinkTimeout( ) const
+{
+   return ( m_status1 & 0x4 );
+}
+
+bool 
+L1CaloRdoRodInfo::getDataTransportError( ) const
+{
+   return ( m_status1 & 0x8 );
+}
+
+bool 
+L1CaloRdoRodInfo::getRodOverflow( ) const
+{
+   return ( m_status1 & 0x10 );
+}
+
+bool 
+L1CaloRdoRodInfo::getModuleLinkError( ) const
+{
+   return ( m_status1 & 0x10000 );
+}
+
+bool 
+L1CaloRdoRodInfo::getCmmParityError( ) const
+{
+   return ( m_status1 & 0x20000 );
+}
+
+bool 
+L1CaloRdoRodInfo::getGlinkError( ) const
+{
+   return ( m_status1 & 0x40000 );
+}
+
+bool 
+L1CaloRdoRodInfo::getRoiOverflow( ) const
+{
+   return ( m_status2 & 0x2 );
+}
+
+bool 
+L1CaloRdoRodInfo::getTriggerTypeTimeout( ) const
+{
+   return ( m_status2 & 0x10000 );
+}
+
+int
+L1CaloRdoRodInfo::getModuleStatus( const int module ) const
+{
+   if ( (module < 0) || (module > 15) )
+      return 0;
+   return m_status[module];
+}
+
+void
+L1CaloRdoRodInfo::setLvl1Id( int lvl1 )
+{
+   m_lvl1 = lvl1;
+}
+
+void
+L1CaloRdoRodInfo::setSize( int size )
+{
+   m_size = size;
+}
+
+void
+L1CaloRdoRodInfo::setStatus1( int status )
+{
+   m_status1 = status;
+}
+
+void
+L1CaloRdoRodInfo::setStatus2( int status )
+{
+   m_status2 = status;
+}
+
+void
+L1CaloRdoRodInfo::setModuleStatus( int module, int status )
+{
+   m_status[module] = status;
+}
+
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/components/L1CaloFEXByteStream_entries.cxx b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/components/L1CaloFEXByteStream_entries.cxx
index ed5888a4cf4439abe7ec9eb6e43f6957c13f6f22..e23c196d0c27aa2cbcd08c3bc369fb0e974d486e 100644
--- a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/components/L1CaloFEXByteStream_entries.cxx
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/components/L1CaloFEXByteStream_entries.cxx
@@ -1,3 +1,5 @@
 #include "../jFexByteStreamTool.h"
+#include "../eFexByteStreamTool.h"
 
 DECLARE_COMPONENT( jFexByteStreamTool )
+DECLARE_COMPONENT( eFexByteStreamTool )
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/eFexByteStreamTool.cxx b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/eFexByteStreamTool.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..71ce6c95195ccc516137e027bf15fbfd7a44c437
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/eFexByteStreamTool.cxx
@@ -0,0 +1,158 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+
+//***************************************************************************
+//                           eFexByteStreamTool  -  description
+//                              -------------------
+//     begin                : 11 05 2022
+//     email                : will@cern.ch
+//  ***************************************************************************/
+
+#include "eFexByteStreamTool.h"
+#include "CxxUtils/span.h"
+#include "eformat/SourceIdentifier.h"
+#include "eformat/Status.h"
+
+#include "xAODTrigger/eFexEMRoIAuxContainer.h"
+#include "xAODTrigger/eFexTauRoIAuxContainer.h"
+#include "bytestreamDecoder/L1CaloRdoEfexTob.h"
+#include "bytestreamDecoder/L1CaloBsDecoderUtil.h"
+#include "bytestreamDecoder/L1CaloRdoRodInfo.h"
+#include "bytestreamDecoder/L1CaloBsDecoderRun3.h"
+
+using ROBF = OFFLINE_FRAGMENTS_NAMESPACE::ROBFragment;
+using WROBF = OFFLINE_FRAGMENTS_NAMESPACE_WRITE::ROBFragment;
+
+namespace {
+    // Helper method for creating the containers
+    template<typename T, typename W>
+    StatusCode addContainer(
+            std::map <std::pair<L1CaloRdoFexTob::TobSource, bool>, SG::WriteHandle<T>> &map,
+            L1CaloRdoFexTob::TobSource source, bool inTime, const SG::WriteHandleKey <T> &handle,
+            const EventContext &ctx) {
+        if (!handle.empty()) {
+            return map.emplace(std::make_pair(source, inTime),
+                               SG::WriteHandle<T>(handle, ctx)).first->second.record(
+                    std::make_unique<T>(), std::make_unique<W>());
+        }
+        return StatusCode::SUCCESS;
+    }
+}
+
+eFexByteStreamTool::eFexByteStreamTool(const std::string& type,
+        const std::string& name,
+        const IInterface* parent)
+    : base_class(type, name, parent) {}
+
+StatusCode eFexByteStreamTool::initialize() {
+
+    std::vector<SG::VarHandleKey*> writeKeys = {
+            &m_eEMTOBWriteKey, &m_eTAUTOBWriteKey, &m_eEMxTOBWriteKey, &m_eTAUxTOBWriteKey };
+    std::vector<SG::VarHandleKey*> readKeys = {
+            &m_eEMTOBReadKey, &m_eTAUTOBReadKey, &m_eEMxTOBReadKey, &m_eTAUxTOBReadKey };
+    std::vector<SG::VarHandleKey*> sliceKeys = {
+            &m_eEMTOBSliceKey, &m_eTAUTOBSliceKey, &m_eEMxTOBSliceKey, &m_eTAUxTOBSliceKey };
+
+    auto previousMode = ConversionMode::Undefined;
+    for(size_t i=0;i<writeKeys.size();i++) {
+        auto mode = getConversionMode(*readKeys.at(i),*writeKeys.at(i),msg());
+        ATH_CHECK(mode!=ConversionMode::Undefined);
+        if(i!=0) ATH_CHECK(mode==previousMode); // if this fails, user is trying to mix encoding and decoding
+        previousMode = mode;
+        ATH_CHECK(writeKeys.at(i)->initialize(mode==ConversionMode::Decoding));
+        ATH_CHECK(readKeys.at(i)->initialize(mode==ConversionMode::Encoding));
+        if(mode==ConversionMode::Decoding && m_multiSlice) {
+            ATH_CHECK(sliceKeys.at(i)->assign(!writeKeys.at(i)->empty() ? writeKeys.at(i)->key()+"OutOfTime" : ""));
+        }
+        ATH_CHECK(sliceKeys.at(i)->initialize(!sliceKeys.at(i)->empty()));
+    }
+
+    return StatusCode::SUCCESS;
+}
+
+// BS->xAOD conversion
+StatusCode eFexByteStreamTool::convertFromBS(const std::vector<const ROBF*>& vrobf, const EventContext& ctx) const {
+    
+
+    // indices are source (tob or xtob) and bool for in/out of time
+    std::map<std::pair<L1CaloRdoFexTob::TobSource,bool>,SG::WriteHandle<xAOD::eFexEMRoIContainer>> eContainers;
+    std::map<std::pair<L1CaloRdoFexTob::TobSource,bool>,SG::WriteHandle<xAOD::eFexTauRoIContainer>> tContainers;
+
+    ATH_CHECK( StatusCode(addContainer<xAOD::eFexEMRoIContainer,xAOD::eFexEMRoIAuxContainer>(eContainers,L1CaloRdoFexTob::TobSource::EfexTob,false,m_eEMTOBWriteKey,ctx)) );
+    ATH_CHECK( StatusCode(addContainer<xAOD::eFexEMRoIContainer,xAOD::eFexEMRoIAuxContainer>(eContainers,L1CaloRdoFexTob::TobSource::EfexXtob,false,m_eEMxTOBWriteKey,ctx)) );
+    ATH_CHECK( StatusCode(addContainer<xAOD::eFexEMRoIContainer,xAOD::eFexEMRoIAuxContainer>(eContainers,L1CaloRdoFexTob::TobSource::EfexTob,true,m_eEMTOBSliceKey,ctx)) );
+    ATH_CHECK( StatusCode(addContainer<xAOD::eFexEMRoIContainer,xAOD::eFexEMRoIAuxContainer>(eContainers,L1CaloRdoFexTob::TobSource::EfexXtob,true,m_eEMxTOBSliceKey,ctx)) );
+
+    ATH_CHECK( StatusCode(addContainer<xAOD::eFexTauRoIContainer,xAOD::eFexTauRoIAuxContainer>(tContainers,L1CaloRdoFexTob::TobSource::EfexTob,false,m_eTAUTOBWriteKey,ctx)) );
+    ATH_CHECK( StatusCode(addContainer<xAOD::eFexTauRoIContainer,xAOD::eFexTauRoIAuxContainer>(tContainers,L1CaloRdoFexTob::TobSource::EfexXtob,false,m_eTAUxTOBWriteKey,ctx)) );
+    ATH_CHECK( StatusCode(addContainer<xAOD::eFexTauRoIContainer,xAOD::eFexTauRoIAuxContainer>(tContainers,L1CaloRdoFexTob::TobSource::EfexTob,true,m_eTAUTOBSliceKey,ctx)) );
+    ATH_CHECK( StatusCode(addContainer<xAOD::eFexTauRoIContainer,xAOD::eFexTauRoIAuxContainer>(tContainers,L1CaloRdoFexTob::TobSource::EfexXtob,true,m_eTAUxTOBSliceKey,ctx)) );
+
+    std::list<L1CaloRdoRodInfo> rodInfos;
+    std::list<L1CaloRdoEfexTob> efexTobs;
+    L1CaloBsDecoderRun3 decoder;
+    for (const ROBF* rob : vrobf) {
+        // Iterate over ROD words and decode
+        ATH_MSG_DEBUG("Decoding " << rob->rod_ndata() << " ROD words from ROB 0x" << std::hex << rob->rob_source_id() << std::dec);
+        L1CaloBsDecoderUtil::decodeRodInfo( rob, rodInfos );
+
+        if(rob->rod_ndata()==0) continue;
+
+        CxxUtils::span data{rob->rod_data(), rob->rod_ndata()};
+        auto lastRod = rodInfos.end(); lastRod--;
+        decoder.decodeEfexTobs( data.begin(), data.end(), efexTobs, lastRod );
+
+        for(const L1CaloRdoEfexTob& tob : efexTobs) {
+            // loop over slices ... create separate tobs for each, where there's a word defined
+            for(size_t slice=0;slice < tob.numSlices(); slice++) {
+                if (!m_multiSlice && int(slice) != tob.getL1aPos()) continue; // ignore out-of-time slices if multiSlice option = false
+                if (tob.getWord0(slice)==0) continue; // this tob isn't in this slice
+                if(tob.getTobType() == L1CaloRdoFexTob::TobType::EM) {
+                    auto cont = eContainers.find({tob.getTobSource(),int(slice)!=tob.getL1aPos()});
+                    if(cont == eContainers.end()) continue; // not writing this tob collection
+                    cont->second->push_back( std::make_unique<xAOD::eFexEMRoI>() );
+                    if(tob.getTobSource() == L1CaloRdoFexTob::TobSource::EfexTob) {
+                        cont->second->back()->initialize(tob.getModule(),tob.getCrate(),tob.getWord0(slice));
+                    } else {
+                        cont->second->back()->initialize(tob.getWord0(slice),tob.getWord1(slice));
+                    }
+                } else if (tob.getTobType() == L1CaloRdoFexTob::TobType::Tau) {
+                    auto cont = tContainers.find({tob.getTobSource(),int(slice)!=tob.getL1aPos()});
+                    if(cont == tContainers.end()) continue; // not writing this tob collection
+                    cont->second->push_back( std::make_unique<xAOD::eFexTauRoI>() );
+                    if(tob.getTobSource() == L1CaloRdoFexTob::TobSource::EfexTob) {
+                        cont->second->back()->initialize(tob.getModule(),tob.getCrate(),tob.getWord0(slice));
+                    } else {
+                        cont->second->back()->initialize(tob.getWord0(slice),tob.getWord1(slice));
+                    }
+                }
+            } // timeslice loop
+        } // tob loop
+    } // fragment loop
+
+    if(msgLevel(MSG::DEBUG)) {
+        std::stringstream msg;
+        for (auto&[k, v]: eContainers) {
+            msg << v->size() << " " << (k.second ? "out-of-time " : "in-time ") << "eg" <<
+                (k.first == L1CaloRdoFexTob::TobSource::EfexTob ? "" : "x") << "TOB, ";
+        }
+        for (auto&[k, v]: tContainers) {
+            msg << v->size() << " " << (k.second ? "out-of-time " : "in-time ") << "tau" <<
+                (k.first == L1CaloRdoFexTob::TobSource::EfexTob ? "" : "x") << "TOB, ";
+        }
+        ATH_MSG_DEBUG("Decoded: " << msg.str());
+    }
+    // this is how to print the in-time em xTOBs:
+//    for(const auto& tob : *(eContainers[{L1CaloRdoFexTob::TobSource::EfexXtob,false}])) {
+//        std::cout << tob->eFexNumber() << "." << tob->fpga() << ": " << tob->fpgaEta() << " " << tob->fpgaPhi() << " " << tob->et() << " " << tob->etXTOB() << " " << tob->etTOB() << " " << std::endl;
+//    }
+
+    return StatusCode::SUCCESS;
+}
+
+StatusCode eFexByteStreamTool::convertToBS(std::vector<OFFLINE_FRAGMENTS_NAMESPACE_WRITE::ROBFragment*>& , const EventContext& ) {
+    // not supported yet
+    return StatusCode::FAILURE;
+}
+
diff --git a/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/eFexByteStreamTool.h b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/eFexByteStreamTool.h
new file mode 100644
index 0000000000000000000000000000000000000000..516d3a1b112f6ceb1eed0222c03e2b0b03bacff5
--- /dev/null
+++ b/Trigger/TrigT1/L1CaloFEX/L1CaloFEXByteStream/src/eFexByteStreamTool.h
@@ -0,0 +1,95 @@
+/*
+  Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+*/
+
+//***************************************************************************
+//                           eFexByteStreamTool  -  description
+//                              -------------------
+//     begin                : 11 05 2022
+//     email                : will@cern.ch
+//  ***************************************************************************/
+
+#ifndef EFEXBYTESTREAMTOOL_H 
+#define EFEXBYTESTREAMTOOL_H
+
+// Trigger includes
+#include "TrigT1ResultByteStream/IL1TriggerByteStreamTool.h"
+
+// Athena includes
+#include "AthenaBaseComps/AthAlgTool.h"
+
+#include "xAODTrigger/eFexEMRoIContainer.h"
+#include "xAODTrigger/eFexTauRoIContainer.h"
+
+#include "bytestreamDecoder/L1CaloRdoEfexTob.h"
+
+// Gaudi includes
+#include "Gaudi/Property.h"
+
+/** @class eFEXRoIByteStreamTool
+ *  @brief Implementation of a tool for L1 RoI conversion from BS to xAOD and from xAOD to BS
+ *  (IL1TriggerByteStreamTool interface)
+ **/
+class eFexByteStreamTool : public extends<AthAlgTool, IL1TriggerByteStreamTool> {
+    public:
+        eFexByteStreamTool(const std::string& type, const std::string& name, const IInterface* parent);
+        virtual ~eFexByteStreamTool() override = default;
+
+        // ------------------------- IAlgTool methods --------------------------------
+        virtual StatusCode initialize() override;
+
+        // ------------------------- IL1TriggerByteStreamTool methods ----------------------
+        /// BS->xAOD conversion
+        virtual StatusCode convertFromBS(const std::vector<const OFFLINE_FRAGMENTS_NAMESPACE::ROBFragment*>& vrobf, const EventContext& eventContext)const override;
+
+        /// xAOD->BS conversion
+        virtual StatusCode convertToBS(std::vector<OFFLINE_FRAGMENTS_NAMESPACE_WRITE::ROBFragment*>& vrobf, const EventContext& eventContext) override;
+
+        /// Declare ROB IDs for conversion
+        virtual const std::vector<uint32_t>& robIds() const override {
+            return m_robIds.value();
+        }
+
+    private:
+
+
+        // ------------------------- Properties --------------------------------------
+        // ROBIDs property required by the interface
+        Gaudi::Property<std::vector<uint32_t>> m_robIds {this, "ROBIDs", {}, "List of ROB IDs required for conversion to/from xAOD RoI"};
+
+        // Write handle keys for the L1Calo EDMs for BS->xAOD mode of operation
+
+
+    // Is multi-slice data decoding required?
+    Gaudi::Property<bool> m_multiSlice {
+            this, "multiSlice", true, "Decode data from all time slices (false = just from triggered BC)"};
+
+    // Only write keys should be set to non-empty string in python configuration if the tool is in BS->xAOD mode of operation
+    // Note that these are the keys for the triggered time slice. In multi-slice mode data from crossings
+    // before or after this will be stored in separate containers whose keys have the suffix "OutOfTime"
+    SG::WriteHandleKey<xAOD::eFexEMRoIContainer> m_eEMTOBWriteKey {
+            this, "eEMTOBContainerWriteKey", "", "Write handle key to eEM TOB (RoI) container for conversion from ByteStream"};
+    SG::WriteHandleKey<xAOD::eFexEMRoIContainer> m_eEMxTOBWriteKey {
+            this, "eEMxTOBContainerWriteKey", "", "Write handle key to eEM xTOB container for conversion from ByteStream"};
+    SG::WriteHandleKey<xAOD::eFexTauRoIContainer> m_eTAUTOBWriteKey {
+            this, "eTAUTOBContainerWriteKey", "", "Write handle key to eTAU TOB (RoI) container for conversion from ByteStream"};
+    SG::WriteHandleKey<xAOD::eFexTauRoIContainer> m_eTAUxTOBWriteKey {
+            this, "eTAUxTOBContainerWriteKey", "", "Write handle key to eTAU xTOB container for conversion from ByteStream"};
+    // Only read keys should be set to non-empty string in python configuration if the tool is in xAOD->BS mode of operation
+    SG::ReadHandleKey<xAOD::eFexEMRoIContainer> m_eEMTOBReadKey {
+            this, "eEMTOBContainerReadKey", "", "Read handle key to eEM TOB (RoI) container for conversion from ByteStream"};
+    SG::ReadHandleKey<xAOD::eFexEMRoIContainer> m_eEMxTOBReadKey {
+            this, "eEMxTOBContainerReadKey", "", "Read handle key to eEM xTOB container for conversion from ByteStream"};
+    SG::ReadHandleKey<xAOD::eFexTauRoIContainer> m_eTAUTOBReadKey {
+            this, "eTAUTOBContainerReadKey", "", "Read handle key to eTAU TOB (RoI) container for conversion from ByteStream"};
+    SG::ReadHandleKey<xAOD::eFexTauRoIContainer> m_eTAUxTOBReadKey {
+            this, "eTAUxTOBContainerReadKey", "", "Read handle key to eTAU xTOB container for conversion from ByteStream"};
+    // Additional keys for multi-slice TOB and xTOB readout (values to be defined in initialize based on the keys specified above)
+    SG::WriteHandleKey<xAOD::eFexEMRoIContainer> m_eEMTOBSliceKey;
+    SG::WriteHandleKey<xAOD::eFexEMRoIContainer> m_eEMxTOBSliceKey;
+    SG::WriteHandleKey<xAOD::eFexTauRoIContainer> m_eTAUTOBSliceKey;
+    SG::WriteHandleKey<xAOD::eFexTauRoIContainer> m_eTAUxTOBSliceKey;
+
+};
+
+#endif // EFEXBYTESTREAMTOOL_H
diff --git a/Trigger/TrigT1/TrigT1ResultByteStream/python/TrigT1ResultByteStreamConfig.py b/Trigger/TrigT1/TrigT1ResultByteStream/python/TrigT1ResultByteStreamConfig.py
index 3793cafc6e7bbfa4b4f21537b147c01cab9d7f37..ae9cd0aa44add12e67ef4ea407be83d78d14bf56 100644
--- a/Trigger/TrigT1/TrigT1ResultByteStream/python/TrigT1ResultByteStreamConfig.py
+++ b/Trigger/TrigT1/TrigT1ResultByteStream/python/TrigT1ResultByteStreamConfig.py
@@ -191,16 +191,16 @@ def L1TriggerByteStreamEncoderCfg(flags):
 if __name__ == '__main__':
   from AthenaConfiguration.AllConfigFlags import ConfigFlags as flags
   from AthenaCommon.Logging import logging
-  from AthenaCommon.Constants import DEBUG
+  from AthenaCommon.Constants import DEBUG,WARNING
   import sys
 
   log = logging.getLogger('TrigT1ResultByteStreamConfig')
   log.setLevel(DEBUG)
 
   if len(sys.argv) != 4:
-    log.error('usage: python -m TrigT1ResultByteStream.TrigT1ResultByteStreamConfig subsystem file')
+    log.error('usage: python -m TrigT1ResultByteStream.TrigT1ResultByteStreamConfig subsystem file nevents')
     sys.exit(1)
-  supportedSubsystems = ['jFex']
+  supportedSubsystems = ['jFex','eFex']
   subsystem = sys.argv[1]
   filename = sys.argv[2]
   events = int(sys.argv[3])
@@ -208,7 +208,7 @@ if __name__ == '__main__':
     log.error(f'subsystem "{subsystem}" not one of supported subsystems: {supportedSubsystems}')
     sys.exit(1)
 
-  flags.Exec.OutputLevel = DEBUG
+  flags.Exec.OutputLevel = WARNING
   flags.Exec.MaxEvents = events
   flags.Input.Files = [filename]
   flags.Concurrency.NumThreads = 1
@@ -240,8 +240,21 @@ if __name__ == '__main__':
     outputEDM += addEDM('xAOD::jFexSumETRoIContainer', jFexTool.jTERoIContainerWriteKey.Path)
     outputEDM += addEDM('xAOD::jFexMETRoIContainer'  , jFexTool.jXERoIContainerWriteKey.Path)
 
+  if subsystem=='eFex':
+    from L1CaloFEXByteStream.L1CaloFEXByteStreamConfig import eFexByteStreamToolCfg
+    eFexTool = eFexByteStreamToolCfg('eFexBSDecoder', flags)
+    decoderTools += [eFexTool]
+    # TOB containers
+    outputEDM += addEDM('xAOD::eFexEMRoIContainer', eFexTool.eEMTOBContainerWriteKey.Path)
+    outputEDM += addEDM('xAOD::eFexTauRoIContainer', eFexTool.eTAUTOBContainerWriteKey.Path)
+    # xTOB containers
+    outputEDM += addEDM('xAOD::eFexEMRoIContainer', eFexTool.eEMxTOBContainerWriteKey.Path)
+    outputEDM += addEDM('xAOD::eFexTauRoIContainer', eFexTool.eTAUxTOBContainerWriteKey.Path)
+
+
+
   decoderAlg = CompFactory.L1TriggerByteStreamDecoderAlg(name="L1TriggerByteStreamDecoder",
-                                                         DecoderTools=decoderTools)
+                                                         DecoderTools=decoderTools, OutputLevel=DEBUG)
   acc.addEventAlgo(decoderAlg, sequenceName='AthAlgSeq')
 
   from OutputStreamAthenaPool.OutputStreamConfig import OutputStreamCfg