diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..239b952 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ +## Description + + + + diff --git a/.travis.yml b/.travis.yml index c82ac59..afaada3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,7 +53,7 @@ before_install: - conda config --add channels lightsource2-tag # MAKE THE CONDA RECIPE - - conda create -n $CONDA_ENV python=$TRAVIS_PYTHON_VERSION epics-base + - conda create -n $CONDA_ENV python=$TRAVIS_PYTHON_VERSION epics-base=3.14.12.6 - source activate $CONDA_ENV install: diff --git a/MANIFEST.in b/MANIFEST.in index 189d7d2..ef7b493 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include README.txt INSTALL MANIFEST.in Changelog license.txt setup.py publish.sh *.bat versioneer.py +include README.txt INSTALL MANIFEST.in Changelog LICENSE setup.py publish.sh *.bat versioneer.py exclude *.pyc core.* *~ *.pdf recursive-include epics *.py recursive-include scripts * diff --git a/debian/changelog b/debian/changelog index 0c8c8a6..dbeb6bd 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,42 @@ +pyepics (3.4.3-1) unstable; urgency=medium + + [ Daron Chabot ] + * BLD: reissue version number patch + + [ Matt Gibbs ] + * Improve/document caget_multi method, add ability for caget to accept lists and pass to caget_multi. + * Doc formatting tweak. + * Move caget_many documentation further down to indicate its relative unimportance. + * Add missing comma. + * Accept tuples of PV names in caget_many as well. + * Add a basic unit test for caget_many. + * Fix minor doc typo ('of' -> 'or'). + * Add a caput_many function, with documentation and a unit test. + * Implement waiting for each PV in a caput_many, or waiting for all PVs. + * Update docs for caput_many changes. + * Remove caget calling caget_many if handed a collection of PVs. + * Simplify caput_many by re-implementing with PV objects. + * Remove a line about auto-monitoring that didn't make any sense in caget_many and caput_many. + + [ Joshua Adelman ] + * Fix MANIFEST.in to correct license file name + + [ Daron Chabot ] + * BUG: install all the libs + + [ Matthew Newville ] + * update docs for caget_many() and discussion of getting many PV values + * more updates to doc for caget/caput/... interface + * tweaks to install doc, use sphinx release directive + + [ Christoph Schröder ] + * New upstream version 3.4.3 + * set compat level to 10 + * refresh patch queue + * update build for python3 + + -- Christoph Schröder Wed, 17 Aug 2022 19:35:05 +0200 + pyepics (3.3.0-1) unstable; urgency=low [ Daron Chabot ] diff --git a/debian/compat b/debian/compat index 7f8f011..f599e28 100644 --- a/debian/compat +++ b/debian/compat @@ -1 +1 @@ -7 +10 diff --git a/debian/control b/debian/control index 8c1fedd..cf1ac4c 100644 --- a/debian/control +++ b/debian/control @@ -2,18 +2,20 @@ Source: pyepics Maintainer: Daron Chabot Section: python Priority: optional -Build-Depends: python-setuptools (>= 0.6b3), python-all (>= 2.6.6-3), debhelper (>= 7) -XS-Python-Version: >= 2.7 -Standards-Version: 3.9.6 +Build-Depends: debhelper (>= 10), dh-python, python3-all, python3-setuptools +Build-Depends-Indep: python3-sphinx, python3-docutils, python3-numpydoc +XS-Python3-Version: >= 3.5 +Standards-Version: 3.9.8 +Homepage: http://pyepics.github.io/pyepics/ Vcs-Git: https://github.com/pyepics/pyepics.git Vcs-Browser: https://github.com/pyepics/pyepics - -Package: python-pyepics +Package: python3-pyepics Architecture: all -Depends: ${misc:Depends}, ${python:Depends}, - python-numpy, epics-dev -Description: Epics Channel Access for Python +Depends: ${misc:Depends}, ${python3:Depends}, + python3-numpy, python3-pyparsing, epics-dev +Suggests: python3-pyepics-doc +Description: Epics Channel Access for Python 3 Python Interface to the Epics Channel Access protocol of the Epics control system. PyEpics provides 3 layers of access to Channel Access (CA): @@ -31,3 +33,21 @@ Description: Epics Channel Access for Python Devices -- collections of PVs that might represent an Epics Record or physical device (say, a camera, amplifier, or power supply), and to help write GUIs for CA. + . + This package contains pyepics for Python 3. + +Package: python3-pyepics-doc +Section: doc +Architecture: all +Depends: ${misc:Depends}, ${sphinxdoc:Depends} +Description: PyEpics documentation + PyEpics is an interface for the Channel Access (CA) library of the Epics + Control System to the Python Programming language. The pyepics package + provides a base epics module to python, with methods for reading from and + writing to Epics Process Variables (PVs) via the CA protocol. + The package includes a thin and fairly complete layer over the low-level + Channel Access library in the ca module, and higher level abstractions built + on top of this basic functionality. + . + This package contains documentation for pyepics. + diff --git a/debian/patches/0001-MNT-hardcode-version.patch b/debian/patches/0001-MNT-hardcode-version.patch index 9d6185a..a9142c9 100644 --- a/debian/patches/0001-MNT-hardcode-version.patch +++ b/debian/patches/0001-MNT-hardcode-version.patch @@ -8,23 +8,19 @@ Subject: [PATCH] MNT: hardcode version setup.py | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) -diff --git a/epics/__init__.py b/epics/__init__.py -index c6fc313..a2b65d9 100644 --- a/epics/__init__.py +++ b/epics/__init__.py @@ -1,6 +1,4 @@ -from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions -+__version__ = '3.3.0' ++__version__ = '3.4.3' __doc__ = """ epics channel access python module -diff --git a/setup.py b/setup.py -index cbd4ae0..00adb3e 100644 --- a/setup.py +++ b/setup.py -@@ -7,7 +7,7 @@ import sys +@@ -7,7 +7,7 @@ import epics import shutil @@ -33,7 +29,7 @@ index cbd4ae0..00adb3e 100644 long_desc = '''Python Interface to the Epics Channel Access protocol of the Epics control system. PyEpics provides 3 layers of access to -@@ -57,8 +57,7 @@ if PY_MAJOR == 2 and PY_MINOR < 6: +@@ -58,8 +58,7 @@ pjoin('epics', 'utils3.py')) setup(name = 'pyepics', @@ -43,6 +39,3 @@ index cbd4ae0..00adb3e 100644 author = 'Matthew Newville', author_email = 'newville@cars.uchicago.edu', url = 'http://pyepics.github.io/pyepics/', --- -2.1.4 - diff --git a/debian/python-pyepics.lintian-overrides b/debian/python-pyepics.lintian-overrides deleted file mode 100644 index 6bdd281..0000000 --- a/debian/python-pyepics.lintian-overrides +++ /dev/null @@ -1 +0,0 @@ -python-pyepics: unusual-interpreter diff --git a/debian/python3-pyepics-doc.doc-base b/debian/python3-pyepics-doc.doc-base new file mode 100644 index 0000000..5e18393 --- /dev/null +++ b/debian/python3-pyepics-doc.doc-base @@ -0,0 +1,15 @@ +Document: python3-pyepics-doc +Title: pyepics Manual +Author: Matt Newville +Abstract: PyEpics is an interface for the Channel Access (CA) library of the + Epics Control System to the Python Programming language. The pyepics package + provides a base epics module to python, with methods for reading from and + writing to Epics Process Variables (PVs) via the CA protocol. + The package includes a thin and fairly complete layer over the low-level + Channel Access library in the ca module, and higher level abstractions built + on top of this basic functionality. +Section: Programming/Python + +Format: HTML +Index: /usr/share/doc/python3-pyepics-doc/html/index.html +Files: /usr/share/doc/python3-pyepics-doc/html/* diff --git a/debian/python3-pyepics-doc.docs b/debian/python3-pyepics-doc.docs new file mode 100644 index 0000000..6f7511e --- /dev/null +++ b/debian/python3-pyepics-doc.docs @@ -0,0 +1 @@ +build/sphinx/html diff --git a/debian/rules b/debian/rules index d2786c1..b6997a5 100755 --- a/debian/rules +++ b/debian/rules @@ -1,13 +1,23 @@ #!/usr/bin/make -f -# This file was automatically generated by stdeb 0.8.5 at -# Wed, 17 May 2017 17:11:54 -0400 - +#export DH_VERBOSE = 1 +#export PYBUILD_VERBOSE = 1 export NOLIBCA=1 +export PYBUILD_NAME=pyepics +export PYBUILD_DISABLE=test %: - dh $@ --with python2 --buildsystem=python_distutils + dh $@ --with python3,sphinxdoc --buildsystem=pybuild + +override_dh_auto_build: export http_proxy=127.0.0.1:9 +override_dh_auto_build: export https_proxy=127.0.0.1:9 +override_dh_auto_build: + dh_auto_build + PYTHONPATH=. python3 -m sphinx -N -bhtml doc/ build/sphinx/html # HTML generator override_dh_auto_clean: dh_auto_clean rm -rf pyepics.egg-info + +override_dh_python3: + dh_python3 --shebang=/usr/bin/python3 diff --git a/doc/advanced.rst b/doc/advanced.rst index c5a251c..90252e8 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -13,13 +13,13 @@ pyepics module. The wait and timeout options for get(), ca.get_complete() ============================================================== -The *get* functions, :func:`epics.caget`, :func:`pv.get` and :func:`ca.get` +The *get* functions, :func:`epics.caget`, :func:`pv.get` and :func:`epics.ca.get` all ask for data to be transferred over the network. For large data arrays or slow networks, this can can take a noticeable amount of time. For PVs that have been disconnected, the *get* call will fail to return a value at all. For this reason, these functions all take a `timeout` keyword option. -The lowest level :func:`ca.get` also has a `wait` option, and a companion -function :func:`ca.get_complete`. This section describes the details of +The lowest level :func:`epics.ca.get` also has a `wait` option, and a companion +function :func:`epics.ca.get_complete`. This section describes the details of these. If you're using :func:`epics.caget` or :func:`pv.get` you can supply a @@ -39,32 +39,32 @@ a PV for a large waveform record reports that it is connected, but a At the lowest level (which :func:`pv.get` and :func:`epics.caget` use), -:func:`ca.get` issues a get-request with an internal callback function. +:func:`epics.ca.get` issues a get-request with an internal callback function. That is, it calls the CA library function :func:`libca.ca_array_get_callback` with a pre-defined callback function. -With `wait=True` (the default), :func:`ca.get` then waits up to the timeout +With `wait=True` (the default), :func:`epics.ca.get` then waits up to the timeout or until the CA library calls the specified callback function. If the callback has been called, the value can then be converted and returned. If the callback is not called in time or if `wait=False` is used but the PV is connected, the callback will be called eventually, and simply waiting -(or using :func:`ca.pend_event` if :data:`ca.PREEMPTIVE_CALLBACK` is +(or using :func:`epics.ca.pend_event` if :data:`epics.ca.PREEMPTIVE_CALLBACK` is ``False``) may be sufficient for the data to arrive. Under this condition, -you can call :func:`ca.get_complete`, which will NOT issue a new request +you can call :func:`epics.ca.get_complete`, which will NOT issue a new request for data to be sent, but wait (for up to a timeout time) for the previous get request to complete. -:func:`ca.get_complete` will return ``None`` if the timeout is exceeded or +:func:`epics.ca.get_complete` will return ``None`` if the timeout is exceeded or if there is not an "incomplete get" that it can wait to complete. Thus, -you should use the return value from :func:`ca.get_complete` with care. +you should use the return value from :func:`epics.ca.get_complete` with care. Note that :func:`pv.get` (and so :func:`epics.caget`) will normally rely on the PV value to be filled in automatically by monitor callbacks. If monitor callbacks are disabled (as is done for large arrays and can be turned off) or if the monitor hasn't been called yet, :func:`pv.get` will -check whether it should can :func:`ca.get` or :func:`ca.get_complete`. +check whether it should can :func:`epics.ca.get` or :func:`epics.ca.get_complete`. -If not specified, the timeout for :func:`ca.get_complete` (and all other +If not specified, the timeout for :func:`epics.ca.get_complete` (and all other get functions) will be set to:: timeout = 0.5 + log10(count) @@ -90,96 +90,145 @@ to do this, say:: pv = epics.PV(name) pv_vals[name] = pv.get() -does incur some small performance penalty. As shown below, the penalty -is generally pretty small in absolute terms, but can be noticeable when -you are connecting to a large number (say, more than 100) PVs at once. +or even just:: -The cause for the penalty, and its remedy, are two-fold. First, a `PV` -object automatically use connection and event callbacks. Normally, these -are advantages, as you don't need to explicitly deal with them. But, -internally, they do pause for network responses using :meth:`ca.pend_event` -and these pauses can add up. Second, the :meth:`ca.get` also pauses for -network response, so that the returned value actually contains the latest -data right away, as discussed in the previous section. + values = [epics.caget(name) for name in pvnamelist] -The remedies are to - 1. not use connection or event callbacks. - 2. not explicitly wait for values to be returned for each :meth:`get`. -A more complicated but faster approach relies on a carefully-tuned use of -the CA library, and would be the following:: +does incur some performance penalty. To minimize the penalty, we need to +understand its cause. +Creating a `PV` object (using any of :class:`pv.PV`, or :func:`pv.get_pv`, +or :func:`epics.caget`) will automatically use connection and event +callbacks in an attempt to keep the `PV` alive and up-to-date during the +seesion. Normally, this is an advantage, as you don't need to explicitly +deal with many aspects of Channel Access. But creating a `PV` does request +some network traffic, and the `PV` will not be "fully connected" and ready +to do a :meth:`PV.get` until all the connection and event callbacks are +established. In fact, :meth:`PV.get` will not run until those connections +are all established. This takes very close to 30 milliseconds for each PV. +That is, for 1000 PVs, the above approach will take about 30 seconds. + +The simplest remedy is to allow all those connections to happen in parallel +and in the background by first creating all the PVs and then getting their +values. That would look like:: + + # improve time to get multiple PVs: Method 1 + import epics + + pvnamelist = read_list_pvs() + pvs = [epics.PV(name) for name in pvnamelist] + values = [p.get() for p in pvs] + +Though it doesn't look that different, this improves performance by a +factor of 100, so that getting 1000 PV values will take around 0.4 seconds. + +Can it be improved further? The answer is Yes, but at a price. For the +discussion here, we'll can the original version "Method 0" and the method +of creating all the PVs then getting their values "Method 1". With both of +these approaches, the script has fully connected PV objects for all PVs +named, so that subsequent use of these PVs will be very efficient. + +But this can be made even faster by turning off any connection or event +callbacks, avoiding `PV` objects altogether, and using the `epics.ca` +interface. This has been encapsulated into :func:`epics.caget_many` which +can be used as:: + + # get multiple PVs as fast as possible: Method 2 + import epics + pvnamelist = read_list_pvs() + values = epics.caget_many(pvlist) + +In tests using 1000 PVs that were all really connected, Method 2 will take +about 0.25 seconds, compared to 0.4 seconds for Method 1 and 30 seconds for +Method 0. To understand what :func:`epics.caget_many` is doing, a more +complete version of this looks like this:: + + # epics.caget_many made explicit: Method 3 from epics import ca pvnamelist = read_list_pvs() pvdata = {} + pvchids = [] + # create, don't connect or create callbacks for name in pvnamelist: chid = ca.create_channel(name, connect=False, auto_cb=False) # note 1 - pvdata[name] = (chid, None) + pvchids.append(chid) - for name, data in pvdata.items(): - ca.connect_channel(data[0]) + # connect + for chid in pvchids: + ca.connect_channel(chid) + + # request get, but do not wait for result ca.poll() - for name, data in pvdata.items(): - ca.get(data[0], wait=False) # note 2 + for chid in pvchids: + ca.get(chid, wait=False) # note 2 + # now wait for get() to complete ca.poll() - for name, data in pvdata.items(): + for chid in pvchids: val = ca.get_complete(data[0]) - pvdata[name][1] = val - - for name, data in pvdata.items(): - print name, data[1] - -The code here probably needs detailed explanation. The first thing to -notice is that this is using the `ca` level, not `PV` objects. Second -(Note 1), the `connect=False` and `auto_cb=False` options to -:meth:`ca.create_channel`. These respectively tell -:meth:`ca.create_channel` to not wait for a connection before returning, -and to not automatically assign a connection callback. Normally, these are -not what you want, as you want a connected channel and to know if the -connection state changes. But we're aiming for maximum speed here, so we -avoid these. - -We then explicitly call :meth:`ca.connect_channel` for all the channels. -Next (Note 2), we tell the CA library to request the data for the channel -without waiting around to receive it. The main point of not having -:meth:`ca.get` wait for the data for each channel as we go is that each -data transfer takes time. Instead we request data to be sent in a separate -thread for all channels without waiting. Then we do wait by calling -:meth:`ca.poll` once and only once, (not len(channels) times!). Finally, -we use the :meth:`ca.get_complete` method to convert the data that has now -been received by the companion thread to a python value. - -How much faster is the more explicit method? In my tests, I used 20,000 -PVs, all scalar values, all actually connected, and all on the same subnet -as the test client, though on a mixture of several vxWorks and linux IOCs. -I found that the simplest, obvious approach as above took around 12 seconds -to read all 20,000 PVs. Using the `ca` layer with connection callbacks and -a normal call to :meth:`ca.get` also took about 12 seconds. The method -without connection callbacks and with delayed unpacking above took about 2 -seconds to read all 20,000 PVs. - -Is that performance boost from 12 to 2 seconds significant? If you're -writing a script that is intended to run once, fetch a large number of PVs -and get their values (say, an auto-save script that runs on demand), then -the boost is definitely significant. On the other hand, if you're writing -a long running process or a process that will retain the PV connections and -get their values multiple times, the difference in start-up speed is less -significant. For a long running auto-save script that periodically writes -out all the PV values, the "obvious" way using automatically monitored PVs -may be much *better*, as the time for the initial connection is small, and -the use of event callbacks will reduce network traffic for PVs that don't -change between writes. - -Note that the tests also show that, with the simplest approach, 1,000 PVs -should connect and receive values in under 1 second. Any application that -is sure it needs to connect to PVs faster than that rate will want to do -careful timing tests. Finally, note also that the issues are not really a -classic *python is slow compared to C* issue, but rather a matter of how -much pausing with :meth:`ca.poll` one does to make sure values are -immediately useful. + pvdata[ca.name(chid)] = val + +The code here probably needs detailed explanation. As mentioned above, it +uses the `ca` level, not `PV` objects. Second, the call to +:meth:`epics.ca.create_channel` (Note 1) uses `connect=False` and `auto_cb=False` +which mean to not wait for a connection before returning, and to not +automatically assign a connection callback. Normally, these are not what +you want, as you want a connected channel and to be informed if the +connection state changes, but we're aiming for maximum speed here. We then +use :meth:`epics.ca.connect_channel` to connect all the channels. Next (Note 2), +we tell the CA library to request the data for the channel without waiting +around to receive it. The main point of not having :meth:`epics.ca.get` wait for +the data for each channel as we go is that each data transfer takes time. +Instead we request data to be sent in a separate thread for all channels +without waiting. Then we do wait by calling :meth:`epics.ca.poll` once and only +once, (not `len(pvnamelist)` times!). Finally, we use the +:meth:`epics.ca.get_complete` method to convert the data that has now been +received by the companion thread to a python value. + +Method 2 and 3 have essentially the same runtime, which is somewhat faster +than Method 1, and much faster than Method 0. Which method you should use +depends on use case. In fact, the test shown here only gets the PV values +once. If you're writing a script to get 1000 PVs, write them to disk, and +exit, then Method 2 (:func:`epics.caget_many`) may be exactly what you +want. But if your script will get 1000 PVs and stay alive doing other +work, or even if it runs a loop to get 1000 PVs and write them to disk once +a minute, then Method 1 will actually be faster. That is doing +:func:`epics.caget_many` in a loop, as with:: + + # caget_many() 10 times + import epics + import time + pvnamelist = read_list_pvs() + for i in range(10): + values = epics.caget_many(pvlist) + time.sleep(0.01) + +will take around considerably *longer* than creating the PVs once and +getting their values in a loop with:: + + # pv.get() 10 times + import epics + import time + pvnamelist = read_list_pvs() + pvs = [epics.PV(name) for name in pvnamelist] + for i in range(10): + values = [p.get() for p in pvs] + time.sleep(0.01) + +In tests with 1000 PVs, looping with :func:`epics.caget_many` took about +1.5 seconds, while the version looping over :meth:`PV.get()` took about 0.5 +seconds. + +To be clear, it is **connecting** to Epics PVs that is expensive, not the +retreiving of data from connected PVs. You can lower the connection +expense by not retaining the connection or creating monitors on the PVs, +but if you are going to re-use the PVs, that savings will be lost quickly. +In short, use Method 1 over :func:`epics.caget_many` unless you've benchmarked +your use-case and have demonstrated that :func:`epics.caget_many` is better for +your needs. .. _advanced-sleep-label: @@ -188,9 +237,9 @@ time.sleep() or epics.poll()? In order for a program to communicate with Epics devices, it needs to allow some time for this communication to happen. With -:data:`ca.PREEMPTIVE_CALLBACK` set to ``True``, this communication will +:data:`epics.ca.PREEMPTIVE_CALLBACK` set to ``True``, this communication will be handled in a thread separate from the main Python thread. This means -that CA events can happen at any time, and :meth:`ca.pend_event` does not +that CA events can happen at any time, and :meth:`epics.ca.pend_event` does not need to be called to explicitly allow for event processing. Still, some time must be released from the main Python thread on occasion @@ -203,8 +252,8 @@ in order for events to be processed. The simplest way to do this is with Unfortunately, the :meth:`time.sleep` method is not a very high-resolution clock, with typical resolutions of 1 to 10 ms, depending on the system. Thus, even though events will be asynchronously generated and epics with -pre-emptive callbacks does not *require* :meth:`ca.pend_event` or -:meth:`ca.poll` to be run, better performance may be achieved with an event +pre-emptive callbacks does not *require* :meth:`epics.ca.pend_event` or +:meth:`epics.ca.poll` to be run, better performance may be achieved with an event loop of:: >>> while True: @@ -234,7 +283,7 @@ value, but if :data:`epics.ca.PREEMPTIVE_CALLBACK` has been set to ``False``, threading will not work. Second, if you are using :class:`PV` objects and not making heavy use of -the :mod:`ca` module (that is, not making and passing around chids), then +the :mod:`epics.ca` module (that is, not making and passing around chids), then the complications below are mostly hidden from you. If you're writing threaded code, it's probably a good idea to read this just to understand what the issues are. @@ -246,12 +295,12 @@ The Channel Access library uses a concept of *contexts* for its own thread model, with contexts holding sets of threads as well as Channels and Process Variables. For non-threaded work, a process will use a single context that is initialized prior doing any real CA work (done in -:meth:`ca.initialize_libca`). In a threaded application, each new thread +:meth:`epics.ca.initialize_libca`). In a threaded application, each new thread begins with a new, uninitialized context that must be initialized or replaced. Thus each new python thread that will interact with CA must -either explicitly create its own context with :meth:`ca.create_context` +either explicitly create its own context with :meth:`epics.ca.create_context` (and then, being a good citizen, destroy this context as the thread ends -with :meth:`ca.destroy_context`) or attach to an existing context. +with :meth:`epics.ca.destroy_context`) or attach to an existing context. The generally recommended approach is to use a single CA context throughout an entire process and have each thread attach to the first context created @@ -283,7 +332,7 @@ you are writing a threaded application in which the first real CA calls are inside a child thread, it is recommended that you initialize CA in the main thread, -As a convenience, the :class:`CAThread` in the :mod:`ca` module is +As a convenience, the :class:`CAThread` in the :mod:`epics.ca` module is is a very thin wrapper around the standard :class:`threading.Thread` which adding a call of :meth:`epics.ca.use_initial_context` just before your threaded function is run. This allows your target functions to not diff --git a/doc/arrays.rst b/doc/arrays.rst index 3e756a1..a8736e8 100644 --- a/doc/arrays.rst +++ b/doc/arrays.rst @@ -15,7 +15,7 @@ Arrays without Numpy ~~~~~~~~~~~~~~~~~~~~~~~~ If you have numpy installed, and use the default *as_numpy=True* in -:meth:`ca.get`, :meth:`pv.get` or :meth:`epics.caget`, you will get a +:meth:`epics.ca.get`, :meth:`pv.get` or :meth:`epics.caget`, you will get a numpy array for the value of a waveform PV. If you do *not* have numpy installed, or explicitly use *as_numpy=False* in a get request, you will get the raw C-like array reference from the Python @@ -38,7 +38,7 @@ Variable Length Arrays: NORD and NELM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ While the maximum length of an array is fixed, the length of data you get -back from a monitor, :meth:`ca.get`, :meth:`pv.get`, or :meth:`epics.caget` +back from a monitor, :meth:`epics.ca.get`, :meth:`pv.get`, or :meth:`epics.caget` may be shorter than the maximum length, reflecting the most recent data put to that PV. That is, if some process puts a smaller array to a PV than its maximum length, monitors on that PV may receive only the changed data. @@ -56,9 +56,8 @@ For example:: To be clear, the :meth:`pv.put` above could be done in a separate process -- the :meth:`pv.get` is not using a value cached from the :meth:`pv.put`. -This feature seems to depend on the record definition, and requires version -3.14.12.1 of Epics base or higher, and can be checked by comparing -:meth:`ca.version` with the string '4.13'. +This feature was introduced in Epics CA 3.14.12.1, and may not work for +data from IOCs running extremely old versions of Epics base. Character Arrays ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -66,8 +65,8 @@ Character Arrays As noted in other sections, character waveforms can be used to hold strings longer than 40 characters, which is otherwise a fundamental limit for native Epics strings. Character waveforms shorter than -:data:`ca.AUTOMONITOR_MAXLENGTH` can be turned into strings with an -optional *as_string=True* to :meth:`ca.get`, :meth:`pv.get` , or +:data:`epics.ca.AUTOMONITOR_MAXLENGTH` can be turned into strings with an +optional *as_string=True* to :meth:`epics.ca.get`, :meth:`pv.get` , or :meth:`epics.caget`. If you've defined a Epics waveform record as:: @@ -94,7 +93,7 @@ Then you can use this record with: >>> print char_val 'T:\\xas_user\\March2010\\FastMap' -This example uses :meth:`pv.get` but :meth:`ca.get` is essentially +This example uses :meth:`pv.get` but :meth:`epics.ca.get` is essentially equivalent, as its *as_string* parameter works exactly the same way. Note that Epics character waveforms as defined as above are really arrays @@ -145,13 +144,13 @@ assured that the latest value is always available. As arrays get larger automatic monitoring is desirable. The Python :mod:`epics.ca` module defines a variable -:data:`ca.AUTOMONITOR_MAXLENGTH` which controls whether array PVs are +:data:`epics.ca.AUTOMONITOR_MAXLENGTH` which controls whether array PVs are automatically monitored. The default value for this variable is 65536, but can be changed at runtime. Arrays with fewer elements than -:data:`ca.AUTOMONITOR_MAXLENGTH` will be automatically monitored, unless -explicitly set, and arrays larger than :data:`AUTOMONITOR_MAXLENGTH` will -not be automatically monitored unless explicitly set. Auto-monitoring of -PVs can be be explicitly set with +:data:`epics.ca.AUTOMONITOR_MAXLENGTH` will be automatically monitored, +unless explicitly set, and arrays larger than +:data:`epics.ca.AUTOMONITOR_MAXLENGTH` will not be automatically monitored +unless explicitly set. Auto-monitoring of PVs can be be explicitly set with >>> pv2 = epics.PV('ScalerPV', auto_monitor=True) >>> pv1 = epics.PV('LargeArrayPV', auto_monitor=False) @@ -162,9 +161,10 @@ Example handling Large Arrays Here is an example reading data from an `EPICS areaDetector `_, as if it -were an image from a digital camera. This uses the `Python Imaging Library -`_ for much of the image -processing: +were an image from a digital camera. This uses the common third-party +library called `Python Imaging Library` or `pillow` for much of the image +processing. This library can be installed with `pip install pillow` or +`conda install pillow`: >>> import epics diff --git a/doc/ca.rst b/doc/ca.rst index 29079c4..c2aacfb 100644 --- a/doc/ca.rst +++ b/doc/ca.rst @@ -2,33 +2,27 @@ ca: Low-level Channel Access module ================================================= -.. module:: ca - :synopsis: low-level Channel Access module. - .. module:: epics.ca :synopsis: low-level Channel Access module. -The :mod:`ca` module provides a low-level wrapping of the EPICS -Channel Access (CA) library, using ctypes. Most users of the `epics` -module will not need to be concerned with most of the details here, and -will instead use the simple functional interface (:func:`epics.caget`, -:func:`epics.caput` and so on), or use the :class:`epics.PV` class to -create and use epics PV objects. + +The :mod:`ca` module provides a low-level wrapping of the EPICS Channel Access +(CA) library, using ctypes. Most users of the `epics` module will not need to +be concerned with most of the details here, and will instead use the simple +procedural interface (:func:`epics.caget`, :func:`epics.caput` and so on), or +use the :class:`epics.PV` class to create and use epics PV objects. General description, difference with C library ================================================= -The goal of the :mod:`ca` module is to provide a fairly complete -mapping of the C interface to the CA library while also providing a -pleasant Python experience. It is expected that anyone looking -into the details of this module is somewhat familiar with Channel -Access and knows where to consult the `Channel Access Reference -Documentation -`_. -This document focuses on the differences with the C interface, -assuming a general understanding of what the functions are meant to -do. +The :mod:`ca` module provides a fairly complete mapping of the C interface to +the CA library while also providing a pleasant Python experience. It is +expected that anyone using this module is somewhat familiar with Channel +Access and knows where to consult the `Channel Access Reference Documentation +`_. Here, we focus on the +differences with the C interface, and assume a general understanding of what +the functions are meant to do. Name Mangling @@ -146,15 +140,19 @@ Using the CA module Many general-purpose CA functions that deal with general communication and threading contexts are very close to the C library: -.. autofunction:: initialize_libca +.. autofunction:: initialize_libca() -.. autofunction:: finalize_libca +.. autofunction:: finalize_libca() + +.. autofunction:: pend_io(timeout=1.0) + +.. autofunction:: pend_event(timeout=1.e-5) + +.. autofunction:: poll(evt=1.e-5[, iot=1.0]) -.. function:: context_create() -.. autofunction:: create_context +.. autofunction:: create_context() -.. function:: context_destroy() -.. autofunction:: destroy_context +.. autofunction:: destroy_context() .. autofunction:: current_context() @@ -174,11 +172,15 @@ threading contexts are very close to the C library: .. autofunction:: replace_printf_handler(fcn=None) -.. autofunction:: pend_io(timeout=1.0) +.. warning:: -.. autofunction:: pend_event(timeout=1.e-5) + `replace_printf_handler()` appears to not actually work. + + We think this is due to a real limitation of Python's `ctypes` module + not supporting the mapping of C *va_list* function arguments to Python. + If you are interested in this or have ideas of how to fix it, please + let us know. -.. autofunction:: poll(evt=1.e-5[, iot=1.0]) Creating and Connecting to Channels @@ -194,7 +196,6 @@ a good idea to treat these as object instances. .. autofunction:: connect_channel(chid, timeout=None, verbose=False) - Many other functions require a valid Channel ID, but not necessarily a connected Channel. These functions are essentially identical to the CA library versions, and include: @@ -213,7 +214,7 @@ library versions, and include: .. autofunction:: field_type(chid) - See the *ftype* column from :ref:`Table of DBR Types `. +See the *ftype* column from :ref:`Table of DBR Types `. .. autofunction:: clear_channel(chid) @@ -228,7 +229,7 @@ A few additional pythonic functions have been added: .. autofunction:: promote_type(chid, [use_time=False, [use_ctrl=False]]) - See :ref:`Table of DBR Types `. +See :ref:`Table of DBR Types `. .. data:: _cache @@ -259,34 +260,29 @@ keyword arguments can be used to specify such options. .. autofunction:: get(chid, ftype=None, count=None, as_string=False, as_numpy=True, wait=True, timeout=None) - - See :ref:`Table of DBR Types ` for a listing of values of - *ftype*, - +See :ref:`Table of DBR Types ` for a listing of values of *ftype*, - See :ref:`arrays-large-label` for a discussion of strategies - for how to best deal with very large arrays. +See :ref:`arrays-large-label` for a discussion of strategies for how to best deal with very large arrays. +See :ref:`advanced-connecting-many-label` for a discussion of when using `wait=False` can give a large performance boost. - See :ref:`advanced-connecting-many-label` for a discussion of when using - `wait=False` can give a large performance boost. - - See :ref:`advanced-get-timeouts-label` for further discussion of the - *wait* and *timeout* options and the associated :func:`get_complete` - function. +See :ref:`advanced-get-timeouts-label` for further discussion of the *wait* and *timeout* options and the associated :func:`get_complete` function. +.. autofunction:: get_with_metadata(chid, ftype=None, count=None, as_string=False, as_numpy=True, wait=True, timeout=None) .. autofunction:: get_complete(chid, ftype=None, count=None, as_string=False, as_numpy=True, timeout=None) - See :ref:`advanced-get-timeouts-label` for further discussion. +See :ref:`advanced-get-timeouts-label` for further discussion. + +.. autofunction:: get_complete_with_metadata(chid, ftype=None, count=None, as_string=False, as_numpy=True, timeout=None) .. autofunction:: put(chid, value, wait=False, timeout=30, callback=None, callback_data=None) - For more on this *put callback*, see :ref:`ca-callbacks-label` below. +See :ref:`ca-callbacks-label` for more on this *put callback*, .. autofunction:: create_subscription(chid, use_time=False, use_ctrl=False, mask=None, callback=None) - For more on writing the user-supplied callback, see :ref:`ca-callbacks-label` below. +See :ref:`ca-callbacks-label` for more on writing the user-supplied callback, .. warning:: @@ -325,17 +321,17 @@ keyword arguments can be used to specify such options. Several other functions are provided: -.. autofunction:: get_timestamp(chid) +.. autofunction:: get_timestamp(chid) -.. autofunction:: get_severity(chid) +.. autofunction:: get_severity(chid) -.. autofunction:: get_precision(chid) +.. autofunction:: get_precision(chid) .. autofunction:: get_enum_strings(chid) .. autofunction:: get_ctrlvars(chid) - See :ref:`Table of Control Attributes ` +See :ref:`Table of Control Attributes ` .. _ctrlvars_table: @@ -370,9 +366,17 @@ states. Synchronous Groups ~~~~~~~~~~~~~~~~~~~~~~~ -Synchronous Groups can be used to ensure that a set of Channel Access calls -all happen together, as if in a *transaction*. Synchronous Groups work in -PyEpics as of version 3.0.10, but more testing is probably needed. +.. warning:: + + Synchronous groups are simulated in pyepics, but are not recommended, + and probably don't really make sense for usage within pyepics and using + asynchronous i/o anyway. + +Synchronous Groups are can be used to ensure that a set of Channel Access +calls all happen together, as if in a *transaction*. Synchronous Groups +should be avoided in pyepics, and are not well tested. They probably make +little sens in the context of asynchronous I/O. The documentation here is +given for historical purposes. The idea is to first create a synchronous group, then add a series of :func:`sg_put` and :func:`sg_get` which do not happen immediately, and @@ -389,55 +393,12 @@ and :func:`sg_get` to execute. .. autofunction:: sg_get(gid, chid[, ftype=None[, as_string=False[, as_numpy=True]]]) - See further example below. - .. autofunction:: sg_put(gid, chid, value) .. autofunction:: sg_test(gid) .. autofunction:: sg_reset(gid) -An example use of a synchronous group:: - - from epics import ca - import time - - pvs = ('X1.VAL', 'X2.VAL', 'X3.VAL') - chids = [ca.create_channel(pvname) for pvname in pvs] - - for chid in chids: - ca.connect_channel(chid) - ca.put(chid, 0) - - # create synchronous group - sg = ca.sg_create() - - # get data pointers from ca.sg_get - data = [ca.sg_get(sg, chid) for chid in chids] - - print 'Now change these PVs for the next 10 seconds' - time.sleep(10.0) - - print 'will now block for i/o' - ca.sg_block(sg) - # - # CALL ca._unpack with data points and chid to extract data - for pvname, dat, chid in zip(pvs, data, chids): - val = ca._unpack(dat, chid=chid) - print "%s = %s" % (pvname, str(val)) - - ca.sg_reset(sg) - - # Now a SG Put - print 'OK, now we will put everything back to 0 synchronously' - - for chid in chids: - ca.sg_put(sg, chid, 0) - - print 'sg_put done, but not blocked / committed. Sleep for 5 seconds ' - time.sleep(5.0) - ca.sg_block(sg) - print 'done.' .. _ca-implementation-label: @@ -512,7 +473,7 @@ used heavily inside of ca.py or are available for your convenience. .. autofunction:: withInitialContext(fcn) - See :ref:`advanced-threads-label` for further discussion. +See :ref:`advanced-threads-label` for further discussion. Unpacking Data from Callbacks @@ -563,7 +524,7 @@ pairs that will include: * `value`: the latest value * `count`: the number of data elements * `ftype`: the numerical CA type indicating the data type - * `status`: the status of the PV (1 for OK) + * `status`: the status of the PV (0 for OK) * `chid`: the integer address for the channel ID. For access rights event callbacks, your function will be passed: @@ -767,7 +728,7 @@ called immediately after successful installation:: import epics import time - + def on_access_rights_change(read_access, write_access): print 'read access = %s, write access = %s' % (read_access, write_access) diff --git a/doc/index.rst b/doc/index.rst index f518605..20472dc 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -5,31 +5,29 @@ Epics Channel Access for Python PyEpics is an interface for the Channel Access (CA) library of the `Epics Control System `_ to the Python Programming -language. The pyepics package provides a base :mod:`epics` module to -python, with methods for reading from and writing to Epics Process -Variables (PVs) via the CA protocol. The package includes a fairly -complete, thin layer over the low-level Channel Access library in the -:mod:`ca` module, and higher-level abstractions built on top of this basic -functionality. - -The package includes a simple, functional approach to CA similar to EZCA -and the Unix command-line tools with functions in the main :mod:`epics` -package including :meth:`epics.caget`, :meth:`epics.caput`, -:meth:`epics.cainfo`, and :meth:`epics.camonitor`. There is also a -:class:`pv.PV` object which represents an Epics Process Variable as an -easy-to-use Python object. Additional modules provide even higher-level -programming support to Epics. These include groups of related PVs in -:class:`device.Device`, a simple method to create alarms in -:class:`alarm.Alarm`, and support for saving PVs values in the -:mod:`autosave` module. Finally, there is support for conveniently -tying epics PVs to wxPython widgets in the :mod:`wx` module. +language. The pyepics package provides a base :mod:`epics` module to python, +with methods for reading from and writing to Epics Process Variables (PVs) via +the CA protocol. The package includes a thin and fairly complete layer over +the low-level Channel Access library in the :mod:`ca` module, and higher level +abstractions built on top of this basic functionality. + +The package includes a very simple interface to CA similar to the Unix +command-line tools and EZCA library with functions :meth:`epics.caget`, +:meth:`epics.caput`, :meth:`epics.cainfo`, and :meth:`epics.camonitor`. +For an object-oriented interface, there is also a :class:`pv.PV` class +which represents an Epics Process Variable as a full-featured and +easy-to-use Python object. Additional modules provide higher-level +programming support to CA, including grouping related PVs into a +:class:`device.Device`, creating alarms in :class:`alarm.Alarm`, and saving +PVs values in the :mod:`autosave` module. There is also support for +conveniently using epics PVs to wxPython widgets in the :mod:`wx` module, +and some support for using PyQt widgets in the :mod:`qt` module. ----------- In addition to the Pyepics library described here, several applications built with pyepics are available at `http://github.com/pyepics/epicsapps/ -`_. -See +`_. See `http://pyepics.github.com/epicsapps/ `_ for further details. @@ -48,4 +46,3 @@ See autosave wx advanced - diff --git a/doc/installation.rst b/doc/installation.rst index 5726f03..9a8c342 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -6,28 +6,30 @@ Downloading and Installation Prerequisites ~~~~~~~~~~~~~~~ -PyEpics works with Python version 2.7, 3.5, or 3.6. It is supported and -regularly used and tested on 64-bit Linux, 64-bit Mac OSX, 64-bit Windows, -and 32-bit Windows. It should still work on 32-bit Linux, and may work with -older versions of Python, but these are rarely tested. For Windows, use of -pyepics with IronPython (Python written with .NET) has been recently -reported, but is not routinely tested. - -Version 3.14 or higher of the EPICS Channel Access library is required for -pyepics to actually communicate with Epics variables. Specifically, the -shared libraries libca and libCom (*libca.so* and *libCom.so* on Linux, -*libca.dylib* and *libCom.dylib* on Mac OSX, or *ca.dll* and *Com.dll* on -Windows) from *Epics Base* are required to use this module. Some features, -including 'subarray records' will only work with version 3.14.12 and -higher, and version 3.15 or higher is recommended. - -For all supported operating systems, pre-built and recent versions of libca -and libCom are provided, and will be installed within the python packages -directory and used by default. Though they will be found by default by -pyepics, these libraries will be hard for other applications to find, and -so should not cause conflicts with other CA client programs. We regularly -test with these libraries and recommend using them. If you want to not use -them or even install them, instructions for how to do this are given below. +PyEpics works with Python version 2.7, 3.5, 3.6, and 3.7. It is supported +and regularly used and tested on 64-bit Linux, 64-bit Mac OSX, and 64-bit +Windows. It is known to work on Linux with ARM processors including +raspberry Pi, though this is not part of the automated testing set. +Pyepics may still work on 32-bit Windows and Linux, but these systems are +not tested regularly. It may also work with older versions of Python (such +as 3.4), but these are no longer tested or supported. For Windows, pyepics +has been reported to work with IronPython (that is, Python written in the +.NET framework), but this is not routinely tested. + +The EPICS Channel Access library Version 3.14.12 or higher is required for +pyepics and 3.15 or higher are strongly recommended. More specifically, +pyepics requires e shared libraries libca and libCom (*libca.so* and +*libCom.so* on Linux, *libca.dylib* and *libCom.dylib* on Mac OSX, or +*ca.dll* and *Com.dll* on Windows) from *Epics Base*. + +For all supported operating systems and some less-well-tested systems (all +of linux-64, linux-32,linux-arm, windows-64, windows-32, and darwin-64), +pre-built versions of *libca* (and *libCom*) built with 3.16.2 are +provided, and will be installed within the python packages directory and +used by default. This means that you do not need to install Epics base +libraries or any other packages to use pyepics. For Epics experts who may +want to use their own versions the *libca* from Epics base, instructions +for how to do this are given below. The Python `numpy module `_ is highly recommended, though it is not required. If available, it will be used @@ -48,14 +50,13 @@ Downloads and Installation .. _pyepics CARS downloads: http://cars9.uchicago.edu/software/python/pyepics3/src/ -The latest stable version of the pyepics package is 3.3.0. Source code -kits and Windows installers can be found at `pyepics PyPI`_. With `Python -Setup Tools`_ now standard for Python 2.7 and above, the simplest way to -install the pyepics is with:: +The latest stable version of the pyepics package is |release|. Source code +kits and Windows installers can be found at `pyepics PyPI`_, and can be +installed with:: pip install pyepics -If you're using Anaconda, there are a few conda channels for pyepics, +If you're using Anaconda Python, there are a few conda channels for pyepics, including:: conda install -c GSECARS pyepics @@ -64,40 +65,32 @@ You can also download the source package, unpack it, and install with:: python setup.py install -If you know that you will not want to use the default version of *libca*, -you can suppress the installation of the default versions by setting the -environmental variable `NOLIBCA` at install time, as with:: - - NOLIBCA=1 python setup.py install - -or:: - - NOLIBCA=1 pip install pyepics - -Note that this should be considered an expert-level option. - Getting Started, Setting up the Epics Environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As mentioned above, pyepics must be able to find and load the Channel Access dynamic library (*libca.so*, *libca.dylib*, or *ca.dll* depending on -the system) at runtime in order to actually work. By default, the provided -versions of these libraries will be installed and used. +the system) at runtime in order to actually work. For the most commonly +used operating systems and architectures, modern version of these libraries +are provided, and will be installed and used with pyepics. We strongly +recommend using these. -If you wish to use a different version of *libca*, there are a few ways to -specify how that will be found. First, you can set the environmental -variable ``PYEPICS_LIBCA`` to the full path of the dynamic library, for -example:: +If these provided versions of *libca* do not work for you, please let us know. +If you need to or wish to use a different version of *libca*, you can set the +environmental variable ``PYEPICS_LIBCA`` to the full path of the dynamic +library to use as *libca*, for example:: > export PYEPICS_LIBCA=/usr/local/epics/base-3.15.5/lib/linux-x86_64/libca.so -For experts who want to never use the default version, installation of -*libca* (and *libCom*) can be turned off by setting the environmental -variable `NOLIBCA` at install time, as shown above. If you do this, you -will want to make sure that *libca.so* can be found in your `PATH` -environmental variable, or in `LD_LIBRARY_PATH` or `DYLD_LIBRARY_PATH` on -Mac OSX. +Note that *libca* will need to find another Epics CA library *libCom*. This +is almost always in the same folder as *libca*, but you may need to make sure +that the *libca* you are pointing to can find the required *libCom*. The +methods for telling shared libraries (or executable files) how to find other +shared libraries varies with system, but you may need to set other +environmental variables such as ``LD_LIBRARY_PATH`` or ``DYLIB_LIBRARY_PATH`` +or use `ldconfig`. If you're having trouble with any of these things, +ask your local Epics gurus or contact the authors. To find out which CA library will be used by pyepics, use: >>> import epics @@ -109,9 +102,9 @@ With the Epics library loaded, you will need to be able to connect to Epics Process Variables. Generally, these variables are provided by Epics I/O controllers (IOCs) that are processes running on some device on the network. If you are connecting to PVs provided by IOCs on your local -subnet, you should have no trouble. If trying to reach further network, -you may need to set the environmental variable ``EPICS_CA_ADDR_LIST`` to -specify which networks to search for PVs. +subnet, you should have no trouble. If trying to reach IOCs outside of +your immediate subnet, you may need to set the environmental variable +``EPICS_CA_ADDR_LIST`` to specify which networks to search for PVs. Testing @@ -130,14 +123,11 @@ Development Version ~~~~~~~~~~~~~~~~~~~~~~~~ Development of pyepics is done through the `pyepics github -repository`_. To get a read-only copy of the latest version, use one -of:: +repository`_. To get a copy of the latest version do:: - git clone http://github.com/pyepics/pyepics.git git clone git@github.com/pyepics/pyepics.git - Getting Help ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -182,10 +172,10 @@ pyepics was originally written and is maintained by Matt Newville . Many important contributions to the library have come from Angus Gratton while at the Australian National University, and from Daron Chabot and Ken Lauer. Several other people have provided -valuable additions, suggestions, or bug reports, which has greatly improved -the quality of the library: Robbie Clarken, Daniel Allen, Michael Abbott, -Thomas Caswell, Alain Peteut, Steven Hartmann, Rokvintar, Georg Brandl, -Niklas Claesson, Jon Brinkmann, Marco Cammarata, Craig Haskins, David Vine, -Pete Jemian, Andrew Johnson, Janko Kolar, Irina Kosheleva, Tim Mooney, Eric -Norum, Mark Rivers, Friedrich Schotte, Mark Vigder, Steve Wasserman, and -Glen Wright. +valuable additions, suggestions, pull requests or bug reports, which has +greatly improved the quality of the library: Robbie Clarken, Daniel Allen, +Michael Abbott, Thomas Caswell, Alain Peteut, Steven Hartmann, Rokvintar, +Georg Brandl, Niklas Claesson, Jon Brinkmann, Marco Cammarata, Craig +Haskins, David Vine, Pete Jemian, Andrew Johnson, Janko Kolar, Irina +Kosheleva, Tim Mooney, Eric Norum, Mark Rivers, Friedrich Schotte, Mark +Vigder, Steve Wasserman, and Glen Wright. diff --git a/doc/overview.rst b/doc/overview.rst index 4342a85..a03bd06 100644 --- a/doc/overview.rst +++ b/doc/overview.rst @@ -3,12 +3,12 @@ PyEpics Overview ============================================ -The python :mod:`epics` package consists of several function, modules, and +The python :mod:`epics` package provides several function, modules, and classes to interact with EPICS Channel Access. The simplest approach uses -the functions :func:`caget`, :func:`caput`, :func:`cainfo`, -:func:`camonitor`, and :func:`camonitor_clear` within the top-level `epics` -module. These functions are similar to the standard command line utilities -and to the EZCA library interface, and are described in more detail below. +the functions :func:`caget`, :func:`caput`, and :func:`cainfo` within the +top-level `epics` module to get and put values of Epics Process Variables. +These functions are similar to the standard command line utilities and the +EZCA library interface, and are described in more detail below. To use the :mod:`epics` package, import it with:: @@ -16,14 +16,15 @@ To use the :mod:`epics` package, import it with:: The main components of this module include - * functions :func:`caget`, :func:`caput`, :func:`camonitor`, - :func:`camonitor_clear`, and :func:`cainfo` as described below. - * a :mod:`ca` module, providing the low-level Epics Channel Access - library as a set of functions. - * a :class:`PV` object, giving a higher-level interface to Epics - Channel Access. - * a :class:`Device` object: a collection of related PVs - * a :class:`Motor` object: a mapping of an Epics Motor + * functions :func:`caget`, :func:`caput`, :func:`cainfo` and others + described in more detail below. + * a :mod:`ca` module, providing the low-level library as a set of + functions, meant to be very close to the C library for Channel Access. + * a :class:`PV` object, representing a Process Variable (PV) and giving + a higher-level interface to Epics Channel Access. + * a :class:`Device` object: a collection of related PVs, similar to an + Epics Record. + * a :class:`Motor` object: a Device that represents an Epics Motor. * an :class:`Alarm` object, which can be used to set up notifications when a PV's values goes outside an acceptable bounds. * an :mod:`epics.wx` module that provides wxPython classes designed for @@ -34,11 +35,12 @@ Channel Access, the :func:`caget` and :func:`caput` functions are probably where you want to start. If you're building larger scripts and programs, using :class:`PV` objects -provided by the :mod:`pv` module is recommended. The :class:`PV` class -provides a Process Variable object that has both methods (including -:meth:`get` and :meth:`put`) to read and change the PV, and attributes that -are kept automatically synchronized with the remote channel. For larger -applications, you may find the :class:`Device` class helpful. +is recommended. The :class:`PV` class provides a Process Variable (PV) +object that has methods (including :meth:`get` and :meth:`put`) to read and +change the PV, and attributes that are kept automatically synchronized with +the remote channel. For larger applications where you find yourself +working with sets of related PVs, you may find the :class:`Device` class +helpful. The lowest-level CA functionality is exposed in the :mod:`ca` module, and companion :mod:`dbr` module. While not necessary recommended for most use @@ -51,9 +53,8 @@ In addition, the `epics` package contains more specialized modules for alarms, Epics motors, and several other *devices* (collections of PVs), and a set of wxPython widget classes for using EPICS PVs with wxPython. -The `epics` package is targeted for use on Unix-like systems (including -Linux and Mac OS X) and Windows with Python versions 2.5, 2.6, 2.7, and -3.1, and 3.2. +The `epics` package is supported and well-tested on Linux, Mac OS X, and +Windows with Python versions 2.7, and 3.5 and above. Quick Start @@ -64,34 +65,64 @@ You'll then be able to use Python's introspection tools and built-in help system, and the rest of this document as a reference and for detailed discussions. -Functional Approach: caget(), caput() +Procedural Approach: caget(), caput() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To get values from PVs, you can use the :func:`caget` function: - >>> from epics import caget, caput - >>> m1 = caget('XXX:m1.VAL') - >>> print m1 - 1.2001 + >>> from epics import caget, caput, cainfo + >>> m1 = caget('XXX:m1.VAL') + >>> print(m1) + 1.2001 To set PV values, you can use the :func:`caput` function: - >>> caput('XXX:m1.VAL', 1.90) - >>> print caget('XXX:m1.VAL') - 1.9000 + >>> caput('XXX:m1.VAL', 1.90) + >>> print(caget('XXX:m1.VAL')) + 1.9000 -For many cases, this approach is ideal because of its simplicity and -clarity. +To see more detailed information about a PV, use the :func:`cainfo` +function: -Object Oriented Approach: PV + >>> cainfo('XXX:m1.VAL') + == XXX:m1.VAL (time_double) == + value = 1.9 + char_value = '1.9000' + count = 1 + nelm = 1 + type = time_double + units = mm + precision = 4 + host = somehost.aps.anl.gov:5064 + access = read/write + status = 0 + severity = 0 + timestamp = 1513352940.872 (2017-12-15 09:49:00.87179) + posixseconds = 1513352940.0 + nanoseconds= 871788105 + upper_ctrl_limit = 50.0 + lower_ctrl_limit = -48.0 + upper_disp_limit = 50.0 + lower_disp_limit = -48.0 + upper_alarm_limit = 0.0 + lower_alarm_limit = 0.0 + upper_warning_limit = 0.0 + lower_warning_limit = 0.0 + PV is internally monitored, with 0 user-defined callbacks: + ============================= + +The simplicity and clarity of these functions make them ideal for many +uses. + +Creating and Using PV Objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you want to repeatedly access the same PV, you may find it more -convenient to ''create a PV object'' and use it in a more object-oriented +If you are repeatedly referencing the same PV, you may find it more +convenient to create a PV object and use it in a more object-oriented manner. - >>> from epics import PV - >>> pv1 = PV('XXX:m1.VAL') + >>> from epics import PV + >>> pv1 = PV('XXX:m1.VAL') PV objects have several methods and attributes. The most important methods are :meth:`get` and :meth:`put` to receive and send the PV's value, and @@ -99,61 +130,63 @@ the :attr:`value` attribute which stores the current value. In analogy to the :func:`caget` and :func:`caput` examples above, the value of a PV can be fetched either with - >>> print pv1.get() - 1.2001 + >>> print(pv1.get()) + 1.90 or - >>> print pv1.value - 1.2001 + >>> print(pv1.value) + 1.90 To set a PV's value, you can either use - >>> pv1.put(1.9) + >>> pv1.put(1.9) or assign the :attr:`value` attribute - >>> pv1.value = 1.9 + >>> pv1.value = 1.9 You can see a few of the most important properties of a PV by simply printing it: - >>> print pv1 - + >>> print(pv1) + -Even more complete information can be seen by printing the PVs :attr:`info` +More complete information can be seen by printing the PVs :attr:`info` attribute:: - >>> print pv1.info - == XXX:m1.VAL (native_double) == - value = 1.9 - char_value = '1.90000' - count = 1 - nelm = 1 - type = double - units = mm - precision = 5 - host = somehost.cars.aps.anl.gov:5064 - access = read/write - status = 0 - severity = 0 - timestamp = 1265996457.212 (2010-Feb-12 11:40:57.212) - upper_ctrl_limit = 12.5 - lower_ctrl_limit = -12.3 - upper_disp_limit = 12.5 - lower_disp_limit = -12.3 - upper_alarm_limit = 0.0 - lower_alarm_limit = 0.0 - upper_warning_limit = 0.0 - lower_warning_limit = 0.0 - PV is internally monitored, with 0 user-defined callbacks: - ============================= - -PV objects have several additional methods, especially related to -monitoring changes to the PVs and defining functions to be run when the -value does change. There are also attributes associated with a PVs -*Control Attributes*, like those shown above in the :attr:`info` attribute. -Further details are at :ref:`pv-label`. + >>> print(pv1.info) + == XXX:m1.VAL (time_double) == + value = 1.9 + char_value = '1.9000' + count = 1 + nelm = 1 + type = time_double + units = mm + precision = 4 + host = somehost.aps.anl.gov:5064 + access = read/write + status = 0 + severity = 0 + timestamp = 1513352940.872 (2017-12-15 09:49:00.87179) + posixseconds = 1513352940.0 + nanoseconds= 871788105 + upper_ctrl_limit = 50.0 + lower_ctrl_limit = -48.0 + upper_disp_limit = 50.0 + lower_disp_limit = -48.0 + upper_alarm_limit = 0.0 + lower_alarm_limit = 0.0 + upper_warning_limit = 0.0 + lower_warning_limit = 0.0 + PV is internally monitored, with 0 user-defined callbacks: + ============================= + +PV objects have several additional methods related to monitoring changes to +the PV values or connection state including user-defined functions to be +run when the value changes. There are also attributes associated with a +PVs *Control Attributes*, like those shown above in the :attr:`info` +attribute. Further details are at :ref:`pv-label`. Functions defined in :mod:`epics`: caget(), caput(), etc. @@ -162,17 +195,21 @@ Functions defined in :mod:`epics`: caget(), caput(), etc. .. module:: epics :synopsis: top-level epics module, and container for simplest CA functions -The simplest interface to EPICS Channel Access provides functions -:func:`caget`, :func:`caput`, as well as functions :func:`camonitor`, -:func:`camonitor_clear`, and :func:`cainfo`. These functions are similar -to the EPICS command line utilities and to the functions in the EZCA -library. They all take the name of an Epics Process Variable (PV) as the -first argument. As with the EZCA library, the python implementation keeps -an internal cache of connected PV (in this case, using `PV` objects) so -that repeated use of a PV name will not actually result in a new connection --- see :ref:`pv-cache-label` for more details. Thus, though the -functionality is limited, the performance of the functional approach can be -quite good. +As shown above, the simplest interface to EPICS Channel Access is found +with the functions :func:`caget`, :func:`caput`, and :func:`cainfo`. There +are also functions :func:`camonitor` and :func:`camonitor_clear` to setup +and clear a simple monitoring of changes to a PV. These functions all take +the name of an Epics Process Variable (PV) as the first argument and are +similar to the EPICS command line utilities of the same names. + +Internally, these functions keeps a cache of connected PV (in this case, +using `PV` objects) so that repeated use of a PV name will not actually +result in a new connection to the PV -- see :ref:`pv-cache-label` for more +details. Thus, though the functionality is simple and straightforward, the +performance of using thes simple function can be quite good. In addition, +there are also functions :func:`caget_many` and :func:`caput_many` for +getting and putting values for multiple PVs at a time. + :func:`caget` ~~~~~~~~~~~~~ @@ -181,7 +218,7 @@ quite good. retrieves and returns the value of the named PV. - :param pvname: name of Epics Process Variable + :param pvname: name of Epics Process Variable. :param as_string: whether to return string representation of the PV value. :type as_string: ``True``/``False`` :param count: number of elements to return for array data. @@ -210,10 +247,9 @@ underlying PV. The default is ``False``, so that each :func:`caget` will explicitly ask the value to be sent instead of relying on the automatic monitoring normally used for persistent PVs. This makes :func:`caget` act more like command-line tools, and slightly less efficient than creating a -PV and getting values with it. If ultimate performance is a concern, using -monitors is recommended. For more details on making :func:`caget` more -efficient, see :ref:`pv-automonitor-label` and -:ref:`advanced-get-timeouts-label`. +PV and getting values with it. If performance is a concern, using monitors +is recommended. For more details on making :func:`caget` more efficient, +see :ref:`pv-automonitor-label` and :ref:`advanced-get-timeouts-label`. The *as_string* argument tells the function to return the **string representation** of the value. The details of the string representation @@ -224,53 +260,52 @@ used to format the string value. For enum types, the name of the enum state is returned:: >>> from epics import caget, caput, cainfo - >>> print caget('XXX:m1.VAL') # A double PV + >>> print(caget('XXX:m1.VAL')) # A double PV 0.10000000000000001 - >>> print caget('XXX:m1.DESC') # A string PV + >>> print(caget('XXX:m1.DESC')) # A string PV 'Motor 1' - >>> print caget('XXX:m1.FOFF') # An Enum PV + >>> print(caget('XXX:m1.FOFF')) # An Enum PV 1 Adding the `as_string=True` argument always results in string being -returned, with the conversion method depending on the data type:: +returned, with the conversion method depending on the data type, for +example using the precision field of a double PV to determine how to format +the string, or using the names of the enumeration states for an enum PV:: - >>> print caget('XXX:m1.VAL', as_string=True) + >>> print(caget('XXX:m1.VAL', as_string=True)) '0.10000' - >>> print caget('XXX:m1.FOFF', as_string=True) + >>> print(caget('XXX:m1.FOFF', as_string=True)) 'Frozen' -For most array data from Epics waveform records, the regular value will be -a numpy array (or a python list if numpy is not installed). The string -representation will be something like '' -depending on the size and type of the waveform. An array of doubles might -be:: +For integer or double array data from Epics waveform records, the regular +value will be a numpy array (or a python list if numpy is not installed). +The string representation will be something like '' depending on the size and type of the waveform. An array of +doubles might be:: - >>> print caget('XXX:scan1.P1PA') # A Double Waveform + >>> print(caget('XXX:scan1.P1PA')) # A Double Waveform array([-0.08 , -0.078 , -0.076 , ..., 1.99599814, 1.99799919, 2. ]) - >>> print caget('XXX:scan1.P1PA', as_string=True) - '' + >>> print(caget('XXX:scan1.P1PA', as_string=True)) + '' -As an important special case, CHAR waveforms will be turned to Python +As an important special case CHAR waveform records will be turned to Python strings when *as_string* is ``True``. This is useful to work around the -low limit of the maximum length (40 characters!) of EPICS strings, and -means that it is fairly common to use CHAR waveforms when long strings are -desired:: - - >>> print caget('XXX:dir') # A CHAR waveform - array([ 84, 58, 92, 120, 97, 115, 95, 117, 115, - 101, 114, 92, 77, 97, 114, 99, 104, 50, 48, - 49, 48, 92, 70, 97, 115, 116, 77, 97, 112]) +low limit of the maximum length (40 characters!) of EPICS strings which has +inspired the fairly common usage of CHAR waveforms to represent longer +strings:: - >>> print caget('XXX:dir',as_string=True) - 'T:\\xas_user\\March2010\\Fastmap' + >>> epics.caget('MyAD:TIFF1:FilePath') + array([ 47, 104, 111, 109, 101, 47, 101, 112, 105, 99, 115, 47, 115, + 99, 114, 97, 116, 99, 104, 47, 0], dtype=uint8) + >>> epics.caget('MyAD:TIFF1:FilePath', as_string=True) + '/home/epics/scratch/' -Of course,character waveforms are not always used for long strings, but -can also hold byte array data, such as comes from some detectors and -devices. +Of course,character waveforms are not always used for long strings, but can +also hold byte array data, such as comes from some detectors and devices. :func:`caput` ~~~~~~~~~~~~~~~~ @@ -397,7 +432,7 @@ and the log file is inspected:: >>> epics.camonitor_clear('XXX:DMM1Ch2_calc.VAL') >>> fh.close() >>> fh = open('PV1.log','r') - >>> for i in fh.readlines(): print i[:-1] + >>> for i in fh.readlines(): print(i[:-1]) XXX:DMM1Ch2_calc.VAL 2010-03-24 11:56:40.536946 -183.5035 XXX:DMM1Ch2_calc.VAL 2010-03-24 11:56:41.536757 -183.6716 XXX:DMM1Ch2_calc.VAL 2010-03-24 11:56:42.535568 -183.5112 @@ -408,71 +443,108 @@ and the log file is inspected:: XXX:DMM1Ch2_calc.VAL 2010-03-24 11:56:47.536623 -183.5223 XXX:DMM1Ch2_calc.VAL 2010-03-24 11:56:48.536434 -183.6832 +:func:`caget_many` +~~~~~~~~~~~~~~~~~~ + +.. function:: caget_many(pvlist[, as_string=False[, count=None[, as_numpy=True[, timeout=None]]]]) -Motivation: Why another Python-Epics Interface? + get a list of PVs as quickly as possible. Returns a list of values for + each PV in the list. Unlike :func:`caget`, this method does not use + automatic monitoring (see :ref:`pv-automonitor-label`). + + :param pvlist: A list of process variable names. + :type pvlist: ``list`` or ``tuple`` of ``str`` + :param as_string: whether to return string representation of the PV values. + :type as_string: ``True``/``False`` + :param count: number of elements to return for array data. + :type count: integer or ``None`` + :param as_numpy: whether to return the Numerical Python representation for array data. + :type as_numpy: ``True``/``False`` + :param timeout: maximum time to wait (in seconds) for value before returning None. + :type timeout: float or ``None`` + +For detailed information about the arguments, see the documentation for +:func:`caget`. Also see :ref:`advanced-connecting-many-label` for more +discussion. + +:func:`caput_many` +~~~~~~~~~~~~~~~~~~ + +.. function:: caput_many(pvlist, values[, wait=False[, connection_timeout=None[, put_timeout=60]]]) + + put values to a list of PVs as quickly as possible. Returns a list of ints + for each PV in the list: 1 if the put was successful, -1 if it timed out. + Unlike :func:`caput`, this method does not use automatic monitoring (see + :ref:`pv-automonitor-label`). + + :param pvlist: A list of process variable names. + :type pvlist: ``list`` or ``tuple`` of ``str`` + :param values: values to put to each PV. + :type values: ``list`` or ``tuple`` + :param wait: if ``'each'``, :func:`caput_many` will wait for each + PV to process before starting the next. If ``'all'``, + :func:`caput_many` will issue puts for all PVs immediately, then + wait for all of them to complete. If any other value, + :func:`caput_many` will not wait for put processing to complete. + :param connection_timeout: maximum time to wait (in seconds) for + a connection to be established to each PV. + :type connection_timeout: float or ``None`` + :param put_timeout: maximum time to wait (in seconds) for processing + to complete for each PV (if ``wait`` is ``'each'``), or for processing + to complete for all PVs (if ``wait`` is ``'all'``). + :type put_timeout: float or ``None`` + +Because connections to channels normally connect very quickly (less than a +second), but processing a put may take a significant amount of time (due to +a physical device moving, or due to complex calculations or data processing +sequences), a separate timeout duration can be specified for connections and +processing puts. + + +Motivation and design concepts ================================================ -PyEpics version 3 is intended as an improvement over EpicsCA 2.1, and -should replace that older Epics-Python interface. That version had -performance issues, especially when connecting to a large number of PVs, is -not thread-aware, and has become difficult to maintain for Windows and -Linux. - -There are a few other Python modules exposing Epics Channel Access -available. Most of these have a interface to the CA library that was both -closer to the C library and lower-level than EpicsCA. Most of these -interfaces use specialized C-Python 'wrapper' code to provide the -interface. - -Because of this, an additional motivation for this package was to allow a -more common interface to be used that built higher-level objects (as -EpicsCA had) on top of a complete lower-level interface. The desire to -come to a more universally-acceptable Python-Epics interface has definitely -influenced the goals for this module, which include: - - 1) providing both low-level (C-like) and higher-level access (Pythonic +There are other Python wrappings for Epics Channel Access, so it it useful +to outline the design goals for PyEpics. The motivations for PyEpics3 +included: + + 1) providing both low-level (C-like) and higher-level access (Python objects) to the EPICS Channel Access protocol. 2) supporting as many features of Epics 3.14 as possible, including preemptive callbacks and thread support. 3) easy support and distribution for Windows and Unix-like systems. - 4) being ready for porting to Python3. + 4) support for both Python 2 and Python 3. 5) using Python's ctypes library. -The main implementation feature used here (and difference from EpicsCA) is -using Python's ctypes library to handle the connection between Python and -the CA C library. Using ctypes has many advantages. Principally, it fully -eliminates the need to write (and maintain) wrapper code either with SWIG -or directly with Python's C API. Since the ctypes module allows access to -C data and objects in pure Python, no compilation step is needed to build -the module, making installation and support on multiple platforms much -easier. Since ctypes loads a shared object library at runtime, the -underlying Epics Channel Access library can be upgraded without having to -re-build the Python wrapper. In addition, using ctypes provides the most -reliable thread-safety available, as each call to the underlying C library -is automatically made thread-aware without explicit code. Finally, by -avoiding the C API altogether, migration to Python3 is greatly simplified. -PyEpics3 does work with both Python 2.* and 3.*. - - -Status and To-Do List +The idea is to provide both a low-level interface to Epics Channel Access +(CA) that closely resembled the C interface to CA, and to build higher +level functionality and complex objects on top of that foundation. The +Python ctypes library conveniently allows such direct wrapping of a shared +libraries, and requires no compiled code for the bridge between Python and +the CA library. This makes it very easy to wrap essentially all of CA from +Python code, and support multiple platforms. Since ctypes loads a shared +object library at runtime, the underlying CA library can be upgraded +without having to re-build the Python wrapper. The ctypes interface +provides the most reliable thread-safety available, as each call to the +underlying C library is automatically made thread-aware without explicit +code. Finally, by avoiding the C API altogether, supporting both Python2 +and Python3 is greatly simplified. + +Status and to-do list ======================= -The PyEpics package is actively maintained, but the core library seems +The PyEpics package is actively maintained, but the core library is reasonably stable and ready to use in production code. Features are being -added slowly, and testing is integrated into development so that the -chance of introducing bugs into existing codes is minimized. The package -is targeted and tested to work with Python 2.6, 2.7, 3.2 and 3.3 -simultaneously (that is, the same code is meant to support all these -versions). +added slowly, and testing is integrated into development so that the chance +of introducing bugs into existing codes is minimized. The package is +targeted and tested to work with Python 2.7 and Python 3 simultaneously. -There are several desired features are left unfinished: +There are several desired features are left unfinished or could use +improvement: * add more Epics Devices, including low-level epics records and more suport for Area Detectors. - * incorporate some or all of the Channel Access Server from `pcaspy - `_ - * build and improve applications using PyEpics, especially for common data acquisition needs. diff --git a/doc/pv.rst b/doc/pv.rst index 73640ca..2ae9ca5 100644 --- a/doc/pv.rst +++ b/doc/pv.rst @@ -85,7 +85,7 @@ callbacks to be executed when the PV changes. .. _pv-get-label: -.. method:: get([, count=None[, as_string=False[, as_numpy=True[, timeout=None[, use_monitor=True]]]]]) +.. method:: get([, count=None[, as_string=False[, as_numpy=True[, timeout=None[, use_monitor=True, [with_ctrlvars=False]]]]]]) get and return the current value of the PV @@ -120,11 +120,47 @@ callbacks to be executed when the PV changes. automatically monitored. Otherwise, the most recently received value will be sent immediately. + The *with_ctrlvars* option requests DBR_CTRL data, including control limits, + precision, and so on, in addition to the value normally returned. This metadata + will be available by accessing various attributes such as + ``lower_ctrl_limit``. + See :ref:`pv-automonitor-label` for more on monitoring PVs and :ref:`advanced-get-timeouts-label` for more details on what happens when a :func:`pv.get` times out. +.. method:: get_with_metadata([, form=None, [count=None[, as_string=False[, as_numpy=True[, timeout=None[, use_monitor=True, [with_ctrlvars=False]]]]]]]) + + Returns a dictionary of the current value and associated metadata + + :param form: EPICS *data type* to request: the 'native', or the 'ctrl' (Control) or 'time' variant. Defaults to the PV instance attribute ``form``. + :type form: {'native', 'time', 'ctrl', None} + :param count: maximum number of array elements to return + :type count: integer or ``None`` + :param as_string: whether to return the string representation of the value. + :type as_string: ``True``/``False`` + :param as_numpy: whether to try to return a numpy array where appropriate. + :type as_string: ``True``/``False`` + :param timeout: maximum time to wait for data before returning ``None``. + :type timeout: float or ``None`` + :param use_monitor: whether to rely on monitor callbacks or explicitly get value now. + :type use_monitor: ``True``/``False`` + + See ``PV.get``, above, for further notes on each of these parameters. + + Each request to EPICS can optionally contain additional metadata associated + with the value. While ``PV.get`` updates the PV instance with any metadata, + ``get_with_metadata`` will return the requested metadata and value in a + dictionary. + + The exception is when the PV is set to auto-monitor and the `use_monitor` + parameter here is set. This means that both the value and metadata will + used the cached values instead of making a new request. Because of this, + the metadata and value returned here will be a full dictionary of all known + metadata for the PV instance. + + .. method:: put(value[, wait=False[, timeout=30.0[, use_complete=False[, callback=None[, callback_data=None]]]]]) set the PV value, optionally waiting to return until processing has @@ -161,11 +197,11 @@ completed. See :ref:`pv-putwait-label` for more details. .. method:: poll([evt=1.e-4, [iot=1.0]]) - poll for changes. This simply calls :meth:`ca.poll` + poll for changes. This simply calls :meth:`epics.ca.poll` - :param evt: time to pass to :meth:`ca.pend_event` + :param evt: time to pass to :meth:`epics.ca.pend_event` :type evt: float - :param iot: time to pass to :meth:`ca.pend_io` + :param iot: time to pass to :meth:`epics.ca.pend_io` :type iot: float .. method:: connect([timeout=None]) @@ -174,12 +210,12 @@ completed. See :ref:`pv-putwait-label` for more details. successfully connected. It is probably not that useful, as connection should happen automatically. See :meth:`wait_for_connection`. - :param timeout: maximum connection time, passed to :meth:`ca.connect_channel` + :param timeout: maximum connection time, passed to :meth:`epics.ca.connect_channel` :type timeout: float :rtype: ``True``/``False`` if timeout is ``None``, the PVs connection_timeout parameter will be used. If that is also ``None``, - :data:`ca.DEFAULT_CONNECTION_TIMEOUT` will be used. + :data:`episc.ca.DEFAULT_CONNECTION_TIMEOUT` will be used. .. method:: wait_for_connection([timeout=None]) @@ -191,7 +227,7 @@ completed. See :ref:`pv-putwait-label` for more details. :rtype: ``True``/``False`` if timeout is ``None``, the PVs connection_timeout parameter will be used. If that is also ``None``, - :data:`ca.DEFAULT_CONNECTION_TIMEOUT` will be used. + :data:`epics.ca.DEFAULT_CONNECTION_TIMEOUT` will be used. .. method:: disconnect() @@ -205,7 +241,7 @@ completed. See :ref:`pv-putwait-label` for more details. turn off automatic monitoring of a PV. Note that this will suspend all event callbacks on a PV at the CA level by calling - :func:`ca.clear_subscription`, but will not clear the list of PVs + :func:`epics.ca.clear_subscription`, but will not clear the list of PVs callbacks. This means that doing :meth:`reconnect` will resume event processing including any callbacks or the PV. @@ -301,7 +337,7 @@ assigned to. The exception to this rule is the :attr:`value` attribute. .. attribute:: status - The PV status, which will be 1 for a Normal, connected PV. + The PV status, which will be 0 for a normal, connected PV. .. attribute:: type @@ -449,7 +485,7 @@ String representation for a PV ================================ The string representation for a `PV`, as returned either with the -*as_string* argument to :meth:`ca.get` or from the :attr:`char_value` +*as_string* argument to :meth:`epics.ca.get` or from the :attr:`char_value` attribute (they are equivalent) needs some further explanation. The value of the string representation (hereafter, the :attr:`char_value`), @@ -510,7 +546,7 @@ Possible values for :attr:`auto_monitor` are: ``None`` The default value for *auto_monitor* is ``None``, and is set to ``True`` if the element count for the PV is smaller than - :data:`ca.AUTOMONITOR_MAXLENGTH` (default of 65536). To suppress + :data:`epics.ca.AUTOMONITOR_MAXLENGTH` (default of 65536). To suppress monitoring of PVs with fewer array values, you will have to explicitly turn *auto_monitor* to ``False``. For waveform arrays with more elements, automatic monitoring will not be done unless you explicitly set @@ -519,7 +555,7 @@ Possible values for :attr:`auto_monitor` are: ``True`` When *auto_monitor* is set to ``True``, the value will be monitored using - the default subscription mask set at :data:`ca.DEFAULT_SUBSCRIPTION_MASK`. + the default subscription mask set at :data:`epics.ca.DEFAULT_SUBSCRIPTION_MASK`. This mask determines which kinds of changes cause the PV to update. By default, the subscription updates when the PV value changes by more @@ -530,7 +566,7 @@ Possible values for :attr:`auto_monitor` are: It is also possible to request an explicit type of CA subscription by setting *auto_monitor* to a numeric subscription mask made up of dbr.DBE_ALARM, dbr.DBE_LOG and/or dbr.DBE_VALUE. This mask will be - passed directly to :meth:`ca.create_subscription` An example would be:: + passed directly to :meth:`epics.ca.create_subscription` An example would be:: pv1 = PV('AAA', auto_monitor=dbr.DBE_VALUE) pv2 = PV('BBB', auto_monitor=dbr.DBE_VALUE|dbr.DBE_ALARM) @@ -579,7 +615,7 @@ following keyword parameters: * `count`: the number of data elements * `ftype`: the numerical CA type indicating the data type * `type`: the python type for the data - * `status`: the status of the PV (1 for OK) + * `status`: the status of the PV (0 for OK) * `precision`: number of decimal places of precision for floating point values * `units`: string for PV units * `severity`: PV severity @@ -801,6 +837,41 @@ or, equivalently >>> print p1.char_value '1.000' +Requests including Metadata +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is also possible to get the metadata associated with a single Channel Access +request using :func:`get_with_metadata`:: + + >>> from epics import PV + >>> p1 = PV('xxx.VAL', form='time') + + >>> print(p1.get()) + 1.00 + + >>> p1.get_with_metadata() + {'status': 0, + 'severity': 0, + 'timestamp': 1543429156.811018, + 'posixseconds': 1543429156.0, + 'nanoseconds': 811018603, + 'value': 1.0} + + >>> print(p1.get_with_metadata(form='ctrl')) + {'upper_disp_limit': 100.0, + 'lower_disp_limit': -100.0, + 'upper_alarm_limit': 0.0, + 'upper_warning_limit': 0.0, + 'lower_warning_limit': 0.0, + 'lower_alarm_limit': 0.0, + 'upper_ctrl_limit': 100.0, + 'lower_ctrl_limit': -100.0, + 'precision': 3, + 'units': 'deg', + 'status': 0, + 'severity': 0, + 'value': 1.0} + Example of using info and more properties examples ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/epics/__init__.py b/epics/__init__.py index c6fc313..0eeac45 100644 --- a/epics/__init__.py +++ b/epics/__init__.py @@ -25,6 +25,7 @@ import time import sys +import threading from . import ca from . import dbr from . import pv @@ -60,13 +61,16 @@ def caput(pvname, value, wait=False, timeout=60): to wait for pv to complete processing, use 'wait=True': >>> caput('xx.VAL',3.0,wait=True) """ - thispv = get_pv(pvname, connect=True) + start_time = time.time() + thispv = get_pv(pvname, timeout=timeout, connect=True) if thispv.connected: + timeout -= (time.time() - start_time) return thispv.put(value, wait=wait, timeout=timeout) def caget(pvname, as_string=False, count=None, as_numpy=True, use_monitor=False, timeout=5.0): - """caget(pvname, as_string=False) + """caget(pvname, as_string=False,count=None,as_numpy=True, + use_monitor=False,timeout=5.0) simple get of a pv's value.. >>> x = caget('xx.VAL') @@ -91,8 +95,8 @@ def caget(pvname, as_string=False, count=None, as_numpy=True, poll() return val -def cainfo(pvname, print_out=True): - """cainfo(pvname,print_out=True) +def cainfo(pvname, print_out=True, timeout=5.0): + """cainfo(pvname,print_out=True,timeout=5.0) return printable information about pv >>>cainfo('xx.VAL') @@ -102,10 +106,13 @@ def cainfo(pvname, print_out=True): If print_out=False, the status report will be printed, and not returned. """ - thispv = get_pv(pvname, connect=True) + start_time = time.time() + thispv = get_pv(pvname, timeout=timeout, connect=True) if thispv.connected: - thispv.get() - thispv.get_ctrlvars() + conn_time = time.time() - start_time + thispv.get(timeout=timeout-conn_time) + get_time = time.time() - start_time + thispv.get_ctrlvars(timeout=timeout-get_time) if print_out: ca.write(thispv.info) else: @@ -151,19 +158,83 @@ def callback(pvname=None, value=None, char_value=None, **kwds): thispv.add_callback(callback, index=-999, with_ctrlvars=True) _PVmonitors_[pvname] = thispv -def caget_many(pvlist): - """get values for a list of PVs - This does not maintain PV objects, and works as fast - as possible to fetch many values. - """ - chids, out = [], [] - for name in pvlist: chids.append(ca.create_channel(name, - auto_cb=False, - connect=False)) - for chid in chids: ca.connect_channel(chid) - for chid in chids: ca.get(chid, wait=False) - for chid in chids: out.append(ca.get_complete(chid)) - return out +def caget_many(pvlist, as_string=False, as_numpy=True, count=None, + timeout=1.0, conn_timeout=1.0): + """get values for a list of PVs, working as fast as possible + + Arguments + --------- + pvlist (list): list of pv names to fetch + as_string (bool): whether to get values as strings [False] + as_numpy (bool): whether to get values as numpy arrys [True] + count (int or None): max number of elements to get [None] + timeout (float): timeout on *each* get() [1.0] + conn_timeout (float): timeout for *all* pvs to connect [1.0] + + Returns + -------- + list of values, with `None` signifying 'not connected' or 'timed out'. + + Notes + ------ + this does not cache PV objects. + """ + chids, connected, out = [], [], [] + for name in pvlist: + chids.append(ca.create_channel(name, auto_cb=False, connect=False)) + + all_connected = False + expire_time = time.time() + timeout + while (not all_connected and (time.time() < expire_time)): + connected = [dbr.CS_CONN==ca.state(chid) for chid in chids] + all_connected = all(connected) + poll() + for (chid, conn) in zip(chids, connected): + if conn: + ca.get(chid, count=count, as_string=as_string, as_numpy=as_numpy, + wait=False) + + poll() + for (chid, conn) in zip(chids, connected): + val = None + if conn: + val = ca.get_complete(chid, count=count, as_string=as_string, + as_numpy=as_numpy, timeout=timeout) + out.append(val) + return out +def caput_many(pvlist, values, wait=False, connection_timeout=None, put_timeout=60): + """put values to a list of PVs, as fast as possible + This does not maintain the PV objects it makes. If + wait is 'each', *each* put operation will block until + it is complete or until the put_timeout duration expires. + If wait is 'all', this method will block until *all* + put operations are complete, or until the put_timeout + duration expires. + Note that the behavior of 'wait' only applies to the + put timeout, not the connection timeout. + Returns a list of integers for each PV, 1 if the put + was successful, or a negative number if the timeout + was exceeded. + """ + if len(pvlist) != len(values): + raise ValueError("List of PV names must be equal to list of values.") + out = [] + pvs = [PV(name, auto_monitor=False, connection_timeout=connection_timeout) for name in pvlist] + conns = [p.connected for p in pvs] + wait_all = (wait == 'all') + wait_each = (wait == 'each') + for p, v in zip(pvs, values): + out.append(p.put(v, wait=wait_each, timeout=put_timeout, use_complete=wait_all)) + if wait_all: + start_time = time.time() + while not all([(p.connected and p.put_complete) for p in pvs]): + ca.poll() + elapsed_time = time.time() - start_time + if elapsed_time > put_timeout: + break + return [1 if (p.connected and p.put_complete) else -1 for p in pvs] + else: + return [o if o == 1 else -1 for o in out] diff --git a/epics/_version.py b/epics/_version.py index 84386be..9098d43 100644 --- a/epics/_version.py +++ b/epics/_version.py @@ -23,9 +23,9 @@ def get_keywords(): # setup.py/versioneer.py will grep for the variable names, so they must # each be defined on a line of their own. _version.py will just call # get_keywords(). - git_refnames = "$Format:%d$" - git_full = "$Format:%H$" - git_date = "$Format:%ci$" + git_refnames = " (tag: 3.4.3)" + git_full = "b070e5cc7c47914d9ac96ce16e471e772a273b8e" + git_date = "2020-09-17 10:36:44 -0500" keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} return keywords diff --git a/epics/alarm.py b/epics/alarm.py index db46427..31e698f 100644 --- a/epics/alarm.py +++ b/epics/alarm.py @@ -30,7 +30,7 @@ class Alarm(object): alert_delay time (in seconds) to stay quiet after executing a callback. this is a minimum time, as it is checked only when a PVs value actually changes. See note below. - + example: >>> from epics import alarm, poll >>> def alarmHandler(pvname=None, value=None, **kw): @@ -44,7 +44,7 @@ class Alarm(object): >>> poll() when 'XX.VAL' exceeds (is 'gt') 2.0, the alarmHandler will be called. - + notes: alarm_delay: The alarm delay avoids over-notification by specifying a @@ -52,18 +52,18 @@ class Alarm(object): sent, even if a value is changing and out-of-range. Since Epics callback are used to process events, the alarm state will only be checked when a PV's value changes. - + callback function: the user-supplied callback function should be prepared for a large number of keyword arguments: use **kw!!! For further explanation, see notes in pv.py. - + These keyword arguments will always be included: - + pvname name of PV value current value of PV char_value text string for PV trip_point will hold the trip point used to define 'out of range' - comparison string + comparison string self.user_callback(pvname=pvname, value=value, char_value=char_value, trip_point=self.trip_point, @@ -82,7 +82,7 @@ class Alarm(object): '>=': operator.__ge__, 'gt': operator.__gt__, '>' : operator.__gt__ } - + def __init__(self, pvname, comparison=None, trip_point=None, callback=None, alert_delay=10): @@ -91,12 +91,12 @@ def __init__(self, pvname, comparison=None, trip_point=None, elif is_string(pvname): self.pv = pv.get_pv(pvname) self.pv.connect() - + if self.pv is None or comparison is None or trip_point is None: msg = 'alarm requires valid PV, comparison, and trip_point' raise UserWarning(msg) - - + + self.trip_point = trip_point self.last_alert = 0 @@ -105,27 +105,27 @@ def __init__(self, pvname, comparison=None, trip_point=None, self.cmp = None self.comp_name = 'Not Defined' - if hasattr(comparison, '__call__'): + if callable(comparison): self.comp_name = comparison.__name__ - self.cmp = comparison + self.cmp = comparison elif comparison is not None: self.cmp = self.ops.get(comparison.replace('_', ''), None) if self.cmp is not None: self.comp_name = comparison - + self.alarm_state = False self.pv.add_callback(self.check_alarm) self.check_alarm() - + def __repr__(self): return "" % (self.pv.name, self.comp_name, self.trip_point) def reset(self): - "resets the alarm state" + "resets the alarm state" self.last_alert = 0 self.alarm_state = False - + def check_alarm(self, pvname=None, value=None, char_value=None, **kw): """checks alarm status, act if needed. """ @@ -138,19 +138,17 @@ def check_alarm(self, pvname=None, value=None, char_value=None, **kw): old_alarm_state = self.alarm_state self.alarm_state = self.cmp(val, self.trip_point) - now = time.time() + now = time.time() if (self.alarm_state and not old_alarm_state and ((now - self.last_alert) > self.alert_delay)) : self.last_alert = now - if hasattr(self.user_callback, '__call__'): + if callable(self.user_callback): self.user_callback(pvname=pvname, value=value, char_value=char_value, trip_point=self.trip_point, comparison=self.comp_name, **kw) - + else: sys.stdout.write('Alarm: %s=%s (%s)\n' % (pvname, char_value, time.ctime())) - - diff --git a/epics/autosave/save_restore.py b/epics/autosave/save_restore.py index 948b15b..cd0e5aa 100755 --- a/epics/autosave/save_restore.py +++ b/epics/autosave/save_restore.py @@ -21,9 +21,9 @@ """ -from pyparsing import Literal, Optional, Word, Combine, Regex, Group, \ - ZeroOrMore, OneOrMore, LineEnd, LineStart, StringEnd, \ - alphanums, alphas, nums +from pyparsing import (Literal, Optional, Word, Combine, Regex, Group, + ZeroOrMore, OneOrMore, LineEnd, LineStart, StringEnd, + alphanums, alphas, nums, printables) import sys import os @@ -178,12 +178,12 @@ def _parse_request_file(request_file, macro_values={}): Optional( point + Optional(number) ) ).setParseAction(lambda t:float(t[0])) -# (originally I had pyparsing pulling out the $(Macro) references from inside names -# as well, but the framework doesn't work especially well without whitespace delimiters between -# tokens so we just do simple find/replace in a second pass -pv_name = Word(alphanums+":._$()") +# PV names according to app developer guide and tech-talk email thread at: +# https://epics.anl.gov/tech-talk/2019/msg01429.php +pv_name = Combine(Word(alphanums+'_-+:[]<>;{}') + + Optional(Combine('.') + Word(printables))) +pv_value = (float_number | Word(printables)) -pv_value = (float_number | Word(alphanums)) pv_assignment = pv_name + pv_value comment = Literal("#") + Regex(r".*") diff --git a/epics/ca.py b/epics/ca.py old mode 100644 new mode 100755 index 8448aec..8dce835 --- a/epics/ca.py +++ b/epics/ca.py @@ -15,17 +15,19 @@ import ctypes import ctypes.util +import atexit +import collections +import functools import os import sys +import threading import time -import logging -from math import log10 -import atexit import warnings -from threading import Thread + +from math import log10 from pkg_resources import resource_filename -from .utils import (STR2BYTES, BYTES2STR, NULLCHAR, NULLCHAR_2, +from .utils import (STR2BYTES, BYTES2STR, NULLCHAR_2, strjoin, memcopy, is_string, is_string_or_bytes, ascii_string, clib_search_path) @@ -63,6 +65,9 @@ def write(msg, newline=True, flush=True): AUTO_CLEANUP = True +# A sentinel to mark libca as going through the shutdown process +_LIBCA_FINALIZED = object() + ## # maximum element count for auto-monitoring of PVs in epics.pv # and for automatic conversion of numerical array data to numpy arrays @@ -74,26 +79,37 @@ def write(msg, newline=True, flush=True): DEFAULT_CONNECTION_TIMEOUT = 2.0 ## Cache of existing channel IDs: -# pvname: {'chid':chid, 'conn': isConnected, -# 'ts': ts_conn, 'callbacks': [ user_callback... ]) -# isConnected = True/False: if connected. -# ts_conn = ts of last connection event or failed attempt. -# user_callback = one or more user functions to be called on -# change (accumulated in the cache) -_cache = {} -_namecache = {} +# Keyed on context, then on pv name (e.g., _cache[ctx][pvname]) +_cache = collections.defaultdict(dict) +_chid_cache = {} + +# Puts with completion in progress: +_put_completes = [] # logging.basicConfig(filename='ca.log',level=logging.DEBUG) -## Cache of pvs waiting for put to be done. -_put_done = {} - -# get a unique python value that cannot be a value held by an -# actual PV to signal "Get is incomplete, awaiting callback" -class Empty: - """used to create a unique python value that cannot be - held as an actual PV value""" - pass -GET_PENDING = Empty() + +class _GetPending: + """ + A unique python object that cannot be a value held by an actual PV to + signal "Get is incomplete, awaiting callback" + """ + def __repr__(self): + return 'GET_PENDING' + + +Empty = _GetPending # back-compat +GET_PENDING = _GetPending() + + +class _SentinelWithLock: + """ + Used in create_channel, this sentinel ensures that two threads in the same + CA context do not conflict if they call `create_channel` with the same + pvname at the exact same time. + """ + def __init__(self): + self.lock = threading.Lock() + class ChannelAccessException(Exception): """Channel Access Exception: General Errors""" @@ -103,6 +119,14 @@ def __init__(self, *args): if type_ is not None: sys.excepthook(type_, value, traceback) +class ChannelAccessGetFailure(Exception): + """Channel Access Exception: _onGetEvent != ECA_NORMAL""" + def __init__(self, message, chid, status): + super(ChannelAccessGetFailure, self).__init__(message) + self.chid = chid + self.status = status + + class CASeverityException(Exception): """Channel Access Severity Check Exception: PySEVCHK got unexpected return value""" @@ -114,6 +138,131 @@ def __str__(self): return " %s returned '%s'" % (self.fcn, self.msg) +class _CacheItem: + ''' + The cache state for a single chid in a context. + + This class itself is not thread-safe; it is expected that callers will use + the lock appropriately when modifying the state. + + Attributes + ---------- + lock : threading.RLock + A lock for modifying the state + conn : bool + The connection status + context : int + The context in which this is CacheItem was created in + chid : ctypes.c_long + The channel ID + pvname : str + The PV name + ts : float + The connection timestamp (or last failed attempt) + failures : int + Number of failed connection attempts + get_results : dict + Keyed on the requested field type -> requested value + callbacks : list + One or more user functions to be called on change of connection status + access_event_callbacks : list + One or more user functions to be called on change of access rights + ''' + + def __init__(self, chid, pvname, callbacks=None, ts=0): + self._chid = None + self.context = current_context() + self.lock = threading.RLock() + self.conn = False + self.pvname = pvname + self.ts = ts + self.failures = 0 + + self.get_results = collections.defaultdict(lambda: [None]) + + if callbacks is None: + callbacks = [] + + self.callbacks = callbacks + self.access_event_callback = [] + self.chid = chid + + @property + def chid(self): + return self._chid + + @chid.setter + def chid(self, chid): + if chid is not None and not isinstance(chid, dbr.chid_t): + chid = dbr.chid_t(chid) + + self._chid = chid + + def __repr__(self): + return ( + '<{} {!r} {} failures={} callbacks={} access_callbacks={} chid={}>' + ''.format(self.__class__.__name__, + self.pvname, + 'connected' if self.conn else 'disconnected', + self.failures, + len(self.callbacks), + len(self.access_event_callback), + self.chid_int, + ) + ) + + def __getitem__(self, key): + # back-compat + return getattr(self, key) + + @property + def chid_int(self): + 'The channel id, as an integer' + return _chid_to_int(self.chid) + + def run_access_event_callbacks(self, ra, wa): + ''' + Run all access event callbacks + + Parameters + ---------- + ra : bool + Read-access + wa : bool + Write-access + ''' + for callback in list(self.access_event_callback): + if callable(callback): + callback(ra, wa) + + def run_connection_callbacks(self, conn, timestamp): + ''' + Run all connection callbacks + + Parameters + ---------- + conn : bool + Connected (True) or disconnected + timestamp : float + The event timestamp + ''' + # Lock here, as create_channel may be setting the chid + with self.lock: + self.conn = conn + self.ts = timestamp + self.failures = 0 + + chid_int = self.chid_int + for callback in list(self.callbacks): + if callable(callback): + # The following sleep is here only to allow other threads the + # opportunity to grab the Python GIL. (see pyepics/pyepics#171) + time.sleep(0) + + # print( ' ==> connection callback ', callback, conn) + callback(pvname=self.pvname, chid=chid_int, conn=self.conn) + + def _find_lib(inp_lib_name): """ find location of ca dynamic library @@ -125,7 +274,7 @@ def _find_lib(inp_lib_name): if dllpath is not None and inp_lib_name != 'ca': _parent, _name = os.path.split(dllpath) dllpath = os.path.join(_parent, _name.replace('ca', inp_lib_name)) - + if (dllpath is not None and os.path.exists(dllpath) and os.path.isfile(dllpath)): return dllpath @@ -155,7 +304,7 @@ def envpath2list(envname, path_sep): plist = [''] try: plist = os.environ.get(envname, '').split(path_sep) - except AttributError: + except AttributeError: pass return plist @@ -214,7 +363,7 @@ def initialize_libca(): if 'EPICS_CA_MAX_ARRAY_BYTES' not in os.environ: os.environ['EPICS_CA_MAX_ARRAY_BYTES'] = "%i" % 2**24 - global libca, initial_context, _cache + global libca, initial_context if os.name == 'nt': load_dll = ctypes.windll.LoadLibrary @@ -278,21 +427,21 @@ def finalize_libca(maxtime=10.0): """ global libca - global _cache - if libca is None: + if libca is None or libca is _LIBCA_FINALIZED: return try: start_time = time.time() flush_io() poll() - for ctxid, ctx in _cache.items(): - for pvname, info in ctx.items(): - try: - clear_channel(info['chid']) - except KeyError: - pass - ctx.clear() + for chid, entry in list(_chid_cache.items()): + try: + clear_channel(chid) + except ChannelAccessException: + pass + + _chid_cache.clear() _cache.clear() + flush_count = 0 while (flush_count < 5 and time.time()-start_time < maxtime): @@ -300,27 +449,42 @@ def finalize_libca(maxtime=10.0): poll() flush_count += 1 context_destroy() - libca = None - except Exception: + libca = _LIBCA_FINALIZED + except: pass time.sleep(0.01) + def get_cache(pvname): - "return cache dictionary for a given pvname in the current context" + "return _CacheItem for a given pvname in the current context" return _cache[current_context()].get(pvname, None) + +def _get_cache_by_chid(chid): + 'return _CacheItem for a given channel id' + try: + return _chid_cache[chid] + except KeyError: + # It's possible that the channel id cache is not yet ready; check the + # context cache before giving up. This branch should not happen often. + context = current_context() + if context is not None: + pvname = BYTES2STR(libca.ca_name(dbr.chid_t(chid))) + return _cache[context][pvname] + raise + + def show_cache(print_out=True): """print out a listing of PVs in the current session to standard output. Use the *print_out=False* option to be returned the listing instead of having it printed out. """ - global _cache out = [] out.append('# PVName ChannelID/Context Connected?') out.append('#--------------------------------------------') for context, context_chids in list(_cache.items()): for vname, val in list(context_chids.items()): - chid = val['chid'] + chid = val.chid if len(vname) < 15: vname = (vname + ' '*15)[:15] out.append(" %s %s/%s %s" % (vname, repr(chid), @@ -341,9 +505,8 @@ def clear_cache(): """ # Clear global state variables - global _cache _cache.clear() - _put_done.clear() + _chid_cache.clear() # Clear the cache of PVs used by epics.caget()-like functions from . import pv @@ -375,15 +538,15 @@ def withCA(fcn): Note that CA functions that take a Channel ID (chid) as an argument are NOT wrapped by this: to get a chid, the library must have been initialized already.""" + @functools.wraps(fcn) def wrapper(*args, **kwds): "withCA wrapper" global libca if libca is None: initialize_libca() + elif libca is _LIBCA_FINALIZED: + return # Avoid raising exceptions when Python shutting down return fcn(*args, **kwds) - wrapper.__doc__ = fcn.__doc__ - wrapper.__name__ = fcn.__name__ - wrapper.__dict__.update(fcn.__dict__) return wrapper def withCHID(fcn): @@ -395,8 +558,13 @@ def withCHID(fcn): # It may be worth making a chid class (which could hold connection # data of _cache) that could be tested here. For now, that # seems slightly 'not low-level' for this module. + @functools.wraps(fcn) def wrapper(*args, **kwds): "withCHID wrapper" + global libca + if libca is _LIBCA_FINALIZED: + return # Avoid raising exceptions when Python shutting down + if len(args)>0: chid = args[0] args = list(args) @@ -406,21 +574,25 @@ def wrapper(*args, **kwds): msg = "%s: not a valid chid %s %s args %s kwargs %s!" % ( (fcn.__name__, chid, type(chid), args, kwds)) raise ChannelAccessException(msg) - + if chid.value not in _chid_cache: + raise ChannelAccessException('Unexpected channel ID') return fcn(*args, **kwds) - wrapper.__doc__ = fcn.__doc__ - wrapper.__name__ = fcn.__name__ - wrapper.__dict__.update(fcn.__dict__) return wrapper + def withConnectedCHID(fcn): """decorator to ensure that the first argument of a function is a fully connected Channel ID, ``chid``. This test is (intended to be) robust, and will try to make sure a ``chid`` is actually connected before calling the decorated function. """ + @functools.wraps(fcn) def wrapper(*args, **kwds): "withConnectedCHID wrapper" + global libca + if libca is _LIBCA_FINALIZED: + return # Avoid raising exceptions when Python shutting down + if len(args)>0: chid = args[0] args = list(args) @@ -431,28 +603,49 @@ def wrapper(*args, **kwds): (fcn.__name__)) if not isConnected(chid): timeout = kwds.get('timeout', DEFAULT_CONNECTION_TIMEOUT) - fmt ="%s() timed out waiting '%s' to connect (%d seconds)" - if not connect_channel(chid, timeout=timeout): + connected = connect_channel(chid, timeout=timeout) + if not connected: + fmt ="%s() timed out waiting '%s' to connect (%d seconds)" raise ChannelAccessException(fmt % (fcn.__name__, - name(chid), timeout)) + name(chid), timeout)) return fcn(*args, **kwds) - wrapper.__doc__ = fcn.__doc__ - wrapper.__name__ = fcn.__name__ - wrapper.__dict__.update(fcn.__dict__) + return wrapper + +def withMaybeConnectedCHID(fcn): + """decorator to **try** to ensure that the first argument of a function + is a connected Channel ID, ``chid``. + """ + @functools.wraps(fcn) + def wrapper(*args, **kwds): + "withMaybeConnectedCHID wrapper" + global libca + if libca is _LIBCA_FINALIZED: + return # Avoid raising exceptions when Python shutting down + + if len(args)>0: + chid = args[0] + args = list(args) + if isinstance(chid, int): + args[0] = chid = dbr.chid_t(chid) + if not isinstance(chid, dbr.chid_t): + raise ChannelAccessException("%s: not a valid chid!" % \ + (fcn.__name__)) + if not isConnected(chid): + timeout = kwds.get('timeout', DEFAULT_CONNECTION_TIMEOUT) + connect_channel(chid, timeout=timeout) + return fcn(*args, **kwds) return wrapper def withInitialContext(fcn): """decorator to ensure that the wrapped function uses the initial threading context created at initialization of CA """ + @functools.wraps(fcn) def wrapper(*args, **kwds): "withInitialContext wrapper" use_initial_context() return fcn(*args, **kwds) - wrapper.__doc__ = fcn.__doc__ - wrapper.__name__ = fcn.__name__ - wrapper.__dict__.update(fcn.__dict__) return wrapper def PySEVCHK(func_name, status, expected=dbr.ECA_NORMAL): @@ -474,186 +667,107 @@ def withSEVCHK(fcn): function whose return value is from a corresponding libca function and whose return value should be ``dbr.ECA_NORMAL``. """ + @functools.wraps(fcn) def wrapper(*args, **kwds): "withSEVCHK wrapper" status = fcn(*args, **kwds) return PySEVCHK( fcn.__name__, status) - wrapper.__doc__ = fcn.__doc__ - wrapper.__name__ = fcn.__name__ - wrapper.__dict__.update(fcn.__dict__) return wrapper ## ## Event Handler for monitor event callbacks def _onMonitorEvent(args): """Event Handler for monitor events: not intended for use""" + try: + entry = _get_cache_by_chid(args.chid) + except KeyError: + # In case the chid is no longer in our cache, exit now. + return # If read access to a process variable is lost, this callback is invoked # indicating the loss in the status argument. Users can use the connection # callback to get informed of connection loss, so we just ignore any # bad status codes. - # for 64-bit python on Windows! - if dbr.PY64_WINDOWS: - args = args.contents - if args.status != dbr.ECA_NORMAL: return value = dbr.cast_args(args) - pvname = name(args.chid) kwds = {'ftype':args.type, 'count':args.count, - 'chid':args.chid, 'pvname': pvname} + 'chid': args.chid, 'pvname': entry.pvname} # add kwds arguments for CTRL and TIME variants # this is in a try/except clause to avoid problems # caused by uninitialized waveform arrays - if args.type >= dbr.CTRL_STRING: - try: - tmpv = value[0] - for attr in dbr.ctrl_limits + ('precision', 'units', 'status', 'severity'): - if hasattr(tmpv, attr): - kwds[attr] = getattr(tmpv, attr) - if attr == 'units': - kwds[attr] = BYTES2STR(getattr(tmpv, attr, None)) - - if (hasattr(tmpv, 'strs') and hasattr(tmpv, 'no_str') and - tmpv.no_str > 0): - kwds['enum_strs'] = tuple([tmpv.strs[i].value for - i in range(tmpv.no_str)]) - except IndexError: - pass - elif args.type >= dbr.TIME_STRING: - try: - tmpv = value[0] - kwds['status'] = tmpv.status - kwds['severity'] = tmpv.severity - kwds['timestamp'] = dbr.make_unixtime(tmpv.stamp) - kwds['posixseconds'] = tmpv.stamp.secs + dbr.EPICS2UNIX_EPOCH - kwds['nanoseconds'] = tmpv.stamp.nsec - except IndexError: - pass + try: + kwds.update(**_unpack_metadata(ftype=args.type, dbr_value=value[0])) + except IndexError: + pass value = _unpack(args.chid, value, count=args.count, ftype=args.type) - if hasattr(args.usr, '__call__'): + if callable(args.usr): args.usr(value=value, **kwds) ## connection event handler: def _onConnectionEvent(args): - """set flag in cache holding whteher channel is - connected. if provided, run a user-function""" - # for 64-bit python on Windows! - if dbr.PY64_WINDOWS: - args = args.contents + "Connection notification - run user callbacks" + try: + entry = _get_cache_by_chid(args.chid) + except KeyError: + return + + entry.run_connection_callbacks(conn=(args.op == dbr.OP_CONN_UP), + timestamp=time.time()) - ctx = current_context() - conn = (args.op == dbr.OP_CONN_UP) - global _cache - - if ctx is None and len(_cache.keys()) > 0: - ctx = list(_cache.keys())[0] - if ctx not in _cache: - _cache[ctx] = {} - - # search for PV in any context... - pv_found = False - for context in _cache: - pvname = name(args.chid) - - if pvname in _cache[context]: - pv_found = True - break - - # logging.debug("ConnectionEvent %s/%i/%i " % (pvname, args.chid, conn)) - # print("ConnectionEvent %s/%i/%i " % (pvname, args.chid, conn)) - if not pv_found: - _cache[ctx][pvname] = {'conn':False, 'chid': args.chid, - 'ts':0, 'failures':0, 'value': None, - 'callbacks': []} - - # set connection time, run connection callbacks - # in all contexts - for context, cvals in _cache.items(): - if pvname in cvals: - entry = cvals[pvname] - ichid = entry['chid'] - if isinstance(entry['chid'], dbr.chid_t): - ichid = entry['chid'].value - - if int(ichid) == int(args.chid): - chid = args.chid - entry.update({'chid': chid, 'conn': conn, - 'ts': time.time(), 'failures': 0}) - for callback in entry.get('callbacks', []): - poll() - if hasattr(callback, '__call__'): - # print( ' ==> connection callback ', callback, conn) - callback(pvname=pvname, chid=chid, conn=conn) - #print('Connection done') - - return ## get event handler: def _onGetEvent(args, **kws): """get_callback event: simply store data contents which will need conversion to python data with _unpack().""" - # for 64-bit python on Windows! - if dbr.PY64_WINDOWS: - args = args.contents - # print("GET EVENT: chid, user ", args.chid, args.usr) # print("GET EVENT: type, count ", args.type, args.count) # print("GET EVENT: status ", args.status, dbr.ECA_NORMAL) - global _cache - if args.status != dbr.ECA_NORMAL: + try: + entry = _get_cache_by_chid(args.chid) + except KeyError: return - if dbr.IRON_PYTHON: - get_cache(name(args.chid))[args.usr.value] = (dbr.cast_args(args)) + ftype = (args.usr.value if dbr.IRON_PYTHON + else args.usr) + + if args.status != dbr.ECA_NORMAL: + result = ChannelAccessGetFailure( + 'Get failed; status code: %d' % args.status, + chid=args.chid, + status=args.status + ) + elif dbr.IRON_PYTHON: + result = dbr.cast_args(args) else: - get_cache(name(args.chid))[args.usr] = memcopy(dbr.cast_args(args)) + result = memcopy(dbr.cast_args(args)) + + with entry.lock: + entry.get_results[ftype][0] = result ## put event handler: def _onPutEvent(args, **kwds): - """set put-has-completed for this channel, - call optional user-supplied callback""" - # for 64-bit python on Windows! - if dbr.PY64_WINDOWS: - args = args.contents - - pvname = name(args.chid) - fcn = _put_done[pvname][1] - data = _put_done[pvname][2] - _put_done[pvname] = (True, None, None) - if hasattr(fcn, '__call__'): - if isinstance(data, dict): - kwds.update(data) - elif data is not None: - kwds['data'] = data - fcn(pvname=pvname, **kwds) + 'Put completion notification - run specified callback' + fcn = args.usr + if callable(fcn): + fcn() def _onAccessRightsEvent(args): - # for 64-bit python on Windows! - global _cache - if dbr.PY64_WINDOWS: - args = args.contents - - chid = args.chid - ra = bool(args.read_access) - wa = bool(args.write_access) - - # Getting bunk result from ca.current_context on channel disconnect - # Do this the long way... - for ctx in _cache.values(): - pvname = name(chid) - if pvname in ctx: - ch = ctx[pvname] - if 'access_event_callback' in ch: - for callback in ch['access_event_callback']: - if callable(callback): - callback(ra, wa) + 'Access rights callback' + try: + entry = _chid_cache[_chid_to_int(args.chid)] + except KeyError: + return + read = bool(args.access & 1) + write = bool((args.access >> 1) & 1) + entry.run_access_event_callbacks(read, write) + # create global reference to these callbacks _CB_CONNECT = dbr.make_callback(_onConnectionEvent, dbr.connection_args) @@ -674,11 +788,7 @@ def context_create(ctx=None): "create a context. if argument is None, use PREEMPTIVE_CALLBACK" if ctx is None: ctx = {False:0, True:1}[PREEMPTIVE_CALLBACK] - ret = libca.ca_context_create(ctx) - new_ctx = current_context() - if new_ctx and new_ctx not in _cache: - _cache[new_ctx] = {} - return ret + return libca.ca_context_create(ctx) def create_context(ctx=None): @@ -703,12 +813,11 @@ def create_context(ctx=None): @withCA def context_destroy(): "destroy current context" - global _cache ctx = current_context() ret = libca.ca_context_destroy() - if ctx in _cache: - _cache[ctx].clear() - _cache.pop(ctx) + ctx_cache = _cache.pop(ctx, None) + if ctx_cache is not None: + ctx_cache.clear() return ret def destroy_context(): @@ -866,54 +975,47 @@ def create_channel(pvname, connect=False, auto_cb=True, callback=None): """ - # # Note that _CB_CONNECT (defined above) is a global variable, holding # a reference to _onConnectionEvent: This is really the connection # callback that is run -- the callack here is stored in the _cache # and called by _onConnectionEvent. - pvn = STR2BYTES(pvname) - ctx = current_context() - global _cache - if ctx not in _cache: - _cache[ctx] = {} - if pvname not in _cache[ctx]: # new PV for this context - entry = {'conn':False, 'chid': None, - 'ts': 0, 'failures':0, 'value': None, - 'callbacks': [ callback ]} - # logging.debug("Create Channel %s " % pvname) - _cache[ctx][pvname] = entry - else: - entry = _cache[ctx][pvname] - if not entry['conn'] and callback is not None: # pending connection - _cache[ctx][pvname]['callbacks'].append(callback) - elif (hasattr(callback, '__call__') and - not callback in entry['callbacks']): - entry['callbacks'].append(callback) - callback(chid=entry['chid'], pvname=pvname, conn=entry['conn']) - - conncb = 0 - if auto_cb: - conncb = _CB_CONNECT - if entry.get('chid', None) is not None: - # already have or waiting on a chid - chid = _cache[ctx][pvname]['chid'] - else: - chid = dbr.chid_t() - ret = libca.ca_create_channel(ctypes.c_char_p(pvn), conncb, 0, 0, - ctypes.byref(chid)) - PySEVCHK('create_channel', ret) - entry['chid'] = chid - - chid_key = chid - if isinstance(chid_key, dbr.chid_t): - chid_key = chid.value - _namecache[chid_key] = BYTES2STR(pvn) - # print("CREATE Channel ", pvn, chid) + + context_cache = _cache[current_context()] + + # {}.setdefault is an atomic operation, so we are guaranteed to never + # create the same channel twice here: + with context_cache.setdefault(pvname, _SentinelWithLock()).lock: + # Grab the entry again from the cache. Between the time the lock was + # attempted and acquired, the cache may have changed. + entry = context_cache[pvname] + is_new_channel = isinstance(entry, _SentinelWithLock) + if is_new_channel: + callbacks = [callback] if callable(callback) else None + entry = _CacheItem(chid=None, pvname=pvname, callbacks=callbacks) + context_cache[pvname] = entry + + chid = dbr.chid_t() + with entry.lock: + ret = libca.ca_create_channel( + ctypes.c_char_p(STR2BYTES(pvname)), _CB_CONNECT, 0, 0, + ctypes.byref(chid) + ) + PySEVCHK('create_channel', ret) + + entry.chid = chid + _chid_cache[chid.value] = entry + + if (not is_new_channel and callable(callback) and + callback not in entry.callbacks): + entry.callbacks.append(callback) + if entry.chid is not None and entry.conn: + # Run the connection callback if already connected: + callback(chid=_chid_to_int(entry.chid), pvname=pvname, + conn=entry.conn) + if connect: - connect_channel(chid) - if conncb != 0: - poll() - return chid + connect_channel(entry.chid) + return entry.chid @withCHID def connect_channel(chid, timeout=None, verbose=False): @@ -961,10 +1063,6 @@ def connect_channel(chid, timeout=None, verbose=False): start_time = time.time() ctx = current_context() pvname = name(chid) - global _cache - if ctx not in _cache: - _cache[ctx] = {} - if timeout is None: timeout = DEFAULT_CONNECTION_TIMEOUT @@ -972,38 +1070,44 @@ def connect_channel(chid, timeout=None, verbose=False): poll() conn = (state(chid) == dbr.CS_CONN) if not conn: - _cache[ctx][pvname]['ts'] = time.time() - _cache[ctx][pvname]['failures'] += 1 + entry = _cache[ctx][pvname] + with entry.lock: + entry.ts = time.time() + entry.failures += 1 return conn # functions with very light wrappings: @withCHID def replace_access_rights_event(chid, callback=None): - global _cache + ch = get_cache(name(chid)) - ctx = current_context() - ch = _cache[ctx][name(chid)] - if 'access_event_callback' not in ch: - ch.update({'access_event_callback': list()}) - - if callback is not None: - ch['access_event_callback'].append(callback) + if ch and callback is not None: + ch.access_event_callback.append(callback) ret = libca.ca_replace_access_rights_event(chid, _CB_ACCESS) PySEVCHK('replace_access_rights_event', ret) +def _chid_to_int(chid): + ''' + Return the integer representation of a chid + + Parameters + ---------- + chid : ctypes.c_long, int + + Returns + ------- + chid : int + ''' + if hasattr(chid, 'value'): + return int(chid.value) + return chid + + @withCHID def name(chid): "return PV name for channel name" - global _namecache - # sys.stdout.write("NAME %s %s\n" % (repr(chid), repr(chid.value in _namecache))) - # sys.stdout.flush() - - if chid.value in _namecache: - val = _namecache[chid.value] - else: - val = _namecache[chid.value] = BYTES2STR(libca.ca_name(chid)) - return val + return BYTES2STR(libca.ca_name(chid)) @withCHID def host_name(chid): @@ -1035,7 +1139,15 @@ def field_type(chid): @withCHID def clear_channel(chid): "clear the channel" - return libca.ca_clear_channel(chid) + ret = libca.ca_clear_channel(chid) + entry = _chid_cache.pop(chid.value, None) + if entry is not None: + context_cache = _cache[entry.context] + context_cache.pop(entry.pvname, None) + with entry.lock: + entry.chid = None + return ret + @withCHID def state(chid): @@ -1058,10 +1170,15 @@ def access(chid): acc = read_access(chid) + 2 * write_access(chid) return ('no access', 'read-only', 'write-only', 'read/write')[acc] +@withCHID def promote_type(chid, use_time=False, use_ctrl=False): """promotes the native field type of a ``chid`` to its TIME or CTRL variant. Returns the integer corresponding to the promoted field value.""" - ftype = field_type(chid) + return promote_fieldtype( field_type(chid), use_time=use_time, use_ctrl=use_ctrl) + +def promote_fieldtype(ftype, use_time=False, use_ctrl=False): + """promotes the native field type to its TIME or CTRL variant. + Returns the integer corresponding to the promoted field value.""" if use_ctrl: ftype += dbr.CTRL_STRING elif use_time: @@ -1070,9 +1187,10 @@ def promote_type(chid, use_time=False, use_ctrl=False): ftype = dbr.TIME_STRING return ftype + def _unpack(chid, data, count=None, ftype=None, as_numpy=True): """unpacks raw data for a Channel ID `chid` returned by libca functions - including `ca_get_array_callback` or subscription callback, and returns + including `ca_array_get_callback` or subscription callback, and returns the corresponding Python data Normally, users are not expected to need to access this function, but @@ -1090,8 +1208,6 @@ def _unpack(chid, data, count=None, ftype=None, as_numpy=True): data type of channel (defaults to native type of chid) as_numpy : bool whether to convert to numpy array. - - """ def scan_string(data, count): @@ -1135,9 +1251,11 @@ def unpack(data, count, ntype, use_numpy, elem_count): # Grab the native-data-type data try: - data = data[1] + extended_data, data = data except (TypeError, IndexError): return None + except ValueError: + extended_data = None if count == 0 or count is None: count = len(data) @@ -1148,12 +1266,133 @@ def unpack(data, count, ntype, use_numpy, elem_count): ftype = field_type(chid) if ftype is None: ftype = dbr.INT + ntype = native_type(ftype) elem_count = element_count(chid) use_numpy = (HAS_NUMPY and as_numpy and ntype != dbr.STRING and count != 1) return unpack(data, count, ntype, use_numpy, elem_count) -@withConnectedCHID + +def _unpack_metadata(ftype, dbr_value): + '''Unpack DBR metadata into a dictionary + + Parameters + ---------- + ftype : int + The field type for the respective DBR value + dbr_value : ctype.Structure + The structure holding the data to be unpacked + + Returns + ------- + md : dict + A dictionary containing zero or more of the following keys, depending + on ftype:: + + {'precision', 'units', 'status', 'severity', 'enum_strs', 'status', + 'severity', 'timestamp', 'posixseconds', 'nanoseconds', + 'upper_disp_limit', 'lower_disp_limit', 'upper_alarm_limit', + 'upper_warning_limit', 'lower_warning_limit','lower_alarm_limit', + 'upper_ctrl_limit', 'lower_ctrl_limit'} + ''' + md = {} + if ftype >= dbr.CTRL_STRING: + for attr in dbr.ctrl_limits + ('precision', 'units', 'status', + 'severity'): + if hasattr(dbr_value, attr): + md[attr] = getattr(dbr_value, attr) + if attr == 'units': + md[attr] = BYTES2STR(getattr(dbr_value, attr, None)) + + if hasattr(dbr_value, 'strs') and getattr(dbr_value, 'no_str', 0) > 0: + md['enum_strs'] = tuple(BYTES2STR(dbr_value.strs[i].value) + for i in range(dbr_value.no_str)) + elif ftype >= dbr.TIME_STRING: + md['status'] = dbr_value.status + md['severity'] = dbr_value.severity + md['timestamp'] = dbr.make_unixtime(dbr_value.stamp) + md['posixseconds'] = dbr_value.stamp.secs + dbr.EPICS2UNIX_EPOCH + md['nanoseconds'] = dbr_value.stamp.nsec + + return md + + +@withMaybeConnectedCHID +def get_with_metadata(chid, ftype=None, count=None, wait=True, timeout=None, + as_string=False, as_numpy=True): + """Return the current value along with metadata for a Channel + + Parameters + ---------- + chid : ctypes.c_long + Channel ID + ftype : int + field type to use (native type is default) + count : int + maximum element count to return (full data returned by default) + as_string : bool + whether to return the string representation of the value. See notes. + as_numpy : bool + whether to return the Numerical Python representation for array / + waveform data. + wait : bool + whether to wait for the data to be received, or return immediately. + timeout : float + maximum time to wait for data before returning ``None``. + + Returns + ------- + data : dict or None + The dictionary of data, guaranteed to at least have the 'value' key. + Depending on ftype, other keys may also be present:: + + {'precision', 'units', 'status', 'severity', 'enum_strs', 'status', + 'severity', 'timestamp', 'posixseconds', 'nanoseconds', + 'upper_disp_limit', 'lower_disp_limit', 'upper_alarm_limit', + 'upper_warning_limit', 'lower_warning_limit','lower_alarm_limit', + 'upper_ctrl_limit', 'lower_ctrl_limit'} + + Returns ``None`` if the channel is not connected, `wait=False` was used, + or the data transfer timed out. + + See also + -------- + See :func:`get` for additional usage notes. + """ + if ftype is None: + ftype = field_type(chid) + if ftype in (None, -1): + return None + if count is None: + count = 0 + # count = element_count(chid) + # don't default to the element_count here - let EPICS tell us the size + # in the _onGetEvent callback + else: + count = min(count, element_count(chid)) + + entry = get_cache(name(chid)) + if not entry: + return + + # implementation note: cached value of + # None implies no value, no expected callback + # GET_PENDING implies no value yet, callback expected. + with entry.lock: + last_get, = entry.get_results[ftype] + if last_get is not GET_PENDING: + entry.get_results[ftype] = [GET_PENDING] + ret = libca.ca_array_get_callback( + ftype, count, chid, _CB_GET, ctypes.py_object(ftype)) + PySEVCHK('get', ret) + + if wait: + return get_complete_with_metadata(chid, count=count, ftype=ftype, + timeout=timeout, as_string=as_string, + as_numpy=as_numpy) + + +@withMaybeConnectedCHID def get(chid, ftype=None, count=None, wait=True, timeout=None, as_string=False, as_numpy=True): """return the current value for a Channel. @@ -1215,42 +1454,20 @@ def get(chid, ftype=None, count=None, wait=True, timeout=None, later with :func:`get_complete`. """ - global _cache + info = get_with_metadata(chid, ftype=ftype, count=count, wait=wait, + timeout=timeout, as_string=as_string, + as_numpy=as_numpy) + return (info['value'] if info is not None else None) - if ftype is None: - ftype = field_type(chid) - if ftype in (None, -1): - return None - if count is None: - count = 0 - # count = element_count(chid) - # don't default to the element_count here - let EPICS tell us the size - # in the _onGetEvent callback - else: - count = min(count, element_count(chid)) - ncache = _cache[current_context()][name(chid)] - # implementation note: cached value of - # None implies no value, no expected callback - # GET_PENDING implies no value yet, callback expected. - if ncache.get('value', None) is None: - ncache['value'] = GET_PENDING - ret = libca.ca_array_get_callback(ftype, count, chid, _CB_GET, - ctypes.py_object('value')) - PySEVCHK('get', ret) +@withMaybeConnectedCHID +def get_complete_with_metadata(chid, ftype=None, count=None, timeout=None, + as_string=False, as_numpy=True): + """Returns the current value and associated metadata for a Channel - if wait: - return get_complete(chid, count=count, ftype=ftype, - timeout=timeout, - as_string=as_string, as_numpy=as_numpy) - -@withConnectedCHID -def get_complete(chid, ftype=None, count=None, timeout=None, - as_string=False, as_numpy=True): - """returns the current value for a Channel, completing an - earlier incomplete :func:`get` that returned ``None``, either - because `wait=False` was used or because the data transfer - did not complete before the timeout passed. + This completes an earlier incomplete :func:`get` that returned ``None``, + either because `wait=False` was used or because the data transfer did not + complete before the timeout passed. Parameters ---------- @@ -1270,21 +1487,14 @@ def get_complete(chid, ftype=None, count=None, timeout=None, Returns ------- - data : object - This function will return ``None`` if the previous :func:`get` - actually completed, or if this data transfer also times out. - - - Notes - ----- - 1. The default timeout is dependent on the element count:: - default_timout = 1.0 + log10(count) (in seconds) - - 2. Consult the doc for :func:`get` for more information. + data : dict or None + This function will return ``None`` if the previous :func:`get` actually + completed, or if this data transfer also times out. + See also + -------- + See :func:`get_complete` for additional usage notes. """ - global _cache - if ftype is None: ftype = field_type(chid) if count is None: @@ -1292,22 +1502,41 @@ def get_complete(chid, ftype=None, count=None, timeout=None, else: count = min(count, element_count(chid)) - ncache = _cache[current_context()][name(chid)] - if ncache['value'] is None: + entry = get_cache(name(chid)) + if not entry: + return + + get_result = entry.get_results[ftype] + + if get_result[0] is None: + warnings.warn('get_complete without initial get() call') return None t0 = time.time() if timeout is None: timeout = 1.0 + log10(max(1, count)) - while ncache['value'] is GET_PENDING: + + while get_result[0] is GET_PENDING: poll() + if time.time()-t0 > timeout: msg = "ca.get('%s') timed out after %.2f seconds." warnings.warn(msg % (name(chid), timeout)) return None - #print("Get Complete> Unpack ", ncache['value'], count, ftype) - val = _unpack(chid, ncache['value'], count=count, + full_value, = get_result + + # print("Get Complete> Unpack ", ncache['value'], count, ftype) + + if isinstance(full_value, Exception): + get_failure_reason = full_value + raise get_failure_reason + + # NOTE: unpacking happens for each requester; this could potentially be put + # in the get callback itself. (different downside there...) + extended_data, _ = full_value + metadata = _unpack_metadata(ftype=ftype, dbr_value=extended_data) + val = _unpack(chid, full_value, count=count, ftype=ftype, as_numpy=as_numpy) # print("Get Complete unpacked to ", val) @@ -1317,8 +1546,54 @@ def get_complete(chid, ftype=None, count=None, timeout=None, val = numpy.ctypeslib.as_array(memcopy(val)) # value retrieved, clear cached value - ncache['value'] = None - return val + metadata['value'] = val + return metadata + +@withMaybeConnectedCHID +def get_complete(chid, ftype=None, count=None, timeout=None, as_string=False, + as_numpy=True): + """returns the current value for a Channel, completing an + earlier incomplete :func:`get` that returned ``None``, either + because `wait=False` was used or because the data transfer + did not complete before the timeout passed. + + Parameters + ---------- + chid : ctypes.c_long + Channel ID + ftype : int + field type to use (native type is default) + count : int + maximum element count to return (full data returned by default) + as_string : bool + whether to return the string representation of the value. + as_numpy : bool + whether to return the Numerical Python representation + for array / waveform data. + timeout : float + maximum time to wait for data before returning ``None``. + + Returns + ------- + data : object + This function will return ``None`` if the previous :func:`get` + actually completed, or if this data transfer also times out. + + + Notes + ----- + 1. The default timeout is dependent on the element count:: + default_timout = 1.0 + log10(count) (in seconds) + + 2. Consult the doc for :func:`get` for more information. + + """ + info = get_complete_with_metadata(chid, ftype=ftype, count=count, + timeout=timeout, as_string=as_string, + as_numpy=as_numpy) + return (info['value'] if info is not None + else None) + def _as_string(val, chid, count, ftype): "primitive conversion of value to a string" @@ -1352,7 +1627,7 @@ def put(chid, value, wait=False, timeout=30, callback=None, before returning. timeout : float maximum time to wait for processing to complete before returning anyway. - callback : ``None`` of callable + callback : ``None`` or callable user-supplied function to run when processing has completed. callback_data : object extra data to pass on to a user-supplied callback function. @@ -1438,29 +1713,47 @@ def put(chid, value, wait=False, timeout=30, callback=None, raise ChannelAccessException(errmsg % (repr(value))) # simple put, without wait or callback - if not (wait or hasattr(callback, '__call__')): + if not (wait or callable(callback)): ret = libca.ca_array_put(ftype, count, chid, data) PySEVCHK('put', ret) poll() return ret + # wait with callback (or put_complete) pvname = name(chid) - _put_done[pvname] = (False, callback, callback_data) start_time = time.time() + completed = dict(status=False) + + def put_completed(): + completed['status'] = True + _put_completes.remove(put_completed) + if not callable(callback): + return + + if isinstance(callback_data, dict): + kwargs = callback_data + else: + kwargs = dict(data=callback_data) + + callback(pvname=pvname, **kwargs) + + _put_completes.append(put_completed) + + ret = libca.ca_array_put_callback(ftype, count, chid, data, _CB_PUTWAIT, + ctypes.py_object(put_completed)) - ret = libca.ca_array_put_callback(ftype, count, chid, - data, _CB_PUTWAIT, 0) PySEVCHK('put', ret) poll(evt=1.e-4, iot=0.05) if wait: - while not (_put_done[pvname][0] or + while not (completed['status'] or (time.time()-start_time) > timeout): poll() - if not _put_done[pvname][0]: + if not completed['status']: ret = -ret return ret -@withConnectedCHID + +@withMaybeConnectedCHID def get_ctrlvars(chid, timeout=5.0, warn=True): """return the CTRL fields for a Channel. @@ -1475,89 +1768,28 @@ def get_ctrlvars(chid, timeout=5.0, warn=True): enum_strs will be a list of strings for the names of ENUM states. """ - global _cache - ftype = promote_type(chid, use_ctrl=True) - ncache = _cache[current_context()][name(chid)] - if ncache.get('ctrl_value', None) is None: - ncache['ctrl_value'] = GET_PENDING - ret = libca.ca_array_get_callback(ftype, 1, chid, _CB_GET, - ctypes.py_object('ctrl_value')) - - PySEVCHK('get_ctrlvars', ret) - - if ncache.get('ctrl_value', None) is None: - return {} + metadata = get_with_metadata(chid, ftype=ftype, count=1, timeout=timeout, + wait=True) + if metadata is not None: + # Ignore the value returned: + metadata.pop('value', None) + return metadata - t0 = time.time() - while ncache['ctrl_value'] is GET_PENDING: - poll() - if time.time()-t0 > timeout: - if warn: - msg = "ca.get_ctrlvars('%s') timed out after %.2f seconds." - warnings.warn(msg % (name(chid), timeout)) - return {} - try: - tmpv = ncache['ctrl_value'][0] - except TypeError: - return {} - - out = {} - for attr in ('precision', 'units', 'severity', 'status', - 'upper_disp_limit', 'lower_disp_limit', - 'upper_alarm_limit', 'upper_warning_limit', - 'lower_warning_limit','lower_alarm_limit', - 'upper_ctrl_limit', 'lower_ctrl_limit'): - if hasattr(tmpv, attr): - out[attr] = getattr(tmpv, attr, None) - if attr == 'units': - out[attr] = BYTES2STR(getattr(tmpv, attr, None)) - - if (hasattr(tmpv, 'strs') and hasattr(tmpv, 'no_str') and - tmpv.no_str > 0): - out['enum_strs'] = tuple([BYTES2STR(tmpv.strs[i].value) - for i in range(tmpv.no_str)]) - ncache['ctrl_value'] = None - return out -@withConnectedCHID +@withCHID def get_timevars(chid, timeout=5.0, warn=True): """returns a dictionary of TIME fields for a Channel. This will contain keys of *status*, *severity*, and *timestamp*. """ - global _cache - ftype = promote_type(chid, use_time=True) - ncache = _cache[current_context()][name(chid)] - if ncache.get('time_value', None) is None: - ncache['time_value'] = GET_PENDING - ret = libca.ca_array_get_callback(ftype, 1, chid, _CB_GET, - ctypes.py_object('time_value')) + metadata = get_with_metadata(chid, ftype=ftype, count=1, timeout=timeout, + wait=True) + if metadata is not None: + # Ignore the value returned: + metadata.pop('value', None) + return metadata - PySEVCHK('get_timevars', ret) - - out = {} - if ncache.get('time_value', None) is None: - return out - - t0 = time.time() - while ncache['time_value'] is GET_PENDING: - poll() - if time.time()-t0 > timeout: - if warn: - msg = "ca.get_timevars('%s') timed out after %.2f seconds." - warnings.warn(msg % (name(chid), timeout)) - return {} - - tmpv = ncache['time_value'][0] - for attr in ('status', 'severity'): - if hasattr(tmpv, attr): - out[attr] = getattr(tmpv, attr) - if hasattr(tmpv, 'stamp'): - out['timestamp'] = dbr.make_unixtime(tmpv.stamp) - - ncache['time_value'] = None - return out def get_timestamp(chid): """return the timestamp of a Channel -- the time of last update.""" @@ -1587,9 +1819,9 @@ def get_enum_strings(chid): # dbr.DBE_LOG for archive changes (ie exceeding ADEL) DEFAULT_SUBSCRIPTION_MASK = dbr.DBE_VALUE|dbr.DBE_ALARM -@withConnectedCHID -def create_subscription(chid, use_time=False, use_ctrl=False, - mask=None, callback=None, count=0): +@withCHID +def create_subscription(chid, use_time=False, use_ctrl=False, ftype=None, + mask=None, callback=None, count=0, timeout=None): """create a *subscription to changes*. Sets up a user-supplied callback function to be called on any changes to the channel. @@ -1601,6 +1833,10 @@ def create_subscription(chid, use_time=False, use_ctrl=False, whether to use the TIME variant for the PV type use_ctrl : bool whether to use the CTRL variant for the PV type + ftype : integer or None + ftype to use, overriding native type, `use_time` or `use_ctrl` + if ``None``, the native type is looked up, which requires a + connected channel. mask : integer or None bitmask combination of :data:`dbr.DBE_ALARM`, :data:`dbr.DBE_LOG`, and :data:`dbr.DBE_VALUE`, to control which changes result in a callback. @@ -1609,6 +1845,9 @@ def create_subscription(chid, use_time=False, use_ctrl=False, callback : ``None`` or callable user-supplied callback function to be called on changes + timeout : ``None`` or int + connection timeout used for unconnected channels. + Returns ------- (callback_ref, user_arg_ref, event_id) @@ -1624,12 +1863,22 @@ def create_subscription(chid, use_time=False, use_ctrl=False, Keep the returned tuple in named variable!! if the return argument gets garbage collected, a coredump will occur. + If the channel is not connected, the ftype must be specified for a + successful subscription. """ mask = mask or DEFAULT_SUBSCRIPTION_MASK + if ftype is None: + if not isConnected(chid): + if timeout is None: + timeout = DEFAULT_CONNECTION_TIMEOUT + fmt ="%s() timed out waiting '%s' to connect (%d seconds)" + if not connect_channel(chid, timeout=timeout): + raise ChannelAccessException(fmt % ("create_subscription", + (chid), timeout)) + ftype = field_type(chid) - ftype = promote_type(chid, use_ctrl=use_ctrl, use_time=use_time) - + ftype = promote_fieldtype(ftype, use_time=use_time, use_ctrl=use_ctrl) uarg = ctypes.py_object(callback) evid = ctypes.c_void_p() poll() @@ -1774,11 +2023,11 @@ def sg_put(gid, chid, value): # poll() return ret -class CAThread(Thread): +class CAThread(threading.Thread): """ Sub-class of threading.Thread to ensure that the initial CA context is used. """ def run(self): use_initial_context() - Thread.run(self) + threading.Thread.run(self) diff --git a/epics/clibs/darwin64/README b/epics/clibs/darwin64/README new file mode 100644 index 0000000..0dfb70c --- /dev/null +++ b/epics/clibs/darwin64/README @@ -0,0 +1,21 @@ +These shared libraries were built using + +Epics Base 3.16.2 on Mac OS X 10.14.1 with +Apple LLVM version 10.0.0 (clang-1000.11.45.5) Target: x86_64-apple-darwin18.2.0 + +and with Perl from MacPorts. + +Readline support was turned off by setting + COMMANDLINE_LIBRARY = + +in + CONFIG_SITE.darwinCommon.darwinCommon + + +and then + mv libca.3.16.2.dylib libca.dylib + mv libCom.3.16.2.dylib libComPYEPICS.dylib + install_name_tool -id libca.dylib libca.dylib + install_name_tool -id libComPYEPICS.dylib libComPYEPICS.dylib + install_name_tool -add-rpath . libca.dylib + install_name_tool -change libComPYEPICS.dylib @loader_path/./libComPYEPICS.dylib libca.dylib diff --git a/epics/clibs/darwin64/libCom.dylib b/epics/clibs/darwin64/libCom.dylib deleted file mode 100755 index 534965e..0000000 Binary files a/epics/clibs/darwin64/libCom.dylib and /dev/null differ diff --git a/epics/clibs/darwin64/libComPYEPICS.dylib b/epics/clibs/darwin64/libComPYEPICS.dylib new file mode 100755 index 0000000..b688090 Binary files /dev/null and b/epics/clibs/darwin64/libComPYEPICS.dylib differ diff --git a/epics/clibs/darwin64/libca.dylib b/epics/clibs/darwin64/libca.dylib index 5805136..211a933 100755 Binary files a/epics/clibs/darwin64/libca.dylib and b/epics/clibs/darwin64/libca.dylib differ diff --git a/epics/clibs/linux32/README b/epics/clibs/linux32/README new file mode 100644 index 0000000..3549b73 --- /dev/null +++ b/epics/clibs/linux32/README @@ -0,0 +1,23 @@ +These shared libraries were built using + +Epics Base 3.16.2 on Centos 6.9 with gcc 4.4.7, glibc 2.12 + +and configure Epics to cross-compile for linux-86 host target +in configure/CONFIG_SITE. + +Readline support was turned off by setting + COMMANDLINE_LIBRARY = + +in + CONFIG_SITE.Common.linux-x86 + + +The patchelf utility (from Anaconda Python) was used to make the shared +libraries portable with the following commands: + + mv libCom.so libComPYEPICS.so + patchelf --set-soname libca.so libca.so + patchelf --set-soname libComPYEPICS.so libComPYEPICS.so + patchelf --set-rpath '$ORIGIN' libca.so + patchelf --set-rpath '$ORIGIN' libComPYEPICS.so + patchelf --replace-needed libCom.so.3.16.2 libComPYEPICS.so libca.so diff --git a/epics/clibs/linux32/libCom.so b/epics/clibs/linux32/libCom.so deleted file mode 100644 index 0199bdb..0000000 Binary files a/epics/clibs/linux32/libCom.so and /dev/null differ diff --git a/epics/clibs/linux32/libComPYEPICS.so b/epics/clibs/linux32/libComPYEPICS.so new file mode 100755 index 0000000..7688488 Binary files /dev/null and b/epics/clibs/linux32/libComPYEPICS.so differ diff --git a/epics/clibs/linux32/libca.so b/epics/clibs/linux32/libca.so old mode 100644 new mode 100755 index 7e1f578..df257f8 Binary files a/epics/clibs/linux32/libca.so and b/epics/clibs/linux32/libca.so differ diff --git a/epics/clibs/linux64/README b/epics/clibs/linux64/README new file mode 100644 index 0000000..0d1a913 --- /dev/null +++ b/epics/clibs/linux64/README @@ -0,0 +1,20 @@ +These shared libraries were built using + +Epics Base 3.16.2 on Centos 6.9 with gcc 4.4.7, glibc 2.12 + +Readline support was turned off by setting + COMMANDLINE_LIBRARY = + +in + CONFIG_SITE.Common.linux-x86 + + +The patchelf utility (from Anaconda Python) was used to make the shared +libraries portable with the following commands: + + mv libCom.so libComPYEPICS.so + patchelf --set-soname libca.so libca.so + patchelf --set-soname libComPYEPICS.so libComPYEPICS.so + patchelf --set-rpath '$ORIGIN' libca.so + patchelf --set-rpath '$ORIGIN' libComPYEPICS.so + patchelf --replace-needed libCom.so.3.16.2 libComPYEPICS.so libca.so diff --git a/epics/clibs/linux64/libCom.so b/epics/clibs/linux64/libCom.so deleted file mode 100755 index a1fb6a1..0000000 Binary files a/epics/clibs/linux64/libCom.so and /dev/null differ diff --git a/epics/clibs/linux64/libComPYEPICS.so b/epics/clibs/linux64/libComPYEPICS.so new file mode 100755 index 0000000..1ca030a Binary files /dev/null and b/epics/clibs/linux64/libComPYEPICS.so differ diff --git a/epics/clibs/linux64/libca.so b/epics/clibs/linux64/libca.so index 342e83e..5083bd4 100755 Binary files a/epics/clibs/linux64/libca.so and b/epics/clibs/linux64/libca.so differ diff --git a/epics/clibs/linuxarm/README b/epics/clibs/linuxarm/README new file mode 100644 index 0000000..5ebf9de --- /dev/null +++ b/epics/clibs/linuxarm/README @@ -0,0 +1,16 @@ + +These shared libraries were built using + +Epics Base 3.16.2 on Raspbian GNU/Linux 9 (stretch) with +gcc (Raspbian 6.3.0-18+rpi1) 6.3.0 20170516 + +The patchelf utility (from Anaconda Python) was used to make the shared +libraries portable with the following commands: + + mv libCom.so libComPYEPICS.so + patchelf --set-soname libca.so libca.so + patchelf --set-soname libComPYEPICS.so libComPYEPICS.so + patchelf --set-rpath '$ORIGIN' libca.so + patchelf --set-rpath '$ORIGIN' libComPYEPICS.so + patchelf --replace-needed libCom.so.3.16.2 libComPYEPICS.so libca.so + patchelf --replace-needed libCom.so libComPYEPICS.so libca.so diff --git a/epics/clibs/linuxarm/libComPYEPICS.so b/epics/clibs/linuxarm/libComPYEPICS.so new file mode 100755 index 0000000..b7864c8 Binary files /dev/null and b/epics/clibs/linuxarm/libComPYEPICS.so differ diff --git a/epics/clibs/linuxarm/libca.so b/epics/clibs/linuxarm/libca.so new file mode 100755 index 0000000..e4a04b7 Binary files /dev/null and b/epics/clibs/linuxarm/libca.so differ diff --git a/epics/clibs/win32/Com.dll b/epics/clibs/win32/Com.dll index b3aad7c..100a67d 100755 Binary files a/epics/clibs/win32/Com.dll and b/epics/clibs/win32/Com.dll differ diff --git a/epics/clibs/win32/README b/epics/clibs/win32/README new file mode 100644 index 0000000..3f57689 --- /dev/null +++ b/epics/clibs/win32/README @@ -0,0 +1,2 @@ +These shared libraries were taken from the automated nightly +build of Epics base 3.16, done on 12-Dec-2018. diff --git a/epics/clibs/win32/ca.dll b/epics/clibs/win32/ca.dll index 321a340..3fcc468 100755 Binary files a/epics/clibs/win32/ca.dll and b/epics/clibs/win32/ca.dll differ diff --git a/epics/clibs/win32/caRepeater.exe b/epics/clibs/win32/caRepeater.exe new file mode 100755 index 0000000..ca567d8 Binary files /dev/null and b/epics/clibs/win32/caRepeater.exe differ diff --git a/epics/clibs/win32/carepeater.exe b/epics/clibs/win32/carepeater.exe deleted file mode 100755 index c32bfdd..0000000 Binary files a/epics/clibs/win32/carepeater.exe and /dev/null differ diff --git a/epics/clibs/win64/Com.dll b/epics/clibs/win64/Com.dll index 03d6f6e..75ab4f7 100755 Binary files a/epics/clibs/win64/Com.dll and b/epics/clibs/win64/Com.dll differ diff --git a/epics/clibs/win64/README b/epics/clibs/win64/README new file mode 100644 index 0000000..3f57689 --- /dev/null +++ b/epics/clibs/win64/README @@ -0,0 +1,2 @@ +These shared libraries were taken from the automated nightly +build of Epics base 3.16, done on 12-Dec-2018. diff --git a/epics/clibs/win64/ca.dll b/epics/clibs/win64/ca.dll index fa0ab8a..3daf507 100755 Binary files a/epics/clibs/win64/ca.dll and b/epics/clibs/win64/ca.dll differ diff --git a/epics/clibs/win64/caRepeater.exe b/epics/clibs/win64/caRepeater.exe index 19f1edc..0c48f10 100755 Binary files a/epics/clibs/win64/caRepeater.exe and b/epics/clibs/win64/caRepeater.exe differ diff --git a/epics/dbr.py b/epics/dbr.py index df20c76..bf82bd6 100644 --- a/epics/dbr.py +++ b/epics/dbr.py @@ -10,6 +10,7 @@ This is mostly copied from CA header files """ import ctypes +import functools import os import sys import platform @@ -317,12 +318,25 @@ def cast_args(args): ctypes.cast(args.raw_dbr, ctypes.POINTER(args.count * Map[ftype])).contents ] -def make_callback(func, args): - """ make callback function""" - # note that ctypes.POINTER is needed for 64-bit Python on Windows - if PY64_WINDOWS: - args = ctypes.POINTER(args) - return ctypes.CFUNCTYPE(None, args)(func) + + +if PY64_WINDOWS: + def make_callback(func, args): + """ make callback function""" + # note that ctypes.POINTER is needed for 64-bit Python on Windows + @functools.wraps(func) + def wrapped(arg, **kwargs): + # On 64-bit Windows, `arg.contents` seems to be equivalent to other + # platforms' `arg` + if hasattr(arg, 'contents'): + return func(arg.contents, **kwargs) + return func(arg, **kwargs) + + return ctypes.CFUNCTYPE(None, ctypes.POINTER(args))(wrapped) +else: + def make_callback(func, args): + """ make callback function""" + return ctypes.CFUNCTYPE(None, args)(func) class event_handler_args(ctypes.Structure): @@ -339,11 +353,11 @@ class connection_args(ctypes.Structure): _fields_ = [('chid', chid_t), ('op', long_t)] + class access_rights_handler_args(ctypes.Structure): "access rights arguments" _fields_ = [('chid', chid_t), - ('read_access', uint_t, 1), - ('write_access', uint_t, 1)] + ('access', ubyte_t)] if PY64_WINDOWS and PY_MAJOR == 2: # need to add padding on 64-bit Windows for Python2 -- yuck! @@ -368,5 +382,4 @@ class access_rights_handler_args(ctypes.Structure): "access rights arguments" _fields_ = [('chid', chid_t), ('_pad_',ctypes.c_int8), - ('read_access', uint_t, 1), - ('write_access', uint_t, 1)] + ('access', ubyte_t)] diff --git a/epics/device.py b/epics/device.py index d30b5d3..dde8e70 100644 --- a/epics/device.py +++ b/epics/device.py @@ -8,6 +8,7 @@ from .ca import poll from .pv import get_pv import time + class Device(object): """A simple collection of related PVs, sharing a common prefix string for their names, but having many 'attributes'. @@ -124,14 +125,12 @@ def __init__(self, prefix='', attrs=None, if attrs is not None: for attr in attrs: - self.PV(attr, connect=False, - connection_timeout=timeout) + self.PV(attr, connect=False, timeout=timeout) if aliases: for attr in aliases.values(): if attrs is None or attr not in attrs: - self.PV(attr, connect=False, - connection_timeout=timeout) + self.PV(attr, connect=False, timeout=timeout) if with_poll: poll() @@ -183,10 +182,11 @@ def put(self, attr, value, wait=False, use_complete=False, timeout=10): return thispv.put(value, wait=wait, use_complete=use_complete, timeout=timeout) - def get(self, attr, as_string=False, count=None): + def get(self, attr, as_string=False, count=None, timeout=None): """get an attribute value, option as_string returns a string representation""" - return self.PV(attr).get(as_string=as_string, count=count) + return self.PV(attr).get(as_string=as_string, count=count, + timeout=timeout) def save_state(self): """return a dictionary of the values of all @@ -305,9 +305,9 @@ def __setattr__(self, attr, val): def __dir__(self): # there's no cleaner method to do this until Python 3.3 - all_attrs = set(self._aliases.keys() + self._pvs.keys() + - list(self._nonpvs) + - self.__dict__.keys() + dir(Device)) + all_attrs = set(list(self._aliases.keys()) + list(self._pvs.keys()) + + list(self._nonpvs) + + list(self.__dict__.keys()) + dir(Device)) return list(sorted(all_attrs)) def __repr__(self): diff --git a/epics/devices/xspress3.py b/epics/devices/xspress3.py index 108e647..d81d11b 100755 --- a/epics/devices/xspress3.py +++ b/epics/devices/xspress3.py @@ -2,7 +2,10 @@ import sys import os import time -from ConfigParser import ConfigParser +if sys.version[0] == '2': + from ConfigParser import ConfigParser +elif sys.version[0] == '3': + from configparser import ConfigParser from epics import Device, caget, caput, poll from epics.devices.mca import MCA, ROI, OrderedDict diff --git a/epics/motor.py b/epics/motor.py index 73073f5..072d517 100644 --- a/epics/motor.py +++ b/epics/motor.py @@ -2,7 +2,7 @@ """ This module provides support for the EPICS motor record. """ -# +# # Author: Mark Rivers / Matt Newville # Created: Sept. 16, 2002 # Modifications: @@ -18,13 +18,13 @@ # # Jun 14, 2010 MN # migrated more fully to pyepics3, using epics.Device -# +# # Jan 16, 2008 MN # use new EpicsCA.PV put-with-user-wait # wait() method no longer needed # added MotorException and MotorLimitException # many fixes to use newer python constructs -# +# # Aug 19, 2004 MN # 1. improved setting/checking of monitors on motor attributes # 2. add 'RTYP' and 'DTYP' to motor parameters. @@ -70,7 +70,7 @@ def __str__(self): class Motor(device.Device): """Epics Motor Class for pyepics3 - + This module provides a class library for the EPICS motor record. It uses the epics.Device and epics.PV classese @@ -79,12 +79,12 @@ class Motor(device.Device): These attributes do not appear in the dictionary for this class, but are implemented with the __getattr__ and __setattr__ methods. They simply get or putthe appropriate motor record fields. All attributes - can be both read and written unless otherwise noted. + can be both read and written unless otherwise noted. Attribute Description Field --------- ----------------------- ----- drive Motor Drive Value .VAL - readback Motor Readback Value .RBV (read-only) + readback Motor Readback Value .RBV (read-only) slew_speed Slew speed or velocity .VELO base_speed Base or starting speed .VBAS acceleration Acceleration time (sec) .ACCL @@ -97,11 +97,11 @@ class Motor(device.Device): backlash Backlash distance .BDST offset Offset from dial to user .OFF done_moving 1=Done, 0=Moving, read-only .DMOV - + Exceptions: The check_limits() method raises an 'MotorLimitException' if a soft limit or hard limit is detected. The move() method calls - check_limits() unless they are called with the + check_limits() unless they are called with the ignore_limits=True keyword set. Example use: @@ -130,7 +130,7 @@ class Motor(device.Device): # _extras = { 'disabled': '_able.VAL', } - + _alias = { 'acceleration': 'ACCL', 'back_accel': 'BACC', @@ -227,11 +227,14 @@ class Motor(device.Device): 'device_type': 'DTYP', 'record_type': 'RTYP', 'status': 'STAT'} - - _init_list = ('VAL', 'DESC', 'RTYP', 'RBV', 'PREC', 'TWV', 'FOFF') + + _init_list = ('VAL', 'DESC', 'RTYP', 'RBV', 'PREC', 'TWV', + 'FOFF', 'VELO', 'STAT', 'SET', 'LLM', 'HLM', + 'SPMG', 'LVIO', 'HLS', 'LLS', 'disabled') + _nonpvs = ('_prefix', '_pvs', '_delim', '_init', '_init_list', '_alias', '_extras') - + def __init__(self, name=None, timeout=3.0): if name is None: raise MotorException("must supply motor name") @@ -242,10 +245,9 @@ def __init__(self, name=None, timeout=3.0): name = name[:-1] self._prefix = name - device.Device.__init__(self, name, delim='.', + device.Device.__init__(self, name, delim='.', attrs=self._init_list, timeout=timeout) - # make sure this is really a motor! rectype = self.get('RTYP') if rectype != 'motor': @@ -254,8 +256,6 @@ def __init__(self, name=None, timeout=3.0): for key, val in self._extras.items(): pvname = "%s%s" % (name, val) self.add_pv(pvname, attr=key) - - # self.put('disabled', 0) self._callbacks = {} def __repr__(self): @@ -278,19 +278,19 @@ def __getattr__(self, attr): else: return self._pvs[attr] - + def __setattr__(self, attr, val): # print 'SET ATTR ', attr, val if attr in ('name', '_prefix', '_pvs', '_delim', '_init', '_alias', '_nonpvs', '_extra', '_callbacks'): self.__dict__[attr] = val - return + return if attr in self._alias: attr = self._alias[attr] if attr in self._pvs: return self.put(attr, val) - elif attr in self.__dict__: - self.__dict__[attr] = val + elif attr in self.__dict__: + self.__dict__[attr] = val elif self._init: try: self.PV(attr) @@ -298,6 +298,27 @@ def __setattr__(self, attr, val): except: raise MotorException("EpicsMotor has no attribute %s" % attr) + def put(self, attr, value, wait=False, use_complete=False, timeout=10): + """put a Motor attribute value, + optionally wait for completion or + up to a supplied timeout value + """ + if attr in self._alias: + attr = self._alias[attr] + thispv = self.PV(attr) + thispv.wait_for_connection() + return thispv.put(value, wait=wait, use_complete=use_complete, + timeout=timeout) + + def get(self, attr, as_string=False, count=None, timeout=None): + """get a Motor attribute value, + option as_string returns a string representation + """ + if attr in self._alias: + attr = self._alias[attr] + return self.PV(attr).get(as_string=as_string, count=count, + timeout=timeout) + def check_limits(self): """ check motor limits: returns None if no limits are violated @@ -308,7 +329,7 @@ def check_limits(self): if self.get(field) != 0: raise MotorLimitException(msg) return - + def within_limits(self, val, dial=False): """ returns whether a value for a motor is within drive limits with dial=True dial limits are used (default is user limits)""" @@ -345,7 +366,7 @@ def move(self, val=None, relative=False, wait=False, timeout=300.0, -3 : move-with-wait finished, hard limit violation seen 0 : move-with-wait finish OK. 0 : move-without-wait executed, not cpmfirmed - 1 : move-without-wait executed, move confirmed + 1 : move-without-wait executed, move confirmed 3 : move-without-wait finished, hard limit violation seen 4 : move-without-wait finished, soft limit violation seen @@ -381,7 +402,7 @@ def move(self, val=None, relative=False, wait=False, timeout=300.0, stat = self.put(drv, val, wait=wait, timeout=timeout) if stat is None: return UNCONNECTED - + if wait and stat == -1: # move started, exceeded timeout if self.get('DMOV') == 0: return TIMEOUT @@ -417,26 +438,26 @@ def get_position(self, dial=False, readback=False, step=False, raw=False): """ Returns the target or readback motor position in user, dial or step coordinates. - + Keywords: readback: Set readback=True to return the readback position in the desired coordinate system. The default is to return the drive position of the motor. - + dial: Set dial=True to return the position in dial coordinates. The default is user coordinates. - + raw (or step): Set raw=True to return the raw position in steps. The default is user coordinates. Notes: The "raw" or "step" and "dial" keywords are mutually exclusive. - The "readback" keyword can be used in user, dial or step + The "readback" keyword can be used in user, dial or step coordinates. - + Examples: m=epicsMotor('13BMD:m38') m.move(10) # Move to position 10 in user coordinates @@ -451,21 +472,21 @@ def get_position(self, dial=False, readback=False, step=False, raw=False): if readback: pos = rbv return self.get(pos) - + def tweak(self, direction='foreward', wait=False, timeout=300.0): """ move the motor by the tweak_val - + takes optional args: direction direction of motion (forward/reverse) [forward] must start with 'rev' or 'back' for a reverse tweak. wait wait for move to complete before returning (T/F) [F] timeout max time for move to complete (in seconds) [300] """ - + ifield = 'TWF' if direction.startswith('rev') or direction.startswith('back'): ifield = 'TWR' - + stat = self.put(ifield, 1, wait=wait, timeout=timeout) ret = stat if stat == 1: @@ -478,30 +499,30 @@ def tweak(self, direction='foreward', wait=False, timeout=300.0): ret = -1 return ret - + def set_position(self, position, dial=False, step=False, raw=False): """ Sets the motor position in user, dial or step coordinates. - + Inputs: position: The new motor position - + Keywords: dial: Set dial=True to set the position in dial coordinates. The default is user coordinates. - + raw: Set raw=True to set the position in raw steps. The default is user coordinates. - + Notes: The 'raw' and 'dial' keywords are mutually exclusive. - + Examples: m=epicsMotor('13BMD:m38') - m.set_position(10, dial=True) # Set the motor position to 10 in + m.set_position(10, dial=True) # Set the motor position to 10 in # dial coordinates m.set_position(1000, raw=True) # Set the motor position to 1000 steps """ @@ -517,10 +538,10 @@ def set_position(self, position, dial=False, step=False, raw=False): drv = 'RVAL' self.put(drv, position) - + # Put the motor back in "Use" mode self.put('SET', 0) - + def get_pv(self, attr): "return PV for a field" return self.PV(attr) @@ -557,7 +578,7 @@ def StopNow(self): def stop(self): "stop motor as soon as possible" self.STOP = 1 - + def make_step_list(self, minstep=0.0, maxstep=None, decades=10): """ create a reasonable list of motor steps, as for a dropdown menu The list is based on motor range Mand precision""" @@ -570,15 +591,15 @@ def make_step_list(self, minstep=0.0, maxstep=None, decades=10): if (step <= maxstep and step > 0.98*minstep): steplist.append(step) return steplist - + def get_info(self): "return information, current field values" out = {} - for attr in ('DESC', 'VAL', 'RBV', 'PREC', 'VELO', 'STAT', + for attr in ('DESC', 'VAL', 'RBV', 'PREC', 'VELO', 'STAT', 'SET', 'TWV','LLM', 'HLM', 'SPMG'): out[attr] = self.get(attr, as_string=True) return out - + def show_info(self): " show basic motor settings " ca.poll() @@ -613,7 +634,7 @@ def show_all(self): value = value + ' '*(18-min(18, len(value))) # print " %s %s %s" % (label, value, pvname) add(" %s %s %s" % (label, value, pvname)) - + ca.write("\n".join(out)) if (__name__ == '__main__'): diff --git a/epics/pv.py b/epics/pv.py index 78121bf..761743f 100755 --- a/epics/pv.py +++ b/epics/pv.py @@ -9,52 +9,154 @@ import time import ctypes import copy +import functools +import warnings from math import log10 from . import ca from . import dbr from .utils import is_string +try: + from types import SimpleNamespace as Namespace +except ImportError: + from argparse import Namespace + _PVcache_ = {} -def get_pv(pvname, form='time', connect=False, - context=None, timeout=5.0, **kws): - """get PV from PV cache or create one if needed. - Arguments - ========= - form PV form: one of 'native' (default), 'time', 'ctrl' - connect whether to wait for connection (default False) - context PV threading context (default None) - timeout connection timeout, in seconds (default 5.0) +def _ensure_context(func): + ''' + Wrapper that ensures a method is called in the correct CA context + + Assumes the instance has a `context` attribute + + Raises + ------ + RuntimeError + If the expected context (self.context) is unset (None), or the current + thread cannot get a valid context. Both conditions would normally + result in a segmentation fault if left unchecked. + ''' + @functools.wraps(func) + def wrapped(self, *args, **kwargs): + initial_context = ca.current_context() + expected_context = self.context + if expected_context is None: + raise RuntimeError('Expected CA context is unset') + elif expected_context == initial_context: + return func(self, *args, **kwargs) + + # If not using the expected context, switch to it here: + if initial_context is not None: + ca.detach_context() + ca.attach_context(expected_context) + try: + return func(self, *args, **kwargs) + finally: + # Then revert back to the initial calling context + if initial_context is not None: + ca.detach_context() + ca.attach_context(initial_context) + + return wrapped + + +def get_pv(pvname, form='time', connect=False, context=None, timeout=5.0, + connection_callback=None, access_callback=None, callback=None, + verbose=False, count=None, auto_monitor=None): + """ + Get a PV from PV cache or create one if needed. + + Parameters + --------- + form : str, optional + PV form: one of 'native', 'time' (default), 'ctrl' + connect : bool, optional + whether to wait for connection (default False) + context : int, optional + PV threading context (defaults to current context) + timeout : float, optional + connection timeout, in seconds (default 5.0) + connection_callback : callable, optional + Called upon connection with keyword arguments: pvname, conn, pv + access_callback : callable, optional + Called upon update to access rights with the following signature: + access_callback(read_access, write_access, pv=epics.PV) + callback : callable, optional + Called upon update to change of value. See `epics.PV.run_callback` for + further information regarding the signature. + count : int, optional + Number of values to request (0 or None means all available values) + verbose : bool, optional + Print additional messages relating to PV state + auto_monitor : bool or epics.dbr.DBE_ flags, optional + None: auto-monitor if count < ca.AUTOMONITOR_MAXLENGTH + False: do not auto-monitor + True: auto-monitor using ca.DEFAULT_SUBSCRIPTION_MASK + dbr.DBE_*: auto-monitor using this event mask. For example: + `epics.dbr.DBE_ALARM|epics.dbr.DBE_LOG` + + Returns + ------- + pv : epics.PV """ if form not in ('native', 'time', 'ctrl'): form = 'native' - thispv = None - if context is None: - context = ca.initial_context - if context is None: - context = ca.current_context() - if (pvname, form, context) in _PVcache_: - thispv = _PVcache_[(pvname, form, context)] + if context is not None: + warnings.warn( + 'The `context` kwarg for epics.get_pv() is deprecated. New PVs ' + 'will _not_ be created in the requested context.' + ) + else: + if ca.current_context() is None: + ca.use_initial_context() + context = ca.current_context() + + pvid = (pvname, form, context) + thispv = _PVcache_.get(pvid, None) - start_time = time.time() - # not cached -- create pv (automaticall saved to cache) if thispv is None: - thispv = PV(pvname, form=form, **kws) + if context != ca.current_context(): + raise RuntimeError('PV is not in cache for user-requested context') + + thispv = default_pv_class( + pvname, form=form, callback=callback, + connection_callback=connection_callback, + access_callback=access_callback, connection_timeout=timeout, + count=count, verbose=verbose, auto_monitor=auto_monitor) + + # Update the cache with this new instance: + _PVcache_[pvid] = thispv + else: + if connection_callback is not None: + if thispv.connected: + connection_callback(pvname=thispv.pvname, + conn=thispv.connected, pv=thispv) + thispv.connection_callbacks.append(connection_callback) + + if access_callback is not None: + if thispv.connected: + access_callback(thispv.read_access, thispv.write_access, + pv=thispv) + thispv.access_callbacks.append(access_callback) + + if callback is not None: + idx = thispv.add_callback(callback) + thispv.run_callback(idx) + + if auto_monitor and not thispv.auto_monitor: + # Start auto-monitoring, if not previously auto-monitoring: + thispv.auto_monitor = auto_monitor if connect: - thispv.wait_for_connection() - while not thispv.connected: - ca.poll() - if time.time()-start_time > timeout: - break - if not thispv.connected: + if not thispv.wait_for_connection(timeout=timeout): ca.write('cannot connect to %s' % pvname) return thispv + def fmt_time(tstamp=None): "simple formatter for time values" if tstamp is None: @@ -104,10 +206,12 @@ def __init__(self, pvname, callback=None, form='time', self.pvname = pvname.strip() self.form = form.lower() self.verbose = verbose - self.auto_monitor = auto_monitor + self._auto_monitor = auto_monitor self.ftype = None self.connected = False self.connection_timeout = connection_timeout + self._user_max_count = count + if self.connection_timeout is None: self.connection_timeout = ca.DEFAULT_CONNECTION_TIMEOUT self._args = {}.fromkeys(self._fields) @@ -127,13 +231,15 @@ def __init__(self, pvname, callback=None, form='time', self.access_callbacks = [access_callback] self.callbacks = {} + self._put_complete = None self._monref = None # holder of data returned from create_subscription + self._monref_mask = None self._conn_started = False if isinstance(callback, (tuple, list)): for i, thiscb in enumerate(callback): - if hasattr(thiscb, '__call__'): + if callable(thiscb): self.callbacks[i] = (thiscb, {}) - elif hasattr(callback, '__call__'): + elif callable(callback): self.callbacks[0] = (callback, {}) self.chid = None @@ -151,27 +257,26 @@ def __init__(self, pvname, callback=None, form='time', use_time= self.form == 'time') self._args['type'] = dbr.Name(self.ftype).lower() - pvid = (self.pvname, self.form, self.context) - if pvid not in _PVcache_: - _PVcache_[pvid] = self - + @_ensure_context def force_connect(self, pvname=None, chid=None, conn=True, **kws): if chid is None: chid = self.chid - if isinstance(chid, ctypes.c_long): + if hasattr(chid, 'value'): chid = chid.value self._args['chid'] = self.chid = chid self.__on_connect(pvname=pvname, chid=chid, conn=conn, **kws) - def force_read_access_rights(self): - """force a read of access rights, not relying + @_ensure_context + def force_read_access_rights(self): + """force a read of access rights, not relying on last event callback. - Note: event callback seems to fail sometimes, + Note: event callback seems to fail sometimes, at least on initial connection on Windows 64-bit. """ self._args['access'] = ca.access(self.chid) self._args['read_access'] = (1 == ca.read_access(self.chid)) self._args['write_access'] = (1 == ca.write_access(self.chid)) + @_ensure_context def __on_access_rights_event(self, read_access, write_access): self._args['read_access'] = read_access self._args['write_access'] = write_access @@ -184,12 +289,12 @@ def __on_access_rights_event(self, read_access, write_access): if callable(cb): cb(read_access, write_access, pv=self) + @_ensure_context def __on_connect(self, pvname=None, chid=None, conn=True): "callback for connection events" # occassionally chid is still None (ie if a second PV is created # while __on_connect is still pending for the first one.) # Just return here, and connection will happen later - t0 = time.time() if self.chid is None and chid is None: ca.poll(5.e-4) return @@ -204,40 +309,19 @@ def __on_connect(self, pvname=None, chid=None, conn=True): self._args['nelm'] = count # allow reduction of elements, via count argument - maxcount = 0 - if self._args['count'] is not None: - maxcount = self._args['count'] - count = min(count,self._args['count']) - - self._args['count'] = count - self._args['host'] = ca.host_name(self.chid) - self.ftype = ca.promote_type(self.chid, - use_ctrl= self.form == 'ctrl', - use_time= self.form == 'time') + self._args['count'] = min(count, self._user_max_count or count) + self._args['host'] = ca.host_name(self.chid) + self.ftype = ca.promote_type(self.chid, + use_ctrl= self.form == 'ctrl', + use_time= self.form == 'time') _ftype_ = dbr.Name(self.ftype).lower() self._args['type'] = _ftype_ self._args['typefull'] = _ftype_ self._args['ftype'] = dbr.Name(_ftype_, reverse=True) - if self.auto_monitor is None: - self.auto_monitor = count < ca.AUTOMONITOR_MAXLENGTH - if self._monref is None and self.auto_monitor: - # you can explicitly request a subscription mask - # (ie dbr.DBE_ALARM|dbr.DBE_LOG) by passing it as the - # auto_monitor arg, otherwise if you specify 'True' you'll - # just get the default set in ca.DEFAULT_SUBSCRIPTION_MASK - mask = None - if isinstance(self.auto_monitor, int): - mask = self.auto_monitor - self._monref = ca.create_subscription(self.chid, - use_ctrl=(self.form == 'ctrl'), - use_time=(self.form == 'time'), - callback=self.__on_changes, - mask=mask, count=maxcount) - for conn_cb in self.connection_callbacks: - if hasattr(conn_cb, '__call__'): + if callable(conn_cb): conn_cb(pvname=self.pvname, conn=conn, pv=self) elif not conn and self.verbose: ca.write("PV '%s' disconnected." % pvname) @@ -249,18 +333,97 @@ def __on_connect(self, pvname=None, chid=None, conn=True): # threads from thinking a connection is complete when it is actually # still in progress. self.connected = conn - return + if conn: + self._check_auto_monitor() + @_ensure_context + def _clear_auto_monitor_subscription(self): + 'Clear an auto-monitor subscription, if set' + if self._monref is None: + return + + cback, uarg, evid = self._monref + + self._monref = None + self._monref_mask = None + ca.clear_subscription(evid) + + @_ensure_context + def _check_auto_monitor(self): + ''' + Check the auto-monitor status + + Clears or adds monitor, if necessary. + ''' + if not self.connected or self.chid is None: + # Auto-monitor will be enabled (or toggled based on count) upon the + # next connection callback. + return + + count = self.count + if count is None: + return + + if self._auto_monitor is None: + self._auto_monitor = count < ca.AUTOMONITOR_MAXLENGTH + + if not self._auto_monitor: + # Turn off auto-monitoring, if necessary: + return self._clear_auto_monitor_subscription() + + mask = (ca.DEFAULT_SUBSCRIPTION_MASK + if self._auto_monitor is True + else self._auto_monitor) + + if self._monref is not None: + if self._monref_mask == mask: + # Same mask; no need to redo subscription + return + + # New mask. + self._clear_auto_monitor_subscription() + + self._monref_mask = mask + self._monref = ca.create_subscription( + self.chid, + use_ctrl=(self.form == 'ctrl'), + use_time=(self.form == 'time'), + callback=self.__on_changes, + mask=mask, + count=self._user_max_count or 0 + ) + + @property + def auto_monitor(self): + ''' + Whether auto_monitor is enabled or not. May be one of the following:: + + None: auto-monitor if count < ca.AUTOMONITOR_MAXLENGTH + False: do not auto-monitor + True: auto-monitor using ca.DEFAULT_SUBSCRIPTION_MASK + dbr.DBE_*: auto-monitor using this event mask. For example: + `epics.dbr.DBE_ALARM|epics.dbr.DBE_LOG` + ''' + return self._auto_monitor + + @auto_monitor.setter + @_ensure_context + def auto_monitor(self, value): + self._auto_monitor = value + self._check_auto_monitor() + + @property + def auto_monitor_mask(self): + 'The current mask in use for auto-monitoring' + return self._monref_mask + + @_ensure_context def wait_for_connection(self, timeout=None): """wait for a connection that started with connect() to finish""" - - # make sure we're in the CA context used to create this PV - if self.context != ca.current_context(): - ca.attach_context(self.context) if not self.connected: start_time = time.time() if not self._conn_started: - self.connect() + self.connect(timeout=timeout) if not self.connected: if timeout is None: @@ -269,6 +432,7 @@ def wait_for_connection(self, timeout=None): ca.poll() return self.connected + @_ensure_context def connect(self, timeout=None): "check that a PV is connected, forcing a connection if needed" if not self.connected: @@ -278,23 +442,20 @@ def connect(self, timeout=None): self._conn_started = True return self.connected and self.ftype is not None + @_ensure_context def clear_auto_monitor(self): - """turn off auto-monitoring: must reconnect to re-enable monitoring""" + """turn off auto-monitoring""" self.auto_monitor = False - if self._monref is not None: - evid = self._monref[2] - ca.clear_subscription(evid) - self._monref = None def reconnect(self): "try to reconnect PV" - self.auto_monitor = None - self._monref = None + self._clear_auto_monitor_subscription() self.connected = False self._conn_started = False self.force_connect() return self.wait_for_connection() + @_ensure_context def poll(self, evt=1.e-4, iot=1.0): "poll for changes" ca.poll(evt=evt, iot=iot) @@ -313,60 +474,152 @@ def get(self, count=None, as_string=False, as_numpy=True, monitor callback (True, default) or to make an explicit CA call for the value. - >>> p.get('13BMD:m1.DIR') + >>> get_pv('13BMD:m1.DIR').get() 0 - >>> p.get('13BMD:m1.DIR', as_string=True) + >>> get_pv('13BMD:m1.DIR').get(as_string=True) 'Pos' + + If the Channel Access status code sent by the IOC indicates a failure, + this method will raise the exception ChannelAccessGetFailure. + """ + data = self.get_with_metadata(count=count, as_string=as_string, + as_numpy=as_numpy, timeout=timeout, + with_ctrlvars=with_ctrlvars, + use_monitor=use_monitor) + return (data['value'] + if data is not None + else None) + + @_ensure_context + def get_with_metadata(self, count=None, as_string=False, as_numpy=True, + timeout=None, with_ctrlvars=False, form=None, + use_monitor=True, as_namespace=False): + """Returns a dictionary of the current value and associated metadata + + count explicitly limit count for array data + as_string flag(True/False) to get a string representation + of the value. + as_numpy flag(True/False) to use numpy array as the + return type for array data. + timeout maximum time to wait for value to be received. + (default = 0.5 + log10(count) seconds) + use_monitor flag(True/False) to use value from latest + monitor callback (True, default) or to make an + explicit CA call for the value. + form {'time', 'ctrl', None} optionally change the type of the + get request + as_namespace Change the return type to that of a namespace with + support for tab-completion + + >>> get_pv('13BMD:m1.DIR', form='time').get_with_metadata() + {'value': 0, 'status': 0, 'severity': 0} + >>> get_pv('13BMD:m1.DIR').get_with_metadata(form='ctrl') + {'value': 0, 'lower_ctrl_limit': 0, ...} + >>> get_pv('13BMD:m1.DIR').get_with_metadata(as_string=True) + {'value': 'Pos', 'status': 0, 'severity': 0} + >>> ns = get_pv('13BMD:m1.DIR').get_with_metadata(as_string=True, + as_namespace=True) + >>> ns + namespace(value='Pos', status=0, severity=0, ...) + >>> ns.status + 0 """ if not self.wait_for_connection(timeout=timeout): return None + + if form is None: + form = self.form + ftype = self.ftype + else: + ftype = ca.promote_type(self.chid, + use_ctrl=(form == 'ctrl'), + use_time=(form == 'time')) + if with_ctrlvars and getattr(self, 'units', None) is None: - self.get_ctrlvars() + if form != 'ctrl': + # ctrlvars will be updated as the get completes, since this + # metadata comes bundled with our DBR_CTRL* request. + pass + else: + self.get_ctrlvars() + + try: + cached_length = len(self._args['value']) + except TypeError: + cached_length = 1 if ((not use_monitor) or - (not self.auto_monitor) or - (self._args['value'] is None) or - (count is not None and count > len(self._args['value']))): + (not self.auto_monitor) or + (ftype != self.ftype) or + (self._args['value'] is None) or + (count is not None and count > cached_length)): # respect count argument on subscription also for calls to get if count is None and self._args['count']!=self._args['nelm']: count = self._args['count'] - ca_get = ca.get - if ca.get_cache(self.pvname)['value'] is not None: - ca_get = ca.get_complete - - self._args['value'] = ca_get(self.chid, ftype=self.ftype, - count=count, timeout=timeout, - as_numpy=as_numpy) - val = self._args['value'] + + # ca.get_with_metadata will handle multiple requests for the same + # PV internally, so there is no need to change between + # `get_with_metadata` and `get_complete_with_metadata` here. + md = ca.get_with_metadata( + self.chid, ftype=ftype, count=count, timeout=timeout, + as_numpy=as_numpy) + if md is None: + # Get failed. Indicate with a `None` as the return value + return + + # Update value and all included metadata. Depending on the PV + # form, this could include timestamp, alarm information, + # ctrlvars, and so on. + self._args.update(**md) + + if with_ctrlvars and form != 'ctrl': + # If the user requested ctrlvars and they were not included in + # the request, return all metadata. + md = self._args.copy() + + val = md['value'] + else: + md = self._args.copy() + val = self._args['value'] + if as_string: - return self._set_charval(val) - if self.count <= 1 or val is None: - return val + char_value = self._set_charval(val, force_long_string=as_string) + md['value'] = char_value + elif self.nelm <= 1 or val is None: + pass + else: + # After this point: + # * self.nelm is > 1 + # * val should be set and a sequence + try: + len(val) + except TypeError: + # Edge case where a scalar value leaks through ca.unpack() + val = [val] - # After this point: - # * self.count is > 1 - # * val should be set and a sequence - try: - len(val) - except TypeError: - # Edge case where a scalar value leaks through ca.unpack() - val = [val] + if count is None: + count = len(val) - if count is None: - count = len(val) - - if (as_numpy and ca.HAS_NUMPY and - not isinstance(val, ca.numpy.ndarray)): - val = ca.numpy.asarray(val) - elif (not as_numpy and ca.HAS_NUMPY and - isinstance(val, ca.numpy.ndarray)): - val = val.tolist() - # allow asking for less data than actually exists in the cached value - if count < len(val): - val = val[:count] - return val + if (as_numpy and ca.HAS_NUMPY and + not isinstance(val, ca.numpy.ndarray)): + val = ca.numpy.asarray(val) + elif (not as_numpy and ca.HAS_NUMPY and + isinstance(val, ca.numpy.ndarray)): + val = val.tolist() + + # allow asking for less data than actually exists in the cached value + if count < len(val): + val = val[:count] + + # Update based on the requested type: + md['value'] = val + if as_namespace: + return Namespace(**md) + return md + + @_ensure_context def put(self, value, wait=False, timeout=30.0, use_complete=False, callback=None, callback_data=None): """set value for PV, optionally waiting until the processing is @@ -387,18 +640,22 @@ def put(self, value, wait=False, timeout=30.0, if val == value: value = ival break - if use_complete and callback is None: - callback = self.__putCallbackStub + + def _put_callback(pvname=None, **kws): + self._put_complete = True + if callback is not None: + callback(pvname=pvname, **kws) + + self._put_complete = (False + if use_complete + else None) + return ca.put(self.chid, value, wait=wait, timeout=timeout, - callback=callback, + callback=_put_callback if use_complete or callback else None, callback_data=callback_data) - def __putCallbackStub(self, pvname=None, **kws): - "null put-calback, so that the put_complete attribute is valid" - pass - - def _set_charval(self, val, call_ca=True): + def _set_charval(self, val, call_ca=True, force_long_string=False): """ sets the character representation of the value. intended only for internal use""" if val is None: @@ -410,7 +667,8 @@ def _set_charval(self, val, call_ca=True): self._args['char_value'] = val return val # char waveform as string - if ntype == dbr.CHAR and self.count < ca.AUTOMONITOR_MAXLENGTH: + if ntype == dbr.CHAR and (self.count < ca.AUTOMONITOR_MAXLENGTH or + force_long_string is True): if ca.HAS_NUMPY and isinstance(val, ca.numpy.ndarray): # a numpy array val = val.tolist() @@ -469,21 +727,25 @@ def _set_charval(self, val, call_ca=True): self._args['char_value'] = cval return cval + @_ensure_context def get_ctrlvars(self, timeout=5, warn=True): "get control values for variable" if not self.wait_for_connection(): return None kwds = ca.get_ctrlvars(self.chid, timeout=timeout, warn=warn) - self._args.update(kwds) + if kwds is not None: + self._args.update(kwds) self.force_read_access_rights() return kwds + @_ensure_context def get_timevars(self, timeout=5, warn=True): "get time values for variable" if not self.wait_for_connection(): return None kwds = ca.get_timevars(self.chid, timeout=timeout, warn=warn) - self._args.update(kwds) + if kwds is not None: + self._args.update(kwds) return kwds @@ -504,6 +766,7 @@ def __on_changes(self, value=None, **kwd): self._args['char_value'], now)) self.run_callbacks() + @_ensure_context def run_callbacks(self): """run all user-defined callbacks with the current data @@ -514,6 +777,7 @@ def run_callbacks(self): for index in sorted(list(self.callbacks.keys())): self.run_callback(index) + @_ensure_context def run_callback(self, index): """run a specific user-defined callback, specified by index, with the current data @@ -533,7 +797,7 @@ def run_callback(self, index): kwd = copy.copy(self._args) kwd.update(kwargs) kwd['cb_info'] = (index, self) - if hasattr(fcn, '__call__'): + if callable(fcn): fcn(**kwd) def add_callback(self, callback=None, index=None, run_now=False, @@ -545,7 +809,7 @@ def add_callback(self, callback=None, index=None, run_now=False, Note that a PV may have multiple callbacks, so that each has a unique index (small integer) that is returned by add_callback. This index is needed to remove a callback.""" - if hasattr(callback, '__call__'): + if callable(callback): if index is None: index = 1 if len(self.callbacks) > 0: @@ -560,15 +824,20 @@ def add_callback(self, callback=None, index=None, run_now=False, self.run_callback(index) return index + @_ensure_context def remove_callback(self, index=None): """remove a callback by index""" if index in self.callbacks: self.callbacks.pop(index) ca.poll() - def clear_callbacks(self): + def clear_callbacks(self, with_access_callback=False, with_connect_callback=False): "clear all callbacks" - self.callbacks = {} + self.callbacks.clear() + if with_access_callback: + self.access_callbacks = [] + if with_connect_callback: + self.connection_callbacks = [] def _getinfo(self): "get information paragraph" @@ -631,8 +900,14 @@ def _getinfo(self): if len(self.callbacks) > 0: for nam in sorted(self.callbacks.keys()): cback = self.callbacks[nam][0] - out.append(' %s in file %s' % (cback.func_name, - cback.func_code.co_filename)) + cbname = getattr(cback, 'func_name', None) + if cbname is None: + cbname = getattr(cback, '__name__', repr(cback)) + cbcode = getattr(cback, 'func_code', None) + if cbcode is None: + cbcode = getattr(cback, '__code__', None) + cbfile = getattr(cbcode, 'co_filename', '?') + out.append(' %s in file %s' % (cbname, cbfile)) else: out.append(' PV is NOT internally monitored') out.append('=============================') @@ -663,7 +938,8 @@ def __setval__(self, val): @property def char_value(self): "character string representation of value" - return self._getarg('char_value') + self._getarg('char_value') # forces lookup of CTRL vars + return self._set_charval(self._getarg('value')) @property def status(self): @@ -695,12 +971,13 @@ def count(self): return self._getarg('count') @property + @_ensure_context def nelm(self): """native count (number of elements). For array data this will return the full array size (ie, the .NELM field). See also 'count' property""" - if self._getarg('count') == 1: - return 1 + # if self._getarg('count') == 1: + # return 1 return ca.element_count(self.chid) @property @@ -801,11 +1078,8 @@ def info(self): @property def put_complete(self): - "returns True if a put-with-wait has completed" - putdone_data = ca._put_done.get(self.pvname, None) - if putdone_data is not None: - return putdone_data[0] - return True + "returns True if the last put-with-wait has completed" + return self._put_complete def __repr__(self): "string representation" @@ -825,6 +1099,7 @@ def __eq__(self, other): except AttributeError: return False + @_ensure_context def disconnect(self): "disconnect PV" self.connected = False @@ -834,24 +1109,20 @@ def disconnect(self): if pvid in _PVcache_: _PVcache_.pop(pvid) - if self._monref is not None: - cback, uarg, evid = self._monref - ctx = ca.current_context() - if self.pvname in ca._cache[ctx]: + cache_item = ca._cache[ctx].pop(self.pvname, None) + if cache_item is not None: + if self._monref is not None: # atexit may have already cleared the subscription - ca.clear_subscription(evid) - ca._cache[ctx].pop(self.pvname) - del cback - del uarg - del evid - try: - self._monref = None - self._args = {}.fromkeys(self._fields) - except: - pass + self._clear_auto_monitor_subscription() + + # TODO: clear channel should be called as well + # ca.clear_channel(cache_item.chid) + self._monref = None + self._monref_mask = None + self.clear_callbacks(True, True) + self._args = {}.fromkeys(self._fields) ca.poll(evt=1.e-3, iot=1.0) - self.callbacks = {} def __del__(self): if getattr(ca, 'libca', None) is None: @@ -861,3 +1132,7 @@ def __del__(self): self.disconnect() except: pass + + +# Allow advanced users to customize the class of PV that `get_pv` would return: +default_pv_class = PV diff --git a/epics/utils.py b/epics/utils.py index c5a5e28..263f9ca 100644 --- a/epics/utils.py +++ b/epics/utils.py @@ -49,9 +49,14 @@ def clib_search_path(lib): try: import platform nbits = platform.architecture()[0] + mach = platform.machine() except: nbits = '32bit' + mach = 'x86_64' + nbits = nbits.replace('bit', '') + if mach.startswith('arm'): + nbits = 'arm' libfmt = 'lib%s.so' if os.name == 'nt': @@ -62,6 +67,7 @@ def clib_search_path(lib): libfmt = 'lib%s.dylib' elif sys.platform.startswith('linux'): libsrc = 'linux' + else: return None diff --git a/epics/wx/motordetailframe.py b/epics/wx/motordetailframe.py index 2c5f7c0..4bd519b 100755 --- a/epics/wx/motordetailframe.py +++ b/epics/wx/motordetailframe.py @@ -11,7 +11,7 @@ DelayedEpicsCallback, EpicsFunction) -from utils import set_sizer, LCEN, RCEN, CEN, FileSave +from .utils import set_sizer, LCEN, RCEN, CEN, FileSave TMPL_TOP = '''file "$(CARS)/CARSApp/Db/motor.db" { @@ -38,7 +38,7 @@ class MotorDetailFrame(wx.Frame): __motor_fields = ('SET', 'LLM', 'HLM', 'LVIO', 'TWV', 'HLS', 'LLS') def __init__(self, parent=None, motor=None): - wx.Frame.__init__(self, parent, wx.ID_ANY, size=MAINSIZE, + wx.Frame.__init__(self, parent, wx.ID_ANY, size=MAINSIZE, style=wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL) self.motor = motor @@ -101,12 +101,12 @@ def _onCopyTemplate(self, event=None): wx.TheClipboard.Open() wx.TheClipboard.SetData(dat) wx.TheClipboard.Close() - + class MotorDetailPanel(ScrolledPanel): """ Detailed Motor Setup Panel""" __motor_fields = ('SET', 'LLM', 'HLM', 'LVIO', 'TWV', 'HLS', 'LLS') - + def __init__(self, parent=None, motor=None): ScrolledPanel.__init__(self, parent, size=MAINSIZE, name='', style=wx.EXPAND|wx.GROW|wx.TAB_TRAVERSAL) @@ -122,16 +122,16 @@ def __init__(self, parent=None, motor=None): dp = wx.Panel(self) ds.Add(xLabel(dp, 'Label'), (0, 0), (1, 1), LCEN, 5) - ds.Add(self.MotorTextCtrl(dp, 'DESC', size=(180, -1)), + ds.Add(self.MotorTextCtrl(dp, 'DESC', size=(180, -1)), (0, 1), (1, 1), LCEN, 5) - + ds.Add(xLabel(dp, 'units'), (0, 2), (1, 1), LCEN, 5) - ds.Add(self.MotorTextCtrl(dp, 'EGU', size=(90, -1)), + ds.Add(self.MotorTextCtrl(dp, 'EGU', size=(90, -1)), (0, 3), (1, 1), LCEN, 5) ds.Add(xLabel(dp, "Precision"), (0, 4), (1, 1), LCEN, 5) ds.Add(self.MotorCtrl(dp, 'PREC', size=(30, -1)), (0, 5), (1, 1), CEN) - set_sizer(dp, ds) + set_sizer(dp, ds) sizer.Add(dp, 0) sizer.Add((3, 3), 0) @@ -387,7 +387,7 @@ def OnMotorEvent(self, pvname=None, field=None, **kws): def MotorCtrl(self, panel, attr, size=(80, -1)): "PVFloatCtrl for a Motor attribute" - return PVFloatCtrl(panel, size=size, + return PVFloatCtrl(panel, size=size, precision= self.motor.PREC, pv=self.motor.PV(attr), style = wx.TE_RIGHT) diff --git a/epics/wx/motorpanel.py b/epics/wx/motorpanel.py index fe4d75c..ecceef0 100755 --- a/epics/wx/motorpanel.py +++ b/epics/wx/motorpanel.py @@ -10,6 +10,7 @@ """ # Aug 21 2004 M Newville: initial working version. # +import six import wx try: from wx._core import PyDeadObjectError @@ -39,7 +40,7 @@ def __init__(self, parent, motor=None, psize='full', wx.Panel.__init__(self, parent, style=wx.TAB_TRAVERSAL) self.parent = parent - if hasattr(messenger, '__call__'): + if callable(messenger): self.__messenger = messenger self.format = None @@ -75,7 +76,7 @@ def SelectMotor(self, motor): except PyDeadObjectError: return - if isinstance(motor, (str, unicode)): + if isinstance(motor, six.string_types): self.motor = epics.Motor(motor) elif isinstance(motor, epics.Motor): self.motor = motor diff --git a/epics/wx/ogllib.py b/epics/wx/ogllib.py index ca52f03..ffa877a 100644 --- a/epics/wx/ogllib.py +++ b/epics/wx/ogllib.py @@ -8,19 +8,19 @@ """ import wx.lib.ogl as ogl -from wxlib import PVMixin +from .wxlib import PVMixin class PVShapeMixin(PVMixin): """ Mixin for any Shape that has PV callback support """ - def __init__(self, pv=None, pvname=None): + def __init__(self, pv=None, pvname=None): PVMixin.__init__(self, pv, pvname) self.brushTranslations = {} self.penTranslations = {} self.shownTranslations = {} - + def SetBrushTranslations(self, translations): """ Set a dictionary of value->brush translations that will be set automatically @@ -42,7 +42,7 @@ def SetPenTranslations(self, translations): """ self.penTranslations = translations - + def SetShownTranslations(self, translations): """ @@ -57,10 +57,10 @@ def OnPVChange(self, raw_value): """ Do not override this method, override PVChanged if you would like to do any custom callback behaviour - + """ if raw_value in self.brushTranslations: - self.SetBrush(self.brushTranslations[raw_value]) + self.SetBrush(self.brushTranslations[raw_value]) if raw_value in self.penTranslations: self.SetPen(self.penTranslations[raw_value]) if raw_value in self.shownTranslations: @@ -80,22 +80,22 @@ def PVChanged(self, raw_value): def Invalidate(self): - """ + """ Invalidate the shape's area on the parent shape canvas to cause a redraw (convenience method) """ - (w, h) = self.GetBoundingBoxMax() + (w, h) = self.GetBoundingBoxMax() x = self.GetX() y = self.GetY() self.GetCanvas().RefreshRect((x-w/2, y-h/2, w, h)) - - + + class PVRectangle(ogl.RectangleShape, PVShapeMixin): """ A RectangleShape which is associated with a particular PV value - - """ + + """ def __init__(self, w, h, pv=None, pvname=None): ogl.RectangleShape.__init__(self, w, h) PVShapeMixin.__init__(self, pv, pvname) @@ -108,4 +108,3 @@ class PVCircle(ogl.CircleShape, PVShapeMixin): def __init__(self, diameter, pv=None, pvname=None): ogl.CircleShape.__init__(self, diameter) PVShapeMixin.__init__(self, pv, pvname) - diff --git a/epics/wx/utils.py b/epics/wx/utils.py index 51632a4..3185123 100755 --- a/epics/wx/utils.py +++ b/epics/wx/utils.py @@ -9,9 +9,15 @@ import os import array -from string import maketrans +import six +if six.PY3: + maketrans = str.maketrans +else: + from string import maketrans -import fpformat +BAD_FILECHARS = ';~,`!%$@$&^?*#:"/|\'\\\t\r\n (){}[]<>' +GOOD_FILECHARS = '_'*len(BAD_FILECHARS) +TRANS_FILE = maketrans(BAD_FILECHARS, GOOD_FILECHARS) HAS_NUMPY = False try: @@ -82,7 +88,7 @@ def pack(window, sizer): def add_button(parent, label, size=(-1, -1), action=None): "add simple button with bound action" thisb = wx.Button(parent, label=label, size=size) - if hasattr(action, '__call__'): + if callable(action): thisb.Bind(wx.EVT_BUTTON, action) return thisb @@ -90,7 +96,7 @@ def add_menu(parent, menu, label='', text='', action=None): "add submenu" wid = wx.NewId() menu.Append(wid, label, text) - if hasattr(action, '__call__'): + if callable(action): parent.Bind(wx.EVT_MENU, action, id=wid) def popup(parent, message, title, style=None): @@ -115,8 +121,7 @@ def fix_filename(fname): fix string to be a 'good' filename. This may be a more restrictive than the OS, but avoids nasty cases. """ - bchars = ' <>:"\'\\\t\r\n/|?*!%$' - out = fname.translate(maketrans(bchars, '_'*len(bchars))) + out = str(s).translate(TRANS_FILE) if out[0] in '-,;[]{}()~`@#': out = '_%s' % out return out @@ -210,7 +215,7 @@ def __init__(self, func=None, *args, **kws): def __call__(self, *args, **kws): self.kws.update(kws) - if hasattr(self.func, '__call__'): + if callable(self.func): self.args = args return self.func(*self.args, **self.kws) @@ -271,7 +276,7 @@ def __init__(self, parent, value='', minval=None, maxval=None, def SetAction(self, action, **kws): "set callback action" - if hasattr(action,'__call__'): + if callable(action): self.__action = Closure(action, **kws) def SetPrecision(self, prec=0): @@ -303,7 +308,7 @@ def SetValue(self, value=None, act=True): if value is not None: wx.TextCtrl.SetValue(self, self.format % value) - if self.is_valid and hasattr(self.__action, '__call__') and act: + if self.is_valid and callable(self.__action) and act: self.__action(value=self.__val) elif not self.is_valid and self.bell_on_invalid: wx.Bell() @@ -313,7 +318,7 @@ def SetValue(self, value=None, act=True): def OnKillFocus(self, event): "focus lost" self.__GetMark() - if self.act_on_losefocus and hasattr(self.__action, '__call__'): + if self.act_on_losefocus and callable(self.__action): self.__action(value=self.__val) event.Skip() @@ -366,7 +371,7 @@ def OnText(self, event=None): def GetValue(self): if self.__prec > 0: - return set_float(fpformat.fix(self.__val, self.__prec)) + return set_float("%%.%ig" % (self.__prec) % (self.__val)) else: return int(self.__val) diff --git a/epics/wx/wxlib.py b/epics/wx/wxlib.py index 63aaa11..d7197b2 100644 --- a/epics/wx/wxlib.py +++ b/epics/wx/wxlib.py @@ -7,13 +7,19 @@ except: PyDeadObjectError = Exception +import six import time import sys import epics import wx.lib.buttons as buttons import wx.lib.agw.floatspin as floatspin -from utils import Closure, FloatCtrl, set_float +try: + from wx._core import PyDeadObjectError +except: + PyDeadObjectError = Exception + +from .utils import Closure, FloatCtrl, set_float def EpicsFunction(f): """decorator to wrap function in a wx.CallAfter() so that @@ -166,7 +172,7 @@ def SetPV(self, pv=None): return if isinstance(pv, epics.PV): self.pv = pv - elif isinstance(pv, (str, unicode)): + elif isinstance(pv, six.string_types): form = "ctrl" if len(self._fg_colour_alarms) > 0 or len(self._bg_colour_alarms) > 0 else "native" self.pv = epics.get_pv(pv, form=form) self.pv.connect() @@ -563,7 +569,9 @@ class PVStaticText(wx.StaticText, PVMixin): This can be overriden or disabled as constructor parameters """ - def __init__(self, parent, pv=None, style=None, **kw): + def __init__(self, parent, pv=None, style=None, + minor_alarm="DARKRED", major_alarm="RED", + invalid_alarm="ORANGERED", **kw): wstyle = wx.ALIGN_LEFT if style is not None: wstyle = style @@ -571,6 +579,11 @@ def __init__(self, parent, pv=None, style=None, **kw): wx.StaticText.__init__(self, parent, wx.ID_ANY, label='', style=wstyle, **kw) PVMixin.__init__(self, pv=pv) + self._fg_colour_alarms = { + epics.MINOR_ALARM : minor_alarm, + epics.MAJOR_ALARM : major_alarm, + epics.INVALID_ALARM : invalid_alarm } + def _SetValue(self, value): "set widget label" @@ -603,7 +616,7 @@ def SetPV(self, pv=None): return if isinstance(pv, epics.PV): self.pv = pv - elif isinstance(pv, (str, unicode)): + elif isinstance(pv, six.string_types): self.pv = epics.get_pv(pv) self.pv.connect() @@ -668,7 +681,7 @@ def SetPV(self, pv=None): return if isinstance(pv, epics.PV): self.pv = pv - elif isinstance(pv, (str, unicode)): + elif isinstance(pv, six.string_types): self.pv = epics.get_pv(pv) self.pv.connect() @@ -683,8 +696,8 @@ def SetPV(self, pv=None): self.Bind(wx.EVT_CHOICE, self._onChoice) - pv_value = pv.get(as_string=True) - enum_strings = pv.enum_strs + pv_value = self.pv.get(as_string=True) + enum_strings = self.pv.enum_strs self.Clear() self.AppendItems(enum_strings) @@ -752,7 +765,7 @@ def SetPV(self, pv=None): "set pv, either an epics.PV object or a pvname" if isinstance(pv, epics.PV): self.pv = pv - elif isinstance(pv, (str, unicode)): + elif isinstance(pv, six.string_types): self.pv = epics.get_pv(pv) if self.pv is None: return @@ -873,7 +886,7 @@ def _SetValue(self, value): self.Value = (value == self.on_value) else: self.Value = bool(self.pv.get()) - if hasattr(self.OnChange, '__call__'): + if callable(self.OnChange): self.OnChange(self) @EpicsFunction @@ -1008,7 +1021,7 @@ def __init__(self, parent, pv=None, pushValue=1, PVCtrlMixin.__init__(self, pv=pv, font="", fg=None, bg=None) self.pushValue = pushValue self.Bind(wx.EVT_BUTTON, self.OnPress) - if isinstance(disablePV, (str, unicode)): + if isinstance(disablePV, six.string_types): disablePV = epics.get_pv(disablePV) disablePV.connect() self.disablePV = disablePV @@ -1197,7 +1210,8 @@ class PVCollapsiblePane(wx.CollapsiblePane, PVCtrlMixin): from a PV value """ - def __init__(self, parent, pv=None, minor_alarm="DARKRED", major_alarm="RED", invalid_alarm="ORANGERED", **kw): + def __init__(self, parent, pv=None, minor_alarm="DARKRED", major_alarm="RED", + invalid_alarm="ORANGERED", **kw): wx.CollapsiblePane.__init__(self, parent, **kw) PVCtrlMixin.__init__(self, pv=pv, font=None, fg=None, bg=None) self._fg_colour_alarms = { diff --git a/make_wininsts.bat b/make_wininsts.bat deleted file mode 100644 index c0c7653..0000000 --- a/make_wininsts.bat +++ /dev/null @@ -1,10 +0,0 @@ -SET PATH=C:\Python26;%PATH% -python setup.py bdist_wininst --target-version=2.6 - -SET PATH=C:\Python27;%PATH% -python setup.py bdist_wininst --target-version=2.7 - -SET PATH=C:\Python32;%PATH% -python setup.py bdist_wininst --target-version=3.2 - - diff --git a/publish_docs.sh b/publish_docs.sh deleted file mode 100644 index c5577df..0000000 --- a/publish_docs.sh +++ /dev/null @@ -1,69 +0,0 @@ -installdir='/www/apache/htdocs/software/python/pyepics3' -docbuild='doc/_build' - -cd doc -echo '# Making docs' -make all -cd ../ - -echo '# Building tarball of docs' -mkdir _tmpdoc -cp -pr doc/pyepics.pdf _tmpdoc/pyepics.pdf -cp -pr doc/_build/html/* _tmpdoc/. -cd _tmpdoc -tar czf ../../pyepics_docs.tar.gz . -cd .. -rm -rf _tmpdoc - -# - -echo "# Switching to gh-pages branch" -git checkout gh-pages - -if [ $? -ne 0 ] ; then - echo ' failed.' - exit -fi - -echo "# Make sure this script is updated!" -git checkout master publish_docs.sh -if [ $? -ne 0 ] ; then - echo ' failed.' - exit -fi - -tar xzf ../pyepics_docs.tar.gz . - -echo "# commit changes to gh-pages branch" -git commit -am "changed docs" - -if [ $? -ne 0 ] ; then - echo ' failed.' - exit -fi - -echo "# Pushing docs to github" -git push - - -echo "# switch back to master branch" -git checkout master - -if [ $? -ne 0 ] ; then - echo ' failed.' - exit -fi - -# install locally -echo "# Installing docs to CARS web pages" -cp ../pyepics_docs.tar.gz $installdir/../. - -cd $installdir -if [ $? -ne 0 ] ; then - echo ' failed.' - exit -fi - -tar xzf ../pyepics_docs.tar.gz . - - diff --git a/publish_tarballs.sh b/publish_tarballs.sh deleted file mode 100644 index 81b1143..0000000 --- a/publish_tarballs.sh +++ /dev/null @@ -1,12 +0,0 @@ -installdir='/www/apache/htdocs/software/python/pyepics3' - -srcdir=$installdir/src - -# -mv $srcdir/pyepics* $srcdir/older/. -cp -pr Changelog INSTALL README.txt $srcdir/. -cp -pr dist/* $srcdir/. - -echo 'use python setup.py sdist upload to upload to PyPI' -echo 'then run upload_wininst.bat from Windows to upload Win installers' - diff --git a/scripts/Motor_Display.py b/scripts/Motor_Display.py index f6f0cd6..24ed3ae 100755 --- a/scripts/Motor_Display.py +++ b/scripts/Motor_Display.py @@ -6,14 +6,10 @@ import sys import time import epics - -from epics.wx import finalize_epics, MotorPanel - -ID_ABOUT = wx.NewId() -ID_EXIT = wx.NewId() -ID_FREAD = wx.NewId() -ID_FSAVE = wx.NewId() -ID_CONF = wx.NewId() +from mpanel import MotorPanel +from epics import Motor +from epics.wx import finalize_epics +from epics.wx.utils import add_menu class SimpleMotorFrame(wx.Frame): def __init__(self, parent=None, motors=None, *args,**kwds): @@ -22,57 +18,50 @@ def __init__(self, parent=None, motors=None, *args,**kwds): wx.DefaultPosition, wx.Size(-1,-1),**kwds) self.SetTitle(" Epics Motors Page") - wx.EVT_CLOSE(self, self.onClose) + self.Bind(wx.EVT_CLOSE, self.onClose) self.SetFont(wx.Font(12,wx.SWISS,wx.NORMAL,wx.BOLD,False)) - + self.xtimer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.onTimer, self.xtimer) self.createSbar() self.createMenus() - self.buildFrame(motors=motors) + motorlist = None + if motors is not None: + motorlist = [Motor(mname) for mname in motors] + self.buildFrame(motors=motorlist) + self.xtimer.Start(250) + + def onTimer(self, evt=None): + pass # print(" tick ", time.ctime()) def buildFrame(self, motors=None): self.mainsizer = wx.BoxSizer(wx.VERTICAL) - if motors is not None: - self.motors= [MotorPanel(self, motor=m) for m in motors] - - for mpan in self.motors: - self.mainsizer.Add(mpan, 1, wx.EXPAND) - self.mainsizer.Add(wx.StaticLine(self, size=(100,3)), - 0, wx.EXPAND) - + self.motors = [MotorPanel(self, motor=m) for m in motors] + + for mpan in self.motors: + self.mainsizer.Add(mpan, 1, wx.EXPAND) + self.mainsizer.Add(wx.StaticLine(self, size=(100,3)), + 0, wx.EXPAND) + self.SetSizer(self.mainsizer) self.mainsizer.Fit(self) self.Refresh() def createMenus(self): + mbar = wx.MenuBar() fmenu = wx.Menu() - fmenu.Append(ID_ABOUT, "&About", - "More information about this program") - fmenu.Append(ID_FREAD, "&Read", "Read Configuration File") - fmenu.Append(ID_FSAVE, "&Save", "Save Configuration File") - fmenu.AppendSeparator() - fmenu.Append(ID_EXIT, "E&xit", "Terminate the program") - - cmenu = wx.Menu() - cmenu.Append(ID_CONF, "&Configure", - "Setup Motors and options") - - menuBar = wx.MenuBar() - menuBar.Append(fmenu, "&File"); - - # menuBar.Append(cmenu, "&Configure"); - self.SetMenuBar(menuBar) - - wx.EVT_MENU(self, ID_ABOUT, self.onAbout) - wx.EVT_MENU(self, ID_EXIT, self.onClose) + add_menu(self, fmenu, "E&xit", "Terminate the program", + action=self.onClose) + mbar.Append(fmenu, "&File") + self.SetMenuBar(mbar) def createSbar(self): "create status bar" - self.statusbar = self.CreateStatusBar(2, wx.CAPTION|wx.THICK_FRAME) + self.statusbar = self.CreateStatusBar(2, wx.CAPTION) self.statusbar.SetStatusWidths([-4,-1]) for index, name in enumerate(("Messages", "Status")): self.statusbar.SetStatusText('', index) - + def write_message(self,text,status='normal'): self.SetStatusText(text) @@ -91,14 +80,18 @@ def onClose(self, event): self.Destroy() if __name__ == '__main__': - motors =('13XRM:m2.VAL',) + motors =('13XRM:m1.VAL', + '13XRM:m2.VAL', + '13XRM:m3.VAL', + '13XRM:m4.VAL', + '13XRM:m5.VAL', + '13XRM:m6.VAL') if len(sys.argv)>1: motors = sys.argv[1:] - + app = wx.App(redirect=False) SimpleMotorFrame(motors=motors).Show() - - app.MainLoop() - + print("App ", time.ctime()) + app.MainLoop() diff --git a/scripts/mpanel.py b/scripts/mpanel.py new file mode 100644 index 0000000..ee42d57 --- /dev/null +++ b/scripts/mpanel.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python +# +""" +provides two classes: + MotorPanel: a wx panel for an Epics Motor, ala medm Motor row + + makes use of these modules + wxlib: extensions of wx.TextCtrl, etc for epics PVs + Motor: Epics Motor class +""" +# Aug 21 2004 M Newville: initial working version. +# +import wx +try: + from wx._core import PyDeadObjectError +except: + PyDeadObjectError = Exception + +import six +import epics +from epics.wx.wxlib import PVText, PVFloatCtrl, PVButton, PVComboBox, \ + DelayedEpicsCallback, EpicsFunction + +from epics.wx.motordetailframe import MotorDetailFrame + +from epics.wx.utils import LCEN, RCEN, CEN, LTEXT, RIGHT, pack, add_button + +from larch.utils import debugtime + +class MotorPanel(wx.Panel): + """ MotorPanel a simple wx windows panel for controlling an Epics Motor + + use psize='full' (defaiult) for full capabilities, or + 'medium' or 'small' for minimal version + """ + __motor_fields = ('SET', 'disabled', 'LLM', 'HLM', 'LVIO', 'TWV', + 'HLS', 'LLS', 'SPMG', 'DESC') + + def __init__(self, parent, motor=None, psize='full', + messenger=None, prec=None, **kw): + + wx.Panel.__init__(self, parent, style=wx.TAB_TRAVERSAL) + self.parent = parent + + if hasattr(messenger, '__call__'): + self.__messenger = messenger + + self.format = None + if prec is not None: + self.format = "%%.%if" % prec + + self.motor = None + self._size = 'full' + if psize in ('medium', 'small'): + self._size = psize + self.__sizer = wx.BoxSizer(wx.HORIZONTAL) + self.CreatePanel() + + if motor is not None: + try: + self.SelectMotor(motor) + except PyDeadObjectError: + pass + + @EpicsFunction + def SelectMotor(self, motor): + " set motor to a named motor PV" + if motor is None: + return + dt = debugtime() + epics.poll() + try: + if self.motor is not None: + for i in self.__motor_fields: + self.motor.clear_callback(attr=i) + except PyDeadObjectError: + return + dt.add('clear callbacks') + + if isinstance(motor, six.string_types): + self.motor = epics.Motor(motor) + dt.add('create motor (name)') + elif isinstance(motor, epics.Motor): + self.motor = motor + dt.add('create motor (motor)') + self.motor.get_info() + dt.add('get motor info') + + if self.format is None: + self.format = "%%.%if" % self.motor.PREC + self.FillPanel() + dt.add('fill panel') + for attr in self.__motor_fields: + self.motor.get_pv(attr).add_callback(self.OnMotorEvent, + wid=self.GetId(), + field=attr) + dt.add('add callbacks %i attrs ' % (len(self.__motor_fields))) + + if self._size == 'full': + self.SetTweak(self.format % self.motor.TWV) + # dt.show() + + + @EpicsFunction + def FillPanelComponents(self): + epics.poll() + try: + if self.motor is None: + return + except PyDeadObjectError: + return + + self.drive.SetPV(self.motor.PV('VAL')) + self.rbv.SetPV(self.motor.PV('RBV')) + self.desc.SetPV(self.motor.PV('DESC')) + + descpv = self.motor.PV('DESC').get() + self.desc.Wrap(45) + if self._size == 'full': + self.twf.SetPV(self.motor.PV('TWF')) + self.twr.SetPV(self.motor.PV('TWR')) + elif len(descpv) > 20: + font = self.desc.GetFont() + font.PointSize -= 1 + self.desc.SetFont(font) + + self.info.SetLabel('') + for f in ('SET', 'LVIO', 'SPMG', 'LLS', 'HLS', 'disabled'): + uname = self.motor.PV(f).pvname + wx.CallAfter(self.OnMotorEvent, + pvname=uname, field=f) + + def CreatePanel(self): + " build (but do not fill in) panel components" + wdesc, wrbv, winfo, wdrv = 200, 105, 90, 120 + if self._size == 'medium': + wdesc, wrbv, winfo, wdrv = 140, 85, 80, 100 + elif self._size == 'small': + wdesc, wrbv, winfo, wdrv = 50, 60, 25, 80 + + self.desc = PVText(self, size=(wdesc, 25), style=LTEXT) + self.desc.SetForegroundColour("Blue") + font = self.desc.GetFont() + font.PointSize -= 1 + self.desc.SetFont(font) + + self.rbv = PVText(self, size=(wrbv, 25), fg='Blue', style=RCEN) + self.info = wx.StaticText(self, label='', + size=(winfo, 25), style=RCEN) + self.info.SetForegroundColour("Red") + + self.drive = PVFloatCtrl(self, size=(wdrv, -1), style = wx.TE_RIGHT) + + try: + self.FillPanelComponents() + except PyDeadObjectError: + return + + spacer = wx.StaticText(self, label=' ', size=(5, 5), style=RIGHT) + if self._size != 'small': + self.__sizer.AddMany([(spacer, 0, CEN)]) + + self.__sizer.AddMany([ (self.desc, 1, LCEN), + (self.info, 0, CEN), + (self.rbv, 0, CEN), + (self.drive, 0, CEN)]) + + if self._size == 'full': + self.twk_list = ['',''] + self.__twkbox = wx.ComboBox(self, value='', size=(100, -1), + choices=self.twk_list, + style=wx.CB_DROPDOWN|wx.TE_PROCESS_ENTER) + self.__twkbox.Bind(wx.EVT_COMBOBOX, self.OnTweakBoxComboEvent) + self.__twkbox.Bind(wx.EVT_TEXT_ENTER, self.OnTweakBoxEnterEvent) + + self.twr = PVButton(self, label='<', size=(30, 30)) + self.twf = PVButton(self, label='>', size=(30, 30)) + + self.stopbtn = add_button(self, label=' Stop ', action=self.OnStopButton) + self.morebtn = add_button(self, label=' More ', action=self.OnMoreButton) + + self.__sizer.AddMany([(self.twr, 0, CEN), + (self.__twkbox, 0, CEN), + (self.twf, 0, CEN), + (self.stopbtn, 0, CEN), + (self.morebtn, 0, CEN)]) + + self.SetAutoLayout(1) + print("create ", self.motor) + pack(self, self.__sizer) + + @EpicsFunction + def FillPanel(self): + " fill in panel components for motor " + try: + if self.motor is None: + return + self.FillPanelComponents() + self.drive.Update() + self.desc.Update() + self.rbv.Update() + if self._size == 'full': + self.twk_list = self.make_step_list() + self.UpdateStepList() + except PyDeadObjectError: + pass + + @EpicsFunction + def OnStopButton(self, event=None): + "stop button" + if self.motor is None: + return + + curstate = str(self.stopbtn.GetLabel()).lower().strip() + if curstate == 'stop': + self.motor.stop() + epics.poll() + else: + self.motor.SPMG = 3 + + @EpicsFunction + def OnMoreButton(self, event=None): + "more button" + if self.motor is not None: + MotorDetailFrame(parent=self, motor=self.motor) + + @DelayedEpicsCallback + def OnTweakBoxEnterEvent(self, event=None): + val = float(self.__twkbox.GetValue()) + wx.CallAfter(self.motor.PV('TWV').put, val) + + @DelayedEpicsCallback + def OnTweakBoxComboEvent(self, event=None): + val = float(self.__twkbox.GetValue()) + wx.CallAfter(self.motor.PV('TWV').put, val) + + @DelayedEpicsCallback + def OnMotorEvent(self, pvname=None, field=None, event=None, **kws): + if pvname is None: + return None + + field_val = self.motor.get(field) + field_str = self.motor.get(field, as_string=True) + + if field == 'LLM': + self.drive.SetMin(self.motor.LLM) + elif field == 'HLM': + self.drive.SetMax(self.motor.HLM) + + elif field in ('LVIO', 'HLS', 'LLS'): + s = 'Limit!' + if field_val == 0: + s = '' + self.info.SetLabel(s) + + elif field == 'SET': + label, color = 'Set:','Yellow' + if field_val == 0: + label, color = '','White' + self.info.SetLabel(label) + self.drive.bgcol_valid = color + self.drive.SetBackgroundColour(color) + self.drive.Refresh() + + elif field == 'disabled': + label = ('','Disabled')[field_val] + self.info.SetLabel(label) + + elif field == 'DESC': + font = self.rbv.GetFont() + if len(field_str) > 20: + font.PointSize -= 1 + self.desc.SetFont(font) + + elif field == 'TWV' and self._size == 'full': + self.SetTweak(field_str) + + elif field == 'SPMG' and self._size == 'full': + label, info, color = 'Stop', '', 'White' + if field_val == 0: + label, info, color = ' Go ', 'Stopped', 'Yellow' + elif field_val == 1: + label, info, color = ' Resume ', 'Paused', 'Yellow' + elif field_val == 2: + label, info, color = ' Go ', 'Move Once', 'Yellow' + self.stopbtn.SetLabel(label) + self.info.SetLabel(info) + self.stopbtn.SetBackgroundColour(color) + self.stopbtn.Refresh() + + else: + pass + + @EpicsFunction + def SetTweak(self, val): + if not isinstance(val, str): + val = self.format % val + try: + if val not in self.twk_list: + self.UpdateStepList(value=val) + self.__twkbox.SetValue(val) + except PyDeadObjectError: + pass + + def make_step_list(self): + """ create initial list of motor steps, based on motor range + and precision""" + if self.motor is None: + return [] + return [self.format % i for i in self.motor.make_step_list()] + + def UpdateStepList(self, value=None): + "add a value and re-sort the list of Step values" + if value is not None: + self.twk_list.append(value) + x = [float(i) for i in self.twk_list] + x.sort() + self.twk_list = [self.format % i for i in x] + # remake list in TweakBox + self.__twkbox.Clear() + self.__twkbox.AppendItems(self.twk_list) diff --git a/setup.py b/setup.py index cbd4ae0..7ca751f 100644 --- a/setup.py +++ b/setup.py @@ -12,15 +12,16 @@ long_desc = '''Python Interface to the Epics Channel Access protocol of the Epics control system. PyEpics provides 3 layers of access to Channel Access (CA): - 1. a light wrapping of the CA C library calls, using ctypes. This - provides a procedural CA library in which the user is expected - to manage Channel IDs. It is mostly provided as a foundation - upon which higher-level access is built. - 2. PV() (Process Variable) objects, which represent the basic object - in CA, allowing one to keep a persistent connection to a remote - Process Variable. - 3. A simple set of functions caget(), caput() and so on to mimic - the CA command-line tools and give the simplest access to CA. + +1. a light wrapping of the CA C library calls, using ctypes. This + provides a procedural CA library in which the user is expected + to manage Channel IDs. It is mostly provided as a foundation + upon which higher-level access is built. +2. PV() (Process Variable) objects, which represent the basic object + in CA, allowing one to keep a persistent connection to a remote + Process Variable. +3. A simple set of functions caget(), caput() and so on to mimic + the CA command-line tools and give the simplest access to CA. In addition, the library includes convenience classes to define Devices -- collections of PVs that might represent an Epics Record @@ -44,8 +45,8 @@ nolibca = os.environ.get('NOLIBCA', None) if nolibca is None: - pkg_data = {'epics.clibs': [epics.utils.clib_search_path("ca"), - epics.utils.clib_search_path("Com")],} + pkg_data = {'epics.clibs': ['darwin64/*', 'linux64/*', 'linux32/*', + 'linuxarm/*', 'win32/*', 'win64/*']} else: pkg_data = dict() diff --git a/tests/Setup/simulator.py b/tests/Setup/simulator.py index ba59356..e8bf626 100644 --- a/tests/Setup/simulator.py +++ b/tests/Setup/simulator.py @@ -86,7 +86,7 @@ def initialize_data(): pause_pv.put(0) str_waves[0].put([" String %i" % (i+1) for i in range(128)]) - print 'Data initialized' + print( 'Data initialized') text = '''line 1 this is line 2 @@ -115,7 +115,7 @@ def initialize_data(): time.sleep(SLEEP_TIME) count = count + 1 - if count == 3: print 'running' + if count == 3: print( 'running') if count > 99999999: count = 1 t0 = time.time() diff --git a/tests/ca_unittest.py b/tests/ca_unittest.py index 1c9b914..a7b1bef 100644 --- a/tests/ca_unittest.py +++ b/tests/ca_unittest.py @@ -63,7 +63,8 @@ def testA_CreateChid(self): def testA_GetNonExistentPV(self): write('Simple Test: get on a non-existent PV') chid = ca.create_channel('Definitely-Not-A-Real-PV') - self.assertRaises(ca.ChannelAccessException, ca.get, chid) + val = ca.get(chid) + self.assertEqual(val, None) def testA_CreateChid_CheckTypeCount(self): write('Simple Test: create chid, check count, type, host, and access') diff --git a/tests/pv_unittest.py b/tests/pv_unittest.py old mode 100644 new mode 100755 index 5939c19..0c64ef0 --- a/tests/pv_unittest.py +++ b/tests/pv_unittest.py @@ -1,13 +1,15 @@ #!/usr/bin/env python # unit-tests for ca interface -import os import sys import time import unittest import numpy +import threading +import pytest + from contextlib import contextmanager -from epics import PV, caput, caget, ca +from epics import PV, get_pv, caput, caget, caget_many, caput_many, ca import pvnames @@ -32,22 +34,22 @@ def onChanges(pvname=None, value=None, **kws): def no_simulator_updates(): '''Context manager which pauses and resumes simulator PV updating''' try: - caput(pvnames.pause_pv, 1) + caput(pvnames.pause_pv, 1, wait=True) + time.sleep(0.05) yield finally: - caput(pvnames.pause_pv, 0) + caput(pvnames.pause_pv, 0, wait=True) class PV_Tests(unittest.TestCase): def testA_CreatePV(self): write('Simple Test: create pv\n') - pv = PV(pvnames.double_pv) + pv = get_pv(pvnames.double_pv) self.assertIsNot(pv, None) def testA_CreatedWithConn(self): write('Simple Test: create pv with conn callback\n') - pv = PV(pvnames.int_pv, - connection_callback=onConnect) + pv = get_pv(pvnames.int_pv, connection_callback=onConnect) val = pv.get() global CONN_DAT @@ -63,19 +65,90 @@ def test_caget(self): sval = caget(pvnames.str_pv) self.assertEqual(sval, 'ao') + def test_caget_many(self): + write('Simple Test of caget_many() function\n') + pvs = [pvnames.double_pv, pvnames.enum_pv, pvnames.str_pv] + vals = caget_many(pvs) + self.assertEqual(len(vals), len(pvs)) + self.assertIsInstance(vals[0], float) + self.assertIsInstance(vals[1], int) + self.assertIsInstance(vals[2], str) + + def test_caput_many_wait_all(self): + write('Test of caput_many() function, waiting for all.\n') + pvs = [pvnames.double_pv, pvnames.enum_pv, 'ceci nest pas une PV'] + #pvs = ["MTEST:Val1", "MTEST:Val2", "MTEST:SlowVal"] + vals = [0.5, 0, 23] + t0 = time.time() + success = caput_many(pvs, vals, wait='all', connection_timeout=0.5, put_timeout=5.0) + t1 = time.time() + self.assertEqual(len(success), len(pvs)) + self.assertEqual(success[0], 1) + self.assertEqual(success[1], 1) + self.failUnless(success[2] < 0) + + + def test_caput_many_wait_each(self): + write('Simple Test of caput_many() function, waiting for each.\n') + pvs = [pvnames.double_pv, pvnames.enum_pv, 'ceci nest pas une PV'] + #pvs = ["MTEST:Val1", "MTEST:Val2", "MTEST:SlowVal"] + vals = [0.5, 0, 23] + success = caput_many(pvs, vals, wait='each', connection_timeout=0.5, put_timeout=1.0) + self.assertEqual(len(success), len(pvs)) + self.assertEqual(success[0], 1) + self.assertEqual(success[1], 1) + self.failUnless(success[2] < 0) + + def test_caput_many_no_wait(self): + write('Simple Test of caput_many() function, without waiting.\n') + pvs = [pvnames.double_pv, pvnames.enum_pv, 'ceci nest pas une PV'] + #pvs = ["MTEST:Val1", "MTEST:Val2", "MTEST:SlowVal"] + vals = [0.5, 0, 23] + success = caput_many(pvs, vals, wait=None, connection_timeout=0.5) + self.assertEqual(len(success), len(pvs)) + #If you don't wait, ca.put returns 1 as long as the PV connects + #and the put request is valid. + self.assertEqual(success[0], 1) + self.assertEqual(success[1], 1) + self.failUnless(success[2] < 0) + def test_get1(self): write('Simple Test: test value and char_value on an integer\n') with no_simulator_updates(): - pv = PV(pvnames.int_pv) + pv = get_pv(pvnames.int_pv) val = pv.get() cval = pv.get(as_string=True) self.failUnless(int(cval)== val) + def test_get_with_metadata(self): + with no_simulator_updates(): + pv = get_pv(pvnames.int_pv, form='native') + + # Request time type + md = pv.get_with_metadata(use_monitor=False, form='time') + assert 'timestamp' in md + assert 'lower_ctrl_limit' not in md + + # Request control type + md = pv.get_with_metadata(use_monitor=False, form='ctrl') + assert 'lower_ctrl_limit' in md + assert 'timestamp' not in md + + # Use monitor: all metadata should come through + md = pv.get_with_metadata(use_monitor=True) + assert 'timestamp' in md + assert 'lower_ctrl_limit' in md + + # Get a namespace + ns = pv.get_with_metadata(use_monitor=True, as_namespace=True) + assert hasattr(ns, 'timestamp') + assert hasattr(ns, 'lower_ctrl_limit') + def test_get_string_waveform(self): write('String Array: \n') with no_simulator_updates(): - pv = PV(pvnames.string_arr_pv) + pv = get_pv(pvnames.string_arr_pv) val = pv.get() self.failUnless(len(val) > 10) self.assertIsInstance(val[0], str) @@ -86,7 +159,7 @@ def test_get_string_waveform(self): def test_putcomplete(self): write('Put with wait and put_complete (using real motor!) \n') vals = (1.35, 1.50, 1.44, 1.445, 1.45, 1.453, 1.446, 1.447, 1.450, 1.450, 1.490, 1.5, 1.500) - p = PV(pvnames.motor1) + p = get_pv(pvnames.motor1) # this works with a real motor, fail if it doesn't connect quickly if not p.wait_for_connection(timeout=0.2): self.skipTest('Unable to connect to real motor record') @@ -107,7 +180,7 @@ def test_putcomplete(self): def test_putwait(self): write('Put with wait (using real motor!) \n') - pv = PV(pvnames.motor1) + pv = get_pv(pvnames.motor1) # this works with a real motor, fail if it doesn't connect quickly if not pv.wait_for_connection(timeout=0.2): self.skipTest('Unable to connect to real motor record') @@ -165,7 +238,7 @@ def onPutdone(pvname=None, **kws): def test_get_callback(self): write("Callback test: changing PV must be updated\n") global NEWVALS - mypv = PV(pvnames.updating_pv1) + mypv = get_pv(pvnames.updating_pv1) NEWVALS = [] def onChanges(pvname=None, value=None, char_value=None, **kw): write( 'PV %s %s, %s Changed!\n' % (pvname, repr(value), char_value)) @@ -184,7 +257,7 @@ def onChanges(pvname=None, value=None, char_value=None, **kw): def test_put_string_waveform(self): write('String Array: put\n') with no_simulator_updates(): - pv = PV(pvnames.string_arr_pv) + pv = get_pv(pvnames.string_arr_pv) put_value = ['a', 'b', 'c'] pv.put(put_value, wait=True) get_value = pv.get(use_monitor=False, as_numpy=False) @@ -193,7 +266,7 @@ def test_put_string_waveform(self): def test_put_string_waveform_single_element(self): write('String Array: put single element\n') with no_simulator_updates(): - pv = PV(pvnames.string_arr_pv) + pv = get_pv(pvnames.string_arr_pv) put_value = ['a'] pv.put(put_value, wait=True) time.sleep(0.05) @@ -203,7 +276,7 @@ def test_put_string_waveform_single_element(self): def test_put_string_waveform_mixed_types(self): write('String Array: put mixed types\n') with no_simulator_updates(): - pv = PV(pvnames.string_arr_pv) + pv = get_pv(pvnames.string_arr_pv) put_value = ['a', 2, 'b'] pv.put(put_value, wait=True) time.sleep(0.05) @@ -213,7 +286,7 @@ def test_put_string_waveform_mixed_types(self): def test_put_string_waveform_empty_list(self): write('String Array: put empty list\n') with no_simulator_updates(): - pv = PV(pvnames.string_arr_pv) + pv = get_pv(pvnames.string_arr_pv) put_value = [] pv.put(put_value, wait=True) time.sleep(0.05) @@ -223,7 +296,7 @@ def test_put_string_waveform_empty_list(self): def test_put_string_waveform_zero_length_strings(self): write('String Array: put zero length strings\n') with no_simulator_updates(): - pv = PV(pvnames.string_arr_pv) + pv = get_pv(pvnames.string_arr_pv) put_value = ['', '', ''] pv.put(put_value, wait=True) time.sleep(0.05) @@ -232,8 +305,8 @@ def test_put_string_waveform_zero_length_strings(self): def test_subarrays(self): write("Subarray test: dynamic length arrays\n") - driver = PV(pvnames.subarr_driver) - subarr1 = PV(pvnames.subarr1) + driver = get_pv(pvnames.subarr_driver) + subarr1 = get_pv(pvnames.subarr1) subarr1.connect() len_full = 64 @@ -254,7 +327,7 @@ def test_subarrays(self): caput("%s.NELM" % pvnames.subarr2, 19) caput("%s.INDX" % pvnames.subarr2, 3) - subarr2 = PV(pvnames.subarr2) + subarr2 = get_pv(pvnames.subarr2) subarr2.get() driver.put(full_data) ; time.sleep(0.1) @@ -273,7 +346,7 @@ def test_subarrays(self): self.failUnless(numpy.all(subval == full_data[13:5+13])) def test_subarray_zerolen(self): - subarr1 = PV(pvnames.zero_len_subarr1) + subarr1 = get_pv(pvnames.zero_len_subarr1) subarr1.wait_for_connection() val = subarr1.get(use_monitor=True, as_numpy=True) @@ -289,6 +362,8 @@ def test_subarray_zerolen(self): def test_waveform_get_with_count_arg(self): with no_simulator_updates(): + # NOTE: do not use get_pv() here, as `count` is incompatible with + # the cache wf = PV(pvnames.char_arr_pv, count=32) val=wf.get() self.assertEquals(len(val), 32) @@ -300,6 +375,8 @@ def test_waveform_get_with_count_arg(self): def test_waveform_callback_with_count_arg(self): values = [] + # NOTE: do not use get_pv() here, as `count` is incompatible with + # the cache wf = PV(pvnames.char_arr_pv, count=32) def onChanges(pvname=None, value=None, char_value=None, **kw): write( 'PV %s %s, %s Changed!\n' % (pvname, repr(value), char_value)) @@ -342,6 +419,7 @@ def test_emptyish_char_waveform_no_monitor(self): numpy.testing.assert_array_equal(zerostr.get(as_string=False), [0, 0]) self.assertEquals(zerostr.get(as_string=True, as_numpy=False), '') numpy.testing.assert_array_equal(zerostr.get(as_string=False, as_numpy=False), [0, 0]) + zerostr.disconnect() def test_emptyish_char_waveform_monitor(self): '''a test of a char waveform of length 1 (NORD=1): value "\0" @@ -366,9 +444,10 @@ def test_emptyish_char_waveform_monitor(self): numpy.testing.assert_array_equal(zerostr.get(as_string=False), [0, 0]) self.assertEquals(zerostr.get(as_string=True, as_numpy=False), '') numpy.testing.assert_array_equal(zerostr.get(as_string=False, as_numpy=False), [0, 0]) + zerostr.disconnect() def testEnumPut(self): - pv = PV(pvnames.enum_pv) + pv = get_pv(pvnames.enum_pv) self.assertIsNot(pv, None) pv.put('Stop') time.sleep(0.1) @@ -378,7 +457,7 @@ def testEnumPut(self): def test_DoubleVal(self): pvn = pvnames.double_pv - pv = PV(pvn) + pv = get_pv(pvn) pv.get() cdict = pv.get_ctrlvars() write( 'Testing CTRL Values for a Double (%s)\n' % (pvn)) @@ -424,15 +503,15 @@ def test_type_converions_2(self): self.assertEqual(a, b) def test_waveform_get_1elem(self): - pv = PV(pvnames.double_arr_pv) + pv = get_pv(pvnames.double_arr_pv) val = pv.get(count=1, use_monitor=False) self.failUnless(isinstance(val, numpy.ndarray)) self.failUnless(len(val), 1) def test_subarray_1elem(self): with no_simulator_updates(): - # pv = PV(pvnames.zero_len_subarr1) - pv = PV(pvnames.double_arr_pv) + # pv = get_pv(pvnames.zero_len_subarr1) + pv = get_pv(pvnames.double_arr_pv) pv.wait_for_connection() val = pv.get(count=1, use_monitor=False) @@ -446,6 +525,96 @@ def test_subarray_1elem(self): self.assertEqual(len(val), 1) +@pytest.mark.parametrize('num_threads', [1, 10, 200]) +@pytest.mark.parametrize('thread_class', [ca.CAThread, threading.Thread]) +def test_multithreaded_get(num_threads, thread_class): + def thread(thread_idx): + result[thread_idx] = (pv.get(), + pv.get_with_metadata(form='ctrl')['value'], + pv.get_with_metadata(form='time')['value'], + ) + + result = {} + ca.use_initial_context() + pv = get_pv(pvnames.double_pv) + + threads = [thread_class(target=thread, args=(i, )) + for i in range(num_threads)] + + with no_simulator_updates(): + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + assert len(result) == num_threads + print(result) + values = set(result.values()) + assert len(values) == 1 + + value, = values + assert value is not None + + +@pytest.mark.parametrize('num_threads', [1, 10, 100]) +def test_multithreaded_put_complete(num_threads): + def callback(pvname, data): + result.append(data) + + def thread(thread_idx): + pv.put(thread_idx, callback=callback, + callback_data=dict(data=thread_idx), + wait=True) + time.sleep(0.1) + + result = [] + ca.use_initial_context() + pv = get_pv(pvnames.double_pv) + + threads = [ca.CAThread(target=thread, args=(i, )) + for i in range(num_threads)] + + with no_simulator_updates(): + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + assert len(result) == num_threads + print(result) + assert set(result) == set(range(num_threads)) + + +def test_force_connect(): + pv = get_pv(pvnames.double_arrays[0], auto_monitor=True) + + print("Connecting") + assert pv.wait_for_connection(5.0) + + print("SUM", pv.get().sum()) + + time.sleep(3) + + print("Disconnecting") + pv.disconnect() + print("Reconnecting") + + pv.force_connect() + assert pv.wait_for_connection(5.0) + + called = {'called': False} + + def callback(value=None, **kwargs): + called['called'] = True + print("update", value.sum()) + + pv.add_callback(callback) + + time.sleep(1) + assert pv.get() is not None + assert called['called'] + + if __name__ == '__main__': suite = unittest.TestLoader().loadTestsFromTestCase( PV_Tests) unittest.TextTestRunner(verbosity=1).run(suite) diff --git a/tests/test_cas.py b/tests/test_cas.py index 8410003..192beba 100644 --- a/tests/test_cas.py +++ b/tests/test_cas.py @@ -1,3 +1,4 @@ +import time import pytest import subprocess from tempfile import NamedTemporaryFile as NTF @@ -65,7 +66,7 @@ def softioc(): df.write(cas_test_db) df.flush() - proc = subprocess.Popen(['softIoc', '-D', + proc = subprocess.Popen(['softIoc', '-D', '/home/travis/mc/envs/testenv/epics/dbd/softIoc.dbd', '-m', 'P=test', '-a', cf.name, '-d', df.name], @@ -73,8 +74,12 @@ def softioc(): stdout=subprocess.PIPE) yield proc - proc.kill() - proc.wait() + try: + proc.kill() + proc.wait() + except OSError: + pass + @pytest.yield_fixture(scope='module') def pvs(): @@ -82,7 +87,7 @@ def pvs(): 'test:permit'] pvs = dict() for name in pvlist: - pv = epics.PV(name) + pv = epics.get_pv(name) pv.wait_for_connection() pvs[pv.pvname] = pv @@ -126,7 +131,7 @@ def lcb(read_access, write_access, pv=None): assert pv.write_access == write_access pv.flag = True - bo = epics.PV('test:bo', access_callback=lcb) + bo = epics.get_pv('test:bo', access_callback=lcb) bo.flag = False # set the run-permit to trigger an access rights event @@ -134,13 +139,14 @@ def lcb(read_access, write_access, pv=None): assert pvs['test:permit'].get(as_string=True, use_monitor=False) == 'ENABLED' assert bo.flag is True + bo.access_callbacks = [] + def test_ca_access_event_callback(softioc, pvs): # clear the run-permit pvs['test:permit'].put(0, wait=True) assert pvs['test:permit'].get(as_string=True, use_monitor=False) == 'DISABLED' - bo_id = None bo_id = epics.ca.create_channel('test:bo') assert bo_id is not None @@ -152,3 +158,23 @@ def lcb(read_access, write_access): epics.ca.replace_access_rights_event(bo_id, callback=lcb) assert lcb.sentinal is True + epics.ca.clear_channel(bo_id) + + +def test_connection_callback(softioc, pvs): + results = [] + + def callback(conn, **kwargs): + results.append(conn) + + pv = epics.PV('test:ao', connection_callback=callback) + pv.wait_for_connection() + softioc.kill() + softioc.wait() + + t0 = time.time() + while pv.connected and (time.time() - t0) < 5: + time.sleep(0.1) + + assert True in results + assert False in results diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 99cdabe..ba11bc0 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -10,7 +10,7 @@ def subprocess(*args): print('==subprocess==', args) - mypvs = [epics.PV(pvname) for pvname in args] + mypvs = [epics.get_pv(pvname) for pvname in args] for i in range(10): time.sleep(0.750) @@ -23,7 +23,7 @@ def monitor(pvname=None, char_value=None, **kwargs): print('--main:monitor %s=%s' % (pvname, char_value)) print('--main:') - pv1 = epics.PV(PVN1) + pv1 = epics.get_pv(PVN1) print('--main:init %s=%s' % (PVN1, pv1.get())) pv1.add_callback(callback=monitor) diff --git a/tests/test_pool.py b/tests/test_pool.py index c324ffd..999f277 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -1,7 +1,9 @@ from __future__ import print_function +import sys from contextlib import contextmanager import epics import multiprocessing as mp +import pytest import pvnames PVS = [pvnames.double_pv, pvnames.double_pv2] @@ -14,6 +16,9 @@ def pool_ctx(): pool.close() pool.join() + +@pytest.mark.skipif(sys.version_info >= (3, 0), + reason='CAPool not functioning in Python 3') def test_caget(): with pool_ctx() as pool: print('Using caget() in subprocess pools:') @@ -28,6 +33,8 @@ def _manager_test_fcn(pv_dict, pv): pv_dict[pv] = epics.caget(pv) +@pytest.mark.skipif(sys.version_info >= (3, 0), + reason='CAPool not functioning in Python 3') def test_manager(): ''' Fill up a shared dictionary using a manager diff --git a/tests/test_threading.py b/tests/test_threading.py index 5d346b7..6d30b05 100644 --- a/tests/test_threading.py +++ b/tests/test_threading.py @@ -7,7 +7,7 @@ def test_basic_thread(): result = [] def thread(): epics.ca.use_initial_context() - pv = epics.PV(pvnames.double_pv) + pv = epics.get_pv(pvnames.double_pv) result.append(pv.get()) epics.ca.use_initial_context() @@ -21,7 +21,7 @@ def thread(): def test_basic_cathread(): result = [] def thread(): - pv = epics.PV(pvnames.double_pv) + pv = epics.get_pv(pvnames.double_pv) result.append(pv.get()) epics.ca.use_initial_context() @@ -36,13 +36,13 @@ def test_attach_context(): result = [] def thread(): epics.ca.create_context() - pv = epics.PV(pvnames.double_pv2) + pv = epics.get_pv(pvnames.double_pv2) assert pv.wait_for_connection() result.append(pv.get()) epics.ca.detach_context() epics.ca.attach_context(ctx) - pv = epics.PV(pvnames.double_pv) + pv = epics.get_pv(pvnames.double_pv) assert pv.wait_for_connection() result.append(pv.get()) @@ -62,7 +62,7 @@ def thread(): result.append(pv.get()) epics.ca.use_initial_context() - pv = epics.PV(pvnames.double_pv2) + pv = epics.get_pv(pvnames.double_pv2) t = epics.ca.CAThread(target=thread) t.start() diff --git a/upload_wininst.bat b/upload_wininst.bat deleted file mode 100644 index 3fdf0a8..0000000 --- a/upload_wininst.bat +++ /dev/null @@ -1,15 +0,0 @@ -REM -REM %HOME%\.pypirc must be setup with PyPI info. -REM -SET HOME=M:\ -SET PATH=C:\Python26;%PATH% -python setup.py bdist_wininst --target-version=2.6 upload - -SET PATH=C:\Python27;%PATH% -python setup.py bdist_wininst --target-version=2.7 upload - -SET PATH=C:\Python32;%PATH% -python setup.py bdist_wininst --target-version=3.2 upload - - - diff --git a/use_anaconda.bat b/use_anaconda.bat deleted file mode 100644 index 8c3a2e1..0000000 --- a/use_anaconda.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -SET PATH=%LOCALAPPDATA%\Continuum\Anaconda2;%LOCALAPPDATA%\Continuum\Anaconda2\Scripts;%PATH% diff --git a/use_py26.bat b/use_py26.bat deleted file mode 100644 index c36bda6..0000000 --- a/use_py26.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -SET PATH=C:\Python26;C:\Python26\Tools\Scripts;%PATH% diff --git a/use_py27.bat b/use_py27.bat deleted file mode 100644 index 115eb16..0000000 --- a/use_py27.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -SET PATH=C:\Python27;C:\Python27\Tools\Scripts;%PATH% diff --git a/use_py31.bat b/use_py31.bat deleted file mode 100755 index a847cf8..0000000 --- a/use_py31.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -SET PATH=C:\Python31;C:\Python31\Tools\Scripts;%PATH% diff --git a/use_py32.bat b/use_py32.bat deleted file mode 100644 index 6672ddb..0000000 --- a/use_py32.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -SET PATH=C:\Python32;C:\Python32\Tools\Scripts;%PATH% diff --git a/use_py34.bat b/use_py34.bat deleted file mode 100644 index 9c42bb7..0000000 --- a/use_py34.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -SET PATH=C:\Python34;C:\Python34\Tools\Scripts;%PATH% diff --git a/use_py35.bat b/use_py35.bat deleted file mode 100644 index 7c2e96f..0000000 --- a/use_py35.bat +++ /dev/null @@ -1,3 +0,0 @@ - -@echo off -SET PATH=%LOCALAPPDATA%\Programs\Python\Python35;%LOCALAPPDATA%\Programs\Python\Python35\Scripts;%PATH%