4.3. Writing the adaptors.

Now that we have a readevt function that is divorced from Spectrodaq we need to adapt this to the RingDaq readout framework. The RingDaq readout framework uses a pair of base classes that separate event and scaler readout. We will be writing derived classes that simply delegate their functionality to the functions implemented in the skeleton.cpp file. If you are familiar with using the CTraditionalReadoutSegment and CTraditionalScalerSegment classes from the SPDAQ production readout framework to adapt it to classical readout code, you will already be familiar with this concept.

4.3.1. Wrapping skeleton.cpp in a CEventSegment

The RingDaq readout framework builds up its response to an event trigger in terms of event segments. Event segments can be simple (CEventSegment derived objects), or they can be composed of other event segments (CCompoundEventSegment). Our job in this section is going to be to build a CEventSegment that will wrap the event related functions in skeleton.cpp.

Let's start by comparing the functions in skeleton.cpp and the related methods in CEventSegment.

Our modified skeleton.cpp provides three functions that are involved in physics event processing:

initevt

Called to perform one-time initialization as data taking begins (both when the run begins and when it resumes).

clearevt

Clears digitizers so that they can accept additional triggers. This is called just after initevt as well as after each event is read.

readevt

Called to read an event in response to an event trigger. The parameter bufpt is a pointer to storage into which the event data must be placed.

By contrast, the CEventSegment provides the following methods:

initialize

Performs the same sort of initialization initevt performs, but only on the devices managed by this event segment.

clear

Similarly analagous to clearevt

disable

This method has no corresponding function in skeleton.cpp. It is called at the end of data taking and can be used to do any shutdown tasks that may be required to disable devices.

The preceeding list implies that we should write an event segment in which initialize calls initevt, clear calls clearevt, read does some adaptation and calls readevt and disable is not implemented.

The header for this sort of event segment looks like:

Example 4-7. Header for event segment adapator to readout classic


#include <config.h>
#include <CEventSegment.h>   (1)

class CTraditionalEventSegment : public CEventSegment (2)
{
public:
  void initialize();
  void clear();
  size_t read(void* pBuffer, size_t maxwords);
};
                      
                    
(1)
Includes the CEventSegment header which is needed to build a derived class.
(2)
Our event segment is derived from the CEventSegment base class.

Now let's look at the implementation and discuss how that adapts to the functions in skeleton.cpp For the most part this is pretty simple as well:

Example 4-8. Implementation for the event segment adaptor to readout classic


#include <config.h>
#include "CTraditionalEventSegment.h"
#include <stdint.h>              
#include <string>


typedef int16_t WORD;     (1)

extern void initevt ();   (2)
extern void clearevt ();
extern WORD readevt (WORD* bufpt);


(3)

void
CTraditionalEventSegment::initialize()
{
  ::initevt();
}
void
CTraditionalEventSegment::clear()
{
  ::clearevt();
}

size_t
CTraditionalEventSegment::read(void* pBuffer, size_t maxwords)
{
  WORD* p = reinterpret_cast<WORD*>(pBuffer);  (4)

  size_t nWords = ::readevt(p);                      (5)
  if (nWords > maxwords) {
    throw std::string("readevt read more than maxwords of data"); (6)
  }
  return nWords;                                    (7)
}

                    

For the most part this is very straightforward, and similar to the SPDAQ production readout wrapper class for classic event segments.

(1)
Without going through the trouble of defining a specific camac controller implementation, this is the simplest way to make the WORD data type known. WORD is intended to be a 16 bit value. stdint.h defines types like uint16_t.
(2)
Defines the functions in skeleton.cpp we will call as external. It would also be possible to define a skeleton.h header and include that instead.
(3)
initialize and clear are trivial delegations to the corresponding skeleton.cpp functions.
(4)
read and readevt have a slight impedance match in their argument signatures that needs to be dealt with. This line creates a pointer p of type WORD* so that we can pass the correct pointer type to readevt.
(5)
This calls readevt funtion saving the number of words returned.
(6)
If the number of words returned is larger than can be accomodated by the buffer we throw an exception. Exceptions of this sort are reported by the readout framework after which the program aborts. In this case, the reported failure likely results in a buffer overrun, so that is the correct action.
(7)
If all goes well the number of words is returned to the caller.

4.3.2. Wrapping skeleton.cpp in a CScaler

We must also write an adaptor taht wraps the scaler parts of skeleton.cpp in a CScaler. This too is relatively straightforward. Let's once more start by comparing the two software interfaces:

The functions the skeleton.cpp uses to manage the scaler readout are:

Scaler interface to skeleton.cpp

iniscl

Called to perform run start initialization of the scalers being managed.

clrscl

Called prior to the start of run and after each scaler readout. This function is supposed to clear all scaler counters.

CScaler methods

initialize

This method is completely analagous to iniscl.

clear

This method is completely analaogous to clrscl

disable

This is called as data taking is shut-down. Any end-run clean up actions can be performed here. This method has no corresponding entry point in skeleton.cpp and therefore need not be implemented in wrapper.

read

This method is called to read the scalers managed by a CScaler object. Unlike the readsc function CScaler objects are assumed to be managing some fixed set of scalers, and therefore know how many scalers they will read. As we will see there are two strategies you can follow for adapting to this difference.

The read method returns an std::vector<uint32_t> that contains the scaler data it has read.

From this comparison we can see it's pretty trivial to wrap iniscl and clrscl. Here is a header and the first part of the implementation of a wrapper that shows how these functions get trivially wrapped:

Example 4-9. Scaler adapter header


#include <CScaler.h>    (1)

class CTraditionalScaler : public CScaler (2)
{
public:                      (3)
  void initialize();            
  void clear();
  std::vector<uint32_t> read();
};

                    
(1)
In order to derive CTraditionalScaler from CScaler we need to make the shape of CScaler known to the compiler. This is done by including this header.
(2)
Declares our class as derived from CScaler this deriviation allows CTraditionalScaler objects to be registered with the framework as CScaler objects.
(3)
As discussed above, these are the methods we need to implement. The disable method need not be implemented as CScaler provides a default implementation that does nothing.

The implementation of the trivial wrappers is shown below along with the front matter of the implementation file.

Example 4-10. Trival methods of the scaler adapter


#include <config.h>
#include "CTraditionalScaler.h"


typedef uint16_t UINT16;   (1)
typedef uint32_t UINT32;

extern void iniscl();      (2)
extern void clrscl();
extern UINT16 readscl(UINT32* buffer, int numscalers);

void
CTraditionalScaler::initialize() (3)
{
  ::iniscl();
}

void
CTraditionalScaler::clear()     (4)
{
  ::clrscl();
}

                    
(1)
The simplest way to get these definitions without pulling in too much of the classic framwework is to make these typedefs. The first defines an unsigned 16 bit integer to be the meaning of UINT16 while the second defines an unsigned 32 bit integer to be the meaning of uint32_t. The uintxx_t types are defined in the header stdint.h which is part of the current C/C++ standard. Going forward those types should be used rather than the UINTxx types because the stdint.h types are required to be correct across all compilers.
(2)
This set of statements defines prototypes for the functions we are going to be calling in the skeleton.cpp file.
(3)
iniscl is trivially wrapped by this method.
(4)
The clrscl function is trivially wrapped by this method.

Wrapping the readscl function is a bit trickier. Specifically we have to make some decisions about how to know the number of scalers that will be read by the readscl function. We need to do this not only to be able to provide the value back to the function (its second parameter), but also to be able to allocate storage for the buffer into which readscl will read its data.

There are several strategies that come to mind:

  1. Hard code the number of scalers in CTraditionalScaler.

  2. Add a function to the skeleton.cpp code allowing it to report the number of scalers it will read.

  3. Make the scaler count a construtor parameter fo the CTraditionalScaler class.

  4. Provide some mechanism that allows both the skeleton.cpp and the CTraditionalScaler code to obtain the number of scalers from some external information (e.g. data file, Tcl Script or environment variable.

In this example we will assume that a function named numScalers has been added to the skeleton.cpp file that reports the number of scaler channesl that will be read. We leave it to you to determine how that function knows this number.

Example 4-11. Adapting the scaler readout


...
extern size_t numScalers();    (1)
...
std::vector<uint32_t>
CTraditionalScaler::read()
{
  size_t nChannels = ::numScalers();  (2)
  uint32_t scalerBuffer[nChannels];  
  std::vector<uint32_t> result;

  readscl(scalerBuffer, nChannels);    (3)
  for (int i =0; i < nChannels; i++) {
    result.push_back(scalerBuffer[i]);  (4)
  }
  return result;                         (5)
}

                    
(1)
We have assumed the existence of a function in the skeleton.cpp that can provide the number of scaler channels. This declares it so that the compiler will allow us to write a call to it.
(2)
These three lines determine how many scaler channels will be read, allocate an ordinary buffer for them and an stl vector to hold our function result.
(3)
Next the scalers are read into the ordinary buffer.
(4)
This loop transfers the scaler channel data into the vector.
(5)
Returns the vector as promised by our method interface