Creating an Odin Detector Driver#

Introduction#

This tutorial walks through creating a FastCS driver for a detector controlled by Odin. The fastcs-odin package builds on FastCS to provide an OdinController that introspects an Odin server and creates sub controllers and attributes for each adapter it finds.

This tutorial will walk through the creation of a driver that can:

  • Introspect an Odin deployment and expose all parameters as PVs

  • Configure and run detector acquisitions from a single top-level API

  • Display a live view of captured frames

  • Use logging and tracing to debug the data path

Set Up#

Odin Deployment#

The odin-data-example deployment container should be started:

docker run --rm -it --security-opt label=disable \
    -v /dev/shm:/dev/shm -v /tmp:/tmp --net=host \
    ghcr.io/odin-detector/odin-data-example-runtime:0.2.3

All applications should start without errors.

Python Environment#

Clone fastcs-odin and open it in VS Code. Reopen in the dev container and install the dependencies:

pip install 'fastcs[epics]' pillow aioca

An example.py file should be created in the project root.

Phoebus#

A Phoebus container should be started. A settings file will likely be needed to configure name servers for both PVA and CA.

A Minimal Controller#

The core of a FastCS device driver is the Controller. An ExampleOdinController should be created that inherits from Controller with a single read-write integer attribute, and launched with FastCS.

The application will start and drop into an interactive shell. The attribute can be read and written from the shell:

In [1]: controller.foo.get()
Out[1]: 0

In [2]: await controller.foo.put(1)

In [3]: controller.foo.get()
Out[3]: 1

Note

There is also a helper if there are errors about running on the wrong event loop:

run(controller.foo.put(1))

Adding an EPICS Transport#

An EPICS CA transport can be added to expose the controller’s attributes as PVs. The PV prefix should be unique.

The IOC will now be serving PVs. They can be listed in the interactive shell and interacted with from a terminal:

In [1]: dbl()
EXAMPLE:Foo_RBV
EXAMPLE:Foo
EXAMPLE:PVI_PV
 caget EXAMPLE:Foo_RBV
EXAMPLE:Foo_RBV            1 caput EXAMPLE:Foo 5
Old : EXAMPLE:Foo                1
New : EXAMPLE:Foo                5 caget EXAMPLE:Foo_RBV
EXAMPLE:Foo_RBV            5

Generating a Phoebus UI#

FastCS can be configured to generate a Phoebus .bob file for a UI.

opis/example.bob should appear and can be opened in Phoebus with File > Open > example.bob. Values can be set from the UI to verify it works.

Connecting to Odin#

The controller should be updated to inherit from OdinController instead of Controller. This controller connects to an Odin server, introspects all of its adapters, and creates sub controllers and attributes for each one automatically.

Note

Warnings about parameters failing to be read are expected for some adapter parameters.

The PVs can be listed in the interactive shell to see what has been created:

In [1]: dbl()
EXAMPLE:DETECTOR:ConfigExposureTime_RBV
...
EXAMPLE:FP:0:HDF:FileUseNumbers_RBV
...
EXAMPLE:FR:2:DecoderEnablePacketLogging_RBV
...
EXAMPLE:DETECTOR:Start

The controller now has sub controllers for each adapter:

In [2]: controller.sub_controllers
Out[2]:
{'DETECTOR': OdinAdapterController(path=DETECTOR, sub_controllers=None),
 'FR': FrameReceiverAdapterController(path=FR, sub_controllers=['0', '1', '2', '3']),
 'FP': FrameProcessorAdapterController(path=FP, sub_controllers=['0', '1', '2', '3']),
 'LIVE': OdinAdapterController(path=LIVE, sub_controllers=None),
 'SYSTEM': OdinAdapterController(path=SYSTEM, sub_controllers=None)}

Sub controllers for adapters can be mapped to specific classes, or the fallback OdinAdapterController, which introspects the parameter tree and adds no additional logic.

Phoebus UI#

The display can be reloaded in Phoebus (right-click > Re-load display). There should now be buttons for each sub controller.

  • Open the DETECTOR screen. Pressing Start should cause the frame counter to tick up.

  • The FP screen has top-level attributes that read/write each individual FP instance. Things that must differ per instance (like CtrlEndpoint) are excluded. These can be seen in the screen for the specific instance, e.g. FP0.

  • The FP screen also has PVs for the example dataset. These are detector-specific and defined in the Odin config file.

Running an Acquisition#

An acquisition can now be run using the sub controller screens:

  1. Set FP.FilePath = /tmp

  2. Set FP.FilePrefix = test

  3. Set FP.Frames = 10

  4. Press FP.StartWriting

  5. Check that FP.Writing is set

  6. Set DETECTOR.Frames = 10

  7. Press DETECTOR.Start

  8. Watch FP.FramesWritten count up to 10 and then FP.Writing unset

The interactive shell will show the parameters that are being set.

Improving the API#

Navigating between sub screens is fiddly. Top-level attributes can be added that fan values out to the relevant sub controllers. The foo attribute should be removed and an initialise method added to create new attributes after the Odin introspection has completed.

After running and reloading the display, the new PVs will appear on the top screen.

Note

FP and DETECTOR are accessed as attributes of self, but they are unknown to static type checkers because they are only created at runtime during Odin introspection. There will be no autocompletion for them yet.

Type Hints for Sub Controllers#

FastCS validates type-hinted attributes, methods and sub controllers during initialisation to fail early if something is wrong. Typed sub controller classes can be created and type hints added to ExampleOdinController.

An ExampleFrameProcessorAdapterController inheriting from FrameProcessorAdapterController with a frames attribute hint, and an ExampleDetectorAdapterController inheriting from OdinAdapterController with a config_frames attribute hint should be created.

Note

frames has since been added to the parent FrameProcessorAdapterController, but is kept here as an example of the pattern that can be applied to any other attribute on the parent controller.

Type hints for FP and DETECTOR should be added on ExampleOdinController, and _create_adapter_controller overridden so that the correct controller types are instantiated.

Without overriding _create_adapter_controller, the application will fail with:

RuntimeError: Controller 'ExampleOdinController' introspection of hinted sub controller
'DETECTOR' does not match defined type. Expected 'ExampleDetectorAdapterController'
got 'OdinAdapterController'.

Type hints for attributes or sub controllers that don’t exist will also fail:

RuntimeError: Controller `ExampleOdinController` failed to introspect hinted
controller `FOO` during initialisation
RuntimeError: Controller `ExampleOdinController` failed to introspect hinted
attribute `foo` during initialisation

Adding Commands#

Parameters can now be set from the top screen, but acquisitions cannot be run yet. start and stop type hints should be added on ExampleDetectorAdapterController, along with acquire and stop command methods on ExampleOdinController.

Note

FrameProcessorAdapterController already has start_writing and stop_writing defined statically, so they do not need to be added to ExampleFrameProcessorAdapterController.

After running and reloading Phoebus, an acquisition can now be started and stopped from the top screen.

Adding Status Attributes#

The top screen can trigger acquisitions but has no status. status_acquiring and status_frames type hints should be added to ExampleDetectorAdapterController, and top-level summary attributes created that aggregate status from multiple sub controllers.

The StatusSummaryAttributeIORef aggregates values from multiple attributes. The any function is used for acquiring (true if any source is active) and min for frames_captured (the lowest count across sources).

Controller Logic#

There is a risk that the file writer is slower to start than the detector and frames will be missed. wait_for_value can be used to ensure that the file writers have started before starting the detector.

Live View with Scan Methods#

The live view adapter can be used to see frames as they pass through. A Waveform attribute can be added to display the image along with a @scan method to periodically query the live view adapter for images.

Note

The image dimensions and dtype could be queried from LIVE.Shape and LIVE.FrameDtype at runtime to create live_view_image dynamically.

The EPICS CA transport does not support 2D arrays, so it will give a warning and the LiveViewImage PV will not be created via CA.

Adding PV Access#

An EpicsPVATransport can be added alongside the existing EpicsCATransport to serve both transports simultaneously. PVA supports 2D array attributes, so the LiveViewImage PV will be available over PVA.

An acquisition can be run and the live view watched (LiveViewImage button):

  1. Set DETECTOR.Frames = 0 (continuous)

  2. Press DETECTOR.Start

Logging and Tracing#

There is a custom logger built into fastcs that allows structured logging and trace-level logging that can be enabled at runtime per-attribute.

Add INFO level log statements to the acquire and stop commands to record when writing starts and stops.

Running an acquisition will then produce log output:

[2025-12-23 12:07:31.615+0000 I] Starting writing
[2025-12-23 12:07:32.012+0000 I] Stopping writing

TRACE level logging must be configured for trace messages to appear. This will not produce additional output until tracing is enabled at runtime, although it will enable DEBUG level output. There are trace level messages logged throughout the fastcs codebase to enable debugging live systems. All classes that inherit Tracer can call log_event to emit trace messages, which can be enabled at runtime by calling enable_tracing. For example, Attributes:

In [1]: controller.FP[0].HDF.file_path.enable_tracing()
[2025-12-23 12:07:31.615+0000 T] Query for parameter [ParameterTreeAttributeIO] uri=api/0.1/fp/0/config/hdf/file/path, response={'path': '/tmp'}
...
In [2]: controller.FP[0].HDF.file_path.disable_tracing()