SpecTcl Programming Guide. | ||
---|---|---|
Prev | Chapter 10. Interfacing SpecTcl with data from other data acquisition systems. | Next |
In this section we are going to build support for a simple, fictional data acquisition system data format. The internal format of the data bears some resemblence to the ring buffer data format used by NSCLDAQ 10.x and later, however that resemblance is purely coincidental and you should not conclude from it that any of this code is actually used by SpecTcl to decode NSCLDAQ data.
Before writing any code we need to have a clear understanding of the format of the data we're going to decode. This is as true for a real case as it is for this imaginary case.
Data will come to us in fixed length blocks. No item will extend across block boundaries (this simplifies things' tremendously).
Each block will have a header that consists of several uint32_t. These are, in order, the number of 8 bit bytes occupied by useful data, the number of items in the block, a sequence number that tells us the number of the block in the event file or data taking run and a 32 bit SpecTcl byte order signature.
The block is divided int items. Each item consists of a self inclusive size in bytes and a type (both are uint32_t) followed by a body whose structure depends on the type value.
Type 1 indicates a run start, type 2 indicates a run end, Type 3 indicates scaler data and type 4 indicates data taken in response to a physics trigger.
The structure of type 1,2 data are the same and look like this:
struct { header s_header; uint32_t s_runNumber; char s_title[]; };
Where header is the item header described above. The title is a null terminated variable length string.
Scaler data (type 3), has the following structure:
struct { header s_header; uint32_t s_numScalers; uint32_t s_counters[]; };
s_counters are the array of scaler values.
s_numScalers
tells us how many
of those there are.
Type 4 (physics event data) is just an array of bytes whose structure and meaning depend on the actual experiment performed using this data acquisition system.
Let's first create a header, expdata.h that captures the structure of the data described above. In order to minimize the chances for name collission, we're going to put all of those definitions in a namespace (SomeDaqSystem):
Example 10-6. Structure of data from SomeDaqSystem
#ifndef EXPDATA_H #define EXPDATA_H #include <cstdint> namespace SomeDaqSystem { static const unsigned BEGIN(1); static const unsigned END(2); static const unsigned SCALER(3); static const unsigned EVENT(4); typedef struct _Header { std::uint32_t s_size; std::uint32_t s_type; } Header; typedef struct _StateChange { Header s_header; std::uint32_t s_runNumber; char s_title[]; } StateChange; typedef struct _Scalers { Header s_header; std::uint32_t s_numScalers; std::uint32_t s_counters[]; } Scalers; typedef struct _Event { Header s_header; char s_body[]; } Event; typedef struct _BufferHeader { std::uint32_t s_bytesUsed; std::uint32_t s_itemCount; std::uint32_t s_sequence; std::uint32_t s_signature; } BufferHeader; }; #endif
The use of static const unsigned above fulfils the same purpose as #define but places those definitions inside the namespace, rather than potentially conflicting with other #defines the preprocessor knows about.
Let's look at the header for our decoder class. We're going to dispatch various types to type specific handlers. We're going to hand events one at a time to the analyzer rather than trying to We're also going to ensure we are able to deal with new item types without failing:
Example 10-7. SomeDaqBufferDecoder
header.
#ifndef SOMEDAQBUFFERDECODER_H #define SOMEDAQBUFFERDECODER_H #include <BufferDecoder.h> #include "expdata.h" #include <string> class SomeDaqBufferDecoder : public CBufferDecoder { private: SomeDaqSystem::BufferHeader m_lastHeader; std::string m_lastTitle; std::uint32_t m_lastRunNumber;unsigned m_entities; SomeDaqSystem::Header* m_pCurrentItem; public:
virtual BufferTranslator* getBufferTranslator(); virtual const Address_t getBody(); virtual UInt_t getBodySize() ; virtual UInt_t getRun() ; virtual UInt_t getEntityCount() ; virtual UInt_t getSequenceNo() ; virtual UInt_t getLamCount() ; virtual UInt_t getPatternCount() ; virtual UInt_t getBufferType() ; virtual void getByteOrder(Short_t& Signature16, Int_t& Signature32) ; virtual std::string getTitle() ; virtual void operator() (UInt_t nBytes,
Address_t pBuffer, CAnalyzer& rAnalyzer); private: void stateChange(CAnalyzer& rAnalyzer);
void scaler(CAnalyzer& rAnalyzer); void event(CAnalyzer& rAnalyzer); void other(CAnalyzer& rAnalyzer);
};
m_lastHeader
Each time we get a new block of data from the source, we'll cache a copy of the block header here. The block header provides byte order signatures as well as other useful book keeping information we'll want in processing the buffer.
m_lastTitle
For each state change item encountered, we'll pull the run title out and copy it into this member data.
m_lastRunNumber
For each state change item encountered we'll pull the run number out and copy it into this member data.
m_entities
We'll put the number of entities in each item here. For all but scaler items, this is just 1. For scaler items, this is the number of scalers in the scaler item.
m_pCurrentItem
Points to the item we're working on in the current buffer (note that this allows us to return the body pointer and body size).
CBufferDecoder
interface.
In spite of the ordering of the methods in the header, the
best place to start is probable with the operator()
method. Once we've coded that and the type specific handlers,
the getter methods should fall into place
fairly easily.
Example 10-8. SomeDaqBufferDecoder::operator()
implementation
void SomeDaqBufferDecoder::operator()( UInt_t nBytes, Address_t pBuffer, CAnalyzer& rAnalyzer ) { SomeDaqSystem::BufferHeader* pHeader =reinterpret_cast<SomeDaqSystem::BufferHeader*>(pBuffer); std::memcpy(&m_lastHeader, pHeader, sizeof(SomeDaqSystem::BufferHeader)); pHeader++;
m_pCurrentItem = reinterpret_cast<SomeDaqSystem::Header*>(pHeader); BufferTranslator* pT =
BufferFactory::CreateBuffer(pBuffer, m_lastHeader.s_signature); for (int i = 0; i < pT->TranslateLong(m_lastHeader.s_itemCount); i++) {
switch(pT->TranslateLong(m_pCurrentItem->s_type)) { case SomeDaqSystem::BEGIN: case SomeDaqSystem::END: stateChange(rAnalyzer); break; case SomeDaqSystem::SCALER:
scaler(rAnalyzer); break; case SomeDaqSystem::EVENT: event(rAnalyzer); break; default: other(rAnalyzer); break; }
std::uint8_t* pBytes = reinterpret_cast<std::uint8_t*>(m_pCurrentItem); pBytes += pT->TranslateLong(m_pCurrentItem->s_size); m_pCurrentItem = reinterpret_cast<SomeDaqSystem::Header*>(pBytes); } delete pT;
}
m_pCurrentItem
will always point to the current item.
We don't use this much so it's not worth making a
translating pointer. In the code that follows we'll use
TranslateLong
to translate 32
bit items from the data acquisition system to our
byte ordering.
TranslateLong
to ensure that
the item count inthe buffer header is translated to our
host's byte order.
TranslateLong
to ensure the
item type is expressed in our own ordering.
TranslateLong
to ensure the
size of the item is expressed in our native byte ordering.
Let's look at the state transition method. What that needs to do is:
Set the entity count to 1.
Extract the run number (remember byte order translation).
Extract the title.
Invoke the analyzer's OnStateChange
method with the appropriate item type. We're going to
provide a helper method called
translateItemType
that will
translate an item type into one of the types expected by
SpecTcl.
Example 10-9. Implementation of SomeDaqBufferDecoder::stateChange
void SomeDaqBufferDecoder::stateChange(CAnalyzer& rAnalyzer) { BufferTranslator* pT = BufferFactory::CreateBuffer(m_pCurrentItem, m_lastHeader.s_signature); SomeDaqSystem::StateChange* pItem = reinterpret_cast<SomeDaqSystem::StateChange*>(m_pCurrentItem); m_entities = 1; m_lastRunNumber = pT->TranslateLong(pItem->>s_runNumber); m_lastTitle = pItem->s_title; rAnalyzer.OnStateChange( translateItemType(pT->TranslateLong(pItem->s_header.s_type)), *this ); delete pT; }
The code above pretty much is what you'd expect to see from the specification provided.
The only differences between scaler
and stateChange
are that we
fill in the entity count from the number of scalers in the
scaler item, and there's no run number or title to extract from
the item.
Example 10-10. Implementation of SomeDaqBufferDecoder::scaler
void SomeDaqBufferDecoder::scaler(CAnalyzer& rAnalyzer) { BufferTranslator* pT = BufferFactory::CreateBuffer(m_pCurrentItem, m_lastHeader.s_signature); SomeDaqSystem::Scalers* pScaler = reinterpret_cast<SomeDaqSystem::Scalers*>(m_pCurrentItem); m_entities = pT->TranslateLong(pScaler->s_numScalers); rAnalyzer.OnScaler(*this); delete pT; }
Again I think the code is pretty well self explanatory.
For events, we're just going to set the entity count to 1
and invoke OnPhysics
. While
OnPhysics
can handle a block of events,
we're not going to try to do that in this simple example.
Example 10-11. Implementation of SomeDaqBufferDecoder::event
void SomeDaqBufferDecoder::event(CAnalyzer& rAnalyzer) { m_entities = 1; rAnalyzer.OnPhysics(*this); }
other
is similarly simple, however, once
more we need to get the item type.
Example 10-12. Implementation of SomeDaqBufferDecoder::other
void SomeDaqBufferDecoder::other(CAnalyzer& rAnalyzer) { BufferTranslator* pT = BufferFactory::CreateBuffer(m_pCurrentItem, m_lastHeader.s_signature); m_entities = 1; rAnalyzer.OnOther( translateItemType(pT->TranslateLong(m_pCurrentItem->s_type)), *this ); delete pT; }
Let's implement translateItemType
before
we turn to the getter menthods. The only choice we really need
to make is how to handle item types that are not yet defined.
For now we'll add the type to MAXSYSBUFTYPE
turning it into a user item type. In doing so, we're making the
implicit assumption item types won't be zero.
Example 10-13. Implementation of SomeDaqBufferDecoder::translateItemType
std::uint32_t SomeDaqBufferDecoder::translateItemType(std::uint32_t type) { static std::map<uint32_t, uint32_t> typeMap = { {1, BEGRUNBF}, {2, ENDRUNBF}, {3, SCALERBF}, {4, DATABF} }; auto p = typeMap.find(type); if (p != typeMap.end()) { return p->second; } else { return MAXSYSBUFTYPE + type; } }
The only thing interesting is the use of an std::map. This is an
associative container. It maps keys to values supporting an efficent
find
method. The auto keyword
declaring p
allows the compiler to infer the type
to be std::map<std::uint32_t, std::uint32_t>::iterator.
This is a pointer like object to an std::pair whose first member is a
map index and whose second is the value of the map entry associated
with that index.
Now we can look at the trivial, one-liner getter methods:
Example 10-14. Trivial getter implementations
const Address_t SomeDaqBufferDecoder::getBody() { return m_pCurrentItem + 1; } UInt_t SomeDaqBufferDecoder::getRun() { return m_lastRunNumber; } UInt_t SomeDaqBufferDecoder::getEntityCount() { return m_entities; } UInt_t SomeDaqBufferDecoder::getLamCount() { return 0; } UInt_t SomeDaqBufferDecoder::getPatternCount() { return 0; } void SomeDaqBufferDecoder::getByteOrder(Short_t& sig16, Int_t& sig32) { sig32 = m_lastHeader.s_signature; sig16 = (m_lastHeader.s_signature & 0xffff); } std::string SomeDaqBufferDecoder::getTitle() { return m_lastTitle; }
The next getters take a bit more work, but not much:
Example 10-15. Not as trivial getters:
BufferTranslator* SomeDaqBufferDecoder::getBufferTranslator() { return BufferFactory::CreateBuffer( m_pCurrentItem, m_lastHeader.s_signature ); } UInt_t SomeDaqBufferDecoder::getBodySize() { BufferTranslator* pT = getBufferTranslator(); UInt_t size = pT->TranslateLong(m_pCurrentItem->s_size); delete pT; return size; } UInt_t SomeDaqBufferDecoder::getSequenceNo() { BufferTranslator* pT = getBufferTranslator(); UInt_t seq = pT->TranslateLong(m_lastHeader.s_sequence); delete pT; return seq; } UInt_t SomeDaqBufferDecoder::getBufferType() { BufferTranslator* pT = getBufferTranslator(); UInt_t rawType = pT->TranslateLong(m_pCurrentItem->s_type); delete pT; return translateItemType(rawType); }
Next we need to write a creator for this that can be passed to
CAttachCommand::addDecoderType
. This
is pretty trivial. Almost the hardest part is to choose
a -format
value to associate with this
decoder. Let's use blockring to indicate
that these are essentially ring items but in fixed sized blocks.
Here's the header, SomeDaqDecoderCreator.h:
Example 10-16. Header for SomeDaqDecoderCreator
#ifndef SOMEDAQDECODERCREATOR_H #define SOMEDAQDECODERCREATOR_H #include <AttachCommand.h> #include <CCreator.h> class SomeDaqDecoderCreator : public CAttachCommand::CDecoderCreator { public: virtual CBufferDecoder* operator()(); virtual std::string describe() const; }; #endif
As previously described, we need to implement both pure virutal
methods. The operator()
just needs
to create a new SomeDaqBufferDecoder object and
describe
just has to return a description
of the decoder.
Here's the entire class implementation:
Example 10-17. Implementation of SomeDaqDecoderCreator
#include "SomeDaqDecoderCreator.h" #include "SomeDaqBufferDecoder.h" CBufferDecoder* SomeDaqDecoderCreator::operator()() { return new SomeDaqBufferDecoder; } std::string SomeDaqDecoderCreator::describe() const { return "Decode buffers from blocked 'ring items'"; }
In CMySpecTclApp::AddCommands
, we can then
add a line that looks like:
CAttachCommand::addDecoderType("blockring", new SomeDaqDecoderCreator);
To make the new decoder we wrote available to SpecTcl