Add a Stream Device with ibek pattern#
In Add the Standard Detector Plugins you vendored a plugin set into an existing instance
with ibek pattern. Here you vendor a stream device definition the same way.
You build a new IOC on the generic ioc-streamdevice image, vendor the
lakeshore340 temperature-controller support into it, and run the device
simulator as a compose sidecar. Substitute your own names throughout.
By the end you will have:
a new IOC instance,
bl01t-ea-temp-01, on theioc-streamdeviceimage;the
lakeshore340StreamDevice support vendored into it at a pinned version, recorded inruntime-lock.yaml;a simulator running as a sidecar, with the IOC reading temperatures from it.
Note
The same image can host any of the StreamDevice drivers in the library — you choose the device per instance by vendoring it, rather than by picking a different image. The IOC loads the vendored support at container start, so no rebuild is needed.
Create the instance#
An instance is a folder under services/; copy the skeleton and open the repo
in your editor, exactly as in Create an IOC Instance:
cd t01-services
cp -r services/.ioc_template services/bl01t-ea-temp-01
code .
In services/bl01t-ea-temp-01/compose.yml, replace every ioc_default_name
with bl01t-ea-temp-01, and set the image to the generic StreamDevice IOC:
image: ghcr.io/epics-containers/ioc-streamdevice-runtime:2.8.26ec1
Vendor the lakeshore340 support#
ibek pattern runs on your workstation. From the repo root, vendor the
driver into the instance you just created:
ibek pattern add ibek-runtime-streamdevice:lakeshore340@0.1.1 services/bl01t-ea-temp-01
Note
ibek pattern needs ibek ≥ 4.6.2. If it is not on your PATH, add it with
uv tool install 'ibek>=4.6.2'.
ibek-runtime-streamdevice is one of ibek’s built-in libraries, resolved from
its name to
its GitHub repo.
The command vendors three files into config/ — each with a
# Vendored … DO NOT EDIT header — and writes a runtime-lock.yaml at the
instance root:
File |
Role |
|---|---|
|
The entity model |
|
The StreamDevice protocol (the serial command set). |
|
The EPICS database of temperature/heater records. |
|
Pins the version and the per-file SHA-256 hashes. |
Commit the lock with the instance. Anyone can later confirm the vendored files are untampered with:
ibek pattern check services/bl01t-ea-temp-01
Configure the IOC#
Open services/bl01t-ea-temp-01/config/ioc.yaml and define the device. The
asyn.AsynIP entity opens a TCP connection to the simulator (added next); the
lakeshore340.lakeshore340 entity — supplied by the support file you just
vendored — drives it over that port:
ioc_name: "{{ _global.get_env('IOC_NAME') }}"
description: A simulated Lakeshore 340 temperature controller
entities:
- type: epics.EpicsEnvSet
name: EPICS_TZ
value: GMT0BST
- type: epics.EpicsEnvSet
name: STREAM_PROTOCOL_PATH
value: /epics/runtime/protocol/
- type: devIocStats.iocAdminSoft
IOC: "{{ ioc_name | upper }}"
- type: asyn.AsynIP
name: p1
port: 170.200.0.100:5401
- type: lakeshore340.lakeshore340
P: BL01T-EA-TEMP-01
PORT: p1
name: lakeshore
STREAM_PROTOCOL_PATH tells StreamDevice where to find the vendored .proto
file at runtime; the IOC’s start.sh copies it there at container start (via
ibek runtime place-files). PORT: p1 wires the device to the Asyn port, and
port: 170.200.0.100:5401 is the fixed address the simulator will be given on
the channel_access network (set up next).
Run the simulator as a sidecar#
The lakeshore340 controller is an external serial device, so the IOC needs
something to talk to. Diamond ships a small pure-stdlib simulator; download it
into the instance folder:
curl -o services/bl01t-ea-temp-01/lakeshore340_sim.py \
https://raw.githubusercontent.com/DiamondLightSource/lakeshore340/main/etc/simulations/lakeshore340_sim.py
Note
The DiamondLightSource/lakeshore340 repository is being retired; the simulator
will move into ibek-runtime-streamdevice. When it does, only the URL above
needs updating.
Add a second service to services/bl01t-ea-temp-01/compose.yml that runs the
script on a stock Python image, on the same channel_access network as the IOC:
lakeshore-sim:
image: python:3-slim
command: python -u /sim/lakeshore340_sim.py 5401
volumes:
- ./lakeshore340_sim.py:/sim/lakeshore340_sim.py:ro
security_opt:
- label=disable
networks:
channel_access:
ipv4_address: 170.200.0.100
The IOC reaches the simulator at 170.200.0.100:5401. A fixed IP is used
deliberately: Diamond’s rootless podman runs the CNI network backend without the
dnsname plugin, so containers on channel_access cannot resolve each other by
name — only by address. Pinning the simulator to 170.200.0.100 (any free
address inside CA_SUBNET, which is set in .env) gives the IOC a stable target
without relying on container DNS. If you change CA_SUBNET, change this address
and the port: in ioc.yaml to match.
python -u runs the script unbuffered so its print() output reaches
docker compose logs immediately; without it, Python block-buffers stdout when
it is not a terminal and the simulator log stays silent.
The whole instance — IOC, config and simulator — is now self-contained and reproducible.
Bring it up#
Register the instance by adding it to the include: list in the repo-root
compose.yaml:
include:
- services/bl01t-ea-temp-01/compose.yml
...
Then start the beamline:
source ./environment.sh
docker compose up -d
Check it works#
The simulator logs every query it receives. Follow its output:
docker compose logs lakeshore-sim -f # ctrl-c to stop following
Initialising ls340 simulator, V2.0 2024.01.21
Listening on port: 5401
Connection from: ('170.200.0.3', 54312)
RECEIVED: *IDN?
RECEIVED: KRDG? 0
RECEIVED: SETP? 1
The IOC reads the device ID, the temperature channels (KRDG0–KRDG3), the
setpoint (SETP) and the heater output (HTR). Confirm a couple of PVs from
inside the IOC container:
docker compose exec bl01t-ea-temp-01 caget BL01T-EA-TEMP-01:ID BL01T-EA-TEMP-01:KRDG0
BL01T-EA-TEMP-01:ID LSCI,MODEL340,123456,02032001
BL01T-EA-TEMP-01:KRDG0 23.4
Note
There are is no PVI device file for this device yet, so no auto-generated PVI screen.
For now, inspect its PVs directly with caget.
TODO: add a PVI device file and auto-generated Phoebus screen for this tutorial.
Manage the running IOC and its sidecar with the same docker compose commands
from Deploy and Manage IOC Instances Locally.
Next steps#
Add the Standard Detector Plugins — vendor a plugin set (the AreaDetector mirror of this device-support workflow) into an existing IOC.
Create a Generic IOC — go the other way and bake your own support into a Generic IOC at build time.