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/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/CONFIG |
|
configure/CONFIG_APP |
|
configure/CONFIG_SITE |
Must specify |
configure/Makefile |
|
configure/RELEASE |
Must specify |
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:
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).An
ao
recordFREQ_S
used to set the frequency for a waveform recordWF
; each timeFREQ_S
is written the waveform is updated and so is aSUM
record. In this case processing ofWF
andSUM
is entirely driven by writes toFREQ_S
.A pair of records
TRIFWF
andCOUNT
which update on an internally generated IOC event. These are controlled by a couple of settingsINTERVAL_S
andSCALING_S
, andCOUNT
can be reset by processingRESET_S
.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
.