Frontend user code (object oriented - TMFE)
This page is a work in progress!
Links
Introduction
This page will document using object-oriented C++ to create a midas Frontend. For the classic C-style frontend see Frontend user code. For python frontends see Python.
Regardless of the framework you use, all midas frontends follow the same concept of "equipment" (which can be periodic or polled) that produce data in midas banks that can eventually get logged to file or the midas history system. If you don't need to produce data, you can write midas clients without using one of the frontend frameworks.
Key differences to the C-style frontend framework
- In the C-style framework you must link against
libmfe.a
andlibmidas.a
. In this framework you should only link againstlibmidas.a
. - In the C-style framework you do not write a
main()
function - you just implement functions (and variables) that the framework uses. In this framework you write your ownmain()
function (generally just 2 lines of code). - In the C-style framework you populate nested structs with important information like the equipment name and type. In this framework you set member variables in classes instead.
- In the C-style framework you write one function per frontend that will get called at the start of each run. In this framework you write one such function for each equipment in your frontend.
Examples
Examples of TMFE-based frontends can be found at
$MIDASSYS/progs/tmfe_example.cxx
- minimal frontend with a periodic equipment$MIDASSYS/progs/tmfe_example_frontend.cxx
- the equivalent of$MIDASSYS/examples/experiment/frontend.cxx
$MIDASSYS/progs/tmfe_example_indexed.cxx
- example of a frontend that handles the-i
argument. If you pass-i 3
on the command line, the equipment will appear asexample_03
and write data toBUF03
$MIDASSYS/progs/tmfe_example_multithread.cxx
- example of a frontend that offloads some tasks to other threads. You can offload RPC communication (ODB updates, run transitions etc), periodic equipment checks, polled equipment checks into 3 separate threads if desired. The default is to run all checks in the main thread.$MIDASSYS/progs/tmfe_example_everything.cxx
- "kitchen sink" example that uses almost all features of the framework.$MIDASSYS/progs/fetest.cxx
- contains several periodic equipment and one that only reacts to RPC commands and never writes any data.
Frontend code
Major classes
The main base classes you will need to derive from are:
TMFrontend
TMFeEquipment
Other classes you will interact with are:
TMFE
- wrapper of some midas functions (cm_msg()/fMfe->Msg()
,cm_yield()/fMfe->Yield()
etc) that also contains some global state (fRunNumber
etc).TMFeResult
- used by the framework to report success/failure of commands (rather than raw integer status codes used in the C-style framework). Note that the framework does NOT use exceptions to report errors.MVOdb
- simplified access to ODB that suits some use cases. You can still use thedb_*
family of C-style functions or themidas::odb
JSON-like interface.
More details about these classes follow below.
Helper classes
Status - TMFEResult and TMFeOk
Most functions in the TMFE framework return their status via a TMFEResult
object. For example, the interface for handling a begin-of-run transition is:
TMFeResult HandleBeginRun(int run_number);
When you implement this in your class that derives from TMFeEquipment
, you must construct and return a TMFeResult
object. Ways to do this are:
# Use the plain TMFeResult constructor. # TMFeResult(int code, const std::string& str); # See $MIDASSYS/include/midas.h for details of pre-defined error codes (like FE_ERR_ODB is 602), but you may use any integer. # Note that midas defines SUCCESS as 1! return TMFeResult(FE_ERR_ODB, "Failed to read value from ODB");
# Use a shorthand to say that everything is okay. return TMFeOk();
# Say there was a failure without specifying a specific midas status code (SUCCESS in midas is 1; this wrapper will set a code of 0) return TMFeErrorMessage("Failed to read value from ODB");
# Apply some formatting to your error message saying which function had the issue. # This example will result in a message of "Failed to read value from ODB, some_func() status 602" return TMFeMidasError("Failed to read value from ODB", "some_func", FE_ERR_ODB);
Note that none of the above examples log the error message in the midas message log, they are simply used to provide more information about the error than just a status code. Call TMFE::Msg()
(a wrapper around cm_msg()
) if you want to log an error.
TMFE singleton
The TMFE
class is implemented as a singleton. That means there is only one instance of it, and you can call its functions from anywhere in your code. It contains a mixture of global state and helper functions.
There are 3 ways to access the TMFE class:
# Access from anywhere TMFE::Instance()-> # Access from within a class that derives from TMFeEquipment: fMfe-> # Access from within a class that derives from TMFrontend: fMfe->
Functions
The main functions that you will use are:
# Add a message to the midas message log (wrapper around cm_msg()). # The simplest way to pass the first 3 parameters is via the MINFO or MERROR macros. # If your compiler supports the __FUNCTION__ macro (which most do), you can use that to automatically generate the value for the 4th argument. # The subsequent arguments behave like any call to printf/sprintf etc. # E.g. fMfe->Msg(MINFO, __FUNCTION__, "6 x 3 = %d", 18); void Msg(int message_type, const char *filename, int line, const char *routine, const char *format, ...);
# Start or stop a run from your code (wrapper around cm_transition()). void StartRun(); void StopRun();
# Get the current UNIX time in seconds (to microsecond precision). double GetTime();
# Raise/trigger an alarm (wrapper around al_trigger_alarm()). Uses the "internal" alarm type. # Pre-defined alarm classes are "Warning" and "Alarm". TMFeResult TriggerAlarm(const char* alarm_name, const char* message, const char* alarm_class);
# Cancel/reset an alarm TMFeResult ResetAlarm(const char* alarm_name);
# Wait for up to N seconds for midas to tell us about any ODB updates, RPC calls etc (wrapper around cm_yield()). # This is automatically called periodically by the frontend system, so you shouldn't need to call it yourself. void Yield(double secs);
# Sleep the current thread for the specified number of seconds (very similar to ss_sleep()). void Sleep(double secs);
Member variables
There are also a few member variable that may be of interest.
# This is set to true if the user passes -v as a command-line argument when running the frontend. bool gfVerbose;
# Current run number. int fRunNumber;
# Whether run is running/paused (true) or stopped (false). bool fStateRunning;
# ODB access via MVOdb. MVOdb* fOdbRoot;
MVOdb
The MVOdb
class is yet another way to access the ODB. The mvodb package defines an API that can be used both by code that is reading from a live ODB (like this frontend) as well as from JSON/XML ODB dumps (when analysing midas files). It provides a simpler but more limited interface compared to the db_*
family of C-style functions or the midas::odb
JSON-like interface.
Access
You can access the ODB in a few ways:
# Access to the top-level (from within classes that derive from TMFeEquipment or TMFrontend). fMfe->fOdbRoot-> # /
# Access to equipment-specific ODB directories (from within classes that derive from TMFeEquipment): fOdbEq-> # /Equipment/<xxx> fOdbEqCommon-> # /Equipment/<xxx>/Common fOdbEqSettings-> # /Equipment/<xxx>/Settings fOdbEqVariables-> # /Equipment/<xxx>/Variables fOdbEqStatistics-> # /Equipment/<xxx>/Statistics
Moving to / creating ODB directories
You can create your own object that points to a specific directory using the ChDir()
function. Note that this returns a new object rather than editing the state of the object you pass in!
MVOdb* Chdir(const char* subdirname, bool create = false, MVOdbError* error = NULL); # E.g. MVOdb* my_dir = fMfe->fOdbRoot->ChDir("/Path/to/my/dir"); # my_dir will access entries in /Path/to/my/dir # fMfe->fOdbRoot still accesses /
If you wish to create a subdirectory, pass true
as the second argument to ChDir()
:
# Will create /Path/to/my/dir if it doesn't exist. MVOdb* my_dir = fMfe->fOdbRoot->ChDir("/Path/to/my/dir", true);
# Will return NULL if /Path/to/my/dir doesn't exist (this is the default behaviour). MVOdb* my_dir = fMfe->fOdbRoot->ChDir("/Path/to/my/dir", false);
Reading/writing ODB entries
To read/write ODB elements use the R
and W
families of functions:
# E.g. for reading a single boolean value void RB(const char* varname, bool *value, bool create = false, MVOdbError* error = NULL); # E.g. for reading a boolean array void RBA(const char* varname, std::vector<bool> *value, bool create = false, int create_size = 0, MVOdbError* error = NULL); # E.g. for reading a single element in a boolean array void RBAI(const char* varname, int index, bool *value, MVOdbError* error = NULL); # E.g. for writing a single boolean value void WB(const char* varname, bool v, MVOdbError* error = NULL); # E.g. for writing a boolean array void WBA(const char* varname, const std::vector<bool>& v, MVOdbError* error = NULL); # E.g. for writing a single element in a boolean array void WBAI(const char* varname, int index, bool v, MVOdbError* error = NULL);
There are similar functions for the following data types. Note that the functions do NOT do type conversions for you - you must know the type that your ODB data is stored as! If you call RD()
for data that is stored as an integer, you will get an error (more on errors later). If your data is stored as a type not supported by MVOdb, you will need to use the C-style functions or JSON-like interface mentioned previously.
bool (TID_BOOL): RB / RBA / RBAI / WB / WBA / WBAI int (TID_INT): RI / RIA / RIAI / WI / WIA / WIAI double (TID_DOUBLE): RD / RDA / RDAI / WD / WDA / WDAI float (TID_FLOAT): RF / RFA / RFAI / WF / WFA / WFAI std::string (TID_STRING): RS / RSA / RSAI / WS / WSA / WSAI uint16_t (TID_WORD): RU16 / RU16A / RU16AI / WU16 / WU16A / WU16AI uint32_t (TID_DWORD): RU32 / RU32A / RU32AI / WU32 / WU32A / WU32AI
Deleting ODB entries
You can delete ODB keys using the Delete function
void Delete(const char* odbname, MVOdbError* error = NULL); # E.g. fMfe->fOdbRoot->Delete("/Path/to/my/dir");
Error-checking
MVOdb uses the MVOdbError
object to report errors. As you will see from the function signatures, you need to create this object yourself, pass a pointer to that object to the function you're calling, then check the status after the call...
MVOdbError err; bool create_dir = false; MVOdb* my_dir = fMfe->fOdbRoot->ChDir("/Path/to/my/dir", create_dir, &err); if (my_dir == nullptr) { # Directory doesn't exist } else if (err.fError) { # Some other problem. See err.fErrorString and err.fStatus. } else { # Directory exists! Try to read an entry... bool create_key = false; bool value = false; my_dir->RB("my_boolean", &value, create_key, &err); if (err.fError) { # Some problem reading the value. See err.fErrorString and err.fStatus. } else { `value` now contains the ODB value! } }
Code you need to implement
Equipment - derive from TMFeEquipment
Frontend - derive from TMFrontend
main()
Your main()
should be very simple - just create an instance of your frontend class and call FeMain()
. That function will handle parsing all the command-line arguments for you.
int main(int argc, char* argv[]) { MyFrontend fe; return fe.FeMain(argc, argv); }
Compilation