This section describes how to extend the scripted readout framework to support additional drivers. To do this you will need to:
Obtain a copy of the scripted readout skeleton and makefile.
Write a module class and a creator for it.
Derive a class from CScriptedSegment overriding the
addUserWrittenCreators
method to
register your own creators.
Modify Skeleton.cpp to use your CScriptedSegment in place
of the base class in SetupReadout
Modify the Makefile as needed to add your code to the build.
Build and use your tailored Scripted readout.
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.
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.
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.
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:
They are derived from CDigitizerModule
They implement an Initialize
That set up the module in accordance with the value of
cofiguration options registered for the module at
construction time. This initialization is performed
once at the beginning of each bout of data taking
(begin or resume).
They implement a Prepare
method which
prepares the module for an event (both the first one
and subsequent events after the first one). This method
is called both at the start of data taking and after
acquiring an event.
They implement a Read
method
which places data from the device into the event buffer
in response to a trigger.
They implement a Clear
method
which clears any data buffered in the digitizer. This
is called bouth at the start of a bout of data taking
and after an event has been read. In all cases,
Clear
is called prior to
Prepare
They implement a getType
method
which returns a string documenting the type of module
they 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 #define __COUNTERMODULE_H #ifndef __CDIGITIZERMODULE_H #include <CDigitizerModule.h%> #endif class CCounterModule : public CDigitizerModule { int m_firstValue; int m_length; int m_increment; public: CCounterModule(const std::string& rName, CTCLInterpreter& interp); virtual ~CCounterModule(); virtual void Initialize(); virtual int Read(void* pBuffer); virtual std::string getType() const{ return std::string("counter"); } }; #endif
CDigitizerModule
that is
the base class of all drivers for the scripted readout
program.
CCounterModule
is going to be
derived from the CDigitizerModule
class.
Initialize
method is called and sgtored for the Read
method.
The destructor is a trivial empty method which allows the parent constructor to be called:
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) { AddIntParam("size", 10); AddIntParam("start", 1); AddIntParam("increment", 1); }
rName
)
and the interpreter that is running the configuration
script (rInterp
). The base
class will take care of regsitering the new command.
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:
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.
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"))); m_length = pConfigItem->getOptionValue(); pConfigItem = (CIntConfigParam*)(*(Find("start"))); m_firstValue = pConfigItem->getOptionValue(); pConfigItem = (CIntConfigParam*)(*(Find("increment"))); m_increment = pConfigItem->getOptionValue(); }
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.
Read
methodThis 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); int value = m_firstValue; for (int i = 0; i < m_length; i++) { *p++ = value; value += m_increment; } return m_length; }
m_firstValue
recall
that Initialize
set this
from the value of the configuration variable
start
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> #endif class CCounterCreator : public CModuleCreator { public: CCounterCreator (); virtual ~CCAENV830Creator ( ); public: virtual CReadableObject* Create (CTCLInterpreter& rInterp, CTCLResult& rResult, int nArgs, char** pArgs) ; virtual std::string Help (); };
CModuleCreator
class. This
#include provides the definition
of that class to the compiler.
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.
Create
method. It is expected
to creat the correct module class and register it with the
Tcl interpreter that is running the configuration script.
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:
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, int nArgs, char** pArgs) { CReadableObject* pModule = new CCounterModule(*pArgs, rInterp); nArgs -= 2; // Get rid of module name and type. pArgs += 2; // If there are any remaining args, configure the mdoule: if (nArgs) { int status = pModule->Configure(rInterp, rResult, nArgs, pArgs); if (status != TCL_OK) { delete pModule; pModule = (CReadableObject*)NULL; } } return pModule; }
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.
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.
nArgs
and
pArgs
over the name and type
so that they point to the first of these.
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.
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.
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
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> #endif class CMyScriptedSegment : public CScriptedSegment { virtual void addUserWrittenCreators(); };
CScriptedSegment
which will be the base class of the
CMyScriptedSegment
class.
CMyScriptedSegment
is defined
using the CScriptedSegment
as its
base class.
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" #include "CCounterCreator.h" void CMyScriptedSegment::addUserWrittenCreators() { addCreator(*(new CCounterCreator())); }
addCreator
.
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.
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.
In the previous several sections, we added several source files. Not counting headers:
The driver for the counting pattern 'device'.
The creator that the module command associates with the counter module type.
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
To incorporate our new classes, this line must be modified to read: