3.2. An Event Processor for our packetized Readout

In this section, we will write an event processor for the packetized event source we wrote in the previous chapter. This event processor will recognize our packet id, and ignore all other packet itds. This will allow it to operate as part of the event unpacking of a larger experiment, as long as all parts of the experiment produce a packetized bunch of data. We will use this process of writing an Event processor and integrating it with SpecTcl as an example that shows how to produce an experiment specific SpecTcl.

Note

The device physicists that maintain the NSCL supported experimental devices all have and maintain event processors for the packets produced by their devices. If your experiment uses one of these devices, rather than writing an event processor for that device yourself, contact the appropriate device physicist for an event processor and integrate it into the rest of your experiment.

The process of writing an experiment specific SpecTcl is identical to that of writing an experiment specific event source:

3.2.1. Getting the Skeleton

At the NSCL we have several versions of SpecTcl installed at any given time. This provides the needed stability for analyses that are underway to complete without forcing scientists to port their software to every new version of SpecTcl as it is released. At the NSCL, SpecTcl is installed in /usr/opt/spectcl numbered subdirectories below this root identify the version of SpecTcl that is installed there, for example, /usr/opt/spectcl/2.2 is the directory in which SpecTcl version 2.2 is installed. Edit levels of SpecTcl within a specific version are supposed to be object compatible with the shared libraries that make up SpecTcl. This allows repairs of defects within a version to be deployed and the users of a specific version of the software to make use of a fix without any explicit action on their part.

A symbolic link current points to the current preferred version of SpecTcl for new development. At the time of writing this, /usr/opt/spectcl/current is a link to /usr/opt/spectcl/3.0-gcc3.x. This version 3.0 of SpecTcl built by the gcc 3.3 compiler. Version 3.0 is a bridge between versions of SpecTcl that only compiled on the gcc-2.95 compilers and those which compile on 3.0 and later. At the NSCL two versions of 3.0 are installed 3.0-gcc3.x and 3.0-gcc2.95. This allows you to port code from SpecTcl 2.2 to SpecTcl 3.0 separately from any work required to port code from gcc 2.95 to gcc 3.x and later compilers.

Within an installation of SpecTcl, the Skel directory contains skeleton files. Therefore, obtain skeleton software:

Example 3-1. Obtaining the SpecTcl Skeleton

cd ~
mkdir spectcl
cd spectcl
cp /usr/opt/spectcl/current/Skel/* .
Note that the Makefiles that are distributed with the skeleton refer to the SpecTcl directories via the actual directory names rather than through the current link so that as the current default SpecTcl changes, your Makefiles will not need to be adjusted to continue to work.

3.2.2. Writing the event processor

Event processors are expected to transform a raw event into a set of named parameters that SpecTcl, in turn can refer to to increment spectra. Event processors are registered with SpecTcl and form an orderd list or Event Processing Pipeline. Each event processor has access both to the raw event and the data that has been event processors that executed prior to it in the pipeline.

Wherever possible, later stages in the pipeline should refer to unpacked data rather than re-decoding the raw event. Not only will this result in more efficient execution, but it allows these later stages to be independent of changes in the event structure.

3.2.2.1. The base class for event processors

Event processors are classes that inherit from CEventProcessor. Event processors are registered with the SpecTcl framework and form an ordered list that is referred to as the Event Processing Pipeline.

The important part of the CEventProcessor header is shown below:CEventProcessor

Example 3-2. CEventProcessor header file main features.


    
class CEventProcessor {
 public:
  virtual Bool_t operator()(const Address_t pEvent, // (1)
			    CEvent& rEvent,
			    CAnalyzer& rAnalyzer,
			    CBufferDecoder& rDecoder);

  // Functions:
  virtual Bool_t OnAttach(CAnalyzer& rAnalyzer); // (2)
  virtual Bool_t OnBegin(CAnalyzer& rAnalyzer,
			 CBufferDecoder& rDecoder); // (3)
  virtual Bool_t OnEnd(CAnalyzer& rAnalyzer,
		       CBufferDecoder& rBuffer); // (4)
  virtual Bool_t OnPause(CAnalyzer& rAnalyzer,
			 CBufferDecoder& rDecoder); // (5)
  virtual Bool_t OnResume(CAnalyzer& rAnalyzer,
			  CBufferDecoder& rDecoder); // (6)
  virtual Bool_t OnOther(UInt_t nType,
			 CAnalyzer& rAnalyzer,
			 CBufferDecoder& rDecoder); // (7)
};

#endif

              
Note that all virtual functions have a default implementation. In most cases this implementation is to just return kfTRUE
(1)
This function is called by the framework to process a physics event. The parameters of all the virtual functions are common and will be described at the end of this discussion of each of the calls.
(2)
The life cycle of an event processor object calls for it to be created, then registered (or attached) to the framework. Once attached, SpecTcl calls its virtual function entry points at appropriate times. The OnAttach function is called at the time the object is registered as an event processor with the framework. Initialization that may not be safe at construction time can be done here, as by the time this function is called it is pretty well assured that all of SpecTcl is upand running.
(3)
This function will be called when SpecTcl detects data that corresponds to the beginning of a run. In the NSCL Data acquisition system, this means a begin run buffer has been read.
(4)
This function will be called when SpecTcl detects data that signals the end of a run. In the NSCL data acquisition system, this means an end run buffer has been read.
(5)
This function will be called when SpecTcl detects data that signals a run has been paused. Note that not all data acquisition systems support pausing a run. In the NSCL data acquisition system, pauses are signaled by a pause run data buffer.
(6)
This function will be called when SpecTcl detects data that signals a paused run has been resumed. In the NSCL data acquisition system, this means a resume run buffer has been detected.
(7)
This function will be called when SpecTcl detects data other than the data types described by the functions above. In the NSCL data acquisition system, this could be a documentation buffer e.g.

The parameters passed into most of the event processor functions are common and will be described below.

Warning

All event processor functions return a Bool_t value (kfTRUE or kfFALSE). If kfFALSE is returned, the event processing pipeline is aborted without further action. A common mistake in writing event processors is to forget to return a value. Be aware that if you do this, you will be randomly throwing out events. Using the -pedantic compilation switch can help you find errors like this.

The parameters passed to event processors may vary but they are drawn from the following elements:

Name: rAnalyzer

Type: CAnalyzer&

Meaning: A reference to the object that is controlling the overall flow of data analysis. As we will see, one thing we will need this object for is to call back into it to inform it of the size of the event we are processing in operator()

Name: rDecoder

Type: CBufferDecoder&

Meaning: A reference to SpecTcl's buffer decoder for this run. Buffer decoders know about the overall structure of data buffers from a data acquisition system. The provide services to SpecTcl and event processors to get information about the buffer as a whole. One key service they provide to operator() is knowledge of the byte ordering of the buffer. This will be used to create an object that can be treated like a pointer but transparently compensates for any byte order differences between the system that created the buffer and the system analyzing the data. Using these allows your code to run unmodified on systems regardless of their native byte ordering.

Name: rEvent

Type: CEvent&

Meaning: A reference to the unpacked data. SpecTcl processes data into histograms from a flat array like object called an CEvent. As we will see, however, the TreeParameter classes, originally introduced by Daniel Bazin, and incorporated into the supported SpecTcl code starting with version 3.0, allow you to define a structuring on top of this flat parameter space.

Name: pEvent

Type: Address_t

Meaning: Points to the raw event. The Address_t data type is a synonym for void*.

Name: nType

Type: UInt_t

Meaning: The type of date discovered when OnOther was called. This is a data acquisition system dependent value that describes the type of data that should be decoded and processed. In the NSCL data acquisition system, this is the buffer type field.

3.2.2.2. The header

We will be writing an event processor that unpacks the data from the event source created in the previous chapter. Recall that this data source produces data in a packet with id 0x8100. The body of the packet contains data from a single CAEN V775 TDC that has a geographical address (either due to its position in the VME crate or due to software assignment) of 10.

We will build a fairly general purpose event processor capable of unpacking data from any packet of that general structure. Take a look at the next example. This contains the header for our event processor:

Example 3-3. Event Processor Header


#ifndef __MYEVENTPROCESSOR_H
#define __MYEVENTPROCESSOR_H

#include <EventProcessor.h>

class MyEventProcessor : public CEventProcessor 
{
private:
  unsigned short   m_id;	// (1)
  unsigned short   m_slot;      // (2)
  unsigned short   m_first;     // (3)
public:
  MyEventProcessor(unsigned short id, 
		   unsigned short slot, 
		   unsigned short first);           // (4)

  virtual Bool_t operator()(const Address_t pEvent,
			    CEvent&         rEvent,
			    CAnalyzer&      rAnalyzer,
			    CBufferDecoder& rDecoder);  // (5)


};

#endif

                

Let's look at the key features of this header.

(1)
The m_id member variable will store the id of the packet that we will be unpacking. By making this a member variable, our unpacker can be easily told to unpack any packet id that has the same general format described above
(2)
While the packet Id should imply the slot number, we will ensure that the packet we find contains data from the correct digitizer. This sort of defensive programming makes errors much easier to ferret out. m_slot will hold the geographical address of the adc we are trying to decode.
(3)
We must unpack our data somewhere. SpecTcl expects us to unpack data into the rEvent object. rEvent is an object that acts a lot like an array. The m_first member will describe the element of this "array" that will receive the data from channel 0. We will unpack the digitzer into a block of 32 parameters starting with the one specified by m_first.
(4)
We need to provide initial values for the member data described above. Therefore we will have a constructor whose paramters provide those values.
(5)
The function call operator (operator()), is called for each event. We will write an implementation of the function call operator that will locate our packet and decode the data it contains.

Note that since we won't need to take any specific action when the event processor is attached, and we are not interested in anything but event data, we don't override any other functions of CEventProcessor.

3.2.2.3. Implementation

The first section of the implementation file is boilerplate that you will typically need for any event processor:

Example 3-4. Event processor boilerplate


#include <config.h>
#include <MyEventProcessor.h>
#include <TCLAnalyzer.h>
#include <Event.h>
#include <TranslatorPointer.h>
#include <iostream>

#ifdef HAVE_STD_NAMESPACE
using namespace std;
#endif
                    

In addition to including various headers (<config> must be included first as with event sources), symbols are imported from the std namespace if the C++ library maintains them there.

The implementation of the constructor is straightforward:

Example 3-5. Implementing the event processor constructor


MyEventProcessor::MyEventProcessor(unsigned short id,
				   unsigned short slot,
				   unsigned short first) :
  m_id(id),
  m_slot(slot),
  m_first(first)
{}

                    

The constructor just initializes its member variables from its parameters.

Let's build up the function call operator a bit at a time. The function call operator body will consist of several sections:

  • Some boilerplate that is required of all event processor unpacking functions.

  • An outer loop that will hunt for our packet in the event

  • Code to unpack the data from the digitizer when our packet is found.

3.2.2.3.1. Unpacking boilerplate

Each event processor must do some standard things:

  • Obtain a translating pointer to the event so that further processing can be byte order independent.

  • Determine the size of the event and let the analyzer know what it is so that it knows how to locate the next event to be processed.

  • Return kfTRUE if the event seems like it should be processed completely, or kfFALSE if there is evidence the event structure is invalid or corrupt.

The next example shows this boilerplate code:

Example 3-6. Event Processor boilerplate code


Bool_t
MyEventProcessor::operator()(const Address_t  pEvent,
			     CEvent&          rEvent,
			     CAnalyzer&       rAnalyzer,
			     CBufferDecoder&  rDecoder)
{
  TranslatorPointer<UShort_t> p(*(rDecoder.getBufferTranslator()), pEvent);    (1)
  CTclAnalyzer&           rAna(dynamic_cast<CTclAnalyzer&>(rAnalyzer));(2)

  UShort_t              nWords = *p;          (3)
  rAna.SetEventSize(nWords*sizeof(UShort_t)); (4)


  //   More code to be inserted here....     (5)

  return kfTRUE;                              (6)

}
                    

Let's dissect this code in some detail:

(1)
As we have said earlier, the buffer decoder is an object that is supposed to know about the overall structure of the buffer. On service the decoder is supposed to offer is knowledge of the byte ordering of the system that wrote the buffer. This line uses the getBufferTranslator() member of the buffer decodeer to obtain a TranslatorPointer object. TranslatorPointer objects act like pointers but either swap or do not swap bytes as needed depending on the byte ordering of what they point to vs. the byte ordering of the host system.
(2)
The analyzer is actually supposed to be a CTclAnalyzer. This specialized version of the CAnalyzer base class integrates some statistics with the Tcl interpreter as well as maintaining the event processing pipeline (CAnalyzer only supports a single event processor and was the analyzer used by SpecTcl prior to version 2.0. The use of dynamic_cast to convert the analyzer reference to a CTclAnalyzer reference uses C++'s Run Time Type Information (RTTI) system to determine if this cast is, in fact a valid cast (rAnalyzer must be either a reference to a CTclAnalyzer or a class ultimately derived from it for a dynamic_cast to succeed.
(3)
The first 16 bit word of each event is a word count. The word count is self inclusive. On this line, we see the translator pointer we produced earlier used as a pointer. While it is possible to *p++, with objects it is most efficient to ++p later as this will not require any additional copy constructors (needed to return the value of p prior to the increment).
(4)
One of the responsibilities of the event processing pipeline is inform the analyzer of the number of bytes in the event. This line does that. At least one event processor must do this. But which one? In order to allow you to mix and match event processors freely from experiment to experiment, a best practice is to have all event processors that work on the raw event execute the code shown in this statement.
(5)
In the next section we will fill in code to search for our packet here.
(6)
By returning true here we allow the event pipline to continue processing this event. Had we returned kfFALSE, SpecTcl would have stopped processing the event pipeline and not histogrammed the event unpacked so far.

Warning

We have issued this warning already, but it is worth stressing again, that forgetting to return kfTRUE from operator() is a common error that will result in a random fraction of events being thrown away!

3.2.2.3.2. Locating our packet

The next job of our event processor is to locate the packet whose id matches m_id. This is done by running through the packets in the event until we either find one that matches or we run out of words in the event. There is an implicit assumption in this code that the event is entirely composed of data in the form of packets (that is word count, id, body). If this is not the case your job will be harder.

Example 3-7. Event processor searching for the right packet.

This code is located just following the comment in the previous example that reads: More code to be inserted here.....


  ++p;
  Int_t words = nWords - 1;                   (1)

  while (words > 0) {                      (2)
    UShort_t packetSize = *p;
    UShort_t packetId   = p[1];           (3)

    if (packetId == m_id) {                    (4)
      // More code will go here...
    }

    p += packetSize;                          (5)
    words -= packetSize;

  }
  // Should hit the end of the event dead on:

  if (words < 0) {                         (6)
    cerr << "Warning event packet structure broke down discarding event\n";
    return kfFALSE;
  }

                    

Now lets pick this part of the code apart.

(1)
These two lines of code point p at the start of the first packet. Converting the remaining word count to an integer from an unsigned short is an important piece of defensive programming. The loop over the event will only terminate if the remaining word count becomes ≤ 0. Unsigned values can never be less than zero, so using one there would have made the loop exit condition an exact match for 0, which may not happen if the event structure is messed up.
(2)
This loop will run through the packets in the event. See the comment above about defensively programming the loop so that it must eventually exit, even in the presence of bad data.
(3)
These two statements extract the size and tag of the packet.
(4)
This if detects a packet id match. The body of the if, which unpacks the digitizer into our parameters will be written in the next section.
(5)
The addition of the packet size to the 'pointer' gets scaled so tht the 'pointer' points to the next packet. Similarly subtracting the packet size from words ensures the while loop exits when we run out of packets in the event.

This loop contains the implicit assumption that the entire event body consists of nothing but packets. If this is not the case you will need another mechanism to determine where the first packet in an event is and when you are out of packets.

(6)
The body of this if can only be executed if the event structure is badly messed up, e.g. one of the packet sizes is not correct. In this case, we emit an error message to stderr, and abort event processing by returning kfFALSE

3.2.2.3.3. Unpacking the data from our packet

Prior to unpacking the body of data from one of the CAEN digitizers, it is helpful to make some symbolic definitions. The following set of definitions appears prior to implementation of the constructor:

Example 3-8. Defining bits, masks and shift counts for data words


static const UShort_t GEOMASK(0xf800);
static const UShort_t GEOSHIFT(11);           (1)

static const UShort_t TYPEMASK(0700);
static const UShort_t TYPE_HEADER(0x0200);
static const UShort_t TYPE_DATA(0);           (2)
static const UShort_t TYPE_TRAILER(0x0400);
static const UShort_t TYPE_OBINVALID(0x0600);

static const UShort_t COUNTMASK(0x3f00);
static const UShort_t COUNTSHIFT(8);         (3)

static const UShort_t CHANMASK(0x3f);        (4)
static const UShort_t UNDERFLOWBIT(0x2000);
static const UShort_t OVERFLOWBIT(0x1000);   (5)
static const UShort_t DATAMASK(0xfff);       (6)
                        
(1)
All data longwords from the device have a Geo field in their first word. This mask and shift count allow us to extract the geo field.
(2)
All data longwords have a type field in their first word. These mask definitions allow the type to be determined.
(3)
The second word of the header longword contains a count of the number of channels of data in the event.
(4)
The channel number is located in the first word of data longwords. This mask extracts that.
(5)
These two bits UNDERFLOWBIT and OVERFLOWBIT could be set in the second word of a data longword indicating that the conversion values would be suspect.
(6)
This mask extracts the conversion value from the second word of a data longword.

Armed with these definitions, lets write the unpacking code for the digitizer:

Example 3-9. Unpacking data from the digitizer:


    if (packetId == m_id) {
      TranslatorPointer<UShort_t> body      = p+2;          (1)
      UShort_t headerHigh   = *body;
      UShort_t headerLow    = body[1];                            (2)
      body += 2;

      if ((headerHigh & TYPEMASK) != TYPE_HEADER) {
	cerr << "Expected header but got something else\n";  (3)
	return kfFALSE;
      }
      if (((headerHigh & GEOMASK) >> GEOSHIFT) != m_slot) { (4)
	cerr << "Expected our slot but didn't get it\n";
	return kfFALSE;
      }

      Int_t channelCount = (headerLow & COUNTMASK) >> COUNTSHIFT;
      do {                                                          (5)
	UShort_t dataHigh = *body;
	UShort_t dataLow  =  body[1];
	body +=2;

	if( (dataHigh & TYPEMASK) != TYPE_DATA) break;         (6)

	UShort_t channel    = dataHigh & CHANMASK;
	UShort_t conversion = dataLow  & DATAMASK;             (7)
	if ((!(dataLow & UNDERFLOWBIT))  && (!(dataLow & OVERFLOWBIT))) {
	  rEvent[m_first + channel] = conversion;
	}

	channelCount--;
      } while (channelCount >= 0);                             (8)


      if (channelCount != 0) {
	cerr << "Warning incorrect channel count!\n";       (9)
	return kfFALSE;
      }
      if ((body.getOffset() - p.getOffset())/sizef(short) != packetSize) {
	cerr << "Warning packet size incorrect\n";         (10)
	return kfFALSE;
      }
      return kfTRUE;
    }
                        

In order to defend against a number of things that can go wrong with the data structure there is a bit of complexity in the code. As usual, let's take you through this step by step.

(1)
The body of the event will immediately follow the two word header. We'll create a new translating pointer from this location because the outer loop we are fitting this code into will need p to point to the beginning of the packet when we are done, but our code \ will need a pointer that can be modified as we step through the packet body.
(2)
The first longword of the digitizer body should be a CAEN header (see figure 4.5 of the digitizer's hardware manual). This code extracts the two words of that longword. Note that the device support software places these words of the longword high order first.
(3)
Here we see a bit of defensive programming. By rights, the first longword should be a header. If it isn't this error message gets emitted and the event discarded.
(4)
Another bit of defensive programming. If somehow our object was constructed to expect a different slot than the one found in our packet, again, we emit an error messages and discard the event.
(5)
This loop will pull the channels of data out of the packet. There's a bit of complication in the loop termination which we will discuss as we describe the body and end condition of the loop. Our goal, however is to have the loop process both all he data longwords (Fig 4.6 of the hardware manual), and the trailer longword (Fig 4.7 of the hardware manual), as well as to ensure that we exit the loop even if the hardware does not deliver a proper data structure.
(6)
When the next longword extracted from the packet is not a data longword we exit the loop. The assumption is that this longword is the trailer.
(7)
On the other hand, if the longword is a properly formatted data longword, we can extract the channel number, and the conversion. If neither the overflow nor underflow bits is set, we can set the appropriate element of the rEvent array. This is where the m_first member data comes in. It holds the index of the first of 32 parameters that will be used by this digitizer.
(8)
Now the all important loop termination. There are two ways to get out of this loop. The event count becomes < 0, or we saw a non event longword. If the channel count is exactly right, the event count will drop to 0 after unpacking the last channel. Our loop will take one more pass, and the break will bounce us out of the loop with channelCount still zero.

If, on the other hand, there is no valid trailer in the data, and there happens to be a longword that looks like a data word, the loop will fall through to the termination condition again and channelCount will be -1 allowing us to detect this condition. See below.

(9)
If the channelCount is not zero, we must have either had an early non data longword, or have been missing a non data longword after seeing all the channels. Either case is an error and we write a message and discard the event.
(10)
Finally, when we are done unpacking the data, body should be pointing just past the end of the packet body. The getOffset member of TranslatorPointer objects returns the offset of the pointer relative to its underlying buffer in bytes (hence the division by sizeof(short)). If the offset between p and body is not different by the packet size, this is an error as well which is reported, and causes the event to be discarded.

3.2.2.3.4. Full function call operator code

For completeness, here's the full code of the function call operator:

Example 3-10. Full event unpacker function call operator implementation


#include <config.h>
#include <MyEventProcessor.h>
#include <TCLAnalyzer.h>
#include <Event.h>
#include <TranslatorPointer.h>
#include <iostream>

#ifdef HAVE_STD_NAMESPACE
using namespace std;
#endif


static const UShort_t GEOMASK(0xf800);
static const UShort_t GEOSHIFT(11);

static const UShort_t TYPEMASK(0x0700);
static const UShort_t TYPE_HEADER(0x0200);
static const UShort_t TYPE_DATA(0);
static const UShort_t TYPE_TRAILER(0x0400);
static const UShort_t TYPE_OBINVALID(0x0600);

static const UShort_t COUNTMASK(0x3f00);
static const UShort_t COUNTSHIFT(8);

static const UShort_t CHANMASK(0x3f);
static const UShort_t UNDERFLOWBIT(0x2000);
static const UShort_t OVERFLOWBIT(0x1000);
static const UShort_t DATAMASK(0xfff);



MyEventProcessor::MyEventProcessor(unsigned short id,
				   unsigned short slot,
				   unsigned short first) :
  m_id(id),
  m_slot(slot),
  m_first(first)
{}


Bool_t
MyEventProcessor::operator()(const Address_t  pEvent,
			     CEvent&          rEvent,
			     CAnalyzer&       rAnalyzer,
			     CBufferDecoder&  rDecoder)
{
  TranslatorPointer<UShort_t> p(*(rDecoder.getBufferTranslator()), pEvent);
  CTclAnalyzer&           rAna(dynamic_cast<CTclAnalyzer&>(rAnalyzer));

  UShort_t              nWords = *p;
  rAna.SetEventSize(nWords*sizeof(UShort_t));


  // Locate our event packet and unpack it if we find it:

  ++p;
  Int_t words = nWords - 1;
  while (words > 0) {
    UShort_t packetSize = *p;
    UShort_t packetId   = p[1];

    if (packetId = m_id) {
      TranslatorPointer<UShort_t> body      = p+2;
      UShort_t headerHigh   = *body;
      UShort_t headerLow    = body[1];
      body += 2;

      if ((headerHigh & TYPEMASK) != TYPE_HEADER) {
	cerr << "Expected header but got something else\n";
	return kfFALSE;
      }
      if (((headerHigh & GEOMASK) >> GEOSHIFT) != m_slot) {
	cerr << "Expected our slot but didn't get it\n";
	return kfFALSE;
      }
      Int_t channelCount = (headerLow & COUNTMASK) >> COUNTSHIFT;
      do {
	UShort_t dataHigh = *body;
	UShort_t dataLow  =  body[1];
	body +=2;

	if( (dataHigh & TYPEMASK) != TYPE_DATA) break;

	UShort_t channel    = dataHigh & CHANMASK;
	UShort_t conversion = dataLow  & DATAMASK;
	if ((!(dataLow & UNDERFLOWBIT))  && (!(dataLow & OVERFLOWBIT))) {
	  rEvent[m_first + channel] = conversion;
	}

	channelCount--;
      } while (channelCount >= 0);


      if (channelCount != 0) {
	cerr << "Warning incorrect channel count!\n";
	return kfFALSE;
      }
      if ((body.getOffset() - p.getOffset())/sizeof(short) != packetSize) {
	cerr << "Warning packet size incorrect\n";
	return kfFALSE;
      }

    }

    p += packetSize;
    words -= packetSize;
  }
  // Should hit the end of the event dead on:

  if (words < 0) {
    cerr << "Warning event packet structure broke down discarding event\n";
    return kfFALSE;
  }

  return kfTRUE;

}


                    

3.2.2.4. Registration

Once we have created our event processor class, we must register an instance of it (an object) with SpecTcl so that it can be appended to the event processing pipeline. This is done by editing MySpecTclApp.cpp, a file that is supplied with the skeleton files you copied when we started this work.

Edit MySpecTclApp.cpp, at the top, amongst the #include directives add an #include for MyEventProcessor.h:

Example 3-11. Adding includes for MyEventProcessor.h


#include <config.h>
#include "MySpecTclApp.h"
#include "EventProcessor.h"
#include "TCLAnalyzer.h"
#include <Event.h>
#include <TreeParameter.h>
#include  "MyEventProcessor.h"
                

Locate the function CMySpecTclApp::CreateAnalysisPipeline. Edit its implementation so that it looks like the code below.

Example 3-12. Registering the event processor


void
CMySpecTclApp::CreateAnalysisPipeline(CAnalyzer& rAnalyzer)
{

    RegisterEventProcessor(*(new MyEventProcessor(0x8100, 10, 100)), "CaenRaw");
}
                
RegisterEventProcessor appends the event processor (first argument) passed to it by reference to the event processing pipeline. Once appeneded, the event processor's OnAttach function is called. The second argument is optional. If supplied, it associates a name with the event processor. The SpecTcl API allows you to dynamically manipulate the event pipeline. Once registered, the event processor can be located and manipulated by name.

Event processing pipeline manipulation functions are described in the reference material.

3.2.3. Building the code

We must modify the skeleton Makefile in order to incorporate our code into our tailored version of SpecTcl. To do this, locate the OBJECTS definition and append MyEventProcessor.o to it as shown below:


OBJECTS=MySpecTclApp.o MyEventProcessor.o
        
Once this is done simply type:

make
        
to build your tailored SpecTcl.

3.2.4. Testing the code

In this section we will test our tailored SpecTcl by attaching it to the online event source we wrote in the previous chapter. To do this, we must

3.2.4.1. Creating parameters and spectra

SpecTcl is usually configured by creating a configuration file. This configuration file can be created by hand, or through the graphical user interface (GUI). Since it can be tedious to create many spectra using the GUI this section will demonstrate the manual creation of a configuration file. When we begin to discuss the tree parameter package we will examine how to create spectra with the GUI, as these two are closely intertwined for SpecTcl 3.0 (not so with SpecTcl 3.1 which is just leaving pre-release at the time this section is being written).

SpecTcl is an extension to the Tcl/Tk scripting language. The reference material describes the commands that SpecTcl adds to the core Tcl/Tk language. In order to create parameter definitions and spectrum definitions, we need to learn about two of these commands:

parameter

Which binds a name to a numbered slot in the rEvent object. This parameter allows you to refer to parameters by name rather than by hard to remember indices. For historical reasons, the parameter command can take serveral forms. We will only use the simplest, and most modern of these.

spectrum

Which creates named spectra. The spectrum command can be quite complex, depending on the type of spectrum you are creating. In this section, however we will only create simple one and 2 dimensional spectra. For more information about the spectrum command, see the material on SpecTcl in the reference part.

3.2.4.1.1. Creating Parameters

The parameter command is used to establish a correspondence between string that is meaningful to you and an index in the rEvent object. The form of the parameter command we will be using is:


parameter name number [units]
                
Where:

name

Is the name you want to assign to the parameter

number

Is the number of the parameter you are naming. This number corresponds to an index into the rEvent array.

units

Is an optional units of measure of the parameter. This is used to label appropriate axes of spectra created on this parameter.

For example:

Example 3-13. Parameter command


parameter slot10.channel00   100 ps
                    
This example says that data in rEvent[100] will be named slot10.channel00, and that it has units of picoseconds.

One nice thing about using Tcl as a command languages is that we are not limited to just listing parameter commands. We can write a script that generates the parameters. In our example, we want to generate parameters named slot10.channel00 through slot10.channel31 which correspond to parameters number 100 through 131. Below is a script that can do this:

Example 3-14. Creating the parameter definitions


set channel0 100                             (1)
for {set i 0} {$i < 32} {incr i} {           (2)
    set param [expr $channel0 + $i]          (3)
    set name [format slot10.channel%02d $i]  (4)
    parameter $name $param ps                (5)
}

                    

If you are not familiar with Tcl programming, you may find the following discussion useful. You should also look at one or more of the Tcl resources listed at the end of the chapter before going much further with the NSCL data acquisition and analysis system as the use of Tcl/Tk is widespread within the system.

(1)
Recall that the first element of the rEvent array we are using is element 100. This element maps to channel 0 of the TDC. This statement sets a Tcl variable named channel0 to have the value 100 to reflect this decision. We could use the literal value 100 throughout the remainder of the script, but best practices are to pull magic numbers like this out of the code and turn them into symbolic constants or variables. This makes them easy to change later on if necessary, as well as making their meaning clear
(2)
This is a Tcl loop called a for loop. The Tcl for loop is very much like the C/C++ for loop. The first command parameter is executed once. The second parameter is executed before each loop pass and if false, the command completes. The third parameter is executed at the end of each loop pass, and the fourth parameter (multiple lines in this case) is the script that is executed for each pass of the loop (loop body).
(3)
This command calculates the parameter number occupied by channel i. Several basic Tcl features are demonstrated by this command:

  • Text enclosed by [] is treated as a command that is executed, and whose result is substituted in place on the command line. The expr command evaluates arbitrary expressions.

  • If a variable name is preceded by a $ sign, the value of the variable is substituted in place on the command line.

(4)
The name of the parameter is calculated. The format command is similar to the C/C++ sprintf function. In this case it produces a string of the form slot10.channelnn where nn is a 2 digit number with leading zeroes if necessary, and assigns it to the name variables. This illustrates another interesting point about the Tcl language, and a point that is at the core of the philosophy of the language: Everything can be treated as a string.
(5)
Finally the a parameter is created for each pass through the loop. Since the parameter command is just another Tcl command as far as the command interpreter is concerned, all substitution rules apply to it as well. In fact, the alert reader will have noticed that we could have made the entire loop body the more succinct, but more confusing:

parameter [format slot10.channel%02d $i] [expr $channel0 + $i] ps
                            

That we didn't points up another best practice: Write programs to be executed by computers but read by people.

3.2.4.1.2. Creating the spectra

The spectrum command creates spectra. The general form of this command is:


spectrum name type parameters axis-specs
                
Where:

name

Is the name of the spectrum we are creating.

type

Is the type of spectrum we are creating. SpecTcl provides a rich variety of spectrum types. See the reference material for more information about spectrum types. For this discussion we only need to know that type 1 means a 1-d spectrum and type 2 means a 2-d spectrum.

parameters

Spectra are histograms defined on a set of parameters. Each spectrum type requires one or more parameters. The parameters are specified as a Tcl list. A Tcl list is a string of elements separated by whitespace. Elements which themselves have whitespace must be quoted either with "" or {}. Lists can be produced as single parameter either by using the list command or by the same quoting mechanims used to specify a list element with whitespace. In fact another core piece of Tcl philosophy is that statements are Tcl lists where the first element is the command being performed and the remaining elements are parameters to the command.

axis-specs

Specifies the axes of the spetrum. Spectra have one or more specifiable axes. axis-specs is a Tcl list. Each element of that list is in turn a three element list whose elements are the low and high limits and the number of channels into which that range is subdivided. The low and high limits are inclusive.

A sample spectrum definition for a 1-d spectrum that histograms channel 0 of the tdc is shown below:


spectrum slot10.channel00  1 slot10.channel00 {{0 4095 4096}}
                
Note the need for double {}'s the inner brackets make the single axis specification a list element of the list that is quoted by the outer {}'s.

Now we can modify our earlier example to make a 1-d spectrum for each parameter. In addition, just to show how to build 2-d spectra, we'll build a 2-d spectrum where the x axis is channel 0 and the y axis is channel 1. The 2-d spectrum will cover the full parameter range, but only have 512 bins per channel on each axis.

Example 3-15. Adding spectrum creation to the configuration file


set channel0 100
for {set i 0} {$i < 32} {incr i} {
    set param [expr $channel0 + $i]
    set name [format slot10.channel%02d $i]
    parameter $name $param ps
    spectrum  $name 1   $name  {{0 4095 4096}}    (1)
}

spectrum slot10.channel00-vs-slot10.channel01 2 \
         {slot10.channel00 slot10.channel01}    \  (2)
         {{0 4095 512} {0 4095 512}}
sbind -all                                         (3)
                
(1)
This command creates the 1-d spectrum for the parameter that was created in the previous command. The first occurence of $name is the spectrum name, the second occurance is the single parameter required by a 1-d spectrum. Note that the spectrum type is 1.
(2)
This command creates the 2-d spectrum we promised. Note how there are two parameters, and two axis specifications. If a line ends in a backslash "\", the command is continued on the next line.
(3)
The sbind command describes which of the spectra SpecTcl accumulates are displayable. While SpecTcl's spectrum storage is only limited by avaialble virtual memory, Xamine views spectra from a fixed sized spectrum storage region. sbind determines which of these spectra are loaded into that region. sbind -all attempts to load all defined spectra into that region.

3.2.4.2. Attaching the software online

To analyze data, SpecTcl must be connected to a data source. A data source is something that can provide event data. SpecTcl understands three data source types, however only two of them are in typical use in "modern times":

File

The data source is a file that contains event data

Pipe

The data source is a program whose standard output produces event data

The attach command is used to specify a event source for SpecTcl. SpecTcl will only analyze data from a single event source at a time. Once attached, the start and stop commands start and stop data taking. Reaching the end of an event source (e.g. end of file on a file event source) automatically stops analysis. In the online environment, it is a bad idea to stop data analysis as that can lead to system buffer exhaustion, which in turn will bring data taking to a halt. SpecTcl will not allow you to issue the attach command while data analysis is in progress on the current event source, however.

The attach command is rather complex, but we will only show two simple forms of it at this time. See the reference material for more information. To attach to a file:


attach -file filename
            

To attach to a pipe data source:


attach -pipe command 
            

SpecTcl connects to the NSCL online system by using a pipe data source that is part of the NSCL DAQ system. This pipe source, spectcldaq, is a program that attaches to the data acquisition system and copies buffers received from that system to its stdout.

spectcldaq actually makes two attachments to the online system:

  • An unreliable sink for event data. Unreliable means that if spectcldaq is not ready to receive an event data when it becomes available, it will skip buffers until it is ready.

  • A reliable sink for control and scaler data. Reliable beans that if necessary, the data acquisition system will stop taking data to let the consumer catch up with control and scaler processing.

To attach to the online system, typically the following commands are issued either by hand or attached to a button on a custom GUI:


attach -pipe /usr/opt/daq/current/bin/spectcldaq tcp://hostname:2602

            
Where hostname is the name of the host that is taking data.

Remember that analysis must be stopped, and that analysis will not start until the start command is issued.

The code below adds a button to the bottom of the button strip in SpecTcl that will attach to the online system of thechad.nscl.msu.edu and strt analyzing data.

Example 3-16. Minimal gui for attaching online


proc online host {                                        (1)
    set url tcp://$host:2602/
    catch stop                                            (2)
    attach -pipe /usr/opt/daq/current/bin/spectcldaq $url (3)
    start                                                 (4)
}

button .onl -text {Attach online} -command [list online thechad] (5)
pack   .onl
            

(1)
The proc command creates a re-usable procedure In Tcl a procedure is called in the same way a command is invoked. Therefore defining a procedure is indistinguishable from adding a command to the interpreter. The proc command takes three parameter; the name of the procedure, a list of formal parameters,and the body of the procedure.
(2)
Using the catch command allows execution of the procedure body to continue even in the event of an error in executing the stop command. In this case, the most common possible error is that the analysis is already stopped.
(3)
This command connects SpecTcl to the spectcldaq pipe data source specifying the url generated from the host parameter.
(4)
Starts data taking.
(5)
Creates and displays a button labelled Attach online that, when clicked, executes the online procedure. Note the use of the list to build up the command as a list of command elements. You will need to modify this command to select the host you are using for data taking as thechad is a test box at the nscl.