ALS-U EPICS environment training
Learn to run an Input/Output Controller (IOC), connect simulated devices, and create reusable configurations with EPICS-env 1.5.0 and EPICS Base 7.0.10. The exercises use local simulators rather than physical hardware.
Scope and intended readers
This book is for engineers learning the ALS-U EPICS environment. You need basic Bash commands and a text editor. Install the prerequisites, then work through the chapters in order.
The binary distribution supplies Base and module libraries. Chapters 2
through 5 compile example IOC applications against those libraries;
they do not rebuild the environment. Chapter 6 uses iocsh.bash to load
installed support without an application executable.
Production deployment, access security policy, and facility services are outside these lessons. Use a test host without competing IOC instances or simulators on the exercise ports. Use one active IOC per PV prefix.
Learning path through the book
| Chapter | What you can do afterward |
|---|---|
| 1. Environment | Select the matching OS tree and read and write one PV |
| 2. IOC applications | Generate, build, and run several boot instances |
| 3. Device communication | Send commands to a TCP simulator through StreamDevice |
| 4. Reusable configuration | Configure multiple devices and load 32-channel databases |
| 5. Build and shell configuration | Explain dependency paths, installation paths, and command syntax |
| 6. Module loader | Run the same devices with installed support through iocsh.bash |
Each lesson names the files to save, the directory to work in, and the result to check. Commands for Bash and the IOC prompt are labeled separately. Stop the preceding IOC before starting another with the same PV names. Save your source files for the later lessons.
Use the glossary to check terminology, the
environment variable reference for local discovery,
and the printable quick reference card (PDF)
for single-page command lookup during exercises. The
online book
contains the published training, and SUMMARY.md defines the book’s
complete navigation.
Install the training prerequisites
Install Linux tools needed to run the binary distribution, build the example applications, and run local device simulators. Use the package manager for the operating system you selected in installation.
-
On Debian 12/13 or Ubuntu 24.04/26.04, install these packages:
sudo apt update sudo apt install -y build-essential perl git libevent-dev libreadline-dev libtirpc-dev binutils sudo apt install -y socat bc procps iproute2On Rocky Linux 8.10/10.2, use these commands instead:
sudo dnf install -y gcc gcc-c++ make perl git libevent-devel readline-devel libtirpc binutils sudo dnf install -y socat bc procps-ng iproute findutils whichThe development packages also supply their runtime libraries. Naming them avoids assuming that Debian 12 uses Ubuntu’s
t64package names. Commands need administrator privileges and working package repositories. -
In an ordinary Bash terminal, verify the tools:
command -v bash perl git make gcc g++ readelf command -v socat bc mktemp pgrep ssEach name must print a path.
readelfcomes frombinutilsand is required by the module loader.pgrepsupports simulator cleanup. -
After activating EPICS in Chapter 1, check the executable’s libraries:
ldd "$(command -v softIocPVX)"Every dependency must resolve to a file.
not foundidentifies a missing runtime library.
Optional parallel launcher
The main lessons launch simulators in separate terminals. GNU Parallel is optional. On Debian or Ubuntu, install it only if you use the optional launcher commands:
sudo apt install -y parallel
parallel --version
On Rocky Linux, continue with separate terminals if GNU Parallel is not available from your configured repositories. No additional repository is required for the main lessons.
Chapter 1: Select and verify the environment
Follow the lessons in order; keep the application source for later exercises.
- Install the binary distribution matching your OS.
- Read and write a PV through CA and PVA.
- Distinguish host architecture from the OS directory.
- Compare the three IOC startup methods.
Chapter exercises
- Add the air temperature record and repeat the failed-search exercise in lesson 1.2. Restore the client address afterward.
- Print
EPICS_HOST_ARCHand explain whylinux-x86_64does not identify a compatible Debian, Ubuntu, or Rocky binary tree. - Stop the IOC with
exitbefore starting Chapter 2.
Before moving on
-
EPICS_BASEandEPICS_HOST_ARCHcome from thesetEpicsEnv.bashscript matching your operating system. -
EPICS_CA_ADDR_LISTis restored to127.0.0.1after the failed-search check in lesson 1.2. - Every
softIocPVXsession from lesson 1.2 is stopped withexit. -
~/ioc-training/first/water.dbstays outside the EPICS distribution directory.
1.1 Install the EPICS environment
Use the pre-built EPICS-env 1.5.0 distribution to run and build the input/output
controllers (IOCs) in this training. Each operating system has its own binary
tree containing EPICS Base 7.0.10, 36 standard modules, commonIocsh scripts,
and the iocsh.bash module loader.
This lesson covers the public distribution. Facility-specific site modules and source builds are outside its scope.
Prerequisites
- A supported Linux system on
x86_64, with Bash, Perl, and Git. - The required system packages.
- An unused
~/epicsdestination for the clone. If your distribution is already installed elsewhere, use its directory instead of~/epicsbelow.
Get and activate the environment
-
Clone the public binary distribution into your home directory:
git clone --depth 1 https://github.com/jeonghanlee/EPICS-env-distribution.git ~/epics -
Identify your architecture and operating system:
uname -m HOST_ID="$(. /etc/os-release && printf '%s-%s' "$ID" "$VERSION_ID")" printf '%s\n' "$HOST_ID" ls ~/epics/1.5.0/Require
x86_64and a directory matchingHOST_IDexactly. Release 1.5.0 provides these trees; all use Base 7.0.10:Operating system Directory name Debian 12 debian-12Debian 13 debian-13Rocky Linux 8.10 rocky-8.10Rocky Linux 10.2 rocky-10.2Ubuntu 24.04 LTS ubuntu-24.04Ubuntu 26.04 LTS ubuntu-26.04Use the tree built for your system. The shared
linux-x86_64architecture name does not make binaries interchangeable between operating systems. -
Activate the matching tree in the current terminal:
TREE="$HOME/epics/1.5.0/$HOST_ID/7.0.10" test -f "$TREE/setEpicsEnv.bash" source "$TREE/setEpicsEnv.bash"The script sets
EPICS_PATH,EPICS_BASE,EPICS_MODULES,EPICS_HOST_ARCH,PATH, andLD_LIBRARY_PATH. On Debian 13, for example,EPICS_BASEends in1.5.0/debian-13/7.0.10/baseandEPICS_HOST_ARCHislinux-x86_64.Source the script once in each terminal used for the exercises. To reduce its output, pass
disable:source "$TREE/setEpicsEnv.bash" disable
Verification
In the activated terminal, inspect the tool paths and build identity:
command -v caget pvxget softIocPVX iocsh.bash
caget -h
iocsh.bash -h
cat "$TREE/.versions"
The four tools must resolve inside the selected 1.5.0 tree, and both help
commands must succeed. .versions records the build timestamp and EPICS-env
source commit. If a tool reports a missing shared library, check the package
requirements for your operating system before proceeding.
Continue with Run your first IOC, Understand architecture and OS trees, and IOC startup choices.
1.2 Run your first IOC
Create one process variable (PV), run an Input/Output Controller (IOC), and read and write the same value through Channel Access (CA) and pvAccess (PVA).
Open two Bash terminals on the same host with these roles and settings:
| Terminal | Role | Working directory | Environment |
|---|---|---|---|
| Terminal 1 | IOC (softIocPVX) | ~/ioc-training/first/ | source "$TREE/setEpicsEnv.bash" |
| Terminal 2 | 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 |
-
In two Bash terminals, activate the matching installation from lesson 1.1. Set
TREEin each terminal to that installation’s absolute path, then direct clients to this host: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 -
In terminal 1, create a workspace outside the distribution:
mkdir -p ~/ioc-training/first cd ~/ioc-training/firstSave this database as
water.db:record(ao, "temperature:water") { field(DESC, "Water temperature setpoint") field(EGU, "degC") field(VAL, "0") }This analog output (
ao) record has no hardware connection. A write changes its stored value;EGUidentifies its engineering units. -
Start the installed PVXS executable in terminal 1:
softIocPVX -d water.dbWait for
iocRun: All initialization complete. At the IOC prompt, list the records:dblThe IOC shell prints the loaded record name:
temperature:waterLeave the IOC running in terminal 1.
-
In terminal 2, read the value and description:
caget -t temperature:water caget -t temperature:water.DESC pvxget temperature:waterCA returns the scalar value and description, while PVA displays a typed
NTScalarstructure:0 Water temperature setpoint temperature:water value double = 0 alarm.severity int32_t = 0 alarm.status int32_t = 2 alarm.message string = "UDF" timeStamp.secondsPastEpoch int64_t = 631152000 timeStamp.nanoseconds int32_t = 0 timeStamp.userTag int32_t = 0 display.limitLow double = 0 display.limitHigh double = 0 display.description string = "Water temperature setpoint" display.units string = "degC" 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 -
Write through CA, then read through both protocols:
caput temperature:water 24 caget -t temperature:water pvxget temperature:waterBoth reads report
24, and the first write clears the initialUDFalarm status and updatestimeStamp:Old : temperature:water 0 New : temperature:water 24 24 temperature:water value double = 24 alarm.severity int32_t = 0 alarm.status int32_t = 0 alarm.message string = "" timeStamp.secondsPastEpoch int64_t = 1794424745 timeStamp.nanoseconds int32_t = 930919750 timeStamp.userTag int32_t = 0 display.limitLow double = 0 display.limitHigh double = 0 display.description string = "Water temperature setpoint" display.units string = "degC" 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 -
Write through PVA and read through CA:
pvxput temperature:water 44 caget -t temperature:waterpvxputexits quietly on success, and CA returns44because both protocols reach the same record:44 -
In terminal 1, stop the IOC:
exit
Exercise: Add an air temperature
Add a second ao record to water.db, named temperature:air, with
DESC set to Air temperature setpoint, EGU to degC, and VAL to
20. Restart the IOC and read both records:
caget temperature:water temperature:air
The restarted records begin at 0 and 20. Client writes in this lesson
are not saved to the database file.
To observe a failed search without changing the host firewall, direct the CA client to an unused local UDP port in terminal 2:
export EPICS_CA_ADDR_LIST=127.0.0.1:59999
caget -w 1 temperature:water
With no IOC search listener on that port, the read times out:
Channel connect timed out: 'temperature:water' not found.
Restore the address and verify that the read succeeds:
export EPICS_CA_ADDR_LIST=127.0.0.1
caget -t temperature:water
An empty address list does not reliably cause failure when automatic broadcast discovery is enabled. Keep it disabled for these local exercises.
1.3 Understand architecture and OS trees
EPICS-env 1.5.0 uses EPICS_HOST_ARCH=linux-x86_64 inside every supported
Linux tree. Each tree is built separately for its operating system and
version. The shared architecture name identifies the EPICS build target;
it does not promise binary compatibility between Linux distributions.
This page explains the installed layout and how to choose a tree. Follow Install the EPICS environment for activation.
Architecture and operating system identity
The architecture and OS directory answer different questions:
| Value | Meaning | Example |
|---|---|---|
uname -m | Machine architecture | x86_64 |
EPICS_HOST_ARCH | EPICS build target and binary subdirectory | linux-x86_64 |
| OS directory | Distribution and release used to build the tree | debian-13 |
| Release directory | EPICS-env distribution version | 1.5.0 |
| Base directory | EPICS Base version | 7.0.10 |
After selecting the Debian 13 tree, for example, the installed paths are:
epics/1.5.0/debian-13/7.0.10/
setEpicsEnv.bash
.versions
base/
bin/linux-x86_64/
lib/linux-x86_64/
modules/
vendor/
The activation script derives EPICS_HOST_ARCH from the selected Base
installation and uses it to select the tool and library directories.
Use the matching OS tree
Base, modules, and vendor libraries are built for each target operating system. The OS directories contain those separate installations, including their binaries, headers, configuration, and module metadata.
Libraries such as glibc, libstdc++, readline, and libevent can differ
between OS releases. A binary built for another release can require library
versions or symbols absent from your host. Matching the CPU architecture
alone is insufficient.
Choose the directory matching ID-VERSION_ID from /etc/os-release, such as
debian-13 or rocky-8.10. The
installation table
lists all six OS trees in the public 1.5.0 distribution. Do not select a
different tree just because both contain bin/linux-x86_64.
Build and run against one tree
Activate the matching tree before generating or building an IOC application.
The generated configure/RELEASE identifies the Base and module paths used
by that build. The loader uses the selected installed tree and its module
metadata when starting an IOC.
Keep Base, module support, and runtime libraries from the selected tree
together. To inspect an executable’s shared-library dependencies on Linux,
use ldd on that executable and check that its EPICS libraries resolve to
the intended installation.
1.4 IOC startup choices
An input/output controller (IOC) needs executable code, record definitions, and configuration. This training shows three ways to provide them.
| Method | Use it to learn | Lesson |
|---|---|---|
softIocPVX -d water.db | Load a database with standard record support | Run your first IOC |
A compiled application and st.cmd | Build an IOC and select support in Makefiles | Generate and run an IOC application |
iocsh.bash st.cmd | Select installed module support at startup | Run an IOC with installed modules |
Chapter 2 starts the application build lessons. Chapter 6 uses the installed loader and reuses the device files developed in those lessons.
Chapter 2: Generate and expand IOC applications
Follow the lessons in order; keep the application source for later exercises.
- Generate, build, and start a compiled IOC application.
- Add boot instances and separate folder and application names.
Chapter exercises
- From
mouse/, runmake clean uninstall. EnableASYNinconfigure/RELEASEwith the path$(MODULES)/asyn_missing, then runmake. Read the missing dependency error. Restore the original file and rebuild successfully. - Add and run the
labinstance described in lesson 2.2. - Compare the generated startup files with
diffas shown in lesson 2.2. Explain which generator option accounts for each changed line.
Before moving on
-
tools/generate_ioc_structure.bashremains in your workspace directory for Chapters 3 and 4. -
mouse/configure/RELEASEis restored after exercise 1 andmakesucceeds inmouse/. - Every
mouseandjeonglee_mouseIOC process is stopped withexit.
2.1 Generate and run an IOC application
Create a compiled Input/Output Controller (IOC) application with the ALS-U template generator. The application links installed support; you do not build EPICS Base or its modules.
-
Activate your matching 1.5.0 environment from Chapter 1. In a development directory, download the generator once:
git clone --depth 1 https://github.com/jeonghanlee/EPICS-IOC-template-tools.git toolsUse an existing
toolscheckout if you already have it. Keep the generated applications outside the binary distribution. -
From the directory containing
tools/, generate the application:bash tools/generate_ioc_structure.bash -l home -p mouseEnter
ywhen asked to continue with the unlistedhomelocation.-pnames the application;-lnames the location. The result ismouse/with the boot instanceiocBoot/iochome-mouse/. Keep the exact namemouserather than substituting your own name so the directory paths and commands in Chapter 2 match as shown.Use ASCII letters, digits, and underscores in application, location, and folder names. The generator rejects
+and-. Avoidiocin names: the script reservesioc,Ioc, andIOC. Keep application name capitalization identical when adding instances. -
Build the generated application:
cd mouse makeA successful build creates
bin/linux-x86_64/mouse, its database definition (DBD), and generatedenvPathsin the boot directory. -
Keep the generated diagnostic files inside this application:
mkdir -p epics-pvinfo/env epics-pvinfo/names export TARGET_TOP="$PWD"The startup writes below
$(TARGET_TOP=/opt)/epics-pvinfo. SetTARGET_TOPagain if you start from another terminal. -
Run the boot instance:
cd iocBoot/iochome-mouse ./st.cmdWait for
iocRun: All initialization complete. At the IOC prompt, inspect the database and available commands:dbl help exitThis initial application loads no records, so
dblhas no PV names to print. Successful initialization does not imply device communication.
Generated directory reference
| Path | Purpose |
|---|---|
configure/RELEASE | Selects installed Base and module paths |
configure/CONFIG_SITE | Controls application build settings |
mouseApp/src/Makefile | Selects DBD files, source files, and linked libraries |
mouseApp/Db/ | Holds database and protocol source files |
mouseApp/iocsh/ | Holds reusable startup fragments |
iocBoot/iochome-mouse/st.cmd | Configures and starts this IOC instance |
iocBoot/iochome-mouse/envPaths | Generated paths for the startup |
The source Makefile includes support conditionally. Enabling a module
in RELEASE activates the corresponding DBD and library entries. Keep
the generator’s module dependency comments when selecting support.
Chapter 3 enables asyn, calc, and StreamDevice.
Exercise: Compare build targets
From the application root, compare the installed executable after each cleanup target:
make clean
ls bin/linux-x86_64
make
make distclean
ls bin/linux-x86_64
make
clean keeps installed products, so the first ls lists mouse.
distclean removes them together with bin/, so the second ls reports
No such file or directory. The final make rebuilds
the application. Lesson 5.3 repeats
this exercise with a separate installation path.
2.2 Add IOC instances to one application
Use one compiled application for several IOC instances. Each instance has
its own startup directory under iocBoot; all use the same mouseApp code.
Complete Generate and run an IOC application first. In each command
below, start in the directory containing tools/ and mouse/.
Choose the names
The generator uses these options:
| Option | Selects | Example |
|---|---|---|
-p | Application code and executable | mouse |
-l | Location in the IOC name | park |
-d | Device part of the IOC name | woodmouse |
-f | Folder containing the application | jeonglee_mouse |
Use ASCII letters, digits, and underscores for APPNAME, LOCATION, and
FOLDER. Keep the application name’s case unchanged when adding an
instance. Avoid ioc in application and location names.
Add another location
-
From the directory containing
mouse/, generate a second instance:bash tools/generate_ioc_structure.bash -l park -p mouseparkis outside the predefined facility locations. Enterywhen asked to continue. The script createsmouse/iocBoot/iocpark-mouse/. -
In the same parent directory, generate an instance with another device name:
bash tools/generate_ioc_structure.bash -l park -p mouse -d woodmouseThis creates
mouse/iocBoot/iocpark-woodmouse/. The application code remainsmouseApp, and both instances run themouseexecutable. -
In
mouse/, build the application and prepare local diagnostic output:cd mouse make mkdir -p epics-pvinfo/env epics-pvinfo/names export TARGET_TOP="$PWD" -
In
mouse/, start the location instance:cd iocBoot/iocpark-mouse ./st.cmdWait for
iocRun: All initialization complete, then enterexit. -
From
iocpark-mouse/, start the device instance:cd ../iocpark-woodmouse ./st.cmdWait for initialization and enter
exitbefore continuing.
Use a different folder name
-
Return to the directory containing
tools/andmouse/. Copy the application to a destination that does not already exist:cp -a mouse jeonglee_mouseKeep the exact folder name
jeonglee_mouserather than substituting your own username so the remaining commands in this lesson match as shown. -
Generate an instance in that folder:
bash tools/generate_ioc_structure.bash -l bts -p mouse -f jeonglee_mouse-fselects the folder.-p mouseselects the application already in that folder. The instance isiocBoot/iocbts-mouse/. -
In
jeonglee_mouse/, rebuild the copied application:cd jeonglee_mouse make clean uninstall make mkdir -p epics-pvinfo/env epics-pvinfo/names export TARGET_TOP="$PWD" cd iocBoot/iocbts-mouse ./st.cmdWait for initialization, then enter
exit.
Check an invalid application name
From the directory containing tools/ and jeonglee_mouse/, try changing
the case of the existing application name:
bash tools/generate_ioc_structure.bash -l ar05 -p mOuse -f jeonglee_mouse
The generator must reject the mismatch between mOuse and mouse.
A rejected command returns a nonzero status; it is the expected result
of this exercise.
Verification
In mouse/, compare the startup files:
diff iocBoot/iochome-mouse/st.cmd iocBoot/iocpark-mouse/st.cmd
diff iocBoot/iochome-mouse/st.cmd iocBoot/iocpark-woodmouse/st.cmd
The IOC identity and location-dependent settings differ. diff returns
status 1 when it finds differences; that status does not mean the
generator failed.
The exercise is complete when all three added instances initialize and
the wrong-case application name is rejected. To add a fourth location,
repeat the generator with -l lab -p mouse, build, and run its startup.
Chapter 3: Connect an IOC to a TCP simulator
Follow the lessons in order; keep the application source for later exercises.
- Configure StreamDevice support and two command records.
- Run a newline-delimited echo server.
- Read replies and observe connection loss and recovery.
Chapter exercises
- Perform the simulator stop and restart exercise in lesson 3.3. Distinguish the last reply value from the command record’s alarm.
- In the
drvAsynIPPortConfigureline ofst.cmd, change9399to an unused port. Restart the IOC and inspect the connection and write errors. Restore9399and confirm a successful round trip. - Explain which protocol writes
Cmd-RBand why reading an old reply alone does not prove that the device is connected.
Before moving on
-
jeonglee_Demo/retainsdb/training.db,db/training.proto, andiocBoot/iocB46_182-jeonglee_Demo/st.cmdset to port9399. -
jeonglee_Demo/simulator/retains executabletcpserver.bashandconnection_handler.shscripts for Chapters 4 and 6. - The
jeonglee_DemoIOC is stopped withexitandtcpserver.bashis stopped with Ctrl+C. -
ss -ltn 'sport = :9399'reports no active TCP listener.
3.1 Configure an IOC for TCP communication
Create an IOC that sends a string to a TCP device and stores its reply. The next lessons provide an echo simulator and verify the complete path.
Complete Chapter 2 first and activate your matching EPICS-env 1.5.0 tree.
Generate the application
From the directory containing tools/, generate the application:
bash tools/generate_ioc_structure.bash -l B46_182 -p jeonglee_Demo
cd jeonglee_Demo
Enter y for the unlisted training location. The executable is
jeonglee_Demo; its startup directory is
iocBoot/iocB46_182-jeonglee_Demo/. Keep the exact name jeonglee_Demo
rather than substituting your own username so the directory paths and
build files in Chapters 3 through 6 match as shown.
Select the device support
In configure/RELEASE, enable these three module definitions:
ASYN = $(MODULES)/asyn
CALC = $(MODULES)/calc
STREAM = $(MODULES)/StreamDevice
Keep the generated Base and default-module definitions. The application Makefile selects each module’s DBD and libraries when its variable is defined. StreamDevice in this distribution uses asyn and calc. Paths must not contain trailing whitespace.
Create the protocol and records
-
Save this protocol as
jeonglee_DemoApp/Db/training.proto:# Read the command reply into the record passed as argument 1. sendRawQuery { ExtraInput = Ignore; out "%s"; in "%(\$1)40c"; }out "%s"sends the stringout record’s value.in "%(\$1)40c"stores up to 40 input characters in the record field named by argument 1. The asyn end-of-string settings delimit each reply. -
Save these records as
jeonglee_DemoApp/Db/training.db:record(stringout, "$(P)$(R)Cmd") { field(DESC, "Raw query") field(DTYP, "stream") field(OUT, "@training.proto sendRawQuery($(P)$(R)Cmd-RB.VAL) $(PORT)") } record(stringin, "$(P)$(R)Cmd-RB") { field(DESC, "Raw query readback") field(DTYP, "Soft Channel") }Writing
Cmdruns the protocol. Its reply goes toCmd-RB.VAL.P,R, andPORTare supplied when the startup loads the database. -
From the application’s top-level directory, build:
makeThe generated
Db/Makefileinstalls the database and protocol indb/.
Configure the startup
In iocBoot/iocB46_182-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("IOC", "iocB46_182-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)/training.db", "P=jeonglee:,R=myoffice:,PORT=LocalTCPServer")
iocInit()
dbl
The application DBD and registration function provide record and device
support. Configure the TCP port and load the records before iocInit().
Keep the exact macro values P=jeonglee:,R=myoffice: rather than
substituting your own username so the jeonglee:myoffice: client
commands in Chapters 3 and 4 match as shown.
Verification
From the application root, check the installed files and libraries:
ls db/training.db db/training.proto dbd/jeonglee_Demo.dbd
ldd bin/linux-x86_64/jeonglee_Demo
The library list must include libasyn.so, libcalc.so, and
libstream.so, with no not found entries.
Continue with Run a TCP echo simulator, then Test IOC communication with the simulator.
3.2 Run a TCP echo simulator
Create a local TCP server that returns each line it receives. This provides the device endpoint for the IOC from the preceding lesson.
Install socat using the package instructions.
The scripts also support tcpsvd where it is installed.
Create the two scripts
-
From the
jeonglee_Demo/application root, create a simulator directory:mkdir simulator cd simulator -
Save this launcher as
tcpserver.bash:#!/usr/bin/env bash # SPDX-License-Identifier: GPL-2.0-or-later # Run a line-based device handler for each local TCP connection. set -euo pipefail PORT=9399 HANDLER=connection_handler.sh [[ $# -le 1 ]] || { printf 'Usage: %s [PORT]\n' "$0" >&2; exit 2; } if [[ $# -eq 1 ]]; then PORT="$1" fi [[ "$PORT" =~ ^[0-9]{1,5}$ ]] || { printf 'Invalid port\n' >&2; exit 2; } PORT="$((10#$PORT))" (( PORT >= 1 && PORT <= 65535 )) || { printf 'Invalid port\n' >&2; exit 2; } SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)" HANDLER_SCRIPT="$SCRIPT_DIR/$HANDLER" [[ -x "$HANDLER_SCRIPT" ]] || { printf 'Handler is not executable: %s\n' "$HANDLER_SCRIPT" >&2; exit 1; } if command -v socat >/dev/null; then printf 'Attempting to start socat echo server on 127.0.0.1:%s\n' "$PORT" exec socat "TCP-LISTEN:$PORT,bind=127.0.0.1,reuseaddr,fork" "EXEC:'$HANDLER_SCRIPT'" elif command -v tcpsvd >/dev/null; then printf 'Attempting to start tcpsvd echo server on 127.0.0.1:%s\n' "$PORT" exec tcpsvd -l localhost -c 1 -vvE 127.0.0.1 "$PORT" "$HANDLER_SCRIPT" fi printf 'Install socat to run this simulator.\n' >&2 exit 1 -
Save this handler as
connection_handler.shin the same directory:#!/usr/bin/env bash # SPDX-License-Identifier: GPL-2.0-or-later # Return each complete input line without changing its contents. while IFS= read -r received_text; do printf '%s\n' "$received_text" done -
Make both files executable:
chmod +x tcpserver.bash connection_handler.sh
The launcher finds the handler beside itself. It prefers socat, listens
on loopback only, and runs a handler for each connection.
Run and test the simulator
-
In terminal 1, start the server from
simulator/:./tcpserver.bashIt must report port 9399. Leave it running.
-
In terminal 2, connect a client:
socat - TCP:127.0.0.1:9399 -
Type each line and press Enter:
First! 123456 ByeBye!The server must return each line unchanged. Press Ctrl+D to close the client.
Verification
Confirm the listener while the server runs:
ss -ltn 'sport = :9399'
Stop the server with Ctrl+C and repeat the command. Its listener must be absent. Leave the server stopped; the next lesson starts it explicitly. Continue with Test IOC communication with the simulator.
3.3 Test IOC communication with the simulator
Verify the complete command and reply path: CA client, stringout record, StreamDevice, asyn TCP, simulator, and readback.
Complete Configure an IOC for TCP communication and Run a TCP echo simulator first. Open three Bash terminals on the same host with these roles and settings:
| Terminal | Role | Working directory | Environment |
|---|---|---|---|
| Terminal 1 | TCP echo simulator (9399) | jeonglee_Demo/simulator/ | No EPICS environment required |
| Terminal 2 | Compiled IOC (./st.cmd) | jeonglee_Demo/iocBoot/iocB46_182-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 |
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".
Start the simulator and IOC
-
In terminal 1, start the simulator from
simulator/:./tcpserver.bash 9399 -
In terminal 2, run
source "$TREE/setEpicsEnv.bash", then start the IOC fromjeonglee_Demo/:cd iocBoot/iocB46_182-jeonglee_Demo ./st.cmdWait for
iocRun: All initialization complete. Enterdblat the IOC prompt. It must list:jeonglee:myoffice:Cmd-RB jeonglee:myoffice:Cmd -
In terminal 3, run
source "$TREE/setEpicsEnv.bash"and select local searches: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
Send commands and read replies
-
Send an identification query:
caput -c jeonglee:myoffice:Cmd "DEVICE_ID?" caget jeonglee:myoffice:Cmd-RBcaput -cwaits for record processing to complete, and the readback returnsDEVICE_ID?:Old : jeonglee:myoffice:Cmd New : jeonglee:myoffice:Cmd DEVICE_ID? jeonglee:myoffice:Cmd-RB DEVICE_ID? -
Send another query and read the reply through both CA and PVA:
caput -c jeonglee:myoffice:Cmd "STATUS?" caget jeonglee:myoffice:Cmd-RB pvxget jeonglee:myoffice:Cmd-RBBoth reads report
STATUS?with zero alarm severity:Old : jeonglee:myoffice:Cmd DEVICE_ID? New : jeonglee:myoffice:Cmd STATUS? jeonglee:myoffice:Cmd-RB STATUS? jeonglee:myoffice:Cmd-RB value string = "STATUS?" alarm.severity int32_t = 0 alarm.status int32_t = 0 alarm.message string = "" timeStamp.secondsPastEpoch int64_t = 1794424750 timeStamp.nanoseconds int32_t = 132968952 timeStamp.userTag int32_t = 0 display.description string = "Raw query readback"
Observe a lost connection
-
Stop the simulator in terminal 1 with Ctrl+C, then send another command from terminal 3:
caput -c jeonglee:myoffice:Cmd "AFTER_STOP" caget jeonglee:myoffice:Cmd.STAT jeonglee:myoffice:Cmd.SEVR caget jeonglee:myoffice:Cmd-RBCmdentersCOMM/INVALIDalarm state, whileCmd-RBretains the priorSTATUS?string:Old : jeonglee:myoffice:Cmd STATUS? New : jeonglee:myoffice:Cmd AFTER_STOP jeonglee:myoffice:Cmd.STAT COMM jeonglee:myoffice:Cmd.SEVR INVALID jeonglee:myoffice:Cmd-RB STATUS?In terminal 2, the IOC console logs the disconnected TCP port:
2026/11/11 11:19:10.630171 LocalTCPServer jeonglee:myoffice:Cmd connectToAsynPort: Can't connect to 127.0.0.1:9399: Operation now in progress 2026/11/11 11:19:10.630201 LocalTCPServer jeonglee:myoffice:Cmd: device LocalTCPServer 0 disconnectedA cached readback value alone does not prove that the device accepted a command after the connection dropped.
-
Restart the simulator in terminal 1:
./tcpserver.bash 9399asynretries a lost connection periodically, so reconnection can take up to 30 seconds. Resend from terminal 3 and check both the reply and the command alarm fields:caput -c jeonglee:myoffice:Cmd "RECOVERED" caget jeonglee:myoffice:Cmd-RB jeonglee:myoffice:Cmd.STAT jeonglee:myoffice:Cmd.SEVRWhile
Cmd-RBstill showsSTATUS?, repeat both commands every few seconds. Once reconnected,Cmd-RBupdates toRECOVEREDandCmdclears its alarm state toNO_ALARM:Old : jeonglee:myoffice:Cmd AFTER_STOP New : jeonglee:myoffice:Cmd RECOVERED jeonglee:myoffice:Cmd-RB RECOVERED jeonglee:myoffice:Cmd.STAT NO_ALARM jeonglee:myoffice:Cmd.SEVR NO_ALARM
Verification
Confirm that each successful readback equals the command just sent. To test an incorrect endpoint, change the startup’s TCP port to an unused port, restart the IOC, and observe the failed transaction. Restore 9399, restart, and verify another round trip.
Enter exit in the IOC terminal and stop the simulator with Ctrl+C.
Chapter 4: Reuse device and database configurations
Follow the lessons in order; keep the application source for later exercises.
- Move device configuration into an iocshLoad fragment.
- Run independently configured request/reply simulators.
- Configure three device connections in one IOC.
- Stream 32 temperature channels from one simulator.
- Load records directly, at build time, or with substitutions.
- Explain the startup sequence and linked libraries.
- Read the Base initialization sequence.
- Locate each complete runnable example.
Chapter exercises
- Copy the Chapter 3
st.cmdtost_break.cmd. MoveiocInit()immediately beforedbLoadRecords, start it, and inspect the startup error. Restore the original order and confirm normal communication. - In lesson 4.5, configure two TC32 simulators and verify all 64 PVs.
- Explain which macros are resolved at build time and which remain for startup. Generating a flat database changes where expansion happens; it does not establish a measured startup-time advantage.
Before moving on
-
jeonglee_Demo/retainsiocsh/training_device.iocsh,simulator/advanced_connection_handler.sh, andsimulator/tc32_emulator.bashfor Chapters 5 and 6. -
jeonglee_Demo/db/TC-32.dbis rebuilt from restored sources sohead -n 7 db/TC-32.dbshows$(P)$(R)CH01and$(PORT). - Every
jeonglee_DemoIOC is stopped withexitand every simulator is stopped with Ctrl+C. -
ss -ltn 'sport = :9399 or sport = :9400 or sport = :9401'reports no active TCP listeners.
4.1 Reuse a device startup fragment
Move the Chapter 3 device setup into a reusable IOC shell (iocsh)
file. Its macros let one file configure several devices.
Complete Test IOC communication with the simulator first, and stop the Chapter 3 IOC and simulator. Open three Bash terminals on the same host with these roles and settings:
| Terminal | Role | Working directory | Environment |
|---|---|---|---|
| Terminal 1 | TCP echo simulator (9399) | jeonglee_Demo/simulator/ | No EPICS environment required |
| Terminal 2 | Build and IOC (make, ./st2.cmd) | jeonglee_Demo/ then jeonglee_Demo/iocBoot/iocB46_182-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 |
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".
-
In terminal 1, start one echo server from
jeonglee_Demo/simulator/:./tcpserver.bash 9399Leave it running for this lesson. Do not start another on the same port.
-
In terminal 2 inside
jeonglee_Demo/jeonglee_DemoApp/iocsh/, save the following file astraining_device.iocsh:# PREFIX, DEVICE, DATABASE_TOP, and PORT_NAME are required. # HOST defaults to loopback; PORT defaults to 9399. # Set ASYNTRACE empty to enable the trace command. drvAsynIPPortConfigure("$(PORT_NAME)", "$(HOST=127.0.0.1):$(PORT=9399)", 0, 0, 0) asynOctetSetInputEos("$(PORT_NAME)", 0, "\n") asynOctetSetOutputEos("$(PORT_NAME)", 0, "\n") $(ASYNTRACE=#--)asynSetTraceMask($(PORT_NAME), -1, ERROR|FLOW|DRIVER) dbLoadRecords("$(DATABASE_TOP)/training.db", "P=$(PREFIX),R=$(DEVICE),PORT=$(PORT_NAME)")$(NAME=default)supplies a default macro value.ASYNTRACEdefaults to a comment prefix; an empty value enables that command. The asyn port and database use the samePORT_NAME. -
In terminal 2 from the
jeonglee_Demo/application root, install the fragment:make ls iocsh/training_device.iocshEdit the source under
jeonglee_DemoApp/iocsh/, then runmaketo update the installed copy underiocsh/. -
In terminal 2 inside
iocBoot/iocB46_182-jeonglee_Demo/, save this asst2.cmd:#!../../bin/linux-x86_64/jeonglee_Demo on error break < envPaths epicsEnvSet("DB_TOP", "$(TOP)/db") epicsEnvSet("DATABASE_TOP", "$(DB_TOP)") epicsEnvSet("STREAM_PROTOCOL_PATH", "$(DB_TOP)") epicsEnvSet("PREFIX", "jeonglee:") epicsEnvSet("DEVICE", "myoffice:") epicsEnvSet("IOCNAME", "B46_182-jeonglee_Demo") epicsEnvSet("IOC", "iocB46_182-jeonglee_Demo") dbLoadDatabase("$(TOP)/dbd/jeonglee_Demo.dbd") jeonglee_Demo_registerRecordDeviceDriver(pdbbase) # Select exactly one call below. iocshLoad("$(TOP)/iocsh/training_device.iocsh", "PORT_NAME=LocalTCPServer") #--iocshLoad("$(TOP)/iocsh/training_device.iocsh", "PORT_NAME=LocalTCPServer,HOST=127.0.0.1,PORT=9399") #--iocshLoad("$(TOP)/iocsh/training_device.iocsh", "PORT_NAME=LocalTCPServer,PORT=9399,ASYNTRACE=") iocInit() dblThe environment supplies
PREFIX,DEVICE, andDATABASE_TOP. The activeiocshLoadcall supplies the port name. This IOC already registered its compiled support; the fragment configures one device. -
In terminal 2 from
iocBoot/iocB46_182-jeonglee_Demo/, start the IOC:chmod +x st2.cmd ./st2.cmdIn terminal 3, direct CA and PVA searches to loopback, send a command, and read the reply:
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 caput -c jeonglee:myoffice:Cmd STATUS? caget -t jeonglee:myoffice:Cmd-RBThe reply is
STATUS?. The IOC lists the same two records as Chapter 3.
Exercise: Change fragment arguments
Stop the IOC in terminal 2 with exit. Test each commented iocshLoad call by
removing its #-- and commenting out the active call. Restart the IOC
for each variant. Exactly one call must be active.
The second call passes the host and port explicitly. The third enables asyn trace output. Both retain the same PV names and echo behavior.
Comment out all three calls and restart. dbl lists no records because
no device database was loaded. Restore one call before the next lesson.
Finish by entering exit in terminal 2 and stopping the simulator in
terminal 1 with Ctrl+C.
iocshLoad runs configuration inside an IOC. It does not load installed
module libraries. Chapter 6 teaches the separate
iocsh.bash module loader.
4.2 Run a command-aware TCP simulator
Extend the echo server so that GetID? returns the handler’s process ID
and GetTemp? returns an integer between 0 and 100. Other lines are echoed.
Use the simulator/ directory created in Chapter 3.
Keep connection_handler.sh for the default echo mode.
Replace the launcher and add a handler
-
In
simulator/, replacetcpserver.bashwith this version:#!/usr/bin/env bash # SPDX-License-Identifier: GPL-2.0-or-later # Run a line-based device handler for each local TCP connection. set -euo pipefail PORT=9399 HANDLER=connection_handler.sh [[ $# -le 2 ]] || { printf 'Usage: %s [PORT] [HANDLER]\n' "$0" >&2; exit 2; } if [[ $# -gt 0 ]]; then if [[ "$1" =~ ^[0-9]+$ ]]; then PORT="$1" shift fi if [[ $# -gt 0 ]]; then HANDLER="$1" shift fi fi [[ $# -eq 0 ]] || { printf 'Too many arguments\n' >&2; exit 2; } [[ "$PORT" =~ ^[0-9]{1,5}$ ]] || { printf 'Invalid port\n' >&2; exit 2; } PORT="$((10#$PORT))" (( PORT >= 1 && PORT <= 65535 )) || { printf 'Invalid port\n' >&2; exit 2; } SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)" case "$HANDLER" in /*) HANDLER_SCRIPT="$HANDLER" ;; *) HANDLER_SCRIPT="$SCRIPT_DIR/$HANDLER" ;; esac [[ -x "$HANDLER_SCRIPT" ]] || { printf 'Handler is not executable: %s\n' "$HANDLER_SCRIPT" >&2; exit 1; } if command -v socat >/dev/null; then printf 'Attempting to start socat echo server on 127.0.0.1:%s\n' "$PORT" exec socat "TCP-LISTEN:$PORT,bind=127.0.0.1,reuseaddr,fork" "EXEC:'$HANDLER_SCRIPT'" elif command -v tcpsvd >/dev/null; then printf 'Attempting to start tcpsvd echo server on 127.0.0.1:%s\n' "$PORT" exec tcpsvd -l localhost -c 1 -vvE 127.0.0.1 "$PORT" "$HANDLER_SCRIPT" fi printf 'Install socat to run this simulator.\n' >&2 exit 1 -
In the same directory, save
advanced_connection_handler.sh:#!/usr/bin/env bash # SPDX-License-Identifier: GPL-2.0-or-later # Reply with a handler ID, a temperature, or the original input. while IFS= read -r received_text; do case "$received_text" in "GetID?") printf '%s\n' "$$" ;; "GetTemp?") printf '%s\n' "$((RANDOM % 101))" ;; *) printf '%s\n' "$received_text" ;; esac done -
Make both scripts executable:
chmod +x tcpserver.bash advanced_connection_handler.sh
Choose a port and handler
Start one of these commands in a simulator terminal:
| Command | Port | Handler |
|---|---|---|
./tcpserver.bash | 9399 | Echo |
./tcpserver.bash 8888 | 8888 | Echo |
./tcpserver.bash advanced_connection_handler.sh | 9399 | Command-aware |
./tcpserver.bash 8888 advanced_connection_handler.sh | 8888 | Command-aware |
Stop each server with Ctrl+C before trying another command on the same port.
Test the command-aware replies
-
Start the advanced handler on 9399:
./tcpserver.bash 9399 advanced_connection_handler.sh -
In another terminal, connect a client:
socat - TCP:127.0.0.1:9399 -
Enter these commands, one per line:
GetID? GetTemp? WhatIsThis?The replies must be a numeric process ID, a temperature from 0 through 100, and the literal string
WhatIsThis?.
Run three independent simulators
In three terminals, start:
./tcpserver.bash 9399 advanced_connection_handler.sh
./tcpserver.bash 9400 advanced_connection_handler.sh
./tcpserver.bash 9401 advanced_connection_handler.sh
If GNU Parallel is installed, the equivalent command is:
parallel ./tcpserver.bash {} advanced_connection_handler.sh ::: 9399 9400 9401
Each connection has its own handler process. This lets the next lesson check that the three IOC connections reach different devices.
Verification
Connect a client to each port and repeat the three commands above. The three handler IDs must differ. Stop all clients and servers with Ctrl+C; their listening ports must be released.
4.3 Connect three devices to one IOC
Use one reusable iocshLoad fragment to configure three independent
TCP connections. Each device needs a unique PV name, asyn port, and TCP port.
Complete Reuse a device startup fragment and Run a command-aware TCP simulator first. Open five Bash terminals on the same host with these roles and settings (or launch the three simulators from one terminal with GNU Parallel as shown in lesson 4.2):
| Terminal | Role | Working directory | Environment |
|---|---|---|---|
| Terminal 1 | Simulator 1 (9399) | jeonglee_Demo/simulator/ | No EPICS environment required |
| Terminal 2 | Simulator 2 (9400) | jeonglee_Demo/simulator/ | No EPICS environment required |
| Terminal 3 | Simulator 3 (9401) | jeonglee_Demo/simulator/ | No EPICS environment required |
| Terminal 4 | Compiled IOC (./st3.cmd) | jeonglee_Demo/iocBoot/iocB46_182-jeonglee_Demo/ | source "$TREE/setEpicsEnv.bash" |
| Terminal 5 | 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 |
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".
Configure the three connections
The example uses these names:
| PV prefix | Asyn port | TCP port |
|---|---|---|
MYDEMO:SIM1: | TCP1 | 9399 |
MYDEMO:SIM2: | TCP2 | 9400 |
MYDEMO:SIM3: | TCP3 | 9401 |
In terminal 4 inside iocBoot/iocB46_182-jeonglee_Demo/, save this as st3.cmd:
#!../../bin/linux-x86_64/jeonglee_Demo
on error break
< envPaths
epicsEnvSet("DB_TOP", "$(TOP)/db")
epicsEnvSet("DATABASE_TOP", "$(DB_TOP)")
epicsEnvSet("STREAM_PROTOCOL_PATH", "$(DB_TOP)")
epicsEnvSet("PREFIX", "MYDEMO:")
dbLoadDatabase("$(TOP)/dbd/jeonglee_Demo.dbd")
jeonglee_Demo_registerRecordDeviceDriver(pdbbase)
iocshLoad("$(TOP)/iocsh/training_device.iocsh", "DEVICE=SIM1:,PORT_NAME=TCP1,PORT=9399")
iocshLoad("$(TOP)/iocsh/training_device.iocsh", "DEVICE=SIM2:,PORT_NAME=TCP2,PORT=9400")
iocshLoad("$(TOP)/iocsh/training_device.iocsh", "DEVICE=SIM3:,PORT_NAME=TCP3,PORT=9401,ASYNTRACE=")
iocInit()
dbl
The fragment reads PREFIX and DATABASE_TOP from the IOC environment.
Each iocshLoad call supplies its device-specific values.
ASYNTRACE= enables tracing for the third connection.
Make the startup executable:
chmod +x st3.cmd
Run and test all three devices
-
Start the command-aware simulator in terminals 1, 2, and 3 from
jeonglee_Demo/simulator/. In terminal 1, listen on port9399:./tcpserver.bash 9399 advanced_connection_handler.shIn terminal 2, listen on port
9400:./tcpserver.bash 9400 advanced_connection_handler.shIn terminal 3, listen on port
9401:./tcpserver.bash 9401 advanced_connection_handler.sh -
In terminal 4 inside
jeonglee_Demo/iocBoot/iocB46_182-jeonglee_Demo/, start the IOC:./st3.cmd -
In terminal 5, direct CA and PVA searches to loopback and request each handler’s identity:
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 caput -c MYDEMO:SIM1:Cmd "GetID?" caput -c MYDEMO:SIM2:Cmd "GetID?" caput -c MYDEMO:SIM3:Cmd "GetID?" caget MYDEMO:SIM1:Cmd-RB MYDEMO:SIM2:Cmd-RB MYDEMO:SIM3:Cmd-RBcaput -cwaits for each asynchronous command to finish before the next command runs, andcagetprints three distinct numeric process IDs:MYDEMO:SIM1:Cmd-RB 764328 MYDEMO:SIM2:Cmd-RB 764331 MYDEMO:SIM3:Cmd-RB 764335 -
Request a simulated temperature from each device:
caput -c MYDEMO:SIM1:Cmd "GetTemp?" caput -c MYDEMO:SIM2:Cmd "GetTemp?" caput -c MYDEMO:SIM3:Cmd "GetTemp?" caget MYDEMO:SIM1:Cmd-RB MYDEMO:SIM2:Cmd-RB MYDEMO:SIM3:Cmd-RBEach reply returns an integer between
0and100:MYDEMO:SIM1:Cmd-RB 32 MYDEMO:SIM2:Cmd-RB 26 MYDEMO:SIM3:Cmd-RB 61In terminal 4,
TCP3printsasyntrace lines for the 9-byte"GetTemp?\n"write and 3-byte"61\n"reply on127.0.0.1:9401, whileTCP1andTCP2stay quiet:2026/11/11 11:19:33.450 TCP3 addr -1 queueRequest priority 0 not lockHolder 2026/11/11 11:19:33.450 TCP3 schedule queueRequest timeout in 5.000000 seconds 2026/11/11 11:19:33.450 asynManager::portThread port=TCP3 callback 2026/11/11 11:19:33.450 TCP3 addr -1 queueRequest priority 0 not lockHolder 2026/11/11 11:19:33.450 TCP3 schedule queueRequest timeout in 0.100000 seconds 2026/11/11 11:19:33.450 asynManager::portThread port=TCP3 callback 2026/11/11 11:19:33.450 127.0.0.1:9401 read. 2026/11/11 11:19:33.452 127.0.0.1:9401 write. 2026/11/11 11:19:33.452 127.0.0.1:9401 write 9 2026/11/11 11:19:33.452 wrote 9 to 127.0.0.1:9401, return asynSuccess. 2026/11/11 11:19:33.452 TCP3 addr -1 queueRequest priority 0 from lockHolder 2026/11/11 11:19:33.452 TCP3 schedule queueRequest timeout in 1.000000 seconds 2026/11/11 11:19:33.452 asynManager::portThread port=TCP3 callback 2026/11/11 11:19:33.452 127.0.0.1:9401 read. 2026/11/11 11:19:33.452 127.0.0.1:9401 read 3
Verification
Enter dbl in the IOC console. All six records, Cmd and Cmd-RB
for each device, must be present. Tracing for TCP3 must appear when
you send its commands.
Comment out one iocshLoad line and restart the IOC. That device’s
records must be absent while the other devices continue to work.
Restore the line, restart, and confirm that all three devices respond.
Enter exit in the IOC terminal and stop each simulator with Ctrl+C.
4.4 Run a 32-channel temperature simulator
Simulate a device that streams temperatures without waiting for commands. You will use this stream to test the 32-channel database in the next lesson.
Prerequisites
Install socat, bc, and procps (pgrep) using the
package instructions.
Use one TCP client per emulator instance. Multiple clients share the same
PTY and do not receive independent copies of every sample.
Create the emulator
In your application’s simulator/ directory, save this file as
tc32_emulator.bash:
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Stream 32 temperature channels through a TCP-to-PTY bridge.
set -euo pipefail
PORT=9399
SOCAT_PID=""
WORK_DIR="$(mktemp -d)"
SERIAL_DEV="$WORK_DIR/serial"
declare -a temps=()
while [[ $# -gt 0 ]]; do
case "$1" in
--port)
[[ $# -ge 2 ]] || { printf 'Usage: %s [--port PORT]\n' "$0" >&2; exit 2; }
PORT="$2"
shift 2
;;
*)
printf 'Usage: %s [--port PORT]\n' "$0" >&2
exit 2
;;
esac
done
[[ "$PORT" =~ ^[0-9]{1,5}$ ]] || { printf 'Invalid port\n' >&2; exit 2; }
PORT="$((10#$PORT))"
(( PORT >= 1 && PORT <= 65535 )) || { printf 'Invalid port\n' >&2; exit 2; }
for cmd in socat bc mktemp pgrep; do
command -v "$cmd" >/dev/null || { printf '%s is required\n' "$cmd" >&2; exit 1; }
done
function cleanup {
local rc=$?
local child=""
if [[ -n "$SOCAT_PID" ]]; then
while IFS= read -r child; do
kill "$child" 2>/dev/null || true
done < <(pgrep -P "$SOCAT_PID" || true)
kill "$SOCAT_PID" 2>/dev/null || true
wait "$SOCAT_PID" 2>/dev/null || true
fi
printf 'Diagnostic directory: %s\n' "$WORK_DIR" >&2
return "$rc"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
socat -d -d "PTY,raw,echo=0,link=$SERIAL_DEV" "TCP-LISTEN:$PORT,bind=127.0.0.1,reuseaddr,fork" \
2>"$WORK_DIR/socat.log" &
SOCAT_PID=$!
for ((attempt=0; attempt<100; attempt++)); do
[[ -c "$SERIAL_DEV" ]] && break
kill -0 "$SOCAT_PID" 2>/dev/null || { cat "$WORK_DIR/socat.log" >&2; exit 1; }
sleep 0.05
done
[[ -c "$SERIAL_DEV" ]] || { printf 'PTY creation timed out\n' >&2; exit 1; }
for ((channel=0; channel<32; channel++)); do
temps[channel]="$(printf 'scale=4; 10 + %d*80/32767\n' "$RANDOM" | bc)"
done
function generate_temp {
local idx="$1"
local change=""
local next=""
change="$(printf 'scale=4; (%d/32767 - 0.5)*1.5\n' "$RANDOM" | bc)"
next="$(printf 't=%s+(%s); if(t<10)t=10; if(t>90)t=90; t\n' "${temps[idx]}" "$change" | bc)"
temps[idx]="$next"
}
printf 'Emulator running on port %s\n' "$PORT"
printf 'Serial emulated at: %s\n' "$SERIAL_DEV"
while true; do
for ((channel=0; channel<32; channel++)); do
generate_temp "$channel"
printf 'CH%02d: %s\n' "$((channel+1))" "${temps[channel]}" > "$SERIAL_DEV"
done
done
Make it executable:
chmod +x tc32_emulator.bash
The script maintains 32 temperatures between 10 and 90 degrees Celsius.
Each update changes a channel by at most 0.75 degrees and limits it to that
range. The stream format is CH01: <temperature> through CH32: <temperature>,
one channel per line. The update interval depends on the host.
Run and inspect the stream
-
In terminal 1, start the emulator on the default port:
./tc32_emulator.bashWait for
Emulator running on port 9399. Leave it running. -
In terminal 2, connect a client:
socat - TCP:127.0.0.1:9399You must see repeated channel numbers
CH01throughCH32, each followed by a number between 10 and 90. -
Stop the client with Ctrl+C before connecting the IOC from the next lesson. Stop the emulator with Ctrl+C when the exercise is finished.
-
To select another port, start another instance:
./tc32_emulator.bash --port 10001Connect its client to the same port:
socat - TCP:127.0.0.1:10001
Run several independent devices
In three terminals, start one emulator per port:
./tc32_emulator.bash --port 9399
./tc32_emulator.bash --port 9400
./tc32_emulator.bash --port 9401
Each process has its own temperatures and PTY. Stop each process with Ctrl+C. If GNU Parallel is installed, the equivalent launch is:
parallel ./tc32_emulator.bash --port ::: 9399 9400 9401
Verification
Check that the chosen ports are listening while the emulators run:
ss -ltn 'sport = :9399 or sport = :9400 or sport = :9401'
After stopping them, repeat the command; their listeners must be absent.
The emulator prints its retained diagnostic directory on exit. Each run
creates another one; remove a printed directory when you no longer need its
socat.log.
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.
4.6 IOC startup sequence
A compiled IOC combines its executable, linked support libraries, and runtime configuration. The startup file joins these parts in an order that lets records find their device support.
Files and runtime responsibilities
| Component | Provides |
|---|---|
bin/linux-x86_64/jeonglee_Demo | IOC program linked to the selected support |
dbd/jeonglee_Demo.dbd | Record types, devices, drivers, and registrars |
db/ | Record instances, templates, substitutions, and protocols |
iocsh/ | Installed reusable configuration fragments |
iocBoot/ | Startup commands and generated envPaths |
configure/RELEASE selects build dependencies. The generated envPaths
sets their runtime paths and the application’s TOP.
The startup in lesson 4.5 follows this order:
- Read
envPathsand define paths and macros. - Load the application DBD and call its support registration function.
- Configure the asyn port and its newline terminators.
- Load database records with their prefix and port macros.
- Call
iocInit()once. - Run diagnostics such as
dbl.
A DBD defines available types and support; it does not instantiate PVs.
dbLoadRecords creates record instances, and iocInit() initializes
those records and starts database scanning and servers. The IOC becomes
ready for clients when it prints iocRun: All initialization complete.
Drivers can create resources or connect to devices during configuration. Do not interpret the pre-initialization phase as a guarantee that no threads or device communication exist.
Initialization and diagnostics
iocInit() runs iocBuild() followed by iocRun() when the build
phase succeeds. The first phase prepares records and scanning; the
second enables database scanning and registered servers. The
initialization reference lists the Base sequence.
dbl lists loaded records even before initialization. Its output alone
does not prove that record processing or network access works. Client
reads and changing simulator values provide that evidence.
To inspect linked libraries, run this from the application root:
ldd bin/linux-x86_64/jeonglee_Demo
With the Chapter 3 support enabled, the list includes libasyn,
libstream, libcalc, libpvxs, and EPICS Base libraries. Every
required library must resolve to a file; not found indicates a missing
runtime dependency. Paths and memory addresses vary between hosts.
EPICS Base initialization reference
EPICS Base 7.0.10 implements iocInit() as iocBuild() || iocRun().
A failed build prevents the run phase. These names describe runtime
initialization, not compilation with make.
Build phase sequence
The source executes these major operations in order:
| Order | Operation | Purpose |
|---|---|---|
| 1 | iocBuild_1 | Checks the database definition, prepares the main thread, time support, watchdog, and callbacks |
| 2 | dbCaLinkInit | Prepares Channel Access database links |
| 3 | initDrvSup, initRecSup, initDevSup | Initializes driver, record, and device support |
| 4 | prepareLinks, dbLockInitRecords | Prepares record links and database lock sets |
| 5 | initDatabase, dbBkptInit | Initializes records and database debugging |
| 6 | finishDevSup | Completes device support initialization |
| 7 | scanInit, asInit, dbProcessNotifyInit | Prepares scanning, access security, and processing notifications |
| 8 | initialProcess | Processes records whose PINI is YES |
| 9 | dbInitServers | Initializes registered database servers |
| 10 | iocBuild_3 | Sets the built state and announces initHookAfterIocBuilt |
Callbacks and driver resources can exist during this phase. The
statement that no threads start before iocRun() is incorrect.
initDatabase performs the record initialization passes; it runs after
lock-set initialization. finishDevSup is a distinct routine, not a
second invocation of initDevSup.
Run phase sequence
iocRun() announces initHookAtIocRun, calls scanRun and dbCaRun,
and announces initHookAfterDatabaseRunning. It then calls
dbRunServers for a normal IOC build.
The first run announces initHookAtEnd, prints the initialization
completion message, sets the running state, and announces
initHookAfterIocRunning. Records with PINI=RUN or PINI=RUNNING
process at their corresponding run hooks; they are distinct from the
PINI=YES processing in the build phase.
The authoritative sequence is in
Base 7.0.10 iocInit.c,
functions iocBuild, iocBuild_2, iocRun, and initialProcess.
Chapter 4 example files
Use the complete file definitions in the lesson where each file is introduced. Run one IOC startup at a time.
| Goal | Files and lesson |
|---|---|
| Configure one TCP connection | training_device.iocsh and st2.cmd |
| Connect three TCP devices | st3.cmd |
| Generate a 32-record database | temperature.template and TC-32.substitutions |
| Compare database loading methods | st.cmd, st2.cmd, and st3.cmd |
| Connect several temperature devices | TC-32.iocsh and st_multi.cmd |
The application is jeonglee_Demo. The TCP lessons use
iocBoot/iocB46_182-jeonglee_Demo/; the temperature lessons use
iocBoot/ioctraining-jeonglee_Demo/.
Use the lesson’s verification commands to confirm record names, metadata, live values, and shutdown. A successful build or visible IOC prompt alone does not establish device communication.
Chapter 5: Inspect IOC application configuration
Follow the lessons in order; keep the application source for later exercises.
- Compare function-style and space-separated commands.
- Select dependency paths with RELEASE and local overrides.
- Change installation and IOC-visible paths.
- Run an operating-system command from the IOC shell.
Chapter exercises
- Save any existing
configure/RELEASE.local. SetEPICS_BASEto/nonexistent/epics/basein that file and runmake. Restore the saved content, or set it to the active Base path, and rebuild. - Complete the command-style comparison in lesson 5.1 and the installation-path exercise in lesson 5.3.
- Explain the difference between selecting dependencies in
RELEASEand selecting build options inCONFIG_SITE.
Before moving on
-
configure/RELEASE.localandconfigure/CONFIG_SITE.localinjeonglee_Demo/are restored somakesucceeds. -
iocBoot/ioctraining-jeonglee_Demo/envPathssetsTOPto yourjeonglee_Demodirectory, withdb/andiocsh/intact for Chapter 6. - Every
jeonglee_DemoIOC,softIoc, andtc32_emulator.bashprocess is stopped. -
ss -ltn 'sport = :9399'reports no active TCP listener.
5.1 Compare IOC shell command styles
Run the Chapter 4 temperature IOC with space-separated commands and compare its records with the function-style startup. Both forms call the same IOC shell commands.
Complete Generate and load 32 channel records
first and stop any IOC or simulator using port 9399. Open three Bash
terminals on the same host with these roles and settings:
| Terminal | Role | Working directory | Environment |
|---|---|---|---|
| Terminal 1 | TC-32 emulator (9399) | jeonglee_Demo/simulator/ | No EPICS environment required |
| Terminal 2 | Compiled IOC (./st4.cmd, ./st2.cmd) | 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 |
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".
-
In terminal 1 inside
jeonglee_Demo/simulator/, start one emulator:./tc32_emulator.bash --port 9399Wait for
Emulator running on port 9399and leave it running forst4.cmd. -
In terminal 2 inside
jeonglee_Demo/iocBoot/ioctraining-jeonglee_Demo/, save this complete startup asst4.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 ClockTime_Report dbl< envPathsreads another IOC shell file. It does not execute an operating-system shell command; lesson 5.4 explainssystem. -
In terminal 2 from that boot directory, start the file:
chmod +x st4.cmd ./st4.cmdThe IOC initializes, prints its clock report, and lists 32 records.
-
In terminal 3, direct CA and PVA searches to loopback and read several channels:
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 caget MYDEMO:TC32:CH01 MYDEMO:TC32:CH10 MYDEMO:TC32:CH32Each value is between
10and90. Stoptc32_emulator.bashwith Ctrl+C in terminal 1, stopst4.cmdwithexitin terminal 2, then restart./tc32_emulator.bash --port 9399and run./st2.cmdfrom the same boot directory to repeat the read. Both startups expose the same 32 names and units; temperatures vary.
Finish the comparison by stopping tc32_emulator.bash in terminal 1 with
Ctrl+C and then entering exit in terminal 2.
Command syntax reference
These forms set the same environment variable:
epicsEnvSet("DEVICE", "TC32:")
epicsEnvSet "DEVICE" "TC32:"
Quote arguments containing spaces. In dbLoadRecords, comma-separated
macro assignments remain inside one quoted argument in either style.
5.2 Dependency paths in RELEASE
configure/RELEASE identifies the EPICS Base and module installations
used to build an IOC application. These are build paths, not network
addresses or record macros.
Paths and configuration order
The generated application defines MODULES relative to EPICS_BASE,
then names the modules it uses. Its Base path comes from the environment
active when you run the generator.
| Variable | Installation selected |
|---|---|
EPICS_BASE | The matching 1.5.0 tree’s base directory |
MODULES | $(EPICS_BASE)/../modules |
ASYN | $(MODULES)/asyn |
CALC | $(MODULES)/calc |
STREAM | $(MODULES)/StreamDevice |
PVXS | $(MODULES)/pvxs |
Commented module definitions are inactive. Each active dependency must resolve to an absolute path containing its installed files.
Keep EPICS_BASE last among dependencies, as the template does. EPICS
uses this list when it builds include and library search paths. This
is distinct from assigning the same Make variable twice: a later
ordinary assignment replaces the earlier value.
Make’s recursive = assignments let MODULES refer to EPICS_BASE
before its definition. They do not copy the Base installation.
PVXS version configuration
The public 1.5.0 distribution contains PVXS 1.5.3. Its installed version
file is $(PVXS)/cfg/CONFIG_PVXS_VERSION. During application
configuration, EPICS includes installed cfg/CONFIG* files from
dependencies, which defines PVXS_MAJOR_VERSION for the application
Makefiles.
The generated RELEASE also contains this optional include:
-include $(PVXS)/configure/CONFIG_PVXS_VERSION
The installed tree has no version file under configure/, so this
line reads nothing and -include reports no error.
To print the version that the build sees, run this from the application root:
make -s PRINT.PVXS_MAJOR_VERSION
The build reports the major version:
PVXS_MAJOR_VERSION = 1
Local override files
The generated RELEASE includes these files in order:
-include $(TOP)/../RELEASE.local
-include $(TOP)/../RELEASE.$(EPICS_HOST_ARCH).local
-include $(TOP)/configure/RELEASE.local
The parent files apply to applications beneath that directory. The
final file applies to this application. EPICS_HOST_ARCH describes the
build host. -include permits a missing file.
Use the Chapter 4 jeonglee_Demo application, which has the training
instance. Before changing dependency paths, run this from its application
root:
make clean uninstall
For a local Base override, write the active installation’s absolute path.
Run these commands in a terminal where setEpicsEnv.bash is sourced;
otherwise EPICS_BASE is empty and the build fails:
printf 'EPICS_BASE=%s\n' "$EPICS_BASE" > configure/RELEASE.local
make
cat iocBoot/ioctraining-jeonglee_Demo/envPaths
The envPaths file must show the selected Base and module paths. Local files are
ignored by the generated application’s Git configuration; inspect them
when two machines build differently.
The EPICS build-system reference specifies these rules under RELEASE file and Modifying configure/RELEASE files.
5.3 Change an application’s installation path
Build the Chapter 4 application into a separate directory and inspect
how INSTALL_LOCATION and IOCS_APPL_TOP affect files and runtime
paths. These settings do not rebuild the binary distribution.
-
In the
jeonglee_Demo/application root, build with existing settings:makeKeep a copy of any existing
configure/CONFIG_SITE.localbefore replacing its contents in this exercise. -
Select an empty installation directory and set the local override:
INSTALL_DIR="$(mktemp -d)" printf 'INSTALL_LOCATION=%s\n' "$INSTALL_DIR" > configure/CONFIG_SITE.local make clean make ls "$INSTALL_DIR" cat iocBoot/ioctraining-jeonglee_Demo/envPathsProducts include
bin,db,dbd,iocsh, andlib. GeneratedTOPpoints toINSTALL_DIR. Boot scripts remain in the source application’siocBoot; the build does not install that directory. -
Compare cleanup targets:
make clean ls "$INSTALL_DIR/bin/linux-x86_64" make make distclean ls "$INSTALL_DIR" make installcleanremoves intermediate files and leaves installed products.distcleanalso removes installed products; directories can remain.make installrebuilds and installs the products. -
Distinguish the build host’s path from the IOC’s runtime path:
RUNTIME_DIR="$(mktemp -d)" printf 'IOCS_APPL_TOP=%s\n' "$RUNTIME_DIR" >> configure/CONFIG_SITE.local make clean make cat iocBoot/ioctraining-jeonglee_Demo/envPathsProducts remain in
INSTALL_DIR;envPathsusesRUNTIME_DIRforTOP. This describes where files are visible to the IOC. It does not copy them there. Do not start the IOC until that path has its files. -
Restore the application’s default path and rebuild:
printf '' > configure/CONFIG_SITE.local make clean make cat iocBoot/ioctraining-jeonglee_Demo/envPathsIf you had a local configuration, restore its saved copy instead.
TOPmust point to the intended application path. -
Remove the two temporary directories:
rm -rf "$INSTALL_DIR" "$RUNTIME_DIR"
Build option reference
CONFIG_SITE controls build settings; RELEASE selects dependencies.
Use configure/CONFIG_SITE.local for application-specific overrides.
| Setting | Effect |
|---|---|
CHECK_RELEASE | YES checks dependencies; WARN reports conflicts and continues; NO disables the check |
HOST_OPT, CROSS_OPT | Optimization for host and cross builds |
CROSS_COMPILER_TARGET_ARCHS | Cross targets for this application |
USR_CPPFLAGS, USR_CFLAGS, USR_LDFLAGS | Additional preprocessor, C compiler, and linker flags |
INSTALL_LOCATION | Installation directory, normally $(TOP) |
IOCS_APPL_TOP | IOC-visible application path used in boot configuration |
The generator sets CHECK_RELEASE=NO. The installed 1.5.0 Base sets
HOST_OPT=YES and CROSS_OPT=YES; inspect the selected tree rather than
assuming they are disabled. A local HOST_OPT=NO requests a non-optimized
host build for debugging.
5.4 Run an operating-system command
Use the IOC shell’s system command to run a Linux shell command.
An IOC command and an operating-system command use different entry points.
-
In the generated application root, find the DBD inclusion:
grep 'system.dbd' jeonglee_DemoApp/src/MakefileCommon_DBDs += system.dbdincludes the registrar forsystemin the application DBD. Support registration makes it available to the IOC. -
In an activated Bash terminal, start a soft IOC in a scratch directory:
SYSTEM_WORK="$(mktemp -d)" cd "$SYSTEM_WORK" softIoc -
At the IOC prompt, compare these commands:
pwd system pwd system(pwd) system("pwd")Each prints the working directory.
pwdis an IOC shell command; the other forms invoke the operating system’spwdthroughsystem. -
Pass commands containing spaces as one quoted argument:
system("mkdir test_folder") system("test -d test_folder && printf 'directory exists\n'")The second command prints
directory exists. An unquotedsystem mkdir test_foldersupplies two arguments to a command that expects one; it does not express the intended shell command. -
Stop the soft IOC:
exitIn the returned Bash shell, inspect the result:
ls -ld test_folderThe directory exists under
SYSTEM_WORK.systemuses the IOC process’s account, environment, and current directory. -
Leave and remove the scratch directory:
cd ~ rm -rf "$SYSTEM_WORK"
Chapter 6: Run IOCs with iocsh.bash
Run an Input/Output Controller (IOC) using installed module support without
compiling an application binary. The iocsh.bash loader resolves module
dependencies, loads shared libraries and database definition (DBD) files, and
starts softIocPVX with your startup configuration.
Complete Chapter 1 first. Lessons 6.2 and 6.3 reuse the simulator scripts, databases, protocols, and fragments created in Chapters 3 and 4.
- Run an IOC with installed modules:
inspect
iocsh.bash -noutput, loadbusy, and verify Channel Access (CA) and pvAccess (PVA) access. - Connect the TCP simulator:
run single-device and three-device
StreamDevicestartups by reusingtraining_device.iocshunchanged. - Read 32 temperature channels:
load
TC-32.dband expose a compositeQSRV2group process variable (PV) withdbLoadGroup. - Add host statistics, autosave, and regex search:
combine
linStat,autosave, andretoolswithcommonIocshto persist a setpoint across restarts. - Module selection and loader errors:
pin module versions, check exported macros in
cfg/iocsh.conf, and reproduce four startup error checks.
iocsh.bash loads installed record and device support. Application-specific C
or C++ code requires a compiled IOC application as shown in Chapters 2 through
5.
Chapter exercises
- In lesson 6.3, add a
ch08scalarmapping for$(P)$(R)CH08.VALtotc32_group.json. Stoptc32_emulator.bashin terminal 1 with Ctrl+C, enterexitin terminal 2, start./tc32_emulator.bash --port 9399again in terminal 1, and restartiocsh.bash st.cmdin terminal 2. Confirm thatpvxgl(1, "*")reportsAtomic Members:5andpvxget LOADER:TC32:Summaryincludesch08.value. - In lesson 6.4, add a second
aorecord named$(P)Limitwithfield(PINI, "YES")andinfo(autosaveFields, "VAL"). Write a value withcaput, wait 6 seconds, restart the IOC, and verify thatsettings.savrestores both records. - Run
iocsh.bash -nonst.cmdfrom lesson 6.4 and explain whycommonIocshfragments (linStat.iocshandautosave.iocsh) useiocshLoadrather thanmoduledirectives.
Before moving on
- Every
iocsh.bash(softIocPVX) session is stopped withexit. - Every
tcpserver.bashandtc32_emulator.bashprocess is stopped with Ctrl+C. -
ss -ltn 'sport = :9399 or sport = :9400 or sport = :9401'reports no active TCP listeners. - Temporary scratch directories from lessons 6.1 and 6.5 are removed or noted outside the EPICS distribution tree.
6.1 Run an IOC with installed modules
Create a process variable (PV) using the installed busy module, start an
Input/Output Controller (IOC) with iocsh.bash, and write and read the PV
through Channel Access (CA) and pvAccess (PVA).
The iocsh.bash loader starts the installed softIocPVX binary, resolves the
modules named in your startup file, loads their recorded dependencies, and
registers their database definition (DBD) support. This exercise needs no IOC
compilation or physical hardware.
Prerequisites
- Complete Install the EPICS environment.
- Ensure
readelffrombinutilsis installed on the host. - Confirm that no other IOC on this host publishes
TRAINING:Run.
Open two Bash terminals on the same host with these roles and settings:
| Terminal | Role | Working directory | Environment |
|---|---|---|---|
| Terminal 1 | IOC (iocsh.bash) | $IOC_WORK (mktemp -d) | 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 |
| Terminal 2 | 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 |
Prepare the database and startup
-
In both terminals, activate the tree matching your operating system and direct clients to loopback. If you installed the distribution outside
~/epics, setTREEto that installation’s directory instead of the second line:HOST_ID="$(. /etc/os-release && printf '%s-%s' "$ID" "$VERSION_ID")" TREE="$HOME/epics/1.5.0/$HOST_ID/7.0.10" 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 -
In terminal 1, create a working directory outside the distribution:
IOC_WORK="$(mktemp -d)" cd "$IOC_WORK" pwdKeep terminal 1 in this directory. Relative database paths resolve from the current working directory even when you pass an absolute startup filename.
-
In
$IOC_WORK, save this database asbusy.db:record(busy, "$(P)Run") { field(DESC, "Training loader demonstration") field(ZNAM, "Idle") field(ONAM, "Busy") } -
Save this startup file as
st.cmdbesidebusy.db:on error break module busy dbLoadRecords("busy.db", "P=TRAINING:") iocInit()module busyselects the installed default version ofbusy. Your startup loads record instances and callsiocInit()once. -
In terminal 1, print the generated startup without starting the IOC:
iocsh.bash -n st.cmdThe preview prints the resolved dependency graph and startup sequence:
# iocsh.bash: generated startup for /tmp/tmp.ysekybke1K/st.cmd (runs as /dev/fd/3) # iocsh.bash: static ELF inspection of 156 dependency edges; resolved files as class, owner, file # elf: module pvxs-1.5.3 /home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/pvxs-1.5.3/lib/linux-x86_64/libpvxsIoc.so.1.5 # elf: base - /home/jeonglee/epics/1.5.0/debian-13/7.0.10/base/lib/linux-x86_64/libdbCore.so.3.25.0 # elf: system - /usr/lib/x86_64-linux-gnu/libevent_core-2.1.so.7.0.2 # Additional # elf: lines and dependency loads (seq, sscan, calc, asyn, autosave) omitted on error break # busy a4a272d dlload("/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/busy-a4a272d/lib/linux-x86_64/libbusy.so") dbLoadDatabase("/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/busy-a4a272d/dbd/busySupport.dbd", "/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/busy-a4a272d/dbd:/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/sscan-ce9660c/dbd:/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/calc-3.8.0/dbd:/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/asyn-4.46.0/dbd:/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/autosave-6.0.0/dbd:/home/jeonglee/epics/1.5.0/debian-13/7.0.10/base/dbd:/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/pvxs-1.5.3/dbd", "") epicsEnvSet("SNCSEQ", "/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/seq-2.2.9") epicsEnvSet("SSCAN", "/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/sscan-ce9660c") epicsEnvSet("CALC", "/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/calc-3.8.0") epicsEnvSet("ASYN", "/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/asyn-4.46.0") epicsEnvSet("AUTOSAVE", "/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/autosave-6.0.0") epicsEnvSet("BUSY", "/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/busy-a4a272d") registerAllRecordDeviceDrivers(pdbbase) iocshLoad("/dev/fd/4") # iocsh.bash: /tmp/tmp.ysekybke1K/st.cmd runs as /dev/fd/4 with 4 lines, directives replaced by commentsEach part of this generated startup serves a distinct role:
# elf:lines verify every shared library edge withreadelfacrossmodule,base, andsystemclasses before the IOC starts.dlloadanddbLoadDatabaseloadbusyand its transitive dependencies (seq,sscan,calc,asyn, andautosave) once in dependency order.epicsEnvSetexports each module’s install directory macro, such asBUSY, from its installedcfg/iocsh.conf.registerAllRecordDeviceDrivers(pdbbase)registers all loaded support once before your database records load./dev/fd/3holds the generated wrapper, while/dev/fd/4holds a line-preserving copy ofst.cmdwithmodulelines turned into comments. Keepst.cmdas your saved startup file; the/dev/fd/paths exist only during one loader invocation.
Start the IOC and read its PV
-
In terminal 1, start the IOC from
$IOC_WORK:iocsh.bash st.cmdWait for
iocRun: All initialization completeand leave the IOC running. -
In terminal 2, read the initial state through CA:
caget -t TRAINING:RunThe initial state is
Idle:Idle -
In terminal 2, set the busy state and confirm it through CA:
caput TRAINING:Run 1 caget -t TRAINING:RunThe CA client prints the state transition and current value:
Old : TRAINING:Run Idle New : TRAINING:Run Busy Busy -
In terminal 2, read the same PV through PVA:
pvxget TRAINING:RunQSRV2insidesoftIocPVXreturns anNTEnumstructure with index1and both state choices:TRAINING:Run value.index int32_t = 1 value.choices string[] = {2}["Idle", "Busy"] alarm.severity int32_t = 0 alarm.status int32_t = 0 alarm.message string = "" timeStamp.secondsPastEpoch int64_t = 1794421214 timeStamp.nanoseconds int32_t = 338775272 timeStamp.userTag int32_t = 0 display.description string = "Training loader demonstration" -
In terminal 2, return the record to
Idle:caput TRAINING:Run 0 caget -t TRAINING:RunCA reports
Idle:Old : TRAINING:Run Busy New : TRAINING:Run Idle Idle -
In terminal 1, stop the IOC at the
iocshprompt:exit
Pin module versions and select trees
To check which directory the default busy symlink targets, run:
readlink -f "$EPICS_MODULES/busy"
In EPICS-env 1.5.0, the resolved directory ends in busy-a4a272d. To pin that
exact version in st.cmd, specify the version token:
module busy a4a272d
Re-run iocsh.bash -n st.cmd after changing module directives. Put module
directives only in the top-level startup file passed to iocsh.bash; entering
them at the iocsh prompt or inside an iocshLoad fragment fails because
module is handled by iocsh.bash, not by softIocPVX.
To preview a startup from a shell where setEpicsEnv.bash has not been
sourced, pass -e with the tree root:
"$TREE/base/bin/linux-x86_64/iocsh.bash" -e "$TREE" -n st.cmd
Set TREE to the absolute installation path first. The -e option activates
the environment only inside iocsh.bash; client shells require
setEpicsEnv.bash for caget and pvxget.
Chapters 2 through 5 build compiled IOC executables that link their own DBD
and registration function. Keep those startup files for compiled IOCs, and use
module directives when launching through iocsh.bash.
Verification
Confirm that iocsh.bash -n st.cmd exits with status 0 before starting the
IOC:
iocsh.bash -n st.cmd
echo "$?"
The status code is 0:
0
During the live run, TRAINING:Run transitions from Idle to Busy and back
to Idle, pvxget reports value.index int32_t = 1 while busy, and exit
returns terminal 1 to the Bash prompt.
6.2 Connect the TCP simulator
Run the Chapter 3 TCP database and protocol, and then the Chapter 4 three-device
fragment, through iocsh.bash without compiling an Input/Output Controller
(IOC) executable. You send commands through StreamDevice and read replies over
Channel Access (CA) and pvAccess (PVA).
Prerequisites
- Complete Configure an IOC for TCP communication, Run a TCP echo simulator, Reuse a device startup fragment, and Run a command-aware TCP simulator.
- Stop any IOC or simulator listening on ports
9399,9400, or9401. - Activate the matching EPICS-env 1.5.0 tree in the IOC and client terminals.
Open three Bash terminals on the same host for the single-device setup, and add terminals 4 and 5 for the three-device section:
| Terminal | Role | Working directory | Environment |
|---|---|---|---|
| Terminal 1 | TCP simulator (9399) | jeonglee_Demo/simulator/ | No EPICS environment required |
| Terminal 2 | IOC (iocsh.bash) | jeonglee_Demo/ then jeonglee_Demo/loader_tcp/ | 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 TCP simulators (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".
Prepare the single-device startup
-
In terminal 2 from your
jeonglee_Demo/application directory, copy the database and protocol files into a loader directory:mkdir -p loader_tcp cp jeonglee_DemoApp/Db/training.db loader_tcp/ cp jeonglee_DemoApp/Db/training.proto loader_tcp/ cd loader_tcp -
In terminal 2 inside
loader_tcp/, save this startup file asst.cmd:on error break module StreamDevice epicsEnvSet("STREAM_PROTOCOL_PATH", ".") drvAsynIPPortConfigure("TCP", "127.0.0.1:9399", 0, 0, 0) asynOctetSetInputEos("TCP", 0, "\n") asynOctetSetOutputEos("TCP", 0, "\n") dbLoadRecords("training.db", "P=LOADER:,R=SIM:,PORT=TCP") iocInit()module StreamDeviceselectsStreamDeviceand its recorded dependencies, includingasynandcalc.iocsh.bashhandles library loading and support registration, whilest.cmdconfigures the TCP port, loads the records, and callsiocInit(). -
In terminal 2 inside
loader_tcp/, preview the generated startup:iocsh.bash -n st.cmd
Run the single-device connection
-
In terminal 1 inside
jeonglee_Demo/simulator/, start the echo server on port9399:./tcpserver.bash 9399 -
In terminal 2 inside
loader_tcp/, start the IOC:iocsh.bash st.cmd -
In terminal 3, direct CA and PVA searches to loopback:
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 -
In terminal 3, send a query with
caput -cand read the reply PV (Cmd-RB) through CA:caput -c LOADER:SIM:Cmd "DEVICE_ID?" caget LOADER:SIM:Cmd-RBCA confirms the write and returns
DEVICE_ID?:Old : LOADER:SIM:Cmd New : LOADER:SIM:Cmd DEVICE_ID? LOADER:SIM:Cmd-RB DEVICE_ID? -
Read the same reply PV through PVA:
pvxget LOADER:SIM:Cmd-RBQSRV2exposesCmd-RBas anNTScalarstring structure:LOADER:SIM:Cmd-RB value string = "DEVICE_ID?" alarm.severity int32_t = 0 alarm.status int32_t = 0 alarm.message string = "" timeStamp.secondsPastEpoch int64_t = 1794422274 timeStamp.nanoseconds int32_t = 859089854 timeStamp.userTag int32_t = 0 display.description string = "Raw query readback" -
Send a second query to confirm repeated message exchange:
caput -c LOADER:SIM:Cmd "STATUS?" caget LOADER:SIM:Cmd-RBThe readback updates to
STATUS?:Old : LOADER:SIM:Cmd DEVICE_ID? New : LOADER:SIM:Cmd STATUS? LOADER:SIM:Cmd-RB STATUS? -
Enter
exitat theiocshprompt in terminal 2 and stop the echo server in terminal 1 with Ctrl+C before starting the three-device setup.
Reuse the Chapter 4 fragment across three devices
-
In terminal 2 inside
loader_tcp/, copytraining_device.iocshfrom Chapter 4 without changing a line:cp ../iocsh/training_device.iocsh . -
In terminal 2 inside
loader_tcp/, save this three-device startup file asst_multi.cmd:on error break module StreamDevice epicsEnvSet("DATABASE_TOP", ".") epicsEnvSet("STREAM_PROTOCOL_PATH", ".") epicsEnvSet("PREFIX", "LOADER:") iocshLoad("training_device.iocsh", "DEVICE=SIM1:,PORT_NAME=TCP1,PORT=9399") iocshLoad("training_device.iocsh", "DEVICE=SIM2:,PORT_NAME=TCP2,PORT=9400") iocshLoad("training_device.iocsh", "DEVICE=SIM3:,PORT_NAME=TCP3,PORT=9401,ASYNTRACE=") iocInit()module StreamDeviceappears once inst_multi.cmd, never insidetraining_device.iocsh.iocsh.bashscans only the top-level startup file before launchingsoftIocPVXand loads each module once for the process. Keepingtraining_device.iocshfree of loader directives lets compiled IOCs (Chapter 4) andiocsh.bashshare the exact same fragment. -
Start one command-aware simulator per port from
jeonglee_Demo/simulator/. In terminal 1, listen on port9399:./tcpserver.bash 9399 advanced_connection_handler.shIn terminal 4, listen on port
9400:./tcpserver.bash 9400 advanced_connection_handler.shIn terminal 5, listen on port
9401:./tcpserver.bash 9401 advanced_connection_handler.sh -
In terminal 2 inside
loader_tcp/, start the three-device IOC:iocsh.bash st_multi.cmd -
In terminal 3, request each handler’s process ID (PID) and read all three replies:
caput -c LOADER:SIM1:Cmd "GetID?" caput -c LOADER:SIM2:Cmd "GetID?" caput -c LOADER:SIM3:Cmd "GetID?" caget LOADER:SIM1:Cmd-RB LOADER:SIM2:Cmd-RB LOADER:SIM3:Cmd-RBEach connection reports a distinct handler PID:
LOADER:SIM1:Cmd-RB 242338 LOADER:SIM2:Cmd-RB 242342 LOADER:SIM3:Cmd-RB 242344 -
Query a simulated temperature from each device:
caput -c LOADER:SIM1:Cmd "GetTemp?" caput -c LOADER:SIM2:Cmd "GetTemp?" caput -c LOADER:SIM3:Cmd "GetTemp?" caget LOADER:SIM1:Cmd-RB LOADER:SIM2:Cmd-RB LOADER:SIM3:Cmd-RBEach readback returns an integer between
0and100, whileTCP3printsasyntrace lines in terminal 2:LOADER:SIM1:Cmd-RB 53 LOADER:SIM2:Cmd-RB 65 LOADER:SIM3:Cmd-RB 11
Verification
In terminal 3, check alarm status and severity across all three command records while the simulators run:
caget LOADER:SIM1:Cmd.SEVR LOADER:SIM2:Cmd.SEVR LOADER:SIM3:Cmd.SEVR
Every record reports NO_ALARM:
LOADER:SIM1:Cmd.SEVR NO_ALARM
LOADER:SIM2:Cmd.SEVR NO_ALARM
LOADER:SIM3:Cmd.SEVR NO_ALARM
Enter exit in terminal 2 and stop the simulators in terminals 1, 4, and 5
with Ctrl+C when finished.
6.3 Read 32 temperature channels
Run the Chapter 4 32-channel temperature database through iocsh.bash without
the compiled jeonglee_Demo executable, and group representative channels into
one structured pvAccess (PVA) object with dbLoadGroup. You inspect both
individual Channel Access (CA) and PVA records and a composite QSRV2 group
process variable (PV) served by the Input/Output Controller (IOC).
Prerequisites
- Complete Run a 32-channel temperature simulator and Generate and load 32 channel records.
- Confirm that
db/TC-32.dbanddb/tc32.protoexist injeonglee_Demo/with$(P)$(R)macros intact after the Chapter 4 exercises. - Stop any IOC or simulator listening on port
9399. - Activate the matching EPICS-env 1.5.0 tree in the IOC and client terminals.
Open three Bash terminals on the same host with these roles and settings:
| Terminal | Role | Working directory | Environment |
|---|---|---|---|
| Terminal 1 | TC-32 emulator (9399) | jeonglee_Demo/simulator/ | No EPICS environment required |
| Terminal 2 | IOC (iocsh.bash) | jeonglee_Demo/ then jeonglee_Demo/loader_tc32/ | 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 |
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".
Prepare the database, group JSON, and startup
-
In terminal 2 from your
jeonglee_Demo/application directory, copy the generated database and protocol into a loader directory:mkdir -p loader_tc32 cp db/TC-32.db db/tc32.proto loader_tc32/ cd loader_tc32 -
In terminal 2 inside
loader_tc32/, save thisQSRV2group definition astc32_group.json:{ "$(P)$(R)Summary": { "+id": "training:nt/TC32Summary:1.0", "device.model": {"+type": "const", "+const": "TC-32"}, "ch01": {"+type": "scalar", "+channel": "$(P)$(R)CH01.VAL"}, "ch16": {"+type": "scalar", "+channel": "$(P)$(R)CH16.VAL"}, "ch32": {"+type": "scalar", "+channel": "$(P)$(R)CH32.VAL", "+trigger": "*"} } }Each mapping key configures how
QSRV2assemblesLOADER:TC32:Summary:"+id"assigns the top-level PVA structure identifiertraining:nt/TC32Summary:1.0."+type": "const"with"+const": "TC-32"publishes a constantdevice.modelstring field without an underlying EPICS record."+type": "scalar"embeds each selected channel’sNTScalarvalue, alarm, timestamp, and display metadata from$(P)$(R)CH*.VAL."+trigger": "*"onch32publishes an atomic subscription update across all group fields when channel 32 finishes a streaming sweep. Omitting+putorderkeeps the group read-only.
-
In terminal 2 inside
loader_tc32/, save this startup file asst.cmd:on error break module StreamDevice epicsEnvSet("STREAM_PROTOCOL_PATH", ".") drvAsynIPPortConfigure("TC32", "127.0.0.1:9399", 0, 0, 0) asynOctetSetInputEos("TC32", 0, "\n") asynOctetSetOutputEos("TC32", 0, "\n") dbLoadRecords("TC-32.db", "P=LOADER:,R=TC32:,PORT=TC32") dbLoadGroup("tc32_group.json", "P=LOADER:,R=TC32:") iocInit()softIocPVXlinkspvxswithQSRV2enabled by default, sodbLoadGroupneeds no extramoduledirective inst.cmd. -
In terminal 2 inside
loader_tc32/, preview the generated startup before launching the IOC:iocsh.bash -n st.cmd
Read scalar channels and the structured group
-
In terminal 1 inside
jeonglee_Demo/simulator/, start the TC-32 emulator on port9399:./tc32_emulator.bash --port 9399 -
In terminal 2 inside
loader_tc32/, start the IOC:iocsh.bash st.cmd -
At the
iocshprompt in terminal 2, list the registeredQSRV2group definition:pvxgl(1, "*")pvxglreports the group name, atomic delivery mode, and four mapped members (device.model,ch01,ch16, andch32):LOADER:TC32:Summary Atomic Get/Put:yes Atomic Members:4 -
In terminal 3, direct CA and PVA searches to loopback:
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 -
In terminal 3, read three scalar temperature channels and channel 01 metadata through CA:
caget LOADER:TC32:CH01 LOADER:TC32:CH16 LOADER:TC32:CH32 caget LOADER:TC32:CH01.EGU LOADER:TC32:CH01.DESC LOADER:TC32:CH01.SEVRLive temperatures fall between
10and90degreesCelsiuswithNO_ALARMseverity:LOADER:TC32:CH01 77.1052 LOADER:TC32:CH16 86.3493 LOADER:TC32:CH32 76.6859 LOADER:TC32:CH01.EGU Celsius LOADER:TC32:CH01.DESC TC temperature at Channel 01 LOADER:TC32:CH01.SEVR NO_ALARM -
In terminal 3, read channel 01 as an individual PVA
NTScalarrecord:pvxget LOADER:TC32:CH01pvxgetprints the value, alarm, timestamp, and engineering units from the sameairecord:LOADER:TC32:CH01 value double = 76.4265 alarm.severity int32_t = 0 alarm.status int32_t = 0 alarm.message string = "" timeStamp.secondsPastEpoch int64_t = 1794422277 timeStamp.nanoseconds int32_t = 691465958 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 -
In terminal 3, read the composite
LOADER:TC32:Summarygroup PV in a single PVA request:pvxget LOADER:TC32:SummaryOne atomic PVA read returns
ch01,ch16,ch32, anddevice.modeltogether from the server:LOADER:TC32:Summary record._options.atomic bool = true ch01.value double = 76.4265 ch01.alarm.severity int32_t = 0 ch01.alarm.status int32_t = 0 ch01.alarm.message string = "" ch01.timeStamp.secondsPastEpoch int64_t = 1794422277 ch01.timeStamp.nanoseconds int32_t = 691465958 ch01.timeStamp.userTag int32_t = 0 ch01.display.limitLow double = 0 ch01.display.limitHigh double = 0 ch01.display.description string = "TC temperature at Channel 01" ch01.display.units string = "Celsius" ch01.display.precision int32_t = 0 ch01.display.form.index int32_t = 0 ch01.display.form.choices string[] = {7}["Default", "String", "Binary", "Decimal", "Hex", "Exponential", "Engineering"] ch01.control.limitLow double = 0 ch01.control.limitHigh double = 0 ch01.valueAlarm.lowAlarmLimit double = nan ch01.valueAlarm.lowWarningLimit double = nan ch01.valueAlarm.highWarningLimit double = nan ch01.valueAlarm.highAlarmLimit double = nan ch16.value double = 86.5753 ch16.alarm.severity int32_t = 0 ch16.alarm.status int32_t = 0 ch16.alarm.message string = "" ch16.timeStamp.secondsPastEpoch int64_t = 1794422277 ch16.timeStamp.nanoseconds int32_t = 729144531 ch16.timeStamp.userTag int32_t = 0 # Repeated display, control, and valueAlarm fields omitted for ch16 and ch32 ch32.value double = 77.3631 ch32.alarm.severity int32_t = 0 ch32.alarm.status int32_t = 0 ch32.alarm.message string = "" ch32.timeStamp.secondsPastEpoch int64_t = 1794422277 ch32.timeStamp.nanoseconds int32_t = 688521675 ch32.timeStamp.userTag int32_t = 0 device.model string = "TC-32"
Verification
-
In terminal 3, read all 32 scalar channels once to confirm every record receives live stream updates between
10and90:for ch in {01..32}; do caget -t "LOADER:TC32:CH$ch"; done -
In terminal 3, inspect the published PVA type identifier on
LOADER:TC32:Summary:pvxinfo LOADER:TC32:Summary | head -n 3The structure header shows the custom
+idcontract:LOADER:TC32:Summary from 127.0.0.1:43259 struct "training:nt/TC32Summary:1.0" { struct { -
In terminal 3, subscribe to three scalar channels with
camonitorto verify continuous updates:camonitor LOADER:TC32:CH01 LOADER:TC32:CH16 LOADER:TC32:CH32Stop
camonitorin terminal 3 with Ctrl+C, stoptc32_emulator.bashin terminal 1 with Ctrl+C, and then enterexitin terminal 2.
6.4 Add host statistics, autosave, and regex search
Load Linux host and process metrics (linStat), automatic setpoint persistence
(autosave), and regular-expression database inspection (retools) through
iocsh.bash and the shared commonIocsh startup scripts. Unlike
Run your first IOC, where runtime writes reset when
the Input/Output Controller (IOC) stops, autosave restores your written
process variable (PV) value across restarts over Channel Access (CA) and
pvAccess (PVA).
Prerequisites
- Complete Run your first IOC and Run an IOC with installed modules.
- Confirm that no other IOC on this host publishes the
LOADER_SVC:orLOADER_SVC-as:prefixes.
Open two Bash terminals on the same host with these roles and settings:
| Terminal | Role | Working directory | Environment |
|---|---|---|---|
| Terminal 1 | IOC (iocsh.bash) | ~/ioc-training/loader_services/ | 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 |
| Terminal 2 | 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 |
Create the setpoint database and service startup
-
In both terminals, activate your EPICS-env 1.5.0 tree and direct CA and PVA searches to loopback. If you installed the distribution outside
~/epics, setTREEto that installation’s directory instead of the second line:HOST_ID="$(. /etc/os-release && printf '%s-%s' "$ID" "$VERSION_ID")" TREE="$HOME/epics/1.5.0/$HOST_ID/7.0.10" 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 -
In terminal 1, create a service directory outside the distribution:
mkdir -p ~/ioc-training/loader_services cd ~/ioc-training/loader_services -
In
~/ioc-training/loader_services/, save this database assetpoint.db:record(ao, "$(P)Setpoint") { field(DESC, "Saved temperature setpoint") field(EGU, "degC") field(VAL, "20") field(PINI, "YES") info(autosaveFields, "VAL") }field(PINI, "YES")processes the analog output (ao) record once duringiocInitso its alarm severity clears toNO_ALARMafter boot-time restore.info(autosaveFields, "VAL")marksVALfor automatic request-file generation byautosave.iocsh. -
Save this startup file as
st.cmdbesidesetpoint.db:on error break module linStat module autosave module retools epicsEnvSet("IOC", "LOADER_SVC") epicsEnvSet("IOCSH_TOP", "$(EPICS_MODULES)/commonIocsh") epicsEnvSet("AS_TOP", "./var") iocshLoad("$(IOCSH_TOP)/iocsh/linStat.iocsh") iocshLoad("$(IOCSH_TOP)/iocsh/autosave.iocsh") dbLoadRecords("setpoint.db", "P=$(IOC):") iocInit()Each setting coordinates
iocsh.bashwithcommonIocsh:module linStat,module autosave, andmodule retoolsload those libraries and exportLINSTAT,AUTOSAVE, andRETOOLSautomatically.IOCis set toLOADER_SVCwithout a trailing colon.autosave.iocshuses$(IOC)as a directory name under./var/LOADER_SVC/and builds the status prefixLOADER_SVC-as:, whilelinStat.iocshsupplies its own colon inLOADER_SVC:UPTIMEandLOADER_SVC:PROCESS_ID. PassingP=$(IOC):todbLoadRecordskeeps$(P)Setpointaligned with the trailing-colonPmacro used across Chapters 3, 4, and 6.- Do not load
linStat.iocshtogether withiocStatsAdmin.iocshin the same IOC. Both fragments define identically named host and process records such as$(IOC):UPTIMEand$(IOC):FD_CNT.
-
Preview the generated startup in terminal 1:
iocsh.bash -n st.cmd
Save a setpoint and inspect live services
-
In terminal 1, start the first IOC run from
~/ioc-training/loader_services/:iocsh.bash st.cmdDuring the initial boot into an empty
./vardirectory,dbrestore.creports that./var/LOADER_SVC/save/settings.savdoes not exist yet. AfteriocInit()completes,afterIocRunninggenerates./var/LOADER_SVC/req/settings.reqfrominfo(autosaveFields, "VAL")and starts a 5-second monitor set. -
At the
iocshprompt in terminal 1, filter loaded record names withreGrep:reGrep("LOADER_SVC:(Setpoint|PROCESS_ID|FD_CNT|UPTIME)")reGrepmatches full record names against a regular expression and prints the four matching records:LOADER_SVC:Setpoint LOADER_SVC:PROCESS_ID LOADER_SVC:FD_CNT LOADER_SVC:UPTIMEUnlike
dbgrep, which uses shell-style*wildcards,reGreprequires the pattern to match the entire record name (.*Setpoint.*or an explicit prefix such asLOADER_SVC:.*). -
In terminal 2, read the initial setpoint,
linStatprocess metrics, andautosavestatus records:caget LOADER_SVC:Setpoint LOADER_SVC:PROCESS_ID LOADER_SVC:FD_CNT LOADER_SVC:UPTIME caget LOADER_SVC-as:SR_0_Name LOADER_SVC-as:SR_statusStrLOADER_SVC:Setpointstarts at20, whilelinStatreports the live IOC process ID (PID), open file-descriptor count, and uptime.PROCESS_IDandFD_CNTread0until their first scan; repeat the read after a few seconds:LOADER_SVC:Setpoint 20 LOADER_SVC:PROCESS_ID 361766 LOADER_SVC:FD_CNT 33 LOADER_SVC:UPTIME 2 S LOADER_SVC-as:SR_0_Name settings LOADER_SVC-as:SR_statusStr Enabling list... -
Write
37.5toLOADER_SVC:Setpointand wait 6 seconds past the default 5-secondSETTINGS_PERIODsave interval:caput LOADER_SVC:Setpoint 37.5 sleep 6CA confirms the setpoint update:
Old : LOADER_SVC:Setpoint 20 New : LOADER_SVC:Setpoint 37.5 -
Inspect the generated request file and saved value file on disk:
cat ~/ioc-training/loader_services/var/LOADER_SVC/req/settings.req cat ~/ioc-training/loader_services/var/LOADER_SVC/save/settings.savsettings.reqlists the tagged field andsettings.savstores37.5before<END>:LOADER_SVC:Setpoint.VAL # autosave R5.3 Automatically generated - DO NOT MODIFY - 261111-102556 LOADER_SVC:Setpoint.VAL 37.5 <END> -
In terminal 1, stop the first IOC run:
exit
Verification
-
In terminal 1, start a second IOC run from
~/ioc-training/loader_services/:iocsh.bash st.cmd -
Without writing to the record, read
LOADER_SVC:Setpointin terminal 2 through CA:caget LOADER_SVC:Setpoint LOADER_SVC:Setpoint.SEVRautosaverestores37.5duringiocInit, andPINIclears the record alarm state toNO_ALARM:LOADER_SVC:Setpoint 37.5 LOADER_SVC:Setpoint.SEVR NO_ALARM -
Read the restored record through PVA:
pvxget LOADER_SVC:Setpointpvxgetconfirmsvalue double = 37.5andalarm.severity int32_t = 0:LOADER_SVC:Setpoint value double = 37.5 alarm.severity int32_t = 0 alarm.status int32_t = 0 alarm.message string = "" timeStamp.secondsPastEpoch int64_t = 1794421561 timeStamp.nanoseconds int32_t = 770665617 timeStamp.userTag int32_t = 0 display.limitLow double = 0 display.limitHigh double = 0 display.description string = "Saved temperature setpoint" display.units string = "degC" 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 = nanIn Run your first IOC,
water.dbreturns to0on every restart because no save set writes changes to disk. Enterexitin terminal 1 when finished.
6.5 Module selection and loader errors
Inspect Input/Output Controller (IOC) startup generation, pin installed module
versions, check exported module macros, and diagnose the four main error
patterns reported by iocsh.bash and softIocPVX.
Loader command options
| Option | Long form | Behavior |
|---|---|---|
| (none) | (none) | Start an interactive IOC from st.cmd |
-n | --show | Print the generated startup and exit without starting the IOC |
-S | --non-interactive | Disable the interactive iocsh prompt |
-v | --verbose | Also display the built-in startup commands of softIocPVX |
-e "$TREE" | --environment "$TREE" | Source "$TREE/setEpicsEnv.bash" inside the loader process |
-h | --help | Print usage information and exit |
Run iocsh.bash from the directory containing st.cmd and its relative
database files. When using -e "$TREE", pass an absolute installation path;
-e sets environment variables only inside the loader process, not in your
interactive client shell.
Module directive syntax
Place one module directive per line in the top-level startup file passed to
iocsh.bash to follow the default symlink:
module StreamDevice
Append an explicit version token to pin one installed build:
module StreamDevice 2.8.26
iocsh.bash also accepts the function-style syntax:
module("StreamDevice", "2.8.26")
mod and m work as short aliases for module. Directives placed inside an
iocshLoad fragment or typed at the interactive iocsh prompt fail because
module is parsed by iocsh.bash before softIocPVX starts.
Installed module metadata and macro exports
In an activated shell (set TREE as in
lesson 1.1 and run
source "$TREE/setEpicsEnv.bash" if opening a fresh terminal), inspect
StreamDevice’s default symlink target and its loader metadata file:
readlink -f "$EPICS_MODULES/StreamDevice"
cat "$EPICS_MODULES/StreamDevice/cfg/iocsh.conf"
cfg/iocsh.conf records the build’s EPICS Base version, target architecture,
exported environment macro, transitive module dependencies, shared libraries,
and database definition (DBD) files:
/home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/StreamDevice-2.8.26
format=1
name=StreamDevice
version=2.8.26
base=7.0.10
arch=linux-x86_64
macro=STREAM
dep=asyn 4.46.0
dep=calc 3.8.0
lib=lib/linux-x86_64/libstream.so
dbd=dbd/stream.dbd
For every resolved module, iocsh.bash reads macro= from cfg/iocsh.conf and
emits epicsEnvSet("<MACRO>", "<module_dir>") before running your startup.
Loading StreamDevice exports STREAM, ASYN, CALC, SSCAN, and SNCSEQ;
loading linStat, autosave, and retools exports LINSTAT, AUTOSAVE, and
RETOOLS. Shared fragments under $(EPICS_MODULES)/commonIocsh/iocsh/ use
these macros to locate installed databases without hardcoded version paths.
Startup errors to recognize
| Error condition | Stage | Corrective action |
|---|---|---|
| Module or requested version absent | iocsh.bash preflight | Select a module name and version present under $EPICS_MODULES |
| Transitive and explicit versions conflict | iocsh.bash preflight | Keep one compatible version per module across the IOC |
module placed inside iocshLoad fragment | softIocPVX runtime | Move all module directives to the top-level st.cmd |
Unknown command or syntax error in st.cmd | softIocPVX runtime (/dev/fd/4) | Fix the reported line number in st.cmd |
| Database or protocol path missing | softIocPVX runtime | Start from the expected directory or correct the path macro |
A clean iocsh.bash -n preview proves module resolution and shared-library
linkage. Run the IOC and read its process variables (PVs) to confirm database
loading, record initialization, and device communication.
Reproduce four loader diagnostic checks
Run these checks in an activated shell (set TREE as in
lesson 1.1 and run
source "$TREE/setEpicsEnv.bash" if opening a fresh terminal). Create a
temporary directory outside the distribution to test four failure modes without
affecting earlier workspaces:
DIAG_WORK="$(mktemp -d)"
cd "$DIAG_WORK"
Check a missing module
Save a startup referencing a nonexistent module as st_missing.cmd:
module missing_training_module
iocInit()
Run a dry-run preview against st_missing.cmd:
iocsh.bash -n st_missing.cmd
iocsh.bash exits with status 1 before starting softIocPVX and names the
file and line number:
iocsh.bash: Module missing_training_module has no installed default link: /home/jeonglee/epics/1.5.0/debian-13/7.0.10/modules/missing_training_module (st_missing.cmd:1)
Check a dependency version conflict
Save a startup requesting both StreamDevice (built against asyn 4.46.0) and
an incompatible asyn version as st_conflict.cmd:
module StreamDevice
module asyn 4.44.0
iocInit()
Preview st_conflict.cmd to trigger dependency resolution:
iocsh.bash -n st_conflict.cmd
iocsh.bash exits with status 1 and identifies both conflicting requests:
iocsh.bash: Module version conflict: asyn
Requested: 4.44.0 at st_conflict.cmd:2
Selected: 4.46.0 at StreamDevice 2.8.26 (st_conflict.cmd:1)
Use one version of asyn in this IOC.
Check a module directive inside a fragment
Save a module directive inside frag.iocsh:
module busy
Save a top-level startup that loads frag.iocsh as st_frag.cmd:
on error break
iocshLoad("frag.iocsh")
iocInit()
Run st_frag.cmd with standard input closed so softIocPVX exits after
processing the startup:
iocsh.bash st_frag.cmd < /dev/null
Because iocsh.bash scans only st_frag.cmd, softIocPVX reads frag.iocsh
directly and rejects module as an unregistered IOC shell command:
ERROR frag.iocsh line 1: Command 'module' not registered.
Check line-number mapping on /dev/fd/4
Save a five-line startup with an invalid command on line 4 as st_fd4.cmd:
on error break
module busy
# line 3 comment
unknownStartupCommand()
iocInit()
Run st_fd4.cmd non-interactively with -S:
iocsh.bash -S st_fd4.cmd
iocsh.bash replaces line 2 (module busy) with a comment rather than
deleting lines, so ERROR 4 line 4 points directly to line 4 of st_fd4.cmd
and on error break exits with status 2:
iocsh.bash: /tmp/tmp.CeRIUTG9uM/st_fd4.cmd runs as /dev/fd/4; generated startup runs as /dev/fd/3
ERROR 4 line 4: Command 'unknownStartupCommand' not registered.
iocsh Error: Break
iocsh Error: Break
ERROR: Error in /dev/fd/3
When finished inspecting the four checks, leave and remove the temporary directory:
cd ~
rm -rf "$DIAG_WORK"
EPICS client discovery variables
The exercises use explicit loopback searches so clients find an IOC on the same host. Activate the selected EPICS tree in every terminal.
| Variable | Role in these lessons |
|---|---|
EPICS_CA_ADDR_LIST | UDP destinations for Channel Access searches |
EPICS_CA_AUTO_ADDR_LIST | Adds automatic interface broadcast addresses when YES |
EPICS_PVA_ADDR_LIST | UDP destinations for pvAccess searches |
EPICS_PVA_AUTO_ADDR_LIST | Adds automatic interface broadcast addresses when YES |
EPICS_PVA_NAME_SERVERS | Optional TCP name servers for PVA discovery |
For an IOC on this host, use these client settings:
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
An explicit address list and automatic addresses are separate settings.
Setting a list does not by itself disable automatic broadcast searches.
Use addresses reachable from the client for remote IOCs; 127.0.0.1
always refers to the client’s own host.
To inspect this host’s IPv4 addresses, use:
ip -4 addr show
Discovery locates the server; subsequent data operations use a TCP connection. Successful discovery alone does not establish that a device is connected or a record has valid data. Read the value and its alarms.
The PVXS network configuration reference describes PVA client and server settings beyond these local exercises.
Further training topics
Chapters 1 through 6 cover local Input/Output Controller (IOC) applications,
text device protocols, database templates, iocsh.bash module loading,
introductory QSRV2 group mapping, and local autosave and linStat startup
fragments. These advanced production topics belong in separate courses:
- Record processing chains, hardware links, and alarm severity design.
- Production
autosavepolicies, NFS storage layouts, andasVerifyaudits beyond the localcommonIocshsetup in lesson 6.4. - Fleet-wide IOC health monitoring,
caPutLogauditing, andrecsyncservice discovery beyond host and process metrics in lesson 6.4. - Sequencer (
snc/seq) state-machine programs. - Full three-layer pvAccess (PVA) device object architecture, write ordering
(
+putorder), and legacy proxy IOCs beyond the compactdbLoadGroupsummary in lesson 6.3. - Managed IOC daemons with
procServandsystemd. - Site deployment and operational acceptance testing.
These subjects extend the exercises in this book and are not prerequisites for Chapters 1 through 6.
Contributors
Here is a list of the contributor(s)
- Jeong Han Lee, Dr.rer.nat. https://git.als.lbl.gov/jeonglee
If you feel you’re missing from this list, feel free to add yourself in a PR.
Training glossary
These terms identify the components used throughout the exercises.
| Term | Meaning |
|---|---|
| Application | A source tree built into an IOC executable and installed configuration files |
| asyn port | A named communication interface used by device support |
| Boot instance | One startup directory that configures an IOC application for a location or device |
| CA | Channel Access, the protocol used by caget, caput, and camonitor |
| Database | A file defining record instances and their fields |
| DBD | Database definition: available record types, device support, drivers, and registrars |
| EOS | End-of-string delimiter used to separate device messages |
| IOC | Input/Output Controller, the EPICS process serving and processing records |
| IOC shell | The command interpreter inside an IOC, also called iocsh |
| iocsh.bash | The EPICS-env launcher that resolves installed modules and starts softIocPVX |
| iocshLoad | An IOC shell command that executes another file with macro arguments |
| Macro | A named value substituted in a database or startup file |
| PVA | pvAccess, the protocol used by pvxget and pvxput |
| PV | Process variable, a named value exposed by an IOC |
| PVXS | The library providing PVA clients and QSRV2 support in this distribution |
| QSRV2 | PVXS support exposing EPICS database records over PVA |
| Record | One database instance containing a value, fields, processing behavior, and alarms |
| Registrar | A function that registers support or commands with the IOC |
| StreamDevice | Device support that exchanges text messages using protocol files |
| Substitution file | A file listing macro values used to instantiate a database template |
| Template | A database definition parameterized with macros |
| TOP | The application’s top-level path, used during builds and IOC startup |