diff --git a/documentation/RELEASE_NOTES.md b/documentation/RELEASE_NOTES.md index 870be17a4..9dd64cbf7 100644 --- a/documentation/RELEASE_NOTES.md +++ b/documentation/RELEASE_NOTES.md @@ -16,6 +16,13 @@ should also be read to understand what has changed since earlier releases. ## Changes made on the 7.0 branch since 7.0.7 +### Extend longout conditions to write the OUT link (OOPT field) + +The longout record has now the capacity to condition its output write operation to +different options, using the OOPT field (similar to calcout record). This is +the first output record to have such feature as a result of the +[Make output records only write on change](https://bugs.launchpad.net/epics-base/+bug/1398215) +issue on Launchpad. ### Tab completion for IOC shell diff --git a/modules/database/src/std/rec/longoutRecord.c b/modules/database/src/std/rec/longoutRecord.c index faab03083..2e608cdfb 100644 --- a/modules/database/src/std/rec/longoutRecord.c +++ b/modules/database/src/std/rec/longoutRecord.c @@ -81,10 +81,14 @@ rset longoutRSET={ }; epicsExportAddress(rset,longoutRSET); +#define DONT_EXEC_OUTPUT 0 +#define EXEC_OUTPUT 1 + static void checkAlarms(longoutRecord *prec); static void monitor(longoutRecord *prec); static long writeValue(longoutRecord *prec); static void convert(longoutRecord *prec, epicsInt32 value); +static long conditional_write(longoutRecord *prec); static long init_record(struct dbCommon *pcommon, int pass) { @@ -119,6 +123,9 @@ static long init_record(struct dbCommon *pcommon, int pass) prec->mlst = prec->val; prec->alst = prec->val; prec->lalm = prec->val; + prec->pval = prec->val; + prec->outpvt = EXEC_OUTPUT; + return 0; } @@ -210,6 +217,14 @@ static long special(DBADDR *paddr, int after) recGblCheckSimm((dbCommon *)prec, &prec->sscn, prec->oldsimm, prec->simm); return(0); } + + /* Detect an output link re-direction (change) */ + if (dbGetFieldIndex(paddr) == longoutRecordOUT) { + if ((after) && (prec->ooch == menuYesNoYES)) + prec->outpvt = EXEC_OUTPUT; + return(0); + } + default: recGblDbaddrError(S_db_badChoice, paddr, "longout: special"); return(S_db_badChoice); @@ -391,7 +406,7 @@ static long writeValue(longoutRecord *prec) switch (prec->simm) { case menuYesNoNO: - status = pdset->write_longout(prec); + status = conditional_write(prec); break; case menuYesNoYES: { @@ -421,10 +436,61 @@ static long writeValue(longoutRecord *prec) static void convert(longoutRecord *prec, epicsInt32 value) { - /* check drive limits */ + /* check drive limits */ if(prec->drvh > prec->drvl) { if (value > prec->drvh) value = prec->drvh; else if (value < prec->drvl) value = prec->drvl; } prec->val = value; } + +/* Evaluate OOPT field to perform the write operation */ +static long conditional_write(longoutRecord *prec) +{ + struct longoutdset *pdset = (struct longoutdset *) prec->dset; + long status = 0; + int doDevSupWrite = 0; + + switch (prec->oopt) + { + case longoutOOPT_On_Change: + /* Forces a write op if a change in the OUT field is detected OR is first process */ + if (prec->outpvt == EXEC_OUTPUT) { + doDevSupWrite = 1; + } else { + /* Only write if value is different from the previous one */ + doDevSupWrite = (prec->val != prec->pval); + } + break; + + case longoutOOPT_Every_Time: + doDevSupWrite = 1; + break; + + case longoutOOPT_When_Zero: + doDevSupWrite = (prec->val == 0); + break; + + case longoutOOPT_When_Non_zero: + doDevSupWrite = (prec->val != 0); + break; + + case longoutOOPT_Transition_To_Zero: + doDevSupWrite = ((prec->val == 0)&&(prec->pval != 0)); + break; + + case longoutOOPT_Transition_To_Non_zero: + doDevSupWrite = ((prec->val != 0)&&(prec->pval == 0)); + break; + + default: + break; + } + + if (doDevSupWrite) + status = pdset->write_longout(prec); + + prec->pval = prec->val; + prec->outpvt = DONT_EXEC_OUTPUT; /* reset status */ + return status; +} diff --git a/modules/database/src/std/rec/longoutRecord.dbd.pod b/modules/database/src/std/rec/longoutRecord.dbd.pod index f3738f620..4ba2c621b 100644 --- a/modules/database/src/std/rec/longoutRecord.dbd.pod +++ b/modules/database/src/std/rec/longoutRecord.dbd.pod @@ -20,6 +20,15 @@ limits. =cut +menu(longoutOOPT) { + choice(longoutOOPT_Every_Time,"Every Time") + choice(longoutOOPT_On_Change,"On Change") + choice(longoutOOPT_When_Zero,"When Zero") + choice(longoutOOPT_When_Non_zero,"When Non-zero") + choice(longoutOOPT_Transition_To_Zero,"Transition To Zero") + choice(longoutOOPT_Transition_To_Non_zero,"Transition To Non-zero") +} + recordtype(longout) { =head2 Parameter Fields @@ -72,7 +81,46 @@ See L
for information on the format of hardware addresses and database links. -=fields OUT, DTYP +=fields OUT, DTYP, OOPT, OOCH + +=head4 Menu longoutOOPT + +The OOPT field determines the condition that causes the output link to be +written to. It's a menu field that has six choices: + +=menu longoutOOPT + +=over + +=item * +C -- write output every time record is processed. (DEFAULT) + +=item * +C -- write output every time VAL changes. + +=item * +C -- when record is processed, write output if VAL is zero. + +=item * +C -- when record is processed, write output if VAL is +non-zero. + +=item * +C -- when record is processed, write output only if VAL +is zero and the last value was non-zero. + +=item * +C -- when record is processed, write output only if +VAL is non-zero and last value was zero. + +=back + +=head4 Changes in OUT field when OOPT = On Change + +If OOCH is C (its default value) and the OOPT field is C, +the record will write to the device support the first time the record gets +processed after its OUT link is modified, even when the output value has +not actually changed. =cut @@ -94,6 +142,7 @@ for information on the format of hardware addresses and database links. } field(OUT,DBF_OUTLINK) { prompt("Output Specification") + special(SPC_MOD) promptgroup("50 - Output") interest(1) } @@ -365,7 +414,7 @@ for more information on simulation mode and its fields. prompt("Sim. Mode Private") special(SPC_NOMOD) interest(4) - extra("epicsCallback *simpvt") + extra("epicsCallback *simpvt") } field(IVOA,DBF_MENU) { prompt("INVALID output action") @@ -378,6 +427,29 @@ for more information on simulation mode and its fields. promptgroup("50 - Output") interest(2) } + field(PVAL,DBF_LONG) { + prompt("Previous Value") + } + field(OUTPVT,DBF_NOACCESS) { + prompt("Output Write Control Private") + special(SPC_NOMOD) + interest(4) + extra("epicsEnum16 outpvt") + } + field(OOCH,DBF_MENU) { + prompt("Output Exec. On Change (Opt)") + promptgroup("50 - Output") + interest(1) + menu(menuYesNo) + initial("1") + } + field(OOPT,DBF_MENU) { + prompt("Output Execute Opt") + promptgroup("50 - Output") + interest(1) + menu(longoutOOPT) + initial("0") + } =begin html diff --git a/modules/database/test/std/rec/Makefile b/modules/database/test/std/rec/Makefile index dc87d23f2..effbbd1cc 100644 --- a/modules/database/test/std/rec/Makefile +++ b/modules/database/test/std/rec/Makefile @@ -86,6 +86,13 @@ testHarness_SRCS += seqTest.c TESTFILES += ../seqTest.db TESTS += seqTest +TESTPROD_HOST += longoutTest +longoutTest_SRCS += longoutTest.c +longoutTest_SRCS += recTestIoc_registerRecordDeviceDriver.cpp +testHarness_SRCS += longoutTest.c +TESTFILES += ../longoutTest.db +TESTS += longoutTest + TARGETS += $(COMMON_DIR)/asTestIoc.dbd DBDDEPENDS_FILES += asTestIoc.dbd$(DEP) asTestIoc_DBD += base.dbd diff --git a/modules/database/test/std/rec/longoutTest.c b/modules/database/test/std/rec/longoutTest.c new file mode 100644 index 000000000..35f161341 --- /dev/null +++ b/modules/database/test/std/rec/longoutTest.c @@ -0,0 +1,270 @@ +/*************************************************************************\ +* Copyright (c) 2020 Joao Paulo Martins +* EPICS BASE is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ + +#include "dbUnitTest.h" +#include "testMain.h" +#include "dbLock.h" +#include "errlog.h" +#include "dbAccess.h" +#include "epicsMath.h" +#include "menuYesNo.h" + +#include "longoutRecord.h" + +void recTestIoc_registerRecordDeviceDriver(struct dbBase *); + +static void test_oopt_everytime(void){ + /* reset rec processing counter_a */ + testdbPutFieldOk("counter_a.VAL", DBF_DOUBLE, 0.0); + + /* write the same value two times */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + + /* write two times with different values*/ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 17); + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 18); + + /* Test if the counter_a was processed 4 times */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 4.0); + + // number of tests = 6 +} + +static void test_oopt_onchange(void){ + /* change OOPT to On Change */ + testdbPutFieldOk("longout_rec.OOPT", DBF_ENUM, longoutOOPT_On_Change); + + /* reset rec processing counter_a */ + testdbPutFieldOk("counter_a.VAL", DBF_DOUBLE, 0.0); + + /* write the same value two times */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + + /* Test if the counter_a was processed only once */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 1.0); + + /* write two times with different values*/ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 17); + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 18); + + /* Test if the counter_a was processed 1 + 2 times */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 3.0); + + //number of tests 8 +} + +static void test_oopt_whenzero(void){ + testdbPutFieldOk("longout_rec.OOPT", DBF_ENUM, longoutOOPT_When_Zero); + + /* reset rec processing counter_a */ + testdbPutFieldOk("counter_a.VAL", DBF_DOUBLE, 0.0); + + /* write zero two times */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 0); + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 0); + + /* Test if the counter_a was processed twice */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 2.0); + + /* write two times with non-zero values*/ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 17); + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 18); + + /* Test if the counter_a was still processed 2 times */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 2.0); + + //number of tests 8 +} + +static void test_oopt_whennonzero(void){ + testdbPutFieldOk("longout_rec.OOPT", DBF_ENUM, longoutOOPT_When_Non_zero); + + /* reset rec processing counter_a */ + testdbPutFieldOk("counter_a.VAL", DBF_DOUBLE, 0.0); + + /* write zero two times */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 0); + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 0); + + /* Test if the counter_a was never processed */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 0.0); + + /* write two times with non-zero values*/ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 17); + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 18); + + /* Test if the counter_a was still processed 2 times */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 2.0); + + //number of tests 8 +} + +static void test_oopt_when_transition_zero(void){ + testdbPutFieldOk("longout_rec.OOPT", DBF_ENUM, longoutOOPT_Transition_To_Zero); + + /* reset rec processing counter_a */ + testdbPutFieldOk("counter_a.VAL", DBF_DOUBLE, 0.0); + + /* write non-zero then zero */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 0); + + /* Test if the counter_a was processed */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 1.0); + + /* write another transition to zero */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 17); + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 0); + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 17); + + /* Test if the counter_a was processed once more */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 2.0); + + //number of tests 9 +} + +static void test_oopt_when_transition_nonzero(void){ + testdbPutFieldOk("longout_rec.OOPT", DBF_ENUM, longoutOOPT_Transition_To_Non_zero); + + /* write non-zero to start fresh */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + + /* reset rec processing counter_a */ + testdbPutFieldOk("counter_a.VAL", DBF_DOUBLE, 0.0); + + /* write non-zero then zero */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 17); + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 0); + + /* Test if the counter_a was never processed */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 0.0); + + /* write a transition to non-zero */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 18); + + /* Test if the counter_a was processed */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 1.0); + + //number of tests 8 +} + +static void test_changing_out_field(void){ + /* change OOPT to On Change */ + testdbPutFieldOk("longout_rec.OOPT", DBF_ENUM, longoutOOPT_On_Change); + + /* write an initial value */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + + /* reset rec processing counters */ + testdbPutFieldOk("counter_a.VAL", DBF_DOUBLE, 0.0); + testdbPutFieldOk("counter_b.VAL", DBF_DOUBLE, 0.0); + + /* write the same value */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + + /* Test if the counter was never processed */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 0.0); + + /* change the OUT link to another counter */ + testdbPutFieldOk("longout_rec.OUT", DBF_STRING, "counter_b.B PP"); + + /* write the same value */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + + /* Test if the counter was processed once */ + testdbGetFieldEqual("counter_b", DBF_DOUBLE, 1.0); + + /* write the same value */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + + /* Test if the counter was not processed again */ + testdbGetFieldEqual("counter_b", DBF_DOUBLE, 1.0); + + /* Set option to write ON CHANGE even when the OUT link was changed */ + testdbPutFieldOk("longout_rec.OOCH", DBF_ENUM, menuYesNoNO); + + /* write an initial value */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + + /* reset rec processing counters */ + testdbPutFieldOk("counter_a.VAL", DBF_DOUBLE, 0.0); + testdbPutFieldOk("counter_b.VAL", DBF_DOUBLE, 0.0); + + /* write the same value */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + + /* Test if the counter_b was never processed */ + testdbGetFieldEqual("counter_b", DBF_DOUBLE, 0.0); + + /* change back the OUT link to counter_a */ + testdbPutFieldOk("longout_rec.OUT", DBF_STRING, "counter_a.B PP"); + + /* write the same value */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + + /* Test if the counter was never processed */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 0.0); + + /* write the same value */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 16); + + /* Test if the counter was not processed again */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 0.0); + + /* write new value */ + testdbPutFieldOk("longout_rec.VAL", DBF_LONG, 17); + + /* Test if the counter was processed once */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 1.0); + + /* reset rec processing counters */ + testdbPutFieldOk("counter_a.VAL", DBF_DOUBLE, 0.0); + + /* test if record with OOPT == On Change will + write to output at its first process */ + testdbPutFieldOk("longout_rec2.VAL", DBF_LONG, 16); + + /* Test if the counter was processed once */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 1.0); + + /* write the same value */ + testdbPutFieldOk("longout_rec2.VAL", DBF_LONG, 16); + + /* Test if the counter was not processed again */ + testdbGetFieldEqual("counter_a", DBF_DOUBLE, 1.0); + + //number of tests 29 +} + +MAIN(longoutTest) { + + testPlan(6+8+8+8+9+8+29); + + testdbPrepare(); + testdbReadDatabase("recTestIoc.dbd", NULL, NULL); + recTestIoc_registerRecordDeviceDriver(pdbbase); + + testdbReadDatabase("longoutTest.db", NULL, NULL); + + eltc(0); + testIocInitOk(); + eltc(1); + + test_oopt_everytime(); + test_oopt_onchange(); + test_oopt_whenzero(); + test_oopt_whennonzero(); + test_oopt_when_transition_zero(); + test_oopt_when_transition_nonzero(); + test_changing_out_field(); + + testIocShutdownOk(); + testdbCleanup(); + + return testDone(); +} diff --git a/modules/database/test/std/rec/longoutTest.db b/modules/database/test/std/rec/longoutTest.db new file mode 100644 index 000000000..6b41b9883 --- /dev/null +++ b/modules/database/test/std/rec/longoutTest.db @@ -0,0 +1,25 @@ +record(calc, "counter_a") { + field(INPA, "counter_a") + field(CALC, "A+1") + field(SCAN, "Passive") +} + +record(calc, "counter_b") { + field(INPA, "counter_b") + field(CALC, "A+1") + field(SCAN, "Passive") +} + +record(longout, "longout_rec") { + field(VAL, "0") + field(OUT, "counter_a.B PP") + field(PINI, "YES") +} + +record(longout, "longout_rec2") { + field(VAL, "16") + field(OUT, "counter_a.B PP") + field(PINI, "NO") + field(OOPT, "On Change") +} +