Author Your Own Runtime Pattern#

In Add the Standard Detector Plugins you vendored a published runtime pattern — the standard AreaDetector plugin set — into an existing IOC instance. Here you author your own pattern: a cut-down plugin set called basicPlugins, published from a fork of the ibek-runtime-support library.

This is the runtime mirror of Create a Generic IOC, where you forked a build-time support library and rebuilt an image. This time nothing is rebuilt — a generic IOC picks up your new support at its next container start, because ibek pattern vendors the file-set into the instance’s config/ and ibek runtime generate2 discovers it at boot.

For this tutorial you are back in your t01-services compose project on the workstation — the same repo you built up in Create an IOC Instance and the earlier compose tutorials — not the generic-IOC devcontainer from Create a Generic IOC. You will use the same bl01t worked example; substitute your own names throughout.

Note

ibek pattern runs on your workstation and needs ibek ≥ 4.8.0. If ibek is not installed, run uv tool install ibek --upgrade.

Fork the pattern library#

Patterns live in curated central libraries, so — exactly as with ibek-support in Create a Generic IOC — you work from a fork and open a pull request later if the pattern is generally useful.

# run alongside your t01-services directory
git clone https://github.com/<your-org>/ibek-runtime-support

Author the pattern#

A pattern is just a top-level folder named after the pattern, holding one or more files. The only required member is the *.ibek.support.yaml support definition. Create the folder and file in your fork:

cd ibek-runtime-support
mkdir basicPlugins
code basicPlugins/basicPlugins.ibek.support.yaml

basicPlugins declares a single entity_model whose sub_entities wire up just two AreaDetector plugins — a viewable Channel Access array (NDStdArrays) and live statistics (NDStats). It mirrors the shape of the real detectorPlugins model but trimmed to the essentials:

# yaml-language-server: $schema=https://github.com/epics-containers/ibek/releases/download/3.0.1/ibek.support.schema.json

module: basicPlugins

entity_models:
  - name: basicPlugins
    description: A minimal AreaDetector plugin set — a viewable array and stats
    parameters:
      P:
        type: str
        description: Prefix for the AreaDetector PVs
      CAM:
        type: str
        description: AreaDetector NDArray port name of the camera
      PORTPREFIX:
        type: str
        description: Prefix for all plugin ports

    sub_entities:
      - type: ADCore.NDStdArrays
        P: "{{ P }}"
        R: ":ARR:"
        PORT: "{{ PORTPREFIX }}.arr"
        NDARRAY_PORT: "{{ CAM }}"
        NELEMENTS: 1048576
        TYPE: Int8
        FTVL: CHAR

      - type: ADCore.NDStats
        P: "{{ P }}"
        R: ":STAT:"
        PORT: "{{ PORTPREFIX }}.stat"
        NDARRAY_PORT: "{{ CAM }}"
        HIST_SIZE: 256
        XSIZE: 1280
        YSIZE: 1024

Only the three wiring parameters are exposed. Most of the plugins’ tuning knobs (channel count, queue depth …) keep their ADCore defaults; the few that have no default are set inline — NELEMENTS so the array is big enough to view, and NDStats’s required HIST_SIZE / XSIZE / YSIZE (the histogram bin count and the maximum image dimensions it computes statistics over). A production pattern like the real detectorPlugins promotes more of these to parameters — add them the same way when you need them.

The ADCore plugins it references are compiled into every AreaDetector image, so this pattern needs no .db / .template of its own — it only adds the ibek entities that instantiate and connect them at runtime.

Optional: declare what gets vendored#

basicPlugins holds one file and every byte of it belongs in the IOC, so there is nothing to declare. That is the usual case: a pattern with no manifest vendors every file in its folder into the instance’s config/ — ibek supplies a default manifest for it:

version: 1
vendor:
  - src: '.*'
    dest: config

Write one only when the folder holds something an IOC must not receive — a README, a datasheet, device notes you want browsable in the library. Everything in config/ is mounted into the IOC container (and on Kubernetes it is the ConfigMap), so whatever lands there travels to the beamline. An ibek.manifest.yaml at the root of the pattern folder lists what counts as a runtime file:

version: 1
vendor:
  - src: '.*\.(template|db|req|proto|protocol|ibek\.support\.yaml|pvi\.device\.yaml)$'
    dest: config

src is a regular expression that must match a file’s whole path within the pattern folder — ibek matches with re.fullmatch, so a rule of '\.proto$' on its own matches nothing; lead each one with .*. dest is where the matched files land, relative to the instance root, and config is the only destination a services repo needs.

The list is an allow-list: a file matched by no entry is not vendored, which is exactly how the README stays out of the IOC. The manifest itself is never vendored either. That cuts both ways — a runtime file your rules forget is dropped just as quietly, with no error and a passing ibek pattern check — so the alternation has to name every extension the pattern really ships. Only a manifest matching nothing at all is reported as a mistake.

A manifest change only takes effect once the instance is re-vendored at a version that carries it — cut a new tag and re-pin with -v <tag> (below, a local --source pins HEAD, so re-running ibek pattern add is enough). When it re-vendors, files that fall outside the manifest’s rules are deleted from config/, so the instance only ever keeps the files the pattern currently claims.

Create a fresh instance to vendor into#

Make a new instance bl01t-ea-cam-02 exactly as you made bl01t-ea-cam-01 in Create an IOC Instance: copy services/.ioc_template, set the name and image in its compose.yml, and register it in the repo-root compose.yml. Use a separate instance so its plugin ports do not clash with the detectorPlugins already running in bl01t-ea-cam-01. Pin the SimDetector image:

    image: ghcr.io/epics-containers/ioc-adsimdetector-runtime:2.11ec3

Give it a camera in services/bl01t-ea-cam-02/config/ioc.yaml:

# yaml-language-server: $schema=https://github.com/epics-containers/ioc-adsimdetector/releases/download/2.11ec3/ibek.ioc.schema.json

ioc_name: "{{ _global.get_env('IOC_NAME') }}"

description: An IOC that simulates an area detector

entities:
  - type: epics.EpicsEnvSet
    name: EPICS_TZ
    value: GMT0BST

  - type: devIocStats.iocAdminSoft
    IOC: "{{ ioc_name | upper }}"

  - type: ADSimDetector.simDetector
    PORT: DET.DET
    P: BL01T-EA-CAM-02
    R: ":DET:"

Vendor your pattern#

Because you are still iterating, vendor straight from your local clone with --source. A local-path source is copied as-is, so you do not have to commit, push or tag the fork between edits:

cd t01-services
ibek pattern add --source ../ibek-runtime-support basicPlugins services/bl01t-ea-cam-02

This writes basicPlugins.ibek.support.yaml into services/bl01t-ea-cam-02/config/ — an exact copy of the file in your fork — and records its sha256 in services/bl01t-ea-cam-02/runtime-lock.yaml. That is all the vendor step needs: at container start ibek runtime generate2 config discovers the vendored support file and loads your entity — no image rebuild, and nothing else to wire up.

The lock now pins the pattern. A local source pins to HEAD:

version: 1
patterns:
  basicPlugins:
    version: HEAD
    source: ../ibek-runtime-support
    files:
      config/basicPlugins.ibek.support.yaml: sha256:…

The files: keys are relative to the instance root, which is why config/ appears in them.

Warning

Keep editing the pattern in your fork and re-run ibek pattern add — never the copy under config/. The copy looks like any other file, and only the lock records that it is not yours: an edit there is overwritten by the next add, update or restore, and fails ibek pattern check before that.

ibek pattern check services/bl01t-ea-cam-02 re-hashes the vendored file and exits non-zero if it has drifted from the lock — run it in CI or a pre-commit hook to guarantee the committed config/ matches what was pinned. Because the copy is byte-for-byte the file in the library, plain diff works as a second opinion:

diff -r services/bl01t-ea-cam-02/config/ ../ibek-runtime-support/basicPlugins/

Vendored files that appear on both sides compare equal; your own ioc.yaml and anything the pattern does not vendor show up as Only in … lines.

Use the new entity#

Add one basicPlugins.basicPlugins entity to the instance’s ioc.yaml, wired to the camera’s Asyn port (CAM: DET.DET, the simDetector PORT above):

  - type: basicPlugins.basicPlugins
    P: BL01T-EA-CAM-02
    CAM: DET.DET
    PORTPREFIX: DET

This single entity expands — via its sub_entities — into both plugins.

Now set the compose project up to run it. First register the instance in the repo-root compose.yml include: list:

include:
  - services/bl01t-ea-cam-01/compose.yml
  - services/bl01t-ea-cam-02/compose.yml
  ...

Then point Phoebus at the new instance. PVI auto-generates an index.bob for it at opi/auto-generated/bl01t-ea-cam-02/index.bob; open it on launch by editing the command: line of the phoebus service in services/phoebus/compose.yml:

    command: phoebus-product/phoebus.sh -settings /config/settings.ini -resource /opi/auto-generated/bl01t-ea-cam-02/index.bob -server 7010

Bring the beamline up:

source ./environment.sh
docker compose up -d

At startup PVI generates an engineering screen per entity under opi/auto-generated/bl01t-ea-cam-02/. Your new pattern adds two panels: an NDStdArrays panel publishing the camera frames as a Channel Access waveform (open it in Phoebus’s image widget after Acquire on the simDetector and Enable on the plugin), and an NDStats panel showing live min/max/mean, sigma and a histogram. A maintainer screenshot will be added here.

Promote the pattern#

Local-path vendoring is for iteration only. Once the pattern is settled, make it shareable:

  1. Commit and push it to your fork, then cut a release tag (vX.Y.Z) — a tag versions the whole library at once.

  2. Re-pin the instance from the published fork instead of the local path. Point the ibek-runtime-support library name at your fork with the IBEK_PATTERN_LIBRARIES environment variable, then add the qualified reference at your new tag:

    export IBEK_PATTERN_LIBRARIES="ibek-runtime-support=https://github.com/<your-org>/ibek-runtime-support"
    ibek pattern add ibek-runtime-support:basicPlugins@v0.1.0 services/bl01t-ea-cam-02
    

    The lock now records version: v0.1.0 and source: github.com/<your-org>/ibek-runtime-support — a fully reproducible pin. Once the pattern is merged upstream you can drop the override and pin the canonical library directly.

  3. If the pattern is generally useful, open a pull request against ibek-runtime-support so every beamline can vendor it.

Note

A pattern’s file-set is not fixed to a single file. The lock simply hashes a file list, so a pattern may also ship .template / .db, autosave .req files, or a .pvi.device.yaml screen descriptor alongside its support yaml — whatever the support definition references. All of it is vendored unless an ibek.manifest.yaml narrows the set. Add a Stream Device with ibek pattern vendors exactly such a multi-file device-support pattern.