6.3. Connecting scripts and your event processors.

In the previous section, we've seen Tcl scripting used to create a simple GUI for SpecTcl using Tk. In this section we're going to explore how to connect Tcl Scripts with your compiled code (normally event processors).

There are two approaches to linking SpecTcl C++ code and Tcl scripts: You can share data (variables) between Tcl and C++ or you can add commands to SpecTcl/Tcl that you execute in your C++ code. We are going to look at both approaches by lookint at the problem of providing a linear calibration that turns a raw parameter into a calibrated parameter. We'll look at the following specific techniques:

6.3.1. Using Tcl variables to communicate with C/C++ code

In our toy example, we need to provde and offset and a slope to calibrate a single parameter. This section will show how to obtain the values of two variables named slope and offset defined in Tcl scripts at the C++ level.

NoteNOTE
 

Before you think hard about extending and generalizing this code for your own use, you might check out the calibrated parameter SpecTcl plugin.

We're going to show two ways to do this. First we'll ask Tcl to return the value of the variables. Second we'll link the variables to C++ variables. The key to all of this are the following classes:

SpecTcl

This class provides and application programming interface (API) that provides access to SpecTcl objects of interest. For this example, and the subsequent examples, we will need this class's getInterpreter method.

Note that SpecTcl is a singleton object. This means there can be only one. The constructor for SpecTcl is private. To get access to the single instance, you must use the getInstance static method of the class.

CTCLInterpreter

SpecTcl (and NSCLDAQ) include a C++ wrapping of much of the Tcl API. This wrapping is called Tcl++. The CTCLInterpreter, is a wrapping of a Tcl_Interp, or Tcl interpreter, object.

SpecTcl::getInterpreter returns a pointer to a CTCLInterpreter object.

CTCLVariable

This class encapsulates a Tcl variable, or array and provides services for accessing, linking and, if derived, tracing the variable.

See the reference guides for detailed information about these classes.

Suppose we are writing a new event processor that will compute the calibrated parameter from the raw parameter. We're going to have a class definition for that event processor that looks like this:

Example 6-4. Calibration event processor


#ifndef CALIB_H
#define CALIB_H

#include <EventProcessor.h>
#include <TreeParameter.h>


class Calibrator : public CEventProcessor
{
private:
  CTreeParameter  m_raw;
  CTreeParameter  m_calibrated;
public:
  Calibrator();
  Bool_t operator()(const Address_t pEvent, CEvent& rEvent,
                    CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder);
private:
  double getSlope();                                      
  double getOffset();

};
#endif
                        
                    

If you've gone through the sections on writing event processors, this should be nothing new. We've added:

The constructor is pretty simple. Assuming the raw parameter is named raw and is from a 4K ADC e.g. and the calibrated parameter is named calibrated and represents energy in KeV between 0 and 2,000;

Example 6-5. Calibrated parameter constructor


#include <config.h>
#include "Calib.h"
...
Calibrator::Calibrator() :
  m_raw("raw", 4096, 0.0, 4095.0, "arbitrary"),
  m_calibrated("calibrated", 4096, 0.0, 2000, "KeV")
{}

                    

Let's look at the event processor. It will be registered in the event processing pipeline after whatever does the raw unpacking and produces the raw parameter. That means that our event processor has access to the raw parameter in the tree parameter rather than having to decode it again from the data:

Example 6-6. Calibrated parameter computatino:


Bool_t
Calibrator::operator()(const Address_t pEvent, CEvent& rEvent,
                    CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder)
{
  if (m_raw.isValid()) {
    m_calibrated = getSlope() * m_raw + getOffset();
  }
  return kfTRUE;
}

                    

Pretty simple. Note that we can only compute the calibrated parameter if the raw parameter has a value for this event. That's what the if statement checks.

Let's look at the getSlope method.

Example 6-7. Getting the slope from a Tcl variable:


#include <SpecTcl.h>
#include <TCLInterpreter.h>
#include <TCLVariable.h>
#include <cstdlib>
...
double
Calibrator::getSlope()
{
  SpecTcl* pApi            = SpecTcl::getInstance();  (1)
  CTCLInterpreter* pInterp = pApi->getInterpreter();  (2)

  CTCLVariable slopeVar(pInterp, "slope", kfFALSE);   (3)
  const char*  slopeString = slopeVar.Get();          (4)
  double slope;

  if (slopeString) {
    slope = std::atof(slopeString);                  (5)
  } else {
    slope = 1.0;
  }

  return slope;
}

                    

If you're not familiar with the classes being used some of this might be a bit confusing, so let's pick it apart step by step:

(1)
Gets a pointer to the SpecTcl API. Recall that the SpecTcl class implements a singleton object. For more on the Singleton design pattern see e.g.: https://msdn.microsoft.com/en-us/library/ee817670.aspx.

Singleton's don't have public constructors. They have a method (in our case getInstance) that returns a poiner to the one allowed instance of that class.

(2)
getInterpreter() returns a pointer to the Tcl++ object that wraps the interpreter that SpecTcl uses for command processing. This is the interpreter in which the variables we are interested in will be defined. Note that applications can, and often do, instantiate more than one interpreter so it's important to do this to get the right one.
(3)
CTCLVariable wraps a variable that lives in Tcl scripts. Note that this constructor provides an interpreter in which the variable is defined. The last parameter of the constructor is a flag that indicates whether or not we want to trace the variable as it changes. Tracing is beyond the scope of this manual.

Note that creating an object like this does not ensure the variable exists, nor does it create it if it does not. This will be important to know in the lines of code that follow.

(4)
The Get method returns the string representation of the variable's value. Recall that in Tcl all objects have a string representation. Not shown are a set of flags that are passed to Get. By default, these flags look only at global variables. That's what we want.

Previously I said that the creation of an instance of CTCLVariable does not gaurantee the existence of that variable. It may never have been given a value. If the variable has not been given a value, then the return value from Get is a null pointer.

(5)
If the return value from Get was not a null pointer it is decoded as a double. This code assumes the string is a value representation of a floating point value. production code might instead use std::strtod which allows the caller to determine if the conversion succeeded.

If the variable has not yet been set, the return value from Get is null and a default value of 1.0 is returned.

The code for offset is pretty much identical. As an exercise, create a method getDouble and rewrite getSlope and getOffset in terms of it.

Example 6-8. Getting the offset from a Tcl variable


double
Calibrator::getOffset()
{
  SpecTcl* pApi            = SpecTcl::getInstance();
  CTCLInterpreter* pInterp = pApi->getInterpreter();

  CTCLVariable offVar(pInterp, "offset", kfFALSE);
  const char*  offString = offVar.Get();
  double offset;

  if (offString) {
    offset = std::atof(offString);
  } else {
    offset = 0.0;
  }

  return slope;
}
                        
                    

The only difference from getSlope is the name of the variable we care about and the default value.

The code shown in these examples is perfectly fine. It will work. It does, however require that for each event with a raw parameter the Tcl interpreter has to give us the variable values and we need to convert those string values into doubles. We can use Variable Linking to speed this up.

Tcl allows us to associate a script variable with a C/C++ variable. When you do this, you specify the underlying type of the variable and Tcl constrains values to valid values for that type. To access the value of a Tcl variable that is linked, you just need to get the value of the local C/C++ variable.

To make all of this work for use we'll:

Let's look at the changes to the header:

Example 6-9. Calibrator header with linked variables:


#ifndef CALIB_H
#define CALIB_H

#include <EventProcessor.h>
#include <TreeParameter.h>


class Calibrator : public CEventProcessor
{
private:
  CTreeParameter  m_raw;                   
  CTreeParameter  m_calibrated;            

  double          m_slope;
  double          m_offset;
public:
  Calibrator();
  virtual ~Calibrator();
  Bool_t operator()(const Address_t pEvent, CEvent& rEvent,
                    CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder);
private:
  double getSlope();
  double getOffset();

};


#endif

                    

The three lines added to the header are emphasized above.

The constructor must be modified to link the variables to the Tcl Variables. Note that the code also ensures there are initial values for the variables that make sense:

Example 6-10. Constructor for linked variables


Calibrator::Calibrator() :
  m_raw("raw", 4096, 0.0, 4095.0, "arbitrary"),
  m_calibrated("calibrated", 4096, 0.0, 2000, "KeV")
{
  SpecTcl* pApi = SpecTcl::getInstance();
  CTCLInterpreter* pInterp = pApi->getInterpreter(); (1)

  CTCLVariable slope(pInterp, "slope", kfFALSE);
  m_slope = 1.0;                                     (2)
  slope.Link(&m_slope, TCL_LINK_DOUBLE);         (3)

  CTCLVariable offset(pInterp, "offset", kfFALSE);
  m_offset = 0.0;                                    (4)
  offset.Link(&m_offset, TCL_LINK_DOUBLE);

}

                    
(1)
This code should be familiar by now. We use the SpecTcl API singleton to get a pointer to the SpecTcl command interpreter.
(2)
When the variable is linked to a C/C++ variable it is given the current value of that variable. This gives the slope a default value of 1.0.
(3)
This call links the slope variable to m_slope forcing it to be a double.
(4)
Similarly the offset is linked with the default value of 0.0

If our event processor is destroyed, referencing the Tcl variables slope and offset will likely cause segmentation faults. We therefore need code to undo the link at destruction time:

Example 6-11. Destructor for linked variable calibrator


Calibrator::~Calibrator()
{
  SpecTcl* pApi = SpecTcl::getInstance();
  CTCLInterpreter* pInterp = pApi->getInterpreter();

  CTCLVariable slope(pInterp, "slope", kfFALSE);
  slope.Unlink();

  CTCLVariable offset(pInterp, "offset", kfFALSE);
  offset.Unlink();

}
                    

Much of this code should be understandable. The Unlink removes any link the variable may have with a C/C++ variable.

By using get getSlope() and getOffset() methods, the operator() method body is unchanged. The two getters, however become significantly simpler:

Example 6-12. Getting slope and offset for linked variables


double
Calibrator::getSlope()
{

  return m_slope;
}

double
Calibrator::getOffset()
{

  return m_offset;
}

                    

No explanation is needed.

6.3.2. Using the Treevariable subsystem

In the previous section, we looked at how to get data from a Tcl variable using the SpecTcl and Tcl++ APIs to both SpecTcl and the Tcl interpreter. We used values stored in Tcl variables to compute a calibrated parameter.

In this section, we will introduce the treevariable subsystem. Daniel Bazin first designed and implemented this system as an extension to SpecTcl. Over time the code migrated into a contributed subsystsem and now, is a fully supported part of SpecTcl.

The idea of tree variables is similar to that of tree parameters. These object provide a hierachical namespace of variables. Each tree parameter can be treated as if it were a double. Tree parameters, in addition to having a value, have units of measure. A tree variable array class allows you to aggregate several tree parameters in to a single object.

Tree parameters are implemented as C/C++ variables that are linked to Tcl variables. In addition to the C/C++ variable they have units of measure.

We will use tree variables to manage a set of calibration parameters. We'll assume we have a treeparameter array of 16 elements that are called raw.0 ... raw.15. Associated with these 16 parameters are two tree variables of calibration constants, one, slopes is a tree variable array of 16 sloops while the other, offsets is a tree variable array of calibration offsets.

Note that by adding a second level of indirection, as with Tree parameters, more than one tree variable can be associated with a single Tcl variable. Thus the mapping of Tcl variables to tree parameters is one to many.

Let's look at a header for the calibrator we've described using tree parameters and tree variables.

Example 6-13. Header for calibrator using treevariables


#ifndef TREECALIB_H
#define TREECALIB_H
#include <EventProcessor.h>
#include <TreeParameter.h>

class TreeCalibrator : public CEventProcessor
{
private:
  CTreeParameterArray m_raw;
  CTreeParameterArray m_calibrated;
  CTreeVariableArray  m_slope;
  CTreeVariableArray  m_offset;

public:
  TreeCalibrator();
  Bool_t operator()(const Address_t pEvent, CEvent& rEvent,
                    CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder);

};

#endif

                    

In addition to tree parameter arrays with the raw and calibrated parameters, we have tree variable arrays for the calibration values. The constructor must instantiate these properly. We're going to assume again, that the raw parameters come from a 12 bit digitizer of some sort.

Here's the constructor:

Example 6-14. TreeCalibratorConstructor


^Cfox@charlie:~/test/sample$ cat TreeCalib.cpp
#include <config.h>
#include "TreeCalib.h"

TreeCalibrator::TreeCalibrator() :
  m_raw("raw", 12, 16, 0),
  m_calibrated("calibrated", 12, 16, 0),
  m_slope("slope", 1.0, "KeV/channel", 16, 0),
  m_offset("offset", 0.0, "KeV", 16, 0)
{}

                    

The form of the tree parameter constructor we are using says that the parameters are 12 bits wide (go from 0 through 4095). The tree variable array initializers create 16 slopes initialized to 1.0 and 16 offsets initialized to 0. The parameters will be calibrated in KeV.

Using Tree parameters and tree variables makes the operator() trivial to write:

Example 6-15. TreeCalibrator::operator()


Bool_t
TreeCalibrator::operator()(const Address_t pEvent, CEvent& rEvent,
                           CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder)
{
  for (int i = 0; i < 16; i++) {
    if (m_raw[i].isValid()) {
      m_calibrated[i] = m_raw[i] * m_slope[i] + m_offset[i];
    }
  }
  return kfTRUE;
}

                    

The really cool thing about tree parameters and tree variables from the programmer's point of view is that they mimic ordinary doubles and that arrays mimic ordinary arrays.

6.3.3. Adding new compiled commands to Tcl

We'll continue to use our calibrated parameter as an example for how to add commands to SpecTcl. We've already seen that using tree parameters and tree variables provides a truly elegant solution.

Going back to our example of a single calibrated parameters: We're going to add a command called setcalib. setcalib accepts two double parameters. The first is the slope and the second the calibration offset.

This example is a bit involved:

6.3.3.1. The Event processor

The header of the event processor look fairly normal:

Example 6-16. Event processor for command steered calibration


#ifndef CMDCALIBRATION_H
#define CMDCALIBRATION_H

#include <EventProcessor.h>
#include <TreeParameter.h>

class CmdCalibrator : public CEventProcessor
{
private:
  CTreeParameter m_raw;
  CTreeParameter m_calibrated;
  double         m_slope;
  double         m_offset;
public:
  CmdCalibrator();
  Bool_t operator()(Address_t pEvent, CEvent& rEvent,
                    CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder);

  void setSlope(double slope);
  void setOffset(double offset);
};


#endif

                        

The constructor, in addition to constructing the tree parameters must initialize the sllope and offset to reasonable default values:

Example 6-17. Constructor for command steered calibration event proc.


CmdCalibrator::CmdCalibrator() :
  m_raw("raw", 4096, 0.0, 4095.0, "arbitrary"),
  m_calibrated("calibrated", 4096, 0.0, "KeV"),
  m_slope(1.0),
  m_offset(0.0)
{}


                        

The remainder of the event processor is equally trivial:

Example 6-18. Command drive event processor implementation


Bool_t
CmdCalibrator::operator()(Address_t pEvent, CEvent& rEvent,
                          CAnalyzer& rAnalyzer, CBufferDecoder& rDecoder)
{
  if (m_raw.isValid()) {
    m_calibrated = m_slope * m_raw + m_offset;
  }
}

void CmdCalibrator::setSlope(double slope) {
  m_slope = slope;
}
void CmdCalibrator::setOffset(double offset) {
  m_offset = offset;
};

                        

6.3.3.2. The command processor

CTCLObjectProcessor is the base class for all Tcl++ command processors based on Tcl_Obj* parameter lists. Tcl_Obj* parameters are, however, wrapped in CTCLObject objects which can do most of the type conversions needed.

Our command processor will contain a CmdCalibration event processor reference and will operate on the slope and offset in that object. This means that our header will look something like this:

Example 6-19. setcalib command processor header:


#ifndef SETCALIBCOMMAND_H
#define SETCLIBCOMMAND_H

#include <TCLObjectProcessor.h>                      (1)
class CmdCalibrator;                                       (2)

class SetCalibCommand : public CTCLObjectProcessor         (3)
{
private:
  CmdCalibrator&   m_eventProcessor;                   (4)

public:
  SetCalibCommand(CTCLInterpreter& interp, CmdCalibrator& evp); (5)

  int operator()(CTCLInterpreter& interp, std::vector<CTCLObject>& objv); (6)
};


#endif

                        
(1)
All Tcl command processors that use Tcl++ should be derived from the CTCLObjectProcessor base class. This header provides the definition of that class needed by derived classes.
(2)
This is a forward, opaque definition of our CmdCalibrator event processor. We could also include the CmdCalibration.h header. We do not, however, need the compiler to know the actual shape of the CmdCalibrator class when it is compiling the header as we're going to hold a reference to the actual event processor object rather than the object itself.

Stylistically and practically, if you can get away with opaque forward definitions like this you should. In addition to the slight increase in compilation speed (the compiler doesn't have to include and compile the header at this time), there are circular definition problems that can be side stepped by using opaque forward references.

For example, suppose I have a class A that holds a reference to class B and class B holds a pointer to class A. There's no way to use header includes to resolve the need to define A to B's and B to A's header. Forward references such as the one shown above resolve this problem.

(3)
Our class, SetCalibCommand is derived, as promised from CTCObjectProcessor.
(4)
This member declaration provides storage for a refrence to the event processor we're configuring.
(5)
The construtor must store the reference to the event processor. As we'll see in the implementation, the Tcl interpreter on which the command is going to be registered must be passed to the CTCLObjectProcessor constructor. Those two constraints determine the argument signature for the constructor.

The command name is going to be hard coded in the implementation's call to the CTCLObjectProcessotr constructor. Note that it's more usual to allow the creator of the object to supply the command name to the constructor as this allows the application control over the command, and the namespace it might live in.

We hard code the command name for simplicity.

(6)
The base class arranges for the virtual method operator() to be invoked when the command is executed. That method is overriden by specific command processors to provdee argument processing and the logic of the command.

The operator() method is passed a reference to the interpreter and a reference to the vector of command words encapsulated in CTCLObject objects. Note that the command words include the command name word itself (objv[0]).

Let's have a look at the constructor. It's quite simple. The base class constructor does all the work of registering the command with Tcl and arranging for the operator() to be called inside a try/catch block when the command is executed:

Example 6-20. setcalib command constructor:


#include <config.h>

#include "SetCalibCommand.h"
#include "CmdCalibration.h"                (1)
#include <TCLInterpreter.h>
#include <TCLObject.h>

SetCalibCommand::SetCalibCommand(CTCLInterpreter& interp, CmdCalibrator& evp) :
  CTCLObjectProcessor(interp, "setcalib", true),  (2)
  m_eventProcessor(evp)                           (3)
{}

                        
(1)
These headers are included to satisfy opaque forward references in various headers.

We made an forward reference to CmdCalibrator in our header and the header for CTCLObjectProcessor makes opaque forward references to both CTCLInterpreter and CTCLObject.

We have code in this implementation that needs to know the actual shape of these classes. You need to know the shape of a class if you are going to invoke its methods or refer to public data it may have.

(2)
The base class constructor takes care of all of the stuff needed to initialize this object and, if requested, make it known to a Tcl interpreter as a command.

The base class constructor we are using, requires the interpreter, on which the object will be registered as a command processor, the verb that will activate the command and a flag indicating whether or not you want the object immediately registered.

It is possible to register a command processor object on more than one interpreter, but that's beyond the scope of this manual. See the reference material that covers Tcl++ for more information about CTCLObjectProcessor and the services it offers.

(3)
All member data that are references must be constructed in class initializers. This line makes the event processor reference we are holding refer to the same one that was passed in by reference to the constructor.

Now we turn our attention to the implementation of the command itself. Command processors provide two things back to the interpreter.

First they must return an integer status to the caller. This can be one of TCL_OK the command worked just fine. TCL_ERROR means the command detected an error and failed. Other return codes are used in command that provide flow control: TCL_RETURN indicates that the command requested a return from any proc the command was invoked in. TCL_BREAK indicates the command wants the inner loop that called this command to break out of the loop. TCL_CONTINUE means that the command wants the inner loop that called this command to skip the remainder of the loop body and start the next loop iteration (if appropriate to the loop condition).

Second, if desired, the command can have an interpreter result. Interpreter results are what will be substituted for the command if it's used inside a[] substitution. The interpreter result is also what's used as the error message in the event TCL_ERROR is returned. That's how we will use the interpreter command value as we have nothing interesting to return on success.

Here's a rough description of what our command will do:

  • Verify the number of command words is three. We need to have the command verb, the slope and the offset.

  • Extract the slope and offset from the command words.

  • Invoke the setSlope and setOffset methods of the event processor to steer its subsequent processing.

Note that we're going to centralize event handling by wrapping pretty much all of the code in a try catch block.

Example 6-21. setcalib Command processor implementation


SetCalibCommand::operator()(CTCLInterpreter& interp, std::vector<CTCLObject>> objv)
{
  int status = TCL_OK;                 (1)

  bindAll(interp, objv);               (2)
  try {
    requireExactly(                    (3)
        objv, 3,
        "setcalib needs only slope and offset params"
    );

    double slope  = objv[1];           (4)
    if (slope == 0.0) {
        throw "Slope equal to zero makes no sense";  (5)
    }
    double offset = objv[2];

    m_eventProcessor.setSlope(slope);   (6)
    m_eventProcessor.setOffset(offset);

  }
  catch (CException& e) {
    interp.setResult(e.ReasonText());
    status = TCL_ERROR;
  }
  catch (std::exception& e) {
    interp.setResult(e.what());
    status = TCL_ERROR;
  }                                     (7)
  catch (std::string msg) {
    interp.setResult(msg);
    status = TCL_ERROR;
  }
  catch (const char* msg) {
    interp.setResult(msg);
    status = TCL_ERROR;
  }
  catch (...) {
    interp.setResult("Unexpected exception type caught executing setcalib");
    status = TCL_ERROR;
  }

  return status;                     (8)

}
                            
                        
(1)
Our error handling strategy will be to wrap the chunks of code that can detect errors in try/catch blocks. Those try/catch blocks will set the return status to TCL_ERROR.

This initialization of status ensures that if no error is detected, the return code will be TCL_OK which indicates success.

(2)
CTCLObject objects can be bound or unbound to interpreters. Some processing of these objects can be done independent of an interpreter, while the underlying API requires other calls to provide an interpreter. We're just going to bind all command words to the interpreter executing the command so that it's available if we use any services that require an interpreter.

The base class, CTCLObjectProcessor provides a service method bindAll that binds an interpreter to all elements of a vector of CTCLObjects.

(3)
CTCLObjectProcessor provides several utility methods for checking that your command has been passed a valid number of parameters. requireExactly throws an std::string exception (the value of the last parameter), if the size of objv is not exactly what is specified by the second parameter.

requireAtLeast and requireAtMost are also provided to handle cases where a command may have a variable number of parameters.

Note as well that the command requires three parameters because objv[0] is always the command verb.

(4)
CTCLObject provides type conversion operators that are capable of extracting the various representations from the Tcl_Obj* it wraps. In this case the implicit type coniversion extracts the double precision floating point representation from that object.

If the required representation cannot be extracted (e.g. the slope parameter might have the value bad), a CTCLException is thrown. This exception type is derived from CException.

(5)
Here we see how error detection and handling can be turned into an exception throw. Of course a slope of zero maps all parameter values onto the offset and is unreasonable. The ability to do this error checking is about the only advantage this approach to calibration has.

Note that while we throw the exception here, it's caught a bit lower down in the code and turned into an error.

(6)
Once the calibration constants have been processed successfully, we can set them in the event processor. Having done so, on the next event, the new constants will be used to produce the calibrated parameter.
(7)
This group of catches extracts an error message from whatever exception was thrown and invokes the CTCLInterpreter setResult to set the command result as well as setting the status variable to TCL_ERROR so that the interpreter knows an error occured.

Before continuing on, an important point of exception handling is illustrated here. The principle that your catch blocks should catch as generic a base class as possible. Thus instead of catching CTCLException we catch the ultimate base class CException, similarly we catch std::exception the ultimate base class of C++ language exceptions.

The reason this is a good idea is that it insulates us as much as possible from knowing the exact exception types the code we use throws. There are cases where you must make an exception (no pun intended) to this rule.

Suppose you use an I/O package that throws a variety of objects derived from iopkgexception. Suppose that an attempt to read past end of file results in a derived type eofexception being thrown. It can make sense to catch eofexception prior to trying to catch the generic iopkgexception because you need to process an eofexception differently than a generic error.

Note as well the final catch(...) which catches unanticipated exception types and turns them into a generic error message and a return code of TCL_ERROR.

(8)
The final return code, TCL_OK if no exception is caught, or TCL_ERROR if one was. is returned to the interpreter.

6.3.3.2.1. Hooking it all together.

To make all of this work we need to:

  1. Ensure that MySpecTclApp.cpp includes the headers for our event processor and our command processor.

  2. Instantiate and register our event processor in the event procesing pipeline after the raw event decoding has been done.

  3. Create an instance of our command processor registered on the SpecTcl command interpreter and given an instance of our event processor.

  4. Make additions to the Makefile so that our event processor and our command processor get built into SpecTcl.

Including headers. Locate all of the #include directives towards the top of MySpecTclApp.h. Inser the following towards the bottom of those directives?


#include "CmdCalibration.h"
#include "SetCalibCommand.h"
                        

Instantiate and register the event processor. We're going to statically create the event processor. Registration will be done in the usual mannner in CreateAnalysisPipeline.

Just prior to CreateAnalysisPipeline create an instance of the event processor. With intervening comments at the front of that method stripped out, this will look like:


CmdCalibrator calibration;
void
CMySpecTclApp::CreateAnalysisPipeline(CAnalyzer& rAnalyzer)
{
  RegisterEventProcessor(*(new SimpleEventProcessor), "Test");
  RegisterEventProcessor(calibration, "Calibrator");

}

                        

We created this static instance of CmdCalibrator so that we can find it when we make our command processor

Creating and adding our command processor. MySpecTclApp.cpp has a method; AddCommands. That method is responsible for adding commands to the base Tcl command set.

Add a line that Creates an instance of our event processor using the interpreter that's passed in to that method. Here's what it looks like after the addition:


void
CMySpecTclApp::AddCommands(CTCLInterpreter& rInterp)
{ CTclGrammerApp::AddCommands(rInterp);

  new SetCalibCommand(rInterp, calibration);
}
                        

Note that you should never remove the call to CTclGrammerApp::AddCommands at the top of this method. That call adds SpecTcl's command set.