9. Code Examples

This file contains a couple of examples of code to show many of the features documented here.

9.1. Basic IOC

The IOC in examples/basic_ioc illustrates the minimum code required to build an IOC using this framework. The source tree is:

File/Directory

Description

Db/

Db/Makefile

Needs rules for building .db from .py

Db/basic_ioc.py

Minimal example building a simple database

src/

src/Makefile

src/main.c

Minimal IOC implementation

st.cmd

Startup script for IOC initialisation

Makefile

configure/

Standard EPICS configure directory, as generated by makeBaseApp.pl.

configure/CONFIG

configure/CONFIG_APP

configure/CONFIG_SITE

Must specify PYTHON

configure/Makefile

configure/RELEASE

Must specify EPICS_DEVICE

configure/RULES

configure/RULES.ioc

configure/RULES_DIRS

configure/RULES_TOP

The EPICS_DEVICE support module must be defined as usual, and the build also needs the symbol PYTHON to identify the Python interpreter to use.

9.1.1. Defining the Database

The database defined here is utterly minimal, consisting of a single PV with a 1 second scan which returns the current timestamp in seconds. The EPICS Device definition to build the database record entry is the following Python line:

longIn('TSEC', DESC = 'Timestamp in seconds', SCAN = '1 second')

This line defines an longin record named TEST with the specified scan interval and description.

A little bit of boilerplate is needed to set things up:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import sys, os
sys.path.append(os.environ['EPICS_DEVICE'])

from epics_device import *
SetTemplateRecordNames()

longIn('TSEC', DESC = 'Timestamp in seconds', SCAN = '1 second')

Waveform('STRINGS', 4, 'STRING', PINI = "YES")

# Write out the generated .db file
WriteRecords(sys.argv[1])

This code picks up the configured EPICS_DEVICE version (as configured in configure/RELEASE) and configures the database to be generated with a $(DEVICE): prefix on each record name. The last line writes out the generated database.

Some support is also needed from the make file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
TOP = ..
include $(TOP)/configure/CONFIG

DB += basic_ioc.db

include $(TOP)/configure/RULES

export EPICS_DEVICE

# We use the python library to construct the .db file
$(COMMON_DIR)/%.db: ../%.py $(wildcard ../*.py)
	$(PYTHON) $< $@

# EPICS 3.15 and later has changed the build system.  The following rules are
# needed to make things work.

%.db.d:
	touch $@

The important point here is that the .db file is generated from the corresponding .py file and the EPICS_DEVICE symbol is exported to the script above.

The result of this is the following file in db/basic_ioc.db:

1
2
3
4
5
6
7
record(longin, "$(DEVICE):TSEC")
{
    field(DESC, "Timestamp in seconds")
    field(DTYP, "epics_device")
    field(INP,  "@TSEC")
    field(SCAN, "1 second")
}

9.1.2. Implementing the IOC

As this IOC does almost nothing its C implementation is pretty small:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdbool.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>

#include <iocsh.h>

#include "error.h"
#include "epics_device.h"

static EPICS_STRING strings[4] = {
    { "Hello" }, { "There" }, { "A longer string" }, { "THE END" }
};

static int read_timestamp(void)
{
    return (int) time(NULL);
}

static error__t publish_pvs(void)
{
    PUBLISH_READER(longin, "TSEC", read_timestamp);
    PUBLISH_WF_READ_VAR(EPICS_STRING, "STRINGS", 4, strings);
    return ERROR_OK;
}

int main(int argc, const char *argv[])
{
    bool ok =
        initialise_epics_device()  ?:
        publish_pvs()  ?:
        TEST_IO(iocsh("st.cmd") == 0)  ?:
        TEST_IO(iocsh(NULL));
    return ok ? 0 : 1;
}

First comes a minimal set of headers. Both stdbool.h and unistd.h are required as a consequence of using error.h, and our implementation will use time.h. We need iocsh.h in order to call iocsh().

Then the EPICS Device headers error.h and epics_device.h are needed for any use of EPICS Device.

The function read_timestamp actually implements the IOC functionality. In this case when the corresponding record is processed we compute a value which is used to update the PV.

The PUBLISH_READER() call binds our PV implementation to its definition in the database, and we’ve chosen the appropriate implementation.

Finally IOC initialisation consists of a stereotyped sequence. initialise_epics_device() must be called early, then records can be published, then the IOC is started. In this particular example we’ve put the rest of the initialisation into an external startup script:

1
2
3
4
dbLoadDatabase("dbd/basic_ioc.dbd", NULL, NULL)
basic_ioc_registerRecordDeviceDriver(pdbbase)
dbLoadRecords("db/basic_ioc.db", "DEVICE=TS-TS-TEST-99")
iocInit()

Note that it is possible perform complete IOC initialisation without a startup script, and with a more complete IOC it can be more convenient to do this.

9.1.3. Internalising st.cmd

It can be more convenient to internalise the startup script to the IOC source code, particularly if a number of template parameters need to be generated. This can be done by replacing the line TEST_IO(iocsh("st.cmd") == 0) in the definition of main above with a call to init_ioc() as defined here:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
extern int basic_ioc_registerRecordDeviceDriver(struct dbBase *pdb);

static error__t init_ioc(void)
{
    return
        TEST_IO(dbLoadDatabase("dbd/basic_ioc.dbd", NULL, NULL))  ?:
        TEST_IO(basic_ioc_registerRecordDeviceDriver(pdbbase))  ?:
        DO(database_add_macro("DEVICE", "TS-TS-TEST-99"))  ?:
        database_load_file("db/basic_ioc.db")  ?:
        TEST_OK(iocInit() == 0);
}

This is a bit more code, but does have the advantage of rather more thorough error checking, and much more flexibility in macro generation.

9.2. A More Complex Example

The source tree in examples/example_ioc illustrates a slightly fuller functioned IOC.

9.2.1. Database Definition

The database code is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import sys, os

sys.path.append(os.environ['EPICS_DEVICE'])

from epics_device import *

set_MDEL_default(-1)
set_out_name(lambda name: name + '_S')
SetTemplateRecordNames()

WF_LENGTH = 128

# In this simple example setting FREQ_S causes WF and SUM to update.
aOut('FREQ', PREC = 4,
    FLNK = create_fanout('WFFAN',
        Waveform('WF', WF_LENGTH, 'DOUBLE', DESC = 'Sine wave'),
        aIn('SUM', PREC = 3, DESC = 'Sum of sine wave')),
    DESC = 'Waveform frequency')

Trigger('TRIG',
    Waveform('TRIGWF', WF_LENGTH, DESC = 'Triggered waveform'),
    longIn('COUNT', DESC = 'Trigger count'))
Action('RESET', DESC = 'Reset trigger count')
aOut('INTERVAL', 1e-2, 100, 's', 2, DESC = 'Trigger interval')
aOut('SCALING', PREC = 3, DESC = 'Frequency scaling')

Action('WRITE', DESC = 'Force update to persistent state')

for prefix in ['A', 'B']:
    push_name_prefix(prefix)
    read = longIn('READ')
    write = longOut('WRITE', FLNK = read)
    pop_name_prefix()

longOut('ADD_ONE', DESC = 'Adds one to the written value')

WaveformOut('STRINGS', 4, 'STRING')

WriteRecords(sys.argv[1])

Here we have the following structures:

  1. Record naming and defaults are set up so that all record names are prefixed with the macro $(DEVICE): (the default template behaviour) and all out records names are suffixed with _S (a very convenient naming convention).

  2. An ao record FREQ_S used to set the frequency for a waveform record WF; each time FREQ_S is written the waveform is updated and so is a SUM record. In this case processing of WF and SUM is entirely driven by writes to FREQ_S.

  3. A pair of records TRIFWF and COUNT which update on an internally generated IOC event. These are controlled by a couple of settings INTERVAL_S and SCALING_S, and COUNT can be reset by processing RESET_S.

  4. Another action WRITE_S which forces the persistent state to be saved.

Of course here the database builder is more useful: 30 lines of source code generates 129 lines of database.

9.2.2. Publishing Records

For each of the records defined above an implementation needs to be defined and published. The following C code publishes the variables above:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
error__t initialise_example_pvs(void)
{
    PUBLISH_WRITER_P(ao, "FREQ", set_frequency);
    PUBLISH_WF_READ_VAR(double, "WF", WF_LENGTH, waveform);
    PUBLISH_READ_VAR(ai, "SUM", sum);

    PUBLISH_ACTION("WRITE", write_persistent_state);

    PUBLISH_WRITE_VAR_P(ao, "INTERVAL", event_interval);
    PUBLISH_WRITE_VAR_P(ao, "SCALING", scaling);

    interlock = create_interlock("TRIG", false);
    PUBLISH_READ_VAR(longin, "COUNT", trigger_count);
    PUBLISH_WF_READ_VAR(int, "TRIGWF", WF_LENGTH, trigger_waveform);
    PUBLISH_ACTION("RESET", reset_trigger_count);

    ...
}

Note that all of the out variables are published with a _P suffix, indicating that persitence support is enabled. Here we see a variety of different implementations being used: calling a function, reading and writing a variable, and finally using an interlock.

9.2.3. Notes on Trigger and Interlock

The most complex structure involves create_interlock(). Before updating the variables trigger_count and trigger_waveform associated with the PVs which will be updated when the record TRIG is processed, interlock_wait() must first be called – this blocks processing until the library knows that the IOC is no longer reading the variables. Once the new state has been written then interlock_signal() can be called to signal the update to EPICS and trigger processing of the associated records.

1
2
3
4
5
6
7
static void process_event(void)
{
    interlock_wait(interlock);
    trigger_count += 1;
    update_waveform();
    interlock_signal(interlock, NULL);
}

Finally, in this example the function process_event is called internally by the driver implementation; in this example a thread is used to call it at an interval governed by the variable event_interval.