Skip to content

Commit

Permalink
Make the input arguments and return type statically typed.
Browse files Browse the repository at this point in the history
It turns out the CA protocol doesn't allow changing field types
after it CA_PROTO_EVENT_ADD has subscribed to channel. This limits
the use of previously implemented adapting types. This commit enforces
all input argument types as well as result to be defined by user
through FTx and FTVL, and the record never changes that.
  • Loading branch information
klemenv committed Dec 31, 2021
1 parent 124503e commit a1d42f7
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 59 deletions.
38 changes: 18 additions & 20 deletions pycalcRecord.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,13 @@ with and can be changed vid dbPuts.
Each INPx field has a corresponding FTx field that specifies the type
of the field. Only scalar type are supported as of now: CHAR, UCHAR,
SHORT, USHOR, LONG, ULONG, INT64, UINT64, FLOAT, DOUBLE, STRING.
Setting a corresponding FTx field is required for constant
values, but optional for links as they automatically determine the
scalar type from the connected link. Default value is DOUBLE.
Default value is DOUBLE.

### Output fields
The result of executed Python code is pushed to VAL field every time
record processes unless Python code could not be executed. The returned
type depends on the return type of evaluated Python code and can be
one of the following: LONG, DOUBLE, STRING. The type of return code
from lastly executed Python code can be found in FTVL field.
record processes unless Python code could not be executed. The result
value is converted to the type specified in FTVL which can be one of the
following: LONG, DOUBLE, STRING.

### Python expression
The purpose of this record is to be able to execute arbitrary Python code
Expand Down Expand Up @@ -72,34 +69,35 @@ pycalcRecord.dbd in the IOC as
record(py, "PyCalcTest:MathExpr") {
field(INPA, "17")
field(INPB, "3")
field(CALC, "%A%*%B%")
field(CALC, "A*B")
}
```

Processing this record will result in its value being 51.0. This may come as
a bit of surprise since both parameters are integers. The constants needs the
FTx field populated which defaults to DOUBLE. When the above record executes,
the 2 integer values are converted to double values before passing to Python
code. The result of the expression is DOUBLE value and can be verified with
FTVL field.
code. The result of the expression is DOUBLE value which happens to be default
value for FTVL.

### Adaptive parameter and return value types

```
record(py, "PyCalcTest:AdaptiveTypes") {
field(INPA, "Test:Input1 CP")
field(INPB, "Test:Input2 CP")
field(CALC, "pow(max([%A%, %B%]), 2)")
field(INPA, "PyCalcTest:Input1 CP")
field(INPB, "PyCalcTest:Input2 CP")
field(FTA, "LONG")
field(FTB, "DOUBLE")
field(CALC, "pow(max([A, B]), 2)")
field(FTVL, "LONG")
}
```

When using links (local PVs on same IOC or external ones), their value type
defines the corresponding parameter type. In the above record, both PVs can
be any numerical value and the Python code will calculate a square of the
largest of the 2 parameters and the return value will be LONG if both
parameters are integer values, or DOUBLE otherwise. If particular return
type is required, it can be casted through Python code by using int() or
float() functions surrounding the expression.
The FTx fields define the parameter type regarless of what the link's type
might be. When types don't match, values are converted and precision may be
lost. In the example above, the PyCalcTest:Input1 PV is ai but the pycalc
definition forces conversion to LONG. Similarly, the result of Python code
is a double value, but we're only intersted in integer precision.

### Trigger record alarm

Expand Down
72 changes: 33 additions & 39 deletions src/pycalcRecord.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "callback.h"
#include "cantProceed.h"
#include "dbAccess.h"
#include "dbConvertFast.h"
#include "dbEvent.h"
#include "devSup.h"
#include "epicsVersion.h"
Expand Down Expand Up @@ -156,35 +157,40 @@ static void processRecordCb(pycalcRecord* rec)
}
std::string code = Util::replaceFields(rec->calc, fields);

PyWrapper::MultiTypeValue ret;
long status = 0;
try {
auto out = PyWrapper::exec(code, (rec->tpro == 1));
switch (out.type) {
case PyWrapper::MultiTypeValue::Type::BOOL:
rec->ftvl = DBR_LONG;
*reinterpret_cast<epicsInt32*>(rec->val) = out.b;
break;
case PyWrapper::MultiTypeValue::Type::INTEGER:
rec->ftvl = DBR_LONG;
*reinterpret_cast<epicsInt32*>(rec->val) = out.i;
break;
case PyWrapper::MultiTypeValue::Type::FLOAT:
rec->ftvl = DBR_DOUBLE;
*reinterpret_cast<epicsFloat64*>(rec->val) = out.f;
break;
case PyWrapper::MultiTypeValue::Type::STRING:
rec->ftvl = DBR_STRING;
strncpy(reinterpret_cast<char*>(rec->val), out.s.c_str(), dbValueSize(DBR_STRING));
reinterpret_cast<char*>(rec->val)[dbValueSize(DBR_STRING)-1] = 0;
break;
default:
rec->ftvl = DBR_STRING;
reinterpret_cast<char*>(rec->val)[0] = 0;
break;
}
rec->ctx->processCbStatus = 0;
ret = PyWrapper::exec(code, (rec->tpro == 1));
} catch (...) {
rec->ctx->processCbStatus = -1;
status = -1;
}

if (status == 0) {
typedef long (*convertRoutineCast)(void*, void*, void*);
if (ret.type == PyWrapper::MultiTypeValue::Type::BOOL) {
epicsInt32 l = ret.b;
auto convert = reinterpret_cast<convertRoutineCast>(dbFastPutConvertRoutine[DBF_LONG][rec->ftvl]);
status = convert(&l, rec->val, 0);
} else if (ret.type == PyWrapper::MultiTypeValue::Type::INTEGER) {
auto convert = reinterpret_cast<convertRoutineCast>(dbFastPutConvertRoutine[DBF_LONG][rec->ftvl]);
status = convert(&ret.i, rec->val, 0);
} else if (ret.type == PyWrapper::MultiTypeValue::Type::FLOAT) {
auto convert = reinterpret_cast<convertRoutineCast>(dbFastPutConvertRoutine[DBF_DOUBLE][rec->ftvl]);
status = convert(&ret.f, rec->val, 0);
} else if (ret.type == PyWrapper::MultiTypeValue::Type::STRING) {
char s[MAX_STRING_SIZE];
strncpy(s, ret.s.c_str(), MAX_STRING_SIZE);
s[MAX_STRING_SIZE-1] = 0;
auto convert = reinterpret_cast<convertRoutineCast>(dbFastPutConvertRoutine[DBF_STRING][rec->ftvl]);
status = convert(s, rec->val, 0);
} else {
char s[MAX_STRING_SIZE] = {};
auto convert = reinterpret_cast<convertRoutineCast>(dbFastPutConvertRoutine[DBF_STRING][rec->ftvl]);
status = convert(s, rec->val, 0);
}
}

rec->ctx->processCbStatus = (status == 0 ? 0 : -1);
callbackRequestProcessCallback(&rec->ctx->callback, rec->prio, rec);
}

Expand Down Expand Up @@ -232,23 +238,11 @@ static long fetchValues(pycalcRecord *rec)
auto inp = &rec->inpa + i;
auto ft = &rec->fta + i;
auto val = &rec->a + i;
auto siz = &rec->siza + i;

if (!dbLinkIsConstant(inp) && dbIsLinkConnected(inp)) {
long nElements;
auto ftype = dbGetLinkDBFtype(inp);
auto ret = dbGetNelements(inp, &nElements);
if (ftype >= 0 && ret == 0 && nElements == 1) {
if (ftype == DBF_ENUM) {
ftype = DBF_STRING;
}
if (*siz < dbValueSize(ftype)) {
free(*val);
*siz = dbValueSize(ftype);
*val = callocMustSucceed(1, *siz, "pycalcRecord::initRecord");
}
*ft = ftype;

if (ret == 0 && nElements == 1) {
dbGetLink(inp, *ft, *val, 0, &nElements);
}
}
Expand Down
3 changes: 3 additions & 0 deletions testApp/Db/pycalcrectest.db
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ record(pycalc, "PyCalcTest:MathExpr") {
record(pycalc, "PyCalcTest:AdaptiveTypes") {
field(INPA, "PyCalcTest:Input1 CP")
field(INPB, "PyCalcTest:Input2 CP")
field(FTA, "LONG")
field(FTB, "DOUBLE")
field(CALC, "pow(max([A, B]), 2)")
field(FTVL, "LONG")
}
record(pycalc, "PyCalcTest:InvalidAlarm") {
field(CALC, "unknown_function()")
Expand Down

0 comments on commit a1d42f7

Please sign in to comment.