diff --git a/README.rst b/README.rst index 14456b55..0db60646 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ pythonIoc This module allows an EPICS IOC with Python Device Support to be run from within the Python interpreter. Records can be programmatically created and arbitrary -Python code run to updated them and respond to caputs. It supports cothread and +Python code run to update them and respond to caputs. It supports cothread and asyncio for concurrency. ============== ============================================================== @@ -15,7 +15,7 @@ Source code https://github.com/dls-controls/pythonIoc Documentation https://dls-controls.github.io/pythonIoc ============== ============================================================== -A simple example of the use of this library is the following: +A simple example of the use of this library: .. code:: python @@ -40,12 +40,12 @@ A simple example of the use of this library is the following: ai.set(ai.get() + 1) cothread.Sleep(1) + cothread.Spawn(update) # Finally leave the IOC running with an interactive shell. softioc.interactive_ioc(globals()) - .. |code_ci| image:: https://github.com/dls-controls/pythonIoc/workflows/Code%20CI/badge.svg?branch=master :target: https://github.com/dls-controls/pythonIoc/actions?query=workflow%3A%22Code+CI%22 :alt: Code CI diff --git a/docs/conf.py b/docs/conf.py index 10f60509..f5e72cac 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -96,6 +96,7 @@ intersphinx_mapping = dict( python=('https://docs.python.org/3/', None), cothread=("https://cothread.readthedocs.org/en/stable/", None), + aioca=("https://dls-controls.github.io/aioca/master/", None), epicsdbbuilder=( "https://dls-controls.github.io/epicsdbbuilder/master/", None) ) diff --git a/docs/examples/example_asyncio_ioc.py b/docs/examples/example_asyncio_ioc.py new file mode 100644 index 00000000..d2c99524 --- /dev/null +++ b/docs/examples/example_asyncio_ioc.py @@ -0,0 +1,29 @@ +# Import the basic framework components. +from softioc import softioc, builder, asyncio_dispatcher +import asyncio + +# Create an asyncio dispatcher, the event loop is now running +dispatcher = asyncio_dispatcher.AsyncioDispatcher() + +# Set the record prefix +builder.SetDeviceName("MY-DEVICE-PREFIX") + +# Create some records +ai = builder.aIn('AI', initial_value=5) +ao = builder.aOut('AO', initial_value=12.45, always_update=True, + on_update=lambda v: ai.set(v)) + +# Boilerplate get the IOC started +builder.LoadDatabase() +softioc.iocInit(dispatcher) + +# Start processes required to be run after iocInit +async def update(): + while True: + ai.set(ai.get() + 1) + await asyncio.sleep(1) + +asyncio.run_coroutine_threadsafe(update(), dispatcher.loop) + +# Finally leave the IOC running with an interactive shell. +softioc.interactive_ioc(globals()) diff --git a/docs/examples/example_cothread_ioc.py b/docs/examples/example_cothread_ioc.py new file mode 100644 index 00000000..7be1c656 --- /dev/null +++ b/docs/examples/example_cothread_ioc.py @@ -0,0 +1,26 @@ +# Import the basic framework components. +from softioc import softioc, builder +import cothread + +# Set the record prefix +builder.SetDeviceName("MY-DEVICE-PREFIX") + +# Create some records +ai = builder.aIn('AI', initial_value=5) +ao = builder.aOut('AO', initial_value=12.45, always_update=True, + on_update=lambda v: ai.set(v)) + +# Boilerplate get the IOC started +builder.LoadDatabase() +softioc.iocInit() + +# Start processes required to be run after iocInit +def update(): + while True: + ai.set(ai.get() + 1) + cothread.Sleep(1) + +cothread.Spawn(update) + +# Finally leave the IOC running with an interactive shell. +softioc.interactive_ioc(globals()) diff --git a/docs/examples/example_read_from_ioc.py b/docs/examples/example_read_from_ioc.py new file mode 100644 index 00000000..1834d303 --- /dev/null +++ b/docs/examples/example_read_from_ioc.py @@ -0,0 +1,9 @@ +from softioc import softioc +from cothread.catools import caget, caput, camonitor + +print(caget("MY-DEVICE-PREFIX:AI")) +print(caget("MY-DEVICE-PREFIX:AO")) +print(caput("MY-DEVICE-PREFIX:AO", "999")) +print(caget("MY-DEVICE-PREFIX:AO")) + +softioc.interactive_ioc(globals()) diff --git a/docs/explanations/why-use-pythonIoc.rst b/docs/explanations/why-use-pythonIoc.rst index c15a6368..d1b74104 100644 --- a/docs/explanations/why-use-pythonIoc.rst +++ b/docs/explanations/why-use-pythonIoc.rst @@ -38,8 +38,6 @@ allows you to write this as: # Leave the IOC running with an interactive shell. softioc.interactive_ioc(globals()) -ADD THE CONCENTRATOR USE CASE HERE - Dynamically created PVs ----------------------- diff --git a/docs/how-to/make-publishable-ioc.rst b/docs/how-to/make-publishable-ioc.rst new file mode 100644 index 00000000..f9ee37df --- /dev/null +++ b/docs/how-to/make-publishable-ioc.rst @@ -0,0 +1,35 @@ +Create a Publishable IOC +======================== + +As seen in `../tutorials/creating-an-ioc`, a single Python script can be an IOC. +It is also possible (and the most common situation) to have an entire Python module +comprising an IOC. This guide explains both, as well as how to publish an IOC within +the DLS environment. + +Single File IOC +---------------- +An IOC that is entirely contained within a single Python source file can be used as an +IOC inside DLS simply by adding this shebang line:: + + #!/dls_sw/prod/python3/RHEL7-x86_64/softioc/3.0b2/prefix/bin/pythonIoc + + +IOC entry point for a module +------------------------------ +If your IOC is more complicated than one file, it is recommended to write a python +module (including docs/tests/etc.). The Panda Blocks Client will be an example of +this. + + +Make an IOC publishable at DLS +------------------------------ +To make the IOC publishable, a makefile is required: + +``Makefile`` + This file is necessary in order to run ``dls-release.py``, and needs to have + both ``install`` and ``clean`` targets, but doesn't need to actually do + anything. Thus the following content for this file is enough:: + + install: + clean: + diff --git a/docs/how-to/read-data-from-ioc.rst b/docs/how-to/read-data-from-ioc.rst new file mode 100644 index 00000000..35196dd3 --- /dev/null +++ b/docs/how-to/read-data-from-ioc.rst @@ -0,0 +1,31 @@ +Read data from an IOC +====================== + +This guide explains how to read data from an IOC in a separate Python program. + +.. note:: + Please ensure your firewall allows both TCP and UDP traffic on ports 5064 and 5065. + These are used by EPICS for channel access to the PVs. + + +To start, run the `cothread` IOC from `../tutorials/creating-an-ioc` or the +`asyncio` IOC from `use-asyncio-in-an-ioc` and leave it running at the +interactive shell. + +We will read data from that IOC using this script: + +.. literalinclude:: ../examples/example_read_from_ioc.py + +.. note:: + You may see warnings regarding the missing "caRepeater" program. This is an EPICS tool + that is used to track when PVs start and stop. It is not required for this simple example, + and so the warning can be ignored. + +From the interactive command line you can now use the ``caget`` and ``caput`` functions to operate on +the PVs exposed in the IOC. Another interesting command to try is:: + + camonitor("MY-DEVICE-PREFIX:AI", lambda val: print(val)) + + +You should observe the value of ``AI`` being printed out, once per second, every time the PV value +updates. \ No newline at end of file diff --git a/docs/how-to/use-asyncio-in-an-ioc.rst b/docs/how-to/use-asyncio-in-an-ioc.rst index 016d0462..4debe46e 100644 --- a/docs/how-to/use-asyncio-in-an-ioc.rst +++ b/docs/how-to/use-asyncio-in-an-ioc.rst @@ -1,4 +1,33 @@ Use `asyncio` in an IOC ======================= -Write about the differences creating an IOC using `AsyncioDispatcher` +There are two libraries available for asynchronous operations in PythonIOC: +`cothread` and `asyncio`. This guide shows how to use the latter in +an IOC. + +.. note:: + This page only explains the differences between using `cothread` and `asyncio`. + For more thorough explanation of the IOC itself see `../tutorials/creating-an-ioc` + +.. literalinclude:: ../examples/example_asyncio_ioc.py + + +The ``dispatcher`` is created and passed to :func:`~softioc.softioc.iocInit`. This is what +allows the use of `asyncio` functions in this IOC. It contains a new event loop to handle +this. + +The ``async update`` function will increment the value of ``ai`` once per second, +sleeping that coroutine between updates. +Note that we run this coroutine in the ``loop`` of the ``dispatcher``, and not in the +main event loop. + +This IOC will, like the one in `../tutorials/creating-an-ioc`, leave an interactive +shell open. The values of the PVs can be queried using the methods defined in the +`softioc.softioc` module. + + +Asynchronous Channel Access +--------------------------- + +PVs can be retrieved externally from a PV in an asynchronous manner by using the :py`aioca` module. +It provides ``await``-able implementations of ``caget``, ``caput``, etc. See that module for more information. diff --git a/docs/index.rst b/docs/index.rst index 53f1c6db..56b06846 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -59,6 +59,8 @@ Table Of Contents :maxdepth: 1 how-to/use-asyncio-in-an-ioc + how-to/make-publishable-ioc + how-to/read-data-from-ioc .. toctree:: :caption: Explanations diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 4acc2830..2f305ef5 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -374,7 +374,7 @@ starting the IOC. This must be called exactly once after creating all the records required by the IOC and before calling :func:`~softioc.softioc.iocInit`. After this function has been called none of the functions provided by - :mod:`softioc.builder` are usable. + `softioc.builder` are usable. .. automodule:: softioc.alarm diff --git a/docs/tutorials/creating-an-ioc.rst b/docs/tutorials/creating-an-ioc.rst index 4de37e3d..62cbbb5b 100644 --- a/docs/tutorials/creating-an-ioc.rst +++ b/docs/tutorials/creating-an-ioc.rst @@ -1,204 +1,104 @@ Creating an IOC =============== -THIS NEEDS UPDATING - -Using ``pythonSoftIoc`` ------------------------ - -Probably the best way to use ``pythonSoftIoc`` is to start by copying fragments -of a simple example such as ``CS-DI-IOC-02``. This consists of the following -elements: - -1. A startup shell script ``start-ioc`` which launches the soft IOC using a - production build of ``pythonSoftIoc``. This script typically looks like - this:: - - #!/bin/sh - - PYIOC=/path/to/pythonSoftIoc/pythonIoc - - cd "$(dirname "$0")" - exec $PYIOC start_ioc.py "$@" - -2. The startup Python script. This establishes the essential component - versions (apart from the ``pythonSoftIoc`` version), performs the appropriate - initialisation and starts the IOC running. The following template is a - useful starting point:: - - from pkg_resources import require - require('cothread==2.12') - require('epicsdbbuilder==1.0') - - # Import the basic framework components. - from softioc import softioc, builder - import cothread - - # Import any modules required to run the IOC - import ... - - # Boilerplate get the IOC started - builder.LoadDatabase() - softioc.iocInit() - - # Start processes required to be run after iocInit - ... - - # Finally leave the IOC running with an interactive shell. - softioc.interactive_ioc(globals()) - - Note that the use of ``require`` is specific to DLS, and you may have a - different way of managing your installations. - -.. _numpy: http://www.numpy.org/ -.. _cothread: https://github.com/dls-controls/cothread -.. _epicsdbbuilder: https://github.com/Araneidae/epicsdbbuilder - - - Introduction ------------ -The Python Soft IOC consists of two components: a command ``pythonIoc`` and an -associated library :mod:`softioc`. The ``pythonIoc`` command consists of a bare -EPICS IOC linked together with the DLS Python interpreter and configured so that -startup arguments are interpreted by the Python interpreter -- this means that -when ``pythonIoc`` is run it behaves the same as running ``dls-python``. +Once the module has been installed (see `installation`) we can create a +simple EPICS Input/Output Controller (IOC). -Scripts run from within ``pythonIoc`` differ from standard Python scripts in one -detail: they have the ability to create and publish PVs through the -:mod:`softioc` library. Typically both :mod:`cothread` and -:mod:`epicsdbbuilder` will be recruited to help: :mod:`cothread` is used for -dispatching OUT record processing callback methods, :mod:`epicsdbbuilder` is -used for constructing records during IOC initialisation. - -An EPICS IOC created with the help of ``pythonIoc`` and :mod:`softioc` is +An EPICS IOC created with the help of ``pythonIoc`` and `softioc` is referred to as a "Python soft IOC". The code below illustrates a simple IOC -with one PV:: - - # DLS requires - from pkg_resources import require - require('cothread==2.12') - require('epicsdbbuilder==1.0') +with two Process Variables (PVs): - # Import basic softioc framework - from softioc import softioc, builder +.. literalinclude:: ../examples/example_cothread_ioc.py - # Create PVs - builder.SetDeviceName('TS-TEST-TEST-01') - builder.stringIn('TEST', initial_value = 'This is a test') +Each section is explained in detail below: - # Run the IOC. This is boilerplate, and must always be done in this order, - # and must always be done after creating all PVs. - builder.LoadDatabase() - softioc.iocInit() +.. literalinclude:: ../examples/example_cothread_ioc.py + :start-after: # Import + :end-before: # Set - softioc.interactive_ioc(globals()) +The `softioc` library is part of ``pythonIoc``. The two submodules +`softioc.softioc` and `softioc.builder` provide the basic +functionality for Python soft IOCs and are the ones that are normally used. -This example script illustrates the following points. +`cothread` is one of the two possible libraries the IOC can use for +asynchronous operations. +(see `../how-to/use-asyncio-in-an-ioc` for the alternative) -- The use of ``pkg_resources.require`` is standard across all use of the - ``dls-python`` Python interpreter at Diamond, and in this example we are using - both :mod:`cothread` and :mod:`epicsdbbuilder`. Of course, in an officially - published IOC specific versions must be specified, in this example I'm using - the most recent versions at the time of writing. -- The :mod:`softioc` library is part of ``pythonIoc`` and is automatically added - to the path. The two submodules :mod:`softioc.softioc` and - :mod:`softioc.builder` provide the basic functionality for Python soft IOCs - and are the ones that are normally used. -- PVs are normally created dynamically using :mod:`softioc.builder`. All PV - creation must be done before initialising the IOC. +.. literalinclude:: ../examples/example_cothread_ioc.py + :start-after: # Create + :end-before: # Boilerplate -- Once PVs have been created then the associated EPICS database can be created - and loaded into the IOC and then the IOC can be started. +PVs are normally created dynamically using `softioc.builder`. All PV +creation must be done before initialising the IOC. We define a lambda function for +`on_update` on ``ao`` such that whenever we set ``ao``, ``ai`` will be set to the +same value. The ``always_update`` flag ensures that the ``on_update`` function is always +triggered, which is not the default behaviour if the updated value is the same as the +current value. -- Finally the application must refrain from exiting until the IOC is no longer - needed. The :func:`~softioc.softioc.interactive_ioc` runs a Python - interpreter shell with a number of useful EPICS functions in scope, and - passing ``globals()`` through can allow interactive interaction with the - internals of the IOC while it's running. The alternative is to call something - like :func:`cothread.WaitForQuit` or some other :mod:`cothread` blocking - action. +.. literalinclude:: ../examples/example_cothread_ioc.py + :start-after: # Boilerplate + :end-before: # Start -Creating a Publishable IOC --------------------------- -As the example script above shows, a single Python script can be an IOC. -However, to fit into the DLS framework for publishing IOCs in ``/dls_sw/prod`` a -bit more structure is needed. I recommend at least four files as shown: - -``Makefile`` - This file is necessary in order to run ``dls-release.py``, and needs to have - both ``install`` and ``clean`` targets, but doesn't need to actually do - anything. Thus the following content for this file is enough:: - - install: - clean: - -``start-ioc`` - An executable file for starting the IOC needs to be created. I recommend - that this consist of the following boilerplate:: - - #!/bin/sh - - PYIOC_VER=2-6 - EPICS_VER=3.14.12.3 - - PYIOC=/dls_sw/prod/R$EPICS_VER/support/pythonSoftIoc/$PYIOC_VER/pythonIoc - - exec $PYIOC ioc_entry.py "$@" - - Here I have given the startup script for the IOC the name ``ioc_entry.py``. - This name should be replaced by any appropriate name. - -``ioc_entry.py`` - I recommend that the top level Python script used to launch the IOC contain - only ``pkg_resources.require`` statements, simple code to start the body - of the IOC, and it should end with standard code to start the IOC. The - following structure can be followed (here I've assumed that the rest of the - IOC is in a single file called ``ioc_body.py``:: - - from pkg_resources import require +Initializing the IOC is simply a matter of calling two functions: +:func:`~softioc.builder.LoadDatabase` and :func:`~softioc.softioc.iocInit`, +which must be called in this order. After calling +:func:`~softioc.builder.LoadDatabase` it is no longer possible to create PVs. - require('cothread==2.12') - require('epicsdbbuilder==1.0') - # Any other requires needed by this IOC +.. literalinclude:: ../examples/example_cothread_ioc.py + :start-after: # Start + :end-before: # Finally - from softioc import softioc +We define a long-running operation that will increment the value of ``ai`` once per +second. This is run as a background thread by `cothread`. - # Do whatever makes sense to create all the PVs and get ready to go - import ioc_body - ioc_body.initialise() +.. literalinclude:: ../examples/example_cothread_ioc.py + :start-after: # Finally - # Start the IOC -- this is boilerplate - builder.LoadDatabase() - softioc.iocInit() +Finally the application must refrain from exiting until the IOC is no longer +needed. The :func:`~softioc.softioc.interactive_ioc` runs a Python +interpreter shell with a number of useful EPICS functions in scope, and +passing ``globals()`` through can allow interactive interaction with the +internals of the IOC while it's running. The alternative is to call something +like :func:`cothread.WaitForQuit` or some other `cothread` blocking +action. - # If activities need to be started after iocInit, now's the time - ioc_body.start() +In this interpreter there is immediate access to methods defined in the +`softioc.softioc` module. For example the :func:`~softioc.softioc.dbgf` function +can be run to observe the increasing value of ``AI``:: - softioc.interactive_ioc(globals()) + >>> dbgf("MY-DEVICE-PREFIX:AI") + DBF_DOUBLE: 36 + >>> dbgf("MY-DEVICE-PREFIX:AI") + DBF_DOUBLE: 37 - Note that *all* requires *must* occur in this initial startup file. +And the :func:`~softioc.softioc.dbpf` method allows data to be set and to observe +the functionality of the lambda passed to ``on_update`` . We set the value on ``AO`` +and read the value on ``AI`` (exact values will vary based on time taken):: -The rest of the IOC - Of course, a Python script can be structured into any number of Python - modules. In the example above I have illustrated just one such module - called ``ioc_body.py`` with two entry points. + >>> dbgf("MY-DEVICE-PREFIX:AI") + DBF_DOUBLE: 15 + >>> dbpf("MY-DEVICE-PREFIX:AO","999") + DBF_DOUBLE: 999 + >>> dbgf("MY-DEVICE-PREFIX:AI") + DBF_DOUBLE: 1010 Creating PVs ------------ -See the documentation of :mod:`softioc.builder` for details, but an overview is +See the documentation of `softioc.builder` for details, but an overview is provided here. PVs are created internally and dynamically using functionality provided by -:mod:`epicsdbbuilder`, which in this context simply provides mechanisms for -creating ``.db`` files, but :mod:`softioc.builder` also binds each created PV to +`epicsdbbuilder`, which in this context simply provides mechanisms for +creating ``.db`` files, but `softioc.builder` also binds each created PV to a special ``Python`` device -- this allows PV processing to be hooked into Python support. @@ -231,19 +131,3 @@ For all records created by these methods both reading and writing the current value of the record. For IN records calling :meth:`~softioc.device.ProcessDeviceSupportIn.set` will trigger a record update (all IN records are by default created with ``SCAN='I/O Intr'``). - - -Initialising the IOC --------------------- - -This is simply a matter of calling two functions: -:func:`~softioc.builder.LoadDatabase` and :func:`~softioc.softioc.iocInit`, -which must be called in this order. After calling -:func:`~softioc.builder.LoadDatabase` it is no longer possible to create PVs. - -It is sensible to start any server background activity after the IOC has been -initialised by calling :func:`~softioc.softioc.iocInit`. After this has been -done (:class:`cothread.Spawn` is recommended for initiating persistent background -activity) the top level script must pause, as as soon as it exits the IOC will -exit. Calling :func:`~softioc.softioc.interactive_ioc` is recommended for this -as the last statement in the top level script. diff --git a/setup.cfg b/setup.cfg index 1fa1d96b..3f234c57 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,7 +47,7 @@ extend-ignore = [tool:pytest] # Run pytest with all our checkers, and don't spam us with massive tracebacks on error -addopts = --tb=native -vv --doctest-modules --ignore=iocStats --ignore=epicscorelibs +addopts = --tb=native -vv --doctest-modules --ignore=iocStats --ignore=epicscorelibs --ignore=docs [coverage:run] # This is covered in the versiongit test suite so exclude it here diff --git a/softioc/asyncio_dispatcher.py b/softioc/asyncio_dispatcher.py index 2ee7246f..c85ceefb 100644 --- a/softioc/asyncio_dispatcher.py +++ b/softioc/asyncio_dispatcher.py @@ -9,6 +9,10 @@ class AsyncioDispatcher(threading.Thread): created. """ def __init__(self): + """Create an AsyncioDispatcher suitable to be used by + `softioc.iocInit`.""" + # Docstring specified to suppress threading.Thread's docstring, which + # would otherwise be inherited by this method and be misleading. super().__init__() #: `asyncio` event loop that the callbacks will run under. self.loop = asyncio.new_event_loop() diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index 6434f8f2..635b4e04 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -34,7 +34,10 @@ def asyncio_ioc(): async def test_asyncio_ioc(asyncio_ioc): import asyncio from aioca import caget, caput, camonitor, CANothing, _catools - # Unregister the atexit handler as it conflicts with cothread + # Unregister the aioca atexit handler as it conflicts with the one installed + # by cothread. If we don't do this we get a seg fault. This is not a problem + # in production as we won't mix aioca and cothread, but we do mix them in + # the tests so need to do this. atexit.unregister(_catools._catools_atexit) # Start