V4 User-defined fields

From EPICSWIKI
(Redirected from User-defined fields)

Overview

Records, the building blocks of an EPICS run-time database, are much more powerful than the fundamental commands of programming languages like C, C++, LabVIEW or PLC ladder logic: A record like the AI record provides scanning, various conversions, alarms, smoothing and more.

On the downside, very few record instances use the full functionality of the record type. Extending their functionality is rather involved, and very few custom record types get shared in the community.

If it was easier to create, modify and extend records, we could start with simpler record types and extend them as needed.

This is a suggestion for supporting user-defined fields in V4, allowing the user to add fields to a record instance without first having to create a new record type.

By now, the 'Vampire' (see V4_Server_Side_Plugins) seems to handle all but the forward link, and seems an overall better idea.

User-defined fields

For example, I'd like to allow users to define "analog input" records with the usual fields and allow them to add new fields to some instance without having to create a new record type:

calc("fred")
{
   field(SCAN, "1 second")
   user_field(SMOO,  iocFloat64T, "0.5", smoo_handler)
}

This record will have a 'SMOO' field of type double (float 64) that presumably implements smoothing in the user-supplied smoo_handler.

The idea is that one can add these user-defined fields on a per-instance basis to pretty much any record type simply by editing the DB file and loading the handler code, without having to recompile the rest of EPICS base.

Example code for a handler and what the database code would have to do follows.

database.h

#include <iostream>
#include <string>
#include <list>

using namespace std;

enum iocType
{
    iocUnknownT,iocBooleanT,iocOctetT,
    iocInt16T,iocUInt16T,iocInt32T,iocUInt32T,iocInt64T,iocUInt64T,
    iocFloat32T,iocFloat64T,
    iocStringT,iocMenuT,iocEnumT,iocLinkT,iocDeviceT,iocArrayT
};
// ------------------------------------------------------------------
// Support for user-defined fields in base
// ------------------------------------------------------------------
// forwards
class record;
class user_field_handler;

// Information for one user-defined field
class user_field_data
{
public:
    string  name;
    iocType type;
    void    *data;
    string  parm;
    user_field_handler *handler;
};

// The 'handler' specified for a user field needs to implement
// this interface:
class user_field_handler
{
public:
    virtual void init(user_field_data *field, record *rec)     {}
    virtual void update(user_field_data *field, record *rec)   {}
    virtual void destroy(user_field_data *field, record *rec)  {}  
};

// Records need to support hooks similar to Java 'listeners'
// so that user fields can hook into record processing.
// Unclear at what levels hooks are needed.
class got_data_hook
{
public:    virtual void run_got_data_hook()=0;
};
class post_monitor_hook
{
public:    virtual void run_post_monitor_hook()=0;
};

// Pieces of record code in order to show where the hooks
// are handled inside of 'process()'
class record
{
public:
    record(string name) : name(name) {}
    
    void add_user_field(string name, iocType type, string parm,
                        user_field_handler *handler)
    {
        user_field_data ufd;
        ufd.name = name;
        ufd.type = type;
        ufd.parm = parm;
        ufd.handler = handler;
        user_fields.push_back(ufd);
    }
    void add_got_data_hook(got_data_hook *hook)
    {
        got_data_hooks.push_back(hook);
    }
    // TODO: remove_got_data_hook();
    void add_post_monitor_hook(post_monitor_hook *hook)
    {
        post_monitor_hooks.push_back(hook);
    }
    // TODO: remove_post_monitor_hook();

    void iocInit();

    void process();
private:
    string                     name;
    list<user_field_data>      user_fields;
    list<got_data_hook *>      got_data_hooks;
    list<post_monitor_hook *>  post_monitor_hooks;
};


database.cpp

#include "database.h"

void *get_iocType_mem(iocType type)
{
    switch (type)
    {
        case iocFloat32T: return new float;
        case iocFloat64T: return new double;
        case iocStringT:  return new string;
    }
    return 0;
}

void record::iocInit()
{
    cout << "record::iocInit(" << name << ")\n";
    list<user_field_data>::iterator ufi;
    for (ufi = user_fields.begin();  ufi != user_fields.end();  ++ufi)
    {
        if ((ufi->data = get_iocType_mem(ufi->type)) != 0)
            ufi->handler->init(&(*ufi), this);
        else
            ufi->type = iocUnknownT;
    }   
}

void record::process()
{
    cout << "record::process(" << name << ")\n";
#ifdef TODO
    status=readValue(pai); /* read the new value */
    if ( !pact && pai->pact ) return(0);
    pai->pact = TRUE;
    
    recGblGetTimeStamp(pai);
    if (status==0) convert(pai);
    else if (status==2) status=0;
#endif
    list<got_data_hook *>::iterator gdhi;
    for (gdhi = got_data_hooks.begin(); gdhi != got_data_hooks.end(); ++gdhi)
        (*gdhi)->run_got_data_hook();
#ifdef TODO
    checkAlarms(pai);
    monitor(pai);
#endif
    list<post_monitor_hook *>::iterator pmhi;
    for (pmhi=post_monitor_hooks.begin(); pmhi!=post_monitor_hooks.end(); ++pmhi)
        (*pmhi)->run_post_monitor_hook();
#ifdef TODO
    pai->pact=FALSE;
#endif
}

// TODO:
// Record will need to check all it's user_fields[].name
// in case CA tries to get/put.

example.cpp

/* Goals:
- Avg or other statistical calc:
  deposits calculated data into new fields,
  doesn't affect the rest of the record.
- Circ Buffer (archive record):
  Similar, does something with the current VAL
  outside of the record.
- Put Logging: Simlar.
- SMOO: Can it modify the VAL field?
- BPM combiner of 4 AIs: Unclear.
- conversions (new breakpt. table): unclear.
- FLNK (w/ delays?): Should happen _after_ all else is done.

The following implements handlers for some of the above.

*/
#include "database.h"
// ------------------------------------------------------------------
// MAX (easier to calc. then e.g. running average
// ------------------------------------------------------------------
class max_handler : public user_field_handler, got_data_hook
{
    void init(user_field_data *field, record *rec)
    {
        max = (double *)field->data;
        // TODO: val = record->get_field_addr("VAL")
        rec->add_got_data_hook(this);
    }
    void run_got_data_hook()
    {
        // if (*val > *max)   *max = *val;
        cout << "Max is " << *max << "\n";
    }
private:
    double *max;
};

// ------------------------------------------------------------------
// SMOO
// ------------------------------------------------------------------
class smoo_handler : public user_field_handler, got_data_hook
{
    void init(user_field_data *field, record *rec)
    {
        smoo = (double *)field->data;
        *smoo = atof(field->parm.c_str());
        // TODO: val = record->get_field_addr("VAL")
        rec->add_got_data_hook(this);
    }
    void run_got_data_hook()
    {
        // *val = smoo*(*val) + (1-smoo)*prev_value;
        // prev_value = *val;
        cout << "Smoothing by " << *smoo << "\n";
    }
private:
    double *smoo;
    double prev_value;
};

// ------------------------------------------------------------------
// FLNK
// ------------------------------------------------------------------
class flnk_handler : public user_field_handler, post_monitor_hook
{
    void init(user_field_data *field, record *rec)
    {
        link_text = (string *)field->data;
        *link_text = field->parm;
        update(field, rec);
        rec->add_post_monitor_hook(this);
    }
    void update(user_field_data *field, record *rec)
    {
        // TODO: ink_info = parse_link_info_from_string(*link_text);
        link_info = *link_text;
    }
    void run_post_monitor_hook()
    {
        cout << "FLNKing to " << link_info << "\n";
        // TODO: dbProcessLink(this->link_info);
        // Or with additional delay parm:
        // wdStart(delay, dbProcessLink, this->link_info);  ?
    }
private:
    string *link_text;
    string link_info;
};

// ------------------------------------------------------------------
// Example database snippet and how it's used
// ------------------------------------------------------------------
/*
ai("fred")
{
  #          NAME, TYPE,         PARM,      HANDLER
  user_field(smoo,  iocFloat64T, "0.5",     smoo_handler)
  user_field(max,   iocFloat64T, "",        max_handler)
  user_field(flnk1, iocStringT,  "recordA", flnk_handler)
  user_field(flnk2, iocStringT,  "recordB", flnk_handler)
  user_field(flnk3, iocStringT,  "recordC", flnk_handler)
  # In these examples, the PARM is the initial value of the field.
  # But it could be any string that's used to configure the handler:
  # user_field(syslog, iocStringT, "server=124.0.0.5:9876,interval=1", syslog_handler)
}      
*/
int main()
{
    // This is sort of what would need to happen inside dbLoadDatabase().
    // The tricky part is the "new xxx_handler":
    // Each handler will have to use the registry to register a factory
    // method so that dbLoadDatabase can invoke "new xxx_handler" without
    // recompilation.
    record fred("fred");
    fred.add_user_field("smoo", iocFloat64T, "0.5",     new smoo_handler());
    fred.add_user_field("max",  iocFloat64T, "",        new max_handler());
    fred.add_user_field("flnk1", iocStringT, "recordA", new flnk_handler());
    fred.add_user_field("flnk2", iocStringT, "recordB", new flnk_handler());
    fred.add_user_field("flnk3", iocStringT, "recordC", new flnk_handler());
    //
    fred.iocInit();
    //
    fred.process();
    cout << "\n";
    fred.process();
    
    return 0;
}

/*
When run, this basically processes the record and the
code for the user-defined fields is invoked as intended:

record::iocInit(fred)
record::process(fred)
Smoothing by 0.5
Max is 0
FLNKing to recordA
FLNKing to recordB
FLNKing to recordC

record::process(fred)
Smoothing by 0.5
Max is 0
FLNKing to recordA
FLNKing to recordB
FLNKing to recordC

UNCLEAR:
Is string parm enough, or are structs requires?

I believe that all the goals 1-7 can be handled
by providing the appropriate hooks.
Question: How many hooks will that be?
  
*/


Comments

The example code for user_field_handler::init() assumes that the record has a routine get_field_addr(string name) that provides a raw pointer to each field of a record. Would have to use dataAccess or whatever the runtime database will ultimately provide.

Some pro and cons when comparing this type of user-defined field to writing a custom record:

Pro

  • One can add fields per record instance as needed. No need to create a possibly confusing plethora of specialized record types: BasicAI, MaxAI, AverageAI, AverageAndMaxAI, BasicAO, MaxAO, ...
  • Under vxWorks, one can do so without recompilation:
  ld <smoo_handlerLib.o
  dbLoadDataBase("..../records_that_use_smoo_handler")

Contra

  • The actions inside a user-field handler are limited to quick, synchronous actions.
  • In order to handle the goals mentioned in example.cpp, only two hooks into the record's process() routine are needed. Unclear if this is sufficient in the long run.
  • While easy to use, the code requires more runtime checks, more type casts, more boilerplate code. For example, the smoo_handler should react accordingly if the record does not have a numeric value field. In a custom record type, compile-time checks would reduce the code size and increase maintainabililty.