diff --git a/documentation/new-notes/PR-817.md b/documentation/new-notes/PR-817.md new file mode 100644 index 000000000..3aa1370a1 --- /dev/null +++ b/documentation/new-notes/PR-817.md @@ -0,0 +1,13 @@ +### AFTC for bi record + +The bi record now also supports the AFTC parameter. +The AFTC field sets the time constant on a low-pass filter that delays the +reporting of limit alarms until the signal has been within the alarm range for +that number of seconds (the default AFTC value of zero retains the previous +behavior). One has to make sure that the bi record is processed at a reasonably fast rate +when using AFTC, so that the record can respond to changes in the input signal +within the time constant. + +### AFTC and LALM fix for mbbi record + +Fixed two issues in the mbbi record where the AFTC and LALM fields were not operating as expected. diff --git a/modules/database/src/std/rec/biRecord.c b/modules/database/src/std/rec/biRecord.c index 941ac563b..e8bae6e86 100644 --- a/modules/database/src/std/rec/biRecord.c +++ b/modules/database/src/std/rec/biRecord.c @@ -23,6 +23,7 @@ #include "dbDefs.h" #include "epicsPrint.h" +#include "epicsMath.h" #include "alarm.h" #include "callback.h" #include "dbAccess.h" @@ -40,6 +41,9 @@ #undef GEN_SIZE_OFFSET #include "epicsExport.h" +/* Hysteresis for alarm filtering: 1-1/e */ +#define THRESHOLD 0.6321 + /* Create RSET - Record Support Entry Table*/ #define report NULL #define initialize NULL @@ -80,7 +84,7 @@ rset biRSET={ }; epicsExportAddress(rset,biRSET); -static void checkAlarms(biRecord *); +static void checkAlarms(biRecord *, epicsTimeStamp *); static void monitor(biRecord *); static long readValue(biRecord *); @@ -119,6 +123,7 @@ static long process(struct dbCommon *pcommon) bidset *pdset = (bidset *)(prec->dset); long status; unsigned char pact=prec->pact; + epicsTimeStamp timeLast; if( (pdset==NULL) || (pdset->read_bi==NULL) ) { prec->pact=TRUE; @@ -126,6 +131,8 @@ static long process(struct dbCommon *pcommon) return(S_dev_missingSup); } + timeLast = prec->time; + status=readValue(prec); /* read the new value */ /* check if device support set pact */ if ( !pact && prec->pact ) return(0); @@ -140,7 +147,7 @@ static long process(struct dbCommon *pcommon) } else if(status==2) status=0; /* check for alarms */ - checkAlarms(prec); + checkAlarms(prec, &timeLast); /* check event list */ monitor(prec); /* process the forward scan link record */ @@ -217,27 +224,52 @@ static long put_enum_str(const DBADDR *paddr, const char *pstring) } -static void checkAlarms(biRecord *prec) +static void checkAlarms(biRecord *prec, epicsTimeStamp *timeLast) { + epicsEnum16 asev; unsigned short val = prec->val; - if(prec->udf == TRUE){ recGblSetSevr(prec,UDF_ALARM,prec->udfs); return; } if(val>1)return; - /* check for state alarm */ - if (val == 0){ - recGblSetSevr(prec,STATE_ALARM,prec->zsv); - }else{ - recGblSetSevr(prec,STATE_ALARM,prec->osv); + + if (val == 0) { + asev = prec->zsv; + } else { + asev = prec->osv; } - /* check for cos alarm */ + unsigned short alarm = asev; /* Initialize alarm with current state severity */ + double aftc = prec->aftc; + double afvl = 0; + + if (aftc > 0) { + afvl = prec->afvl; + if (afvl == 0) { + afvl = (double) alarm; + } else { + double t = epicsTimeDiffInSeconds(&prec->time, timeLast); + double alpha = aftc / (t + aftc); + + afvl = alpha * afvl + + ((afvl > 0) ? (1.0 - alpha) : (alpha - 1.0)) * alarm; + if (afvl - floor(afvl) > THRESHOLD) + afvl = -afvl; + + alarm = abs((int)floor(afvl)); + } + } + prec->afvl = afvl; + + asev = alarm; + recGblSetSevr(prec,STATE_ALARM,asev); + + /* check for cos alarm */ if(val == prec->lalm) return; - recGblSetSevr(prec,COS_ALARM,prec->cosv); + recGblSetSevr(prec,COS_ALARM,prec->cosv); prec->lalm = val; return; } diff --git a/modules/database/src/std/rec/biRecord.dbd.pod b/modules/database/src/std/rec/biRecord.dbd.pod index 2b772b5e3..8db4634a3 100644 --- a/modules/database/src/std/rec/biRecord.dbd.pod +++ b/modules/database/src/std/rec/biRecord.dbd.pod @@ -107,13 +107,20 @@ C. The ZSV field holds the severity for the zero state; OSV, for the one state. COSV causes an alarm whenever the state changes between 0 and 1 and the severity is configured as MINOR or MAJOR. +Since UNRELEASED, the alarm filter time constant (AFTC) is available for the bi record. +The AFTC field sets the time constant on a low-pass filter that delays the +reporting of limit alarms until the signal has been within the alarm range for +that number of seconds (the default AFTC value of zero retains the previous +behavior). Be sure that the bi record is processed at a reasonably fast rate +when using AFTC, so that the record can respond to changes in the input signal +within the time constant. + See L for a complete explanation of record alarms and of the standard fields. L lists other fields related to alarms that are common to all record types. -=fields ZSV, OSV, COSV - +=fields ZSV, OSV, COSV, AFTC =head3 Run-time Parameters These parameters are used by the run-time code for processing the binary @@ -220,6 +227,16 @@ is not equal to VAL. special(SPC_NOMOD) interest(3) } + field(AFTC, DBF_DOUBLE) { + prompt("Alarm Filter Time Constant") + promptgroup("70 - Alarm") + interest(1) + } + field(AFVL, DBF_DOUBLE) { + prompt("Alarm Filter Value") + special(SPC_NOMOD) + interest(3) + } =head3 Simulation Mode Parameters diff --git a/modules/database/src/std/rec/mbbiRecord.c b/modules/database/src/std/rec/mbbiRecord.c index 018c861c1..141c6692d 100644 --- a/modules/database/src/std/rec/mbbiRecord.c +++ b/modules/database/src/std/rec/mbbiRecord.c @@ -336,15 +336,15 @@ static void checkAlarms(mbbiRecord *prec, epicsTimeStamp *timeLast) alarm = abs((int)floor(afvl)); } } + prec->afvl = afvl; asev = alarm; recGblSetSevr(prec, STATE_ALARM, asev); /* Check for COS alarm */ - if (val == prec->lalm || - recGblSetSevr(prec, COS_ALARM, prec->cosv)) + if (val == prec->lalm) return; - + recGblSetSevr(prec, COS_ALARM, prec->cosv); prec->lalm = val; } diff --git a/modules/database/test/std/rec/Makefile b/modules/database/test/std/rec/Makefile index c5eca0dbe..9debe231d 100644 --- a/modules/database/test/std/rec/Makefile +++ b/modules/database/test/std/rec/Makefile @@ -195,6 +195,13 @@ testHarness_SRCS += simmTest_registerRecordDeviceDriver.cpp TESTFILES += $(COMMON_DIR)/simmTest.dbd $(COMMON_DIR)/simmTest.db TESTS += simmTest +TESTPROD_HOST += mbbiTest +mbbiTest_SRCS += mbbiTest.c +mbbiTest_SRCS += recTestIoc_registerRecordDeviceDriver.cpp +testHarness_SRCS += mbbiTest.c +TESTFILES += ../mbbiTest.db +TESTS += mbbiTest + TESTPROD_HOST += mbbioDirectTest mbbioDirectTest_SRCS += mbbioDirectTest.c mbbioDirectTest_SRCS += recTestIoc_registerRecordDeviceDriver.cpp diff --git a/modules/database/test/std/rec/biTest.c b/modules/database/test/std/rec/biTest.c index ff7c975ce..c4861e6f6 100644 --- a/modules/database/test/std/rec/biTest.c +++ b/modules/database/test/std/rec/biTest.c @@ -9,6 +9,7 @@ #include "errlog.h" #include "dbAccess.h" #include "menuAlarmSevr.h" +#include "menuScan.h" void recTestIoc_registerRecordDeviceDriver(struct dbBase *); @@ -96,9 +97,71 @@ static void test_alarm(void){ // number of tests = 13 } +static void test_aftc(void){ + const double aftc = 3.0; + testMonitor* test_mon = NULL; + epicsTimeStamp startTime; + epicsTimeStamp endTime; + double diffTime = 0; + + /* set soft channel */ + testdbPutFieldOk("test_bi_rec2.DTYP", DBF_STRING, "Soft Channel"); + testdbPutFieldOk("test_bi_rec2.INP", DBF_STRING, "test_bi_link_rec2.VAL"); + testdbPutFieldOk("test_bi_link_rec2.FLNK", DBF_STRING, "test_bi_rec2"); + + /* set alarm parameters */ + testdbPutFieldOk("test_bi_rec2.ZSV", DBF_SHORT, menuAlarmSevrMINOR); + testdbPutFieldOk("test_bi_rec2.OSV", DBF_SHORT, menuAlarmSevrMAJOR); + + /* set start VAL to FALSE (NO_ALARM) */ + testdbPutFieldOk("test_bi_link_rec2.VAL", DBF_SHORT, FALSE); + + /* test AFTC using a monitor and time stamps */ + testdbPutFieldOk("test_bi_rec2.AFTC", DBF_DOUBLE, aftc); + testdbPutFieldOk("test_bi_rec2.SCAN", DBF_SHORT, menuScan_1_second); + + /* set VAL to TRUE (MAJOR alarm) */ + testdbPutFieldOk("test_bi_link_rec2.VAL", DBF_SHORT, TRUE); + + /* Create test monitor for alarm SEVR */ + test_mon = testMonitorCreate("test_bi_rec2.VAL", DBE_ALARM, 0); + + /* Get start time */ + epicsTimeGetCurrent(&startTime); + + /* wait for monitor to trigger on the new alarm status */ + testMonitorWait(test_mon); + epicsTimeGetCurrent(&endTime); + + /* Verify that alarm status is now MAJOR */ + testdbGetFieldEqual("test_bi_rec2.SEVR", DBF_SHORT, menuAlarmSevrMAJOR); + + /* set VAL back to FALSE (NO_ALARM) */ + testdbPutFieldOk("test_bi_link_rec2.VAL", DBF_SHORT, FALSE); + + /* Create test monitor for alarm SEVR */ + test_mon = testMonitorCreate("test_bi_rec2.VAL", DBE_ALARM, 0); + + /* Get start time */ + epicsTimeGetCurrent(&startTime); + + /* wait for monitor to trigger on the new alarm status */ + testMonitorWait(test_mon); + epicsTimeGetCurrent(&endTime); + + /* Verify that alarm status is now NO_ALARM */ + testdbGetFieldEqual("test_bi_rec2.SEVR", DBF_SHORT, menuAlarmSevrMINOR); + + /* Verify that time is at least equal to configured aftc */ + diffTime = epicsTimeDiffInSeconds(&endTime, &startTime); + testOk(diffTime >= aftc, "AFTC time %lf", diffTime); + + // number of tests = 13 +} + MAIN(biTest) { - testPlan(6+6+11+13); + testPlan(6+6+11+13+13); testdbPrepare(); testdbReadDatabase("recTestIoc.dbd", NULL, NULL); @@ -114,6 +177,7 @@ MAIN(biTest) { test_raw_soft_input(); test_operator_display(); test_alarm(); + test_aftc(); testIocShutdownOk(); testdbCleanup(); diff --git a/modules/database/test/std/rec/biTest.db b/modules/database/test/std/rec/biTest.db index 4304cce91..4877c90d2 100644 --- a/modules/database/test/std/rec/biTest.db +++ b/modules/database/test/std/rec/biTest.db @@ -4,3 +4,10 @@ record(bi, "test_bi_rec") { record(bo, "test_bi_link_rec") { } + +record(bi, "test_bi_rec2") { +} + +record(bo, "test_bi_link_rec2") { + +} diff --git a/modules/database/test/std/rec/mbbiTest.c b/modules/database/test/std/rec/mbbiTest.c new file mode 100644 index 000000000..6f5889737 --- /dev/null +++ b/modules/database/test/std/rec/mbbiTest.c @@ -0,0 +1,201 @@ +/*************************************************************************\ +* Copyright (c) 2026 Edmund Blomley +* 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 "errlog.h" +#include "dbAccess.h" +#include "menuAlarmSevr.h" +#include "menuScan.h" + +void recTestIoc_registerRecordDeviceDriver(struct dbBase *); + +static void test_soft_input(void){ + /* set soft channel */ + testdbPutFieldOk("test_mbbi_rec.DTYP", DBF_STRING, "Soft Channel"); + testdbPutFieldOk("test_mbbi_rec.INP", DBF_STRING, "test_mbbi_link_rec.VAL"); + testdbPutFieldOk("test_mbbi_link_rec.FLNK", DBF_STRING, "test_mbbi_rec"); + + /* set VAL to on linked record */ + testdbPutFieldOk("test_mbbi_link_rec.VAL", DBF_SHORT, 1); + + /* verify that this record VAL is updated but RVAL is not */ + testdbGetFieldEqual("test_mbbi_rec.VAL", DBF_SHORT, 1); + testdbGetFieldEqual("test_mbbi_rec.RVAL", DBF_SHORT, 0); + + // number of tests = 6 +} + +static void test_operator_display(void){ + /* set operator display parameters */ + testdbPutFieldOk("test_mbbi_rec.ZRST", DBF_STRING, "ZRST_TEST"); + testdbPutFieldOk("test_mbbi_rec.ONST", DBF_STRING, "ONST_TEST"); + testdbPutFieldOk("test_mbbi_rec.TWST", DBF_STRING, "TWST_TEST"); + testdbPutFieldOk("test_mbbi_rec.THST", DBF_STRING, "THST_TEST"); + testdbPutFieldOk("test_mbbi_rec.FRST", DBF_STRING, "FRST_TEST"); + testdbPutFieldOk("test_mbbi_rec.FVST", DBF_STRING, "FVST_TEST"); + testdbPutFieldOk("test_mbbi_rec.SXST", DBF_STRING, "SXST_TEST"); + testdbPutFieldOk("test_mbbi_rec.SVST", DBF_STRING, "SVST_TEST"); + testdbPutFieldOk("test_mbbi_rec.EIST", DBF_STRING, "EIST_TEST"); + testdbPutFieldOk("test_mbbi_rec.NIST", DBF_STRING, "NIST_TEST"); + testdbPutFieldOk("test_mbbi_rec.TEST", DBF_STRING, "TEST_TEST"); + testdbPutFieldOk("test_mbbi_rec.ELST", DBF_STRING, "ELST_TEST"); + testdbPutFieldOk("test_mbbi_rec.TVST", DBF_STRING, "TVST_TEST"); + testdbPutFieldOk("test_mbbi_rec.TTST", DBF_STRING, "TTST_TEST"); + testdbPutFieldOk("test_mbbi_rec.FTST", DBF_STRING, "FTST_TEST"); + testdbPutFieldOk("test_mbbi_rec.FFST", DBF_STRING, "FFST_TEST"); + testdbPutFieldOk("test_mbbi_rec.DESC", DBF_STRING, "DESC_TEST"); + + /* verify operator display parameters */ + testdbGetFieldEqual("test_mbbi_rec.ZRST", DBF_STRING, "ZRST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.ONST", DBF_STRING, "ONST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.TWST", DBF_STRING, "TWST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.THST", DBF_STRING, "THST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.FRST", DBF_STRING, "FRST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.FVST", DBF_STRING, "FVST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.SXST", DBF_STRING, "SXST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.SVST", DBF_STRING, "SVST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.EIST", DBF_STRING, "EIST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.NIST", DBF_STRING, "NIST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.TEST", DBF_STRING, "TEST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.ELST", DBF_STRING, "ELST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.TVST", DBF_STRING, "TVST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.TTST", DBF_STRING, "TTST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.FTST", DBF_STRING, "FTST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.FFST", DBF_STRING, "FFST_TEST"); + testdbGetFieldEqual("test_mbbi_rec.NAME", DBF_STRING, "test_mbbi_rec"); + testdbGetFieldEqual("test_mbbi_rec.DESC", DBF_STRING, "DESC_TEST"); + + /* verify conversion */ + testdbPutFieldOk("test_mbbi_link_rec.VAL", DBF_SHORT, 1); + testdbGetFieldEqual("test_mbbi_rec.VAL", DBF_STRING, "ONST_TEST"); + testdbPutFieldOk("test_mbbi_link_rec.VAL", DBF_SHORT, 0); + testdbGetFieldEqual("test_mbbi_rec.VAL", DBF_STRING, "ZRST_TEST"); + + // number of tests = 39 +} + +static void test_alarm(void){ + /* set soft channel */ + testdbPutFieldOk("test_mbbi_rec.DTYP", DBF_STRING, "Soft Channel"); + testdbPutFieldOk("test_mbbi_rec.INP", DBF_STRING, "test_mbbi_link_rec.VAL"); + testdbPutFieldOk("test_mbbi_link_rec.FLNK", DBF_STRING, "test_mbbi_rec"); + testdbPutFieldOk("test_mbbi_rec.ZRST", DBF_STRING, "ZRST_TEST"); + testdbPutFieldOk("test_mbbi_rec.ONST", DBF_STRING, "ONST_TEST"); + testdbPutFieldOk("test_mbbi_rec.ZRVL", DBF_STRING, "0"); + testdbPutFieldOk("test_mbbi_rec.ONVL", DBF_STRING, "1"); + + /* set start VAL to 0*/ + testdbPutFieldOk("test_mbbi_link_rec.VAL", DBF_SHORT, 0); + + /* set alarm parameters */ + testdbPutFieldOk("test_mbbi_rec.ZRSV", DBF_SHORT, menuAlarmSevrNO_ALARM); + testdbPutFieldOk("test_mbbi_rec.ONSV", DBF_SHORT, menuAlarmSevrMINOR); + testdbPutFieldOk("test_mbbi_rec.COSV", DBF_SHORT, menuAlarmSevrINVALID); + + /* verify alarm status is NO_ALARM*/ + testdbGetFieldEqual("test_mbbi_rec.SEVR", DBF_SHORT, menuAlarmSevrNO_ALARM); + + /* set ZRSV to MAJOR and verify that SEVR is now MAJOR */ + testdbPutFieldOk("test_mbbi_rec.ZRSV", DBF_SHORT, menuAlarmSevrMAJOR); + testdbGetFieldEqual("test_mbbi_rec.SEVR", DBF_SHORT, menuAlarmSevrMAJOR); + + /* set VAL to 1 on linked record and verify that COSV now sets the SEVR to INVALID */ + testdbPutFieldOk("test_mbbi_link_rec.VAL", DBF_SHORT, 1); + testdbGetFieldEqual("test_mbbi_rec.SEVR", DBF_SHORT, menuAlarmSevrINVALID); + + /* verify LAML */ + testdbGetFieldEqual("test_mbbi_rec.LALM", DBF_SHORT, 1); + + // number of tests = 17 +} + +static void test_aftc(void){ + const double aftc = 3.0; + testMonitor* test_mon = NULL; + epicsTimeStamp startTime; + epicsTimeStamp endTime; + double diffTime = 0; + + /* set soft channel */ + testdbPutFieldOk("test_mbbi_rec2.DTYP", DBF_STRING, "Soft Channel"); + testdbPutFieldOk("test_mbbi_rec2.INP", DBF_STRING, "test_mbbi_link_rec2.VAL"); + testdbPutFieldOk("test_mbbi_link_rec2.FLNK", DBF_STRING, "test_mbbi_rec2"); + + /* set alarm parameters */ + testdbPutFieldOk("test_mbbi_rec2.ZRSV", DBF_SHORT, menuAlarmSevrMINOR); + testdbPutFieldOk("test_mbbi_rec2.ONSV", DBF_SHORT, menuAlarmSevrMAJOR); + + /* set start VAL to FALSE (NO_ALARM) */ + testdbPutFieldOk("test_mbbi_link_rec2.VAL", DBF_SHORT, FALSE); + + /* test AFTC using a monitor and time stamps */ + testdbPutFieldOk("test_mbbi_rec2.AFTC", DBF_DOUBLE, aftc); + testdbPutFieldOk("test_mbbi_rec2.SCAN", DBF_SHORT, menuScan_1_second); + + /* set VAL to TRUE (MAJOR alarm) */ + testdbPutFieldOk("test_mbbi_link_rec2.VAL", DBF_SHORT, TRUE); + + /* Create test monitor for alarm SEVR */ + test_mon = testMonitorCreate("test_mbbi_rec2.VAL", DBE_ALARM, 0); + + /* Get start time */ + epicsTimeGetCurrent(&startTime); + + /* wait for monitor to trigger on the new alarm status */ + testMonitorWait(test_mon); + epicsTimeGetCurrent(&endTime); + + /* Verify that alarm status is now MAJOR */ + testdbGetFieldEqual("test_mbbi_rec2.SEVR", DBF_SHORT, menuAlarmSevrMAJOR); + + /* set VAL back to FALSE (NO_ALARM) */ + testdbPutFieldOk("test_mbbi_link_rec2.VAL", DBF_SHORT, FALSE); + + /* Create test monitor for alarm SEVR */ + test_mon = testMonitorCreate("test_mbbi_rec2.VAL", DBE_ALARM, 0); + + /* Get start time */ + epicsTimeGetCurrent(&startTime); + + /* wait for monitor to trigger on the new alarm status */ + testMonitorWait(test_mon); + epicsTimeGetCurrent(&endTime); + + /* Verify that alarm status is now NO_ALARM */ + testdbGetFieldEqual("test_mbbi_rec2.SEVR", DBF_SHORT, menuAlarmSevrMINOR); + + /* Verify that time is at least equal to configured aftc */ + diffTime = epicsTimeDiffInSeconds(&endTime, &startTime); + testOk(diffTime >= aftc, "AFTC time %lf", diffTime); + + // number of tests = 13 +} + +MAIN(mbbiTest) { + + testPlan(6+39+17+13); + + testdbPrepare(); + testdbReadDatabase("recTestIoc.dbd", NULL, NULL); + recTestIoc_registerRecordDeviceDriver(pdbbase); + + testdbReadDatabase("mbbiTest.db", NULL, NULL); + + eltc(0); + testIocInitOk(); + eltc(1); + + test_soft_input(); + test_operator_display(); + test_alarm(); + test_aftc(); + + testIocShutdownOk(); + testdbCleanup(); + + return testDone(); +} diff --git a/modules/database/test/std/rec/mbbiTest.db b/modules/database/test/std/rec/mbbiTest.db new file mode 100644 index 000000000..106e99db8 --- /dev/null +++ b/modules/database/test/std/rec/mbbiTest.db @@ -0,0 +1,13 @@ +record(mbbi, "test_mbbi_rec") { +} + +record(longout, "test_mbbi_link_rec") { + +} + +record(mbbi, "test_mbbi_rec2") { +} + +record(longout, "test_mbbi_link_rec2") { + +}