66.2. Writing a driver

This section describes how to extend the scripted readout framework to support additional drivers. To do this you will need to:

Note that the process for adding scaler drivers is identical except that you must extend CScriptedScalers and register your class in SetupScalers

As you read the remainder of this section, the important concept to keep in mind is that both CScriptedSegment and CScriptedScalers create a Tcl interpreter and register the module and readout commands in that interpreter. Furthermore, the module command can be thought of as a factory for actual modules.

When asked to create a new module instance, the module command does so by locating a module creator object that has been registered with a matching module type. That creator is then given the actual task of creating the module. The module itself is a Tcl Command processor with a dispatcher in the base class. The module is registered as a Tcl command with the name of the module as the Tcl command. The module name is also associated with the module object in a dictionary maintained by the module command.

66.2.1. Obtaining the scripted reaodut skeleton

The scripted readout skeleton is located in skeletons/scriptedReadout/skel relative to the top level of the nscldaq installation. If, your installation of NSCLDAQ is at /usr/opt/nscldaq/11.0 for example, the following sequence of commands will create a new directory and copy the skeleton into that directory.


mkdir development
cd    development
cp /usr/opt/nscldaq/11.0/skeletons/scriptedReadout/skel/* .
                

In the remainder of this chapter we are gonig to assume your current working directory is a directory in which the skeleton has been added.

66.2.2. Writing a module and a module creator

Writing a module and module creator is the hard work of supporting a new device. The module class supports the module and the creator makes the new device known to the module command. To illustrate the basic techniques of writing modules and their creators without referring to any specific hardware, we are going to develop a 'module' that produces a counting pattern. You will be able to configure the number of words in the counting pattern, the starting value of the pattern and the direction of the count (ascending or descending).

The key features of a module clasa are:

Note that any methods that are not required may be omitted since all methods are default in the base class to do nothing.

Armed with the knowledge above the class header is easy to write. Since we are not talking to hardware we can omit the Prepare and Clear methods:


#ifndef __COUNTERMODULE_H             (1)
#define __COUNTERMODULE_H

#ifndef __CDIGITIZERMODULE_H
#include <CDigitizerModule.h%>  (2)
#endif
class CCounterModule : public CDigitizerModule { (3)
    int  m_firstValue;
    int  m_length;                    (4)
    int  m_increment;
    
public:
    CCounterModule(const std::string& rName, CTCLInterpreter& interp); (5)
    virtual ~CCounterModule();
    
    virtual void Initialize();        
    virtual int  Read(void* pBuffer);     (6)
    virtual std::string getType() const{
       return std::string("counter");
    }
};
#endif
                

(1)
This so-called include-guard is recommended for all headers. It ensures that multiple inclusions of the header won't cause errors. The scope of the #ifndef is the entire file.
(2)
This #include is mandatory since we are going to derive a class from the CDigitizerModule that is the base class of all drivers for the scripted readout program.
(3)
As previously discussed, the driver class CCounterModule is going to be derived from the CDigitizerModule class.
(4)
The variables shown will be read from the configuration parameters at the time the Initialize method is called and sgtored for the Read method.
(6)
These are the member functions we need to comiplete the functionality of this driver.

66.2.2.1. Constructor and destructor

The destructor is a trivial empty method which allows the parent constructor to be called:


CCounterModule::~CCounterModule() {}
                    

The constructor is a bit more interesting. It must register the configuration parameters with the base class. The base class not only maintains the database of configuration values but also handles the configuration and cget subcommands that manipulate and list that data:


CCounterModule::CCounterModule(const string& rName, CTCLInterpreter& rInterp) :
    CDigitizerModule(rName, rInterp)               (1)
{
    AddIntParam("size", 10);                       (2)
    AddIntParam("start", 1);
    AddIntParam("increment", 1);
}
                    

(1)
The constructor initializes the base class passing the name of the module (rName) and the interpreter that is running the configuration script (rInterp). The base class will take care of regsitering the new command.
(2)
The AddInt base class method adds a configuration parameter to the configuration database maintained by the base class. The first parameter, size is the name of the parameter. The second the initial value. This section of code defines three integer parameter: size, start and increment set the sequence size, starting value and increment value respectively.

These are settable using the module instance's config sub-command for example:


module acounter counter
acounter config size 100 start 5 increment -1; # 5,4,3,2...
                            

Note that several data types are supported by the configuration database and automatically checked to ensure values are valid. One can also impose range limits on parameters. A real module might have configuration parameters for the base address of the module as well as the settable values of the hardware that are relevant.

66.2.2.2. The Initialize method

Initialize is invoked after the configuration script has completely run, but before event or scaler triggers are enabled.

It is pretty common practice to fetch the configuration values from the base class configuration database and where appropriate set hardware from them or save them as member variables for use by the Read method. Since we have no hardware we're going to just save the data to our member variables.


void
CCounterModule::Initialize()
{
    CIntConfigParam* pConfigItem;
    
    pConfigItem = (CIntConfigParam*)(*(Find("size"))); (1)
    m_length    = pConfigItem->getOptionValue();
    
    pConfigItem = (CIntConfigParam*)(*(Find("start")));
    m_firstValue = pConfigItem->getOptionValue();
    
    pConfigItem = (CIntConfigParam*)(*(Find("increment")));
    m_increment = pConfigItem->getOptionValue();
    
        
}
                    

(1)
This section of code gets the value of the size configuration parameter and sets it as the value of m_length. The Find base class method returns an object called a ParameterIterator. ParameterIterator supports dereferencing in a way that returns a pointer to the underlying parameter object (in this case a CIntConfigParam). That object has a getOptionValue which will return the value of an option.

66.2.2.3. The Read method

This method is what actually puts data into the buffer. It accepts a pointer to the raw buffer and returns the number of 16 bit entities it has added to the buffer.


int CCounterModule::Read(void* pEvent)
{
    uint16_t* p = reinterpret_cast<uint16_t*>(pEvent); (1)
    
    int value = m_firstValue;                                (2)
    for (int i = 0; i < m_length; i++) {                 
        *p++ = value;                                        (3)
        value += m_increment;
    }
    return m_length;                                         (4)
}
                    

(1)
Casts the pointer to the buffer to a pointer to unsigned 16 bit words. To do this requires that the implementation file #include <stdint.h> in order to define uint32_t.
(2)
Sets the first value of the output sequence from m_firstValue recall that Initialize set this from the value of the configuration variable start
(3)
Puts the sequence into the event buffer
(4)
Returns the number of 16 bit words that were put into the event buffer.

66.2.2.4. The creator

in order to be known to the module command the CCounterModue must have a corresponding creator. The creator is responsible for creating module instances with specific names. If additional parameters are supplied on the module create command line it is normal to treat them as configuration parameters.

The important parts of the header of the module creator for the counter are shown below.


#ifndef __CMODULECREATOR_H     //CModuleCreator
#include <CModuleCreator.h>                                 (1)
#endif
class CCounterCreator  : public CModuleCreator                    (2)
{	
public:
  CCounterCreator ();                                             (3)
  virtual ~CCAENV830Creator ( );


public:

   virtual   CReadableObject* Create (CTCLInterpreter& rInterp, 
				      CTCLResult& rResult,    (4)
				      int nArgs, char** pArgs)   ;  
   virtual   std::string  Help ();                                (5)

};

                    

(1)
All module creator classes must b derivced from the CModuleCreator class. This #include provides the definition of that class to the compiler.
(2)
As descdribed above, our creator, which we're going to call a CCounterCreator must be derived from the CModuleCreator base class. In general if I'm writing support for a scripted readout extension I'll call the actual driver class CxxxxModule and the creator CxxxxCreator where xxxx is some word or phrase that represents the hardware or function being supported.
(3)
Creator classes must invoke the base class constructor as we will see in the implementation file. This implies we need our own constructor to ensure the correct parameterization of the base class constructor. Any other initialization we need to do can also be added to our part of the constructor.
(4)
The key method for the creator is the Create method. It is expected to creat the correct module class and register it with the Tcl interpreter that is running the configuration script.
(5)
The module command also implements a -types keyword. When the module -types command is executed, each registered creator is askerd via its help method to contribute a string that provides the module name and a short description of the module.

The important parts of the module creator implementation are shown in the next few code fragments. Specifically we'll look at the implementation of the constructor, the Create method and the help methods.

Let's look at the constructor:


CCounterCreator::CCounterCreator() :
   CModuleCreator("counter")
{}
                    

The base class of the module creator associates a modle type string with the creator. This is used by the module command to select the appropriate creator when creating a new module. In this case we will make this creator respond to the counter module type.


 CCounterCreator::Create(CTCLInterpreter& rInterp,
                       CTCLResult& rResult,                    (1)
                       int nArgs, char** pArgs)
{

  CReadableObject* pModule = new CCounterModule(*pArgs, rInterp);  (2)
  nArgs -= 2;                   // Get rid of module name and type. (3)
  pArgs += 2;

  // If there are any remaining args, configure the mdoule:

  if (nArgs) {
    int status = pModule->Configure(rInterp, rResult,              (4)
                                    nArgs, pArgs);
    if (status != TCL_OK) {
      delete pModule;                                              (5)
      pModule = (CReadableObject*)NULL;
    }

  }
  return pModule;                                                 (6)
}
                    

(1)
The Create method is used to actually construct modules. rInterp is a reference to the Tcl interpreter that is executing the configuration script (specifically the module command). The nArgs and pArgs parameters are the number of command words and a pointer to an array of pointers to the command words that remain after the module command keyword.

Specifically: pArgs[0] points to the name of the module to create. pArgs[1] points to the module type (counter). The module command has already ensured that there are at least these two command words.

(2)
The creator must construct a new CCounterModule object. This line does so, passing in the requested module name and interpreter. Recall that the module itself registers itself as a new Tcl Command.
(3)
By convention any command line words following the module type are treated as an initial set of configuration parameters. These lines skip nArgs and pArgs over the name and type so that they point to the first of these.
(4)
You may recall the CDigitizerModule base class for all module drivers maintains a database of configuration parameters and their values. If there are any command line words remaining the creator invokes the Configure method of that base class which processes the remaining parameters and updates the configuration database.
(5)
The Configure method returns a Tcl status code which should be TCL_OK on success and TCL_ERROR on failure. If we failed to configure the module, the module is deleted and a NULL pointer is returned indicating the failure to the module command. The Configure method is assumed to have set the Tcl result to an appropriate error message string.
(6)
Finally on success, a pointer to the newly created counter instance is returned to the module command processor. The module will be put in a lookup dictionary so that it can be found given its name.

We'll omit a detailed discussion of the help method other than to say that it should return a string like: counter - Adds a counting pattern to the event

66.2.3. Extending CScriptedSegment

Recall that the Readout framework itself organizes the hardware into event segments. Each event segment is normally responsible for some part of the detector system. Event segments are added to the readout program by initialization code in the skeleton software (which we will cover in the next section).

The scripted readout framework defines a special CScriptedSegment which which handles reading the configuration file creating the appropriate driver modules, configuring them and calling them in the appropriate order in response to a trigger. CScriptedSegment will need to be extended if you have added your own module drivers. Specifically, the segment will need to register creators for your new modules with the module command so that that command both knows about the new module and has a mechanism to create instancs of it.

This section will continue the example from the last section and show how to derive a new class from the CScriptedSegment that registeres the CCounterCreator. The next section will show how to modify Skeleton.cpp so that it uses your scripted event segment instead of the base class.

When adding scaler module support, the concept is identical however you will derive your class from CScriptedScalers wich is a CScaler class that operates in a manner similiar to CScriptedSegment.

As usual, we start by looking at the important featurs of the header for CMyScriptedSegement the new scripted segment we're going to write.


// CMyScriptedSegment.h

#ifndef __CEVENTSEGMENT_H
#include <CEventSegment.h>                   (1)
#endif

class CMyScriptedSegment : public CScriptedSegment (2)
{
  virtual void addUserWrittenCreators();           (3)
};
                

(1)
This #include includes defines the CScriptedSegment which will be the base class of the CMyScriptedSegment class.
(2)
The CMyScriptedSegment is defined using the CScriptedSegment as its base class.
(3)
The CScriptedSegment base class uses the Strategy pattern. One of the strategies that is exported by the class is a method for registering user written creators via the virtual function addUserWrittenCreators.

In the base class CScriptedSegment the implementation of this function is an empty body ( {} ). Here we declare our version to override the base class version. The base class code will automatically call our addUserWrittenCreators method at the right time inthe life-cycle of the CScriptedSegment

Now let's look at the key implementation features.


#include "CMyScriptedSegment.h"                   (1)
#include "CCounterCreator.h"

void
CMyScriptedSegment::addUserWrittenCreators() {   (2)
    addCreator(*(new CCounterCreator()));        (3)
}
                

(1)
The implementation will need to include a minimum of the header for the class itself and the headers for any creators it is going to register.
(2)
As already discussed, the class we are implementing will produce an implementation that overrides the base class implementation of addCreator.
(3)
The addCreator method in the base class (CScriptedSegment) Takes a reference to a CModuleCreator object and adds it to the set of creators known by it's module command.

The construction *(new CCounterCreator()) creates a reference to a dynamically allocated CCounterCreator object.

66.2.4. Modifying Skeleton.cpp

If you have worked with the SBS readout framework, you know that everything gets knit together in the Skeleton.cpp file. Continuing the example in the previous section, we're going to point out how to modify Skeleton.cpp so that your scripted event segment CMyScriptedSegment is used as the event segment rather than the normal CScritedSegment. Doing this ensures that your hardware configuration file can use addtional module types (such as the counter type) you have added to the system.

We are going to modify the Skeleton::SetupReadout method. If you were adding scaler modules you would make very similar modifications to the Skeleton::SetupScalers

The first thing you will need to do is add an #include "MyScriptedSegment.h" directive towards the top of the headers in Skeleton.cpp I like to add #include directives at the end of those that are already there.

Below is what them modified SetupReadout should look like. Comments have been removed for the sake of brevity. The code assumes you are using a V977 coincidence register at 0x11110000 of crate 0 as both the trigger and busy module.


Skeleton::SetupReadout(CExperiment* pExperiment)
{
  CReadoutMain::SetupReadout(pExperiment);

  pExperiment->EstablishTrigger(new CV977Trigger(0x11110000));  (1)
  pExperiment->EstablishTrigger(new CV977Busy(0x11110000));
  
  pExperiment->AddEventSegment(new CMyScriptedSegment());       (2)

}
                

(1)
Sets up the trigger and busy management for the experiment to be a CAEN V977 coincidence register module.
(2)
Sets up the event segment to be the scripted segment we wrote in the previous section.

66.2.5. Modifying the Makefile

In the previous several sections, we added several source files. Not counting headers:

CCounterModule.cpp

The driver for the counting pattern 'device'.

CCounterCreator.cpp

The creator that the module command associates with the counter module type.

CMyScriptedSegment.cpp

Scripted event segment that added the CCounterCreator to the list of creators known by the scriptable event segment.

In addition we modified Skeleton.cpp but that does not require any changes to the Makefile. As with most templated NSCL makefiles there is a Makefile definition OBJECTS which is the list of object files that must be built and linked into the readout framework.

This is a whitespace separated list. Initially it looks like


    OBJECTS=Skeleton.o
                        

To incorporate our new classes, this line must be modified to read:


    OBJECTS=Skeleton.o CCounterModule.o CCounterCreator.o CMyScriptedSegment.o