PVDatabase Service

editions

2021.04.20
Original
2021.05.16
latest
Editors:
Marty Kraimer

This product is available via an open source license

Table of Contents

Overview

The following describes how services can be implemented via pvaClient and pvDatabase. The basic idea is that a number of support modules, each implemented via a PVRecord, can be created. The process method implements the support details.

A service can be created that uses the support modules. The service can implement a PVDatabase or can be part of an IOC.

In order to understand the following exampleCPP should be cloned. It has all the examples described below.

The next section shows how to build exampleCPP from an EPICS7 release.

After you have build exampleCPP then start one of the following:

pwd
SOMEWHERE/exampleCPP/database/iocBoot/exampleDatabase
../../bin/linux-x86_64/exampleDatabase st.cmd 
or
pwd
SOMEWHERE/exampleCPP/serviceMain
bin/linux-x86_64/serviceMain 
NOTE: do not start both because they have many of the same channels.

Section 3 describes serviceMain, which implements a service as a main program.

Following serviceMain is a description of database, which implements a service as part of an IOC database.

The remaining sections describes the support modules used by both serviceMain and database.

Building exampleCPP from an EPICS7 release

The following examples are using linux. I have built release 7.0.5 at

pwd
/home/install/epics/base-7.0.5
cd modules

Releases up to and including 7.0.5

exampleCPP requires changes to pvaClient and pvDatabase that have been made since 7.0.5. But each can be replaced by the latest via the following:

/bin/rm -fr pvaClient pvDatabase
git clone https://github.com/epics-base/pvaClientCPP pvaClient
git clone https://github.com/epics-base/pvDatabaseCPP pvDatabase
cd pvaClient
make
cd ..
cd pvDatabase
make
cd ..

Build exampleCPP

git clone https://github.com/epics-base/exampleCPP
cd exampleCPP
cp MasterRELEASE.local RELEASE.local

You have to edit RELEASE.local. The only line You need to change is the one starting with EPICS7_DIR= This must show where you are building the EPICS7 release.
Then enter:

cp RELEASE.local configure
make

serviceMain

top

The top of exampleCPP/serviceMain has the following:
Makefile
Standard top level Makefile.
RELEASE.support
SCALARLIMITRECORD=$(TOP)/../scalarLimit
POWERSUPPLYRECORD=$(TOP)/../powerSupplyRecord
LINKRECORD=$(TOP)/../linkRecord
HELLOPUTGETRECORD=$(TOP)/../helloPutGetRecord
HELLORPCRECORD=$(TOP)/../helloRPCRecord
CONTROLRECORD=$(TOP)/../controlRecord
This refers to each of the support modules being used.
configure
Standard top level configure directory. But note:
grep RELEASE.support configure/RELEASE
include $(TOP)/RELEASE.support
src
Described next.

src

Makefile

TOP=..
include $(TOP)/configure/CONFIG
# NOTE: The order of the following is important
EPICS_BASE_PVA_CORE_LIBS = pvaClient  pvDatabase pvAccess pvAccessCA pvData ca Com

PROD_HOST += serviceMain
# NOTE: The order of the following is important
serviceMain_SRCS += serviceMain.cpp
serviceMain_LIBS += scalarLimitRecord
serviceMain_LIBS += powerSupplyRecord
serviceMain_LIBS += linkRecord
serviceMain_LIBS += helloPutGetRecord
serviceMain_LIBS += helloRPCRecord
serviceMain_LIBS += controlRecord
serviceMain_LIBS +=  $(EPICS_BASE_PVA_CORE_LIBS)

include $(TOP)/configure/RULES

seviceMain.cpp

The following are just a few important details. Look at the source for more.

Note the following includes for the support modules:

#include <pv/pvdbcrScalarRecord.h>
#include <pv/pvdbcrScalarArrayRecord.h>
#include <pv/pvdbcrAddRecord.h>
#include <pv/pvdbcrRemoveRecord.h>
#include <pv/pvdbcrProcessRecord.h>
#include <pv/pvdbcrTraceRecord.h>
#include <scalarLimit/scalarLimitRecord.h>
#include <powerSupply/powerSupplyRecord.h>
#include <linkRecord/getLinkScalarRecord.h>
#include <linkRecord/getLinkScalarArrayRecord.h>
#include <linkRecord/putLinkScalarRecord.h>
#include <linkRecord/putLinkScalarArrayRecord.h>
#include <helloPutGet/helloPutGetRecord.h>
#include <helloRPC/helloRPCRecord.h>
#include <pvcontrol/controlRecord.h>

These are for all the supports used by serviceMain. The order is important.

The main program creates all it's PVRecord via the support. Excerpts from the main program are:

int main(int argc,char *argv[])
{
    PVDatabasePtr master = PVDatabase::getMaster();
    ChannelProviderLocalPtr channelProvider = getChannelProviderLocal();
    
    std::vector<std::string> recordNames;
    std::vector<std::string> valueType;   
    recordNames.push_back("PVRboolean"); valueType.push_back("boolean");
    recordNames.push_back("PVRbyte"); valueType.push_back("byte"); 
...
    recordNames.push_back("PVRdouble"); valueType.push_back("double"); 
    recordNames.push_back("PVRstring"); valueType.push_back("string"); 
    for(size_t i=0;i<recordNames.size(); ++i)
    {
        if(!master->addRecord(PvdbcrScalarRecord::create(recordNames[i],valueType[i]))) { 
            cerr << "record " << recordNames[i] << " not added to master\n";
        }
    }
    
    recordNames.clear();
    valueType.clear();
    recordNames.push_back("PVRbooleanArray"); valueType.push_back("boolean");
    recordNames.push_back("PVRbyteArray"); valueType.push_back("byte");
...
    recordNames.push_back("PVRdoubleArray"); valueType.push_back("double"); 
    recordNames.push_back("PVRstringArray"); valueType.push_back("string"); 
    for(size_t i=0;i<recordNames.size(); ++i)
    {
        if(!master->addRecord(PvdbcrScalarArrayRecord::create(recordNames[i],valueType[i]))) { 
            cerr << "record " << recordNames[i] << " not added to master\n";
        }
    }
    
    std::string recordName;
    recordName = "PVRaddRecord";
    if(!master->addRecord(PvdbcrAddRecord::create(recordName))) { 
        cerr << "record " << recordName << " not added to master\n";
    }
    recordName = "PVRremoveRecord";
    if(!master->addRecord(PvdbcrRemoveRecord::create(recordName))) { 
        cerr << "record " << recordName << " not added to master\n";
    }
    recordName = "PVRprocessRecord";
    if(!master->addRecord(PvdbcrProcessRecord::create(recordName))) { 
        cerr << "record " << recordName << " not added to master\n";
    }
    recordName = "PVRtraceRecord";
    if(!master->addRecord(PvdbcrTraceRecord::create(recordName))) { 
        cerr << "record " << recordName << " not added to master\n";
    }

    recordName = "PVRcontrolDouble";
    epics::example::control::ControlRecordPtr controlRecordDouble
       = epics::example::control::ControlRecord::create(recordName,"double");
    if(!master->addRecord(controlRecordDouble)) {
        cerr << "record " << recordName << " not added to master\n";
    }
    recordName = "PVRcontrolUbyte";
    epics::example::control::ControlRecordPtr controlRecordUbyte
       = epics::example::control::ControlRecord::create(recordName,"ubyte");
    master->addRecord(controlRecordUbyte);

    recordName = "PVRpowerSupply";
    epics::example::powerSupply::PowerSupplyRecordPtr powerSupply
       = epics::example::powerSupply::PowerSupplyRecord::create(recordName);
    if(!master->addRecord(powerSupply)) {
        cerr << "record " << recordName << " not added to master\n";
    }

    recordName = "PVRgetLinkScalar";  
    epics::example::linkRecord::GetLinkScalarRecordPtr getLinkScalar
       = epics::example::linkRecord::GetLinkScalarRecord::create(recordName);
    if(!master->addRecord(getLinkScalar)) {
        cerr << "record " << recordName << " not added to master\n";
    }

    recordName = "PVRgetLinkScalarArray";
    epics::example::linkRecord::GetLinkScalarArrayRecordPtr getLinkScalarArray
       = epics::example::linkRecord::GetLinkScalarArrayRecord::create(recordName);
    if(!master->addRecord(getLinkScalarArray)) {
        cerr << "record " << recordName << " not added to master\n";
    }

    recordName = "PVRputLinkScalar";
    epics::example::linkRecord::PutLinkScalarRecordPtr putLinkScalar
       = epics::example::linkRecord::PutLinkScalarRecord::create(recordName);
    if(!master->addRecord(putLinkScalar)) {
        cerr << "record " << recordName << " not added to master\n";
    }

    recordName = "PVRputLinkScalarArray";
    epics::example::linkRecord::PutLinkScalarArrayRecordPtr putLinkScalarArray
       = epics::example::linkRecord::PutLinkScalarArrayRecord::create(recordName);
    if(!master->addRecord(putLinkScalarArray)) {
        cerr << "record " << recordName << " not added to master\n";
    }

    recordName = "PVRhelloPutGet";
    epics::example::helloPutGet::HelloPutGetRecordPtr helloPutGet
       = epics::example::helloPutGet::HelloPutGetRecord::create(recordName);
    if(!master->addRecord(helloPutGet)) { 
        cerr << recordName << " not added to master\n";
    }

    recordName = "PVRhelloRPC";
    epics::example::helloRPC::HelloRPCRecordPtr helloRPC
       = epics::example::helloRPC::HelloRPCRecord::create(recordName);
    if(!master->addRecord(helloRPC)) { 
        cerr << recordName << " not added to master\n";
    }

    recordName = "PVRscalarLimitUbyte";  
    epics::scalarLimit::ScalarLimitRecordCreate::create(recordName,"ubyte");

    recordName = "PVRscalarLimitDouble";  
    epics::scalarLimit::ScalarLimitRecordCreate::create(recordName,"double");

    ServerContext::shared_pointer ctx =
        startPVAServer("local",0,true,true);

    string str;
    while(true) {
        cout << "enter: pvdbl or exit \n";
        getline(cin,str);
        if(str.compare("exit")==0) break;
        if(str.compare("pvdbl")==0) {
            PVStringArrayPtr pvNames = master->getRecordNames();
            PVStringArray::const_svector xxx = pvNames->view();
            for(size_t i=0; i<xxx.size(); ++i) cout<< xxx[i] << endl;
        }
    }
    return 0;
}

After starting enter pvdbl to see all that records.

database

top

The top of exampleCPP/database has the following:
Makefile
Standard top level Makefile. It references src, ioc, and iocBoot
RELEASE.support
This refers to each of the support modules being used. It is the same as for serviceMain,
configure
Just like serviceMain.
src
Described next.
ioc
Described below.
iocBoot
Described below.

database/src

This directory has the following files:

Makefile
TOP=..
include $(TOP)/configure/CONFIG
# NOTE: The order of the following is important
EPICS_BASE_PVA_CORE_LIBS = pvaClient  pvDatabase pvAccess pvAccessCA nt pvData ca Com
INC += pv/exampleDatabase.h
DBD += exampleDatabaseRegister.dbd
LIBRARY = exampleDatabase
LIBSRCS += exampleDatabase.cpp
LIBSRCS += exampleDatabaseRegister.cpp
exampleDatabase_LIBS += $(EPICS_BASE_PVA_CORE_LIBS)
# needed for Windows
LIB_SYS_LIBS_WIN32 += netapi32 ws2_32
include $(TOP)/configure/RULES
exampleDatabase.cpp
Code that creates many PVRecords. Most are soft records but it also creates records that implement other semantics. The code is described in the following section.
exampleDatabaseRegister.dbd and exampleDatabaseRegister.cpp
Code that allows the PVRecords to be part of a V3 IOC.
This is the code that supports the exampleDatabase shell command in database/iocBoot/exampleDatabase/st.cmd

database/src/exampleDatabase.cpp

Private Methods

This has a number of private methods:

createStructureArrayRecord
Creates a record with a value field that is a structureArray.
For example if the name is PVRstructureArray It creates the record:
PVRstructureArray
structure 
    structure[] value
createRestrictedUnionRecord
Creates a record with a value field that is a restricted union.
For example if the name is PVRrestrictedUnion It creates the record:
PVRrestrictedUnion
epics:nt/NTUnion:1.0 
    union value
        (none)
    time_t timeStamp
        long secondsPastEpoch 0
        int nanoseconds 0
        int userTag 0
createVariantUnionRecord
Creates a record with a value field that a variant union.
For example if the name is PVRvariantUnion It creates the record:
PVRvariantUnion
epics:nt/NTUnion:1.0 
    any value
        (none)
    time_t timeStamp
        long secondsPastEpoch 0
        int nanoseconds 0
        int userTag 0
createRestrictedUnionArrayRecord
Creates a record with a value field that a structureArray.
For example if the name is PVRrestrictedUnionArray It creates the record:
PVRrestrictedUnionArray
structure 
    time_t timeStamp
        long secondsPastEpoch 0
        int nanoseconds 0
        int userTag 0
    union[] value
createVariantUnionArrayRecord
Creates a record with a value field that a structureArray.
For example if the name is PVRvariantUnionArray It creates the record:
PVRvariantUnionArray
structure 
    time_t timeStamp
        long secondsPastEpoch 0
        int nanoseconds 0
        int userTag 0
    any[] value
createBigRecord
If the name is PVRBigRecord It creates the record:
PVRBigRecord
structure 
    time_t timeStamp <undefined> 0
    structure scalar
        structure boolean
            boolean value false
        structure byte
            byte value 0
        structure long
            long value 0
        structure double
            double value 0
        structure string
            string value 
    structure scalarArray
        structure boolean
            boolean[] value []
        structure byte
            byte[] value []
        structure long
            long[] value []
        structure double
            double[] value []
        structure string
            string[] value []
    structure[] structureArray
    union restrictedUnion
        (none)
    any variantUnion
        (none)

ExampleDatabase::create

This is a static method of ExampleDatabase that creates all the PVRecords that make up the example database.

It does the following:

createStructureArrayRecord
Creates PVRstructureArray
createRestrictedUnionRecord
Creates PVRrestrictedUnion
createVariantUnionRecord
Creates PVRvariantUnion
createRestrictedUnionArrayRecord
Creates PVRrestrictedUnionArray
createVariantUnionArrayRecord
Creates PVRvariantUnionArray
createBigRecord
Creates PVRBigRecord

database/ioc

database/ioc/Db

This has V3 record templates for creating DBRecords. They are used when the example database is started as part of a V3 IOC.

database/ioc/src

This has:

Makefile
TOP=../..
include $(TOP)/configure/CONFIG

LIBRARY_IOC += example
# create DBRecord types
DBD += exampleDatabase.dbd
DBDINC += simpleBusyRecord
DBDINC += charoutRecord
DBDINC += shortoutRecord
DBDINC += ucharoutRecord
DBDINC += ushortoutRecord
DBDINC += ulongoutRecord
DBDINC += floatoutRecord

example_SRCS += simpleBusyRecord.c
example_SRCS += charoutRecord.c
example_SRCS += shortoutRecord.c
example_SRCS += ucharoutRecord.c
example_SRCS += ushortoutRecord.c
example_SRCS += ulongoutRecord.c
example_SRCS += floatoutRecord.c
example_LIBS += $(EPICS_BASE_IOC_LIBS)

USR_CPPFLAGS += -DUSE_TYPED_RSET

#=============================
# build an ioc application
PROD_IOC += exampleDatabase
# <name>_registerRecordDeviceDriver.cpp will be created from <name>.dbd
exampleDatabase_SRCS += exampleDatabase_registerRecordDeviceDriver.cpp
exampleDatabase_SRCS_DEFAULT += exampleDatabaseMain.cpp

exampleDatabase_LIBS += exampleDatabase
exampleDatabase_LIBS += example
exampleDatabase_LIBS += scalarLimitRecord
exampleDatabase_LIBS += powerSupplyRecord
exampleDatabase_LIBS += linkRecord
exampleDatabase_LIBS += helloPutGetRecord
exampleDatabase_LIBS += helloRPCRecord
exampleDatabase_LIBS += controlRecord

exampleDatabase_LIBS += pvaClient pvDatabase qsrv pvAccessIOC pvAccess pvAccessCA nt pvData
exampleDatabase_LIBS += $(EPICS_BASE_IOC_LIBS)

# needed for Windows
LIB_SYS_LIBS_WIN32 += netapi32 ws2_32

include $(TOP)/configure/RULES
exampleDatabaseInclude.dbd
include "base.dbd"
include "PVAServerRegister.dbd"
include "registerChannelProviderLocal.dbd"
include "qsrv.dbd"
include "pvdbcrAllRecords.dbd"
include "powerSupplyRecord.dbd"
include "controlRecord.dbd"
include "getLinkScalarRecord.dbd"
include "getLinkScalarArrayRecord.dbd"
include "putLinkScalarRecord.dbd"
include "putLinkScalarArrayRecord.dbd"
include "helloPutGetRecord.dbd"
include "helloRPCRecord.dbd"
include "scalarLimitRecord.dbd"
include "exampleDatabaseRegister.dbd"
include "simpleBusyRecord.dbd"
include "charoutRecord.dbd"
include "shortoutRecord.dbd"
include "ucharoutRecord.dbd"
include "ushortoutRecord.dbd"
include "ulongoutRecord.dbd"
include "floatoutRecord.dbd"
exampleDatabaseMain.cpp

database/iocBoot/exampleDatabase

This is where the example database is started as part of a V3 IOC.

Starting example database

pwd
/home/install/epics/base-7.0.5/modules/exampleCPP/database/iocBoot/exampleDatabase
../../bin/linux-x86_64/exampleDatabase st.cmd 

After starting enter pvdbl to see all that records. You will see all the records created by serviceMain and all the additional records created by database.

Testing various record types

Sample configured records are available for various record types. Since epics-base does not provide scalar records of type DBF_CHAR, DBF_UCHAR, DBF_SHORT, DBF_USHORT, DBF_ULONG, or DBF_FLOAT. database/ioc provides a simplified implementation for each of these types. NOTE that the implementations do not allow attached device support.

These can be used to test:

caget, caput, and camonitor
many possibilities
pvget and pvput
Provider pva has proper support for all DBF types.
Provider ca has support similar to caget, caput, and camonitor. Note that uint16 and uint32 are prompted to another type. This is the behavior forced by channel access.
exampleClient
get, put , and monitor provide another way to test providers pva and ca.

pvDatabase Records

Overview

PVDatabase implements a number of PVRecords. Record instances can be created via iocshell commands or from a non IOC application.

In the source file where You build your IOC the appInclude.dbd file must include the following:

include "base.dbd"
include "PVAServerRegister.dbd"
include "PVAClientRegister.dbd"
include "registerChannelProviderLocal.dbd"
include "qsrv.dbd"
include "pvdbcrAllRecords.dbd"
#...

Then in the st.cmd file you can issue help for each of the shell commands implemented what is descibed below.
You can also create PVRecord instances.
For example:

epics> help pvdbcrScalarRecord
pvdbcrScalarRecord recordName scalarType asLevel asGroup
epics> pvdbcrScalarRecord PVRscalarDouble double
This creates a record:
pvinfo PVRscalarDouble
PVRscalarDouble
Server: 10.0.0.194:5075
Type:
    structure
        double value
        time_t timeStamp
            long secondsPastEpoch
            int nanoseconds
            int userTag
        alarm_t alarm
            int severity
            int status
            string message

pvdbcrScalarRecord and pvdbcrScalarArrayRecord

Both create a record that has fields value, timeStamp, and alarm.

For pvdbcrScalarRecord, the type for the value field is one of the following:
boolean,byte,short,int,long, ubyte,ushort,uint,ulong, ,float,double, or string.

For pvdbcrScalarArrayRecord the value field is an scalarArray of one of the above types.

pvdbcrTraceRecord

This is a record that sets the trace level in another record in the same pvDatabase.

structure 
    structure argument
        string recordName 
        int level
    structure result
        string status 
where
recordName
The name of another record in the same pvDatabase.
level
The trace level to set.
status
The result of the request.

pvdbcrAddRecord

This is a record that adds a record to the same pvDatabase. This can only be used to generate "soft" records, i. e. the only process support is that provided by PVRecord. This means timeStamp support.

structure 
    structure argument
        string recordName 
        any union
    structure result
        string status 
where
recordName
The name of the new record to add to the same pvDatabase.
union
The caller must give this a PVStructure, which will be used to generate the top level PVStructure for the new record.
status
The result of the request.

The following is an example:

pwd
/home/install/epics/base-7.0.5/modules/exampleCPP/exampleClient
bin/linux-x86_64/addRecord PVRaddRecord PVRdouble test
result=structure 
    structure result
        string status success

pvinfo test
test
Server: 10.0.0.194:5075
Type:
    structure
        double value
        time_t timeStamp
            long secondsPastEpoch
            int nanoseconds
            int userTag
        alarm_t alarm
            int severity
            int status
            string message


pvdbcrRemoveRecord

This is a record that removes another record in the same pvDatabase.

structure 
    structure argument
        string recordName 
    structure result
        string status 
where
recordName
The name of another record in the same pvDatabase.
status
The result of the request.

The following shows now it can be used:

pvput PVRremoveRecord argument='{"recordName":"test"}'
Old : structure 
    structure argument
        string recordName 
    structure result
        string status 
New : structure 
    structure argument
        string recordName test
    structure result
        string status success

pvdbcrProcessRecord

This is a record that processes other records in the same pvDatabase. It keeps a map of all records to process. Records can be added or removed from the list.

structure 
    structure argument
        string command
        string recordName
    structure result
        string status
where
command
The command can be add or remove
recordName
The name of another record in the same pvDatabase.
status
The result of the request.

scalarLimit

Overview

This is an example that implements a record that has the following structure:

pvget -v  PVRscalarLimitUbyte
PVRscalarLimitUbyte structure 
    ubyte value 100
    time_t timeStamp <undefined>              
        long secondsPastEpoch 0
        int nanoseconds 0
        int userTag 0
    alarm_t alarm 
        int severity 0
        int status 0
        string message 
    structure control
        ubyte limitLow 0
        ubyte limitHigh 250
        ubyte maxStep 10
    structure alarmLimit
        ubyte lowAlarmLimit 20
        ubyte lowWarningLimit 50
        ubyte highAlarmLimit 230
        ubyte highWarningLimit 200
Both serviceMain and database create record PVRscalarLimitUbyte as well as record PVRscalarLimitDouble. The difference is the field types. Wherever ubyte appears in PVRscalarLimitUbyte double appears in PVRscalarLimitDouble.

After starting either serviceMain or database then in another window enter:

pvget -m  -v PVRscalarLimitUbyte
You will see the above structure. Leave this running.

In another window enter

pvput PVRprocessRecord argument='{"command":"add","recordName":"PVRscalarLimitUbyte"}'

Then enter:

pvput PVRscalarLimitUbyte 1

In the window where you started monitoring You will see:

PVRscalarLimitUbyte structure 
    ubyte value 90
    time_t timeStamp 2021-05-07 07:10:40.325  
        long secondsPastEpoch 1620385840
        int nanoseconds 324700934
PVRscalarLimitUbyte structure 
    ubyte value 80
    time_t timeStamp 2021-05-07 07:10:40.676  
        int nanoseconds 676307219
PVRscalarLimitUbyte structure 
    ubyte value 70
    time_t timeStamp 2021-05-07 07:10:41.677  
        long secondsPastEpoch 1620385841
        int nanoseconds 676575626
PVRscalarLimitUbyte structure 
    ubyte value 60
    time_t timeStamp 2021-05-07 07:10:42.677  
        long secondsPastEpoch 1620385842
        int nanoseconds 676853907
PVRscalarLimitUbyte structure 
    ubyte value 50
    time_t timeStamp 2021-05-07 07:10:43.677  
        long secondsPastEpoch 1620385843
        int nanoseconds 677155943
    alarm_t alarm MINOR minor low alarm 
        int severity 1
        string message "minor low alarm"
PVRscalarLimitUbyte structure 
    ubyte value 40
    time_t timeStamp 2021-05-07 07:10:44.677  
        long secondsPastEpoch 1620385844
        int nanoseconds 677432384
PVRscalarLimitUbyte structure 
    ubyte value 30
    time_t timeStamp 2021-05-07 07:10:45.678  
        long secondsPastEpoch 1620385845
        int nanoseconds 677795226
PVRscalarLimitUbyte structure 
    ubyte value 20
    time_t timeStamp 2021-05-07 07:10:46.678  
        long secondsPastEpoch 1620385846
        int nanoseconds 678169012
    alarm_t alarm MAJOR major low alarm 
        int severity 2
        string message "major low alarm"
PVRscalarLimitUbyte structure 
    ubyte value 10
    time_t timeStamp 2021-05-07 07:10:47.678  
        long secondsPastEpoch 1620385847
        int nanoseconds 678459518
PVRscalarLimitUbyte structure 
    ubyte value 1
    time_t timeStamp 2021-05-07 07:10:48.679  
        long secondsPastEpoch 1620385848
        int nanoseconds 678830388
Note the following:
value
This changes by 10 until 1 is reached. This is because control.maxStep is 10.
alarm
Note that this changes when the value 50 is reached and again when the value 20 is reached. Look at alarmLimit to see why.

Now enter:

pvput PVRscalarLimitUbyte 255
To see why the final value is 250 instead of 255 look at control.limitHigh.

Building

The top level Makefile and configure are typical. It has two subdirectories: scalarLimit/src and scalarLimit/src/scalarLimit. The later has the interface for code that wants to create scalarLimit records. scalarLimit/src has the implementation.

scalarLimit/src/scalarLimit/scalarLimitRecord.h

#ifndef SCALARLIMITRECORDCREATE_H
#define SCALARLIMITRECORDCREATE_H

#include <shareLib.h>

namespace epics { namespace scalarLimit {

class ScalarLimitRecord;
typedef std::tr1::shared_ptr<ScalarLimitRecord> ScalarLimitRecordPtr;

class epicsShareClass ScalarLimitRecordCreate
{
public:
    /**
     * @brief Create a record.
     *
     * @param recordName The record name.
     * @param scalarType The type for the value field
     * @param asLevel  The access security level.
     * @param asGroup  The access security group.
     */
     static void create(
        std::string const & recordName,std::string const &  scalarType,
        int asLevel=0,std::string const & asGroup = std::string("DEFAULT"));
};

}}

#endif  /* SCALARLIMITRECORDCREATE_H */
NOTE:
shareLib.h and epicsShareClass
These are for windows DLL.
ScalarLimitRecord and ScalarLimitRecordPtr
These are required for code that calls ScalarLimitRecordCreate::create.
ScalarLimitRecordCreate
This has a single static method that creates a ScalarLimitRecord. The caller must supply a recordName and a scalarType.
The scalarType must can be any numeric scalar type, i.e. byte, short, int, long, ubyte, ushort, uint, ulong, float, double.
The implementation is in scalarLimit/src.

scalarLimit.src

This has the following:
Makefile
scalarLimitRecord.cpp
scalarLimitRecord.dbd

Makefile

TOP=..
include $(TOP)/configure/CONFIG
EPICS_BASE_PVA_CORE_LIBS = pvDatabase pvAccess pvAccessCA nt pvData ca Com
INC += scalarLimit/scalarLimitRecord.h
DBD += scalarLimitRecord.dbd
LIBRARY = scalarLimitRecord
LIBSRCS += scalarLimitRecord.cpp
scalarLimitRecord_LIBS += $(EPICS_BASE_PVA_CORE_LIBS)
# needed for Windows
LIB_SYS_LIBS_WIN32 += netapi32 ws2_32
include $(TOP)/configure/RULES

scalarLimitRecord.cpp

This starts with

#include <iocsh.h>
#include <pv/standardField.h>
#include <pv/standardPVField.h>
#include <pv/timeStamp.h>
#include <pv/pvTimeStamp.h>
#include <pv/alarm.h>
#include <pv/pvAlarm.h>
#include <pv/convert.h>
#include <pv/pvDatabase.h>

// The following must be the last include for code scalarLimit implements
#include <epicsExport.h>
#define epicsExportSharedSymbols
#include "scalarLimit/scalarLimitRecord.h"
using namespace epics::pvData;
using namespace epics::pvDatabase;
using namespace std;

Next is the class definition for ScalarLimitRecord

namespace epics { namespace scalarLimit {

class epicsShareClass ScalarLimitRecord :
     public epics::pvDatabase::PVRecord
{
private:
friend class ScalarLimitRecordCreate;
    ScalarLimitRecord(
      std::string const & recordName,epics::pvData::PVStructurePtr const & pvStructure,
      int asLevel,std::string const & asGroup);
   double desiredValue;
   double currentValue;
public:
    virtual ~ScalarLimitRecord() {}
    virtual void process();
    virtual bool init(){
        initPVRecord();
        return true;
    }
}; 

ScalarLimitRecord::ScalarLimitRecord(
    std::string const & recordName,epics::pvData::PVStructurePtr const & pvStructure,
    int asLevel,std::string const & asGroup)
: PVRecord(recordName,pvStructure,asLevel,asGroup)
{}

Next is the definition of process. Only the beginning is shown. See the source code for details.

void ScalarLimitRecord::process()
{
    ConvertPtr convert = getConvert();
    PVStructurePtr top = this->getPVStructure();
... LOTS OF CODE
    PVRecord::process();
}

Next is the code for ScalarLimitRecordCreate. Only the beginning is shown. See the source code for details.

void ScalarLimitRecordCreate::create(
    std::string const & recordName,std::string const &  scalarType,
    int asLevel,std::string const & asGroup)
{
    ConvertPtr convert = getConvert();
    ScalarType st = epics::pvData::ScalarTypeFunc::getScalarType(scalarType);
    FieldCreatePtr fieldCreate = getFieldCreate();
    StandardFieldPtr standardField = getStandardField();
    PVDataCreatePtr pvDataCreate = getPVDataCreate();
    StructureConstPtr top = fieldCreate->createFieldBuilder()->
        add("value",st) ->
        add("timeStamp",standardField->timeStamp()) ->
        add("alarm",standardField->alarm()) ->
        addNestedStructure("control") ->
            add("limitLow",st) ->
            add("limitHigh",st) ->
            add("maxStep",st) ->
        endNested()->
        addNestedStructure("alarmLimit") ->
            add("lowAlarmLimit",st) ->
            add("lowWarningLimit",st) ->
            add("highAlarmLimit",st) ->
            add("highWarningLimit",st) ->
        endNested()->
        createStructure();
... CODE to initialize fields
    PVDatabasePtr master = PVDatabase::getMaster();
    if(!master->addRecord(pvRecord)) {
        cerr << recordName << " not added to master\n";
    }
}

}}

Next is code to create an iocshell command.

static const iocshArg arg0 = { "recordName", iocshArgString };
static const iocshArg arg1 = { "scalarType", iocshArgString };
static const iocshArg arg2 = { "asLevel", iocshArgInt };
static const iocshArg arg3 = { "asGroup", iocshArgString };
static const iocshArg *args[] = {&arg0,&arg1,&arg2,&arg3};

static const iocshFuncDef scalarLimitFuncDef = {"scalarLimitRecord", 4,args};

static void scalarLimitCallFunc(const iocshArgBuf *args)
{
    char *sval = args[0].sval;
    if(!sval) {
        throw std::runtime_error("scalarLimitRecord recordName not specified");
    }
    string recordName = string(sval);
    sval = args[1].sval;
    if(!sval) {
        throw std::runtime_error("scalarLimitRecord scalarType not specified");
    }
    string scalarType = string(sval);
    int asLevel = args[2].ival;
    string asGroup("DEFAULT");
    sval = args[3].sval;
    if(sval) {
        asGroup = string(sval);
    }
    epics::scalarLimit::ScalarLimitRecordCreate::create(recordName,scalarType,asLevel,asGroup); 
}

static void scalarLimitRecord(void)
{
    static int firstTime = 1;
    if (firstTime) {
        firstTime = 0;
        iocshRegister(&scalarLimitFuncDef, scalarLimitCallFunc);
    }
}

extern "C" {
    epicsExportRegistrar(scalarLimitRecord);
}

scalarLimitRecord.dbd

registrar("scalarLimitRecord")

powerSupplyRecord

Overview

This is an example of creating a service that requires a somewhat complicated top level PVStructure.

The example has a top level pvStructure:

pvinfo PVRpowerSupply
PVRpowerSupply
Server: 10.0.0.194:5075
Type:
    structure
        alarm_t alarm
            int severity
            int status
            string message
        time_t timeStamp
            long secondsPastEpoch
            int nanoseconds
            int userTag
        structure power
            double value
        structure voltage
            double value
        structure current
            double value

It is designed to be accessed via a channelPutGet request. The client sets power.value and voltage.value When the record processes it computes current.value. In addition the timeStamp is set to the time when process is called.

Building

This goes into some detail because it is like all the other support modules that are used by serviceMain and database. The other support modules have a similar build structure.

The top level Makefile and configure are typical. It has a subdirectory src which has:

Makefile

TOP=..
include $(TOP)/configure/CONFIG
#NOTE order is important
EPICS_BASE_PVA_CORE_LIBS = pvaClient  pvDatabase pvAccess pvAccessCA pvData ca Com

INC += powerSupply/powerSupplyRecord.h
DBD += powerSupplyRecord.dbd

LIBRARY = powerSupplyRecord
LIBSRCS += powerSupplyRecord.cpp
powerSupplyRecord_LIBS += $(EPICS_BASE_PVA_CORE_LIBS)

PROD_HOST += powerSupplyClient
powerSupplyClient_SRCS += powerSupplyClient.cpp
powerSupplyClient_LIBS += $(EPICS_BASE_PVA_CORE_LIBS)

PROD_HOST += powerSupplyMonitor
powerSupplyMonitor_SRCS += powerSupplyMonitor.cpp
powerSupplyMonitor_LIBS += $(EPICS_BASE_PVA_CORE_LIBS)

# needed for Windows
LIB_SYS_LIBS_WIN32 += netapi32 ws2_32

include $(TOP)/configure/RULES
where:
powerSupplyRecord
This is the library is used by both serviceMain and database. Details are shown below.
powerSupplyClient
This is client code. What it does is shown below.
powerSupplyMonitor
This is also client code. What it does is shown below.

powerSupply

This subdirectory has a single include file powerSupplyRecord.h. It is what allows serviceMain to use the support.
It is:
#ifndef POWERSUPPLYRECORD_H
#define POWERSUPPLYRECORD_H


#include <pv/timeStamp.h>
#include <pv/alarm.h>
#include <pv/pvTimeStamp.h>
#include <pv/pvAlarm.h>
#include <pv/pvDatabase.h>

#include <shareLib.h>

namespace epics { namespace example { namespace powerSupply {

class PowerSupplyRecord;
typedef std::tr1::shared_ptr<PowerSupplyRecord> PowerSupplyRecordPtr;

class epicsShareClass PowerSupplyRecord :
    public epics::pvDatabase::PVRecord
{
public:
    POINTER_DEFINITIONS(PowerSupplyRecord);
    static PowerSupplyRecordPtr create(
        std::string const & recordName);
    virtual ~PowerSupplyRecord() {}
    virtual bool init() {return false;}
    virtual void process();
    
private:
    PowerSupplyRecord(std::string const & recordName,
        epics::pvData::PVStructurePtr const & pvStructure);
    void initPvt();

    epics::pvData::PVDoublePtr pvCurrent;
    epics::pvData::PVDoublePtr pvPower;
    epics::pvData::PVDoublePtr pvVoltage;
    epics::pvData::PVAlarm pvAlarm;
    epics::pvData::Alarm alarm;
};


}}}

#endif  /* POWERSUPPLYRECORD_H */

powerSupplyRecord.dbd

The dbd definition used to create an iocshell command used by database:
registrar("powerSupplyRecord")

powerSupplyRecord.cpp

//code that implements LIBRARY = powerSupplyRecord
#include <iocsh.h>
#include <pv/pvDatabase.h>
#include <pv/pvStructureCopy.h>
#include <pv/timeStamp.h>
#include <pv/standardField.h>
#include <pv/pvAlarm.h>
#include <pv/channelProviderLocal.h>

// The following must be the last include for code database uses
#include <epicsExport.h>
#define epicsExportSharedSymbols
#include "powerSupply/powerSupplyRecord.h"

using namespace epics::pvData;
using namespace epics::pvDatabase;
using std::string;

namespace epics { namespace example { namespace powerSupply {

PowerSupplyRecordPtr PowerSupplyRecord::create(
    string const & recordName)
{
    FieldCreatePtr fieldCreate = getFieldCreate();
    StandardFieldPtr standardField = getStandardField();
    PVDataCreatePtr pvDataCreate = getPVDataCreate();

    StructureConstPtr  topStructure = fieldCreate->createFieldBuilder()->
            add("alarm",standardField->alarm()) ->
            add("timeStamp",standardField->timeStamp()) ->
            addNestedStructure("power") ->
               add("value",pvDouble) ->
               endNested()->
            addNestedStructure("voltage") ->
               add("value",pvDouble) ->
               endNested()->
            addNestedStructure("current") ->
               add("value",pvDouble) ->
               endNested()->
            createStructure();
    PVStructurePtr pvStructure = pvDataCreate->createPVStructure(topStructure);
    PowerSupplyRecordPtr pvRecord(
        new PowerSupplyRecord(recordName,pvStructure));
    pvRecord->initPvt();
    return pvRecord;
}

PowerSupplyRecord::PowerSupplyRecord(
    string const & recordName,
    PVStructurePtr const & pvStructure)
: PVRecord(recordName,pvStructure)
{
}


void PowerSupplyRecord::initPvt()
{
    initPVRecord();
    PVStructurePtr pvStructure = getPVStructure();
    PVFieldPtr pvField;
    pvField = pvStructure->getSubField("alarm");
    pvAlarm.attach(pvField);
    pvCurrent = pvStructure->getSubField<PVDouble>("current.value");
    pvVoltage = pvStructure->getSubField<PVDouble>("voltage.value");
    pvPower = pvStructure->getSubField<PVDouble>("power.value");
    alarm.setMessage("bad voltage");
    alarm.setSeverity(majorAlarm);
    pvAlarm.set(alarm);
}

void PowerSupplyRecord::process()
{
    double voltage = pvVoltage->get();
    double power = pvPower->get();
    if(voltage<1e-3 && voltage>-1e-3) {
        alarm.setMessage("bad voltage");
        alarm.setSeverity(majorAlarm);
        pvAlarm.set(alarm);
        throw std::runtime_error("bad voltage exception");
    }
    double current = power/voltage;
    pvCurrent->put(current);
    pvAlarm.get(alarm);
    if(alarm.getSeverity()!=noAlarm) {
        alarm.setMessage("");
        alarm.setSeverity(noAlarm);
        pvAlarm.set(alarm);
    }
    PVRecord::process();
}
}}}

// code that implements the iocshell command
static const iocshArg testArg0 = { "recordName", iocshArgString };
static const iocshArg *testArgs[] = {
    &testArg0};

static const iocshFuncDef powerSupplyFuncDef = {
    "powerSupplyRecord", 1, testArgs};
static void powerSupplyCallFunc(const iocshArgBuf *args)
{
    char *recordName = args[0].sval;
    if(!recordName) {
        throw std::runtime_error("powerSupplyRecord invalid number of arguments");
    }
    epics::example::powerSupply::PowerSupplyRecordPtr record
       = epics::example::powerSupply::PowerSupplyRecord::create(recordName);
    bool result = PVDatabase::getMaster()->addRecord(record);
    if(!result) std::cout << string(recordName) << " not added" << "\n";
}

static void powerSupplyRecord(void)
{
    static int firstTime = 1;
    if (firstTime) {
        firstTime = 0;
        iocshRegister(&powerSupplyFuncDef, powerSupplyCallFunc);
    }
}

extern "C" {
    epicsExportRegistrar(powerSupplyRecord);
}

Testing

Start Server

Start either serviceMain or database.

Start Monitor

In another window:
pwd
/home/install/epics/base-7.0.5/modules/exampleCPP/powerSupplyRecord
bin/linux-x86_64/powerSupplyMonitor

Start Client

In another window:
pwd
/home/install/epics/base-7.0.5/modules/exampleCPP/powerSupplyRecord
bin/linux-x86_64/powerSupplyClient 

NOTE: The client ends by requesting a voltage of 0. This results in the power supply record thowing an exception.

linkRecord

The build structure is similar to powerSupplyRecord.

Overview

exampleLink implements PVRecords that link to another record. Each provides two ways to access the other record:

client
This uses pvaClient to connect to another record in the same or another ioc.
database
This uses PVDatabase to connect to another record in the same ioc.

The following records are implemented:

putLinkScalar
A record that, when processed, puts a value to another scalar record.
getLinkScalar
A record that, when processed, gets a value from another scalar record.
putLinkScalarArray
A record that, when processed, puts a value to another scalar array record.
getLinkScalarArray
A record that, when processed, gets a value from another scalar array record.

Running the example

Start Server

Start either serviceMain or database.

Start monitoring all records

In another window run:

pvget -m PVRdouble PVRdoubleArray PVRgetLinkScalar PVRgetLinkScalarArray PVRputLinkScalar PVRputLinkScalarArray PVRstring PVRstringArray

Try some simple tests

Try the following:

pvput PVRputLinkScalar 1
Old : <undefined>               
New : 2021-03-18 13:51:46.816  1 
pvput PVRputLinkScalarArray [1,2,3]
Old : <undefined>              []
New : 2021-03-18 13:52:23.698  [1, 2, 3]

Look at the the window where you are monitoring and see what happened.

fuctionalty

record structure

The record being accessed must have a top level value field, which must be a scalar or scalar array.

An example of a valid record is:

pvinfo PVRdoubleArray
PVRdoubleArray
Server: 10.0.0.194:49943
Type:
    structure
        double[] value
        time_t timeStamp
            long secondsPastEpoch
            int nanoseconds
            int userTag
        alarm_t alarm
            int severity
            int status
            string message

putLinkScalar, putLinkScalarArray, getLinkScalar, and getLinkScalarArray all have the same basic structure. For example:

pvinfo PVRputLinkScalar
PVRputLinkScalar
Server: 10.0.0.194:49943
Type:
    structure
        string value
        string linkRecord
        string accessMethod
        time_t timeStamp
            long secondsPastEpoch
            int nanoseconds
            int userTag
        alarm_t alarm
            int severity
            int status
            string message
        boolean reconnect

The only difference is if value has type string or string[].

processing

pvput and pvget are used for examples that demonstrate how the link records work. They are all that is needed for the put records. They can also be used for the get records but if pvput changes field value and processing is succesful, value will be replaced by the value obtained from the link record. Note that exampleCPP/exampleClient has an example that just asks for a record to process.

The following is an example of how a client can specify all fields that are related to processing.

pvput PVRputLinkScalarArray '{"value":["1","2"],"linkRecord":"PVRdoubleArray","accessMethod":"client"}'

Thus a client can put values to the following:

value
The value field.
linkRecord
The name of the link record.
accessMethod
The method used to access the link record. This must be client or database

This is actually the default for linkRecord and accessMethod when the ioc is started.

Once the desired linkRecord and accessMethod have been specified the following will work.

pvput PVRputLinkScalarArray ["100","200","300"]

For the get records the above will also work. But it is better to use exampleClient/process or the following:

pvput PVRgetLinkScalarArray '{"linkRecord":"PVRdoubleArray","accessMethod":"client"}'

connection management

When the IOC is started PVRputLinkScalar, PVRputLinkScalarArray, PVRgetLinkScalar, and gPVRetLinkScalarArray are not connected to linkRecord for either client or database access. The first time client connects PvaClientChannelPtr and PvaClientGetPtr are saved. The first time database connects PVRecordPtr is saved. Thus initialization overhead is not incured during further processing. This also means that if the client specifies linkRecord it has no effect.

There is an additional field that a client can access so that it can again put linkRecord An example is:

pvput PVRputLinkScalar reconnect="true"
Old : <undefined>               
New : 2021-03-19 10:53:26.708   MINOR reconnecting 
pvput PVRputLinkScalar '{"value":"a value","linkRecord":"PVRstring","accessMethod":"client"}'
Old : 2021-03-19 10:53:26.708   MINOR reconnecting 
New : 2021-03-19 10:55:41.463  "a value" 

more client examples

pvput PVRputLinkScalar '{"value":"a value","linkRecord":"PVRstring","accessMethod":"client"}'
Old : <undefined>               
New : 2021-03-19 11:20:29.381  "a value" 
pvput PVRputLinkScalarArray '{"value":["a value","b value"],"linkRecord":"PVRstringArray","accessMethod":"client"}'
Old : <undefined>              []
New : 2021-03-19 11:22:10.000  ["a value", "b value"]
pvput PVRputLinkScalar '{"value":"a value","linkRecord":"PVRstring","accessMethod":"database"}'
Old : 2021-03-19 11:20:29.381  "a value" 
New : 2021-03-19 11:22:29.608  "a value" 
pvput PVRputLinkScalarArray '{"value":["a value","b value"],"linkRecord":"PVRstringArray","accessMethod":"database"}'
Old : 2021-03-19 11:22:10.000  ["a value", "b value"]
New : 2021-03-19 11:22:49.790  ["a value", "b value"]

helloPutGetRecord

The build structure is similar to powerSupplyRecord.

Overview

The example implements a simple putGet that has a top level pvStructure:

structure
    time_t timeStamp
        long secondsPastEpoch
        int nanoseconds
        int userTag
    structure argument
        string value
    structure result
        string value

It is designed to be accessed via a channelPutGet request. The client sets argument.value When the record processes it sets result.value to "Hello " concatenated with argument.value. Thus if the client sets argument.value equal to "World" result.value will be "Hello World". In addition the timeStamp is set to the time when process is called.

Start Server

Start either serviceMain or database.

Hello World Client

This example requires that the server is running. In addition start a monitor as follows:

pvget -r "field()"  -m PVRhelloPutGet

When the following is executed:

pwd
/home/install/epics/base-7.0.5/modules/exampleCPP/helloPutGetRecord
bin/linux-x86_64/helloPutGetClient

The pvget shows:

PVRhelloPutGet structure 
    time_t timeStamp 2021-05-15 10:25:59.502  
        long secondsPastEpoch 1621088759
        int nanoseconds 502028194
    structure argument
        string value World
    structure result
        string value "Hello World"

Non Blocking Client

A client with more functionality is also available. It can be started either before of after the server is started. It options are:

 pwd
/home/install/epics/base-7.0.5/modules/exampleCPP/helloPutGet
 bin/linux-x86_64/helloNoWaitPutGetClient -help
providerName channelName request debug
default
pva PVRhelloPutGet "putField(argument)getField(result)" false

After it is started as follows:

 bin/linux-x86_64/helloNoWaitPutGetClient

Then it accepts several commands. To see the commands just type help:

help
exit putGet getPut getGet

The commands are:

exit
The program terminates.
putGet
The client is prompted for a value, which is sent to the server via a putGet request.
getPut
A getPut request is sent to the server and the result displayed.
getGet
A getGet request is sent to the server and the result displayed.

helloRPCRecord

The build structure is similar to powerSupplyRecord.

This is an example of a channel RPC request. Look at the source code for details but note that it simulates a RPC server that take time to process each event.

The following shows how to use the service:

pwd
/home/install/epics/base-7.0.5/modules/exampleCPP/exampleClient
bin/linux-x86_64/helloWorldRPC
_____HelloWorldRPC starting_______
_____exampleSimple___
send SimpleWorld
result
epics:nt/NTScalar:1.0 
    string value "Hello SimpleWorld"

_____exampleMore___
send MoreWorld
result
epics:nt/NTScalar:1.0 
    string value "Hello MoreWorld"

send MoreAgain
result
epics:nt/NTScalar:1.0 
    string value "Hello MoreAgain"

_____exampleEvenMore___
example channeRPC more
send EvenMoreWorld
response
epics:nt/NTScalar:1.0 
    string value "Hello EvenMoreWorld"

send EvenMoreAgain
response
epics:nt/NTScalar:1.0 
    string value "Hello EvenMoreAgain"

send EvenMore one more
Expected exception channel PVRhelloRPC PvaClientRPC::request request timeout 
Expected exception channel PVRhelloRPC PvaClientRPC::request request aleady active 
_____HelloWorldRPC done_______

controlRecord

The build structure is similar to powerSupplyRecord. It uses the support code from pvDatabase, which is deprecated for the following reasons:

minStep
This should be named maxStep.
value
When a client puts to value the value immediately changes to that value. Then each time the record processes outputValue changes by minStep until it reaches value.
I think these are the wrong semantics. The semantics should be like scalarLimit.

controlRecord is support for any field that is a numeric scalar. The following must appear in the same structure as the field being supported:

control_t control
    double limitLow
    double limitHigh
    double minStep
    scalarType outputValue
where:
limitLow
When the record is processed and the associated value field is modified, the new value is not allowed to be less than limitLow.
limitHigh
When the record is processed and the associated value field is modified, the new value is not allowed to be greater than limitHigh.
minStep
When to associated value field is changed then outputValue is not allowed to change by more then minStep each time the the record is processed.
outputValue
The current output value. Note that it has the same scalarType as the associated value field.
NOTES:

scalarAlarmSupport

scalarAlarmSupport is support for any field that is a numeric scalar. The following must appear in the same structure as the field being supported:

scalarAlarm_t scalarAlarm
    double lowAlarmLimit
    double lowWarningLimit
    double highWarningLimit
    double highAlarmLimit
    double hysteresis
where:
lowAlarmLimit
When the record is processed the associated value field is checked to see if it is less than lowAlarmLimit. If it is the alarm field is set to major alarm.
lowWarningLimit
When the record is processed the associated value field is checked to see if it is less than lowWarningLimit. If it is the alarm field is set to minor alarm.
highWarningLimit
When the record is processed the associated value field is checked to see if it is less than highWarningLimit. If it is the alarm field is set to minor alarm.
highAlarmLimit
When the record is processed the associated value field is checked to see if it is less than highAlarmLimit. If it is the alarm field is set to major alarm.
hysteresis
If an alarm is raised then it is not set to a lower alarm severity until the associated value changes from the current limit by at least hysteresis.
NOTES:

Start Server

Start either serviceMain or database.

supportRecordCreate

This is an iocshell command implemented in exampleCPP/support. It creates PVRecord instances that are used and described later in this tutorial. The created record uses the control and scalarAlarm support from pvDatabase.

A created record has the following structure:

pvinfo PVRcontrolDouble
PVRcontrolDouble
Server: 10.0.0.9:5075
Type:
    structure
        double value
        boolean reset
        alarm_t alarm
            int severity
            int status
            string message
        time_t timeStamp
            long secondsPastEpoch
            int nanoseconds
            int userTag
        display_t display
            double limitLow
            double limitHigh
            string description
            string format
            string units
        control_t control
            double limitLow
            double limitHigh
            double minStep
            double outputValue
        scalarAlarm_t scalarAlarm
            double lowAlarmLimit
            double lowWarningLimit
            double highWarningLimit
            double highAlarmLimit
            double hysteresis
example usage appears later in this tutorial.

Use configAll script to configure records

Run the following:

pwd
/home/install/epics/base-7.0.5/modules/exampleCPP/support/client/scripts
./configAll

configAll does the following:

Run an example client

Look at the following

pvget -r "control,scalarAlarm" PVRcontrolDouble
PVRcontrolDouble structure 
    control_t control
        double limitLow -10
        double limitHigh 10
        double minStep 0.5
        double outputValue 0
    scalarAlarm_t scalarAlarm
        double lowAlarmLimit -8
        double lowWarningLimit -6
        double highWarningLimit 6
        double highAlarmLimit 8
        double hysteresis 0.1

In one window enter

pvget -m -r "value,control.outputValue,alarm,timeStamp" -v PVRcontrolDouble
The output from this window will be described after you enter the next command.

Enter:

pvput PVRcontrolDouble  20

Results

In the window where you issued pvget you will first see:
PVRcontrolDouble structure 
    double value 10
    structure control
        double outputValue 0.5
    alarm_t alarm MAJOR RECORD major high alarm 
        int severity 2
        int status 3
        string message major high alarm
    time_t timeStamp 2019-07-01 09:32:51.793  
        long secondsPastEpoch 1561987971
        int nanoseconds 792512003

value only went to 10 because that is the control limit. alarm shows the values determined by scalarAlarm . outputValue is .5 because of minStep.

Next you will see output like:

PVRcontrolDouble structure 
    structure control
        double outputValue .5
    time_t timeStamp 2021-04-06 10:39:28.987
        int nanoseconds 968025553

This will continue until You see:

PVRcontrolDouble structure 
    structure control
        double outputValue 10
    time_t timeStamp  2021-04-06 10:44:50.660
        int nanoseconds 973958435

No more output occurs because outputValue is now equal to value.

Later in the tutorial there is a similar example except for channel PVRcontrolUbyte.

scripts

The scripts all appear in:

pwd
/home/install/epics/base-7.0.5/modules/exampleCPP/controlRecord/client/scripts

The scripts with names that start with config can be run either interactively or via arguments. The only exception is configAll

configControl

An example usage is:

./configControl
recordName?
PVRcontrolDouble
limitLow
-10
limitHigh
10
minStep
.5
Old : structure 
    control_t control
        double limitLow -10
        double limitHigh 10
        double minStep 0.5
        double outputValue 2.5
New : structure 
    control_t control
        double limitLow -10
        double limitHigh 10
        double minStep 0.5
        double outputValue 2.5

It could do the same without prompts via:

./configControl PVRcontrolDouble -10 10 .5
Old : structure 
    control_t control
        double limitLow -10
        double limitHigh 10
        double minStep 0.5
        double outputValue 10
New : structure 
    control_t control
        double limitLow -10
        double limitHigh 10
        double minStep 0.5
        double outputValue 10

The same could be done using pvput as follows:

pvput -r "control" PVRcontrolDouble control='{"limitLow":"-10","limitHigh":"10","minStep":".5"}'

configScalarAlarm

An example usage is:

./configScalarAlarm
recordName?
PVRcontrolDouble
lowAlarmLimit
-8
lowWarningLimit
-6
highWarningLimit
6
highAlarmLimit
8
hysteresis
.1
Old : structure 
    scalarAlarm_t scalarAlarm
        double lowAlarmLimit -8
        double lowWarningLimit -6
        double highWarningLimit 6
        double highAlarmLimit 8
        double hysteresis 0.1
New : structure 
    scalarAlarm_t scalarAlarm
        double lowAlarmLimit -8
        double lowWarningLimit -6
        double highWarningLimit 6
        double highAlarmLimit 8
        double hysteresis 0.1

Just like configControl it can be invoked by giving all values as arguments.

configProcessRecord

example usage is:

./configProcessRecord
processRecordName
PVRprocessRecord
command?
remove
recordName?
PVRcontrolDouble
// result is
PVRprocessRecord
argument={"command":"remove","recordName":"PVRcontrolDouble"}
Old : structure 
    structure argument
        string command add
        string recordName PVRcontrolDouble
    structure result
        string status success
New : structure 
    structure argument
        string command remove
        string recordName PVRcontrolDouble
    structure result
        string status success

It can also be invoked with arguments.

The following also works:

pvput -r "argument,result" PVRprocessRecord argument='{"command":"add","recordName":"PVRcontrolDouble"}'
Old : structure 
    structure argument
        string command remove
        string recordName PVRcontrolDouble
    structure result
        string status PVRcontrolDouble already present
New : structure 
    structure argument
        string command add
        spwd
tring recordName PVRcontrolDouble
    structure result
        string status PVRcontrolDouble already present

controlSigned

Convenience script for signed control limits.

#!/bin/sh
./configControl PVRcontrolDouble -10 10 .5

controlUnsigned

Convenience script for unsigned control limits.

#!/bin/sh
./configControl PVRcontrolUbyte 1 20 1

scalarAlarmSigned

Convenience script for signed scalarAlarm limits.

#!/bin/sh
./configScalarAlarm PVRcontrolDouble -8 -6 6 8 .1

scalarAlarmUnsigned

Convenience script for unsigned scalarAlarm limits.

#!/bin/sh
./configScalarAlarm PVRcontrolUbyte 2 4 16 18 1

configAll

Use of this was shown above.

#!/bin/bash
./controlSigned
./scalarAlarmSigned
./controlUnsigned
./scalarAlarmUnsigned
./configProcessRecord PVRprocessRecord add PVRcontrolDouble
./configProcessRecord PVRprocessRecord add PVRcontrolUbyte

Support Examples

Records PVRcontrolDouble and PVRcontrolUbyte both use the control and scalarAlarm record support from pvDatabaseCPP.

Previously there was an example accessing PVRcontrolDouble. Now lets look at an example accessing PVRcontrolUbyte.

After starting the database and running configAll, look at:

pvget -r "control,scalarAlarm" PVRcontrolUbyte
PVRcontrolUbyte structure 
    control_t control
        double limitLow 1
        double limitHigh 20
        double minStep 1
        ubyte outputValue 0
    scalarAlarm_t scalarAlarm
        double lowAlarmLimit 2
        double lowWarningLimit 4
        double highWarningLimit 16
        double highAlarmLimit 18
        double hysteresis 1
Look at the control and scalarAlarm settings.

In one window:

pvget -r "value,alarm,timeStamp,control.outputValue" -v -m PVRcontrolUbyte

Keep this window and see what happens when you run the following in another window:

pvput PVRcontrolUbyte 40

In the first window you will see that value goes to 20 and the alarm changes. It did NOT go to 40 because control.limitHigh is 20. But control.outputValue changes by 1 each time the record processes. On each process except the first only timeStamp and control.outputValue change value. This continues until control.outputValue is 20. Then no more monitors occur.

Now issue some more pvputs. For example to 19 then 15 then 5 then 0.