Frontend user code (object oriented - TMFE)

From MidasWiki
Revision as of 13:26, 21 April 2023 by Bsmith (talk | contribs)
Jump to navigation Jump to search


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 and libmidas.a. In this framework you should only link against libmidas.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 own main() 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 as example_03 and write data to BUF03
  • $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 the db_* family of C-style functions or the midas::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