Recently, I chatted with one of the engineers I work with about the problem of supporting several beamlines/projects with a slightly different version of the software for a given device. While this might sound like an easy task, it becomes a burden when the number of devices and projects grow.

We discussed several strategies for accomplishing this at scale, but I would like to start with the simplest solution first to learn its pros and cons. I wondered if there is a possibility of splitting the record definition between two files. One file could serve as a blueprint for a record and the other as a defining/altering for a specific project.

Consider the following database file standard.db

record(ai, pv) {
field(DESC, "Description of PV")
}

After loading it in the st.cmd

dbLoadRecords("../../db/standard.db")

the pv record is present as follows

epics> dbpr pv 5
ACKS: NO_ALARM      ACKT: YES           ADEL: 0             AFTC: 0
AFVL: 0             ALST: 0             AMSG:               AOFF: 0
ASG :               ASLO: 1             ASP : PTR 0x0
BKLNK: ELL 0 [0x0 .. 0x0]               BKPT: 00
DESC: Description of PV                 DISA: 0             DISP: 0
DISS: NO_ALARM      DISV: 1             DPVT: PTR 0x0
DSET: PTR 0x1006e1620                   DTYP: Soft Channel  EGU :
EGUF: 0             EGUL: 0             EOFF: 0             ESLO: 1
EVNT:               FLNK: CONSTANT      HHSV: NO_ALARM      HIGH: 0
HIHI: 0             HOPR: 0             HSV : NO_ALARM      HYST: 0
INIT: 1             INP : CONSTANT      LALM: 0             LBRK: 0
LCNT: 0             LINR: NO CONVERSION LLSV: NO_ALARM      LOLO: 0
LOPR: 0             LOW : 0             LSET: PTR 0x6000039a40c0
LSV : NO_ALARM      MDEL: 0             MLIS: ELL 0 [0x0 .. 0x0]
MLOK: 60 83 2b 02 00 60 00 00           MLST: 0             NAME: pv
NAMSG:              NSEV: NO_ALARM      NSTA: NO_ALARM      OLDSIMM: NO
ORAW: 0             PACT: 0             PBRK: PTR 0x0       PHAS: 0
PINI: NO            PPN : PTR 0x0       PPNR: PTR 0x0       PREC: 0
PRIO: LOW           PROC: 0             PUTF: 0
RDES: PTR 0x6000019a8240                ROFF: 0             RPRO: 0
RSET: PTR 0x1006e0148                   RVAL: 0             SCAN: Passive
SDIS: CONSTANT      SDLY: -1            SEVR: INVALID       SIML: CONSTANT
SIMM: NO            SIMPVT: PTR 0x0     SIMS: NO_ALARM      SIOL: CONSTANT
SMOO: 0             SPVT: PTR 0x0       SSCN: 65535         STAT: UDF
SVAL: 0             TIME: <undefined>   TPRO: 0             TSE : 0
TSEL: CONSTANT      UDF : 1             UDFS: INVALID       UTAG: 0
VAL : 0
epics>

What if we would change its fields for a specific project without changing it for the remaining ones? I’m assuming that the standard.db is shared between a few projects.

Suppose the HIHIand LOLO fields must be changed to reflect the unique setup of one of the projects.

Let’s create another database file, call it project.db , where we define our record again with the changes related only to the project

record(ai, pv){
field(HIHI, 100)
field(LOLO, 20)
}

And load it after the standard.db

dbLoadRecords("../../db/standard.db")
dbLoadRecords("../../db/project.db")

Now, our pv record looks as follows

epics> dbpr pv 5
ACKS: NO_ALARM      ACKT: YES           ADEL: 0             AFTC: 0
AFVL: 0             ALST: 0             AMSG:               AOFF: 0
ASG :               ASLO: 1             ASP : PTR 0x0
BKLNK: ELL 0 [0x0 .. 0x0]               BKPT: 00
DESC: Description of PV                 DISA: 0             DISP: 0
DISS: NO_ALARM      DISV: 1             DPVT: PTR 0x0
DSET: PTR 0x101305620                   DTYP: Soft Channel  EGU :
EGUF: 0             EGUL: 0             EOFF: 0             ESLO: 1
EVNT:               FLNK: CONSTANT      HHSV: NO_ALARM      HIGH: 0
HIHI: 100           HOPR: 0             HSV : NO_ALARM      HYST: 0
INIT: 1             INP : CONSTANT      LALM: 0             LBRK: 0
LCNT: 0             LINR: NO CONVERSION LLSV: NO_ALARM      LOLO: 20
LOPR: 0             LOW : 0             LSET: PTR 0x600000cac180
LSV : NO_ALARM      MDEL: 0             MLIS: ELL 0 [0x0 .. 0x0]
MLOK: f0 c3 7a 01 00 60 00 00           MLST: 0             NAME: pv
NAMSG:              NSEV: NO_ALARM      NSTA: NO_ALARM      OLDSIMM: NO
ORAW: 0             PACT: 0             PBRK: PTR 0x0       PHAS: 0
PINI: NO            PPN : PTR 0x0       PPNR: PTR 0x0       PREC: 0
PRIO: LOW           PROC: 0             PUTF: 0
RDES: PTR 0x600002ca8240                ROFF: 0             RPRO: 0
RSET: PTR 0x101304148                   RVAL: 0             SCAN: Passive
SDIS: CONSTANT      SDLY: -1            SEVR: INVALID       SIML: CONSTANT
SIMM: NO            SIMPVT: PTR 0x0     SIMS: NO_ALARM      SIOL: CONSTANT
SMOO: 0             SPVT: PTR 0x0       SSCN: 65535         STAT: UDF
SVAL: 0             TIME: <undefined>   TPRO: 0             TSE : 0
TSEL: CONSTANT      UDF : 1             UDFS: INVALID       UTAG: 0
VAL : 0
epics>

HIHI and LOLO of record defined in standard.db get overwritten with the values defined in project.db

But what if we don’t allow the redeclaration of the record and flag it if it happens?

There is a variable dbRecordsOnceOnly. It can be set in the st.cmd

var dbRecordsOnceOnly 1

An attempt to start the IOC will produce

var dbRecordsOnceOnly 1
## Load record instances
dbLoadRecords("../../db/standard.db")
dbLoadRecords("../../db/project.db")
ERROR: Record "pv" already defined and dbRecordsOnceOnly set.
Used record type "*" to append.
ERROR at or before ')' in file "../../db/project.db" line 1
dbReadCOM: Parser stack dirty w/o error. 1
ERROR failed to load '../../db/project.db'

You may wonder what does the Used record type “*” to append. mean.

Following the Release notes of epics base

Database field setting updates
A database (.db) file loaded by an IOC does not have to repeat the record type of a record that has already been loaded. It may replace the first parameter of the record(type, name) statement with an asterisk character inside double-quotes, "" instead. Thus the following is a legal database file:

record(ao, "ao1") {}
record("*", "ao1") {
    field(VAL, 10)
}

If you are keen to handle the problem I mentioned in the first paragraph at the IOC level, this is one of the options to make it happen.