Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

4.5 Generate and load 32 channel records

Define one temperature record and instantiate it for every TC-32 channel. You will compare three ways to load the same database: repeated dbLoadRecords, build-time expansion, and runtime dbLoadTemplate.

Complete Run a 32-channel temperature simulator first, using the jeonglee_Demo application with asyn, calc, and StreamDevice enabled. Open three Bash terminals on the same host for the single-device steps, and add terminals 4 and 5 for the three-device section:

TerminalRoleWorking directoryEnvironment
Terminal 1TC-32 emulator (9399)jeonglee_Demo/simulator/No EPICS environment required
Terminal 2Generator, build, and IOCWorkspace parent, jeonglee_Demo/, then jeonglee_Demo/iocBoot/ioctraining-jeonglee_Demo/source "$TREE/setEpicsEnv.bash"
Terminal 3CA and PVA clientAny directorysource "$TREE/setEpicsEnv.bash", export EPICS_CA_AUTO_ADDR_LIST=NO EPICS_CA_ADDR_LIST=127.0.0.1, export EPICS_PVA_AUTO_ADDR_LIST=NO EPICS_PVA_ADDR_LIST=127.0.0.1
Terminals 4-5Extra TC-32 emulators (9400, 9401, three-device section)jeonglee_Demo/simulator/No EPICS environment required

In each EPICS terminal, set TREE to your installation path from lesson 1.1 (such as "$HOME/epics/1.5.0/debian-13/7.0.10") before running source "$TREE/setEpicsEnv.bash".

Add the training IOC instance

In terminal 2, from the directory containing tools/ and jeonglee_Demo/, add an instance:

bash tools/generate_ioc_structure.bash -l training -p jeonglee_Demo
cd jeonglee_Demo

Enter y for the training location. The startup directory is iocBoot/ioctraining-jeonglee_Demo/.

Define the protocol and template

  1. Save this as jeonglee_DemoApp/Db/tc32.proto:

    # Parse the channel number supplied by the database link.
    get_temp {
        in "CH\$1: %f";
    }
    

    The protocol argument supplies 01 through 32. Only the matching channel line updates each record.

  2. Save this as jeonglee_DemoApp/Db/temperature.template:

    record(ai, "$(P)$(R)CH$(CH)") {
        field(DESC, "TC temperature at Channel $(CH)")
        field(DTYP, "stream")
        field(INP, "@tc32.proto get_temp($(CH)) $(PORT)")
        field(EGU, "$(EGU)")
        field(SCAN, "I/O Intr")
    }
    

    I/O Intr lets StreamDevice process the record when matching input arrives. P and R form the PV prefix; PORT selects the asyn port.

  3. Save this as jeonglee_DemoApp/Db/TC-32.substitutions:

    global {EGU="Celsius"}
    file "temperature.template" {
        pattern {CH}
        {01}
        {02}
        {03}
        {04}
        {05}
        {06}
        {07}
        {08}
        {09}
        {10}
        {11}
        {12}
        {13}
        {14}
        {15}
        {16}
        {17}
        {18}
        {19}
        {20}
        {21}
        {22}
        {23}
        {24}
        {25}
        {26}
        {27}
        {28}
        {29}
        {30}
        {31}
        {32}
    }
    

    This fixes CH and EGU during database expansion. P, R, and PORT remain available for startup substitution.

  4. In jeonglee_DemoApp/Db/Makefile, use this complete file:

    TOP = ../..
    include $(TOP)/configure/CONFIG
    DB += training.db training.proto
    DB += tc32.proto temperature.template TC-32.substitutions TC-32.db
    include $(TOP)/configure/RULES
    

    Listing TC-32.db makes the Base build rules expand the matching TC-32.substitutions. Listing the sources also installs them for the runtime-loading exercises.

  5. From the application root, build and inspect the database:

    make
    ls db/TC-32.db db/tc32.proto db/temperature.template db/TC-32.substitutions
    

    db/TC-32.db must contain 32 record definitions.

Load the template once per channel

In iocBoot/ioctraining-jeonglee_Demo/, save this as st.cmd:

#!../../bin/linux-x86_64/jeonglee_Demo
on error break
< envPaths

epicsEnvSet("DB_TOP", "$(TOP)/db")
epicsEnvSet("STREAM_PROTOCOL_PATH", "$(DB_TOP)")
epicsEnvSet("IOCNAME", "training-jeonglee_Demo")
epicsEnvSet("IOC", "ioctraining-jeonglee_Demo")
dbLoadDatabase("$(TOP)/dbd/jeonglee_Demo.dbd")
jeonglee_Demo_registerRecordDeviceDriver(pdbbase)

drvAsynIPPortConfigure("LocalTCPServer", "127.0.0.1:9399", 0, 0, 0)
asynOctetSetInputEos("LocalTCPServer", 0, "\n")
asynOctetSetOutputEos("LocalTCPServer", 0, "\n")

dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=01")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=02")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=03")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=04")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=05")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=06")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=07")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=08")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=09")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=10")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=11")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=12")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=13")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=14")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=15")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=16")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=17")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=18")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=19")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=20")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=21")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=22")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=23")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=24")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=25")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=26")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=27")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=28")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=29")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=30")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=31")
dbLoadRecords("$(DB_TOP)/temperature.template", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer,EGU=Celsius,CH=32")
iocInit()
dbl

Each call expands the template for one channel at startup.

Load the database generated at build time

In the same boot directory, save this as st2.cmd:

#!../../bin/linux-x86_64/jeonglee_Demo
on error break
< envPaths

epicsEnvSet("DB_TOP", "$(TOP)/db")
epicsEnvSet("STREAM_PROTOCOL_PATH", "$(DB_TOP)")
epicsEnvSet("IOCNAME", "training-jeonglee_Demo")
epicsEnvSet("IOC", "ioctraining-jeonglee_Demo")
dbLoadDatabase("$(TOP)/dbd/jeonglee_Demo.dbd")
jeonglee_Demo_registerRecordDeviceDriver(pdbbase)

drvAsynIPPortConfigure("LocalTCPServer", "127.0.0.1:9399", 0, 0, 0)
asynOctetSetInputEos("LocalTCPServer", 0, "\n")
asynOctetSetOutputEos("LocalTCPServer", 0, "\n")

dbLoadRecords("$(DB_TOP)/TC-32.db", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer")
iocInit()
dbl

This loads all 32 records with one call. Channel numbers and units were already expanded by make; the prefix and port are supplied at startup.

Expand substitutions at startup

In the same boot directory, save this as st3.cmd:

#!../../bin/linux-x86_64/jeonglee_Demo
on error break
< envPaths

epicsEnvSet("DB_TOP", "$(TOP)/db")
epicsEnvSet("STREAM_PROTOCOL_PATH", "$(DB_TOP)")
epicsEnvSet("IOCNAME", "training-jeonglee_Demo")
epicsEnvSet("IOC", "ioctraining-jeonglee_Demo")
dbLoadDatabase("$(TOP)/dbd/jeonglee_Demo.dbd")
jeonglee_Demo_registerRecordDeviceDriver(pdbbase)

drvAsynIPPortConfigure("LocalTCPServer", "127.0.0.1:9399", 0, 0, 0)
asynOctetSetInputEos("LocalTCPServer", 0, "\n")
asynOctetSetOutputEos("LocalTCPServer", 0, "\n")

epicsEnvSet("EPICS_DB_INCLUDE_PATH", "$(DB_TOP)")
dbLoadTemplate("$(DB_TOP)/TC-32.substitutions", "P=MYDEMO:,R=TC32:,PORT=LocalTCPServer")
iocInit()
dbl

dbLoadTemplate reads the substitution file at startup. EPICS_DB_INCLUDE_PATH lets it find the referenced template.

Verify each loading method

  1. In terminal 1 inside jeonglee_Demo/simulator/, start one emulator:

    ./tc32_emulator.bash --port 9399
    
  2. In terminal 2 inside jeonglee_Demo/iocBoot/ioctraining-jeonglee_Demo/, make the startups executable:

    chmod +x st.cmd st2.cmd st3.cmd
    
  3. Run ./st.cmd, then ./st2.cmd, then ./st3.cmd, one at a time in terminal 2. Between runs, stop tc32_emulator.bash with Ctrl+C in terminal 1, enter exit in terminal 2, and start ./tc32_emulator.bash --port 9399 again so each IOC gets a fresh stream. Start the per-channel template loader first:

    ./st.cmd
    

    After testing and restarting the emulator, start the build-expanded database loader:

    ./st2.cmd
    

    After testing and restarting the emulator, start the runtime substitution loader:

    ./st3.cmd
    
  4. For each running IOC, direct CA and PVA searches to loopback in terminal 3 and read the channels and metadata:

    export EPICS_CA_AUTO_ADDR_LIST=NO EPICS_CA_ADDR_LIST=127.0.0.1
    export EPICS_PVA_AUTO_ADDR_LIST=NO EPICS_PVA_ADDR_LIST=127.0.0.1
    for ch in {01..32}; do caget "MYDEMO:TC32:CH$ch"; done
    caget MYDEMO:TC32:CH01 MYDEMO:TC32:CH16 MYDEMO:TC32:CH32
    caget MYDEMO:TC32:CH10.EGU MYDEMO:TC32:CH10.DESC MYDEMO:TC32:CH10.SEVR
    

    All 32 PVs report temperatures between 10 and 90 degrees Celsius with NO_ALARM severity:

    MYDEMO:TC32:CH01               58.9072
    MYDEMO:TC32:CH16               58.0812
    MYDEMO:TC32:CH32               80.2712
    MYDEMO:TC32:CH10.EGU           Celsius
    MYDEMO:TC32:CH10.DESC          TC temperature at Channel 10
    MYDEMO:TC32:CH10.SEVR          NO_ALARM
    
  5. Read channel 01 through PVA in terminal 3:

    pvxget MYDEMO:TC32:CH01
    

    pvxget prints the live NTScalar value, alarm, timestamp, and display fields from the same ai record:

    MYDEMO:TC32:CH01
        value double = 58.9072
        alarm.severity int32_t = 0
        alarm.status int32_t = 0
        alarm.message string = ""
        timeStamp.secondsPastEpoch int64_t = 1794424960
        timeStamp.nanoseconds int32_t = 982112605
        timeStamp.userTag int32_t = 0
        display.limitLow double = 0
        display.limitHigh double = 0
        display.description string = "TC temperature at Channel 01"
        display.units string = "Celsius"
        display.precision int32_t = 0
        display.form.index int32_t = 0
        display.form.choices string[] = {7}["Default", "String", "Binary", "Decimal", "Hex", "Exponential", "Engineering"]
        control.limitLow double = 0
        control.limitHigh double = 0
        valueAlarm.lowAlarmLimit double = nan
        valueAlarm.lowWarningLimit double = nan
        valueAlarm.highWarningLimit double = nan
        valueAlarm.highAlarmLimit double = nan
    
  6. Subscribe to live updates across three channels in terminal 3:

    camonitor -t sI MYDEMO:TC32:CH01 MYDEMO:TC32:CH16 MYDEMO:TC32:CH32
    

    Incremental timestamps confirm continuous I/O Intr stream updates (an initial static value or INVALID alarm alone is insufficient evidence of live data):

    MYDEMO:TC32:CH01               2026-11-11 11:22:41.071670 59.029
    MYDEMO:TC32:CH16               2026-11-11 11:22:41.024813 58.2835
    MYDEMO:TC32:CH32               2026-11-11 11:22:41.069307 80.2058
    MYDEMO:TC32:CH16                                +0.087333 57.6804
    MYDEMO:TC32:CH32                                +0.086682 79.7561
    MYDEMO:TC32:CH01                                +0.087075 58.9939
    

    Stop camonitor with Ctrl+C.

Configure several temperature devices

Stop tc32_emulator.bash in terminal 1 with Ctrl+C, then stop the single-device IOC in terminal 2 with exit before starting the multiple-device setup.

Save this fragment as jeonglee_DemoApp/iocsh/TC-32.iocsh:

# Configure one streaming device and load its generated database.
drvAsynIPPortConfigure("$(PORT)", "127.0.0.1:$(TCP_PORT)", 0, 0, 0)
asynOctetSetInputEos("$(PORT)", 0, "\n")
asynOctetSetOutputEos("$(PORT)", 0, "\n")
dbLoadRecords("$(DB_TOP)/TC-32.db", "P=$(P),R=$(R),PORT=$(PORT)")

From the application root, run make to install the fragment. In the training IOC boot directory, save this as st_multi.cmd:

#!../../bin/linux-x86_64/jeonglee_Demo
on error break
< envPaths

epicsEnvSet("DB_TOP", "$(TOP)/db")
epicsEnvSet("STREAM_PROTOCOL_PATH", "$(DB_TOP)")
dbLoadDatabase("$(TOP)/dbd/jeonglee_Demo.dbd")
jeonglee_Demo_registerRecordDeviceDriver(pdbbase)

iocshLoad("$(TOP)/iocsh/TC-32.iocsh", "P=MYDEMO:,R=TC32A:,PORT=TC32A,TCP_PORT=9399")
iocshLoad("$(TOP)/iocsh/TC-32.iocsh", "P=MYDEMO:,R=TC32B:,PORT=TC32B,TCP_PORT=9400")
iocshLoad("$(TOP)/iocsh/TC-32.iocsh", "P=MYDEMO:,R=TC32C:,PORT=TC32C,TCP_PORT=9401")
iocInit()
dbl

Make it executable:

chmod +x st_multi.cmd

Start three emulators on ports 9399, 9400, and 9401 in terminals 1, 4, and 5 from jeonglee_Demo/simulator/, then start this IOC in terminal 2:

./st_multi.cmd

Its three prefixes are MYDEMO:TC32A:, MYDEMO:TC32B:, and MYDEMO:TC32C:. Each must have 32 live channels: 96 records in total. To practice two devices, stop the three emulators with Ctrl+C before entering exit in terminal 2, comment out the third iocshLoad line, start fresh emulators on ports 9399 and 9400 in terminals 1 and 4, and run ./st_multi.cmd. Those two devices provide 64 live records. Stop those emulators with Ctrl+C before entering exit, restore the third iocshLoad line, and start fresh emulators on ports 9399, 9400, and 9401 when returning to three devices.

Exercises

Stop all running emulators with Ctrl+C before entering exit in terminal 2. These exercises use the single-device st2.cmd and its MYDEMO:TC32: prefix. Before each IOC run below, start ./tc32_emulator.bash --port 9399 in terminal 1, and stop tc32_emulator.bash with Ctrl+C before entering exit in terminal 2.

From the application root, save both source files before changing them:

cp jeonglee_DemoApp/Db/temperature.template jeonglee_DemoApp/Db/temperature.template.saved
cp jeonglee_DemoApp/Db/TC-32.substitutions jeonglee_DemoApp/Db/TC-32.substitutions.saved
  • Add field(HIGH, "80") to the template and run make from the application root. Start ./tc32_emulator.bash --port 9399 in terminal 1, run ./st2.cmd from its boot directory in terminal 2, and verify that MYDEMO:TC32:CH10.HIGH is 80.

  • Fix P in the substitution file instead of the startup macros, rebuild, and inspect where that prefix appears in the generated database.

  • Compare the startup times of the three methods on your host. From the boot directory, time each startup with standard input closed so the IOC exits after initialization:

    time ./st.cmd < /dev/null
    

    Repeat with st2.cmd and st3.cmd. A 32-record example does not establish a general performance difference.

Stop tc32_emulator.bash with Ctrl+C in terminal 1 and enter exit in terminal 2 after the exercises. Restore both source files and rebuild from the application root:

cp jeonglee_DemoApp/Db/temperature.template.saved jeonglee_DemoApp/Db/temperature.template
cp jeonglee_DemoApp/Db/TC-32.substitutions.saved jeonglee_DemoApp/Db/TC-32.substitutions
make
head -n 7 db/TC-32.db

The first record name must contain $(P)$(R)CH01, and PORT must remain an unresolved macro in its input link. Chapters 5 and 6 use this restored database so each startup can select its own prefix and port.

Before leaving this lesson, confirm that every tc32_emulator.bash process is stopped with Ctrl+C and no IOC session remains running.