The ophyd-api is used in production on most beamlines at the Australian Synchrotron. This github version
is just the latest commit, with some small edits for GitHub. The active development is current done
in an on prem repository. Note that it depends on some internal libraries that are yet to be released to
GitHub, so this code is more to demonstrate our approach, than be a directly useable system. There is a strong
dependence on how ophyd devices are instantiated by our beamline libraries. We are more than happy to discuss
this with interested parties.
This project is built off the work previously done on the SAXS/WAXS beamline
This is a fastapi server that provides websocket based subscription over ophyd device components that are suitable to be monitored (components that are low level type of components (like EPICS signals)).
It is designed to be provided with a python library that contains the relevant ophyd device instances.
At a basic high level, you provide it with a python library (typically a beamline library) by way of running it in a virtual environment or a system that has the relevant library in its path. You then provide it with the library to use via a launch option, and then you can subscribe to the low level attributes (typically corresponding to a single EPICS PV) of any of the OPHYD device instances provided by the library, via GET and POST requests and a websocket connection to this API.
This API works with some standard base epics types of ophyd devices (like EpicsMotor, EpicsSignal, EpicsSignalBase) out of the box.
If you supply your own custom device types, then you may find you run into difficulties when querying for that device type through this API.
For example, if the following describes your setup:
- this API is accessible at
localhost:8080 - beamline library called xas_beamline_library accessible in its venv
- started with the command option --library xas_beamline_library
- xas_beamline_library has a custom ophyd device defined at
xas_beamline_library.setup.beamline_builder.devices.dcm
then you should be able to query the dcm device through this api via a GET request to:
http://localhost:8080/api/v1.0/devices/setup.beamline_builder.devices.dcm?describe=true
However you find that the fields reported don't report the proper values, then the reason
is due to the code at @app.get(f"{base_url}/devices/{{device}}") in the
ophyd_api.py
You may be tempted to add in your own isinstance check,
There is a plugin option available to you instead:
You can supply another argument to the startup command, --describer or -d
and this value should be similar to the --library value. A string that would
be value to the importlib.import_module() call when running in the API's venv.
so for example if you had a module in your beamline library called
describer_plugin.py then you could pass
--describer xas_beamline_library.describer_plugin and then if your file
describer_plugin.py contains code similar to the below, it will work:
from xas_beamline_library.devices.xas_dcm import MotorDCM
def describe(device_cpt, description):
"""to be a valid plugin it must be named describe and take two args
and raise NotImplementedError if it does not want to supply an updated
description dict."""
if isinstance(device_cpt, MotorDCM):
my_description = dict(description)
# following three lines add to the description dict, need to be
# values that will readily parse to JSON.
my_description["write_pv"] = device_cpt.setpoint.get()
my_description["lower_disp_limit"] = device_cpt.energy.lo_lim.get()
my_description["upper_disp_limit"] = device_cpt.energy.hi_lim.get()
return my_description
else:
raise NotImplementedError("I was made to process components of type "
"MotorDCM as defined by the "
"xas-beamline-library")The interface for the plugins supported by this API is that they contain a
describe function which takes a device component instance as the first arg
and a description dict as the second arg.
the function is then free to create a new description dict filling in whatever
values for whatever keys it wants, but if you expect it to work with some of the
react ophyd components that might be making use of this API then you should adhere
to the key name conventions.
The describe function should additionally raise a NotImplementedError if it does not know or want to supply an updated description dict.
multiple plugin modules can be specified at the command line, simply repeat
the --describer or -d option multiple times.
plugins will be given a chance to modify the description dict in the order in which they are supplied.
Using the XAS beamline's DCM bragg angle (which indicates energy being output) as an example to display in the browser.
First check that you are able to read the value without this API. First try to read the value using a plain caget. This will prove that your computer is able to 'speak' to the EPICS network in question.
The command to read would be:
caget SR12ID01DCM01:ENERGY_RBV
If that works then you know you can safely move on to the next part.
Next check that the beamline library you want to use is configured correctly and is working.
Get yourself a python virtual environment that have the beamline library installed in.
Open a python REPL interpretor and then try to import the beamline library.
The commands we'll use is:
from xas_beamline_library.queueserver.qs_config import *
dcm.bragg.user_readback.get()You can see that the above corresponds to these lines of the code.
if the above works then we can move onto launching this ophyd-api:
The command to use will be:
python ophyd_api.py --library xas_beamline_library --port 8080 --log-level debug
Note that the value provided for --library is the value that would be valid as an import statement in the code or in the REPL. So for example if you look at that line we ran previously:
from xas_beamline_library.queueserver.qs_config import *
The --library value needs to match that. This value is different to the name of
the library in the python packaging index (which for xas beamline library is
xas-beamline-library (the value you would use if you do pip install or
poetry add))
Now the ophyd-api should be up and running on port 8080 and configured to search through the xas-beamline-library for ophyd devices when we later use a client to connect and subscribe to that device.
Now we're ready to try and connect to it with a react app.
First you will need to add the necessary files to your react app to allow websocket communication with the ophyd api.
Then the value you would want to use for an ophyd status field component's
"device" prop would be "queueserver.qs_config.dcm.bragg.user_readback" because
remember that when we confirmed it just in the python REPL we used this command:
from xas_beamline_library.queueserver.qs_config import *
dcm.bragg.user_readback.get()The first part (xas_beamline_library) was already provided to the ophyd-api
via the --library option when we launched it. But in the python repl we had
to go into the queueserver module and then the qs_config submodule to pull
out the instance named dcm and then get its bragg attribute and then get
the bragg attribute's user_readback attribute which corresponded to the PV
that we were actually interested in.
So that's why we use the string
"queueserver.qs_config.dcm.bragg.user_readback" because that points the
ophyd-api all the way down into the actual low-level ophyd component that maps
to an EPICS PV that we can subscribe to ("monitor" in EPICS speak).
For a slightly different example, we can still test the functionality of the ophyd-api even if we don't have access to the 'real' EPICS network, by using a mock IOC powered by caproto, defining a "beamline-library" to wrap one of its exposed PVs and then seeing its value change when we subscribe to it.
For convenience there is such an example is provided in this repo in the
directory ophyd_epics_api/test_local_ioc, which is based on the
confluence page on caproto.
The python script called mock_ioc.py is the mock IOC obviously, and the
devices.py python script is the beamline-library we'll use (technically it will
see the entire test_local_ioc as the beamline library, but when we supply device
addresses for it to use we will start with devices.).
To run it, in a separate terminal run the mock_ioc.py file with simply:
python mock_ioc.py
You now have an IOC running on your machine with a few PVs
Next you'll start the ophyd-api with the following launch command:
EPICS_CA_ADDR_LIST=127.0.0.1 python ophyd_api.py --library test_local_ioc --port 8080 --log-level debug
The EPICS_CA_ADDR_LIST environment variable is required to tell it (or rather,
the pyepics that ophyd uses under the hood) to look for ioc's on your localhost,
and to use the library test_local_ioc as the beamline library to look for
ophyd devices in. This works because that's the name of the folder (python
package) in the same directory as the ophyd_api.py script (so
import test_local_ioc is valid python for the ophyd_api script to execute by
virtue of the current directory being one of the places that the import action
traverses in looking for packages to import).
Next here is an example of the device prop value that you would then use in a
suitable react app would be:
"devices.mock_record.ai0"
to test it quickly without the use of a full react application you can use the provided postman scripts.