V4 User-defined fields

From EPICSWIKI
Revision as of 23:44, 4 May 2005 by KayKasemir (talk | contribs) (added)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

This is a suggestion for supporting user-defined fields in V4. 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,interval=1", flnk_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?
  
*/