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:
| Terminal | Role | Working directory | Environment |
|---|---|---|---|
| Terminal 1 | TC-32 emulator (9399) | jeonglee_Demo/simulator/ | No EPICS environment required |
| Terminal 2 | Generator, build, and IOC | Workspace parent, jeonglee_Demo/, then jeonglee_Demo/iocBoot/ioctraining-jeonglee_Demo/ | source "$TREE/setEpicsEnv.bash" |
| Terminal 3 | CA and PVA client | Any directory | source "$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-5 | Extra 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
-
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
01through32. Only the matching channel line updates each record. -
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 Intrlets StreamDevice process the record when matching input arrives.PandRform the PV prefix;PORTselects the asyn port. -
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
CHandEGUduring database expansion.P,R, andPORTremain available for startup substitution. -
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/RULESListing
TC-32.dbmakes the Base build rules expand the matchingTC-32.substitutions. Listing the sources also installs them for the runtime-loading exercises. -
From the application root, build and inspect the database:
make ls db/TC-32.db db/tc32.proto db/temperature.template db/TC-32.substitutionsdb/TC-32.dbmust 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
-
In terminal 1 inside
jeonglee_Demo/simulator/, start one emulator:./tc32_emulator.bash --port 9399 -
In terminal 2 inside
jeonglee_Demo/iocBoot/ioctraining-jeonglee_Demo/, make the startups executable:chmod +x st.cmd st2.cmd st3.cmd -
Run
./st.cmd, then./st2.cmd, then./st3.cmd, one at a time in terminal 2. Between runs, stoptc32_emulator.bashwith Ctrl+C in terminal 1, enterexitin terminal 2, and start./tc32_emulator.bash --port 9399again so each IOC gets a fresh stream. Start the per-channel template loader first:./st.cmdAfter testing and restarting the emulator, start the build-expanded database loader:
./st2.cmdAfter testing and restarting the emulator, start the runtime substitution loader:
./st3.cmd -
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.SEVRAll 32 PVs report temperatures between
10and90degreesCelsiuswithNO_ALARMseverity: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 -
Read channel 01 through PVA in terminal 3:
pvxget MYDEMO:TC32:CH01pvxgetprints the liveNTScalarvalue, alarm, timestamp, and display fields from the sameairecord: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 -
Subscribe to live updates across three channels in terminal 3:
camonitor -t sI MYDEMO:TC32:CH01 MYDEMO:TC32:CH16 MYDEMO:TC32:CH32Incremental timestamps confirm continuous
I/O Intrstream updates (an initial static value orINVALIDalarm 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.9939Stop
camonitorwith 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 runmakefrom the application root. Start./tc32_emulator.bash --port 9399in terminal 1, run./st2.cmdfrom its boot directory in terminal 2, and verify thatMYDEMO:TC32:CH10.HIGHis 80. -
Fix
Pin 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/nullRepeat with
st2.cmdandst3.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.