Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

ChapterWhat you can do afterward
1. EnvironmentSelect the matching OS tree and read and write one PV
2. IOC applicationsGenerate, build, and run several boot instances
3. Device communicationSend commands to a TCP simulator through StreamDevice
4. Reusable configurationConfigure multiple devices and load 32-channel databases
5. Build and shell configurationExplain dependency paths, installation paths, and command syntax
6. Module loaderRun 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.

  1. 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 iproute2
    

    On 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 which
    

    The development packages also supply their runtime libraries. Naming them avoids assuming that Debian 12 uses Ubuntu’s t64 package names. Commands need administrator privileges and working package repositories.

  2. In an ordinary Bash terminal, verify the tools:

    command -v bash perl git make gcc g++ readelf
    command -v socat bc mktemp pgrep ss
    

    Each name must print a path. readelf comes from binutils and is required by the module loader. pgrep supports simulator cleanup.

  3. After activating EPICS in Chapter 1, check the executable’s libraries:

    ldd "$(command -v softIocPVX)"
    

    Every dependency must resolve to a file. not found identifies 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.

Chapter exercises

  1. Add the air temperature record and repeat the failed-search exercise in lesson 1.2. Restore the client address afterward.
  2. Print EPICS_HOST_ARCH and explain why linux-x86_64 does not identify a compatible Debian, Ubuntu, or Rocky binary tree.
  3. Stop the IOC with exit before starting Chapter 2.

Before moving on

  • EPICS_BASE and EPICS_HOST_ARCH come from the setEpicsEnv.bash script matching your operating system.
  • EPICS_CA_ADDR_LIST is restored to 127.0.0.1 after the failed-search check in lesson 1.2.
  • Every softIocPVX session from lesson 1.2 is stopped with exit.
  • ~/ioc-training/first/water.db stays 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 ~/epics destination for the clone. If your distribution is already installed elsewhere, use its directory instead of ~/epics below.

Get and activate the environment

  1. Clone the public binary distribution into your home directory:

    git clone --depth 1 https://github.com/jeonghanlee/EPICS-env-distribution.git ~/epics
    
  2. 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_64 and a directory matching HOST_ID exactly. Release 1.5.0 provides these trees; all use Base 7.0.10:

    Operating systemDirectory name
    Debian 12debian-12
    Debian 13debian-13
    Rocky Linux 8.10rocky-8.10
    Rocky Linux 10.2rocky-10.2
    Ubuntu 24.04 LTSubuntu-24.04
    Ubuntu 26.04 LTSubuntu-26.04

    Use the tree built for your system. The shared linux-x86_64 architecture name does not make binaries interchangeable between operating systems.

  3. 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, and LD_LIBRARY_PATH. On Debian 13, for example, EPICS_BASE ends in 1.5.0/debian-13/7.0.10/base and EPICS_HOST_ARCH is linux-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:

TerminalRoleWorking directoryEnvironment
Terminal 1IOC (softIocPVX)~/ioc-training/first/source "$TREE/setEpicsEnv.bash"
Terminal 2CA and PVA clientAny directorysource "$TREE/setEpicsEnv.bash", export EPICS_CA_AUTO_ADDR_LIST=NO EPICS_CA_ADDR_LIST=127.0.0.1, export EPICS_PVA_AUTO_ADDR_LIST=NO EPICS_PVA_ADDR_LIST=127.0.0.1
  1. In two Bash terminals, activate the matching installation from lesson 1.1. Set TREE in 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
    
  2. In terminal 1, create a workspace outside the distribution:

    mkdir -p ~/ioc-training/first
    cd ~/ioc-training/first
    

    Save 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; EGU identifies its engineering units.

  3. Start the installed PVXS executable in terminal 1:

    softIocPVX -d water.db
    

    Wait for iocRun: All initialization complete. At the IOC prompt, list the records:

    dbl
    

    The IOC shell prints the loaded record name:

    temperature:water
    

    Leave the IOC running in terminal 1.

  4. In terminal 2, read the value and description:

    caget -t temperature:water
    caget -t temperature:water.DESC
    pvxget temperature:water
    

    CA returns the scalar value and description, while PVA displays a typed NTScalar structure:

    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
    
  5. Write through CA, then read through both protocols:

    caput temperature:water 24
    caget -t temperature:water
    pvxget temperature:water
    

    Both reads report 24, and the first write clears the initial UDF alarm status and updates timeStamp:

    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
    
  6. Write through PVA and read through CA:

    pvxput temperature:water 44
    caget -t temperature:water
    

    pvxput exits quietly on success, and CA returns 44 because both protocols reach the same record:

    44
    
  7. 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:

ValueMeaningExample
uname -mMachine architecturex86_64
EPICS_HOST_ARCHEPICS build target and binary subdirectorylinux-x86_64
OS directoryDistribution and release used to build the treedebian-13
Release directoryEPICS-env distribution version1.5.0
Base directoryEPICS Base version7.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.

MethodUse it to learnLesson
softIocPVX -d water.dbLoad a database with standard record supportRun your first IOC
A compiled application and st.cmdBuild an IOC and select support in MakefilesGenerate and run an IOC application
iocsh.bash st.cmdSelect installed module support at startupRun 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.

Chapter exercises

  1. From mouse/, run make clean uninstall. Enable ASYN in configure/RELEASE with the path $(MODULES)/asyn_missing, then run make. Read the missing dependency error. Restore the original file and rebuild successfully.
  2. Add and run the lab instance described in lesson 2.2.
  3. Compare the generated startup files with diff as shown in lesson 2.2. Explain which generator option accounts for each changed line.

Before moving on

  • tools/generate_ioc_structure.bash remains in your workspace directory for Chapters 3 and 4.
  • mouse/configure/RELEASE is restored after exercise 1 and make succeeds in mouse/.
  • Every mouse and jeonglee_mouse IOC process is stopped with exit.

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.

  1. 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 tools
    

    Use an existing tools checkout if you already have it. Keep the generated applications outside the binary distribution.

  2. From the directory containing tools/, generate the application:

    bash tools/generate_ioc_structure.bash -l home -p mouse
    

    Enter y when asked to continue with the unlisted home location. -p names the application; -l names the location. The result is mouse/ with the boot instance iocBoot/iochome-mouse/. Keep the exact name mouse rather 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 -. Avoid ioc in names: the script reserves ioc, Ioc, and IOC. Keep application name capitalization identical when adding instances.

  3. Build the generated application:

    cd mouse
    make
    

    A successful build creates bin/linux-x86_64/mouse, its database definition (DBD), and generated envPaths in the boot directory.

  4. 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. Set TARGET_TOP again if you start from another terminal.

  5. Run the boot instance:

    cd iocBoot/iochome-mouse
    ./st.cmd
    

    Wait for iocRun: All initialization complete. At the IOC prompt, inspect the database and available commands:

    dbl
    help
    exit
    

    This initial application loads no records, so dbl has no PV names to print. Successful initialization does not imply device communication.

Generated directory reference

PathPurpose
configure/RELEASESelects installed Base and module paths
configure/CONFIG_SITEControls application build settings
mouseApp/src/MakefileSelects 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.cmdConfigures and starts this IOC instance
iocBoot/iochome-mouse/envPathsGenerated 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:

OptionSelectsExample
-pApplication code and executablemouse
-lLocation in the IOC namepark
-dDevice part of the IOC namewoodmouse
-fFolder containing the applicationjeonglee_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

  1. From the directory containing mouse/, generate a second instance:

    bash tools/generate_ioc_structure.bash -l park -p mouse
    

    park is outside the predefined facility locations. Enter y when asked to continue. The script creates mouse/iocBoot/iocpark-mouse/.

  2. In the same parent directory, generate an instance with another device name:

    bash tools/generate_ioc_structure.bash -l park -p mouse -d woodmouse
    

    This creates mouse/iocBoot/iocpark-woodmouse/. The application code remains mouseApp, and both instances run the mouse executable.

  3. 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"
    
  4. In mouse/, start the location instance:

    cd iocBoot/iocpark-mouse
    ./st.cmd
    

    Wait for iocRun: All initialization complete, then enter exit.

  5. From iocpark-mouse/, start the device instance:

    cd ../iocpark-woodmouse
    ./st.cmd
    

    Wait for initialization and enter exit before continuing.

Use a different folder name

  1. Return to the directory containing tools/ and mouse/. Copy the application to a destination that does not already exist:

    cp -a mouse jeonglee_mouse
    

    Keep the exact folder name jeonglee_mouse rather than substituting your own username so the remaining commands in this lesson match as shown.

  2. Generate an instance in that folder:

    bash tools/generate_ioc_structure.bash -l bts -p mouse -f jeonglee_mouse
    

    -f selects the folder. -p mouse selects the application already in that folder. The instance is iocBoot/iocbts-mouse/.

  3. 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.cmd
    

    Wait 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.

Chapter exercises

  1. Perform the simulator stop and restart exercise in lesson 3.3. Distinguish the last reply value from the command record’s alarm.
  2. In the drvAsynIPPortConfigure line of st.cmd, change 9399 to an unused port. Restart the IOC and inspect the connection and write errors. Restore 9399 and confirm a successful round trip.
  3. Explain which protocol writes Cmd-RB and why reading an old reply alone does not prove that the device is connected.

Before moving on

  • jeonglee_Demo/ retains db/training.db, db/training.proto, and iocBoot/iocB46_182-jeonglee_Demo/st.cmd set to port 9399.
  • jeonglee_Demo/simulator/ retains executable tcpserver.bash and connection_handler.sh scripts for Chapters 4 and 6.
  • The jeonglee_Demo IOC is stopped with exit and tcpserver.bash is 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

  1. 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.

  2. 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 Cmd runs the protocol. Its reply goes to Cmd-RB.VAL. P, R, and PORT are supplied when the startup loads the database.

  3. From the application’s top-level directory, build:

    make
    

    The generated Db/Makefile installs the database and protocol in db/.

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

  1. From the jeonglee_Demo/ application root, create a simulator directory:

    mkdir simulator
    cd simulator
    
  2. 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
    
  3. Save this handler as connection_handler.sh in 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
    
  4. 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

  1. In terminal 1, start the server from simulator/:

    ./tcpserver.bash
    

    It must report port 9399. Leave it running.

  2. In terminal 2, connect a client:

    socat - TCP:127.0.0.1:9399
    
  3. 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:

TerminalRoleWorking directoryEnvironment
Terminal 1TCP echo simulator (9399)jeonglee_Demo/simulator/No EPICS environment required
Terminal 2Compiled IOC (./st.cmd)jeonglee_Demo/iocBoot/iocB46_182-jeonglee_Demo/source "$TREE/setEpicsEnv.bash"
Terminal 3CA and PVA clientAny directorysource "$TREE/setEpicsEnv.bash", export EPICS_CA_AUTO_ADDR_LIST=NO EPICS_CA_ADDR_LIST=127.0.0.1, export EPICS_PVA_AUTO_ADDR_LIST=NO EPICS_PVA_ADDR_LIST=127.0.0.1

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

  1. In terminal 1, start the simulator from simulator/:

    ./tcpserver.bash 9399
    
  2. In terminal 2, run source "$TREE/setEpicsEnv.bash", then start the IOC from jeonglee_Demo/:

    cd iocBoot/iocB46_182-jeonglee_Demo
    ./st.cmd
    

    Wait for iocRun: All initialization complete. Enter dbl at the IOC prompt. It must list:

    jeonglee:myoffice:Cmd-RB
    jeonglee:myoffice:Cmd
    
  3. 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

  1. Send an identification query:

    caput -c jeonglee:myoffice:Cmd "DEVICE_ID?"
    caget jeonglee:myoffice:Cmd-RB
    

    caput -c waits for record processing to complete, and the readback returns DEVICE_ID?:

    Old : jeonglee:myoffice:Cmd
    New : jeonglee:myoffice:Cmd          DEVICE_ID?
    jeonglee:myoffice:Cmd-RB       DEVICE_ID?
    
  2. 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-RB
    

    Both 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

  1. 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-RB
    

    Cmd enters COMM / INVALID alarm state, while Cmd-RB retains the prior STATUS? 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 disconnected
    

    A cached readback value alone does not prove that the device accepted a command after the connection dropped.

  2. Restart the simulator in terminal 1:

    ./tcpserver.bash 9399
    

    asyn retries 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.SEVR
    

    While Cmd-RB still shows STATUS?, repeat both commands every few seconds. Once reconnected, Cmd-RB updates to RECOVERED and Cmd clears its alarm state to NO_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.

Chapter exercises

  1. Copy the Chapter 3 st.cmd to st_break.cmd. Move iocInit() immediately before dbLoadRecords, start it, and inspect the startup error. Restore the original order and confirm normal communication.
  2. In lesson 4.5, configure two TC32 simulators and verify all 64 PVs.
  3. 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/ retains iocsh/training_device.iocsh, simulator/advanced_connection_handler.sh, and simulator/tc32_emulator.bash for Chapters 5 and 6.
  • jeonglee_Demo/db/TC-32.db is rebuilt from restored sources so head -n 7 db/TC-32.db shows $(P)$(R)CH01 and $(PORT).
  • Every jeonglee_Demo IOC is stopped with exit and 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:

TerminalRoleWorking directoryEnvironment
Terminal 1TCP echo simulator (9399)jeonglee_Demo/simulator/No EPICS environment required
Terminal 2Build and IOC (make, ./st2.cmd)jeonglee_Demo/ then jeonglee_Demo/iocBoot/iocB46_182-jeonglee_Demo/source "$TREE/setEpicsEnv.bash"
Terminal 3CA and PVA clientAny directorysource "$TREE/setEpicsEnv.bash", export EPICS_CA_AUTO_ADDR_LIST=NO EPICS_CA_ADDR_LIST=127.0.0.1, export EPICS_PVA_AUTO_ADDR_LIST=NO EPICS_PVA_ADDR_LIST=127.0.0.1

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".

  1. In terminal 1, start one echo server from jeonglee_Demo/simulator/:

    ./tcpserver.bash 9399
    

    Leave it running for this lesson. Do not start another on the same port.

  2. In terminal 2 inside jeonglee_Demo/jeonglee_DemoApp/iocsh/, save the following file as training_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. ASYNTRACE defaults to a comment prefix; an empty value enables that command. The asyn port and database use the same PORT_NAME.

  3. In terminal 2 from the jeonglee_Demo/ application root, install the fragment:

    make
    ls iocsh/training_device.iocsh
    

    Edit the source under jeonglee_DemoApp/iocsh/, then run make to update the installed copy under iocsh/.

  4. In terminal 2 inside iocBoot/iocB46_182-jeonglee_Demo/, save this as st2.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()
    dbl
    

    The environment supplies PREFIX, DEVICE, and DATABASE_TOP. The active iocshLoad call supplies the port name. This IOC already registered its compiled support; the fragment configures one device.

  5. In terminal 2 from iocBoot/iocB46_182-jeonglee_Demo/, start the IOC:

    chmod +x st2.cmd
    ./st2.cmd
    

    In 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-RB
    

    The 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

  1. In simulator/, replace tcpserver.bash with 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
    
  2. 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
    
  3. 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:

CommandPortHandler
./tcpserver.bash9399Echo
./tcpserver.bash 88888888Echo
./tcpserver.bash advanced_connection_handler.sh9399Command-aware
./tcpserver.bash 8888 advanced_connection_handler.sh8888Command-aware

Stop each server with Ctrl+C before trying another command on the same port.

Test the command-aware replies

  1. Start the advanced handler on 9399:

    ./tcpserver.bash 9399 advanced_connection_handler.sh
    
  2. In another terminal, connect a client:

    socat - TCP:127.0.0.1:9399
    
  3. 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):

TerminalRoleWorking directoryEnvironment
Terminal 1Simulator 1 (9399)jeonglee_Demo/simulator/No EPICS environment required
Terminal 2Simulator 2 (9400)jeonglee_Demo/simulator/No EPICS environment required
Terminal 3Simulator 3 (9401)jeonglee_Demo/simulator/No EPICS environment required
Terminal 4Compiled IOC (./st3.cmd)jeonglee_Demo/iocBoot/iocB46_182-jeonglee_Demo/source "$TREE/setEpicsEnv.bash"
Terminal 5CA and PVA clientAny directorysource "$TREE/setEpicsEnv.bash", export EPICS_CA_AUTO_ADDR_LIST=NO EPICS_CA_ADDR_LIST=127.0.0.1, export EPICS_PVA_AUTO_ADDR_LIST=NO EPICS_PVA_ADDR_LIST=127.0.0.1

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 prefixAsyn portTCP port
MYDEMO:SIM1:TCP19399
MYDEMO:SIM2:TCP29400
MYDEMO:SIM3:TCP39401

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

  1. Start the command-aware simulator in terminals 1, 2, and 3 from jeonglee_Demo/simulator/. In terminal 1, listen on port 9399:

    ./tcpserver.bash 9399 advanced_connection_handler.sh
    

    In terminal 2, listen on port 9400:

    ./tcpserver.bash 9400 advanced_connection_handler.sh
    

    In terminal 3, listen on port 9401:

    ./tcpserver.bash 9401 advanced_connection_handler.sh
    
  2. In terminal 4 inside jeonglee_Demo/iocBoot/iocB46_182-jeonglee_Demo/, start the IOC:

    ./st3.cmd
    
  3. 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-RB
    

    caput -c waits for each asynchronous command to finish before the next command runs, and caget prints three distinct numeric process IDs:

    MYDEMO:SIM1:Cmd-RB             764328
    MYDEMO:SIM2:Cmd-RB             764331
    MYDEMO:SIM3:Cmd-RB             764335
    
  4. 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-RB
    

    Each reply returns an integer between 0 and 100:

    MYDEMO:SIM1:Cmd-RB             32
    MYDEMO:SIM2:Cmd-RB             26
    MYDEMO:SIM3:Cmd-RB             61
    

    In terminal 4, TCP3 prints asyn trace lines for the 9-byte "GetTemp?\n" write and 3-byte "61\n" reply on 127.0.0.1:9401, while TCP1 and TCP2 stay 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

  1. In terminal 1, start the emulator on the default port:

    ./tc32_emulator.bash
    

    Wait for Emulator running on port 9399. Leave it running.

  2. In terminal 2, connect a client:

    socat - TCP:127.0.0.1:9399
    

    You must see repeated channel numbers CH01 through CH32, each followed by a number between 10 and 90.

  3. 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.

  4. To select another port, start another instance:

    ./tc32_emulator.bash --port 10001
    

    Connect 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:

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

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

Add the training IOC instance

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

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

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

Define the protocol and template

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Load the template once per channel

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

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

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

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

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

Each call expands the template for one channel at startup.

Load the database generated at build time

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

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

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

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

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

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

Expand substitutions at startup

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

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

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

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

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

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

Verify each loading method

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

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

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

    ./st.cmd
    

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

    ./st2.cmd
    

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

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

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

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

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

    pvxget MYDEMO:TC32:CH01
    

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

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

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

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

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

    Stop camonitor with Ctrl+C.

Configure several temperature devices

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

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

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

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

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

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

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

Make it executable:

chmod +x st_multi.cmd

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

./st_multi.cmd

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

Exercises

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

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

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

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

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

    time ./st.cmd < /dev/null
    

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

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

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

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

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

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

ComponentProvides
bin/linux-x86_64/jeonglee_DemoIOC program linked to the selected support
dbd/jeonglee_Demo.dbdRecord 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:

  1. Read envPaths and define paths and macros.
  2. Load the application DBD and call its support registration function.
  3. Configure the asyn port and its newline terminators.
  4. Load database records with their prefix and port macros.
  5. Call iocInit() once.
  6. 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:

OrderOperationPurpose
1iocBuild_1Checks the database definition, prepares the main thread, time support, watchdog, and callbacks
2dbCaLinkInitPrepares Channel Access database links
3initDrvSup, initRecSup, initDevSupInitializes driver, record, and device support
4prepareLinks, dbLockInitRecordsPrepares record links and database lock sets
5initDatabase, dbBkptInitInitializes records and database debugging
6finishDevSupCompletes device support initialization
7scanInit, asInit, dbProcessNotifyInitPrepares scanning, access security, and processing notifications
8initialProcessProcesses records whose PINI is YES
9dbInitServersInitializes registered database servers
10iocBuild_3Sets 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.

GoalFiles and lesson
Configure one TCP connectiontraining_device.iocsh and st2.cmd
Connect three TCP devicesst3.cmd
Generate a 32-record databasetemperature.template and TC-32.substitutions
Compare database loading methodsst.cmd, st2.cmd, and st3.cmd
Connect several temperature devicesTC-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.

Chapter exercises

  1. Save any existing configure/RELEASE.local. Set EPICS_BASE to /nonexistent/epics/base in that file and run make. Restore the saved content, or set it to the active Base path, and rebuild.
  2. Complete the command-style comparison in lesson 5.1 and the installation-path exercise in lesson 5.3.
  3. Explain the difference between selecting dependencies in RELEASE and selecting build options in CONFIG_SITE.

Before moving on

  • configure/RELEASE.local and configure/CONFIG_SITE.local in jeonglee_Demo/ are restored so make succeeds.
  • iocBoot/ioctraining-jeonglee_Demo/envPaths sets TOP to your jeonglee_Demo directory, with db/ and iocsh/ intact for Chapter 6.
  • Every jeonglee_Demo IOC, softIoc, and tc32_emulator.bash process 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:

TerminalRoleWorking directoryEnvironment
Terminal 1TC-32 emulator (9399)jeonglee_Demo/simulator/No EPICS environment required
Terminal 2Compiled IOC (./st4.cmd, ./st2.cmd)jeonglee_Demo/iocBoot/ioctraining-jeonglee_Demo/source "$TREE/setEpicsEnv.bash"
Terminal 3CA and PVA clientAny directorysource "$TREE/setEpicsEnv.bash", export EPICS_CA_AUTO_ADDR_LIST=NO EPICS_CA_ADDR_LIST=127.0.0.1, export EPICS_PVA_AUTO_ADDR_LIST=NO EPICS_PVA_ADDR_LIST=127.0.0.1

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".

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

    ./tc32_emulator.bash --port 9399
    

    Wait for Emulator running on port 9399 and leave it running for st4.cmd.

  2. In terminal 2 inside jeonglee_Demo/iocBoot/ioctraining-jeonglee_Demo/, save this complete startup as st4.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
    

    < envPaths reads another IOC shell file. It does not execute an operating-system shell command; lesson 5.4 explains system.

  3. In terminal 2 from that boot directory, start the file:

    chmod +x st4.cmd
    ./st4.cmd
    

    The IOC initializes, prints its clock report, and lists 32 records.

  4. 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:CH32
    

    Each value is between 10 and 90. Stop tc32_emulator.bash with Ctrl+C in terminal 1, stop st4.cmd with exit in terminal 2, then restart ./tc32_emulator.bash --port 9399 and run ./st2.cmd from 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.

VariableInstallation selected
EPICS_BASEThe 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.

  1. In the jeonglee_Demo/ application root, build with existing settings:

    make
    

    Keep a copy of any existing configure/CONFIG_SITE.local before replacing its contents in this exercise.

  2. 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/envPaths
    

    Products include bin, db, dbd, iocsh, and lib. Generated TOP points to INSTALL_DIR. Boot scripts remain in the source application’s iocBoot; the build does not install that directory.

  3. Compare cleanup targets:

    make clean
    ls "$INSTALL_DIR/bin/linux-x86_64"
    make
    make distclean
    ls "$INSTALL_DIR"
    make install
    

    clean removes intermediate files and leaves installed products. distclean also removes installed products; directories can remain. make install rebuilds and installs the products.

  4. 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/envPaths
    

    Products remain in INSTALL_DIR; envPaths uses RUNTIME_DIR for TOP. 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.

  5. Restore the application’s default path and rebuild:

    printf '' > configure/CONFIG_SITE.local
    make clean
    make
    cat iocBoot/ioctraining-jeonglee_Demo/envPaths
    

    If you had a local configuration, restore its saved copy instead. TOP must point to the intended application path.

  6. 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.

SettingEffect
CHECK_RELEASEYES checks dependencies; WARN reports conflicts and continues; NO disables the check
HOST_OPT, CROSS_OPTOptimization for host and cross builds
CROSS_COMPILER_TARGET_ARCHSCross targets for this application
USR_CPPFLAGS, USR_CFLAGS, USR_LDFLAGSAdditional preprocessor, C compiler, and linker flags
INSTALL_LOCATIONInstallation directory, normally $(TOP)
IOCS_APPL_TOPIOC-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.

  1. In the generated application root, find the DBD inclusion:

    grep 'system.dbd' jeonglee_DemoApp/src/Makefile
    

    Common_DBDs += system.dbd includes the registrar for system in the application DBD. Support registration makes it available to the IOC.

  2. In an activated Bash terminal, start a soft IOC in a scratch directory:

    SYSTEM_WORK="$(mktemp -d)"
    cd "$SYSTEM_WORK"
    softIoc
    
  3. At the IOC prompt, compare these commands:

    pwd
    system pwd
    system(pwd)
    system("pwd")
    

    Each prints the working directory. pwd is an IOC shell command; the other forms invoke the operating system’s pwd through system.

  4. 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 unquoted system mkdir test_folder supplies two arguments to a command that expects one; it does not express the intended shell command.

  5. Stop the soft IOC:

    exit
    

    In the returned Bash shell, inspect the result:

    ls -ld test_folder
    

    The directory exists under SYSTEM_WORK. system uses the IOC process’s account, environment, and current directory.

  6. 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.

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

  1. In lesson 6.3, add a ch08 scalar mapping for $(P)$(R)CH08.VAL to tc32_group.json. Stop tc32_emulator.bash in terminal 1 with Ctrl+C, enter exit in terminal 2, start ./tc32_emulator.bash --port 9399 again in terminal 1, and restart iocsh.bash st.cmd in terminal 2. Confirm that pvxgl(1, "*") reports Atomic Members:5 and pvxget LOADER:TC32:Summary includes ch08.value.
  2. In lesson 6.4, add a second ao record named $(P)Limit with field(PINI, "YES") and info(autosaveFields, "VAL"). Write a value with caput, wait 6 seconds, restart the IOC, and verify that settings.sav restores both records.
  3. Run iocsh.bash -n on st.cmd from lesson 6.4 and explain why commonIocsh fragments (linStat.iocsh and autosave.iocsh) use iocshLoad rather than module directives.

Before moving on

  • Every iocsh.bash (softIocPVX) session is stopped with exit.
  • Every tcpserver.bash and tc32_emulator.bash process 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 readelf from binutils is 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:

TerminalRoleWorking directoryEnvironment
Terminal 1IOC (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 2CA and PVA clientAny directorysource "$TREE/setEpicsEnv.bash", export EPICS_CA_AUTO_ADDR_LIST=NO EPICS_CA_ADDR_LIST=127.0.0.1, export EPICS_PVA_AUTO_ADDR_LIST=NO EPICS_PVA_ADDR_LIST=127.0.0.1

Prepare the database and startup

  1. In both terminals, activate the tree matching your operating system and direct clients to loopback. If you installed the distribution outside ~/epics, set TREE to 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
    
  2. In terminal 1, create a working directory outside the distribution:

    IOC_WORK="$(mktemp -d)"
    cd "$IOC_WORK"
    pwd
    

    Keep terminal 1 in this directory. Relative database paths resolve from the current working directory even when you pass an absolute startup filename.

  3. In $IOC_WORK, save this database as busy.db:

    record(busy, "$(P)Run") {
        field(DESC, "Training loader demonstration")
        field(ZNAM, "Idle")
        field(ONAM, "Busy")
    }
    
  4. Save this startup file as st.cmd beside busy.db:

    on error break
    module busy
    dbLoadRecords("busy.db", "P=TRAINING:")
    iocInit()
    

    module busy selects the installed default version of busy. Your startup loads record instances and calls iocInit() once.

  5. In terminal 1, print the generated startup without starting the IOC:

    iocsh.bash -n st.cmd
    

    The 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 comments
    

    Each part of this generated startup serves a distinct role:

    • # elf: lines verify every shared library edge with readelf across module, base, and system classes before the IOC starts.
    • dlload and dbLoadDatabase load busy and its transitive dependencies (seq, sscan, calc, asyn, and autosave) once in dependency order.
    • epicsEnvSet exports each module’s install directory macro, such as BUSY, from its installed cfg/iocsh.conf.
    • registerAllRecordDeviceDrivers(pdbbase) registers all loaded support once before your database records load.
    • /dev/fd/3 holds the generated wrapper, while /dev/fd/4 holds a line-preserving copy of st.cmd with module lines turned into comments. Keep st.cmd as your saved startup file; the /dev/fd/ paths exist only during one loader invocation.

Start the IOC and read its PV

  1. In terminal 1, start the IOC from $IOC_WORK:

    iocsh.bash st.cmd
    

    Wait for iocRun: All initialization complete and leave the IOC running.

  2. In terminal 2, read the initial state through CA:

    caget -t TRAINING:Run
    

    The initial state is Idle:

    Idle
    
  3. In terminal 2, set the busy state and confirm it through CA:

    caput TRAINING:Run 1
    caget -t TRAINING:Run
    

    The CA client prints the state transition and current value:

    Old : TRAINING:Run                   Idle
    New : TRAINING:Run                   Busy
    Busy
    
  4. In terminal 2, read the same PV through PVA:

    pvxget TRAINING:Run
    

    QSRV2 inside softIocPVX returns an NTEnum structure with index 1 and 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"
    
  5. In terminal 2, return the record to Idle:

    caput TRAINING:Run 0
    caget -t TRAINING:Run
    

    CA reports Idle:

    Old : TRAINING:Run                   Busy
    New : TRAINING:Run                   Idle
    Idle
    
  6. In terminal 1, stop the IOC at the iocsh prompt:

    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

Open three Bash terminals on the same host for the single-device setup, and add terminals 4 and 5 for the three-device section:

TerminalRoleWorking directoryEnvironment
Terminal 1TCP simulator (9399)jeonglee_Demo/simulator/No EPICS environment required
Terminal 2IOC (iocsh.bash)jeonglee_Demo/ then jeonglee_Demo/loader_tcp/source "$TREE/setEpicsEnv.bash"
Terminal 3CA and PVA clientAny directorysource "$TREE/setEpicsEnv.bash", export EPICS_CA_AUTO_ADDR_LIST=NO EPICS_CA_ADDR_LIST=127.0.0.1, export EPICS_PVA_AUTO_ADDR_LIST=NO EPICS_PVA_ADDR_LIST=127.0.0.1
Terminals 4-5Extra 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

  1. 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
    
  2. In terminal 2 inside loader_tcp/, save this startup file as st.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 StreamDevice selects StreamDevice and its recorded dependencies, including asyn and calc. iocsh.bash handles library loading and support registration, while st.cmd configures the TCP port, loads the records, and calls iocInit().

  3. In terminal 2 inside loader_tcp/, preview the generated startup:

    iocsh.bash -n st.cmd
    

Run the single-device connection

  1. In terminal 1 inside jeonglee_Demo/simulator/, start the echo server on port 9399:

    ./tcpserver.bash 9399
    
  2. In terminal 2 inside loader_tcp/, start the IOC:

    iocsh.bash st.cmd
    
  3. 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
    
  4. In terminal 3, send a query with caput -c and read the reply PV (Cmd-RB) through CA:

    caput -c LOADER:SIM:Cmd "DEVICE_ID?"
    caget LOADER:SIM:Cmd-RB
    

    CA confirms the write and returns DEVICE_ID?:

    Old : LOADER:SIM:Cmd
    New : LOADER:SIM:Cmd                 DEVICE_ID?
    LOADER:SIM:Cmd-RB              DEVICE_ID?
    
  5. Read the same reply PV through PVA:

    pvxget LOADER:SIM:Cmd-RB
    

    QSRV2 exposes Cmd-RB as an NTScalar string 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"
    
  6. Send a second query to confirm repeated message exchange:

    caput -c LOADER:SIM:Cmd "STATUS?"
    caget LOADER:SIM:Cmd-RB
    

    The readback updates to STATUS?:

    Old : LOADER:SIM:Cmd                DEVICE_ID?
    New : LOADER:SIM:Cmd                STATUS?
    LOADER:SIM:Cmd-RB              STATUS?
    
  7. Enter exit at the iocsh prompt 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

  1. In terminal 2 inside loader_tcp/, copy training_device.iocsh from Chapter 4 without changing a line:

    cp ../iocsh/training_device.iocsh .
    
  2. In terminal 2 inside loader_tcp/, save this three-device startup file as st_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 StreamDevice appears once in st_multi.cmd, never inside training_device.iocsh. iocsh.bash scans only the top-level startup file before launching softIocPVX and loads each module once for the process. Keeping training_device.iocsh free of loader directives lets compiled IOCs (Chapter 4) and iocsh.bash share the exact same fragment.

  3. Start one command-aware simulator per port from jeonglee_Demo/simulator/. In terminal 1, listen on port 9399:

    ./tcpserver.bash 9399 advanced_connection_handler.sh
    

    In terminal 4, listen on port 9400:

    ./tcpserver.bash 9400 advanced_connection_handler.sh
    

    In terminal 5, listen on port 9401:

    ./tcpserver.bash 9401 advanced_connection_handler.sh
    
  4. In terminal 2 inside loader_tcp/, start the three-device IOC:

    iocsh.bash st_multi.cmd
    
  5. 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-RB
    

    Each connection reports a distinct handler PID:

    LOADER:SIM1:Cmd-RB             242338
    LOADER:SIM2:Cmd-RB             242342
    LOADER:SIM3:Cmd-RB             242344
    
  6. 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-RB
    

    Each readback returns an integer between 0 and 100, while TCP3 prints asyn trace 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

Open three Bash terminals on the same host with these roles and settings:

TerminalRoleWorking directoryEnvironment
Terminal 1TC-32 emulator (9399)jeonglee_Demo/simulator/No EPICS environment required
Terminal 2IOC (iocsh.bash)jeonglee_Demo/ then jeonglee_Demo/loader_tc32/source "$TREE/setEpicsEnv.bash"
Terminal 3CA and PVA clientAny directorysource "$TREE/setEpicsEnv.bash", export EPICS_CA_AUTO_ADDR_LIST=NO EPICS_CA_ADDR_LIST=127.0.0.1, export EPICS_PVA_AUTO_ADDR_LIST=NO EPICS_PVA_ADDR_LIST=127.0.0.1

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

  1. 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
    
  2. In terminal 2 inside loader_tc32/, save this QSRV2 group definition as tc32_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 QSRV2 assembles LOADER:TC32:Summary:

    • "+id" assigns the top-level PVA structure identifier training:nt/TC32Summary:1.0.
    • "+type": "const" with "+const": "TC-32" publishes a constant device.model string field without an underlying EPICS record.
    • "+type": "scalar" embeds each selected channel’s NTScalar value, alarm, timestamp, and display metadata from $(P)$(R)CH*.VAL.
    • "+trigger": "*" on ch32 publishes an atomic subscription update across all group fields when channel 32 finishes a streaming sweep. Omitting +putorder keeps the group read-only.
  3. In terminal 2 inside loader_tc32/, save this startup file as st.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()
    

    softIocPVX links pvxs with QSRV2 enabled by default, so dbLoadGroup needs no extra module directive in st.cmd.

  4. 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

  1. In terminal 1 inside jeonglee_Demo/simulator/, start the TC-32 emulator on port 9399:

    ./tc32_emulator.bash --port 9399
    
  2. In terminal 2 inside loader_tc32/, start the IOC:

    iocsh.bash st.cmd
    
  3. At the iocsh prompt in terminal 2, list the registered QSRV2 group definition:

    pvxgl(1, "*")
    

    pvxgl reports the group name, atomic delivery mode, and four mapped members (device.model, ch01, ch16, and ch32):

    LOADER:TC32:Summary
      Atomic Get/Put:yes Atomic Members:4
    
  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
    
  5. 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.SEVR
    

    Live temperatures fall between 10 and 90 degrees Celsius with NO_ALARM severity:

    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
    
  6. In terminal 3, read channel 01 as an individual PVA NTScalar record:

    pvxget LOADER:TC32:CH01
    

    pvxget prints the value, alarm, timestamp, and engineering units from the same ai record:

    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
    
  7. In terminal 3, read the composite LOADER:TC32:Summary group PV in a single PVA request:

    pvxget LOADER:TC32:Summary
    

    One atomic PVA read returns ch01, ch16, ch32, and device.model together 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

  1. In terminal 3, read all 32 scalar channels once to confirm every record receives live stream updates between 10 and 90:

    for ch in {01..32}; do caget -t "LOADER:TC32:CH$ch"; done
    
  2. In terminal 3, inspect the published PVA type identifier on LOADER:TC32:Summary:

    pvxinfo LOADER:TC32:Summary | head -n 3
    

    The structure header shows the custom +id contract:

    LOADER:TC32:Summary from 127.0.0.1:43259
    struct "training:nt/TC32Summary:1.0" {
        struct {
    
  3. In terminal 3, subscribe to three scalar channels with camonitor to verify continuous updates:

    camonitor LOADER:TC32:CH01 LOADER:TC32:CH16 LOADER:TC32:CH32
    

    Stop camonitor in terminal 3 with Ctrl+C, stop tc32_emulator.bash in terminal 1 with Ctrl+C, and then enter exit in 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

Open two Bash terminals on the same host with these roles and settings:

TerminalRoleWorking directoryEnvironment
Terminal 1IOC (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 2CA and PVA clientAny directorysource "$TREE/setEpicsEnv.bash", export EPICS_CA_AUTO_ADDR_LIST=NO EPICS_CA_ADDR_LIST=127.0.0.1, export EPICS_PVA_AUTO_ADDR_LIST=NO EPICS_PVA_ADDR_LIST=127.0.0.1

Create the setpoint database and service startup

  1. 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, set TREE to 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
    
  2. In terminal 1, create a service directory outside the distribution:

    mkdir -p ~/ioc-training/loader_services
    cd ~/ioc-training/loader_services
    
  3. In ~/ioc-training/loader_services/, save this database as setpoint.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 during iocInit so its alarm severity clears to NO_ALARM after boot-time restore. info(autosaveFields, "VAL") marks VAL for automatic request-file generation by autosave.iocsh.

  4. Save this startup file as st.cmd beside setpoint.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.bash with commonIocsh:

    • module linStat, module autosave, and module retools load those libraries and export LINSTAT, AUTOSAVE, and RETOOLS automatically.
    • IOC is set to LOADER_SVC without a trailing colon. autosave.iocsh uses $(IOC) as a directory name under ./var/LOADER_SVC/ and builds the status prefix LOADER_SVC-as:, while linStat.iocsh supplies its own colon in LOADER_SVC:UPTIME and LOADER_SVC:PROCESS_ID. Passing P=$(IOC): to dbLoadRecords keeps $(P)Setpoint aligned with the trailing-colon P macro used across Chapters 3, 4, and 6.
    • Do not load linStat.iocsh together with iocStatsAdmin.iocsh in the same IOC. Both fragments define identically named host and process records such as $(IOC):UPTIME and $(IOC):FD_CNT.
  5. Preview the generated startup in terminal 1:

    iocsh.bash -n st.cmd
    

Save a setpoint and inspect live services

  1. In terminal 1, start the first IOC run from ~/ioc-training/loader_services/:

    iocsh.bash st.cmd
    

    During the initial boot into an empty ./var directory, dbrestore.c reports that ./var/LOADER_SVC/save/settings.sav does not exist yet. After iocInit() completes, afterIocRunning generates ./var/LOADER_SVC/req/settings.req from info(autosaveFields, "VAL") and starts a 5-second monitor set.

  2. At the iocsh prompt in terminal 1, filter loaded record names with reGrep:

    reGrep("LOADER_SVC:(Setpoint|PROCESS_ID|FD_CNT|UPTIME)")
    

    reGrep matches 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:UPTIME
    

    Unlike dbgrep, which uses shell-style * wildcards, reGrep requires the pattern to match the entire record name (.*Setpoint.* or an explicit prefix such as LOADER_SVC:.*).

  3. In terminal 2, read the initial setpoint, linStat process metrics, and autosave status 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_statusStr
    

    LOADER_SVC:Setpoint starts at 20, while linStat reports the live IOC process ID (PID), open file-descriptor count, and uptime. PROCESS_ID and FD_CNT read 0 until 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...
    
  4. Write 37.5 to LOADER_SVC:Setpoint and wait 6 seconds past the default 5-second SETTINGS_PERIOD save interval:

    caput LOADER_SVC:Setpoint 37.5
    sleep 6
    

    CA confirms the setpoint update:

    Old : LOADER_SVC:Setpoint            20
    New : LOADER_SVC:Setpoint            37.5
    
  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.sav
    

    settings.req lists the tagged field and settings.sav stores 37.5 before <END>:

    LOADER_SVC:Setpoint.VAL
    # autosave R5.3	Automatically generated - DO NOT MODIFY - 261111-102556
    LOADER_SVC:Setpoint.VAL 37.5
    <END>
    
  6. In terminal 1, stop the first IOC run:

    exit
    

Verification

  1. In terminal 1, start a second IOC run from ~/ioc-training/loader_services/:

    iocsh.bash st.cmd
    
  2. Without writing to the record, read LOADER_SVC:Setpoint in terminal 2 through CA:

    caget LOADER_SVC:Setpoint LOADER_SVC:Setpoint.SEVR
    

    autosave restores 37.5 during iocInit, and PINI clears the record alarm state to NO_ALARM:

    LOADER_SVC:Setpoint            37.5
    LOADER_SVC:Setpoint.SEVR       NO_ALARM
    
  3. Read the restored record through PVA:

    pvxget LOADER_SVC:Setpoint
    

    pvxget confirms value double = 37.5 and alarm.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 = nan
    

    In Run your first IOC, water.db returns to 0 on every restart because no save set writes changes to disk. Enter exit in 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

OptionLong formBehavior
(none)(none)Start an interactive IOC from st.cmd
-n--showPrint the generated startup and exit without starting the IOC
-S--non-interactiveDisable the interactive iocsh prompt
-v--verboseAlso display the built-in startup commands of softIocPVX
-e "$TREE"--environment "$TREE"Source "$TREE/setEpicsEnv.bash" inside the loader process
-h--helpPrint 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 conditionStageCorrective action
Module or requested version absentiocsh.bash preflightSelect a module name and version present under $EPICS_MODULES
Transitive and explicit versions conflictiocsh.bash preflightKeep one compatible version per module across the IOC
module placed inside iocshLoad fragmentsoftIocPVX runtimeMove all module directives to the top-level st.cmd
Unknown command or syntax error in st.cmdsoftIocPVX runtime (/dev/fd/4)Fix the reported line number in st.cmd
Database or protocol path missingsoftIocPVX runtimeStart 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.

VariableRole in these lessons
EPICS_CA_ADDR_LISTUDP destinations for Channel Access searches
EPICS_CA_AUTO_ADDR_LISTAdds automatic interface broadcast addresses when YES
EPICS_PVA_ADDR_LISTUDP destinations for pvAccess searches
EPICS_PVA_AUTO_ADDR_LISTAdds automatic interface broadcast addresses when YES
EPICS_PVA_NAME_SERVERSOptional 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 autosave policies, NFS storage layouts, and asVerify audits beyond the local commonIocsh setup in lesson 6.4.
  • Fleet-wide IOC health monitoring, caPutLog auditing, and recsync service 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 compact dbLoadGroup summary in lesson 6.3.
  • Managed IOC daemons with procServ and systemd.
  • 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)

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.

TermMeaning
ApplicationA source tree built into an IOC executable and installed configuration files
asyn portA named communication interface used by device support
Boot instanceOne startup directory that configures an IOC application for a location or device
CAChannel Access, the protocol used by caget, caput, and camonitor
DatabaseA file defining record instances and their fields
DBDDatabase definition: available record types, device support, drivers, and registrars
EOSEnd-of-string delimiter used to separate device messages
IOCInput/Output Controller, the EPICS process serving and processing records
IOC shellThe command interpreter inside an IOC, also called iocsh
iocsh.bashThe EPICS-env launcher that resolves installed modules and starts softIocPVX
iocshLoadAn IOC shell command that executes another file with macro arguments
MacroA named value substituted in a database or startup file
PVApvAccess, the protocol used by pvxget and pvxput
PVProcess variable, a named value exposed by an IOC
PVXSThe library providing PVA clients and QSRV2 support in this distribution
QSRV2PVXS support exposing EPICS database records over PVA
RecordOne database instance containing a value, fields, processing behavior, and alarms
RegistrarA function that registers support or commands with the IOC
StreamDeviceDevice support that exchanges text messages using protocol files
Substitution fileA file listing macro values used to instantiate a database template
TemplateA database definition parameterized with macros
TOPThe application’s top-level path, used during builds and IOC startup