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.
DemandandMeasuredarrays 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
multiclass 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_mscbit 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 driversdevice: 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.nulldevis a device driver that does nothing, for testing and as a templatebus: bus drivers for RS-232 (rs232), TCP/IP (tcpip) and MSCB (mscbbus), andnull
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 */
},
{""}
};
epics_cais defined in drivers/device/epics_ca.cxxcd_gen_readis defined in drivers/class/generic.cxxcd_genis defined in drivers/class/generic.cxxepics_driveris 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:
mdev_mscbfor MSCB devices. It is compiled into themidas::driverslibrarymdev_revpifor the Revolution Pi, andmdev_tdkfor TDK power supplies. Add their source files to your frontend's build (see Building an mdev frontend)
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.