Skip to content
537 changes: 537 additions & 0 deletions docs/design/rcdaq-event-source.md

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion src/global/tracking/tracking.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
#include <podio/detail/Link.h>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <utility>
Expand Down
1 change: 1 addition & 0 deletions src/services/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# io/podio exports podio_datamodel_glue target needed by plugin_add
add_subdirectory(io/podio)
add_subdirectory(io/rcdaq)
add_subdirectory(algorithms_init)
add_subdirectory(evaluator)
add_subdirectory(geometry/dd4hep)
Expand Down
29 changes: 29 additions & 0 deletions src/services/io/rcdaq/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Optional rcdaq binary-format event source plugin. The plugin is only built
# when the rcdaq format headers are found. Point cmake to the headers via:
# -Drcdaq_INCLUDE_DIR=/path/to/rcdaq

# ---- Locate rcdaq format headers ----------------------------------------
find_path(
rcdaq_INCLUDE_DIR
NAMES EvtStructures.h
HINTS ${rcdaq_INCLUDE_DIR} $ENV{rcdaq_INCLUDE_DIR}
${PROJECT_SOURCE_DIR}/../rcdaq /opt/rcdaq/include
DOC "Directory containing rcdaq format headers (EvtStructures.h etc.)")

if(NOT rcdaq_INCLUDE_DIR)
message(STATUS "rcdaq headers not found — skipping io/rcdaq plugin")
return()
endif()

message(STATUS "Building io/rcdaq plugin (rcdaq headers: ${rcdaq_INCLUDE_DIR})")

# ---- Plugin target -------------------------------------------------------
get_filename_component(PLUGIN_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)

plugin_add(${PLUGIN_NAME} WITH_STATIC_LIBRARY)
plugin_glob_all(${PLUGIN_NAME})

# Expose rcdaq format headers to this target only
plugin_include_directories(${PLUGIN_NAME} PRIVATE ${rcdaq_INCLUDE_DIR})

plugin_link_libraries(${PLUGIN_NAME} fmt::fmt podio::podioIO log_library)
149 changes: 149 additions & 0 deletions src/services/io/rcdaq/JEventSourceRCDAQ.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright 2024, EIC
// Subject to the terms in the LICENSE file found in the top-level directory.

#include "JEventSourceRCDAQ.h"

#include <JANA/JApplication.h>
#include <JANA/JEvent.h>
#include <JANA/JException.h>
#include <podio/Frame.h>
#include <fmt/format.h>
#include <algorithm>

#include "RCDAQFrameData.h"
#include "services/log/Log_service.h"

// ---------------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------------
JEventSourceRCDAQ::JEventSourceRCDAQ(std::string resource_name, JApplication* app)
: JEventSource(std::move(resource_name), app) {
SetTypeName(NAME_OF_THIS);
SetCallbackStyle(CallbackStyle::ExpertMode);

m_log = GetApplication()->GetService<Log_service>()->logger("JEventSourceRCDAQ");
}

// ---------------------------------------------------------------------------
// addDecoder
// ---------------------------------------------------------------------------
void JEventSourceRCDAQ::addDecoder(std::unique_ptr<RCDAQDecoder> decoder) {
const int32_t id = decoder->packetID();
m_decoders[id] = std::move(decoder);
}

// ---------------------------------------------------------------------------
// Open
// ---------------------------------------------------------------------------
void JEventSourceRCDAQ::Open() {
GetApplication()->SetDefaultParameter("rcdaq:dump", m_dump,
"Print raw sub-event packet headers and payload words "
"without decoding (useful for exploring new files)");

// Build the non-owning decoder map that will be shared across all frames.
m_decoder_map.clear();
for (auto& [id, dec] : m_decoders) {
m_decoder_map[id] = dec.get();
}

if (!m_decoders.empty()) {
m_log->info("Registered {} rcdaq decoder(s):", m_decoders.size());
for (const auto& [id, dec] : m_decoders) {
m_log->info(" packet_id {:5d} → {} ({})", id, dec->collectionName(), dec->collectionType());
}
} else {
m_log->warn("No rcdaq decoders registered; Frame will contain no collections.");
}

try {
m_reader.open(GetResourceName());
m_log->info("Opened rcdaq file \"{}\"", GetResourceName());
} catch (const std::exception& e) {
throw JException(
fmt::format("JEventSourceRCDAQ: failed to open \"{}\": {}", GetResourceName(), e.what()));
}
}

// ---------------------------------------------------------------------------
// Close
// ---------------------------------------------------------------------------
void JEventSourceRCDAQ::Close() {
m_reader.close();
m_log->info("Closed rcdaq file \"{}\"", GetResourceName());
}

// ---------------------------------------------------------------------------
// Emit
// ---------------------------------------------------------------------------
JEventSourceRCDAQ::Result JEventSourceRCDAQ::Emit(JEvent& event) {
RCDAQFileReader::Event rcdaq_event;

try {
if (!m_reader.nextEvent(rcdaq_event)) {
return Result::FailureFinished;
}
} catch (const std::exception& e) {
m_log->error("Error reading rcdaq event: {}", e.what());
return Result::FailureFinished;
}

event.SetRunNumber(rcdaq_event.run_number);
event.SetEventNumber(rcdaq_event.evt_sequence);

if (m_dump) {
m_log->info("[rcdaq] evt_seq={} run={} format={} npackets={}", rcdaq_event.evt_sequence,
rcdaq_event.run_number,
(m_reader.format() == RCDAQFileReader::Format::PRDF ? "PRDF" : "ONCS"),
rcdaq_event.subevents.size());
for (const auto& se : rcdaq_event.subevents) {
// Build hex dump of first 8 payload words
std::string hex;
const int nprint = static_cast<int>(std::min(se.data.size(), std::size_t{8}));
for (int i = 0; i < nprint; i++) {
hex += fmt::format(" {:08x}", static_cast<uint32_t>(se.data[i]));
}
if (static_cast<int>(se.data.size()) > nprint) {
hex += " ...";
}
m_log->info(" packet_id={:5d} (0x{:04x}) sub_id={:5d} len={:6d}w"
" decoding={:3d} type={:2d} payload[0..{}]:{}",
se.packet_id, static_cast<uint32_t>(se.packet_id), se.sub_id,
static_cast<int>(se.data.size()), static_cast<int>(se.sub_decoding),
static_cast<int>(se.sub_type), nprint - 1, hex);
}
}

// Wrap the raw event in a FrameData object that satisfies podio::FrameDataType.
// The Frame will call RCDAQFrameData::getCollectionBuffers() lazily when a
// collection is first accessed, invoking the appropriate RCDAQDecoder.
auto frame_data = std::make_unique<RCDAQFrameData>(std::move(rcdaq_event), m_decoder_map);
auto frame = std::make_unique<podio::Frame>(std::move(frame_data));
event.Insert(frame.release());

return Result::Success;
}

// ---------------------------------------------------------------------------
// GetDescription
// ---------------------------------------------------------------------------
std::string JEventSourceRCDAQ::GetDescription() {
return "rcdaq binary data file (ONCS/PRDF format)";
}

// ---------------------------------------------------------------------------
// CheckOpenable
//
// Return a positive score for filenames that look like rcdaq data files.
// Common extensions used by rcdaq: .prdf, .evt, .rcdaq
// ---------------------------------------------------------------------------
template <>
double JEventSourceGeneratorT<JEventSourceRCDAQ>::CheckOpenable(std::string resource_name) {
for (const auto* ext : {".prdf", ".evt", ".rcdaq"}) {
if (resource_name.size() >= std::strlen(ext) &&
resource_name.compare(resource_name.size() - std::strlen(ext), std::strlen(ext), ext) ==
0) {
return 0.9;
}
}
return 0.0;
}
74 changes: 74 additions & 0 deletions src/services/io/rcdaq/JEventSourceRCDAQ.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2024, EIC
// Subject to the terms in the LICENSE file found in the top-level directory.

#pragma once

#include <JANA/JApplicationFwd.h>
#include <JANA/JEventSource.h>
#include <JANA/JEventSourceGeneratorT.h>
#include <spdlog/logger.h>
#include <memory>
#include <string>
#include <unordered_map>

#include "RCDAQDecoder.h"
#include "RCDAQFileReader.h"
#include "RCDAQFrameData.h"

/// JANA2 event source for rcdaq binary data files.
///
/// Reads ONCS-format rcdaq files and, for every DATA event, constructs a
/// podio::Frame backed by an RCDAQFrameData object and inserts it into the
/// JEvent. Collections are decoded lazily: a registered RCDAQDecoder is
/// called only when the Frame is asked for the corresponding collection.
///
/// Register decoders before the source is opened (e.g. in InitPlugin):
/// @code
/// auto src = std::make_shared<JEventSourceRCDAQ>("myfile.prdf", app);
/// src->addDecoder(std::make_unique<MyCaloDecoder>());
/// app->Add(src);
/// @endcode
///
/// Downstream code accesses collections through the Frame:
/// @code
/// auto* frame = event.GetSingle<podio::Frame>();
/// auto& hits = frame->get<edm4hep::RawCalorimeterHitCollection>("CaloHits");
/// @endcode
///
/// File format detection: CheckOpenable returns a positive score for files
/// whose names end in ".prdf", ".evt", or ".rcdaq".
class JEventSourceRCDAQ : public JEventSource {
public:
JEventSourceRCDAQ(std::string resource_name, JApplication* app);

~JEventSourceRCDAQ() override = default;

void Open() override;

void Close() override;

Result Emit(JEvent& event) override;

static std::string GetDescription();

/// Register a decoder. Ownership is transferred to this event source.
/// Must be called before Open().
void addDecoder(std::unique_ptr<RCDAQDecoder> decoder);

private:
RCDAQFileReader m_reader;
std::shared_ptr<spdlog::logger> m_log;

/// When true, each event's raw sub-event headers and first payload words are
/// printed to the log without decoding. Set via -Prcdaq:dump=1 at runtime.
bool m_dump{false};

/// Owned decoders, keyed by packet ID.
std::unordered_map<int32_t, std::unique_ptr<RCDAQDecoder>> m_decoders;

/// Non-owning view of m_decoders, rebuilt in Open() and passed by const-ref
/// to each RCDAQFrameData instance.
RCDAQFrameData::DecoderMap m_decoder_map;
};

template <> double JEventSourceGeneratorT<JEventSourceRCDAQ>::CheckOpenable(std::string);
59 changes: 59 additions & 0 deletions src/services/io/rcdaq/RCDAQDecoder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2024, EIC
// Subject to the terms in the LICENSE file found in the top-level directory.

#pragma once

#include <podio/CollectionBuffers.h>
#include <cstdint>
#include <string>

/// Abstract interface for decoding a single rcdaq sub-event into a podio
/// CollectionReadBuffers object.
///
/// Implement this interface for each sub-event ID that should be exposed as an
/// EDM4hep (or other podio) collection. The implementer is responsible for:
/// 1. Calling podio::CollectionBufferFactory::instance().createBuffers() to
/// allocate properly-typed buffers.
/// 2. Filling the data vector (via buffers.dataAsVector<DataT>()) from the
/// raw rcdaq words.
/// 3. Returning the filled buffers.
///
/// Example usage:
/// @code
/// auto src = std::make_shared<JEventSourceRCDAQ>("myfile.prdf", app);
/// src->addDecoder(std::make_unique<MyCaloDecoder>());
/// @endcode
class RCDAQDecoder {
public:
virtual ~RCDAQDecoder() = default;

/// The packet ID this decoder handles (matches RCDAQSubevent::packet_id).
/// For PRDF files this is hdrinfo & 0xFFFF (e.g. 12001 for detector packets).
/// For ONCS files this equals the sub_id.
virtual int32_t packetID() const = 0;

/// The name of the collection that will appear in the podio Frame.
virtual std::string collectionName() const = 0;

/// The fully-qualified collection type name, e.g.
/// "edm4hep::RawCalorimeterHitCollection"
/// This must match the string registered in podio::CollectionBufferFactory.
virtual std::string collectionType() const = 0;

/// Decode \p nwords int32_t words starting at \p data into CollectionReadBuffers.
///
/// \p sub_type mirrors the trigger event type (DATA1EVENT=1, etc.).
/// \p sub_decoding identifies the payload encoding scheme (see
/// SubevtConstants.h: IDCRAW=0, ID4EVT=6, IDDCFEM=51, …).
///
/// The implementation should:
/// 1. Validate \p sub_decoding against the expected format.
/// 2. Obtain properly-initialised buffers from CollectionBufferFactory.
/// 3. Fill them with decoded data.
/// 4. Return the buffers (or an empty optional on failure / unknown encoding).
///
/// Called lazily by RCDAQFrameData::getCollectionBuffers() when the podio
/// Frame is asked for this collection for the first time.
virtual std::optional<podio::CollectionReadBuffers> decode(int16_t sub_type, int16_t sub_decoding,
const int32_t* data, int nwords) = 0;
};
Loading
Loading