As was mentioned in Running the VMUSBReadout program, the user can pass a
compiled library to VMUSBReadout at runtime.
The path to the library is provided as the --timestamplib
option and the contents of it are linked into the program dynamically.
There are three functions that can be defined in that library and they
will each be detailed in this section.
If the data from the VMUSBReadout program is intended for event building later, it must be labeled with a timestamp. This is particularly important for event data. To cause VMUSBReadout to output event data labeled with timestamps, the user must define a function whose signature is:
The pBuffer
parameter is a pointer to the very beginning
of the event's body data. The responsibility of this function is to
process the event data and return a 64-bit integer that will be used as
the timestamp. The function can be defined to suit the needs of the
experimenter and need not depend on the data at all. In most cases
though, it will be extracted from the data referred to by the
pBuffer
pointer.
Example 4-3. A sample getEventTimestamp() implementation
The getEventTimestamp() function should be defined to meet the needs of a specific experiment. For our hypothetical experiment, we will be reading out a set of digitizers whose data should be timestamped with a value stored in a latching scaler. The details of the electronics are unimportant. In each readout cycle, the VM-USB will first read out two 32-bit words from the latching scaler containing the 64-bit timestamp followed by the digitized data. In other words our event buffer will look something like this when printed as a list of 16-bit words:
0x0007 0x12ab 0x0001 0x0000 0x0000 0x1234 0x1234 0x1234
From the Understanding VMUSBReadout Output, we know the first 16-bit word is the event header for the subsequent data words. The header specifies that the event data was generated by stack 0 and that it consists of seven 16-bit words. The four 16-bits words that follow the event header correspond to the two 32-bit scaler values, which are then followed by the digitizer output. (This is hypothetical data...). Our task is to use the second, third, forth, and fifth words to reconstruct the complete 64-bit integer. Here is how we do that:
#include <cstdint> #include <cstring> using namespace std; extern "C" { uint64_t getEventTimestamp(void* pBuffer) { uint64_t tstamp = 0; uint16_t header = 0; const char* pData = reinterpret_cast<const char*>(pBuffer); memcpy(&header, pData, sizeof(header)); if ((header&0x0fff)>=4) { pData += 2; memcpy(&tstamp, pData, sizeof(tstamp)); } return tstamp; } } // end of extern
pBuffer
refers to the event
header initially and our timestamp begin two bytes further into
the body. This skips our pointer directly to the least significant
16-bits of the timestamp.
Sometimes it may be useful to label the scaler data with a timestamp as well. It is not necessary but is useful. You have two options for implementing the scaler timestamp (these technically also applies to the event timestamp):
Always return zero
Returning computed value
If you have implemented a timestamp extractor for event data and you do not care about have a precise timestamp labeling your scaler data, the first option is a good one. A zero timestamp is treated specially by the event builder in that the corresponding event will simply be labeled with timestamp of the event preceding it. In other words, it gets dealt with in the order it was received.
If on the other hand, you have not defined an extractor for your event data, and you want to send your scaler data to the event builder, it is quintessential that you return a non-zero timestamp. Consider what happens when the event timestamps are always zero or the event data is non-existent. In such a case, the timestamp will always be zero because the event preceding will have a timestamp of 0 or never have happened. The event builder will then queue data in ways you did not originally expect. The other reason you might want to define a scaler timestamp extractor would be for having precise scaler information. The readout period of the scaler stack in the VM-USB historically has not been particularly precise, which makes comparing its value to data non-trivial at times. If instead, the data is timestamped, you can know for sure how it should be interpreted with the event data.
There is very little different to defining this function in comparison to the getEventTimestamp() function. Actually, if the first two scaler channels of your scaler data contained a 64-bit timestamp, you would define it in in an identical way. The prototype of the getScalerTimestamp is:
Example 4-4. Defining a null getScalerTimestamp()
Probably the most common case for the getScalerTimestamp() function will be to return zero. To do this, you would add three lines for your function just prior to the close of the extern block in the previous example. Here is what the end of that file might look like with that addition.
// ... extern "C" { uint64_t getEventTimestamp(void* pBuffer) { // ... } uint64_t getScalerTimestamp(void* pBuffer) { return uint64_t(0); } } // end of extern
The third function that can be defined in the timestamp extractor library has the prototype:
This can be used for whatever you can think of. A sample usage might be to define an offset for the timestamp that gets read from a file every begin run. This could be done by declaring a global variable that is referred to in the getEventTimestamp() function.
Example 4-5. Adding an offset to the timestamp
Consider the following scenario. The data streams of two Readout programs are being merged together using the event builder and their clocks have been synchronized at the hardware level. In this way, the offset between the timestamps for these systems is fixed from run to run besides maybe a single clock tick variation. If the offset is 10 clock ticks and it is repeatable, the user might want to add an offset to one of the timestamps to remove the offset. The utility in doing this is questionable but let's consider that someone really needed to do so. We could accomplish this by storing the offset in a file that gets read in everytime a run begins. The getEventTimestamp() function could then correct its timestamp using this user-provided offset. Here is how we might do that.
#include <fstream> #include <cstring> #include <cstdint> using namespace std; uint64_t offset = 0 extern "C" { uint64_t getEventTimestamp(void* pBuffer) { uint64_t tstamp = 0; uint16_t header = 0; const char* pData = reinterpret_cast<const char*>(pBuffer); memcpy(&header, pData, sizeof(header)); if ((header&0x0fff)<=4) { pData += 2; memcpy(&, pData, sizeof(tstamp)); } return tstamp + offset; } void onBeginRun() { ifstream myOffsetFile("offset.dat"); myOffsetFile >> offset; } } // end of extern
This is arguably a very naive implementation because the onBeginRun() does not check for any errors that might have happened when opening the file offset.dat. However, as long as the file exists and is readable, it should do what we expect. On the other hand, if there is a failure opening the file, the offset will have been initialized to zero. In a production experiment, you would probably want to alert the user by throwing an exception or printing a message to stderr.
Unless have introduced dependencies on other software in your implementation of these methods, you should not need any complicated build system to compile it. Let's assume we put the code in a file named mytstamplib.cpp. We can compile it with nothing more than the following command
spdaqXX> g++ -std=c++11 -fPIC -shared -o mytstamplib.so mytstamplib.cpp