Add AFTC flag for bi record and fix mbbi AFTC bug (#817)

* Add AFTC feature to bi record with tests.

* Fix broken AFTC handling for mbbi record, also fix bug in COSV and LALM handling.

* Add release notes and update record docs.
This commit is contained in:
Edmund Blomley authored and GitHub committed 2026-04-01 11:53:39 -05:00
1 parent 631c8831b9
commit c9817fa591
9 files changed
+371 -17

No files matched your search

+13
View File
@@ -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.
+43 -11
View File
@@ -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;
}
+19 -2
View File
@@ -107,13 +107,20 @@ C<MAJOR>. 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<Alarm Specification|https://docs.epics-controls.org/en/latest/process-database/EPICS_Process_Database_Concepts.html#alarm-specification>
for a complete explanation of record alarms and of the standard fields.
L<Alarm Fields|dbCommonRecord/Alarm Fields> 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
+3 -3
View File
@@ -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;
}
+7
View File
@@ -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
+65 -1
View File
@@ -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();
+7
View File
@@ -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") {
}
+201
View File
@@ -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();
}
+13
View File
@@ -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") {
}