Skip to content

Slow control: programming

This page covers writing slow control frontends and drivers.

For the concepts, and whether to use a slow control framework at all, see Slow control: overview.

The slow control frameworks are optional. If you write a normal frontend for your slow control equipment instead, see Frontends: programming.

Choosing a framework

Many experiments write a normal frontend, even for their slow control equipment. The slow control frameworks help most when midas already has drivers for your hardware. Midas has two frameworks, and a frontend uses one or the other. See Do you need a slow control framework? for how they differ.

Normal frontend Class driver framework mdev framework
Write it with mfe (C++) ✔ ✔ ✔
Write it with TMFE or Python ✔
Drivers for common hardware included in midas Many MSCB, Revolution Pi, TDK
Only a few lines of code, if a driver exists ✔
Names, units and formats for the equipment page set up for you ✔ ✔
Same ODB layout for all devices of a kind (e.g. Demand, Measured) ✔
Limits and ramping for high voltage, without extra code ✔
Each device read in its own thread ✔
Modern C++ interface for writing new drivers ✔ ✔
Full control over the ODB layout and the event format ✔

With either framework, all control goes through the ODB, so neither is suitable for systems that need to be safe if the ODB or the frontend fails (e.g. interlocks).

Pros and cons of the class driver framework

Pros

  • If midas already has a driver for your hardware, you only write a few lines of code: the equipment and driver lists
  • Every frontend that uses the same class driver has the same ODB layout, e.g. Demand and Measured arrays for high voltage. Other programs, the web interface and scripts change the settings by writing to the ODB, without talking to the frontend
  • The class drivers create the names, units and formats that the equipment page uses, so the values are shown and can be edited there without any extra work
  • The class drivers add features such as voltage limits and ramping (for high voltage), or factors, offsets and formulas (for the multi class driver)
  • Each device can be read in its own thread, so a slow device doesn't block the others or the frontend

Cons

  • It only works with the mfe frontend framework, in C++. It doesn't work with TMFE or Python frontends
  • If there is no driver for your hardware, you have to write a device driver with a C-style interface. That can be as much work as writing a normal frontend
  • The class driver decides the data model: each channel is a single float, and the ODB layout and event format are fixed. Devices that don't fit this model are awkward to support
  • Many of the drivers are old, and some haven't been updated to compile as C++ yet (see Drivers included in midas)

Pros and cons of the mdev framework

Pros

  • Each driver is a C++ class that uses odbxx, which is easier to read and extend than the C-style interface of the class driver framework
  • The frontend code chooses the channels, with their names, units and formats. With mdev_mscb it can also convert the values with a function, and check an output value with a function before it is set
  • Helper functions create history panels for the channels

Cons

  • There are only three drivers: MSCB, the Revolution Pi and TDK power supplies
  • You write more of the frontend yourself: the equipment, the loop function and the readout routine
  • Features such as limits and ramping depend on each driver, rather than coming from a shared class driver
  • It only works with C++ frontends, and the examples are mfe frontends

Class driver framework

A class driver frontend is an mfe frontend, whose equipment has the type EQ_SLOW and names a class driver and a list of device drivers. You don't write a readout routine: the class driver provides it.

Drivers included in midas

The drivers for the class driver framework are in $MIDASSYS/drivers:

  • class: the class drivers
  • device: device drivers, for example for CAEN (SY2527, SY4527), iseg, Wiener and LeCroy high voltage supplies, MSCB devices, EPICS channels, network power switches and several PSI beamline devices. nulldev is a device driver that does nothing, for testing and as a template
  • bus: bus drivers for RS-232 (rs232), TCP/IP (tcpip) and MSCB (mscbbus), and null

The most common drivers are compiled into the midas::drivers library: the class drivers except cd_fgd (whose source file is called cd_fdg.cxx), the null bus driver, nulldev, mscbdev and mscbhv4. To use another driver, add its source file to your frontend's build (see Building a class driver frontend).

For a list of every driver, see the Driver library.

Not all drivers have been updated

Midas was converted from C to C++ in 2019. Some of the older drivers haven't been updated since, and may need changes before they compile or work. Check a driver before you rely on it.

Example class driver frontends are in $MIDASSYS/examples/slowcont (scfe.cxx and mscb_fe.cxx) and $MIDASSYS/examples/epics.

Declaring the equipment

The example below uses the generic class driver with the EPICS device driver. It is based on $MIDASSYS/examples/epics/frontend.cxx. $MIDASSYS/examples/slowcont/scfe.cxx is a similar example using the hv and multi class drivers.

Example equipment listing

DEVICE_DRIVER epics_driver[] = {
    {"Beamline", epics_ca, 20, NULL}, //(1)!
    {""}
};

EQUIPMENT equipment[] = {
    {"Beamline",                    /* equipment name */
        { 3, 0,                     /* event ID, trigger mask */
        "SYSTEM",                   /* event buffer */
        EQ_SLOW,                    /* equipment type */
        0,                          /* event source */
        "MIDAS",                    /* format */
        TRUE,                       /* enabled */
        RO_RUNNING|RO_TRANSITIONS,  /* read when running and on transitions */
        60000,                      /* read every 60 sec */
        0,                          /* stop run after this event limit */
        0,                          /* number of sub events */
        1,                          /* log history every event */
        "", "", "" },
        cd_gen_read,                /* readout routine */ //(2)!
        cd_gen,                     /* class driver main routine */ //(3)!
        epics_driver,               /* device driver list */ //(4)!
        NULL,                       /* init string */
    },

    {""}
};
  1. epics_ca is defined in drivers/device/epics_ca.cxx
  2. cd_gen_read is defined in drivers/class/generic.cxx
  3. cd_gen is defined in drivers/class/generic.cxx
  4. epics_driver is defined above

The readout routine and class driver come in pairs: cd_gen_read and cd_gen for the generic class driver, cd_hv_read and cd_hv for hv, and cd_multi_read and cd_multi for multi. Several equipments with different class drivers can be in one frontend.

The device driver list

Each element of the DEVICE_DRIVER list is one device, and has these fields:

Field Meaning
name Name of the device, used for its directory Settings/Devices/<name> in the ODB
device driver The device driver function, e.g. epics_ca or nulldev
channels Number of channels of this device. The channels of all the devices are joined into one list in the ODB
bus driver The bus driver function (e.g. rs232 or tcpip), or NULL if the device driver doesn't use one
flags A combination of the DF_ flags below. Optional

The most common flags are:

Flag Meaning
DF_INPUT, DF_OUTPUT The channels are inputs or outputs. Needed by the multi class driver
DF_MULTITHREAD Talk to the device in its own thread (see below)
DF_PRIO_DEVICE Read the demand values from the device when the frontend starts, instead of setting them from the ODB
DF_READ_ONLY Never write demand values to the device
DF_POLL_DEMAND Keep reading the demand values from the device, e.g. if they can also be changed on the device itself
DF_QUICKSTART Don't read all the channels when the frontend starts, so that it starts faster
DF_HW_RAMP The high voltage device ramps the voltage itself, so the hv class driver doesn't
DF_LABELS_FROM_DEVICE Take the channel names from the device
DF_REPORT_TEMP, DF_REPORT_STATUS, DF_REPORT_CHSTATE, DF_REPORT_CRATEMAP High voltage devices: also read the temperature, status, channel state or crate map

Multi-threaded support

The slow-control system supports multi-threaded applications. Each device gets its own thread in the front-end, which has several advantages:

  • the communication of all devices runs in parallel and therefore is much faster
  • slow devices can no longer block the frontend. Response times to run transitions etc. therefore become much faster.

Note this is different to the EQ_MULTITHREAD option!

To enable the multi-threaded slow-control, you must set the DF_MULTITHREAD bit in the 5th argument of the DEVICE_DRIVER listing.

Example device driver listing with multi-threading support

DEVICE_DRIVER multi_driver[] = {
    {"Input", nulldev, 2, null, DF_INPUT | DF_MULTITHREAD},
    {"Output", nulldev, 2, null, DF_OUTPUT | DF_MULTITHREAD},
    {""}
};
Example ODB config of a slow control frontend

The following example shows the ODB keys of a slow control equipment called "Epics":

Key name                        Type    #Val  Size  Last Opn Mode Value
---------------------------------------------------------------------------
Epics                          DIR
   Settings                    DIR
       Channels                DIR
           Epics               INT     1     4     25h  0   RWD  3
       Devices                 DIR
           Epics               DIR
               Channel name    STRING  10    32    25h  0   RWD  
                                       [0]             GPS:VAR1
                                       [1]             GPS:VAR2
                                       [2]             GPS:VAR3
       Names                   STRING  10    32    17h  1   RWD  
                                       [0]             Current
                                       [1]             Voltage
                                       [2]             Watchdog
       Update Threshold MeasureFLOAT   10    4     17h  0   RWD  
                                       [0]             2
                                       [1]             2
                                       [2]             2
   Common                      DIR
       Event ID                WORD    1     2     17h  0   RWD  3
       Trigger mask            WORD    1     2     17h  0   RWD  0
       Buffer                  STRING  1     32    17h  0   RWD  SYSTEM
       Type                    INT     1     4     17h  0   RWD  4
       Source                  INT     1     4     17h  0   RWD  0
       Format                  STRING  1     8     17h  0   RWD  FIXED
       Enabled                 BOOL    1     4     17h  0   RWD  y
       Read on                 INT     1     4     17h  0   RWD  121
       Period                  INT     1     4     17h  0   RWD  60000
       Event limit             DOUBLE  1     8     17h  0   RWD  0
       Num subevents           DWORD   1     4     17h  0   RWD  0
       Log history             INT     1     4     17h  0   RWD  1
       Frontend host           STRING  1     32    17h  0   RWD  hostname
       Frontend name           STRING  1     32    17h  0   RWD  Epics
       Frontend file name      STRING  1     256   17h  0   RWD  feepic.c
   Variables                   DIR
       Demand                  FLOAT   10    4     0s   1   RWD  
                                       [0]             1.56
                                       [1]             120
                                       [2]             87
       Measured                FLOAT   10    4     2s   0   RWD  
                                       [0]             1.56
                                       [1]             120
                                       [2]             87
   Statistics                  DIR
       Events sent             DOUBLE  1     8     17h  0   RWDX 26
       Events per sec.         DOUBLE  1     8     17h  0   RWDX 0
       kBytes per sec.         DOUBLE  1     8     17h  0   RWDX 0

Setting up devices in code (mdevice)

Instead of a fixed DEVICE_DRIVER list, a class driver frontend can add its devices in frontend_init() with the mdevice classes from $MIDASSYS/drivers/device/mdevice.h. They add the device to the equipment's device driver list, and write the names, update thresholds, factors and offsets of its channels to the ODB. mdevice_mscb does this for MSCB devices. This example is from $MIDASSYS/examples/slowcont/mscb_fe.cxx, which uses the multi class driver:

mdevice_mscb envIn("Environment", "Input", DF_INPUT, "mscbxxx.psi.ch");
envIn.define_var(1, 0, "Temperature 1", 0.1);   // node 1, variable 0, name, update threshold
envIn.define_var(1, 1, "Temperature 2", 0.1);
envIn.define_history_panel("Temperatures", 0, 1);

Not the mdev framework

Despite the similar name, mdevice is part of the class driver framework. It is not related to the mdev framework.

Writing a device driver

If midas has no driver for your hardware, start from $MIDASSYS/drivers/device/nulldev.cxx, which does nothing but has the full structure. A device driver is a single function, which the class driver calls with a command and the command's arguments:

INT mydevice(INT cmd, ...);

Every device driver must handle these commands:

Command Arguments What to do
CMD_INIT ODB key of Settings/Devices/<name>, pointer to the driver's private data, number of channels, flags, bus driver Read the settings, allocate the private data and connect to the device
CMD_EXIT Private data Disconnect and free the private data
CMD_SET Private data, channel, value Set the demand value of a channel
CMD_GET Private data, channel, pointer to the value Read the measured value of a channel

The hv class driver also uses commands such as CMD_GET_CURRENT, CMD_SET_VOLTAGE_LIMIT, CMD_SET_RAMPUP and CMD_GET_STATUS, and CMD_GET_THRESHOLD sets the default update threshold of a channel. A device driver can ignore the commands it doesn't support. The commands are listed in midas.h.

A device driver that uses a bus driver can send and receive with the BD_PUTS(), BD_GETS() and BD_WRITES() macros from midas.h. See e.g. $MIDASSYS/drivers/device/lrs1454.cxx.

Building a class driver frontend

Link the frontend with midas::mfed and midas::drivers, which contains the class drivers and the most common device drivers. To use another driver, add its source file to the frontend:

add_executable(my_sc_fe my_sc_fe.cxx $ENV{MIDASSYS}/drivers/device/<driver>.cxx)
target_link_libraries(my_sc_fe midas::mfed midas::drivers)

Some of the older drivers haven't been updated to compile as C++ yet. See Drivers included in midas.

mdev framework

API reference: mdev slow control framework

In the mdev framework, each device is an object of a class derived from mdev (in $MIDASSYS/drivers/mdev/mdev.h). An mdev frontend doesn't use the class drivers, device drivers or EQ_SLOW equipment of the class driver framework.

mdev drivers included in midas

The mdev drivers are in $MIDASSYS/drivers/mdev:

Example mdev frontends are $MIDASSYS/drivers/mdev/mdev_fe.cxx (MSCB) and $MIDASSYS/examples/revolutionpi/revpi_fe.cxx (Revolution Pi).

Writing an mdev frontend

Each mdev driver class has these functions:

Function Called What it does
odb_setup() When the frontend starts Creates the device's keys under /Equipment/<name>
init() After odb_setup() Connects to the device
loop() Continuously Reads the device when it is due, writes the values to the ODB, and sets new values
read_event() When the equipment is read Writes the values to an event
exit() Only if you call it (e.g. from frontend_exit()) Disconnects from the device

The constructor takes the name of the equipment, which must already exist in the ODB. Devices of a disabled equipment (Common/Enabled is n) are skipped. set_readout_period() sets how often loop() reads the device (default 1 s), and define_history_panel() creates a history panel for the device's variables.

The frontend is a normal mfe frontend, with an EQ_PERIODIC equipment for each device. It keeps a table of the devices, and calls the static functions mdev::mdev_odb_setup(), mdev::mdev_init() and mdev::mdev_loop() for all of them. This example, shortened from $MIDASSYS/drivers/mdev/mdev_fe.cxx, reads one MSCB node:

EQUIPMENT equipment[] = {
   {"MDEV",                     /* equipment name */
    {1, 0,                      /* event ID, trigger mask */
     "SYSTEM",                  /* event buffer */
     EQ_PERIODIC,               /* equipment type */
     0,                         /* event source */
     "MIDAS",                   /* format */
     TRUE,                      /* enabled */
     RO_ALWAYS,
     60000,                     /* read event every 60 sec */
     0,                         /* readout pause */
     0,                         /* number of sub events */
     10,                        /* log history every 10 seconds */
     "", "", ""} ,
     mdev_read                  /* readout routine */
    },
   {""}
};

std::vector<mdev *> mdev_table;

int frontend_init()
{
   auto mscb = new mdev_mscb("MDEV");
   auto n = new mscb_node("mscb394", 43);
   n->add_output("HV", "Voltage");
   n->add_input("HVMeas", "Voltage Measured");
   mscb->add_node(*n);
   mdev_table.push_back(mscb);

   install_frontend_loop(mdev_loop);

   try {
      mdev::mdev_odb_setup(mdev_table);
      mdev::mdev_init(mdev_table);
   } catch (mexception &e) {
      cm_msg(MERROR, "frontend_init", "%s", e.what());
      return FE_ERR_HW;
   }
   return FE_SUCCESS;
}

int mdev_loop()
{
   mdev::mdev_loop(mdev_table);
   ss_sleep(10); // don't use all the CPU
   return FE_SUCCESS;
}

int mdev_read(char *pevent, int off)
{
   // send the event of the device that belongs to this equipment
   for (mdev *m : mdev_table)
      if (EVENT_ID(pevent) == m->get_event_id())
         return m->read_event(pevent, off);
   return 0;
}

Building an mdev frontend

To build the example, put this CMakeLists.txt next to mdev_fe.cxx. It links midas::mfed, midas::drivers (which contains mdev_mscb) and midas::mscb:

cmake_minimum_required(VERSION 3.14)
project(mdev_fe LANGUAGES CXX)

# Find the midas installation given by $MIDASSYS
find_package(Midas REQUIRED PATHS $ENV{MIDASSYS})

add_executable(mdev_fe mdev_fe.cxx)
target_link_libraries(mdev_fe midas::mfed midas::drivers midas::mscb)

Then build it with:

mkdir build
cd build
cmake ..
make

For mdev_revpi or mdev_tdk, add the driver's source file to add_executable(), e.g. $ENV{MIDASSYS}/drivers/mdev/mdev_tdk.cxx.