Skip to content

Frontends: programming

This page covers writing frontends.

For the concepts and day-to-day use, see Frontends: overview.

For full descriptions of the programming interfaces (e.g. all the optional parameters that aren't mentioned here), see the API reference for C++ (mfe), C++ (TMFE) and Python (midas.frontend).

There are three frontend frameworks available. These frameworks help you integrate your readout code in a way that other tools expect, and they also provide some helpful functions to reduce the amount of boiler-plate code required.

Framework mfe TMFE midas.frontend
Language C++ C++ Python
Style C-like Object-oriented Object-oriented
Define equipment Using structs As classes As classes
Supports midas format events ✔ ✔ ✔
Supports FIXED format events ✔
Supports subevents/superevents ✔
Supports deferred transitions ✔ ✔
Supports periodic readout ✔ ✔ ✔
Supports polled readout ✔ ✔ ✔
Supports manually-triggered readout ✔ ✔
Supports interrupt-triggered readout ✔
Has a set of device drivers ✔

Frontend flow

For all the frameworks, the general flow is the same:

  • User code registers its equipment in the midas experiment
  • Framework sets up ODB structures, and registers callback functions for run transitions etc
  • Framework enters a loop:
    • Inform user code of any run transitions etc
    • Check if user code has collected any data from the hardware and produced events
      • If so, send them to an event buffer and (optionally) copy them to the ODB
    • Compute summary statistics and write them to the ODB

Code structure

In the mfe framework, you do not define a main() function. Instead, you define various functions that the framework will call at the relevant times.

mfe vs mfed

The standard mfe framework requires all such functions to be defined, even if they're irrelevant for your frontend (e.g. you still need to define an interrupt_configure function, even if you don't care about interrupts).

mfed was introduced to reduce the amount of boiler-plate code needed. As mfe/mfed are so similar, they will be documented together, with sub-sections for "Standard mfe" and "With mfed" when needed.

There are no classes in mfe/mfed - you implement bare functions and define global variables.

The framework code is found in:

  • include/mfe.h
  • src/mfe.cxx
  • src/mfed.cxx

In TMFE, you implement frontend/equipment classes that inherit from base classes:

  • TMFrontend is the base class for the overall frontend
  • `TMFeEquipment" is the base class for an equipment

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.

The framework code is found in:

  • include/tmfe.h
  • src/tmfe.cxx

In the python framework, you implement frontend/equipment classes that inherit from base classes:

The framework code is found in frontend.py.

Minimal examples

#include <stdio.h>
#include <stdlib.h>
#include "midas.h"
#include "mfe.h"

/*-- Globals -------------------------------------------------------*/
/* The frontend name (client name) as seen by other MIDAS clients   */
const char *frontend_name = "largefe";

/* The frontend file name, don't change it */
const char *frontend_file_name = __FILE__;

/* frontend_loop is called periodically if this variable is TRUE    */
BOOL frontend_call_loop = TRUE;

/* a frontend status page is displayed with this frequency in ms */
INT display_period = 0000;

/* maximum event size produced by this frontend */
INT max_event_size = 10000;

/* maximum event size for fragmented events (EQ_FRAGMENTED) */
INT max_event_size_frag = 5 * 1024 * 1024;

/* buffer size to hold events */
INT event_buffer_size = 10 * 10000;

/*-- Function declarations -----------------------------------------*/

INT frontend_init();
INT frontend_exit();
INT begin_of_run(INT run_number, char *error);
INT end_of_run(INT run_number, char *error);
INT pause_run(INT run_number, char *error);
INT resume_run(INT run_number, char *error);
INT frontend_loop();
INT read_large_event(char *pevent, INT off);

/*-- Equipment list ------------------------------------------------*/

#undef USE_INT
BOOL equipment_common_overwrite = FALSE;

EQUIPMENT equipment[] = {

    {"large",                 /* equipment name */
    {3, 0,                    /* event ID, trigger mask */
    "SYSTEM",                /* event buffer */
    EQ_PERIODIC | EQ_FRAGMENTED,     /* equipment type */
    0,                       /* event source */
    "MIDAS",                 /* format */
    TRUE,                    /* enabled */
    RO_ALWAYS,               /* read when running and on transitions */
    2000,                    /* read every 2 sec */
    0,                       /* stop run after this event limit */
    0,                       /* number of sub events */
    0,                       /* log history */
    "", "", ""},
    read_large_event,        /* readout routine */
    NULL, NULL,              /* keep null */
    NULL,                    /* init string */
    },

    {""}
};

INT frontend_init() {
    return SUCCESS;
}

INT frontend_exit() {
    return SUCCESS;
}

INT begin_of_run(INT run_number, char *error) {
    return SUCCESS;
}

INT end_of_run(INT run_number, char *error) {
    return SUCCESS;
}

INT pause_run(INT run_number, char *error) {
    return SUCCESS;
}

INT resume_run(INT run_number, char *error) {
    return SUCCESS;
}

INT frontend_loop() {
    // Do something very frequently
    return SUCCESS;
}

INT poll_event(INT source, INT count, BOOL test) {
    return 0;
}

INT interrupt_configure(INT cmd, INT source, PTYPE adr) {
    switch (cmd) {
        case CMD_INTERRUPT_ENABLE:
            break;
        case CMD_INTERRUPT_DISABLE:
            break;
        case CMD_INTERRUPT_ATTACH:
            break;
        case CMD_INTERRUPT_DETACH:
            break;
    }
    return SUCCESS;
}

INT read_large_event(char *pevent, INT off) {
    DWORD *pddata;

    /* init bank structure */
    bk_init32(pevent);

    bk_create(pevent, "BIGG", TID_DWORD, (void **) &pddata);
    memset((char *) pddata, 0x0000, 100);
    pddata += 1000000;
    memset((char *) pddata - 100, 0xFFFF, 100);
    bk_close(pevent, pddata);

    return bk_size(pevent);
}

Note that you only need boiler-plate for the features you're actually going to use.

#include <stdio.h>
#include <stdlib.h>
#include "midas.h"
#include "mfe.h"

/*-- Globals -------------------------------------------------------*/
/* The frontend name (client name) as seen by other MIDAS clients   */
const char *frontend_name = "largefe";

/* The frontend file name, don't change it */
const char *frontend_file_name = __FILE__;


/*-- Function declarations -----------------------------------------*/
INT frontend_init();
INT my_frontend_loop();
INT read_large_event(char *pevent, INT off);

/*-- Equipment list ------------------------------------------------*/

#undef USE_INT
BOOL equipment_common_overwrite = FALSE;

EQUIPMENT equipment[] = {

    {"large",                 /* equipment name */
    {3, 0,                    /* event ID, trigger mask */
    "SYSTEM",                /* event buffer */
    EQ_PERIODIC | EQ_FRAGMENTED,     /* equipment type */
    0,                       /* event source */
    "MIDAS",                 /* format */
    TRUE,                    /* enabled */
    RO_ALWAYS,               /* read when running and on transitions */
    2000,                    /* read every 2 sec */
    0,                       /* stop run after this event limit */
    0,                       /* number of sub events */
    0,                       /* log history */
    "", "", ""},
    read_large_event,        /* readout routine */
    NULL, NULL,              /* keep null */
    NULL,                    /* init string */
    },

    {""}
};

INT frontend_init() {
    install_frontend_loop(my_frontend_loop);

    // Can also do install_begin_of_run() etc...

    return SUCCESS;
}

INT my_frontend_loop() {
    // Do something frequently
    return SUCCESS;
}

INT read_large_event(char *pevent, INT off) {
    DWORD *pddata;

    /* init bank structure */
    bk_init32(pevent);

    bk_create(pevent, "BIGG", TID_DWORD, (void **) &pddata);
    memset((char *) pddata, 0x0000, 100);
    pddata += 1000000;
    memset((char *) pddata - 100, 0xFFFF, 100);
    bk_close(pevent, pddata);

    return bk_size(pevent);
}
#include <stdio.h>
#include <math.h> // M_PI

#include "tmfe.h"

class EqPeriodic : public TMFeEquipment {
    public:
        EqPeriodic(const char* eqname, const char* eqfilename) : TMFeEquipment(eqname, eqfilename) {
            fEqConfReadConfigFromOdb = false;
            fEqConfEventID = 2;
            fEqConfBuffer = "SYSTEM";
            fEqConfPeriodMilliSec = 1000; // in milliseconds
            fEqConfLogHistory = 1;
            fEqConfReadOnlyWhenRunning = true;
            fEqConfWriteEventsToOdb = true;
        }

        void HandlePeriodic() {
            char buf[1024];

            ComposeEvent(buf, sizeof(buf));
            BkInit(buf, sizeof(buf));

            /* create SCLR bank */
            uint32_t* pdata = (uint32_t*)BkOpen(buf, "PRDC", TID_UINT32);

            /* following code "simulates" some values in sine wave form */
            for (int a = 0; a < 16; a++) {
                *pdata++ = 100*sin(M_PI*time(NULL)/60+a/2.0)+100;
            }

            BkClose(buf, pdata);

            EqSendEvent(buf);
        }
};

class FeExample: public TMFrontend {
    public:
        FeExample() {
            /* register with the framework */
            FeSetName("Sample Frontend");
            FeAddEquipment(new EqTrigger("Trigger", __FILE__));
            FeAddEquipment(new EqPeriodic("Periodic", __FILE__));
        }
};

int main(int argc, char* argv[]) {
    FeExample fe_example;
    return fe_example.FeMain(argc, argv);
}

More examples can be found at:

  • $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.
import midas
import midas.frontend
import midas.event

class MyPeriodicEquipment(midas.frontend.EquipmentBase):
    """
    We define an "equipment" for each logically distinct task that this frontend
    performs. For example, you may have one equipment for reading data from a
    device and sending it to a midas buffer, and another equipment that updates
    summary statistics every 10s.

    Each equipment class you define should inherit from 
    `midas.frontend.EquipmentBase`, and should define a `readout_func` function.
    If you're creating a "polled" equipment (rather than a periodic one), you
    should also define a `poll_func` function in addition to `readout_func`.
    """
    def __init__(self, client):
        # The name of our equipment. This name will be used on the midas status
        # page, and our info will appear in /Equipment/MyPeriodicEquipment in
        # the ODB.
        equip_name = "MyPeriodicEquipment"

        # Define the "common" settings of a frontend. These will appear in
        # /Equipment/MyPeriodicEquipment/Common. The values you set here are
        # only used the very first time this frontend/equipment runs; after 
        # that the ODB settings are used.
        default_common = midas.frontend.InitialEquipmentCommon()
        default_common.equip_type = midas.EQ_PERIODIC
        default_common.buffer_name = "SYSTEM"
        default_common.trigger_mask = 0
        default_common.event_id = 1
        default_common.period_ms = 100
        default_common.read_when = midas.RO_RUNNING
        default_common.log_history = 1

        # You MUST call midas.frontend.EquipmentBase.__init__ in your equipment's __init__ method!
        midas.frontend.EquipmentBase.__init__(self, client, equip_name, default_common)

        # You can set the status of the equipment (appears in the midas status page)
        self.set_status("Initialized")

    def readout_func(self):
        """
        For a periodic equipment, this function will be called periodically
        (every 100ms in this case). It should return either a `midas.event.Event`
        or None (if we shouldn't write an event).
        """

        # In this example, we just make a simple event with one bank.
        event = midas.event.Event()

        # Create a bank (called "MYBK") which in this case will store 8 ints.
        # data can be a list, a tuple or a numpy array.
        data = [1,2,3,4,5,6,7,8]
        event.create_bank("MYBK", midas.TID_INT, data)

        return event

class MyFrontend(midas.frontend.FrontendBase):
    """
    A frontend contains a collection of equipment.
    You can access self.client to access the ODB etc (see `midas.client.MidasClient`).
    """
    def __init__(self):
        # You must call __init__ from the base class.
        midas.frontend.FrontendBase.__init__(self, "myfe_name")

        # You can add equipment at any time before you call `run()`, but doing
        # it in __init__() seems logical.
        self.add_equipment(MyPeriodicEquipment(self.client))

    def begin_of_run(self, run_number):
        """
        This function will be called at the beginning of the run.
        You don't have to define it, but you probably should.
        You can access individual equipment classes through the `self.equipment`
        dict if needed.
        """
        self.set_all_equipment_status("Running", "greenLight")
        self.client.msg("Frontend has seen start of run number %d" % run_number)
        return midas.status_codes["SUCCESS"]

    def end_of_run(self, run_number):
        self.set_all_equipment_status("Finished", "greenLight")
        self.client.msg("Frontend has seen end of run number %d" % run_number)
        return midas.status_codes["SUCCESS"]

    def frontend_exit(self):
        """
        Most people won't need to define this function, but you can use
        it for final cleanup if needed.
        """
        print("Goodbye from user code!")

if __name__ == "__main__":
    # The main executable is very simple - just create the frontend object,
    # and call run() on it.
    with MyFrontend() as my_fe:
        my_fe.run()

More examples can be found at:

  • $MIDASSYS/python/examples/basic_frontend.py - basic example with one periodic equipment
  • $MIDASSYS/python/examples/multi_frontend.py - 2 equipment, uses the frontend index (-i flag), custom ODB settings, altering transition sequence numbers
  • $MIDASSYS/python/examples/deferred_transition_frontend.py - like the basic example, but registers a deferred transition
  • $MIDASSYS/python/examples/manual_frontend.py - like the basic example, but using a manually-triggered equipment

Helper functions

For the mfe framework, all the helper functions are covered in other sections of this page.

Status and error-checking - TMFEResult/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->

The main functions of the TMFE class 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);

There are also a few member variables of the TMFE class 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;

// ODB handle if using db_* C-style family of functions
HNDLE fDB;

For the python framework, all the helper functions are covered in other sections of this page.

Code you need to implement

Declaring your equipment

Equipment names within an experiment must be unique!

All the frameworks require you to declare the name of your equipment, and various parameters configuring their behaviour. Many of these parameters are stored in the ODB in /Equipment/xxx/Common, though some frameworks have extra parameters that are only stored in the code.

For all the parameters stored in Common, see /Equipment/xxx/Common.

The tabs below show how to declare your equipment in each framework.

Equipment are defined as a list of structs. You must define the global variable equipment as an array of EQUIPMENT structs, and the last struct in the list must be empty.

typedef struct {
    char name[NAME_LENGTH];         /**< Equipment name                            */
    EQUIPMENT_INFO info;            /**< See below                                 */
    INT(*readout) (char *, INT);    /**< Pointer to user readout routine           */
    INT(*cd) (INT cmd, PEQUIPMENT); /**< For EQ_SLOW, the class driver routine     */
    DEVICE_DRIVER *driver;          /**< For EQ_SLOW, the device driver list       */
    void *event_descrip;            /**< For FIXED format, text description of format */
    // .... and then some "internal" fields that you don't need to set
} EQUIPMENT;

typedef struct {
    WORD event_id;                   /**< Common/Event ID      */
    WORD trigger_mask;               /**< Common/Trigger mask  */
    char buffer[NAME_LENGTH];        /**< Common/Buffer        */
    INT eq_type;                     /**< Common/Type          */
    INT source;                      /**< Common/Source        */
    char format[8];                  /**< Common/Format        */
    BOOL enabled;                    /**< Common/Enabled       */
    INT read_on;                     /**< Common/Read on       */
    INT period;                      /**< Common/Period        */
    double event_limit;              /**< Common/Event limit   */
    DWORD num_subevents;             /**< Common/Num subevent  */
    INT history;                     /**< Common/Log history   */
    // .... and then some "internal" fields that you don't need to set
} EQUIPMENT_INFO;

Example for a frontend with one periodic equipment

EQUIPMENT equipment[] = {
    {"large",                    /* equipment name */
        {3, 0,                   /* event ID, trigger mask */
        "SYSTEM",                /* event buffer */
        EQ_PERIODIC,             /* equipment type */
        0,                       /* event source */
        "MIDAS",                 /* format */
        TRUE,                    /* enabled */
        RO_ALWAYS,               /* read when running and on transitions */
        2000,                    /* read every 2 sec */
        0,                       /* stop run after this event limit */
        0,                       /* number of sub events */
        0,                       /* log history */
        "", "", ""},
        read_large_event,        /* readout routine */
        NULL, NULL,              /* keep null */
        NULL,                    /* init string */
    },

    {""}                         /* empty struct indicates end of list */
};

In the case of a polled equipment, the equipment type would be EQ_POLLED, and the period determines how long to poll for an event before handling other work.

The readout routine (read_large_event in the above example) is a function that you will create to actually read out data - see the creating events section for more details.

Placeholder in equipment name

In mfe, the equipment name supports two placeholders:

  • %d, %02d etc can be used as a placeholder for the frontend index
  • ${HOSTNAME} can be used as a placeholder for the machine's hostname. This is case-sensitive!

Each equipment is defined by a separate class that inherits from TMFeEquipment.

Your class constructor should call the TMFeEquipment constructor.

MyEquip(const char* eqname, const char* eqfilename) : TMFeEquipment(eqname, eqfilename) { }

Within your constructor you may optionally set the default values for parameters that define the equipment behaviour. Below are the parameters you may set, their default values, and their ODB locations.

Type Variable name Default Comment
bool fEqConfEnablePeriodic true Equivalent of setting Common/Type to EQ_POLLED.
bool fEqConfEnablePoll false Equivalent of setting Common/Type to EQ_POLLED.
bool fEqConfEnabled true Common/Enabled
uint16_t fEqConfEventID 1 Common/EventID
uint16_t fEqConfTriggerMask 0 Common/TriggerMask
std::string fEqConfBuffer SYSTEM Common/Buffer
int fEqConfReadOn 0 Common/ReadOn. See note below.
int fEqConfPeriodMilliSec 1000 Common/Period
double fEqConfEventLimit 0 Common/Event limit
int fEqConfLogHistory 0 Common/Log History
bool fEqConfHidden 0 Common/Hidden
int fEqConfWriteCacheSize 10000000 Common/Write cache size

Note about fEqConfReadOn

As of April 2023 TMFE does not support all values here! It "simplifies"/mangles the configuration into

  • fEqConfReadOnlyWhenRunning = !(fEqConfReadOn & (RO_PAUSED|RO_STOPPED))
  • fEqConfWriteEventsToOdb = (fEqConfReadOn & RO_ODB).

i.e. RO_RUNNING|RO_PAUSED will implicitly mean events will be written all the time, even when the run is stopped...

You can of course also define and initialise your own member variables in the equipment constructor.

Example

Myfe(const char* eqname, const char* eqfilename) : TMFeEquipment(eqname, eqfilename) {
    fEqConfEnablePeriodic = true;
    fEqConfEventID = 3;
    fEqConfReadOn = RO_ALWAYS;
    fEqConfPeriodMilliSec = 2000;
}

Each equipment is defined by a separate class that inherits from midas.frontend.EquipmentBase.

Your class __init__() must call the base class __init__():

midas.frontend.EquipmentBase.__init__(self, client, equip_name, default_common)

The default_common parameter is a midas.frontend.InitialEquipmentCommon object. It contains the following members:

Member Default ODB key
self.equip_type None Common/Type
self.event_id None Common/Event ID
self.buffer_name "" Common/Buffer
self.trigger_mask 0 Common/Trigger mask
self.period_ms 1000 Common/Period
self.read_when midas.RO_RUNNING Common/Read on
self.log_history False Common/Log history

Example

class LargeEquipment(midas.frontend.EquipmentBase):
    def __init__(self, client):
        equip_name = "large"

        default_common = midas.frontend.InitialEquipmentCommon()
        default_common.equip_type = midas.EQ_PERIODIC
        default_common.buffer_name = "SYSTEM"
        default_common.event_id = 3
        default_common.period_ms = 2000
        default_common.read_when = midas.RO_RUNNING

        midas.frontend.EquipmentBase.__init__(self, client, equip_name, default_common)

Equipment types

Often each equipment talks to a piece of hardware, but you can also have purely software-based equipment. You may even want to have multiple equipment for a single piece of hardware, e.g. a polled equipment that sends the main data stream, plus a periodic equipment that sends some summary statistics every 5 seconds.

You need to create at least one equipment in your frontend. The main types of equipment are:

Equipment type Supported by Meaning
EQ_PERIODIC All frameworks Events are read out at a set frequency
EQ_POLLED All frameworks Framework repeatedly asks if any data is available
EQ_INTERRUPT mfe only Like EQ_POLLED, but can use a hardware interrupt to trigger readout (e.g. for some VME/CAMAC modules)
EQ_MANUAL_TRIG mfe and Python only Event readout is triggered by another client (e.g. webpage or user C++/python code)

Amount of equipment your frontend can contain

  • Any number of periodic equipment
  • Any number of manual trigger equipment
  • At most one polled or interrupt equipment (not both)

There are other "informational" flags that can be set when declaring your equipment type:

Flag Supported by Meaning
EQ_MULTITHREAD mfe only Split the readout functions into separate threads. Cannot be combined with EQ_INTERRUPT
EQ_SLOW mfe only Enables mfe device drivers
EQ_EB All frameworks Flags the equipment as one that the event builder should get data from
EQ_FRAGMENTED mfe only Allow sending very large events (bigger than the max event size) by splitting them into smaller fragments

Example equipment types

  • EQ_PERIODIC - classic periodic equipment
  • EQ_POLLED|EQ_MULTITHREAD|EQ_EB - polled equipment that should be considered by the event builder
  • EQ_INTERRUPT|EQ_SLOW - interrupt equipment that should use the device driver system
  • EQ_POLLED|EQ_MANUAL_TRIG - polled equipment that can also be triggered manually

When to read data

In most cases, your frontend will want to read data either only when a run is in progress, or all the time (even when a run is stopped). However, you can get more fine-grained control using the "Read on" flags:

  • In certain run states: RO_RUNNING, RO_STOPPED, RO_PAUSED (or RO_NOTRANS for all of them)
  • At run transitions: RO_BOR, RO_EOR, RO_PAUSE, RO_RESUME (or RO_TRANSITIONS for all of them)
  • Always: RO_ALWAYS

The "read on" flags are a bitmask, so you can have any arbitrary combination you like.

/Common in ODB vs code

All frameworks allow you to decide whether the ODB or code values should take precedence when starting the frontend. For example, what should happen if a user has changed the readout period in the ODB from 2000ms to 1000ms, but the code still says 2000ms? When starting the frontend, should the code overwrite the ODB, or accept the new value?

Set the global variable equipment_common_overwrite. True means the code will overwrite the ODB; false means the ODB value will be used. There is no default value - you must declare this variable.

Example to accept ODB values

BOOL equipment_common_overwrite = FALSE;

Set the member variable fEqConfReadConfigFromOdb in your equipment's constructor. False means the code will overwrite the ODB; true means the ODB value will be used. The default is true.

Exceptions

  • fEqConfEnabled and fEqConfEventLimit are always read from the ODB
  • fEqConfEnablePeriodic and fEqConfEnablePolled are always read from the code

Example to accept ODB values

Myfe(const char* eqname, const char* eqfilename) : TMFeEquipment(eqname, eqfilename) {
    fEqConfReadConfigFromOdb = true;
}

Pass the overwrite_common flag when calling the base class constructor. True means the code will overwrite the ODB; False means the ODB value will be used. The default is False.

Example to accept ODB values

class MyPeriodicEquipment(midas.frontend.EquipmentBase):
    def __init__(self, client):
        equip_name = "MyPeriodicEquipment"
        default_common = midas.frontend.InitialEquipmentCommon()

        midas.frontend.EquipmentBase.__init__(self, client, equip_name, default_common, overwrite_common=False)

Declaring your frontend

/* The frontend name (client name) as seen by other MIDAS clients   */
const char *frontend_name = "largefe";

/* The frontend file name, don't change it */
const char *frontend_file_name = __FILE__;

/* frontend_loop is called periodically if this variable is TRUE    */
BOOL frontend_call_loop = TRUE;

/* a frontend status page is displayed with this frequency in ms */
INT display_period = 0000;

/* maximum event size produced by this frontend */
INT max_event_size = 10000;

/* maximum event size for fragmented events (EQ_FRAGMENTED) */
INT max_event_size_frag = 5 * 1024 * 1024;

/* buffer size to hold events */
INT event_buffer_size = 10 * 10000;

/*-- Function declarations -----------------------------------------*/

INT frontend_init();
INT frontend_exit();
INT begin_of_run(INT run_number, char *error);
INT end_of_run(INT run_number, char *error);
INT pause_run(INT run_number, char *error);
INT resume_run(INT run_number, char *error);
INT frontend_loop();
/* The frontend name (client name) as seen by other MIDAS clients   */
const char *frontend_name = "largefe";

/* The frontend file name, don't change it */
const char *frontend_file_name = __FILE__;


/*-- Function declarations -----------------------------------------*/
INT frontend_init();
INT my_frontend_loop();

Your frontend is defined by a class that inherits from TMFeEquipment.

The main purpose of your frontend class is to instantiate your equipment classes in the frontend constructor, but you may implement the following functions:

// Initialisation - handle any arguments that appeared after "--" on the command-line.
// Order of operations is:
// - HandleArguments()
// - Connect to midas
// - HandleFrontendInit()
// - Call equip->HandleInit() for all equipments
// - HandleFrontendReady()
TMFeResult HandleArguments(const std::vector<std::string>& args);
TMFeResult HandleFrontendInit(const std::vector<std::string>& args);
TMFeResult HandleFrontendReady(const std::vector<std::string>& args);

// Print any usage information to screen (e.g. if you handle any arguments after "--" on the command-line).
void       HandleUsage();

// Do any cleanup just before we disconnect from midas.
void       HandleFrontendExit();

There are other functions defined in TMFrontend that you need to call, but do not need to implement:

// Set the name of this program as it will appear to other midas clients.
void       FeSetName(const char* program_name);

// Add an equipment.
TMFeResult FeAddEquipment(TMFeEquipment* eq);

There are some member variables in TMFrontend that may be useful:

// Access to TMFE singleton.
TMFE* fMfe;

// Frontend index (see below).
int  fFeIndex = 0;

// Set this to true if you don't want to allow the frontend to start if a run is already in progress.
bool fFeIfRunningCallExit = false;

// Call the begin-of-run logic for this frontend if the program is started while a run is already in progress.
bool fFeIfRunningCallBeginRun = true;

Example

In many cases, your frontend class consists of just a constructor, with all the initialization logic etc located in the equipment classes.

class MyFrontend: public TMFrontend {
public:
    MyFrontend() {
        FeSetName("Example Frontend");
        FeAddEquipment(new MyEquip("MyEquipment", __FILE__));
    }
};

The frontend is defined by a class that inherits from midas.frontend.FrontendBase.

Configuration of the frontend/equipment should be done in your class' __init__(). It should call the base class' __init__(), then create and add any equipment.

You may implement the begin_of_run() functions etc in your derived class, and they will be called at the appropriate transition.

There are some member variables in FrontendBase that may be useful:

# `midas.client.MidasClient` - the object we can use for ODB access etc.
self.client 

# (dict of {str: `EquipmentBase`}) - the equipment you added, keyed by equipment name.
self.equipment

# int - the current run state (midas.STATE_RUNNING etc)
self.run_state (int)

Example

class MyFrontend(midas.frontend.FrontendBase):
    def __init__(self):
        # You MUST call __init__ from the base class.
        midas.frontend.FrontendBase.__init__(self, "myfe_name")

        self.add_equipment(MyPeriodicEquipment(self.client))

Handling command-line arguments

The function mfe_get_args gives access to the frontend command line parameters.

Example

int argc;
char **argv;
mfe_get_args(&argc, &argv);
for (int i = 0; i < argc; i++) {
    // Use argv[i] 
}

The framework handles the following arguments automatically. All are optional:

-i NNN                  set frontend index number (if running multiple instances of the same frontend)
-h hostname[:tcpport]   connect to MIDAS mserver on given host and tcp port number
-e exptname             connect to given MIDAS experiment
-D                      become a daemon
-O                      become a daemon but keep stdout for saving in a log file: frontend -O >file.log 
-v [level]              set verbosity level (as an integer). A bare `-v` implies `-v 1`
-d                      used for debugging the frontend

The framework allows the user to specify arbitrary arguments on the command-line. Any arguments after -- are passed to all the equipments in the frontend. The arguments are passed as simple vector of strings to your equipment's HandleInit() function.

TMFeResult HandleInit(const std::vector<std::string>& args);

Example

For example, if the user runs my_fe.exe -v -- --some-arg 6 --other-arg, then

  • the framework will handle the -v argument (setting TMFE::gfVerbose to true)
  • your HandleInit() function will be given {"--some-arg", "6", "--other-arg"}

If your HandleInit() function returns an error, then the program will quit.

Another function related to this is the HandleUsage() function, which should print to screen any usage information (e.g. about --some-arg and --other-arg in the above example).

void HandleUsage();

The HandleUsage() function will be called if the user passes an invalid argument on the command-line or if the argument --help is present.

The framework recognises the following arguments automatically (before any --). All are optional:

--help                  print help message
-i NNN                  set frontend index number (if running multiple instances of the same frontend)
-h hostname[:tcpport]   connect to MIDAS mserver on given host and tcp port number
-e exptname             connect to given MIDAS experiment
-D                      become a daemon
-O                      become a daemon but keep stdout for saving in a log file: frontend -O >file.log 2>&1
-v                      print debug output to screen

The python framework uses argparse to handle command-line arguments. If you want to support custom arguments, you can work on the global parser object and call add_argument() etc.

Example

parser = midas.frontend.parser
parser.add_argument("-x", type=int, help="Some custom argument")

args = midas.frontend.parse_args()

print(f"X argument was {args.x}")

# Presumably you will use the argument for something in your frontend class.
my_fe = SomeFrontendClass(args.x)
my_fe.run()

The framework recognises the following arguments automatically. All are optional:

-i NNN                  set frontend index number (if running multiple instances of the same frontend)
-h hostname[:tcpport]   connect to MIDAS mserver on given host and tcp port number
-e exptname             connect to given MIDAS experiment
-D                      become a daemon
-O                      become a daemon but keep stdout for saving in a log file: frontend -O >file.log 2>&1
-d                      enable debug logging

If you are not adding custom arguments, you do not need to interact with the midas.frontend.parser object or call parse_args() - the framework will do it automatically for you.

Frontend index (-i flag)

If you have 3 digitizers in your experiment, you may wish to run 3 copies of your frontend program, each with slightly different settings. You can achieve this by specifying the -i flag on the command-line. This flag is supported by all the frameworks, but how to use it varies.

The mfe framework also supports the $MIDAS_FRONTEND_INDEX environment variable. Setting this variable overrides any -i flag. This variable is not supported by TMFE or python frontends - they only support the -i flag.

In mfe, the frontend index is available using the get_frontend_index() function.

INT get_frontend_index(void);

You can get the framework to create a unique name for your frontend (based on the frontend index) by specifying %02d as part of your equipment names.

Example

EQUIPMENT equipment[] = {
    {"my_equip_%02d",       /* equipment name */
    {3, 0,                  /* event ID, trigger mask */
     "BUFDRIVER%02d",       /* can also adjust the event buffer name! */
     //....

The frontend name is automatically gets the index appended using %02d (e.g. my_fe automatically becomes my_fe04 etc.).

In TMFE, the frontend index appears in the frontend as fFeIndex.

You can get the framework to create a unique name for your frontend (based on the frontend index) by specifying %02d as part of your frontend/equipment names.

Example

// In equipment constructor
EqPeriodic(const char* eqname, const char* eqfilename) : TMFeEquipment(eqname, eqfilename) {
    fEqConfBuffer = "BUF%02d";
}

// In frontend constructor
MyFrontend() {
    FeSetName("my_frontend_%02d");
    FeAddEquipment(new MyEquip("my_equip_%02d", __FILE__));
}

If you run with -i 4, for example, then the frontend will appear in midas as my_frontend_04 and the equipment as my_equip_04 (with ODB settings in /Equipment/my_equip_04).

In python, the frontend index appears in the frontend as the global variable frontend_index. You may use that variable to adjust frontend/equipment names as you wish:

Example

class MyMultiPeriodicEquipment(midas.frontend.EquipmentBase):
    def __init__(self, client):
        # If using the frontend_index, you should encode the index in your
        # equipment name.
        equip_name = f"MyMultiPeriodicEquipment_{midas.frontend.frontend_index:02d}"

class MyMultiFrontend(midas.frontend.FrontendBase):
    def __init__(self):
        # If using the frontend_index, you should encode the index in your
        # equipment name.
        fe_name = f"multife_{midas.frontend.frontend_index:02d}"

if __name__ == "__main__":
    # We must call this function to parse the "-i" flag, so it is available
    # as `midas.frontend.frontend_index` when we init the frontend object.
    midas.frontend.parse_args()

    # Now we just create and run the frontend like in the basic example.
    with MyMultiFrontend() as my_fe:
        my_fe.run()

Accessing the ODB

The framework provides a global variable hDB that can be used with any of the C++ ODB access tools. It does not provide any helper functions for getting the ODB directories for your equipment - you must build those paths yourself.

Example

int eq_idx = 0;
std::string settings_path = std::string("/Equipment/") + equipment[eq_idx].name + "/Settings";

midas::odb settings(settings_path);
double some_var = settings["some_odb_setting"];

The internal code of TMFE uses the MVOdb tools for ODB access. You can access MVOdb objects for certain paths from your derived classes:

# 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

Example MVOdb

double some_var = fOdbEqSettings->RD("some_odb_setting");

If you want to use the low-level db_xxx functions, you can use the fMfe->fDB member as your database handle:

Example low-level access

uint8_t data = 0;
INT size = sizeof(data);
INT status = db_get_value(fMfe->fDB, 0, "/Path/to/some/uint8", &data, &size, TID_UINT8, false);

if (status == SUCCESS) { 
    // data is valid
}

You can also use the midas::odb map-like interface to the ODB.

You can access the ODB using the odb_get() and odb_set() functions etc in midas.client.MidasClient. A MidasClient object is available as self.client in both EquipmentBase and FrontendBase.

Example in frontend

some_var = self.client.odb_get("/Path/to/something")

The following member variables are defined in the EquipmentBase, to help you access specific directories. They are all strings.

self.odb_common_dir      # /Equipment/xxx/Common
self.odb_stats_dir       # /Equipment/xxx/Statistics
self.odb_variables_dir   # /Equipment/xxx/Variables
self.odb_settings_dir    # /Equipment/xxx/Settings

Example in equipment

some_var = self.client.odb_get(f"{self.odb_variables_dir}/something)

Equipment settings

The variable self.settings is also defined, which is a dictionary representation of the content of self.odb_settings_dir.

The EquipmentBase also contains some callback functions that will be called when a variable in self.odb_settings_dir changes. You can implement either (or both) of these functions in your derived class:

self.settings is updated before either of these functions is called, but you may want to implement these function to validate any settings the user has set, for example.

Example use of settings callbacks in an equipment

def settings_changed_func(self):
    print("Something changed in self.settings!")

def detailed_settings_changed_func(self, path, idx, new_value):
    if idx is None:
        # A non-array value changed
        print(f"ODB setting {path} changed to {new_value})
    else:
        # An element in an array changed
        print(f"ODB setting {path}[{idx}] changed to {new_value})

Run transitions

The frameworks all register their own functions that get called during run transitions. As part of their work, they can call functions that you implement.

Do NOT deregister the functions that the frameworks have already registered!

In mfe, you MUST implement the following functions. If you don't need to do any work, the body can just contain return SUCCESS;.

INT begin_of_run(INT run_number, char *error);
INT end_of_run(INT run_number, char *error);
INT pause_run(INT run_number, char *error);
INT resume_run(INT run_number, char *error);

The functions should return:

  • SUCCESS if the transition should proceed
  • Any other value if the transition should not proceed. You can optionally populate error with a message indicating the reason why (max 255 characters).

Example

INT begin_of_run(INT run_number, char *error) {
    if (something_bad) {
        snprintf(error, 255, "Can't start run due to XYZ");
        return FE_ERR_DRIVER;
    }

    return SUCCESS;
}

In mfed, you may implement transition functions and register them with the framework. If you do not need to do any work, you don't need to implement the function.

Example

INT my_begin_of_run() {
    if (something_bad) {
        return FE_ERR_DRIVER;
    }

    return SUCCESS;
}

// Later, maybe in frontend_init():
install_begin_of_run(my_begin_of_run);

These functions will be called for each equipment in your frontend when the run starts/stops etc. Return TMFeOK() if you're happy for the transition to proceed, or an error if not.

TMFeResult HandleBeginRun(int run_number);
TMFeResult HandleEndRun(int run_number);
TMFeResult HandlePauseRun(int run_number);
TMFeResult HandleResumeRun(int run_number);
TMFeResult HandleStartAbortRun(int run_number);

To change the order in which your equipment is handled in the transition sequence, you can call:

fMfe->SetTransitionSequenceStart(int seqno);
fMfe->SetTransitionSequenceStop(int seqno);
fMfe->SetTransitionSequencePause(int seqno);
fMfe->SetTransitionSequenceResume(int seqno);
fMfe->SetTransitionSequenceStartAbort(int seqno);

To register multiple transition callbacks (e.g. to do a bit of work early in the transition sequence, and some more later in the sequence) you will need to call cm_register_transition() yourself (no TMFE wrapper).

These functions will be called in your frontend class when the run starts/stops etc. You are welcome to call functions in your equipment classes if you have transition-specific code that lives there.

If you implement your own overrides, they should return:

  • None or 1 - Transition was successful
  • int that isn't 1 - Transition failed
  • 2-tuple of (int, str) - Transition failed and the reason why

To change the order in which your equipment is handled in the transition sequence, you can use set_transition_sequence in MidasClient.

Example

self.client.set_transition_sequence(midas.TR_START, 200)

To register a callback function to get called on the "start abort" transition, you can use register_transition_callback in MidasClient.

Example

self.client.register_transition_callback(midas.TR_STARTABORT, 500, self.my_startabort_callback)

The same function can be used to register multiple transition callbacks (e.g. to do a bit of work early in the transition sequence, and some more later in the sequence).

Creating events

Polling

For polled equipment (EQ_POLLED), you must implement a function that tells the framework whether data is ready or not. The framework will repeatedly call this function.

In mfe, the function signature is

INT poll_event(INT source, INT count, BOOL test)

The arguments are:

  • source - contains the info.source member of this equipment's EQUIPMENT struct. You can set this to any number you like, and interpret it however you like, but a common use case is to encode a module/channel number to check. You can then have multiple frontends running the exact same code, just checking different modules based on the source parameter. Or change which module is checked at run-time by editing the ODB value /Equipment/xxx/Common/Source.
  • count - the framework can request you to do your test multiple times. See "timing" note below.
  • test - whether this is just a timing test. See note below.

Timing tests

During frontend initialization, the framework will try to determine how often your code needs to be called to match the period declaring in your equipment struct. The other frameworks just call your function multiple times, but mfe wants you to implement a loop yourself.

During initialization, the test flag is set to 1. In this case you should only return after you've performed the requested number of checks (even if data is available). After initialization, you can return as soon as data is available, or after you've performed the requested number of checks.

The return value of your polling function is passed to your readout function. This may be useful if you have multiple modules to check, and want to tell the readout function which module to read from.

Return 0 if the readout function should not be called at this time, as there is no data available.

In mfe, you MUST implement the poll_event() function

Example

INT poll_event(INT source, INT count, BOOL test) {
    for (int i = 0; i < count; i++) {
        int data_available = some_function();

        if (data_available && !test) {
            return data_available;
        }
    }

    return 0;
}

In mfed, you register your polling function using install_poll_event():

Example

INT my_poll_event(INT source, INT count, BOOL test) {
    for (int i = 0; i < count; i++) {
        int data_available = some_function();

        if (data_available && !test) {
            return data_available;
        }
    }

    return 0;
}

// Later, maybe in frontend_init():
install_poll_event(my_poll_event);

For polled equipment you should implement HandlePoll() in your equipment class to state whether you have any data to send.

bool HandlePoll();

It should return true if data should be read out, and false if not.

For polled equipment, you should implement poll_func() in your equipment class to state whether you have any data to send.

def poll_func(self):

It should return True if data should be read out, and False if not.

Event readout

All equipment types require a readout function to be implemented. It is responsible for reading data from the hardware, and packaging it into a midas event. The framework determines when the function gets called, depending on the equipment type and other configuration parameters.

In mfe, your readout function should be of the form

INT my_readout_function_name(char *pevent, int offset);

You specify a pointer to this function as part of the EQUIPMENT struct.

The arguments are:

  • pevent - the location where you should write your data. For polled events, the initial value points to an integer that was returned from poll_event(). You will overwrite this value as part of your readout routine.
  • offset - for super-events, the current super-event size

Return value

You readout function must return the size of the event in bytes.

This is a rare exception in midas where an integer return value is a size, rather than a status code.

If you don't actually want to generate an event, return 0.

The framework will automatically increment the event serial number for every call to the readout routine, as long as the returned size is non-zero.

Example

This code will create an event with 2 banks (TDBL and TINT), each containing a few values:

INT read_event(char *pevent, INT off) {
    double* ptr_d;
    INT* ptr_i;

    // Initialize bank structure
    bk_init32(pevent);

    // Write a bank. 
    // bk_create() will set `ptr_d` to where we should write our doubles.
    bk_create(pevent, "TDBL", TID_DOUBLE, (void **) &ptr_d);
    *ptr_d++ = 1.23;
    *ptr_d++ = 3.14;
    bk_close(pevent, ptr_d);

    bk_create(pevent, "TINT", TID_INT, (void **) &ptr_i);
    *ptr_i++ = 5;
    *ptr_i++ = 8;
    *ptr_i++ = 12;
    bk_close(pevent, ptr_i);

    // Return the size of the event
    return bk_size(pevent);
}

You need to call EqSendEvent() in your equipment to write a data event.

// Implement this for a periodic equipment
void HandlePeriodic();

// Implement this for a polled equipment 
void HandlePollRead();

To actually create/send events in HandlePeriodic()/HandlePollRead(), use the following functions:

TMFeResult ComposeEvent(char* pevent, size_t size) const;
TMFeResult BkInit(char* pevent, size_t size) const;
void*      BkOpen(char* pevent, const char* bank_name, int bank_type) const;
TMFeResult BkClose(char* pevent, void* ptr) const;

TMFeResult EqSendEvent(const char* event, bool write_to_odb=true);

Example

This code will create an event with 2 banks (TDBL and TINT), each containing a few values.

void HandlePeriodic() {
    // buf must be large enough for all your data plus some header words!
    char buf[1024];

    // Initialise the event
    ComposeEvent(buf, sizeof(buf));
    BkInit(buf, sizeof(buf));      

    // Create and populate the first bank (with values 1.23/3.14
    double* ptr_d = (double*)BkOpen(buf, "TDBL", TID_DOUBLE);
    *ptr_d++ = 1.23;
    *ptr_d++ = 3.14;
    BkClose(buf, ptr_d);      

    // Create and populate another bank
    int32_t* ptr_i = (int32_t*)BkOpen(buf, "TINT", TID_INT32);
    *ptr_i++ = 5;
    *ptr_i++ = 8;
    *ptr_i++ = 12;
    BkClose(buf, ptr_i);

    // Send the event to the midas buffer specified by fEqConfBuffer
    EqSendEvent(buf);

    // The midas buffer now contains an event with 2 banks. TDBL (with data [1.23,3.14]) and TINT (with data [5,8,12]).
}

To create events, you need to implement the readout_func() in your equipment class.

It should return a midas.event.Event, a list of such, or None (if no data is available).

Example periodic readout

This code will create an event with 2 banks (TDBL and TINT), each containing a few values:

def readout_func(self):
    event = midas.event.Event()

    event.create_bank("TDBL", midas.TID_DOUBLE, [1.23, 3.14])
    event.create_bank("TINT", midas.TID_INT, [5, 8, 12])

    return event

Sending to buffers

The events you create in your readout function can be sent to event buffers. The midas logger can be configured to read data from these buffers and write them to disk. All frontend frameworks support writing events to buffers by setting the Buffer for your equipment.

In your list of EQUIPMENT structs, set info.buffer.

In your equipment class, set fEqConfBuffer.

In your equipment class, set InitialEquipmentCommon.buffer_name.

The default buffer that the logger reads from is called SYSTEM. Other clients can also be configured to read events from buffers (e.g. for analysis purposes).

Recording in history

The midas logger looks at entries in /Equipment/xxx/Variables, and can periodically record them in the history system.

There are two ways to write data to the Variables directory:

  1. Write to the ODB directly
  2. Ask the frontend to copy your events there

There are also 2 flags that control this behaviour - one saying whether the frontend should copy events to the Variables directory, and one saying whether data that's in the Variables directory should be logged to the ODB.

  • To copy events to the ODB, set the RO_ODB bit in info.read_on in your list of EQUIPMENT structs.
  • To ask the logger to record your Variables in the history system, set info.history in your list of EQUIPMENT structs.
  • To copy events to the ODB, set the RO_ODB bit in fEqConfReadOn in your equipment class.
  • To ask the logger to record your Variables in the history system, set fEqConfLogHistory in your equipment class.
  • To copy events to the ODB, set the RO_ODB bit in InitialEquipmentCommon.read_when in your equipment class.
  • To ask the logger to record your Variables in the history system, set InitialEquipmentCommon.log_history in your equipment class.

Note that the python frontend will automatically copy events to the ODB if you set log_history to a non-zero value. In the C++ frontends you must explicitly set both RO_ODB and log_history.

The downside of the python approach is that you cannot have one equipment that produces midas events in the readout function (that it doesn't want logged in the history) and manually writes to Variables in the ODB (which is does want logged in the history).

Example 'read on' flags

Setting Decimal value Read events when run is in progress Read events when run is stopped Copy events to ODB
(C++)
Copy events to ODB
(Python)
RO_RUNNING 1 ✔ If log_history set
RO_ALWAYS 255 ✔ ✔ If log_history set
RO_RUNNING|RO_ODB 257 ✔ ✔ ✔
RO_ALWAYS|RO_ODB 511 ✔ ✔ ✔ ✔

The "log history" flag encodes both whether to log data to the history system, and the maximum rate at which to record them. The maximum rate is 1Hz (1s intervals).

Example 'log history` flags

  • 0 - do not log in the history system
  • 1 - record at most every 1s, and only if the ODB variable was written to
  • 10 - record at most every 10s, and only if the ODB variable was written to

Restrictions on manually writing to ODB Variables

When you copy midas events to ODB Variables, the ODB entry is the same as the bank name (i.e. 4 letters long). Manually writing to Variables gives you more flexibility, but there are some restrictions:

  • The colon character (:) should be avoided
  • The variable name cannot end in a space ()
  • You may only add one intermediate directory - /Equipment/xxx/Variables/yyy and /Equipment/xxx/Variables/dir/yyy are okay; /Equipment/xxx/Variables/dir/dir/yyy is prohibited.
  • The overall combination of tag name + variable name must be less than 31 characters. The tag name is a little complicated to determine, so easiest to just try to keep names short, and make them shorter if the logger complains...

Supported variable types

The history system does not support all variable types:

  • Supported - all numerical values (except 64-bit integers)
    • TID_UINT8, TID_INT8, TID_CHAR, TID_UINT16, TID_INT16, TID_UINT32, TID_INT32, TID_BOOL, TID_FLOAT, TID_DOUBLE, TID_BITFIELD
  • Unsupported - strings, 64-bit integers, other non-numerical types
    • TID_STRING, TID_INT64, TID_UINT64, TID_ARRAY, TID_STRUCT, TID_KEY, TID_LINK

Manually-triggered events

The mfe and Python frameworks support the "manual trigger" type of equipment. Enable this by setting the EQ_MANUAL_TRIG bit in the equipment type. You can combine this with other types within the same equipment (e.g. EQ_MANUAL_TRIG|EQ_POLLED).

Detecting if this is a manually-triggered call

In some cases, the same readout code may be used for two types of event: a manual trigger and (say) a poll event. The frameworks allow you to distinguish between the two types of trigger in the readout functions:

In the readout routine, use the DATA_SIZE macro to determine if the "manual trigger" flag has been set:

flag = DATA_SIZE(pevent);
// flag = 0 - normal call
// flag = 1 - manual trigger

In the readout routine, check the is_manual_trig member of the equipment class.

flag = self.is_manual_trig

An event can be triggered either by other code, or from a web call.

Triggering the readout

BOOL trigger_manual_event(char* client_name) {
    HNDLE hconn;
    INT other_event_id = 2;

    if (run_state != STATE_RUNNING) {
        return FALSE;
    }

    // Check the frontend client exists
    if (!cm_exist(client_name, TRUE)) {  
        cm_msg(MERROR, __FUNCTION__, "Frontend client %s not running (%d)", client_name, status);
        return FALSE;
    }

    INT status = cm_connect_client(client_name, &hconn);
    if (status != RPC_SUCCESS) {
        cm_msg(MERROR, __FUNCTION__, "Cannot connect to frontend \"%s\" (%d)", client_name status);
        return FALSE;
    } 

    // Successfully connected to frontend client
    // Do the actual triggering
    status = rpc_client_call(hconn, RPC_MANUAL_TRIG, other_event_id);

    if (status != CM_SUCCESS) {
        cm_msg(MERROR, __FUNCTION__, "Error triggering event from frontend (%d)", status);
        return FALSE;
    }

    // Successfully triggered event
    status = cm_disconnect_client(hconn, FALSE);

    if (status != CM_SUCCESS) {
        cm_msg(MERROR, __FUNCTION__, "Error disconnecting client (%d)",status);
    }

    return TRUE;
}
import midas.client
other_event_id = 2

with midas.client.MidasClient("my_client") as client:
    conn = client.connect_to_other_client("other_client_name")
    client.manual_trig_client_call(conn, other_event_id)
    client.disconnect_from_other_client(conn)

The URL ?cmd=Trigger/<equipment_name> can be used to trigger an event (e.g. http://my.host:8080/?cmd=Trigger/MyEquipmentName).

If you add this URL as an alias, a link will appear in the menu on each midas webpage. Clicking the link will trigger the event readout, and return the user to the main midas status page.

You can also write Javascript/HTML code to add a button to a custom webpage:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="midas.css">
    <script src="controls.js"></script>
    <script src="midas.js"></script>
    <script src="mhttpd.js"></script>
    <script type="text/javascript">
        async function trigger_event() {
            const url = "?cmd=Trigger/MyEquipmentName";
            try {
                const response = await fetch(url);
                if (!response.ok) {
                    throw new Error(`Response status: ${response.status}`);
                }

                // result will contain HTML - either the status page or an error message!
                const result = await response.text();

                if (result.indexOf("<title>MIDAS error</title>") == -1) {
                    console.log("SUCCESS!");
                } else {
                    // Made the request, but couldn't trigger. Extract the error message.
                    let h1_start = result.indexOf("<H1>");
                    let h1_end = result.indexOf("</H1>");
                    let error_text = result.substring(h1_start + 4, h1_end);
                    console.error(error_text);
                }
            } catch (error) {
                // Couldn't even make the request
                console.error(error.message);
            }
        }
    </script>
    <title>My page</title>
</head>

<body class="mcss" onload="mhttpd_init('My page');">

    <!-- header and side navigation will be filled in mhttpd_init -->
    <div id="mheader"></div>
    <div id="msidenav"></div>

    <div id="mmain">

    <button class="mbutton" onclick="trigger_event()">Click to trigger an event!</button>

    </div>
</body>
</html>

There is no mjsonrpc function for triggering an event, so this example requests the ?cmd=Trigger URL instead. If an mjsonrpc function would be useful for your experiment, let us know on the midas discussion forum.

Note that TMFE does not natively support manually-triggered equipment, but you could implement something similar using RPC handlers. Other clients could use JRPC to request an event get readout, your equipment could set an internal flag saying that a trigger has been requested, and you could check that flag in your HandlePoll() function. You would not be able to trigger using the ?cmd=Trigger/xxx URL, but you could send JRPC commands from C++/Python/Javascript code.

Equipment status

You can update the status of your equipment at any time. The status of your equipment is shown on the main midas status page.

The status string should be fairly short, to avoid creating a messy layout on the webpage. You can specify any CSS color as the color, but midas also has some pre-defined colors if you want to match the webpage theme: --var(mgreen), --var(myellow), --var(mred), --var(mblue), --var(morange), --var(mgray).

You may call set_equipment_status() at any time to update the status of your equipment in the ODB.

INT set_equipment_status(const char *name, const char *eq_status, const char *status_class);

Example

// Set a happy message
set_equipment_status("largefe", "All systems go!", "--var(mgreen)");

// Set a warning message
set_equipment_status("largefe", "Trouble brewing...", "--var(myellow)");

You may call EqSetStatus() (from your equipment class) at any time to update the status of your equipment in the ODB.

TMFeResult EqSetStatus(char const* eq_status, char const* eq_color);

Example

// Set a happy message
EqSetStatus("All systems go!", "--var(mgreen)");

// Set a warning message
EqSetStatus("Trouble brewing...", "--var(myellow)");

You can set the equipment status from either the equipment or frontend classes.

Example from the equipment class

# Set a happy message
self.set_status("All systems go!", "--var(mgreen)")

Example from the frontend class

# Set a warning message for a specific equipment
self.set_equipment_status("MyPeriodicEquipment", "Trouble brewing...", "--var(myellow)"):

# Set a happy message for ALL equipment in this frontend
self.set_all_equipment_status("All systems go!", "--var(mgreen)"):

RPC handling

Midas allows programs to talk to each other via RPC. The most common user-facing approach is the "RPC_JRPC" call, which can be integrated with the midas webserver and custom webpages, and also has a wrapper in the Python library.

When another program calls JRPC, they say which client they want to talk to, specify a "command" (a short text string), and give some "arguments" (a text or JSON string).

The mfe framework does not provide a specific wrapper for this. See the passing data between programs documentation for the standard tools for this.

TMFE provides a wrapper for handling RPC calls. You can implement the HandleRpc() function in your equipment:

# JRPC handler
TMFeResult HandleRpc(const char* cmd, const char* args, std::string& result);

# BRPC handler
TMFeResult HandleBinaryRpc(const char* cmd, const char* args, std::vector<char>& result);

The data you put in result will be returned to the calling program. As of April 2023, the TMFeResult you return is ignored.

Multiple equipment

If you have multiple equipment in your frontend, HandleRpc() will be called on each in turn until one of them returns a non-empty result string. Take care over your command naming to avoid collisions!

Example

An example function might be:

TMFeResult HandleRpc(const char* cmd, const char* args, std::string& result) {
    if (strcmp(cmd, "my_command") == 0) {
        // Do some work.
        // Could return some data as JSON, or a status string, or...
        result = "OK";
    }

    return TMFeOk();
}

See the passing data between programs documentation for the standard tools for this.

Frontend exit

You can do work when the frontend exits. Common use cases are to restore some equipment to a safe state.

Note that this is not guaranteed to work in all situations! If the computer crashes, or the operating system kills your process, the "frontend exit" code may not be called!

You MUST define the frontend_exit() function (but the body can just be return SUCCESS;).

Example

INT frontend_exit() {
    printf("Goodbye!\n");
    return SUCCESS;
}

You MAY define a function and then register it with install_frontend_exit().

Example

INT my_frontend_exit() {
    printf("Goodbye!\n");
    return SUCCESS;
}

// Later, maybe in frontend_init():
install_frontend_exit(my_frontend_exit);

You can implement the HandleFrontendExit() function in your frontend class.

Example

void HandleFrontendExit() {
    printf("Goodbye!\n");
}

You can implement the frontend_exit() function in your frontend class.

Example

def frontend_exit(self):
    print("Goodbye!")

frontend_exit() is only guaranteed to be called if you create your frontend with a with statement, as in the main example. See Doing work when the program exits for why.

main

You do NOT need to implement a main() function for mfe-based frontends. It is implemented for you in mfe.cxx, which will be used when you compile your code.

You need to implement a main() function, but it is 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.

Example

int main(int argc, char* argv[]) {
    MyFrontend fe;
    return fe.FeMain(argc, argv);
}

You need to call the run() function in your frontend class. It is STRONGLY recommended that you use the with syntax (context manager) for your frontend object.

Example

if __name__ == "__main__":
    with MyFrontend("my_fe") as my_fe:
        my_fe.run()

Compilation

Below are minimal CMakeLists.txt files that can be used to compile mfe-based frontends.

Compilation can then be done using the following commands:

mkdir build
cd build
cmake ..
make

Some older operating systems may require you to use the cmake3 command instead of cmake.

In the below examples we specify the midas::mfe target as we're building an mfe-based frontend. See the list of CMake targets for more details about these targets.

Modern CMake example

cmake_minimum_required(VERSION 3.14)
project(myfe)
find_package(Midas REQUIRED PATHS $ENV{MIDASSYS})
add_executable(myfe.exe myfe.cxx)
target_link_libraries(myfe.exe midas::mfe)

Older CMake example (explicit dependencies)

cmake_minimum_required(VERSION 3.0)
project(myfe)

# Check for MIDASSYS environment variable
if (NOT DEFINED ENV{MIDASSYS})
    message(SEND_ERROR "MIDASSYS environment variable not defined.")
endif()

set(CMAKE_CXX_STANDARD 17)
set(MIDASSYS $ENV{MIDASSYS})

if (${CMAKE_SYSTEM_NAME} MATCHES Linux)
    set(LIBS -lpthread -lutil -lrt)
endif()

# Define the executable to be built, and the source code files
add_executable(myfe.exe myfe.cxx)

# Directories to search for 
target_include_directories(myfe.exe PRIVATE ${MIDASSYS}/include)

# Libraries to link to
target_link_libraries(myfe.exe ${MIDASSYS}/lib/libmfe.a ${MIDASSYS}/lib/libmidas.a ${LIBS})

CMake example

cmake_minimum_required(VERSION 3.14)
project(myfe)
find_package(Midas REQUIRED PATHS $ENV{MIDASSYS})
add_executable(myfe.exe myfe.cxx ${MIDASSYS}/src/mfed.cxx) #(1)!
target_link_libraries(myfe.exe midas::mfe)
  1. Note the extra source of mfed.cxx!

Below is a minimal CMakeLists.txt files that can be used to compile TMFE-based frontends.

Compilation can then be done using the following commands:

mkdir build
cd build
cmake ..
make

Modern CMake example

cmake_minimum_required(VERSION 3.14)
project(myfe)
find_package(Midas REQUIRED PATHS $ENV{MIDASSYS})
add_executable(myfe.exe myfe.cxx)
target_link_libraries(myfe.exe midas::midas)

Older CMake example (explicit dependencies)

cmake_minimum_required(VERSION 3.0)
project(myfe)

# Check for MIDASSYS environment variable
if (NOT DEFINED ENV{MIDASSYS})
    message(SEND_ERROR "MIDASSYS environment variable not defined.")
endif()

set(CMAKE_CXX_STANDARD 17)
set(MIDASSYS $ENV{MIDASSYS})

if (${CMAKE_SYSTEM_NAME} MATCHES Linux)
    set(LIBS -lpthread -lutil -lrt)
endif()

# Define the executable to be built, and the source code files
add_executable(myfe.exe myfe.cxx)

# Directories to search for 
target_include_directories(myfe.exe PRIVATE ${MIDASSYS}/include)

# Libraries to link to
target_link_libraries(myfe.exe ${MIDASSYS}/lib/libmidas.a ${LIBS})

No need to compile anything in python!

Framework-specific features

mfe interrupt events

An interrupt equipment is very similar to a polled equipment, but uses hardware interrupts to determine if data is available, rather than a software loop.

An example use case is if you're running a frontend on a single-board computer in a VME crate. Such machines have access to the VME bus, and other VME modules can send interrupts when they have data available. Similar concepts work for CAMAC crates.

Configuring interrupts

You must implement the interrupt_configure() function, and configure your hardware accordingly. The three parameters are:

  • cmd - the command the mfe framework requires you to do - CMD_INTERRUPT_ENABLE, CMD_INTERRUPT_DISABLE, CMD_INTERRUPT_ATTACH, CMD_INTERRUPT_DETACH
  • source - when cmd is CMD_INTERRUPT_ATTACH, this contains the index of this equipment within the list of EQUIPMENT structs. For all other commands, it is zero.
  • adr - the function that should be called when an interrupt happens. This function is provided by the mfe framework, but it will call your readout function as part of its work.

The attach/detach commands are only called when the frontend is initialized/exited. The enable/disable commands may be called multiple times by the mfe framework, mostly to prevent deadlocks with other pieces of code that need to run.

Example interrupt_configure() function

INT interrupt_configure(INT cmd, INT source, PTYPE adr) {
    int vec = 0;

    switch (cmd) {
        case CMD_INTERRUPT_ENABLE:
            if (inRun) mvme_write_value(myvme, VLAM_BASE+4, 0x1);
            break;

        case CMD_INTERRUPT_DISABLE:
            if (inRun) mvme_write_value(myvme, VLAM_BASE+4, 0x0);
            break;

        case CMD_INTERRUPT_ATTACH:
            mvme_set_dmode(myvme, MVME_DMODE_D32);
            mvme_interrupt_attach(myvme, INT_LEVEL, INT_VECTOR, (void *)adr, &myinfo);
            mvme_write_value(myvme, VLAM_BASE+0x10, INT_VECTOR);
            vec = mvme_read_value(myvme, VLAM_BASE+0x10);
            printf("Interrupt attached to 0x%x for vector:0x%x\n",cadr, vec&0xFF);
            break;

        case CMD_INTERRUPT_DETACH:
            printf("Interrupt detach\n");
            break;
    }
    return SUCCESS;
}

Configuring readout

As with a polled event, you can determine the interrupt "source" in your readout function by looking at the content of pevent, before you use that pointer for writing your data. It contains the interrupt source bitwise register. This feature can be exploited in order to identify which hardware module has triggered the readout when multiple interrupts have been assigned to the same readout function.

The readout routine would contain code such as:

INT read_trigger_event(char *pevent, INT off) {
    DWORD  *pdata;

    /* read ADC0 data */
    v792_EvtCntRead(myvme, VADC0_BASE, &evtcnt);
    ........
    /* Read Event */
    v792_EventRead(myvme, VADC0_BASE, pdata, &nentry);
    ........
    v792_DataClear(myvme, VADC0_BASE);
    ........
    return (size);
}

The data needs to be packed into events as shown in the event readout section above (i.e. using bk_init() etc.).

mfe slow control (EQ_SLOW)

See Slow control: programming.

mfe fragmented events

The mfe framework allows you to split very large data payloads into multiple events. This was primarily designed for old, embedded hardware with limited memory. It is generally unnecessary in modern systems!

When the equipment type includes the EQ_FRAGMENTED bit, the maximum event size (max_event_size) becomes the maximum fragment size. The true "unfragmented" event size is set by max_event_size_frag.

Your readout frontend generates events as normal. The mfe framework fragments the data for you. The data gets split into multiple event fragments:

  • A "preliminary" fragment is written to tell you the total amount of data to expect (so your analysis code knows how to reconstruct the full payload). The event ID of this "preliminary" fragment gets OR'd with EVENTID_FRAG1 (0xC000). So if the original equipment event ID was 3, the event ID of this fragment will be 0xC003. The event contains just a single 32-bit integer, which is the total number of bytes of the full event.
  • Subsequent fragments contain the actual data. The event ID of these subsequent fragments gets OR'd with EVENTID_FRAG (0xD000). So if the original equipment event ID was 3, the event ID of the subsequent fragments will be 0xD003. All these fragments have a standard MIDAS_HEADER before their data.
  • There is no "trailer" fragment - your analysis code knows that all the fragments have been sent when the size of all the data fragments matches the size specified in the preliminary fragment.

Each fragment just contains a chunk of raw data, up to max_event_size. If you're using MIDAS format events, the split could happen in the middle of a bank, or even in the middle of a bank header! Your analysis code should reconstruct the event fragments into a single event before trying to parse it.

mfe super-events and sub-events

The mfe framework allows you to reduce the overhead of writing midas bank headers for small events. A BANK32 header is 20 bytes, so if your data is only 10s of bytes in size, then the bank headers can be a non-negligible overhead. To address this problem, the equipment can be set up to generate a "super-event" which is an event composed of the initial standard bank header, then the data of multiple "sub-events" concatenated together.

In TMFE/Python, it is expected that you will just batch up your data in memory, and write out the larger event yourself when needed.

You can specify the maximum number of "sub-events" in your "super-event" using the num_subevents member of the EQUIPMENT_INFO struct.

Example equipment declaration

 // Define equipment to be able to generate the Super-Event 
{ "GE",                 // equipment name 
    2, 0x0002,            // event ID, trigger mask 
    "SYSTEM",             // event buffer 
    EQ_INTERRUPT,         // equipment type 
    LAM_SOURCE(GE_C, LAM_STATION(GE_N)), // interrupt source 
    "MIDAS",              // format 
    TRUE,                 // enabled 
    RO_RUNNING,           // read only when running 
    200,                  // poll for 200ms 
    0,                    // stop run after this event limit 
    1000,                 // number of sub events > 0 ... enables Super-event
    0,                    // don't log history 
    "", "", "",
    read_ge_event},       // readout routine 
    ...
}

Your readout function will be called multiple times, indicating the current event size so far.

  • offset = 0 indicates that this is the first event, and you should initialise any banks etc.
  • offset = -1 indicates that the superevent limit has been reached, and you should close any banks etc.
  • Any other offset indicates this is an intermediate event, and denotes the total event size so far

Example readout code

// Global and fixed -- Expect NWORDS 16bits data readout per sub-event
#define NWORDS 3

INT read_ge_event(char *pevent, INT offset) {
    static WORD *pdata;

    // Super-event structure 
    if (offset == 0) {
        // FIRST event of the Super-event 
        bk_init(pevent);
        bk_create(pevent, "GERM", TID_WORD, &pdata);
    } else if (offset == -1) {
        // close the Super-event if offset is -1
        bk_close(pevent, pdata);

        // End of Super-Event
        return bk_size(pevent);
    }

    // read GE sub-event (ADC) 
    cam16i(GE_C, GE_N, 0, GE_READ, pdata++);
    cam16i(GE_C, GE_N, 1, GE_READ, pdata++);
    cam16i(GE_C, GE_N, 2, GE_READ, pdata++);

    // clear hardware 
    re_arm_ge(); 

    if (offset == 0) {
        // Compute the proper event length on the FIRST event in the Super-Event
        // NWORDS correspond to the !! NWORDS WORD above !!
        // sizeof(BANK_HEADER) + sizeof(BANK) will make the 16 bytes header
        // sizeof(WORD) is defined by the TID_WORD in bk_create()

        return NWORDS * sizeof(WORD) + sizeof(BANK_HEADER) + sizeof(BANK);
    } else {
        // Return the data section size only
        // sizeof(WORD) is defined by the TID_WORD in bk_create()

        return NWORDS * sizeof(WORD);
    }
}

As shown in the example above:

  • For the first event, the correct size of the event, including the header, must be calculated and returned
  • Subsequent events return the size of the data only, excluding the header.
  • The input parameter offset is used to indicate whether the event is the first, last or intermediate. * After the last event, the bank is closed.

The encoding of the data section is left to the user. If the number of words per sub-event is fixed (i.e. NWORDS in the above example), the sub-event extraction by an analyzer is simple. In the case of variable sub-event length, it is necessary to tag the first or the last word of each sub-event. The contents of the sub-event is the choice of the user.

Note

  • Since no particular tagging is applied to the Super-Event by the midas transfer mechanism, the user must provide code in the backend analyzer to interpret the contents of the Super-Event bank(s).
  • If the Super-Event is composed by an equipment on a remote processor running a different Endian mode than the backend processor, it would be necessary to ensure the data type consistency throughout the Super-Event in order to guarantee the proper byte-swapping of the data content. Byte Swap Macros are available for this purpose.
  • It may be convenient to change the time-stamp of the super-event using the TIME_STAMP Macro.
  • The ODB key /Equipment/xxx/Statistics/Event rate will indicate the rate of sub-events.

mfe FIXED event format

The mfe framework supports writing FIXED format events and having them logged in the history system. To enable this, you must provide a textual description of the format and pass it as the event_descrip member of the EQUIPMENT struct.

Example config for a FIXED event struct

// FIXED event structure definition
typedef struct {
    int adc0;
    int adc1;
    int tdc0;
    int tdc1;
} TRIGGER_EVENT;

// Text version of the above format
char *trigger_event_str[] = {
"adc0 = INT : 0",
"adc1 = INT : 0",
"tdc0 = INT : 0",
"tdc0 = INT : 0",
};

// Equipment list includes the trigger_event_str
EQUIPMENT equipment[] = {

    {"example_eqp",          /* equipment name */
    {3, 0,                   /* event ID, trigger mask */
    "SYSTEM",                /* event buffer */
    EQ_PERIODIC,             /* equipment type */
    0,                       /* event source */
    "FIXED",                 /* format */
    TRUE,                    /* enabled */
    RO_ALWAYS,               /* read when running and on transitions */
    2000,                    /* read every 2 sec */
    0,                       /* stop run after this event limit */
    0,                       /* number of sub events */
    1,                       /* log history */
    "", "", ""},
    read_event,              /* readout routine */
    NULL, NULL,              /* keep null */
    trigger_event_str,       /* init string */
    },

    {""}
};

INT read_event(char* pevent) {
    TRIGGER_EVENT* ptrg = (TRIGGER_EVENT*)pevent;
    ptrg->adc0 = 123;
    ptrg->adc1 = 456;
    ptrg->tdc0 = 12345;
    ptrg->tdc1 = 67890;
    return sizeof(TRIGGER_EVENT);
}

In principle, one can also use the event_descrip member for midas format events, by specifying a pointer to a BANK_LIST structure. This allows mfe to pre-populate the /Equipment/xxx/Variables tree during frontend initialization. In practice this is unnecessary, as the structure will be created the first time an event is read out, which presumably will only be a few seconds later...

mfe frontend loop

If the global variable frontend_call_loop is set to TRUE, the function frontend_loop() will be called when the frontend is idle and at least once between every event. You could use it for example to check if there has been a timeout from hardware.

For other frontend frameworks, such checks are usually implemented as part of a polled equipment's polling check, or as part of a periodic equipment's readout function.

Example frontend loop

BOOL frontend_call_loop = TRUE;

INT frontend_loop() {
    // Implement any code that needs to be run very frequently
    return SUCCESS;
}

mfe display

The global variable display_period allows you to display the state of the frontend on console. It shows the same information as on the main midas status webpage, but only for the equipment in this frontend. It is almost completely obsolete, and most users set it to 0 (do not display the status). Numbers greater than zero are the update frequency in milliseconds.

Example display

my_fe connected to <local>. Press "!" to exit
================================================================================
Run status:   Running    Run number 12345
================================================================================
Equipment     Status     Events     Events/sec Rate[B/s]  ODB->FE    FE->ODB
--------------------------------------------------------------------------------
equip_a       OK         1.372M     5.634k     7.891M     0          0
other_equip   OK         143        1.132      1.032k     0          143