The areaDetector module provides a general-purpose interface for area (2-D) detectors in EPICS. It is intended to be used with a wide variety of detectors and cameras, ranging from high frame rate CCD and CMOS cameras, pixel-array detectors such as the Pilatus, and large format detectors like the MAR-345 online imaging plate.
The goals of this module are:
The architecture of the areaDetector module is shown in Figure 1.

The plugin architecture is very powerful, because plugins can be reconfigured at run-time. For example the NDPluginStdArrays can switch from getting its array data from a detector driver to an NDPluginROI plugin. That way it will switch from displaying the entire detector to whatever sub-region the ROI driver has selected. Any Channel Access clients connected to the NDPluginStdArrays driver will automatically switch to displaying this subregion. Similarly, the NDPluginFile plugin can be switched at run-time from saving the entire image to saving a selected ROI, just by changing its input source.
The use of plugins is optional, and it is only plugins that require the driver to make the image data available. If there are no plugins being used then EPICS can be used simply to control the detector, without accessing the data itself. This is most useful when the vendor API has the ability to save the data to a file.
What follows is a detailed description of the software, working from the bottom up. Most of the code is object oriented, and written in C++. The parts of the code that depend on anything from EPICS except libCom and asyn have been kept in in separate C files, so that it should be easy to build applications that do not run as part of an EPICS IOC.
Drivers and plugins each need to support a number of parameters that control their operation and provide status information. Most of these can be treated as 32-bit integers, 64-bit floats, or strings. When the new value of a parameter is sent to a driver, (e.g. detector binning in the X direction) from an asyn client (e.g. an EPICS record), then the driver will need to take some action. It may change some other parameters in response to this new value (e.g. image size in the X direction). The sequence of operations in the driver can be summarized as
The following are the public definitions in the asynPortDriver class:
#define asynCommonMask 0x00000001
#define asynDrvUserMask 0x00000002
#define asynOptionMask 0x00000004
#define asynInt32Mask 0x00000008
#define asyUInt32DigitalMask 0x00000010
#define asynFloat64Mask 0x00000020
#define asynOctetMask 0x00000040
#define asynInt8ArrayMask 0x00000080
#define asynInt16ArrayMask 0x00000100
#define asynInt32ArrayMask 0x00000200
#define asynFloat32ArrayMask 0x00000400
#define asynFloat64ArrayMask 0x00000800
#define asynGenericPointerMask 0x00001000
class asynPortDriver {
public:
asynPortDriver(const char *portName, int maxAddr, int paramTableSize, int interfaceMask, int interruptMask);
virtual asynStatus getAddress(asynUser *pasynUser, const char *functionName, int *address);
virtual asynStatus findParam(asynParamString_t *paramTable, int numParams, const char *paramName, int *param);
virtual asynStatus readInt32(asynUser *pasynUser, epicsInt32 *value);
virtual asynStatus writeInt32(asynUser *pasynUser, epicsInt32 value);
virtual asynStatus getBounds(asynUser *pasynUser, epicsInt32 *low, epicsInt32 *high);
virtual asynStatus readFloat64(asynUser *pasynUser, epicsFloat64 *value);
virtual asynStatus writeFloat64(asynUser *pasynUser, epicsFloat64 value);
virtual asynStatus readOctet(asynUser *pasynUser, char *value, size_t maxChars,
size_t *nActual, int *eomReason);
virtual asynStatus writeOctet(asynUser *pasynUser, const char *value, size_t maxChars,
size_t *nActual);
virtual asynStatus readInt8Array(asynUser *pasynUser, epicsInt8 *value,
size_t nElements, size_t *nIn);
virtual asynStatus writeInt8Array(asynUser *pasynUser, epicsInt8 *value,
size_t nElements);
virtual asynStatus doCallbacksInt8Array(epicsInt8 *value,
size_t nElements, int reason, int addr);
virtual asynStatus readInt16Array(asynUser *pasynUser, epicsInt16 *value,
size_t nElements, size_t *nIn);
virtual asynStatus writeInt16Array(asynUser *pasynUser, epicsInt16 *value,
size_t nElements);
virtual asynStatus doCallbacksInt16Array(epicsInt16 *value,
size_t nElements, int reason, int addr);
virtual asynStatus readInt32Array(asynUser *pasynUser, epicsInt32 *value,
size_t nElements, size_t *nIn);
virtual asynStatus writeInt32Array(asynUser *pasynUser, epicsInt32 *value,
size_t nElements);
virtual asynStatus doCallbacksInt32Array(epicsInt32 *value,
size_t nElements, int reason, int addr);
virtual asynStatus readFloat32Array(asynUser *pasynUser, epicsFloat32 *value,
size_t nElements, size_t *nIn);
virtual asynStatus writeFloat32Array(asynUser *pasynUser, epicsFloat32 *value,
size_t nElements);
virtual asynStatus doCallbacksFloat32Array(epicsFloat32 *value,
size_t nElements, int reason, int addr);
virtual asynStatus readFloat64Array(asynUser *pasynUser, epicsFloat64 *value,
size_t nElements, size_t *nIn);
virtual asynStatus writeFloat64Array(asynUser *pasynUser, epicsFloat64 *value,
size_t nElements);
virtual asynStatus doCallbacksFloat64Array(epicsFloat64 *value,
size_t nElements, int reason, int addr);
virtual asynStatus readGenericPointer(asynUser *pasynUser, void *pointer);
virtual asynStatus writeGenericPointer(asynUser *pasynUser, void *pointer);
virtual asynStatus doCallbacksGenericPointer(void *pointer, int reason, int addr);
virtual asynStatus drvUserCreate(asynUser *pasynUser, const char *drvInfo,
const char **pptypeName, size_t *psize);
virtual asynStatus drvUserGetType(asynUser *pasynUser,
const char **pptypeName, size_t *psize);
virtual asynStatus drvUserDestroy(asynUser *pasynUser);
virtual void report(FILE *fp, int details);
virtual asynStatus connect(asynUser *pasynUser);
virtual asynStatus disconnect(asynUser *pasynUser);
virtual asynStatus setIntegerParam(int index, int value);
virtual asynStatus setIntegerParam(int list, int index, int value);
virtual asynStatus setDoubleParam(int index, double value);
virtual asynStatus setDoubleParam(int list, int index, double value);
virtual asynStatus setStringParam(int index, const char *value);
virtual asynStatus setStringParam(int list, int index, const char *value);
virtual asynStatus getIntegerParam(int index, int * value);
virtual asynStatus getIntegerParam(int list, int index, int * value);
virtual asynStatus getDoubleParam(int index, double * value);
virtual asynStatus getDoubleParam(int list, int index, double * value);
virtual asynStatus getStringParam(int index, int maxChars, char *value);
virtual asynStatus getStringParam(int list, int index, int maxChars, char *value);
virtual asynStatus callParamCallbacks();
virtual asynStatus callParamCallbacks(int list, int addr);
virtual void reportParams();
/* asynUser connected to ourselves for asynTrace */
asynUser *pasynUser;
};
A brief explanation of the methods and data in this class is provided here. Users should look at the
example driver (simDetector) and plugins provided with areaDetector for examples of how this
class is used.
asynPortDriver(const char *portName, int maxAddr, int paramTableSize, int interfaceMask, int interruptMask);
This is the constructor for the class.
portName is the name of the asyn port for this driver or plugin.
maxAddr is the maximum number of asyn addresses that this driver supports.
This number returned by the pasynManager-> getAddr() function. Typically it is 1, but some
plugins (e.g. NDPluginROI) support values > 1. This controls the number of parameter
tables that are created.
parmTableSize is the maximum number of parameters that this driver or plugin supports.
This controls the size of the parameter tables.
interfaceMask is a mask with each bit defining which asyn interfaces this driver
or plugin supports.
The bit mask values are defined in asynPortDriver.h, e.g. asynInt32Mask.
interruptMask is a mask with each bit defining which of the asyn interfaces this driver
or plugin supports can generate interrupts.
The bit mask values are defined in asynPortDriver.h, e.g. asynInt8ArrayMask.
virtual asynStatus getAddress(asynUser *pasynUser, const char *functionName, int *address);
Returns the value from pasynManager-> getAddr(pasynUser,...).
Returns an error if the address is not valid, e.g. >= this-> maxAddr.
virtual asynStatus readInt32(asynUser *pasynUser, epicsInt32 *value);
virtual asynStatus readFloat64(asynUser *pasynUser, epicsFloat64 *value);
virtual asynStatus readOctet(asynUser *pasynUser, char *value, size_t maxChars,
size_t *nActual, int *eomReason);
These methods are called by asyn clients to return the current cached value for the
parameter indexed by pasynUser-> reason in the parameter table defined by getAddress().
Derived classed typically do not need to implement these methods.
virtual asynStatus writeInt32(asynUser *pasynUser, epicsInt32 value);
virtual asynStatus writeFloat64(asynUser *pasynUser, epicsFloat64 value);
virtual asynStatus writeOctet(asynUser *pasynUser, const char *value, size_t maxChars,
size_t *nActual);
These methods are called by asynClients to set the new value of a parameter. The
implementation of these methods in asynPortDriver copies the parameter into a cached
location for use by the asynRead(Int32, Float64, and Octet) methods. Most drivers
will provide their own implementations of these methods to do driver-dependent operations
when there is a new value of the parameter.
virtual asynStatus readXXXArray(asynUser *pasynUser, epicsInt8 *value,
size_t nElements, size_t *nIn);
virtual asynStatus writeXXXArray(asynUser *pasynUser, epicsInt8 *value,
size_t nElements);
virtual asynStatus doCallbacksXXXArray(epicsInt8 *value,
size_t nElements, int reason, int addr);
virtual asynStatus readGenericPointer(asynUser *pasynUser, void *handle);
virtual asynStatus writeGenericPointer(asynUser *pasynUser, void *handle);
virtual asynStatus doCallbacksGenericPointer(void *handle, int reason, int addr);
where XXX=(Int8, Int16, Int32, Float32, or Float64).
The readXXX and writeXXX methods only have stub methods that return an error in asynPortDriver, so they
must be implemented in the derived classes if the corresponding interface is
used. They are not pure virtual functions so that the derived class need not implement
the interface if it is not used. The doCallbacksXXX methods in asynPortDriver
call any registered asyn clients on the corresponding interface if the reason
and addr values match. It typically does not need to be implemented in derived classes.
virtual asynStatus findParam(asynParamString_t *paramTable, int numParams, const char *paramName, int *param);
virtual asynStatus drvUserCreate(asynUser *pasynUser, const char *drvInfo,
const char **pptypeName, size_t *psize);
virtual asynStatus drvUserGetType(asynUser *pasynUser,
const char **pptypeName, size_t *psize);
virtual asynStatus drvUserDestroy(asynUser *pasynUser);
drvUserCreate must be implemented in derived classes that use the parameter facilities of asynPortDriver.
The findParam method is a convenience function that searches an array of {enum, string} structures
and returns the enum (parameter number) matching the string. This is typically used in the
implementation of drvUserCreate in derived classes. drvUserGetType and
drvUserDestroy typically do not need to be implemented in derived classes.
virtual void report(FILE *fp, int details);
virtual asynStatus connect(asynUser *pasynUser);
virtual asynStatus disconnect(asynUser *pasynUser);
The report function prints information on registered interrupt clients if details > 0, and prints
parameter table information if details > 5. It is typically called by the implementation of
report in derived classes before or after
they print specific information about themselves. connect and disconnect call
pasynManager-> exceptionConnect and pasynManager-> exceptionDisconnect respectively.
Derived classes may or may not need to implement these functions.
virtual asynStatus setIntegerParam(int index, int value);
virtual asynStatus setIntegerParam(int list, int index, int value);
virtual asynStatus setDoubleParam(int index, double value);
virtual asynStatus setDoubleParam(int list, int index, double value);
virtual asynStatus setStringParam(int index, const char *value);
virtual asynStatus setStringParam(int list, int index, const char *value);
virtual asynStatus getIntegerParam(int index, int * value);
virtual asynStatus getIntegerParam(int list, int index, int * value);
virtual asynStatus getDoubleParam(int index, double * value);
virtual asynStatus getDoubleParam(int list, int index, double * value);
virtual asynStatus getStringParam(int index, int maxChars, char *value);
virtual asynStatus getStringParam(int list, int index, int maxChars, char *value);
virtual asynStatus callParamCallbacks();
virtual asynStatus callParamCallbacks(int list, int addr);
The setXXXParam methods set the value of a parameter in the parameter table
in the object. If the value is different from the previous value of the parameter
they also set the flag indicating that the value has changed. The getXXXParam
methods return the current value of the parameter. There are two versions of the
setXXXParam and getXXXParam methods, one with a list
argument, and one without. The one without uses list=0, since there
is often only a single parameter list (i.e. if maxAddr=1). The callParamCallbacks
methods call back any registered clients for parameters that have changed since the last
time callParamCallbacks was called. The version of callParamCallbacks
with no arguments uses the first parameter list and matches asyn address=0. There is a
second version of callParamCallbacks that takes an argument specifying the parameter list
number, and the asyn address to match.
#define ND_ARRAY_MAX_DIMS 10
/* Enumeration of array data types */
typedef enum
{
NDInt8,
NDUInt8,
NDInt16,
NDUInt16,
NDInt32,
NDUInt32,
NDFloat32,
NDFloat64
} NDDataType_t;
typedef struct NDDimension {
int size;
int offset;
int binning;
int reverse;
} NDDimension_t;
typedef struct NDArrayInfo {
int nElements;
int bytesPerElement;
int totalBytes;
} NDArrayInfo_t;
class NDArray {
public:
/* Data: NOTE this must come first because ELLNODE must be first, i.e. same address as object */
/* The first 2 fields are used for the freelist */
ELLNODE node;
int referenceCount;
/* The NDArrayPool object that created this array */
void *owner;
int uniqueId;
double timeStamp;
int ndims;
NDDimension_t dims[ND_ARRAY_MAX_DIMS];
NDDataType_t dataType;
int dataSize;
void *pData;
/* Methods */
NDArray();
int initDimension (NDDimension_t *pDimension, int size);
int getInfo (NDArrayInfo_t *pInfo);
int copy (NDArray *pOut);
int reserve();
int release();
};
An NDArray is a general purpose class for handling array data. An NDArray object is self-describing,
meaning it contains enough information to describe the data itself. It is not intended to
contain meta-data describing how the data was collected, etc.
An NDArray can have up to ND_ARRAY_MAX_DIMS dimensions, currently 10. A fixed maximum number of dimensions is used to significantly simplify the code compared to unlimited number of dimensions. Each dimension of the array is described by an NDDimension_t structure. The fields in NDDimension_t are as follows:
size is the number of elements in this dimension.
offset is the starting element in this dimension
relative to the first element of the detector in unbinned units. If a selected
region of the detector is being read, then this value may be
> 0. The offset value is cumulative, so if a plugin such as NDPluginROI
further selects a subregion, the offset is relative
to the first element in the detector and not to the first element of
the region passed to NDPluginROI.
binning is the binning (sumation of elements) in this
dimension. The offset value is cumulative, so if a plugin such as
NDPluginROI performs binning,
the binning is expressed relative
to the pixels in the detector and not to the possibly binned
pixels passed to NDPluginROI.
reverse is 0 if the data are in their normal order
as read out from the detector in this dimension,
and 1 if they are in reverse order. This value is cumulative,
so if a plugin such as
NDPluginROI reverses the data, the value must reflect the
orientation relative to the original detector, and not to
the possibly reversed data passed to NDPluginROI.
(node, referenceCount, owner) are used
by the NDArrayPool class discussed below.
The remaining data fields are as follows:
uniqueId This should be a number that uniquely identifies this array. Detector
drivers should assign this number to the NDArray before calling the plugins.
timeStamp This should be a timestamp value in seconds recording when the frame
was collected. The time=0 reference is driver-dependent because of differences in vendor
libraries. If there is a choice, it is recommended to use timeStamp=0 for Epoch, (00:00:00 UTC,
January 1, 1970).
ndims The number of dimensions in this array.
dims Array of NDDimension_t structures. The array is of length ND_MAX_DIMS, but
only the first ndims values must contain valid information.
dataType The data type of this array, one of the NDDataType_t enum values.
The data types supported are signed and unsigned 8, 16, and 32-bit integers, and 32 and 64-bit floats.
dataSize The size of the memory buffer pointed to by pData in bytes. This may be
larger than the amount actually required to hold the data for this array.
pData Pointer to the memory for this array. The data is assumed to be stored
in the order of dims[0] changing fastest, and dims[ndims-1] changing slowest.
initDimension This method simply initializes the dimension structure to size=size,
binning=1, reverse=0, offset=0.
getInfo. This convenience method returns information about an NDArray, including the total number
of elements, the number of byte per element, and the total number of bytes in the array.
copy. This method makes a copy of an NDArray object. The output array object must already exist
and must have sufficient memory allocated to it to hold the data.
reserve. This method calls NDArrayPool->reserve() for this object. It increases the reference
count for this array.
release. This method calls NDArrayPool->release() for this object. It decreases the reference
count for this array.
class NDArrayPool {
public:
NDArrayPool (int maxBuffers, size_t maxMemory);
NDArray* alloc (int ndims, int *dims, NDDataType_t dataType, int dataSize, void *pData);
int reserve (NDArray *pArray);
int release (NDArray *pArray);
int convert (NDArray *pIn,
NDArray **ppOut,
NDDataType_t dataTypeOut,
NDDimension_t *outDims);
int report (int details);
The methods of the NDArrayPool class are:
NDArrayPool This is the constructor for the class. The maxBuffers argument is the maximum
number of NDArray objects that the pool is allowed to contain. The maxmMemory argument is the maxiumum
number of bytes of memory the the pool is allowed to use, summed over all of the NDArray objects.
alloc This method allocates a new NDArray object. The first 3 arguments are required. ndims
is the number of dimensions in the NDArray. dims is an array of dimensions, whose size must be at least ndims.
dataType is the data type of the NDArray data. dataSize is the number of bytes to allocate for the array data.
If it is 0 then alloc() will compute the size required from ndims, dims, and dataType. pData is a pointer
to a data buffer. If it is NULL then alloc will allocate a new array buffer. If pData is not NULL then
it is assumed to point to a valid buffer. In this case
dataSize must contain the actual number of bytes in the existing array, and this array must be large enough
to hold the array data. alloc() searches its free list to find a free NDArray buffer. If is cannot find one
then it will allocate a new one and add it to the free list. If doing so would exceed maxBuffers then alloc()
will return an error. Similarly if allocating the memory required for this NDArray would cause the
cumulative memory allocated for the pool to exceed maxMemory then an error will be returned. alloc() sets
the reference count for the returned NDArray to 1.
reserve. This method increases the reference count for the NDArray object. Plugins must call
reserve() when an NDArray is placed on a queue for later processing.
release. This method decreases the reference count for the NDArray object. Plugins must call
release() when an NDArray is removed from the queue and processing on it is complete. Drivers must call
release() after calling all plugins.
convert This method creates a new output NDArray from an input NDArray, performing conversion
operations. The conversion can change the data type if dataTypeOut is different from
pIn->dataType. It can also change the dimensions. outDims may have different values of size, binning, offset and
reverse for each of its dimensions from input array dimensions (pIn->dims).
report This method reports on the free list size and other properties of the NDArrayPool object.
class asynNDArrayDriver : public asynPortDriver {
public:
asynNDArrayDriver(const char *portName, int maxAddr, int paramTableSize, int maxBuffers, size_t maxMemory,
int interfaceMask, int interruptMask);
virtual asynStatus readGenericPointer(asynUser *pasynUser, void *genericPointer);
virtual asynStatus writeGenericPointer(asynUser *pasynUser, void *genericPointer);
virtual void report(FILE *fp, int details);
};
The methods of the asynNDArrayDriver class are:
asynNDArrayDriver This is the constructor for the class. portName, maxAddr, paramTableSize,
interfaceMask and interruptMask are simply passed to the asynPortDriver base class constructor.
asynNDArray creates an NDArrayPool object to allocate NDArray objects.
maxBuffers and maxMemory are passed to the constructor for the NDArrayPool object.
readGenericPointer This method copies an NDArray object from the asynNDArrayDriver to an NDArray
whose address is passed by the caller in the genericPointer argument.
The caller must allocate the memory for the array, and pass the size in NDArray->dataSize. The method will
limit the amount of data copied to the actual array size or the input dataSize, whichever is smaller.
writeGenericPointer This method currently does nothing. Derived classes must implement this method
as required.
report This method calls the report function in the asynPortDriver base class. It then
calls the NDArrayPool->report() method if details > 5.
class ADDriver : public asynNDArrayDriver {
public:
ADDriver(const char *portName, int maxAddr, int paramTableSize, int maxBuffers, size_t maxMemory,
int interfaceMask, int interruptMask);
/* These are the methods that we override from asynPortDriver */
virtual asynStatus drvUserCreate(asynUser *pasynUser, const char *drvInfo,
const char **pptypeName, size_t *psize);
/* These are the methods that are new to this class */
int createFileName(int maxChars, char *fullFileName);
The methods of the ADDriver class are:
ADDriver This is the constructor for the class. All of the arguments are simply passed to the
constructor for the asynNDArrayDriver base class. After calling the base class constructor this method sets
reasonable default values for all of the parameters defined in ADStdDriverParams.h.
drvUserCreate This method returns one of the enum values for the parameters defined in
ADStdDriverParams.h if the driverInfo field matches one the strings defined in that file. Derived classes will
typically provide an implementation of drvUserCreate() that searches for parameters that are unique to that
detector driver. If a parameter is not matched, then ADDriver->drvUserCreate() will be called to see if it
is a standard driver parameter (defined in ADStdDriverParams.h).
createFileName This is a convenience function that constructs a complete file name in
the ADFullFileName parameter from the
ADFilePath, ADFileName, ADFileNumber, and ADFileTemplate parameters.
class simDetector : public ADDriver {
public:
simDetector(const char *portName, int maxSizeX, int maxSizeY, NDDataType_t dataType,
int maxBuffers, size_t maxMemory);
/* These are the methods that we override from ADDriver */
virtual asynStatus writeInt32(asynUser *pasynUser, epicsInt32 value);
virtual asynStatus writeFloat64(asynUser *pasynUser, epicsFloat64 value);
virtual asynStatus drvUserCreate(asynUser *pasynUser, const char *drvInfo,
const char **pptypeName, size_t *psize);
void report(FILE *fp, int details);
The portName, maxBuffers, and maxMemory arguments are passed to the base class constructors. The maxSizeX, maxSizeY, and
dataType arguments are specific to the simulation driver, controlling the maximum image size and initial data type of the
computed images. The writeInt32 and writeFloat64 methods override those in the base class. The driver takes action
when new parameters are passed via those interfaces. For example, the ADAcquire parameter (on the asynInt32 interface) is
used to turn acquisition (i.e. computing new images) on and off.
The simulation driver initially sets the image[i, j] = i*gainX + j*gainY * gain * exposureTime * 1000. Thus the image is a linear ramp in the X and Y directions, with the gains in each direction being detector-specific parameters. Each subsquent acquisition increments each pixel value by gain*exposureTime*1000. Thus if gain=1 and exposureTime=.001 second then the pixels are incremented by 1. If the array is an unsigned 8 or 16 bit integer then the pixels will overflow and wrap around to 0 after some period of time. This gives the appearance of bands that appear to move with time. The slope of the bands and their periodicity can be adjusted by changing the gains and exposure times.
The driver creates a thread that waits for a signal to start acquisition. When acquisition is started that thread computes new images and then calls back any registered plugins as follows:
/* Put the frame number and time stamp into the buffer */
pImage->uniqueId = imageCounter;
pImage->timeStamp = startTime.secPastEpoch + startTime.nsec / 1.e9;
/* Call the NDArray callback */
/* Must release the lock here, or we can get into a deadlock, because we can
* block on the plugin lock, and the plugin can be calling us */
epicsMutexUnlock(this->mutexId);
asynPrint(this->pasynUser, ASYN_TRACE_FLOW,
"%s:%s: calling imageData callback\n", driverName, functionName);
doCallbacksGenericPointer(pImage, NDArrayData, addr);
epicsMutexLock(this->mutexId);
The 3 driver-specific parameters are defined in the driver as follows:
/* If we have any private driver parameters they begin with ADFirstDriverParam and should end
with ADLastDriverParam, which is used for setting the size of the parameter library table */
typedef enum {
SimGainX
= ADFirstDriverParam,
SimGainY,
SimResetImage,
ADLastDriverParam
} SimDetParam_t;
static asynParamString_t SimDetParamString[] = {
{SimGainX, "SIM_GAINX"},
{SimGainY, "SIM_GAINY"},
{SimResetImage, "RESET_IMAGE"},
};
#define NUM_SIM_DET_PARAMS (sizeof(SimDetParamString)/sizeof(SimDetParamString[0]))
The drvUserCreate function first checks to see if the parameter is one of these
3 parameters that are unique to the simulation driver. If not it checks to
see if it is a standard parameter by calling the ADDriver base class method.
asynStatus simDetector::drvUserCreate(asynUser *pasynUser,
const char *drvInfo,
const char **pptypeName, size_t *psize)
{
asynStatus status;
int param;
const char *functionName = "drvUserCreate";
/* See if this is one of our standard parameters */
status = findParam(SimDetParamString, NUM_SIM_DET_PARAMS,
drvInfo, ¶m);
if (status == asynSuccess) {
pasynUser->reason = param;
if (pptypeName) {
*pptypeName = epicsStrDup(drvInfo);
}
if (psize) {
*psize = sizeof(param);
}
asynPrint(pasynUser, ASYN_TRACE_FLOW,
"%s:%s: drvInfo=%s, param=%d\n",
driverName, functionName, drvInfo, param);
return(asynSuccess);
}
/* If not, then see if it is a base class parameter */
status = ADDriver::drvUserCreate(pasynUser, drvInfo, pptypeName, psize);
return(status);
}
The following EPICS records are used by pilatusROI. All records are prefixed by the macro $(DET) which must be passed to the template file when the records are loaded.
AcquireMode (mbbo)MinImageUpdateTime PV is ignorred in
this mode, so that all images are sent to EPICS for display.
It is similar to the exposem command in TVX.
Exposure,
ExtEnable, ExtTrigger, and ExtMTriggerrespectively.
Alignment mode uses the Exposure command as well, but continuously takes images into
the same temporary file (alignment.tif).
ExposureTime (ao)NImages (longout)ExposurePeriod (ao)NExposures (longout)DelayTime (ao)Acquire (busy)Armed (bo)Abort (bo)StatusMessage (stringout)
ThresholdEnergy (ao)Gain (mbbo)
FilePath (waveform, FTVL=UCHAR, NELM=256)This record is an EPICS waveform record with FTVL=UCHAR and NELM=256. This removes the 40 character restriction on path name lengths that arise if an EPICS "string" PV is used. medm allows one to edit and display such records correctly. EPICS clients will typically need to convert the path name from a string to an integer or byte array before sending the path name to EPICS. This is easy to do in clients like SPEC, Matlab, and IDL.
Filename (stringout)FileNumber (longout)FileFormat (stringout)
epicsSnprintf(FullFilename, sizeof(FullFilename), FileFormat,
FilePath, Filename, FileNumber);
FilePath, Filename, FileNumber are converted in that order with FileFormat.
The default file format is "%s%s%4.4d.tif". The first %s converts the FilePath,
followed immediately by another %s for Filename.
FileNumber is formatted with %4.4d, which results in a
fixed field with of 4 digits, with leading zeros as required. Finally, the .tif extension
is added to the file name. This will make camserver save files in TIFF format with that extension.
This mechanism for creating file names is very flexible. Other characters, such as _ can be put in Filename or FileFormat as desired. If one does not want to have FileNumber in the file name at all, then just omit the %d format specifier from FileFormat.
Note that when saving multiple images (NImages>1) camserver has its own rules for creating the names of the individual files. The rules are as follows:
The name constructed using the above algorithm is used as a basename. The following examples show the interpretation of the basename.
Basename Files produced
test6.tif test6_00000.tif, test6_00001.tif, ...
test6_.tif test6_00000.tif, test6_00001.tif, ...
test6_000.tif test6_000.tif, test6_001.tif, ...
test6_014.tif test6_014.tif, test6_015.tif, ...
test6_0008.tif test6_0008.tif, test6_0009.tif, ...
test6_2_0035.tif test6_2_0035.tif, test6_2_0036.tif, ...
The numbers following the last '_' are taken as a format template,
and as a start value. The minimum format is 3; there is no maximum; the
default is 5. The format is also constrained by the requested number of images.
AutoIncrement (bo)FullFilename (waveform, FTVL=UCHAR, NELM=256)ROI$(N)XMin (longout)ROI$(N)XMax (longout)ROI$(N)YMin (longout)ROI$(N)YMax (longout)ROI$(N)BgdWidth (longout)ROI$(N)TotalCounts (ao)ROI$(N)NetCounts (ao)ROI$(N)MinCounts (longout)ROI$(N)MaxCounts (longout)ROI$(N)Label (stringout)ROI$(N)WFTotalCounts (waveform, FTVL=DOUBLE, NELM=$(NCHANS))ROI$(N)WFNetCounts (waveform, FTVL=DOUBLE, NELM=$(NCHANS))MinWFUpdateTime (ao)ImageData (waveform, FTVL=LONG, NELM=$(NPIXELS))NXPixels (longout)NYPixels (longout)HighlightROIs (bo)PostImages (bo)MinImageUpdateTime (ao)BadPixelFile (waveform, FTVL=UCHAR, NELM=256)
badX1,badY1 replacementX1,replacementY1
badX2,badY2 replacementX2,replacementY2
...
The X and Y coordinates range from 0 to NXPixels-1 and NYPixels-1. Up to 100 bad pixels can be defined.
The bad pixel mapping simply replaces the bad pixels with another pixel's value.
It does not do any averaging. It is felt that this is sufficient for the purpose for which
pilatusROI was written, namely fast on-line viewing of ROIs and ImageData. More sophisticated
algorithms can be used for offline analysis of the image files themselves.
The following is an example bad pixel file for the GSECARS detector:
263,3 262,3
264,3 266,3
263,3 266,3
300,85 299,85
300,86 299,86
471,129 472,129
NBadPixels (longout)FlatFieldFile (waveform, FTVL=UCHAR, NELM=256)
ImageData[i] = (averageFlatField * ImageData[i])/flatField[i];
MinFlatField (ao)FlatFieldValid (bo)ReadTiffTimeout (ao)SendMessage (waveform, FTVL=UCHAR, NELM=256)ReplyMessage (waveform, FTVL=UCHAR, NELM=256)Connect (bo)Disconnect (bo)scan1, scan2, scan3, scan4, scanH (sscan)
pilatusROI.template is loaded with the following macro parameters:
| Macro parameter | Description |
|---|---|
$(DET) |
PV name prefix. This identifies this Pilatus detector from others that may be running on the same subnet. |
$(NXPIXELS) |
The number of pixels in the X (fast index) direction on the detector. This is 487 for the Pilatus 100K. |
$(NYPIXELS) |
The number of pixels in the Y (slow index) direction on the detector. This is 195 for the Pilatus 100K. |
$(NPIXELS) |
The total number of pixels on the detector. This is NXPIXELS*NYPIXELS=94965 for the Pilatus 100K. |
$(PORT) |
The name of the asyn port connected to the Pilatus via a TCP/IP socket. |
pilatusROI_N.template is loaded for each ROI with the following macro parameters:
| Macro parameter | Description |
|---|---|
$(DET) |
PV name prefix. This identifies this Pilatus detector from others that may be running on the same subnet. |
$(N) |
The number of this ROI. Starts with 1. |
$(XMIN) |
The minimum value of X for this ROI. Starts with 0. |
$(XMAX) |
The maximum value of X for this ROI. Starts with 0. |
$(YMIN) |
The minimum value of Y for this ROI. Starts with 0. |
$(YMAX) |
The maximum value of Y for this ROI. Starts with 0. |
$(BGD_WIDTH) |
The background width for this ROI. If BGD_WIDTH <=0 then no background subtraction is done, and NetCounts=TotalCounts. |
$(NCHANS) |
The maximum number of elements in the WFTotalCounts and WFNetCounts waveform arrays. This sets the maximum value of NImages for collecting ROI arrays. |
The pilatusROIs SNL program is started with the following macro parameters:
| Macro parameter | Description |
|---|---|
DET |
PV name prefix. This identifies this Pilatus detector from others that may be running on the same subnet. |
PORT |
The name of the asyn port connected to the Pilatus via a TCP/IP socket. |
NROIS |
The number of ROIs loaded in the substitutions file with pilatusROI_N.template. Maximum=32. |
The following is an example st.cmd startup script:
< envPaths
###
# Load the EPICS database file
dbLoadDatabase("$(PILATUS)/dbd/pilatus.dbd")
pilatus_registerRecordDeviceDriver(pdbbase)
###
# Create the asyn port to talk to the Pilatus on port 41234.
drvAsynIPPortConfigure("pilatus","gse-pilatus1:41234")
# Set the input and output terminators.
asynOctetSetInputEos("pilatus", 0, "\030")
asynOctetSetOutputEos("pilatus", 0, "\n")
# Define the environment variable pointing to stream protocol files.
epicsEnvSet("STREAM_PROTOCOL_PATH", "$(PILATUS)/pilatusApp/Db")
###
# Specify where save files should be
set_savefile_path(".", "autosave")
###
# Specify what save files should be restored. Note these files must be
# in the directory specified in set_savefile_path(), or, if that function
# has not been called, from the directory current when iocInit is invoked
set_pass0_restoreFile("auto_settings.sav")
set_pass1_restoreFile("auto_settings.sav")
###
# Specify directories in which to to search for included request files
set_requestfile_path("./")
set_requestfile_path("$(AUTOSAVE)", "asApp/Db")
set_requestfile_path("$(CALC)", "calcApp/Db")
set_requestfile_path("$(SSCAN)", "sscanApp/Db")
set_requestfile_path("$(PILATUS)", "pilatusApp/Db")
###
# Load the save/restore status PVs
dbLoadRecords("$(AUTOSAVE)/asApp/Db/save_restoreStatus.db", "P=PILATUS:")
###
# Load the substitutions for for this IOC
dbLoadTemplate("PILATUS_all.subs")
# Load an asyn record for debugging
dbLoadRecords("$(ASYN)/db/asynRecord.db", "P=PILATUS:,R=asyn1,PORT=pilatus,ADDR=0,IMAX=80,OMAX=80")
# Load sscan records for scanning
dbLoadRecords("$(SSCAN)/sscanApp/Db/scan.db", "P=PILATUS:,MAXPTS1=2000,MAXPTS2=200,MAXPTS3=20,MAXPTS4=10,MAXPTSH=2048")
###
# Set debugging flags if desired
#asynSetTraceIOMask("pilatus",0,2)
#asynSetTraceMask("pilatus",0,3)
###
# Start the IOC
iocInit
###
# Save settings every thirty seconds
create_monitor_set("auto_settings.req", 30, "P=PILATUS:")
###
# Start the SNL program
seq(pilatusROIs, "DET=PILATUS:, PORT=pilatus, NROIS=16")
The following is the substutitions file PILATUS_all.subs referenced above.
It creates 16 ROIS, and defines 5 valid ones: the entire chip, and the 4 quadrants of the chip:
ffile $(PILATUS)/db/pilatusROI.template {
pattern
{DET, NXPIXELS, NYPIXELS, NPIXELS, PORT}
{PILATUS:, 487, 195, 94965, pilatus}
}
file $(PILATUS)/db/pilatusROI_N.template {
pattern
{DET, N, XMIN, XMAX, YMIN, YMAX, BGD_WIDTH, NCHANS}
{PILATUS:, 1, 0, 486, 0, 194, 1, 2000}
{PILATUS:, 2, 0, 243, 0, 97, 1, 2000}
{PILATUS:, 3, 0, 243, 98, 194, 1, 2000}
{PILATUS:, 4, 244, 486, 0, 97, 1, 2000}
{PILATUS:, 5, 244, 486, 98, 194, 1, 2000}
{PILATUS:, 6, -1, -1, -1, -1, 1, 2000}
{PILATUS:, 7, -1, -1, -1, -1, 1, 2000}
{PILATUS:, 8, -1, -1, -1, -1, 1, 2000}
{PILATUS:, 9, -1, -1, -1, -1, 1, 2000}
{PILATUS:, 10, -1, -1, -1, -1, 1, 2000}
{PILATUS:, 11, -1, -1, -1, -1, 1, 2000}
{PILATUS:, 12, -1, -1, -1, -1, 1, 2000}
{PILATUS:, 13, -1, -1, -1, -1, 1, 2000}
{PILATUS:, 14, -1, -1, -1, -1, 1, 2000}
{PILATUS:, 15, -1, -1, -1, -1, 1, 2000}
{PILATUS:, 16, -1, -1, -1, -1, 1, 2000}
}
The following show the MEDM screens that are used to control the pilatusROI software.
pilatusROI.adl is the main screen used to control the pilatusROI SNL
program. All records except those that are specific to each ROI are accessed through this
screen.

pilatus8ROIs.adl is used to define the ROIs, and to display the statistics for
each ROI. In this example there are 2 valid ROIs defined. ROI 1 is a small rectangle near the center
containing the Bragg diffraction peak from a crystal. ROI 2 is the entire chip.

pilatusROI_waveform.adl is used to plot the net or total counts in an ROI when
NImages>1. In this example the plot is the net counts in ROI 1 as the diffractometer chi was scanned
+- 1 degree with 1000 points at .02 seconds/point. This was done with the SPEC command
lup chi -1 1 1000 .02using trajectory scanning on a Newport kappa diffractometer. This was a compound motor scan with the Newport XPS putting out pulses every .02 seconds. These pulses triggered the Pilatus in External Enable mode. The pilatusROI program read each TIFF file as it was created and updated this plot every 0.2 seconds. The total time to collect this scan with 1000 images was 20 seconds.

scan_more.adl is used to define a scan. In this example the sscan record is set up
to scan the ThresholdEnergy PV and to collect the total counts in ROI2, which was defined to include
the entire detector.

scanDetPlot.adl is used to plot the results of a scan after it is complete.
In this example the total counts in ROI 2 are plotted as a function of the ThresholdEnergy as it was
scanned from 3000 to 10000 eV in 250 eV steps. The source was Fe55, and the cut-off is at 6 keV, as
expected for the Mn Ka and Mn Kb x-rays that this source produces.

asynRecord.adl is used to control the debugging information printed by the asyn TCP/IP driver
(asynTraceIODriver) and the SNL program (asynTraceIODevice).

asynOctet.adl can be used to send any command to camserver and display the response. It can
be loaded from the More menu in asynRecord.adl above.

epics_image_display
that can be used to display the ImageData PV that pilatusROI sends over EPICS. This IDL
client is available as source code (which requires an IDL license), and also as a pre-built IDL .sav
file that can be run for free under the IDL Virtual Machine. This IDL program can run on any machine that IDL
runs on, and that has the ezcaIDL shareable library built for it. This includes Windows, Linux, Solaris, and Mac.
epics_image_display is included in the
CARS IDL imaging software.
The control window for epics_image_display is shown below. It has fields to input the name of
the EPICS PV with the image data, which is $(DET)ImageData in the case of pilatusROI. It also has fields
for the number of pixels in the X and Y directions. This is needed because EPICS waveform records are
1-dimensional only, and so do not contain the information on the number of rows and columns in the image.

epics_image_display uses the routine
image_display.pro
to display the images. This routine displays row and column profiles as the cursor is moved. It allows
changing the color lookup tables, and zooming in and out with the left and right mouse buttons. The following
is an example of image_display displaying a Pilatus image with an Fe55 source in front of the detector.

# need some more globals (kludge)
global PILATUS_ROI_PV
global PILATUS_IMGPATH_PV
global PILATUS_FNAME_PV
global PILATUS_FILENUMBER_PV
global PILATUS_FILEFORMAT_PV
global PILATUS_EXPSRTM_PV
global PILATUS_NFRAME_PV
global PILATUS_EXPPRD_PV
global PILATUS_NEXPFRM_PV
global PILATUS_ACQ_PV
global PILATUS_ACQMODE_PV
###############################################################
def _setup_img '{
local j, str
# PILATUS_PREFIX should be detector aquisition pv (GSE-PILATUS1:)
if ( PILATUS_PREFIX == "") PILATUS_PREFIX = "GSE-PILATUS1:"
PILATUS_PREFIX = getval("Enter PILATUS pv prefix",PILATUS_PREFIX)
# rois pvs
PILATUS_ROI_PV = PILATUS_PREFIX "ROI1NetCounts"
PILATUS_IMGPATH_PV = PILATUS_PREFIX "FilePath"
PILATUS_FNAME_PV = PILATUS_PREFIX "Filename"
PILATUS_FILENUMBER_PV = PILATUS_PREFIX "FileNumber"
PILATUS_FILEFORMAT_PV = PILATUS_PREFIX "FileFormat"
PILATUS_EXPSRTM_PV = PILATUS_PREFIX "ExposureTime"
PILATUS_NFRAME_PV = PILATUS_PREFIX "NImages"
PILATUS_EXPPRD_PV = PILATUS_PREFIX "ExposurePeriod"
PILATUS_NEXPFRM_PV = PILATUS_PREFIX "NExposures"
PILATUS_ACQ_PV = PILATUS_PREFIX "Acquire"
PILATUS_ACQMODE_PV = PILATUS_PREFIX "AcquireMode"
...
def epics_pilatus_count '{
...
# write to data base fields
# Need to convert path from string to byte array
# Note: we use the "wait" parameter in epics_put here (new to spec5.7.02) so that
# it uses ca_put_callback, to know that all PVs have been processed
# before we start counting. Use 1 second timeout, will actually be
# much faster than this unless something is wrong.
array _temp[256]
_temp = PILATUS_IMAGE_DIR
# Do not change path for now
#epics_put(PILATUS_IMGPATH_PV,_temp, 1)
epics_put(PILATUS_FNAME_PV,img_fname, 1)
epics_put(PILATUS_FILENUMBER_PV,NPTS, 1)
epics_put(PILATUS_FILEFORMAT_PV,_fileformat, 1)
epics_put(sc_prtm_pv,cnt_time_val, 1)
epics_put(PILATUS_EXPSRTM_PV,cnt_time_val, 1)
epics_put(PILATUS_ACQMODE_PV,0, 1) # Internal trigger
epics_put(PILATUS_NFRAME_PV, 1, 1)
epics_put(PILATUS_NEXPFRM_PV, 1, 1)
def user_getcounts '{
local pv_roi, j, pv
...
# using image_count routine
} else if ( EPICS_COUNT == 4 ) {
S[iroi] = 0
S[iroi] = epics_get(PILATUS_ROI_PV)
lup chi -2 2 1000 .015
This tells SPEC to do a relative scan of the chi axis from -2 degrees to +2 degrees with 1000 points
at .015 seconds/point. On our kappa diffractometer this entails a coordinated motion of the phi, kappa
and omega axes. The EPICS trajectory scanning software downloads the non-linear trajectory that SPEC computes
into the XPS controller, which executes it. As the motors are moving the XPS outputs synchronization pulses
at the period of the collection time, .015 seconds in this case. These pulses are stretched
(see Hardware notes below) and used as the external input to the Pilatus.
The time to execute this scan should be 15.0 seconds. The actual time was 16.3 seconds, measured
using camonitor on the Acquire PV. Again, this includes the time for camserver to save all 1000 images to disk
(366 MB), and for pilatusROI to read each file, correct the bad pixels and flat field, compute the ROIs, and post the ROIs
to EPICS. It also posted the images to EPICS at 1Hz (15 images total). The total additional time was less
than 1.3 seconds for all 1000 images. As soon as the acquisition was complete SPEC plotted the net counts in
the first ROI (containing the Bragg peak) as follows:

For comparison this identical scan was executed in traditional step-scanning mode, where the motors stopped at each point in the scan. The Pilatus was run in Internal mode with NImages=1. The total time for the scan was 870 seconds (more than 14 minutes), compared to 16.3 seconds in trajectory mode. Most of this overhead is the settling time for the motors, with only a small fraction due to the Pilatus single-exposure mode. The trajectory scanning mode is thus more than 50 times faster to execute the identical SPEC scan.
In External Enable mode (the camserver ExtEnable command) the Pilatus uses the external signal to control acquisition. Only NImages and NExposures are used, ExposureTime and ExposurePeriod are not used. When the signal is high the detector counts, and on the transition to low it begins its readout.
In External MultiTrigger Mode (the camserver ExtMTrigger command) the Pilatus uses the programmed ExposureTime, in addition to NImages and NExposures. Each external trigger pulse causes the Pilatus to collect one image at the programmed exposure time. This mode works well with a trigger source like the Newport motor controllers or the SIS380x multichannel scaler, that put out a short trigger pulse for each image. One only needs to take care that the time between external trigger pulses is at least 4msec longer than the programmed exposure time, to allow time for the detector to read out before the next trigger pulse arrives.
When using the External Enable mode, we use an inexpensive analog pulse generator to convert the trigger pulses from the MM4005 and XPS to a form suitable for External Enable mode with the Pilatus. This is the solution we have developed that seems to be reliable:
Dectris has since informed me that they have increased the power supply voltage on all new Pilatus systems, so this should no longer be an issue.